Skip to content

fix: prevent SQLite B-tree corruption with WAL tuning and batched agent logs - #30

Merged
gsxdsm merged 17 commits into
Runfusion:mainfrom
timothyjlaurent:fix/sqlite-corruption-wal-pragmas
May 5, 2026
Merged

fix: prevent SQLite B-tree corruption with WAL tuning and batched agent logs#30
gsxdsm merged 17 commits into
Runfusion:mainfrom
timothyjlaurent:fix/sqlite-corruption-wal-pragmas

Conversation

@timothyjlaurent

@timothyjlaurent timothyjlaurent commented May 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes recurring SQLite B-tree corruption in fusion.db caused by extreme WAL (Write-Ahead Logging) pressure from unbatched writes to the agentLogEntries table.

Three commits:

  1. 0a09c68a — WAL PRAGMA tuning + write-behind agent log buffer + startup integrity check
  2. 78ff1be1 — Review-driven fixes: buffer exception safety, integrity check ordering before migrations
  3. 1976297b — Include dbPath in corruption log messages for multi-instance operators

Root Cause

The agentLogEntries table was the corruption hotspot. Every agent log entry was written as an individual auto-committed INSERT — during normal operation, 29K+ rows accumulated, each triggering its own WAL write and (with synchronous=FULL) an fsync. Combined with the default 1000-page autocheckpoint interval, the WAL file grew to hundreds of MB. Eventually B-tree pages corrupted, causing SQLITE_CORRUPT errors on reads.

The fix addresses all three contributing factors: WAL growth rate, checkpoint frequency, and write amplification.


Changes by File

packages/core/src/db.ts — WAL PRAGMAs + Integrity Check

WAL tuning (constructor, disk DBs only):

PRAGMA Value Rationale
synchronous NORMAL Safe in WAL mode (WAL itself provides durability). Eliminates fsync per commit — the biggest write amplification source.
wal_autocheckpoint 100 Checkpoints every 100 pages instead of 1000. Keeps WAL files small (~400 KB vs multi-GB). Upstream had adopted synchronous=NORMAL and journal_size_limit=4MB but kept autocheckpoint at 1000; we lower it to 100.
journal_size_limit 4194304 (4 MB) Caps WAL file size. Prevents runaway growth even under heavy write load. Already present upstream.

All three PRAGMAs are skipped for in-memory databases (no WAL to tune).

Startup integrity check (init() method):

  • Runs PRAGMA integrity_check before migrate() and schema operations. Running migrations against a corrupted database would make corruption worse by writing new pages into a damaged B-tree.
  • On corruption detection: attempts PRAGMA wal_checkpoint(TRUNCATE) to force a full WAL checkpoint, then rechecks integrity. If the checkpoint resolves it, logs recovery success. If not, logs a manual recovery command with the DB path.
  • Sets corruptionDetected flag for upstream's health-check mechanisms.
  • All log messages include this.dbPath so operators can identify which database is affected when multiple instances run.

packages/core/src/store.ts — Write-Behind Agent Log Buffer

Problem: Each appendAgentLog() call was a standalone INSERT INTO agentLogEntries — auto-committed, hitting WAL individually.

Solution: A write-behind buffer that batches up to 50 entries (or flushes every 2 seconds), then writes them all in a single transaction.

Key design decisions:

  • Swap-after-commit safety: The buffer is slice()d before the transaction starts. Only after the transaction commits successfully does splice(0, flushCount) remove the entries. If the transaction fails, the buffer is untouched and will retry on the next flush cycle.
  • Immediate event emission: Even though writes are buffered, this.emit("agentLog", ...) fires immediately so UI and consumers see log entries in real-time.
  • Flush triggers: Buffer flushes before reads (getAgentLogs, getAgentLogCount), before deleteTask, and on close() — ensuring queries always see complete data.
  • Timer .unref(): The flush timer calls .unref() so it doesn't prevent the Node process from exiting naturally.
  • Error tolerance: FK violations on taskId are caught and skipped (stale buffer entries after task deletion).
  • Uses upstream normalizedDetail: Buffer pushes sanitized detail via the upstream normalizedDetail method rather than raw user input.

packages/core/src/__tests__/db.test.ts — 6 new tests

  • Asserts all three WAL PRAGMA values on a fresh disk-backed database
  • Verifies in-memory databases skip WAL PRAGMAs
  • Verifies startup integrity check runs and handles corruption

packages/core/src/__tests__/store.test.ts — 8 new tests

  • Buffer fills to capacity and flushes
  • Flush-on-read (getAgentLogs triggers flush)
  • Flush-on-count (getAgentLogCount triggers flush)
  • Flush-before-delete
  • Flush-on-close persists to disk
  • FK violation tolerance (stale entries don't crash flush)
  • Immediate event emission despite buffering
  • Multi-task log entries batched correctly

Why wal_autocheckpoint=100 (not 1000)?

The default 1000 pages means the WAL can grow to ~4 MB before checkpointing starts. Under heavy write load (agent log bursts), this window is large enough for corruption to take hold before a checkpoint consolidates pages. At 100 pages (~400 KB), checkpoints happen frequently enough to keep the WAL small, but not so frequently that they create I/O contention. This is the single value upstream has not adopted — their v0.16.0 kept 1000.


Test plan

  • 6 new tests for PRAGMA settings (wal_autocheckpoint, journal_size_limit, synchronous, busy_timeout, in-memory skip, fresh DB init)
  • 8 new tests for agent log buffering (buffer fill, flush-on-read, flush-on-count, flush-before-delete, flush-on-close with disk persistence, FK violation tolerance, immediate event emission, multi-task batching)
  • All 811 tests in db.test.ts + store.test.ts pass
  • Rebased onto origin/main (v0.16.0 upstream with integrityCheck() and recoverDatabase())
  • Manual: run Fusion with a high-volume task and verify no corruption after 24h

Refs: #24

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Startup integrity checks with automatic recovery attempts and clearer failure guidance on unrecoverable DB corruption.
    • Database path exposed and clearer in-memory DB behavior; WAL startup tuning adjusted (more frequent auto-checkpoints).
  • New Behavior

    • Buffered, batched agent-log persistence with immediate event emission, auto-flush rules, and best-effort flush on shutdown.
  • Tests

    • Expanded coverage for init idempotency, WAL tuning/timeouts, agent-log buffering/flush behaviors, and startup integrity.
  • Chores

    • CI now builds plugins during PR checks.

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Database initialization adds a pre-write integrity check with WAL checkpoint recovery on corruption and exposes the DB path; SQLite WAL auto-checkpoint tuning reduced to 100. Store now buffers agent-log writes with timed/batch flush, ensuring read-after-write consistency and flushing on task deletes/close. Tests and CI/workflow updates exercise these behaviors.

Changes

Agent Log Buffering (store.ts + tests)

Layer / File(s) Summary
Buffer data & config
packages/core/src/store.ts
Introduces agentLogBuffer, flush timer, constants: buffer flush size 50, flush interval 2000ms, hard backlog cap 5000.
Core buffering & flush logic
packages/core/src/store.ts
appendAgentLog() buffers entries (truncates detail), emits agent:log immediately, drops oldest on hard cap, triggers flush at capacity or via timer; adds flushAgentLogBuffer() which snapshots buffer, inserts only entries whose taskId still exists within a single transaction, bumps LastModified, drains flushed slice, and re-queues valid entries on transaction failure.
Integration / wiring
packages/core/src/store.ts
Flushes before getAgentLogs(), getAgentLogCount(), getAgentLogsByTimeRange(), and within deleteTask() while holding the task lock; appendAgentLogBatch() flushes pending single-entry buffered logs first; close() best-effort flushes remaining buffer and catches/logs errors. DB/Archive getters and init() wrap DB init with try/catch to close new connections on init failure.
Tests / Validation
packages/core/src/__tests__/store.test.ts
Adds describe("agent log buffering") tests: flush-on-capacity, auto-flush on reads/counts, flush-before-delete for cascade correctness, close-time flush behavior (including disk-backed persistence), non-throwing close when flushing for deleted tasks, immediate agent:log events, and interleaved-task buffering correctness. Existing tests updated to call (store as any).flushAgentLogBuffer() where needed.

Database startup, integrity, and WAL tuning (db.ts + tests)

Layer / File(s) Summary
Configuration / PRAGMAs
packages/core/src/db.ts, packages/core/src/__tests__/db.test.ts
Disk-backed WAL tuning: PRAGMA wal_autocheckpoint changed from 1000 to 100. Tests updated/added to assert WAL-related PRAGMAs (wal_autocheckpoint=100, journal_size_limit=4194304, synchronous=1, busy_timeout=5000) and that in-memory DB uses journal_mode = "memory".
Core startup & recovery
packages/core/src/db.ts
init() now runs PRAGMA integrity_check before schema writes; on failure marks corruptionDetected, attempts PRAGMA wal_checkpoint(TRUNCATE) recovery, re-runs integrity check, clears flag on success and logs recovery, or throws an error with explicit sqlite3 .recover manual instructions if recovery fails. Previous simple failure path removed.
API surface
packages/core/src/db.ts
Adds public get path(): string to return underlying DB path (including ":memory:").
Tests / Validation
packages/core/src/__tests__/db.test.ts
Adds idempotency test ensuring re-running db.init() does not overwrite existing config row, and a startup integrity check suite asserting integrity_check returns ok on initialized DB and that init() completes without throwing on a fresh empty DB directory (cleanup via rmSync).

Repo toolchain / release metadata

Layer / File(s) Summary
CI step
.github/workflows/pr-checks.yml
Adds a new PR-checks step “Build plugins” running pnpm -r --filter './plugins/**' build before lint/typecheck/tests.
Changeset / release note
.changeset/auto-provision-default-agents.md
Adds a changeset for @runfusion/fusion patch: fixes heartbeat routing in multi-project setups and auto-provisions default agents on engine start when agents table is empty.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

"A rabbit hops with buffered logs in tow,
Flushing little memories in a tidy row.
WAL checkpoints hum a careful tune,
Fresh DBs wake beneath the moon.
Hooray for safe writes and tests that glow!" 🐇✨

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main changes: WAL tuning to prevent SQLite corruption and batched agent log writes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Tip

💬 Introducing Slack Agent: The best way for teams to turn conversations into code.

Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.

  • Generate code and open pull requests
  • Plan features and break down work
  • Investigate incidents and troubleshoot customer tickets together
  • Automate recurring tasks and respond to alerts with triggers
  • Summarize progress and report instantly

Built for teams:

  • Shared memory across your entire org—no repeating context
  • Per-thread sandboxes to safely plan and execute work
  • Governance built-in—scoped access, auditability, and budget controls

One agent for your entire SDLC. Right inside Slack.

👉 Get started


Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps

greptile-apps Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

  • Addresses SQLite B-tree corruption by lowering wal_autocheckpoint from 1000→100 pages, capping WAL at 4 MB, and moving the startup integrity check (with WAL checkpoint recovery) to before any DDL/DML in init() — resolving the previously flagged ordering P1.
  • Introduces a write-behind agent log buffer in TaskStore that batches up to 50 entries (or 2 s) into a single transaction, with flush-on-read/count/delete/close, a retry requeue on transient errors, and a timer-cancel-before-DB-close that resolves the previously flagged timer re-arm P1.
  • Two minor inconsistencies remain: the close() flush warning omits the db path (unlike all other buffer log messages), and integrityCheck() in init() runs unconditionally without the _inMemory guard used by the WAL PRAGMAs (benign but inconsistent).

Confidence Score: 5/5

Safe to merge — all previously flagged P1s are resolved and no new P1/P0 issues found.

All prior P1 findings (timer re-arm after close, integrity check ordering, flush-before-delete race, missing try/finally, size-triggered flush without error handling, appendAgentLogBatch ordering inversion) are verifiably addressed in this diff. Only two P2 style-consistency nits remain. Test coverage for both subsystems is solid (14 new tests).

No files require special attention — db.ts and store.ts are the core of the change and both look correct.

Important Files Changed

Filename Overview
packages/core/src/db.ts Moves integrity check to before SCHEMA_SQL (fixes prior P1), adds WAL checkpoint recovery with dbPath in all log messages, lowers wal_autocheckpoint from 1000→100, and exposes a path getter. Minor: integrity check not gated on _inMemory.
packages/core/src/store.ts Adds write-behind agent log buffer (50-entry capacity, 2 s timer) with flush-on-read, flush-before-delete (inside lock), flush-on-close with timer cancel, retry requeue, and MAX_AGENT_LOG_BACKLOG drop cap. All previously identified P1s (timer re-arm, ordering inversion, missing try/finally) are addressed. Minor: close() warning log omits db path.
packages/core/src/tests/db.test.ts Adds 6 new tests covering WAL PRAGMA values (wal_autocheckpoint=100, journal_size_limit, synchronous, busy_timeout), in-memory skip, and startup integrity check on a fresh disk DB.
packages/core/src/tests/store.test.ts Adds 8 new tests for buffer fill, flush-on-read/count/delete/close, FK violation tolerance, immediate event emission, and multi-task batching. Also fixes two existing tests that accessed the DB directly without first flushing the buffer.
.github/workflows/pr-checks.yml Adds Bun install + plugin build step to the lint job, and a full pnpm build before the test shard job so type-checked artifacts are available.
packages/cli/src/commands/tests/task.test.ts Isolates GITHUB_REPOSITORY env var in one test to prevent CI environment leakage between test cases.

Sequence Diagram

sequenceDiagram
    participant Caller
    participant TaskStore
    participant Buffer as agentLogBuffer
    participant Timer as flushTimer
    participant DB as SQLite (WAL)

    Caller->>TaskStore: appendAgentLog(taskId, text, type)
    TaskStore->>Buffer: push entry
    TaskStore->>Caller: emit("agent:log") immediately

    alt buffer.length >= 50 (BUFFER_SIZE)
        TaskStore->>TaskStore: flushAgentLogBuffer()
        TaskStore->>DB: transaction { SELECT live taskIds, INSERT batch, bumpLastModified }
        DB-->>TaskStore: commit
        TaskStore->>Buffer: splice(0, flushCount)
    else buffer.length < 50 and no timer
        TaskStore->>Timer: setTimeout(2000ms).unref()
    end

    Note over Timer,DB: 2 s later (or on read/delete/close)
    Timer->>TaskStore: flushAgentLogBuffer()
    TaskStore->>Timer: clearTimeout
    TaskStore->>DB: transaction { filter live tasks, INSERT valid, bumpLastModified }
    alt transaction succeeds
        DB-->>TaskStore: commit
        TaskStore->>Buffer: splice(0, flushCount)
    else transaction fails
        DB-->>TaskStore: error
        TaskStore->>Buffer: splice(0, flushCount)
        TaskStore->>Buffer: unshift(validEntries) — requeue
        TaskStore->>Timer: setTimeout(2000ms).unref() — retry
    end

    Caller->>TaskStore: getAgentLogs / getAgentLogCount / deleteTask / close
    TaskStore->>TaskStore: flushAgentLogBuffer() before operation
Loading

Reviews (17): Last reviewed commit: "fix: resolve Greptile P1 close() timer l..." | Re-trigger Greptile

Comment thread packages/core/src/store.ts Outdated
Comment thread packages/core/src/store.ts Outdated
Comment thread packages/core/src/store.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/db.ts (1)

848-908: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Run the integrity check before any writes, and abort if recovery does not return ok.

Lines 849-869 already create/alter schema and seed rows before this check runs, so a damaged database can still be mutated during init(). Then Lines 889-906 only log on an unrecovered failure and leave the connection usable, which means the process can keep writing to a known-corrupt DB.

Suggested direction
  init(): void {
+    if (!this._inMemory) {
+      this.assertHealthyOrRecover();
+    }
+
     this.db.exec(SCHEMA_SQL);
@@
-    if (!this._inMemory) {
-      try {
-        const result = this.db.prepare("PRAGMA integrity_check").get() as
-          | { integrity_check?: string }
-          | undefined;
-        if (result?.integrity_check !== "ok") {
-          console.warn(
-            `[fusion] Database integrity check failed at startup. ` +
-            `Attempting WAL checkpoint recovery...`,
-          );
-          try {
-            this.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
-            const recheck = this.db.prepare("PRAGMA integrity_check").get() as
-              | { integrity_check?: string }
-              | undefined;
-            if (recheck?.integrity_check === "ok") {
-              console.warn(`[fusion] Database recovered via WAL checkpoint.`);
-            } else {
-              console.error(
-                `[fusion] Database is corrupted and could not be auto-recovered. ` +
-                `Run: sqlite3 ${this.dbPath} ".recover" | sqlite3 ${this.dbPath}.recovered`,
-              );
-            }
-          } catch {
-            console.error(
-              `[fusion] Database corruption detected and checkpoint recovery failed. ` +
-              `Manual recovery required.`,
-            );
-          }
-        }
-      } catch {
-        // Integrity check itself failed — serious corruption
-        console.error(`[fusion] Could not run integrity check on ${this.dbPath}`);
-      }
-    }
   }
+
+  private assertHealthyOrRecover(): void {
+    const result = this.db.prepare("PRAGMA integrity_check").get() as
+      | { integrity_check?: string }
+      | undefined;
+    if (result?.integrity_check === "ok") return;
+
+    console.warn("[fusion] Database integrity check failed at startup. Attempting WAL checkpoint recovery...");
+    this.db.exec("PRAGMA wal_checkpoint(TRUNCATE)");
+
+    const recheck = this.db.prepare("PRAGMA integrity_check").get() as
+      | { integrity_check?: string }
+      | undefined;
+    if (recheck?.integrity_check !== "ok") {
+      throw new Error(`Database integrity check failed for ${this.dbPath}`);
+    }
+  }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/db.ts` around lines 848 - 908, Move the integrity check to
run at the very start of init() before any db.exec or schema/seed writes (i.e.,
before SCHEMA_SQL, INSERT OR IGNORE into __meta and config, and before calling
migrate() / ensureRoutinesSchemaCompatibility()), and if the first
integrity_check != "ok" then attempt the PRAGMA wal_checkpoint(TRUNCATE)
recovery and immediately re-run integrity_check; if recovery does not produce
"ok" abort initialization by throwing an error or closing the connection so no
further writes occur (use the same logic currently in the try/catch that
inspects integrity_check and wal_checkpoint, but relocate and convert the
non-recovered path into a hard failure referencing this.dbPath and this.db
operations like this.db.prepare("PRAGMA integrity_check").get()).
🧹 Nitpick comments (1)
packages/core/src/store.ts (1)

5269-5281: 💤 Low value

getAgentLogsByTimeRange may return stale data — buffer is not flushed.

Unlike getAgentLogs() and getAgentLogCount() which flush before reading, this method reads directly from the database. If buffered entries fall within the requested time range, they won't be returned.

If this method is expected to have read-after-write consistency like its siblings, add a flush call at the start:

♻️ Optional fix for consistency
   async getAgentLogsByTimeRange(
     taskId: string,
     startIso: string,
     endIso: string | null,
   ): Promise<AgentLogEntry[]> {
+    this.flushAgentLogBuffer();
     const end = endIso ?? new Date().toISOString();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/store.ts` around lines 5269 - 5281, getAgentLogsByTimeRange
reads directly from the DB and can miss buffered entries; add the same flush
call used by getAgentLogs() and getAgentLogCount() at the start of
getAgentLogsByTimeRange so pending buffer entries are written before the SELECT.
Locate getAgentLogsByTimeRange and insert a call to the existing flush method
(the one invoked by getAgentLogs()/getAgentLogCount()) as the first statement,
then proceed with computing end and querying the DB.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/__tests__/db.test.ts`:
- Around line 212-219: The test creates an extra temp directory (freshDir via
makeTmpDir) that isn't removed by the suite-level afterEach; after closing
freshDb you must delete the freshDir so it doesn't leak on repeated runs. Update
the test that creates freshDir/freshFusionDir and Database(freshFusionDir)
(symbols: makeTmpDir, freshDir, freshFusionDir, Database, freshDb) to remove the
freshDir at the end (e.g., call the existing cleanup helper or
fs.rmSync/fs.rmdir equivalent with recursive/force) or register freshDir with
the suite-level temp cleanup so the directory is removed after the test. Ensure
removal happens after freshDb.close().

In `@packages/core/src/__tests__/store.test.ts`:
- Around line 4716-4730: The test title claims a "single flush" but only asserts
per-task counts; add an explicit assertion that the store's flush method was
invoked exactly once to prove batching. Locate the test that uses
createTestTask(), store.createTask(), store.appendAgentLog(), and
store.getAgentLogCount(); before the loop spyOn the store flush/commit function
(e.g., jest.spyOn(store, "flushBufferedLogs") or the actual internal method that
performs the batched write) or inject a mock, run the appends, then assert the
spy was called once (and keep the existing per-task count assertions), or
alternatively change the test title to stop claiming a single flush if you
cannot reliably spy the flush method.
- Around line 4629-4631: The test currently uses getAgentLogCount(task.id) which
implicitly triggers an auto-flush and makes the test a false positive; instead,
assert the buffered entries directly (e.g., inspect store.agentLogBuffer or
store._agentLogBuffer for task.id and expect length 50), verify the DB count
remains unchanged (call a non-flushing DB count helper or query the underlying
DB adapter directly), then explicitly call the flush method (e.g.,
store.flushAgentLogBuffer(task.id) or store.flushAllAgentLogs()) and finally
call getAgentLogCount(task.id) to assert the DB now has 50 entries; replace the
single getAgentLogCount assertion with these steps and avoid relying on
getAgentLogCount to observe the buffer state.

---

Outside diff comments:
In `@packages/core/src/db.ts`:
- Around line 848-908: Move the integrity check to run at the very start of
init() before any db.exec or schema/seed writes (i.e., before SCHEMA_SQL, INSERT
OR IGNORE into __meta and config, and before calling migrate() /
ensureRoutinesSchemaCompatibility()), and if the first integrity_check != "ok"
then attempt the PRAGMA wal_checkpoint(TRUNCATE) recovery and immediately re-run
integrity_check; if recovery does not produce "ok" abort initialization by
throwing an error or closing the connection so no further writes occur (use the
same logic currently in the try/catch that inspects integrity_check and
wal_checkpoint, but relocate and convert the non-recovered path into a hard
failure referencing this.dbPath and this.db operations like
this.db.prepare("PRAGMA integrity_check").get()).

---

Nitpick comments:
In `@packages/core/src/store.ts`:
- Around line 5269-5281: getAgentLogsByTimeRange reads directly from the DB and
can miss buffered entries; add the same flush call used by getAgentLogs() and
getAgentLogCount() at the start of getAgentLogsByTimeRange so pending buffer
entries are written before the SELECT. Locate getAgentLogsByTimeRange and insert
a call to the existing flush method (the one invoked by
getAgentLogs()/getAgentLogCount()) as the first statement, then proceed with
computing end and querying the DB.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8197c8be-c030-4105-b513-f27d1786ee4f

📥 Commits

Reviewing files that changed from the base of the PR and between 6984bcd and 80f59fa.

📒 Files selected for processing (4)
  • packages/core/src/__tests__/db.test.ts
  • packages/core/src/__tests__/store.test.ts
  • packages/core/src/db.ts
  • packages/core/src/store.ts

Comment thread packages/core/src/__tests__/db.test.ts Outdated
Comment thread packages/core/src/__tests__/store.test.ts Outdated
Comment thread packages/core/src/__tests__/store.test.ts Outdated
Comment thread packages/core/src/store.ts Outdated
Comment thread packages/core/src/store.ts Outdated
userUnderC and others added 2 commits May 3, 2026 08:56
…nt logs

The agentLogEntries table was doing individual auto-committed INSERTs,
creating extreme WAL pressure that caused recurring B-tree corruption.
This adds three WAL PRAGMAs (aggressive autocheckpoint, size limit,
synchronous=NORMAL), a write-behind buffer for agent log entries, and
a startup integrity check with auto-recovery attempt.

Refs: Runfusion#24

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ck ordering

- Move integrity check before migrations/seeds to avoid writing to a
  corrupted database (makes corruption worse)
- Fix buffer swap-before-commit hazard: entries are now only removed from
  the buffer after the transaction succeeds
- Add try-catch to timer-triggered flush to prevent uncaught exceptions
  from crashing the process
- Log warning in close() catch instead of silently swallowing errors

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@timothyjlaurent
timothyjlaurent force-pushed the fix/sqlite-corruption-wal-pragmas branch from 7a53c4d to 78ff1be Compare May 3, 2026 15:59

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/store.ts`:
- Around line 5449-5450: getAgentLogsByTimeRange (and the other time-range read
helper around the later block) currently reads only persisted rows and can miss
recent buffered appends; before performing the time-range query, call
this.flushAgentLogBuffer() to ensure buffered entries are visible. Locate the
methods getAgentLogsByTimeRange and the corresponding time-range read block (the
later block around the second occurrence noted) and insert
this.flushAgentLogBuffer() just prior to executing the DB/time-range read so the
read-after-write consistency matches getAgentLogs()/getAgentLogCount().
- Around line 3755-3756: appendAgentLog() can enqueue logs for tasks that have
been deleted concurrently with deleteTask(), causing FK violations during
flushAgentLogBuffer() and leaving stale rows in memory; to fix, make
appendAgentLog() validate the target task is still live (e.g., call the existing
task-liveness check or look up task status) and reject or drop appends for
non-live tasks, and additionally harden flushAgentLogBuffer() to filter
out/stage-remove any buffered entries whose task no longer exists before
starting the DB transaction so a late log can’t abort and leave stale entries in
memory; update code paths referencing appendAgentLog(), deleteTask(), and
flushAgentLogBuffer() accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1f14126c-0ab5-41a0-8542-131f077dd73d

📥 Commits

Reviewing files that changed from the base of the PR and between 7a53c4d and 78ff1be.

📒 Files selected for processing (4)
  • packages/core/src/__tests__/db.test.ts
  • packages/core/src/__tests__/store.test.ts
  • packages/core/src/db.ts
  • packages/core/src/store.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/tests/store.test.ts

Comment thread packages/core/src/store.ts Outdated
Comment thread packages/core/src/store.ts
Ensures operators can identify which database has integrity issues
when multiple DB instances are running.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gsxdsm

gsxdsm commented May 3, 2026

Copy link
Copy Markdown
Collaborator

Thanks for creating this. Will merge in once comments addressed

…in logs

- Flush buffer before appendAgentLogBatch to prevent rowid ordering
  inversion when callers interleave buffered and direct writes (P1)
- Wrap size-triggered flush in try-catch for consistency with timer path
- Move bumpLastModified inside transactions for atomicity
- Expose db.path getter and include DB path in all flush error logs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@timothyjlaurent

Copy link
Copy Markdown
Contributor Author

Thanks for the thorough review. Addressed all flagged issues in 62072a64:

P1 — appendAgentLogBatch ordering inversion: Fixed. appendAgentLogBatch now calls flushAgentLogBuffer() before writing batch entries, guaranteeing buffered entries land before batch entries and preserving insertion order. (store.ts:4773)

Size-triggered flush missing try-catch: Fixed. The appendAgentLog size-trigger path now wraps flushAgentLogBuffer() in try-catch, consistent with the timer path. (store.ts:4704-4709)

bumpLastModified outside transaction: Fixed in both flushAgentLogBuffer and appendAgentLogBatch. bumpLastModified() now runs inside the transaction so the timestamp update is atomic with the inserts. (store.ts:4749, store.ts:4801)

DB identification in logs: Added a public db.path getter on the Database class and included the DB path in all flush error messages (size-triggered, timer-triggered, close) so operators can identify which database is having problems. (db.ts:702, store.ts:4708, store.ts:4718, store.ts:6137)

Re: deleteTask flush race outside withTaskLock: The flush at store.ts:3756 runs before acquiring the task lock intentionally — it flushes buffered entries for all tasks to ensure FK cascade deletes find them. The actual delete inside the lock is safe because the flush is synchronous and completes before withTaskLock is called.

Re: "entries not spliced on transaction failure": The current pattern uses slice() before the transaction and splice(0, flushCount) after success. If the transaction throws, the buffer is untouched — entries remain and will be retried on the next flush cycle. This is intentional: on failure we don't want to lose entries, we want to retry them.

All 811 tests pass.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

♻️ Duplicate comments (2)
packages/core/src/store.ts (2)

3755-3756: ⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

A deleted task can still poison the shared agent-log buffer.

deleteTask() flushes too early to make the buffer safe: appendAgentLog() is still free to enqueue for the same task after that pre-delete flush, and flushAgentLogBuffer() writes the whole batch in one transaction. One FK violation aborts every row and the bad entry stays in memory, so later reads/close calls that flush first can keep failing too.

Also applies to: 4692-4700, 4743-4757

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/store.ts` around lines 3755 - 3756, deleteTask() currently
calls flushAgentLogBuffer() too early, allowing appendAgentLog() to race and
leave FK-violating rows in the shared in-memory buffer; change deleteTask() to
first remove any buffered entries for that task from the agent-log buffer (i.e.,
filter out entries with the task's id) and mark the task as deleted (or add its
id to a deletedTask set) before performing the DB delete, and update
appendAgentLog() to consult that marker (reject/apply backpressure) so no new
entries for a deleted task are enqueued; alternatively, perform the flush+delete
in a single DB transaction, but at minimum ensure flushAgentLogBuffer() is not
called until buffered entries for the target task have been purged or new
appends are prevented.

5520-5532: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Flush buffered rows before time-range reads too.

getAgentLogsByTimeRange() still reads only persisted rows, so an immediate append followed by a time-range query can miss entries that getAgentLogs() and getAgentLogCount() now include. Add the same pre-read flush here for consistent read-after-write behavior.

Proposed fix
   async getAgentLogsByTimeRange(
     taskId: string,
     startIso: string,
     endIso: string | null,
   ): Promise<AgentLogEntry[]> {
+    this.flushAgentLogBuffer();
     const end = endIso ?? new Date().toISOString();
     const selectClause = this.getAgentLogSelectClause();
     const rows = this.db.prepare(`
       SELECT ${selectClause} FROM agentLogEntries
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/store.ts` around lines 5520 - 5532, getAgentLogsByTimeRange
currently reads only persisted rows; add the same pre-read flush used by
getAgentLogs() and getAgentLogCount() so buffered appends are included. Insert a
call to the flush method (e.g. this.flushBufferedAgentLogRows()) at the start of
getAgentLogsByTimeRange (before computing end/select and running the query) to
flush in-memory/buffered agent log rows to the DB, then proceed with the
existing select and mapping logic.
🧹 Nitpick comments (1)
packages/core/src/db.ts (1)

701-702: 💤 Low value

Consider removing duplicate API surface.

The new path getter duplicates the existing getPath() method at line 2518. Both return this.dbPath. Having two ways to access the same value can cause confusion. Consider either:

  • Removing getPath() and using only the getter (breaking change for existing callers)
  • Keeping only getPath() for consistency with CentralDatabase.getPath() (see relevant code snippet 3)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/db.ts` around lines 701 - 702, The new getter path
duplicates the existing getPath() API (both return this.dbPath); remove the
redundant path getter to avoid a duplicate surface, and update any internal
callers of the path property to call getPath() instead (or add a small
forwarding implementation that calls getPath() if you need a transitional
change). Ensure consistency with CentralDatabase.getPath() by keeping getPath()
as the single source-of-truth for the database path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/db.ts`:
- Around line 944-949: The catch block that handles checkpoint recovery is
currently empty and discards the thrown error; update the catch to capture the
error (e.g., catch (err)) and include the error message/stack in the
console.error call so the log for `[fusion:db] Database corruption detected for
${this.dbPath} and checkpoint recovery failed.` also prints the underlying error
details (use err.message and/or err.stack) to aid diagnosis when checkpoint
recovery fails.
- Around line 923-927: The integrity check is currently called after performing
writes (db.exec(SCHEMA_SQL) and the INSERT OR IGNORE INTO __meta statements)
which contradicts the comment; either move the integrityCheck() invocation to
run immediately after opening the DB connection and before db.exec(SCHEMA_SQL)
and any INSERT OR IGNORE INTO __meta operations, or update the comment to
reflect that integrityCheck() runs after schema/seed writes. Locate the
integrityCheck() call and the db.exec(SCHEMA_SQL) / INSERT OR IGNORE INTO __meta
statements and reorder so integrityCheck() runs first, or change the comment
text to accurately describe the current ordering.

---

Duplicate comments:
In `@packages/core/src/store.ts`:
- Around line 3755-3756: deleteTask() currently calls flushAgentLogBuffer() too
early, allowing appendAgentLog() to race and leave FK-violating rows in the
shared in-memory buffer; change deleteTask() to first remove any buffered
entries for that task from the agent-log buffer (i.e., filter out entries with
the task's id) and mark the task as deleted (or add its id to a deletedTask set)
before performing the DB delete, and update appendAgentLog() to consult that
marker (reject/apply backpressure) so no new entries for a deleted task are
enqueued; alternatively, perform the flush+delete in a single DB transaction,
but at minimum ensure flushAgentLogBuffer() is not called until buffered entries
for the target task have been purged or new appends are prevented.
- Around line 5520-5532: getAgentLogsByTimeRange currently reads only persisted
rows; add the same pre-read flush used by getAgentLogs() and getAgentLogCount()
so buffered appends are included. Insert a call to the flush method (e.g.
this.flushBufferedAgentLogRows()) at the start of getAgentLogsByTimeRange
(before computing end/select and running the query) to flush in-memory/buffered
agent log rows to the DB, then proceed with the existing select and mapping
logic.

---

Nitpick comments:
In `@packages/core/src/db.ts`:
- Around line 701-702: The new getter path duplicates the existing getPath() API
(both return this.dbPath); remove the redundant path getter to avoid a duplicate
surface, and update any internal callers of the path property to call getPath()
instead (or add a small forwarding implementation that calls getPath() if you
need a transitional change). Ensure consistency with CentralDatabase.getPath()
by keeping getPath() as the single source-of-truth for the database path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 24350fe9-a4fc-4559-94a9-d1ffc4cad321

📥 Commits

Reviewing files that changed from the base of the PR and between 1976297 and 62072a6.

📒 Files selected for processing (2)
  • packages/core/src/db.ts
  • packages/core/src/store.ts

Comment thread packages/core/src/db.ts Outdated
Comment thread packages/core/src/db.ts Outdated
- Move integrity check before SCHEMA_SQL writes (was after, contradicting comment)
- Log error message in checkpoint recovery catch block
- Make flushAgentLogBuffer private
- Move deleteTask flush inside withTaskLock to prevent race
- Use try/finally in flush so splice always runs on failure
- Filter stale task entries during flush to prevent buffer poisoning
- Add flush before getAgentLogsByTimeRange for read consistency
- Clean up leaked temp dir in fresh DB test
- Fix false-positive buffer-capacity test (query DB directly)
- Rename misleading "single flush" test title

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@timothyjlaurent

Copy link
Copy Markdown
Contributor Author

Addressed all remaining review feedback in a5a87022. Summary of what changed:

db.ts:

  • Integrity check ordering (@coderabbitai major): Moved integrity check to run before SCHEMA_SQL and __meta seeds, matching the stated intent. The previous placement was after initial writes — now it truly runs first.
  • Empty catch discards error (@coderabbitai minor): Added err capture and included the error message in the log output.

store.ts:

  • flushAgentLogBuffer visibility (@greptile P2): Made private. All test callers already use (store as any).flushAgentLogBuffer().
  • deleteTask flush race (@greptile P2): Moved flushAgentLogBuffer() inside withTaskLock callback, eliminating the window where new appends can queue between flush and DELETE.
  • bumpLastModified outside transaction (@greptile P2): Already fixed in prior commit — confirmed bumpLastModified() runs inside the transaction in both flushAgentLogBuffer and appendAgentLogBatch.
  • Perpetual flush failure from failed transaction (@greptile P1): Changed to try/finallysplice(0, flushCount) now runs regardless of whether the transaction succeeded, so a single FK violation can't block all future flushes.
  • Deleted-task rows wedging buffer (@coderabbitai major): Added stale entry filtering — before each flush, we query live task IDs and drop buffered entries for deleted tasks, logging the count. This prevents FK violations from ever reaching the transaction.
  • Time-range reads miss buffer (@coderabbitai minor): Added flushAgentLogBuffer() to getAgentLogsByTimeRange for read-after-write consistency.

Tests:

  • Leaked temp dir (@coderabbitai minor): Added rmSync(freshDir, { recursive: true, force: true }) in a finally block.
  • False-positive buffer test (@coderabbitai major): Replaced getAgentLogCount() with direct DB query to avoid triggering read-path auto-flush.
  • Misleading test title (@coderabbitai minor): Renamed to "flushes interleaved entries from multiple tasks correctly".

All 811 tests pass. Ready for re-review.

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor

Review rate limit: 10/10 reviews remaining, available now.

@timothyjlaurent

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 3, 2026

Copy link
Copy Markdown
Contributor
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
packages/core/src/__tests__/store.test.ts (1)

4976-4991: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Avoid getAgentLogCount() for the size-triggered flush assertion.

This has the same false-positive risk as the earlier count-based buffering test: getAgentLogCount() auto-flushes, so the test can still pass even if the buffer-full flush path is broken. Query agentLogEntries directly after the loop to verify the batch write actually landed.

Suggested fix
-        const countA = await store.getAgentLogCount(taskA.id);
-        const countB = await store.getAgentLogCount(taskB.id);
-        expect(countA).toBe(25);
-        expect(countB).toBe(25);
+        const rows = (store as any).db
+          .prepare(`
+            SELECT taskId, COUNT(*) AS count
+            FROM agentLogEntries
+            WHERE taskId IN (?, ?)
+            GROUP BY taskId
+          `)
+          .all(taskA.id, taskB.id) as Array<{ taskId: string; count: number }>;
+        expect(rows).toEqual(
+          expect.arrayContaining([
+            { taskId: taskA.id, count: 25 },
+            { taskId: taskB.id, count: 25 },
+          ]),
+        );
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/core/src/__tests__/store.test.ts` around lines 4976 - 4991, The test
uses store.getAgentLogCount() which itself triggers an auto-flush, causing a
false positive; instead, after the interleaved append loop, query the persisted
entries directly via store.agentLogEntries (or whatever read method returns
stored rows for a given taskId) for taskA.id and taskB.id and assert those
lengths equal 25; keep the same setup that calls store.appendAgentLog in the
loop (and createTestTask/store.createTask), but replace the getAgentLogCount
assertions with direct queries to store.agentLogEntries(taskId) and compare the
returned arrays' lengths to 25 to confirm the buffer-triggered batch write
actually occurred.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/__tests__/store.test.ts`:
- Around line 4916-4927: The test currently only checks the final DB count and
can pass if deleteTask() drops buffered entries instead of flushing; modify the
test to prove flush happens first by either (a) spying/mocking the
store.flushAgentLogBuffer method and asserting it was called before
store.deleteTask, or (b) asserting the buffered row exists in the SQLite table
prior to calling store.deleteTask (e.g., after appendAgentLog use (store as
any).db.prepare("SELECT COUNT(*)... FROM agentLogEntries WHERE taskId =
?").get(task.id) and expect count > 0), then call deleteTask() and assert count
becomes 0, referencing the functions appendAgentLog, flushAgentLogBuffer, and
deleteTask to locate the code to change.

In `@packages/core/src/db.ts`:
- Around line 913-939: The startup integrity check currently continues
initialization after WAL checkpoint recovery fails; modify the block that calls
this.integrityCheck(), this.db.exec("PRAGMA wal_checkpoint(TRUNCATE)"), and the
subsequent recheck so that if recheck.ok is false you throw an Error (or
otherwise abort initialization) instead of proceeding, and likewise rethrow or
throw after catching an exception from db.exec; ensure you set
this.corruptionDetected before throwing and include this.dbPath and the caught
error message in the thrown Error to surface context to callers.

In `@packages/core/src/store.ts`:
- Around line 4770-4774: The finally block unconditionally calls
this.agentLogBuffer.splice(0, flushCount) which drops buffered entries even when
the DB flush transaction failed; change the logic so that splice is only
executed after a confirmed successful commit (or when the flush operation
indicates success). In practice, add a success flag around the flush/transaction
code in the method containing the try/catch/finally, set success = true after
the transaction completes/commits, and move or guard the
this.agentLogBuffer.splice(0, flushCount) call to run only when success is true
(keep rollback/error handling separate so failed flushes leave the buffer intact
for retry). Ensure flushCount is still computed correctly and used only when
success is true.

---

Duplicate comments:
In `@packages/core/src/__tests__/store.test.ts`:
- Around line 4976-4991: The test uses store.getAgentLogCount() which itself
triggers an auto-flush, causing a false positive; instead, after the interleaved
append loop, query the persisted entries directly via store.agentLogEntries (or
whatever read method returns stored rows for a given taskId) for taskA.id and
taskB.id and assert those lengths equal 25; keep the same setup that calls
store.appendAgentLog in the loop (and createTestTask/store.createTask), but
replace the getAgentLogCount assertions with direct queries to
store.agentLogEntries(taskId) and compare the returned arrays' lengths to 25 to
confirm the buffer-triggered batch write actually occurred.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 41873b5a-fc95-4e79-af5d-5302dd49513a

📥 Commits

Reviewing files that changed from the base of the PR and between 1976297 and a5a8702.

📒 Files selected for processing (4)
  • packages/core/src/__tests__/db.test.ts
  • packages/core/src/__tests__/store.test.ts
  • packages/core/src/db.ts
  • packages/core/src/store.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/tests/db.test.ts

Comment thread packages/core/src/__tests__/store.test.ts
Comment thread packages/core/src/db.ts
Comment thread packages/core/src/store.ts
- Throw from init() when integrity check fails and recovery doesn't help,
  preventing writes to a known-corrupt database
- Only drain buffer on successful flush; requeue valid entries on transient
  failures (busy/IO) so they aren't silently lost
- Add spy on flushAgentLogBuffer in deleteTask test to prove flush-before-delete

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@timothyjlaurent

Copy link
Copy Markdown
Contributor Author

Re: "Stop initialization when integrity remains failed after recovery" (db.ts)

Fixed in 3a93758d. init() now throws when the integrity check fails and WAL checkpoint recovery doesn't help — either from the failed recheck or from a checkpoint exception. The error message includes dbPath and the first 3 integrity errors so operators know what went wrong. This prevents any writes to a known-corrupt database.

@timothyjlaurent

Copy link
Copy Markdown
Contributor Author

Re: "Avoid dropping valid buffered logs when a flush transaction fails" (store.ts)

Fixed in 3a93758d. Added a flushSucceeded flag — on transient failures (busy/IO), the original slice is drained from the buffer but valid entries are requeued via unshift. Stale entries (deleted tasks) are filtered out before the transaction and dropped permanently. This means:

  • FK violations from deleted tasks → dropped permanently, not requeued
  • Transient SQLite errors (busy/IO) → valid entries requeued for retry
  • Success → normal drain

@timothyjlaurent

Copy link
Copy Markdown
Contributor Author

Re: "Prove the flush happens before delete" (store.test.ts)

Fixed in 3a93758d. Added vi.spyOn(store as any, "flushAgentLogBuffer") and assert it was called before checking the DB count. Now the test proves flush-before-delete actually happens, not just that the final count is zero.

@gsxdsm

gsxdsm commented May 4, 2026

Copy link
Copy Markdown
Collaborator

Thanks for addressing the comments on this will merge in asap

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/core/src/store.ts`:
- Around line 4747-4770: The current flushAgentLogBuffer() captures liveTaskIds
before starting the transaction, creating a TOCTOU where a concurrent delete can
cause an FK rollback; make the delete-skip atomic by performing the existence
check inside the DB transaction: inside the this.db.transaction block use a
prepared statement that inserts only when the task exists (e.g. an INSERT ...
SELECT ... FROM tasks WHERE id = ? pattern or INSERT INTO agentLogEntries(...)
SELECT ?, ?, ?, ?, ?, ? WHERE EXISTS (SELECT 1 FROM tasks WHERE id = ?)), loop
over the batch and execute that statement for each entry, and remove the
pre-filter/liveTaskIds logic; alternatively, implement a single retry on FK
constraint failure inside the transaction (catch FK error from the insert loop,
re-filter by querying tasks inside the same transaction and retry the inserts
once) to close the TOCTOU window (refer to agentLogEntries, flushAgentLogBuffer,
this.db.transaction and this.db.prepare).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ee3cc77c-eccb-4bc0-83db-431c0d0ed553

📥 Commits

Reviewing files that changed from the base of the PR and between a5a8702 and 3a71f13.

📒 Files selected for processing (3)
  • packages/core/src/__tests__/store.test.ts
  • packages/core/src/db.ts
  • packages/core/src/store.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/core/src/db.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/tests/store.test.ts

Comment thread packages/core/src/store.ts Outdated
Addresses CodeRabbit TOCTOU concern: querying live task IDs inside the
transaction ensures the check and inserts are atomic under SQLite locking,
preventing FK violations from concurrent deletes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@timothyjlaurent

Copy link
Copy Markdown
Contributor Author

CI Failure Analysis — Pre-existing, Not Related to This PR

Both failing shards contain failures that are unrelated to our changes (db.ts, store.ts, and their tests):

Shard 1/3plugin-loader.test.ts: Vite fails to resolve @fusion/core package entry (ERR_MODULE_NOT_FOUND). This test file was not modified by this PR.

Shard 3/3 — 161 packages/dashboard tests: All fail with ERR_MODULE_NOT_FOUND. The dashboard package is not touched by this PR.

The last CI run on main itself (run 24855093942, April 23) also failed — this is a pre-existing CI environment / dependency issue.

Our changes: All 3,354 core package tests pass locally. The PR only modifies packages/core/src/{db,store}.ts and their test files.

Merges origin/main to bring in the droid runtime plugin (needed by
plugin-loader test) and all recent changes. Adds a "Build plugins"
step to pr-checks.yml so plugin dist/ directories are compiled before
tests run — fixes ERR_MODULE_NOT_FOUND for hermes and droid plugins.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/db.ts`:
- Around line 950-989: The Store getters are caching this._db / this._archiveDb
before awaiting Database.init(), which can leave a partially initialized (and
potentially corrupt) Database instance in the cache if init() throws; change the
getter logic to await db.init() first using a local variable (e.g., const db =
new Database(...); await db.init();) and only assign this._db = db after init()
succeeds, and mirror the exact same pattern for the archive
getter/this._archiveDb so the instance is only published to the Store once
initialization completes successfully.

In `@packages/core/src/store.ts`:
- Around line 4756-4832: The agent log retry backlog needs a hard cap to avoid
unbounded memory growth: add a constant (e.g. AGENT_LOG_BACKLOG_CAP) and enforce
it in appendAgentLog() before pushing new entries and in flushAgentLogBuffer()
when requeuing validEntries; if accepting the new entries would exceed the cap,
drop entries deterministically (either oldest or newest), increment/log a clear
warning including this.db.path and the drop count, and ensure the buffer length
is trimmed to the cap after any unshift() requeueing so transient failures
cannot grow agentLogBuffer unbounded; update any related comments and tests for
appendAgentLog, flushAgentLogBuffer, and the
AGENT_LOG_BUFFER_SIZE/AGENT_LOG_FLUSH_MS behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 1ffa46d0-c515-4cdc-adaa-2086b6214e62

📥 Commits

Reviewing files that changed from the base of the PR and between 8dee02f and d431ac3.

📒 Files selected for processing (6)
  • .changeset/auto-provision-default-agents.md
  • .github/workflows/pr-checks.yml
  • packages/core/src/__tests__/db.test.ts
  • packages/core/src/__tests__/store.test.ts
  • packages/core/src/db.ts
  • packages/core/src/store.ts
✅ Files skipped from review due to trivial changes (2)
  • .changeset/auto-provision-default-agents.md
  • packages/core/src/tests/db.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/core/src/tests/store.test.ts

Comment thread packages/core/src/db.ts
Comment thread packages/core/src/store.ts
userUnderC and others added 2 commits May 4, 2026 11:51
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…failure

Two fixes from CodeRabbit review:
- Add MAX_AGENT_LOG_BACKLOG cap (5000 entries) to prevent unbounded memory
  growth during prolonged SQLite outages. Oldest entries are dropped when
  cap is exceeded.
- Only assign this._db/this._archiveDb after init() succeeds. On failure,
  close the DB handle before throwing so callers never get a partially
  initialized instance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/core/src/store.ts (1)

4881-4903: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Make batch inserts tolerate deleted tasks too.

appendAgentLogBatch() still inserts every row blindly. If one entry targets a task deleted concurrently, the FK violation rolls back the whole transaction and drops unrelated valid rows in the same batch. flushAgentLogBuffer() already hardened this path; the batch API should apply the same live-task filtering inside the transaction and only emit events for rows that were actually persisted.

💡 Proposed fix
-    this.db.transaction(() => {
-      for (const entry of normalizedEntries) {
+    const persistedEntries = this.db.transaction(() => {
+      const liveTaskIds = new Set(
+        (this.db.prepare("SELECT id FROM tasks").all() as Array<{ id: string }>).map((r) => r.id),
+      );
+      const validEntries = normalizedEntries.filter((entry) => liveTaskIds.has(entry.taskId));
+      const dropped = normalizedEntries.length - validEntries.length;
+      if (dropped > 0) {
+        console.warn(
+          `[fusion] Dropped ${dropped} batched agent log entries for deleted tasks (${this.db.path})`,
+        );
+      }
+      for (const entry of validEntries) {
         stmt.run(
           entry.taskId,
           timestamp,
@@
           entry.detail ?? null,
           entry.agent ?? null,
         );
       }
       this.db.bumpLastModified();
+      return validEntries;
     });
 
-    for (const entry of normalizedEntries) {
+    for (const entry of persistedEntries) {
       this.emit("agent:log", {
         timestamp,
         taskId: entry.taskId,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core/src/store.ts` around lines 4881 - 4903, appendAgentLogBatch()
currently inserts all normalizedEntries inside this.db.transaction() causing the
whole batch to roll back on FK violations for concurrently deleted tasks;
instead, inside the same transaction (where stmt is prepared and
this.db.bumpLastModified() is called) look up or check live task IDs and filter
normalizedEntries to only those whose taskId still exists before calling
stmt.run for each, collect which rows were actually inserted and emit events
only for those persisted rows (mirroring the live-task filtering behavior in
flushAgentLogBuffer()); ensure you do the existence check using the same DB
handle/transaction so race conditions are avoided.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/core/src/store.ts`:
- Around line 4853-4860: The flush code currently requeues validEntries on
transient failure but leaves agentLogFlushTimer cleared so buffered logs may
never be retried; update the finally block in flushAgentLogBuffer to re-arm the
flush timer when you unshift validEntries back into agentLogBuffer (use the
existing scheduling helper or set agentLogFlushTimer to a new timeout to call
flushAgentLogBuffer after the normal delay) so retries are scheduled
automatically after a transient SQLite/IO failure.

---

Outside diff comments:
In `@packages/core/src/store.ts`:
- Around line 4881-4903: appendAgentLogBatch() currently inserts all
normalizedEntries inside this.db.transaction() causing the whole batch to roll
back on FK violations for concurrently deleted tasks; instead, inside the same
transaction (where stmt is prepared and this.db.bumpLastModified() is called)
look up or check live task IDs and filter normalizedEntries to only those whose
taskId still exists before calling stmt.run for each, collect which rows were
actually inserted and emit events only for those persisted rows (mirroring the
live-task filtering behavior in flushAgentLogBuffer()); ensure you do the
existence check using the same DB handle/transaction so race conditions are
avoided.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f84bcc8e-5c11-4727-a02e-31750f3ab185

📥 Commits

Reviewing files that changed from the base of the PR and between d431ac3 and 2800c3c.

📒 Files selected for processing (1)
  • packages/core/src/store.ts

Comment thread packages/core/src/store.ts
userUnderC and others added 3 commits May 4, 2026 12:10
When flushAgentLogBuffer() requeues valid entries on transient failure,
the timer was cleared but never re-armed, leaving entries in memory
indefinitely. Now schedules a retry after AGENT_LOG_FLUSH_MS.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add `pnpm build` step to test-shards job so plugin dist/ and core
  packages are compiled before tests run (fixes plugin-loader test)
- Add cli-alias/index.js to ESLint node scripts pattern so Node globals
  (process, AbortController, fetch) are recognized

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gsxdsm

gsxdsm commented May 4, 2026

Copy link
Copy Markdown
Collaborator

Looks good. Thanks !!

- task.test.ts: save/delete GITHUB_REPOSITORY env before
  runTaskPrCreate fallback test so CI env doesn't bypass the
  getCurrentRepo code path
- app.test.tsx: increase waitForFrameContains timeout for ASCII QR
  render from 3s to 6s to accommodate slower CI environments
- remote-auth.test.ts: switch createRemoteSettings default from
  tailscale to cloudflare with a static ingressUrl so
  resolveRemoteBaseUrl doesn't 409 when no tunnel is running

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread packages/core/src/store.ts
Combined both branches: TaskStore import from main + rmSync import
from our branch (needed for fresh DB test cleanup).

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Comment thread packages/core/src/store.ts
…rd test

- store.ts close(): clear retry timer and buffer before nulling _db to
  prevent re-opening a new connection after close (Greptile P1)
- merger-staging-allowlist.test.ts: use `git init -b main` so the
  default branch is always named 'main' regardless of git config
- SettingsModal.test.tsx: fix expected text for compact DroidCli card
  — shows "✓ Active" badge, not "✓ Connected — 1.2.3"

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@gsxdsm
gsxdsm merged commit f909688 into Runfusion:main May 5, 2026
5 of 6 checks passed
@gsxdsm

gsxdsm commented May 5, 2026

Copy link
Copy Markdown
Collaborator

Thank you for the hard work on this!!

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.

3 participants