Skip to content

fix(memory): Atomic save_local + orphan-skip for FAISS docstore desync - #1797

Open
wgnrai wants to merge 1 commit into
agent0ai:mainfrom
wgnrai:fix/faiss-docstore-desync
Open

fix(memory): Atomic save_local + orphan-skip for FAISS docstore desync#1797
wgnrai wants to merge 1 commit into
agent0ai:mainfrom
wgnrai:fix/faiss-docstore-desync

Conversation

@wgnrai

@wgnrai wgnrai commented Aug 1, 2026

Copy link
Copy Markdown

Atomic save_local + Desync-Resilient Search for FAISS Vectorstore

Problem

Langchain's FAISS.save_local() writes index.faiss and index.pkl as two separate, non-atomic file operations. If a crash, process kill, or asyncio task cancellation occurs between these two writes, the FAISS index file reflects the new state (with newly added vectors) while the pickle file reflects the old state (without the corresponding docstore entries). On next load, similarity_search_with_score_by_vector raises ValueError: Could not find document for id {_id} when it encounters an orphaned ID.

This bug has been observed in production Agent Zero deployments, where it caused every-session crashes in memory recall and automatic memory consolidation.

Solution

This PR adds three complementary fixes:

Fix 1: Atomic save_local (Root Cause Prevention)

Overrides save_local() to use the temp-file + os.replace() pattern:

  1. Write index.faiss to index.faiss.tmp
  2. Write index.pkl to index.pkl.tmp with fsync
  3. Atomically rename both via os.replace() (POSIX rename is atomic)
  4. On failure: clean up temp files, originals remain untouched

This narrows the desync window from milliseconds (full pickle write duration) to microseconds (two os.replace syscalls).

Fix 2: Desync-Resilient similarity_search_with_score_by_vector (Defensive)

Overrides similarity_search_with_score_by_vector() to catch ValueError on orphaned docstore lookups:

  1. Call parent method
  2. On ValueError matching "Could not find document for id":
    • Identify orphaned positions (in index_to_docstore_id but not in docstore)
    • Remove orphaned vectors from the FAISS index via remove_ids()
    • Rebuild index_to_docstore_id with remaining entries
    • Retry the search
  3. If retry still fails, return empty results instead of crashing

This ensures that even if desync occurs, searches self-heal instead of crashing the application.

Fix 3: Atomic _write_index_hash

Updates _write_index_hash() to use the same temp-file + os.replace() pattern, preventing hash file corruption from a crash mid-write.

How They Work Together

  • Fix 1 (atomic save) prevents the desync from occurring in the first place
  • Fix 2 (orphan-skip) handles the residual window and pre-existing corrupted stores
  • Fix 3 (atomic hash) ensures the integrity verification hash is not itself corrupted

Together they provide defense-in-depth: prevention + resilience.

Testing

Comprehensive test suite (30 checks, all passing):

  1. Atomic save basic: Save 5 docs, verify both files exist, correct counts, no temp files left
  2. Crash simulation: Monkey-patch pickle.dump to raise mid-write, verify originals are NOT corrupted, temp files cleaned up, store still loadable
  3. Load roundtrip: Save then load, verify all vectors/docs present, search returns correct results
  4. Orphan-skip regression: Inject fake orphan ID, verify search self-heals (removes orphan, returns results, no crash)
  5. Atomic hash write: Verify hash file written atomically, no temp files left, verification passes

Implementation

All overrides are added to the MyFaiss(FAISS) subclass. The changes are backward-compatible. No changes to method signatures.

Production Evidence

  • Before fix: 3 orphaned IDs in a production memory store, causing ValueError on every session
  • After fix: 0 orphans after rebuild, defensive override verified via crash simulation
  • 13 project memory stores scanned: only the one with highest write frequency was affected

Files Changed

  • plugins/_memory/helpers/memory.py: Added save_local() override (~40 lines), similarity_search_with_score_by_vector() override (~50 lines), and atomic _write_index_hash() update (~10 lines)

@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

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