Skip to content

Commit 159e30a

Browse files
cemililikclaude
andcommitted
fix(cli): remove the SIGINT handler AFTER finalize, not before (unmount-window race)
Valid review finding. The finally removed our SIGINT listener BEFORE awaiting renderer.finalize() (ink's unmount + waitUntilExit, which takes a beat). In that window our listener was gone but ink's `signal-exit` listener was still registered — so a second, impatient Ctrl-C during unmount would find signal-exit as the SOLE SIGINT listener → it re-raises → exit 130 (the very failure mode a67cbd6 fixed for the first press). Moving `process.removeListener('SIGINT', onSigint)` to AFTER finalize keeps us in the SIGINT set across the whole unmount: a Ctrl-C during finalize hits our handler (cancelRequested already true → clean exit 1) and signal-exit never sees itself as the sole listener. After finalize, ink has unsubscribed its own listener, so removing ours then leaves the set clean. The abnormal-unwind `handle.cancel()` stays before finalize. 202 cli tests green (SIGINT/STALL + finalize-wiring unchanged); full gate green; format clean; Leakwatch 0. (The exact race — a second Ctrl-C within the ~tens-of-ms unmount window — is hard to hit manually; the fix is the same listener-set invariant a67cbd6 established, now held through teardown.) Refs: phase-2-cli.md 2.D/2.E Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 9b4fc51 commit 159e30a

1 file changed

Lines changed: 5 additions & 1 deletion

File tree

apps/cli/src/commands/run.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,6 @@ export async function runCommand(args: RunCommandArgs, deps: RunCommandDeps): Pr
160160
}
161161
}
162162
} finally {
163-
process.removeListener('SIGINT', onSigint);
164163
// No terminal/paused outcome means we're unwinding abnormally (renderer construction threw, or the
165164
// event stream rejected) — cancel the still-live engine run so it doesn't keep executing unsupervised
166165
// in the background while the error propagates (cancel is idempotent + safe post-terminal).
@@ -178,6 +177,11 @@ export async function runCommand(args: RunCommandArgs, deps: RunCommandDeps): Pr
178177
`renderer teardown failed: ${teardownErr instanceof Error ? teardownErr.message : String(teardownErr)}\n`,
179178
);
180179
}
180+
// Remove our SIGINT handler LAST — keep it registered across ink's unmount (renderer.finalize), so a
181+
// Ctrl-C during unmount still hits us (forcing a clean exit 1) and ink's `signal-exit` never becomes the
182+
// sole SIGINT listener (which would re-raise → 130). After finalize, ink has unsubscribed its own
183+
// listener, so removing ours here leaves the SIGINT set clean.
184+
process.removeListener('SIGINT', onSigint);
181185
}
182186

183187
switch (outcome) {

0 commit comments

Comments
 (0)