fix(background-agent): add OMO_DISABLE_PROCESS_CLEANUP env opt-out for global handlers (fixes #3856)
Currently `registerManagerForCleanup` unconditionally installs global `uncaughtException` and `unhandledRejection` listeners that call `process.exit(1)` after cleanup. For users who load the plugin but never run background-agent tasks, these handlers turn transient streaming errors (e.g. undici `UND_ERR_SOCKET` mid-stream resets from `api.githubcopilot.com`) into a full process kill — opencode dies after every flaky response. Add an `OMO_DISABLE_PROCESS_CLEANUP` env var (accepts 1/true/yes/on, case-insensitive) that skips just the error-event registration. Signal handlers (SIGINT/SIGTERM/SIGBREAK/beforeExit/exit) remain installed so graceful shutdown of any in-flight cleanup targets still runs. This is the lowest-risk near-term mitigation suggested in the issue (option #2): users opting in pay the cost of unhandled rejections themselves, but no longer lose their session to a transient socket reset. Verification: 6 new test cases cover env-var precedence (set/unset, truthy/falsy values), signal-handler preservation, and behavior under `uncaughtException`. All 20 tests in process-cleanup.test.ts pass. Typecheck clean. Manual QA confirms env-var detection works end-to-end.
This commit is contained in:
@@ -3,6 +3,26 @@ import { log } from "../../shared"
|
||||
type ProcessCleanupSignal = NodeJS.Signals | "beforeExit" | "exit"
|
||||
type ProcessCleanupErrorEvent = "uncaughtException" | "unhandledRejection"
|
||||
|
||||
/**
|
||||
* When set to a truthy value (1/true/yes/on), suppresses the global
|
||||
* uncaughtException / unhandledRejection handlers that force-exit the host
|
||||
* process. Use this when the plugin is installed but background-agent tasks
|
||||
* are not actively in use, to avoid OpenCode dying on transient streaming
|
||||
* errors propagated as unhandled rejections (see issue #3856).
|
||||
*
|
||||
* Signal handlers (SIGINT/SIGTERM/SIGBREAK/beforeExit/exit) remain registered
|
||||
* because they are needed for graceful shutdown of any in-flight cleanup
|
||||
* targets that were registered before the user noticed the issue.
|
||||
*/
|
||||
const PROCESS_CLEANUP_DISABLE_ENV = "OMO_DISABLE_PROCESS_CLEANUP"
|
||||
const TRUTHY_ENV_VALUES = new Set(["1", "true", "yes", "on"])
|
||||
|
||||
function isProcessCleanupErrorHandlersDisabled(): boolean {
|
||||
const raw = process.env[PROCESS_CLEANUP_DISABLE_ENV]
|
||||
if (!raw) return false
|
||||
return TRUTHY_ENV_VALUES.has(raw.trim().toLowerCase())
|
||||
}
|
||||
|
||||
/** @internal test-only seam: prevents process.exitCode from contaminating bun test runner */
|
||||
let _scheduleForcedExitEnabled = true
|
||||
|
||||
@@ -116,6 +136,15 @@ export function registerManagerForCleanup(manager: CleanupTarget): void {
|
||||
}
|
||||
registerSignal("beforeExit", false)
|
||||
registerSignal("exit", false)
|
||||
|
||||
if (isProcessCleanupErrorHandlersDisabled()) {
|
||||
log(
|
||||
`[background-agent] ${PROCESS_CLEANUP_DISABLE_ENV} is set; skipping global uncaughtException/unhandledRejection handler registration. `
|
||||
+ "Signal handlers (SIGINT/SIGTERM/beforeExit/exit) remain active.",
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
cleanupErrorHandlers.set("uncaughtException", registerErrorEvent("uncaughtException", cleanupAll))
|
||||
cleanupErrorHandlers.set("unhandledRejection", registerErrorEvent("unhandledRejection", cleanupAll))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user