Repository navigation
Release v0.7.8: queue SQLite writes, upgrade libsql to 0.18 - #117
Merged
Merged
Conversation
libsql's local client hands its connection to each transaction and opens a new one for later statements without a busy timeout, so any write that overlapped an open transaction failed at once with SQLITE_BUSY. Parallel MCP writes hit this. Writes against a local SQLite file now run one at a time through a per-handle queue (transactions and non-SELECT statements; reads stay concurrent under WAL), and busy_timeout is re-applied to each replacement connection. A write through db inside db.transaction() now throws NestedWriteError instead of deadlocking. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
App and CLI both at 0.7.8. Ships the SQLite write queue: writes to each local database file (per workspace, in multi-tenant deployments) run one at a time, and busy_timeout is kept on the connections libsql opens after a transaction, so parallel MCP writes no longer fail with SQLITE_BUSY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ions 0.17.3 opened a new connection for every transaction and never closed it, leaving about two file handles open per transaction until garbage collection, and the new connections had no busy timeout. 0.18.0 pools connections: a transaction borrows one and returns it on commit or rollback (rolling back or closing one left mid-transaction), and the new `timeout` client option sets the busy timeout on every pooled connection. The busy timeout moves to that option and the PRAGMA re-apply workaround goes. The write queue stays: without it, a write overlapping a transaction busy-waits synchronously on another pooled connection, blocking the event loop the transaction needs (the parallel test took 41.7s and still failed). Adds a test that 100 transactions don't leave connections open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Parallel CodePlans MCP writes against a SQLite database failed with
SQLITE_BUSY. This was seen on the multi-tenant MindStaq instance, where each workspace has its own SQLite file. Tracked as the CodePlans work item "SQLite mode: concurrent MCP writes interleave on the shared libsql connection and silently lose rows". Releases as v0.7.8: app and CLI.Root cause
The work item blamed statements interleaving on one shared connection. That isn't what
@libsql/client0.17.3 does:client.transaction()handed its connection to the transaction and lazily opened a new connection for later statements.PRAGMA busy_timeout=5000only ever applied to the first one. So after the first transaction in a process, any write that overlapped an open transaction failed immediately.Change
lib/db/sqlite-writes.ts, new): oneWriteQueueper local SQLite file, so one per workspace database in multi-tenant deployments.db.transaction()and every statement that isn't a SELECT run one at a time.dbinsidedb.transaction()now throwsNestedWriteErrorinstead of deadlocking. None of the 10 existing transaction sites do this.@libsql/client0.17.3 → 0.18.0.timeoutoption sets the busy timeout on every pooled connection, so the PRAGMA workaround is gone.lib/db/index.ts: creates the queue forfile:and:memory:URLs and sendsdb.transaction()through it from the existing proxy. Postgres and hosted libsql (Turso) are unchanged.package.jsonandcli/package.json.Why the queue is still needed with 0.18
libsql runs statements synchronously. With a pool, a write that overlaps a transaction runs on another connection and busy-waits on the lock, blocking the event loop the transaction needs in order to commit. With the queue disabled on 0.18, the parallel test took 41.7 s and still failed with
SQLITE_BUSY.Alternatives considered
Switching to
better-sqlite3was set aside. Drizzle's better-sqlite3 driver requires synchronous transaction callbacks, and all of oursawait. It would also add a native build to deploys.Testing
tests/lib/db/concurrent-writes.test.tscovers:WriteQueueSQLITE_BUSYagainst the oldlib/db/index.ts.tsc --noEmitis clean (apart from stale.next/output).pnpm@10.33.0 install --frozen-lockfile: the only strict-build warning issharp, the same as on master.After merge
Tag
v0.7.8and publish the GitHub release; the Publish Release workflow also publishes the CLI.Not addressed
The work item's "returned an id, then the row vanished" symptom never reproduced.
🤖 Generated with Claude Code