Skip to content

Type Alias: AgentPaneCommand

agentmux


agentmux / agent-pane-state-store / AgentPaneCommand

AgentPaneCommand = { type: "InitStart"; } | { at: number; type: "InitReady"; } | { at: number; reason: string; type: "InitFailed"; } | { at: number; type: "StreamSubscribe"; } | { at: number; type: "StreamUnsubscribe"; } | { addedCount: number; at: number; turnRelevant?: boolean; type: "StreamFlushObserved"; } | { nowMs: number; type: "StreamWatchdogTick"; } | { active: boolean; at: number; type: "ReconcileTurnActive"; } | { at?: number | null; model?: string | null; reportedWindows?: ReportedContextWindows | null; tokens?: number | null; type: "ReconcileContextFromHistory"; } | { type: "ContextWindowsReported"; windows: ReportedContextWindows; } | { reason: "fresh_session" | "transcript_replaced"; type: "ContextInvalidated"; } | { model: string; type: "ContextModelSwitched"; } | { at: number; content?: string; hidden?: boolean; type: "TurnStart"; } | { stats: SessionStats | null; type: "TurnEnd"; } | { type: "TurnReset"; } | { ledger: TurnLedger; type: "TurnObserved"; } | { type: "TurnStartFailed"; } | { arg?: string; id?: string; name: string; params?: Record<string, unknown>; parentId?: string; type: "ToolStart"; } | { id: string; text: string; type: "ToolProgress"; } | { at: number; id: string; type: "ToolOutput"; } | { text: string; type: "ThinkingHeadline"; } | { id?: string; type: "ToolEnd"; } | { cacheCreation?: number; cacheRead?: number; freshInput?: number; input: number; model?: string; type: "TokensIn"; } | { output: number; type: "TokensOut"; } | { type: "StreamSessionStarted"; } | { chars: number; kind?: "text" | "thinking" | "tool_input"; type: "OutputStreamed"; } | { type: "RequestStarted"; } | { type: "ModelEndedTurn"; } | { type: "ModelMessageStarted"; } | { at: number; type: "RequestStop"; } | { type: "StopFailed"; } | { at: number; type: "InterruptTimeoutElapsed"; } | { at: number; type: "SubmitTimeoutElapsed"; } | { at: number; attachments?: object[]; enqueuedWhileBusy: boolean; id: string; text: string; type: "PendingMessageQueued"; } | { id: string; type: "PendingMessageFlushStarted"; } | { id: string; type: "PendingMessageAccepted"; } | { id: string; type: "PendingMessageRejected"; } | { id: string; type: "PendingMessageExpired"; } | { at: number; reason: "rate_limited"; retryAfterMs: number | null; type: "ProviderWaiting"; } | { at: number; failure: AgentFailure; turnAttempted?: boolean; type: "FailureObserved"; } | { type: "FailureCleared"; } | { type: "DetailsToggle"; } | { type: "DetailsExpand"; } | { type: "DetailsCollapse"; } | { type: "StashToggle"; } | { type: "StashExpand"; } | { type: "StashCollapse"; } | { at: number; trigger: "manual" | "auto"; type: "CompactionStarted"; } | { at: number; boundaryUuid?: string | null; durationMs: number; frameTimestamp?: string | null; postTokens: number; preTokens: number; trigger: "manual" | "auto"; type: "CompactionBoundary"; } | { at: number; type: "AttachedTaskObserved"; } | { type: "AttachedTaskCleared"; } | { at: number; type: "ResumeRetryStarted"; } | { type: "ResumeRetryResolved"; } | { at: number; type: "RegistryAttachedTaskObserved"; } | { type: "RegistryAttachedTaskCleared"; }

Defined in: agent-pane-state/types.ts:636

{ type: "InitStart"; }

Caller signal: history fetch began. Idempotent — if the pane is already in any terminal state (InitReady / InitFailed), InitStart is a no-op (we don’t reset back to pending).


{ at: number; type: "InitReady"; }

Caller signal: history fetch resolved successfully.


{ at: number; reason: string; type: "InitFailed"; }

Caller signal: history fetch failed; reason surfaced for diagnostics.


{ at: number; type: "StreamSubscribe"; }

Hook signal: subscription is up.


{ at: number; type: "StreamUnsubscribe"; }

Hook signal: subscription torn down.


{ addedCount: number; at: number; turnRelevant?: boolean; type: "StreamFlushObserved"; }

RAF flush observed by useAgentStream — bumps bufferSize + lastEventTime. No state change beyond those counters.

addedCount: number

at: number

optional turnRelevant?: boolean

Did this flush carry anything that is evidence the TURN is alive — document nodes, node updates, tool chunks, or a new shell node?

false means the flush was shell OUTPUT only. That is a live BACKGROUND process writing to its dock row; it says nothing about whether the model is doing anything, so it must not refresh the turn idle clock or resurrect a finished turn. Omitted is treated as true — only the flush queue knows the difference, and only it sets this.

See docs/reports/REPORT_AGENT_PANE_PROGRESS_INDICATORS_CONSOLIDATION_2026_09_09.md.

type: "StreamFlushObserved"


{ nowMs: number; type: "StreamWatchdogTick"; }

Periodic tick from useAgentStream’s watchdog interval. Emits a stream-stuck diagnostic past STUCK_THRESHOLD_MS, and — past the longer LIVENESS_RECOVERY_MS — force-recovers a hung Streaming turn (no tool active, not rate-limited) to Idle, clearing a stuck “Working” that never received its terminal session_end. See SPEC_WORKING_STATE_LIVENESS_MODEL_2026_06_29.md.


{ active: boolean; at: number; type: "ReconcileTurnActive"; }

Reconciliation from BlockControllerRuntimeStatus.turn_active (backend-verified, from the health monitor wired to the NDJSON stream — see crates/srv/src/backend/blockcontroller/health.rs), fetched via GetControllerStatus at mount AND dispatched on every live controllerstatus MPS event thereafter (useControllerStatusEvents). Bidirectional:

  • active: true — promote the mount-default Idle to Streaming when the backend reports a turn already in flight. Corrects ONLY an Idle phase; never overrides a phase a real stream/user event already produced (TurnStart, StreamFlushObserved, etc. win if they got there first). #2005 Finding 1.
  • active: false — demote a stuck Streaming phase to Idle. The backend flips turn_active false only on the CLI’s result event (genuine turn-end), so this is authoritative. Normally the frontend reaches Done on its own via session_end; this covers the case where it MISSED that event (pane backgrounded/unmounted across the transition, or a dropped MPS event) and would otherwise stay Streaming forever — the Agent1 stuck-”Working” / Agent2 stuck-”Queued” class. Only Streaming is demoted: Submitting is covered by SUBMIT_TIMEOUT (and is where the send-race lives — a just-dispatched local TurnStart racing a backend that hasn’t yet marked the turn active), Interrupting by INTERRUPT_TIMEOUT, Done/Idle/Disconnected are already correct. See docs/retro/retro-agent2-stuck-queued-message-2026-07-16.md.

{ at?: number | null; model?: string | null; reportedWindows?: ReportedContextWindows | null; tokens?: number | null; type: "ReconcileContextFromHistory"; }

Mount-time reconciliation: history replay found the main agent’s last API call from before this pane went live (HistoryParser.lastContext — that call’s whole prompt and model, never a result’s turn total). Seeds context so the meter shows a real number immediately instead of sitting blank until the first live TokensIn. Seeds only while contextSeedable (no live reading, invalidation, compaction or reset yet) — a historical snapshot must never clobber real-time data. An implausible seed is refused (context-reading-rejected). tokens is absent when the restored window holds no main-agent call. reportedWindows (the history’s own result.modelUsage windows) merge UNDER any reported live, either way. See docs/plans/PLAN_PANE_REOPEN_SESSION_RESUME_AND_STATS_BAR_2026_07_10.md and docs/reports/REPORT_AGENT_PANE_CONTEXT_METER_2026_10_05.md.

optional at?: number | null

When that call was received (unix ms), if the line was stamped.

optional model?: string | null

optional reportedWindows?: ReportedContextWindows | null

optional tokens?: number | null

type: "ReconcileContextFromHistory"


{ type: "ContextWindowsReported"; windows: ReportedContextWindows; }

A result frame reported context windows (modelUsage[*].contextWindow). Recorded per model; the current reading’s window is re-resolved.


{ reason: "fresh_session" | "transcript_replaced"; type: "ContextInvalidated"; }

The conversation the reading measured is gone — a fresh session replaced it, or the transcript was archived or restored. Clears context (and ends contextSeedable), so the next call’s reading isn’t read as a compaction by the TokensIn heuristic and a late history seed can’t bring the old conversation back.


{ model: string; type: "ContextModelSwitched"; }

The pane’s model setting changed (/model, the model menu). The conversation, and so the reading’s tokens, carry over; the window is the new model’s, known only when its first reply reports it, so the reading’s window becomes unknown until then. model is the setting’s value (often a family alias, e.g. sonnet, which doesn’t say its window).


{ at: number; content?: string; hidden?: boolean; type: "TurnStart"; }

User pressed send — turn becomes active. Also clears stale sessionStats from the previous turn.

content — the literal text just submitted, threaded through into TurnPhase.Submitting.pendingContent (optional so the ~90 existing reducer/hook test call sites that construct this action without it keep compiling — the reducer defaults to "" when absent). Used by useAgentActivitySummary.ts to derive the session-title Haiku call without a separate FileStore read. See docs/specs/SPEC_AMBIENT_PANE_TITLE_OVERALL_GOAL_TRACKING_2026_08_17.md.

at: number

optional content?: string

optional hidden?: boolean

True for a hidden memory-reinjection turn (memory-reinjection-controller.ts) — never set by any ordinary send. Propagated onto TurnPhase.Submitting.hidden so every ambient side-effect hook keyed off turnPhase (useAgentActivitySummary.ts, useNextPromptSuggestion.ts, and any future one) can skip its own LLM call for this turn without needing separate wiring — turnPhase already reaches them as their effect dependency. Found necessary the hard way: useAgentActivitySummary.ts forwarded pendingContent (unconditionally, regardless of source) to an ambient summarization call whose result became the human-visible pane title — reagentx P0 on PR #3502. Defense-in-depth, not the only fix: content for a hidden turn is ALSO never the real message (see memory-reinjection-controller.ts) — a consumer that forgets to check hidden still never sees real content, only a safe placeholder.

type: "TurnStart"


{ stats: SessionStats | null; type: "TurnEnd"; }

Stream produced session_end (or fallback timer fired). Final stats merged with current turn-tokens, which are still there when the backend’s turn-ended push already moved the phase to Idle. Clears currentTool and turnTokens, and transitions the phase to Done (interrupting → Done.stopped, otherwise Done.completed).


{ type: "TurnReset"; }

Truncate path: the doc was reset (or whatever caused the local stream-restart). Clears all per-turn state but does NOT touch pending or streaming.


{ ledger: TurnLedger; type: "TurnObserved"; }

srv published the block’s turn ledger (agentturn). An older turn’s, or an older state of this one, landing late is ignored.


{ type: "TurnStartFailed"; }

Revert an OPTIMISTIC TurnStart when the turn never actually began — the initiating send’s own RPC call failed synchronously (no controller registered, the identity spawn gate blocked it, a plain network rejection), or the input was a slash/bang command handled locally. Phase-only: unlike TurnReset, this must NOT touch sessionStats/sessionTotals/context — those accumulate across a pane’s whole lifetime, and a transient send failure on an agent with prior completed turns must not wipe that history (reagent/codex P2 on PR #2318 — TurnReset was reused here first and incorrectly cleared them). See useAgentCommands.ts’s deliverToBackend.


{ arg?: string; id?: string; name: string; params?: Record<string, unknown>; parentId?: string; type: "ToolStart"; }

id: the tool_use id, so its end removes exactly it; params: its input, for the live status’s words (tool-labels.ts).


{ id: string; text: string; type: "ToolProgress"; }

A running call’s test progress, from its live output (status/test-progress.ts).


{ at: number; id: string; type: "ToolOutput"; }

A running call wrote output (at most every few seconds per call).


{ text: string; type: "ThinkingHeadline"; }

The model’s thinking block’s gist (status/thinking-headline.ts).


{ id?: string; type: "ToolEnd"; }


{ cacheCreation?: number; cacheRead?: number; freshInput?: number; input: number; model?: string; type: "TokensIn"; }


{ output: number; type: "TokensOut"; }


{ type: "StreamSessionStarted"; }

The stream shows a new CLI session starting (system/init, not the one a compaction writes mid-turn; view/agent/session-start.ts): any turn still holding tokens ended without a result (the process died), so they belong to no turn that is coming.


{ chars: number; kind?: "text" | "thinking" | "tool_input"; type: "OutputStreamed"; }

Characters the main agent’s latest call streamed (text, thinking, tool input) since the last one: its output estimate until TokensOut.


{ type: "RequestStarted"; }

The main agent’s tool results went back: a request is in flight (↑) until the next call’s TokensIn.


{ type: "ModelEndedTurn"; }

The main agent’s model sent stop_reason: end_turn; see modelEndedTurn on the Streaming phase.


{ type: "ModelMessageStarted"; }

The main agent’s model began a new message; it is working again.


{ at: number; type: "RequestStop"; }

User pressed Esc / clicked Stop.


{ type: "StopFailed"; }

Stop RPC failed — bail on the stopping state.


{ at: number; type: "InterruptTimeoutElapsed"; }

Interrupt timeout watchdog fired (caller-scheduled setTimeout — see schedule-interrupt-timeout event). Bounded Interrupting → Done force-transition: if the agent never acks the SIGINT (backend died, stream dropped, etc.) the pane was previously stuck in Interrupting forever and the working animation would never settle. This command is a no-op if the phase has already moved off Interrupting (e.g. agent acked first, user already resumed). Spec §8.


{ at: number; type: "SubmitTimeoutElapsed"; }

Submit timeout watchdog fired (caller-scheduled setTimeout — see schedule-submit-timeout event). PR D — bounded Submitting → Done.errored force-transition: if the backend never acks a TurnStart with a chunk / token / tool event within SUBMIT_TIMEOUT_MS, the pane was previously stuck in Submitting forever (no isWorking → false signal) and the working animation would never settle. No-op if the phase has already moved off Submitting (e.g. promotion to Streaming via flush / bumpEvent, user already stopped, stream dropped). Spec §8 / issue #728 gap 2.


{ at: number; attachments?: object[]; enqueuedWhileBusy: boolean; id: string; text: string; type: "PendingMessageQueued"; }

at: number

optional attachments?: object[]

Images sent with the message (ids + names, tray order).

enqueuedWhileBusy: boolean

True when the user sent this message while a turn was already in-flight (isWorking was true before TurnStart). False for idle sends. Stored on the PendingMessage entry and used by PendingMessagesPanel to gate visibility — idle-send messages must never flash in the amber queued zone. See ANALYSIS_IDLE_SEND_RACE_2026_06_11 (never committed to this repo).

id: string

text: string

type: "PendingMessageQueued"


{ id: string; type: "PendingMessageFlushStarted"; }

flushHeldMessages (useAgentCommands.ts) is about to actually deliver this held message — dispatched right before the deliverToBackend call, i.e. the moment sending genuinely begins, not merely “the turn it was queued behind has ended” (those can be far apart when a controller refresh is deferred — see the PendingMessageFlushStarted reducer arm’s own doc comment). Marks the entry flushing so PendingMessagesPanel can stop claiming “sends at the agent’s next step” once that’s no longer true. See docs/specs/SPEC_AGENT_WORKING_STATE_UNIFICATION_2026_09_04.md Phase 1.


{ id: string; type: "PendingMessageAccepted"; }

Backend acknowledged the message — remove from pending.


{ id: string; type: "PendingMessageRejected"; }

RPC failed — remove the entry so user doesn’t see a ghost row.


{ id: string; type: "PendingMessageExpired"; }

Pending entry ageing watchdog fired (caller-scheduled setTimeout). Removes the entry by id; idempotent no-op if already accepted/rejected. Issue #728 gap 2 — prevents zombie pending rows when the backend never acknowledges a queued message (e.g. RPC hang post-send).


{ at: number; reason: "rate_limited"; retryAfterMs: number | null; type: "ProviderWaiting"; }

Provider is rate-limited (429) and waiting to retry. Updates lastEventMs (keeps the stuck-stream watchdog quiet) and sets waitingReason on Streaming so the working row can show “Rate limited — retrying…” instead of the thinking phrase. Cleared automatically on the next real activity (bumpEvent).


{ at: number; failure: AgentFailure; turnAttempted?: boolean; type: "FailureObserved"; }

Backend classified a failure for this pane (the agentfailure wave event). Unconditionally force-ends a working turn (Submitting / Streaming / Interrupting → Done.errored) — this is what closes the “stuck Waiting after a rate-limit interruption” bug: an authoritative failure classification no longer depends on the CLI process actually exiting (persistent-mode agents never do between turns) to clear the turn phase. See SPEC_AGENT_PANE_UNIFIED_FAILURE_REDUCER_2026_07_06.md.

at: number

failure: AgentFailure

optional turnAttempted?: boolean

See PaneFailure.turnAttempted. Omitted by the backend-event path (a classified failure always FOLLOWS a turn that ran), set to false only by the pane’s own synthetic pre-launch auth failure.

type: "FailureObserved"


{ type: "FailureCleared"; }

User dismissed the failure row (Dismiss / ×). Episode over.


{ type: "DetailsToggle"; }

Toggle the log panel open/closed.


{ type: "DetailsExpand"; }

Explicitly open the log panel. Idempotent if already open.


{ type: "DetailsCollapse"; }

Explicitly close the log panel. Idempotent if already closed.


{ type: "StashToggle"; }

Toggle the Stash drawer open/closed. Drives the header icon’s own toggle.


{ type: "StashExpand"; }

Explicitly open the Stash drawer. Idempotent if already open.


{ type: "StashCollapse"; }

Explicitly close the Stash drawer. Idempotent if already closed.


{ at: number; trigger: "manual" | "auto"; type: "CompactionStarted"; }

The PreCompact hook fired — compaction has begun. Sourced from the compaction_started MPS event (agentmux-bashwrap precompact → useCompactionStream.ts). Sets compacting so the view can show a live “Compacting…” status chip + elapsed counter (Tier 1/2). Also bumps lastEventMs like ProviderWaiting does, so the stuck-stream watchdog doesn’t misfire during a long compaction that produces no other stream activity.


{ at: number; boundaryUuid?: string | null; durationMs: number; frameTimestamp?: string | null; postTokens: number; preTokens: number; trigger: "manual" | "auto"; type: "CompactionBoundary"; }

The real compact_boundary frame arrived — compaction finished. Sourced from the backend’s AgentEvent::CompactionBoundary (translated 1:1 from Claude Code’s stream-json system frame — exact data, not inferred). Clears compacting, records lastCompactionBoundaryAt (dedup guard for the TokensIn heuristic), and reconciles context to postTokens so the context-fill bar reflects the real post-compaction size immediately rather than waiting for the next TokensIn.

at: number

optional boundaryUuid?: string | null

The frame’s own uuid: keys the emitted context-compacted node’s id, so it matches parseHistoryLines.ts’s id for the same line. Optional for callers without a frame.

durationMs: number

optional frameTimestamp?: string | null

The compact_boundary frame’s own timestamp field, raw (not parsed/reformatted) — absent/null if the frame didn’t have one (or the caller doesn’t have a raw frame at all, e.g. a test constructing this command directly). A time value only: the boundary’s completion time for the stale-boundary checks and the node’s time, falling back to at / receipt time when absent (the CLI’s stdout frame has none). Never an id — see boundaryUuid.

postTokens: number

preTokens: number

trigger: "manual" | "auto"

type: "CompactionBoundary"


{ at: number; type: "AttachedTaskObserved"; }

Fires on the 0→1 transition of “≥1 PinnedActivity is running for this block.” No-op (idempotent) if an episode is already in progress — must NOT reset since when a second task starts running alongside an already-running one.


{ type: "AttachedTaskCleared"; }

Fires on the 1→0 transition — the last currently-running PinnedActivity for this block just ended. No-op if already null.


{ at: number; type: "ResumeRetryStarted"; }

mps.EVENT_AGENT_RESUME_RETRY’s {status:"retrying"} landed — a stale --resume was just detected and a retry/recovery attempt is about to fire. No-op (idempotent) if already reconnecting, mirroring AttachedTaskObserved — a cascaded second retry must not reset startedAt back to a later time.


{ type: "ResumeRetryResolved"; }

{status:"resolved"} landed — the retry’s outcome (Fresh or Resumed) is now known. No-op if already null.


{ at: number; type: "RegistryAttachedTaskObserved"; }

Sets registryAttachedTaskSince verbatim (idempotent set, not an edge-triggered toggle like AttachedTaskObserved — the registry’s own earliest-running-task computation already handles the “already observed” case, so the reducer doesn’t need its own no-op guard).


{ type: "RegistryAttachedTaskCleared"; }

Clears registryAttachedTaskSince back to null.