A SIGINT delivered while the event loop is parked in its selector is raised in
the loop's own frame: it escapes run_until_complete, the Runner cancels the main
task, and the REPL is left half-shut-down (no prompt, and the next line the
human types is eaten by an abandoned prompt reader) until the following Ctrl+C
kills the process. Off-loop prompts (ask_user_* / wait / tool confirms run in a
worker thread via asyncio.to_thread) and the auto-retry countdown are the usual
windows.
Keep one asyncio-level SIGINT handler installed for the whole session instead:
it cancels the innermost registered target (run_cancellable registers the turn
task, sleep_cancellable the retry countdown, off_loop_prompt the waiting prompt)
and never raises. Reads that block on the main thread inside a coroutine (the
REPL entry prompt, synchronous click prompts) suspend the router and take SIGINT
as a KeyboardInterrupt. Off-loop readline prompts cannot be interrupted without
abandoning their reader thread on stdin, so they ignore SIGINT with a one-line
hint.
A terminal Ctrl+C is delivered to every process in the foreground process
group, so a server spawned in our group died on any Ctrl+C during a turn -
killing in-flight tool calls and leaving the connection dead until
/mcp reconnect. start_new_session (POSIX) / CREATE_NEW_PROCESS_GROUP
(Windows) puts each server in its own session/group instead.
Tests assert the spawned server's process group and session differ from the
CLI's (they used to be the same group).
Appending via edit_lineno required the last line to be read, which made
writing to an empty file impossible (line 0 does not exist). An empty
file has nothing to clobber, so the append needs no prior read.