cmux mcp serve: start other MCP servers (cmux-cua) on first call, not to list them
Leo: "Yes, fix it" (the goal he approved: Nightly starts agent hosts only when a session is opened or has queued work, and computer-use/REPL helpers only on first use). Coordinator, with measurements from Leo's Mac: "Fix the helpers so they're started on first use and reaped when idle or when their session stops: node_repl per Codex session, the agent-memory MCP (could one shared server per host work?), and cmux-cua."
Root cause (Leo's Mac, 2026-10-29)
- Load 113 to 369, swap 14.8 of 15.4 GB, 116 live agent hosts, 85
codex app-server, 75cmux mcp serveeach with its owncmux-cua mcpchild. - The burst: at 19:31Z one agent message, sent from another machine through the peer link, reached about 64 idle sessions on Leo's Mac within two minutes. Each woke its harness for the turn: the daemon's live count went 58 to 122 (
session/new admitted past the lane cap ... 122 live sessions). So it was queued work, not restore-on-launch or a respawn loop: every running host had a host record, only 2 of 116 had none. - Why they never went away: the idle pass refused to end almost every harness (
idle harness stays up: a process that is not one of its MCP servers runs under it, 1,600+ times that day). The servers' paths have spaces (/Applications/cmux NIGHTLY.app/...), which #27925 fixes (it supersedes #27558), and npm's Codex launcher, which #27681 fixed. So every harness ever woken stayed up with all its MCP servers. - What each harness carries:
cmux mcp servestartedcmux-cuathe moment the harness listed its tools (same second, every chat), whether or not the chat ever used computer use. Codex starts its full MCP set, node_repl among them, once per thread.
This PR
cmux mcp serve stores the list each upstream (cmux-cua, secrets, offers) gives at its start in ~/.cache/cmux/mcp-upstreams, and answers tools/list from it without starting the server. The first tools/call starts the server, which lists again and stores again. A chat that never uses computer use never runs a cmux-cua.
- Key: command, args, env names (never values: tokens, session ids), the real path, size and mtime of the executable the server's own
PATHresolves the command to (an app update is a new key) and of every arg that names a file (python3 server.py: editing the script is a new key), cmux's version. - Folder
0700, files0600, each entry written whole (temp file, then rename). A missing, torn or unreadable entry means the server starts to be listed, exactly as before. - Reaping:
cmux-cuaruns undercmux mcp serve, which runs under the harness, so it ends with the harness when the idle pass hibernates it (#27925, #27681) or the session stops. No new timer.
The other pieces of the same fix land on their own PRs:
- #27925 (supersedes #27558): idle census matches MCP servers whose path has a space, by exact argv and executable, so nightly harnesses hibernate and take node_repl, agent-memory and cmux-cua with them.
- #27681 (merged): idle Codex behind npm's launcher hibernates.
- teamleaderleo/leo-workspace#1543 (merged): one shared agent-memory backend per host; each session's
agent-memory mcpis a thin front (26.4 to 16.9 MB on macOS). - #27594: a daemon whose home is gone stops (the two leaked test daemons), and
ACPMUX_SOCKETonly pairs withACPMUX_HOME. - #27731: CLI resumes and new sessions from an interactive Mac are placed on a fleet host;
session stop --idworks without--mesh.
Tests
a_stored_list_answers_without_starting_the_server_until_a_call(upstream_tests.rs): with a stored list,tools/liststarts nothing; the first call starts the server and runs.a_stored_tool_the_server_dropped_is_not_sent: a call to a tool the restarted server no longer lists is refused, not forwarded.upstream_listing::tests: a list is shared across chats whose env values differ, dropped when the binary or a script arg (absolute or bare relative) changes; the key follows the server's ownPATHand skips files nobody may run; a torn entry reads as none; a0644entry or a symlink is never read; the folder is0700.
Performance
| metric | before | after | delta | harness or run link |
|---|---|---|---|---|
| upstream servers running, 20 chats that list tools and never call one | 20 | 0 | -20 | bench below, run 41043089021 |
RSS of the 20 cmux mcp serve trees (debug build, stand-in upstream) | 1,411.7 MB | 1,166.7 MB | -245.0 MB (-17%) | same run |
| upstream RSS alone | 216.1 MB | 0 MB | -216.1 MB | same run |
| time for 20 chats to get their tool list | 0.06 s | 0.04 s | -0.02 s | same run |
cmux-cua on the Mac in the census, 75 cmux mcp serve | 75 | 0 until a computer-use call | -75 | ps census on the beachballed Mac, 2026-10-29 (coordinator's later census: 93 cmux-cua) |
The bench (fleet Linux CI, 32 vCPU) starts 20 cmux mcp serve processes at once, each with one upstream, and sends initialize then tools/list as a harness does at thread start, then counts the upstream processes under them and sums VmRSS two seconds later. "Before" is the same binary with no stored list possible (~/.cache is a file), which is base's code path: every list starts its upstream. "After" first lets one chat list once (that chat starts its upstream and stores the list), then starts the 20. The upstream is a small Python MCP server, so its 10.8 MB per process is a floor: the real cmux-cua is a Mac binary and could not run there. The upstream count is the platform-independent number; the Mac census row is the measured one for cmux-cua.
No UI change, no capture.
Agent session: agent session
š¤ Generated with Claude Code