Skip to content

fix: harden SQLite lock handling and WAL retention - #335

Merged
edheltzel merged 4 commits into
mainfrom
harden/sqlite-pragmas
Oct 17, 2026
Merged

edheltzel merged 4 commits into
mainfrom
harden/sqlite-pragmas

Conversation

@Atlas-Key

Copy link
Copy Markdown
Collaborator

What Changed

  • Set a 5-second busy timeout before WAL setup across CLI/MCP and hook connections, and configure writers to retain at most 64 MiB of WAL space after log resets.
  • Remove redundant published-message view recreation from schema initialization.
  • Add pragma and concurrent recovery regression coverage, extend the locked-extraction test timeout, and document WAL contention and retention behavior.

Risk Assessment

⚠️ Medium: The runtime change is bounded with no substantiated correctness regression, but duplicated policy and invalid test evidence warrant follow-up.

Testing

Installed locked dependencies, built CLI/MCP runtime, ran focused SQLite checks, and drove eight isolated product scenarios successfully, including 20 crash-recovery rounds. Saved transcripts and database-state evidence, then cleaned disposable data and build output. Extraction used a local protocol fixture; the WAL comparison used an unlimited-policy control on the same product.

  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Initialize Recall, add a decision, and find it after reopening ✅ pass live Product transcript; Persisted state
Write through MCP while a reader pins its snapshot, then reclaim oversized WAL after reader release ✅ pass live MCP WAL comparison: target shrank to 67,108,864 bytes; unlimited control retained 73,937,552 bytes; all 43 records persisted
Add memory while another process briefly locks the database ✅ pass live Product transcript: CLI addition completed after the peer released its startup lock
Retry PreCompact after a database timeout without losing or duplicating messages ✅ pass live Product transcript; Persisted state: timeout preserved the watermark, retry saved one message, replay added none
Recover a killed WAL writer and retain committed data plus every concurrent CLI addition ✅ pass live Product transcript; bun test tests/db/recovery-concurrency.test.ts: 20 rounds of six concurrent CLI additions, with integrity and FTS checks
Run lifecycle hooks and replay extraction with one extracted decision, preserved transcript, recorded progress, and no remaining locks ✅ pass live Product transcript; Persisted state; Runtime SQLite settings
Start a session and recall durable memory while sweeping expired breadcrumbs ✅ pass live Product transcript; Persisted state: recalled decision displayed and expired breadcrumb removed
Consolidate old decisions while preserving their source lineage ✅ pass live Product transcript; Persisted state: one derived summary references three demoted source decisions
Evidence: Product transcript
Live Recall SQLite validation. Runtime data is disposable and isolated under the worktree.

SCENARIO: CLI initializes memory, persists additions, and finds them after reopening
{
  "HOME": "~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/normal/home",
  "RECALL_HOME": "~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/normal/home/.agents/Recall",
  "RECALL_DB_PATH": "~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/normal/recall.db"
}
$ bun dist/index.js init
{
  "exit": 0,
  "stdout": "✓ Created database at ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/normal/recall.db\n",
  "stderr": ""
}
$ bun dist/index.js add decision SQLITE_GATE durable memory survives reopen --project sqlite-gate
{
  "exit": 0,
  "stdout": "✓ Added decision #1 [sqlite-gate] (medium)\n",
  "stderr": ""
}
$ bun dist/index.js search SQLITE_GATE --project sqlite-gate
{
  "exit": 0,
  "stdout": "Found 1 result(s):\n\n[decisions#1] [sqlite-gate] 2026-10-17 11:42:09\n  SQLITE_GATE durable memory survives reopen\n\n",
  "stderr": ""
}
{
  "persisted": [
    {
      "decision": "SQLITE_GATE durable memory survives reopen",
      "provenance": "user_authored"
    }
  ]
}

SCENARIO: CLI and PreCompact wait for database startup locks; timed-out PreCompact retries without losing messages
$ bun dist/index.js init
{
  "exit": 0,
  "stdout": "✓ Created database at ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/recall.db\n",
  "stderr": ""
}
$ bun dist/index.js add breadcrumb STARTUP_LOCK memory saved after peer releases
{
  "peer": "LOCKED 1792237329648\nRELEASING 1792237330549"
}
{
  "exit": 0,
  "stdout": "✓ Added breadcrumb #1 [project]\n",
  "stderr": ""
}
$ bun hooks/RecallPreCompact.ts
{
  "exit": 0,
  "stdout": "",
  "stderr": ""
}
{
  "peer": "LOCKED 1792237330698\nRELEASING 1792237345702"
}
$ bun hooks/RecallPreCompact.ts
{
  "exit": 0,
  "stdout": "",
  "stderr": ""
}
$ bun hooks/RecallPreCompact.ts
{
  "exit": 0,
  "stdout": "",
  "stderr": ""
}
{
  "hookLog": "[2026-10-17T11:42:17.527Z] FLUSH_ERROR: ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/locked-session.jsonl: database is locked\n[2026-10-17T11:42:25.754Z] PRECOMPACT_FIRED: trigger=manual conv=~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/locked-session.jsonl\n[2026-10-17T11:42:25.765Z] FLUSHED: ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/locked-session.jsonl +1 messages (200 bytes, watermark 0 → 200)\n[2026-10-17T11:42:25.792Z] PRECOMPACT_FIRED: trigger=manual conv=~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/locked-session.jsonl\n[2026-10-17T11:42:25.793Z] NO_NEW_DATA: ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/locked-session.jsonl (size=200, watermark=200)\n",
  "persisted": [
    {
      "session_id": "locked-session",
      "content": "LOCK_TIMEOUT this transcript must survive the failed compact flush and retry.",
      "provenance": "verbatim"
    }
  ]
}

SCENARIO: Concurrent CLI writers recover a crashed WAL without losing committed data or publishing uncommitted data
$ bun dist/index.js init
{
  "exit": 0,
  "stdout": "✓ Created database at ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/crash-recovery-1792237345799/recall.db\n",
  "stderr": ""
}
$ bun dist/index.js add breadcrumb RECOVERY_WRITER_0
$ bun dist/index.js add breadcrumb RECOVERY_WRITER_1
$ bun dist/index.js add breadcrumb RECOVERY_WRITER_2
$ bun dist/index.js add breadcrumb RECOVERY_WRITER_3
$ bun dist/index.js add breadcrumb RECOVERY_WRITER_4
$ bun dist/index.js add breadcrumb RECOVERY_WRITER_5
{
  "exit": 0,
  "stdout": "✓ Added breadcrumb #2 [project]\n",
  "stderr": ""
}
{
  "exit": 0,
  "stdout": "✓ Added breadcrumb #3 [project]\n",
  "stderr": ""
}
{
  "exit": 0,
  "stdout": "✓ Added breadcrumb #4 [project]\n",
  "stderr": ""
}
{
  "exit": 0,
  "stdout": "✓ Added breadcrumb #5 [project]\n",
  "stderr": ""
}
{
  "exit": 0,
  "stdout": "✓ Added breadcrumb #6 [project]\n",
  "stderr": ""
}
{
  "exit": 0,
  "stdout": "✓ Added breadcrumb #7 [project]\n",
  "stderr": ""
}
{
  "persisted": [
    {
      "content": "CRASH_COMMITTED memory"
    },
    {
      "content": "RECOVERY_WRITER_0"
    },
    {
      "content": "RECOVERY_WRITER_1"
    },
    {
      "content": "RECOVERY_WRITER_2"
    },
    {
      "content": "RECOVERY_WRITER_3"
    },
    {
      "content": "RECOVERY_WRITER_4"
    },
    {
      "content": "RECOVERY_WRITER_5"
    }
  ],
  "integrity": [
    {
      "integrity_check": "ok"
    }
  ]
}
{
  "results": [
    {
      "name": "CLI initializes memory, persists additions, and finds them after reopening",
      "result": "pass",
      "live": true,
      "detail": [
        {
          "decision": "SQLITE_GATE durable memory survives reopen",
          "provenance": "user_authored"
        }
      ]
    },
    {
      "name": "CLI and PreCompact wait for database startup locks; timed-out PreCompact retries without losing messages",
      "result": "pass",
      "live": true,
      "detail": {
        "startupWriteFinished": 1792237330684,
        "peerReleased": 1792237330549,
        "persisted": [
          {
            "session_id": "locked-session",
            "content": "LOCK_TIMEOUT this transcript must survive the failed compact flush and retry.",
            "provenance": "verbatim"
          }
        ],
        "hookLog": "[2026-10-17T11:42:17.527Z] FLUSH_ERROR: ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/locked-session.jsonl: database is locked\n[2026-10-17T11:42:25.754Z] PRECOMPACT_FIRED: trigger=manual conv=~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/locked-session.jsonl\n[2026-10-17T11:42:25.765Z] FLUSHED: ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/locked-session.jsonl +1 messages (200 bytes, watermark 0 → 200)\n[2026-10-17T11:42:25.792Z] PRECOMPACT_FIRED: trigger=manual conv=~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/locked-session.jsonl\n[2026-10-17T11:42:25.793Z] NO_NEW_DATA: ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/locked-session.jsonl (size=200, watermark=200)\n"
      }
    },
    {
      "name": "Concurrent CLI writers recover a crashed WAL without losing committed data or publishing uncommitted data",
      "result": "pass",
      "live": true,
      "detail": {
        "persisted": [
          {
            "content": "CRASH_COMMITTED memory"
          },
          {
            "content": "RECOVERY_WRITER_0"
          },
          {
            "content": "RECOVERY_WRITER_1"
          },
          {
            "content": "RECOVERY_WRITER_2"
          },
          {
            "content": "RECOVERY_WRITER_3"
          },
          {
            "content": "RECOVERY_WRITER_4"
          },
          {
            "content": "RECOVERY_WRITER_5"
          }
        ],
        "integrity": [
          {
            "integrity_check": "ok"
          }
        ]
      }
    }
  ]
}

SCENARIO: Stop, PreCompact, in-session progress, SessionStart, and consolidation preserve memory with hardened SQLite settings
$ bun dist/index.js init
{
  "exit": 0,
  "stdout": "✓ Created database at ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/lifecycle/recall.db\n",
  "stderr": ""
}
$ bun hooks/RecallExtract.ts
{
  "exit": 0,
  "stdout": "",
  "stderr": ""
}
$ bun hooks/RecallExtract.ts --reextract .sqlite-gate-runtime/lifecycle/home/.c

... [2042 bytes truncated] ...

\n",
  "stderr": ""
}
$ bun dist/index.js add decision CONSOLIDATE_GATE old low-importance decision 1 --project project
{
  "exit": 0,
  "stdout": "✓ Added decision #3 [project] (medium)\n",
  "stderr": ""
}
$ bun dist/index.js add decision CONSOLIDATE_GATE old low-importance decision 2 --project project
{
  "exit": 0,
  "stdout": "✓ Added decision #4 [project] (medium)\n",
  "stderr": ""
}
$ bun dist/index.js consolidate --execute --table decisions --min-cluster-size 2
{
  "exit": 0,
  "stdout": "[EXECUTE — consolidating records]\n\nRecommended: run 'recall export --backup' before consolidating.\n\nAge cutoff: 180d  ·  importance threshold: <5  ·  window: 30d  ·  min cluster: 2\n\ndecisions [project] window 2019-12-10: 3 record(s) → demote importance to 1 (rep #2; ids: 2, 3, 4)\n\nTotal: 1 cluster(s), 3 record(s) to consolidate\n\nConsolidated: wrote 1 derived summary(ies), demoted 3 source record(s).\n",
  "stderr": "[FabricExtract] Trying ollama extraction...\n[FabricExtract] Ollama extraction successful\nRECALL_CONSOLIDATE_PROGRESS {\"written\":1,\"demoted\":3}\n"
}
{
  "externalModelBoundary": "disposable localhost Ollama protocol fixture; no model-quality claim",
  "requests": 3,
  "tracker": [
    {
      "conversation_path": "~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/lifecycle/home/.claude/projects/-Users-ed--no-mistakes-worktrees-fd32172f1538-01M52TP6ZQ2ARNP0T3RN3BBG9G--sqlite-gate-runtime-lifecycle-project/lifecycle-session.jsonl",
      "extracted_at": "2026-10-17 11:42:26",
      "error": null
    },
    {
      "conversation_path": "~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/lifecycle/old-session.jsonl",
      "extracted_at": "2026-01-01T00:00:00Z",
      "error": null
    }
  ],
  "progress": [
    {
      "session_id": "lifecycle-session",
      "turns_seen": 1
    }
  ],
  "derived": [
    {
      "title": "Consolidated decisions — project (2019-12-10)",
      "provenance": "derived",
      "source_ids": "[{\"table\":\"decisions\",\"id\":2},{\"table\":\"decisions\",\"id\":3},{\"table\":\"decisions\",\"id\":4}]"
    }
  ],
  "hookLog": "[2026-10-17T11:42:26.228Z] TRACKER_MIGRATE: imported 1 rows from JSON tracker into SQLite\n[2026-10-17T11:42:26.231Z] SPAWNED: Self-extract PID 78227 for ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/lifecycle/home/.claude/projects/-Users-ed--no-mistakes-worktrees-fd32172f1538-01M52TP6ZQ2ARNP0T3RN3BBG9G--sqlite-gate-runtime-lifecycle-project/lifecycle-session.jsonl\n[2026-10-17T11:42:26.244Z] BACKGROUND: Starting extraction for ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/lifecycle/home/.claude/projects/-Users-ed--no-mistakes-worktrees-fd32172f1538-01M52TP6ZQ2ARNP0T3RN3BBG9G--sqlite-gate-runtime-lifecycle-project/lifecycle-session.jsonl\n[2026-10-17T11:42:26.282Z] QUALITY GATE PASSED: extraction contains required sections\n[2026-10-17T11:42:26.282Z] SQLITE_WRITE: sessions=1 decisions=1 learnings=1 breadcrumbs=3 errors=1 loa=1\n[2026-10-17T11:42:26.282Z] SCRUB: redacted 0 secret kind(s) before SQLite write\n[2026-10-17T11:42:26.285Z] SUCCESS: All memory files updated for session=project\n[2026-10-17T11:42:26.285Z] BACKGROUND: Extraction complete\n[2026-10-17T11:42:26.307Z] REEXTRACT: Forcing re-extraction of ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/lifecycle/home/.claude/projects/-Users-ed--no-mistakes-worktrees-fd32172f1538-01M52TP6ZQ2ARNP0T3RN3BBG9G--sqlite-gate-runtime-lifecycle-project/lifecycle-session.jsonl\n[2026-10-17T11:42:26.343Z] QUALITY GATE PASSED: extraction contains required sections\n[2026-10-17T11:42:26.343Z] SQLITE_WRITE: sessions=1 decisions=0 learnings=0 breadcrumbs=0 errors=1 loa=1\n[2026-10-17T11:42:26.343Z] SCRUB: redacted 0 secret kind(s) before SQLite write\n[2026-10-17T11:42:26.346Z] SUCCESS: All memory files updated for session=project\n[2026-10-17T11:42:26.346Z] REEXTRACT: Complete\n"
}
{
  "results": [
    {
      "name": "CLI initializes memory, persists additions, and finds them after reopening",
      "result": "pass",
      "live": true,
      "detail": [
        {
          "decision": "SQLITE_GATE durable memory survives reopen",
          "provenance": "user_authored"
        }
      ]
    },
    {
      "name": "CLI and PreCompact wait for database startup locks; timed-out PreCompact retries without losing messages",
      "result": "pass",
      "live": true,
      "detail": {
        "startupWriteFinished": 1792237330684,
        "peerReleased": 1792237330549,
        "persisted": [
          {
            "session_id": "locked-session",
            "content": "LOCK_TIMEOUT this transcript must survive the failed compact flush and retry.",
            "provenance": "verbatim"
          }
        ],
        "hookLog": "[2026-10-17T11:42:17.527Z] FLUSH_ERROR: ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/locked-session.jsonl: database is locked\n[2026-10-17T11:42:25.754Z] PRECOMPACT_FIRED: trigger=manual conv=~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/locked-session.jsonl\n[2026-10-17T11:42:25.765Z] FLUSHED: ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/locked-session.jsonl +1 messages (200 bytes, watermark 0 → 200)\n[2026-10-17T11:42:25.792Z] PRECOMPACT_FIRED: trigger=manual conv=~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/locked-session.jsonl\n[2026-10-17T11:42:25.793Z] NO_NEW_DATA: ~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/startup-locks/locked-session.jsonl (size=200, watermark=200)\n"
      }
    },
    {
      "name": "Concurrent CLI writers recover a crashed WAL without losing committed data or publishing uncommitted data",
      "result": "pass",
      "live": true,
      "detail": {
        "persisted": [
          {
            "content": "CRASH_COMMITTED memory"
          },
          {
            "content": "RECOVERY_WRITER_0"
          },
          {
            "content": "RECOVERY_WRITER_1"
          },
          {
            "content": "RECOVERY_WRITER_2"
          },
          {
            "content": "RECOVERY_WRITER_3"
          },
          {
            "content": "RECOVERY_WRITER_4"
          },
          {
            "content": "RECOVERY_WRITER_5"
          }
        ],
        "integrity": [
          {
            "integrity_check": "ok"
          }
        ]
      }
    },
    {
      "name": "Stop, PreCompact, in-session progress, SessionStart, and consolidation preserve memory with hardened SQLite settings",
      "result": "pass",
      "live": true,
      "detail": {
        "modelBoundary": "local protocol fixture",
        "requests": 3,
        "tracker": [
          {
            "conversation_path": "~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/lifecycle/home/.claude/projects/-Users-ed--no-mistakes-worktrees-fd32172f1538-01M52TP6ZQ2ARNP0T3RN3BBG9G--sqlite-gate-runtime-lifecycle-project/lifecycle-session.jsonl",
            "extracted_at": "2026-10-17 11:42:26",
            "error": null
          },
          {
            "conversation_path": "~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/lifecycle/old-session.jsonl",
            "extracted_at": "2026-01-01T00:00:00Z",
            "error": null
          }
        ],
        "progress": [
          {
            "session_id": "lifecycle-session",
            "turns_seen": 1
          }
        ],
        "derived": [
          {
            "title": "Consolidated decisions — project (2019-12-10)",
            "provenance": "derived",
            "source_ids": "[{\"table\":\"decisions\",\"id\":2},{\"table\":\"decisions\",\"id\":3},{\"table\":\"decisions\",\"id\":4}]"
          }
        ]
      }
    }
  ]
}
Evidence: MCP WAL comparison
{
  "result": "pass",
  "live": true,
  "control": {
    "policy": "unlimited control",
    "pinnedBytes": 73900472,
    "resetBytes": 73937552,
    "pinnedSnapshotRows": 1,
    "durableRows": 43,
    "added": 40,
    "integrity": {
      "integrity_check": "ok"
    },
    "stderr": "RECALL MCP server running\n"
  },
  "target": {
    "policy": "target",
    "pinnedBytes": 73900472,
    "resetBytes": 67108864,
    "pinnedSnapshotRows": 1,
    "durableRows": 43,
    "added": 40,
    "integrity": {
      "integrity_check": "ok"
    },
    "stderr": "RECALL MCP server running\n"
  },
  "note": "Control uses the same real product with only journal_size_limit overridden to -1; it is not a base-commit run."
}
Evidence: Persisted state
{
  "normal": {
    "decisions": [
      {
        "decision": "SQLITE_GATE durable memory survives reopen",
        "project": "sqlite-gate",
        "provenance": "user_authored"
      }
    ]
  },
  "startup-locks": {
    "retried_messages": [
      {
        "session_id": "locked-session",
        "content": "LOCK_TIMEOUT this transcript must survive the failed compact flush and retry.",
        "provenance": "verbatim"
      }
    ]
  },
  "lifecycle": {
    "extracted_decisions": [
      {
        "decision": "HOOK_MEMORY use the shared database for durable extracted decisions",
        "provenance": "extracted"
      }
    ],
    "tracker": [
      {
        "conversation_path": "~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/lifecycle/old-session.jsonl",
        "extracted_at": "2026-01-01T00:00:00Z",
        "error": null
      },
      {
        "conversation_path": "~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/.sqlite-gate-runtime/lifecycle/home/.claude/projects/-Users-ed--no-mistakes-worktrees-fd32172f1538-01M52TP6ZQ2ARNP0T3RN3BBG9G--sqlite-gate-runtime-lifecycle-project/lifecycle-session.jsonl",
        "extracted_at": "2026-10-17 11:42:26",
        "error": null
      }
    ],
    "remaining_locks": [
      {
        "n": 0
      }
    ],
    "messages": [
      {
        "session_id": "lifecycle-session",
        "role": "user",
        "provenance": "verbatim"
      },
      {
        "session_id": "lifecycle-session",
        "role": "assistant",
        "provenance": "verbatim"
      }
    ],
    "progress": [
      {
        "session_id": "lifecycle-session",
        "turns_seen": 1
      }
    ],
    "expired_breadcrumbs": [
      {
        "n": 0
      }
    ],
    "consolidated_sources": [
      {
        "decision": "CONSOLIDATE_GATE old low-importance decision 0",
        "importance": 1
      },
      {
        "decision": "CONSOLIDATE_GATE old low-importance decision 1",
        "importance": 1
      },
      {
        "decision": "CONSOLIDATE_GATE old low-importance decision 2",
        "importance": 1
      }
    ],
    "consolidated_summary": [
      {
        "title": "Consolidated decisions \u2014 project (2019-12-10)",
        "provenance": "derived",
        "source_ids": "[{\"table\":\"decisions\",\"id\":2},{\"table\":\"decisions\",\"id\":3},{\"table\":\"decisions\",\"id\":4}]"
      }
    ]
  }
}
Evidence: Runtime SQLite settings
{
  "runtime_writer_observations": {
    "at closeDb (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/dist/chunk-LISQL7NL.js:3313:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 2,
      "wal_autocheckpoint": 1000
    },
    "at flushConversation (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/RecallPreCompact.ts:393:10)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at migrateTrackerJson (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/extraction-migration.ts:99:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at getExtractionRecord (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/extraction-tracker.ts:33:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at acquireSemaphore (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/extraction-semaphore.ts:146:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at ensureDbWritable (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/sqlite-writers.ts:457:10)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at writeExtractionSession (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/sqlite-writers.ts:134:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at writeDecisionsBatch (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/sqlite-writers.ts:202:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at writeLearningsBatch (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/sqlite-writers.ts:281:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at writeBreadcrumbsBatch (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/sqlite-writers.ts:335:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at writeExtractionErrors (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/sqlite-writers.ts:441:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at writeLoaEntryFromExtraction (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/sqlite-writers.ts:391:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at markAsExtracted (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/extraction-tracker.ts:44:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at childReleaseLock (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/extraction-lock.ts:80:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at releaseSemaphore (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/extraction-semaphore.ts:159:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at clearTrackerRow (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/RecallExtract.ts:864:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at childAcquireLock (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/extraction-lock.ts:60:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at ensureSession (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/session-progress.ts:74:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at incrementTurns (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/session-progress.ts:86:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at getSessionProgress (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/session-progress.ts:52:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    },
    "at sweepExpiredBreadcrumbs (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/dist/index.js:2101:10)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 2,
      "wal_autocheckpoint": 1000
    },
    "at applyConsolidation (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/hooks/lib/consolidate-core.ts:211:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": 67108864,
      "synchronous": 1,
      "wal_autocheckpoint": 1000
    }
  },
  "excluded_from_writer_assertions": {
    "at ingestHostTranscriptBatch (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/dist/index.js:3548:14)": {
      "busy_timeout": 0,
      "journal_mode": "memory",
      "journal_size_limit": -1,
      "synchronous": 0
    },
    "already_closed_connections": "A second close cannot expose PRAGMA values; successful first-close observations are retained.",
    "at tableColumns (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/dist/index.js:2112:8)": {
      "busy_timeout": 0,
      "journal_mode": "wal",
      "journal_size_limit": -1,
      "synchronous": 2
    },
    "at queryDb (~/.no-mistakes/worktrees/fd32172f1538/01M52TP6ZQ2ARNP0T3RN3BBG9G/dist/index.js:2054:8)": {
      "busy_timeout": 5000,
      "journal_mode": "wal",
      "journal_size_limit": -1,
      "synchronous": 2
    }
  },
  "note": "Values read from actual SQLite connections at close during live CLI and hook execution; they supplement persisted-state and CLI evidence."
}
- Outcome: ⚠️ 1 info across 2 runs (19m28s)

Pipeline

Updates from git push no-mistakes

⏭️ **intent** - skipped

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 2 warnings
  • ⚠️ src/db/connection.ts:30 - Simplification: harden: bound WAL growth — centralize journal_size_limit across all Recall DB writers #192 explicitly requests a shared pragma helper, but the new journal-limit policy has ten separate hook copies alongside this constant. No requirement needs these parallel definitions. Remove the copies by defining the common setup under hooks/lib and consuming it from both CLI and hooks. Other sites: hooks/RecallExtract.ts:862, hooks/RecallPreCompact.ts:346, hooks/lib/consolidate-core.ts:108, hooks/lib/extraction-lock.ts:18, hooks/lib/extraction-migration.ts:12, hooks/lib/extraction-semaphore.ts:15, hooks/lib/extraction-tracker.ts:11, hooks/lib/session-progress.ts:18, hooks/lib/session-start-context.ts:93, hooks/lib/sqlite-writers.ts:59.
  • ⚠️ tests/hooks/extract-core.test.ts:488 - The edited suite retains two source-content-only tests: these assertions inspect RecallExtract.ts text, and tests/hooks/extract-core.test.ts:499 inspects extract-core.ts text. Neither executes the claimed behavior; commented or unreachable calls can satisfy the positive assertions. Under the explicit test-quality rule for directly encountered tests, remove both tests or replace them with isolated execution that asserts actual files and side effects.
⚠️ **Test** - 1 info
  • 🚨 src/lib/memory.ts:295 - Concurrent crash recovery intermittently rejects a real recall add breadcrumb command with SQLITE_SCHEMA: vtable constructor failed: breadcrumbs_fts. One of seven attempts without instrumentation persisted only five of six requested breadcrumbs. Concurrent recovery remains unreliable; whether this predates the change was not established.
  • 🚨 live validation verdict: no-go (8 of 8 scenarios were driven live against the product); failed: Recover a killed WAL writer and accept all six concurrent CLI additions
  • Live validation: ❌ no-go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Initialize Recall, add a decision, and find it after reopening ✅ pass live cli-and-lock-evidence.log
Write through MCP while a reader pins the WAL; preserve its snapshot and reclaim excess WAL after release ✅ pass live mcp-wal-results.json
Add memory while another process holds a startup lock; save it after the lock releases ✅ pass live cli-and-lock-evidence.log
Retry PreCompact after a lock timeout; persist one message without prematurely advancing the watermark ✅ pass live cli-and-lock-evidence.log
End and re-extract a session; migrate its tracker, preserve extracted memory, and release extraction locks ✅ pass live lifecycle-product-transcript.log; hook-runtime-pragmas.json
Submit a prompt and resume the session; retain progress, remove expired memory, and render captured context ✅ pass live lifecycle-product-transcript.log; hook-runtime-pragmas.json
Consolidate three old decisions into one derived summary while retaining and demoting their sources ✅ pass live lifecycle-product-transcript.log
Recover a killed WAL writer and accept all six concurrent CLI additions ❌ fail live recovery-failure.log
  • bun install --frozen-lockfile
  • ./node_modules/.bin/tsup src/index.ts --format esm --external bun:sqlite
  • ./node_modules/.bin/tsup src/mcp-server.ts --format esm --external bun:sqlite
  • bun test tests/db/pragmas.test.ts tests/hooks/sqlite-writers-concurrency.test.ts with isolated HOME and Recall paths
  • bun test tests/hooks/extract-core.test.ts --test-name-pattern &#39;exclusively locked&#39; with isolated HOME and Recall paths
  • bun ~/.no-mistakes/evidence/01M52TP6ZQ2ARNP0T3RN3BBG9G/validate-sqlite.ts
  • bun ~/.no-mistakes/evidence/01M52TP6ZQ2ARNP0T3RN3BBG9G/validate-sqlite.ts --hooks-only after correcting fixture setup
  • bun ~/.no-mistakes/evidence/01M52TP6ZQ2ARNP0T3RN3BBG9G/validate-sqlite.ts --only=&#39;Concurrent CLI&#39; for seven uninstrumented recovery attempts
  • bun ~/.no-mistakes/evidence/01M52TP6ZQ2ARNP0T3RN3BBG9G/validate-mcp-wal.ts
  • SQLite persisted-state and integrity readbacks; removed disposable databases, build output, and locally installed test dependencies

🔧 Fix applied.
1 info still open:

  • ℹ️ tests/db/recovery-concurrency.test.ts - new test file written by agent: tests/db/recovery-concurrency.test.ts
  • Live validation: ✅ go - 8 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Initialize Recall, add a decision, and find it after reopening ✅ pass live Product transcript; Persisted state
Write through MCP while a reader pins its snapshot, then reclaim oversized WAL after reader release ✅ pass live MCP WAL comparison: target shrank to 67,108,864 bytes; unlimited control retained 73,937,552 bytes; all 43 records persisted
Add memory while another process briefly locks the database ✅ pass live Product transcript: CLI addition completed after the peer released its startup lock
Retry PreCompact after a database timeout without losing or duplicating messages ✅ pass live Product transcript; Persisted state: timeout preserved the watermark, retry saved one message, replay added none
Recover a killed WAL writer and retain committed data plus every concurrent CLI addition ✅ pass live Product transcript; bun test tests/db/recovery-concurrency.test.ts: 20 rounds of six concurrent CLI additions, with integrity and FTS checks
Run lifecycle hooks and replay extraction with one extracted decision, preserved transcript, recorded progress, and no remaining locks ✅ pass live Product transcript; Persisted state; Runtime SQLite settings
Start a session and recall durable memory while sweeping expired breadcrumbs ✅ pass live Product transcript; Persisted state: recalled decision displayed and expired breadcrumb removed
Consolidate old decisions while preserving their source lineage ✅ pass live Product transcript; Persisted state: one derived summary references three demoted source decisions
  • bun install --frozen-lockfile --ignore-scripts
  • bun run --bun tsup src/index.ts src/mcp-server.ts --format esm --clean --external bun:sqlite
  • bun test tests/db/pragmas.test.ts tests/db/recovery-concurrency.test.ts tests/hooks/sqlite-writers-concurrency.test.ts with isolated HOME, runtime paths, and database
  • bun test tests/hooks/extract-core.test.ts --test-name-pattern 'exclusively locked'
  • bun ~/.no-mistakes/evidence/01M52TP6ZQ2ARNP0T3RN3BBG9G/round-2/validate-sqlite.ts
  • bun ~/.no-mistakes/evidence/01M52TP6ZQ2ARNP0T3RN3BBG9G/round-2/validate-mcp-wal.ts
  • Checked live connection PRAGMAs and queried persisted SQLite state; saved evidence before removing disposable databases and build output
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@Atlas-Key
Atlas-Key requested a review from edheltzel as a code owner October 17, 2026 11:47

@edheltzel edheltzel left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

64MiB should be large enough but we might consider bumping this to 128 or 256MiB per session run.

@edheltzel
edheltzel force-pushed the harden/sqlite-pragmas branch from 711054e to c82471e Compare October 17, 2026 14:12
@edheltzel
edheltzel merged commit a130387 into main Oct 17, 2026
3 checks passed
@edheltzel
edheltzel deleted the harden/sqlite-pragmas branch October 17, 2026 15:16
edheltzel pushed a commit that referenced this pull request Oct 17, 2026
* fix(db): busy_timeout before WAL and cap journal size on every writer (#212, #192)

* docs(db): journal_size_limit truncates after checkpoint, not a hard WAL cap (#192)

* no-mistakes(test): Prevent concurrent SQLite schema failures during crash recovery

* no-mistakes(document): Clarify SQLite locking and WAL retention documentation

---------

Co-authored-by: Atlas-Key <296298943+Atlas-Key@users.noreply.github.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.

2 participants