acpmux: list and stop Claude Code's background shells (process registry)

Merged
TE
23d ago
+938-89

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, -3 and so on.
  • Each record holds the tool call, harness, command, cwd and start/end times. Its state is running, exited, failed, killed or lost, with a reason of person, agent or harness_exit.
  • Each change is logged once as a mux process_update record. 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_task control request through an internal _acpmux/claude/stop_task. It never starts a harness: with none alive, the process is just marked lost.
  • 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_updated that says stopped or killed now ends the shell, because Claude Code may send nothing after stop_task. A stop reported twice ends the shell once.
  • completed and failed still 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, filter fork_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_task and 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 completed update dropping the summary
  • duplicate ids
  • a leaked stop mark
  • a hard-coded harness name
  • the harness_exit reason 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


View with [code]smith Autofix with [code]smith
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.rs keeps 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/list and _acpmux/process/kill, advertised in initialize; _acpmux/process_update notifies attached and watching local connections.
  • A kill sends Claude Code's stop_task via the internal _acpmux/claude/stop_task and never starts a harness; an ended process is left alone, and an unknown or malformed id is refused.
  • A task_updated that says stopped or killed ends the shell, so a stop reported twice ends once; completed and failed still wait for the notification and keep Claude's summary. The feat-cmux-next merge left one stop-<id> branch, so a stop_task is answered once.
  • Shells still running when Claude exits become lost with endedBy: "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.