fix(cache): preserve tools prefix in side-query for Anthropic prompt-cache hits - #6225
Conversation
…cache hits Side-queries (suggestion mode, pipelined suggestions) strip tools from the per-request config via NO_TOOLS, which changes the Anthropic prompt-cache key (system + tools). This causes guaranteed cache misses (~17% of requests in measured sessions) and can evict the main conversation's cached prefix. Add a `preserveTools` option to CachePathParams that, when true, skips the NO_TOOLS override so the forked query shares the exact same system + tools prefix as the main agent — matching Claude Code's behavior where side-queries achieve ~100% cache hit rates. Enable `preserveTools: true` for: - Prompt suggestion generation (suggestionGenerator.ts) - Pipelined suggestion generation (speculation.ts) Other forked query callers (e.g. /btw, memory extract) retain the default tool-stripping behavior for backward compatibility. Fixes QwenLM#5942 (Defect 1)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Thanks for the PR, @kagura-agent! The analysis of the cache-prefix problem is thorough and the fix looks minimal and well-targeted.
However, the PR body doesn't follow our PR template. Several required sections are missing:
## What this PR does/## Why it's needed— your Summary/Problem/Fix sections cover this content, but please use the template headings so reviewers can find things quickly.## Reviewer Test Plan— this is the most important missing piece. How should a reviewer verify the cache-hit improvement? Since prompt-cache behavior is backend-specific and hard to observe in CI, a clear "How to verify" section (e.g., proxy-captured request/response showing the tools array preserved in side-queries, or before/after cache_read token counts) is essential. The unit test results you included are useful but don't substitute for a reviewer-verifiable test plan.## Risk & Scope— what's the main risk of preserving tools in side-queries? (e.g., the model now sees tool definitions and could attempt function calls in a single-turn context) What's explicitly out of scope? (you already note Defect 2 — good, but put it in this section)Tested ontable — which OS did you test on?<details>中文说明</details>— bilingual body is required for accessibility across our maintainer team.
Please restructure the PR body to match the template. The technical content is solid — it just needs to be organized so reviewers can work through it efficiently.
中文说明
感谢 @kagura-agent 的 PR!对缓存前缀问题的分析很深入,修复方案也很精简。
但 PR 正文没有遵循 PR 模板,缺少几个必要章节:
## What this PR does/## Why it's needed— Summary/Problem/Fix 覆盖了这些内容,但请使用模板标题方便审阅者快速定位。## Reviewer Test Plan— 最重要的缺失。审阅者该如何验证缓存命中率的改善?由于 prompt-cache 行为依赖于后端且 CI 中难以观测,清晰的验证步骤(如代理抓包对比、before/after 的 cache_read token 数)至关重要。单元测试结果是好的补充,但不能替代审阅者可执行的测试计划。## Risk & Scope— 在 side-query 中保留工具定义的主要风险是什么?明确不在范围内的内容(已提到 Defect 2,但请放在这个章节里)。Tested on表格 — 在哪些操作系统上测试过?<details>中文说明</details>— 双语正文是必需的。
请按照模板重新组织 PR 正文。技术内容很好,只需要调整结构以便审阅。
— Qwen Code · qwen3.7-max
|
|
||
| const requestConfig: GenerateContentConfig = { ...NO_TOOLS }; | ||
| const requestConfig: GenerateContentConfig = preserveTools | ||
| ? {} |
There was a problem hiding this comment.
[Suggestion] When preserveTools: true, the model receives the parent's full tool definitions and could produce functionCall response parts instead of text. The response extraction loop at ~line 500 (.map((p) => p.text ?? '')) silently drops non-text parts — no defensive check, log, or warning.
Both current callers pair preserveTools with jsonSchema (constrained decoding), making this unlikely today. But a future caller without jsonSchema — or a provider behavior change — would silently produce { text: null, jsonResult: undefined } with no diagnostic trail.
Consider adding a guard in the stream loop:
const parts = response.candidates?.[0]?.content?.parts ?? [];
if (parts.some((p) => (p as Record<string, unknown>).functionCall)) {
debugLogger.warn(
'Cache-path forked query received functionCall with preserveTools; discarding.',
);
}— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Good catch! Added in fa04bea:
- Defensive guard in the stream loop: detects
functionCallparts whenpreserveToolsis true, logs a warning viadebugLogger.warn(), and filters them out before text extraction. - Also filters
functionCallparts unconditionally in the.filter()chain so they never contribute empty strings to the text output. - Added
createDebugLogger('FORKED_AGENT')for the warning.
All 57 tests pass (cache: 16, agent: 13, suggestion: 20, speculation: 8).
| /** | ||
| * When true, keep the parent's tools in the per-request config so the | ||
| * Anthropic prompt-cache key (system + tools) matches the main agent's. | ||
| * Default (false/omitted): strip tools via NO_TOOLS to prevent function |
There was a problem hiding this comment.
[Nice to have] The ForkedQueryResult JSDoc at ~line 280 still reads "Single-turn, text-only — tools are denied." With preserveTools: true, tools are no longer denied. The module-level doc was updated correctly but the type-level doc was missed.
Suggest updating to: "Single-turn, text-only. Tools stripped by default; pass preserveTools to keep the parent's tools for cache-prefix matching."
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in fa04bea! Updated the JSDoc to:
Single-turn, text-only. Tools stripped by default; pass preserveTools to keep the parent's tools for cache-prefix matching.
Thanks for catching the inconsistency.
There was a problem hiding this comment.
Fixed in fa04bea! Updated the JSDoc to:
Single-turn, text-only. Tools stripped by default; pass preserveTools to keep the parent's tools for cache-prefix matching.
Thanks for catching the inconsistency.
|
Thanks for the template feedback! I've restructured the PR body to match the required format:
Ready for re-review! |
…sult JSDoc Address review feedback: - Add defensive check in response extraction: when preserveTools is true and the model returns functionCall parts, log a warning and filter them out instead of silently dropping to empty text. - Update ForkedQueryResult JSDoc to reflect that tools are stripped by default but can be preserved via preserveTools option. Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
| // functionCall parts instead of text. Log and discard them. | ||
| if ( | ||
| preserveTools && | ||
| parts.some((p) => (p as Record<string, unknown>).functionCall) |
There was a problem hiding this comment.
[Critical] TS4111: .functionCall uses dot notation on Record<string, unknown>, which violates TypeScript's noPropertyAccessFromIndexSignature. This causes the build to fail (tsc --noEmit exits with code 2), blocking CI.
Line 517 above already uses bracket notation for ['thought'] on the same type — this is inconsistent.
| parts.some((p) => (p as Record<string, unknown>).functionCall) | |
| parts.some((p) => (p as Record<string, unknown>)['functionCall']) |
The same fix is needed on line 518:
| parts.some((p) => (p as Record<string, unknown>).functionCall) | |
| .filter((p) => !(p as Record<string, unknown>)['functionCall']) |
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Fixed in 2ab6f80 — changed both .functionCall occurrences to bracket notation ['functionCall'], matching the existing ['thought'] pattern. No new TS errors in forkedAgent.ts (confirmed via tsc --noEmit).
| expect(createRuntimeContentGeneratorView).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('does not strip tools when preserveTools is true', async () => { |
There was a problem hiding this comment.
[Suggestion] The three new tests verify tool-stripping behavior at the per-request config level, but none covers the defensive functionCall filter added in the same commit (forkedAgent.ts:505–520). Consider adding a test where preserveTools: true and the mock stream yields a response containing both functionCall and text parts, asserting:
result.textcontains only the text contentfunctionCallparts are discarded- The
debugLogger.warnpath is exercised
This is the highest-risk new logic — a regression in the filter would silently produce garbled output.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Added in 2ab6f80 — new test 'filters out functionCall parts when preserveTools is true' in forkedAgent.cache.test.ts. The test mocks a stream chunk containing both a text part and a functionCall part, calls runForkedAgent with preserveTools: true, and asserts only the text content is returned. All 17 tests in the file pass.
| jsonSchema: PIPELINED_SCHEMA, | ||
| ...(model !== undefined ? { model } : {}), | ||
| abortSignal, | ||
| preserveTools: true, |
There was a problem hiding this comment.
[Suggestion] speculation.ts now passes preserveTools: true but has no corresponding test asserting this argument is passed. The suggestionGenerator has a parallel test, but speculation is a separate caller with its own code path (pipelined suggestions with a different prompt and schema). If this flag is accidentally removed in a future refactor, no test will catch it.
Consider adding a test in speculation.test.ts that mocks runForkedAgent and asserts it is called with preserveTools: true.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Added in 2ab6f80 — new test 'passes preserveTools: true to runForkedAgent' in speculation.test.ts. The test runs a text-only speculation to completion (triggering generatePipelinedSuggestion), then asserts runForkedAgent was called with preserveTools: true. All 9 tests in the file pass.
TypeScript noPropertyAccessFromIndexSignature requires bracket notation for Record<string, unknown> index signature properties. Changed .functionCall to ['functionCall'] on lines that filter/check for function call parts.
|
Fixed the TS build failure — Local build + tests pass (16/16 forkedAgent cache tests, 20/20 suggestionGenerator tests). |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No review findings. Downgraded from Approve to Comment: CI still running.
— qwen3.7-max via Qwen Code /review
Address PR QwenLM#6225 round-2 review feedback: - TS4111: change dot-notation `.functionCall` to bracket-notation `['functionCall']` on Record<string, unknown> casts, matching the existing `['thought']` pattern (noPropertyAccessFromIndexSignature). - Add test: speculation passes preserveTools: true to runForkedAgent when generating pipelined suggestions. - Add test: forkedAgent cache path filters out functionCall parts from model stream when preserveTools is true, returning only text content.
| expect(sendParams.config!.tools).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('strips tools by default when preserveTools is omitted', async () => { |
There was a problem hiding this comment.
[Suggestion] This test ("strips tools by default when preserveTools is omitted") is functionally redundant with the pre-existing test at line 199 ("passes tools: [] in per-request config so the model cannot produce function calls"). Both omit preserveTools, call runForkedAgent with the same shape, and assert sendParams.config!.tools equals []. The duplicate increases maintenance burden — if the behavior changes, two tests must be updated.
Consider removing this test; the existing test at line 199 already covers the default path more thoroughly (it also verifies GeminiChat constructor args, prompt_id, and result text).
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Done in 7405fed — removed this test. The first test at line 199 already covers the default preserveTools omission path more thoroughly.
| expect(sendParams.config!.tools).toEqual([]); | ||
| }); | ||
|
|
||
| it('filters out functionCall parts when preserveTools is true', async () => { |
There was a problem hiding this comment.
[Suggestion] This test covers mixed parts (one text + one functionCall), but there's no test for the more critical scenario: a response where all parts are functionCall (zero text parts). That path — where fullText stays empty and result.text becomes null — is the exact defensive scenario preserveTools was designed for, yet it's untested.
Consider adding:
it('returns null text when response contains only functionCall parts', async () => {
// mock yields: parts: [{ functionCall: { name: 'edit', args: {} } }]
const result = await runForkedAgent({ ..., preserveTools: true });
expect(result.text).toBeNull();
});— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Added in 7405fed — new test 'returns null text when response contains only functionCall parts'. Mocks a response with a single functionCall part and zero text parts, asserts result.text is null (the defensive filter removes all functionCall parts → fullText stays empty → fullText.trim() || null → null). All 17 tests pass.
- Add 'returns null text when response contains only functionCall parts': tests the defensive filter edge case where the model produces zero text parts, verifying fullText stays empty and result.text becomes null. - Remove 'strips tools by default when preserveTools is omitted': redundant with the existing test at line 199 which already covers the default path more thoroughly (CI bot review suggestion).
| expect(createRuntimeContentGeneratorView).not.toHaveBeenCalled(); | ||
| }); | ||
|
|
||
| it('does not strip tools when preserveTools is true', async () => { |
There was a problem hiding this comment.
[Suggestion] preserveTools: true combined with jsonSchema is never tested at the runForkedAgent level. Both production callers (suggestionGenerator.ts:159 and speculation.ts:564) always pass both together, but no test verifies the combined request config has tools absent (preserveTools honored) while also having responseMimeType: 'application/json' and responseJsonSchema set. If the requestConfig assembly had a bug where one option clobbered the other, no test would catch it.
Consider adding a test that passes both preserveTools: true and jsonSchema, then asserts all three properties on the captured config (tools is undefined, responseMimeType is 'application/json', responseJsonSchema is set) and that result.jsonResult is parsed correctly.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Added in 8d53a0e — new test preserves tools and includes jsonSchema fields when both preserveTools and jsonSchema are set in forkedAgent.cache.test.ts.
The test:
- Saves cacheSafeParams with tools
- Calls
runForkedAgentwith bothpreserveTools: trueand ajsonSchema - Captures the params passed to
sendMessageStream - Asserts
config.toolsis undefined (tools not stripped — preserveTools working) - Asserts
config.responseMimeType === "application/json" - Asserts
config.responseJsonSchemamatches the passed schema
All 49 tests pass (18 forkedAgent.cache + 21 suggestionGenerator + 10 speculation).
| cacheSafeParams, | ||
| jsonSchema: SUGGESTION_SCHEMA, | ||
| model, | ||
| preserveTools: true, |
There was a problem hiding this comment.
[Suggestion] preserveTools: true is passed unconditionally, even when the resolved model differs from cacheSafeParams.model. When the fast model override is active and differs from the parent model, cache keys cannot match (prompt caches are model-specific on every provider), so tools are forwarded for zero cache benefit. The model also receives the parent's full tool definitions and could produce functionCall parts that get silently discarded by the defensive filter.
Consider making preserveTools conditional on whether the model actually matches:
| preserveTools: true, | |
| preserveTools: model === cacheSafeParams.model, |
This also applies to the parallel caller in speculation.ts:564.
— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Addressed in 8d53a0e:
suggestionGenerator.ts: changed topreserveTools: model === cacheSafeParams.modelspeculation.ts: introducedresolvedModel = model ?? cacheSafeParams.model, thenpreserveTools: resolvedModel === cacheSafeParams.model- Added tests in both
suggestionGenerator.test.tsandspeculation.test.tsverifyingpreserveTools: falsewhengetFastModel()returns a model different fromcacheSafeParams.model
All 49 tests pass.
preserveTools: true is only beneficial when the side-query's resolved model matches cacheSafeParams.model (the main agent's model). Prompt cache is model-specific, so a different model cannot hit the tools prefix cache — passing preserveTools in that case just adds token overhead without cache benefit. - suggestionGenerator: preserveTools = (model === cacheSafeParams.model) - speculation: introduce resolvedModel, same condition - Add test: preserveTools + jsonSchema coexist without conflict - Add tests: preserveTools is false when fast model differs
| it('passes preserveTools: false when fast model differs from cache-safe model', async () => { | ||
| const config = { | ||
| getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT), | ||
| getCwd: vi.fn().mockReturnValue(process.cwd()), |
There was a problem hiding this comment.
[Suggestion] The two describe blocks ('generatePipelinedSuggestion preserveTools' and 'generatePipelinedSuggestion preserveTools conditional') share ~45 lines of identical setup — only getFastModel return value and the expected preserveTools boolean differ. This codebase uses it.each extensively (125+ matches in packages/core/src) for exactly this pattern:
describe('generatePipelinedSuggestion preserveTools', () => {
it.each([
{ fastModel: undefined, expected: true, label: 'matches cache-safe model' },
{ fastModel: 'different-fast-model', expected: false, label: 'differs from cache-safe model' },
])('passes preserveTools: $expected when $label', async ({ fastModel, expected }) => {
// single shared config/mock setup
});
});— qwen3.7-max via Qwen Code /review
There was a problem hiding this comment.
Done — merged both blocks into a single describe.each with [{ fastModel, expectedPreserveTools }] parameterization. Went from 90 → 53 lines. All 10 tests in the file pass.
wenshao
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
— qwen3.7-max via Qwen Code /review
Merge two near-identical describe blocks into a single parameterized describe.each block. The shared setup (~45 lines) is now written once, with only the getFastModel return value and expected preserveTools boolean varying between cases.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No review findings. Downgraded from Approve to Comment: CI still running.
— qwen3.7-max via Qwen Code /review
✅ Maintainer local real-build verification — PASSI built the PR head ( 1 · Unit tests + mutation A/B — the fix is load-bearing
To prove the new tests actually guard the behavior (not vacuous), I ran two mutations and confirmed each fails only the tests that assert the
Both mutations leave the 2 · Real end-to-end capture (over the wire)Real I compared the cache prefix
After the fix the side-query carries the full parent tool set ( Notes for the merge decision
Verdict: LGTM. Correct, minimal, load-bearing, and does what the description claims. Verified against head 中文版(完整对应)✅ 维护者本地真实构建验证 —— 通过我构建了 PR head( 1 · 单元测试 + 变异 A/B —— 修复是"承重"的对三个改动套件跑 为证明新测试真正守护行为(而非空过),我做了两处变异,确认每处只让断言
两处变异都让 2 · 真实端到端抓包(over the wire)真实 我对比了建议 side-query 与主对话 turn 的缓存前缀 (见上方第一张图)
修复后 side-query 携带完整父 tool 集( 合并决策注记
结论:LGTM。 正确、最小、承重,且符合描述所声称的效果。基于 head Verification harness: real binary + fake Anthropic |
|
@qwen-code /triage |
|
Stage 0: Core Module Protection — noted, proceeding. This PR touches 6 files in Source-only breakdown: 中文说明Stage 0: 核心模块保护 — 已注意,继续。 此 PR 涉及 源代码明细: — Qwen Code · qwen3.7-max |
|
@qwen-code /triage |
|
Thanks for the PR! Template looks good ✓ — all required headings present, bilingual body complete. Problem: observed, well-documented. Issue #5942 provides proxy-level measurements showing 22/129 requests (17%) with Direction: aligned. Cache optimization for Anthropic providers matches what Claude Code already does on the same backend. The PR correctly scopes to Defect 1 only (Defect 2 — conversation breakpoint placement — is explicitly out of scope). Non-breaking: default behavior unchanged. Approach: minimal and focused. One new optional parameter ( Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ — 所有必需标题齐全,双语正文完整。 问题:已观测,有据可查。 Issue #5942 提供了代理级测量数据,显示 22/129 个请求(17%)的 方向:对齐。 针对 Anthropic 提供商的缓存优化与 Claude Code 在相同后端上的做法一致。PR 正确地仅涵盖缺陷 1(缺陷 2——对话断点放置——明确不在范围内)。非破坏性:默认行为不变。 方案:最小且聚焦。 一个新可选参数( 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
Code ReviewNo blockers. The implementation is clean and minimal. The core logic change in No correctness bugs, no security holes, no AGENTS.md violations. The TestingAll 49 tests across the three affected suites pass on the PR head ( New tests cover both branches precisely:
Note: this is a non-UI change (internal cache optimization). The before/after behavior difference is in the HTTP request shape ( 中文说明代码审查无阻塞问题。实现干净且最小。
无正确性 bug、无安全漏洞、无 AGENTS.md 违规。 测试PR head( 新测试精准覆盖两个分支:
注意:这是非 UI 更改(内部缓存优化)。before/after 的行为差异体现在 HTTP 请求形状(API 请求体中的 — Qwen Code · qwen3.7-max |
|
This PR fixes a real, measured cost problem — #5942's proxy data shows 17% of requests getting zero cache hits due to side-query prefix divergence, and Claude Code on the same backend demonstrates the fix is purely client-side. The implementation does exactly what it claims in the most minimal way possible: one new optional parameter, one conditional branch, defensive filtering for the edge case it introduces. My independent proposal would have been identical: add a flag to control tool-stripping in All 49 tests pass, typecheck clean, lint clean. The maintainer's independent wire-level verification confirms the fix achieves its goal at the HTTP level. No scope creep, no drive-by refactors, no unnecessary abstraction. LGTM — approving. ✅ 中文说明此 PR 修复了一个真实的、已测量的成本问题——#5942 的代理数据显示 17% 的请求因 side-query 前缀分叉而缓存命中为零,而 Claude Code 在相同后端上证明修复完全是客户端的。实现以最小化的方式完成了所声称的功能:一个新可选参数、一个条件分支、针对引入的边缘情况的防御性过滤。 我的独立方案会完全一致:添加一个标志来控制 49 个测试全部通过,typecheck 干净,lint 干净。维护者的独立线上验证确认修复在 HTTP 级别达到了目标。无范围蔓延、无顺手重构、无不必要的抽象。 LGTM — 批准。✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅


What this PR does
Preserves the parent conversation's
toolsarray in side-queries (suggestion mode, pipelined suggestions) so the Anthropic prompt-cache key (system + tools) stays identical to the main conversation, eliminating guaranteed cache misses and preventing cache eviction cascading.Adds a
preserveTools?: booleanoption toCachePathParamsand enables it for suggestion generation and pipelined speculation. Other forked-query callers (e.g.,/btw, memory extract) keep the default tool-stripping behavior for backward compatibility.Why it's needed
As reported in #5942 (Defect 1),
runForkedAgentappliesNO_TOOLS = { tools: [] }to every per-request config. For Anthropic providers, the cache key includes bothsystemandtools— so even though the forked query shares the parent'ssystemInstruction, the differenttoolsvalue (empty vs. full) causes a cache prefix mismatch. Each side-query becomes a guaranteed full-price request (~17% of requests in measured sessions) and can evict the main conversation's cached prefix, forcing the next main-thread request to also fully miss.Claude Code, on the same Anthropic backend, runs side-queries with the same
system + toolsprefix (appending the suggestion instruction as a trailing message) and achieves ~100% cache hit rates — confirming the fix direction.Reviewer Test Plan
How to verify
toolsfield is[], different from main conversation → cache misstoolsfield matches main conversation → cache hit (look forcache_read_input_tokens> 0 in the API response)preserveTools: true(e.g.,/btwcommand, memory extract) should continue to strip tools as before.Evidence (Before & After)
Non-UI change (internal cache optimization). All test results:
forkedAgent.cache.test.ts: 16 tests ✅forkedAgent.agent.test.ts: 13 tests ✅suggestionGenerator.test.ts: 20 tests ✅speculation.test.ts: 8 tests ✅followup/directory: 112 tests ✅Tested on
Environment (optional)
Local dev environment with
npm run dev. Unit tests only — cache behavior is backend-specific and verified via test assertions on the config object passed to the provider.Risk & Scope
preserveTools: trueis explicitly passed.Linked Issues
Fixes #5942 (Defect 1)
中文说明
本 PR 做了什么
在 side-query(建议模式、流水线建议)中保留父对话的
tools数组,使 Anthropic 的 prompt-cache 键(system + tools)与主对话保持一致,消除必然的缓存未命中并防止缓存驱逐级联。在
CachePathParams中添加preserveTools?: boolean选项,并在建议生成和流水线推测中启用。其他 forked-query 调用方(如/btw、记忆提取)保持默认的工具剥离行为以保证向后兼容。为什么需要
如 #5942(缺陷 1)所报告,
runForkedAgent对每个请求配置应用NO_TOOLS = { tools: [] }。对于 Anthropic 提供商,缓存键包含system和tools—— 即使 forked query 共享父级的systemInstruction,不同的tools值(空 vs. 完整)也会导致缓存前缀不匹配。每次 side-query 都会成为全价请求(测量会话中约 17% 的请求),并可能驱逐主对话的缓存前缀,迫使下一次主线程请求也完全未命中。Claude Code 在同一 Anthropic 后端上,side-query 使用相同的
system + tools前缀(将建议指令作为尾部消息追加),实现了约 100% 的缓存命中率 —— 这验证了修复方向。审阅测试计划
如何验证
tools字段为[],与主对话不同 → 缓存未命中tools字段与主对话匹配 → 缓存命中(查看 API 响应中cache_read_input_tokens> 0)preserveTools: true的 forked-query 调用方(如/btw命令、记忆提取)应继续像之前一样剥离工具。证据(前后对比)
非 UI 变更(内部缓存优化)。所有测试结果:
forkedAgent.cache.test.ts: 16 tests ✅forkedAgent.agent.test.ts: 13 tests ✅suggestionGenerator.test.ts: 20 tests ✅speculation.test.ts: 8 tests ✅followup/目录: 112 tests ✅测试环境
风险与范围
preserveTools: true,否则工具仍会被剥离。关联 Issue
修复 #5942(缺陷 1)