Skip to content

fix: share one SQLite connection across KV stores on same path (Bug #5)#53

Open
avrabe wants to merge 1 commit intomainfrom
fix/dedup-db-shared-connection
Open

fix: share one SQLite connection across KV stores on same path (Bug #5)#53
avrabe wants to merge 1 commit intomainfrom
fix/dedup-db-shared-connection

Conversation

@avrabe
Copy link
Copy Markdown
Contributor

@avrabe avrabe commented May 2, 2026

Summary

  • `initKVStore` now caches `Database` connections by absolute file path with ref-counted close. Two stores against the same file (e.g. `webhook_dedup` and `ai_rate_limits` both in `dedup.db`) share one connection.
  • Closes Bug fix: adjust branch protection for solo developer workflow #5 from `docs/agent-fleet/bugs.md`.

Why

Two writer connections against the same SQLite file can SQLITE_BUSY each other under WAL. When that happens mid-handler, `markProcessed` gets skipped → the delivery is reprocessed → the AI review runs twice.

Behaviour

Path Before After
Two file-backed stores on same path Two Database instances, race-prone One shared Database
Two `:memory:` stores Two Databases (each `:memory:` is its own DB) Unchanged
close() with other stores still active Closes the shared db, breaks the other Decrements ref count, keeps db open

Test plan

  • 4 new tests in `tests/unit/persistent-kv.test.js`
  • `npm test` → 838 pass (was 834)
  • `npm run lint` → clean

Risk

Low. Module-level cache scoped to this file. Public API unchanged.

🤖 Generated with Claude Code

Why: QA bug-hunter flagged that app.js calls initKVStore twice against
the same dedup.db file — once for webhook idempotency, once for AI
rate limits. WAL tolerates multi-connection reads but two writers can
SQLITE_BUSY each other. When markProcessed and recordReview race, one
throws → the handler crashes mid-flight → markProcessed is skipped →
the same delivery is reprocessed and the AI review runs twice.

What:
  - Add a module-level Map cache in persistent-kv.js keyed by absolute
    dbPath. The second initKVStore call against the same file returns
    a store that wraps the same Database instance.
  - Ref-counted close(): the Database is only really closed when ALL
    stores against that path have called close(). Tests that rely on
    closing one store while another is still active now work
    correctly.
  - `:memory:` stays per-call. Each `:memory:` open is its own DB
    by SQLite design; caching the literal string would produce
    cross-store contamination in tests that use it.

Test plan:
  - 4 new tests in __tests__/unit/persistent-kv.test.js:
    - identity check that two stores share one Database
    - mid-life close of one store leaves the other usable
    - last close actually tears down the connection
    - clean reopen-after-full-close round-trip
  - npm test → 838 pass (was 834)
  - npm run lint → clean

Risk: low — narrow, internal change. The previous test ('persists
across separate stores on the same shared db (in-memory check)') still
passes because :memory: paths bypass the cache.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

1 participant