Summary
Under concurrent zcode processes (a fleet of headless agents plus interactive TUI sessions on one host), turns die fatally with Error: Turn execution failed (traceId: …). The CLI log always shows the same cause under that wrapper — the shared session database reporting SQLITE_BUSY:
{"timestamp":"2026-09-18T06:43:14.165Z","level":"error","event":"turn.failed","module":"core.runtime",
"error":{"message":"Turn execution failed","code":"UNKNOWN_ERROR","type":"unknown_error",
"context":{"errorPayloadRole":"wrapper"},
"cause":{"name":"Error","message":"database is locked","code":"ERR_SQLITE_ERROR"}}}
The installed runtime bundle (vendor/zcode.cjs in zcode-app-cli 3.11.2-25) sets no busy_timeout pragma at all (grepped the bundle: zero hits), so session-DB writes run on the driver default with no backoff and no retry. WAL mode lets readers proceed, but writers still serialize; when a write at a turn boundary (turnPhase: "processing_input") waits past the timeout, the whole turn fails — and a headless agent exits mid-task with nothing written.
Environment
zcode-app-cli 3.11.2-25, zcode-runtime 0.16.5, Linux x64 (npm)
- Shared db:
~/.zcode/cli/db/db.sqlite (~200 MB, -wal/-shm sidecars present)
Reproduction / observed scale
One host running ~10–15 concurrent zcode processes (headless CI-style agents + interactive TUIs), all writing to the single shared session DB:
- 28 distinct sessions hit by a fatal
turn.failed with cause database is locked in 3 days, interactive and headless alike
- ~3× as many lock errors are absorbed at lower layers (
usage.tool.write.failed ×69, usage.turn.write.failed ×4, usage.model.write.failed ×2, checkpoint.persist.failed, session_input.admit_failed) — the contention is constant; only writes landing at the turn boundary are fatal
- Failures come in bursts (e.g. 7–8 fatal turns across the 02:00–05:59 UTC hours) — one lock pileup kills several sessions at once
- Wall-time-to-death varies 4–18 min because the agent dies at whichever turn first tries to persist while another writer holds the lock
You can check any install with: grep -c 'database is locked' ~/.zcode/cli/log/zcode-*.jsonl
Suggested directions
- Set
PRAGMA busy_timeout (e.g. 5000–10000 ms) on the session-DB connections in the vendored runtime.
- Treat
SQLITE_BUSY at the persistence layer as retryable with short backoff instead of surfacing it as a fatal wrapped UNKNOWN_ERROR that kills the turn.
- Longer term: shard the session DB per project/session so fleet-style concurrency doesn't serialize every process on one file.
Happy to share more log evidence or test a build. (Related but distinct: #160 / #161 cover the provider-registry migration fallout, not this.)
Summary
Under concurrent zcode processes (a fleet of headless agents plus interactive TUI sessions on one host), turns die fatally with
Error: Turn execution failed (traceId: …). The CLI log always shows the same cause under that wrapper — the shared session database reportingSQLITE_BUSY:{"timestamp":"2026-09-18T06:43:14.165Z","level":"error","event":"turn.failed","module":"core.runtime", "error":{"message":"Turn execution failed","code":"UNKNOWN_ERROR","type":"unknown_error", "context":{"errorPayloadRole":"wrapper"}, "cause":{"name":"Error","message":"database is locked","code":"ERR_SQLITE_ERROR"}}}The installed runtime bundle (
vendor/zcode.cjsinzcode-app-cli 3.11.2-25) sets nobusy_timeoutpragma at all (grepped the bundle: zero hits), so session-DB writes run on the driver default with no backoff and no retry. WAL mode lets readers proceed, but writers still serialize; when a write at a turn boundary (turnPhase: "processing_input") waits past the timeout, the whole turn fails — and a headless agent exits mid-task with nothing written.Environment
zcode-app-cli 3.11.2-25,zcode-runtime 0.16.5, Linux x64 (npm)~/.zcode/cli/db/db.sqlite(~200 MB,-wal/-shmsidecars present)Reproduction / observed scale
One host running ~10–15 concurrent zcode processes (headless CI-style agents + interactive TUIs), all writing to the single shared session DB:
turn.failedwith causedatabase is lockedin 3 days, interactive and headless alikeusage.tool.write.failed×69,usage.turn.write.failed×4,usage.model.write.failed×2,checkpoint.persist.failed,session_input.admit_failed) — the contention is constant; only writes landing at the turn boundary are fatalYou can check any install with:
grep -c 'database is locked' ~/.zcode/cli/log/zcode-*.jsonlSuggested directions
PRAGMA busy_timeout(e.g. 5000–10000 ms) on the session-DB connections in the vendored runtime.SQLITE_BUSYat the persistence layer as retryable with short backoff instead of surfacing it as a fatal wrappedUNKNOWN_ERRORthat kills the turn.Happy to share more log evidence or test a build. (Related but distinct: #160 / #161 cover the provider-registry migration fallout, not this.)