Repository navigation
fix(memory): improve recall reliability and candidate coverage #8716
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 19 commits
Commits
Show all changes
23 commits
Select commit
Hold shift + click to select a range
aa0bc54
fix(memory): improve recall delivery and multilingual fallback
yiliang114 ec0c746
fix(memory): bound heuristic recall scoring
yiliang114 143a372
test(memory): pin initial recall budget with fake timers
yiliang114 7a079d1
test(memory): pin recall budget and scoring contracts
yiliang114 c9bec59
fix(memory): preserve recall field weighting
yiliang114 bbd040d
fix(memory): recall relevant topics beyond scan cap (#8803)
yiliang114 d8b4e2b
fix(memory): address recall review feedback
yiliang114 da48034
test(memory): measure recall rollout gate against the pre-change scorer
c50c69d
fix(memory): deliver a deterministic fast recall result on the initia…
30dda5e
docs(memory): record the fast-path decision and phase/strategy split
4563b19
test(memory): report the mixed-language slice in the rollout gate
d6aa8c5
docs(memory): align recall docs on the deterministic fast path
4cad8a3
Merge branch 'main' into codex/7040-memory-recall
wenshao 823ac2a
fix(memory): use Array<T> for the fast-path test doc lists
5d3f9b9
docs(memory): clarify recall delivery telemetry
yiliang114 9adc612
fix(memory): report already-delivered recall count
yiliang114 ef4c5fa
Merge remote-tracking branch 'origin/main' into HEAD
yiliang114 6085ef0
docs(memory): align recall delivery claims
yiliang114 57281e8
Merge branch 'main' into codex/7040-memory-recall
yiliang114 8a5a524
fix(memory): rank ties by recency and record fast-delivered discards
yiliang114 d2598bd
docs(memory): state the candidate-cap trade and the per-turn document…
yiliang114 01ef7d7
fix(memory): end the initial recall wait on the fast result, widen to…
yiliang114 db006f8
docs(memory): correct the initial-turn preference claim, pin it with …
yiliang114 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
140 changes: 140 additions & 0 deletions
140
docs/design/2026-08-08-native-memory-recall-reliability.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| # Native Memory Recall Reliability | ||
|
|
||
| ## Problem | ||
|
|
||
| Managed-memory recall starts asynchronously for each user query. The initial | ||
| request originally performed a zero-wait consume, so a useful selector result | ||
| could miss the first prompt. If the turn has no tool call, that result has no | ||
| later safe delivery point and is discarded. | ||
|
|
||
| A fixed 100 ms initial budget was the first attempt at a fix. Measurement | ||
| showed it is not sufficient on its own. Recall awaits the model selector | ||
| whenever a `Config` is present — the normal case — and that selector is a | ||
| network side query whose abort ceiling is 30 s. The budget is therefore | ||
| dominated by round-trip time, not by the incidental scheduler timing it was | ||
| sized for, so it expires on the common path. Delivery then falls through to the | ||
| ToolResult point, which a tool-free turn never reaches. Tool-free turns are | ||
| exactly the ones where user-level memory matters most: short questions answered | ||
| from context rather than from the repository. | ||
|
|
||
| The model selector remains the normal precision gate. Its failure fallback had | ||
| two independent correctness problems: it tokenized only ASCII text and gave | ||
| every non-empty document a positive score even without a lexical match. | ||
|
|
||
| ## Decision | ||
|
|
||
| Keep a single recall lifecycle and model-primary selection. Add one | ||
| deterministic delivery stage in front of it — not the two-stage shared-scan | ||
| Fast/Refined architecture originally proposed in RFC #7040. | ||
|
|
||
| - Give user-query recall a fixed 100 ms initial wait budget. | ||
| - Deliver a result that settles inside the budget in the initial prompt. | ||
| - If the budget expires and the deterministic candidate pass found relevant | ||
| documents, deliver that bounded result instead of nothing. | ||
|
yiliang114 marked this conversation as resolved.
|
||
| `selectModelCandidateDocuments` already computes lexically ranked candidates | ||
| in order to build the model manifest, so the fast result reuses them and | ||
| costs no extra scan or I/O. It is capped at two documents | ||
| (`MAX_FAST_RECALL_DOCS`), well below the five-document prompt limit, because | ||
| it carries no model judgement. | ||
| - Leave recall pending after a fast delivery so the model-selected result still | ||
| lands at the existing same-query ToolResult delivery point. | ||
| - Exclude documents the fast phase already delivered from that later delivery, | ||
| rebuilding the prompt from what remains. Both results come from one scan, so | ||
| the selector never saw the fast documents as excluded and can legitimately | ||
| re-select them. When every selected document was already delivered, record | ||
| `already_delivered`; when the selector returned no documents at all, record | ||
| `no_relevant_results`. | ||
| - Do not abort recall merely because the initial budget expires. | ||
| - Preserve the existing cancellation and exactly-once terminal telemetry paths. | ||
| A cancelled turn delivers no fast result. | ||
|
|
||
| The 100 ms budget stays internal, per RFC #7040's direction of a small fixed | ||
| internal budget determined by benchmark rather than exposed as public | ||
| configuration; telemetry can show whether a later change is justified. | ||
|
|
||
| ### Why not the original Fast/Refined architecture | ||
|
|
||
| RFC #7040 specified two results produced from one shared scan, with the refined | ||
| pass excluding already-delivered fast documents and filling up to a combined | ||
| five-document limit. The delivery guarantee that design existed to provide is | ||
| worth having; its machinery is not. A second selection pathway needs its own | ||
| scan plumbing, its own budget accounting, and cross-phase document bookkeeping — | ||
| and that bookkeeping is the source of the duplicate-injection class of bug the | ||
| RFC itself warned about. Reusing the candidates the selector was already going | ||
| to score gets the same guarantee from one added callback and one exclusion set. | ||
|
|
||
| ### Telemetry: `phase` and `strategy` are orthogonal | ||
|
|
||
| `phase` is the **delivery stage**: `fast` for a deterministic result injected | ||
| at budget expiry, `refined` for the model-selected result. `strategy` is the | ||
| **selection method**: `none`, `heuristic`, or `model`. They are not redundant | ||
| and neither subsumes the other. A `fast` delivery is always `heuristic`, but a | ||
| `refined` delivery is `model` normally and `heuristic` when the selector failed | ||
| and the fallback ran. Reading delivery-stage behaviour off `strategy` alone | ||
| would silently merge "the deterministic result arrived first" with "the model | ||
| selector broke". | ||
|
|
||
| Improve the deterministic scorer, which now serves both the fast path and the | ||
| selector-failure fallback: | ||
|
|
||
| - normalize query and document text with Unicode NFKC; | ||
| - keep ASCII alphanumeric tokens of at least three characters; | ||
| - generate Unicode code-point bigrams for Han, Hiragana, Katakana, and Hangul | ||
| runs; | ||
| - ignore isolated CJK characters; | ||
| - bound fallback query tokens while retaining tokens from both ends; | ||
| - score only the body window that can be surfaced in the prompt; | ||
| - require a title, description, or body lexical match before applying a type | ||
| boost; | ||
| - weight each title and description token match above a body token match. | ||
|
|
||
| ## Non-goals | ||
|
|
||
| - No second scan, second selector, or separate fast/refined budget accounting. | ||
| - No public recall timing or retrieval-mode setting. | ||
| - No new tokenizer or retrieval dependency. | ||
| - No change to memory writes, scopes, extraction, DREAM, forget, or compaction. | ||
| - No removal of the shared scanner's 200-document cap for non-recall callers. | ||
| Recall alone uses the bounded broad-candidate design documented in | ||
| `2026-08-09-bounded-memory-recall-candidates.md`. | ||
|
|
||
| ## Verification | ||
|
|
||
| Recall quality is measured in `packages/core/src/memory/recall-eval.test.ts` | ||
| against a 45-case labeled corpus, scored both by the shipped scorer and by a | ||
| frozen copy of the pre-change one so "no regression" is reproducible rather | ||
| than asserted. Delivery is measured separately in `recall-delivery-eval.test.ts`, | ||
| because a correct selection that never reaches the model is worth nothing. | ||
|
|
||
| - Recall settling inside the budget is delivered initially. | ||
| - A budget expiry with deterministic candidates delivers that bounded result | ||
| rather than nothing, and leaves recall alive for later ToolResult delivery. | ||
| - The later delivery never repeats a document the fast phase already sent. | ||
| - Cancellation ends the bounded wait and prevents stale delivery. | ||
| - A fast result never crosses a query boundary. | ||
| - No-result queries stay silent under both designs. | ||
| - A labeled set covers Chinese, English, Japanese, Korean, mixed text, | ||
| NFKC normalization, body-only matches, and no-result queries. | ||
| - Long CJK queries keep bounded scoring work and preserve both query ends. | ||
| - Existing active-tool noise filtering remains unchanged on the deterministic | ||
| candidate path. The model-selector failure fallback still triggers and | ||
|
yiliang114 marked this conversation as resolved.
|
||
| returns at most five documents; its scoring quality intentionally improves | ||
| per the scorer changes above (measured by the frozen-scorer comparison in | ||
| `recall-eval.test.ts`). | ||
|
|
||
| ### Known limitations | ||
|
|
||
| - The delivery evaluation models selector latency rather than measuring it; a | ||
| network round trip cannot be timed in a unit test. Results are reported per | ||
| latency scenario, and the structural claim — that a selector slower than the | ||
| budget leaves a tool-free turn with no delivery point under the single-path | ||
| design — holds for every scenario above the budget. | ||
| - The fast result has no model judgement behind it. It is capped at two | ||
| documents to bound the cost of being wrong, but on a tool-free turn where the | ||
| selector never lands, a mis-ranked fast document is what the model sees. | ||
| - Scoring is substring-based, so a query token can match inside a longer word | ||
| ("owner" inside "ownership"). The evaluation corpus records one such case | ||
| rather than hiding it. | ||
| - Recall can see older documents outside the shared 200-document scanner cap, | ||
| but non-recall callers, including Forget, keep the existing capped scanner. | ||
| A broader manageability pass is separate from this recall-only change. | ||
68 changes: 68 additions & 0 deletions
68
docs/design/2026-08-09-bounded-memory-recall-candidates.md
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,68 @@ | ||
| # Bounded Memory Recall Candidates | ||
|
|
||
| ## Problem | ||
|
|
||
| The project and user memory scanners enumerate, read, and parse every topic, | ||
| then return only the 200 most recent documents. Recall uses those shared scanner | ||
| APIs, so an older relevant document outside either 200-document window cannot | ||
| reach the heuristic or model selector even though the expensive scan work has | ||
| already happened. | ||
|
|
||
| The same capped APIs are also used by Forget, Indexer, Status, and Extraction. | ||
| Removing their limit globally would widen unrelated behavior. | ||
|
|
||
| ## Decision | ||
|
|
||
| Keep the existing scanner APIs and their 200-document limit unchanged. Add | ||
| explicit all-topic variants used only by recall. | ||
|
|
||
| Recall ranks the combined project and user pool before model selection: | ||
|
|
||
| - retain up to 180 documents with a lexical match using the existing scorer; | ||
| - fill the remaining candidate slots by recency, preserving at least 20 recent | ||
| opportunities when enough lexical matches exist; | ||
| - interleave recent opportunities with lexical candidates so the manifest byte | ||
| budget cannot systematically exclude the entire recent reserve; | ||
| - send at most 200 candidates to the model selector; | ||
| - append manifest entries only while their cumulative UTF-8 size remains at or | ||
| below 25,000 bytes; | ||
| - validate selector output only against documents actually present in that | ||
| bounded manifest. | ||
|
|
||
| The heuristic fallback continues to score the complete recall pool and still | ||
| returns at most five documents. Existing body and prompt limits remain | ||
| unchanged. | ||
|
|
||
| Forget, Indexer, Status, and Extraction keep the capped scanner. That preserves | ||
| their current behavior but means an older document can become recallable before | ||
| it becomes manageable by those non-recall flows. | ||
|
|
||
| ## Failure and compatibility boundaries | ||
|
|
||
| Project scanning remains required. User scanning remains best-effort. Invalid | ||
| or unreadable files keep the existing skip behavior. Empty candidate manifests | ||
| return no model selection rather than sending an unbounded request. | ||
|
|
||
| There is no public setting, persistent index, new dependency, provider API, or | ||
| second selection pathway. Each recall enumerates, reads, and parses the full | ||
| project and user memory trees once, then performs O(n) local ranking and | ||
| active-tool filtering over the parsed documents. The deterministic fast path | ||
| described in `2026-08-08-native-memory-recall-reliability.md` reuses the | ||
| candidates produced by that single pass, so it adds no scan, no ranking work, | ||
| and no state machine — only an earlier delivery point for results already | ||
| computed. The model candidate count and manifest are | ||
| bounded, but the local I/O and filtering work grow with the memory tree; a | ||
| persistent catalog requires separate measurement and evidence. | ||
|
|
||
| ## Verification | ||
|
|
||
| - A deliberately old relevant topic beyond the regular 200-document result is | ||
| recalled from a real temporary memory tree. | ||
| - The regular scanner still returns 200 documents and omits that topic. | ||
| - The model candidate set contains the lexical target and recent reserve while | ||
| remaining at 200 documents. | ||
| - A manifest built from large multibyte descriptions stays within 25,000 UTF-8 | ||
| bytes. | ||
| - A real temporary memory-tree integration test verifies overflow-topic recall; | ||
| client tests independently verify bounded initial waiting and later | ||
| ToolResult delivery. |
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.