One embedding model, not two — routing the RAG core through the brain
The Tauri process and the Python brain were each loading the same 384-dim model into RAM. Consolidated the RAG core's four embed sites to brain-first with a Rust fallback, so normal operation runs one model.
The duplication
Phase 1 of the Python brain moved ranking over to the daemon: graph_query and
workitem_context call brain_rank, and on a brain-down they fall back to keyword scoring. That
part shipped and is observable (a 🧠/⚠️ keyword badge).
But the embeddings underneath the RAG core never moved. memory.rs still held its own in-process
Rust fastembed model — ParaphraseMLMiniLML12V2, 384-dim — and called it directly at four sites:
the semantic_rank document batch, the query embed, the single-node write-time embed, and the
triples batch. Meanwhile the Python brain loads the identical model
(paraphrase-multilingual-MiniLM-L12-v2) over fastembed/ONNX.
So a normal session kept the same ~90 MB ONNX model resident twice — once in the Tauri process, once in the brain. Whichever call site you hit decided which engine did the work. That's not a fallback; that's two owners of the same thing.
The fix: pick one owner
ai_brain::brain_embed(texts) -> Option<Vec<Vec<f32>>>POSTs to the brain's/embed({"vectors": [...], "dim": 384}), parsing each row tof32.None⇒ brain unreachable.memory::embed_batch(model_guard, texts)tries the brain first; only when it's down (or returns a row count that doesn't match the input) does itensure_modelthe in-process Rust model and embed locally.- All four
.embed()sites now go throughembed_batch. No call site reaches for a raw model anymore.
The reason this is safe and not just convenient: both engines run the same model in the same normalized 384-dim space. I verified that earlier (환불→refund cosine 0.529 both ways), so a vector the brain produced and a vector the Rust model produced are interchangeable. That's what lets the on-disk embedding cache stay valid regardless of which engine wrote a given vector — a brain-embedded query still scores correctly against Rust-embedded nodes cached from before the change. If the two sides had been different models, this same refactor would have silently corrupted the cache.
Verified
cargo check— 0 errors.cargo test --lib memory— 34 passed, 0 failed, 1 ignored.- Live
curl POST /embedon the running launchd daemon — 2 rows, dim 384, modelparaphrase-multilingual-MiniLM-L12-v2(the same model name the Rust crate resolves to).
Normal operation now runs one model — the warm launchd brain. The Rust model stays in the binary purely as a cold fallback for the window where the brain is starting up or absent.
The lesson
When two services load the same model, don't let call sites each grab whatever engine is nearest — name one owner and make the other an explicit fallback. The trade is HTTP latency per embed, but the brain is a warm loopback daemon (sub-ms), and in return one model leaves RAM and the brain gets a single, observable embedding path. The whole thing only works because "same model" was measured, not assumed.