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

Merged
TE
7d ago
+948-291

Carries #27558 forward. Its owner is gone, it conflicted with #27681, and the review at 07ebf4d9 BLOCKed it. This PR merges its commits as they were, with their authorship, onto feat-cmux-next, resolves the conflict and fixes the BLOCK.

Idle hibernation (#26127) never ended a harness: every chat's idle pass found its harness "busy". acpmux's own MCP servers run from /Applications/cmux NIGHTLY.app/.... The census split ps's space-joined line into words, so a path with a space never matched a server. #27558's description has the full cause and fix. In short: argv is read word by word (KERN_PROCARGS2, /proc/<pid>/cmdline), and the kernel's executable (proc_pidpath, /proc/<pid>/exe) must match the server's program, or for a script the interpreter its #! line runs. A process that can't be read fails closed.

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

What this PR adds on top of #27558

  • Merge with #27681 (npm's Codex launcher relay), and later a base merge past 4154db06. Base's launcher relay (the relayed native program and its codex-* helpers) now goes by the same identity as servers do.
  • The BLOCK at 07ebf4d9: a relative server path. ./mcp-server (or a relative #! script) was canonicalized against acpmux's own cwd, so it never matched and its harness never hibernated.
    • It now resolves in the folder the harness was started in (LaunchServers::Known keeps it).
    • Only absolute paths are canonicalized.
    • A relative script must also be that file from the folder the process itself runs in. Identity carries the process's cwd: /proc/<pid>/cwd on Linux, proc_pidinfo(PROC_PIDVNODEPATHINFO) on macOS. So cd /tmp && node ./server.js is another script.
  • Review rounds on this PR (gpt-6.1-sol high, fast), each fixed with a red test on the code before it:
    • e53952b2: the relay and helper rules still read the ps line, so exec -a could pass. They now need exact argv plus the kernel's executable (runs_file).
    • 37d01e14: the launcher was read from the leader's argv alone. The leader's executable must now be the interpreter its argv names.
    • 07eb7d46:
      • A relative script wasn't tied to the process's folder. It is now (above).
      • A bare node launcher matched any executable named node. It must now be the node the harness's PATH runs; with no PATH known (a harness another daemon started), a bare name is no launcher.
      • Not changed, and the reviewer answered HELPER: keep: a codex-* helper must be the very executable in the relayed program's own install folder. The census guards against work the agent ran, not against files written into the harness's install, which could equally replace the harness.
    • ac3d4977: relays accepted any program with the launcher's name and args, so /tmp/codex -c ... app-server became the harness. The relayed program's real file must now be under the outermost node_modules folder that holds the launcher script's real file. npm puts the native package there, whether nested or hoisted.
  • The census tests move to hub/idle_census_tests.rs, since idle_census.rs was at its 1000-line cap. The launcher tests now build a real npm tree, in a folder with a space.
  • scripts/cmux-next/process-scan-baseline.tsv: the agent_host/tree.rs row goes from 5 to 7 for the two folder reads. It stays class remove, with the census.

Tests (CI, Linux, via cmux-ci test: run 41029177009 at ac3d4977 and run 41038865323 at 62f41ee8):

  • Red on the code before each fix, by the same test names:
    • a_relative_server_is_the_one_in_the_harnesss_folder (at b18c55a7): left: false, right: true.
    • A spoofed relay (at e53952b2) passed as the harness.
    • A node off PATH, and a relative script from another folder (at 07eb7d46), both passed.
    • At 62f41ee8 with the npm-install check removed: both launcher tests fail on the stray codex cases.
  • Green at 62f41ee8: all 16 census tests (idle_census, census_tests), including:
    • a_relay_or_helper_is_its_executable_not_its_argv;
    • cwd_is_the_folder_a_live_process_runs_in (a live child, in a folder with a space).
  • cargo clippy -p acpmux --all-targets -- -D warnings is clean. The panic ratchet's deny lints have no hits in either changed file.

Observed / Not run

  • Observed: the runs above; prepush-gate.py reports no new violations.
  • Not run:
    • A macOS compile of the cfg(target_os = "macos") reads. A cross cargo check stops at ring's build script, so this PR's macOS CI is the check.
    • bench=hibernate on a fleet Mac.
  • Reds on this PR that are base reds:
    • cmux-next checks: "Sidebar look owner" names 3 base commits; prepush-gate.py --only sidebar fails the same way on base and finds 0 in this PR's commits. "String tables" is also red there, and this PR touches no strings.
    • release-notary.

Takes back #27681: its tree::leader_args_in (the leader's ps line) and the (String, Vec<Under>) return of tree::under. The census reads the leader's exact argv and executable through tree::identity instead, because a ps line can be spoofed and splits a path with a space in it. #27681's launcher relay itself stays, now checked by identity.

Review (codex gpt-6.1-sol high, fast): SHIP at 62f41ee8ebeebbd5e91c27fa73665a691f223771. It reviewed the whole diff from the merge base e8630779. The four rounds before it BLOCKed, and each is fixed above.

Risk: a false match would let the idle pass end a harness's process group with a non-server under it. Exact argv, the kernel's executable and failing closed guard against that. A relative path resolves only in the harness's own folder.

No capture: no UI.

Changelog

Fixed: idle cmux NEXT chats hibernate their agent again when an MCP server's path has a space in it or is relative.

Agent session: agent session

šŸ¤– Generated with Claude Code