//! Session actor command enum and associated public types. //! //! `SessionCommand` defines the message protocol used to drive a session //! actor. It was extracted from `acp_session.rs` to keep the actor //! implementation focused on behaviour. use super::acp_types::*; use super::plan_mode::PromptMode; use crate::extensions::notification::SessionNotification; use crate::session::signals::TurnDeltaSnapshot; use agent_client_protocol as acp; use tokio::sync::oneshot; /// Structured context for a cancelled turn, replacing stringly-typed JSON. #[derive(Debug, Clone, Default, serde::Deserialize)] pub struct CancellationContext { pub tool_name: Option, pub reason: Option, pub hook_name: Option, /// What triggered the cancel (`"send_now"`, `"esc"`, `"ctrl_c"`); surfaced /// as `cancelTrigger` on the `PromptResponse`/`TurnCompleted` `_meta`. /// `None` for graceful in-turn cancels and older clients. pub trigger: Option, } /// Prompt completion kind returned to the ACP layer. #[derive(Debug, Clone)] pub enum PromptCompletionKind { Completed, Cancelled { category: Option, context: Option, }, MaxTurnsReached { limit: usize, }, Rewound, /// A queued prompt was removed (or cleared) from the server-authoritative /// queue before it ever ran. Used to resolve the still-pending /// `session/prompt` RPC of the client that submitted it WITHOUT triggering /// any turn-completion side effects: the prompt never started a turn, so the /// `prompt_complete` broadcast (which carries no `promptId` and would tell /// every attached leader-mode client the *running* turn ended) and the /// roster `Idle` delta (which would flip the dashboard off `Working` while /// the real turn is still in flight) must be skipped. See /// `MvpAgent::prompt`'s short-circuit and `respond_removed_queued_prompt`. RemovedFromQueue, } /// Successful prompt/turn payload returned to the ACP layer and trace uploaders. #[derive(Debug, Clone)] pub struct PromptTurnOk { pub stop_reason: acp::StopReason, pub total_tokens: u64, pub turn_snapshot: Option, pub completion_kind: PromptCompletionKind, /// Schema-validated `--json-schema` output, delivered to the client in the /// prompt-response `_meta`. `None` unless a schema was requested; /// `Some(Err)` carries a parse/validation error message. pub structured_output: Option>, pub usage: Option, } /// Result of a prompt turn, containing the stop reason, accumulated token count, /// and an optional turn-end signals snapshot (for trace metadata enrichment). pub type PromptTurnResult = Result; /// Convenience: successful end-of-turn result. pub(crate) fn ok_end_turn(tokens: u64, snapshot: Option) -> PromptTurnResult { Ok(PromptTurnOk { stop_reason: acp::StopReason::EndTurn, total_tokens: tokens, turn_snapshot: snapshot, completion_kind: PromptCompletionKind::Completed, structured_output: None, usage: None, }) } /// Priority levels for notification drain timing. /// /// Ordering: `Next < Later` (derived from declaration order). /// `Next` = more urgent, eligible for mid-turn drain (future enhancement). /// `Later` = deferred to end-of-turn or idle drain. #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum NotificationPriority { /// Drain mid-turn (between tool calls). For urgent monitor events. Next, /// Drain only at end-of-turn or when idle. Used for bash task completions. Later, } #[derive(Debug, Clone)] pub enum NotificationSource { MonitorEvent { task_id: String }, BashTaskCompleted { task_id: String }, } impl NotificationSource { pub fn task_id(&self) -> &str { match self { Self::MonitorEvent { task_id } | Self::BashTaskCompleted { task_id } => task_id, } } } pub enum SessionCommand { Initialize { system_prompt: String, }, /// Non-destructive system-prompt sync on session attach: swaps only the /// leading `System` message, keeping user/assistant turns. Backed by the /// atomic `ChatStateCommand::ReplaceSystemHead` (see its doc for the /// serialization guarantees); no-op when the live head already matches. ReplaceSystemPrompt { system_prompt: String, }, /// Resume hook: after a session is restored with /// `awaiting_plan_approval == true`, re-issue the `exit_plan_mode` /// reverse-request so the client re-shows approval chrome over a real live /// waiter. Fire-and-forget; the actor spawns the round-trip + decision. RestorePlanApproval, Prompt { prompt_id: String, prompt_blocks: Vec, /// Prompt mode parsed from request `_meta.mode`. prompt_mode: PromptMode, /// Optional client identifier from the prompt request meta (overrides session-level one) client_identifier: Option, /// Optional screen mode from the prompt request meta (`_meta.screenMode`, /// pager-only: `fullscreen` | `inline` | `minimal` | `headless`). /// Telemetry-only; `None` for other clients and synthetic prompts. screen_mode: Option, /// Skip `` wrapping and large-prompt truncation. verbatim: bool, /// W3C traceparent from the caller's OTEL span context, used to link /// `session.handle_prompt` back to `agent.prompt` across the channel hop. traceparent: Option, json_schema: Option, /// Cancel-and-send: cancel the running turn and run this prompt next. /// Also derived server-side during an interruptible wait (see /// [`SessionActor::queue_input`]). send_now: bool, respond_to: oneshot::Sender, /// Optional oneshot fired after the user message has been appended to /// chat history and a persistence flush barrier has completed, before /// LLM inference begins. Used by callers that need to ensure /// `chat_history.jsonl` includes the prompt before trace snapshots or /// `session/load`. persist_ack: Option>, }, SessionMode { session_mode: acp::SessionModeId, responds_to: oneshot::Sender<()>, }, SetSessionModel { sampling_config: kigi_sampler::SamplerConfig, /// The catalog KEY the picker resolved (`{platform}/{model}` for a /// registry model), which `sampling_config.model` — the bare routing /// slug — cannot express when an API-key platform and its /// subscription-OAuth twin list the same id. The session stores it as /// its OWN selection instead of reading the process-global /// `ModelsManager::current_model_id()` (H4). catalog_key: Option, use_concise: bool, /// When `false`, skip the system prompt rewrite (concise/default swap). /// Set to `false` for forked sessions so mid-session model switches /// cannot contaminate the inherited prompt configuration. apply_prompt_override: bool, /// When `true`, suppress the system prompt rewrite even though /// `apply_prompt_override` may be `true`. Set by the model-switch /// orchestrator immediately after a successful /// `RebuildAgentForDefinition` so the fresh harness's prompt /// (already installed by the rebuild handler) is not clobbered by /// the concise/default swap below. skip_prompt_rewrite: bool, /// Re-resolved auto-compact threshold for the new model. Computed /// by `MvpAgent` against the new model id so per-model remote settings /// and per-model user TOML overrides target the right model after a /// `/model` switch. The session actor stores this on /// `compaction.threshold_percent` (which is `Cell` so it can /// update without `&mut self`). auto_compact_threshold_percent: u8, responds_to: oneshot::Sender>, }, /// Zero-turn harness rebuild: build a brand-new `Agent` from the /// session's `AgentRebuildSpec` and the new `AgentDefinition`, /// re-register MCP tools, swap the live `Agent`, rewrite the /// system message in the conversation, persist the new prompt /// artifacts, and update `active_agent_type`. /// /// Triggered by `MvpAgent::set_session_model` when the new model's /// `agent_type` differs from the session's current one and no user /// message has been sent yet (`turn_count == 0`). RebuildAgentForDefinition { definition: kigi_agent::AgentDefinition, responds_to: oneshot::Sender>, }, /// Override the model name and optionally inject extra HTTP headers /// into the session's sampling config. /// /// Unlike `SetSessionModel` (which requires a fully resolved `ModelEntry` /// and does NOT update `primaryModelId` in signals — the resolved model /// is already tracked via inference responses), this command also calls /// `set_primary_model()` so that signals report the override model /// rather than the agent-level default (e.g. `kigi-4.5`). /// /// Keeps the existing base_url, api_key, and other config — only changes /// the `model` field sent in the `x-kigi-model-override` header and merges /// any additional headers (e.g. `x-openrouter-api-key` for BYOK). /// /// Used to set model IDs (e.g. opaque third-party routing names) that are /// routing hints for the backend and don't need to exist in the /// agent's local model registry. OverrideModelName { model_name: String, extra_headers: indexmap::IndexMap, /// Override the context window size for the new model. Without this, /// forked sessions inherit the source session's context window, causing /// auto-compact and context-usage signals to use the wrong threshold. context_window: Option, }, GetCurrentModel { responds_to: oneshot::Sender, }, GetCurrentPromptMode { responds_to: oneshot::Sender, }, GetModelMetadata { responds_to: oneshot::Sender, }, /// Snapshot for `/session-info`. GetSessionInfo { responds_to: oneshot::Sender, }, /// Compacts the current session, saving on the context window CompactSession { /// Optional user-provided context to guide the compaction user_context: Option, respond_to: oneshot::Sender>, }, /// Reload plugin hooks and registry mid-session. ReloadPlugins { registry: Option>, }, /// Re-discover the session's own project hooks (`.kigi/hooks`, /// `.cursor/hooks.json`, …) mid-session, re-evaluating folder trust. Used by /// the interactive folder-trust grant so a granted folder's repo-local hooks /// start without a session restart (plugin-contributed hooks are handled by /// `ReloadPlugins`; this covers the non-plugin project hook registry). ReloadHooks, /// Re-discover skills from disk and update the session's skill baseline. RefreshSkillBaseline, /// Trigger an on-demand memory flush for this session. /// /// Calls `run_memory_flush("user_requested", None)` on the session actor. /// Returns an error if memory is not enabled for this session, or /// `Ok(true/false)` indicating whether a flush actually ran (false if /// another flush was already in progress). FlushMemory { respond_to: oneshot::Sender>, }, /// Auto-approve all permission prompts when `enabled`. SetYoloMode { enabled: bool, }, /// Set auto permission mode (LLM classifier for non-fast-path tools). SetAutoMode { enabled: bool, }, ResetPermissionState, Rewind { request: RewindRequest, respond_to: oneshot::Sender>, }, /// Out-of-band history repair (`kigi/session/repair`): fix tool-pairing /// violations (orphaned/displaced `ToolResult`s, duplicates, unanswered /// calls) that would otherwise 400 on every request. `dry_run` only /// reports. Refused while a turn is in flight. RepairHistory { dry_run: bool, respond_to: oneshot::Sender>, }, GetRewindPoints { respond_to: oneshot::Sender, }, /// Local file-snapshot counts keyed by `prompt_index`, read straight from /// the file-state tracker (independent of the chat-state prompt index, /// which is empty in bridge mode). The bridge joins these onto the /// server's rewind points so `num_file_snapshots`/`has_file_changes` match /// what local-mode rewind reports. GetRewindFileCounts { respond_to: oneshot::Sender>, }, /// Reconcile the file-state rewind tracker after a bridge-mode /// `ConversationOnly` rewind that already committed server-side. Runs the /// same tracker bookkeeping `handle_rewind` does for `ConversationOnly` /// (merge the discarded prompts' file effects into the prior rewind point + /// persist), without reverting files or rewinding the conversation — both /// live server-side in bridge mode. Without it, bridge `ConversationOnly` /// commits leave orphaned local rewind points. Fire-and-forget (no ack): /// the server rewind has already committed, and the local truncation in /// `handle_rewind` is itself fire-and-forget, so the bridge does not block /// its response on the merge. ReconcileRewindTracker { target_prompt_index: usize, }, /// xAI extension session notification - client-side events to store in persistence XaiSessionNotification { notification: SessionNotification, }, /// Apply subagent usage into parent ledgers. Acks `()` once chat-state /// applied (prompt-attributed or session-only). Drop the oneshot on failure /// so the child treats the fold as not landed. RecordSubagentUsage { by_model: Vec<(String, kigi_chat_state::UsageTotals)>, parent_prompt_id: Option, /// Nested subagent bill may under-count. incomplete: bool, respond_to: oneshot::Sender<()>, }, /// Sticky incomplete for a parent prompt (or the live pin when `None`). Acks when marked. MarkSubagentUsageNotApplied { parent_prompt_id: Option, respond_to: oneshot::Sender<()>, }, /// Shared error-path usage attach (same policy as durable TurnCompleted). ErrorPathUsageFallback { prompt_id: Option, respond_to: oneshot::Sender>, }, /// Persist the monotonic telemetry turn counter ("next trace turn") for the session. SetNextTraceTurn { next_trace_turn: u64, request_id: Option, }, /// Flush pending writes and copy the current session directory contents to memory. /// The caller can then tar.gz + upload to GCS (or similar). CopyFile { respond_to: oneshot::Sender>, }, /// Flush the replay buffer and persistence, then signal completion. /// Used during reconnect to ensure all buffered content is persisted before replay. FlushComplete { respond_to: oneshot::Sender<()>, }, /// Update MCP servers for an existing session (used during reconnect or /// mid-session via the `kigi/session/update_mcp_servers` extension method). /// This replaces the current MCP server configuration and triggers re-initialization. /// /// The caller is notified via `respond_to` once MCP re-initialization /// completes (or immediately if configs are unchanged). UpdateMcpServers { mcp_servers: Vec, respond_to: oneshot::Sender>, }, /// Toggle an MCP server on/off within the session actor's event loop. /// Atomic read-modify-write avoids TOCTOU races with background config /// refreshes that can change `mcp_state.configs` between a snapshot read /// and an `UpdateMcpServers` command. ToggleMcpServer { server_name: String, enabled: bool, /// Fully-formed server config to add when re-enabling. Built by the /// caller via `merge_managed_mcp_servers`. /// `None` when disabling. server_config: Option, respond_to: oneshot::Sender>, }, /// Toggle a single MCP tool on/off within a server. The server stays connected; /// only the tool's registration in ToolBridge is affected. ToggleMcpTool { server_name: String, tool_name: String, enabled: bool, respond_to: oneshot::Sender>, }, /// Read MCP status: which servers are configured, which clients are healthy, what tools. GetMcpStatus { respond_to: oneshot::Sender, }, /// Snapshot the session's live MCP client pool for subagent inheritance. SnapshotMcpPool { respond_to: oneshot::Sender>, }, /// Snapshot the session's client-registered hooks so a subagent inherits the same /// PreToolUse gate and observe hooks over the parent's connection. SnapshotClientHooks { respond_to: oneshot::Sender, }, /// Snapshot the session's resolved tool schema (same list the parent's own turn /// sends) so a verbatim-fork child can present a byte-identical tool prefix. SnapshotToolDefinitions { respond_to: oneshot::Sender>, }, /// Replace the session's client-registered hooks. Sent on `load_session` reconnect to a /// live actor so a client can re-register (or clear) its hooks without a fresh session. SetClientHooks { hooks: crate::extensions::hooks::ClientHooks, }, /// Client-driven MCP tool call outside the LLM loop. CallMcpTool { server_name: String, server_url: Option, tool_name: String, arguments: serde_json::Value, respond_to: oneshot::Sender>, }, ReadMcpResource { server_name: String, uri: String, respond_to: oneshot::Sender>, }, McpAuthStatus { respond_to: oneshot::Sender>, }, McpAuthTrigger { server_name: String, respond_to: oneshot::Sender>, }, RetryAuthRequiredServers { respond_to: oneshot::Sender<()>, }, RefreshMcpSearchIndex, /// Move a foreground bash command to background by tool_call_id. /// Unblocks the agent loop so it can continue with the next action. BackgroundForegroundCommand { tool_call_id: String, respond_to: oneshot::Sender, }, /// Kill a background task by task_id. /// Routes through the ToolBridge's TerminalBackend (lock-free, Arc-shared). KillBackgroundTask { task_id: String, respond_to: oneshot::Sender>, }, DeleteScheduledTask { task_id: String, respond_to: oneshot::Sender>, }, /// List all background tasks. /// Routes through the ToolBridge's TerminalBackend. ListTasks { respond_to: oneshot::Sender>>, }, /// Query whether the session has work in flight: a running turn /// (`running_task.is_some()`) **or** queued inputs /// (`pending_inputs` non-empty). Used by the leader's idle-unload decision /// on client disconnect (the no-evict keystone) to avoid unloading a /// session that still has pending work. IsBusy { respond_to: oneshot::Sender, }, GetHooksList { respond_to: oneshot::Sender, }, /// Execute a hooks management action from the pager modal. HooksAction { action: kigi_hooks_plugins_types::HooksAction, respond_to: oneshot::Sender, }, /// Broadcast a plugin updates notification to the session. NotifyPluginUpdates { updates: Vec<(String, String, String)>, }, /// Execute a plugins management action from the pager modal. PluginsAction { action: kigi_hooks_plugins_types::PluginsAction, respond_to: oneshot::Sender, }, /// This session's plugin registry, as served by `kigi/plugins/list`. PluginsList { respond_to: oneshot::Sender>>, }, /// Inject a notification (monitor event or bash task completion) into /// the session's notification queue. Notifications are idle-gated and /// batched by `maybe_drain_notifications`. InjectNotification { prompt_id: String, prompt_blocks: Vec, priority: NotificationPriority, source: NotificationSource, }, /// Drop queued / mid-turn-buffered `MonitorEvent` notifications for a /// task. Used when natural monitor exit already auto-woke via /// `TaskCompleted` so stdout + terminal pipeline events do not start a /// second `NotificationDrain` turn for the same completion. DropMonitorNotifications { task_id: String, }, /// Dispatch a compat `Notification` hook (e.g. `task_complete` /// from the notification bridge, which does not go through `send_xai_notification`). DispatchNotificationHook { notification_type: String, message: Option, title: Option, level: Option, }, /// Record background-task ids reparented from a harness-internal /// verifier/planner subagent's surviving dev server on subagent exit. The /// handler inserts them into `goal_turn_task_ids` whenever the goal harness /// is enabled (not gated on the racy `Active` status), so their late /// auto-wake completions are suppressed by `maybe_drain_notifications` even /// when a final verification round has already flipped the goal to Blocked. RecordGoalTurnTaskIds { task_ids: Vec, }, /// Remove a queued (not-yet-running) prompt from the authoritative prompt /// queue. Versioned + idempotent: a stale `expected_version` /// or an already-drained `id` is a no-op (the actor just re-broadcasts the /// current queue so the client reconciles). When `owner` is `Some`, the /// removal only applies if the item's attribution matches (edit authority: /// a client edits its own items). RemoveQueuedPrompt { id: String, expected_version: u64, owner: Option, }, /// Reorder the queued (not-yet-running) prompts to match `ordered_ids`. /// Ids not present in the live queue are ignored; queued items missing /// from `ordered_ids` keep their relative order at the back. The actor /// re-broadcasts the resulting queue. Idempotent. ReorderQueue { ordered_ids: Vec, }, /// Clear queued (not-yet-running) prompts. When `owner` is `Some`, only /// that client's items are cleared. The running turn is never touched. ClearQueue { owner: Option, }, /// Replace the text of a queued (not-yet-running) prompt in place /// (server-side LWW). Last write wins via the actor's /// serialized mailbox; the rebroadcast of `kigi/queue/changed` is the /// truth signal for every attached client. The original `owner` /// attribution is preserved; `editor` is recorded as the most recent /// editor (for future "alice edited this" UX). A missing id, or an id /// that names the currently-running turn, is a benign no-op. EditQueuedPrompt { id: String, new_text: String, editor: Option, }, /// Atomically interject a queued (not-yet-running) prompt into the running /// turn: the actor removes it from `pending_inputs` and pushes /// its text into `pending_interjections` in a single mailbox op, so the /// in-flight turn merges it at the next safe point and the prompt can never /// both interject *and* later run as its own turn. Versioned + idempotent /// like [`RemoveQueuedPrompt`]. A benign no-op (the prompt stays queued and /// runs normally) when no turn is running, the id names the running turn, is /// stale/already-drained, or `owner` doesn't match. The rebroadcast of /// `kigi/queue/changed` is the truth signal for every attached client. InterjectQueuedPrompt { id: String, expected_version: u64, owner: Option, /// Optional replacement text (client-edited row). When `Some`, it is /// interjected INSTEAD of the stored queue text, under the same single /// version check — edit + interject is one atomic op (a stale version /// no-ops the whole thing, edited text included). new_text: Option, }, /// Cancel the running turn. `kill_background_tasks` distinguishes a hard /// teardown (subagent shutdown — drains the whole queue) from a normal /// interactive cancel (Ctrl+C — preserves the queued prompts so the next /// one auto-runs). On an interactive cancel only the running turn (the front /// of `pending_inputs`) is torn down; the follow-up `maybe_start_running_task` /// promotes the new front so the user's next queued prompt auto-runs. Cancel { cancel_subagents: bool, kill_background_tasks: bool, rewind_if_pristine: bool, /// Free-form discriminator for *what* triggered the cancel, taken from /// the `session/cancel` request `_meta.cancelTrigger` (e.g. `"esc"`, /// `"ctrl_c"`). `None` for older clients and programmatic teardowns /// (subagent shutdown). Recorded in the `mid_turn_abort` turn-end's /// `cancellation_context` JSON; the category stays `MidTurnAbort`. trigger: Option, }, Shutdown, /// Force-trigger a feedback request notification for local client testing. /// Bypasses all heuristics, sampling, and cooldown checks. TriggerTestFeedback { tier: crate::session::feedback::FeedbackTier, mode: crate::session::feedback::FeedbackMode, respond_to: oneshot::Sender>, }, /// Persist a local feedback entry via the persistence actor. This ensures /// feedback.jsonl is written through the same channel as other session /// files and is included in GCS CopyFile snapshots. PersistFeedback(Box), AdvertiseCommands, /// Re-discover skills from disk, update the SkillManager baseline, /// and re-advertise slash commands to the client. ReloadSkills, /// Dispatch session_start hook using the actor's loaded HookRegistry. DispatchSessionStartHook { /// "new" for brand new sessions, "load" for sessions loaded from disk. source: String, }, /// Retrieve session context for enriching a feedback Slack notification. GetFeedbackContext { turn_number: Option, responds_to: oneshot::Sender, }, /// Retrieve the session's active agent type. /// /// Returns the name of the `AgentDefinition` that was used to initialize /// this session (or the most recent one applied via `handle_session_mode`). /// Used by `mvp_agent.set_session_model` to check whether a model's /// `agent_type` is compatible with the current session before switching. GetActiveAgent { responds_to: oneshot::Sender>, }, /// Ask a side question without interrupting the current turn. /// The session snapshots the conversation context, makes a single /// tool-free model call, and returns the response text. SideQuestion { question: String, respond_to: oneshot::Sender>, }, /// Generate a session recap (a short "where was I" summary) and broadcast /// it to clients via `SessionUpdate::SessionRecap`. /// /// Fire-and-forget: the session snapshots the conversation, makes a single /// tool-free model call, and emits the result for display only. It never /// mutates the conversation, so unlike `SideQuestion` it needs no reply /// channel — the answer travels back as a notification. Recap { /// `true` when triggered automatically on return-from-away, /// `false` for an explicit `/recap`. auto: bool, }, /// Request an AI-generated shell command suggestion. /// /// The session actor builds a minimal prompt from `prefix` + `cwd`, /// calls the sampler with low temperature / max_tokens, and returns /// the suggested completion via `respond_to`. AISuggest { prefix: String, cwd: String, model_override: Option, respond_to: oneshot::Sender>, }, /// Predict the user's likely next prompt (tab autocomplete ghost text). /// /// Fired by the client after a turn completes. The session builds a /// compact text-only transcript of the recent conversation, makes one /// tool-free model call (default `kigi-0.1` when available via /// `model_override`, else the session model), sanitizes the output, and /// returns the predicted prompt via `respond_to`. Best-effort: any /// failure returns `None`. SuggestPrompt { model_override: Option, respond_to: oneshot::Sender>, }, /// Rewrite a raw memory note into well-structured markdown via a one-shot /// LLM call. The session uses `prepare_chat_completion()` with /// `kigi` model, low temperature, and capped output tokens. RewriteMemoryNote { raw_text: String, context_summary: String, respond_to: oneshot::Sender>, }, /// Inject a user message into the active turn without canceling it. /// The text is queued in `pending_interjections` and drained at the /// next safe point in `process_conversation_turn`. Fire-and-forget: /// no response channel needed since the command just pushes to a Mutex. Interject { text: String, /// Client-minted id echoed back on the broadcast /// `kigi/session/interjection` so the originating pager can dedup its /// optimistic local block. `None` from older clients. id: Option, /// Pasted images riding along with the interjection. Empty from /// text-only / older clients. images: Vec, }, /// Trigger a model turn so the model can print a visible goal progress /// summary. The goal orchestrator injects a system reminder into context /// (via `push_parent_reminder`) *before* sending this command. The session /// actor queues a short synthetic prompt instructing the model to summarize /// the reminder, then calls `maybe_start_running_task`. Fire-and-forget. GoalSummaryTurn { /// Short instruction appended as a verbatim user message. prompt_text: String, }, /// Take turn messages from the chat state actor (proxied from mvp_agent). TakeTurnMessages { respond_to: oneshot::Sender>, }, /// Take and clear the session actor's out-of-band streaming-turn capture. /// /// Returns `Some(...)` when the model streamed reasoning or text in the /// current turn but the canonical assistant response never reached /// `chat_state` (user cancel mid-stream, sampler terminal failure such as /// `MaxTokensTruncation`). The consumer uploads it as /// `streaming_partial.json` for trace inspection; `chat_state` is never /// mutated by this command. /// /// `prompt_id` is passed so the handler can detect a race where a queued /// turn's `StreamStarted` reset the live slot to a different prompt /// between cancel and take. On mismatch the handler emits a /// `tracing::warn!` tripwire and returns `None`; there is no stash, so /// the tail-race data is dropped rather than misattributed. TakeStreamingCapture { prompt_id: String, #[allow(private_interfaces)] respond_to: oneshot::Sender>, }, /// Persist the current git HEAD commit and branch to summary.json. /// /// Sent at the end of each prompt turn so `--restore-code` sees the latest /// HEAD even when the `GitHeadChanged` filesystem watcher misses events. PersistGitHead { commit: Option, branch: Option, }, }