Skip to content

fix(daemon): Retry a busy store instead of failing live sessions - #126

Merged
4ndreello merged 1 commit into
mainfrom
claude/busy-einstein-2n3ho9
Sep 25, 2026
Merged

4ndreello merged 1 commit into
mainfrom
claude/busy-einstein-2n3ho9

Conversation

@4ndreello

Copy link
Copy Markdown
Owner

Problem

attachDriverEvents persisted each event in a deferred BEGIN, and events.append reads (the source_key dedupe and MAX(sequence)) before it inserts. If another connection commits in between, the read-to-write upgrade fails at once with SQLITE_BUSY_SNAPSHOT (errcode 517), which busy_timeout never retries.

The catch treated that as a harness failure. It wrote session.failed (UNKNOWN, harness blame), marked the row failed and left the loop, while the harness kept running. Its later events and usage were never persisted, and wait returned a false terminal state.

#125 removed the main source of concurrent writers (stray daemons). This PR makes the loop safe against any other writer, such as usage backfill or a lock held for longer than 5s.

Change

  • persistDriverEvent commits the event, its cursor and the status update with BEGIN IMMEDIATE. The write lock is taken before the read, so busy_timeout applies again.
  • On SQLITE_BUSY (base errcode 5, 517 included) it rolls back and retries the same event with capped backoff (25ms doubling, max 2s) instead of ending the loop.
    • There is no retry limit while the daemon runs: the harness is alive and its output is on disk.
    • The first busy retry per event is logged to daemon.log.
    • On shutdown it returns and lets the drain set the outcome.
  • Non-busy persistence errors still fail the session.
  • isStoreBusy() and a STORE_BUSY failure code: classifyFailure("database is locked") now returns infra blame, not UNKNOWN with harness blame.
  • Spec in .specs/features/store-busy-healing/spec.md.

Validation

tests/daemon-store-busy.test.ts (4 tests), written first and seen red:

  • A second DatabaseSync commits between the loop's read and write, the real interleaving. Before the fix, the event log held only session.failed. After it, the session ends completed with every event stored.
  • Busy appends (5, then 517) are retried and no session.failed is written.
  • A non-busy error still fails the session.
  • Classification.

Full suite in a Node 22 container: 1634 pass, 1 fail. The failure (open-claude-probes "probes again when the binary size changes") also fails on main.

Follow-up

A reconciler that reattaches sessions already marked failed by a store error while their pid is alive, from the persisted offsets.

🤖 Generated with Claude Code

https://claude.ai/code/session_01R6o1wJTTJWcHm9mZMGvDTn


Generated by Claude Code

The event loop persisted each event in a deferred BEGIN that read before
it wrote. A commit from another connection in between made the upgrade
fail at once with SQLITE_BUSY_SNAPSHOT, which busy_timeout never
retries. The catch then marked the session failed and left the loop
while the harness kept running, so later events and usage were lost.

Persist with BEGIN IMMEDIATE, and on any SQLITE_BUSY retry the same
event with capped backoff instead of ending the loop. Other persistence
errors still fail the session. classifyFailure now maps "database is
locked" to STORE_BUSY with infra blame instead of UNKNOWN harness.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01R6o1wJTTJWcHm9mZMGvDTn
@sonarqubecloud

Copy link
Copy Markdown

@4ndreello
4ndreello merged commit 386e3af into main Sep 25, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants