fix(memory): recall relevant topics beyond scan cap - #8803
Conversation
Scope and verification reportThis PR addresses one specific problem from #7040: topic documents were capped to the 200 most recent files before relevance ranking, so an older relevant memory could never reach either the heuristic path or the model selector even though the scanner had already read and parsed it. The new recall flow is: This does not close the whole RFC. It does not change delivery telemetry, the bounded initial-wait lifecycle, multilingual evaluation, provider integration, persistent indexing, or the final five-document recall limit. Impact on other memory modules
The two uncapped scanner entry points have one production consumer: recall. The existing capped scanner APIs remain unchanged for every other consumer. The scanner already read and parsed all topic files before applying the old cap, so this change does not introduce additional file I/O; it adds relevance ranking over the complete parsed pool. A persistent catalog remains out of scope unless measured topic counts show that ranking becomes too slow. E2E verificationVerified the current head ( The fixture contains 200 recent unrelated topics plus one deliberately old relevant topic containing The mock fails the run if the old target is absent, the candidate count exceeds 200, the manifest exceeds 25,000 UTF-8 bytes, or the recalled body does not reach the main-model request. Regression checks
The image above is a rendering of the recorded headless CLI results. The test used the real local CLI execution path with a deterministic mock provider; it verifies scan, ranking, selector bounds, and delivery, but does not claim real-model semantic quality. |
|
Thanks for the PR! Template looks good ✓
Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓
进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewMy independent take on "recall should see past the 200-doc cap without unbounding model cost" was exactly what this PR does: uncapped scan variant scoped to recall, a bounded candidate shortlist, and a byte-bounded manifest. The implementation matches, and I found no correctness issues:
No critical findings. Testing evidenceThis run is unattended CI, so per policy I did not build or run any PR code. The evidence below is the PR's own CI, read via the API — and the headline is that the standard test suite does not run on this PR: Check-runs on
Additionally, a The PR description lists local verification ( Sandboxed verification would settle this: 中文说明代码审查我对"召回应能越过 200 文档上限、同时不给模型成本解绑"的独立方案与本 PR 完全一致:仅供召回使用的无上限扫描变体、有界的候选短名单、按字节设限的 manifest。实现与之一致,未发现正确性问题:
无关键阻塞项。 测试证据本次为无人值守 CI 运行,按策略不构建、不运行任何 PR 代码。以下证据来自对该 PR 自身 CI 的 API 读取——结论是标准测试套件没有在本 PR 上运行: 此外,head 分支上有一次 PR 描述中列出的本地验证( 沙箱验证可以补齐这一点: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 3/5 — clean code review, but this commit has never been through the standard suite and its base PR is still open; a human should decide when it lands, not the gate. Stepping back: the approach matches what I would have written — smaller than I feared going in (~99 production lines), every edit is in scope, no drive-by changes, and the tests genuinely pin the behavior rather than decorating it. Six months from now this reads as a careful, bounded change with a design doc that says what it deliberately did not do. My reservation is not the code; it is the evidence:
Suggested path to merge: land #8716 → retarget this PR to No change requests — nothing to fix in the code itself. 中文说明置信度:3/5 —— 代码审查干净,但该 commit 从未跑过标准套件,且其基 PR 仍未合并;何时合入应由人来决定,而不是由门禁决定。 退一步看:方案与我独立设想的一致——比我预期的更小(约 99 行生产代码),每处改动都在范围内,没有顺手夹带的改动,测试真正钉住了行为而不是装饰。六个月后再看,这会是一次有界、克制、附带设计文档(并明确说明了刻意不做什么)的改动。 我的保留意见不在代码,而在证据:
建议的合入路径:合并 #8716 → 将本 PR 重新指向 不请求修改——代码本身没有需要修的问题。 — Qwen Code · qwen3.8-max Reviewed at |
|
⏸️ Deferring to @yiliang114 — no shared test evidence exists for this commit: the standard CI suite does not trigger on the stacked base ( ⏸️ 转交 @yiliang114 —— 该 commit 目前没有共享测试证据:标准 CI 套件不会在堆叠基分支上触发( |
|
Closeout for
中文摘要本轮不修改代码或 PR body;现有 review 未发现正确性问题。PR 已记录聚焦本地测试、typecheck、lint、build、bundle 和有界 recall E2E,但由于当前堆叠在 |
wenshao
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline. Test Plan (not a blocker): src/memory/recall.test.ts — no such file or directory; src/memory/relevanceSelector.test.ts — no such file or directory; src/memory/memoryLifecycle.integration.test.ts — no such file or directory; src/memory/scan.test.ts — no such file or directory.
中文说明
已审查——无阻断问题。 建议见行内评论。 Test Plan(非阻断):src/memory/recall.test.ts — no such file or directory; src/memory/relevanceSelector.test.ts — no such file or directory; src/memory/memoryLifecycle.integration.test.ts — no such file or directory; src/memory/scan.test.ts — no such file or directory。
— glm-5.2 via Qwen Code /review (v0.21.8)
|
Changed: strengthened the existing bounded-selector tests so truncated-out paths are rejected and lexical matches keep first claim on the manifest budget. Verified: focused relevance-selector and recall tests passed 55/55, Prettier passed, and 中文摘要已加强现有 selector 测试,覆盖截断路径拒绝与词法候选优先。聚焦测试 55/55、Prettier 和 diff 检查通过。未新增独立 telemetry/logging 职责;自动 review 正在运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not explored to full depth (tool budget reached): This PR (fix(memory): recall relevant topics beyond scan ...: did not execute the new tests — the review worktree has no node_modules and installing was out of budget; all analysis is static.. Test Plan (not a blocker): src/memory/recall.test.ts — no such file or directory; src/memory/relevanceSelector.test.ts — no such file or directory; src/memory/memoryLifecycle.integration.test.ts — no such file or directory; src/memory/scan.test.ts — no such file or directory; 61 passed — this review observed 19386, 1101, 18349, 1466, 481, 2941, 454 passed.
中文说明
已审查。 建议见行内评论。 未探索到全部深度(达到工具调用预算):This PR (fix(memory): recall relevant topics beyond scan ...:did not execute the new tests — the review worktree has no node_modules and installing was out of budget; all analysis is static.。 Test Plan(非阻断):src/memory/recall.test.ts — no such file or directory; src/memory/relevanceSelector.test.ts — no such file or directory; src/memory/memoryLifecycle.integration.test.ts — no such file or directory; src/memory/scan.test.ts — no such file or directory; 61 passed — this review observed 19386, 1101, 18349, 1466, 481, 2941, 454 passed。
— qwen3.8-max via Qwen Code /review (v0.21.8)
|
Closeout for this feedback pass:
中文摘要已在候选截断前过滤活跃工具噪音,并将 20 个近期候选稳定放在词法候选前,同时避免近期填充挤掉词法命中。提交 59c7711 已推送;61 个聚焦测试、Core typecheck/build 和下游验证通过。远端 CI 与自动 review 仍在运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Not explored to full depth (tool budget reached): This PR (fix(memory): recall relevant topics beyond scan ...: could not execute the mutation checks — no node_modules in the worktree or parent checkout; all mutation analysis above is static reasoning from the source (t…; This PR (fix(memory): recall relevant topics beyond scan ...: Since node_modules isn't installed in the worktree, the changed unit tests couldn't be run.; This PR (fix(memory): recall relevant topics beyond scan ...: could not execute the changed unit tests — the review worktree has no node_modules and npx vitest fails to resolve vitest/config ; a full monorepo install …. Test Plan (not a blocker): src/memory/recall.test.ts — no such file or directory; src/memory/relevanceSelector.test.ts — no such file or directory; src/memory/memoryLifecycle.integration.test.ts — no such file or directory; src/memory/scan.test.ts — no such file or directory; 61 passed — this review observed 19390, 1101, 18350, 1466, 481, 2941, 454 passed.
中文说明
已审查。 建议见行内评论。 未探索到全部深度(达到工具调用预算):This PR (fix(memory): recall relevant topics beyond scan ...:could not execute the mutation checks — no node_modules in the worktree or parent checkout; all mutation analysis above is static reasoning from the source (t…;This PR (fix(memory): recall relevant topics beyond scan ...:Since node_modules isn't installed in the worktree, the changed unit tests couldn't be run.;This PR (fix(memory): recall relevant topics beyond scan ...:could not execute the changed unit tests — the review worktree has no node_modules and npx vitest fails to resolve vitest/config ; a full monorepo install …。 Test Plan(非阻断):src/memory/recall.test.ts — no such file or directory; src/memory/relevanceSelector.test.ts — no such file or directory; src/memory/memoryLifecycle.integration.test.ts — no such file or directory; src/memory/scan.test.ts — no such file or directory; 61 passed — this review observed 19390, 1101, 18350, 1466, 481, 2941, 454 passed。
— qwen3.8-max via Qwen Code /review (v0.21.8)
|
Changed: no code or PR body changes. One outdated thread was resolved, and two late mutation-only test-hardening requests were declined with scope evidence and resolved. Verified: the exact production code dynamically fills remaining candidate capacity and deduplicates by file path. The PR is mergeable, active review threads are zero, and applicable product checks are green. Intentionally not changed: no additional test matrix was added after five review rounds without a current behavior defect. Pending: none. The cancelled route job was reply-dispatch noise; product checks completed successfully. 中文摘要未修改代码或 PR body;关闭一条过时 thread,并按晚轮 mutation-only 测试硬化处理两条建议。当前生产逻辑正确、可合并、active thread 为 0、产品检查全绿。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline. Test Plan (not a blocker): src/memory/recall.test.ts — no such file or directory; src/memory/relevanceSelector.test.ts — no such file or directory; src/memory/memoryLifecycle.integration.test.ts — no such file or directory; src/memory/scan.test.ts — no such file or directory; 61 passed — this review observed 19391, 1101, 18350, 1466, 481, 2941 passed.
中文说明
已审查——无阻断问题。 建议见行内评论。 Test Plan(非阻断):src/memory/recall.test.ts — no such file or directory; src/memory/relevanceSelector.test.ts — no such file or directory; src/memory/memoryLifecycle.integration.test.ts — no such file or directory; src/memory/scan.test.ts — no such file or directory; 61 passed — this review observed 19391, 1101, 18350, 1466, 481, 2941 passed。
— qwen3.8-max via Qwen Code /review (v0.21.8)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline. Test Plan (not a blocker): src/memory/recall.test.ts — no such file or directory; src/memory/relevanceSelector.test.ts — no such file or directory; src/memory/memoryLifecycle.integration.test.ts — no such file or directory; src/memory/scan.test.ts — no such file or directory; 61 passed — this review observed 1101, 18350, 1466, 481, 2941, 454 passed.
中文说明
已审查——无阻断问题。 建议见行内评论。 Test Plan(非阻断):src/memory/recall.test.ts — no such file or directory; src/memory/relevanceSelector.test.ts — no such file or directory; src/memory/memoryLifecycle.integration.test.ts — no such file or directory; src/memory/scan.test.ts — no such file or directory; 61 passed — this review observed 1101, 18350, 1466, 481, 2941, 454 passed。
— qwen3.8-max via Qwen Code /review (v0.21.8)
doudouOUC
left a comment
There was a problem hiding this comment.
src/memory/recall.test.ts — no such file or directory; src/memory/relevanceSelector.test.ts — no such file or directory; src/memory/memoryLifecycle.integration.test.ts — no such file or directory; src/memory/scan.test.ts — no such file or directory.
中文说明
src/memory/recall.test.ts — no such file or directory; src/memory/relevanceSelector.test.ts — no such file or directory; src/memory/memoryLifecycle.integration.test.ts — no such file or directory; src/memory/scan.test.ts — no such file or directory。
— deepseek-v4-flash via Qwen Code /review (v0.21.8)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed — no blockers. Suggestions are inline. Test Plan (not a blocker): src/memory/recall.test.ts — no such file or directory; src/memory/relevanceSelector.test.ts — no such file or directory; src/memory/memoryLifecycle.integration.test.ts — no such file or directory; src/memory/scan.test.ts — no such file or directory; 61 passed — this review observed 1101, 18350, 1466, 481, 2941, 454 passed.
中文说明
已审查——无阻断问题。 建议见行内评论。 Test Plan(非阻断):src/memory/recall.test.ts — no such file or directory; src/memory/relevanceSelector.test.ts — no such file or directory; src/memory/memoryLifecycle.integration.test.ts — no such file or directory; src/memory/scan.test.ts — no such file or directory; 61 passed — this review observed 1101, 18350, 1466, 481, 2941, 454 passed。
— qwen3.8-max via Qwen Code /review (v0.21.8)
…M#8716) * fix(memory): improve recall delivery and multilingual fallback * fix(memory): bound heuristic recall scoring * test(memory): pin initial recall budget with fake timers Rewrite the slow-recall test to assert with fake timers that the main request is still held 1 ms inside the 100 ms initial budget and proceeds without memory at expiry, so budget changes can no longer pass unnoticed. * test(memory): pin recall budget and scoring contracts Address review findings with mutation-verified pins: - settle-early: bounded wait ends when recall settles, not at full budget - Cron and ToolResult consume points stay zero-wait - post-wait replacement guard refuses stale handles - type boost flips the winner (tie-break no longer masks its removal) - hiragana-only coverage for the CJK tokenizer - design doc: RFC QwenLM#7040 sets no numeric overhead target; fix attribution * fix(memory): preserve recall field weighting * fix(memory): recall relevant topics beyond scan cap (QwenLM#8803) * fix(memory): bound recall candidates after full scan * test(memory): pin bounded selector inputs * fix(memory): preserve bounded recall candidates * fix(memory): preserve lexical recall candidates * fix(memory): prioritize lexical model candidates * fix(core): preserve UTF-16 manifest boundaries * fix(memory): address recall review feedback * test(memory): measure recall rollout gate against the pre-change scorer RFC QwenLM#7040 gates the multilingual precision change on evidence that English Recall@5 and no-result precision do not regress. Add a labeled 45-case corpus and an evaluation harness that scores both the shipped deterministic selector and a frozen copy of the pre-change scorer over it, so the gate is reproducible rather than asserted. * fix(memory): deliver a deterministic fast recall result on the initial turn The initial-turn budget is 100 ms, but recall awaits the model selector, which is a network side query with a 30 s ceiling. The budget therefore expires on the common path and delivery falls through to the ToolResult point — which a tool-free turn never reaches, so the result is discarded as no_safe_delivery_point. That is the case memory matters most for. Publish the deterministic candidates that selectModelCandidateDocuments already computes, before blocking on the selector, and inject them when the budget expires. The refined result still lands at ToolResult, with documents the fast phase already delivered filtered out. phase telemetry now carries both stages: phase is the delivery stage, strategy is the selection method, and they are orthogonal. * docs(memory): record the fast-path decision and phase/strategy split * test(memory): report the mixed-language slice in the rollout gate * docs(memory): align recall docs on the deterministic fast path memory-system.md documented recall selection but never documented delivery, so the delivery telemetry from QwenLM#7393 was undocumented and the fast path had no home in the canonical reference. Add a delivery section and the delivery event table, and correct two docs that still described the single-path behaviour. * fix(memory): use Array<T> for the fast-path test doc lists @typescript-eslint/array-type forbids T[] for non-simple types. * docs(memory): clarify recall delivery telemetry * fix(memory): report already-delivered recall count * docs(memory): align recall delivery claims * fix(memory): rank ties by recency and record fast-delivered discards Three review follow-ups on the recall reliability change. Tie-break: `selectRelevantAutoMemoryDocuments` broke score ties with `type.localeCompare`, which orders feedback < project < reference < user. That was tolerable while the result was five documents wide; the fast path takes only MAX_FAST_RECALL_DOCS = 2, so a tied user-typed document was dropped every time — the exact memory a tool-free turn exists to surface. Ties now fall to recency, then to input order, which keeps the project-before-user precedence the concatenation already establishes. Corpus: the case labeled `semantic-no-lexical` had no relevant documents, so it was a no-result case wearing the wrong label and nothing measured the cost of "no lexical match, no score". Relabel it and add three genuine answerable-but-lexically-disjoint cases. Both scorers return nothing for them, so the slice sits outside the quality floor and is asserted separately: the fast path closes the timing gap, not the matching gap. Tool-free delivery is 92.3%, not 100%, and the residual is that slice. Telemetry: a tool-free turn logs its terminal event from the discard path, which did not apply the fast-phase exclusion. A turn whose every selected document had already been fast-delivered was recorded as `no_safe_delivery_point`, inflating the "memory never reached the model" bucket with turns that got it. Apply the same rule the ToolResult consume point uses; a partial overlap still reports the cancellation reason. * docs(memory): state the candidate-cap trade and the per-turn document count Two review follow-ups, documentation only. No behaviour change. "Removes the 200-document cap" oversold the candidate change. What it does is swap a per-scope, query-blind recency truncation for a global, query-aware one, and the effect is not a uniform widening: at or under 200 documents nothing was excluded by count under either design, but the new 25,000-byte manifest budget is a ceiling the old path lacked; between 200 and 400 with neither scope over 200 the old path sent every document and the new one sends at most 200, so fewer reach the model; only a scope over 200 is the case the change is actually for. Record all three, plus the fact that the manifest budget packs rather than prefixes. MAX_RELEVANT_DOCS = 5 bounds one prompt, not one turn. A fast delivery of two plus a refined delivery of five disjoint documents puts seven in front of the model; dedupe removes repeats, not the sum. This follows from dropping combined fast/refined budget accounting, which was a deliberate choice, but the number was never written down next to the constant that reads like a hard cap. * fix(memory): end the initial recall wait on the fast result, widen tokenization The 100 ms initial budget was a fixed cost, and the evidence for it measured the wrong thing. Deterministic *scoring* is microseconds, but the fast result is only published once recall has enumerated, read, and parsed the memory tree — and this branch removed the 200-document cap for recall, so that scan grows with the tree. recall-scan-latency.test.ts adds that measurement against a real temporary tree: ~29 ms at 200 topics, ~70 ms at 500, ~130 ms at 1000. So for any tree small enough to scan in time — the ordinary case — the fast result was in hand tens of milliseconds before the budget expired, and the rest of the budget was spent waiting on a model selector this design already assumes will miss it. The wait now ends on whichever comes first: recall settling, the fast result being published, cancellation, or the ceiling. The preference order is unchanged, because the code after the wait still prefers a settled recall. Past roughly a thousand topics the scan alone exceeds the ceiling and the turn pays the full budget for nothing; that is recorded as a known limitation rather than fixed, since the fix is a persistent catalog. Tokenization kept only [a-z0-9]{3,} runs, so Cyrillic, Greek, Arabic, and accented Latin produced no tokens at all and the deterministic path was unconditionally silent for them. Keep whole runs of non-CJK letters, marks, and digits instead. CJK is excluded per character rather than by alternation order: \p{L} also matches Han, so a Latin-initial run would otherwise swallow the CJK after it and turn abc漢字 into one token. Scripts without word separators outside the CJK set still collapse to one run, which is recorded rather than claimed as segmentation. Two smaller follow-ups. The active-tool alias set is now derived once per recall instead of once per scanned document, which mattered little under the old 200-document cap and more without it. And the eval prints the Recall@5 a query-blind random scorer would score on this corpus (20%), with a test holding that floor at or below 25%, because a small corpus flatters every design and the headline was unreadable without it. * docs(memory): correct the initial-turn preference claim, pin it with a test Local end-to-end verification on QwenLM#8716 found the claim added in 01ef7d7 — "the preference order is unchanged: whatever ends the wait, a settled recall is still delivered in preference to the fast result" — to be false in the case that matters. `onFastResult` is published before recall issues the selector request at all, so the recall promise cannot be settled when the wait ends on the fast result. Measured against a selector settling in 15 ms, comfortably inside the ceiling, the initial turn still delivers the deterministic pair and discards the model's picks. The behaviour is right and stays: a model side query does not return inside a 100 ms ceiling in production, so arbitrating would spend the rest of the budget on every turn to win a race that does not happen, and the selector's judgement still lands at ToolResult with the fast documents excluded. What was wrong was the description. State it directly instead — on the initial turn, once the deterministic scorer matches, the fast result wins regardless of selector latency — and pin it with a test that fails when the early exit is removed, so it reads as a decision rather than an accident. Two measurements corrected while here. The scan crossover is machine- dependent, not a fixed topic count: the same three sizes measure 9/21/46 ms on faster hardware against 29/70/130 ms on the machine the tables were written from, so the ceiling is not reached there at all. And MAX_MODEL_CANDIDATE_DOCS = 200 is rarely the binding constraint — MAX_MODEL_MANIFEST_BYTES is, at roughly 90-150 documents once absolute paths and timestamps are counted. Measured runs sent 94 and 96 manifest lines where the document cap would have allowed 200, which also explains why the recency reserve has to be interleaved rather than appended. --------- Co-authored-by: yiliang114 <jinjing.zzj@gmail.com> Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>

What this PR does
This PR lets native memory recall consider relevant project and user topics beyond the shared 200-document scan window, while keeping model selection bounded. Recall ranks the complete parsed pool, sends at most 200 candidates with a lexical/recent balance, and limits the selector manifest to 25,000 UTF-8 bytes. Forget, Indexer, Status, Extraction, and team-memory scanning keep their existing limits.
Why it's needed
Before this change, the scanner parsed every topic and then returned only the 200 most recent documents. Recall consumed that capped result, so an older but directly relevant memory could never reach either the heuristic fallback or the model selector. This preserves the existing safety bounds at the model boundary without discarding relevant topics before ranking.
Reviewer Test Plan
How to verify
Local verification completed:
npx vitest run src/memory/recall.test.ts src/memory/relevanceSelector.test.ts src/memory/memoryLifecycle.integration.test.ts src/memory/scan.test.ts— 61/61 passednpm run lintnpm run typechecknpm run buildnpm run bundleEvidence (Before & After)
Before: the regular 200-document scan excludes the deliberately old target topic.
After, the bundled CLI receives the recalled marker at the next safe delivery point after the bounded initial wait:
The E2E is headless and has no visual UI surface, so there is no meaningful screenshot to attach.
Tested on
Environment (optional)
Local bundled CLI with an isolated memory home and an OpenAI-compatible mock endpoint.
Risk & Scope
Linked Issues
Refs #7040
Depends on #8716. This PR is stacked on
codex/7040-memory-recalland should be retargeted tomainafter #8716 merges.中文说明
这个 PR 做了什么
这个 PR 让原生记忆召回可以考虑共享 200 文档扫描窗口之外的相关项目级和用户级主题,同时保持模型选择有界。召回会对完整的已解析候选池排序,以词法相关性和近期文档相结合的方式最多向模型选择器发送 200 个候选,并把选择器 manifest 限制在 25,000 UTF-8 字节以内。Forget、Indexer、Status、Extraction 和团队记忆扫描继续保留原有上限。
为什么需要它
改动前,扫描器会解析全部主题,但最终只返回最近的 200 个文档。召回直接使用这个有上限的结果,因此较旧但明确相关的记忆无法进入启发式回退或模型选择器。这个改动在模型边界继续保留已有安全上限,同时避免在排序前丢弃相关主题。
Reviewer 测试计划
如何验证
已完成本地验证:
npx vitest run src/memory/recall.test.ts src/memory/relevanceSelector.test.ts src/memory/memoryLifecycle.integration.test.ts src/memory/scan.test.ts— 61/61 通过npm run lintnpm run typechecknpm run buildnpm run bundleBefore & After 证据
改动前:常规 200 文档扫描会排除刻意设为较旧的目标主题。
改动后:在有界初始等待结束后的下一个安全注入点,bundle CLI 收到了召回标记:
这个 E2E 是 headless 流程,没有可视 UI 界面,因此没有有意义的截图可附加。
测试平台
环境(可选)
本地 bundle CLI、隔离的记忆目录和 OpenAI 兼容 mock 端点。
风险与范围
关联 Issue
Refs #7040
依赖 #8716。这个 PR 堆叠在
codex/7040-memory-recall上,应在 #8716 合并后把 base 改回main。