Expand description
SubprocessController: manages agent CLI as stateless per-turn subprocess invocations.
Architecture:
Each user message spawns a fresh claude -p process.
Multi-turn continuity uses --resume <session-id>.
The process reads one JSON message from stdin, runs the agentic loop,
streams NDJSON on stdout, then exits.
State machine: INIT ─(spawn)─> RUNNING ─(process exits)─> DONE DONE ─(new message)─> RUNNING (re-spawn with –resume)
I/O model (2 async tasks per turn):
- stdout_reader: piped stdout → .jsonl persistence + MPS blockfile events on “output” subject
- process_waiter: wait for exit, update status, publish lifecycle event
§Module layout
This module was split from a single ~1770-line subprocess file into a
directory so each piece of the (large, deliberately-not-decomposed-further)
turn state machines could live in its own file:
mod.rs(this file): spawn config, inner state, controller struct + bookkeeping (new/lock helpers/status snapshot/publish), andimpl Controller for SubprocessController.session: session-id capture/hydration state machine.host_spawn:spawn_turn— host-subprocess turn viastd::process.container_spawn:spawn_container_turn— Docker-exec turn viabollard, plus itspublish_lineoutput helper.tests(cfg(test)): unit tests, mostly session-id focused.
host_spawn and container_spawn are impl SubprocessController blocks
in their own files — Rust allows multiple impl <Type> blocks across
files/modules for the same type. Each turn method is moved WHOLE into its
file rather than decomposed further: both are continuous, non-trivially-
ordered state machines where splitting the body would make the control
flow harder to follow, not easier.
Modules§
- argv 🔒
- Provider-aware construction of per-turn subprocess arguments.
- container_
spawn 🔒 - Container-exec turn spawn (
spawn_container_turn) forSubprocessController. - host_
spawn 🔒 - Host-subprocess turn spawn (
spawn_turn) forSubprocessController. - session 🔒
- Session-id capture and hydration state machine for
SubprocessController.
Structs§
- Subprocess
Controller - SubprocessController manages per-turn subprocess lifecycle for agent blocks.
- Subprocess
Controller 🔒Inner - Inner state protected by mutex.
- Subprocess
Spawn Config - Configuration for spawning a subprocess turn.
- User
Record 🔒Sink - Writes the user’s message to the transcript, as the record the persistent
Claude controller writes for each stdin line (Phase 5a-3c): a
{"type":"user","message":{...}}line in the block’s file and the agent’s global zone, published as anecho: "stdin"event — the pane already shows the message (itsagent-message-acceptednode), so the event only keeps its stream gap-free. Without it these CLIs’ transcripts hold no trace of what the user sent: gone on reload, absent from History, and a turn the live feed can’t roll off (SPEC_AGENT_PANE_BOUNDED_LIVE_WINDOW_MIGRATION_2026_09_23.md §6.9).
Constants§
- BLOCK_
CONTROLLER_ SUBPROCESS - SUBPROCESS_
OUTPUT_ SUBJECT - MPS file subject name for subprocess output (replaces “term” from PTY).
Functions§
- persists_
user_ 🔒record - Output formats whose CLI never writes the user’s prompt into its own output, so the transcript would otherwise hold no record of it. Gemini’s CLI echoes the prompt itself (rendered since #3620) — a second record would show the message twice on replay. ACP and the Codex App Server run through other controllers.
- user_
record 🔒 - The transcript record for a user message: the same shape the persistent Claude controller writes, which every replay translator reads.