Expand description
Block controller: manages lifecycle of each block (terminal, command, web app). Port of Go’s pkg/blockcontroller/blockcontroller.go.
Architecture:
- Global controller registry maps block_id → Controller
- Each controller manages the lifecycle of one block
- ShellController handles “shell” and “cmd” block types
- Controllers dispatch I/O between the user and the process/service
Modules§
- acp
- AcpController: manages agent CLIs that speak the Agent Client Protocol (ACP).
- agent_
lock - In-memory registry of PtyShell agent leases — the authoritative, hot-path answer to “is a human’s keystroke for this block currently locked out?”.
- app_
server - Provider-neutral App Server process and JSONL/JSON-RPC transport.
- app_
server_ controller - First-class controller shell for a Codex App Server process.
- app_
server_ protocol - Codex App Server’s stable thread/turn adapter.
- core
- Shared helpers used by all block controllers (persistent, subprocess, acp).
- durable_
mode - Whether a shell pane runs durable (SPEC_REMOTE_TERMINALS_AND_DURABLE_SESSIONS_2026_10_02.md §7.1): plain, durable because a setting says so, or durable by default.
- durable_
ssh - Durable SSH terminals (P4 of SPEC_REMOTE_TERMINALS_AND_DURABLE_SESSIONS_2026_10_02.md, §7).
- health
- Per-block turn-activity tracking (is a turn currently in flight, and
the last process exit code). Previously part of a larger “agent health”
detector that also did silence-based unresponsive detection; that
detection logic was removed (see
docs/specs/SPEC_REMOVE_AGENT_UNRESPONSIVE_DETECTION_2026_08_25.md) —
this struct keeps only the turn-active bookkeeping, which several other
subsystems depend on independently of the removed detector
(
broker::process::lifecycle_from, the Swarm pane’s running/idle badge, subagent-watcher reconciliation,muxspect describe). - persistent
- PersistentSubprocessController: manages agent CLI as a long-running process with bidirectional NDJSON streaming via stdin/stdout.
- persistent_
resume 🔒 - One spawn attempt’s stale-
--resumeretry decision, plus any terminal error-result line observed for it, as a single explicit state machine. - pidregistry
- Block PID registry: maps blockId → OS process ID for per-pane metrics collection. Used by the sysinfo loop to query per-process CPU/memory stats.
- process_
tree - Process tree traversal: BFS from a root PID to collect all descendant PIDs. Used by the sysinfo loop to aggregate CPU/memory across entire process trees.
- session_
recovery - Session recovery after unclean shutdown (Phase 4.2 — ultra-long-sessions).
- session_
stats - Session metadata tracking for agent panes (Phase 1.4 — ultra-long-sessions).
- shell
- ShellController: manages lifecycle of shell and command blocks. Port of Go’s pkg/blockcontroller/shellcontroller.go.
- subprocess
- SubprocessController: manages agent CLI as stateless per-turn subprocess invocations.
- term_
size - The size each block’s PTY was last given: at spawn (its runtime options) and on every resize. Kept so a reader that doesn’t drive the terminal (a paired device watching it) can render at the desktop’s width; the PTY itself keeps no record srv can ask.
- watchdog
- Agent process watchdog: kills agent panes that exceed max-runtime or idle-output limits.
Structs§
- Block
Controller Runtime Status - Runtime status of a block controller, sent to the UI.
- Block
Input Union - Input sent to a block controller. Can be raw terminal data, a signal, or a resize event.
- Message
Accepted Event - The
agent-message-acceptedevent’s payload (mps_events::AgentMessageAccepted).
Enums§
- Agent
Delivery - How a controller-aware agent message was delivered.
- Send
Outcome - What an automated send to a persistent agent did with one message.
- Stop
Outcome - How a
Controller::shutdownended. Logged per closed agent (spec §9.8) — a provider that is oftenKilledneeds a better shutdown or a longer grace period.
Constants§
- BLOCK_
CONTROLLER_ ACP - BLOCK_
CONTROLLER_ APP_ SERVER - BLOCK_
CONTROLLER_ CMD - BLOCK_
CONTROLLER_ PERSISTENT - BLOCK_
CONTROLLER_ SHELL - BLOCK_
CONTROLLER_ SUBPROCESS - DEFAULT_
GRACEFUL_ KILL_ WAIT_ MS - Grace period before forceful kill in milliseconds.
- DEFAULT_
TIMEOUT_ MS - Default controller operation timeout in milliseconds.
- META_
KEY_ CMD - META_
KEY_ CMD_ ARGS - META_
KEY_ CMD_ CLEAR_ ON_ START - META_
KEY_ CMD_ CLOSE_ ON_ EXIT - META_
KEY_ CMD_ CLOSE_ ON_ EXIT_ DELAY - META_
KEY_ CMD_ CLOSE_ ON_ EXIT_ FORCE - META_
KEY_ CMD_ CWD - META_
KEY_ CMD_ ENV - META_
KEY_ CMD_ INIT_ SCRIPT - META_
KEY_ CMD_ INIT_ SCRIPT_ BASH - META_
KEY_ CMD_ INIT_ SCRIPT_ FISH - META_
KEY_ CMD_ INIT_ SCRIPT_ PWSH - META_
KEY_ CMD_ INIT_ SCRIPT_ ZSH - META_
KEY_ CMD_ JWT - META_
KEY_ CMD_ RUN_ ONCE - META_
KEY_ CMD_ RUN_ ON_ START - META_
KEY_ CMD_ SHELL - META_
KEY_ CONNECTION - META_
KEY_ CONTROLLER - META_
KEY_ SHELL_ SUBBLOCK_ ID - On an agent block: the block id of its shell drawer, the headless terminal the drawer, the agent’s PtyShell and a paired device’s pane list share.
- META_
KEY_ TERM_ LOCAL_ SHELL_ OPTS - META_
KEY_ TERM_ LOCAL_ SHELL_ PATH - RESYNC_
ERR_ ALREADY_ EXITED - Sentinel error string
resync_controllerreturns when it declines to respawn aSTATUS_DONEcontroller because its caller passedrespawn_if_done: false(see that parameter’s own doc comment). Callers that want to treat “already exited” differently from a genuine failure (e.g.try_attach_to_existing_shellfalling through to a fresh shell instead of surfacing a 500) match on this exact string rather than parsing every other possible error messageresync_controllercan return. - SHUTDOWN_
GRACE - Grace period for closing agents: one deadline per close, shared by every agent it stops (spec §9.2). Matches the kill task’s long-standing 5s.
- STATUS_
DONE - STATUS_
INIT - STATUS_
RUNNING
Statics§
- CLOSE_
ON_ 🔒EXIT_ HANDLER - CLOSING_
BLOCKS 🔒 - Blocks whose pane is being closed. Between stopping a block’s
controller and deleting its record, a
resync_controller(frontend ControllerResync,agent.open, …) would otherwise find the block still present and spawn a fresh process for it — an orphan the close never sees. See docs/specs/SPEC_AGENT_PANE_CLOSE_GRACEFUL_SHUTDOWN_2026_09_18.md §4.2 step 1. - CONTROLLER_
REGISTRY 🔒 - Thread-safe global controller registry.
Maps block_id → Arc
.
Traits§
- Controller
- Trait for block controllers. Each block type has its own implementation.
Port of Go’s
blockcontroller.Controllerinterface.
Functions§
- agent_
id_ 🔒from_ env - The agent id an env map names:
AGENTMUX_AGENT_ID, else the legacyWAVEMUX_AGENT_ID, trimmed, skipping blank values. - agent_
runtime_ 🔒status - The status an agent-CLI controller reports: a local agent pane with no PID, program name or spawn time, so only these five fields vary between sites.
- close_
on_ 🔒exit - Trigger the installed close-on-exit handler for
(tab_id, block_id), if one has been installed. Fire-and-forget: the handler owns spawning whatever async work it needs (the saga call isasync; this function itself is not). A missing handler (astronomically early in boot, beforemain.rsfinishes wiringAppState— no real caller can reach this before then, since nothing spawns a PTY that early) just skips closing; the pane sits inert the same way it did before this feature existed, not a hard failure. - closing_
blocks_ still_ running - Blocks mid-close whose process has not exited yet. Their controllers have
already left
CONTROLLER_REGISTRY, so a registry scan can’t see them. - cmd_
env_ of - A block’s
cmd:envas a string map. The frontend writes an object; the JSON-string form is accepted too, so every spawn path reads the same env. - delete_
controller - Unregister (delete) a controller by block ID, stopping it first. Removes from the registry before calling stop() so no new callers can reach it.
- deliver_
agent_ message - Deliver an inter-agent / muxbus message to a running agent the way its controller expects.
- deliver_
agent_ message_ from deliver_agent_message, saying who the input is from (turn provenance, SPEC_AGENT_SELF_QUIT §6.3). Only the persistent controller records it (seesend_user_message_outcome_from); ACP and App Server turns carry no provenance on any path today, the user’s own typed turn included.- get_
all_ controllers - Get all controllers (snapshot).
- get_
block_ controller_ status - Get the runtime status for a block’s controller. Returns None if no controller is registered.
- get_
controller - Get a controller by block ID.
- is_
closing - is_
false 🔒 - is_
runtime_ 🔒config_ only_ replace - Is this forced replace merely a runtime-config change (model / effort / permission) on a controller that stays persistent — i.e. the one case that may be deferred to the end of an in-flight turn rather than killing it?
- mark_
closing - Refuse to (re)spawn a controller for
block_iduntilunmark_closing. - mark_
closing_ stopped - Signal that a closing block’s process has exited. The block stays marked
closing (no respawn) until
unmark_closing. - meta_
string_ map - A meta value that is an object of strings, or a JSON string encoding one, as a string map. Non-string values are skipped; any other shape is empty.
- notify_
tracked_ 🔒blocks_ changed - Announce a
CONTROLLER_REGISTRYmembership change to the Process Broker, if one is running (never set in most unit tests). SeeProcessBroker::emit_tracked_blocks_changed’s doc comment for why this exists:agent.tracked-blocksreads this registry directly, but nothing used to tell a subscriber when a write here changed what that read would return — Swarm’s client-side list depended on two unrelated, independently- timed events instead, and went stale whenever neither happened to fire for a given block. See docs/reports/REPORT_SWARM_MOUNT_DEPENDENT_TRACKING_GAP_2026_09_15.md. - pane_
note - A line from AgentMux in a terminal pane’s output: dimmed, in brackets, on a line of its own, so it reads as AgentMux’s and not the shell’s.
- publish_
controller_ status - Publish a controller status event via MPS broker. The sole publish point
for
controllerstatus, used by every controller type (persistent CLI, subprocess CLI, ACP agents, plain shell/PTY panes — 13 call sites). - publish_
message_ 🔒accepted - Publish
agent-message-acceptedformessage_idonblock:<id>, telling the frontend to promote that pending entry from queued to in-document. - register_
controller - Register a controller, stopping any previous one for the same block.
- release_
block_ processes - Drop the block’s process tracker (killing anything left in its tree) and
its Process Broker entry. The second half of
delete_controller; a graceful close calls it only after the process has exited. - remove_
controller_ 🔒entry_ only - Remove a controller from
CONTROLLER_REGISTRYonly — does NOT touch the process tracker or the Process Broker’s cached status, unlikedelete_controllerbelow. Forresync_controller’s replace path (session restart), where a NEW controller for the same block is about to be registered right after this call:AgentProcessRegistry:: ensure_trackeris idempotent by design (“the job survives controller re-creation” — its own doc comment), so leaving the tracker alone here means any declared-background descendant (e.g.task dev) the old controller’s process spawned stays alive and gets reattached to the new controller’s owntrack_spawnedcall, instead of dying with the old one. The caller is responsible for actually terminating the OLD controller’s own process first (viastop_for_replace, notstop) — this function only ever touches the registry map. See docs/specs/SPEC_BACKGROUND_TASK_TEARDOWN_SURVIVAL_2026_08_20.md. - resync_
controller - Resync a block’s controller — the main entry point for starting/restarting blocks.
Port of Go’s
ResyncController. - send_
input - Send input to a block’s controller.
- set_
close_ on_ exit_ handler - Install the close-on-exit callback. Called exactly once, from
bootstrap::install_close_on_exit_handlerright afterAppStateis built inmain.rs— see that function’s doc comment for what the callback actually does. A second call is a no-op (OnceLock::setsilently returnsErron an already-initialized cell, which this discards — matchingbroker::process::set_global’s identical fire-and-forget style; there is exactly one legitimate caller). - stop_
all_ controllers - Stop all running controllers gracefully.
- stop_
block_ controller - Stop a block’s controller gracefully.
- take_
controller - Remove a controller from
CONTROLLER_REGISTRYand hand it back, without stopping it — for the graceful close, which awaitsController::shutdownand only then callsrelease_block_processes. Once removed, no new input reaches it. - unmark_
closing - Lift
mark_closing. Called once the block’s record is gone (or the close failed and the block stays). Wakes anyone still waiting. - wait_
closing_ stopped - Wait until a closing block’s process has exited, or
timeout. Returns immediately for a block that isn’t closing.
Type Aliases§
- Close
OnExit 🔒Fn shell/lifecycle.rs’s PTY wait/cleanup task needs to actually CLOSE a pane (delete its block, prune it from the owning tab’s layout tree, and notify any connected frontend) when the shell inside it exits — but that task lives deep inbackend::blockcontroller, which has noAppStatehandle (only the individualStore/EventBus/BrokerpiecesShellControllerwas constructed with). The actual close logic (crate::sagas::delete_block::run) needs the fullAppState— its reducer state, saga-id allocator, and saga log, not just those three pieces. Rather than threadingAppStatethrough everyShellController::new/resync_controllercall site (a much larger, architecture-inverting change), this mirrors the exact shapecrate::broker::process::set_global/globalalready use for the identical class of problem (broker::process’sAppState-derivedProcessBrokeralso needs to be reachable from deep inblockcontrollerwithout a direct dependency): a process-wide callback slot, set once at boot frombootstrap::install_close_on_exit_handler(which hasAppStatein scope) and read fromshell/lifecycle.rs.- Shutdown
Future