acpmux: lift Claude and Codex subagent limits for cmux's chats; the idle reaper spares background shells

Merged
TE
23d ago
+5235-762

Chats acpmux starts no longer hit Claude Code's or Codex's subagent caps. An idle chat with a dev server still running in the background is no longer reaped after 5 minutes, which used to take the server down with it.

What changes

Limits lifted at spawn. Every session spawn goes through agent::harness_command: direct, hosted and pooled. So a new chat gets these without a restart. A value the user set always wins.

HarnessKnobWasNowUser value that wins
Claude CodeCLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS201000profile env, login env, daemon env
CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION—100000″
CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH316″
CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY1064″
BASH_MAX_TIMEOUT_MS (how long a background command may run unattended)2 h~24.8 days (Claude's own ceiling)″
Codex app-serveragents.max_concurrent_threads_per_session61000-c in the profile argv; or the key in codex's config.toml (profile CODEX_HOME first), in its active profile, or in the project's .codex/config.toml
agents.max_depth116″

Three things stay as they are:

  • BASH_DEFAULT_TIMEOUT_MS (30 min) is unchanged. Raising it would hold every foreground command that long.
  • Codex's limit of 64 unified-exec processes per session is compiled in.
  • Model probes and cli/harness* launches have no session, so they get no lift.

The lifted limits also apply to chats driven from the Web or a remote relay (coordinator, 10-14). Nothing here bounds in-process subagents' API spend; spend accounting lives in subrouter, not acpmux.

Idle reaper.

  • When a shell goes to the background, Claude's translator now sends a background_task_update with state running.
  • The hub tracks running shells per chat, including a subagent's shells, and keeps them across turns. It never reaps a chat with one running.
  • When the hub adopts a running host, it rebuilds the set from the log since the harness last started or exited.
  • The web client ignores the running state.

No resource valve yet. An earlier revision added a per-chat memory and CPU budget that read ps and /proc. cmux-next never scans processes (plans/cmux-next/pty-holder.md §4, check-no-process-scan), so I took it out. The valve comes back without scanning in cx-v9s8g:

  • Linux: a per-session cgroup scope at spawn.
  • macOS: the harness in its own process group, lowered with process-group setpriority.
  • Once cx-t2sqm.3 lands: rusage reported by the holder.

Evidence

Each test went red, then green, with the same focused hosted command (scripts/verify-cmux-tui-hosted.sh --filter <name>):

TestRed on d9a65ff (tests only)Green on 2c45bbc
Claude knobs (a_chat_starts_claude…)38567379348: every knob null38555851737
Codex -c, with profile-argv and project-config values winning (a_chat_starts_codex…)38555281752: no -c overrides38555851737
Idle reaper: the chat's own shell, a subagent's shell, and after a later turn38555282067: no running update38555851478

The exact head after the base merges is 71a4aaa. It runs compile, clippy, the fork_through::background_task:: suite and the Codex test in 38586236195 (fork_through::background_task::) and 38586235320 (a_chat_starts_codex).

Locally on the merged head:

  • check-no-process-scan, the crash ratchet and check-no-godfiles.sh --only rust all pass. agent.rs and hub/mod.rs are held at the base's length.
  • Three review passes found issues that are fixed here: correctness, safety, and a follow-up on the fixes. They covered a subagent's shells, the per-turn reset, codex config precedence, and clippy.

The follow-up design that lists, tails and kills these processes is #20550.

Changelog

  • Changed: Chats started by cmux run Claude Code and Codex with their subagent and background limits lifted, and an idle chat with a running background shell is no longer ended.

🤖 Generated with Claude Code