Repository navigation
fix(core): Bound active tool result history - #5111
Conversation
Co-authored-by: Qwen-Coder <[email protected]>
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 |
✅ 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
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 /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
✅ 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