fix(core): Bound active tool result history#5111
Conversation
Co-authored-by: Qwen-Coder <[email protected]>
|
Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs. |
Move the new tool-result history budget default into a lightweight config defaults module so microcompaction does not load the full Config graph during service tests. Update the ACP worktree test mock to include the public default export used by settings schema imports. Co-authored-by: Qwen-Coder <[email protected]>
|
@qwen-code /triage |
|
Thanks for the PR! Template looks good ✓ On direction: direct fix for #5101 — sequential tool-result turns growing provider history unbounded is a real P1 problem on the context performance roadmap. The issue is well-scoped: per-result truncation alone can't prevent cumulative growth across many rounds, and full compression arrives too late when the compression request itself is already oversized. This is squarely within qwen-code's core mission of context management. On approach: the scope feels right. Adding a cumulative size-based trigger alongside the existing time-based one is the natural complement — time-based alone can't fire during continuous tool use (no idle gap). 789+/149- across 12 files is focused for what it does. The helper extraction ( Moving on to code review. 🔍 中文说明感谢贡献! 模板完整 ✓ 方向:直接修复 #5101 — sequential tool-result 轮次导致 provider history 无限增长是 context performance roadmap 上的真实 P1 问题。问题边界清晰:单结果截断无法阻止多轮累计增长,而 full compression 在 compression 请求本身已经过大时来得太晚。完全在 qwen-code 上下文管理的核心职责范围内。 方案:范围合理。在现有 time-based trigger 之外增加累计 size-based trigger 是自然的补充——连续使用工具时不会出现 idle gap,单靠时间触发无法覆盖。789+/149- 跨 12 个文件,范围聚焦。helper 抽取( 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
✅ Local verification report — build, focused tests, real-execution harness, and a live
|
| Check | Result |
|---|---|
vitest run microcompact.test.ts client.test.ts config.test.ts (core) |
460 passed (microcompact 43, client 197, config 220) |
vitest run settingsSchema.test.ts (cli) |
19 passed |
npm run build |
✅ exit 0 |
npm run typecheck (tsc --noEmit, all workspaces) |
✅ no errors |
git diff --check |
✅ clean |
2) Real-execution harness against the built module — 17/17
I imported the compiled microcompactHistory from dist and fed it controlled histories. Every behavior the PR claims holds:
PASS 1. threshold -1 disables size trigger
PASS 2. legacy minutes:-1 (new field unset) disables size trigger
PASS 3. total (1500) <= threshold (3000) → no clearing
PASS 4a. size trigger fired (triggerReason=size) — {before:8000, after:3000, thr:3000, cleared:5, kept:2}
PASS 4b–4f. before=8000, after≤3000, cleared OLDEST first, recent 2 preserved, history length kept
PASS 5. error results not counted → no clearing despite 30000 raw chars
PASS 6. already-cleared results not re-counted/cleared
PASS 7. non-compactable tool not counted → no clearing
PASS 8. pending tail counts toward virtual total (before=3500) but is NOT itself cleared; history length unchanged
==== 17 passed, 0 failed ====
This independently confirms: oldest-first clearing, total bounded to the threshold, recent-N protected, and correct skipping of error / already-cleared / non-compactable / pending-tail parts — matching the PR's own new tests (size-compacts old tool results…, counts pending content as a virtual tail…, does not clear protected recent results…, does not size-compact errors or already-cleared…, etc.).
3) Live tmux session with --debug — the size trigger fires for real
Test project with context.clearContextOnIdle.toolResultsTotalCharsThreshold: 4000, toolResultsNumToKeep: 2. Launched qwen --yolo --debug, had the model run seq 1 800 (~3,092 chars/result) several times via run_shell_command, accumulating compactable tool history. The new [TOOL-RESULT MC] path fired (debug log):
[CLIENT] [TOOL-RESULT MC] tool result chars 9603 > 4000, cleared 1 tool result(s) (~801 tokens), now 6402, kept 1 tool result(s)
- ✅ Size-triggered microcompaction fires on the
ToolResultpre-send path, with the pending result counted as a virtual tail (9603 > 4000). - ✅
0microcompaction failures in the whole session. - ✅ The idle (
[TIME-BASED MC]) path correctly did not fire (the session was active, not idle) — the two triggers are cleanly separated.
Observations (none are blockers)
- The threshold is a soft bound, by design. When the most-recent
toolResultsNumToKeepresults alone exceed the threshold, clearing stops with history still above it (the live log showsnow 6402 > 4000, kept 1). This is correct — the protected recent tail is never evicted — but worth a one-line doc note so it isn't mistaken for under-clearing. - Size trigger accounts for and clears text tool outputs only (
functionResponse.response.outputstrings); media/inline-data parts are intentionally preserved on the size path (mediaKept = 0) and remain bounded only by the idle path. This matches the PR's "media preservation for size-triggered cleanup" note — flagging it purely as a scope reminder. - Default
500000is conservative and the legacytoolResultsThresholdMinutes: -1compatibility (disables the size trigger too unless explicitly set) is verified, so existing users see no behavior change.
Verdict
Solid, well-tested fix that does exactly what #5101 needs: a client-side cumulative budget that bounds active tool-result history before the provider request, reusing the existing microcompaction path. Build/typecheck/tests are clean, the algorithm is correct across all edge cases under direct execution, and it fires correctly in a live session. LGTM — recommend merge. The only follow-up I'd suggest is a one-line doc note that the threshold is a soft bound (recent results are always preserved).
中文版(点击展开)
✅ 本地验证报告 — 构建、focused tests、真实执行 harness,以及一次 --debug 实时会话
本地用三种方式验证了该 PR:(1) 复现作者的 build/test/typecheck 命令;(2) 直接用已构建的 size-budget 算法跑受控的合成 history,覆盖所有边界;(3) 在 tmux 里以 --debug 跑真实交互会话,确认新的 size-triggered microcompaction 确实触发。作为合并参考。
环境
- 分支
codex/fix-5101-tool-result-history-budget@3f44646(全新 worktree +npm install+npm run build),CLIv0.18.0 - 平台 🐧 Linux / Node
v22.22.2—— PR 表格里 Linux 标注为 ⚠ 未测,这里补上。
1) 复现作者命令 —— 全绿
- core:
microcompact.test.ts/client.test.ts/config.test.ts共 460 passed(microcompact 43) - cli:
settingsSchema.test.ts19 passed npm run build✅、npm run typecheck✅ 无错误、git diff --check✅ 干净
2) 针对构建产物的真实执行 harness —— 17/17
从 dist 导入编译后的 microcompactHistory,喂入受控 history,PR 声称的行为全部成立:
-1禁用、legacyminutes:-1(未设新字段)也禁用;- 低于阈值不清理;超阈值从最旧开始清、总量被压到阈值内、保留最近 N 个、history 长度不变;
- error / 已清理 / 非可压缩 结果都被正确跳过(不计入、不清理);
- pending 内容作为 virtual tail 计入总量(before=3500),但自身不会被清,history 长度不变。
==== 17 passed, 0 failed ====
这与 PR 自带的新测试一一对应。
3) tmux + --debug 实时会话 —— size trigger 真实触发
测试工程设 context.clearContextOnIdle.toolResultsTotalCharsThreshold: 4000、toolResultsNumToKeep: 2,qwen --yolo --debug 启动,让模型多次用 run_shell_command 跑 seq 1 800(每次约 3092 字符)累积可压缩历史。debug 日志显示新路径触发:
[CLIENT] [TOOL-RESULT MC] tool result chars 9603 > 4000, cleared 1 tool result(s) (~801 tokens), now 6402, kept 1 tool result(s)
- ✅ 在
ToolResult发送前路径触发,pending 结果作为 virtual tail 计入(9603 > 4000); - ✅ 整个会话
0次 microcompaction 失败; - ✅ idle(
[TIME-BASED MC])路径正确地未触发(会话处于活跃态),两个 trigger 干净分离。
观察(均非阻塞项)
- 阈值是软上限(设计如此):当最近
toolResultsNumToKeep个结果本身就超过阈值时,清理会在仍高于阈值处停止(实时日志now 6402 > 4000, kept 1)。这是正确的——被保护的最近结果永不驱逐——但建议在文档加一句说明,以免被误读为“清理不彻底”。 - size trigger 只统计并清理文本类工具输出(
functionResponse.response.output字符串);media / inline-data 在 size 路径上被有意保留(mediaKept = 0),只由 idle 路径约束。与 PR 的 "media preservation for size-triggered cleanup" 一致,这里仅作范围提示。 - 默认
500000较保守,且 legacytoolResultsThresholdMinutes: -1(在未显式设置新字段时同样禁用 size trigger)兼容性已验证,存量用户行为不变。
结论
功能扎实、测试充分,正是 #5101 所需:在 provider 请求发送前,对 active tool-result history 施加客户端累计预算,复用既有 microcompaction 路径。build/typecheck/tests 全绿,算法在所有边界下直接执行均正确,实时会话中也正确触发。LGTM — 建议合并。 唯一建议的后续项:文档补一句说明该阈值是软上限(最近结果始终保留)。
| return null; | ||
| } | ||
|
|
||
| const keepToolRefs = buildKeepRefs(tool, keepRecent); |
There was a problem hiding this comment.
[Suggestion] buildKeepRefs takes the last keepRecent refs from the virtual-history tool array, which includes pending content refs. If a ToolResult turn returns ≥ keepRecent compactable results, pending displaces all history entries from the keepRecent budget — so zero existing history items are protected, and all older results above the threshold get cleared.
Consider building keepRefs from only history refs so pending content doesn't consume the protection budget:
const historyToolRefs = tool.filter((r) => r.contentIndex < history.length);
const keepToolRefs = buildKeepRefs(historyToolRefs, keepRecent);— qwen3.7-max via Qwen Code /review
| return null; | ||
| } | ||
|
|
||
| const keepToolRefs = buildKeepRefs(tool, keepRecent); |
There was a problem hiding this comment.
[Suggestion] buildKeepRefs takes the last keepRecent refs from the virtual-history tool array, which includes pending content refs. On a ToolResult turn where the model batched ≥ keepRecent parallel tool calls (common in agentic workflows — e.g., reading 5 files simultaneously), pending refs occupy all keepRecent slots, so zero existing history items are protected and the clearing loop can clear all clearable history down to the threshold.
Consider building keepRefs from only history refs so pending content doesn't consume the protection budget:
const historyToolRefs = tool.filter((r) => r.contentIndex < history.length);
const keepToolRefs = buildKeepRefs(historyToolRefs, keepRecent);— qwen3.7-max via Qwen Code /review
Code ReviewNo critical blockers found. Implementation is clean and well-structured. Independent proposal: before reading the diff, I'd add a cumulative char-count trigger alongside the existing idle trigger in Correctness: the size-based clearing path correctly filters pending content out of Config handling: the legacy compatibility path ( Trigger separation: the Client integration: the Unit TestsAll pass:
Real-Scenario TestingNon-UI behavior (internal microcompaction) — no visible TUI difference. Smoke-tested both builds with a tool-using prompt. Before (installed build)After (this PR)Both builds start, execute tool calls, and produce correct output. No crashes or regressions observed. 中文说明代码审查未发现关键阻断问题。实现清晰,结构良好。 独立方案对比: 在读 diff 之前,我的方案是在 正确性: size-based 清理路径正确地从 配置处理: 向后兼容路径( 触发器分离: 客户端集成: 单元测试全部通过:microcompact 44/44 ✓、config 221/221 ✓、client 198/198 ✓、settingsSchema 19/19 ✓、build ✓、typecheck ✓ 真实场景测试非 UI 行为(内部 microcompaction)——TUI 无可视差异。对两个 build 都进行了工具使用 smoke test,均正确响应。 — Qwen Code · qwen3.7-max |
|
Stepping back: this is a clean, focused fix for a real P1 problem. Sequential tool-result turns growing provider history unbounded is the kind of creeping issue that causes pain at scale — full compression arrives too late because the compression request itself is already oversized. The size-based trigger is the natural complement to the time-based one, and the default of 500K chars is a conservative starting point. The implementation is straightforward — no over-abstraction, no speculative features. The helper extraction is what you'd want when adding a second clearing path through the same function. The "pending content as virtual tail" design handles the ToolResult turn edge case correctly without complicating the common path. The Tests are thorough — 7 new focused tests covering the important edge cases (disabled threshold, protected recent results, errors, already-cleared, media preservation, pending content counting, legacy compatibility). All 482 tests pass. Build and typecheck clean. My independent proposal matched the PR's approach almost exactly. I don't see a materially simpler path that solves the cumulative growth problem. The legacy compatibility path ( Verdict: Approve ✅ 中文说明退一步看:这是一个清晰、聚焦的修复,解决的是真实的 P1 问题。sequential tool-result 轮次导致 provider history 无限增长,正是那种在规模上会造成痛苦的渐进式问题——full compression 来得太晚,因为 compression 请求本身已经过大。size-based trigger 是 time-based trigger 的自然补充,500K chars 的默认值保守合理。 实现直截了当——没有过度抽象,没有投机性功能。helper 抽取是在同一函数增加第二条清理路径时应有的重构。"pending content 作为虚拟尾部"的设计正确处理了 ToolResult 轮次的边界情况,没有增加常见路径的复杂度。 测试全面——7 个新增 focused tests 覆盖了重要的边界情况(禁用阈值、最近结果保护、错误、已清理、media 保留、pending content 计数、legacy 兼容)。全部 482 个测试通过。Build 和 typecheck 均通过。 我的独立方案与 PR 的方案几乎完全一致。没看到更简单的路径能解决累计增长问题。向后兼容路径( 结论:批准 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs. |
Handle negative legacy idle thresholds consistently, clarify size compaction diagnostics for pending tool results, promote successful microcompaction logs to info, and strengthen tests/docs around skipped results and soft thresholds. Co-authored-by: Qwen-Coder <[email protected]>
Co-authored-by: Qwen-Coder <[email protected]>
|
Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs. |
|
@qwen-code /triage |
wenshao
left a comment
There was a problem hiding this comment.
No review findings. Downgraded from Approve to Comment: CI failing: review-pr, Test (macos-latest, Node 22.x). All 9 review dimensions clean (correctness, security, code quality, performance, test coverage, 3× undirected audit). 482 focused tests pass. tsc 0, eslint 0. R1 suggestions (=== -1 → < 0, debug → info, pending chars tracking, test threshold fix) all addressed. — qwen3.7-max via Qwen Code /review
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
|
Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs. |
2 similar comments
|
Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs. |
|
Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs. |
✅ Local runtime verification — active tool-result history is bounded on the wire (A/B)I drove the real CLI through an agentic tool loop and captured the outbound provider payload to confirm this PR clears older compactable tool results once the cumulative-char budget is exceeded — with a before/after A/B that uses the PR's own on/off setting. Tested at PR head How I tested
Result — REQ#3 (the provider request after round 2's tool result)
With the budget active, once Result — PR's automated coverage + build
Notes for review
VerdictConfirmed end-to-end on the wire: older compactable tool results are cleared once the cumulative char budget is exceeded, recent results are preserved, and the change is fully gated by the new (and 中文说明✅ 本地运行时验证 —— active tool-result 历史在线协议上被限界(A/B 对照)我用真实 CLI 跑了一个 agentic 工具循环,并抓取出站 provider payload,确认本 PR 在累计字符预算被超过后会清理较早的可压缩工具结果——并用 PR 自带的开关做了前后 A/B。基于 PR HEAD 测试方法
结果 —— REQ#3(第 2 轮工具结果之后的 provider 请求)
预算生效时,一旦 结果 —— PR 自带覆盖 + 构建
评审说明
结论线协议上端到端确认:累计字符预算被超过后较早的可压缩工具结果会被清理,最近结果保留,且整改动完全受新设置(及 |
What this PR does
This PR adds an active history budget for compactable tool results. When successful compactable tool outputs accumulate beyond the configured character threshold, the client clears older results through the existing microcompaction path while preserving recent results and keeping pending ToolResult content as a virtual tail for the size decision.
It also exposes
context.clearContextOnIdle.toolResultsTotalCharsThresholdwith a default of500000, supports-1to disable the size trigger, keeps legacy disabled idle cleanup compatible, updates settings schema and documentation, and expands focused tests around size-triggered cleanup and ToolResult turns.Why it's needed
Issue #5101 shows that per-result truncation and per-batch budgeting do not prevent sequential tool-result turns from growing provider history across many rounds. Full compression can then run too late because the compression request itself already receives an oversized history. This change gives qwen-code a client-side cumulative budget for active tool-result history before the provider request is sent.
Reviewer Test Plan
How to verify
Reviewers can verify that repeated compactable tool outputs trigger size-based microcompaction even when the idle trigger has not fired, that ToolResult turns still do not run idle cleanup, and that pending ToolResult content can push the virtual total over the threshold before the next provider request. The focused tests cover disabled thresholds, legacy disable compatibility, protected recent results, skipped error/already-cleared/non-compactable outputs, media preservation for size-triggered cleanup, file-read-cache disarming, and schema exposure.
Commands run locally:
cd packages/core && npx vitest run src/services/microcompaction/microcompact.test.ts src/core/client.test.ts src/config/config.test.ts;cd packages/cli && npx vitest run src/config/settingsSchema.test.ts;npm run build;npm run typecheck;git diff --check.Evidence (Before & After)
N/A. This is non-UI behavior covered by focused unit tests, schema generation, build, and typecheck.
Tested on
Environment (optional)
Node.js v22.22.3; local repository checkout; no sandbox-specific runtime required for the focused tests.
Risk & Scope
toolResultsThresholdMinutes: -1without the new setting keep tool-result cleanup disabled for both idle and size triggers; users can explicitly settoolResultsTotalCharsThreshold: -1to disable only the size trigger.Linked Issues
Fixes #5101
中文说明
What this PR does
这个 PR 为可压缩工具结果增加了 active history 总量预算。当成功的可压缩工具输出在历史中累计超过配置的字符阈值时,客户端会复用现有 microcompaction 路径清理更早的工具结果,同时保留最近结果,并在 size 判断中把待发送的 ToolResult 内容作为虚拟尾部参与计算。
同时新增
context.clearContextOnIdle.toolResultsTotalCharsThreshold配置,默认值为500000,支持-1禁用 size trigger,兼容旧的 idle cleanup 禁用配置,并更新 settings schema、用户文档和围绕 size-triggered cleanup 与 ToolResult 轮次的 focused tests。Why it's needed
#5101 表明,单个工具结果截断和单批预算无法阻止多轮 sequential tool-result 在 provider history 中持续膨胀。full compression 可能触发得太晚,因为 compression 请求自身已经会收到过大的 history。这个改动在 provider request 发送前为 qwen-code 增加客户端侧的工具结果历史累计预算。
Reviewer Test Plan
How to verify
Reviewer 可以确认:即使 idle trigger 没触发,重复的大型可压缩工具输出也会触发基于 size 的 microcompaction;ToolResult 轮次仍不会触发 idle cleanup;pending ToolResult 内容可以在下一次 provider 请求前作为虚拟总量的一部分触发清理。focused tests 覆盖了禁用阈值、legacy 禁用兼容、最近结果保护、跳过 error/已清理/非可压缩输出、size trigger 不清 media、file-read-cache disarm、以及 schema 暴露。
本地运行过的命令:
cd packages/core && npx vitest run src/services/microcompaction/microcompact.test.ts src/core/client.test.ts src/config/config.test.ts;cd packages/cli && npx vitest run src/config/settingsSchema.test.ts;npm run build;npm run typecheck;git diff --check。Evidence (Before & After)
N/A。这是非 UI 行为,已通过 focused unit tests、schema 生成、build 和 typecheck 覆盖。
Tested on
Environment (optional)
Node.js v22.22.3;本地仓库 checkout;focused tests 不依赖额外 sandbox runtime。
Risk & Scope
toolResultsThresholdMinutes: -1且未设置新字段,会继续同时禁用 idle 和 size 两类工具结果清理;用户也可以显式设置toolResultsTotalCharsThreshold: -1只禁用 size trigger。Linked Issues
Fixes #5101