acpmux: list and stop Claude Code's background shells (process registry)
acpmux now keeps a list of the commands Claude Code runs in the background (dev servers, watchers), one per shell, with how each one ended. A client can list them and stop one. This is the first slice of the design in #20550, for Claude Code only.
What changes
The registry (hub/processes.rs). It keeps one record per background shell on its chat, a subagent's shells included:
- The id is
<sessionId>/<taskId>. If a later harness reuses a task id, the record gets-2,-3and so on. - Each record holds the tool call, harness, command, cwd and start/end times. Its state is
running,exited,failed,killedorlost, with a reason ofperson,agentorharness_exit. - Each change is logged once as a mux
process_updaterecord. A chat's table is read back from its log the first time it's needed, and again when a daemon adopts a running host. - A harness that exits or is replaced takes its shells with it, so any still running are marked
lost.
It replaces the interim StreamState.background_running from #20585. The idle reaper now asks the registry whether anything is running.
Methods. Both are advertised in initialize.
_acpmux/process/list {sessionId?}_acpmux/process/kill {processId, signal?}- The notification
_acpmux/process_update {process}goes to attached and watching local connections.
Like the secret methods, these are routed before everything else. They are never forwarded to a peer, and Web and Peer connections are refused. That matches _acpmux/kill: the local socket is the same-user trust boundary, so there's no person.rs change (coordinator, 10-14). Follow-up bead cx-r6tvw covers a person-only list across all chats.
Kill.
- A kill sends Claude Code's
stop_taskcontrol request through an internal_acpmux/claude/stop_task. It never starts a harness: with none alive, the process is just markedlost. - An ended process is left alone. An unknown or malformed id is refused, and so is a chat that isn't Claude Code.
Translator.
- A
task_updatedthat says stopped or killed now ends the shell, because Claude Code may send nothing afterstop_task. A stop reported twice ends the shell once. completedandfailedstill wait for the notification, so the record keeps Claude's summary.- A shell left running when Claude exits gets
_meta.acpmux.endedBy: "harness_exit"next to its unchanged update. The web client keeps showing Claude's own words.
docs/acpmux-schema.json describes the methods, the notification and the record.
Not in this PR (all in the design, #20550):
- tailing a process's output
- Codex
- mapping the budget valve onto processes (cx-v9s8g)
- a per-task stop button in the UI
After a daemon restart, a shell whose host is still alive stays running until its harness reports how it ended.
Evidence
Same focused hosted command, red then green (scripts/verify-cmux-tui-hosted.sh --filter <filter>):
- Red on the test-only commit
359a2d6, filterfork_through::processes::(12 tests): 38610629535 - Green on
547a328, same filter: 38610733111 - #20585's idle-reaper suite,
fork_through::background_task::, now on the registry: 38610781255
The tests cover:
- listing a running shell, a subagent's shell, and each way a shell ends
- a kill that sends
stop_taskand ends the shell as killed by the person - a stop reported twice, and a finished shell with its summary
- a harness exit marking running shells
lost - refusals: an ended process, an unknown id, Web/Peer connections, a non-Claude chat
Locally, check-no-process-scan, the crash ratchet and check-no-godfiles.sh --only rust pass. hub/mod.rs and server/mod.rs hold their line counts. Review passes before the rebase found these issues, all fixed here:
- a first-load race
- a
completedupdate dropping the summary - duplicate ids
- a leaked stop mark
- a hard-coded harness name
- the
harness_exitreason showing as Claude's words
Changelog
- Added: acpmux lists Claude Code's background shells per chat and can stop one (
_acpmux/process/list,_acpmux/process/kill).
🤖 Generated with Claude Code
Need help on this PR? Tag @codesmith-bot with what you need.
Summary by cubic
Adds a background-process registry to acpmux so clients can list and stop the commands Claude Code runs in the background (dev servers, watchers), one record per background shell per chat.
hub/processes.rskeeps one record per background shell with its tool call, harness, command, cwd, start/end times, and an end state (running,exited,failed,killed,lost) with a reason (person,agent,harness_exit).- New methods
_acpmux/process/listand_acpmux/process/kill, advertised ininitialize;_acpmux/process_updatenotifies attached and watching local connections. - A kill sends Claude Code's
stop_taskvia the internal_acpmux/claude/stop_taskand never starts a harness; an ended process is left alone, and an unknown or malformed id is refused. - A
task_updatedthat says stopped or killed ends the shell, so a stop reported twice ends once;completedandfailedstill wait for the notification and keep Claude's summary. Thefeat-cmux-nextmerge left onestop-<id>branch, so astop_taskis answered once. - Shells still running when Claude exits become
lostwithendedBy: "harness_exit". - Replaces the interim
StreamState.background_running; the idle reaper now asks the registry whether anything is running. - Web and Peer connections are refused; only this machine's clients (unix socket, local app) get the methods.
Migration
- The new methods are only advertised after a daemon restart; older clients see no change.