fix(cli): mark estimated compression token counts in banners - #9568
Conversation
/compress-fast anchors its banner on the API-reported prompt count (system prompt + tools + history) minus a locally estimated reduction, while a later /compress re-estimates history-only once the stored count is estimate-derived. Consecutive banners therefore show numbers on different scales (e.g. 170k -> 75.8k followed by 26.6k -> 14.5k), which reads as ~50k of context silently lost even though nothing was dropped. Expose per-side token-count provenance on ChatCompressionInfo (originalTokenCountIsEstimated, mirroring newTokenCountIsEstimated) from both tryCompress and compressFast, plumb it through the compression history item, and render estimated figures with a '~' prefix in the CompressionMessage banner, the auto-compaction notice, and the headless/ACP messages. Display-only: no compression behavior, thresholds, or recorded counts change. Fixes #9309
|
Thanks for the PR! Re-run after Template looks good ✓ Problem: observed bug, not theory — #9309 reports the exact Direction: aligned — this is the "clearly mark the numbers as estimated" direction endorsed in the issue thread. The deliberately rejected alternative (forcing both banners onto one scale) would change what the numbers measure, which the thread explicitly did not want. No direct claude-code CHANGELOG counterpart for marking estimated compression counts, but the area is relevant (several recent entries there concern token-estimate transparency). Size: core paths touched ( Approach: scope remains right. The new commit is exactly what the Critical asked for — provenance now follows the count source selected for this attempt (provider-reported override carries its own flag; a precomputed effective count is always estimated; otherwise the stored count's tracked provenance) instead of being inferred from which inputs merely happen to be present. No unrelated changes; the formatter consolidation stays tracked in #9603. Risk: Stage 1e matches Moving on to code review. 🔍 中文说明感谢贡献! 本轮为 模板完整 ✓ 问题:已观测到的 bug,非理论问题——#9309 带截图和复现步骤报告了完全一致的 方向:对齐——这正是 issue 线程认可的"明确标注估算数字"方向。被刻意排除的替代方案(把两个横幅统一到同一口径)会改变数字的计量含义,issue 线程明确不要那个。claude-code CHANGELOG 中没有直接对应条目,但该方向相关(近期多条与 token 估算透明度有关)。 规模:触及核心路径( 方案:范围仍然合理。新提交正是 Critical 所要求的——provenance 现在跟随本次尝试所选用的计数来源(provider 报告的 override 自带标志;预计算的有效计数恒为估算;否则沿用存储计数所跟踪的 provenance),而不再从"恰好存在哪些输入"推断。无无关改动;格式化助手的合并继续记录在 #9603。 风险:Stage 1e 命中 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewThis round verifies @wenshao's Critical (R3-1 on Verified fixed at this head. The test suite probes every branch individually: precomputed effective count with an authoritative stored baseline → estimated (the "review probe" case), fallback override → estimated while still publishing its count, Two things worth naming beyond the Critical, neither blocking:
Re-verified at this head: NOOP surfaces never render counts (the fast command short-circuits to "No compression needed." in all modes, the TUI banner renders "Nothing to compress."), so the fast-NOOP info carrying only one flag is harmless; the Not verified: the live-model banner sequence — this run has no compressible real session, so rendering is pinned through real-component tests with simulated token state instead. Files changed (14)
Test evidence (this PR's own CI — nothing here ran PR code locally)The Linux unit suite is the PR's gate and was still in flight at review time; the table below updates once CI settles. The macOS/Windows unit jobs and the CLI integration job are merge-queue-only by workflow design ( Final CI results for
One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。 Sandboxed verification would settle the one claim CI cannot: 中文说明代码审查本轮针对 已在当前 head 验证修复。 测试逐分支探测:预计算有效计数 + 权威存储基线 → 估算("review probe" 用例)、回退 override → 估算但仍发布其计数、 Critical 之外有两点值得点名,均不阻塞:
在当前 head 复核:NOOP 表面从不渲染数字(fast 命令在所有模式下短路为 "No compression needed.",TUI 横幅渲染 "Nothing to compress."),因此 fast-NOOP info 只带一个标志是无害的; 未验证:真实模型的横幅序列——本次运行没有可压缩的真实会话,渲染通过真实组件 + 模拟 token 状态的测试钉死。 测试证据(本 PR 自己的 CI——本地未运行任何 PR 代码)Linux 单元测试套件是本 PR 的门控,审查时仍在运行;下方表格会在 CI 落定后更新。macOS/Windows 单元测试与 CLI 集成任务按工作流设计仅在合并队列( 沙箱验证可以解决 CI 无法覆盖的唯一主张: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — the maintainer's Critical is resolved exactly as prescribed, with per-branch probes; what remains is disclosure-level: the "display-only" claim is slightly too strong for the auto-compaction path, the formatter consolidation stays deferred (#9603), and the live banner sequence still rests on component tests. Stepping back: the question this round was whether What keeps this at 4 rather than 5: the live Approval is deferred until CI lands green on 中文说明置信度:4/5 —— 维护者的 Critical 已按规定形态完全解决,且逐分支有探测用例;剩余问题均属披露层面:"纯展示"的说法对自动压缩路径略嫌过强,格式化助手合并继续延期(#9603),真实横幅序列仍只有组件测试覆盖。 退一步看:本轮的问题是 为什么是 4 而不是 5:真实的 批准延迟到 CI 在 — Qwen Code · qwen3.8-max Reviewed at |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Test Plan (not a blocker): 332 passed — this review observed 1638, 1616, 494, 3849, 553 passed; 179 passed — this review observed 1638, 1616, 494, 3849, 553 passed; 649 passed — this review observed 1638, 1616, 494, 3849, 553 passed; 35 passed — this review observed 1638, 1616, 494, 3849, 553 passed; 228 passed — this review observed 1638, 1616, 494, 3849, 553 passed.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
Test Plan(非阻断):332 passed — this review observed 1638, 1616, 494, 3849, 553 passed; 179 passed — this review observed 1638, 1616, 494, 3849, 553 passed; 649 passed — this review observed 1638, 1616, 494, 3849, 553 passed; 35 passed — this review observed 1638, 1616, 494, 3849, 553 passed; 228 passed — this review observed 1638, 1616, 494, 3849, 553 passed。
— qwen3.8-max via Qwen Code /review (v0.21.14)
The ACP session auto-compaction diagnostic interpolated raw token counts and ignored the #9309 provenance flags, so estimated figures reached ACP clients unmarked. Route both counts through the same '~'-prefix formatting as the TUI notice and cover it with an asymmetric-flag regression test. Also harden the marker tests per review: asymmetric estimation flags in compressCommand/useGeminiStream fixtures so a swapped flag-argument mutation is detectable, an executionMode:'acp' case for the /compress stream_messages branch, and a null-event ChatCompressed case pinning the 'unknown' fallback.
|
Closeout for the 10:07Z review round (5 findings on 9272f4b) — fixed 4, deferred 1 with follow-up, pushed 6039fcf (one non-force push).
Verification on 6039fcf: Session.test.ts 667/667, compressCommand.test.ts 14/14, useGeminiStream compaction-targeted cases green, cli All 5 threads answered with evidence and resolved (0 unresolved). @qwen-code /triage |
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 2453 passed · 0 failed · 2453 total Flakiness gate: 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:2453 通过 · 0 失败 · 2453 总计 抖动门: Verification reportPR #9568 verification — fix(cli): mark estimated compression token counts in bannersVerdict: 中文摘要
Central claim and A/BCentral claim: locally estimated compression token counts are rendered with a Secondary claims: (1) display-only — compression behavior, thresholds, recorded counts, telemetry unchanged; (2) old/new checkpoints both load and render sanely. A/B table (core provenance, real #9309 sequence)Same test file (
Witnesses: A/B table (CLI rendering, real dist output)
14/14 checks pass. Witness: Findings (non-blocking, informational)
Swept and clean: Mutation matrix (vacuity of the new tests)Core side: the base arm above is the vacuity proof — with the PR hunks absent, the 4 new core cases go red on the intended assertion ( CLI side (
No survivors. Witness: Targeted gates (all at head)
Checkpoint compatibility ( Not covered
MethodologyEnvironment: CI verify container ( Flakiness gate logEvidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
|
Triage re-run completed without a new review.
The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "agent 1b": (none — no check was cut short).
Test Plan (not a blocker): 332 passed — this review observed 20715, 1638, 1616, 494, 3849, 553 passed; 179 passed — this review observed 20715, 1638, 1616, 494, 3849, 553 passed; 649 passed — this review observed 20715, 1638, 1616, 494, 3849, 553 passed; 35 passed — this review observed 20715, 1638, 1616, 494, 3849, 553 passed; 228 passed — this review observed 20715, 1638, 1616, 494, 3849, 553 passed.
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
packages/cli/src/ui/commands/compressCommand.ts:23 — [review] R1-2 still stands: the ~ -prefix formatter is duplicated across five files under three names — round-1 finding, deferred by agreement to #9603packages/core/src/core/geminiChat.ts:2130 — [probe] Provenance stamp treats a config-derived reactive-overflow fallback (contextWindowSize/DEFAULT_TOKEN_LIMIT) as an API-authoritative countpackages/cli/src/ui/commands/compressFastCommand.ts:93 — [probe] The /compress-fast ACP branch has no estimated-flag test; flag-swap and unmark mutations on the yield survive the whole suite
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"agent 1b":(none — no check was cut short)。
Test Plan(非阻断):332 passed — this review observed 20715, 1638, 1616, 494, 3849, 553 passed; 179 passed — this review observed 20715, 1638, 1616, 494, 3849, 553 passed; 649 passed — this review observed 20715, 1638, 1616, 494, 3849, 553 passed; 35 passed — this review observed 20715, 1638, 1616, 494, 3849, 553 passed; 228 passed — this review observed 20715, 1638, 1616, 494, 3849, 553 passed。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.14)
chiga0
left a comment
There was a problem hiding this comment.
LGTM — approving. Standard-tier review at head 6039fcf4.
What I verified
Provenance stamp wiring (turn.ts → geminiChat.ts)
ChatCompressionInfogainsoriginalTokenCountIsEstimated?: booleanandnewTokenCountIsEstimated?: boolean— optional on both sides, backward-compatible.tryCompressstampsoriginalTokenCountIsEstimatedfrom the caller-supplied boolean; the compressed-path setsnewTokenCountIsEstimated: false(API-counted, not estimated).compressFastNOOP branch:originalTokenCountIsEstimated: this.promptCountIsEstimateDerived()— correct, the pre-compression count came from a local estimate.compressFastCOMPRESSED branch:originalTokenCountIsEstimated: baselineIsEstimated— same source, consistent.
Banner rendering (5 formatCount-style helpers)
Five copies across CompressionMessage.tsx, compressCommand.ts, compressFastCommand.ts, useGeminiStream.ts, Session.ts. The two that accept number | undefined (stream/session paths where count may be absent) distinguish from the three that accept number (command paths where count is always present) — the signatures are consistent with their respective call sites, no silent coercions.
Tests
All new test fixtures use asymmetric isEstimated flags (one true, one false); this forces the two positional arguments to be distinguishable and catches argument-swap mutations. Symmetric flags would let a transposed call produce a wrong result that still satisfies the assertion.
CI
Only the route check posted a conclusion (success). The build-and-test / integration-test jobs did not run in this CI pass — no evidence either way from automated suites. The logic path is simple enough (flag read → ternary → string prefix) that I consider the manual review and unit tests sufficient cover, but noting the gap for the record.
Notes on bot round 2's deferred items
The bot's round-2 CHANGES_REQUESTED (since superseded by its own round-3 APPROVE) deferred three items as non-blockers:
- R1-2 (
formatCountduplication) — tracked in #9603, not blocking this PR. - Probe: reactive-overflow fallback —
promptCountIsEstimateDerived()returnstruewhen the count came fromcontextWindowSize / DEFAULT_TOKEN_LIMIT; stamping that path as estimated is the correct semantic. - Probe: compressFast ACP branch — the
yield'dHistoryItemCompressionin the ACP path has no flag-swap test. Agreed non-blocker for this PR; the path is reachable but the consequence of a swap is a cosmetic~appearing on the wrong number, not a correctness failure.
All three assessments align with my own read.
wenshao
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not reviewed: build-and-test — required Node 22 configuration was not exercised because the local harness ran Node 24.18.1.
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未审查:build-and-test — required Node 22 configuration was not exercised because the local harness ran Node 24.18.1。
— gpt-5.6-sol via Qwen Code /review (v0.21.14)
The estimated flag published on compression banners was computed from the stored count's provenance or the mere presence of an override, not from the count source selected for the attempt: - Auto-compaction / hard-tier rescue: a previously API-reported stored count forced the flag false even though the attempt publishes the precomputed effective count, which always folds in locally estimated parts (pending user message, previous output). - Reactive overflow: a limit/config/default fallback passed as the override forced the flag false whenever the provider did not report actualTokens. Both paths omitted the `~` marker and presented projected or fallback values as API-reported counts (review on #9568). Derive the flag from the selected source: a caller-precomputed effective count is always estimated, and the override now carries explicit provenance ({ count, isEstimated }) so only a provider-reported actualTokens override marks the count authoritative.
f460259
|
Addressed the Critical from @wenshao in f460259 ( Fix (
Evidence:
Re-requested review from @wenshao. |
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 2848 passed · 0 failed · 2848 total Flakiness gate: ✅ 6 changed test file(s) x 5 identical rounds, no divergence 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:2848 通过 · 0 失败 · 2848 总计 抖动门:✅ 6 changed test file(s) x 5 identical rounds, no divergence Verification reportPR #9568 verification (follow-up round) — fix(cli): mark estimated compression token counts in bannersVerdict: This is a follow-up round: the previous round verified head Previous-finding status
中文摘要展开
Central claim and A/BCentral claim (refined for this head): token-count provenance is derived from the count source selected for the attempt — a caller-precomputed effective count (auto-compaction / hard-tier rescue) is always estimated; an override is authoritative only when it carries a provider-reported count (reactive Key environment fact: the base tip A/B table (core, same test file
|
| arm | source | oracle | result |
|---|---|---|---|
| head | f460259f (PR head) |
full suite, 336 tests | 336/336 pass |
| base | db0519563d (HEAD^1) |
same file | 326 pass / 10 red — exactly the provenance set: 4 commit-1/2 cases (expected undefined to be <bool>), 3 commit-3 tryCompress cases, 3 reactive e2e flag/count assertions |
| c2 (delta) | 6039fcf4 (commit 2) |
same file | 330 pass / 6 red — 5 are exactly commit 3's new assertions (expected false to be true on the fallback tiers, override-shape mismatches); the 6th (repairOrphanedToolUseTurns > preserves selected call ids) is a confounder: that test does not exist at commit 2 (`git show 6039fcf:…test.ts |
The delta arm proves commit 3 alone is load-bearing: removing only commit 3 flips exactly its own new assertions red, and nothing else in the compression surface.
Witnesses: 01-ab-head-arm-336-green.png, 02-ab-base-arm-10-red.png, 03-ab-c2-arm-commit3-red.png. Logs: logs/ab-head-core.log, logs/ab-base-core.log, logs/ab-c2-core.log.
Correction (description inaccuracy, not a code request)
The PR description says the change is display-only with "recorded counts … untouched". For the auto-compaction path with a previously API-reported stored count, commit 3 intentionally changes the published originalTokenCount from the bare stored baseline to the precomputed effective count (baseline + locally estimated pending message/previous output) — the updated test comment states this ("the published number is the effective count, not the bare seed", assertion relaxed from .toBe(123_456) to .toBeGreaterThanOrEqual(123_456)). That value flows into the banner, the transcript record (recordChatCompression({ info })) and the originalTokenCount of failure/NOOP infos. No behavioral effect verified: the service's threshold gate and cache-sharing math consume opts.precomputedEffectiveTokens directly (path 1, chatCompressionService.ts:449-458,752-753), not originalTokenCount; history mutation and telemetry (setLastPromptTokenCount(info.newTokenCount), makeChatCompressionEvent counts) are unchanged — the reactive tiers keep byte-identical counts across arms (the c2 arm's red cells fail only on flag assertions, after the count assertions pass). One narrow residue: when the summary carries no usage data, newTokenCount initializes from originalTokenCount (chatCompressionService.ts:1022), so in that rare fallback the recorded/resumed new count also moves baseline → effective count. Severity: informational; the description's "recorded counts untouched" should read "recorded counts unchanged except the auto path now publishes the effective count (marked estimated)".
Findings
No blocking findings.
- (Informational) The description correction above.
- (Informational, carried over)
compressFastNOOP still omitsnewTokenCountIsEstimatedwhile setting the original-side flag (geminiChat.ts:2249-2258). Re-measured at new head: NOOP renders no counts on any surface — banner'Nothing to compress.', both command paths'No compression needed.'with no count substring, ACP stream likewise (harness c5/h3/a3). No display impact; leaving it or adding the flag for symmetry are both defensible.
Mutation matrix (10 rows, no survivors)
CLI rows run the pinned suite in the same file as each mutant; core rows run geminiChat.test.ts (the base/c2 A/B arms are the harness-level positive control for the core file — the identical command produced the intended reds there). Every mutant restored byte-exact (git checkout + sha256sum -c against pre-hash, all OK).
| row | mutant | suite | result |
|---|---|---|---|
| C1 | positive control: pending text → 'Squeezing chat history' | CompressionMessage.test.tsx | 1 red / 10 green — CAUGHT |
| M1 | banner formatTokens ~ removed |
CompressionMessage.test.tsx + harness | 5 red (2 PR marker cases + harness c2/c3/c4; authoritative cells stay green) — CAUGHT |
| M2 | compressCommand flag args swapped at all 4 call sites | compressCommand.test.ts | 2 red (asymmetric fixtures catch the swap on both branches) — CAUGHT |
| M3 | compressFastCommand formatTokenCount ~ removed |
compressFastCommand.test.ts | 1 red — CAUGHT |
| M4 | useGeminiStream formatCount ~ removed |
useGeminiStream.test.tsx (full) | 1 red / 231 green — CAUGHT |
| M5 | acp Session formatCount ~ removed |
Session.test.ts (full) | 1 red / 678 green — CAUGHT |
| M6 | commit-3 derivation reverted to commit-2 logic | geminiChat.test.ts | 4 red — exactly the new-derivation tests — CAUGHT |
| M7 | reactive actualTokens === undefined → false |
geminiChat.test.ts | 2 red — exactly the two reactive fallback e2e tests — CAUGHT |
| M8 | `precomputedEffectiveTokens !== undefined | ` clause removed | |
| M9 | override provenance inverted (!override.isEstimated) |
geminiChat.test.ts | 5 red — fallback + authoritative + all 3 reactive e2e — CAUGHT |
Witness: 04-mutation-matrix-10-rows-caught.png. Logs: logs/mx-*.log.
CLI wire-oracle harness (carried over, re-run at new head)
zz-verify-9568-harness.test.tsx (archived in this artifact dir) renders the real built CompressionMessage via ink-testing-library and drives the real compressCommand/compressFastCommand actions (headless + ACP stream_messages branches) with a fake GeminiClient peer. 11 tests / 19 scripted assertions:
| cell | scenario | oracle | result |
|---|---|---|---|
| c1 | authoritative counts | from 100 to 50 tokens, no ~ |
pass |
| c2 | fast shape (new side estimated) | from 170000 to ~75800 tokens |
pass |
| c3 | original-side only (asymmetric) | from ~26600 to 14500 tokens |
pass |
| c4 | both estimated | from ~26600 to ~14500 tokens |
pass |
| c5 | NOOP with counts + flags | Nothing to compress., no counts, no ~ (F2 re-measure) |
pass |
| h1 | /compress headless, asymmetric flags | exactly Context compressed (~26600 -> 14500). |
pass |
| h2 | /compress-fast headless | exactly Context compressed (170000 -> ~75800). |
pass |
| h3 | /compress-fast headless NOOP | No compression needed., no count substring |
pass |
| a1 | /compress ACP stream | exactly info:Context compressed (~26600 -> 14500). |
pass |
| a2 | /compress-fast ACP stream | exactly info:Context compressed (170000 -> ~75800). |
pass |
| a3 | /compress-fast ACP NOOP | No compression needed., no count substring |
pass |
Witness: 05-cli-harness-11-cells-green.png. Log: logs/cli-harness-head-3.log.
Targeted gates (all at head)
| gate | result |
|---|---|
core geminiChat.test.ts |
336/336 |
core chatCompressionService.test.ts + turn.test.ts |
179/179 |
core client.test.ts + sessionService.test.ts + session-transcript-reader.test.ts |
661/661 (up from 649 last round — suites grew on main) |
cli CompressionMessage + compressCommand + compressFastCommand |
36/36 |
cli useGeminiStream.test.tsx (full) |
232/232 |
cli Session.test.ts (full) |
679/679 (up from 668) |
npm run typecheck core + cli |
both clean — gate proven live: it caught this round's scratch harness's type errors (12 on the first pass, 6 on the second) before the file was fixed and removed |
Checkpoint compatibility (checkpoint-probe.mjs, drives the real built dist reader session-resume-token-counts.js): new-format records with both flags resume with the recorded new-side flag (both true and false variants); old-format records without flags fall back to isEstimated: true via ?? true; a later assistant usage record still overrides the baseline — 4/4.
Display-only audit: grep-census of non-test read/write sites of originalTokenCountIsEstimated = 2 writers (geminiChat.ts tryCompress publication + compressFast branches) and 5 display surfaces (+ type declarations); zero telemetry or threshold consumers; makeChatCompressionEvent carries counts only. All "compressed from" surfaces enumerated by grep and covered; all i18n locales keep {{originalTokens}}/{{newTokens}} placeholders so ~ rides the interpolated value.
Flakiness gate
3 rounds × 6 changed suites = 18 runs: 18/18 pass, zero failures, all rounds agree (reduced from the previous round's 5 rounds to keep budget for the commit-3 delta probes).
| suite | r1 | r2 | r3 |
|---|---|---|---|
| cli Session.test.ts | P | P | P |
| cli compressCommand.test.ts | P | P | P |
| cli compressFastCommand.test.ts | P | P | P |
| cli CompressionMessage.test.tsx | P | P | P |
| cli useGeminiStream.test.tsx | P | P | P |
| core geminiChat.test.ts | P | P | P |
Raw: logs/flake-results.txt, per-run logs logs/flake-r*.
Not covered
- Live TUI session with a real compressible context (no model in this environment) — the harnesses render the real component/actions with the real flag shapes instead; this reproduces the rendering of the banners, not the live trigger.
- Repo-wide lint/prettier and other CI lanes (covered by the PR's own CI); typecheck was re-run here and is cited above.
- Flakiness gate reduced to 3 rounds (previous round: 5) to keep budget for the commit-3 delta probes; completed rounds listed above.
- The c2 arm's 6th red test (
repairOrphanedToolUseTurns) reflects main advancing past commit 2, not this PR; per-commit attribution for commits 1–2 against the ORIGINAL base was carried from the previous round's aggregate A/B — here commits 1–2's core half is already in the base tip, and their CLI half is covered by the aggregate base arm. - The
newTokenCountfallback residue described in the Correction was traced statically, not reproduced end-to-end (requires a live summary response with no usage metadata).
Methodology
Environment: CI verify container (node:22-bookworm), merge-ref checkout 04d32233 (HEAD^1 = base tip db0519563d, HEAD^2 = PR head f460259f), npm ci + npm run build pre-run at head. Core A/B used scratch git worktrees at HEAD^1 and 6039fcf4; each built only packages/core from its own sources (root node_modules symlinked, package-local dir hardlink-copied — lockfile identical across all three trees, verified), then ran the head's test file via vitest so the only delta per arm is the source under test; no @qwen-code/* imports cross the worktree boundary (grep-verified closure). The CLI harness drove the real component/command modules with a fake client peer only. The mutation matrix edited files in place, ran the pinned suite, and restored via git checkout with sha256 verification. Checkpoint probe imported the built dist reader directly. Raw logs in logs/, harnesses and scripts in the artifact dir root.
Flakiness gate log
rounds=5 files=6 skipped=0
file packages/cli/src/acp-integration/session/Session.test.ts: (cd packages/cli) npx --no-install vitest run ./src/acp-integration/session/Session.test.ts
file packages/cli/src/ui/commands/compressCommand.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/commands/compressCommand.test.ts
file packages/cli/src/ui/commands/compressFastCommand.test.ts: (cd packages/cli) npx --no-install vitest run ./src/ui/commands/compressFastCommand.test.ts
file packages/cli/src/ui/components/messages/CompressionMessage.test.tsx: (cd packages/cli) npx --no-install vitest run ./src/ui/components/messages/CompressionMessage.test.tsx
file packages/cli/src/ui/hooks/useGeminiStream.test.tsx: (cd packages/cli) npx --no-install vitest run ./src/ui/hooks/useGeminiStream.test.tsx
file packages/core/src/core/geminiChat.test.ts: (cd packages/core) npx --no-install vitest run ./src/core/geminiChat.test.ts
per-file results (P=pass F=fail I=infra-exit, one letter per run):
packages/cli/src/acp-integration/session/Session.test.ts: PPPPP
packages/cli/src/ui/commands/compressCommand.test.ts: PPPPP
packages/cli/src/ui/commands/compressFastCommand.test.ts: PPPPP
packages/cli/src/ui/components/messages/CompressionMessage.test.tsx: PPPPP
packages/cli/src/ui/hooks/useGeminiStream.test.tsx: PPPPP
packages/core/src/core/geminiChat.test.ts: PPPPP
verdict: pass
summary: 6 changed test file(s) x 5 identical rounds, no divergence
--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/commands/compressCommand.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/commands/compressFastCommand.test.ts: P (exit 0)
round 1 · packages/cli/src/ui/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 1 · packages/cli/src/ui/hooks/useGeminiStream.test.tsx: P (exit 0)
round 1 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 2 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/commands/compressCommand.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/commands/compressFastCommand.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 2 · packages/cli/src/ui/hooks/useGeminiStream.test.tsx: P (exit 0)
round 2 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 3 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/commands/compressCommand.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/commands/compressFastCommand.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 3 · packages/cli/src/ui/hooks/useGeminiStream.test.tsx: P (exit 0)
round 3 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 4 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 4 · packages/cli/src/ui/commands/compressCommand.test.ts: P (exit 0)
round 4 · packages/cli/src/ui/commands/compressFastCommand.test.ts: P (exit 0)
round 4 · packages/cli/src/ui/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 4 · packages/cli/src/ui/hooks/useGeminiStream.test.tsx: P (exit 0)
round 4 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 5 · packages/cli/src/acp-integration/session/Session.test.ts: P (exit 0)
round 5 · packages/cli/src/ui/commands/compressCommand.test.ts: P (exit 0)
round 5 · packages/cli/src/ui/commands/compressFastCommand.test.ts: P (exit 0)
round 5 · packages/cli/src/ui/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 5 · packages/cli/src/ui/hooks/useGeminiStream.test.tsx: P (exit 0)
round 5 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
|
Triage re-run completed without a new review.
The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Test Plan (not a blocker): 332 passed — this review observed 20718, 1638, 1616, 494, 3849, 553 passed; 179 passed — this review observed 20718, 1638, 1616, 494, 3849, 553 passed; 649 passed — this review observed 20718, 1638, 1616, 494, 3849, 553 passed; 35 passed — this review observed 20718, 1638, 1616, 494, 3849, 553 passed; 228 passed — this review observed 20718, 1638, 1616, 494, 3849, 553 passed.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
Test Plan(非阻断):332 passed — this review observed 20718, 1638, 1616, 494, 3849, 553 passed; 179 passed — this review observed 20718, 1638, 1616, 494, 3849, 553 passed; 649 passed — this review observed 20718, 1638, 1616, 494, 3849, 553 passed; 35 passed — this review observed 20718, 1638, 1616, 494, 3849, 553 passed; 228 passed — this review observed 20718, 1638, 1616, 494, 3849, 553 passed。
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
Closeout — R4-1 (one-sided test bounds on the send-path effective count) fixed in
Push auto-triggered CI + bot re-review; wenshao's re-review request stays pending. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No blocking issues. LGTM! ✅
Not explored to full depth (tool budget reached): "agent 6b": running geminiChat.test.ts locally to confirm the suite is green at HEAD — the shared worktree has no node_modules/dist and a full npm ci + monorepo build exc…; "agent 1b": runtime execution of the changed/new vitest cases — the worktree and parent checkout have no node_modules installed, and a full monorepo npm ci was disproport….
Test Plan (not a blocker): 332 passed — this review observed 20718, 1638, 22450, 1616, 494, 3849, 553 passed; 179 passed — this review observed 20718, 1638, 22450, 1616, 494, 3849, 553 passed; 649 passed — this review observed 20718, 1638, 22450, 1616, 494, 3849, 553 passed; 35 passed — this review observed 20718, 1638, 22450, 1616, 494, 3849, 553 passed; 228 passed — this review observed 20718, 1638, 22450, 1616, 494, 3849, 553 passed.
Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:
packages/core/src/core/geminiChat.test.ts:15545 — [review] Fast-compression fixture duplicated 4x in one describepackages/core/src/core/geminiChat.test.ts:3703 — [review] Unguarded events.find(...) + narrower cast diverges from sibling idiom (same pattern at ~3758)packages/core/src/core/geminiChat.test.ts:3700 — [review] Send-path COMPRESSED event has no provenance-flag assertionpackages/core/src/core/geminiChat.test.ts:15615 — [probe] Authoritative-branch test pins the flag but not the count value
中文说明
无阻断问题。LGTM!✅
未探索到全部深度(达到工具调用预算):"agent 6b":running geminiChat.test.ts locally to confirm the suite is green at HEAD — the shared worktree has no node_modules/dist and a full npm ci + monorepo build exc…;"agent 1b":runtime execution of the changed/new vitest cases — the worktree and parent checkout have no node_modules installed, and a full monorepo npm ci was disproport…。
Test Plan(非阻断):332 passed — this review observed 20718, 1638, 22450, 1616, 494, 3849, 553 passed; 179 passed — this review observed 20718, 1638, 22450, 1616, 494, 3849, 553 passed; 649 passed — this review observed 20718, 1638, 22450, 1616, 494, 3849, 553 passed; 35 passed — this review observed 20718, 1638, 22450, 1616, 494, 3849, 553 passed; 228 passed — this review observed 20718, 1638, 22450, 1616, 494, 3849, 553 passed。
收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 4 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.22.0)
PR 9568 — Deep Verification Report (local maintainer round)Verdict: 中文摘要结论:可合并(merge-ready)——27/27 条脚本化断言全部通过,0 失败。 A/B 结论(真实 CLI + 本地回环模型服务器驱动,两臂对照):
突变矩阵:4 个突变(渲染忽略标志、commit 3 推导回退、ACP 通知忽略标志、headless 参数交换)全部被对应测试捕获,无幸存者;注意 ACP 测试必须整文件跑, 门禁:head 树 core 514/514、cli 932/932、typecheck 干净;merge 树(main tip 95b2cf4 + head,无冲突 920fb8f5dd)core 516/516、cli 962/962。 未覆盖:真实模型会话(用回环模型替代);i18n 非英文locale;Windows。观察项:hard-tier rescue 路径在压缩后仍超限时直接 throw、不渲染任何压缩通知(两臂相同,先于本 PR 存在)。 Central claim and A/B proofClaim: every surface that reports compression results marks locally estimated token counts with Harness: the real bundled CLI ( Cell table — scenario
|
| cell | oracle (rendered TUI banner) | outcome |
|---|---|---|
base 313f1911 |
Chat history compressed from 120000 to 115423 tokens. then Chat history compressed from 2925 to 1485 tokens. |
unmarked; scale switch reads as ~117k lost — the bug |
head 97a6ab6b |
Chat history compressed from 120000 to ~115423 tokens. then Chat history compressed from ~2925 to ~1485 tokens. |
6/6 assertions: API original unmarked, both estimates marked, ~120000 never appears |
Counts are byte-identical across arms — the only delta is the ~ prefix.
Cell table — scenario auto (send crossing 85% of the 200k default window)
| cell | oracle (auto-compaction notice) | outcome |
|---|---|---|
| base | ... (compressed from: 173000 to 171681 tokens). |
unmarked |
| head | ... (compressed from: ~173013 to ~171694 tokens). |
3/3 assertions: both sides marked — including the original side even though the stored count was API-derived, which is exactly commit f460259's derivation (precomputed effective count ⇒ estimated) |
Witness: evidence/01-banner-ab-base-vs-head.png (both scenarios, both arms).

Mutation matrix (vacuity check)
Witness: evidence/02-mutation-matrix.png.

Each mutation was applied to the head tree, the named suite run, then reverted (git status clean after each).
| # | mutation (head tree) | suite that must catch it | result |
|---|---|---|---|
| M1 | formatTokens ignores isEstimated (CompressionMessage.tsx) — positive control |
CompressionMessage.test.tsx |
caught, 2 failed / 9 passed |
| M2 | provenance derivation reverted to pre-PR logic (geminiChat.ts, commit f460259) |
geminiChat.test.ts |
caught, 5 failed / 330 passed |
| M3 | ACP formatCount ignores flags (Session.ts) |
Session.test.ts (full file) |
caught, 1 failed / 666 passed |
| M4 | flag arguments swapped in headless message (compressCommand.ts) |
compressCommand.test.ts |
caught, 2 failed / 12 passed (the asymmetric fixtures from commit 6039fcf) |
No survivors. Methodological note: M3 initially survived a vitest -t "compaction" run because that filter collects only 1 unrelated test — the pinning ACP test lives outside the filter's net. Full-file runs are what the matrix reports.
Gates
| gate | tree | result |
|---|---|---|
core suites geminiChat + chatCompressionService + turn |
head | 514/514 |
cli suites CompressionMessage + compressCommand + compressFastCommand + useGeminiStream + Session |
head | 932/932 |
npm run typecheck (all workspaces) |
head | clean |
trial merge into current main tip 95b2cf4cb1 |
merge 920fb8f5dd |
conflict-free |
| core suites (same three files) | merge | 516/516 (+2 tests main added since base) |
| cli suites (same five files) | merge | 962/962 |
The trial merge matters: main has touched 4 of the 8 production files this PR changes since the PR's base (Session.ts, useGeminiStream.ts, types.ts, geminiChat.ts), and the merged tree is green.
Back-compat spot-check: the new-count side defaults conservatively — newTokenCountIsEstimated ?? true at packages/core/src/core/client.ts:4376 and packages/core/src/services/session-resume-token-counts.ts:44 — so checkpoints written before this PR resume with the new count treated as estimated; the original-count side (originalTokenCountIsEstimated) is simply absent on old checkpoints and renders unmarked, exactly as before, matching the PR's migration note.
Findings
None attributable to this PR. One pre-existing observation (identical on both arms, out of scope):
- Hard-tier rescue emits no compression notice at all. When the stored count crosses the hard tier (177k here), the send-time hard-rescue compresses and then throws
Context is too large to send safely after automatic compression…before thecompressedstream event is emitted, so the auto-compaction notice (with or without~) never renders. Verified A/A on the first auto-scenario attempt (185000 > 177000): both arms showed only the API error. Pre-existing behavior; this PR neither causes nor worsens it. Worth a separate issue if the notice is expected to survive that path.
Not covered
- Live model session: the model endpoint was a loopback server (deterministic counts); no real tokenizer or real API-reported usage was exercised end-to-end. The compression side-query wire shape — including the
<state_snapshot>directive the real prompt carries — was reproduced, not model-side degradation. - Headless/ACP structured surface end-to-end: the
Context compressed (~N -> ~M).headless string and the ACP diagnostic are covered by the unit suites and mutations M3/M4, not by a separate headless process run. - i18n: only the default (English) locale rendered.
- Windows / Linux TUI: macOS terminal only.
- Per-commit attribution beyond commit
f460259's derivation: commits were verified in aggregate (M2 isolates commit 3's logic; M4 isolates commit 2's fixtures), not one table row per commit.
Methodology
Maintainer-local round on macOS (Apple Silicon), 2026-08-24. Three isolated git worktrees under tmp/ (head 97a6ab6b, base 313f1911, merge 920fb8f5dd), each with its own npm ci install so node_modules/@qwen-code/* symlinks resolve inside the same tree (verified: relative ../../packages/core links, worktrees clean after every mutation). A/B drove the real CLI in tmux (NO_COLOR, isolated QWEN_HOME, seeded settings to skip the first-run wizard) against harnesses/mock-server.mjs; pane text was captured to logs/run-<arm>-<scenario>/pane.txt and every assertion is a scripted regex over that capture or a suite exit code (harnesses/drive-arm.mjs). Raw mock request logs per run are in the same directories. Evidence PNGs rendered with scripts/verify-capture.mjs.
# Conflicts: # packages/core/src/core/geminiChat.ts
0d94698
|
Merged the latest |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No issues found. LGTM! ✅
Test Plan (not a blocker): 332 passed — this review observed 23694, 21201, 1688, 1658, 601, 4227, 626 passed; 179 passed — this review observed 23694, 21201, 1688, 1658, 601, 4227, 626 passed; 649 passed — this review observed 23694, 21201, 1688, 1658, 601, 4227, 626 passed; 35 passed — this review observed 23694, 21201, 1688, 1658, 601, 4227, 626 passed; 228 passed — this review observed 23694, 21201, 1688, 1658, 601, 4227, 626 passed.
中文说明
未发现问题。LGTM!✅
Test Plan(非阻断):332 passed — this review observed 23694, 21201, 1688, 1658, 601, 4227, 626 passed; 179 passed — this review observed 23694, 21201, 1688, 1658, 601, 4227, 626 passed; 649 passed — this review observed 23694, 21201, 1688, 1658, 601, 4227, 626 passed; 35 passed — this review observed 23694, 21201, 1688, 1658, 601, 4227, 626 passed; 228 passed — this review observed 23694, 21201, 1688, 1658, 601, 4227, 626 passed。
— qwen3.8-max via Qwen Code /review (v0.22.0)
|
Local UI verification: PASS
Before (merge-base After (PR head Both captures are from the same tmux session shape; the footer git revision confirms which arm was running. This is runtime evidence only — unit tests for |
1 similar comment
|
Local UI verification: PASS
Before (merge-base After (PR head Both captures are from the same tmux session shape; the footer git revision confirms which arm was running. This is runtime evidence only — unit tests for |
chiga0
left a comment
There was a problem hiding this comment.
Re-review at head 0d946980 (prior review 6039fcf4 was dismissed after author addressed comments).
No blocking findings.
Scope: Standard tier — 17 files, 731 lines diff.
Checked:
turn.tsChatCompressionInfo:originalTokenCountIsEstimated?: booleanadded as optional — backward-compatible.geminiChat.tsprovenance logic: Three-way determination — (1)originalTokenCountOverridecarries its ownisEstimatedflag; (2)precomputedEffectiveTokens !== undefinedforcesisEstimated=true(caller folded in local estimates); (3) otherwise inheritspromptCountIsEstimateDerived(). This covers the key bug: precomputed effective count was previously reported as authoritative when the stored baseline happened to come from the API.- Reactive overflow call site:
originalTokenCountOverridechanged from barenumberto{count, isEstimated}— the only call site in the diff. TypeScript compilation would catch any missed sites. compressFast(): NOOP path setsoriginalTokenCountIsEstimated: this.promptCountIsEstimateDerived(); COMPRESSED path setsoriginalTokenCountIsEstimated: baselineIsEstimated. Both are correct.- UI propagation:
CompressionPropsgains optional fields;CompressionMessage.tsx,compressCommand.ts,compressFastCommand.ts,useGeminiStream.ts,Session.tsall thread through the~prefix via localformatCount/formatTokenCounthelpers. - Test at
geminiChat.test.ts:857("review probe on #9568"):seedResumeTokenCounts(5000, 0, false)(authoritative baseline) +precomputedEffectiveTokens:6200→originalTokenCountIsEstimated=true. Asymmetric flag tests catch swapped argument mutations informatCount. - Prior criticals: ACP notice in
Session.tsnow updated (diff line 6762+).geminiChat.ts:2364provenance assignment now uses the three-way logic. Both addressed.
CI: precheck-pr SKIPPED — fork PR security model. Integration Tests (CLI, No Sandbox) also SKIPPED; test plan ran locally per prior automated review (332+ passed).
Reviewed with AI assistance.
PR #9568 — deep local verification (post-merge head)Verdict:
中文摘要结论:可合并(merge-ready)。 在真实二进制上验证通过,并且与今天的
1. HarnessThe unit under test is unmocked: real
Each arm ran under its own 2. The #9309 sequence, end to endThis is the path the PR description explicitly could not execute ("needs a live model with a compressible context"). It executes here.
The counts are byte-identical across arms — the entire delta is the i18n is fine: the 3. FindingsF1 — "display-only" is not accurate for the auto-compaction / hard-rescue path (disclosure, non-blocking)The PR body says "Display-only: compression thresholds, history mutation, recorded counts, and telemetry are untouched." On the auto path that is not what happens. Commit
And it is persisted, not merely rendered — from the session checkpoint The same value also feeds What is not affected, and this is the important half: the compression decision is unchanged. The cheap gate prefers The new accounting is arguably the better one (both sides of the comparison on the same basis, and the banner now says it is an estimate). The ask is only to stop describing it as display-only — the Risk & Scope section should say that on the auto/hard-rescue path the reported, recorded and telemetry counts shift by (previous-turn output tokens + pending-message estimate), and that dashboards on F2 —
|
| # | mutation applied to the PR-head tree | suite | result |
|---|---|---|---|
| M1 | CompressionMessage formatTokens ignores the flag |
CompressionMessage.test.tsx |
caught 2 fail / 9 pass |
| M2 | provenance reverted to the pre-f460259 inference |
geminiChat.test.ts |
caught 2 fail / 372 pass |
| M3 | ACP Session formatCount ignores the flags |
Session.test.ts |
caught 1 fail / 692 pass |
| M4 | /compress headless message swaps the two flags |
compressCommand.test.ts |
caught 1 fail / 13 pass |
| M5 | auto-compaction formatCount ignores the flags |
useGeminiStream.test.tsx |
caught 1 fail / 232 pass |
| M6 | compressFast NOOP hardcodes isEstimated: false |
geminiChat.test.ts |
survived 374 pass → F3 |
| M7 | reactive override hardcodes isEstimated: false |
geminiChat.test.ts |
caught 2 fail / 372 pass |
Tree restored (git status clean) after every cell.
Load-bearing: reverting the eight production files that carry the behaviour to the main-side parent while keeping the PR's test files turns 20 of the PR's own assertions red — 11 in geminiChat.test.ts (374 total), 9 across the five CLI suites (962 total). The change is not decorative.
Gates:
| gate | head | merge |
|---|---|---|
core geminiChat + chatCompressionService + turn |
553 / 553 | 556 / 556 |
cli CompressionMessage + compressCommand + compressFastCommand + useGeminiStream + Session |
962 / 962 | 962 / 962 |
npm run typecheck (all workspaces) |
clean | clean |
The merge arm matters: main has moved on 8 of this PR's 17 files since the PR's merge point, and the merged tree is conflict-free and green. Everything ran on Node 22.22.2, which closes the "required Node 22 configuration was not exercised" gap disclosed in the round-3 review.
5. Not covered
- Live model. The endpoint was a loopback server with deterministic counts; no real tokenizer or real provider usage report was exercised. The compression side-query wire shape was reproduced, not model-side summary degradation.
- Live ACP client. The ACP auto-compaction notice is covered by
Session.test.tsplus mutation M3, not by driving a realqwen --acpsession. - Telemetry exporter. The
tokens_beforeshift in F1 is established from the code path plus the persisted checkpoint delta; no OTLP export was captured (usage statistics were disabled in the isolated home). - macOS / Windows. Linux only this round.
- Reactive-overflow path at runtime. Verified through unit tests and M7, not through a live provider 400.
6. Reproducing this
git worktree add --detach <dir>/head 0d946980c7 # PR head
git worktree add --detach <dir>/base d128998779 # main-side parent = "without PR"
# npm install in one arm, hardlink node_modules into the others, npm run build in each
# then drive packages/cli/dist/index.js in tmux against a loopback OpenAI server that
# reports usage.prompt_tokens and answers the <state_snapshot> side-query
Note for anyone rebuilding this box-side: packages/core did not compile until @opentelemetry/* was reinstalled — the checked-out package.json wants ^0.221.0, the machine had 0.203.0 hoisted. Environment drift, unrelated to this PR.
|
Released in v0.22.2. |















What this PR does
Marks locally estimated compression token counts with a
~prefix in every place the CLI reports compression results: the interactive/compressand/compress-fastbanners, the automatic-compaction notice, and the headless/ACP info messages. To make that possible without changing any compression behavior,ChatCompressionInfogains anoriginalTokenCountIsEstimatedfield (mirroring the existingnewTokenCountIsEstimated):tryCompressfills it from the same provenance check that already decides whether the "before" number is a local re-estimate or the API-reported prompt count, andcompressFastfills it for both the COMPRESSED and NOOP paths. The CLI then plumbs both flags through the compression history item and renders each estimated figure as~N. Display-only: compression thresholds, history mutation, recorded counts, and telemetry are untouched.Why it's needed
Issue #9309: running
/compress-fast(banner170k -> 75.8k) and then/compress(banner26.6k -> 14.5k) in sequence makes it look like ~50k tokens of context silently vanished. Nothing is lost — the two paths simply measure on different scales./compress-fastanchors its banner on the last API-reported prompt count (system prompt + tool definitions + history, server tokenizer) and subtracts a locally estimated reduction, while a later/compresssees the stored count is estimate-derived and re-estimates history-only with the local estimator. Because neither banner said which numbers were estimates, the scale change read as data loss. This implements the fix direction endorsed in the issue thread: "clearly mark the /compress-fast numbers as estimated", so consecutive compression actions stop producing confusing numbers.Reviewer Test Plan
How to verify
Deterministic unit-level reproduction (no live model needed), red before the fix and green after:
cd packages/core && npx vitest run src/core/geminiChat.test.ts— the newtryCompress (per-chat state)cases reproduce the Somewhere in compression seems incorrect. #9309 sequence: seed an API-authoritative prompt count, runcompressFast()(info now reportsoriginalTokenCountIsEstimated: false/newTokenCountIsEstimated: true), thentryCompress(), which re-estimates history-only (a different scale than the fast banner) and now reportsoriginalTokenCountIsEstimated: true. Before this PR the field wasundefinedin all four new cases.cd packages/cli && npx vitest run src/ui/components/messages/CompressionMessage.test.tsx src/ui/commands/compressCommand.test.ts src/ui/commands/compressFastCommand.test.ts src/ui/hooks/useGeminiStream.test.tsx— banner/headless/ACP/auto-compaction renderings gain~exactly on estimated figures and keep authoritative figures unmarked./compress-fastthen/compress; estimated figures now carry~, so the second banner no longer presents its re-estimate as a continuation of the first banner's API-based number.Evidence (Before & After)
Rendered banner text (from
ink-testing-libraryin the component tests):Before:
After:
No live-session screenshot: reproducing the real banners end to end requires a live model with a compressible context, which this environment does not have; the rendered frames above come from the real component, not a mock.
Tested on
Environment (optional)
Unit tests only (vitest), no live model request. Verified suites: core
geminiChat.test.ts332 passed (incl. 4 new),chatCompressionService.test.ts+turn.test.ts179 passed,client.test.ts+sessionService.test.ts+session-transcript-reader.test.ts649 passed; cliCompressionMessage/compressCommand/compressFastCommand35 passed,useGeminiStream.test.tsx228 passed;npm run typecheckclean in core and cli; ESLint and Prettier clean on all changed files.Risk & Scope
~before estimated numbers; these are display strings (not the structured stream-json result), but any consumer scraping them must tolerate the prefix.newTokenCountIsEstimated ?? true), and older checkpoints simply render unmarked as before.Linked Issues
Fixes #9309
中文说明
这个 PR 做了什么
在 CLI 展示压缩结果的所有位置,为本地估算的 token 数加上
~前缀:交互式/compress与/compress-fast横幅、自动压缩通知、headless/ACP 信息消息。为了在不改变任何压缩行为的前提下做到这一点,ChatCompressionInfo新增originalTokenCountIsEstimated字段(与已有的newTokenCountIsEstimated对称):tryCompress用本来就已经决定"压缩前"数字是本地重估还是 API 报告值的同一来源判断来填充它;compressFast在 COMPRESSED 与 NOOP 两条路径都填充。CLI 随后把两个标志透传到压缩历史项,并把每个估算数字渲染为~N。纯展示改动:压缩阈值、历史变更、记录的计数与遥测均不受影响。为什么需要
Issue #9309:先执行
/compress-fast(横幅170k -> 75.8k)再执行/compress(横幅26.6k -> 14.5k),看起来像约 50k token 的上下文被悄悄丢掉了。实际上什么都没丢——只是两条路径的计量口径不同。/compress-fast的横幅锚定上一次 API 报告的 prompt 数(系统提示 + 工具定义 + 历史,服务端 tokenizer),再减去本地估算的缩减量;而随后的/compress发现存储计数是估算派生的,就用本地估算器只对历史重新计数。由于两个横幅都没有说明哪些数字是估算值,口径切换被读成了数据丢失。本 PR 实现了 issue 线程中认可的修复方向:"至少明确标注 /compress-fast 的数字是估算值",让连续的压缩操作不再产生令人困惑的数字。审阅者测试计划
如何验证
确定性单元级复现(无需真实模型),修复前红、修复后绿:
cd packages/core && npx vitest run src/core/geminiChat.test.ts—— 新增的tryCompress (per-chat state)用例复现 Somewhere in compression seems incorrect. #9309 序列:先种入 API 权威 prompt 计数,运行compressFast()(info 现在报告originalTokenCountIsEstimated: false/newTokenCountIsEstimated: true),再运行tryCompress(),它会对历史做本地重估(与 fast 横幅不同的口径),现在报告originalTokenCountIsEstimated: true。本 PR 之前四个新用例中该字段均为undefined。cd packages/cli && npx vitest run src/ui/components/messages/CompressionMessage.test.tsx src/ui/commands/compressCommand.test.ts src/ui/commands/compressFastCommand.test.ts src/ui/hooks/useGeminiStream.test.tsx—— 横幅/headless/ACP/自动压缩展示恰好只在估算数字上加~,权威数字保持无标记。/compress-fast再/compress;估算数字现在带~,第二个横幅不会再把它的重估呈现为第一个横幅 API 口径数字的延续。证据(前后对比)
组件测试中
ink-testing-library渲染出的横幅文本:修复前:
修复后:
没有真实会话截图:端到端复现真实横幅需要带可压缩上下文的真实模型,本环境不具备;上面的渲染帧来自真实组件,不是 mock。
测试环境
仅单元测试(vitest),无真实模型请求。已验证套件:core
geminiChat.test.ts332 通过(含 4 条新增)、chatCompressionService.test.ts+turn.test.ts179 通过、client.test.ts+sessionService.test.ts+session-transcript-reader.test.ts649 通过;cliCompressionMessage/compressCommand/compressFastCommand35 通过、useGeminiStream.test.tsx228 通过;core 与 clinpm run typecheck干净;所有改动文件 ESLint 与 Prettier 干净。风险与范围
~;这些是展示字符串(不是结构化 stream-json 结果),但任何解析它们的消费者需要容忍该前缀。newTokenCountIsEstimated ?? true),旧检查点则照旧无标记渲染。关联 Issue
Fixes #9309