Skip to content

fix(workspace-feeds): surface real sqlite errors and skip no-op boot writes - #15

Open
dshipper wants to merge 1 commit into
mainfrom
fix/workspace-feeds-boot-write
Open

fix(workspace-feeds): surface real sqlite errors and skip no-op boot writes#15
dshipper wants to merge 1 commit into
mainfrom
fix/workspace-feeds-boot-write

Conversation

@dshipper

Copy link
Copy Markdown
Collaborator

Summary

  • SqliteWorkspaceFeedRepository.setFeedIds used a hand-rolled BEGIN IMMEDIATE / COMMIT / ROLLBACK. When a statement inside the transaction fails with an error SQLite auto-rolls-back on (SQLITE_BUSY, SQLITE_FULL, SQLITE_IOERR), the explicit ROLLBACK in the catch throws cannot rollback - no transaction is active and masks the real error. That is the line logged 4× during the 2026-08-29 tend-live restart. Switched to db.transaction(...).immediate(), matching the other two transaction sites in server/sqlite.ts; Bun's wrapper only issues ROLLBACK while db.inTransaction is true.
  • MirroredWorkspaceFeedRepository.init rewrote workspace_feeds (sqlite) and workspace.json (mirror) on every boot, and every CLI command boots the same store — so even a read-only state call took a write lock, and a busy database made boot fatal instead of harmless. It now only writes when the two copies actually disagree.
  • SqliteWorkspaceFeedRepository is exported so the test can drive it on a connection with the default busy_timeout of 0 (the store's own connection waits 5 s).

Diagnosis correction

The initial hypothesis was that BEGIN IMMEDIATE itself failing got masked. It does not: BEGIN sits outside the try, and the red-check confirmed a BEGIN lock conflict already surfaced SQLITE_BUSY on the old code. The masking happens when a statement inside the transaction fails after SQLite has already rolled back — the live stack is bun:sqlite runsqlite.ts:987 (the ROLLBACK in the catch) ← setFeedIdsworkspaceFeeds.ts:67. Because the inner error was swallowed, the specific trigger (most likely a lock conflict with the previous api's PRAGMA wal_checkpoint(TRUNCATE) on close, or a concurrent bun run cli.ts) is inferred rather than observed; the log has zero SQLITE_BUSY / locked lines. Either way, fix 2 removes the write entirely on a steady-state boot, so this path no longer runs in the common case, and fix 1 means that if it does fail we will see the real error next time.

Tests — test/workspace-feeds.test.ts (temp dirs only, never touches the workbench)

  • injected post-rollback failure surfaces SQLITE_BUSY with its own message, leaves the connection out of a transaction, and leaves the rows intact
  • BEGIN IMMEDIATE lock conflict surfaces SQLITE_BUSY and the repository recovers once the lock clears
  • a consistent boot performs zero writes (recording wrapper on both repositories; created_at sentinel unchanged; mirror untouched)
  • disagreeing copies still merge into both

The first and third fail on main and pass here (verified by stashing server/ and re-running).

Verification

  • bun test — 132 pass / 1 skip (Supabase e2e, env-gated) / 0 fail
  • bunx tsc --noEmit — clean
  • bunx oxlint . — clean
  • Live (tend-live, pid 26373) was not restarted; it is still running main. Restart when you want this live.

Branch lives in the .worktrees/fix-workspace-feeds-boot-write worktree; the main checkout stayed on feat/ffo-editions.

🤖 Generated with Claude Code

…writes

Tend boots logged `SQLiteError: cannot rollback - no transaction is active`
from SqliteWorkspaceFeedRepository.setFeedIds (4x in one tend-live restart)
and the api only came up on a later attempt. Two problems stacked:

1. setFeedIds rolled back by hand inside a catch. When a statement inside
   the transaction fails with an error SQLite auto-rolls-back on (BUSY,
   FULL, IOERR), the explicit ROLLBACK then throws and masks the real
   error. Use db.transaction(...).immediate() like the rest of sqlite.ts;
   it only issues ROLLBACK while a transaction is still open.

2. MirroredWorkspaceFeedRepository.init unconditionally rewrote
   workspace_feeds in sqlite and the json mirror on every boot, and every
   CLI command boots the same store, so a read-only `state` call took a
   write lock and a busy database made boot fatal. Only write when the
   two copies actually disagree.

Tests cover the masked-error path (injected post-rollback failure), a
BEGIN IMMEDIATE lock conflict, a no-op consistent boot (created_at and
mirror untouched), and a real merge when the copies disagree.

Co-Authored-By: Claude <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