bug: fact extraction replaces verbatim content causing 50% score regression — should supplement not replace #20
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
LongMemEval benchmark results show v5-full mode scores 33.6% overall — nearly half of v5-raw's 65.6%. The fact extraction pipeline is actively hurting retrieval rather than helping it.
Benchmark evidence
165 empty recalls in full mode vs 1 in raw mode — the extracted facts simply don't match recall queries.
Root cause: semantic gap between facts and questions
Fact extraction transforms conversational content:
The recall query is:
"What degree did I graduate with?"The embedding similarity between the natural question and the compact extracted fact is lower than between the question and the original verbatim text. Valkey can't find the fact, returns empty, Claude abstains.
Two categories are catastrophic:
Temporal reasoning (7.5%) — fact extraction strips or normalises timestamps. "I moved to Edinburgh last March" loses its temporal anchor when converted to a fact. The timestamp metadata in
haystack_datesis available but not being preserved in extracted facts.Preference (6.7%) — subtle preferences buried in conversation ("I prefer leather straps", "I like earthy tones") are either missed during extraction or phrased differently, breaking recall.
Root cause: facts replace verbatim content
The current pipeline stores extracted facts instead of the original verbatim chunks. This is the wrong architecture for a retrieval system — you lose the original vocabulary that the recall query would naturally match against.
Proposed fix: supplementary storage, not replacement
Store both the extracted facts AND the verbatim chunks:
This way:
Additional fix: preserve timestamps in extracted facts
Extracted facts should carry the session timestamp as a field, not just in content:
This directly addresses the 7.5% temporal reasoning score.
Expected impact
Switching from replacement to supplementary storage should recover most of the raw mode score (65.6%) while adding the benefits of fact extraction for contradiction detection and knowledge updates. Combined with timestamp preservation, temporal reasoning should improve significantly above the raw baseline.
Related issues
Highest-leverage single fix: timestamp preservation in extracted facts
The temporal reasoning collapse from 53.4% → 7.5% is almost entirely explained by timestamps being stripped during fact extraction.
The problem
Recall query: "When did the user move to Edinburgh?"
The extracted fact has no temporal anchor. Valkey embedding similarity between a temporal question and a dateless fact is near zero → empty recall → abstention.
The fix — prepend timestamp to every extracted fact
Also store timestamp as a searchable metadata field:
Expected impact
This single change should recover most of the 7.5% → ~40-50% temporal reasoning score, because:
haystack_datesfield is already available in LongMemEval sessions — it just needs threading through to the extracted factSupplementary storage implementation notes
For the verbatim + facts dual storage approach:
Linking — extracted facts should carry
source_keymetadata pointing to their parent verbatim chunk. When the verbatim chunk is deleted, its facts are deleted too.doc_idfromremember_document()is the natural grouping key.Recall priority — verbatim chunks should score slightly higher than extracted facts on direct recall (they preserve original vocabulary). Facts should score higher on contradiction checks and knowledge updates.
Quoted phrase / name boost (from MemPalace hybrid_v4) — post-retrieval reranking step that boosts results containing exact proper nouns or quoted phrases from the query. Implementable as a
rerank()step, doesn't change storage.Nostalgia patterns — temporal anchor phrases ("last year", "used to", "when I was") in the query should boost chunks with
fact_datemetadata. Complements the timestamp preservation fix.Priority order for maximum benchmark impact
source_keylinking for clean deletionFix Plan
Root cause (confirmed)
The issue description identifies the semantic gap correctly, but the actual mechanism is namespace crowding, not replacement. Both verbatim chunks and extracted facts coexist, but extracted facts are stored in the same namespace as source chunks (e.g. both in
episodic). During recall, vector search returnstop_k=20per namespace then clamps totop_k=5final results. Extracted facts (semantically narrower, 1-2 sentences) crowd out the richer verbatim content that would otherwise match recall queries.Additionally, temporal info from source memories is lost when extracted facts don't carry their own
event_date.Fix: 3 files, 6 changes
1. Route facts to
knowledgenamespace —mcp_server/memory/enrichment.py:124The
knowledgenamespace already has its own HNSW index and is already searched during recall. Verbatim chunks stay inepisodicwith their owntop_k=20budget. No crowding.2. Preserve source timestamps —
mcp_server/memory/enrichment.pyIn
_enrich(), readevent_dateandcreated_atfrom the source memory and apply a fallback chain:Pass
created_atthroughenqueue()andenqueue_batch()payloads for batch mode (where the worker doesn't read individual source memories).3. Pass
created_atat call sites —mcp_server/tools/core.pyAll three
enqueue()/enqueue_batch()call sites updated to passcreated_at=now.4. Update dead code —
mcp_server/tools/core.py:116_store_extracted_fact(currently unused) updated for consistency.5. Tests —
mcp_server/tests/test_enrichment.pyknowledgenamespaceNo migration required
Existing enriched facts in
episodicremain searchable. New facts go toknowledgegoing forward. Old enriched facts age out naturally via lifecycle.Fixed in v5.3.1 on branch
v5(commit80e43fe).The fix follows the plan in the comments above, with one correction to the diagnosis: it was indeed namespace crowding rather than replacement — both verbatim chunks and facts coexisted, but compact facts competed for the same per-namespace KNN candidate budget.
What landed
1. Facts route to the
knowledgenamespace (memory/enrichment.py)Preference-shaped facts still go to
preference. Verbatim chunks keep the episodic candidate budget to themselves, so facts can no longer crowd out the original wording that recall queries naturally match.2.
event_datefallback chain — the temporal fixA fact carries its own date if extraction found one, else it inherits the source memory's
event_date, else the source's ingest time.enqueue()/enqueue_batch()now threadcreated_atthrough the queue payload so batch mode gets timestamps too, and batch mode also falls back to reading the first chunk's timestamps so pre-upgrade queued payloads still get an anchor. Date-shaped recall queries ("when did I…") hit the existing temporal boost (1.0–1.5x within ±7 days, linear falloff to ±60 days) against the inherited anchor.3. Verbatim-first ranking + source linking
Facts are written with
surface_score0.5 so the verbatim chunk outranks its own derived fact on direct recall. When a fact and itsenriched_fromsource both match a query, the merge keeps only the source, promoted to the fact's score if the fact ranked higher — so the verbatim stands in at the fact's rank in both orderings, and no result slot is wasted on a near-duplicate.enriched_fromwas already being written; it's now returned by search and exposed inrecall()output.4. The silent project-filter drop
_NAMESPACE_RETURN_FIELDSfor knowledge was missingprojectentirely, so a project-filtered recall discarded every knowledge result without a trace — facts would have been invisible the moment they moved namespaces. Knowledge results now carryproject,event_date,tagsandenriched_from, and the knowledge index gains an indexedprojecttag (picked up automatically by the startup index migration) so the project filter is pushed into the vector search itself.Validation
event_datedriving the temporal boostNot included (from the priority list above)
Benchmark re-run against LongMemEval is the remaining validation step — tracked under #17.