fix: stop swallowing interrupts forever when an agent won't exit - #249
Merged
Conversation
runAgent registered no-op SIGINT/SIGTERM handlers for the whole lifetime of the child so the terminal's interrupt would reach the agent without tearing the hub down first. The handlers were never escalated, so if the child stopped responding the hub could not be interrupted at all: the promise never settled and no number of ctrl+c presses could reach it. The user's only way out was closing the terminal window. Windows is where this bites. A hub-launched agent runs through four nested cmd.exe batch shims (our .cmd shim, npm's codevhub.cmd, the shell:true wrapper, npm's codev.cmd), and a batch host that catches a console break stops at "Terminate batch job (Y/N)?" rather than exiting. Swallow the first interrupt as before, then stop: a second one abandons the wait, tells the user the agent may still be shutting down, and resolves 130. The child is deliberately not killed — on Windows that would TerminateProcess the cmd.exe wrapper and orphan the real agent still attached to the console. The escalation does not fire during a normal session: interactive agents hold the terminal in raw mode, where ctrl+c arrives as input rather than as a signal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
quickbeard
force-pushed
the
launcher-interrupt-escalation
branch
from
August 6, 2026 04:07
dbb9d84 to
e4db661
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What users report
CoDev Code can't be quit on Windows: double ctrl+c leaves the terminal hung and frozen.
From the user's screenshot: the launch banner (
Starting CoDev Code...), then the TUI's exit epilogue (Continue codev -s ses_…) — and no shell prompt after it. The agent had already run its shutdown path and printed its parting line; what never came back was the terminal.What this PR fixes
runAgentregistered no-op SIGINT/SIGTERM handlers for the entire lifetime of the child:The intent is right — the terminal already delivers the interrupt to the child, so the hub shouldn't tear itself down first and yank the console out from under an agent that is still cleaning up. But it never escalates. If the child stops responding, the hub cannot be interrupted at all: the promise never settles, and no number of ctrl+c presses reaches it. The only way out is closing the terminal window, which is exactly what "hanging and frozen" describes.
This is demonstrable, not theoretical — the two new tests hang for the full 30s timeout against
mainand pass with the change.Windows is where a wedged child is most likely. A hub-launched agent runs through four nested
cmd.exebatch shims:A batch host that catches a console break stops at
Terminate batch job (Y/N)?and waits for input rather than exiting — and with the agent's alternate screen just torn down, that prompt is easy to miss entirely.The change
Swallow the first interrupt as before; a second one stops waiting, tells the user the agent may still be shutting down, logs it, and resolves
130.The child is deliberately not killed. On Windows
child.kill()isTerminateProcess, which would take out thecmd.exewrapper and orphan the real agent still attached to the console — strictly worse than letting it finish. Giving the shell back is the goal.The escalation does not fire during a normal session: interactive agents hold the terminal in raw mode, where ctrl+c is delivered as input rather than as a signal, so the counter only moves once the terminal is generating real interrupts.
Tests
Three new tests in
tests/lib/run.test.ts, driven throughprocess.emitagainst a fake child that never exits, so they behave identically on Windows:130and prints the noticeFull suite green via the pre-commit hook:
biome check,tsc --noEmit, 1387 passed / 2 skipped,pnpm build.What this does not do
The root cause of the freeze is not confirmed. I could not reproduce it — the same chain on macOS unwinds cleanly in 0.74s (
node codevhub→codev, both gone), so whatever wedges is Windows-only, and I have no Windows host to bisect on.What this PR guarantees is that the failure is survivable: the user gets their terminal back instead of having to close the window. It does not explain why the child stops exiting in the first place.
To close that out, a Windows reporter can run
codev, quit with double ctrl+c, and while it is hung check which processes remain:If
cmd.exelayers survive aftercodev.exeis gone, it is the batch-shim break prompt and the fix belongs in the shim chain (acall-based shim, or resolving past npm's.cmdsoshell: trueisn't needed). Ifcodev.exeitself is still alive, the hang is in the agent's own shutdown and belongs in codev-code.Two related observations in codev-code, neither changed here:
cli/cmd/tui.tsbounds the worker shutdown RPC at 5s beforeprocess.exit(0), so a slow shutdown holds the console for up to five seconds after the epilogue is already on screen — the exact window in which a user concludes it is hung and starts mashing ctrl+c.Tui.runcallswin32FlushInputBuffer()before that shutdown and beforewin32InstallCtrlCGuard's restore, so ctrl+c presses during those seconds stay buffered and land on the shell afterwards.🤖 Generated with Claude Code