73a12c7ea2
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