Skip to main content

Module blockcontroller

Module blockcontroller 

Source
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---resume retry 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§

BlockControllerRuntimeStatus
Runtime status of a block controller, sent to the UI.
BlockInputUnion
Input sent to a block controller. Can be raw terminal data, a signal, or a resize event.
MessageAcceptedEvent
The agent-message-accepted event’s payload (mps_events::AgentMessageAccepted).

Enums§

AgentDelivery
How a controller-aware agent message was delivered.
SendOutcome
What an automated send to a persistent agent did with one message.
StopOutcome
How a Controller::shutdown ended. Logged per closed agent (spec §9.8) — a provider that is often Killed needs 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_controller returns when it declines to respawn a STATUS_DONE controller because its caller passed respawn_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_shell falling through to a fresh shell instead of surfacing a 500) match on this exact string rather than parsing every other possible error message resync_controller can 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.Controller interface.

Functions§

agent_id_from_env 🔒
The agent id an env map names: AGENTMUX_AGENT_ID, else the legacy WAVEMUX_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 is async; this function itself is not). A missing handler (astronomically early in boot, before main.rs finishes wiring AppState — 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:env as 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 (see send_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_id until unmark_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_REGISTRY membership change to the Process Broker, if one is running (never set in most unit tests). See ProcessBroker::emit_tracked_blocks_changed’s doc comment for why this exists: agent.tracked-blocks reads 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-accepted for message_id on block:<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_REGISTRY only — does NOT touch the process tracker or the Process Broker’s cached status, unlike delete_controller below. For resync_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_tracker is 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 own track_spawned call, instead of dying with the old one. The caller is responsible for actually terminating the OLD controller’s own process first (via stop_for_replace, not stop) — 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_handler right after AppState is built in main.rs — see that function’s doc comment for what the callback actually does. A second call is a no-op (OnceLock::set silently returns Err on an already-initialized cell, which this discards — matching broker::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_REGISTRY and hand it back, without stopping it — for the graceful close, which awaits Controller::shutdown and only then calls release_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§

CloseOnExitFn 🔒
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 in backend::blockcontroller, which has no AppState handle (only the individual Store/EventBus/Broker pieces ShellController was constructed with). The actual close logic (crate::sagas::delete_block::run) needs the full AppState — its reducer state, saga-id allocator, and saga log, not just those three pieces. Rather than threading AppState through every ShellController::new/resync_controller call site (a much larger, architecture-inverting change), this mirrors the exact shape crate::broker::process::set_global/global already use for the identical class of problem (broker::process’s AppState-derived ProcessBroker also needs to be reachable from deep in blockcontroller without a direct dependency): a process-wide callback slot, set once at boot from bootstrap::install_close_on_exit_handler (which has AppState in scope) and read from shell/lifecycle.rs.
ShutdownFuture