Repository navigation
fix(server): MCP plugin servers die with the Claude CLI's process group - #19045
akushonkamen wants to merge 1 commit into
Conversation
Closing or deleting a session leaked the CLI's MCP plugin servers (each plugin's bun server.ts): the SDK transport signals only the CLI PID, so the plugin tree survived as orphans logging into a stdout pipe nobody drains, pinning a core per wedged server until kill -9 (pingdotgg#3089). The query runner now hands the SDK a spawner that runs the CLI detached in its own POSIX process group, turns every kill into a group kill, and SIGKILLs whatever is left in the group when the CLI exits for any reason, so the transport's graceful close reaps the whole tree. Caller-supplied spawners (remote runtimes, tests) keep ownership; Windows keeps the SDK's own spawn. This work was done by GLM (a model) in the ZCode agent harness.
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
ApprovabilityVerdict: Not approved Macroscope's review found this PR not approvable — This focused fix changes the default Claude CLI process lifecycle on POSIX systems by introducing process-group ownership and cleanup, with real-process tests covering the behavior. Human review is required because the PR adds static-analysis diagnostic suppression directives in both production and test code. You can add or adjust custom eligibility rules. Learn more. |
|
Note 🤖 Claude Opus 5.5 responding on behalf of Theo Closing because this PR states that GLM wrote code in it:
We do not accept code written by GLM, Kimi, MiniMax, Mistral, Gemini, or alpha models, or by Muse models other than Muse Wonder Full. See Models we do not accept. To have this considered again, open a new PR with the code written by a different model and link this one. |
Problem
Closing or deleting a session leaked the MCP plugin servers the Claude CLI spawns under the hood (each plugin's
bun server.ts): one orphan per session, each logging into a stdout pipe nobody drains, wedging a CPU core untilkill -9(#3089, two machines, reproduces on 0.0.33).Reproduced outside the desktop app by driving
@anthropic-ai/claude-agent-sdkexactly the way the Claude adapter does (a stdio plugin launched throughbun run --cwd … --shell=bun --silent start, Claude CLI 2.1.241, macOS 15 arm64). Afterquery.close()the CLI is gone but the plugin tree survives, still inside the CLI's process group:(The
--plugin-dirpath — the CLI reading the plugin's own.mcp.json, the real telegram-plugin shape — reproduces the identical orphan pair.)Root cause: the SDK's
ProcessTransportspawns the CLI non-detached and on close signals only the CLI PID (SIGTERM, then SIGKILL after 5 s). The CLI's own shutdown does not reap the plugin server tree, so nothing owns those processes anymore. The reported "SIGTERM respawn" did not reproduce on CLI 2.1.241 in either thesetMcpServersor--plugin-dirpath; consistent with @iskyiskyisky's Detail 1, the wedge alone explains it — a server stuck in the dead-pipe write never reaches its SIGTERM handler.Change
layerQueryRunnernow hands the SDK aspawnClaudeCodeProcessimplementation (makeClaudeProcessGroupSpawner, wired withHostProcess.Platform) that:detached, so it leads its own POSIX process group (precondition for the rest; without it a group signal would hit the server's own group);killsignal that group after Node's own PID kill, so the transport's SIGTERM → SIGKILL escalation reaps the CLI together with every plugin server, wedged or not;Caller-supplied
spawnClaudeCodeProcess(remote runtimes, tests) keeps ownership of its process; on Windows the SDK's own spawn is kept.Scope and approval
Focused fix for the triaged bug issue #3089 (
bug,needs-triage). No public behavior changes beyond reclamation of leaked processes.Verification
Focused tests (
ClaudeAdapterV2.process-group.test.ts), real processes, macOS 15 arm64:reaps the whole CLI process group on kill, including plugin-server descendantsreaps the whole CLI process group when the CLI exits by itself(the graceful-close leak shape)reaps a wedged descendant that ignores SIGTERM when the CLI exits(trap "" TERMgrandchild, the [Bug]: MCP plugin processes (bun server.ts) are never killed when closing conversations #3089 wedge)spawns the CLI detached, so it leads its own process grouphands the SDK a process-group spawn for the CLI(wiring throughlayerQueryRunner, mock-spiedquery)Red → green: with the import/wiring missing, the file fails 4/4; with the exit hook removed (kill patch only), the two lifecycle tests fail (2 failed / 3 passed) — that is the E2E-discovered graceful-close gap; with the full change, 5/5 pass. Existing suites still pass:
ClaudeAdapterV2.test.ts252/252,ClaudeAgentSdkQueryRunner.test.ts1/1 (their fake-CLI spawn hooks keep precedence over the new default).tsc-rs --noEmitclean for the touched files;vp lint0 errors (1 pre-existing unused-var warning onmain).End-to-end, the same harness as the reproduction with the adapter's spawner attached: after
query.close(),cliGone=trueand zero matching processes remain (before: the orphan pair above).Could not check: Windows (group semantics are skipped there by design) and the Linux systemd-scope wrap (
agentScopewraps only native-binary launches, and the wrapperexecs into the CLI, so the detached PID is the CLI's; not exercised here).This work was done by GLM (a model) in the ZCode agent harness.