Bound ACP dispose with SIGKILL escalation; skip spawn when pre-aborted (Codex review round 1)
Two lifecycle findings from the review: - A (blocker): dispose() could hang forever. It only sent SIGTERM and awaited exit, with no escalation — a child that traps SIGTERM (or our acp-agent if it doesn't quiesce on stdin EOF) would wedge dispose, stranding tool-subagent's finally cleanup and orphaning child-owned work (e.g. bash subprocesses). dispose now: ends stdin (graceful ACP close so the child can flush + exit), SIGTERM, then escalates to SIGKILL if it doesn't exit within a grace period (DEFAULT_DISPOSE_GRACE_MS, injectable via spec.disposeGraceMs), awaiting the certain exit. Mirrors the bash executor's bounded teardown. Regression test drives a SIGTERM-trapping mock subprocess and asserts dispose returns promptly — proven to hang (red) without the escalation. - B: an already-aborted request still spawned the configured binary. startAcpRun now returns an inert already-aborted run BEFORE spawning, so a pre-cancelled request launches nothing. Test points the command at `touch <sentinel>` and asserts the sentinel never appears. The dispose regression test exposed (via systematic-debugging) that the child must signal trap-armed readiness before the test cancels — a bare timeout raced the trap install and the default SIGTERM handler killed the child, making the guard a no-op. The mock now touches its ready file once the trap is in place and the test waits on that condition. The `cancelled` flag moved onto a holder object so TS control-flow doesn't narrow the catch-time read to always-false.
This commit is contained in:
@@ -148,3 +148,17 @@ new AgentSideConnection(
|
||||
Readable.toWeb(process.stdin) as ReadableStream<Uint8Array>,
|
||||
),
|
||||
)
|
||||
|
||||
// Under MOCK_TRAP_SIGTERM, ignore SIGTERM and keep stdin open so the process
|
||||
// neither quiesces on EOF nor dies on the graceful signal — exercising the
|
||||
// backend dispose path's SIGKILL escalation. Without this the process exits
|
||||
// normally on SIGTERM / stdin end. Touch READY_FILE once the trap is armed, so
|
||||
// a test waits for that CONDITION before disposing (the trap must be in place,
|
||||
// not merely the process spawned — otherwise SIGTERM hits the default handler).
|
||||
if (process.env.MOCK_TRAP_SIGTERM === '1') {
|
||||
process.on('SIGTERM', () => { /* trapped: refuse to exit on the graceful signal */ })
|
||||
// Keep the event loop alive (a bare timer) so nothing else lets it exit.
|
||||
setInterval(() => { /* stay alive until SIGKILL */ }, 1000)
|
||||
if (READY_FILE !== undefined) writeFileSync(READY_FILE, 'trap-armed')
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user