fix(embedding): fail closed instead of wiping a corrupt RAG index - #3293
Conversation
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Pull request overview
This PR changes the local JSON RAG store load behavior to fail closed (throw a typed registry error) instead of silently resetting and overwriting a corrupted index.json, preventing data loss and surfacing corruption to callers.
Changes:
- Added a new registry error
RAG_STORE_CORRUPTand registered it under the SERVER category. - Updated
rag-storeload logic to throw on malformed JSON or failed shape validation instead of resetting to an empty store. - Updated RAG store tests and error-registry counts to reflect the new behavior and new registered error.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/errors/index.ts | Re-exports the new RAG_STORE_CORRUPT registry error. |
| src/errors/error-registry/server.ts | Defines and registers the new rag-store-corrupt SERVER error. |
| src/errors/error-registry.test.ts | Updates the registry total and SERVER category count assertions. |
| src/embedding/rag-store.ts | Changes store loading to throw on corruption instead of wiping; adds helper to create the new error. |
| src/embedding/rag-store.test.ts | Replaces “reset to empty” expectations with “fail closed and do not overwrite” assertions. |
Suppressed comments (1)
src/embedding/rag-store.ts:389
load()rethrows non-NotFound filesystem errors, so permission/IO failures will bypass the newRAG_STORE_CORRUPTregistry error (and may get wrapped later asunknown-error). This contradicts the stated goal of failing closed with a typed registry error for IO failures as well. Consider wrapping these errors withcorruptStoreError(...)so the store is preserved and the caller gets a stable slug.
try {
snapshot = await readStoreFileSnapshot();
} catch (err) {
// Expected on first run, and when the file disappears between stat and read.
if (!isNotFoundError(err)) throw err;
snapshot = null;
}
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
b0e023d to
715f3a4
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/embedding/rag-store.ts:396
- The comment in this catch block says the NotFound path is "expected on first run", but first-run behavior is handled by
getStoreFileMetadata()returningnull(no exception). This catch is only reached for race conditions like the file disappearing betweenstatandreadTextFile. Updating the comment will avoid misleading future readers about the control flow.
} catch (err) {
// Expected on first run, and when the file disappears between stat and read.
if (isNotFoundError(err)) {
snapshot = null;
715f3a4 to
3ab6a38
Compare
3ab6a38 to
b6a968f
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/embedding/rag-store.ts:448
save()switched the temp path from a stable${storagePath}.tmpto${storagePath}.tmp.<uuid>. This reduces temp-name collisions across concurrent processes, but it also increases the chance of orphaned temp files accumulating after crashes (each crash can leave a unique*.tmp.*that later saves will not overwrite).
If the intended deployment is single-process (the in-file mutex already serializes writes), consider either returning to a stable temp filename or adding a small cleanup step for stale ${storagePath}.tmp.* files to avoid long-term disk clutter.
// Atomic write: write to temp file then rename to prevent corruption on crash
const tmpPath = `${storagePath}.tmp.${crypto.randomUUID()}`;
await writeTextFile(tmpPath, payload);
b6a968f to
fcfc221
Compare
fcfc221 to
72b4d3a
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.
Suppressed comments (1)
src/embedding/rag-store.ts:532
save()throwsRAG_STORE_UNAVAILABLEwhen the in-memory update fails persisted-data validation. This error’s title/suggestion (“file is unavailable”, “check storage availability and file permissions”) does not match a validation/invariant failure, making it harder to diagnose. Consider using a dedicated invariant/limit error (orINVALID_ARGUMENTfor quota/limit overruns) so the title/suggestion align with the failure mode.
if (!isRagStoreData(data)) {
throw RAG_STORE_UNAVAILABLE.create({
detail: "The RAG store update violated persisted-data limits or relationships.",
context: { storagePath },
});
}
72b4d3a to
a985447
Compare
a985447 to
7f22cd3
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 51 out of 52 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/embedding/local-json-store-lock.ts:423
- The doc comment for
tryRecoverStaleLock()says the post-rename check "restores a newer generation" when ownership changes, butrestoreUnexpectedRecovery()never restores anything (it only throws). This makes the comment misleading for future maintainers; either implement restoration or update the comment to describe the current fail-closed behavior.
* Move a stale lock through one deterministic recovery directory before
* deleting it. The post-rename owner check restores a newer generation if
* ownership changed in the observation-to-rename window. Recovery never
* writes inside the observed directory, because doing so would refresh the
* only safe fallback timestamp for an ownerless partial acquisition.
A lock directory can disappear after lstat but before or during readDir when another process completes release. Treat that canonical not-found observation as an absent generation so acquisition retries instead of leaking a raw filesystem error. Constraint: Only canonical not-found enumeration failures are transient. Rejected: Retry every readDir failure | permission and validation errors must remain visible. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Keep observation retries limited to races that prove the inspected directory no longer exists. Tested: DENO_TESTING=1 npx --yes deno@2.7.7 test --no-check --allow-all src/embedding/rag-store.test.ts Tested: npx --yes deno@2.7.7 fmt --check, lint, and check on touched files Tested: pinned Deno API reference generator test Not-tested: Full pre-push suite before branch reconciliation
Main already contains the broader lock and snapshot hardening through a previously merged dependent branch. Join the original PR history while keeping main's newer implementation and the focused readDir race fix. Constraint: The PR branch must advance without a force push. Rejected: Resolve conflicts by favoring the old PR tree | that would revert newer main behavior and tests. Confidence: high Scope-risk: narrow Reversibility: clean Directive: Review the effective diff against main, not the already-landed historical commits. Tested: Merge result tree differs from origin/main only in the focused lock race fix and regression tests. Not-tested: Full pre-push suite before this reconciliation commit
|
Reconciled this PR with current main without a force push at exact head cc199c5. The broader RAG lock and snapshot hardening had already reached main through the merged dependent branch. The updated PR therefore keeps the newer main implementation and reduces the effective diff to the still-missing CI race fix:
This directly covers the failure in main run 30804720593, job 91657279661. Its failed-job rerun is green; this fix removes the race instead of relying on retry luck. Verification on the reconciled tree:
Fresh exact-head CI and independent review are still required before any merge-confidence comment or queueing. |
|
Merge confidence: 94%. Reasoning before scheduling:
Residual risk is low and localized to the local JSON RAG lock observation path. The change only converts canonical lock-directory NotFound races during observation into a retry/absent-observation outcome; non-NotFound filesystem errors still propagate. |
|
Merge confidence: 94%. Reasoning: exact head cc199c5 is clean against current main, has zero unresolved review threads, and GitHub reports no failing or pending required checks. The effective diff is narrow: src/embedding/local-json-store-lock.ts now treats a lock directory that disappears during readDir as a retryable missing-observation case, and src/embedding/rag-store.test.ts adds deterministic coverage for both open-time and iteration-time readDir disappearance plus lease-write cleanup. Local verification on the exact worktree passed with Residual risk: narrow filesystem race behavior across platforms, but the change is scoped to lock observation and the regression now covers the CI failure mode seen on main. |
Ported from
codex/module-reconcile-20260723as a focused data-integrity fix.The bugs
A corrupt or oversized
index.json—malformed JSON, invalid UTF-8, structurally invalid entries, duplicate identities, or broken chunk relationships—could be treated as an empty or mutable store and later overwritten. Operational read failures could also become ordinary absence. Atomic replacement failures could fall back to rewriting the live index, and concurrent store instances or processes could both report success while silently losing one update.The original stale-lock recovery also observed only the owner-selected lease. A foreign or replacement lease could therefore be missed, and owner equality alone did not bind cleanup to the exact lease generation observed before the lock directory moved.
The fix
The local JSON store now fails closed and preserves persisted bytes:
RAG_STORE_CORRUPT, including when the live index changes to invalid UTF-8 or oversize during save;RAG_STORE_UNAVAILABLE, and only canonical runtime not-found errors mean absence;local-jsondiagnosis with Node.js andveryfront-cloudalternatives.The public RAG guide and generated embedding/error references document the local persistence constraints and current source links. No third-party core dependency was added.
Verification
On exact head
16ce5f3ebe234dc52a287d7ab2d66f44d8bf8679, based onorigin/mainatbf06bf85a58e44184baa91b22cc16e5c6a91a179:deno check;deno task verify:quickpasses, including formatting, lint, core-dependency, dependency-boundary, module-boundary, extension-contract, generated-reference, public-doc, link, and production typecheck gates;deno task lint:core-depsreports no disallowed third-party imports;Fresh exact-head GitHub CI and independent exact-head re-review are required before merge.
Summary by CodeRabbit
Bug Fixes
Documentation
Tests