acpmux: an idle harness ends when its MCP server's path has a space in it

Closed
TE
7d ago
+440-56

Idle hibernation (#26127) never ended a harness: every chat's idle pass found its harness "busy".

Cause. The idle census matched each process under a harness against its MCP servers, using the line ps prints (argv joined by spaces), and it refused any server whose command or args held a space. acpmux's own MCP servers (forum, offer, secret and so on) run the acpmux binary from the app bundle, /Applications/cmux NIGHTLY.app/Contents/Resources/bin/acpmux. So in the nightly no chat ever hibernated. The fleet bench (bench=hibernate, builds under /Library/Application Support/...) showed it:

  • every agent process was still running after 135 s idle;
  • with #26928's log lines, every chat logged idle harness stays up: a process that is not one of its MCP servers runs under it ... comm=Application.

Fix (hub/idle_census.rs, agent_host/tree.rs):

  • Exact argv. Each candidate's argv is read word by word: KERN_PROCARGS2 on macOS, /proc/<pid>/cmdline on Linux. It is compared with the server's argv exactly, so a path with a space in it matches, and the same words split differently never do.
  • Executable. Argv is the process's own to rewrite (exec -a, process.title), so the executable the kernel says it runs must match too: proc_pidpath on macOS, /proc/<pid>/exe on Linux.
    • A direct server's executable must be its program.
    • A script server's executable must be the interpreter its #! line runs (env NAME resolved on the harness's PATH), never a file merely named node.
  • Fails closed. A child whose executable or argv can't be read is not a server.
  • Bounded. The listing, the reads and the match all run in the census's bounded blocking task.

Tests (Linux, 3 runs, 12/12 each; clippy -D warnings clean):

  • spaced paths, including the nightly bundle path;
  • argv split differently;
  • exec -a with a live child;
  • process.title padding;
  • a non-interpreter executable, and a fake node;
  • shebang parsing (env, env -S, direct, none).

After landing, bench=hibernate on a fleet Mac should show the chats' agent processes gone after the 2-minute idle.

Leo's words for the stack: "also when it comes to hibernation...can't you hibernate the agent but not hibernate the bg tasks?"

Risk: a false match would let the idle pass end a harness's process group with a non-server under it. Exact argv plus the kernel's executable, failing closed, is what guards it. The reviews below went four rounds on exactly this.

No capture: no UI.

Review (codex gpt-6-astra high, fast): SHIP at 5357a148a5824878a925e0e9c99f4cdd24ebf894. Rounds 1-3 BLOCKed, all fixed:

Review (codex gpt-6-astra high, fast): SHIP at 01bab617b94d64574d98c772bf522ea6f3adad4c. This is a delta review of a base merge, whose conflict with base's panic-ratchet cleanup in runs() was resolved with get() in place of indexing. It also covers #[cfg(unix)] on runs(), added for the Windows GNU build, which a Linux cross-check confirms. 12/12 census tests pass, clippy is clean, and the crash ratchet adds no acpmux hits.

Review (codex gpt-6-astra high, fast): SHIP at 07ebf4d91c99fdc5e272fb1bf41fc9b79e2e817d. This is a delta for the fast-gates fixes:

  • a test path no longer names a literal state root;
  • scripts/cmux-next/process-scan-baseline.tsv's row for agent_host/tree.rs goes from 1 to 5, still class remove with the census. The census now reads each child's argv (KERN_PROCARGS2, /proc/<pid>/cmdline) and executable (proc_pidpath, /proc/<pid>/exe) next to the existing ps listing.
  1. Whitespace-normalized argv could match a different split.
  2. Argv alone could be spoofed, so the executable is now checked.
  3. Any file named like an interpreter passed, so the script's #! interpreter is now required.

Agent session: agent session

šŸ¤– Generated with Claude Code