perf(core): clear tool results to a low watermark to preserve prompt cache - #8464
Conversation
…cache Size-triggered microcompaction now clears oldest compactable tool results down to half the threshold instead of stopping just below it, so the conversation prefix stays stable between clearings and provider prompt caches keep matching. The recent-result budget now protects committed results only; pending results no longer consume protection slots but stay counted, uncleared, and live for file-read-cache resolution. Adds the watermark to cleanup metadata and the debug log. Fixes QwenLM#8463
|
@qwen-code /verify |
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 39 passed · 0 failed · 39 total 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:39 通过 · 0 失败 · 39 总计 Verification reportVerification report — PR #8464 (perf(core): clear tool results to a low watermark to preserve prompt cache)Verdict: 中文摘要
Central claim + A/BCentral claim: clearing oldest compactable tool results down to Environment: head = prebuilt Witness:
The W cell reproduces the PR's claimed numbers exactly (148 and 14), and the P1 cell shows the protection-accounting fix is load-bearing in the regime the threshold-stop previously masked: with pending alone over the threshold, base blanked all committed results including the five most recent ( Reviewer Test Plan walk (per step): ① size debug log fires once then stays silent — W cell (14 metas, 153 silent turns) ✔; ② totals-exceeded log with FindingsS-1 (Suggestion, non-blocking): the S-2 (cosmetic): stale test counts in the description. The body cites 439 core / 34 cli tests; at the verified head the same commands run 460 / 40 (all passing). The numbers were evidently written mid-development; no action needed beyond awareness. Not covered
MethodologyNode v22.23.2 in the CI verify container; merge-ref checkout (HEAD = Evidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
|
✅ Qwen Triage finished — CI landed green on ✅ Qwen Triage 已完成 —— |
|
Thanks for the PR! Template looks good ✓ Problem: observed, not theoretical. Linked issue #8463 carries quantitative production evidence: long sessions pinned at a ~5.43% cache rate vs 99.90% for comparable sessions without history rewrites, ~63% of all cache loss in the analysis window — plus a concrete second defect (pending tool results consuming the Direction: aligned. The size trigger shipped in #5111 achieves its bounding goal but rewrites the prompt prefix every turn once the budget rides the threshold, defeating provider prompt caches. Prompt-cache preservation is a recurring, explicit focus in this CLI category (Claude Code's changelog has several entries fixing cache invalidation from mid-session changes). No new settings, no public contract change. Size: core paths touched ( Approach: low-watermark (hysteresis) clearing is the standard fix for threshold flapping and matches what I'd propose independently. Scope is minimal: the trigger condition, Risk: no elevated risk signals — no changed file matches the revert-correlated high-risk paths. Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 问题:已观测到的问题,不是理论性的。关联 issue #8463 带有定量的生产证据:长会话缓存率被钉在约 5.43%(对比无历史改写会话的 99.90%),占分析窗口内缓存损失约 63%——外加第二个具体缺陷(待发送工具结果占用 方向:对齐。#5111 引入的 size 触发达成了预算上限目标,但预算贴着阈值时每轮都会改写 prompt 前缀,破坏 provider prompt cache。prompt cache 保护在这一类 CLI 中是反复出现的明确重点(Claude Code changelog 有多条修复中途变更导致缓存失效的条目)。无新配置项,无公共契约变更。 规模:触及核心路径。约 43 行生产逻辑(microcompact.ts +28、client.ts +13、settingsSchema.ts +2),216 行测试,32 行文档/生成 schema——远低于任何阈值。 方案:低水位(迟滞)清理是阈值抖动的标准修法,与我的独立提议一致。范围最小:触发条件、 风险:无升级风险信号——改动文件均未命中与 revert 相关的高风险路径。 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewMy independent proposal before reading the diff: threshold flapping calls for hysteresis — when the total crosses the threshold, clear down to a low watermark (~half the threshold) instead of "just below", and fix the What I verified by reading the code at all three
No correctness, security, or convention issues found. The Test evidenceAt the reviewed commit, the fork-approval gate leaves only the Linux unit suite running — macOS/Windows and integration jobs are skipped, and nothing has failed: Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 The main unit suite is still running, so there is no result to quote yet — not verified: final unit-suite outcome on this commit. The author reports 439 + 34 focused unit tests passing locally; that is the author's claim, not independently re-run here (this review never executes PR code). Sandboxed verification would settle the one claim static review and unit CI cannot: that the watermark actually reduces provider-visible history rewrites on a live session. A 中文说明代码审查:读 diff 之前我的独立提议就是迟滞式低水位清理——越过阈值后清到低水位(约阈值一半)而非"刚好低于阈值",并让
未发现正确性、安全性或规范问题;元数据字段为纯增量,配置描述在 schema 源、生成的 VS Code schema、文档三处同步;diff 无夹带改动。 测试证据:受 fork 审批门限制,当前仅 Linux 单元测试在运行(macOS/Windows/集成测试被跳过),暂无失败。主套件尚未出结果——本提交上的最终单测结果未验证。作者本地报告 439+34 个相关单测通过——这是作者声明,非本审查独立复跑(本审查从不执行 PR 代码)。 沙箱验证将解决静态审查与单测 CI 无法覆盖的核心主张:水位是否真的减少了真实会话中 provider 可见的历史改写。本 PR 已有一个进行中的 — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — clean review throughout; what's left unverified is procedural (unit suite still running, live A/B verify run still in flight), not substantive doubt. This PR does one thing, minimally. When the tool-result budget crosses the threshold it now clears down to half the threshold instead of stopping at "just below" — the standard hysteresis fix for threshold flapping — and it fixes the protection-accounting bug that deeper clearing would otherwise have exposed. That matches the proposal I wrote down before reading the diff; I found no simpler path. Every line in the diff earns its place: the behavior change, the tests pinning it (including the 167-result workload simulation asserting exactly 14 rewrites), and the settings description kept in sync across three places. No drive-bys, no new settings, no public contract change. The motivating problem is real and measured — the linked issue carries production cache-rate data and was maintainer-triaged as P2 on the context-performance roadmap — not hypothetical. The honest cost, old outputs becoming placeholders earlier (retention at clearing drops from ~500K chars to ~250K), is disclosed in the PR itself. Two reservations, neither blocking: the headline 80–90% cache-rate recovery is an estimate pending replay validation (the mechanism it rests on — rewrite count dropping from ~148 to 14 — is the part the tests pin), and the unit suite for this commit hasn't finished. The in-flight Verdict: approve. Approval is deferred until CI lands green on 中文说明反思:这个 PR 只做一件事且改动最小——工具结果预算越过阈值时清理到阈值一半(低水位迟滞,阈值抖动的标准修法),而不是清到"刚好低于阈值",同时修复了更深清理会暴露的保护配额缺陷。这与我读 diff 前写下的独立提议一致,没有找到更简路径。diff 每一行都必要:行为变更、钉住它的测试(包括断言恰好 14 次改写的 167 条结果负载模拟)、三处同步的配置描述。无夹带改动、无新配置项、无公共契约变更。 解决的问题真实且经过测量——关联 issue 带有生产缓存率数据,已被维护者定为 P2 并列入 context-performance roadmap——不是假设性问题。诚实的代价——旧输出更早变成占位符(清理时保留量从约 500K 字符降到约 250K)——PR 本身已明确披露。 两点保留,均不构成阻塞:80–90% 缓存率恢复是待回放验证的估算(其依赖的机制——改写次数从约 148 降到 14——正是测试钉住的部分);本提交的单元测试套件尚未跑完。进行中的 结论为 approve;因 CI 未完成,正式批准推迟到被审提交上 CI 全绿后自动执行。 — Qwen Code · qwen3.8-max Reviewed at |
|
Triage re-run completed without a new review.
The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 82 passed · 0 failed · 82 total 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:82 通过 · 0 失败 · 82 总计 Verification reportVerification report — PR #8464 (perf(core): clear tool results to a low watermark to preserve prompt cache) — follow-up roundVerdict: 中文 — 判定:✅ 通过 · 可合入(agent 判定)· 跟进轮
Previous-finding status (follow-up round)
Central claim + A/BCentral claim: clearing oldest compactable tool results down to Environment: head = prebuilt
The W cell reproduces the PR's claimed numbers exactly (148 and 14). The protection fix is load-bearing in the regime the threshold-stop previously masked: with pending alone over the threshold, base blanked all committed results including the five most recent; head keeps exactly five (P1). The two new cells cover the shape production actually sends — one user Content carrying the whole ToolResult batch — and behave identically to the array fixtures on both arms, including the batched-sibling case C2 where base wipes both committed results while head clears nothing. Reviewer Test Plan walk (per step): ① size debug log fires once then stays silent — W cell (14 metas, 153 silent same-object turns) ✔; the CorrectionsNone this round — no inaccurate descriptions of the code found in prior review material. FindingsS-1 (carried, Suggestion, non-blocking): the S-2 (carried, cosmetic): stale test counts in the description. 439 core / 34 cli cited vs 460 / 40 measured at this head (all passing). No action needed beyond awareness. N-1 (note, environmental, not attributable to the PR): a from-scratch Not covered
MethodologyNode v22.23.2 in the CI verify container; merge-ref checkout (HEAD = Evidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
doudouOUC
left a comment
There was a problem hiding this comment.
Blocking review findings at a66751b8c2535a9c8984495f28ebdedc488627a5: the high/low-watermark direction is sound for the reported uniform workload, but the current implementation has two correctness regressions described inline. I also found a counterexample to the PR body's universal "never worse / trigger fires no more often" claim: with H=500K, L=250K, K=5, committed outputs of 1 + 5×100K chars and a 1-char pending result, the parent clears two results in the first checkpoint and is stable on the next one, while this head clears the 1-char result first, remains at 500001 > H, and rewrites another old result at the next checkpoint. Please narrow that guarantee (or add state/logic for an unreachable watermark) and add a cross-checkpoint regression test. The 167×25.5K 148 → 14 result remains valid for that workload, but it does not establish monotonic behavior for all workloads or the projected provider cache-hit recovery.
…ng refs Review follow-up for the low-watermark change: keepRecent now selects from committed results that are actually clearable (positive, successful, uncleared output), so trailing errors, prior placeholders, and empty outputs no longer absorb protection slots. Pending refs are dropped from the keep set entirely — a pending read may be a cache-hit placeholder rather than file bytes, so it must not suppress eviction reporting; over-disarming only costs a redundant re-read. Adds regression tests for both plus the protected-saturation consecutive-trigger corner.
|
Re the review-body counterexample (1 + 5×100K committed, 1-char pending, K=5): confirmed and adopted in 86db1b7. The universal "never worse / trigger fires no more often" claim was wrong — it holds only when the watermark is reachable (protected results fit under it). The PR body now states the narrowed guarantee, and a cross-checkpoint regression test pins the protected-saturation corner: the trigger fires on two consecutive checkpoints (clearing the 1-char result, then the 100K result that rotated out of the protection window) and stabilizes once the total drops back under the threshold — matching the pre-watermark rolling regime rather than improving on it. No extra state machinery was added for the unreachable-watermark case, per the option you offered. The 167×25.5K result (148 → 14) is unchanged and now sits alongside these bounds in the body. All three findings from this review round are addressed: keep-slot selection from clearable committed results, conservative disarm for pending same-path reads, and the narrowed monotonicity claim. |
Pin the (soft-exceeded) log marker with the one-line assertion suggested by the sandboxed verification report (finding S-1): the all-protected overage test now asserts 'target 250000 (soft-exceeded)', killing the surviving mutant M4.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): 442 passed — this review observed 15492, 17835, 473 passed; 34 passed — this review observed 15492, 17835, 473 passed.
中文说明
Test Plan(非阻断):442 passed — this review observed 15492, 17835, 473 passed; 34 passed — this review observed 15492, 17835, 473 passed。
— qwen3.8-max via Qwen Code /review (v0.21.3)
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): 442 passed — this review observed 473 passed; 34 passed — this review observed 473 passed.
中文说明
已审查——无阻断问题。 建议见行内评论。 Test Plan(非阻断):442 passed — this review observed 473 passed; 34 passed — this review observed 473 passed。
— qwen3.8-max via Qwen Code /review (v0.21.3)
yiliang114
left a comment
There was a problem hiding this comment.
The low-watermark direction and the current committed/pending protection fixes look sound. I found two blocking context-integrity regressions in the current head: media-only tool results can fall out of keepRecent, and a kept edit can suppress eviction after the full read is removed. Details inline.
Two P1 context-integrity fixes from review: (1) media-only tool results (image/PDF reads with empty text output and bytes on functionResponse.parts) stay in the idle-path keepRecent candidates instead of being dropped by the zero-char filter; (2) only write_file results vouch for file residency in kept-path accounting — edit calls carry just old/new snippets while still setting the cache's sticky full-read flags, so a kept edit can no longer suppress eviction reporting after the full read is blanked. Regression tests for both.
DragonnZhang
left a comment
There was a problem hiding this comment.
Reviewed exact head e596dc182db2a08641b8d4b1ffa6538f694d4b59 and the current GitHub merge result.
No blocking findings. The high/low-watermark implementation addresses #8452 as intended: the 167 × 25.5K workload drops from 148 history rewrites on the merge base to exactly 14, and non-compaction checkpoints return the history unchanged. I also checked the committed-vs-pending keep budget, zero-char and media-only protection, and conservative file-read-cache disarming. The three production consumers remain consistent: GeminiClient pre-send checkpoints, GeminiChat /compress-fast, and MemoryPressureMonitor history compaction.
Verification on the current merge result: 466 focused core tests passed, 40 CLI settings-schema tests passed, core typecheck passed, and git diff --check passed.
Non-blocking follow-up, intentionally deferred after the existing review rounds: on idle/force cleanup, an error response carrying nested media can still consume a keepRecent slot. This reproduces identically on the merge base, so it is pre-existing rather than introduced by this PR and should not hold this cache fix.
Pin the absence of the (soft-exceeded) marker at the exact watermark boundary: clearing that lands the virtual total exactly on the watermark must not be flagged. Kills the >= and always-true mutants of the marker condition that previously survived the suite.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Test Plan (not a blocker): 442 passed — this review observed 15398, 17831, 473 passed; 34 passed — this review observed 15398, 17831, 473 passed.
中文说明
Test Plan(非阻断):442 passed — this review observed 15398, 17831, 473 passed; 34 passed — this review observed 15398, 17831, 473 passed。
— qwen3.8-max via Qwen Code /review (v0.21.5)
ebf7ae9 to
0c5842f
Compare
|
Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration. 中文请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。 |
Maintainer verification — real-stack A/B (macOS, local build)I built both sides from source and ran the real
1. Core claim: the watermark holds the prefix stable
main rewrites history on every single turn from request 18 onward. The PR rewrites at 18, 27, 36, 45, 54 — one per ~9 results, matching the "half the threshold / average result size" prediction. The PR's break set is a strict subset of main's, and the first break lands at the same request (18) on both, so the claim that the trigger fires no more often and no earlier holds on this workload. The same run through the interactive TUI, read off the app's own
2. The protection-accounting fix is a real data-loss fix, not just bookkeepingSteady-state debug line on the main workload already shows it — main reports Pushed harder — 5 parallel On main the only full outputs the model still sees are the 5 pending ones; every committed result is a placeholder. The PR keeps the 5 most recent committed results as documented. This is the strongest reason to merge, independent of the cache win. 3. Disable semantics unchanged (byte-identical)
In both cases the recorded provider request streams are byte-identical between main and the PR ( 4. Soft-exceeded reportingWith 5. TestsUnit suites + necessity check + gates
Notes / caveats for reviewers
Verdict: verified, supports merge. The cache claim reproduces end-to-end on a real build, the disable paths are byte-identical, and the protection fix prevents a case where main blanks 100% of committed tool output. 中文版本维护者验证 —— 真实环境 A/B(macOS,本地构建)我把两侧都从源码构建,用真实
1. 核心结论:低水位确实稳住了前缀
main 从第 18 个请求起每一轮都改写历史。PR 只在 18、27、36、45、54 改写——约每 9 条结果一次,与"半阈值 / 平均结果大小"的预测吻合。PR 的断裂集合是 main 的严格子集,且两侧首次断裂都落在第 18 个请求,因此"触发不会更频繁、断点不会更靠前"这一说法在该负载下成立。 交互式 TUI 里跑同样负载,直接看 app 自己的 2. 保护配额修复是真实的数据丢失修复,不只是记账问题主负载的稳态日志已经能看出来——main 报 再加压——每轮 5 个并行 在 main 上,模型能看到的完整输出只剩那 5 条待发送结果,所有已提交结果都是占位符。PR 按文档保留了最近 5 条已提交结果。抛开缓存收益不谈,这是最值得合并的理由。 3. 禁用语义未变(字节级一致)
两种场景下,main 与 PR 记录到的 provider 请求流逐字节一致(全部 41 / 58 个请求 4. 软超限上报
5. 测试
注意事项 / 保留意见
结论:验证通过,支持合并。 缓存收益在真实构建上端到端复现,禁用路径字节级一致,保护配额修复堵住了 main 会把 100% 已提交工具输出抹成占位符的场景。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Reviewed. Suggestions are inline. Test Plan (not a blocker): 442 passed — this review observed 17101, 475 passed; 34 passed — this review observed 17101, 475 passed.
中文说明
已审查。 建议见行内评论。 Test Plan(非阻断):442 passed — this review observed 17101, 475 passed; 34 passed — this review observed 17101, 475 passed。
— qwen3.8-max via Qwen Code /review (v0.21.5)
| expect(cleared.parts).toBeUndefined(); | ||
| }); | ||
|
|
||
| it('keeps a media-only tool result in the recent-result budget (idle path)', () => { |
There was a problem hiding this comment.
[Suggestion] The new idle-path media-only handling is only half-tested: this test pins "protected when recent", but "cleared when old" has no test. Failure scenario: a future refactor simplifying the idle zero-char guard to match the size path's unconditional skip — dropping the !(part && hasNestedMedia(part)) term at microcompact.ts:623 — would make media-only image/PDF results (empty text output, bytes on functionResponse.parts) permanently unblankable by idle/force clearing, and the size path never clears zero-char results, so the idle path is the only reclaim mechanism. Nothing would catch it: that mutant survives the entire shipped suite (verified at this commit — 77/77 microcompact + 24/24 client microcompaction tests stay green). Suggested fix: add the companion regression — a media-only read_file result (empty output, parts: [{ inlineData }]) positioned outside the keep window (one newer normal tool result, toolResultsNumToKeep: 1, idle trigger fired), asserting its output becomes MICROCOMPACT_CLEARED_MESSAGE, its functionResponse.parts is stripped, and meta.toolsCleared counts it. That exact test was run against this commit: it passes on the PR code and kills the mutant.
中文说明
[Suggestion] 新增的 idle 路径纯媒体结果处理只测了一半:本测试钉住了"较新时受保护",但"较旧时被清理"没有测试。失败场景:未来若把 idle 的零字符守护简化为与 size 路径一致的无条件跳过——即去掉 microcompact.ts:623 处的 !(part && hasNestedMedia(part)) 项——纯媒体的图片/PDF 结果(文本输出为空、字节在 functionResponse.parts 上)将永远无法被 idle/force 清理抹除,而 size 路径从不清理零字符结果,因此 idle 路径是唯一的回收机制。且现有测试无法发现该回归:该变异体在整个现有测试套件下存活(已在本提交上验证——microcompact 77/77 与 client microcompaction 24/24 全部保持通过)。建议修复:补充配套回归测试——将一个纯媒体 read_file 结果(output 为空、parts: [{ inlineData }])放在保留窗口之外(另有 1 条更新的普通工具结果,toolResultsNumToKeep: 1,触发 idle 清理),断言其输出变为 MICROCOMPACT_CLEARED_MESSAGE、functionResponse.parts 被剥离、meta.toolsCleared 计入该条。该测试已在本提交上实际运行:在 PR 代码上通过,并能杀死上述变异体。
— qwen3.8-max via Qwen Code /review (v0.21.5)
yiliang114
left a comment
There was a problem hiding this comment.
LGTM. Watermark math is correct (trigger > threshold, clear oldest-first to floor(threshold/2)), and the vouching tightening to write_file is a genuine dangling-placeholder safety fix. Verified tests have teeth (18-failure mutation check) and cache preservation is pinned by the amortization test. Non-blocking notes: (1) release note should mention the halved steady-state retention (~250K-500K oscillation) for users who tuned the threshold; (2) the soft-exceeded log can repeat per-turn when protections pin total above threshold — optional dedupe.
|
Released in v0.21.6. |











What this PR does
When the cumulative size of compactable tool results crosses the configured threshold, the client now clears oldest results down to a low watermark at half the threshold, instead of stopping as soon as the total is just below the threshold. The trigger condition, the
-1disable semantics, and the legacy idle-disable compatibility are unchanged; only the clearing depth changes, so between clearings the conversation prefix stays byte-stable and provider prompt caches keep matching.It also fixes protection-accounting defects that deeper clearing would have amplified. The recent-result budget was computed over history plus the pending, about-to-be-sent tool results, so a ToolResult turn carrying enough pending results left zero committed results protected; it also counted error responses, previously cleared placeholders, and empty outputs — entries that can never be cleared — letting them absorb protection slots from real recent outputs, while media-only results (image/PDF reads whose bytes live outside the text output) stay protected. The budget now selects from committed results with actual clearable content. File-residency vouching in eviction reporting is now strictly conservative: pending results and kept read or edit results cannot prove a file's complete bytes remain in history — only a kept write result can, since its call carries the full content — so the file-read fast path is disarmed in every ambiguous case, at worst costing one redundant re-read.
For observability, the size-cleanup metadata and its debug log line now report the watermark target, with a soft-exceeded marker when protected results keep the total above it. Settings descriptions are updated accordingly.
Why it's needed
Production traffic analysis over a two-day window showed long active sessions stuck at a ~5.43% cache rate (only a fixed ~37,840-token head kept matching) while comparable large-context sessions without history rewrites cached at 99.90%; this pattern accounted for about 63% of all cache loss in the window. The cause is the "just below the threshold" stop: once the budget rides the limit, nearly every turn blanks one more old result and invalidates the cache from that position on. With the watermark, the same #5101-shaped workload (167 results of ~25.5K chars) goes from ~148 rewrites to 14 — about one per 11 average-size results. For workloads where the watermark is reachable — protected results fit under it — the trigger fires no more often than before and each batch break lands no earlier in the prefix. When protections alone pin the total above the threshold (for example five protected 100K results against a 500K threshold), the trigger can fire on consecutive checkpoints, matching the pre-existing rolling regime rather than improving on it; a regression test pins this corner.
Reviewer Test Plan
How to verify
Drive a session past the size threshold with repeated large compactable tool outputs and watch the size-cleanup debug log: it should fire once, report clearing down to about half the threshold with a
targetvalue, and then stay silent while subsequent turns reuse the now-stable prefix, instead of logging one cleanup per turn. Confirm the totals-exceeded log still appears withcleared 0and a soft-exceeded marker when everything above the watermark is protected. Confirm a ToolResult turn that batches five or more pending results still leaves the five most recent committed results intact. Confirm-1(and the legacy negative idle threshold without the new setting) still disables the size trigger entirely.Commands run locally:
cd packages/core && npx vitest run src/services/microcompaction/microcompact.test.ts src/core/client.test.ts src/services/fileReadCache.integration.test.ts src/services/memoryPressureMonitor.test.ts(442 passed);cd packages/cli && npx vitest run src/config/settingsSchema.test.ts(34 passed);npm run build;npm run typecheck;npm run generate:settings-schema;git diff --check.Evidence (Before & After)
N/A. Non-UI behavior covered by focused unit tests, including a workload simulation asserting the rewrite count drops to exactly 14 for 167 sequential 25.5K-char results, a steady-state test asserting the history object is returned untouched between clearings, and review-driven regressions: trailing zero-char results cannot absorb protection slots, a pending cache-hit placeholder cannot suppress fast-path disarming, and the protected-saturation corner where consecutive checkpoints re-trigger.
Tested on
Environment (optional)
Node.js v24; local repository checkout; unit tests only, no sandbox-specific runtime required.
Risk & Scope
toolResultsTotalCharsThreshold, and-1still disables the size trigger.Linked Issues
Fixes #8452
Fixes #8463
中文说明
What this PR does
当可压缩工具结果的累计大小越过配置阈值时,客户端现在会按最老优先清理到阈值一半的低水位,而不是清到刚好低于阈值就停止。触发条件、
-1禁用语义、legacy idle 禁用兼容都不变;只有清理深度变化,因此两次清理之间会话前缀保持字节级稳定,provider 的 prompt cache 能持续命中。同时修复了会被更深清理放大的保护配额缺陷。最近结果保护配额此前按“历史 + 待发送结果”计算,携带足够多待发送结果的 ToolResult 轮次会让已提交历史完全失去保护;它还把错误响应、既有占位符、空输出这些永远不会被清理的条目计入配额,任由它们挤占真实近期输出的保护名额;而字节在文本输出之外的纯媒体结果(图片/PDF 读取)仍保持受保护。现在配额只从具有实际可清理内容的已提交结果中选取。驱逐上报中的文件驻留背书改为严格保守:待发送结果以及保留的 read/edit 结果都无法证明文件完整字节仍在历史中——只有保留的 write 结果可以(其调用参数携带完整 content)——因此所有模糊情形下文件读取快速路径一律解除,最多多付出一次冗余重读。
为了可观测性,size 清理的元数据与调试日志现在会报告水位目标,并在受保护结果使总量高于水位时标注软超限。配置描述已同步更新。
Why it's needed
两天窗口的生产流量分析显示,长活跃会话的缓存率被钉在约 5.43%(只有固定的约 37,840 token 头部还能命中),而同样大上下文但没有历史改写的会话缓存率为 99.90%;该模式占窗口内全部缓存损失的约 63%。原因就是“清到刚好低于阈值”:预算一旦贴着上限,几乎每一轮都会再抹掉一条旧结果,并使缓存从该位置起失效。改为低水位后,与 #5101 复现器同形态的负载(167 条、每条约 25.5K 字符)从约 148 次改写降到 14 次——约每 11 条平均大小的结果一次。在水位可达的负载下(受保护结果能落在水位之下),触发不会比以前更频繁,每次批量断点在前缀中的位置也不会更靠前;当保护项本身就把总量钉在阈值之上时(例如五条受保护的 100K 结果对 500K 阈值),触发可能在连续 checkpoint 上重复出现,退回既有的滚动机制而非改善它;已有回归测试钉住该角落。
Reviewer Test Plan
How to verify
用重复的大型可压缩工具输出把会话推过阈值,然后观察 size 清理调试日志:应当只触发一次、报告清理到约一半阈值并带
target值,随后保持静默,后续轮次复用已稳定的前缀,而不是每轮都出现一条清理日志。确认当水位以上全部为受保护结果时,超限日志仍会出现且带cleared 0与软超限标注。确认一次携带五条及以上待发送结果的 ToolResult 轮次仍会保留最近五条已提交结果。确认-1(以及未设置新配置时的 legacy 负值 idle 阈值)仍会完全禁用 size 触发。本地运行过的命令:
cd packages/core && npx vitest run src/services/microcompaction/microcompact.test.ts src/core/client.test.ts src/services/fileReadCache.integration.test.ts src/services/memoryPressureMonitor.test.ts(442 通过);cd packages/cli && npx vitest run src/config/settingsSchema.test.ts(34 通过);npm run build;npm run typecheck;npm run generate:settings-schema;git diff --check。Evidence (Before & After)
N/A。非 UI 行为,由聚焦单测覆盖,包括断言 167 条连续 25.5K 字符结果的改写次数恰好降为 14 次的负载仿真、断言两次清理之间 history 对象原样返回的稳态测试,以及评审驱动的回归:尾随零字符结果不得挤占保护名额、待发送 cache-hit 占位符不得压制快速路径解除、保护饱和角落的连续 checkpoint 重复触发。
Tested on
Environment (optional)
Node.js v24;本地仓库 checkout;仅单元测试,无需额外 sandbox 运行时。
Risk & Scope
toolResultsTotalCharsThreshold的一半推导,-1仍可禁用 size 触发。Linked Issues
Fixes #8452
Fixes #8463