Skip to content

fix(core): Bound active tool result history#5111

Merged
wenshao merged 4 commits into
QwenLM:mainfrom
doudouOUC:codex/fix-5101-tool-result-history-budget
Jun 15, 2026
Merged

fix(core): Bound active tool result history#5111
wenshao merged 4 commits into
QwenLM:mainfrom
doudouOUC:codex/fix-5101-tool-result-history-budget

Conversation

@doudouOUC

Copy link
Copy Markdown
Collaborator

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.toolResultsTotalCharsThreshold with a default of 500000, supports -1 to 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

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

Node.js v22.22.3; local repository checkout; no sandbox-specific runtime required for the focused tests.

Risk & Scope

  • Main risk or tradeoff: Large historical tool outputs may be replaced with the existing cleared-result placeholder once the cumulative size threshold is exceeded, so very old tool output text is no longer available to the model unless it is among the protected recent results.
  • Not validated / out of scope: This does not introduce transcript-level replacement state and does not change full chat compression behavior beyond using the existing microcompaction path earlier for active tool-result history.
  • Breaking changes / migration notes: No breaking changes. Existing users who set toolResultsThresholdMinutes: -1 without the new setting keep tool-result cleanup disabled for both idle and size triggers; users can explicitly set toolResultsTotalCharsThreshold: -1 to 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.tscd packages/cli && npx vitest run src/config/settingsSchema.test.tsnpm run buildnpm run typecheckgit diff --check

Evidence (Before & After)

N/A。这是非 UI 行为,已通过 focused unit tests、schema 生成、build 和 typecheck 覆盖。

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ not tested
🐧 Linux ⚠️ not tested

Environment (optional)

Node.js v22.22.3;本地仓库 checkout;focused tests 不依赖额外 sandbox runtime。

Risk & Scope

  • Main risk or tradeoff: 当累计 size 阈值被超过后,较早的历史工具输出会被替换为现有的 cleared-result placeholder,因此除被保护的最近结果外,非常旧的工具输出文本不再可供模型使用。
  • Not validated / out of scope: 本 PR 不引入 transcript-level replacement state,也不改变 full chat compression 行为,只是更早复用现有 microcompaction 路径处理 active tool-result history。
  • Breaking changes / migration notes: 无破坏性变更。已有用户如果只设置了 toolResultsThresholdMinutes: -1 且未设置新字段,会继续同时禁用 idle 和 size 两类工具结果清理;用户也可以显式设置 toolResultsTotalCharsThreshold: -1 只禁用 size trigger。

Linked Issues

Fixes #5101

@doudouOUC
doudouOUC marked this pull request as ready for review June 14, 2026 16:16
Copilot AI review requested due to automatic review settings June 14, 2026 16:16

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

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]>
@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

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 (refKey, buildClearMap, buildKeepRefs, planSizeBasedClearing) is appropriate when adding a second clearing path through the same function. The "pending content as virtual tail" design handles the ToolResult turn edge case correctly. I don't see a materially simpler path that solves the cumulative growth problem.

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 抽取(refKeybuildClearMapbuildKeepRefsplanSizeBasedClearing)在同一函数增加第二条清理路径时是合适的重构。"pending content 作为虚拟尾部"的设计正确处理了 ToolResult 轮次的边界情况。没看到更简单的方案能解决累计增长问题。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

✅ Local verification report — build, focused tests, real-execution harness, and a live --debug session

I verified this PR locally three ways: (1) reproduced the author's build/test/typecheck commands, (2) drove the built size-budget algorithm directly with controlled synthetic history to check every edge case, and (3) ran a real interactive session under tmux with --debug and confirmed the new size-triggered microcompaction actually fires. Posting as a merge reference.

Setup

  • Branch codex/fix-5101-tool-result-history-budget @ 3f44646 (fresh git worktree + npm install + npm run build), CLI v0.18.0
  • Platform 🐧 Linux / Node v22.22.2 — the PR table marks Linux ⚠ untested, so this covers it.

1) Author's commands reproduced — all green

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 ToolResult pre-send path, with the pending result counted as a virtual tail (9603 > 4000).
  • 0 microcompaction 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)

  1. The threshold is a soft bound, by design. When the most-recent toolResultsNumToKeep results alone exceed the threshold, clearing stops with history still above it (the live log shows now 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.
  2. Size trigger accounts for and clears text tool outputs only (functionResponse.response.output strings); 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.
  3. Default 500000 is conservative and the legacy toolResultsThresholdMinutes: -1 compatibility (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),CLI v0.18.0
  • 平台 🐧 Linux / Node v22.22.2 —— PR 表格里 Linux 标注为 ⚠ 未测,这里补上。

1) 复现作者命令 —— 全绿

  • core:microcompact.test.ts / client.test.ts / config.test.ts460 passed(microcompact 43
  • cli:settingsSchema.test.ts 19 passed
  • npm run build ✅、npm run typecheck ✅ 无错误、git diff --check ✅ 干净

2) 针对构建产物的真实执行 harness —— 17/17

dist 导入编译后的 microcompactHistory,喂入受控 history,PR 声称的行为全部成立:

  • -1 禁用、legacy minutes:-1(未设新字段)也禁用;
  • 低于阈值不清理;超阈值从最旧开始清、总量被压到阈值内、保留最近 N 个、history 长度不变;
  • error / 已清理 / 非可压缩 结果都被正确跳过(不计入、不清理);
  • pending 内容作为 virtual tail 计入总量(before=3500),但自身不会被清,history 长度不变。
==== 17 passed, 0 failed ====

这与 PR 自带的新测试一一对应。

3) tmux + --debug 实时会话 —— size trigger 真实触发

测试工程设 context.clearContextOnIdle.toolResultsTotalCharsThreshold: 4000toolResultsNumToKeep: 2qwen --yolo --debug 启动,让模型多次用 run_shell_commandseq 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 干净分离。

观察(均非阻塞项)

  1. 阈值是软上限(设计如此):当最近 toolResultsNumToKeep 个结果本身就超过阈值时,清理会在仍高于阈值处停止(实时日志 now 6402 > 4000, kept 1)。这是正确的——被保护的最近结果永不驱逐——但建议在文档加一句说明,以免被误读为“清理不彻底”。
  2. size trigger 只统计并清理文本类工具输出functionResponse.response.output 字符串);media / inline-data 在 size 路径上被有意保留(mediaKept = 0),只由 idle 路径约束。与 PR 的 "media preservation for size-triggered cleanup" 一致,这里仅作范围提示。
  3. 默认 500000 较保守,且 legacy toolResultsThresholdMinutes: -1(在未显式设置新字段时同样禁用 size trigger)兼容性已验证,存量用户行为不变。

结论

功能扎实、测试充分,正是 #5101 所需:在 provider 请求发送前,对 active tool-result history 施加客户端累计预算,复用既有 microcompaction 路径。build/typecheck/tests 全绿,算法在所有边界下直接执行均正确,实时会话中也正确触发。LGTM — 建议合并。 唯一建议的后续项:文档补一句说明该阈值是软上限(最近结果始终保留)。

Comment thread packages/core/src/services/microcompaction/microcompact.ts Outdated
return null;
}

const keepToolRefs = buildKeepRefs(tool, keepRecent);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Comment thread packages/core/src/services/microcompaction/microcompact.ts Outdated
Comment thread packages/core/src/core/client.ts Outdated
Comment thread packages/core/src/services/microcompaction/microcompact.test.ts
Comment thread packages/core/src/services/microcompaction/microcompact.ts Outdated
return null;
}

const keepToolRefs = buildKeepRefs(tool, keepRecent);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Comment thread packages/core/src/services/microcompaction/microcompact.ts Outdated
Comment thread packages/core/src/core/client.ts Outdated
Comment thread packages/core/src/services/microcompaction/microcompact.test.ts
wenshao
wenshao previously approved these changes Jun 14, 2026
@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

Code Review

No 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 microcompactHistory, with a new config field toolResultsTotalCharsThreshold, a planSizeBasedClearing helper that collects compactable tool refs, counts chars, and clears oldest-first while protecting recent results. The PR matches this approach exactly, plus the "pending content as virtual tail" design for ToolResult turns.

Correctness: the size-based clearing path correctly filters pending content out of clearRefs (ref.contentIndex >= history.length), skips errors and already-cleared results via getToolOutputChars, and preserves media (size trigger only clears compactable tool outputs). The toolsKept calculation change from Math.min(tool.length, keepRecent) to tool.filter((r) => keepRefs.has(refKey(r))).length is a correctness improvement for the size-trigger case — Math.min overcounts when some tool refs are already-cleared.

Config handling: the legacy compatibility path (toolResultsThresholdMinutes < 0 → disable size trigger when new field unset) is well-thought-out. The getToolResultsTotalCharsThreshold fallback chain is clear. !Number.isFinite(threshold) catches NaN/Infinity.

Trigger separation: the force / idle / size branching in microcompactHistory is clean — force runs full clearing (tool + media + nested-media), idle runs the same full clearing on time-based trigger, size runs tool-only clearing. The sizeOnly flag correctly skips the time-based trigger check.

Client integration: the ToolResult turn in client.ts correctly passes { sizeOnly: true, pendingContent: createUserContent(requestToSend) } to microcompactHistoryBeforeSend, and the debug log format [TOOL-RESULT MC] vs [TIME-BASED MC] makes the two paths easy to distinguish in logs.

Unit Tests

All pass:

  • microcompact.test.ts: 44/44 ✓
  • config.test.ts: 221/221 ✓
  • client.test.ts: 198/198 ✓
  • settingsSchema.test.ts: 19/19 ✓
  • npm run build: ✓ (0 errors)
  • npm run typecheck: ✓ (all packages)

Real-Scenario Testing

Non-UI behavior (internal microcompaction) — no visible TUI difference. Smoke-tested both builds with a tool-using prompt.

Before (installed build)

runner@runnervm1li68:~/work/qwen-code/qwen-code$ qwen -p 'Read package.json and tell me the project name' --max-session-turns 3 -y 2>&1 | tee tmp/triage-test-183537/before.log
Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Enable a sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice.
The project name is `@qwen-code/qwen-code`.
runner@runnervm1li68:~/work/qwen-code/qwen-code$

After (this PR)

runner@runnervm1li68:~/work/qwen-code/qwen-code$ node /home/runner/work/qwen-code/qwen-code/.qwen/worktrees/triage/dist/cli.js -p 'Read package.json and tell me the project name' --max-session-turns 3 -y 2>&1 | tee tmp/triage-test-183537/after.log
Warning: running headless with --yolo / approval-mode=yolo and no sandbox. All tool calls (shell, write, edit) auto-execute at this process's privilege level. Enable a sandbox via --sandbox / QWEN_SANDBOX, or set QWEN_CODE_SUPPRESS_YOLO_WARNING=1 to silence this notice.
The project name is `@qwen-code/qwen-code` (v0.18.0).
runner@runnervm1li68:~/work/qwen-code/qwen-code$

Both builds start, execute tool calls, and produce correct output. No crashes or regressions observed.

中文说明

代码审查

未发现关键阻断问题。实现清晰,结构良好。

独立方案对比: 在读 diff 之前,我的方案是在 microcompactHistory 中增加累计字符数触发器,配合新配置字段 toolResultsTotalCharsThreshold、一个 planSizeBasedClearing helper 来收集 compactable tool refs、计数 chars、从最旧开始清理同时保护最近结果。PR 的方案完全匹配,还增加了 ToolResult 轮次中"pending content 作为虚拟尾部"的设计。

正确性: size-based 清理路径正确地从 clearRefs 中过滤 pending content(ref.contentIndex >= history.length),通过 getToolOutputChars 跳过错误和已清理的结果,并保留 media(size trigger 只清理 compactable 工具输出)。toolsKeptMath.min 改为 filter 计数,是 size-trigger 场景下的正确性改进——Math.min 在部分 tool refs 已清理时会多算。

配置处理: 向后兼容路径(toolResultsThresholdMinutes < 0 → 未设新字段时禁用 size trigger)考虑周到。!Number.isFinite 处理 NaN/Infinity。

触发器分离: force / idle / size 分支清晰——force 运行完整清理(tool + media + nested-media),idle 在时间触发时运行相同的完整清理,size 仅运行 tool 清理。sizeOnly 标志正确跳过时间触发检查。

客户端集成: client.ts 中的 ToolResult 轮次正确传递 { sizeOnly: true, pendingContent }microcompactHistoryBeforeSend,debug 日志格式 [TOOL-RESULT MC] vs [TIME-BASED MC] 使两条路径易于区分。

单元测试

全部通过: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

@qwen-code-ci-bot

qwen-code-ci-bot commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

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 force/idle/size trigger separation is clean and easy to reason about.

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 (toolResultsThresholdMinutes < 0 inheriting to disable the size trigger) is a thoughtful touch that avoids surprising existing users.

Verdict: Approve

中文说明

退一步看:这是一个清晰、聚焦的修复,解决的是真实的 P1 问题。sequential tool-result 轮次导致 provider history 无限增长,正是那种在规模上会造成痛苦的渐进式问题——full compression 来得太晚,因为 compression 请求本身已经过大。size-based trigger 是 time-based trigger 的自然补充,500K chars 的默认值保守合理。

实现直截了当——没有过度抽象,没有投机性功能。helper 抽取是在同一函数增加第二条清理路径时应有的重构。"pending content 作为虚拟尾部"的设计正确处理了 ToolResult 轮次的边界情况,没有增加常见路径的复杂度。force/idle/size 触发器分离清晰,易于理解。

测试全面——7 个新增 focused tests 覆盖了重要的边界情况(禁用阈值、最近结果保护、错误、已清理、media 保留、pending content 计数、legacy 兼容)。全部 482 个测试通过。Build 和 typecheck 均通过。

我的独立方案与 PR 的方案几乎完全一致。没看到更简单的路径能解决累计增长问题。向后兼容路径(toolResultsThresholdMinutes < 0 继承禁用 size trigger)考虑周到,避免了对现有用户的意外行为变化。

结论:批准

Qwen Code · qwen3.7-max

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

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]>
@doudouOUC
doudouOUC dismissed stale reviews from qwen-code-ci-bot and wenshao via 0bd0476 June 14, 2026 17:24
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs.

@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao wenshao left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

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-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs.

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully: Qwen review aborted with an API error before posting comments. See workflow logs.

@wenshao

wenshao commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

✅ 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 aad757e50 on 🐧 Linux, Node v22.22.2.

How I tested

  • Built the PR in an isolated worktree (npm install → full workspace tsc build + bundle, exit 0). The fetched head is exactly the 12 PR files (no unrelated commits bundled).
  • Ran the real CLI non-interactively (-p … --approval-mode yolo, which routes tool results through SendMessageType.ToolResult → the new size-clear checkpoint in client.ts before each provider request) against a mock OpenAI provider that captures every outbound messages[].
  • The mock drives a 2-round run_shell_command loop; each call emits ~22k chars of compactable output (under the 30k shell cap), tagged MARKER_R1 / MARKER_R2.
  • A/B is a pure settings flip (no code edit — this is the PR's own switch), with QWEN_MC_KEEP_RECENT=1 so only the 1 most-recent result is protected:
    • ON: context.clearContextOnIdle.toolResultsTotalCharsThreshold = 30000
    • OFF: … = -1 (documented disable)

Result — REQ#3 (the provider request after round 2's tool result)

tool result WITH PR (threshold 30000) without PR (threshold -1)
R1 (older) [Old tool result content cleared] (60 chars, MARKER_R1 gone) full (~16.6k chars, MARKER_R1 present)
R2 (recent, protected) full (~16.6k chars, MARKER_R2) full (~22.2k chars, MARKER_R2)

With the budget active, once R1 + pending R2 exceeded 30000 chars the older result was replaced by the placeholder before the request left the client, while the most-recent result was protected — exactly the cumulative client-side bound #5101 asked for. With the size trigger disabled (-1), both results stay full and history grows unbounded across rounds (the bug). The only variable changed between the two runs is toolResultsTotalCharsThreshold.

Result — PR's automated coverage + build

Check Result
core microcompact.test.ts 44 passed
core config.test.ts 221 passed
core client.test.ts 198 passed
cli settingsSchema.test.ts 19 passed
tsc --noEmit (core + cli) clean
full workspace build + bundle exit 0

Notes for review

  • The clear happens on ToolResult turns before the provider dispatch (client.ts microcompactHistoryBeforeSend({sizeOnly:true, pendingContent})), and the pending (not-yet-in-history) tool result is counted as a virtual tail toward the budget but is itself protected — I confirmed R2 (the pending tail) was never cleared.
  • Recent protection is count-based (toolResultsNumToKeep, default 5; I used 1 for a 2-round demo via QWEN_MC_KEEP_RECENT). It's a soft threshold — protected recents can keep the total above it by design.
  • Compactable tools are read_file, run_shell_command, grep, glob, web_fetch, replace, write_file; error / already-cleared / non-string (media) outputs are not counted or cleared on the size path.
  • Disable semantics verified as documented: toolResultsTotalCharsThreshold: -1 disables only the size trigger; legacy toolResultsThresholdMinutes: -1 (with the new key unset) disables both idle and size.
  • Risk noted in the PR holds: very old tool output beyond the protected window is replaced by the placeholder and no longer visible to the model — acceptable trade for bounding provider history.

Verdict

Confirmed 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 -1-disable) setting. Focused core/cli suites + typecheck + build all green. Resolves #5101. LGTM from a verification standpoint. 👍

中文说明

✅ 本地运行时验证 —— active tool-result 历史在线协议上被限界(A/B 对照)

我用真实 CLI 跑了一个 agentic 工具循环,并抓取出站 provider payload,确认本 PR 在累计字符预算被超过后会清理较早的可压缩工具结果——并用 PR 自带的开关做了前后 A/B。基于 PR HEAD aad757e50,在 🐧 Linux、Node v22.22.2 上测试。

测试方法

  • 在隔离 worktree 构建 PR(npm install → 全工作区 tsc 构建 + bundle,exit 0)。拉取的 head 恰好是这 12 个 PR 文件(未混入无关提交)。
  • 非交互运行真实 CLI(-p … --approval-mode yolo,工具结果走 SendMessageType.ToolResultclient.ts 中发送前的新 size-clear 检查点),对接会抓取每次出站 messages[] 的 mock OpenAI provider。
  • mock 驱动 2 轮 run_shell_command 循环,每次输出 ~22k 字符可压缩内容(低于 30k shell 上限),分别打上 MARKER_R1 / MARKER_R2
  • A/B 仅切换设置(不改代码——这是 PR 自带开关),并用 QWEN_MC_KEEP_RECENT=1 让只保护最近 1 条:
    • ON: context.clearContextOnIdle.toolResultsTotalCharsThreshold = 30000
    • OFF: … = -1(文档化的禁用值)

结果 —— REQ#3(第 2 轮工具结果之后的 provider 请求)

工具结果 有本 PR(阈值 30000) 无本 PR(阈值 -1)
R1(较早) [Old tool result content cleared](60 字符,MARKER_R1 消失) 完整(~16.6k 字符,含 MARKER_R1)
R2(最近,被保护) 完整(~16.6k 字符,MARKER_R2) 完整(~22.2k 字符,MARKER_R2)

预算生效时,一旦 R1 + pending R2 超过 30000 字符,较早结果在请求离开客户端之前就被替换为占位符,而最近结果被保护——正是 #5101 要求的客户端累计限界。把 size trigger 禁用(-1)后,两条结果都保持完整,历史随轮次无界增长(即该 bug)。两次运行唯一变量就是 toolResultsTotalCharsThreshold

结果 —— PR 自带覆盖 + 构建

检查 结果
core microcompact.test.ts 44 通过
core config.test.ts 221 通过
core client.test.ts 198 通过
cli settingsSchema.test.ts 19 通过
tsc --noEmit(core + cli) 干净通过
全工作区 build + bundle exit 0

评审说明

  • 清理发生在 ToolResult 轮次、provider 发送之前(client.tsmicrocompactHistoryBeforeSend({sizeOnly:true, pendingContent}));待发送(尚未入历史)的工具结果作为虚拟尾部计入预算但本身受保护——我确认 R2(pending 尾部)从未被清理。
  • 最近保护是按条数(toolResultsNumToKeep,默认 5;本次用 QWEN_MC_KEEP_RECENT 设为 1 做 2 轮演示)。这是软阈值——被保护的最近结果可按设计让总量仍高于阈值。
  • 可压缩工具为 read_file, run_shell_command, grep, glob, web_fetch, replace, write_file;error / 已清理 / 非字符串(media)输出在 size 路径不计入也不清理。
  • 禁用语义符合文档:toolResultsTotalCharsThreshold: -1 只禁用 size trigger;旧的 toolResultsThresholdMinutes: -1(且未设新键)会同时禁用 idle 与 size。
  • PR 提到的风险成立:超过保护窗口的非常旧的工具输出会被替换为占位符,模型不再可见——这是为限界 provider 历史付出的可接受代价。

结论

线协议上端到端确认:累计字符预算被超过后较早的可压缩工具结果会被清理,最近结果保留,且整改动完全受新设置(及 -1 禁用)控制。focused core/cli 套件 + typecheck + 构建全绿。修复 #5101。从验证角度 LGTM。👍

@wenshao
wenshao merged commit 57a90f7 into QwenLM:main Jun 15, 2026
45 of 50 checks passed
@doudouOUC
doudouOUC deleted the codex/fix-5101-tool-result-history-budget branch June 15, 2026 07:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Qwen Code carries repeated large tool results through provider history

4 participants