RAG Outbox SQLite — Single-Writer Topology, Checkpoint Discipline & Integrity Watchdog (MC #106047)
RAG Outbox SQLite — Single-Writer Topology, Checkpoint Discipline & Integrity Watchdog
Status: ACTIVE Created: 2026-07-20 Owner: FlowForge Related: MC #106047 (root cause + fix), MC #106066 (follow-up: fsevents debounce)
Purpose
~/system/state/ingest-queue.sqlite is the durable outbox queue that feeds documents into LightRAG (BookStack pages, MC task outcomes, evidence files, specs, rules). It corrupted repeatedly (Apr 23, May 9, Jul 15→20-silent, Jul 20 2026) with the same SQLITE_CORRUPT / "database disk image is malformed" signature. This runbook documents the root cause, the fix, the writer topology, and the guard now in place so future recurrences are caught in minutes instead of days.
Writer Topology (as of 2026-07-20)
All writers use the shared library ~/system/lib/rag-outbox.js (openOutbox()), which owns WAL-mode pragmas and the SQLite connection lifecycle.
| Daemon (LaunchAgent) | Script | Trigger | Lifetime | Checkpoint mode |
|---|---|---|---|---|
com.alai.rag-drain-worker |
tools/rag-drain-worker.js |
KeepAlive | Long-lived, continuous | TRUNCATE (explicit) — the single owner of periodic WAL truncation |
com.alai.rag-bookstack-adapter |
tools/rag-bookstack-adapter.js |
StartInterval=300s | Fresh process per run | PASSIVE (default) |
com.alai.rag-mc-adapter |
tools/rag-mc-adapter.js |
StartInterval=300s | Fresh process per run | PASSIVE (default) |
com.alai.rag-fsevents-adapter |
tools/rag-fsevents-adapter.js |
WatchPaths (evidence/, specs/, rules/) |
Fresh process per fs-event | PASSIVE (default) |
com.alai.lightrag-outbox-ingest |
tools/lightrag-outbox-ingest.js |
StartInterval=21600s (6h) | Fresh process per run | PASSIVE (default) |
com.john.outbox-processor (daemons/outbox-processor.js) is not a writer to this DB — it polls unrelated outbox tables in durable-runner.db, mission-control.db, drafts.db.
Drain Worker Rate Configuration (MC #101501, 2026-07-29)
com.alai.rag-drain-worker is deployed with:
| Setting | Value | Source |
|---|---|---|
MAX_UPLOADS_PER_MINUTE |
20 |
~/Library/LaunchAgents/com.alai.rag-drain-worker.plist and ~/system/config/launchagents/com.alai.rag-drain-worker.plist |
DRAIN_INTERVAL_MS |
10000 |
LaunchAgent environment |
BATCH_SIZE |
5 |
~/system/tools/rag-drain-worker.js default |
MAX_CONCURRENT |
1 |
LaunchAgent environment; serial uploads remain the design |
rag-drain-worker.js now treats DRAIN_INTERVAL_MS as the deployed alias for DRAIN_SLEEP_MS. Effective behavior: batch-5 every 10s can burst near 30 docs/min, while the token bucket caps steady-state uploads at 20 docs/min. This implements the MC #101501 increase from the previous deployed cap of 10 docs/min without adding concurrency.
Verification command after edits/restarts:
launchctl print "gui/$(id -u)/com.alai.rag-drain-worker" | grep -E 'state =|pid =|MAX_UPLOADS_PER_MINUTE|DRAIN_INTERVAL_MS'
tail -12 ~/system/logs/rag-drain-worker.log
Expected evidence lines:
MAX_UPLOADS_PER_MINUTE => 20
DRAIN_INTERVAL_MS => 10000
[drain] Rate config: max_uploads_per_min=20 batch_size=5 drain_sleep_ms=10000
Root Cause (MC #106047)
rag-outbox.js's own header comment says "Do NOT require() this from two processes simultaneously" — the design assumed a single writer. The deployed topology has five. openOutbox() unconditionally ran PRAGMA wal_checkpoint(TRUNCATE) on every open, with no busy_timeout set anywhere in the file.
TRUNCATE mode requires exclusive WAL access to zero out the WAL file. The four short-lived adapters each open a brand-new connection (and therefore force a new TRUNCATE checkpoint) on every invocation — for rag-fsevents-adapter this can mean several times per minute during write bursts (1,965+ file touches observed under evidence/ alone since 2026-07-15). Meanwhile rag-drain-worker holds one long-lived WAL connection open continuously. Without busy_timeout, contending checkpoints don't wait each other out — and a TRUNCATE checkpoint that's interrupted or races another process's checkpoint on the same WAL/shm can leave the b-tree in an inconsistent state. This matches the observed corruption signature exactly (tree2/page2 btree errors).
The Jul 20 event was silent for 5 days (corruption occurred ~Jul 15 12:57 per file mtime, discovered manually by John on Jul 20) — proof no integrity guard existed before this fix.
Confirmed not the cause: unclean shutdown/reboot (no reboot near either corruption window per last reboot), disk pressure (6% used, 198Gi free at time of investigation).
Fix (applied 2026-07-20)
File: ~/system/lib/rag-outbox.js
- Added
db.pragma('busy_timeout = 5000')right after thejournal_mode/synchronouspragmas, so a connection waits up to 5s for a lock instead of failing/racing immediately. openOutbox(dbPath, opts)now acceptsopts.checkpointMode('PASSIVE'default, or'TRUNCATE').PASSIVEnever blocks or forces exclusivity — it checkpoints whatever it safely can.
File: ~/system/tools/rag-drain-worker.js
- Its
openOutbox(DB_PATH)call now passes{ checkpointMode: 'TRUNCATE' }explicitly — it remains the single designated owner of periodic WAL truncation, per the original single-writer spec.
All four other callers (rag-bookstack-adapter.js, rag-mc-adapter.js, rag-fsevents-adapter.js, lightrag-outbox-ingest.js) pass no second argument and therefore get the new PASSIVE default automatically — no code changes needed in those files.
Verification: all 5 writer daemons were restarted via launchctl kickstart -k gui/<uid>/<label> (NOT nohup — orphaned nohup processes hold the SQLite lock and are a known trap on this system). Post-restart: rag-drain-worker confirmed alive and actively draining (LightRAG health OK, pipeline_busy=true); no new SQLITE_CORRUPT/malformed entries in any .err log; node ~/system/tools/rag-ingest-integrity-watchdog.js --no-alert returned integrity=ok quick=ok.
Integrity Watchdog (guard)
~/system/tools/rag-ingest-integrity-watchdog.js runs PRAGMA integrity_check + PRAGMA quick_check against the live DB (read-only), writes Prometheus metrics to ~/system/metrics/rag-ingest-integrity.prom, appends JSONL events to ~/system/logs/rag-ingest-integrity-events.jsonl, and alerts Slack (#alerts) + HiveMind on failure with a 4h cooldown. It never mutates the queue DB.
LaunchAgent staged at ~/Library/LaunchAgents/com.alai.rag-ingest-integrity-watchdog.plist (StartInterval=900s / 15 min, RunAtLoad, plutil -lint validated). Not yet loaded — persistent daemon registration is an outward-facing action; loading it requires CEO go-ahead, tracked separately by John.
Manual check any time:
node ~/system/tools/rag-ingest-integrity-watchdog.js # alerts on failure
node ~/system/tools/rag-ingest-integrity-watchdog.js --no-alert # dry-run, no Slack/HiveMind
node ~/system/tools/rag-ingest-integrity-watchdog.js --json # machine-readable
Housekeeping (deferred)
Stale corruption/backup snapshots from Apr 23 and May 9 (4 files, ~46M, in ~/system/state/) are superseded by two later recovery cycles and are deletion candidates, but were not deleted — disk is at 6% (no pressure) and John did not create these files, so deletion is held pending explicit CEO/John confirmation. Today's pair (.corrupt-20260720-0825, .pre-recover-20260720-0825, ~91M) is kept as forensic evidence until MC #106047 closes.
Follow-up
MC #106066 — rag-fsevents-adapter.js has no debounce on WatchPaths bursts; every single fs event spawns a fresh process + fresh openOutbox() call. Even with PASSIVE checkpoint + busy_timeout landed, this remains unnecessary connection churn on the shared outbox DB during evidence-writing storms. Recommend a 2-5s debounce/coalesce window before triggering an enqueue pass.
No comments to display
No comments to display