You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
opencode run: concurrent sessions crash with SQLITE_BUSY (busy_timeout=0) #21215
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:
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 sessionsforiin$(seq 1 8);do
opencode run "Run: echo step1_$i && echo step2_$i && echo step3_$i && echo done_$i"&donewait# Some/all sessions will exit with 0 but produce incomplete output
Evidence
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 DESCLIMIT1;
-- Result: created=1775489146949, completed=NULL, tokens=NULL
Suggested 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.
Thanks for flagging the related issues. After reviewing:
database is locked #19521 — same symptom ("database is locked") from concurrent terminals. Our case is the headless opencode run variant of this, where the process exits silently with code 0 instead of surfacing the error.
This issue can be treated as a duplicate of #15188 with additional evidence: 8 concurrent opencode run sessions, reproducible across both Anthropic and OpenAI providers, and the specific busy_timeout=0 finding. Happy to close this in favour of #15188 if preferred — the key new data point is that a PRAGMA busy_timeout = 5000 one-liner would resolve the immediate crash-on-contention behaviour while the sharding work in #20935 is developed.
Tested headless opencode run with a trivial task (read a file, return the version string):
Version
Result
1.3.15
Completes in ~10s, exit 0
1.3.16
Completes in ~10s, exit 0
1.3.17
Hangs until killed (60s timeout, or indefinitely in worker dispatch)
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 init
The Cloudflare provider commit moved yield* dep.auth(input.id)before the env-var fast-bail:
// v1.3.16 — fast bail, no auth call for users without CloudflareconstaccountId=Env.get("CLOUDFLARE_ACCOUNT_ID")if(!accountId)return{autoload: false}// v1.3.17 — auth call runs for ALL users before env checkconstauth=yield*dep.auth(input.id)constaccountId=Env.get("CLOUDFLARE_ACCOUNT_ID")||(auth?.type==="api" ? auth.metadata?.accountId : undefined)
Both cloudflare-workers-ai and cloudflare-ai-gateway were also added to INTERNAL_PLUGINS, so they load for every user regardless of configuration. In headless mode (opencode run), if dep.auth() doesn't resolve cleanly for an unconfigured provider, it blocks provider initialization and the entire session hangs.
Environment
macOS (Apple Silicon)
Headless mode via opencode run -m anthropic/claude-sonnet-4-20250514 "..."
No Cloudflare env vars set (CLOUDFLARE_ACCOUNT_ID, CLOUDFLARE_API_KEY, etc.)
Affects both Anthropic and OpenAI model providers (provider init blocks before model selection)
Suggested fix
Restore the env-var fast-bail before dep.auth(), or skip auth for providers not configured in the user's settings:
// Check env and baseURL first — only call dep.auth() if there's reason toif(input.options?.baseURL)return{autoload: false}constaccountId=Env.get("CLOUDFLARE_ACCOUNT_ID")if(!accountId){constauth=yield*dep.auth(input.id)// ... check auth metadata}
Additional data point: 1.3.17 hangs with --pure flag too (which skips external plugins), confirming this is in OpenCode core — specifically the Cloudflare internal plugins added to INTERNAL_PLUGINS in #20399, which load regardless of --pure.
Test:
$ opencode run --pure -m anthropic/claude-sonnet-4-20250514 --dir ~/Git/aidevops "Reply with exactly: CANARY_OK"
> Build+ · claude-sonnet-4-20250514
# ← hangs here indefinitely, killed after 30s timeout
Same test with 1.3.16: completes in ~10s with exit 0.
Correction: root cause is SQLite contention, not Cloudflare provider init
Our earlier bisect was misleading. The actual root cause is concurrent headless opencode run instances sharing opencode.db with busy_timeout=0. When multiple workers write simultaneously, SQLITE_BUSY fires immediately and silently breaks the streaming connection handler — the worker hangs at step_start with zero API errors logged.
Evidence:
v1.3.17 with --pure hung → we blamed Cloudflare internal plugins
v1.3.15 and v1.3.16 also hung with concurrent workers → not version-specific
v1.3.17 completes immediately when given its own DB via XDG_DATA_HOME isolation
Interactive sessions (single instance, no contention) never had the issue
Our fix: set XDG_DATA_HOME per worker so each gets its own opencode.db, then merge session data back to the shared DB after completion.
Suggestion for opencode: consider setting busy_timeout to 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 current busy_timeout=0 means any lock collision is an immediate SQLITE_BUSY error, which appears to be swallowed somewhere in the streaming pipeline.
Apologies for the noise on the Cloudflare provider theory — the timing correlation was coincidental.
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
ERROR 2026-04-18T02:50:17 +5015ms service=session.processor sessionID=… messageID=… error=database is locked stack=SQLiteError: database is locked
The +5015ms is 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.log from 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 — bumping busy_timeout alone won't help if a large bash-tool output takes 8s to serialize into part.
I'd read this as "some code path opens without applying the default" — but the +5015ms log contradicts that, so it's more likely that PRAGMA busy_timeout is session-local (expected) and the failing connection did have 5000ms set. The 0 just reflects the CLI's own new connection, not opencode's runtime state. Leaving this here because if anyone else repros with sqlite3 they'll see the same 0 and draw the wrong conclusion.
11 opencode PIDs, 23 FDs on one DB file. A single TUI holds multiple FDs on opencode.db concurrently, 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 rows
part: 168,942 rows
Free pages: ~0% (VACUUM isn't the main win here — lock-hold time is)
User-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_BUSY in 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.
Weekly wal_checkpoint(TRUNCATE) + conditional VACUUM + PRAGMA optimize via 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.
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.