Skip to content

fix(cli): agent console sometimes ignores Ctrl-C, leaks /dev/tty handles and keeps burning CPU #295

Description

@codeaholicguy

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

  • A repeatable reproduction exists: a pseudo-terminal harness running the console N times with Ctrl-C at randomised times. Document the hit rate before the fix.
  • The root cause is identified, i.e. what opens /dev/tty repeatedly or keeps the loop alive, and fixed.
  • After Ctrl-C, the console exits within 2 s in 50/50 harness runs, with no leftover /dev/tty descriptors.
  • SIGTERM and SIGHUP also terminate the console within 2 s, with the terminal state restored.
  • Defensive guard: after the Ink app has unmounted, the command makes sure the process exits, e.g. process.exit once cleanup is done, or an unref'd watchdog. It shouldn't depend on the event loop draining on its own.
  • A regression test or e2e smoke test covers exit on Ctrl-C (see e2e: add agent console pseudo-terminal smoke coverage #220, console pseudo-terminal smoke coverage).

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions