Files
Kigi-CLI/crates/codegen/kigi-shell/src/session/commands.rs
T
雷电芽衣 63d26d4a3e fix(persistence): FlushAndAck is a durability barrier that reports lost writes
The persistence actor acked FlushAndAck unconditionally after writing its
pending merged notification: nothing was fsynced and every storage failure
was only warn-logged, so persist_ack, the reconnect flush and history
repair treated lost writes as persisted.

The barrier now writes the pending notification, reports the first storage
failure latched since the last barrier (every actor arm latches), and
fsyncs the whole session directory tree (files, directories, deletions).
Session directories are created durably (init and fork copy), and the
shared summary writer syncs its staged bytes before publishing and the
parent afterwards, so a concurrent /rename cannot publish unsynced bytes
over a synced summary.json. FlushComplete and the persist_ack, history
repair and end-of-turn callers propagate the io::Result; CopyFile fails on
a sync error but keeps the latch for the barrier.

Ported from upstream 1.0.1x (grok-build persistence.rs pending_write_error
+ durable storage helpers), reshaped without the dirty-file set.

Claude-Session: https://claude.ai/code/session_01RUqV8HVGXcAETyYbZ2tPQ4
2026-09-13 06:51:27 -04:00

715 lines
33 KiB
Rust

//! Session actor command enum and associated public types.
//!
//! `SessionCommand` defines the message protocol used to drive a session actor.
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.
#[derive(Debug, Clone, Default, serde::Deserialize)]
pub struct CancellationContext {
pub tool_name: Option<String>,
pub reason: Option<String>,
pub hook_name: Option<String>,
/// 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<String>,
}
/// Prompt completion kind returned to the ACP layer.
#[derive(Debug, Clone)]
pub enum PromptCompletionKind {
Completed,
Cancelled {
category: Option<kigi_file_utils::events::types::CancellationCategory>,
context: Option<CancellationContext>,
},
MaxTurnsReached {
limit: usize,
},
/// One tool call repeated past its ceiling. Reported as `EndTurn`:
/// the turn ended, nobody interrupted it.
StationarityHalted {
tool_name: String,
run_len: u32,
true_noop: bool,
},
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<TurnDeltaSnapshot>,
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<Result<serde_json::Value, String>>,
pub usage: Option<crate::extensions::notification::PromptUsage>,
}
pub type PromptTurnResult = Result<PromptTurnOk, acp::Error>;
pub(crate) fn ok_end_turn(tokens: u64, snapshot: Option<TurnDeltaSnapshot>) -> 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<acp::ContentBlock>,
/// 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<String>,
/// 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<String>,
/// Skip `<user_query>` 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<String>,
json_schema: Option<serde_json::Value>,
/// 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<PromptTurnResult>,
/// 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<oneshot::Sender<()>>,
},
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<String>,
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<u8>` so it can
/// update without `&mut self`).
auto_compact_threshold_percent: u8,
/// Ok: the applied model id and the session's effective context
/// window, which clients repaint from without waiting for a turn.
responds_to: oneshot::Sender<Result<(acp::ModelId, std::num::NonZeroU64), acp::Error>>,
},
/// 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<Result<(), acp::Error>>,
},
/// 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<String, String>,
/// 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<std::num::NonZeroU64>,
},
GetCurrentModel {
responds_to: oneshot::Sender<String>,
},
GetCurrentPromptMode {
responds_to: oneshot::Sender<PromptMode>,
},
GetModelMetadata {
responds_to: oneshot::Sender<kigi_chat_state::ModelMetadata>,
},
/// Snapshot for `/session-info`.
GetSessionInfo {
responds_to: oneshot::Sender<SessionInfoData>,
},
/// Compacts the current session, saving on the context window
CompactSession {
/// Optional user-provided context to guide the compaction
user_context: Option<String>,
respond_to: oneshot::Sender<acp::Result<()>>,
},
/// Reload plugin hooks and registry mid-session.
ReloadPlugins {
registry: Option<std::sync::Arc<kigi_agent::plugins::PluginRegistry>>,
},
/// 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<acp::Result<bool>>,
},
/// 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<anyhow::Result<RewindResponse>>,
},
/// 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<anyhow::Result<kigi_chat_state::compaction_utils::HistoryRepairReport>>,
},
GetRewindPoints {
respond_to: oneshot::Sender<RewindPointsResponse>,
},
/// 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<std::collections::HashMap<usize, usize>>,
},
/// 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<String>,
/// 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<String>,
respond_to: oneshot::Sender<()>,
},
/// Shared error-path usage attach (same policy as durable TurnCompleted).
ErrorPathUsageFallback {
prompt_id: Option<String>,
respond_to: oneshot::Sender<Option<crate::extensions::notification::PromptUsage>>,
},
/// Persist the monotonic telemetry turn counter ("next trace turn") for the session.
SetNextTraceTurn {
next_trace_turn: u64,
request_id: Option<String>,
},
/// 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<anyhow::Result<crate::session::persistence::SessionStateCopy>>,
},
/// Flush the replay buffer and persistence, then report the durability
/// barrier result. Used during reconnect so replay follows persisted content.
FlushComplete {
respond_to: oneshot::Sender<std::io::Result<()>>,
},
/// 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<acp::McpServer>,
respond_to: oneshot::Sender<Result<(), acp::Error>>,
},
/// 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<acp::McpServer>,
respond_to: oneshot::Sender<Result<(), acp::Error>>,
},
/// 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<Result<(), acp::Error>>,
},
/// Read MCP status: which servers are configured, which clients are healthy, what tools.
GetMcpStatus {
respond_to: oneshot::Sender<crate::extensions::mcp::McpStatusSnapshot>,
},
/// Snapshot the session's live MCP client pool for subagent inheritance.
SnapshotMcpPool {
respond_to: oneshot::Sender<Option<crate::session::mcp_servers::SharedMcpPool>>,
},
/// 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<crate::extensions::hooks::ClientHooks>,
},
/// 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<Vec<kigi_sampling_types::ToolSpec>>,
},
/// 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<String>,
tool_name: String,
arguments: serde_json::Value,
respond_to: oneshot::Sender<Result<crate::extensions::mcp::McpCallResponse, String>>,
},
ReadMcpResource {
server_name: String,
uri: String,
respond_to:
oneshot::Sender<Result<crate::extensions::mcp::McpReadResourceResponse, String>>,
},
McpAuthStatus {
respond_to: oneshot::Sender<Vec<crate::extensions::mcp::McpAuthStatusEntry>>,
},
McpAuthTrigger {
server_name: String,
respond_to: oneshot::Sender<Result<(), String>>,
},
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<bool>,
},
/// 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<Result<kigi_tools::types::KillOutcome, String>>,
},
DeleteScheduledTask {
task_id: String,
respond_to: oneshot::Sender<Result<bool, String>>,
},
/// List all background tasks.
/// Routes through the ToolBridge's TerminalBackend.
ListTasks {
respond_to: oneshot::Sender<Option<Vec<kigi_tools::types::TaskSnapshot>>>,
},
/// 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<bool>,
},
GetHooksList {
respond_to: oneshot::Sender<kigi_hooks_plugins_types::HooksListResponse>,
},
/// Execute a hooks management action from the pager modal.
HooksAction {
action: kigi_hooks_plugins_types::HooksAction,
respond_to: oneshot::Sender<kigi_hooks_plugins_types::ActionOutcome>,
},
/// 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<kigi_hooks_plugins_types::ActionOutcome>,
},
/// This session's plugin registry, as served by `kigi/plugins/list`.
PluginsList {
respond_to: oneshot::Sender<Option<std::sync::Arc<kigi_agent::plugins::PluginRegistry>>>,
},
/// 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<acp::ContentBlock>,
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<String>,
title: Option<String>,
level: Option<String>,
},
/// 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<String>,
},
/// 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<String>,
},
/// 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<String>,
},
/// 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<String>,
},
/// 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<String>,
},
/// 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<String>,
/// 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<String>,
},
/// 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<String>,
},
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<anyhow::Result<acp::ExtResponse>>,
},
/// 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<crate::session::persistence::LocalFeedbackEntry>),
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<i64>,
responds_to: oneshot::Sender<FeedbackContext>,
},
/// 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<Option<String>>,
},
/// 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<Result<String, String>>,
},
/// 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<String>,
respond_to: oneshot::Sender<Option<String>>,
},
/// 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<String>,
respond_to: oneshot::Sender<Option<String>>,
},
/// 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<Result<String, String>>,
},
/// 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<String>,
/// Pasted images riding along with the interjection. Empty from
/// text-only / older clients.
images: Vec<acp::ImageContent>,
},
/// 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<Option<kigi_chat_state::TurnCapture>>,
},
/// 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<Option<crate::session::acp_session::StreamingTurnCapture>>,
},
/// 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<String>,
branch: Option<String>,
},
}