fix(client): reap the bridge process tree on close#432
Conversation
|
Codex review: needs real behavior proof before merge. Reviewed July 4, 2026, 11:10 PM ET / 03:10 UTC. Summary Reproducibility: yes. at source level: current main only calls Review metrics: 2 noteworthy metrics.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Land generic process-group cleanup only after preserving or explicitly replacing parent-death/process-group signal behavior, with live adapter proof that descendants are reaped without introducing a headless/signal regression. Do we have a high-confidence way to reproduce the issue? Yes, at source level: current main only calls Is this the best way to solve the issue? No, not as submitted. Process-group killing is a plausible narrow cooperative-close fix, but enabling detached for every POSIX bridge before parent-death and process-group signal semantics are handled is not the safest merge path. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against 6a24a546d234. Label changesLabel justifications:
Evidence reviewedWhat I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
killAgentIfRunning() signalled only the bridge PID, so processes the bridge itself spawned (the model process, MCP servers such as serena) were never signalled and survived as orphans, accumulating across runs until the agent app-server choked and new sessions hung at session/new. On POSIX, spawn the bridge in its own process group (detached) and signal the whole group on close, gated on an agentGroupLeader flag so acpx never signals its own group. A final best-effort group sweep also runs after the bridge PID has gone (e.g. a well-behaved bridge that exits on stdin-close), so descendants that outlived the bridge are still reaped. Signal failures other than ESRCH are traced (verbose) instead of silently swallowed. Windows and any non-detached path keep the previous single-PID child.kill. Scope: reaps descendants that stay in the bridge's process group. A descendant that re-detaches into its own session (setsid) is out of group scope, as is abrupt death of acpx itself (no PR_SET_PDEATHSIG) -- both left to a follow-up. POSIX-only; Windows behaviour is unchanged. Tests (real-process, Unix): a mock agent forks a long-lived grandchild; after AcpClient.close() the grandchild is dead -- both when the bridge is stubborn (ignores SIGTERM, forcing the group SIGKILL) and when it exits cleanly on stdin-close (--exit-on-stdin-end, exercising the post-exit group sweep). Co-Authored-By: Codex <codex@users.noreply.github.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DeuCHKuxEBNVTxHm9bSmwL
b44b19a to
e13ace6
Compare
|
Updated to address the ClawSweeper review:
@clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. |
Problem
When acpx terminates an agent bridge,
killAgentIfRunning(src/acp/client.ts) calledchild.kill(signal)— signalling only the bridge PID. The bridge (codex-acp etc.) spawns its own children: the model process and MCP servers (e.g.serena). Those descendants were never signalled, so they survived as orphans and accumulated until the agent app-server choked and new sessions hung atsession/new.Fix
On POSIX, spawn the bridge in its own process group (
detached,pgid == pid) and signal the whole group on close (process.kill(-pid, signal)), gated on anagentGroupLeaderflag so acpx never signals its own group. Windows and any non-detached path keep the previous single-PIDchild.kill.A final best-effort group sweep also runs after the bridge PID itself has gone — e.g. a well-behaved bridge that exits during the stdin-close grace — so descendants that outlived the bridge are still reaped. Signal failures other than
ESRCHare traced (verbose) rather than silently swallowed.Scope & limitations (explicit)
setsid) leaves the group and is out of scope.agentGroupLeaderis false and behaviour is unchanged (no group reaping). A Job-Object path can follow.AcpClient.close(). It does not cover abrupt death of acpx itself (SIGKILL/OOM) — there is noPR_SET_PDEATHSIGhere. Note that detaching also means the bridge no longer receives signals sent to acpx's process group; both are left to a follow-up that adds a real die-with-parent mechanism.detachedputs the bridge in a new session without a controlling terminal; tools that need/dev/ttydirectly would be affected. acpx bridges are headless ACP adapters, so this is expected.Real behavior proof
Deterministic regression (
--exit-on-stdin-endmakes the mock bridge exit on stdin EOF, so only the post-exit sweep can reap the grandchild). Same test, with and without the fix:Full suite:
850 tests, 847 pass, 0 fail, 3 skipped.typecheck+lintclean.Tests
test/client.test.ts(+test/mock-agent.ts), real-process, Unix:--ignore-sigterm --stay-alive-after-stdin-end, forcing the groupSIGKILL);--exit-on-stdin-end, exercising the post-exit group sweep — the case ClawSweeper flagged).Addresses the ClawSweeper review of the previous revision (the stdin-close descendant leak). Maintainer edits enabled (personal fork).