Skip to content

Own process termination on detached throws and OS signals - #406

Merged
TheGreatAxios merged 2 commits into
mainfrom
cl-5552-own-process-termination
Aug 8, 2026
Merged

Own process termination on detached throws and OS signals#406
TheGreatAxios merged 2 commits into
mainfrom
cl-5552-own-process-termination

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Collaborator

Summary

  • A throw that escapes runTUI's own try/catch (e.g. a fire-and-forget void call) reached the process-level uncaughtException/unhandledRejection handler, but that handler had no way to restore the terminal — it only knew about run.json state, not the terminal-restore closure bound inside runTUI. A module-level slot (src/session/active-host.ts, mirroring the existing active-run.ts pattern) closes that gap.
  • No SIGINT/SIGTERM/SIGHUP handler existed anywhere. OpenTUI's own vendored listener for those signals restores the terminal when a TUI is mounted but never calls process.exit, so an external kill left the process (and run.json) hanging. Outside an interactive session (exec mode, or before a host mounts), nothing handled the signal at all.
  • The new signal handler restores the terminal and finalizes run state directly, rather than depending on OpenTUI's own listener's registration order/internals — disposeHost is idempotent, so a redundant call from OpenTUI's own listener is harmless.
  • A forked-pty regression test (tests/integration/rawmode-sigint.test.ts) pins the empirical finding the whole design depends on: Bun's raw-mode stdin clears ISIG, so a real Ctrl+C keypress during an interactive session is delivered only as a stdin byte, never as SIGINT — the in-session double-tap-to-quit gesture stays the sole owner of in-session Ctrl+C.

Test plan

  • bun run typecheck
  • bun run build
  • bun run test (full suite via the project's serialized runner) — 4162 pass / 1 fail, the one failure (src/agent/lsp-availability.test.ts) is a pre-existing environment gap (missing typescript-language-server binary in this worktree), unrelated to this change
  • New unit test: src/session/active-host.test.ts
  • New integration tests: tests/integration/signal-finalize.test.ts (SIGINT/SIGTERM/SIGHUP each finalize run.json as failed and exit with the correct 128+n code), tests/integration/rawmode-sigint.test.ts (pty-based, pins the raw-mode/ISIG finding)
  • Reviewed with Greybeard (two rounds — first caught a live design bug requiring empirical verification of raw-mode signal delivery; second caught reliance on unverified OpenTUI internals for teardown ordering), Critique (per commit), a broader code-review pass, a security review (no findings), and a pedantic pass — no blocking issues found in any

A throw that escapes runTUI's own try/catch (e.g. inside a
fire-and-forget void call) only reached the top-level
uncaughtException/unhandledRejection handler, which had no way to
call runTUI's terminal-restore routine since it lives as a closure
bound only once the OpenTUI host mounts. The process exited with
the terminal left in the alternate screen and raw mode still on.

A module-level slot mirroring the existing active-run.ts pattern
lets the handler reach it.
Corbits registered no signal handlers of its own. OpenTUI's vendored
renderer restores the terminal on these signals when a TUI is
mounted, but never calls process.exit, so the process (and the
run's state on disk) was left hanging indefinitely after an external
kill. Outside an interactive session (exec mode, or before a host
mounts) nothing handled the signal at all, so Bun's default
disposition killed the process with no chance to close out run.json.

The new handler restores the terminal and finalizes run state itself
rather than relying on OpenTUI's own listener to run first, since
that would make correctness depend on a vendored listener's
registration order and internals this codebase doesn't own; the
terminal-restore call is idempotent so a redundant call from
OpenTUI's own listener is harmless.

A forked-pty regression test pins the empirical finding this design
depends on: Bun's raw-mode stdin clears ISIG, so a real Ctrl+C
keypress during an interactive session is delivered only as a stdin
byte, never as a SIGINT, leaving the existing double-tap-to-quit
gesture as the sole owner of in-session Ctrl+C.
@linear-code

linear-code Bot commented Aug 8, 2026

Copy link
Copy Markdown

CL-5552

@TheGreatAxios
TheGreatAxios merged commit 64e85dc into main Aug 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant