mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-07-09 17:19:02 +00:00
* feat(core)!: redesign auto-compaction thresholds with three-tier ladder
Replaces the single 70% proportional threshold with a three-tier ladder
(warn/auto/hard) that combines proportional fallback with absolute
reservation. Large-window models (>=128K) now reserve ~33K instead of
30% of the window, freeing tens of thousands of context tokens that the
old formula wasted.
Other improvements bundled in the same redesign:
- Compression sideQuery now disables thinking and caps maxOutputTokens
at 20K, matching claude-code so the buffer math is predictable across
providers (Anthropic/OpenAI/Gemini handle thinking budgets
inconsistently)
- Failure handling upgraded from one-shot permanent lock to a 3-strike
circuit breaker; reactive overflow still latches immediately
- New estimatePromptTokens helper closes the lag-by-one-turn and
first-send-is-0 gaps in lastPromptTokenCount
- Hard-tier rescue pulls reactive overflow recovery forward to before
the API call, saving an oversized round-trip
- /context command displays the three-tier ladder + current tier
- tipRegistry's context-* tips track the new thresholds instead of
fixed 50/80/95 percentages
BREAKING CHANGE: chatCompression.contextPercentageThreshold setting is
removed. Settings files containing the field log a one-line deprecation
warning at startup and the value is ignored; behaviour is now controlled
by built-in thresholds via the new computeThresholds() function.
Design: docs/design/auto-compaction-threshold-redesign.md
Plan: docs/plans/2026-05-14-auto-compaction-threshold-redesign.md
* test(core): fix leftover hasFailedCompressionAttempt option in compress test
A pre-existing test case at chatCompressionService.test.ts:678 still
passed `hasFailedCompressionAttempt: false` in the CompressOptions
shape; rebasing onto current main surfaced this as a typecheck error
because the field was renamed to `consecutiveFailures` (Task 7 of the
three-tier ladder migration). Update to `consecutiveFailures: 0` —
semantically equivalent, the test asserts the side-query is called
when `force: true`, no other behaviour change.
* fix(core): drop compaction summary when output hits maxOutputTokens cap
Adds a defensive guard in ChatCompressionService.compress() that detects
when the side-query summary hit COMPACT_MAX_OUTPUT_TOKENS (20K). In that
case the summary is likely truncated mid-content, so we drop it and
return NOOP rather than persist a half-summary. The next send re-tries;
reactive overflow still catches the catastrophic case where the API
rejects the next request as too large.
Documented in the design doc as risk #2; the bot reviewer on PR #4168
correctly pushed for it to land alongside the threshold redesign rather
than as a follow-up since the new 20K cap is what makes truncation
likely in the first place.
* fix(cli): render three-tier thresholds in /context TUI view
The Task 11 redesign updated the non-interactive text formatter
(formatContextUsageText) but left ContextUsage.tsx — the interactive
React component that real /context users see — unchanged. As a result
the TUI still showed the old single "Autocompact buffer" line and none
of the new warn/auto/hard ladder.
Adds a "Compaction thresholds" section after the per-category breakdown:
- Effective window
- Warn / Auto / Hard threshold rows with a ▶ marker on the row the
current usage has crossed
- Current tier label coloured by severity (safe→green, warn/auto→
yellow, hard→red)
The existing progress bar legend (Used / Free / Autocompact buffer)
is preserved because it's tied to the three-segment progress bar
visualisation; the new section adds the absolute numbers + tier badge
on top of that.
Caught by the tmux e2e test (PR #4168 ci-monitor follow-up). Pre-fix
the assertion 'Compaction thresholds' missed completely from the TUI;
post-fix the new section renders correctly for fresh and live sessions
on 1M / 200K / 128K windows.
* fix(core,cli): address PR #4168 review batch 4
Behavior fixes:
- MAX_TOKENS truncation guard now returns COMPRESSION_FAILED_EMPTY_SUMMARY
instead of NOOP so the consecutive-failure breaker actually trips after
repeated max-length summaries (R1.1).
- Reactive overflow failure increments consecutiveFailures by 1 instead
of latching to MAX in one shot, so a transient network blip doesn't
permanently disable auto-compaction. The hard-tier rescue resets the
counter, which remains the designated recovery path (R1.2).
- /context current-tier classification uses rawOverhead (system + tools +
memory + skills) as the tier input when API data is not yet available,
rather than 0 — large inherited contexts no longer silently show 'safe'
(R2.2).
Performance:
- sendMessageStream computes effectiveTokens ONCE and passes it through
TryCompressOptions.precomputedEffectiveTokens, so the cheap-gate inside
service.compress doesn't redo the estimation. Also fixes the
imageTokenEstimate inconsistency between the rescue and cheap-gate
paths (R1.3 + R1.4).
- Steady-state path (lastPromptTokenCount > 0) skips the costly
getHistory(true) clone — estimatePromptTokens only needs the user
message in that branch.
Code hygiene:
- BYTES_PER_TOKEN → CHARS_PER_TOKEN (inputs are char counts, not byte
counts; CJK text would mislead under the old name) (R3.1).
- Drop dead getContextUsagePercent helper + index re-export — no callers
in source after the threshold rewire (R1.5).
- Add a comment on estimatePromptTokens' first-send fallback documenting
the ~15-20K under-estimate (system prompt + tools + skills) and that
reactive overflow is the safety net (R3.3).
Tests:
- New CLI ContextUsage.test.tsx exercises the React renderer for the
three-tier section: section presence, ▶ marker placement per tier,
current-tier label coloring (R1.6).
- New chatCompressionService.test.ts case pins that a stale
contextPercentageThreshold: 0 value in user settings no longer
short-circuits compaction (R2.1).
- New tokenEstimation.test.ts case covers functionResponse (distinct
nested-parts branch from functionCall) (R3.5).
- New geminiChat.test.ts integration test exercises the real
ChatCompressionService — not a mock — for the first-send-after-
inherited-history scenario where lastPromptTokenCount=0 and only the
full-history estimate can cross the auto threshold (R3.4).
Declined: R3.2 (change `>=` to `>` on the MAX_TOKENS guard). The current
operator catches the at-cap case as suspicious, which is intentional —
landing exactly at the output cap is far more likely truncation than
clean stop given p99.99 ≈ 17K. With R1.1 in place, persistent truncations
trip the breaker after MAX_CONSECUTIVE_FAILURES so the worst case is
bounded.
* fix(core,cli): address PR #4168 review batch 5
- R5.1: tighten /context tier comment + TODO. The rawOverhead-based fix
doesn't cover `--continue` restores with many history messages (since
rawOverhead excludes messagesTokens). UI may still show 'safe' for one
render until the first send. Documented inline and added a TODO to plumb
chat history into collectContextData for same-source-of-truth as the
cheap-gate.
- R5.2a: add TODO(finish_reason) at the truncation guard. The `>= cap`
heuristic false-positives on legitimate at-cap summaries; the proper
signal is finish_reason which runSideQuery doesn't surface today.
- R5.2b: split telemetry — new CompressionStatus.COMPRESSION_FAILED_OUTPUT_TRUNCATED
enum value. Distinct from EMPTY_SUMMARY so logs/telemetry can tell
prompt-quality failures (tune prompt / splitter) from capacity failures
(raise cap / shrink splitter input). isCompressionFailureStatus()
treats both as failures so the breaker behavior is unchanged.
- R5.3: expand consecutiveFailures JSDoc to clarify it tracks
"non-force, non-hard-rescue consecutive failures" — hard-rescue resets
the counter and force=true skips increments, so the counter is the
"regular path" health signal only; reactive overflow is the real
safety net for the force-only paths.
- R5.4: document the CompressOptions field rename
(hasFailedCompressionAttempt: boolean → consecutiveFailures: number)
as an SDK breaking change in the design doc with migration guide.
* fix(core): disambiguate hard-rescue from manual /compress orphan-strip
Self-review (dual reviewer / pr-triage round 1) caught a correctness
regression in the hard-rescue path:
`sendMessageStream` calls `tryCompress(force=true)` from inside the
pre-push window when `effectiveTokens >= hard`. The service's
orphan-strip predicate at `chatCompressionService.ts:426-429` gated on
`force` alone, which conflated two distinct call shapes:
- manual `/compress` (force=true, trigger='manual'): user-initiated
between turns; trailing model funcCall IS orphaned because no
funcResponse is coming
- hard-rescue (force=true, trigger='auto'): automatic mid-turn;
trailing model funcCall is ACTIVE because its matching funcResponse
is sitting in the pending `userContent` waiting to be pushed
The strip fired for both, so a hard-rescue triggered mid tool-use loop
would drop the active funcCall. After compression returned and
`userContent` (the funcResponse) was pushed, the next API request
carried tool_result with no matching tool_use → provider validation
error.
The in-code comment at L422-424 already documented this exact
constraint for the auto-compress case (`force=false`), but reusing
`force=true` for hard-rescue silently violated the same constraint.
Fix:
- Gate `hasOrphanedFuncCall` on `compactTrigger === 'manual'` instead
of `force`. The trigger field already disambiguates intent.
- `sendMessageStream` hard-rescue now passes `trigger: 'auto'`
explicitly (without it, `force=true` defaults to `trigger='manual'`
via the `?? (force ? 'manual' : 'auto')` resolver).
Sibling audit for "force=true non-manual callsites":
- `GeminiClient.tryCompressChat` (manual /compress): correct — manual
- `sendMessageStream` hard-rescue: fixed in this commit
- `sendMessageStream` reactive overflow catch: already passes
trigger='auto'; runs AFTER API call (userContent in history), so if
it observes a trailing funcCall it IS orphaned but findCompressSplitPoint
handles the case without needing the strip
RED-first regression test added:
`preserves trailing model+funcCall under hard-rescue (force=true + trigger=auto)`
in `chatCompressionService.test.ts`. Failed against pre-fix code (the
strip dropped the funcCall); passes against the fix.
Adjacent fixes from the same triage round:
- `docs/users/configuration/settings.md`: the
`chatCompression.contextPercentageThreshold` row still said "use 0
to disable compression entirely" — code has ignored the value since
the removal commit. Marked the row REMOVED with migration guidance
pointing at the design doc.
- `packages/core/src/config/config.ts`: the deprecation warning now
tells users how to silence it (remove the key) and where to read
current behavior, instead of just announcing the removal.
- `docs/design/auto-compaction-threshold-redesign.md`: closed Open
Question 2 (small-window hard/auto collapse) — decision is to NOT
annotate `/context`, with rationale on file.
Tests: 2395 core tests passing, typecheck clean.
* docs(core): fix tier-collapse direction in auto-compaction design doc
Self-review on the
|
||
|---|---|---|
| .. | ||
| 2026-03-22-agent-tool-display-design.md | ||
| 2026-05-14-auto-compaction-threshold-redesign.md | ||
| 2026-05-18-qwen-runtime-memory-investigation.md | ||
| memory-diagnostics-reference-design.md | ||