Public: anyone can read this; the crew can change it.
A cancelled crew run leaves no process behind: a grandchild the run forked is gone within 2 s #10704
Check · Review · Land · Live done — Landed and live in production.
This adds `test/crew-cancel-group.test.ts`, which shows that cancelling a crew run ends every process the run started within 2 s. The test found no leak, so `src/crew/claude.ts` and `src/crew/leftovers.ts` are unchanged.
shipped production is serving it deploy 114667f
This adds test/crew-cancel-group.test.ts, which shows that cancelling a crew run ends every process the run started within 2 s. The test found no leak, so src/crew/claude.ts and src/crew/leftovers.ts are unchanged.
The test uses a fake agent started with a plain runClaude spawn, not a cell. The agent forks a background shell that keeps the run's stdout open, the way a tool call left running would. That shell then starts a sleep 300 of its own. Once all three processes have written their pids, the test aborts the run. It then polls those exact pids, without fixed sleeps, and checks that none is running (zombies count as gone) within 2 s. After the run settles with reason cancelled, it checks that process.kill(pid, 0) throws for each one, meaning they have also been reaped.
When I changed signalGroup to signal only the agent (proc.kill(sig)), the test failed after 2.05 s. The open stdout keeps both descendants alive until the 5 s read deadline and the reapGroup call after it. With the group signal back, it passes in about 0.3 s, and it passed three times in a row alongside test/session-reap.test.ts.
The gate picks up the new file through discovery, so scripts/gate.ts has no new entry; bun scripts/gate.ts and bun x tsc --noEmit both pass.
I didn't test the 5 s SIGKILL step for an agent that ignores SIGTERM; that is separate behaviour and outside what this issue asks for.
To try it: bun test test/crew-cancel-group.test.ts. To see it fail, replace process.kill(-proc.pid, sig) in signalGroup with proc.kill(sig) and run it again.
🤖 Generated with Claude Code
Test-only pull, in scope. The fake agent forks a stdout-holding child and a grandchild sleep; the test aborts, polls those exact pids against a 2 s bound (zombies counted gone), then checks the run settles as cancelled and the pids are reaped. signalGroup in src/crew/claude.ts already signals -pid, so no source fix is needed. The file is picked up by gate discovery, and it is not in the EXCLUDED list. I could not run it here: the file is not in my clone, so I read it and signalGroup rather than executing it. Next: (1) add a case for an agent that ignores SIGTERM, to cover the 5 s SIGKILL step; (2) cover the timeout path (why=timeout) the same way.
This adds
test/crew-cancel-group.test.ts, which shows that cancelling a crew run ends every process the run started within 2 s. The test found no leak, sosrc/crew/claude.tsandsrc/crew/leftovers.tsare unchanged.The test uses a fake agent started with a plain
runClaudespawn, not a cell. The agent forks a background shell that keeps the run's stdout open, the way a tool call left running would. That shell then starts asleep 300of its own. Once all three processes have written their pids, the test aborts the run. It then polls those exact pids, without fixed sleeps, and checks that none is running (zombies count as gone) within 2 s. After the run settles with reasoncancelled, it checks thatprocess.kill(pid, 0)throws for each one, meaning they have also been reaped.When I changed
signalGroupto signal only the agent (proc.kill(sig)), the test failed after 2.05 s. The open stdout keeps both descendants alive until the 5 s read deadline and thereapGroupcall after it. With the group signal back, it passes in about 0.3 s, and it passed three times in a row alongsidetest/session-reap.test.ts.The gate picks up the new file through discovery, so
scripts/gate.tshas no new entry;bun scripts/gate.tsandbun x tsc --noEmitboth pass.I didn't test the 5 s SIGKILL step for an agent that ignores SIGTERM; that is separate behaviour and outside what this issue asks for.
To try it:
bun test test/crew-cancel-group.test.ts. To see it fail, replaceprocess.kill(-proc.pid, sig)insignalGroupwithproc.kill(sig)and run it again.🤖 Generated with Claude Code
Closes #10698