Skip to content

fix(opencode): private session DB per worker — the 'database is locked' root cause - #34

Merged
usjoh merged 1 commit into
mainfrom
fix/opencode-private-session-db
Aug 16, 2026
Merged

fix(opencode): private session DB per worker — the 'database is locked' root cause#34
usjoh merged 1 commit into
mainfrom
fix/opencode-private-session-db

Conversation

@usjoh

@usjoh usjoh commented Aug 16, 2026

Copy link
Copy Markdown
Owner

The real cure for the contention the 0-4s spawn jitter has been papering over since 2026-07-06. Fourth defect from the upstream audit (see #32, #33).

The bug

OpenCode keeps its session state at $XDG_DATA_HOME/opencode/opencode.dbone SQLite file, shared by every instance, opened for write and growing without bound. On this machine it stands at 145 MB. Fan several workers out at once and the losers of the lock race die before the model emits a token:

Error: Unexpected error
database is locked

Ringer can only record that as an ordinary FAIL. So the scoreboard reads it as the model failing when no model ever ran.

The part that makes this worth chasing: every non-Codex model routes through this engine, so the contention penalises exactly the cheap tier you fan out widest, and it worsens as parallelism rises. It reads as "the cheap lane can't cope." That's a surface misstating what happened — the failure class this repo treats as unforgivable, and the same class as the lou-call-transcript path-contract bug and the ANSI corruption in #33.

The jitter patch (f380521, bb3aa64) never fixed this. It only narrowed the window.

The fix

Point XDG_DATA_HOME at the per-run scratch root the wrapper already creates and tears down. Each sandboxed worker gets a private store.

Two load-bearing details:

  • Credentials live inside the relocated directory, so the private root is seeded with auth.json by copying, never symlinking — a link resolves straight back to the shared file this exists to avoid, and the profile denies writes outside SCRATCH. The copy is guarded, because set -euo pipefail would otherwise make a missing auth.json fatal on machines that authenticate another way.
  • --no-sandbox deliberately keeps the shared store. That path execs, so the scratch dir's EXIT trap would never fire and the root would leak on every full-access run. Now says so in a comment. Fine for a lone full-access task, not for a fan-out.

The jitter stays, demoted in its comment from cure to secondary net — it still spreads the startup burst of provider calls. It can be deleted once a wide fan-out is observed clean; that's a one-line follow-up, deliberately not bundled here so a regression can't be ambiguous.

Verification

Against the real binary, not just the script text:

$ opencode debug paths                      # without the wrapper
data       /Users/usjoh/.local/share/opencode

$ ./engines/opencode-sandboxed.sh <task> debug paths    # through the wrapper
data       /private/var/folders/.../ringer-opencode-scratch.zkjd12U36c/share/opencode

A sandboxed run exits 0 and leaves the shared DB's mtime unchanged.

New tests/test_opencode_wrapper.py drives the wrapper with a stub opencode on PATH and asserts what the worker actually receives, not what the script appears to say: a private data home, two workers never sharing one, credentials seeded by copy and not symlink, and a missing auth.json not aborting the run. macOS/sandbox-exec gated.

  • Full suite: 336 pass (332 baseline + 4)
  • Prove-fail: with the wrapper reverted and the tests kept, all four fail

Upstream: PR NateBJones-Projects#79 (unmerged). Its OC_BASE profile subpath is included — verified that sandbox-exec accepts a subpath param for a path that doesn't exist, so it's safe on machines without ~/.opencode.

🤖 Generated with Claude Code

OpenCode keeps its session state at $XDG_DATA_HOME/opencode/opencode.db — one
SQLite file, shared by every instance, opened for write and growing without
bound (145 MB on this machine). Fan several workers out at once and the losers
of the lock race die before the model emits a token:

    Error: Unexpected error
    database is locked

Ringer can only record that as an ordinary FAIL, so the scoreboard reads it as
the model failing when no model ever ran. Every non-Codex model routes through
this engine, so the penalty lands hardest on exactly the cheap tier that gets
fanned out widest, and worsens as parallelism rises. A surface misstating what
happened is the failure class this project treats as unforgivable.

Point XDG_DATA_HOME at the per-run scratch root the wrapper already creates and
tears down, giving each sandboxed worker a private store. Two load-bearing
details:

  - Credentials live inside the relocated directory, so the private root is
    seeded with auth.json by COPYING. A symlink would resolve straight back to
    the shared file this exists to avoid, and the profile denies writes outside
    SCRATCH. The copy is guarded: `set -euo pipefail` would otherwise make a
    missing auth.json fatal on machines that authenticate another way.
  - --no-sandbox deliberately keeps the shared store. That path `exec`s, so the
    scratch dir's EXIT trap would never fire and the root would leak on every
    full-access run. Now says so in a comment.

The 0-4s spawn jitter is kept but demoted in its comment from cure to secondary
net — it only ever narrowed the collision window, and it still spreads the
startup burst of provider calls. It can be deleted once a wide fan-out has been
observed clean.

Verified against the real binary, not just the script text: `opencode debug
paths` through the wrapper resolves `data` to the per-run scratch dir, and a
sandboxed run leaves the shared DB's mtime untouched. New tests drive the
wrapper with a stub opencode on PATH and assert what the worker actually
receives — private data home, two workers never sharing one, credentials
seeded by copy and not symlink, and a missing auth.json not aborting the run.

Matches upstream PR NateBJones-Projects#79, which is unmerged. Its OC_BASE profile subpath is
included; sandbox-exec accepts a subpath param for a path that does not exist,
so it is safe on machines without ~/.opencode.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@usjoh
usjoh merged commit 468725e into main Aug 16, 2026
2 of 3 checks passed
@usjoh
usjoh deleted the fix/opencode-private-session-db branch August 16, 2026 11:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant