Skip to content

Fix stale FTS cache detection#100

Merged
Moskize91 merged 5 commits into
mainfrom
codex/fts-current-fingerprint-check
Jul 5, 2026
Merged

Fix stale FTS cache detection#100
Moskize91 merged 5 commits into
mainfrom
codex/fts-current-fingerprint-check

Conversation

@Moskize91

Copy link
Copy Markdown
Contributor

Summary

  • Treat stale external FTS caches as dirty when the source archive is missing, the FTS database is unreadable, or the indexed fingerprint no longer matches the archive.
  • Make archive search/index current checks use the stored FTS fingerprint instead of chapter revision.
  • Clear dirty FTS databases before rebuilding and let GC remove dirty FTS overlays without force.
  • Also includes follow-up CLI/job output commits from Tao.

Tests

  • pnpm test:run test/gc/gc.test.ts test/wikg/spine-digest-file.test.ts test/archive/query/archive-view.test.ts test/cli/archive.test.ts -- --runInBand
  • pnpm typecheck
  • pnpm format:check
  • pnpm lint

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

Summary by CodeRabbit

  • New Features

    • Added clearer help text for archive creation, including default non-overwrite behavior and the --replace option.
    • Expanded CLI guidance for output formats, with better explanations for text, JSON, and JSONL output.
    • Added job-level LLM configuration notes and clearer queue/job estimation details.
  • Bug Fixes

    • Improved archive search-index freshness checks so outdated indexes are detected more reliably.
    • Refined archive cleanup and cache handling to better avoid stale or invalid overlay data.

Walkthrough

This PR adds a --replace flag for archive creation with safe temp-file-and-rename semantics, refactors search-index freshness checking into a fingerprint-based status model (current/dirty/missing) with new archive-specific wrapper functions, and introduces a generation-planning module providing time/token cost estimates and performance hints used by both archive inspect and queue add commands. GC overlay cache eligibility is rewritten to use fingerprint comparison for dirtiness detection. Help documentation across multiple topics was updated to reflect the new create/replace semantics, job-local LLM overrides, and JSON/JSONL output-format guidance. Corresponding tests were added or updated across CLI, archive query, GC, and document modules.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as "CLI (archive create)"
    participant ArgsParser as "args.ts"
    participant FS as "Filesystem"
    participant Archive as "archive.ts"

    CLI->>ArgsParser: parse create --replace
    ArgsParser->>ArgsParser: rejectNonCreateReplaceFlag()
    ArgsParser-->>CLI: parsed args (replace: true/false)
    CLI->>Archive: createArchive(args)
    Archive->>FS: prepareCreateArchiveOutputPath()
    alt archive exists and no --replace
        Archive-->>CLI: throw "already exists" error
    else safe to write
        Archive->>FS: writeCreatedArchiveFile(outputPath)
        Archive->>FS: rename(tempPath, archivePath)
        Archive->>FS: rm(tempPath) [finally, if temp remains]
        Archive-->>CLI: success
    end
Loading
sequenceDiagram
    participant Caller as "findArchiveObjectsIndexed / rebuildArchiveSearchIndex"
    participant ArchiveView as "archive-view.ts"
    participant SearchIndex as "search-index.ts"
    participant DB as "search_index_state (DB)"

    Caller->>ArchiveView: isArchiveSearchIndexCurrent(document)
    ArchiveView->>SearchIndex: readSearchIndexStatus(document, input)
    SearchIndex->>SearchIndex: createSearchIndexFingerprint(input)
    SearchIndex->>DB: readSearchIndexFingerprintFromDatabase()
    DB-->>SearchIndex: stored fingerprint or undefined
    SearchIndex-->>ArchiveView: "current" | "dirty" | "missing"
    ArchiveView-->>Caller: boolean / status
    alt status is "dirty"
        Caller->>ArchiveView: deleteSearchIndexDatabase()
        Caller->>SearchIndex: ensureSearchIndex(input)
    end
Loading

Possibly related PRs

  • oomol-lab/wiki-graph#90: Both PRs modify archive search-index freshness/outdated checks and the rebuildArchiveSearchIndex flow in src/archive/query/archive-view.ts.
  • oomol-lab/wiki-graph#95: Both PRs touch the same query flow requiring a current FTS-backed search index for related/evidence queries via isArchiveSearchIndexCurrent.
  • oomol-lab/wiki-graph#97: Both PRs modify src/cli/archive.ts's inspect planning logic and token/wait-time estimation output.
🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Title check ⚠️ Warning The title is related to the change, but it does not follow the required <type>(<scope>): <subject> format. Rename it to something like fix(fts): detect stale FTS cache and keep the subject concise.
✅ Passed checks (3 passed)
Check name Status Explanation
Description check ✅ Passed The description matches the PR goals and mentions the FTS cache fix, fingerprint checks, GC behavior, and follow-up CLI changes.
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.
✨ Finishing Touches
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch codex/fts-current-fingerprint-check

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

@Moskize91 Moskize91 merged commit ae7bcb4 into main Jul 5, 2026
2 of 3 checks passed
@Moskize91 Moskize91 deleted the codex/fts-current-fingerprint-check branch July 5, 2026 10:39

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/wikg/wikg-coordinator.ts (1)

489-522: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy lift

Dirty-check extracts the full archive on every GC pass for each fts.db overlay.

isSqliteCacheOverlayDirty is evaluated before the TTL/force gates in canRemoveSqliteCacheOverlay (Line 489), so createCurrentArchiveSearchIndexFingerprint unzips the entire archive into a temp dir and rebuilds the search-index records (Line 567-583) even for freshly-updated, within-TTL overlays that would otherwise be retained cheaply. On routine/opportunistic GC over many or large archives this becomes a significant, repeated I/O and CPU cost.

Consider gating the expensive fingerprint computation using the already-persisted archive_signature (compare current archive signature vs the stored overlay signature, and only extract+fingerprint when the signature differs), preserving the "remove dirty regardless of TTL" semantics while avoiding extraction on unchanged archives.

Also applies to: 567-583

🤖 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 `@src/wikg/wikg-coordinator.ts` around lines 489 - 522, The dirty-check in
canRemoveSqliteCacheOverlay is doing an expensive archive fingerprint before the
TTL/force checks, so move the costly work behind a quick archive-signature
comparison. In isSqliteCacheOverlayDirty, use the persisted archive_signature on
the EntryOverlay to compare the current archive signature first, and only call
createCurrentArchiveSearchIndexFingerprint/readSearchIndexCacheStatus when the
signature has changed. Keep the existing semantics that dirty overlays are
removable regardless of TTL, but avoid extracting and rebuilding search-index
data for unchanged fts.db overlays.
src/cli/queue.ts (1)

1048-1062: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Performance-hint rendering diverges from archive.ts's equivalent formatter.

formatQueuePerformanceHintLines renders only message and Command per hint, while src/cli/archive.ts's formatInspectPerformanceHints also renders a Current {kind}: {current}; suggested: {recommended}. line for the same GenerationPerformanceHint shape. Users get less context in queue add output than in archive inspect output for identical hint data. Consider extracting a single shared line-formatter into generation-planning.ts to keep both surfaces consistent.

♻️ Suggested consolidation
-function formatQueuePerformanceHintLines(
-  hints: readonly GenerationPerformanceHint[],
-): readonly string[] {
-  if (hints.length === 0) {
-    return [];
-  }
-
-  return [
-    "Performance hints:",
-    ...hints.flatMap((hint) => [
-      `  ${hint.message}`,
-      `  Command: ${hint.command}`,
-    ]),
-  ];
-}
+function formatQueuePerformanceHintLines(
+  hints: readonly GenerationPerformanceHint[],
+): readonly string[] {
+  if (hints.length === 0) {
+    return [];
+  }
+
+  return [
+    "Performance hints:",
+    ...hints.flatMap((hint) => [
+      `  ${hint.message}`,
+      `  Current ${hint.kind}: ${hint.current}; suggested: ${hint.recommended}.`,
+      `  Command: ${hint.command}`,
+    ]),
+  ];
+}
🤖 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 `@src/cli/queue.ts` around lines 1048 - 1062, The performance-hint rendering in
formatQueuePerformanceHintLines is missing the same detail shown by archive.ts’s
formatInspectPerformanceHints, so queue add output is inconsistent for identical
GenerationPerformanceHint data. Update the queue formatter to include the
current-versus-suggested line for each hint, or better, extract a shared helper
in generation-planning.ts and have both formatQueuePerformanceHintLines and
formatInspectPerformanceHints use it so the rendered hint lines stay consistent.
src/cli/generation-planning.ts (1)

29-57: 🎯 Functional Correctness | 🔵 Trivial | ⚖️ Poor tradeoff

concurrent.job is accepted but never used in the time estimate.

planGenerationTask only factors concurrent.request into callBatches; concurrent.job is unused. Since callers (archive.ts inspect improvements, queue.ts createQueueAddEstimate) queue multiple missing chapters as separate per-chapter jobs governed by job concurrency, the "Wait: min-max" estimate may significantly overstate wall-clock time for multi-chapter work when job concurrency lets several chapter-jobs run in parallel. Conversely, the "raise job concurrency" hint (createGenerationPerformanceHints) implies job concurrency does matter for these workloads.

Worth confirming whether the time estimate should also factor in job-level parallelism (e.g. divide callBatches further by concurrent.job when multiple chapters are involved), or whether the current single-request-concurrency model is an intentional simplification.

🤖 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 `@src/cli/generation-planning.ts` around lines 29 - 57, `planGenerationTask` is
ignoring `concurrent.job` in the wall-clock estimate, so multi-chapter workloads
can be overestimated. Update the time calculation in `planGenerationTask` to
account for job-level parallelism when chapters are processed as separate jobs,
likely by folding `concurrent.job` into the batching logic alongside
`concurrent.request`. Keep the existing `getGenerationPlanningProfile` and
`createQueueAddEstimate`/`createGenerationPerformanceHints` behavior aligned
with the new estimate.
🤖 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 `@src/archive/search-index/search-index.ts`:
- Around line 204-207: The freshness check in readSearchIndexStatus and
ensureSearchIndex only compares the stored fingerprint, so a
SEARCH_INDEX_VERSION bump can be missed when content is unchanged. Update
createSearchIndexFingerprint to incorporate SEARCH_INDEX_VERSION, or add an
explicit version comparison in ensureSearchIndex before the early return that
uses readSearchIndexFingerprintFromDatabase. Keep the logic centered around
createSearchIndexFingerprint, readSearchIndexStatus, and ensureSearchIndex so
old indexes are invalidated when the schema/tokenizer version changes.

---

Nitpick comments:
In `@src/cli/generation-planning.ts`:
- Around line 29-57: `planGenerationTask` is ignoring `concurrent.job` in the
wall-clock estimate, so multi-chapter workloads can be overestimated. Update the
time calculation in `planGenerationTask` to account for job-level parallelism
when chapters are processed as separate jobs, likely by folding `concurrent.job`
into the batching logic alongside `concurrent.request`. Keep the existing
`getGenerationPlanningProfile` and
`createQueueAddEstimate`/`createGenerationPerformanceHints` behavior aligned
with the new estimate.

In `@src/cli/queue.ts`:
- Around line 1048-1062: The performance-hint rendering in
formatQueuePerformanceHintLines is missing the same detail shown by archive.ts’s
formatInspectPerformanceHints, so queue add output is inconsistent for identical
GenerationPerformanceHint data. Update the queue formatter to include the
current-versus-suggested line for each hint, or better, extract a shared helper
in generation-planning.ts and have both formatQueuePerformanceHintLines and
formatInspectPerformanceHints use it so the rendered hint lines stay consistent.

In `@src/wikg/wikg-coordinator.ts`:
- Around line 489-522: The dirty-check in canRemoveSqliteCacheOverlay is doing
an expensive archive fingerprint before the TTL/force checks, so move the costly
work behind a quick archive-signature comparison. In isSqliteCacheOverlayDirty,
use the persisted archive_signature on the EntryOverlay to compare the current
archive signature first, and only call
createCurrentArchiveSearchIndexFingerprint/readSearchIndexCacheStatus when the
signature has changed. Keep the existing semantics that dirty overlays are
removable regardless of TTL, but avoid extracting and rebuilding search-index
data for unchanged fts.db overlays.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 584d38ba-ff01-4b3f-b0a5-90730a24ab2c

📥 Commits

Reviewing files that changed from the base of the PR and between 467bc82 and 269b451.

📒 Files selected for processing (24)
  • data/help/commands/predicate.jinja
  • data/help/commands/root.jinja
  • data/help/commands/uri.jinja
  • data/help/topics/config.jinja
  • data/help/topics/format.jinja
  • data/help/topics/recipe.jinja
  • data/help/topics/uri.jinja
  • src/archive/query/archive-view.ts
  • src/archive/query/index.ts
  • src/archive/search-index/index.ts
  • src/archive/search-index/search-index.ts
  • src/cli/archive-index.ts
  • src/cli/archive.ts
  • src/cli/args.ts
  • src/cli/generation-planning.ts
  • src/cli/queue.ts
  • src/document/document.ts
  • src/wikg/wikg-coordinator.ts
  • test/archive/query/archive-view.test.ts
  • test/cli/archive.test.ts
  • test/cli/args.test.ts
  • test/cli/queue.test.ts
  • test/gc/gc.test.ts
  • test/wikg/spine-digest-file.test.ts

Comment on lines +204 to +207
const indexedFingerprint =
await readSearchIndexFingerprintFromDatabase(database);

if (
state.get("version") === SEARCH_INDEX_VERSION &&
state.get("fingerprint") === fingerprint &&
state.get("chaptersRevision") === String(chaptersRevision)
) {
if (indexedFingerprint === fingerprint) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the file and inspect the relevant sections.
wc -l src/archive/search-index/search-index.ts
sed -n '140,230p' src/archive/search-index/search-index.ts
printf '\n---\n'
sed -n '620,710p' src/archive/search-index/search-index.ts

printf '\n=== search for version usage ===\n'
rg -n "SEARCH_INDEX_VERSION|readSearchIndexStatus|createSearchIndexFingerprint|readSearchIndexFingerprintFromDatabase|ensureSearchIndex" src/archive/search-index/search-index.ts src -S

Repository: oomol-lab/wiki-graph

Length of output: 8559


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the version write path and any other consumers of the stored search index state.
sed -n '230,280p' src/archive/search-index/search-index.ts
printf '\n---\n'
sed -n '540,590p' src/wikg/wikg-coordinator.ts
printf '\n---\n'
sed -n '1030,1105p' src/archive/query/archive-view.ts

printf '\n=== additional searches ===\n'
rg -n "archive_index_settings|search_index_state|SEARCH_INDEX_VERSION|fingerprint" src -S

Repository: oomol-lab/wiki-graph

Length of output: 8965


🏁 Script executed:

#!/bin/bash
set -euo pipefail

sed -n '110,150p' src/cli/archive-index.ts
printf '\n---\n'
sed -n '280,330p' src/document/schema.ts
printf '\n---\n'
rg -n "SET value = value WHERE key = 'version'|WHERE key = 'version'|search_index_state" src -S

Repository: oomol-lab/wiki-graph

Length of output: 3526


Freshness should include SEARCH_INDEX_VERSION

readSearchIndexStatus and ensureSearchIndex only compare the stored fingerprint, and createSearchIndexFingerprint does not include SEARCH_INDEX_VERSION. A tokenizer/schema version bump can therefore leave an old index marked current if the content is unchanged. Fold the version into the fingerprint, or compare it before the early return.

🤖 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 `@src/archive/search-index/search-index.ts` around lines 204 - 207, The
freshness check in readSearchIndexStatus and ensureSearchIndex only compares the
stored fingerprint, so a SEARCH_INDEX_VERSION bump can be missed when content is
unchanged. Update createSearchIndexFingerprint to incorporate
SEARCH_INDEX_VERSION, or add an explicit version comparison in ensureSearchIndex
before the early return that uses readSearchIndexFingerprintFromDatabase. Keep
the logic centered around createSearchIndexFingerprint, readSearchIndexStatus,
and ensureSearchIndex so old indexes are invalidated when the schema/tokenizer
version changes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant