Repository navigation
Description
Description
When multiple opencode run sessions execute concurrently (e.g., a CI/CD pipeline dispatching 8 workers simultaneously), sessions die silently mid-turn. The model initiates an API call (step_start event), but 0 tokens are generated and the process exits with code 0 — no error message, no retry.
Root Cause
The OpenCode session database (~/.local/share/opencode/opencode.db) uses WAL journal mode but has PRAGMA busy_timeout = 0:
$ sqlite3 ~/.local/share/opencode/opencode.db "PRAGMA busy_timeout"
0
WAL allows concurrent readers but only one writer at a time. With busy_timeout=0, any write that hits a lock from another session fails immediately with SQLITE_BUSY instead of retrying. When 8 concurrent opencode run sessions all write session state after tool calls, lock contention is constant and every session eventually hits SQLITE_BUSY.
Reproduction
# Launch 8 concurrent opencode run sessions
for i in $(seq 1 8); do
opencode run "Run: echo step1_$i && echo step2_$i && echo step3_$i && echo done_$i" &
done
wait
# Some/all sessions will exit with 0 but produce incomplete outputEvidence
- 8 workers dispatched in 3 minutes, all died mid-turn at random points (step 0, 7, 17, 20, 37)
- Both Anthropic (claude-sonnet-4.6) and OpenAI (gpt-5.3-codex) models affected — it's the DB, not the API
- Single worker tests pass every time — no contention
- DB stats: 17,550 sessions, 1.86M part rows, 11.4 GB — large DB amplifies lock hold times
Session DB entries show the failure clearly:
-- Last message started but never completed (0 tokens)
SELECT id, json_extract(data, '$.time.created'), json_extract(data, '$.time.completed'), json_extract(data, '$.tokens.total')
FROM message WHERE session_id = '<session>' ORDER BY rowid DESC LIMIT 1;
-- Result: created=1775489146949, completed=NULL, tokens=NULLSuggested Fix
Set PRAGMA busy_timeout = 5000 (5 seconds) on database connections. This gives concurrent writers time to retry instead of failing immediately. SQLite's WAL mode handles this well — the retry loop is built into the SQLite library, no application-level retry needed.
// In the database connection setup:
db.Exec("PRAGMA busy_timeout = 5000")Alternatively, use a per-session data directory for opencode run to eliminate contention entirely, similar to how --session already scopes conversation state.
Environment
- OpenCode v1.3.17 (also tested on 1.3.15, 1.3.16)
- macOS 15 (Darwin 25.4.0)
- SQLite WAL mode, 11.4 GB database
Activity
github-actions commentedon Apr 6, 2026
This issue might be a duplicate of existing issues. Please check:
opencodeprocessesbusy_timeoutas a companion fix)GH#17549: fix concurrent worker deaths — DB archival + dispatch stagger
GH#17549: fix concurrent worker deaths — DB archival + dispatch stagg…
marcusquinn commentedon Apr 6, 2026
Thanks for flagging the related issues. After reviewing:
opencode runvariant of this, where the process exits silently with code 0 instead of surfacing the error.PRAGMA busy_timeout = 0is the specific reason writes fail immediately rather than retrying.busy_timeoutfix suggested here is the minimal companion change that [FEATURE]: Per-session-tree database sharding to eliminate SQLite lock contention #20935 also mentions.This issue can be treated as a duplicate of #15188 with additional evidence: 8 concurrent
opencode runsessions, reproducible across both Anthropic and OpenAI providers, and the specificbusy_timeout=0finding. Happy to close this in favour of #15188 if preferred — the key new data point is that aPRAGMA busy_timeout = 5000one-liner would resolve the immediate crash-on-contention behaviour while the sharding work in #20935 is developed.marcusquinn commentedon Apr 6, 2026
Bisect result: v1.3.16 works, v1.3.17 hangs
Tested headless
opencode runwith a trivial task (read a file, return the version string):Only 4 commits between v1.3.16 and v1.3.17. Two are server-side (
zen:rate limiter changes), one is Windows-only TUI. The remaining client-side change is #20399 (Cloudflare provider env vars).Likely cause: unconditional
dep.auth()in provider initThe Cloudflare provider commit moved
yield* dep.auth(input.id)before the env-var fast-bail:Both
cloudflare-workers-aiandcloudflare-ai-gatewaywere also added toINTERNAL_PLUGINS, so they load for every user regardless of configuration. In headless mode (opencode run), ifdep.auth()doesn't resolve cleanly for an unconfigured provider, it blocks provider initialization and the entire session hangs.Environment
opencode run -m anthropic/claude-sonnet-4-20250514 "..."CLOUDFLARE_ACCOUNT_ID,CLOUDFLARE_API_KEY, etc.)Suggested fix
Restore the env-var fast-bail before
dep.auth(), or skip auth for providers not configured in the user's settings:marcusquinn commentedon Apr 6, 2026
Additional data point: 1.3.17 hangs with
--pureflag too (which skips external plugins), confirming this is in OpenCode core — specifically the Cloudflare internal plugins added toINTERNAL_PLUGINSin #20399, which load regardless of--pure.Test:
Same test with 1.3.16: completes in ~10s with exit 0.
marcusquinn commentedon Apr 6, 2026
Correction: root cause is SQLite contention, not Cloudflare provider init
Our earlier bisect was misleading. The actual root cause is concurrent headless
opencode runinstances sharingopencode.dbwithbusy_timeout=0. When multiple workers write simultaneously,SQLITE_BUSYfires immediately and silently breaks the streaming connection handler — the worker hangs atstep_startwith zero API errors logged.Evidence:
--purehung → we blamed Cloudflare internal pluginsXDG_DATA_HOMEisolationOur fix: set
XDG_DATA_HOMEper worker so each gets its ownopencode.db, then merge session data back to the shared DB after completion.Suggestion for opencode: consider setting
busy_timeoutto a non-zero value (e.g. 5000ms) in the SQLite connection. This would let concurrent instances coexist on a shared DB without silent failures. The currentbusy_timeout=0means any lock collision is an immediateSQLITE_BUSYerror, which appears to be swallowed somewhere in the streaming pipeline.Apologies for the noise on the Cloudflare provider theory — the timing correlation was coincidental.
t2174: feat(opencode): weekly DB maintenance routine (r913) for SQLit…
marcusquinn commentedon Apr 18, 2026
Still reproducing on v1.4.11 — and the live log contradicts the obvious "busy_timeout was 0" diagnosis.
I filed this originally against
opencode run(headless), but it's now surfacing in interactive TUI sessions too. Wanted to share fresh evidence in case it helps pin down the exact path.The smoking gun
The
+5015msis the critical bit:busy_timeout = 5000msis being honored on the failing connection — the writer just held the WAL lock for longer than 5s. A single~/.local/share/opencode/log/2026-04-17T234221.logfrom my install has 28 of these in one session. So this is a writer-duration problem, not a "timeout not set" problem. That reframes the fix — bumpingbusy_timeoutalone won't help if a large bash-tool output takes 8s to serialize intopart.What a fresh CLI connection actually shows
I'd read this as "some code path opens without applying the default" — but the
+5015mslog contradicts that, so it's more likely thatPRAGMA busy_timeoutis session-local (expected) and the failing connection did have 5000ms set. The0just reflects the CLI's own new connection, not opencode's runtime state. Leaving this here because if anyone else repros withsqlite3they'll see the same0and draw the wrong conclusion.A single interactive TUI is already multi-writer
11 opencode PIDs, 23 FDs on one DB file. A single TUI holds multiple FDs on
opencode.dbconcurrently, so even with only one "user-visible session", the app is competing against itself for the single WAL writer slot. Any 9-13 pane workload amplifies this. I suspect this is why the per-session-tree sharding idea in #20935 and PR #21579 would help even for solo users.Current DB profile (context for anyone repro'ing)
opencode.db: 1.3 GB (1,314,107,392 bytes)message: 60,123 rowspart: 168,942 rowsUser-visible failure mode (the biggest quality-of-life ask)
When a lock fires, the session halts with the error banner and there's no automatic retry. I have to reload the thread and sometimes lose the in-flight tool call. An exponential-backoff retry on
SQLITE_BUSYin the write path (50ms, 100ms, 250ms, 500ms, 1s → give up) would make almost all of these invisible without changing the architecture. That would buy time while the larger sharding work in #20935/#21579 lands.Related upstream threads
What I'm shipping on my side (mitigation only)
Weekly
wal_checkpoint(TRUNCATE)+ conditionalVACUUM+PRAGMA optimizevia a maintenance routine, scheduled off-hours. Doesn't touch busy_timeout or retry — those can only be fixed in opencode itself. Code: marcusquinn/aidevops#19630 if useful as a reference for what external tooling can and can't do.Happy to supply a full log sample or run instrumented builds if it would help.
aidevops.sh v3.8.68 plugin for OpenCode v1.4.11 with claude-opus-4-7 spent 3h 30m and 5,001 tokens on this as a headless worker. Overall, 11d 10h since this issue was created.
t2174: feat(opencode): weekly DB maintenance routine (r913) for SQLit…
t2174: feat(opencode): weekly DB maintenance routine (r913) for SQLit…
hardwork: fix restart resilience and parallel agent hangs
github-actions commentedon Jul 6, 2026
To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.