scheduleForcedExit() sets process.exitCode which taints the bun test runner's
own exit code for the entire suite. This caused CI to fail even though all
tests passed individually.
Fix:
- Add __disableScheduledForcedExitForTesting / __enableScheduledForcedExitForTesting
seams to skip scheduleForcedExit() during tests
- beforeEach disables forced exit; afterEach re-enables
- The 'fallback exit timer' test explicitly re-enables to verify setTimeout/clearTimeout
- Remove process.exitCode and exitSpy assertions that required forced exit to be active
(shutdown call counts are sufficient to verify behavior)
When shutdown() itself emitted uncaughtException (e.g. EPIPE while closing
a broken pipe), the error listener re-entered itself, re-logged, re-ran
cleanup, and threw EPIPE again. The 6 s forced-exit timer could not fire
because every re-entry stalled the event loop with fresh synchronous work.
Users hit this after v3.17.5 and observed 100+ GB of log lines written to
disk within minutes, with one confirmed report of a 157 GB log file filling
the filesystem.
Detaching the listener with process.off() before running log() + handler()
breaks the loop at the first re-emit: the second event has no listener to
invoke, and the first invocation's scheduleForcedExit() proceeds normally.
Signal handlers covered SIGINT/SIGTERM/SIGBREAK/beforeExit/exit, but a
synchronous throw or a top-level rejected promise terminated the process
without letting TmuxSessionManager (or any other registered manager) run
its shutdown hook. That reliably left orphan tmux panes after an opencode
crash.
Added registration for uncaughtException and unhandledRejection that fan
out through the existing cleanupAll() path, set process.exitCode = 1,
and arm the same 6 second forced-exit guard we use for signals. Test
helpers hold process-level spies so the new tests do not leak listeners
between runs.
Two issues fixed:
1. process-cleanup.ts used fire-and-forget void Promise for shutdown
handlers — now properly collects and awaits all cleanup promises
via Promise.allSettled, with dedup guard to prevent double cleanup
2. TmuxSessionManager was never registered for process cleanup —
now registered in create-managers.ts via registerManagerForCleanup
Also fixed setTimeout().unref() which could let the process exit
before cleanup completes.
- Revert getMessageDir to original join(MESSAGE_STORAGE, sessionID) behavior
- Fix dead subagentSessions.delete by capturing previousSessionID before tryFallbackRetry
- Add .unref() to process cleanup setTimeout to prevent 6s hang on Ctrl-C
- Add missing isUnstableAgent to fallback retry input mapping
- Fix process-cleanup tests to use exit listener instead of SIGINT at index 0
- Swap test filenames in compaction-aware-message-resolver to exercise skip logic correctly