The postmortem SIGTERM/SIGHUP/uncaughtException handlers only ran the registered
cleanup callback list before process.exit, and the only session-related callback
was session-manager-flush. So a real kernel signal (terminal close, process
manager killing omp, IDE stop) skipped saveDraft, session.dispose (session_shutdown
emit, owned async job disposal, kernel disposal, MCP disconnect, browser tab
release), and violated the SessionShutdownEvent docstring contract that promises
delivery on SIGINT/SIGTERM. The LSP client also owned its own SIGINT/SIGTERM
handlers that called shutdownAll then process.exit(0), which could race postmortem's
async runCleanup and short-circuit the session teardown.
- Extracted a promise-memoized createSessionTeardown helper (modes/session-teardown.ts)
that snapshots the editor draft, persists it via sessionManager.saveDraft, then
invokes session.dispose. A saveDraft failure is logged but never aborts disposal.
- Memoized AgentSession.dispose so the keypress path and the signal path share one
settled promise and cannot double-emit session_shutdown or double-drain the owned
AsyncJobManager.
- Registered the teardown on postmortem as "session-teardown" in InteractiveMode.init,
replacing the narrower session-manager-flush callback. InteractiveMode.shutdown
now delegates the draft+dispose steps to the same helper.
- Replaced the LSP client's SIGINT/SIGTERM handlers with a "lsp-shutdown" postmortem
callback so LSP cleanup runs alongside every other session teardown instead of
racing them via process.exit(0). beforeExit is unchanged.
- Added session-teardown.test.ts covering: draft-then-dispose ordering, disposal
after saveDraft rejects, empty-string clears stale sidecar, promise memoization
under concurrent invocation, and snapshot-at-first-call semantics.
Fixes#4080