Skip to content

Fatal "Turn execution failed" under concurrent processes: shared session DB hits "database is locked" (SQLITE_BUSY) with no busy_timeout or retry #162

Description

@mwotton

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

  1. Set PRAGMA busy_timeout (e.g. 5000–10000 ms) on the session-DB connections in the vendored runtime.
  2. 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.
  3. 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.)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions