Follow-up to #11746 for the case @fcatuhe reported on #11599: a second
shutdown trigger cut off cleanup.
Problem: Paseo stops pi by closing stdin and sending SIGTERM in the same
tick (SIGKILL 2 s later); systemd sends SIGTERM and stdin closes after.
In RPC mode:
- stdin EOF, then SIGTERM: `shutdown()` removed pi's SIGTERM handler
when it started, so the signal killed pi (exit 143) mid-`dispose()`.
- SIGTERM, then stdin EOF: the second `shutdown()` call ran
`process.exit()`.
A pending MCP token refresh was lost: the server had rotated the refresh
token, the next pi sent the old one, and the server revoked the grant.
Rule after this change: once a shutdown has started, a stdin EOF or a
single signal does not interrupt it; a second signal exits at once.
SIGKILL still always works.
- **RPC mode** (`rpc-mode.ts`): signal handlers stay registered until
exit; `shutdown()` runs once and later calls do nothing; the signal
handler counts signals and exits only on a repeated one.
- **Print mode** (`print-mode.ts`): the final `finally` removed the
signal handlers before `disposeRuntime()`, so a signal during the final
disposal killed pi. Handlers are now removed after it,
`disposeRuntime()` returns the shared in-flight disposal (a signal then
waits for it), and a repeated signal exits at once, as before.
- **Interactive mode**: unchanged. It already lets a started shutdown
finish.
Docs (`docs/rpc.md`) and the CHANGELOG are updated.
## Testing
- `test/rpc-prompt-response-semantics.test.ts`: EOF then SIGTERM,
SIGTERM then EOF (both wait for a slow dispose, exit once with 0 and
143), and EOF then two SIGTERMs (exits at once with 143). All three fail
on main.
- `test/print-mode.test.ts`: SIGTERM during the final disposal waits for
it, a second SIGTERM exits with 143, handlers are removed afterwards.
Fails on main.
- Real process, `pi --mode rpc` with a `session_shutdown` handler that
takes 1 s and real signals:
- EOF then SIGTERM: cleanup done, exit 0 (main: no cleanup, exit 143)
- SIGTERM then EOF: cleanup done, exit 143
- EOF then two SIGTERMs: exit 143 at once
- `npm run check` passes; all `test/rpc*` and `test/print-mode` tests
pass.
Print mode now writes a warning to stderr and exits 1 when the final
response stopped at the model's output token limit. The partial text
is still written to stdout.
fixes#10799