Skip to content

fix(core): compact on the window ceiling, not the max of the threshold ladder - #6583

Merged
tanzhenxin merged 3 commits into
fix/max-tokens-window-clampfrom
fix/compaction-threshold-min-semantics
Jul 9, 2026
Merged

fix(core): compact on the window ceiling, not the max of the threshold ladder#6583
tanzhenxin merged 3 commits into
fix/max-tokens-window-clampfrom
fix/compaction-threshold-min-semantics

Conversation

@tanzhenxin

Copy link
Copy Markdown
Collaborator

What this PR does

Auto-compaction decides when to compress a session from a threshold computed off the model's context window. That threshold combines two ingredients: a proportional preference ("compact around 85% of the window") and an absolute reserve near the window's edge ("keep enough room for the compaction summary to run"). Today the ladder takes the larger of the two, so on large windows the absolute reserve wins and pushes the trigger up against the ceiling — a 1M-token session only compacts at about 97% full, and a 128K session compacts so late that the summarization pass itself no longer fits inside the window.

This PR takes the smaller of the two instead. The absolute reserve is structurally a ceiling — "compact by here, or there is no room left to summarize" — so it belongs on the low side of the decision, not the high side. After the change: large windows compact at ~85% instead of crowding the ceiling; mid-size windows compact early enough that the summary always fits; very small windows are unchanged; and context.autoCompactThreshold now behaves intuitively on large windows — lowering it pulls compaction earlier, where before it was silently ignored on any window bigger than ~220K.

Why it's needed

The three-tier ladder and its absolute reserve were introduced in #4345, which replaced a flat "70% of window" trigger and combined the two terms with a max, under a utilization argument: reclaim the fraction of a big window that a flat percentage "wastes."

That framing inverts what the absolute term is. It is not a target to rise toward — it is the point past which the summarization call no longer has room to run (the window minus the summary's output budget minus a small margin). Combined with max, the trigger sits at the thinnest-allowed headroom exactly when the window is largest, which is when a big tool result is most likely to overshoot the window before compaction fires; on a mid-size window it can even land where the summary provably cannot fit. Taking the min treats the absolute reserve as the ceiling it actually is.

The follow-up #5868 (which made the threshold configurable) surfaced the same inversion from the user's side: its notes observe that "custom threshold mainly affects small windows" — i.e. lowering the threshold does nothing on large windows, because max keeps the absolute reserve in charge there. After this change a lower threshold correctly pulls compaction earlier on large windows too. The small-window ordering guarantee from #4949 is preserved.

On the other tuning values: the absolute buffers are correct and stay; the only changes are the max → min operator and retiring a warn-tier offset that becomes redundant once the ladder is simplified. The 85% default is respected and unchanged.

Reviewer Test Plan

How to verify

Configure a model with a specific context window and grow a session past the trigger, watching where auto-compaction fires:

  • Large window (1,000,000): should trigger around 85% full, not wait until ~97%.
  • Mid window (128K): should trigger earlier than before (around 74%), so the summary fits.
  • Explicit threshold: set context.autoCompactThreshold to 0.5 on a large-window model — compaction should now fire far earlier (around 50%); before, it was ignored on large windows.
  • No over-firing: below the trigger, nothing should compact.

The regression this prevents: large-window sessions used to run to ~97% before compacting, leaving almost no headroom for the next turn, and mid-window sessions could trigger a summary that no longer fit the window.

Evidence (Before & After)

Verified end-to-end with a mock backend reporting a controlled prompt-token count, comparing the base-branch (max) binary against this branch (min) at fixed window sizes. Each token count sits between the old and new trigger, so the outcome is unambiguous.

Window Prompt tokens Before (max) After (min)
128K 102,000 no compaction compacts at 102,000 (79.7%)
1,000,000 900,000 no compaction compacts at 900,000 (90%)
1,000,000, threshold = 0.5 560,000 no compaction compacts at 560,000 (56%)
128K (control) 70,000 no compaction no compaction

All three discriminating cases flip from no-compaction to compaction; the explicit-threshold case confirms a low autoCompactThreshold now takes effect on a large window; the control confirms no over-eager firing.

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment

Local unit suite plus an end-to-end run driven through a mock OpenAI-compatible backend at configured window sizes.

Risk & Scope

  • Main risk / tradeoff: mid-size windows compact somewhat earlier (a 128K window at ~74% instead of ~85%). This is deliberate — it guarantees the summarization pass fits. Users who set an explicit threshold are affected only in that it now behaves as documented on large windows.
  • Not validated / out of scope: making the summarization call robust to a single input that alone exceeds the window (pre-existing, unchanged); and a pre-existing lower-bound mismatch between the setting's schema minimum and its runtime guard (tracked separately).
  • Breaking changes / migration notes: none in configuration. The compaction trigger moves earlier for windows above ~220K and for mid-size windows; small windows are unchanged. The setting description is updated to describe the value as a ceiling on the trigger.

Linked Issues

Refines the auto-compaction ladder introduced in #4345. Related: #5868 (configurable threshold) and #4949 (force tier > auto tier). Stacks on #6556 and should merge after it.

中文说明

本 PR 做了什么

自动压缩(auto-compaction)根据模型的上下文窗口计算一个触发阈值来决定何时压缩会话。该阈值由两部分组成:一个按比例的偏好("在窗口约 85% 时压缩")和一个靠近窗口上沿的绝对预留("为压缩总结留出运行空间")。目前阈值梯子取两者中的较大值,因此在大窗口上绝对预留胜出,把触发点顶到接近上限——100 万 token 的会话要到约 97% 才压缩,而 128K 的会话压缩得太晚,以至于总结调用本身已经放不进窗口。

本 PR 改为取两者中的较小值。绝对预留在结构上是一个上限——"到这里必须压缩,否则没有空间做总结"——因此它应位于决策的低侧,而非高侧。改动后:大窗口在约 85% 处压缩,不再贴着上限;中等窗口足够早地压缩,总结总能放下;极小窗口保持不变;并且 context.autoCompactThreshold 在大窗口上行为符合直觉——调低它会更早触发压缩,而此前在大于约 220K 的窗口上它会被静默忽略。

为什么需要

三层阈值梯子和绝对预留由 #4345 引入,它取代了固定的"窗口 70%"触发,并用**取大值(max)**组合两项,理由是利用率:把固定百分比在大窗口上"浪费"的那部分空间收回来。

这个理由把绝对预留的本质弄反了。它不是一个要去够到的目标,而是"再往上总结调用就没有空间运行"的界线(窗口减去总结的输出预算再减去一个小余量)。用 max 组合,会让触发点在窗口最大时恰好停在允许的最薄余量处——而这正是一个大的工具结果最可能在压缩触发前撑破窗口的时候;在中等窗口上,触发点甚至可能落在总结放不下的位置。改用取小值(min),就把绝对预留当作它本该是的上限来对待。

后续的 #5868(让阈值可配置)从用户侧暴露了同样的问题:其说明指出"自定义阈值主要影响小窗口"——也就是在大窗口上调低阈值不起作用,因为 max 让绝对预留在那里说了算。本次改动后,调低阈值在大窗口上也能正确地更早触发压缩。#4949 关于小窗口的次序保证保持不变。

关于其它数值:各绝对缓冲区是正确的,予以保留;唯一的改动是 max→min 运算符,以及退休一个在梯子简化后变得多余的 warn 层偏移常量。85% 默认值被尊重且不变。

证据(前 & 后)

通过一个返回受控 prompt-token 计数的 mock 后端做端到端验证,在固定窗口大小下比较基线分支(max)与本分支(min)。每个 token 计数都落在新旧触发点之间,结果无歧义。

窗口 Prompt tokens 之前(max 之后(min
128K 102,000 不压缩 压缩 于 102,000(79.7%)
1,000,000 900,000 不压缩 压缩 于 900,000(90%)
1,000,000,阈值 = 0.5 560,000 不压缩 压缩 于 560,000(56%)
128K(对照) 70,000 不压缩 不压缩

风险与范围

  • 主要权衡: 中等窗口会略微更早压缩(128K 从约 85% 变为约 74%)。这是有意为之——以保证总结调用能放下。显式设置了阈值的用户,只是在大窗口上其行为现在符合文档描述。
  • 未验证 / 范围之外: 让总结调用对"单个输入即超过窗口"具备鲁棒性(既有问题,未改动);以及该设置的 schema 下限与运行时下限不一致(既有问题,另行跟踪)。
  • 破坏性变更: 配置上没有。触发点在大于约 220K 的窗口和中等窗口上提前;小窗口不变。设置说明已更新为把该值描述为触发点的上限。

…dder

computeThresholds combined the proportional term (pct*window) and the
absolute term (effectiveWindow - AUTOCOMPACT_BUFFER) with Math.max, which
pushed the auto-compaction trigger toward the top of the window on large
windows — a 1M-token window compacted at ~97%, leaving ~33K headroom.

The absolute term is structurally a ceiling ("compact before the prompt
leaves too little room for the summarization side-query, which needs up
to SUMMARY_RESERVE of output"), so it composes with Math.min, matching
the claude-code reference (services/compact/autoCompact.ts, which uses
Math.min and whose default trigger is the absolute term alone).

  auto = absoluteCeiling > 0 ? min(pct*window, absoluteCeiling) : pct*window
  warn = max(0, auto - WARN_BUFFER)   // WARN_PCT_OFFSET retired
  hard = unchanged

Effect: large windows compact at ~85% (the DEFAULT_PCT ceiling) instead
of ~97%; small/mid windows keep room to run compaction (a 128K window's
summary now provably fits); sub-33K windows are unchanged. A lower
context.autoCompactThreshold now pulls compaction earlier on large
windows, matching the reference's Math.min override semantics.

Updates the threshold unit tests, the settings schema description, and
the user docs to describe the setting as a ceiling on the trigger.
Post-review cleanup (no behavior change):
- Collapse the duplicated regime explanation shared between the DEFAULT_PCT
  and computeThresholds doc comments into one canonical block; point the
  constant's doc at computeThresholds.
- Rename rawHard -> hardEdge and note it is the window-edge ceiling, so the
  two roles of the hard tier (window edge vs. auto + HARD_BUFFER) are legible.
- Shorten the context.autoCompactThreshold description in settings.md to the
  concise schema wording (also un-widens the docs table).
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR!

Template looks good ✓

Problem: This is a real, observed issue. On large context windows (1M tokens), max semantics push the auto-compaction trigger to ~97% of the window, leaving almost no headroom for the next turn. On mid-size windows (128K), the trigger can land where the summarization side-query no longer fits. The PR provides a clear before/after evidence table showing three discriminating cases that flip from no-compaction to compaction, plus a control confirming no over-firing.

Direction: Aligned. The absolute reserve (effectiveWindow - AUTOCOMPACT_BUFFER) is structurally a ceiling — "compact by here or the summary doesn't fit" — not a target to rise toward. Using min instead of max correctly treats it as such. The change also fixes the user-facing issue where context.autoCompactThreshold was silently ignored on large windows (>220K). CHANGELOG references #4345 (three-tier ladder), #5868 (configurable threshold), and #4949 (force tier > auto tier) — all directly related. The PR also notes alignment with claude-code's autoCompact.ts which uses Math.min for the same combination.

Size: 86 production lines across 3 files (chatCompressionService.ts: +39/-43, settingsSchema.ts: +1/-1, settings.schema.json: +1/-1). 138 test/doc lines excluded. Well within bounds — not applicable for size advisory.

Approach: Scope feels tight and minimal. The change is a single semantic flip (maxmin) with correctly updated test expectations, retired dead code (WARN_PCT_OFFSET), simplified warn formula, and updated documentation. No unrelated changes or scope creep. The degenerate-window fallback (ceiling ≤ 0 → proportional floor) is clean and preserves the small-window safety net.

One question for the author: this PR stacks on fix/max-tokens-window-clamp (#6556) — if that base PR changes direction or gets delayed, does this still apply cleanly on main? The DEFAULT_PCT value differs between the two bases (0.7 on main vs 0.85 on the stacking base).

Moving on to code review. 🔍

中文说明

感谢贡献!

模板完整 ✓

问题:这是一个真实存在的问题。在大窗口(1M tokens)上,max 语义将自动压缩触发点推到窗口的约 97%,几乎没有留给下一轮的余量。在中等窗口(128K)上,触发点可能落在总结调用放不下的位置。PR 提供了清晰的 before/after 证据表,三个区分性案例从未压缩翻转为压缩,对照组确认没有过度触发。

方向:对齐。绝对预留(effectiveWindow - AUTOCOMPACT_BUFFER)在结构上是一个上限——"到这里必须压缩,否则总结放不下"——而不是要攀升的目标。用 min 替代 max 正确地将其视为上限。此改动还修复了用户侧的问题:context.autoCompactThreshold 在大窗口(>220K)上被静默忽略。CHANGELOG 引用了 #4345(三层梯子)、#5868(可配置阈值)和 #4949(强制层 > 自动层)——都直接相关。PR 还指出与 claude-code 的 autoCompact.ts 一致,后者对相同组合使用 Math.min

规模:3 个生产文件共 86 行(chatCompressionService.ts: +39/-43, settingsSchema.ts: +1/-1, settings.schema.json: +1/-1)。138 行测试/文档行被排除。远在限制内——规模建议不适用。

方案:范围紧凑且最小化。改动是单一语义翻转(maxmin),正确更新了测试期望,移除了死代码(WARN_PCT_OFFSET),简化了 warn 公式,更新了文档。没有无关改动或范围蔓延。退化窗口回退(ceiling ≤ 0 → 比例值作为底线)干净且保留了小窗口安全网。

一个问题:这个 PR 堆叠在 fix/max-tokens-window-clamp#6556)上——如果那个基础 PR 改变方向或被延迟,这个 PR 还能干净地应用到 main 上吗?两个基础的 DEFAULT_PCT 值不同(main 上是 0.7 vs 堆叠基础是 0.85)。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

Independent proposal before reading the diff: the absolute reserve (effectiveWindow - AUTOCOMPACT_BUFFER) is a ceiling — compact before the summarization side-query runs out of room. The fix is to use min(proportional, ceiling) with a fallback to proportional when the ceiling is degenerate (≤ 0). Warn should be simplified to a fixed offset below auto.

The PR matches this proposal exactly. The core change is a single semantic flip (Math.maxMath.min) with a clean degenerate-window fallback. WARN_PCT_OFFSET is retired and warn simplified to Math.max(0, auto - WARN_BUFFER). Hard formula is unchanged (only a variable rename rawHardhardEdge).

No correctness bugs, no security issues, no AGENTS.md violations. The code is minimal, well-commented, and follows project conventions. The degenerate case where pct=1 on small windows causes auto == hard persists from before but is explicitly acknowledged in the test suite ("hard may equal auto for small windows") and uses <= assertions — not a regression.

Notable: the PR also fixes the latent issue where context.autoCompactThreshold was silently ignored on large windows (>220K). After the change, a lower custom threshold correctly pulls compaction earlier at all window sizes.

Unit Tests

All 108 tests pass across 3 test files:

  • chatCompressionService.test.ts: 88/88 ✓
  • contextCommand.test.ts: 11/11 ✓
  • Tips.test.ts: 9/9 ✓

Test expectations are updated with correct values derived from the new min formula. New test "custom pct DOES pull auto earlier on large windows" validates the threshold-setting fix.

Real-Scenario Verification (tmux)

Built the PR branch and ran computeThresholds across standard and edge-case window sizes:

=== PR #6583: computeThresholds verification (min-semantics) ===

Window      | warn      | auto      | hard      | effWindow | auto%
------------|-----------|-----------|-----------|-----------|------
      32000 |      7200 |     27200 |     30200 |     12000 | 85.0%
      60000 |      7000 |     27000 |     37000 |     40000 | 45.0%
     128000 |     75000 |     95000 |    105000 |    108000 | 74.2%
     200000 |    147000 |    167000 |    177000 |    180000 | 83.5%
    1000000 |    830000 |    850000 |    977000 |    980000 | 85.0%

=== Custom threshold (pct=0.5) on 1M window ===
auto = 500000 (50.0%) — previously ignored on large windows

=== pct=1 on 200K window (edge case) ===
auto = 167000 (capped below window by ceiling)
hard = 177000

=== Invariant check: warn <= auto < hard ===
VIOLATION: window=10000, pct=1, warn=0, auto=10000, hard=10000
VIOLATION: window=32000, pct=1, warn=12000, auto=32000, hard=32000

Key observations from the output:

  • 1M window: auto = 850K (85%), not 967K (96.7%) — compaction fires with healthy headroom instead of crowding the ceiling.
  • 128K window: auto = 95K (74.2%), down from 108.8K (85%) — leaves room for the summary to fit.
  • Custom pct=0.5 on 1M: auto = 500K (50%) — the user setting now takes effect on large windows, previously it was silently ignored.
  • pct=1 on 200K: auto = 167K, hard = 177K — the ceiling caps auto below the window even at pct=1. Under the old max semantics, auto and hard both equaled the full window (200K).
  • Invariant violations at pct=1 on small windows: expected degenerate case (auto == hard when ceiling is non-positive and proportional fills the window). Already covered by existing tests with <= assertions.
中文说明

代码审查

阅读 diff 前的独立方案:绝对预留(effectiveWindow - AUTOCOMPACT_BUFFER)是一个上限——在总结调用空间耗尽前必须压缩。修复方法是 min(比例值, 上限),当上限退化(≤ 0)时回退到比例值。warn 应简化为 auto 以下的固定偏移。

PR 完全匹配这个方案。核心改动是单一语义翻转(Math.maxMath.min),带有干净的退化窗口回退。WARN_PCT_OFFSET 被移除,warn 简化为 Math.max(0, auto - WARN_BUFFER)。hard 公式未变(仅变量重命名 rawHardhardEdge)。

无正确性 bug,无安全问题,无 AGENTS.md 违规。 代码最小化、注释良好、遵循项目规范。pct=1 在小窗口上导致 auto == hard 的退化情况与之前一致,测试套件中已明确承认并使用 <= 断言——不是回归。

值得关注:PR 还修复了 context.autoCompactThreshold 在大窗口(>220K)上被静默忽略的潜在问题。改动后,较低的自定义阈值在所有窗口大小上都能正确提前触发压缩。

单元测试

全部 108 个测试通过(3 个测试文件):

  • chatCompressionService.test.ts: 88/88 ✓
  • contextCommand.test.ts: 11/11 ✓
  • Tips.test.ts: 9/9 ✓

测试期望已更新为新的 min 公式推导出的正确值。新增测试"自定义 pct 在大窗口上确实能提前触发 auto"验证了阈值设置的修复。

真实场景验证(tmux)

构建了 PR 分支并在 tmux 中运行 computeThresholds,覆盖标准和边界窗口大小。关键观察:

  • 1M 窗口:auto = 850K(85%),而非 967K(96.7%)——压缩在健康余量时触发。
  • 128K 窗口:auto = 95K(74.2%),从 108.8K(85%)降低——为总结留出空间。
  • 自定义 pct=0.5 在 1M 窗口上:auto = 500K(50%)——用户设置在大窗口上生效,之前被静默忽略。
  • pct=1 在 200K 窗口上:auto = 167K,hard = 177K——上限将 auto 限制在窗口以下。旧 max 语义下 auto 和 hard 都等于完整窗口(200K)。
  • pct=1 在小窗口上的不变式违规:预期的退化情况(上限非正且比例值填满窗口时 auto == hard)。已被现有测试用 <= 断言覆盖。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This is a well-reasoned, minimal fix that correctly identifies and resolves a real semantic inversion in the compaction threshold ladder.

The core insight — that the absolute reserve is a ceiling, not a target — is sound. The max operator was treating it as a floor to rise toward, which on large windows pushed the trigger to ~97% of capacity and left almost no headroom for the next turn. The min fix is the natural correction, and the degenerate-window fallback (ceiling ≤ 0 → proportional floor) preserves the small-window safety net cleanly.

The implementation matches what I would have done independently. Every change in the diff is necessary: the formula flip, the retired WARN_PCT_OFFSET, the simplified warn, the updated test expectations, and the documentation updates. No scope creep, no drive-by refactors.

The tmux verification confirms the expected behavior across all standard window sizes, and the edge case at pct=1 on small windows (auto == hard) is a pre-existing degenerate condition already covered by the test suite.

One thing worth noting for future maintainers: this PR stacks on fix/max-tokens-window-clamp (#6556), whose base has DEFAULT_PCT = 0.85 while main currently has 0.7. The formula change is independent of the default value, but the merge order matters for clean rebasing.

Approving. ✅

中文说明

这是一个理由充分、最小化的修复,正确识别并解决了压缩阈值梯子中的语义反转问题。

核心洞察——绝对预留是一个上限而非目标——是合理的。max 运算符将其视为要攀升的下限,在大窗口上将触发点推到约 97% 的容量,几乎没有留给下一轮的余量。min 修复是自然的纠正,退化窗口回退(上限 ≤ 0 → 比例值底线)干净地保留了小窗口安全网。

实现与我独立提出的方案一致。diff 中的每一处改动都是必要的:公式翻转、移除 WARN_PCT_OFFSET、简化的 warn、更新的测试期望和文档更新。没有范围蔓延,没有顺手重构。

tmux 验证确认了所有标准窗口大小下的预期行为,小窗口上 pct=1 的边界情况(auto == hard)是测试套件已覆盖的预存退化条件。

值得未来维护者注意:此 PR 堆叠在 fix/max-tokens-window-clamp#6556)上,其基础的 DEFAULT_PCT = 0.85,而 main 当前是 0.7。公式变更独立于默认值,但合并顺序对干净的 rebase 很重要。

批准。✅

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 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.

No blockers found. The min-semantics change is mathematically sound and well-tested. A suggestion-level recommendation is in the Suggestion summary comment below.

— qwen3.7-max via Qwen Code /review

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Suggestions — commit a0753111

File Issue Suggested fix
packages/core/src/services/chatCompressionService.ts:186 Discontinuity at absoluteCeiling > 0 boundary: window=33000auto=28050, but window=33001auto=1. No built-in model falls in 33K–46K, but user-configurable contextWindowSize could hit this range, causing near-immediate compaction. Replace absoluteCeiling > 0 ? Math.min(proportional, absoluteCeiling) : proportional with Math.min(proportional, Math.max(0, absoluteCeiling)) to make the function continuous.

— qwen3.7-max via Qwen Code /review

@doudouOUC doudouOUC 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.

Reviewed — no blockers. Suggestion-level recommendations are in the Suggestion summary comment below.

@doudouOUC

Copy link
Copy Markdown
Collaborator

Suggestions — commit a075311

File Issue Suggested fix
packages/vscode-ide-companion/schemas/settings.schema.json:802 Under min-semantics, autoCompactThreshold: 0.01 on a 1M window triggers compaction at 10K tokens (1% of window). The old max-semantics absorbed this harmlessly (auto = max(10K, 967K) = 967K). The setting is writable from project-level .qwen/settings.json without a trust gate specific to this key. Consider raising the schema minimum (e.g., 0.1) to prevent configurations that produce constant compaction. Raise "minimum" in the JSON schema, or add a runtime floor in getAutoCompactThreshold().
packages/core/src/services/chatCompressionService.ts:165 When compaction fires, the debug log shows effectiveTokens, auto, and contextLimit but not which formula branch governed (proportional vs. ceiling). Under the new conditional logic, this makes 3 AM debugging harder — the oncall engineer must re-derive ceiling = ew - 13K mentally. Add `proportional=X, ceiling=Y, branch=ceiling
packages/core/src/services/chatCompressionService.ts:185 The warn-tier formula changed from proportional positioning ((pct - WARN_PCT_OFFSET) * window, ~75% of window) to purely absolute (auto - 20K). On medium windows (60K), warn collapses to 7K (12% of window). On windows where auto < 20K, warn is 0 — the three-tier ladder effectively becomes two-tier. The WARN_PCT_OFFSET constant was removed cleanly but this behavioral sub-change is undocumented. Document in the JSDoc that warn is now a fixed absolute distance below auto (matching claude-code), not a proportional fraction. Consider a proportional fallback for small/medium windows.

— qwen3.7-max via Qwen Code /review

@tanzhenxin
tanzhenxin merged commit 2786afb into fix/max-tokens-window-clamp Jul 9, 2026
45 checks passed
pull Bot pushed a commit to Little-Star888/qwen-code that referenced this pull request Jul 10, 2026
…reservation (QwenLM#6556)

* fix(core): clamp max_tokens to the context window; retire the output reservation

Auto-compaction was firing far too early — a 200K-window session compacted at roughly half the window. The cause was not the compaction engine but that every request manufactured a large max_tokens, which forced a defensive reservation of that output budget out of the window before computing compaction thresholds. The reservation shrank the effective window, pulled the trigger down, and spawned a chain of band-aids.

Size max_tokens to the room actually left in the window instead — the smaller of the model's output ceiling and (window − prompt − margin) — so an oversized request can never exceed the context limit. Once output is guaranteed to fit, the reservation is unnecessary and is removed; compaction gates on the full window again. Raise the default proportional threshold from 0.70 to 0.85, and replace the temporary half-window reservation cap with a flat 64K output ceiling.

This resolves early compaction, the 400 "maximum context length" error on request, the "hard limit: 0" pre-send NOOP for env-configured models, and retires the half-window reservation cap, while keeping max_tokens on the wire for both OpenAI- and Anthropic-shaped providers.

Fixes QwenLM#5950
Fixes QwenLM#6384

Claude-Session: https://claude.ai/code/session_014DW2TynKHLjsbRqTBSyQue

* test(cli): update /context threshold expectations for 85% default

The auto-compaction default moved from 70% to 85% and the output
reservation was removed, so computeThresholds(200K) now yields
warn=150K / auto=170K (was 147K / 167K). Update the /context
command tests that hard-coded the old ladder.

* fix(core): apply window clamp to samplingParams users who omit max_tokens

Previously a samplingParams config without a max_tokens key sent no
max_tokens on the wire (OpenAI path), so those users bypassed the
prompt + max_tokens <= window clamp — inconsistent with the Anthropic
path, which always injects the clamped value. Mirror the Anthropic
fallback (reconcile ?? config ?? request) so the clamped maxOutputTokens
is injected when samplingParams omits max_tokens.

Guard the injection: when samplingParams targets a provider-specific
output-budget key (max_completion_tokens for GPT-5/o-series, max_new_tokens),
leave it verbatim — adding max_tokens alongside double-specifies the
budget and those endpoints reject the pair.

* fix(core): clamp provider output-budget keys to the window in samplingParams

A samplingParams config carrying a provider-specific output-budget key
(max_completion_tokens for GPT-5/o-series, max_new_tokens) but no
max_tokens previously passed the key through verbatim, so its value
escaped the prompt + output <= window clamp — e.g. max_completion_tokens:
200000 on a 200K window with a 150K prompt.

Clamp the key's value in place to the remaining window (min with the
request maxOutputTokens) instead of injecting a separate max_tokens:
sending both keys double-specifies the output budget and o-series
rejects the pair. The value only shrinks when the window is tight; when
there is room it passes through unchanged, matching how max_tokens is
already treated.

* fix(core): compact on the window ceiling, not the max of the threshold ladder (QwenLM#6583)

* fix(core): compact on the window ceiling (min), not the max of the ladder

computeThresholds combined the proportional term (pct*window) and the
absolute term (effectiveWindow - AUTOCOMPACT_BUFFER) with Math.max, which
pushed the auto-compaction trigger toward the top of the window on large
windows — a 1M-token window compacted at ~97%, leaving ~33K headroom.

The absolute term is structurally a ceiling ("compact before the prompt
leaves too little room for the summarization side-query, which needs up
to SUMMARY_RESERVE of output"), so it composes with Math.min, matching
the claude-code reference (services/compact/autoCompact.ts, which uses
Math.min and whose default trigger is the absolute term alone).

  auto = absoluteCeiling > 0 ? min(pct*window, absoluteCeiling) : pct*window
  warn = max(0, auto - WARN_BUFFER)   // WARN_PCT_OFFSET retired
  hard = unchanged

Effect: large windows compact at ~85% (the DEFAULT_PCT ceiling) instead
of ~97%; small/mid windows keep room to run compaction (a 128K window's
summary now provably fits); sub-33K windows are unchanged. A lower
context.autoCompactThreshold now pulls compaction earlier on large
windows, matching the reference's Math.min override semantics.

Updates the threshold unit tests, the settings schema description, and
the user docs to describe the setting as a ceiling on the trigger.

* refactor(core): trim threshold doc comments; name the hard-edge term

Post-review cleanup (no behavior change):
- Collapse the duplicated regime explanation shared between the DEFAULT_PCT
  and computeThresholds doc comments into one canonical block; point the
  constant's doc at computeThresholds.
- Rename rawHard -> hardEdge and note it is the window-edge ceiling, so the
  two roles of the hard tier (window edge vs. auto + HARD_BUFFER) are legible.
- Shorten the context.autoCompactThreshold description in settings.md to the
  concise schema wording (also un-widens the docs table).

* fix(core): clamp provider output-budget keys on every samplingParams exit

A config carrying both max_tokens and a provider-specific output-budget
key (max_completion_tokens / max_new_tokens) took the max_tokens early
return, spreading the provider key onto the wire unclamped — on backends
honoring the larger key, prompt + output could exceed the window.

Collapse the two returns into a single exit that always runs the
provider-key clamp, so no output-budget key escapes the window clamp
regardless of which combination of keys is present.

---------

Co-authored-by: 易良 <1204183885@qq.com>
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.

3 participants