Skip to content

Desktop sidecar exits silently with code 1 — third-party plugin's global uncaughtException handler calls process.exit(1) on any unhandled rejection in the sidecar #27557

Closed as not planned

Description

@ottosulin

Summary

OpenCode Desktop's sidecar process exits silently with code 1 anywhere from 30 seconds to ~10 minutes after server ready, with no stderr written to ~/Library/Logs/@opencode-ai/desktop/main.log. The Desktop UI then displays "server is dead". Repeats on every relaunch.

Root cause traced: a third-party opencode plugin (oh-my-openagent) installs a global Node process.on('uncaughtException', …) handler that catches ANY unhandled rejection in the sidecar — including ones thrown by opencode core itself — and unconditionally calls process.exit(1). The plugin's maintainer has acknowledged this in code-yeongyu/oh-my-openagent#3856 (open, with proposed fixes) and the v4.1.1 regression specifically in code-yeongyu/oh-my-openagent#3997 (open).

I'm filing this against opencode anyway because the architecture that lets a single plugin's global error handler take down the entire sidecar with zero diagnostic surface is itself the bug to fix in opencode. Even after the upstream plugin fixes #3856, any future plugin can do the same thing and reproduce this exact failure.

A secondary, independent failure mode (concurrent SQLite access from a co-running CLI server) is documented further down — same observable symptom, different root cause, both worth fixing.

Reproducing

Reproduction A — silent crash from third-party plugin's global error handler

  1. Configure an opencode plugin that registers global uncaughtException/unhandledRejection handlers in opencode.json. [email protected] and @4.1.2 both reproduce reliably (the offending code is in src/features/background-agent/process-cleanup.ts → registerManagerForCleanup, which calls process.exit(1) inside scheduleForcedExit).
  2. Quit any running opencode processes. Verify with ps -ef | grep -i opencode showing nothing.
  3. Launch OpenCode Desktop.
  4. Wait. Sidecar exits silently with code: 1 somewhere between 30 sec and 10 min later. Nothing in the Desktop log explains why.

100% reproduction rate. Removing oh-my-openagent from "plugin" in opencode.json makes the sidecar stay alive indefinitely — confirmed by my isolation testing today (table below).

The actual chain of events for one specific crash today, recovered from oh-my-openagent's log file at $TMPDIR/oh-my-opencode.log:

[2026-05-14T12:44:51.253Z] [auto-compact] session.error received {
  "sessionID": "ses_…",
  "error": {
    "name": "UnknownError",
    "data": {
      "message": "DrizzleError: Failed to run the query 'begin immediate'
        at NodeSQLiteSession.run (.../app.asar/out/main/chunks/node-DbvQoYZ8.js:74693:15)
        at SessionProcessor.cleanup (.../node-DbvQoYZ8.js:285666:68)
        at SessionProcessor.process (.../node-DbvQoYZ8.js:321876:42)
        ...
        [cause]: Error: file is not a database
          at NodeSQLitePreparedQuery.run (.../node-DbvQoYZ8.js:74814:19)"
    }
  }
}

So opencode's own session processor hit a Drizzle "file is not a database" error (likely cascade from the secondary SQLite-contention failure mode below). The error became an unhandled rejection in opencode core. The plugin's global handler caught it and called process.exit(1) — silent death.

Reproduction B — silent crash from concurrent SQLite access

Independent failure mode that overlaps in symptoms but has a different root cause:

  1. Open a terminal and start an opencode CLI session: opencode .
  2. Launch OpenCode Desktop.
  3. Both processes write to the same SQLite database at ~/.local/share/opencode/opencode.db. Node's experimental node:sqlite doesn't safely handle concurrent writers from independent processes.
  4. After 5–9 minutes the Desktop sidecar exits silently with code: 1 again — same observable symptom, totally different cause.

Diagnostic signature: log show --predicate 'eventMessage CONTAINS "192644 of"' --last 60m | wc -l returns thousands of SQLITE_MISUSE errors from libsqlite3.dylib. opencode has previously detected this state and renamed the corrupted DB files to *.broken in ~/.local/share/opencode/.

This Drizzle "file is not a database" error (above) almost certainly comes from a previous co-running session that left the WAL in a bad state — the Drizzle layer hits the corruption, throws, and (because of the plugin's handler) the sidecar dies silently.

Environment

  • OpenCode Desktop: v1.14.50 (also reproduced on v1.14.48)
  • opencode CLI: v1.14.50
  • macOS: 26.4.1 (Build 25E253)
  • Architecture: Apple Silicon (arm64, Darwin 25.4.0)
  • Node SQLite: experimental node:sqlite (warning visible on every sidecar startup)
  • Triggering plugin: [email protected] (also reproduces on @4.1.2)

Evidence

1. Isolation test confirms the offending plugin

Config tested today Sidecar uptime Result
plugin: ["cc-safety-net", "[email protected]"] + memory MCP + playwright MCP 30s – 9 min dies code 1 every time
Same plugins, playwright MCP disabled 1m 49s still dies code 1
plugin: ["cc-safety-net"] (oh-my-openagent removed) + memory MCP + playwright MCP enabled >>15 min and still alive sidecar STABLE

Removing oh-my-openagent from the plugin list is the variable that fixed it. MCP server choice was irrelevant.

2. The process.exit(1) call in the plugin

From src/features/background-agent/process-cleanup.ts at v4.1.2 (still present, NOT fixed by v4.1.2):

function scheduleForcedExit(cleanupResult, exitCode, exitAfterCleanup = false) {
  if (!_scheduleForcedExitEnabled) return
  process.exitCode = exitCode
  const exitTimeout = setTimeout(() => process.exit(), 6000)
  void Promise.resolve(cleanupResult).finally(() => {
    clearTimeout(exitTimeout)
    if (exitAfterCleanup) {
      process.exit(exitCode)            // <-- the silent code-1 exit you see in main.log
    }
  })
}

function registerErrorEvent(signal, handler) {
  const listener = (error) => {
    process.off(signal, listener)
    log(`[background-agent] ${signal} received during shutdown cleanup:`, error)
    scheduleForcedExit(handler(error), 1, true)        // <-- triggers the exit
  }
  process.on(signal, listener)
  return listener
}

export function registerManagerForCleanup(manager) {
  // ... unconditionally registers global handlers on first manager creation:
  cleanupErrorHandlers.set("uncaughtException", registerErrorEvent("uncaughtException", cleanupAll))
  cleanupErrorHandlers.set("unhandledRejection", registerErrorEvent("unhandledRejection", cleanupAll))
}

Once any CleanupManager is constructed (which happens at plugin load), opencode's process gets a global uncaughtException/unhandledRejection handler that always calls process.exit(1). The handler doesn't check whether the error came from the plugin's own code or from somewhere else in opencode — it just exits.

The error log goes to $TMPDIR/oh-my-opencode.log, not opencode's main.log. That's why opencode's main.log shows only [warn] sidecar exited { code: 1 } with no preceding stderr.

3. macOS runningboardd shows ~9 child node processes terminating in lockstep with the sidecar

At the exact millisecond the sidecar (PID 49810) exited at 19:08:02, 9 child node processes terminated via proc_exit:

2026-05-14 19:08:02.050 runningboardd: [anon<node>(501):50483] termination reported by proc_exit
2026-05-14 19:08:02.050 runningboardd: [anon<node>(501):50025] termination reported by proc_exit
2026-05-14 19:08:02.050 runningboardd: [anon<node>(501):50484] termination reported by proc_exit
2026-05-14 19:08:02.050 runningboardd: [anon<node>(501):50044] termination reported by proc_exit
2026-05-14 19:08:02.051 runningboardd: [anon<node>(501):50045] termination reported by proc_exit
2026-05-14 19:08:02.051 runningboardd: [anon<node>(501):50221] termination reported by proc_exit
2026-05-14 19:08:02.052 runningboardd: [anon<node>(501):49903] termination reported by proc_exit
2026-05-14 19:08:02.052 runningboardd: [anon<node>(501):49957] termination reported by proc_exit
2026-05-14 19:08:02.052 runningboardd: [anon<node>(501):49902] termination reported by proc_exit

These are background-agent and MCP-server child processes the plugin and opencode spawned. They die because their parent (the sidecar) called process.exit(1).

4. Sidecar exit-code timeline today

[15:11:30] sidecar exited { code: 1 }      uptime ~7 min   plugins+MCP, no CLI conflict
[15:17:09] sidecar exited { code: 1 }      uptime ~5 min   plugins+MCP, no CLI conflict
[15:44:52] sidecar exited { code: 1 }      uptime ~8 min   plugins+MCP, no CLI conflict
[15:50:00] sidecar exited { code: 0 }      manual quit
[15:52:29] sidecar exited { code: 1 }      uptime ~2 min   plugins+MCP
[16:28:39] sidecar exited { code: 1 }      uptime ~9 min   plugins+MCP
...
[19:08:02] sidecar exited { code: 1 }      uptime  67 sec  plugins+MCP, no CLI running
[19:21:47] sidecar exited { code: 1 }      uptime ~2 min   playwright MCP disabled, oh-my-openagent still active
[19:30:00+] STILL ALIVE                    oh-my-openagent removed from plugin list

Why this is an opencode bug, not just a plugin bug

The third-party plugin is the proximate trigger, and a fix at the plugin level (#3856) would resolve my immediate problem. But the design that lets a plugin install global error handlers that exit the host process — with zero diagnostic signal — is the deeper failure to fix in opencode itself:

  1. Plugins should not be able to install global uncaughtException/unhandledRejection handlers that take down the host. Either run plugins in worker threads / vm contexts where their handlers don't affect the main process, or detect/refuse global handler registration from plugins, or document this as forbidden and audit the ecosystem. Today opencode's plugin loader hands the plugin direct access to process.on(…) and there's no protection.
  2. The [warn] sidecar exited { code: 1 } log line should include a reason. Capture stderr deltas, the last hook invoked, the last MCP server activity, the last plugin-emitted log line — anything. Right now there is exactly one log line for what is, to the user, a hard crash.
  3. The Desktop UI's "server is dead" should be replaceable with a dialog like "Sidecar crashed (Plugin X registered process.exit). Restart? Disable plugin X? View log?" Letting the user click "disable" gets them back to working state without needing to grep main.log + edit JSON + relaunch.
  4. Consider an MCP-server / plugin watchdog: respawn dead plugin children with backoff and a circuit breaker. Other MCP clients (Claude Desktop, Cursor) already do this.

v1.14.49's "Show clearer wrapped server errors in the app" is moving in this direction but doesn't cover the plugin-uncaughtException → sidecar-exit-1 path.

Secondary issue: SQLITE_MISUSE under concurrent access

When an opencode CLI server runs concurrently with the Desktop sidecar under the same user account, both processes write to ~/.local/share/opencode/opencode.db and Node's experimental node:sqlite accumulates ~1 SQLITE_MISUSE error per second from libsqlite3.dylib until one process's SQLite handle becomes unusable. This is a separate failure mode that produces the same observable symptom (silent code-1 exit) and creates the conditions for the Drizzle "file is not a database" cascade above.

Suggested fixes:

  • Single-server-per-user enforcement: take an advisory lock on ~/.local/share/opencode/server.lock at startup; refuse to start (or auto-connect to the existing server) if the lock is held.
  • Per-process DB isolation as fallback: per-PID or per-mode DB path (opencode-desktop.db vs opencode-cli.db).
  • Surface SQLITE_MISUSE to stderr: install a libsqlite3 error log callback so users see something before silent exit.

Updated reproduction steps (no CLI server needed, current behavior)

  1. Add [email protected] (or @4.1.2) to the plugin array in opencode.json.
  2. killall opencode opencode-cli OpenCode 'OpenCode Helper' 2>/dev/null; verify ps -ef | grep -i opencode is empty.
  3. open -a OpenCode
  4. Wait. Sidecar dies silently with code: 1 between 30 sec and 10 min.
  5. Confirm via ~/Library/Logs/@opencode-ai/desktop/main.log (single warn line) and tail $TMPDIR/oh-my-opencode.log (you'll see the actual error and shutdown trace there).

Workarounds while this is being fixed

  • Plugin failure mode: review your plugin list in opencode.json. Remove oh-my-openagent (or any plugin that installs global uncaughtException handlers — see race condition when applying changes immediatly after each other on same file #3856). Track the upstream fix at code-yeongyu/oh-my-openagent#3856 and #3997.
  • SQLite contention failure mode: only run one opencode server process per user account at a time. Quit opencode CLI sessions before launching Desktop, and vice versa. Delete *.broken files in ~/.local/share/opencode/ after confirming opencode.db integrity with sqlite3 ~/.local/share/opencode/opencode.db "PRAGMA integrity_check;".

Cross-referenced upstream issues

  • code-yeongyu/oh-my-openagent#3856 — "Make global uncaughtException/unhandledRejection handlers opt-in to background-agent feature". Open. Has the same diagnosis and proposes fixes at the plugin level.
  • code-yeongyu/oh-my-openagent#3997 — "Sidecar crashes (exit code 1) after first request since v4.1.1 update". Open. Filed 1 day ago; matches my repro precisely.
  • code-yeongyu/oh-my-openagent#3772 — "OmO creates huge log file due to EPIPE error". Open. Different symptom of the same underlying handler design.

opencode v1.14.41 release notes: "Moved the desktop app's local server into a separate utility process for more reliable startup and shutdown" — this isolation is what makes the silent crash possible without taking the whole Desktop process down.

opencode v1.14.49 bugfix: "Show clearer wrapped server errors in the app" — moves in the right direction; doesn't cover the plugin-crash path observed here.

Activity

  1. github-actions commented on May 14, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

    These all stem from the same structural issue: multiple opencode server processes sharing a single SQLite database without process-level write coordination.

  2. ottosulin commented on May 14, 2026

    @ottosulin
    Author

    Update: SQLite contention is NOT the only cause — also reproduces with no other opencode process running

    After filing this issue I tested again under different conditions and found this also reproduces with zero other opencode processes running on the system. So my SQLite-contention root cause is incomplete — it's a real symptom and a real failure mode, but it's not the only one.

    New repro (today, 2026-05-14 19:06:51)

    Killed all opencode processes (verified with ps -ef | grep opencode returning nothing). Launched OpenCode Desktop fresh. Sidecar started cleanly, server ready at http://127.0.0.1:63572. 67 seconds later the sidecar exited with code: 1. Same silent death pattern, no stderr.

    [19:06:54.196] [info]  spawning sidecar { url: 'http://127.0.0.1:63572' }
    [19:06:55.611] [info]  server ready { url: 'http://127.0.0.1:63572' }
    [19:07:00.924] [info]  no update available
    [19:08:02.016] [warn]  sidecar exited { code: 1 }   <-- no stderr between server-ready and exit
    

    log show --predicate 'eventMessage CONTAINS "192644 of"' --start "2026-05-14 19:00:00" --end "2026-05-14 19:09:00" | wc -l returns 0 for that window. No SQLITE_MISUSE this time.

    What's likely actually happening

    runningboardd shows that at the EXACT millisecond of sidecar death, 9 child node processes also terminated via proc_exit:

    2026-05-14 19:08:02.050 [com.apple.runningboard:process] [anon<node>(501):50483] termination reported by proc_exit
    2026-05-14 19:08:02.050 [com.apple.runningboard:process] [anon<node>(501):50025] termination reported by proc_exit
    2026-05-14 19:08:02.050 [com.apple.runningboard:process] [anon<node>(501):50484] termination reported by proc_exit
    2026-05-14 19:08:02.050 [com.apple.runningboard:process] [anon<node>(501):50044] termination reported by proc_exit
    2026-05-14 19:08:02.051 [com.apple.runningboard:process] [anon<node>(501):50045] termination reported by proc_exit
    2026-05-14 19:08:02.051 [com.apple.runningboard:process] [anon<node>(501):50221] termination reported by proc_exit
    2026-05-14 19:08:02.052 [com.apple.runningboard:process] [anon<node>(501):49903] termination reported by proc_exit
    2026-05-14 19:08:02.052 [com.apple.runningboard:process] [anon<node>(501):49957] termination reported by proc_exit
    2026-05-14 19:08:02.052 [com.apple.runningboard:process] [anon<node>(501):49902] termination reported by proc_exit
    

    All 9 are /Users/ottosulin/.nvm/versions/node/v20.19.2/bin/node — these are MCP server children spawned by the sidecar. My opencode.json configures these MCP servers (relevant entries):

    "mcp": {
      "memory":     { "type": "local", "command": ["npx", "-y", "@modelcontextprotocol/server-memory"], ... },
      "playwright": { "type": "local", "command": ["npx", "@playwright/mcp@latest", "--extension"], ... }
    }

    The first 7 MCP node children spawned 3–5 seconds after sidecar start. The last 2 (PIDs 50483, 50484) spawned at 19:07:50.448 and 19:07:50.454 — 12 seconds before sidecar death. This timing strongly suggests an MCP-server-side error propagated up and crashed the sidecar.

    Suspect: @playwright/mcp@latest --extension mode

    I'm running @playwright/mcp v0.0.75 (latest as of repro), which ships an alpha playwright-core (1.61.0-alpha-1778188671000). Looking at upstream playwright-mcp release notes, the --extension code path has been actively bug-fixed in the last three releases:

    • v0.0.75 (May 7): "Serialize shared browser launch in --isolated mode", "Forward browser-level CDP commands in extension mode" — directly touches --extension
    • v0.0.74 (May 6): "Auto-recover when the remote browser disconnects mid-session", "Recover from page renderer crash" — without these fixes, MCP server would crash on disconnect/renderer-crash
    • v0.0.72 (Apr 30): "Surface unhandled rejections in browser_run_code_unsafe instead of crashing the server" — direct admission that previous versions crashed the MCP server on unhandled rejection
    • v0.0.73: more --extension fixes around channel/executablePath resolution

    I'll be testing whether disabling playwright (and/or memory) in my opencode.json prevents the silent sidecar death. Will report back.

    Implications for opencode

    Even if the upstream MCP server is the proximate trigger, the sidecar dying silently with no stderr and no UI surface is the bug to fix in opencode itself:

    1. An MCP server's stdio process dying should NOT take the entire opencode sidecar down. The MCP child should be supervised: if it dies, log it (visible in ~/Library/Logs/@opencode-ai/desktop/main.log), surface "MCP server X disconnected" in the Desktop UI, and continue serving the user with the remaining MCP servers + built-in tools. v1.14.49's "Show clearer wrapped server errors in the app" is moving in this direction but evidently doesn't cover this code path.
    2. The [warn] sidecar exited { code: 1 } line in main.log should include WHY the sidecar exited. Capture the last N stderr lines, the last MCP server activity, the last tool call — anything. Right now there is literally one log line and zero context.
    3. Consider adding an MCP-server health/restart loop: when an MCP child stdio process closes, attempt to respawn it (with backoff and a max-retry circuit-breaker) instead of letting the failure cascade. This matches what other MCP clients (Claude Desktop, Cursor) already do.

    Updated reproduction steps (no CLI server needed)

    1. Configure at least one stdio MCP server in opencode.json (the more, the faster repro). Especially @playwright/mcp@latest --extension if you have Chrome installed.
    2. killall opencode opencode-cli OpenCode 'OpenCode Helper' 2>/dev/null; verify ps -ef | grep -i opencode is empty.
    3. open -a OpenCode
    4. Wait. Sidecar dies silently with code: 1 somewhere between 30 seconds and 10 minutes. No stderr. No crash report.
    5. Confirm via ~/Library/Logs/@opencode-ai/desktop/main.log and log show --predicate 'eventMessage CONTAINS[c] "termination reported by proc_exit"' --predicate 'eventMessage CONTAINS[c] "anon<node>"' --last 10m | head — you'll see N child node processes terminating in lock-step with the sidecar at the moment of death.

    Apologies for the partial diagnosis in the original post

    The SQLite-contention story is a real second failure mode (still reproducible if you have a CLI server running concurrently), but it's not the primary cause. The primary cause looks like MCP-server failure cascading into sidecar death. Both behaviors should be fixed:

    1. Don't share ~/.local/share/opencode/opencode.db between processes without locking (the SQLite issue from the original post).
    2. Don't let an MCP stdio child's failure kill the whole sidecar (the new finding).

    I'll add another comment once I've confirmed which MCP server is the trigger by selectively disabling them.

  3. changed the title [-]Desktop sidecar exits silently with code 1 after 5–9 min when an `opencode` CLI server runs concurrently (shared SQLite DB)[/-] [+]Desktop sidecar exits silently with code 1 — caused by plugin crash (most cases) and concurrent SQLite access (secondary)[/+] on May 14, 2026
  4. changed the title [-]Desktop sidecar exits silently with code 1 — caused by plugin crash (most cases) and concurrent SQLite access (secondary)[/-] [+]Desktop sidecar exits silently with code 1 — third-party plugin's global uncaughtException handler calls process.exit(1) on any unhandled rejection in the sidecar[/+] on May 14, 2026
  5. github-actions commented on Jul 14, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions