Problem
ai-devkit agent console sometimes doesn't exit on Ctrl-C after running for a while. The stuck process then:
- keeps running forever at ~3.5–4% CPU;
- ignores
SIGTERM (only SIGKILL stops it);
- stays stuck even after its terminal is closed.
This happens on both 0.65.0 (a78f668) and current main (190b562), so it predates the recent performance work.
Evidence
- Seen 2 times in about 8 automated Ctrl-C tests. The console ran under a pseudo-terminal for ~90 s, then got
\x03. It couldn't be reproduced on demand.
- In both cases the test harness blocked forever waiting for the process to exit.
- State of the stuck process (0.65.0 build, via
lsof -p and sample):
- 138 open
/dev/tty file descriptors and 138 Unix sockets;
- no
ps / lsof children being spawned any more, so the React tree and its polling had stopped;
sample shows time mostly in uv__stream_osx_select (libuv's per-TTY select thread on macOS), plus stream writes;
- CPU time grew ~15 s over ~7 min.
- A healthy console that has run for 44+ minutes has 0
/dev/tty descriptors, so the handles appear to pile up at or around shutdown, not in normal running.
- No first-party code opens
/dev/tty; the only match is a comment in TerminalFocusManager.ts. So the source is probably in Ink's input/exit handling, the CLI's signal or raw-mode handling, or something reopening the TTY in a loop during teardown.
- When the terminal closes normally, without Ctrl-C, both builds exit cleanly.
Likely mechanism (to confirm)
On exit, something repeatedly opens the TTY, for example a raw-mode restore or stdin reset that reopens /dev/tty on every attempt, or a retry loop. The leaked handles keep libuv's event loop alive after waitUntilExit() resolves. So the process never exits, and it spins on the per-handle select threads.
Acceptance criteria
Problem
ai-devkit agent consolesometimes doesn't exit on Ctrl-C after running for a while. The stuck process then:SIGTERM(onlySIGKILLstops it);This happens on both
0.65.0(a78f668) and currentmain(190b562), so it predates the recent performance work.Evidence
\x03. It couldn't be reproduced on demand.lsof -pandsample):/dev/ttyfile descriptors and 138 Unix sockets;ps/lsofchildren being spawned any more, so the React tree and its polling had stopped;sampleshows time mostly inuv__stream_osx_select(libuv's per-TTY select thread on macOS), plus stream writes;/dev/ttydescriptors, so the handles appear to pile up at or around shutdown, not in normal running./dev/tty; the only match is a comment inTerminalFocusManager.ts. So the source is probably in Ink's input/exit handling, the CLI's signal or raw-mode handling, or something reopening the TTY in a loop during teardown.Likely mechanism (to confirm)
On exit, something repeatedly opens the TTY, for example a raw-mode restore or stdin reset that reopens
/dev/ttyon every attempt, or a retry loop. The leaked handles keep libuv's event loop alive afterwaitUntilExit()resolves. So the process never exits, and it spins on the per-handle select threads.Acceptance criteria
/dev/ttyrepeatedly or keeps the loop alive, and fixed./dev/ttydescriptors.SIGTERMandSIGHUPalso terminate the console within 2 s, with the terminal state restored.process.exitonce cleanup is done, or an unref'd watchdog. It shouldn't depend on the event loop draining on its own.