Conversation
Every mcode process opens the same runtime-state.sqlite in WAL mode, so a concurrent process holding the single write lock made a foreign writer's SQLITE_BUSY surface as "database is locked" and aborted the Turn. PR #287 covered only the v2 message-upsert path; turn admission, session state projection, settlement, queue writes and the v1 Goal store still failed after the five-second native busy timeout. Move the retry policy into @mavis/shared/sqlite-write-retry and apply it to every turn-critical write. Each attempt takes only a 50ms native stall and the rest of the ten-second budget is asynchronous backoff, so a foreign writer can no longer block the event loop for the whole budget. Only lock acquisition is replayed: a callback that already started rolls back and propagates unchanged. Reads keep their synchronous path because WAL readers are not blocked by a foreign writer. Cron orchestration stays on the old policy - its scheduler handler is synchronous end to end and a failed persist is already reported as CRON_PERSIST_FAILED. Related to #282.
3 tasks
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.
Problem
Every
mcodeprocess opens the sameruntime-state.sqlitein WAL mode, whichallows a single writer at a time. A concurrent process holding that write lock
longer than the five-second native
busy_timeoutsurfacedSQLITE_BUSY("database is locked") straight into the Turn, aborting it.
#287 added bounded-stall retry admission, but scoped it to the v2 message-upsert
path and stated that other SQLite writers keep their existing policy. The paths
named in #282 — turn admission, the event/session writer and the goal repo —
were still unprotected.
Reproduction
test/sqlite-turn-contention.test.tsforks a second process that holdsBEGIN IMMEDIATEon the real database for 8 seconds, then drives the realDatabaseClient. Onmain, turn admission, session agent-state projection andturn settlement all rejected with
SQLITE_BUSY; only message upsert survived.Change
Move the retry policy into
@mavis/shared/sqlite-write-retryand apply it toevery turn-critical write:
deletion, steering receipts, restart recovery
runInImmediateTransactionEach attempt takes only a 50ms native stall; the rest of the ten-second budget
is asynchronous backoff. better-sqlite3 blocks the Node event loop for the whole
native wait, so raising
busy_timeoutinstead would only move the TUI freezefrom five seconds to thirty.
Only lock acquisition is replayed. A callback that already started rolls back
and propagates its own error unchanged, which keeps the fail-closed guarantees
#287 established for cancellation. A genuinely abandoned wait still reports as
WriteLockWaitAbortedError, now via a dedicated marker so a callback error thatmerely happens to equal the signal's abort reason is not reclassified.
Reads keep their synchronous path: WAL readers are not blocked by a foreign
writer. Cron orchestration is also unchanged — its scheduler handler is
synchronous end to end, and a failed persist already reports
CRON_PERSIST_FAILED.Validation
pnpm verify: 13 of 14 gates pass on the committed tree (macOS arm64,Node v26.7.0), including typecheck, build, standalone boundary, source
inventory, export preview and
test:byok.test:sandboxfails, but it fails identically on the base commit44b13d3(3 failures there vs 2 here): the
rmprobes are routed through the desktoprecoverable-delete launcher, which the sandbox denies. Unrelated to this change.
driving real better-sqlite3 against a foreign lock holder.
and no-replay cases.
Fixes #282
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.