diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index a5654ecfd..fe3386ad7 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -41,6 +41,7 @@ ## [17.0.3] - 2026-07-17 +- Fixed orphaned detached MCP stdio server process trees surviving session dispose by escalating stdin-EOF → group SIGTERM → group SIGKILL on close() (#5578) ### Changed - `omp usage` and the in-session `/usage` view now show the Anthropic organization next to the account for org-scoped credentials (with `--redact` masking applied per part in the CLI, falling back to the org id when no display name is available), attribute "no usage data" rows per organization, and match the "in use by this session" marker by organization so only the active subscription is flagged. The OAuth login success message names the account and organization that was stored — a login landing on an unintended subscription is visible immediately. diff --git a/packages/coding-agent/src/mcp/transports/stdio.test.ts b/packages/coding-agent/src/mcp/transports/stdio.test.ts index 7d91685f4..9c2d60f86 100644 --- a/packages/coding-agent/src/mcp/transports/stdio.test.ts +++ b/packages/coding-agent/src/mcp/transports/stdio.test.ts @@ -153,13 +153,21 @@ describe.skipIf(process.platform === "win32")("StdioTransport request write stal }, 8000); }); +// `kill(pid, 0)` succeeds for a zombie too: a grandchild whose parent (the +// killed leader) is gone sits as until whatever reaps orphans +// (init/subreaper) gets around to it — which can lag on some hosts. A +// zombie already received and honored the group SIGKILL; it is just not +// harvested yet, so treating it as "still alive" would make the group-kill +// assertions below flaky rather than testing what they claim to test. function processExists(pid: number): boolean { try { process.kill(pid, 0); - return true; } catch { return false; } + const result = Bun.spawnSync(["ps", "-o", "stat=", "-p", String(pid)]); + const state = result.stdout.toString().trim(); + return result.exitCode === 0 && state.length > 0 && !state.startsWith("Z"); } // Regression for #5578: `close()` used a bare `this.#process.kill()` (direct