fix(recall): embed the QUESTION as a query, not as a memory - #319
Merged
Conversation
Asking "What is my name?" answered "I don't have a memory about that" while
the memory "My name is Jayesh" sat in the corpus, encoded and indexed.
Cause: `mxbai-embed-large` is trained ASYMMETRICALLY. Documents are embedded
bare, but a retrieval query must carry the prompt "Represent this sentence for
searching relevant passages: ". `ask()` called `embed()` for the question, the
same method the encoder uses for memory text, so every query vector landed in a
systematically different place from the corpus it was being compared against.
Cosine was depressed across the board — not for one query, for all of them.
Measured on the live corpus, "What is my name?" vs "My name is Jayesh":
without the prompt top cosine 0.5337 → confidence 0.4410 → MISS
with the prompt top cosine 0.5663 → confidence 0.4695 → answers
It missed the §05.6 floor by 0.009. The retrieval was never broken; the
question was simply being asked in the wrong vector space.
- `EmbeddingProvider` gains an OPTIONAL `embedQuery`. Symmetric providers
(OpenAI, Voyage, the fake) omit it and the shared `embedQueries` helper
falls back to `embed`, so nothing changes for them and no call site needs
to know which providers are asymmetric.
- The Ollama provider implements it via `queryPrefixFor(model)`, keyed by
model-name prefix so a `:latest` tag or a quantisation suffix still matches
(mxbai and nomic use different schemes). Documents deliberately do NOT get
the prefix — the asymmetry IS the trained behaviour, and prefixing both
sides would be exactly as wrong as prefixing neither.
- Only the recall path changes. The encode path still calls `embed`, so
stored vectors keep their meaning and no re-encode is required.
Verified against the live stack (Ollama mxbai-embed-large + gemma3:4b), same
questions that failed before:
"What is my name?" → "My name is Jayesh" 1 citation
"Who am I?" → "My name is Jayesh." 1 citation
"What is Priya allergic to?" → "peanuts" 1 citation
type-check, lint, build green; the 82 embedding/LLM/model-router unit tests
pass unchanged.
NOT included, deliberately: the §05.6 confidence bands are still calibrated for
a different model's cosine distribution (relevant pairs land ~0.5-0.6 here, and
the miss floor is 0.45). Retuning them is an eval-gated change (#257), not a
guess — this commit only fixes the query encoding that was plainly wrong.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This was referenced Jul 31, 2026
Open
Open
CI `test (3)` failed on two suites with: [vitest] No "embedQueries" export is defined on the "../src/modules/memory/embedding/index.js" mock Both suites `vi.mock` the embedding module with an explicit factory, so adding a new export to the real module leaves the mock incomplete — `ask()` now calls `embedQueries`, which the factory did not provide. A mock factory replaces the module wholesale; it does not inherit new exports. The stub mirrors the real helper's fallback (no `embedQuery` on the provider → plain `embed`), which is exactly the behaviour these suites depend on: they assert recall wiring, not query-prefixing, so the vectors they get are unchanged and neither assertion moves. Both suites pass locally; lint green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Asking "What is my name?" answered "I don't have a memory about that" while "My name is Jayesh" sat in the corpus, encoded and indexed.
Cause:
mxbai-embed-largeis trained asymmetrically — documents are embedded bare, but a retrieval query must carry the prompt"Represent this sentence for searching relevant passages: ".ask()calledembed(), the same method the encoder uses for memory text, so every query vector landed in a systematically different place from the corpus it was compared against. Cosine was depressed for all queries, not one.Measured on the live corpus, "What is my name?" vs "My name is Jayesh":
It missed the §05.6 floor by 0.009. Retrieval was never broken — the question was being asked in the wrong vector space.
Changes
EmbeddingProvidergains an optionalembedQuery. Symmetric providers (OpenAI, Voyage, fake) omit it and the sharedembedQuerieshelper falls back toembed— nothing changes for them, and no call site needs to know which providers are asymmetric.queryPrefixFor(model), keyed by model-name prefix so a:latesttag or quantisation suffix still matches. Documents deliberately do not get the prefix — the asymmetry is the trained behaviour.embed, so stored vectors keep their meaning and no re-encode is required.Verification — same questions that failed before, against the live stack:
type-check,lint,buildgreen; 82 embedding/LLM/model-router unit tests pass unchangedDeliberately not included: the §05.6 confidence bands are still calibrated for a different model's cosine distribution (relevant pairs land ~0.5–0.6 here against a 0.45 miss floor). Retuning is eval-gated (#257), not a guess. This PR only fixes the query encoding that was plainly wrong.
🤖 Generated with Claude Code