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

@ottosulin

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. oh-my-openagent@4.1.1 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: oh-my-openagent@4.1.1 (also reproduces on @4.1.2)

Evidence

1. Isolation test confirms the offending plugin

Config tested today Sidecar uptime Result
plugin: ["cc-safety-net", "oh-my-openagent@v4.1.1"] + 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 oh-my-openagent@4.1.1 (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.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions