Skip to content

fix(db): retry transient SQLITE_IOERR instead of failing the caller - #1030

Open
halindrome wants to merge 1 commit into
mksglu:nextfrom
halindrome:fix/sqlite-ioerr-retry
Open

fix(db): retry transient SQLITE_IOERR instead of failing the caller#1030
halindrome wants to merge 1 commit into
mksglu:nextfrom
halindrome:fix/sqlite-ioerr-retry

Conversation

@halindrome

Copy link
Copy Markdown
Contributor

Problem

withRetry in src/db-base.ts only treats SQLITE_BUSY / database is locked as retryable and rethrows everything else on the first attempt. SQLITE_IOERR falls into a gap:

  • no retries — it fails the caller immediately, and
  • isSQLiteCorruptionError() does not match it either, so no recovery path runs.

A single transient I/O blip — fs pressure, Spotlight/iCloud touching the DB mid-write, a WAL hiccup — therefore hard-fails the write. Observed in the field as ctx_fetch_and_index aborting a page index with disk I/O error, after which the session stops using context-mode tools entirely. The DB itself is healthy (PRAGMA integrity_check returns ok); only the one write failed.

Change

  • Add SQLITE_IOERR / disk I/O error to a named TRANSIENT_ERROR_PATTERNS list so they get the same 3-attempt exponential backoff as SQLITE_BUSY.
  • Derive the retry-exhaustion message's error class from the actual failure instead of hardcoding SQLITE_BUSY — reporting "database is locked" for an I/O failure sends the caller after the wrong root cause.

Deliberately NOT added to isSQLiteCorruptionError(): that path renames or deletes the DB file, which would destroy a healthy knowledge base over a momentary stall. Retry here; recover there.

Tests

tests/store.test.ts gains 50 lines covering: SQLITE_IOERR retried and succeeding on a later attempt, retry exhaustion reporting the I/O error class rather than SQLITE_BUSY, and non-transient errors still rethrowing on the first attempt.

Verification

  • npm run typecheck — clean
  • npm test — 4715 passed, 28 skipped, 3 failed
  • tests/store.test.ts — 127/127 pass

The 3 failures (tests/session-hooks-smoke.test.ts ×2, tests/hooks/claude-stop.test.ts ×1) reproduce identically on unmodified next on this machine, so they are pre-existing and environmental, not introduced here.

Bundles are left untouched — .github/workflows/bundle.yml regenerates and commits them.

withRetry only treated SQLITE_BUSY / "database is locked" as retryable,
rethrowing everything else on the first attempt. SQLITE_IOERR therefore
fell into a gap: no retries here, and isSQLiteCorruptionError() does not
match it either, so no recovery ran. A single transient I/O blip — fs
pressure, Spotlight/iCloud touching the DB mid-write, a WAL hiccup —
hard-failed the write, e.g. ctx_fetch_and_index aborting a page index
with "disk I/O error".

Add SQLITE_IOERR / "disk I/O error" to a named TRANSIENT_ERROR_PATTERNS
list so they get the same 3-attempt backoff as BUSY.

Deliberately NOT added to isSQLiteCorruptionError(): that path renames or
deletes the DB file, which would destroy a healthy knowledge base over a
momentary stall. Retry here; recover there.

Also derive the exhaustion message's error class from the actual failure
rather than hardcoding SQLITE_BUSY — reporting "database is locked" for
an I/O failure sends the caller after the wrong root cause.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Rm5HmMLjj3ZjvA1iRkjZRs
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