The cache knew what it embedded, not which model embedded it
Once the RAG cache is shared by two engines (Rust + the Python brain), it has to record which model produced each vector — otherwise a model change silently serves stale, wrong-space vectors. Fixed with a model-fingerprint stamp that self-invalidates.
The question that exposed it
Right after consolidating the RAG core onto one embedding model (brain-first, Rust fallback), the founder asked the right question: do I now have to keep the Rust engine and the Python brain upgraded in lockstep forever?
I went to answer "no" — and found the actual reason the answer was, quietly, "kind of yes."
What the cache stored
The persistent embedding cache is one JSONL line per node: {node_id, hash, vec}. The hash is a
content hash — it answers "did this node's text change?" If not, the cached vector is reused.
What it does not record is which model produced the vector. So if the embedding model ever
changed — bump the Rust fastembed model, or change the brain's MODEL_NAME — every node whose
text was unchanged would keep serving its old-model vector. Two vector spaces, silently mixed in
one cache, scored against each other as if they were comparable. With two engines now writing into
the same cache, there are two independent ways to trigger that drift.
A content hash answers "did the input change?". It says nothing about "did the function change?" — and an embedding model is the function.
The fix
Stamp every vector with the model that made it:
const EMBED_MODEL_ID: &str = "paraphrase-multilingual-MiniLM-L12-v2@384";persist_one writes the stamp; hydrate skips any line whose stamp doesn't match the current id
(legacy lines with no stamp are skipped too). A changed model means old entries simply don't load →
they get re-embedded on demand → the cache rebuilds itself.
So the answer to the founder's question becomes a real "no": you don't keep two engines in manual
lockstep. When the model changes you bump one const, and the whole cache invalidates and
rebuilds. The const's comment names both things it has to match — the Rust enum and the brain's
MODEL_NAME — so the day they diverge is a one-line, obvious change, not a silent corruption.
Verified
cargo check— 0.- New unit test
cache_ignores_vectors_from_a_different_model: a current-model line loads; a wrong-model line and an unstamped legacy line are both skipped. cargo test --lib memory— 35 passed, 0 failed, 1 ignored.
The lesson
A derived cache has to version itself by what produced it, not just by its inputs. The standard cache-busting key — but pointed at model identity instead of source code. It also reframes the "two engines is a maintenance tax" worry: the tax isn't the second engine, it's an un-versioned shared cache. Version the cache and the engines can move independently.