fix(indexer): resume interrupted indexing runs from incremental checkpoints - #285
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6148767bce
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Helweg
left a comment
There was a problem hiding this comment.
Blocking issues found in interrupted global-index recovery:
-
A recovered global
clear/ clearing-phaseforce-indexis replayed through the recoveringIndexer, butIndexLockOwnerhas no originating project identity. If project B reclaims project A's dead lease,clearGlobalIndexUnlocked()derives B's roots and can remove B's data while not repairing A's partially cleared state. Persist the originating scope/project identity with the recovery marker and replay against that owner. -
Recovery unconditionally calls
healthCheckUnlocked()after checkpoints. A failed checkpointed chunk has an SQLite chunk row but nobranch_chunksassociation until embedding succeeds, so orphan GC can delete its row before retry. The subsequent retry can then create a branch reference with missing chunk metadata. Preserve pending failures across recovery GC or restore their chunk rows before retry.
Please add regressions for both recovery scenarios and rerun the full hosted gate.
|
Thanks a lot for the review, Helweg! The two blocking points were spot on, and they led us to a third issue we hadn't seen. Here is what we did: Fixes1. Recovered clears are now bound to the originating project
While implementing this, we found a subtle trap: the scoped clear purges the file-hash cache, but the recovery ran before the persisted cache was loaded, so the purge was silently lost. Recovery now loads the cache before replaying clears. 2. Failed checkpoint rows survive recovery GC A failed chunk checkpointed before its embedding attempt has a committed SQLite row but no 3. Bonus find: cross-project clear recovery could damage the reclaiming project's branch catalog While reviewing the first fix, we noticed Validation
Thanks again for the careful review, it made the recovery path noticeably safer. |
|
Thanks for the detailed follow-up. I can only review pushed changes, and PR #285 still points to |
|
Sorry for the premature follow-up: I described the fixes before pushing the commit for review. The corrected commit ( |
Helweg
left a comment
There was a problem hiding this comment.
Thanks for pushing the follow-up. The targeted checkpoint tests, typecheck, and lint pass locally, and the new project-scoped recovery coverage addresses the primary issue for newly written leases. I found three remaining recovery hazards, so this still needs another update before it is safe to merge:
-
High: legacy leases can clear the reclaiming project.
parseOwnerintentionally accepts older owners with no persisted scope, but recovery falls back to the reclaimer'sthis.projectRootandthis.getScopedRoots(). If project B reclaims a dead pre-change project-A clear lease, it can replay A's clear against B. The new tests only exercise owners that already have the new scope fields. Please make legacy recovery fail safe, for example retain the recovery marker and require the originating scope to be known, rather than replaying a destructive clear against the current project. -
Medium: recovery applies the reclaimer's compatibility/configuration decision to the dead owner's project. The replay has persisted paths but calls
clearGlobalIndexUnlockedusing the current Indexer and its configured provider/knowledge-base setup. A project B process with a different embedding configuration can therefore make a destructive compatibility decision while replaying project A's interrupted clear. Please either persist the needed recovery decision/configuration with the lease or avoid automatically replaying cross-project destructive clears when those settings differ. -
Medium:
force-index-phaseis global and not owner-bound. Recovery identifies every deadforce-indexowner as being in its clearing phase whenever the single shared marker exists. A stale marker from a different lease can cause an unrelated recovered force-index to replay a clear. Please bind phase state to the lease token, or persist the phase on the owner/recovery marker, and add a regression that covers a stale marker from another owner.
Please also rebase onto current main once these are addressed, so the updated recovery path receives full current-head CI coverage.
…ptions While a project-scoped forced re-embed is pending: - checkpoints no longer stamp embedding metadata, branch commit, or the compatibility certificate, so an interrupted migration cannot resume into a silent mix of old and new vector spaces; - unchanged scope files are re-embedded instead of skipped, so cached files cannot be dropped from the branch catalog mid-migration; - retryFailedBatches only clears the pending migration after the main run has committed its new metadata, instead of closing the migration from a partially migrated store. Adds simulated-interruption tests covering the pending flag, full scope re-embed on resume, and unchanged-file re-embedding.
…points Interrupted indexing runs now persist incremental checkpoints (database, vectors, BM25, failed batches, and file hashes), so a subsequent run resumes incrementally instead of re-embedding the whole project. - Complete interrupted global clears scoped to the current project when foreign data is present, and replay only the indexing phase for interrupted force-indexes. - Persist the BM25 index before the vector store at checkpoints so a crash between the two writes cannot orphan chunks from keyword search. - Persist pending retries at checkpoints and exclude resolved retries from the final failed-batches file; deduplicate pending retries across checkpoints and at finalization, keeping the highest attempt count. - Preserve checkpointed artifacts on project-scope dead-lease recovery for incremental resume; reset only for clear operations and force-indexes with a clearing-phase marker.
Address review feedback on interrupted global-index recovery: - Persist the originating project root and scoped roots in the index lock owner, so a recovered global clear or clearing-phase force-index is replayed against the project that started it instead of the project that reclaims the dead lease. Load the persisted file-hash cache before replaying clears so the scoped purge is written back. - Restore missing SQLite chunk rows before retrying failed batches: a recovery health check can collect checkpointed failure rows as orphans, which previously left branch references without chunk metadata. - Keep the reclaiming project's branch catalog intact during a cross-project clear recovery: current-project branch keys are only added to the cleanup set when the cleared project is the current one. Adds regression tests for both recovery scenarios and updates the changelog.
6010f2a to
7f5c31d
Compare
Persist the destructive clear phase, effective embedding configuration, and compatibility decision on the active lease owner. Recovery now rejects legacy or configuration-mismatched global clears before mutation, replays knowledge-base scopes and compatibility decisions from the originating lease, and ignores stale phase files for token-bound owners. Add regressions for legacy clear leases, cross-project configuration and compatibility recovery, stale force-index markers, and ambiguous legacy force-index phases.
7f5c31d to
7a86c97
Compare
|
Thanks for the careful follow-up. I addressed all three remaining recovery hazards and rebased the PR onto the current What changed:
During an independent review, we found one additional legacy edge case: simply ignoring the old phase marker could resume a pre-change Validation:
@Helweg, could you please take another look when convenient? Thank you again for the detailed review. @codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7a86c972f2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Publish filtered failed-batch state before file hashes at checkpoints and final index exits. Persist an atomic empty state when stale retries are discarded, and cover crash recovery with and without an intermediate checkpoint.
|
Small follow-up after the latest automated review: commit The fix now publishes filtered failed-batch state before new file hashes at both intermediate checkpoints and final index exits, including an atomic empty state when obsolete retries are discarded. Regression coverage exercises crashes with and without an intermediate checkpoint. Validation: the 27 checkpoint-recovery tests and 79 related recovery/lock/multiprocess tests pass; build, typecheck, and lint pass. The four watcher failures in the full parallel run were all @Helweg, the PR head is now |
Helweg
left a comment
There was a problem hiding this comment.
Re-reviewed the updated recovery protocol. The prior blockers are addressed with targeted regressions, and local plus hosted validation is green.
Context
When an indexing run was interrupted (crash, kill, shutdown), the next run re-embedded the whole project: artifacts (vectors, BM25, database) were only persisted at the end. On large repositories, an interruption after 90% of the work cost a full re-run.
Changes
Incremental checkpoints (
src/indexer/index.ts): during indexing, each chunk threshold (checkpointIntervalChunks, floored attotalChunks / 10) now persists the database, vectors, BM25, failed batches, and file-hash cache. An interrupted run resumes where it stopped: already-checkpointed files are skipped, only the remaining ones are re-embedded.Interrupted lease recovery:
clearis completed on recovery, scoped to the current project when foreign data is present (instead of wiping other projects); an interruptedforce-indexreplays only the indexing phase.clearoperations andforce-indexruns with a clearing-phase marker.Write consistency:
failed-batches.json.Tests: 17 tests in
tests/indexer-checkpoint-resume.test.tscovering incremental resume, multi-batch checkpoints, dead-lease recovery (global and project), interrupted clear/force-index, retries, and forced re-embed migration.Validation
npm run typecheck,npm run lint,git diff --check: OKnpm run test:run: 1457 passed (2 pre-existing failures tied to the fr_FR locale, unrelated to this PR)