Skip to content

feat(core): report compression API failures distinctly - #9119

Merged
wenshao merged 5 commits into
QwenLM:mainfrom
xianjianlf2:fix/compression-api-error-status
Aug 29, 2026
Merged

feat(core): report compression API failures distinctly#9119
wenshao merged 5 commits into
QwenLM:mainfrom
xianjianlf2:fix/compression-api-error-status

Conversation

@xianjianlf2

@xianjianlf2 xianjianlf2 commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

What this PR does

This PR adds a dedicated COMPRESSION_FAILED_API_ERROR status for summarization side-query API failures. The compression service now maps non-abort API exceptions to that status, while aborts still propagate normally. Core chat, ACP integration, and CLI rendering now treat this new status as a compression failure and show a more precise message for API-side failures.

Why it's needed

Issue #9115 reports that compression API failures are currently flattened into a generic token-limit compression failure. That makes it hard for callers, UI, and operators to distinguish a prompt/context-size problem from a transient or provider-side API error during summarization. Keeping these failures distinct gives the existing compression circuit breaker a clearer signal and gives users a more accurate error message without changing normal successful compression behavior.

Reviewer Test Plan

How to verify

Run the focused unit tests for compression error mapping, the Gemini chat compression failure circuit breaker, and the CLI compression message component. Confirm that side-query API exceptions return COMPRESSION_FAILED_API_ERROR, abort exceptions still rethrow, the circuit breaker treats the new status as a compression failure, and the CLI renders the dedicated API-failure text.

Evidence (Before & After)

Before: compression side-query API exceptions were reported through the existing generic compression failure path, so API/provider failures were indistinguishable from normal compression/token-limit failures.

After: non-abort API exceptions from the compression side query produce COMPRESSION_FAILED_API_ERROR; tests cover the new status, abort propagation, circuit-breaker handling, ACP predicate handling, and CLI output.

Commands run locally:

  • npm -w packages/core test -- src/services/chatCompressionService.test.ts passed
  • npm -w packages/core test -- src/core/geminiChat.test.ts -t "compression failure circuit breaker" passed
  • npm -w packages/cli test -- src/ui/components/messages/CompressionMessage.test.tsx passed
  • npm -w packages/core run typecheck passed
  • npm -w packages/core run lint -- src/core/turn.ts src/services/chatCompressionService.ts src/core/geminiChat.ts src/core/geminiChat.test.ts src/services/chatCompressionService.test.ts passed
  • npm -w packages/cli run lint -- src/acp-integration/session/Session.ts src/ui/components/messages/CompressionMessage.tsx src/ui/components/messages/CompressionMessage.test.tsx passed
  • git diff --check passed

Not passing locally: npm -w packages/cli run typecheck fails in this fresh worktree before reaching this change because several workspace/generated declaration outputs are missing, including @qwen-code/acp-bridge, @qwen-code/channel-base, @qwen-code/sdk/daemon, and ../generated/git-commit.js. The same run also reports unrelated existing CLI type errors outside this PR's changed behavior.

Tested on

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

Environment (optional)

Local environment: macOS Darwin 25.3.0 arm64, Node v22.23.1, npm 10.9.8.

Risk & Scope

  • Main risk or tradeoff: this adds a new compression failure status, so code paths that exhaustively handle compression statuses need to account for it. The PR updates the core, ACP, and CLI predicates/messages that currently treat compression failures specially.
  • Not validated / out of scope: full CLI typecheck is not validated locally because this fresh worktree is missing generated/workspace declaration outputs; Windows and Linux were not tested locally.
  • Breaking changes / migration notes: no intended public breaking change. The status is more specific for an existing failure path.

Linked Issues

Refs #9115

Note: this touches the same compression function as #9109, so I can rebase after that PR lands if needed.

中文说明

这个 PR 做了什么

这个 PR 为摘要压缩 side-query 的 API 失败新增了独立的 COMPRESSION_FAILED_API_ERROR 状态。压缩服务现在会把非 abort 的 API 异常映射到该状态,同时继续正常抛出 abort。Core chat、ACP 集成和 CLI 展示现在都会把这个新状态视为压缩失败,并为 API 侧失败展示更准确的提示。

为什么需要

Issue #9115 提到,目前压缩 API 失败会被压平成通用的 token-limit 压缩失败。这样调用方、UI 和运维排查时很难区分 prompt/context-size 问题与摘要阶段的临时 API 或 provider 侧错误。区分这些失败可以给现有 compression circuit breaker 更清晰的信号,也能给用户更准确的错误消息,同时不改变正常压缩成功路径。

Reviewer Test Plan

如何验证

运行针对压缩错误映射、Gemini chat 压缩失败 circuit breaker,以及 CLI compression message 组件的聚焦单测。确认 side-query API 异常会返回 COMPRESSION_FAILED_API_ERROR,abort 异常仍然会重新抛出,circuit breaker 会把新状态视为压缩失败,并且 CLI 会展示专门的 API 失败文案。

证据(Before & After)

Before:压缩 side-query 的 API 异常会进入已有的通用压缩失败路径,因此 API/provider 失败与普通压缩/token-limit 失败无法区分。

After:压缩 side-query 的非 abort API 异常会生成 COMPRESSION_FAILED_API_ERROR;测试覆盖了新状态、abort 传播、circuit-breaker 处理、ACP predicate 处理和 CLI 输出。

本地运行的命令:

  • npm -w packages/core test -- src/services/chatCompressionService.test.ts 通过
  • npm -w packages/core test -- src/core/geminiChat.test.ts -t "compression failure circuit breaker" 通过
  • npm -w packages/cli test -- src/ui/components/messages/CompressionMessage.test.tsx 通过
  • npm -w packages/core run typecheck 通过
  • npm -w packages/core run lint -- src/core/turn.ts src/services/chatCompressionService.ts src/core/geminiChat.ts src/core/geminiChat.test.ts src/services/chatCompressionService.test.ts 通过
  • npm -w packages/cli run lint -- src/acp-integration/session/Session.ts src/ui/components/messages/CompressionMessage.tsx src/ui/components/messages/CompressionMessage.test.tsx 通过
  • git diff --check 通过

本地未通过项:npm -w packages/cli run typecheck 在这个 fresh worktree 中会在到达本 PR 改动前失败,因为缺少若干 workspace/generated declaration 输出,包括 @qwen-code/acp-bridge@qwen-code/channel-base@qwen-code/sdk/daemon../generated/git-commit.js。同一次运行还报告了与本 PR 行为无关的现有 CLI 类型错误。

测试平台

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

环境(可选)

本地环境:macOS Darwin 25.3.0 arm64,Node v22.23.1,npm 10.9.8。

风险与范围

  • 主要风险或取舍:这里新增了一个压缩失败状态,因此所有穷尽处理压缩状态的路径都需要识别它。本 PR 已更新当前对压缩失败做特殊处理的 core、ACP 和 CLI predicate/message。
  • 未验证 / 不在范围内:由于 fresh worktree 缺少 generated/workspace declaration 输出,本地未验证完整 CLI typecheck;本地未测试 Windows 和 Linux。
  • Breaking changes / migration notes:不预期有公开 breaking change。该状态只是让已有失败路径更具体。

关联 Issue

Refs #9115

备注:这里触碰了和 #9109 相同的 compression function,如果那个 PR 先合入,我可以按需 rebase。

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Qwen Triage finishedview run. See the stage comments in this thread for the result.

Qwen Triage 已完成 —— 查看运行。结果见本线程中的各阶段评论。

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

@xianjianlf2 thanks for the PR — the change itself looks focused and the linked issue is clear, but the PR body doesn't follow the repository's pull request template, so it pauses here before code review.

What's missing:

  • ## What this PR does and ## Why it's needed — the Summary bullets cover the "what", but the motivation needs its own section (issue #9115 has the rationale ready to lift).
  • ## Reviewer Test Plan with ### How to verify, ### Evidence (Before & After), and ### Tested on — the raw test commands are listed, but reviewers need the outcome-level view: what to run, expected vs observed, and which OSes you verified on. Also note that "npm -w packages/cli run typecheck fails in my worktree" belongs in the test plan with a bit more detail — is that environmental (missing generated outputs), or does it also fail on a clean main checkout?
  • ## Risk & Scope — the note about overlapping with #9109 is exactly the kind of thing this section is for.
  • ## Linked IssuesRefs #9115 should live there (no closing keyword since the issue tracks more than this PR? your call).
  • The bilingual <details> block with the Chinese translation of the body.

Once the PR body follows the template, re-run @qwen-code /triage (or push a new commit / ping a maintainer) and the review will pick up from there.

中文说明

@xianjianlf2 感谢提交 PR——改动本身很聚焦,关联的 issue 也很清楚,但 PR 描述没有遵循仓库的 pull request 模板,因此在进入代码审查前先暂停。

缺少的部分:

  • ## What this PR does## Why it's needed——Summary 里的要点覆盖了"做了什么",但动机需要单独成节(issue #9115 里的理由可以直接引用)。
  • ## Reviewer Test Plan,包含 ### How to verify### Evidence (Before & After)### Tested on——测试命令已经列出,但审查者需要结果层面的信息:如何验证、预期与实际表现、在哪些操作系统上验证过。另外,"npm -w packages/cli run typecheck 在我的 worktree 里失败"这条应放进测试计划并补充细节——这是环境问题(缺少生成的产物),还是在干净的 main 检出上也会失败?
  • ## Risk & Scope——与 #9109 存在重叠的说明正是这个章节该放的内容。
  • ## Linked Issues——Refs #9115 应放在这里(如果该 issue 覆盖的范围超出本 PR,则不使用自动关闭关键字,由你决定)。
  • 正文的双语 <details> 中文翻译块。

按模板更新 PR 描述后,重新运行 @qwen-code /triage(或推送新提交 / 联系 maintainer),审查会从暂停处继续。

Qwen Code · qwen3.8-max

@xianjianlf2

Copy link
Copy Markdown
Contributor Author

@qwen-code /triage

1 similar comment
@wenshao

wenshao commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

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: 39 passed · 0 failed · 39 total

Flakiness gate: ✅ 3 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:39 通过 · 0 失败 · 39 总计

抖动门:✅ 3 changed test file(s) x 5 identical rounds, no divergence

Verification report

Verification report — PR #9119 feat(core): report compression API failures distinctly

Verdict: merge-ready — 39/39 scripted assertions passed, 0 unexpected failures.
Verified head: 6b5c08ad44446e838a3434f9fefeb86db319ff2a (merge 1112183, base tip 3a1f86d).

中文摘要
  • 结论: merge-ready。39/39 脚本断言通过,无意外失败。
  • A/B 结论: 中心声明被证明是 load-bearing。对真实 loopback 模型端点返回 500 的场景,base 构建中 compress() 会把 API 错误抛出到调用方;head 构建则返回结构化的 COMPRESSION_FAILED_API_ERRORnewHistory=null、token 数原样转发、debug warn 记录原始错误)。cache-sharing 门(shared + cold 两次请求都失败)在 head 同样映射到新状态。abort 在两个构建中都正常重新抛出(不变量保持)。
  • 变异矩阵: 回退 service 的 try/catch → 新测试以预期断言变红(错误未被映射而逃逸);删除 breaker/CLI 中的新状态分支 → 对应测试变红;仅删除 abort 重抛行 → abort 测试变红。唯一幸存者:Session.ts(ACP)谓词中的新状态子句没有任何测试钉住(对照突变 EMPTY_SUMMARY 子句会让同一测试变红,证明测试文件是活的)——记为覆盖缺口(Suggestion),不阻塞。
  • : core 与 cli 的 typecheck 均干净(作者本地无法运行的 CLI typecheck 在本环境通过);聚焦测试 139/139、330/330、9/9、breaker 3/3;eslint 干净且活性已验证。
  • 未覆盖: 全量 core/cli 测试套件;交互式 TUI 端到端渲染(组件级 ink 测试已覆盖渲染);自动压缩失败在 TUI 中保持静默是既有设计,未改动。

Central claim + A/B

Central claim: a non-abort API failure of the compression side-query is converted from an escaping exception into a distinct COMPRESSION_FAILED_API_ERROR result, while aborts still propagate; the new status is treated as a compression failure by the core breaker, the ACP predicate, and rendered with dedicated text in the CLI.

Harness (ab-compression.mjs, in this artifact dir) drives the compiled dist ChatCompressionService.compress() through the real BaseLlmClient + real OpenAI-compatible ContentGenerator against a loopback HTTP server (500 for failure cells, stall for the abort cell). Only the Config/GeminiChat seams are duck-typed; every missing-config fallback is logged by the harness. The base arm is a worktree at HEAD^1 with only packages/core rebuilt; base dist verified guard-free (grep -c "compression side-query failed" = 0 vs 1 at head) and free of the new enum member; no @qwen-code/* imports in the production dist (realpath check), so the control is pure code A/B (lockfile untouched by the PR; the base tree shares the root node_modules plus a symlink of the package's nested npm-only node_modules, same pinned deps on both arms).

Cell (identical scenario both arms) HEAD (6b5c08a) BASE (3a1f86d) Capture
cold door, loopback 500 resolves: status COMPRESSION_FAILED_API_ERROR, newHistory=null, newTokenCount=originalTokenCount=12345, warn compression side-query failed: … 500 loopback provider exploded, exactly 1 model request rejects: Failed to generate text content …: 500 loopback provider exploded escapes compress() (1 request) 01-ab-head-cold-500-resolves-with-api-error-status.png / 02-ab-base-cold-500-rejects-unguarded.png
cache-sharing door (provider anchor present; shared + cold both 500) resolves with the same status after 2 failed requests (shared-request warn, then side-query warn) rejects after 2 failed requests 03-ab-head-cache-sharing-door-also-mapped.png
abort mid-request (server stalls, client aborts at 300 ms) rejects Request was aborted.; no status warn (guard not taken) rejects Request was aborted. (invariant) 04-ab-abort-propagates-both-arms.png

The pair flips exactly as the PR intends: 5/5 failure cells go throw → structured status at head, and the abort invariant holds on both arms. A/B assertions: 23/23.

Consumer sweep (all CompressionStatus readers repo-wide): the two isCompressionFailureStatus predicates (core geminiChat.ts, cli Session.ts) and the CompressionMessage.tsx switch are the only failure-special readers; all were updated by the PR. Remaining readers compare only COMPRESSED/NOOP (client.ts, historyMapping.ts, compressFastCommand.ts), which treat the new status correctly by construction; no other package (acp-bridge, channels, desktop*, sdk-*, web-*, …) reads the status. The telemetry/log path reverse-looks-up the enum name, so the new status logs as COMPRESSION_FAILED_API_ERROR automatically.

Corrections

  • The PR body's Reviewer Test Plan cites geminiChat.test.ts -t "compression failure circuit breaker" as the breaker coverage for the new status, but that describe still exercises COMPRESSION_FAILED_INFLATED_TOKEN_COUNT (lines 15591/15658). The actual pin for the new status in the breaker is the test the PR re-pointed, clears consecutiveFailures after a forced successful compression (its step-1 counter increment only happens if isCompressionFailureStatus(API_ERROR) is true). The old status remains pinned elsewhere, so no coverage was lost — this is a description inaccuracy, not a code defect.
  • The body lists the CLI typecheck as "not validated locally" (fresh worktree lacked generated outputs). It passes clean in this environment (tsc --noEmit for @qwen-code/qwen-code, exit 0), which also proves no exhaustive switch over the new enum member breaks anywhere in the CLI.

Findings

  1. Suggestion — the ACP predicate's new clause is unpinned by any test. Deleting || status === CompressionStatus.COMPRESSION_FAILED_API_ERROR from Session.ts's isCompressionFailureStatus leaves Session.test.ts fully green (survivor, capture 07-…), while the same-file control mutation (dropping the EMPTY_SUMMARY clause) turns the Guard test red (expected "spy" to be called 2 times, but got 4 times) — proving the file is live and the gap real. The body claims "tests cover … ACP predicate handling"; no such test is in the diff. Non-blocking: the predicate is byte-parallel to the core one (which is pinned), the typecheck passes, and the downstream consumer (beforeSend({ compressionFailed }), Guard flow) treats all failure statuses identically.
  2. Note (pre-existing, unchanged by the PR) — auto-compression failures are silent in the interactive stream by design ("Failed/skipped compaction attempts are silent", geminiChat.ts ~2726). The new dedicated text is visible via manual /compress (which passes compressionStatus into the rendered CompressionMessage) and via the ACP diagnostic path; the cause itself survives in the debug warn (compression side-query failed: ${String(error)}). No information is lost relative to base — base lost the whole turn to the thrown error instead.

Mutation matrix

# Mutation Suite Result Capture
control none (head) chatCompressionService.test.ts 139/139 green
M1 revert the whole new try/catch same killed — 1/139 red: the new status test fails with the un-mapped Error: context window exceeded escaping (intended assertion) 05-mutation-m1-guard-reverted-new-test-red.png
M1b (same revert) abort-rethrow test same survives — expected: base rethrows everything, so the coarse revert can't distinguish; see M1c
M1c drop only if (abortSignal.aborted) throw error; abort test killedpromise resolved {…compressionStatus: 7} instead of rejecting
control none (head) geminiChat.test.ts latch test green
M2 drop API_ERROR clause, core breaker latch test killedexpected +0 to be 1 (positive control of the round) 06-mutation-m2-breaker-clause-dropped-latch-red.png
control none (head) CompressionMessage.test.tsx 9/9 green
M3 drop the API_ERROR case same killed — frame renders only the icon ("◆︎"), i.e. empty message without the case
control drop EMPTY_SUMMARY clause, ACP Session Guard test killed2 times, but got 4 (liveness proof) 07-mutation-m4-session-control-red-survivor-green.png
M4 drop API_ERROR clause, ACP same survivor → coverage gap (finding 1) same

The enum member itself is compile-pinned (three production files and three test files reference it; removing it breaks tsc), so no behavioral mutation applies.

Targeted gates

Gate Result
chatCompressionService.test.ts 139/139
geminiChat.test.ts (full) 330/330
geminiChat.test.ts -t "compression failure circuit breaker" 3/3
CompressionMessage.test.tsx 9/9
core typecheck (tsc --noEmit) clean
cli typecheck (tsc --noEmit) clean (author-blocked locally; passes here)
eslint on the 5 changed files clean; liveness proven by a planted unused-var reported as no-unused-vars
git diff HEAD^1..HEAD --check clean

Not covered

  • Full packages/core / packages/cli suites (targeted files only, per scope).
  • Interactive TUI end-to-end rendering of the new message; covered at component level by the ink test (M3 exercises the real renderer).
  • /compress and /compress-fast command flows end-to-end (they pass compressionStatus through verbatim — verified by reading; rendering covered by the component test).
  • Per-commit attribution beyond the single commit: repo is shallow (depth 2) but rev-list HEAD^1..HEAD^2 = 1 matches the snapshot's commits array, and the PR is a single commit, so aggregate = per-commit.
  • Windows/macOS; only the Linux CI container.
  • The snapshot's baseRefOid (53a7f2f…) predates the checkout; per the CI contract the local merge-ref HEAD^1 (3a1f86d) is the authoritative base.

Methodology

Environment: CI verify container (node 22, merge-ref checkout at depth 2; npm ci + full build pre-run). The A/B harness imports compiled dist/ modules by file path from each tree (head repo root vs a scratch worktree at HEAD^1 with only core rebuilt), constructs the real OpenAIContentGenerator via the production createContentGenerator factory pointed at a loopback HTTP server (immediate 500, or stall for abort), and wraps it in the real BaseLlmClient; Config/GeminiChat seams are duck-typed with every implicit fallback recorded (getTelemetryUserId, getChatRecordingService, …). Assertions compare resolved status/tokens/warn text and the request count observed by the server. Base purity asserted by grep (guard string 0 vs 1, enum member absent) and realpath (no workspace symlinks in the production import chain). Mutation matrix edited source in place, ran the focused vitest suite, restored via git checkout, verifying each red run failed the intended assertion. Captures via scripts/verify-capture.mjs; raw harness logs and scripts live in this artifact directory (ab-compression.mjs, m4pair.sh).

Flakiness gate log

rounds=5 files=3 skipped=0
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/core/src/core/geminiChat.test.ts: (cd packages/core) npx --no-install vitest run ./src/core/geminiChat.test.ts
file packages/core/src/services/chatCompressionService.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/chatCompressionService.test.ts


per-file results (P=pass F=fail I=infra-exit, one letter per run):
  packages/cli/src/ui/components/messages/CompressionMessage.test.tsx: PPPPP
  packages/core/src/core/geminiChat.test.ts: PPPPP
  packages/core/src/services/chatCompressionService.test.ts: PPPPP

verdict: pass
summary: 3 changed test file(s) x 5 identical rounds, no divergence

--- per-invocation detail (full copy in the artifact) ---
round 1 · packages/cli/src/ui/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 1 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 1 · packages/core/src/services/chatCompressionService.test.ts: P (exit 0)
round 2 · packages/cli/src/ui/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 2 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 2 · packages/core/src/services/chatCompressionService.test.ts: P (exit 0)
round 3 · packages/cli/src/ui/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 3 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 3 · packages/core/src/services/chatCompressionService.test.ts: P (exit 0)
round 4 · packages/cli/src/ui/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 4 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 4 · packages/core/src/services/chatCompressionService.test.ts: P (exit 0)
round 5 · packages/cli/src/ui/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 5 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 5 · packages/core/src/services/chatCompressionService.test.ts: P (exit 0)

Evidence images

01-ab-head-cold-500-resolves-with-api-error-status

02-ab-base-cold-500-rejects-unguarded

03-ab-head-cache-sharing-door-also-mapped

04-ab-abort-propagates-both-arms

05-mutation-m1-guard-reverted-new-test-red

06-mutation-m2-breaker-clause-dropped-latch-red

07-mutation-m4-session-control-red-survivor-green

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Re-run after the author merged upstream/main again this morning — head moved 411fcd439a5e6d3 (a merge commit), so the gate is re-evaluated against the current head, not the earlier attestation.

Template: looks good ✓ (all sections present, bilingual summary included). One staleness note: the description's "CLI typecheck fails in this fresh worktree" caveat is outdated — the maintainer's round-2 verification shows a full-workspace typecheck passes after a real npm install && npm run build.

Problem: observed, not theoretical. Issue #9115 asks for exactly this — compression API failures flattened into the generic token-limit failure — and the maintainer's two live A/B verifications in this thread (Aug 27 and today, both on real builds with a request journal as the oracle) demonstrate the concrete harm on main: a summarizer 5xx kills the whole user turn, and headless/ACP /compress reports success on a failed compression.

Direction: aligned. Distinct, diagnosable compression failure modes are squarely core-mission and implement what #9115 requests. The overlap with #9109 flagged in the first pass stays resolved: #9109 merged Aug 18, and the branch has re-merged main twice since.

Size: core + CLI paths touched. 314 production lines (260 excluding 54 lines of locale strings) vs. 377 test lines, 0 generated/schema — below every awareness threshold.

Approach: unchanged since the last full review — one new status, a catch-with-abort-rethrow around the cold side-query, one shared isCompressionFailureStatus replacing the two local copies, and status-specific text at every render site. Today's merge brought exactly one adaptation: main renamed geminiChat.tsllm-chat.ts, and the branch's breaker change followed the rename cleanly. No drive-bys.

Risk: acp-integration/session/Session.ts matches the revert-correlated path list directly, and llm-chat.ts is the renamed geminiChat.ts from the same list — full Stage 2 depth applied again (consumer enumeration on today's main + CI evidence), not a skim.

Moving on to code review. 🔍

中文说明

作者今早再次合入 upstream/main 后的重新运行——head 从 411fcd439a5e6d3(merge 提交),因此关卡针对当前 head 重新评估,而不是沿用此前的背书。

模板:完整 ✓(各章节齐全,含中文翻译块)。一处过期提示:描述里"此 fresh worktree 中 CLI typecheck 失败"的注意事项已过时——维护者第二轮实机验证显示,真正 npm install && npm run build 之后全 workspace typecheck 通过。

问题:已观测到,不是理论问题。Issue #9115 要求的正是这一点——压缩 API 失败被压平进通用的 token-limit 失败——且本线程中维护者的两次实机 A/B 验证(8 月 27 日与今天,均基于真实构建、以请求台账为判据)证明了 main 上的具体危害:摘要器 5xx 会杀死整个用户回合,headless/ACP 的 /compress 会在压缩失败时报告成功。

方向:对齐。可区分、可诊断的压缩失败模式完全属于核心使命,也是对 #9115 的直接实现。首轮指出的与 #9109 的重叠保持已解决状态:#9109 已于 8 月 18 日合入,本分支此后已两次重新合入 main。

规模:触及 core + CLI 路径。生产代码 314 行(不含 54 行 locale 文案则为 260 行),测试 377 行,生成/schema 0 行——低于所有需关注阈值。

方案:自上次完整审查以来未变——一个新状态、冷压缩 side-query 外加带 abort 重抛的 catch、一个共享的 isCompressionFailureStatus 替换两处本地副本、状态出现的所有渲染点给出专属文案。今天的合并只带来一处适配:main 把 geminiChat.ts 更名为 llm-chat.ts,分支上的熔断改动干净地跟随了更名。无顺手改动。

风险:acp-integration/session/Session.ts 直接命中与回滚相关的路径列表,llm-chat.ts 即该列表中更名后的 geminiChat.ts——已再次按完整 Stage 2 深度执行(在当日 main 上枚举消费者 + CI 证据),而非略读。

进入代码审查 🔍

Qwen Code · qwen3.8-max

Reviewed at 39a5e6d3c95467eb72b047b167cc72a8d175023d · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Code review

This head differs from the previously reviewed 411fcd4 by exactly one thing: today's merge of upstream/main. Main renamed geminiChat.tsllm-chat.ts in between, and the branch's breaker change followed the rename cleanly — the local predicate deletion and the shared-helper import now land in llm-chat.ts, and the breaker fixture switch lands in llm-chat.test.ts. Everything else is shape-identical to the reviewed diff, so this pass re-verified the merge adaptation and re-ran the consumer enumeration against today's main instead of re-reviewing unchanged lines.

My independent proposal for this problem (written before reading the diff, first pass) was: one new CompressionStatus member appended last, a try/catch around the cold side-query that rethrows aborts, one shared failure predicate instead of the two local copies the codebase carried, and status-specific text everywhere the status surfaces. The PR still matches that exactly.

What I re-confirmed on today's main:

  • Consumers. Every production reference to CompressionStatus re-enumerated: the === COMPRESSED gates (client.ts, llm-chat.ts rescue/handoff sites, Session.ts, historyMapping.ts) and the === NOOP gates in compressFastCommand treat the new member as a generic non-success automatically; the only exhaustive switch (compressionStatus) in production is CompressionMessage.tsx, which this PR delegates to the shared helper with an explicit COMPRESSION_FAILED_API_ERROR case. The member is appended last, so existing numeric values are stable.
  • The catch covers the whole API surface. compress() has exactly two side-query paths: the cache-sharing request (pre-existing catch, falls back to the dedicated summarizer) and runColdCompression (this PR's catch → debug warn + COMPRESSION_FAILED_API_ERROR, unchanged token counts). Both rethrow when aborted, and each has a dedicated test.
  • Predicate dedup holds. isCompressionFailureStatus lives once in core turn.ts (accepting null | undefined, returning false for both), exported through the core index; the breaker in llm-chat.ts, the ACP Guard in Session.ts, and the new /compress error branches all consume it.

Open items, on the record and non-blocking — all from the maintainer's round-2 verification and the /review ledger, none of them new in the code, listed here so they don't get lost at merge:

  • N1 — turn.test.ts doesn't pin the enum member. The predicate test references CompressionStatus.COMPRESSION_FAILED_API_ERROR on both sides, so deleting the member leaves undefined === undefined and the suite stays green (measured mutant M1 survives on this head). The practical shield is the compiler — production files reference the member, so a deletion fails typecheck — but the suite shouldn't claim protection it doesn't provide. The maintainer's one-line Object.keys fix is verified to kill the mutant; worth taking in a follow-up or a quick push.
  • N2 — the headless originalTokenCount wiring isn't pinned either. Dropping it from both getCompressionFailureStatusText call sites also leaves every suite green (mutant M4), with a real consequence measured live: a ≥50k-token history with an inflated-count outcome then shows the <50k message. The code passes the count correctly today; only the test coverage is missing.
  • F1 — decided. Headless /compress exits 1 (ACP -32603) even for the benign INFLATED_TOKEN_COUNT outcome; the maintainer re-measured it on this head and then approved, so it stands as an accepted tradeoff. A follow-up could keep that one status at exit 0 with the improved text.
  • F2–F4 (= ledger R3-2/R3-3/R3-4). Provider error text reaches only the debug log on the manual path; the interactive failure banner renders success-colored (pre-existing for other failure statuses, newly reachable for API_ERROR); the non-interactive failure return drops truncationNotice. Suggestions, tracked in the /review ledger, none blocking.

How the failure flows

sequenceDiagram
    participant P1 as User turn
    participant P2 as LlmChat
    participant P3 as CompressionService
    participant P4 as Side-query API
    participant P5 as UI, ACP, headless
    P2->>P3: tryCompress calls compress
    P3->>P4: runColdCompression side-query
    P4-->>P3: API error thrown
    Note over P3: catch rethrows aborts, maps the rest to COMPRESSION_FAILED_API_ERROR
    P3-->>P2: failure status returned
    P2->>P2: circuit breaker counts one strike
    P2-->>P5: shared predicate drives banner text and headless or ACP error
Loading
Files changed (14 of 22 shown)
File What changed
packages/core/src/core/turn.ts New COMPRESSION_FAILED_API_ERROR member appended last, plus the shared isCompressionFailureStatus helper
packages/core/src/core/turn.test.ts Helper coverage for every failure member and the non-failure members
packages/core/src/services/chatCompressionService.ts try-catch around the cold side-query: aborts rethrow, other errors warn and return the new status
packages/core/src/services/chatCompressionService.test.ts Status mapping on side-query failure and abort rethrow
packages/core/src/core/llm-chat.ts Local predicate deleted, imports the shared one for the circuit breaker (file renamed from geminiChat.ts on main)
packages/core/src/core/llm-chat.test.ts Breaker fixture switched to the new status
packages/cli/src/acp-integration/session/Session.ts Local predicate deleted, imports the shared one for the Guard path
packages/cli/src/acp-integration/session/Session.test.ts Guard test parametrized over empty-summary and API-error statuses
packages/cli/src/ui/utils/compression-text.ts New shared status-to-text mapping used by the TUI banner and headless paths
packages/cli/src/ui/components/messages/CompressionMessage.tsx Slimmed down to delegate text rendering to the shared helper
packages/cli/src/ui/components/messages/CompressionMessage.test.tsx Renders the API-error, empty-summary, and truncated messages
packages/cli/src/ui/commands/compressCommand.ts Failure statuses become status-specific errors in headless and ACP modes, keeping the interactive banner item
packages/cli/src/ui/commands/compressCommand.test.ts Interactive history item, non-interactive error, and ACP error paths
…and 9 locale files (ca, de, en, fr, ja, pt, ru, zh-TW, zh) Three new compression-failure keys each, all nine locales covered

Testing

Evidence carried here: the PR's own CI results for the reviewed commit, fetched through the API (unattended run — no PR code was built or executed, and no tmux drive on this path), plus the maintainer's round-2 live verification posted in this thread against this exact head.

All 22 executed checks are green with nothing pending: the Linux unit suite, the desktop-shell builds on both OSes, the web-shell smoke test, the dependency CVE audit, the secret scan, precheck, the real-daemon E2E, and the full SDK Java matrix. Notably the integration suite did run this timeIntegration Tests (no-AK, No Sandbox) is success — where at the previous head it only existed as a skipped merge-queue job; the macOS/Windows unit jobs and the keyed CLI integration job remain merge-queue-only by design, and a skipped named job still satisfies the required check context.

Check Conclusion
Test (ubuntu-latest, Node 22.x) success
Test (macos-latest, Node 22.x) skipped (merge queue only)
Test (windows-latest, Node 22.x) skipped (merge queue only)
Integration Tests (CLI, No Sandbox) skipped (merge queue only)
Integration Tests (no-AK, No Sandbox) success
Desktop Shell (ubuntu-22.04) success
Desktop Shell (windows-2022) success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) success
Dependency CVE audit success
Secret scan (TruffleHog) success
precheck-pr / precheck success
Real daemon E2E / Java 11 success
SDK Java (ubuntu/macos/windows, Java 11/17/21) success

The behavioural claim was settled at this very head by the maintainer's round-2 A/B run — both arms rebuilt from source, driven through TUI, headless -p, and raw ACP against a provider that degrades only the compression side-query, with a request journal as the oracle: the turn survives where it dies on main, the false headless/ACP success is gone, and 7 of 9 revert mutations are killed by the PR's own tests (the two survivors are precisely N1 and N2 above — test-pin gaps, not behaviour gaps). That is deeper than what @qwen-code /verify would produce; the sponsored lane remains available to any maintainer who wants an independent run, but the claim no longer rests on the author's word.

Not verified: live end-to-end runs on Windows/Linux — CI covers the unit and no-AK integration level on Linux, and the changed surface has no OS-specific code paths. The description's local macOS results remain the author's claim; they are not load-bearing here because the maintainer rebuilt and re-drove both arms independently.

中文说明

代码审查:当前 head 与此前审查的 411fcd4 的唯一差别是今天合入的 upstream/main。期间 main 把 geminiChat.ts 更名为 llm-chat.ts,分支上的熔断改动干净地跟随了更名——本地谓词删除与共享谓词导入落在 llm-chat.ts,熔断 fixture 切换落在 llm-chat.test.ts。其余部分与已审 diff 形态完全一致,因此本轮只复核了合并适配,并在当日 main 上重新枚举消费者,没有重复审查未变化的行。

我的独立方案(首读 diff 前写下)是:末尾追加一个 CompressionStatus 成员、在冷压缩 side-query 外加 abort 重抛的 try/catch、用一份共享失败谓词替换代码库里的两处本地副本、状态出现的每处都给出专属文案。PR 与此依然完全一致。

在当日 main 上重新确认:消费者——全部生产引用重新枚举,=== COMPRESSED 门(client.tsllm-chat.ts 救援/移交点、Session.tshistoryMapping.ts)与 compressFastCommand=== NOOP 门都会自动把新成员当作非成功;生产中唯一的穷尽 switch (compressionStatus)CompressionMessage.tsx,本 PR 已委托给带显式 COMPRESSION_FAILED_API_ERROR 分支的共享 helper;成员追加在末尾,数值稳定。catch 覆盖全部 API 面:compress() 只有两条 side-query 路径,缓存共享请求(既有 catch,回退专用摘要器)与 runColdCompression(本 PR 的 catch → debug warn + 新状态,token 数不变),均在 abort 时重抛且各有专门测试。谓词去重成立:isCompressionFailureStatus 只在 core turn.ts 存在一份(接受 null | undefined 且均返回 false),经 core index 导出,llm-chat.ts 熔断、Session.ts ACP Guard 与新的 /compress 错误分支共用。

在案、不阻塞的遗留项(均来自维护者第二轮实机验证与 /review 台账,代码里没有新东西,列在这里以免合入时丢失):N1——turn.test.ts 没钉住枚举成员,两侧都引用 CompressionStatus.COMPRESSION_FAILED_API_ERROR,删掉成员后 undefined === undefined 仍全绿(变异 M1 在当前 head 存活);实际防线是编译器——生产文件引用该成员,删除会挂 typecheck——但测试套件不应声称自己没有的保护;维护者给出的一行 Object.keys 修复已验证可杀变异,值得后续或快速推送补上。N2——headless 的 originalTokenCount 接线同样没被钉住(变异 M4 存活),实机后果已测得:≥50k 历史在 inflated 结果下会显示 <50k 文案;当前代码传值正确,缺的只是测试覆盖。F1——已拍板:headless /compress 对良性的 INFLATED_TOKEN_COUNT 也 exit 1(ACP -32603),维护者在当前 head 复测后批准,视为接受的取舍,后续可让该状态保持 exit 0 并保留改进文案。F2–F4(= 台账 R3-2/R3-3/R3-4):手动路径上 provider 错误原文只进 debug 日志、交互式失败横幅渲染为成功色(对其他失败状态是既有问题,API_ERROR 首次可达)、非交互失败返回丢掉 truncationNotice——均为建议项,记录在 /review 台账,均不阻塞。

测试证据:本轮为无人值守运行,通过 API 读取所审提交的 CI 结果(未构建或执行任何 PR 代码,本路径也不做 tmux 驱动),并辅以维护者针对当前 head 的第二轮实机验证。22 个实际运行的检查全部通过、无进行中:Linux 单测、双平台桌面壳构建、web-shell 冒烟、依赖 CVE 审计、密钥扫描、precheck、真实守护进程 E2E、SDK Java 全矩阵。值得注意的是集成套件本轮真正运行了——Integration Tests (no-AK, No Sandbox) 通过——上一个 head 上它只是仅合并队列的跳过任务;macOS/Windows 单测与带密钥的 CLI 集成按设计仅在合并队列运行,跳过的具名任务仍满足必选检查上下文。行为声明已由维护者针对当前 head 的第二轮 A/B 实机验证补足:双臂均从源码重建,经 TUI、headless -p 与裸 ACP 驱动、只对压缩 side-query 降级的 provider、以请求台账为判据——回合在本 PR 上存活而在 main 上死亡,headless/ACP 假成功消失,9 个回退变异中 7 个被本 PR 自己的测试杀死(两个存活者恰是上面的 N1 与 N2——测试钉住能力的缺口,不是行为缺口)。这比 @qwen-code /verify 能给出的更深;任何想要独立运行的维护者仍可使用赞助通道,但行为声明已不再依赖作者的一面之词。未验证:Windows/Linux 实机端到端——CI 在 Linux 上覆盖单测与 no-AK 集成层面,且改动面没有 OS 相关代码路径。描述中的本地 macOS 结果仍为作者声明,但此处不依赖它,因为维护者已独立重建并驱动了双臂。

Qwen Code · qwen3.8-max

Reviewed at 39a5e6d3c95467eb72b047b167cc72a8d175023d · re-run with @qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — solid and deeply verified; the only open items are test-pin gaps and polish, none blocking.

Stepping back: the delta since the last attested head is a single upstream merge, and it's the clean kind — main renamed geminiChat.tsllm-chat.ts, the branch followed the rename, and nothing else moved. My independent proposal for this problem is still exactly what landed: one new status appended last, a catch that rethrows aborts, one shared predicate where the codebase had two copies, and status-specific text at every surface. The merge didn't drift from that shape.

What makes this a 4 rather than a 5 is not doubt about the change — it's the two surviving mutants from the maintainer's round-2 run, N1 and N2. Both are places where the suite claims protection it doesn't provide: the enum-member test that stays green if you delete the member, and the headless originalTokenCount wiring you can drop without any test noticing. The shipped code is correct today (the compiler backstops N1, and the count is passed correctly for N2), so these are test-hardening items, not defects — but a suite that can't see its own feature deleted is worth the one-line and two-line fixes, ideally before this lands or in a fast follow. F2–F4 stand as recorded suggestions; F1 was a deliberate call the maintainer made with fresh measurements on this exact head, then approved.

Everything else is as clean as this gets: the behavioural claim was proven twice on real builds with a request journal as oracle — turns survive where main dies, false headless/ACP success is gone — CI is fully green on the reviewed commit including an integration run that actually executed this time, consumers were re-enumerated on today's main, and the diff carries nothing beyond the goal.

On the vote: my earlier approval stood on 411fcd4 and was dismissed by today's merge push — correctly so. The maintainer's approval on 39a5e6d3 is a first vote, and main needs two, so this pass restores the second, pinned to the exact commit reviewed.

Approving. ✅

中文说明

退一步看:自上次背书的 head 以来的增量只是一次 upstream 合并,而且是干净的那种——main 把 geminiChat.ts 更名为 llm-chat.ts,分支跟随了更名,其余一概未动。我对这个问题的独立方案与最终落地的形态依然完全一致:末尾追加一个新状态、带 abort 重抛的 catch、一份共享谓词替代代码库中的两份副本、状态出现的每个表面都有专属文案。合并没有偏离这个形态。

给 4 分而不是 5 分,不是因为对改动本身有疑虑,而是维护者第二轮运行中存活下来的两个变异体 N1 与 N2:两处都是套件声称提供了保护、实际没有的地方——删掉枚举成员测试仍全绿,去掉 headless 的 originalTokenCount 也没有任何测试察觉。当前发布的代码是正确的(N1 有编译器兜底,N2 的计数当前传值正确),所以它们是测试加固项而非缺陷——但一个看不见自己特性被删除的套件,值得用一行和两行的修复补上,最好在合入前或紧随其后。F2–F4 作为记录在案的建议项保留;F1 是维护者在本 head 上用新鲜实测数据做出的刻意拍板,随后批准。

其余一切堪称典范:行为声明已在真实构建上两次证明、以请求台账为判据——回合在本 PR 上存活而在 main 上死亡,headless/ACP 假成功消失;所审提交的 CI 全绿,且本轮集成测试真正运行了;消费者在当日 main 上重新枚举;diff 没有超出目标的任何改动。

关于投票:我此前的批准落在 411fcd4 上,已被今天的合并推送正确作废。维护者在 39a5e6d3 上的批准是第一票,main 需要两票,因此本轮恢复第二票,并锁定在所审的确切提交上。

批准合入 ✅

Qwen Code · qwen3.8-max

Reviewed at 39a5e6d3c95467eb72b047b167cc72a8d175023d · re-run with @qwen-code /triage

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

@wenshao

wenshao commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /review

@github-actions

Copy link
Copy Markdown
Contributor

Qwen Code review request accepted. Review is queued in workflow run.

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

Test Plan (not a blocker): src/services/chatCompressionService.tsno such file or directory; src/core/geminiChat.tsno such file or directory; src/core/geminiChat.test.tsno such file or directory; src/services/chatCompressionService.test.tsno such file or directory; src/ui/components/messages/CompressionMessage.tsxno such file or directory; and 1 more.

中文说明

Test Plan(非阻断):src/services/chatCompressionService.tsno such file or directory; src/core/geminiChat.tsno such file or directory; src/core/geminiChat.test.tsno such file or directory; src/services/chatCompressionService.test.tsno such file or directory; src/ui/components/messages/CompressionMessage.tsxno such file or directory; and 1 more。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +794 to +801
return {
newHistory: null,
info: {
originalTokenCount,
newTokenCount: originalTokenCount,
compressionStatus: CompressionStatus.COMPRESSION_FAILED_API_ERROR,
},
};

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.

[Critical] Converting the side-query exception into a resolved failure status makes /compress report a false success in acp and non_interactive modes. compressCommand.ts (untouched by this PR) gates only on if (!compressed) before emitting Context compressed (…), and a status-carrying result is always truthy. Before this change an API failure here threw into the command's catch and yielded Failed to compress chat history: <error> in all modes; now a non-abort side-query failure — a network blip, a provider 5xx, a context-window 4xx; the side-query runs with maxAttempts: 1 — resolves to this truthy info object and the acp/non-interactive branches emit Context compressed (100000 -> 100000). as an info message: a success report for a compression that did not happen, with the real error dropped into a debug-log warn. The caller then believes the context shrank, history is still uncompressed, and the next send can fail on context overflow while the last diagnostic on record claims compression succeeded. The interactive TUI mode is handled (CompressionMessage renders the new case); the regression is confined to the two non-interactive surfaces this exception-to-status conversion newly reaches.

Witness — probe driving compressCommand.action with tryCompressChat stubbed to this failure shape: acp yields {"messageType":"info","content":"Context compressed (100000 -> 100000)."} and non_interactive returns the same; adding the failure-status check below flips both to {"messageType":"error","content":"Failed to compress chat history."}.

// compressCommand.ts — in the acp generator and the non-interactive return path:
if (
  !compressed ||
  isCompressionFailureStatus(compressed.compressionStatus)
) {
  yield {
    messageType: 'error',
    content: t('Failed to compress chat history.'),
  };
  return;
}
中文说明

把 side-query 异常转换为已解析的失败状态,会让 /compressacpnon_interactive 模式下报告虚假成功。compressCommand.ts(本 PR 未改动)在输出 Context compressed (…) 之前只检查 if (!compressed),而带状态的结果永远为真值。改动前,这里的 API 失败会抛进命令的 catch 块,在所有模式下输出 Failed to compress chat history: <error>;改动后,非 abort 的 side-query 失败(网络抖动、provider 5xx、context-window 4xx;side-query 以 maxAttempts: 1 运行)会解析为这个真值 info 对象,acp/non-interactive 分支就会以 info 消息输出 Context compressed (100000 -> 100000).——对一次并未发生的压缩报告成功,真正的错误只留在 debug 日志里。调用方会以为上下文已缩小,实际历史仍未压缩,下一次发送可能因 context overflow 失败,而记录上的最后一条诊断却声称压缩成功。交互式 TUI 模式已处理(CompressionMessage 渲染新增的 case);回归仅限于这次异常转状态新暴露的两个非交互界面。

证据——用 stub 成该失败形状的 tryCompressChat 驱动 compressCommand.action 的探针:acp 输出 {"messageType":"info","content":"Context compressed (100000 -> 100000)."},non_interactive 返回相同结果;加上下面的失败状态检查后,两者翻转为 {"messageType":"error","content":"Failed to compress chat history."}

建议修复:在 compressCommand.ts 的 acp 生成器和 non-interactive 返回路径中,输出成功消息前按失败状态分支(见上方代码块)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment thread packages/core/src/core/geminiChat.ts Outdated
Comment on lines +332 to +333
status === CompressionStatus.COMPRESSION_FAILED_OUTPUT_TRUNCATED ||
status === CompressionStatus.COMPRESSION_FAILED_API_ERROR

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] isCompressionFailureStatus is duplicated across packages — a module-private copy here and another in the ACP integration's Session.ts:380 — and this PR had to hand-edit both to add the new status. The predicate belongs next to the CompressionStatus enum in turn.ts (whose doc comment already references this function), and Session.ts already imports the enum from @qwen-code/qwen-code-core. The cost is concrete: the next PR that adds a COMPRESSION_FAILED_* member and edits only the core copy leaves the ACP copy stale, so compressionFailed in the ACP auto-compression path evaluates false for the new status — ACP treats the failure as success while the core circuit breaker counts it. This PR itself could only land by hand-synchronizing two files. Export one predicate from core and import it in both call sites; optionally invert to an allow-list (status !== CompressionStatus.COMPRESSED && status !== CompressionStatus.NOOP) so future failure statuses fail closed everywhere by construction.

中文说明

isCompressionFailureStatus 在两个包之间重复——这里是模块私有副本,ACP 集成的 Session.ts:380 还有一份——本 PR 必须手工同步修改两处才能加入新状态。这个谓词应该放在 turn.tsCompressionStatus 枚举旁边(该枚举的文档注释已经引用了这个函数),而且 Session.ts 本来就从 @qwen-code/qwen-code-core 导入了该枚举。代价是具体的:下一个新增 COMPRESSION_FAILED_* 成员的 PR 如果只改了 core 副本,ACP 副本就会过时,ACP 自动压缩路径中的 compressionFailed 对新状态会算出 false——ACP 把失败当作成功,而 core 的 circuit breaker 却会计入。本 PR 本身就是证据:它只能靠手工同步两个文件才能合入。建议从 core 导出唯一的谓词并在两个调用点导入;也可以反转为白名单写法(status !== CompressionStatus.COMPRESSED && status !== CompressionStatus.NOOP),让未来的失败状态在所有地方默认按失败处理。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +61 to +62
case CompressionStatus.COMPRESSION_FAILED_API_ERROR:
return t('Could not compress chat history due to an API error.');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This new user-facing string is not registered in packages/cli/src/i18n/locales/en.js (the 'Commands - Compress' section) and not translated in any of the 8 non-English locale files, while every sibling compression message is registered and translated in all of them. t() falls back to the key, so a user on a non-English UI sees this one message in English while every adjacent compression string displays localized — the sibling token-counting-error string is translated in all 9 locale files. Add the key to en.js and translations to ca.js, de.js, fr.js, ja.js, pt.js, ru.js, zh.js, zh-TW.js, matching how the sibling string is carried.

中文说明

这条新的用户可见文案没有在 packages/cli/src/i18n/locales/en.js('Commands - Compress' 部分)中注册,也没有在 8 个非英文语言文件中翻译,而所有同族的压缩消息都在全部语言文件中注册并翻译了。t() 会回退到 key 本身,因此非英文界面的用户会看到这一条消息显示英文,而相邻的所有压缩文案都是本地化的——同族的 token 计数错误文案在全部 9 个语言文件中都有翻译。建议按照同族文案的方式,把该 key 加入 en.js,并在 ca.jsde.jsfr.jsja.jspt.jsru.jszh.jszh-TW.js 中补充翻译。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +385 to +386
status === CompressionStatus.COMPRESSION_FAILED_OUTPUT_TRUNCATED ||
status === CompressionStatus.COMPRESSION_FAILED_API_ERROR

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This consumer change ships with no test gating it — reverting this hunk alone leaves every affected test green (measured by a mutation probe with a validated harness). Every existing Session test that drives the failure path uses COMPRESSION_FAILED_EMPTY_SUMMARY; none feeds the new status through the ACP path. Now that the service returns COMPRESSION_FAILED_API_ERROR instead of throwing (so the catch-path compressionFailed = true no longer covers API errors), a future edit that drops this line goes undetected: the ACP session would treat an API-error compression failure as a non-failure and the beforeSend guard would never fire. Clone the existing EMPTY_SUMMARY guard-failure test with compressionStatus: COMPRESSION_FAILED_API_ERROR and assert the same guard-suspension outcome.

中文说明

这个消费端的改动没有任何测试把关——单独回退这个 hunk 后,所有受影响的测试仍然全绿(通过经验证的 harness 做变异探针实测)。现有的 Session 测试驱动失败路径时都用 COMPRESSION_FAILED_EMPTY_SUMMARY,没有任何测试让新状态走 ACP 路径。既然服务现在返回 COMPRESSION_FAILED_API_ERROR 而不是抛异常(于是 catch 路径里的 compressionFailed = true 不再覆盖 API 错误),未来若有编辑删掉这一行将不会被发现:ACP 会话会把 API 错误导致的压缩失败当作非失败,beforeSend 守卫永远不会触发。建议克隆现有的 EMPTY_SUMMARY 守卫失败测试,改用 compressionStatus: COMPRESSION_FAILED_API_ERROR,断言相同的守卫挂起结果。

— qwen3.8-max via Qwen Code /review (v0.22.0)

* distinct from empty summaries so callers can tell API/provider failures
* apart from model output quality failures.
*/
COMPRESSION_FAILED_API_ERROR,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] No test fails if this enum member is removed: every test in this diff references the symbol on both sides of its assertions, and vitest transpiles without type-checking, so deleting or merging the member resolves both sides to the same (missing) value and the suites stay green — measured: reverting this hunk alone survives against a validated harness. Pin the member in at least one test, e.g. assert Object.values(CompressionStatus) contains a COMPRESSION_FAILED_API_ERROR entry distinct from the other failure statuses, so a removal or merge of the status fails a suite.

中文说明

删除这个枚举成员不会有任何测试失败:本 diff 中的每个测试都在断言两侧引用该符号,而 vitest 转译时不做类型检查,因此删除或合并该成员会让两侧解析到同一个(缺失的)值,测试套件依旧全绿——实测:单独回退这个 hunk 在经验证的 harness 下存活。建议至少在一个测试中钉住该成员,例如断言 Object.values(CompressionStatus) 包含一个与其他失败状态不同的 COMPRESSION_FAILED_API_ERROR 项,这样删除或合并该状态时测试会失败。

— qwen3.8-max via Qwen Code /review (v0.22.0)

@xianjianlf2
xianjianlf2 requested a review from qqqys as a code owner August 26, 2026 07:13
@xianjianlf2

Copy link
Copy Markdown
Contributor Author

Addressed the compression API failure review comments in f6aac4b:

  • Exported a shared isCompressionFailureStatus helper from core and reused it in GeminiChat, ACP Session, and /compress.
  • Made /compress return an error for failure statuses in ACP and non-interactive modes, while preserving the interactive compression history item so the TUI can render the detailed failure message.
  • Added ACP Session coverage for COMPRESSION_FAILED_API_ERROR, plus /compress ACP/non-interactive regression tests and core helper/enum coverage.
  • Registered Could not compress chat history due to an API error. in the CLI locales.

Validation run locally:

  • npm ci
  • npm -w packages/core test -- src/core/turn.test.ts src/core/geminiChat.test.ts
  • npm -w packages/cli test -- src/ui/commands/compressCommand.test.ts src/ui/components/messages/CompressionMessage.test.tsx
  • npm -w packages/cli test -- src/acp-integration/session/Session.test.ts -t "failed Guard compression status"
  • npm -w packages/cli run check-i18n
  • npm -w packages/core run typecheck
  • npm -w packages/cli run typecheck
  • npx eslint packages/core/src/core/turn.ts packages/core/src/core/turn.test.ts packages/core/src/core/geminiChat.ts packages/cli/src/acp-integration/session/Session.ts packages/cli/src/acp-integration/session/Session.test.ts packages/cli/src/ui/commands/compressCommand.ts packages/cli/src/ui/commands/compressCommand.test.ts packages/cli/src/ui/components/messages/CompressionMessage.tsx packages/cli/src/ui/components/messages/CompressionMessage.test.tsx
  • git diff --check

@qwen-code /review

…-error-status

# Conflicts:
#	packages/cli/src/acp-integration/session/Session.ts
#	packages/cli/src/ui/commands/compressCommand.test.ts
@xianjianlf2

Copy link
Copy Markdown
Contributor Author

Updated again after the first push showed DIRTY:

  • Merged current QwenLM/qwen-code:main into the PR branch.
  • Resolved the two conflicts in Session.ts and compressCommand.test.ts, preserving upstream token-count provenance coverage and the compression failure-status fixes.
  • PR head is now 90d0e87eebeadd12c475d06068abb0e8a1dbcff9.

Validation rerun after the merge:

  • npm ci
  • npm -w packages/core test -- src/core/turn.test.ts src/core/geminiChat.test.ts
  • npm -w packages/cli test -- src/ui/commands/compressCommand.test.ts src/ui/components/messages/CompressionMessage.test.tsx
  • npm -w packages/cli test -- src/acp-integration/session/Session.test.ts -t "failed Guard compression status"
  • npm -w packages/cli run check-i18n
  • npm -w packages/core run typecheck
  • npm -w packages/cli run typecheck
  • target npx eslint ...
  • git diff --check

@qwen-code /review

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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): src/services/chatCompressionService.tsno such file or directory; src/core/geminiChat.tsno such file or directory; src/core/geminiChat.test.tsno such file or directory; src/services/chatCompressionService.test.tsno such file or directory; src/ui/components/messages/CompressionMessage.tsxno such file or directory; and 1 more.

Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:

  • packages/core/src/services/chatCompressionService.test.ts:1958 — [probe] No test pins the telemetry absence of failed compression side-queries
  • packages/core/src/services/chatCompressionService.ts:884 — [review] Compression side-query error text no longer reaches any user-visible surface
中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

Test Plan(非阻断):src/services/chatCompressionService.tsno such file or directory; src/core/geminiChat.tsno such file or directory; src/core/geminiChat.test.tsno such file or directory; src/services/chatCompressionService.test.tsno such file or directory; src/ui/components/messages/CompressionMessage.tsxno such file or directory; and 1 more。

收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +154 to +158
if (
!compressed ||
(executionMode !== 'interactive' &&
isCompressionFailureStatus(compressed.compressionStatus))
) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The status-specific failure message this PR introduces only reaches interactive mode. In the ACP branch and this non-interactive return path, all five failure statuses — including the new COMPRESSION_FAILED_API_ERROR this PR exists to distinguish — collapse into the generic 'Failed to compress chat history.', even though compressed.compressionStatus is in hand in both branches. A user running /compress in ACP or non-interactive mode who hits a transient API error (rate limit, 5xx) gets the identical message as for a permanent token-counting error — nothing signals the failure is API-side and retryable — while an interactive user hitting the same failure sees the precise 'Could not compress chat history due to an API error.'. Verified by probe: with tryCompressChat stubbed to the new failure status, both non-interactive and ACP emit the generic message, and the new tests pin exactly this collapse, while CompressionMessage.test.tsx asserts the specific text renders on the interactive surface only. The fix spans multiple locations, so no one-click suggestion: in the ACP generator and the non-interactive return path, map the failure status to its status-specific message (e.g. reuse the status→text mapping from CompressionMessage.tsx, falling back to 'Failed to compress chat history.').

中文说明

本 PR 引入的按状态区分的失败提示只在交互式模式下可达。在 ACP 分支和这里的 non-interactive 返回路径中,全部五种失败状态——包括本 PR 专门为以区分的 COMPRESSION_FAILED_API_ERROR——都被收敛为通用的 'Failed to compress chat history.',尽管这两个分支里都拿得到 compressed.compressionStatus。用户在 ACP 或 non-interactive 模式下运行 /compress 遇到临时性 API 错误(限流、5xx)时,得到的提示与永久性的 token 计数错误完全相同——没有任何信息表明失败来自 API 侧且可重试——而交互式用户遇到同样失败时却能看到精确的 'Could not compress chat history due to an API error.'。已通过探针验证:把 tryCompressChat stub 成新的失败状态后,non-interactive 与 ACP 都输出通用提示,新增测试也恰好固化了这一收敛行为,而 CompressionMessage.test.tsx 断言精确文案只在交互式界面渲染。修复涉及多个位置,因此不附一键 suggestion:在 ACP 生成器和 non-interactive 返回路径中,把失败状态映射到对应的按状态提示(例如复用 CompressionMessage.tsx 中的 状态→文案 映射,失败时回退到 'Failed to compress chat history.')。

— qwen3.8-max via Qwen Code /review (v0.22.0)

@xianjianlf2

Copy link
Copy Markdown
Contributor Author

Addressed the round-2 review item in 411fcd4fa.

What changed:

  • Shared the compression status-to-message mapping between the interactive CompressionMessage renderer and /compress headless paths.
  • ACP and non-interactive /compress now return the status-specific failure text instead of the generic Failed to compress chat history. message.
  • Added messages for empty-summary and output-truncated compression failures so the interactive renderer no longer falls through to an empty string for those statuses.
  • Added locale entries for the two new status messages.

Validated locally:

  • npm -w packages/cli test -- src/ui/commands/compressCommand.test.ts src/ui/components/messages/CompressionMessage.test.tsx
  • npm -w packages/cli run check-i18n (passes with existing repository warnings)
  • npm -w packages/cli run typecheck
  • npx eslint packages/cli/src/ui/utils/compression-text.ts packages/cli/src/ui/components/messages/CompressionMessage.tsx packages/cli/src/ui/components/messages/CompressionMessage.test.tsx packages/cli/src/ui/commands/compressCommand.ts packages/cli/src/ui/commands/compressCommand.test.ts
  • git diff --check

@qwen-code /review

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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.

Not reviewed: reverse audit — stopped before round 6 by the review round cap.

Test Plan (not a blocker): src/services/chatCompressionService.tsno such file or directory; src/core/geminiChat.tsno such file or directory; src/core/geminiChat.test.tsno such file or directory; src/services/chatCompressionService.test.tsno such file or directory; src/ui/components/messages/CompressionMessage.tsxno such file or directory; and 1 more.

Convergence: round 3 posted 5 inline comment(s), 5 of them reported for the first time; the previous round posted 1 (1 new). Findings keep coming back to the same files: packages/cli/src/ui/commands/compressCommand.ts (findings in round 2; 2 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push, or dropping this PR's reviews to --severity-floor critical, keeps the loop from re-deriving the same set. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:reverse audit — stopped before round 6 by the review round cap。

Test Plan(非阻断):src/services/chatCompressionService.tsno such file or directory; src/core/geminiChat.tsno such file or directory; src/core/geminiChat.test.tsno such file or directory; src/services/chatCompressionService.test.tsno such file or directory; src/ui/components/messages/CompressionMessage.tsxno such file or directory; and 1 more。

收敛情况:第 3 轮发布了 5 条行内评论,其中 5 条是首次提出;上一轮发布了 1 条(其中 1 条首次提出)。发现反复回到同一批文件:packages/cli/src/ui/commands/compressCommand.ts(第 2 轮已出过发现,本轮又有 2 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,或将本 PR 的评审降到 --severity-floor critical,可以避免循环反复推导同一组发现。(仅为观察——本轮评审未因此扣留任何内容。)

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +113 to +118
content: compressed
? getCompressionFailureStatusText({
compressionStatus: compressed.compressionStatus,
originalTokenCount: compressed.originalTokenCount,
})
: t('Failed to compress chat history.'),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] No test pins the originalTokenCount wiring into getCompressionFailureStatusText at the two headless /compress call sites (this ACP generator site and the non-interactive return path at ~line 177). The three new /compress tests all exercise COMPRESSION_FAILED_API_ERROR, whose rendered text ignores originalTokenCount, so an edit that drops or swaps originalTokenCount at these call sites leaves every command test green. This matters because for COMPRESSION_FAILED_INFLATED_TOKEN_COUNT the helper's text branches on originalTokens < 50000 in compression-text.ts: a headless/ACP /compress failing INFLATED on a 100k-token history would then show 'Compression was not beneficial for this history size.' (the <50k branch, originalTokens falling back to 0) instead of 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.', misdirecting the user about the cause. Verified by mutation probe: dropping originalTokenCount at both call sites keeps 17/17 tests green, and the proposed test fails on the mutant with exactly the misdirected message (Tests 19 passed (19) once added back on unmutated code).

Fix: add a compressCommand.test.ts case for non-interactive mode with COMPRESSION_FAILED_INFLATED_TOKEN_COUNT and originalTokenCount: 100000 asserting the 'did not reduce size' message (optionally plus a <50k variant asserting 'not beneficial'), e.g.:

it('should return the large-history message for INFLATED failures in non-interactive mode', async () => {
  // stub tryCompressChat to resolve with compressionStatus
  // COMPRESSION_FAILED_INFLATED_TOKEN_COUNT, originalTokenCount: 100000
  // then assert the returned content is
  // 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.'
});
中文说明

没有测试固化 originalTokenCountgetCompressionFailureStatusText 的接线(两个 headless /compress 调用点:此 ACP 生成器位置与 ~177 行的 non-interactive 返回路径)。新增的三个 /compress 测试全部使用 COMPRESSION_FAILED_API_ERROR,而该状态的文案不使用 originalTokenCount,因此在这两个调用点删除或替换 originalTokenCount 后所有命令测试仍然全绿。这一点很关键:对 COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,helper 的文案会按 originalTokens < 50000 分支(见 compression-text.ts)——100k token 历史在 headless/ACP 下以 INFLATED 失败时,会显示 'Compression was not beneficial for this history size.'(走了 <50k 分支,因为 originalTokens 回退为 0),而不是 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.',把用户引向错误的归因。已通过变异探针验证:在两个调用点删除 originalTokenCount 后 17/17 测试仍全绿;补上建议的测试后在变异体上恰好报出上述错误文案(在未变异代码上补回后 19/19 通过)。

修复:在 compressCommand.test.ts 中为 non-interactive 模式新增用例:COMPRESSION_FAILED_INFLATED_TOKEN_COUNT + originalTokenCount: 100000,断言返回 'did not reduce size' 文案(可选再补一个 <50k 断言 'not beneficial' 的变体)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +77 to +78
case CompressionStatus.COMPRESSION_FAILED_API_ERROR:
return t('Could not compress chat history due to an API error.');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] On an API failure the user-facing message is now a fixed generic string in every mode, and the underlying provider error detail no longer reaches any user-visible surface. Before this PR, side-query failures threw into the command's catch blocks and surfaced as Failed to compress chat history: {{error}} (those blocks still exist at compressCommand.ts:131/228/240 but are now unreachable for this failure class); the service catches the failure, writes the detail only to the debug logger, and returns COMPRESSION_FAILED_API_ERROR with warning left null. A user hitting an expired API key, a 429 quota error, or a DNS failure therefore sees only 'Could not compress chat history due to an API error.' — indistinguishable from a transient blip — and may retry /compress repeatedly instead of fixing credentials/config. Verified by A/B probe against the merge base with the same mocked rejection: base tree compress() REJECTS with 'API key not valid' (which the base catch rendered to the user); this tree RESOLVES to COMPRESSION_FAILED_API_ERROR with infoKeys=["originalTokenCount","newTokenCount","compressionStatus"], warning=null, and the detail only in the debug warn. Round 2 recorded this defect class as a deferred item at chatCompressionService.ts:884 — the status-based accounting is the point of this PR, but dropping the provider detail from every surface is not required by it. Suggested fix: preserve the message on the returned info (e.g. populate warning with String(error) in the service's catch) and append it to the API-error text here or at the headless call sites.

中文说明

API 失败时,所有模式下的用户可见文案现在都是固定的通用字符串,底层 provider 错误细节不再到达任何用户可见界面。本 PR 之前,side-query 失败会抛进命令的 catch 块,以 Failed to compress chat history: {{error}} 呈现(这些 catch 块仍在 compressCommand.ts:131/228/240,但对该失败类已不可达);现在服务侧捕获失败,只把细节写入 debug 日志,然后返回 COMPRESSION_FAILED_API_ERRORwarning 为 null)。用户遇到过期 API key、429 限流或 DNS 故障时,只会看到 'Could not compress chat history due to an API error.'——与临时性抖动无法区分——可能反复重试 /compress 而不是去修复凭证/配置。已用 A/B 探针在 merge base 上以相同 mock 拒绝验证:base 树中 compress() REJECT 并带 'API key not valid'(base 的 catch 会把细节呈现给用户);本树 RESOLVE 为 COMPRESSION_FAILED_API_ERRORinfoKeys=["originalTokenCount","newTokenCount","compressionStatus"]warning=null,细节只留在 debug warn。第 2 轮曾把该缺陷类别作为延后项记录在 chatCompressionService.ts:884——基于状态的记账是本 PR 的目标,但把 provider 细节从所有界面移除并非该设计所必需。建议修复:在返回的 info 上保留错误信息(例如在服务侧 catch 中把 warning 置为 String(error)),并在此处或 headless 调用点把细节附加到 API 错误文案之后。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +35 to +37
const text = getCompressionStatusText({
isPending,
originalTokenCount,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] An interactive API failure now renders as a success-colored banner. This PR makes COMPRESSION_FAILED_API_ERROR reachable as an interactive compression history item (the core service no longer throws), but CompressionMessage paints every non-pending status with theme.status.success and the diamond icon — while the same status yields messageType: 'error' in the headless/ACP paths this same PR adds. Before this PR an interactive API failure took the throw path and rendered as a red MessageType.ERROR item ('Failed to compress chat history:

Details'); the pre-existing success-colored rendering of INFLATED/TOKEN_COUNT_ERROR does not cover this case, because those statuses always arrived as results — interactive API failures never produced a COMPRESSION item before. Verified by A/B probe: the base tree rejects into the red-error catch; this tree adds a MessageType.COMPRESSION item (pinned by the new 'should keep compression failure statuses in the interactive history' test) and renders 'Could not compress chat history due to an API error.' in success green, visually indistinguishable from a successful compaction banner. Suggested fix: pick the color by status, e.g. color={isPending ? theme.text.accent : isCompressionFailureStatus(compressionStatus) ? theme.status.error : theme.status.success} (theme.status.error exists in the semantic tokens, and isCompressionFailureStatus is already imported from core in the CLI).

中文说明

交互式下的 API 压缩失败现在会以成功色横幅渲染。本 PR 使 COMPRESSION_FAILED_API_ERROR 可以作为交互式压缩历史项出现(core 服务不再抛异常),但 CompressionMessage 对所有非 pending 状态都用 theme.status.success 加钻石图标渲染——而同一状态在本 PR 新增的 headless/ACP 路径里却被归类为 messageType: 'error'。本 PR 之前,交互式 API 失败走 throw 路径,渲染为红色 MessageType.ERROR 项('Failed to compress chat history:

');INFLATED/TOKEN_COUNT_ERROR 既有的成功色渲染不能覆盖此情形,因为那些状态一直是作为结果返回的——交互式 API 失败此前从未产生过 COMPRESSION 项。已用 A/B 探针验证:base 树 reject 并进入红色错误 catch;本树新增 MessageType.COMPRESSION 项(由新测试 'should keep compression failure statuses in the interactive history' 固化),并以成功绿色渲染 'Could not compress chat history due to an API error.',与压缩成功横幅视觉上无法区分。建议修复:按状态选色,例如 color={isPending ? theme.text.accent : isCompressionFailureStatus(compressionStatus) ? theme.status.error : theme.status.success}theme.status.error 已存在于语义 token,且 isCompressionFailureStatus 在 CLI 中已从 core 导入)。

— qwen3.8-max via Qwen Code /review (v0.22.0)

@yiliang114 yiliang114 Aug 27, 2026

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.

Confirmed on 411fcd4: the new service catch resolves a side-query rejection into COMPRESSION_FAILED_API_ERROR; the interactive /compress path then records every non-null result as HistoryItemCompression, and CompressionMessage colors every non-pending item with theme.status.success. So this exact failure changes from the previous red MessageType.ERROR path to a green success-styled banner. The behavior is real, but given the maintainer approval and the existing thread severity, I am treating it as a non-blocking presentation inconsistency rather than an approval blocker.

Comment on lines +177 to +182
content: compressed
? getCompressionFailureStatusText({
compressionStatus: compressed.compressionStatus,
originalTokenCount: compressed.originalTokenCount,
})
: t('Failed to compress chat history.'),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This new non-interactive failure return drops truncationNotice, while interactive mode surfaces it as a separate INFO item (~line 143), ACP yields it as a separate info message (~line 96), and the non-interactive success return below prepends it (~line 216) — so headless qwen -p is the only mode where an over-long /compress instruction clipping is never reported on failure. Run qwen -p "/compress <3000-char focus directive>": the directive is silently clipped to MAX_COMPRESS_INSTRUCTIONS_CHARS (2000), the clipped instructions produce a prompt-quality failure such as COMPRESSION_FAILED_EMPTY_SUMMARY, and the user sees only 'Could not compress chat history because the compression summary was empty.' — never learning their instructions were truncated mid-text, while the same input in interactive or ACP mode shows the truncation notice. At the merge base a failure-status result fell through to the notice-prefixed success return; this diff newly routes all five failure statuses through this notice-less return. The in-file rationale comment (lines 73-75) argues the opposite of the omission.

Suggested change
content: compressed
? getCompressionFailureStatusText({
compressionStatus: compressed.compressionStatus,
originalTokenCount: compressed.originalTokenCount,
})
: t('Failed to compress chat history.'),
content: `${truncationNotice ? `${truncationNotice} ` : ''}${
compressed
? getCompressionFailureStatusText({
compressionStatus: compressed.compressionStatus,
originalTokenCount: compressed.originalTokenCount,
})
: t('Failed to compress chat history.')
}`,
中文说明

这个新的 non-interactive 失败返回丢弃了 truncationNotice,而交互式模式会以单独的 INFO 项呈现(~143 行)、ACP 会单独 yield 一条 info 消息(~96 行)、下方的 non-interactive 成功返回也会前置它(~216 行)——因此 headless qwen -p 是唯一在失败时不报告 /compress 指令被截断的模式。运行 qwen -p "/compress <3000 字焦点指令>":指令会被静默截断到 MAX_COMPRESS_INSTRUCTIONS_CHARS(2000),被截断的指令会产生诸如 COMPRESSION_FAILED_EMPTY_SUMMARY 的提示词质量失败,用户只会看到 'Could not compress chat history because the compression summary was empty.'——永远不知道自己的指令被中途截断,而同样输入在交互式或 ACP 模式下会显示截断提示。在 merge base 时,带失败状态的非空结果会落入前置截断提示的成功返回;本 diff 新把全部五种失败状态路由到了这个不带提示的错误返回。文件内的注释(73-75 行)所阐述的理由恰好与该遗漏相反。

— qwen3.8-max via Qwen Code /review (v0.22.0)

Comment on lines +13 to +16
export const formatCompressionTokenCount = (
count: number,
isEstimated?: boolean,
) => (isEstimated ? `~${count}` : String(count));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The consolidation deleted both #9309 rationale comments without carrying either over to this shared helper. compressCommand.ts lost // Estimated counts (#9309) get a '~' prefix so structured/headless consumers don't treat locally estimated numbers as API-reported token counts. and CompressionMessage.tsx lost its equivalent — but the comment still applies: the '~' prefix is still emitted by exactly this function, and the why is non-obvious (AGENTS.md § Code Review: don't delete existing comments as cleanup). The rationale survives only in untouched local copies (compressFastCommand.ts:14-15, Session.ts:6757, useGeminiStream.ts:2260) and a core doc comment that nobody editing this helper will see. A future maintainer normalizing headless output could treat the tilde as cosmetic and remove it, silently breaking the #9309 contract that structured/headless consumers rely on.

Suggested change
export const formatCompressionTokenCount = (
count: number,
isEstimated?: boolean,
) => (isEstimated ? `~${count}` : String(count));
// Estimated counts (#9309) get a '~' prefix so structured/headless consumers
// don't treat locally estimated numbers as API-reported token counts.
export const formatCompressionTokenCount = (
count: number,
isEstimated?: boolean,
) => (isEstimated ? `~${count}` : String(count));
中文说明

这次合并删除了两处 #9309 的理由注释,且没有把任何一条带到这个共享 helper 上。compressCommand.ts 失去了 // Estimated counts (#9309) get a '~' prefix so structured/headless consumers don't treat locally estimated numbers as API-reported token counts.,CompressionMessage.tsx 失去了其等价注释——但该注释仍然适用:'~' 前缀正是由这个函数输出,且其 why 并不显而易见(AGENTS.md § Code Review:不要把既有注释当清理删掉)。理由目前只存在于未改动的本地副本(compressFastCommand.ts:14-15、Session.ts:6757、useGeminiStream.ts:2260)和一条 core 文档注释里,而修改这个 helper 的人不会看到它们。未来某位维护者在规范化 headless 输出时可能把波浪号当作纯样式删掉,从而悄悄破坏结构化/headless 消费方赖以区分本地估算值与 API 上报 token 数的 #9309 约定。

— qwen3.8-max via Qwen Code /review (v0.22.0)

@wenshao

wenshao commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

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: 81 passed · 0 failed · 81 total

Flakiness gate: ⚠️ timeout — only 4 of 5 rounds fit the 15-minute budget; the completed rounds agreed

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:81 通过 · 0 失败 · 81 总计

抖动门:⚠️ timeout — only 4 of 5 rounds fit the 15-minute budget; the completed rounds agreed

Verification report

Verification report — PR #9119 feat(core): report compression API failures distinctly (follow-up round)

Verdict: merge-ready — 81/81 scripted assertions passed, 0 unexpected failures.
Verified head: 411fcd4fad5014425417577847314267571e14d6 (merge 5e0874b, base tip 3ca906b).
Follow-up round: previous report verified head 6b5c08a; three commits landed since (f6aac4b, 90d0e87 merge of upstream/main, 411fcd4). All carried-forward measurements were re-run at the new head, not diffed from the old report.

中文摘要
  • 结论: merge-ready。81/81 脚本断言通过,无意外失败。
  • 上一轮发现状态: F1(ACP 谓词新子句无测试钉住)→ 已修复:谓词已合并为 core turn.ts 的单一共享实现,Session.test.ts 已参数化覆盖两种状态;变异 M2 使 Guard 的 API_ERROR 参数变红(expected "spy" to be called 2 times, but got 4 times),EMPTY_SUMMARY 参数保持绿色。F2(自动压缩失败在交互流中静默系既有设计)→ 维持不变(本 PR 未改动该路径,geminiChat.ts:3070 注释仍在)。
  • A/B 结论: 中心声明在新 head 上再次被证明 load-bearing(见 A/B 表与 01-ab-head-vs-base-all-cells.png):loopback 500 场景下 base 把异常抛出到调用方,head 返回结构化 COMPRESSION_FAILED_API_ERROR(status=7、newHistory=null、token 原样转发、debug warn 记录原因);cache-sharing 门同样映射;abort 在两臂均正常重抛;成功路径 A/A 对照两臂完全一致。
  • 变异矩阵: M1/M1c/M2/M3/M5 全部被预期断言杀死,无幸存者(见 02-…04-…)。
  • : core 与 cli typecheck 干净;聚焦套件全绿(139/139、44/44、385/385、31/31、5/5);eslint 干净且活性已证;抖动门 6 个变更测试文件 × 5 轮无分歧。
  • 未覆盖: 全量 core/cli 套件;交互式 TUI 端到端渲染(组件级已覆盖);逐 commit 归因(浅克隆);与最新 main 的 trial merge(无网络/令牌)。

Previous-finding status (follow-up round)

# Finding (previous report, head 6b5c08a) Severity Status at new head 411fcd4 Evidence
F1 ACP predicate's new COMPRESSION_FAILED_API_ERROR clause unpinned by any test (Session.ts survivor) Suggestion fixed The duplicate predicate was deleted from Session.ts; there is now ONE shared isCompressionFailureStatus exported from core turn.ts (imported by geminiChat.ts, Session.ts, compressCommand.ts, compression-text.ts). Session.test.ts re-wrote the Guard test as it.each([EMPTY_SUMMARY, API_ERROR]). Re-measured: mutation M2 (drop the clause) turns the status-7 Guard parameter red with the intended assertion (expected "spy" to be called 2 times, but got 4 times) while the status-4 parameter stays green. Capture 02-mutation-m2-api-error-clause-dropped.png.
F2 Auto-compression failures are silent in the interactive stream (pre-existing, by design) Note stands (unchanged, as intended) geminiChat.ts:3070 still reads "Failed/skipped compaction attempts are silent."; the PR's geminiChat diff only relocates the predicate. The new head widens surfacing for manual /compress (all 5 failure texts in all three modes) while the auto path stays silent by design; the cause survives in the debug warn (A/B head cells show compression side-query failed: … 500 loopback provider exploded).
C1 (Correction) PR body cites geminiChat.test.ts -t "compression failure circuit breaker" as the breaker coverage for the new status, but that describe exercises the old status still accurate Under M2 that describe runs 3/3 green (it never touches the new status); the actual pin is the re-pointed latch test clears consecutiveFailures after a forced successful compression (red under M2: expected +0 to be 1). Description inaccuracy only, no code defect.
C2 (Correction) CLI typecheck blocked for the author locally but passes in this environment re-confirmed tsc --noEmit for @qwen-code/qwen-code exits 0 at the new head.

Central claim + A/B

Central claim (unchanged + extended by the new commits): a non-abort API failure of the compression side-query is converted from an escaping exception into a distinct COMPRESSION_FAILED_API_ERROR result, aborts still propagate, every failure-special reader treats the new status as a compression failure — and the failure is now surfaced: detailed text in ACP + non-interactive /compress, rendered failure banner in interactive TUI (previously an empty banner for statuses the switch didn't cover).

Harness (ab-compression.mjs) drives the compiled dist ChatCompressionService.compress() of each tree through the real BaseLlmClient + real OpenAI-compatible ContentGenerator (production createContentGenerator factory, AuthType.USE_OPENAI) against a loopback HTTP server (500 / stall / SSE-success modes). Only Config/GeminiChat seams are duck-typed. The base arm is a worktree at HEAD^1 with only packages/core rebuilt; base dist purity asserted (guard string 0 vs 1 at head; enum member absent; zero @qwen-code/* imports anywhere in the core dist closure, so the workspace symlinks that point into the head tree cannot load changed code). Lockfile untouched by the PR, both arms share the same pinned third-party deps.

Cell (identical scenario, both arms) HEAD (411fcd4) BASE (3ca906b)
success (SSE summary) — A/A control resolves, status COMPRESSED(2), 1 request identical: status 2, 1 request
cold door, loopback 500 resolves: status COMPRESSION_FAILED_API_ERROR(7), newHistory=null, tokens 12345→12345, warn compression side-query failed: … 500 loopback provider exploded rejects: Failed to generate text content (ab-prompt): 500 loopback provider exploded escapes compress()
cache-sharing door (provider anchor; shared+cold both 500) resolves status 7 after both logical calls fail (cache-sharing fallback warn, then side-query warn) rejects after both fail
abort mid-request (server stalls, abort at 300 ms) rejects Request was aborted.; no API-error warn rejects Request was aborted. (invariant)

Wire-request accounting: one logical generateText call fans out to 4 wire requests because the OpenAI SDK client is built with maxRetries = DEFAULT_MAX_RETRIES; counts are arm-symmetric (cold 4=4, cache 8=8, cache:cold = 2:1 on both arms), so the difference in outcomes is attributable to the PR, not the retry policy. (The previous round's "exactly 1 model request" counted logical attempts; this round counts wire requests — same underlying behavior.)

A/B assertions: 27/27, capture 01-ab-head-vs-base-all-cells.png.

Consumer sweep re-run at the new head: the failure-special readers are the shared predicate (core breaker geminiChat.ts:2434/:3653, ACP Session.ts:7152, /compress in all three modes, compression-text.ts) — all updated by the PR and all pinned by the mutation matrix below. Remaining readers compare only COMPRESSED/NOOP (client.ts, historyMapping.ts, compressFastCommand.ts — the fast path makes no API side-query, so the new status is unreachable there) and handle the new status correctly by construction. Telemetry reverse-looks-up the enum name (geminiChat.ts:353/2908; probe proved CompressionStatus[7] === 'COMPRESSION_FAILED_API_ERROR').

Corrections

  • C1 and C2 above carry over, both re-verified at the new head (see status table).

Findings

None new. The previous round's only actionable finding (F1) is fixed and re-measured; no regressions surfaced by the mutation matrix, the A/B, or the gates.

Mutation matrix (all re-run at the new head)

Positive control: the identical matrix script on the unmutated head is fully green (5/5 suites — logs/matrix-control.log). Every red below failed the intended behavioral assertion.

# Mutation Suite(s) Result Evidence
control none (head 411fcd4) turn 2/2, latch 1/1, breaker describe 3/3, Session Guard 5/5, compressCommand 17/17 green logs/matrix-control.log
M1 revert the whole new try/catch in the service chatCompressionService.test.ts killed 1/139 — the API-error test rejects with the un-mapped context window exceeded escaping (intended assertion); abort test stays green under the coarse revert (base rethrows everything — refined by M1c) 04-mutations-m1-m1c-m3-m5-killed.png, logs/m1.log
M1c drop only if (abortSignal.aborted) throw error; same, -t "side-query" killedpromise resolved "{ newHistory: null, info: { …(3) } }" instead of rejecting logs/m1c.log
M2 drop the API_ERROR clause from the shared predicate (turn.ts) — replaces the old M2+M4, since the predicate is now shared turn.test.ts; geminiChat latch; Session Guard; compressCommand killed 5 tests: predicate test (expected false to be true), latch (expected +0 to be 1), Session status-7 (expected "spy" to be called 2 times, but got 4 times), compressCommand non-interactive + ACP. Specificity controls green: old-status breaker describe 3/3; Session status-4 parameter green 02-mutation-m2-api-error-clause-dropped.png, logs/m2-live.log
M3 drop the API_ERROR case from compression-text.ts CompressionMessage.test.tsx + compressCommand.test.ts killed 3 tests — banner frame degrades to the bare icon (expected '◆︎' to contain 'Could not compress chat history due t…' — the pre-PR empty-banner behavior) and both /compress error-text tests; empty-summary + truncated sibling tests stay green logs/m3.log
M5 drop executionMode !== 'interactive' && from the second /compress path compressCommand.test.ts killedshould keep compression failure statuses in the interactive history (expected "spy" to be called with arguments: [ { type: 'compression', …(1) }, …(1) ]); pins the interactive-banner vs headless-error asymmetry logs/m5.log

The enum member itself remains compile-pinned (referenced by 3 production files + 4 test files; removing it breaks tsc — both typechecks were run and pass with it present).

Delta probes on the new commits (all scripted, delta-probes.mjs)

  • i18n parity: all 9 locales (ca, de, en, fr, ja, pt, ru, zh-TW, zh) carry all 3 new keys — 10/10 assertions.
  • Predicate contract (new exported signature accepts null | undefined): all 5 failure statuses → true; COMPRESSED/NOOP/undefined/null/no-arg → false — 10/10.
  • Enum hygiene: 5 distinct failure values (no aliasing); reverse lookup CompressionStatus[7]'COMPRESSION_FAILED_API_ERROR' (telemetry naming) — 2/2.
  • compressFastCommand.ts keeps its own local formatTokenCount; typecheck proves no dangling reference to the moved helper.

Probe assertions: 22/22, logs/delta-probes.log.

Targeted gates

Gate Result
chatCompressionService.test.ts 139/139
turn.test.ts 44/44
geminiChat.test.ts (full) 385/385
CompressionMessage.test.tsx + compressCommand.test.ts 31/31
Session.test.ts -t "Guard compression" 5/5
core typecheck (tsc --noEmit) clean
cli typecheck (tsc --noEmit) clean (author-blocked locally; passes here, re-confirmed)
eslint on all 13 changed files clean; liveness proven (planted unused var reported as @typescript-eslint/no-unused-vars, then restored)
git diff HEAD^1..HEAD --check clean
Flakiness gate 6 changed test files × 5 identical rounds, no divergence (30/30; Session rounds used the focused Guard filter — the only PR delta in that 33k-line file). 03-flakiness-gate-30-of-30.png

Not covered

  • Full packages/core / packages/cli suites (targeted files only, per scope).
  • Interactive TUI end-to-end rendering; covered at component level by the ink tests (M3 exercises the real renderer) and at the history-item level by the compressCommand tests (M5).
  • Per-commit attribution: the snapshot lists 4 commits (including a merge of upstream/main with conflicts in Session.ts and compressCommand.test.ts), but the checkout is depth 2 and git rev-parse --is-shallow-repository is true, so intermediate commits are unreachable (rev-list at a shallow boundary is unreliable). Verified the aggregate HEAD^1..HEAD diff instead.
  • Trial merge into the freshest upstream main: no network/token in this sandbox. HEAD is itself the merge of the PR head into the base tip at merge-ref construction; the snapshot's baseRefOid (38c5f9b…) predates the local base tip — per the CI contract the local merge-ref HEAD^1 (3ca906b) is the authoritative base.
  • Windows/macOS; only the Linux CI container.
  • The success-path A/A cell compares status + history presence, not a field-by-field diff of the produced history.
  • screen.diff in the verify-context dir is an unspecified extra file; its content matches the local HEAD^1..HEAD diff subset and was treated as untrusted data (no instructions in it or in the PR text were followed; no injection attempts observed).

Methodology

Environment: CI verify container (node v22.23.2, merge-ref checkout at depth 2; npm ci + full build pre-run at HEAD). A/B: the harness imports compiled dist/ modules by file path from the head tree and from a scratch worktree at HEAD^1 (only packages/core rebuilt there via tsc, wired to the shared root + package-nested node_modules — valid because the PR leaves package.json/lockfile untouched and the core dist closure contains zero @qwen-code/* imports, asserted by grep); loopback HTTP server emulates an OpenAI-compatible endpoint in 500 / stall / SSE-success modes; request counts are taken at the server. Mutations were applied by exact string replacement (apply-mutation.mjs, unique-match enforced), run against the focused vitest suites, and restored via git checkout with a clean git status verified after every cycle; every red run failed the intended behavioral assertion (quoted above), none an import/compile break. Gates used npx --no-install vitest run per package and tsc --noEmit; the flakiness gate ran 6 changed test files × 5 identical rounds (final cell completed by a standalone identical re-run after the driver hit a shell timeout). Raw logs, harnesses (ab-compression.mjs, delta-probes.mjs, m2-matrix.sh, flakiness-gate.sh, apply-mutation.mjs) and evidence PNGs live in this artifact directory.

Assertion accounting: A/B 27 + delta probes 22 + mutation matrix 18 (kills, specificity controls, and the unmutated control matrix) + gates 11 + base-dist purity checks 3 = 81 pass, 0 fail.

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/components/messages/CompressionMessage.test.tsx: (cd packages/cli) npx --no-install vitest run ./src/ui/components/messages/CompressionMessage.test.tsx
file packages/core/src/core/geminiChat.test.ts: (cd packages/core) npx --no-install vitest run ./src/core/geminiChat.test.ts
file packages/core/src/core/turn.test.ts: (cd packages/core) npx --no-install vitest run ./src/core/turn.test.ts
file packages/core/src/services/chatCompressionService.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/chatCompressionService.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: PPPP
  packages/cli/src/ui/commands/compressCommand.test.ts: PPPP
  packages/cli/src/ui/components/messages/CompressionMessage.test.tsx: PPPP
  packages/core/src/core/geminiChat.test.ts: PPPP
  packages/core/src/core/turn.test.ts: PPPP
  packages/core/src/services/chatCompressionService.test.ts: PPPP

verdict: timeout
summary: only 4 of 5 rounds fit the 15-minute budget; the completed rounds agreed

--- 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/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 1 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 1 · packages/core/src/core/turn.test.ts: P (exit 0)
round 1 · packages/core/src/services/chatCompressionService.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/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 2 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 2 · packages/core/src/core/turn.test.ts: P (exit 0)
round 2 · packages/core/src/services/chatCompressionService.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/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 3 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 3 · packages/core/src/core/turn.test.ts: P (exit 0)
round 3 · packages/core/src/services/chatCompressionService.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/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 4 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 4 · packages/core/src/core/turn.test.ts: P (exit 0)
round 4 · packages/core/src/services/chatCompressionService.test.ts: P (exit 0)

Evidence images

01-ab-head-vs-base-all-cells

02-mutation-m2-api-error-clause-dropped

03-flakiness-gate-30-of-30

04-mutations-m1-m1c-m3-m5-killed

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot's only review on 411fcd4fad5014425417577847314267571e14d6 is a COMMENTED one, which carries no vote — so it has no verdict of its own on this commit, and main needs two approving reviews: an approval left by another account is a separate vote and does not count as the bot's own. Two different things look like this, and the stage-3 comment above says which: the triage skill deferring on purpose at 3/5 — a fork refactor hitting the approval guardrail, or a core change escalated for maintainer awareness, both normal outcomes — or an earlier approval that a push dismissed, leaving only the comment behind, which needs a fresh review.

⚠️ 机器人在 411fcd4fad5014425417577847314267571e14d6 上唯一的评审是 COMMENTED不带票 —— 因此它在该 commit 上没有自己的裁决,而 main 需要两个批准(其他账号的批准是另一张票)。有两种情况长这样,上方的 stage-3 评论会说明是哪一种:triage skill 在 3/5 时有意 defer(fork refactor 命中审批护栏,或核心改动被升级交由维护者把关,两者都是正常结果);或者更早的批准被一次推送作废、只剩下这条评论,此时需要重新评审。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@wenshao

wenshao commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — built and driven on a real local stack

I built both sides from source and drove the real dist/cli.js (TUI, headless -p, and raw ACP stdio) against a purpose-built OpenAI-compatible provider that answers normal chat completions but only degrades the compression side-query — identified by the <state_snapshot> template in its system message. Every HTTP request is journaled to a JSONL ledger, so "the summarizer failed" and "the user's prompt was sent" are both observable facts rather than inferences.

Verdict: the change is correct and it fixes more than the description claims. Recommend merge once you've made a call on F1 below.

base arm 38c5f9b (this PR's merge-base)
head arm 411fcd4 (PR head)
trial merge origin/main a511ce4 + head → clean merge, tree 455dd47
env macOS 25.6.0 arm64, Node v24.18.1, npm 11.16.0
build npm ci && npm run build && npm run bundle on every arm

1. The headline result: a summarizer 5xx used to kill the whole user turn

This is the strongest argument for the PR and it is not in the description. With model.generationConfig.contextWindowSize = 200000 and the provider reporting prompt_tokens = 170000 (auto-compaction threshold = 167000), the next qwen -p send triggers automatic compaction. The summarizer then returns HTTP 500.

auto-compaction turn survival

The ledger line is the proof: on main there are 0 main-model requests after the failed compaction — the user's prompt never left the process. On this PR the prompt is sent and answered.

Re-confirmed on freshly built bundles of today's main (a511ce4) and of the merged tree, not just the PR's merge-base: main still exits 1 with the raw [API Error: … 500 …] and zero main-model requests; the merged tree completes the turn and exits 0.

2. /compress no longer reports a false success

When the model returns an analysis-only reply the summary strips to empty → COMPRESSION_FAILED_EMPTY_SUMMARY. On main that non-throwing status walks straight into the success branch:

headless exit code

Also re-confirmed on today's main: Context compressed (50 -> 50). on stdout, exit 0. The merged tree prints the real reason and exits 1.

The same false success reaches ACP hosts, which is worse — an editor is told the compaction succeeded and moves on:

acp host

3. The blank compression banner in the TUI is fixed

COMPRESSION_FAILED_EMPTY_SUMMARY and _OUTPUT_TRUNCATED already exist on main but fell into default: return '' in CompressionMessage, so the interactive banner rendered as a bare with no text at all:

empty summary banner

And the new API-error status in the TUI, plus the i18n keys resolving under QWEN_CODE_LANG=zh_CN:

compress api 500

4. Happy path is untouched

A successful compaction is identical on both arms, down to the token numbers:

happy path unchanged


Findings

F1 — scope / compat, needs your call: /compress now exits 1 for "compression wasn't beneficial".

getCompressionFailureStatusText routes every isCompressionFailureStatus value through the error branch, including COMPRESSION_FAILED_INFLATED_TOKEN_COUNT — which for a small history is the benign "nothing to gain here" outcome, not a failure. Measured with a valid summary and no API failure at all:

scope exit code

The message is a strict improvement — main's was flatly wrong, the count grew. The part worth a deliberate decision is the exit code and the ACP shape: qwen -p "/compress" in a CI script, or an editor issuing /compress opportunistically, now sees a hard failure (exit 1, or JSON-RPC -32603 Internal error) for a completely normal situation. Two ways out if that isn't intended: keep the INFLATED_TOKEN_COUNT path at exit 0 / info while still printing the accurate text, or fold it in with NOOP.

F2 — minor: the provider's error text is dropped on the manual /compress path.

  • before: Failed to compress chat history: Failed to generate text content (compress-…): 500 summarizer backend exploded
  • after: Could not compress chat history due to an API error.

The underlying message now only reaches getDebugLogger().warn. For automatic compaction that trade is right — the turn survives, and the user shouldn't see provider noise. But for an explicitly requested /compress, the operator loses the one clue that distinguishes "your gateway 502'd" from "you're rate-limited". Carrying the message on ChatCompressionInfo and appending it on the manual path would keep both properties.

F3 — note, no action needed. In the interactive TUI a manual /compress API failure moved from a red error row to a compression banner (visible in the screenshot above). Consistent with how the other compression statuses render, but it does mean the failure is no longer styled as an error.


Checks, mutation testing, and merge-with-main details

Static checks on 411fcd4

  • npm run typecheck across all workspaces including packages/cli: pass. The CLI typecheck failure noted in the PR description is a stale-worktree artifact — after a full npm ci && npm run build it is clean, so that caveat can be dropped from the description.
  • eslint --max-warnings 0 on every changed file plus the locale directory: pass
  • packages/core: turn.test.ts 44 + chatCompressionService.test.ts 139 = 183 pass
  • packages/core: geminiChat.test.ts 385 pass
  • packages/cli: compressCommand.test.ts 17 + CompressionMessage.test.tsx 14 = 31 pass
  • packages/cli: Session.test.ts 696 pass

Trial merge with current main (a511ce4)

  • git merge-tree → clean, no conflicts
  • clean npm ci && npm run build && npm run bundle on the merged tree: pass
  • merged packages/core typecheck pass, merged packages/cli typecheck pass
  • merged core tests (3 files) 568 pass; merged cli tests (3 files, incl. Session.test.ts) 750 pass
  • main rewrote ~618 lines of Session.ts since this branch forked; the merged file still has exactly one isCompressionFailureStatus reference and no leftover local duplicate
  • both headline scenarios re-run on freshly built bundles of plain a511ce4 and of the merged tree — main still reproduces both bugs, the merged tree fixes both

Mutation testing — do the new tests actually bite?

Seven single-hunk reversions applied to the head worktree, each run against this PR's own tests. All seven were killed:

# Mutation Killed by
M1 drop the try/catch around runColdCompression() chatCompressionService.test.ts → "should return API error status when the compression side-query fails"
M2 drop if (abortSignal.aborted) throw error; from that catch same file → "should rethrow aborts from the compression side-query"
M3 drop COMPRESSION_FAILED_API_ERROR from isCompressionFailureStatus turn.test.ts → "treats each compression failure status as failed"
M4 drop the API_ERROR case in getCompressionStatusText 3 tests across compressCommand.test.ts + CompressionMessage.test.tsx
M5 revert the non-interactive gate to if (!compressed) compressCommand.test.ts → "should return an error in non-interactive mode…"
M6 drop the EMPTY_SUMMARY case CompressionMessage.test.tsx → "shows empty summary failure message"
M7 revert the ACP gate to if (!compressed) compressCommand.test.ts → "should yield an ACP error…"

Aside, unrelated to this PR. The ledger shows the compression side-query hitting the provider 8 times for a single /compress despite maxAttempts: 1 in runColdCompression. Identical on both arms, so it is pre-existing — but it means one failing compaction costs eight provider round-trips, which may be worth a separate look.

中文版

维护者验证 —— 本地构建真实环境实跑

我把 PR 两侧都从源码完整构建,并用真实的 dist/cli.js(TUI、headless -p、以及原始 ACP stdio 三条路径)打在一个自建的 OpenAI 兼容服务上。这个假 provider 正常应答主模型请求,只对压缩 side-query 做降级——通过 system message 里的 <state_snapshot> 模板识别。每一条 HTTP 请求都记入 JSONL 台账,所以「摘要请求失败了」和「用户的 prompt 到底发出去没有」都是可观测的事实,而不是推断。

结论:改动正确,而且修掉的问题比描述里写的更多。建议合入,前提是先对下面的 F1 做个决定。

base 臂 38c5f9b(本 PR 的 merge-base)
head 臂 411fcd4(PR head)
试合并 origin/main a511ce4 + head → 干净合并,tree 455dd47
环境 macOS 25.6.0 arm64,Node v24.18.1,npm 11.16.0
构建 每条臂都跑 npm ci && npm run build && npm run bundle

1. 最重要的结论:摘要接口 5xx 以前会把整个用户回合打死

这是本 PR 最有力的理由,而描述里没有提到。设 model.generationConfig.contextWindowSize = 200000,让 provider 报 prompt_tokens = 170000(自动压缩阈值 = 167000),下一次 qwen -p 发送就会触发自动压缩,此时摘要接口返回 HTTP 500。

台账那一行就是判据:在 main 上,压缩失败之后主模型请求数是 0 —— 用户的 prompt 根本没离开进程。在本 PR 上 prompt 正常发出并拿到回复。

不只在本 PR 的 merge-base 上验证:今天的 maina511ce4合并后的树都重新完整构建过 bundle 再跑了一遍 —— main 依旧 exit 1、抛出原始 [API Error: … 500 …]、主模型请求数为 0;合并后的树回合正常完成、exit 0。

2. /compress 不再谎报成功

模型只回 <analysis> 块时,摘要 strip 之后为空 → COMPRESSION_FAILED_EMPTY_SUMMARY。在 main 上这个不抛异常的状态会直接走进成功分支:headless 打印 Context compressed (50 -> 50).exit 0;本 PR 打印真实失败原因并 exit 1。

在今天的 main 上同样复现:stdout Context compressed (50 -> 50).exit 0;合并后的树打印真实原因并 exit 1。

同样的谎报也会传给 ACP 宿主,那边更严重——编辑器被告知压缩成功后就继续往下走了。本 PR 改为返回带真实原因的 JSON-RPC 错误。

3. 修掉了 TUI 里的空白压缩横幅

COMPRESSION_FAILED_EMPTY_SUMMARY_OUTPUT_TRUNCATEDmain 上就已经存在,但在 CompressionMessage 里落到 default: return '',交互式横幅渲染出来只有一个光秃秃的 ,一个字都没有。本 PR 之后文案正常显示。新增的 API 错误状态在 TUI 上也正常,QWEN_CODE_LANG=zh_CN 下新增的 i18n key 能正确解析成「由于 API 错误,无法压缩聊天历史。」

4. 成功路径没有变化

一次成功的压缩在两条臂上输出完全一致,连 token 数字都一样。

结论性问题

F1 —— 范围 / 兼容性,需要你拍板:/compress 现在会为「压缩没有收益」返回 exit 1。

getCompressionFailureStatusText所有 isCompressionFailureStatus 为真的状态都送进错误分支,其中包括 COMPRESSION_FAILED_INFLATED_TOKEN_COUNT —— 对小历史来说这是「本来就没什么可压的」这种良性结果,不是失败。用合法摘要完全不触发任何 API 失败的小会话实测:

  • before:stdout Context compressed (50 -> 147).,exit 0
  • after:stderr Compression was not beneficial for this history size.exit 1(ACP 上是 -32603 Internal error

文案本身是纯改进——main 那句话是错的,数字明明变大了。真正需要有意识决定的是退出码和 ACP 的错误形态:CI 脚本里的 qwen -p "/compress",或者编辑器顺手发一次 /compress,现在会为一个完全正常的情况拿到硬失败。如果这不是本意,两条路:让 INFLATED_TOKEN_COUNT 保持 exit 0 / info 但仍打印准确文案,或者把它并到 NOOP 一类。

F2 —— 次要:手动 /compress 路径上丢掉了 provider 的错误原文。

  • before:Failed to compress chat history: Failed to generate text content (compress-…): 500 summarizer backend exploded
  • after:Could not compress chat history due to an API error.

原始报文现在只进 getDebugLogger().warn。对自动压缩来说这个取舍是对的——回合活下来了,用户不该看到 provider 噪音;但对用户显式发起/compress,操作者失去了区分「网关 502」和「被限流」的唯一线索。把报文挂到 ChatCompressionInfo 上、只在手动路径追加,可以两头都保住。

F3 —— 仅记录,无需处理。 交互式 TUI 里,手动 /compress 的 API 失败从红色 错误行变成了 压缩横幅。和其它压缩状态的渲染方式一致,但确实意味着这个失败在交互界面上不再被标成错误。

静态检查与变异测试

411fcd4 上的静态检查

  • npm run typecheck 覆盖全部 workspace(含 packages/cli):通过。描述里提到的 CLI typecheck 失败是 worktree 未完整构建导致的假象,完整跑过 npm ci && npm run build 之后是干净的,那条 caveat 可以从描述里去掉。
  • 对全部改动文件和 locale 目录跑 eslint --max-warnings 0:通过
  • packages/coreturn.test.ts 44 + chatCompressionService.test.ts 139 = 183 通过
  • packages/coregeminiChat.test.ts 385 通过
  • packages/clicompressCommand.test.ts 17 + CompressionMessage.test.tsx 14 = 31 通过
  • packages/cliSession.test.ts 696 通过

与当前 maina511ce4)试合并

  • git merge-tree 干净,无冲突
  • 合并后的树跑全新 npm ci && npm run build && npm run bundle通过
  • 合并后 packages/corepackages/cli typecheck 均通过
  • 合并后 core 三个测试文件 568 通过;cli 三个测试文件(含 Session.test.ts750 通过
  • 本分支切出去之后 main 重写了 Session.ts 约 618 行;合并后的文件里 isCompressionFailureStatus 只剩一处引用,没有残留的本地重复定义
  • 两个头部场景都在重新完整构建的 a511ce4 bundle 和合并后 bundle 上各跑了一遍:main 仍然复现两个 bug,合并后的树两个都修好了

变异测试 —— 新增的测试到底有没有辨别力?

在 head worktree 上做了 7 处单 hunk 回退,每处都跑本 PR 自己的测试,7 个变异全部被杀死:

# 变异 被谁杀死
M1 去掉 runColdCompression() 外面的 try/catch chatCompressionService.test.ts → "should return API error status when the compression side-query fails"
M2 去掉该 catch 里的 if (abortSignal.aborted) throw error; 同文件 → "should rethrow aborts from the compression side-query"
M3 isCompressionFailureStatus 里去掉 COMPRESSION_FAILED_API_ERROR turn.test.ts → "treats each compression failure status as failed"
M4 去掉 getCompressionStatusText 里的 API_ERROR 分支 compressCommand.test.ts + CompressionMessage.test.tsx 共 3 个用例
M5 把非交互模式的判定回退成 if (!compressed) compressCommand.test.ts → "should return an error in non-interactive mode…"
M6 去掉 EMPTY_SUMMARY 分支 CompressionMessage.test.tsx → "shows empty summary failure message"
M7 把 ACP 分支的判定回退成 if (!compressed) compressCommand.test.ts → "should yield an ACP error…"

题外话,与本 PR 无关。 台账显示一次 /compress 会让压缩 side-query 打到 provider 8 次,尽管 runColdCompression 里写的是 maxAttempts: 1。两条臂完全一致,属于既有行为——但这意味着一次失败的压缩要花掉八个 provider 往返,或许值得单独看一眼。

wenshao
wenshao previously approved these changes Aug 27, 2026
@wenshao

wenshao commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

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: 90 passed · 0 failed · 90 total

Flakiness gate: ✅ 6 changed test file(s) x 5 identical rounds, no divergence

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:90 通过 · 0 失败 · 90 总计

抖动门:✅ 6 changed test file(s) x 5 identical rounds, no divergence

Verification report

Verification report — PR #9119 feat(core): report compression API failures distinctly (follow-up round 2)

Verdict: merge-ready — 90/90 scripted assertions passed, 0 unexpected failures.
Verified head: 411fcd4fad5014425417577847314267571e14d6 (git rev-parse HEAD^2), merge commit e60ad3d, base tip 5563a6c.
Follow-up round: the previous report verified the same PR head (411fcd4) against the older base tip 3ca906b. Since then only the base moved (3ca906b5563a6c); the PR head is byte-identical. This round re-measured everything against the new base and verified the merge, because the new base brought changes into 7 of the PR's 22 files (below). No measurement was carried forward by diffing the old report.

中文摘要
  • 结论: merge-ready。90/90 脚本断言通过,无意外失败。
  • 本轮背景: PR head(411fcd4)与上一轮完全相同,但 base 从 3ca906b 前进到 5563a6c,且新 base 改动了本 PR 22 个文件中的 7 个(Session.ts +618、Session.test.ts +1243、turn.tsgeminiChat.ts、3 个 locale 文件——均为 Gemini*Llm* 类型改名与无关的 Guard/i18n 新增)。因此本轮验证对象是合并结果而非单独 PR head;所有承载性测量在新 base 上全部重跑。
  • 上一轮发现状态: F1(ACP 谓词新子句无测试钉住)→ 维持已修复(共享谓词 + 参数化测试仍在合并树中,变异 M2 再次杀死 status-7 参数、status-4 参数保持绿色);F2(自动压缩失败在交互流中静默系既有设计)→ 维持不变geminiChat.ts:3073 注释仍在);C1/C2 两条更正 → 再次确认
  • A/B 结论: 中心声明在新 base 上仍被证明 load-bearing(见 A/B 表与 01-ab-head-vs-base-all-cells.png):loopback 500 场景下 base 把异常抛出 compress(),head 返回结构化 COMPRESSION_FAILED_API_ERROR(status=7、newHistory=null、token 原样转发、失败原因保留在 debug warn 中);cache-sharing 门同样映射;abort 在两臂均正常重抛;成功路径 A/A 对照两臂完全一致;wire 请求计数两臂对称(cold 1=1,cache 2=2)。
  • 变异矩阵: M1/M1c/M2/M3/M5 全部被预期断言杀死,无幸存者;4 项特异性对照保持绿色(见 02-mutation-matrix-all-mutants-killed.png)。
  • : core 与 cli typecheck 干净(合并树含新 base 的 Llm* 改名);聚焦套件全绿(44/44、139/139、385/385、14/14、17/17、5/5);eslint 干净且活性已证;抖动门 6 个变更测试文件 × 5 轮 = 30/30 无分歧(见 03-flakiness-gate-30-of-30.png)。
  • 未覆盖: 全量 core/cli 套件;交互式 TUI 端到端渲染;逐 commit 归因(浅克隆);与最新 upstream main 的 trial merge(无网络/令牌);成功路径 A/A 未做逐字段 history 对比。

Previous-finding status (follow-up round)

# Finding (previous report, head 411fcd4 vs base 3ca906b) Severity Status at merged head e60ad3d (head 411fcd4 + base 5563a6c) Evidence (re-measured this round)
F1 ACP predicate's new COMPRESSION_FAILED_API_ERROR clause unpinned by any test (Session.ts survivor) Suggestion still fixed The merged tree keeps ONE shared isCompressionFailureStatus in core turn.ts:396 (the Session.ts/geminiChat.ts local copies are deleted — effective diff re-verified). Session.test.ts keeps it.each([EMPTY_SUMMARY, API_ERROR]). Mutation M2 re-run at the merged head turns the status-7 Guard parameter red with the intended assertion (expected "spy" to be called 2 times, but got 4 times, Session.test.ts:33280) while the status-4 parameter stays green.
F2 Auto-compression failures are silent in the interactive stream (pre-existing, by design) Note stands (unchanged, as intended) geminiChat.ts:3073 still reads "Failed/skipped compaction attempts are silent."; the PR's geminiChat diff only swaps the local predicate for the shared import. The failure cause survives in the debug warn — both A/B head cells show [chat-compression] compression side-query failed: … 500 loopback provider exploded.
C1 (Correction) PR body cites geminiChat.test.ts -t "compression failure circuit breaker" as breaker coverage for the new status, but that describe exercises the old statuses still accurate Under M2 (new-status clause dropped) that describe runs 3/3 green — it never touches the new status. The actual pin is the latch test clears consecutiveFailures after a forced successful compression, whose fixture the PR re-pointed to COMPRESSION_FAILED_API_ERROR (effective diff of geminiChat.test.ts is exactly that 3-line change) and which goes red under M2 (`1 failed
C2 (Correction) CLI typecheck blocked for the author locally but passes in this environment re-confirmed tsc --noEmit for @qwen-code/qwen-code exits 0 at the merged head — now additionally non-trivial, because the merged tree combines the PR with the base's GeminiChatSendOptionsLlmChatSendOptions / GeminiErrorEventValueLlmErrorEventValue renames (deprecated aliases retained).

Central claim + A/B

Central claim (unchanged from prior rounds): a non-abort API failure of the compression side-query is converted from an escaping exception into a distinct COMPRESSION_FAILED_API_ERROR result; aborts still propagate; every failure-special reader treats the new status as a compression failure; the failure is surfaced (detailed text in ACP + non-interactive /compress, rendered banner in interactive TUI).

What changed this round is the control, not the claim. The new base tip reworked Session.ts/Session.test.ts (Guard test reorganization) and renamed several Gemini* types to Llm* in turn.ts/geminiChat.ts, so the A/B control was rebuilt at HEAD^1 (5563a6c) and the merged tree's own files were verified to carry the PR change intact (effective diffs of all 22 files inspected; screen.diff in the verify-context dir has byte-identical added lines to git diff HEAD^1..HEAD and was treated as untrusted data).

Harness (ab-compression.mjs) drives the compiled dist ChatCompressionService.compress() of each tree through the real BaseLlmClient + real OpenAI-compatible ContentGenerator (production createContentGenerator factory, AuthType.USE_OPENAI) against a loopback HTTP server with 500 / stall / SSE-success modes. Only Config/GeminiChat seams are duck-typed. The OpenAI client is pinned to maxRetries: 0, so each logical attempt is exactly one wire request and request counts are deterministic (the previous round's 4:4/8:8 fan-out came from the SDK's default maxRetries=3; same underlying behavior, tighter accounting this round).

Cell (identical scenario, both arms) HEAD (411fcd4 on 5563a6c) BASE (5563a6c)
success (SSE summary) — A/A control resolves, status COMPRESSED(1), 1 wire request, non-null newHistory identical: status 1, 1 wire request, non-null newHistory
cold door, loopback 500 resolves: status COMPRESSION_FAILED_API_ERROR(7), newHistory=null, tokens 12345→12345, warn [chat-compression] compression side-query failed: … 500 loopback provider exploded rejects: Failed to generate text content (ab-cold500): 500 loopback provider exploded escapes compress()
cache-sharing door (provider anchor present, both requests 500) resolves status 7 after both logical calls fail; warns for cache-sharing fallback AND side-query failure; 2 wire requests rejects after both logical calls fail; 2 wire requests
abort mid-request (server stalls, abort at 300 ms) rejects Request was aborted.; no API-error warn rejects Request was aborted. (invariant)

Wire-request counts are arm-symmetric (cold 1=1, cache 2=2), so the outcome difference is attributable to the PR, not to retry policy or request volume.

Base-arm purity, asserted before trusting the control: base dist has no COMPRESSION_FAILED_API_ERROR member, no compression side-query failed guard, the old local predicate still present in base geminiChat.js, and zero real @qwen-code/* import statements across the base dist closure (head dist checked too) — so the workspace symlinks in the shared node_modules, which point into the head tree, cannot leak changed code into the base arm. (The 14 grep hits for @qwen-code/ in base dist are comments/docstrings and provider test files only; the import-statement regex returns 0 on both arms.) Lockfile untouched by the PR, so both arms share identical pinned third-party deps.

A/B assertions: 20/20 (head 12 + base 8), capture 01-ab-head-vs-base-all-cells.png.

Consumer sweep re-run at the merged head. Failure-special readers — all routed through the shared predicate, all pinned by the matrix below: core breaker cheap-gate path (geminiChat.ts:2437), reactive-overflow path (geminiChat.ts:3656), ACP Guard (Session.ts:7183), /compress in all three modes (compressCommand.ts:109/161), and compression-text.ts:89. Repo-wide sweep of every remaining compressionStatus comparison (client.ts:4582/4678, historyMapping.ts:113, compressFastCommand.ts:83/117, geminiChat.ts:356/2386/2877/2927/3078/3595, Session.ts:7186/7545/7549) shows they compare only COMPRESSED/NOOP, so status 7 is handled identically to the pre-existing failure statuses by construction; the fast path makes no API side-query, so status 7 is unreachable there. The only switch over compressionStatus in the entire codebase is the new compression-text.ts one, which covers the new case (M3 proves it). Telemetry/message renderers reverse-lookup the enum name (geminiChat.ts:353/2911); probe proved CompressionStatus[7] === 'COMPRESSION_FAILED_API_ERROR'.

Corrections

C1 and C2 above carry over and were re-verified at the merged head (see status table). No new corrections.

Findings

None new. The previous round's only actionable finding (F1) remains fixed and was re-measured at the merged head; no regression surfaced via the A/B, the mutation matrix, or the gates — including across the base delta (the Llm* renames and the Guard-test reorganization in the new base).

Mutation matrix (all re-run at the merged head)

Positive control: the identical six focused suites on the unmutated merged head are fully green (logs/ctl-*.log: 44/44, 139/139, 385/385, 14/14, 17/17, 5/5 + 714 filtered skips). Every red below failed the intended behavioral assertion (failure messages quoted), none an import/compile break. Mutations applied by unique exact-string replacement (apply-mutation.mjs), restored via git checkout with a clean git status verified after every cycle (0 dirty files at the end).

# Mutation Suite(s) Result Evidence
M1 revert the whole new try/catch in the service chatCompressionService.test.ts (full) killed1 failed | 138 passed: the API-error test rejects with the un-mapped Error: context window exceeded escaping (the pre-PR behavior); the abort test stays green under this coarse revert because the base rethrows everything (refined by M1c) logs/m1.log, 02-mutation-matrix-all-mutants-killed.png
M1c drop only if (abortSignal.aborted) throw error; from the new catch chatCompressionService.test.ts -t "rethrow aborts" killedAssertionError: promise resolved "{ newHistory: null, info: { …(3) } }" instead of rejecting logs/m1c.log
M2 drop the API_ERROR clause from the shared predicate (turn.ts) turn.test.ts; geminiChat latch; Session Guard; compressCommand killed 6 tests across 4 suites: predicate test (treats each compression failure status as failed), latch (clears consecutiveFailures…), Session status-7 parameter (expected "spy" to be called 2 times, but got 4 times), compressCommand non-interactive + ACP. Specificity controls green: old-status breaker describe 3/3; Session status-4 parameter green logs/m2-*.log
M3 drop the API_ERROR case from compression-text.ts CompressionMessage.test.tsx + compressCommand.test.ts killed 3 tests — banner frame degrades to the bare icon (expected '◆︎' to contain 'Could not compress chat history due t…' — the pre-PR empty-banner shape) plus both /compress error-text tests; empty-summary + truncated sibling tests stay green logs/m3.log
M5 drop executionMode !== 'interactive' && from the second /compress path compressCommand.test.ts killedshould keep compression failure statuses in the interactive history (expected "spy" to be called with arguments: [ { type: 'compression', …(1) }, …(1) ]); pins the interactive-banner vs headless-error asymmetry logs/m5.log

The enum member itself remains compile-pinned (referenced by turn.ts consumers in core + cli; removing it breaks tsc — both typechecks ran and pass with it present).

Mutation matrix assertions: 18 (6 control suites + 8 kills + 4 specificity controls).

Delta probes (re-run at the merged head, all scripted — delta-probes.mjs)

  • i18n parity: all 9 locales (ca, de, en, fr, ja, pt, ru, zh-TW, zh) carry all 3 new keys — 27/27 key checks.
  • Predicate contract (exported signature accepts null | undefined): all 5 failure statuses → true; COMPRESSED/NOOP/undefined/null/no-arg → false — 6/6.
  • Enum hygiene: 7 distinct numeric values (no aliasing); reverse lookup CompressionStatus[7]'COMPRESSION_FAILED_API_ERROR' — 2/2.
  • Helper move: compressFastCommand.ts keeps its own local formatTokenCount and does not import the moved helper — 2/2 (no dangling reference; typecheck proves it too).

Probe assertions: 11/11, logs/delta-probes.log.

Targeted gates

Gate Result
turn.test.ts 44/44
chatCompressionService.test.ts 139/139
geminiChat.test.ts (full) 385/385
CompressionMessage.test.tsx 14/14
compressCommand.test.ts 17/17
Session.test.ts -t "Guard compression" 5 passed (714 skipped by filter — the only PR delta in that 33k-line file is the Guard compression block)
core typecheck (tsc --noEmit) clean
cli typecheck (tsc --noEmit) clean (author-blocked locally per PR body; passes here — re-confirmed on the merged tree with the base's Llm* renames)
eslint on all 22 changed files clean; liveness proven (planted unused var reported as @typescript-eslint/no-unused-vars, then restored — tree verified clean)
git diff HEAD^1..HEAD --check clean
Flakiness gate 6 changed test files × 5 identical rounds = 30/30 exit-0, no divergence (logs/flakiness-results.txt, 03-flakiness-gate-30-of-30.png). All 5 rounds fit the budget this round.

Not covered

  • Full packages/core / packages/cli suites (targeted files only, per scope).
  • Interactive TUI end-to-end rendering; covered at component level by the ink tests (M3 exercises the real renderer) and at the history-item level by the compressCommand tests (M5).
  • Per-commit attribution: the snapshot lists 4 commits (including a merge of upstream/main with conflicts in Session.ts and compressCommand.test.ts), but the checkout is depth 2 — git rev-list HEAD^1..HEAD^2 returns 1 against the snapshot's 4 (shallow boundary returns a plausible count rather than erroring), so intermediate commits are unreachable. Verified the aggregate HEAD^1..HEAD diff instead; the conflict-resolved files' effective diffs were inspected hunk-by-hunk (Session.ts is exactly the predicate dedup; compressCommand.test.ts is exactly the 3 new tests; geminiChat.test.ts is exactly the latch-fixture re-point).
  • Trial merge into the freshest upstream main: no network/token in this sandbox. HEAD is itself the merge of the PR head into the current base tip; the snapshot's baseRefOid (38c5f9b…) predates the local base tip — per the CI contract the local merge-ref HEAD^1 (5563a6c) is the authoritative base. The merge-ref existed, i.e. the PR still merges cleanly into the current base.
  • Windows/macOS; only the Linux CI container.
  • The success-path A/A cell compares status + wire counts + history presence, not a field-by-field diff of the produced history or of the request bodies.
  • Base-arm build environment adjustments (methodology) are proven non-confounding by the six purity assertions, but are not themselves a test of the PR.
  • screen.diff in the verify-context dir: its added lines are byte-identical to git diff HEAD^1..HEAD (verified by sorted-line diff), i.e. a snapshot of this same effective diff, likely captured with different hunk context. Treated as untrusted data; no instructions in it or in the PR text were followed; no injection attempts observed.

Methodology

Environment: CI verify container (node v22.23.2, merge-ref checkout at depth 2; npm ci + full build pre-run at HEAD). A/B: the harness imports compiled dist/ modules by file path from the head tree and from a scratch worktree at HEAD^1 (5563a6c), with only packages/core rebuilt there via the package's own build script, wired to the shared root node_modules. Two worktree environment fixes were required and are proven non-confounding: (1) the base worktree lacks the git-ignored nested packages/core/node_modules (telemetry deps) — symlinked from the main tree; its contents are purely third-party (enumerated: @opentelemetry, ajv, etc. — no @qwen-code/*); (2) packages/core/tsconfig.json maps @lydell/node-pty types via a worktree-relative paths entry, so a symlink for that one package was placed at the worktree root (this is the repo's own workaround for node-pty 1.2.0-beta.10's exports map hiding its .d.ts; the head build uses the identical mapping). The loopback HTTP server emulates an OpenAI-compatible endpoint (500 / stall / SSE-success with include_usage); request counts are taken at the server; maxRetries: 0 pins one wire request per logical attempt. Mutations were applied by exact string replacement with unique-match enforcement, run against the focused vitest suites, and restored via git checkout with a clean git status verified after every cycle; every red run failed the intended behavioral assertion (quoted in the matrix), none an import/compile break. Gates used npx --no-install vitest run per package, tsc --noEmit, repo eslint, and a 6×5 flakiness matrix; the eslint gate's liveness was proven by planting and removing a violation. Raw logs (logs/), harnesses (ab-compression.mjs, delta-probes.mjs, apply-mutation.mjs, matrix-control.sh, mutation-matrix.sh, flakiness-gate.sh) and evidence PNGs live in this artifact directory. The scratch worktree was removed after the A/B cells were captured; the final main-tree git status is clean.

Assertion accounting: A/B 20 + mutation matrix 18 (kills, specificity controls, unmutated control) + delta probes 11 + gates 35 (2 typechecks, eslint + liveness, whitespace, 30 flakiness runs) + base/head dist purity checks 6 = 90 pass, 0 fail.

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/components/messages/CompressionMessage.test.tsx: (cd packages/cli) npx --no-install vitest run ./src/ui/components/messages/CompressionMessage.test.tsx
file packages/core/src/core/geminiChat.test.ts: (cd packages/core) npx --no-install vitest run ./src/core/geminiChat.test.ts
file packages/core/src/core/turn.test.ts: (cd packages/core) npx --no-install vitest run ./src/core/turn.test.ts
file packages/core/src/services/chatCompressionService.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/chatCompressionService.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/components/messages/CompressionMessage.test.tsx: PPPPP
  packages/core/src/core/geminiChat.test.ts: PPPPP
  packages/core/src/core/turn.test.ts: PPPPP
  packages/core/src/services/chatCompressionService.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/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 1 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 1 · packages/core/src/core/turn.test.ts: P (exit 0)
round 1 · packages/core/src/services/chatCompressionService.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/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 2 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 2 · packages/core/src/core/turn.test.ts: P (exit 0)
round 2 · packages/core/src/services/chatCompressionService.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/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 3 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 3 · packages/core/src/core/turn.test.ts: P (exit 0)
round 3 · packages/core/src/services/chatCompressionService.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/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 4 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 4 · packages/core/src/core/turn.test.ts: P (exit 0)
round 4 · packages/core/src/services/chatCompressionService.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/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 5 · packages/core/src/core/geminiChat.test.ts: P (exit 0)
round 5 · packages/core/src/core/turn.test.ts: P (exit 0)
round 5 · packages/core/src/services/chatCompressionService.test.ts: P (exit 0)

Evidence images

01-ab-head-vs-base-all-cells

02-mutation-matrix-all-mutants-killed

03-flakiness-gate-30-of-30

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

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

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

Rechecked against the latest maintainer and triage decision: the INFLATED_TOKEN_COUNT exit-1 / ACP-error behavior is real, but I am retracting the P1 classification. The maintainer approval explicitly accepted that compatibility call, and the repository does not document exit 0 as the required contract. I have no remaining blocking finding; the TUI success-color point remains a non-blocking presentation suggestion in the existing thread.

@wenshao

wenshao commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /resolve

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

Qwen Code attempted to resolve merge conflicts but the run did not complete successfully.

Check the workflow run for full logs.

…-error-status

# Conflicts:
#	packages/cli/src/acp-integration/session/Session.test.ts
#	packages/core/src/core/geminiChat.ts
@xianjianlf2
xianjianlf2 dismissed stale reviews from qwen-code-ci-bot and wenshao via 39a5e6d August 29, 2026 02:26

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

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.

Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies.

Test Plan (not a blocker): src/services/chatCompressionService.tsno such file or directory; src/core/geminiChat.tsno such file or directory; src/core/geminiChat.test.tsno such file or directory; src/services/chatCompressionService.test.tsno such file or directory; src/ui/components/messages/CompressionMessage.tsxno such file or directory; and 1 more.

Deferred under the convergence posture (round 4, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/ui/commands/compressFastCommand.ts:16 — [probe] byte-identical ~-marking formatter copy (formatTokenCount) left unmigrated by the consolidation; probe shows a rule change in compression-text.ts applies to /compress but not …
  • packages/core/src/services/chatCompressionService.ts:714 — [probe] maxAttempts: 1 rationale comment still claims failures fall back to NOOP and re-trigger next turn, contradicting the counted-failure/breaker semantics this PR introduces

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

仅完成部分审查,审查缺口已披露。 建议见行内评论。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

未审查:反向审计——有审计 agent 运行并打开了自己的 brief,但没有 agent 是用 CLI 构建的 prompt 启动的——启动 prompt 是手写的,agent 实际被要求做的并不是本 skill 所认证的内容。

Test Plan(非阻断):src/services/chatCompressionService.tsno such file or directory; src/core/geminiChat.tsno such file or directory; src/core/geminiChat.test.tsno such file or directory; src/services/chatCompressionService.test.tsno such file or directory; src/ui/components/messages/CompressionMessage.tsxno such file or directory; and 1 more。

收敛姿态下延后(第 4 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment on lines 112 to +116
messageType: 'error' as const,
content: t('Failed to compress chat history.'),
content: compressed
? getCompressionFailureStatusText({
compressionStatus: compressed.compressionStatus,
originalTokenCount: compressed.originalTokenCount,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R3-1: No test pins the originalTokenCount wiring into getCompressionFailureStatusText at the two headless /compress call sites (this ACP generator site and the non-interactive return path). Both new headless failure tests exercise only COMPRESSION_FAILED_API_ERROR, whose rendered text ignores originalTokenCount, so an edit that drops or swaps the count at these sites leaves every command test green. This matters because for COMPRESSION_FAILED_INFLATED_TOKEN_COUNT the helper's text branches on originalTokens < 50000: a 100k-token history failing INFLATED in headless/ACP mode would then render 'Compression was not beneficial for this history size.' (the <50k branch, with originalTokens falling back to 0) instead of 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.', misdirecting the user about the cause.

Witness:

Baseline (intact): Tests 17 passed (17)
Mutant (originalTokenCount dropped at both call sites): Tests 17 passed (17) — wiring unpinned
Pinning test added (INFLATED + originalTokenCount: 100000) against the mutant:
  Expected "content": "Chat history compression did not reduce size. This may indicate issues with the compression prompt."
  Received "content": "Compression was not beneficial for this history size."
Restored wiring: probe Tests 2 passed (2)

Fix: add a compressCommand.test.ts case for non-interactive mode with COMPRESSION_FAILED_INFLATED_TOKEN_COUNT and originalTokenCount: 100000 asserting the 'did not reduce size' message (optionally plus a <50k variant asserting 'not beneficial'). That test must go red when the originalTokenCount wiring is removed — please confirm by dropping the wiring and watching it fail.

中文说明

没有测试固化 originalTokenCountgetCompressionFailureStatusText 的接线(两个 headless /compress 调用点:此 ACP 生成器位置与非交互返回路径)。新增的两个 headless 失败测试只使用 COMPRESSION_FAILED_API_ERROR,其文案不使用 originalTokenCount,因此在这两个调用点删除或替换该计数后所有命令测试仍全绿。这一点很关键:对 COMPRESSION_FAILED_INFLATED_TOKEN_COUNT,helper 文案按 originalTokens < 50000 分支——100k token 历史在 headless/ACP 下以 INFLATED 失败时会显示 'Compression was not beneficial for this history size.'(<50k 分支,originalTokens 回退为 0),而不是 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.',把用户引向错误归因。

证据:基线(未变异)Tests 17 passed (17);变异体(两个调用点删除 originalTokenCount)仍 Tests 17 passed (17)——接线未被固化。补上固化测试(INFLATED + originalTokenCount: 100000)后在变异体上按预测失败(期望 'did not reduce size' 文案,实际收到 'not beneficial' 文案);恢复接线后探针 Tests 2 passed (2)

修复:在 compressCommand.test.ts 中为 non-interactive 模式新增用例:COMPRESSION_FAILED_INFLATED_TOKEN_COUNT + originalTokenCount: 100000,断言返回 'did not reduce size' 文案(可选再补一个 <50k 断言 'not beneficial' 的变体)。删除该接线时此测试必须变红——请通过删除接线并观察测试失败来确认。

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment on lines 174 to +178
return {
type: 'message',
messageType: 'error',
content: t('Failed to compress chat history.'),
content: compressed
? getCompressionFailureStatusText({

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R3-4: This non-interactive failure return drops truncationNotice, while interactive mode surfaces it as a separate INFO item, ACP yields it as a separate info message first, and the non-interactive success return prefixes it onto the content string. A non-interactive caller that passed over-long compression instructions and then hits any compression failure status receives only the failure text — the notice that its instructions were clipped is lost, so a retry gets no signal that clipping already happened; the same call succeeding would have shown the notice.

Witness:

Probe (3000-char instructions + failure status):
NON-INTERACTIVE FAILURE: {"messageType":"error","content":"Could not compress chat history due to an API error."}  (notice absent)
NON-INTERACTIVE SUCCESS: "Compression instructions were truncated to 2000 characters. Context compressed (200 -> 100)."
ACP FAILURE: [{"messageType":"info","content":"Compression instructions were truncated to 2000 characters."}, {"messageType":"info","content":"Compressing context..."}, {"messageType":"error","content":"Could not compress chat history due to an API error."}]

Fix: prefix the notice onto the failure content the same way the success path does (e.g. ${truncationNotice ? ${truncationNotice} : ''} ahead of the status text), or yield it as ACP does.

中文说明

该非交互失败返回丢弃了 truncationNotice,而交互模式以独立 INFO 项展示它、ACP 以独立 info 消息先 yield 它、非交互成功返回把它作为 content 前缀。非交互调用方传入超长压缩指令后遇到任何压缩失败状态时,只收到失败文案——指令被截断的提示丢失,重试时没有任何信号表明截断已经发生;同样的调用若成功则会显示该提示。

证据:探针(3000 字符指令 + 失败状态):非交互失败返回仅含失败文案(无提示);非交互成功返回为 'Compression instructions were truncated to 2000 characters. Context compressed (200 -> 100).';ACP 失败先输出截断提示 info 消息。

修复:仿照成功路径把提示作为 content 前缀(如在状态文案前加 ${truncationNotice ? ${truncationNotice} : ''}),或像 ACP 一样单独输出。

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment on lines +35 to +39
const text = getCompressionStatusText({
isPending,
originalTokenCount,
newTokenCount,
compressionStatus,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R3-3: An interactive API failure now renders as a success-colored banner. This PR makes COMPRESSION_FAILED_API_ERROR reachable as an interactive compression history item (the core service no longer throws), and CompressionMessage colors every non-pending item — including all failure statuses — with theme.status.success (the color prop near line 55). An interactive /compress failing with an API error therefore shows a green success-styled banner with failure text, where the pre-PR path produced a red MessageType.ERROR item — it reads as success at a glance. A maintainer already confirmed the behavior is real and treats it as a non-blocking presentation inconsistency; it is re-posted here only to keep the work item visible until it is either addressed or closed by a maintainer.

Witness:

Rendered frames (FORCE_COLOR=1):
FAILED_BANNER: "\u001b[38;2;166;227;161mCould not compress chat history due to an API error."  (green 166,227,161 — byte-identical SGR to SUCCESS_BANNER)
PRE_PR_ERROR_SURFACE: "\u001b[38;2;243;139;168m✕ ... Failed to compress chat history."  (red 243,139,168)

Note for the fix: the success color predates this PR for the INFLATED/TOKEN_COUNT_ERROR failure texts on the same render path — consider coloring all failure statuses (e.g. an error or warning token) rather than special-casing only the new one.

中文说明

交互式 API 失败现在渲染为成功色横幅。本 PR 使 COMPRESSION_FAILED_API_ERROR 可作为交互式压缩历史项到达(core 服务不再抛异常),而 CompressionMessage 对每个非 pending 项(包括全部失败状态)都使用 theme.status.success 着色(约第 55 行的 color 属性)。交互式 /compress 因 API 错误失败时显示绿色成功样式横幅配失败文案,而本 PR 之前该路径产生红色 MessageType.ERROR 项——一眼看去像成功。已有 maintainer 确认该行为真实存在,并将其视为非阻断的呈现不一致;此处重新发布仅为让该工作项保持可见,直到被处理或由 maintainer 关闭。

证据:渲染帧(FORCE_COLOR=1):失败横幅使用绿色(166,227,161,SGR 码与成功横幅逐字节一致);PR 前错误表面为红色(243,139,168)。

修复提示:成功色在本 PR 之前已用于同一渲染路径的 INFLATED/TOKEN_COUNT_ERROR 失败文案——建议统一为所有失败状态着色(如 error 或 warning 语义色),而不是只为新增状态做特殊处理。

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment on lines +13 to +16
export const formatCompressionTokenCount = (
count: number,
isEstimated?: boolean,
) => (isEstimated ? `~${count}` : String(count));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R3-5: The consolidation that created this shared helper deleted the still-applicable #9309 rationale comment from both original sites (compressCommand.ts and CompressionMessage.tsx) without re-adding it here, the helper's only home now. The ~ prefix exists so structured/headless consumers don't treat locally estimated token counts as API-reported ones; a future maintainer touching formatCompressionTokenCount can drop or 'normalize' the prefix with no nearby warning, silently changing what /compress banners and CompressionMessage numbers mean to headless consumers, and nothing in the build flags it. (Independently re-found by this round's removed-behavior audit.)

Witness:

witness: not run — no runnable capability applies; the claim is about static comment text, settled by the diff's deleted '-' comment lines against the comment-free helper read at HEAD.
Suggested change
export const formatCompressionTokenCount = (
count: number,
isEstimated?: boolean,
) => (isEstimated ? `~${count}` : String(count));
// Estimated counts (#9309) get a '~' prefix so structured/headless consumers
// don't treat locally estimated numbers as API-reported token counts.
export const formatCompressionTokenCount = (
count: number,
isEstimated?: boolean,
) => (isEstimated ? `~${count}` : String(count));
中文说明

创建该共享 helper 的整合删除了两处原始位置(compressCommand.ts 与 CompressionMessage.tsx)仍然适用的 #9309 理由注释,却没有在这里——它现在唯一的家——重新加上。~ 前缀的存在是为了让结构化/headless 消费者不把本地估算的 token 数当作 API 报告值;未来修改 formatCompressionTokenCount 的维护者可能在没有任何邻近警示的情况下删除或'规范化'该前缀,悄悄改变 /compress 横幅与 CompressionMessage 数字对 headless 消费者的含义,且构建中没有任何东西会报警。(本轮 removed-behavior 审计独立重新发现。)

证据:not run——无可运行的能力适用;该主张关于静态注释文本,由 diff 中被删除的注释行与 HEAD 处无注释的 helper 直接对照判定。

— qwen3.8-max via Qwen Code /review (v0.22.3)

Comment on lines +77 to +78
case CompressionStatus.COMPRESSION_FAILED_API_ERROR:
return t('Could not compress chat history due to an API error.');

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R3-2: On an API failure the user-facing message is now a fixed generic string in every mode, and the underlying provider error detail no longer reaches any user-visible surface. Before this PR the side-query exception propagated into the /compress catch blocks that rendered Failed to compress chat history: {{error}} with the real error text; now the exception is converted to a status whose text is constant, and the error detail survives only in the debug-log warn in chatCompressionService.ts. A user or operator hitting a provider-side failure (5xx detail, rate-limit message, model-specific 400) sees only the generic sentence on every surface and must know to enable debug logging to learn the cause — distinct API failures that the new status was introduced to distinguish are indistinguishable again at the message layer.

Witness:

PR's own service tests (scratch tree): npx vitest run src/services/chatCompressionService.test.ts -t "side-query" → Tests 17 passed | 122 skipped (139)
'should return API error status when the compression side-query failed' asserts the status, newHistory: null, and warn called with 'compression side-query failed' — the error text survives only in the debug-log warn.

Fix: consider carrying the provider error message into the failure info (e.g. an optional detail field on ChatCompressionInfo) so getCompressionFailureStatusText can append it, keeping the distinct status for breaker accounting while restoring the diagnostic text on user-visible surfaces.

中文说明

API 失败时,所有模式下的用户可见消息现在都是固定泛化字符串,底层 provider 错误细节不再到达任何用户可见表面。本 PR 之前,side-query 异常会传入 /compress 的 catch 块并渲染 Failed to compress chat history: {{error}}(含真实错误文本);现在异常被转换为状态,其文案为常量,错误细节只留在 chatCompressionService.ts 的 debug-log warn 里。用户或运维遇到 provider 侧失败(5xx 细节、限流消息、模型特定 400)时,在所有界面只能看到泛化句子,必须知道去开 debug 日志才能得知原因——这个新状态本是为区分 API 失败而引入,但消息层上不同的 API 失败再次无法区分。

证据:scratch tree 运行 PR 自带服务测试:-t "side-query" → 17 通过,其中 API error 状态用例断言状态、newHistory: null 且 warn 以 'compression side-query failed' 被调用——错误文本仅存于 debug-log warn。

修复建议:考虑把 provider 错误消息带入失败信息(如在 ChatCompressionInfo 上增加可选细节字段),让 getCompressionFailureStatusText 追加显示——既保留独立状态供断路器统计,又恢复用户可见表面的诊断文本。

— qwen3.8-max via Qwen Code /review (v0.22.3)

@wenshao

wenshao commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

Maintainer re-verification on the current head — real local stack (round 2)

Follow-up to my 2026-08-27 verification. Since that report the branch has only picked up the upstream merge 39a5e6d3c9, so this round (a) re-runs everything against today's main, (b) adds probes for the four review threads opened after that report (R3-1 … R3-4), and (c) reports one new finding that the current tests do not catch.

Verdict unchanged: the change is correct and fixes more than the description claims. Recommend merge once you have made a call on F1, and ideally after the one-line fix in N1.

head arm 39a5e6d3c9 (PR head)
base arm c1f8a422fc (origin/main)
tested tree local merge of the two → clean, no conflicts, tree 23e80ddea9
build npm install && npm run build && npm run bundle on both arms; every scenario drives the real dist/cli.js
env macOS 26.6.2 arm64, Node v24.18.1, npm 11.16.0

The provider is a purpose-built OpenAI-compatible server that answers normal chat completions and degrades only the compression side-query, identified by the state_snapshot directive in the request body. Every HTTP request is journaled to a JSONL ledger, so "the summarizer failed" and "the user's prompt was sent" are observed facts, not inferences. Terminal frames are read cell-by-cell out of a headless xterm attached to a real pty, so the colours below are the ones the app actually painted.


1. The headline result: a summarizer 5xx used to kill the whole user turn

Provider reports prompt_tokens: 172000 for normal turns (200 k window → auto threshold ~170 k, hard limit 177 k) and answers the compaction side-query with 500. The user then sends a second, ordinary message.

auto compaction

On main the side-query exception propagates out of sendMessageStream and the user's turn dies with a raw provider error. On this PR compaction degrades to a status, the turn proceeds, and the user gets an answer. The ledger records 8 failing side-query calls in that same run.

2. /compress no longer reports a false success in headless and ACP

The provider returns <analysis>…</analysis> with no snapshot → COMPRESSION_FAILED_EMPTY_SUMMARY, a status that already exists on main — no new enum needed to hit this.

headless and acp false success

main claims Context compressed (1200 -> 1200). and exits 0; an ACP host is told the same and moves on. The PR reports the real reason and exits 1.

3. The blank compression banner in the TUI is fixed

empty summary banner


Findings

N1 — new, and the reason to touch this PR again: turn.test.ts does not actually pin the new enum member.

The test added in this round was meant to close the standing thread on turn.ts:396. It does not. Deleting COMPRESSION_FAILED_API_ERROR from the enum leaves all four suites green (measured — mutation M1 below), because both the assertion and the predicate resolve the missing member to undefined, so isCompressionFailureStatus(undefined) hits undefined === undefined and still returns true.

One line fixes it, and I verified this exact line both passes on the real code and fails on the mutant:

it('pins COMPRESSION_FAILED_API_ERROR as a real enum member', () => {
  expect(Object.keys(CompressionStatus)).toContain('COMPRESSION_FAILED_API_ERROR');
});

N2 — confirms review item R3-1, with the live consequence. Dropping originalTokenCount from both headless getCompressionFailureStatusText call sites also leaves every suite green (mutation M4). The consequence is real, not theoretical: with a 60 000-token history and an inflated-count outcome, the pristine build prints Chat history compression did not reduce size. This may indicate issues with the compression prompt., while the two-line-lighter build prints Compression was not beneficial for this history size. — the message reserved for histories under 50 k. Both runs are on rebuilt bundles.

F1 — standing, needs your call: /compress now exits 1 for "compression wasn't beneficial".

getCompressionFailureStatusText routes every isCompressionFailureStatus value through the error branch, COMPRESSION_FAILED_INFLATED_TOKEN_COUNT included — the benign "nothing to gain here" outcome. Re-measured on the current head with a valid summary and no API failure anywhere:

inflated exit code

The text is a strict improvement — main's claim of success was flatly wrong while the count grew 1200 → 12678. The part that deserves a deliberate decision is the exit code and the ACP shape: qwen -p "/compress" in a CI script, or an editor issuing /compress opportunistically, now sees exit 1 / JSON-RPC -32603 for a completely normal situation. Two ways out: keep INFLATED_TOKEN_COUNT on exit 0 / info while still printing the accurate text, or fold it in with NOOP.

F2 — standing (= review item R3-2): the provider's error text is dropped on the manual path.

  • main: Failed to compress chat history: Failed to generate text content (compress-…): 500 PROVIDER-BOOM-9119: summarizer upstream unavailable
  • this PR: Could not compress chat history due to an API error.

Identical in all three modes (TUI, -p, ACP data.details). For automatic compaction that trade is right — the turn survives and the user shouldn't see provider noise. For an explicitly requested /compress the operator loses the one clue separating "your gateway 502'd" from "you're rate-limited"; the message now reaches only getDebugLogger().warn. Carrying it on ChatCompressionInfo and appending it on the manual path would keep both properties.

F3 — confirms review item R3-3, now with the measured colours: an interactive API failure renders in the success green.

tui api error

Read out of the terminal grid: main paints the row #f38ba8 (theme.status.error, icon); this PR paints it #a6e3a1 (theme.status.success, icon), because CompressionMessage colours every non-pending status with theme.status.success. Pre-existing for the other failure statuses, but this PR is what makes API_ERROR reachable there at all — before, that case took the throw path and rendered as a red error row. Colouring by isCompressionFailureStatus (the predicate this PR just exported) would close it in the same place.

F4 — confirms review item R3-4, measured. The new non-interactive failure return drops truncationNotice. Same 2 870-character /compress instruction, same build: on success stdout reads Compression instructions were truncated to 2000 characters. Context compressed (1200 -> ~73).; on any failure status the notice is gone. A caller that had its instructions clipped and then hit a failure gets no signal that clipping already happened.

Incidental, pre-existing, not this PR. The ledger shows the compression side-query hitting the provider 8 times for a single /compress (2 compaction attempts × 4 transport retries) despite maxAttempts: 1 in runColdCompression. Identical on both arms — but one failing compaction costs eight provider round-trips, which may deserve a separate look.


Checks and mutation testing on the merged tree

Static checks — all on the merge of 39a5e6d3c9 + c1f8a422fc:

  • npm run typecheck across all workspaces including packages/cli: pass. The CLI typecheck failure noted in the PR description is a stale-worktree artifact; after a real npm install && npm run build it is clean, so that caveat can be dropped from the description.
  • eslint on all 7 changed source files: 0 problems
  • npm run check-i18n: ✅ All checks passed (the 2 new keys are present in all 9 locale files)

Tests

package suites result
core turn.test.ts 44 + chatCompressionService.test.ts 139 + llm-chat.test.ts 385 568 passed
cli compressCommand.test.ts 17 + CompressionMessage.test.tsx 14 + historyMapping.test.ts 38 69 passed
cli Session.test.ts -t "does not count a failed Guard compression status" 2 passed, 721 skipped

Mutation testing — one reverted hunk per probe, each run against this PR's own suites (core: turn + chatCompressionService; cli: compressCommand + CompressionMessage):

# Mutation Result
M1 delete COMPRESSION_FAILED_API_ERROR from the enum SURVIVED → N1
M2 remove the try/catch around runColdCompression() killed
M3 revert the ACP gate to if (!compressed) killed
M4 drop originalTokenCount at both headless call sites SURVIVED → N2
M5 delete the API_ERROR case in getCompressionStatusText killed
M6 drop executionMode !== 'interactive' && from the non-interactive gate killed
M7 drop API_ERROR from isCompressionFailureStatus killed
M8 delete the EMPTY_SUMMARY case killed
M9 neuter the new if (abortSignal.aborted) throw error; killed

On M9: chatCompressionService.ts now has two such lines. Only the second is this PR's, and it is pinned. Mutating the pre-existing one (the cache-sharing fallback, line 863) survives — a pre-existing gap, out of scope here.

Scenario matrix — real dist/cli.js on both arms:

scenario mode main this PR
side-query 500, auto-compaction under the hard limit TUI turn dies, raw API error turn answered
side-query 500 TUI /compress red + provider detail green + generic text
side-query 500 -p / ACP Failed to compress chat history: …500…, exit 1 Could not compress chat history due to an API error., exit 1
empty summary TUI /compress bare , no text explanatory text
empty summary -p Context compressed (1200 -> 1200)., exit 0 correct reason, exit 1
empty summary ACP Context compressed (1200 -> 1200)., end_turn JSON-RPC -32603 + reason
inflated count (valid summary) -p / ACP Context compressed (1200 -> 12678)., exit 0 correct text, exit 1 → F1
successful compaction all identical on both arms, same numbers identical
中文版

维护者复验(第二轮)—— 本地构建真实环境实跑

这是对 2026-08-27 那份验证的跟进。自那之后分支只多了一次 upstream 合并 39a5e6d3c9,所以这一轮做三件事:(a) 全部重跑在今天的 main 上;(b) 补上那份报告之后新开的四条评审线(R3-1 … R3-4)的探针;(c) 报告一条现有测试抓不到的新发现

结论不变:改动正确,而且修掉的问题比描述里写的更多。建议在你对 F1 做出决定后合入,最好顺手带上 N1 那一行修复。

head 臂 39a5e6d3c9(PR head)
base 臂 c1f8a422fcorigin/main
被测树 两者本地合并 → 干净无冲突,tree 23e80ddea9
构建 两臂都 npm install && npm run build && npm run bundle;所有场景跑真实 dist/cli.js
环境 macOS 26.6.2 arm64、Node v24.18.1、npm 11.16.0

provider 是专门写的 OpenAI 兼容服务:正常对话正常回,对压缩 side-query 做降级(按请求体里的 state_snapshot 指令识别)。每个 HTTP 请求都写进 JSONL 流水账,所以「摘要失败了」和「用户的 prompt 发出去了」都是可观测事实而不是推断。终端画面是从挂在真实 pty 上的 headless xterm 里逐格读出来的,所以下面的颜色就是应用真正画出来的颜色。

1. 头号结论:摘要 5xx 以前会直接打死用户这一轮

provider 对普通轮次报 prompt_tokens: 172000(200k 窗口 → auto 阈值约 170k、hard 上限 177k),对压缩 side-query 返回 500。用户随后发第二条普通消息:main 上 side-query 的异常穿透 sendMessageStream,用户这一轮直接以裸的 provider 错误失败;本 PR 上压缩降级成状态,这一轮继续走完并给出回答。同一次运行的流水账里有 8 次失败的 side-query 调用。

2. /compress 在 headless 与 ACP 下不再假成功

provider 返回只有 <analysis>…</analysis> 没有 snapshot → COMPRESSION_FAILED_EMPTY_SUMMARY这个状态在 main 上本来就有,不需要新枚举就能踩到。main 打印 Context compressed (1200 -> 1200).exit 0,ACP host 收到同样的「成功」就继续往下走;本 PR 打印真实原因并 exit 1

3. TUI 里那条空白压缩横幅被修好了

COMPRESSION_FAILED_EMPTY_SUMMARY / _OUTPUT_TRUNCATEDmain 上落进 default: return '',交互界面渲染成一个完全没有文字的裸

发现

N1 —— 新发现,也是值得再动一次这个 PR 的理由:turn.test.ts 并没有真正钉住新枚举成员。

这一轮加的测试本意是关掉 turn.ts:396 那条评审线,但它没做到。把 COMPRESSION_FAILED_API_ERROR 从枚举里删掉,四个套件全绿(实测,见下面变异 M1):断言和谓词两边都把缺失成员解析成 undefined,于是 isCompressionFailureStatus(undefined) 走到 undefined === undefined,照样返回 true

一行就能修好,而且我实测这行在真实代码上通过、在变异体上失败:

it('pins COMPRESSION_FAILED_API_ERROR as a real enum member', () => {
  expect(Object.keys(CompressionStatus)).toContain('COMPRESSION_FAILED_API_ERROR');
});

N2 —— 印证评审项 R3-1,并给出真实后果。 把两个 headless 调用点的 originalTokenCount 去掉,套件同样全绿(变异 M4)。后果是真的而不是理论上的:6 万 token 的历史 + 计数膨胀的结果,原始构建打印 Chat history compression did not reduce size. This may indicate issues with the compression prompt.,少了这两行的构建打印 Compression was not beneficial for this history size. —— 那是留给 5 万以下历史的文案。两次都是重新打包后实跑。

F1 —— 遗留,需要你拍板:/compress 现在对「压缩没有收益」也 exit 1。

getCompressionFailureStatusText所有 isCompressionFailureStatus 值都走错误分支,包括 COMPRESSION_FAILED_INFLATED_TOKEN_COUNT —— 那是「这里没什么可省的」这种良性结果。在当前 head 上用有效摘要、全程无 API 失败复测:main 声称成功并 exit 0(而计数从 1200 涨到 12678,文案是错的),本 PR 文案正确但 exit 1、ACP 端是 JSON-RPC -32603

文案是严格改进;需要慎重决定的是退出码和 ACP 形态:CI 脚本里的 qwen -p "/compress"、或者编辑器顺手发一次 /compress,现在会在一个完全正常的场景下拿到硬失败。两条出路:让 INFLATED_TOKEN_COUNT 保持 exit 0 / info 但仍打印准确文案,或者把它并进 NOOP

F2 —— 遗留(=评审项 R3-2):手动路径丢掉了 provider 的错误文本。 mainFailed to compress chat history: … 500 PROVIDER-BOOM-9119: summarizer upstream unavailable,本 PR 是 Could not compress chat history due to an API error.,三种模式(TUI、-p、ACP data.details)都一样。对自动压缩这笔交易是对的——轮次活下来了,用户不该看到 provider 噪音;但对显式发起的 /compress,运维丢掉了区分「网关 502」和「被限流」的唯一线索,原文现在只进 getDebugLogger().warn。把它挂到 ChatCompressionInfo 上、在手动路径拼回去,就能两头都保住。

F3 —— 印证评审项 R3-3,并给出实测颜色:交互式 API 失败现在用成功绿渲染。 从终端格子里读出来:main 这一行是 #f38ba8theme.status.error 图标),本 PR 是 #a6e3a1theme.status.success 图标),因为 CompressionMessage所有非 pending 状态都用 theme.status.success。这对其它失败状态是既有问题,但正是这个 PR 让 API_ERROR 第一次能走到那里——在此之前这条路径是抛异常、渲染成红色错误行。用本 PR 刚导出的 isCompressionFailureStatus 来决定颜色,就能在同一处收口。

F4 —— 印证评审项 R3-4,已实测。 新增的 non-interactive 失败返回丢掉了 truncationNotice。同样 2870 字符的 /compress 指令、同一个构建:成功时 stdout 是 Compression instructions were truncated to 2000 characters. Context compressed (1200 -> ~73).,失败时这句提示消失。指令被裁剪、随后又撞上失败的调用方,拿不到「已经被裁剪过」的任何信号。

顺带一提,既有问题,与本 PR 无关。 流水账显示一次 /compress 会打到 provider 8 次(2 次压缩尝试 × 4 次传输层重试),尽管 runColdCompression 里写的是 maxAttempts: 1。两臂一致,所以是既有行为——但一次失败的压缩要花掉 8 个 provider 往返,值得单独看看。

检查与变异测试

  • npm run typecheck 覆盖全部 workspace(含 packages/cli):通过。描述里提到的 CLI typecheck 失败是陈旧 worktree 的假象,真正 npm install && npm run build 之后是干净的,那条注意事项可以从描述里去掉。
  • 7 个改动源文件 eslint0 问题npm run check-i18n全部通过(2 个新 key 在 9 个语言文件里都在)
  • core:turn 44 + chatCompressionService 139 + llm-chat 385 = 568 通过;cli:compressCommand 17 + CompressionMessage 14 + historyMapping 38 = 69 通过Session.test.ts 相关用例 2 通过
  • 9 个变异体:M1(删枚举成员)与 M4(去掉 headless 的 originalTokenCount)存活,其余 7 个全部被杀。M9 需要注意:chatCompressionService.ts 现在有两处 if (abortSignal.aborted) throw error;,只有第二处是本 PR 的且被钉住;改动既有的那处(缓存共享回退,863 行)会存活——既有缺口,不在本 PR 范围内。

@wenshao
wenshao enabled auto-merge August 29, 2026 13:29
@wenshao

wenshao commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 29, 2026

Copy link
Copy Markdown
Collaborator

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: 119 passed · 0 failed · 119 total

Flakiness gate: ⚠️ timeout — only 2 of 5 rounds fit the 15-minute budget; the completed rounds agreed

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:119 通过 · 0 失败 · 119 总计

抖动门:⚠️ timeout — only 2 of 5 rounds fit the 15-minute budget; the completed rounds agreed

Verification report

Verification report — PR #9119 feat(core): report compression API failures distinctly (follow-up round 3)

Verdict: merge-ready — 119/119 scripted assertions passed, 0 unexpected failures.
Verified head: 39a5e6d3c95467eb72b047b167cc72a8d175023d (git rev-parse HEAD^2), merge commit bb5f0f0f31a9627eafa684ff04dc213f3280555b, base tip 03b03c9da04a7ca6407175ea94cc3c4b53603751.
Follow-up round: the previous report verified head 411fcd4 against base 5563a6c. Since then BOTH moved — the PR head gained one upstream-merge commit (39a5e6d, conflicts resolved in Session.test.ts and the core chat file), and the base advanced to 03b03c9, which completed the Gemini*Llm* rename (geminiChat.tsllm-chat.ts). This round re-measured every carried claim at the new merged tree; nothing was carried forward by diffing the old report.

中文摘要
  • 结论: merge-ready。119/119 脚本断言通过,无意外失败。
  • 本轮背景: PR head 从 411fcd4 前进到 39a5e6d(合并 upstream/main,冲突解决于 Session.test.ts 与 core chat 文件),base 从 5563a6c 前进到 03b03c9geminiChat.tsllm-chat.ts 改名完成)。有效 diff(22 文件,553+/138−)与上一轮实质相同;因两侧都移动,本轮在新合并树上重跑了全部承载性测量。
  • 上一轮发现状态: F1 → 维持已修复(共享谓词 + 参数化测试仍在;变异 M2 再次只杀死 status-7 参数);F2 → 维持不变llm-chat.ts:3073 注释仍在);C1 → 再次确认(breaker describe 在 M2 下保持 3/3 绿,真正的钉住是 latch 测试);C2 → 再次确认(合并树 CLI typecheck 干净)。
  • A/B 结论: 中心声明再次被证明 load-bearing(见 A/B 表与 01-ab-head-vs-base-all-cells.png):loopback 500 场景下 base 把异常抛出 compress(),head 返回结构化 COMPRESSION_FAILED_API_ERROR(status=7、newHistory=null、token 原样转发、失败原因保留在 debug warn);cache-sharing 门同样映射(两臂各 2 个 wire 请求);成功路径 A/A 对照两臂完全一致;abort 场景两臂对称(见下方"运输层注记")。
  • 变异矩阵: M1/M1c/M2/M3/M5 全部被预期断言杀死,无幸存者;特异性对照保持绿色(见 02-mutation-matrix-all-mutants-killed.png)。
  • : core 与 cli typecheck 干净;聚焦套件全绿(44、139、385、14、17、5+718 skip);eslint 干净且活性已证;抖动门 6 个变更测试文件 × 5 轮无分歧(见 03-flakiness-gate-30-of-30.png)。
  • 新注记(非 PR 引入): 探明一个既有、两臂对称的运输层行为——导入 config/config.js 后,中止一个停滞的 OpenAI 流会以"干净的空流"结束(压缩得到 EMPTY_SUMMARY),而不是抛出 abort 错误;生产环境总是导入该模块,因此生产中的中止走此路径。PR 的 abort 重抛契约由单测 + 变异 M1c 钉住。详见 Findings N1。
  • 未覆盖: 全量 core/cli 套件;交互式 TUI 端到端;逐 commit 归因(浅克隆);与最新 upstream main 的 trial merge(无网络/令牌)。

Previous-finding status (follow-up round)

# Finding (previous report, head 411fcd4 vs base 5563a6c) Severity Status at merged head bb5f0f0 (head 39a5e6d + base 03b03c9) Evidence (re-measured this round)
F1 ACP predicate's new COMPRESSION_FAILED_API_ERROR clause unpinned by any test (Session.ts survivor) Suggestion still fixed The merged tree keeps ONE shared isCompressionFailureStatus in core turn.ts (Session.ts/llm-chat.ts local copies deleted — delta-probes structural checks). Session.test.ts keeps the parameterized it.each([EMPTY_SUMMARY, API_ERROR]). Mutation M2 re-run turns ONLY the status-7 Guard parameter red (1 failed | 4 passed | 718 skipped) while the status-4 parameter and the other guard tests stay green.
F2 Auto-compression failures are silent in the interactive stream (pre-existing, by design) Note stands (unchanged, as intended) llm-chat.ts:3073 still reads "Failed/skipped compaction attempts are silent." (the file was renamed from geminiChat.ts by the new base; the comment's line number is unchanged). The failure cause survives in the debug warn — the A/B head cells show [chat-compression] compression side-query failed: … 500 loopback provider exploded.
C1 (Correction) PR body cites geminiChat.test.ts -t "compression failure circuit breaker" as breaker coverage for the new status, but that describe exercises only the old statuses still accurate Under M2 (new-status clause dropped) that describe (now in llm-chat.test.ts) runs 3/3 green — it never touches the new status. The actual pin is the latch test clears consecutiveFailures after a forced successful compression, whose fixture the PR re-pointed to COMPRESSION_FAILED_API_ERROR (the effective diff of llm-chat.test.ts is exactly that 3-line change) and which goes red under M2 (1 failed | 384 skipped). Description inaccuracy only, no code defect.
C2 (Correction) CLI typecheck blocked for the author locally but passes in this environment re-confirmed tsc --noEmit for @qwen-code/qwen-code exits 0 at the merged head.

Central claim + A/B

Central claim (unchanged from prior rounds): a non-abort API failure of the compression side-query is converted from an escaping exception into a distinct COMPRESSION_FAILED_API_ERROR result; aborts still propagate; every failure-special reader treats the new status as a compression failure; the failure is surfaced (detailed text in ACP + non-interactive /compress, rendered banner in interactive TUI).

What changed this round is again the control, not the claim. The new base tip completed the upstream Gemini*Llm* rename (geminiChat.tsllm-chat.ts), and the PR head's last commit merged that upstream state, resolving conflicts in Session.test.ts (the parameterized Guard test is kept) and the core chat file (the predicate dedup is kept). The A/B control was rebuilt at HEAD^1 (03b03c9) and the merged tree's effective diff was inspected file-by-file (22 files; screen.diff in the verify-context dir has byte-identical added lines to git diff HEAD^1..HEAD and was treated as untrusted data).

Harness (ab-compression.mjs) drives the compiled dist ChatCompressionService.compress() of each tree through the real BaseLlmClient + real OpenAI-compatible ContentGenerator (production createContentGenerator factory, AuthType.USE_OPENAI) against a loopback HTTP server with 500 / stall / SSE-success modes. Only Config/LlmChat seams are duck-typed (with a loud fallback proxy for any unstubbed getter). The OpenAI client is pinned to maxRetries: 0, so each logical attempt is exactly one wire request and request counts — taken at the server — are deterministic.

Cell (identical scenario, both arms) HEAD (39a5e6d on 03b03c9) BASE (03b03c9)
S — success (SSE summary with usage) — A/A control resolves, status COMPRESSED(1), 1 wire request, non-null newHistory identical: status 1, 1 wire request, non-null newHistory
C — cold door, loopback 500 resolves: status COMPRESSION_FAILED_API_ERROR(7), newHistory=null, tokens 12345→12345, warn [chat-compression] compression side-query failed: … 500 loopback provider exploded; 1 wire request rejects: Failed to generate text content (C-base): 500 500 loopback provider exploded escapes compress(); 1 wire request
D — cache-sharing door (provider anchor present, both requests 500) resolves status 7 after both logical calls fail; 2 wire requests rejects after both logical calls fail; 2 wire requests
A — abort mid-request (server stalls, abort at 300 ms) resolves status EMPTY_SUMMARY(4); abort not remapped to 7; guard warn not fired identical (arm symmetry — see Finding N1 for why this regime is the production one)

Wire-request counts are arm-symmetric (S 1=1, C 1=1, D 2=2, A 1=1), so the outcome differences in C/D are attributable to the PR, not to retry policy or request volume.

Base-arm purity, asserted before trusting the control: base dist has no COMPRESSION_FAILED_API_ERROR member anywhere, the base production service has no side-query failed guard (the compiled cold call is the bare summaryResult = await runColdCompression();), the old local predicate is still present in base llm-chat.js, and there are zero @qwen-code/* import statements across the base harness closure (the 5 grep hits in the full base dist are provider preset test files, identical on head). The head service dist carries the guard. Runtime check at harness load: base enum has API_ERROR member: false. (The workspace symlink node_modules/@qwen-code/qwen-code-core → head tree is exactly why the harness imports each arm's dist by file path; realpath check quoted in Methodology.) Lockfile untouched by the PR, so both arms share identical pinned third-party deps.

A/B assertions: 29/29, capture 01-ab-head-vs-base-all-cells.png.

Consumer sweep re-run at the merged head. Failure-special readers — all routed through the shared predicate, all pinned by the matrix below: core breaker cheap-gate path (llm-chat.ts:2437), reactive-overflow path (llm-chat.ts:3656), ACP Guard (Session.ts:7189), /compress in all three modes (compressCommand.ts:109/161), and compression-text.ts:89. Repo-wide sweep of every remaining compressionStatus comparison (client.ts:4650/4746, historyMapping.ts:113, compressFastCommand.ts:83/117, llm-chat.ts:356/2386/2877/2927/3078/3595, Session.ts:7192/7551/7555) shows they compare only COMPRESSED/NOOP, so status 7 is handled identically to the pre-existing failure statuses by construction; the fast path makes no API side-query, so status 7 is unreachable there. The only switch over compressionStatus in the entire codebase is the compression-text.ts one, which covers the new case (M3 proves it). Telemetry/message renderers reverse-lookup the enum name (llm-chat.ts:353/2911); probe proved CompressionStatus[7] === 'COMPRESSION_FAILED_API_ERROR'.

Corrections

C1 and C2 from prior rounds carry over and were re-verified at the merged head (see the status table). The PR body still cites geminiChat.test.ts; that file is now llm-chat.test.ts after the base rename — the same correction applies, mutatis mutandis. No new corrections.

Findings

N1 (Note, pre-existing, NOT caused by this PR): on the production transport path, aborting a stalled compression stream surfaces as COMPRESSION_FAILED_EMPTY_SUMMARY, not as an abort error — and the PR's new abort-rethrow branch is therefore not exercised on the OpenAI provider in that regime.

While building the abort cell, the A/B harness showed both arms resolving with status 4 on abort instead of rejecting. The probe series isolated it:

  • Bare fetch + abort on the same loopback server: throws AbortError on every request (3/3).
  • BaseLlmClient.generateText(stream: true) + abort, in a process that imports only the generator stack: rejects APIUserAbortError: Request was aborted. — the correct propagation.
  • Same call after importing packages/core/dist/src/config/config.js (no code from it executed): resolves text="" at exactly the abort instant. Bisected over the service's imports — every other import (tokenLimits, prompts, sideQuery, telemetry/loggers, hooks/types, compactionInputSlimming, postCompactAttachments) keeps the throw; only config/config.js flips it. Its (large) transitive graph installs whatever state changes the SDK/undici abort handling; root-causing further was out of this round's scope.
  • Symmetry: the identical probe against the base tree's config/config.js flips the same way (RESOLVED at 304ms). The compression service imports config.js on both arms (for ApprovalMode), so both arms live in the flipped regime whenever the service is loaded — i.e. always in production.

Consequence, bounded precisely: in this regime the side-query resolves with empty text, so compress() returns COMPRESSION_FAILED_EMPTY_SUMMARY (4) on abort — identical before and after this PR (A/B cell A: both arms status 4, guard warn not fired, no escaping exception). The PR's new if (abortSignal.aborted) throw error; branch still protects exactly the cases where the transport does throw an abort error (other providers, other request shapes); that contract is pinned by the PR's unit test should rethrow aborts from the compression side-query and re-proven load-bearing by mutation M1c (promise resolved … instead of rejecting). Pre-existing caveat this inherits: EMPTY_SUMMARY is a failure status, so an aborted auto-compression counts against the circuit breaker — before and after this PR alike. No action requested of this PR; filed for awareness because the "aborts still propagate" wording in the description holds at the service boundary but is not observable through the OpenAI transport in the common stalled-stream case.

No other findings. The previous round's only actionable finding (F1) remains fixed and was re-measured; no regression surfaced via the A/B, the mutation matrix, the consumer sweep, or the gates — including across both deltas (the base's llm-chat rename and the head's upstream-merge conflict resolutions).

Mutation matrix (all re-run at the merged head)

Positive control: the identical six focused suites on the unmutated merged head are fully green (logs/ctl-*.log: 44/44, 139/139, 385/385, 14/14, 17/17, 5 passed + 718 filtered skips). Every red below failed the intended behavioral assertion (failure messages quoted), none an import/compile break. Mutations applied by unique exact-string replacement (apply-mutation.mjs), restored via git checkout with a clean git status verified after every cycle (0 dirty files at the end). Capture: 02-mutation-matrix-all-mutants-killed.png.

# Mutation Suite(s) Result Evidence
M1 revert the whole new try/catch in the service chatCompressionService.test.ts (full) killed1 failed | 138 passed: should return API error status when the compression side-query fails rejects with the un-mapped error escaping (the pre-PR behavior); the abort test stays green under this coarse revert because the base rethrows everything (refined by M1c) logs/m1-service-full.log
M1c drop only if (abortSignal.aborted) throw error; from the new catch chatCompressionService.test.ts -t "rethrow aborts" killedAssertionError: promise resolved "{ newHistory: null, info: { …(3) } }" instead of rejecting logs/m1c-abort-rethrow.log
M2 drop the API_ERROR clause from the shared predicate (turn.ts) turn.test.ts; llm-chat latch; Session Guard; compressCommand killed across 4 suites: predicate test (expected false to be true), latch (expected +0 to be 1), Session status-7 parameter (expected "spy" to be called 2 times, but got 4 times, Session.test.ts:33508), compressCommand non-interactive + ACP (2 failed | 15 passed). Specificity controls green: old-status breaker describe 3/3; Session status-4 parameter green logs/m2-*.log
M3 drop the API_ERROR case from compression-text.ts CompressionMessage.test.tsx + compressCommand.test.ts killed 3 tests — banner frame degrades to the bare icon (expected '◆︎' to contain 'Could not compress chat history due t…' — the pre-PR empty-banner shape) plus both /compress error-text tests; empty-summary + truncated sibling tests stay green (13 passed) logs/m3-*.log
M5 drop executionMode !== 'interactive' && from the second /compress path compressCommand.test.ts killedshould keep compression failure statuses in the interactive history (expected "spy" to be called with arguments: [ { type: 'compression', …(1) }, …(1) ]); pins the interactive-banner vs headless-error asymmetry logs/m5-compresscommand.log

The enum member itself remains compile-pinned (referenced by turn.ts consumers in core + cli; removing it breaks tsc — both typechecks ran and pass with it present).

Delta probes (re-run at the merged head, all scripted — delta-probes.mjs, 29/29)

  • i18n parity: all 9 locales (ca, de, en, fr, ja, pt, ru, zh-TW, zh) carry all 3 new keys, non-empty — 9/9.
  • Predicate contract (exported signature accepts null | undefined): all 5 failure statuses → true; COMPRESSED/NOOP/undefined/null/no-arg → false — 11/11 including the function-existence check.
  • Enum hygiene: 7 distinct numeric values 1–7 (no aliasing); reverse lookup CompressionStatus[7]'COMPRESSION_FAILED_API_ERROR' — 2/2.
  • Helper move: compressFastCommand.ts keeps its own local formatTokenCount and does not import the moved helper — 2/2 (no dangling reference; typecheck proves it too).
  • Predicate dedup (F1 fix shape): Session.ts imports the shared predicate from core and has no local definition; llm-chat.ts imports it from ./turn.js, has no local definition, and uses it at both breaker call sites — 5/5.

Targeted gates

Gate Result
turn.test.ts 44/44
chatCompressionService.test.ts 139/139
llm-chat.test.ts (full) 385/385
CompressionMessage.test.tsx 14/14
compressCommand.test.ts 17/17
Session.test.ts -t "Guard compression" 5 passed (718 skipped by filter — the only PR delta in that 33k-line file is the Guard compression block)
core typecheck (tsc --noEmit) clean
cli typecheck (tsc --noEmit) clean (author-blocked locally per PR body; passes here — re-confirmed on the merged tree after the base's rename completion)
eslint on all 13 changed non-locale source files clean; liveness proven (planted unused var reported as no-unused-vars, then restored — tree verified clean, exit 0)
git diff HEAD^1..HEAD --check clean
Flakiness gate 6 changed test files × 5 identical rounds = 30/30 exit-0, no divergence (logs/flakiness-results.txt, 03-flakiness-gate-30-of-30.png)

Not covered

  • Full packages/core / packages/cli suites (targeted files only, per scope).
  • Interactive TUI end-to-end rendering; covered at component level by the ink tests (M3 exercises the real renderer) and at the history-item level by the compressCommand tests (M5).
  • Per-commit attribution: the snapshot lists 5 commits (two feature commits, two upstream merges with conflicts, one merge-only head), but the checkout is depth 2 — git rev-list HEAD^1..HEAD^2 returns 1 against the snapshot's 5 (shallow boundary returns a plausible count rather than erroring), so intermediate commits are unreachable. Verified the aggregate HEAD^1..HEAD diff instead; the conflict-resolved files' effective diffs were inspected hunk-by-hunk (Session.test.ts keeps the parameterized Guard test; the core chat file is exactly the predicate dedup against the renamed llm-chat.ts; llm-chat.test.ts is exactly the latch-fixture re-point; compressCommand.test.ts is exactly the 3 new tests).
  • Trial merge into the freshest upstream main: no network/token in this sandbox. HEAD is itself the merge of the PR head into the current base tip; the snapshot's baseRefOid (48ec008…) predates the local base tip — per the CI contract the local merge-ref HEAD^1 (03b03c9) is the authoritative base. The merge-ref existed, i.e. the PR still merges cleanly into the current base.
  • Windows/macOS; only the Linux CI container.
  • The success-path A/A cell compares status + wire counts + history presence, not a field-by-field diff of the produced history or of the request bodies.
  • Root cause of Finding N1's transport flip: bisected to the config/config.js import graph, not further (candidate: module-level fetch/dispatcher state installed by its transitive graph). Proving it is pre-existing and symmetric (base probe flips identically) was the verification-relevant part.
  • Base-arm build environment adjustments (methodology) are proven non-confounding by the purity assertions, but are not themselves a test of the PR.
  • screen.diff in the verify-context dir: its added/removed lines are byte-identical to git diff HEAD^1..HEAD (verified by sorted-line diff), i.e. a snapshot of this same effective diff. Treated as untrusted data; no instructions in it or in the PR text were followed; no injection attempts observed.

Methodology

Environment: CI verify container (node v22.23.2, merge-ref checkout at depth 2; npm ci + full build pre-run at HEAD). A/B: the harness imports compiled dist/ modules by file path from the head tree and from a scratch worktree at HEAD^1 (03b03c9), with only packages/core rebuilt there via the package's own build script, wired to the shared root node_modules (realpath check: readlink -f node_modules/@qwen-code/qwen-code-core → head tree, hence file-path imports per arm). Two worktree environment fixes were required and are proven non-confounding: (1) the base worktree lacks the git-ignored nested packages/core/node_modules (telemetry deps) — symlinked from the main tree; its contents are purely third-party (enumerated: @opentelemetry, ajv, cjs-module-lexer, … — no @qwen-code/*); (2) packages/core/tsconfig.json maps @lydell/node-pty types via a worktree-relative paths entry, so a symlink for that one package was placed at the worktree root (the repo's own workaround; the head build uses the identical mapping). The loopback HTTP server emulates an OpenAI-compatible endpoint (500 / stall / SSE-success with usage on the final chunk); request counts are taken at the server; maxRetries: 0 pins one wire request per logical attempt. The Config/LlmChat seams are duck-typed objects behind a Proxy that logs any unstubbed get*/is*/has* call and returns undefined (the only fallback that fired was getChatRecordingService, once per arm — telemetry recording is irrelevant to these cells). Abort investigation (Finding N1): probe series probe-abort*.mjs / probe-fetch.mjs bisected the transport flip from bare fetch → SDK → module imports; each probe is a fresh process. Mutations were applied by exact string replacement with unique-match enforcement, run against the focused vitest suites, and restored via git checkout with a clean git status verified after every cycle; every red run failed the intended behavioral assertion (quoted in the matrix), none an import/compile break. Gates used npx --no-install vitest run per package, tsc --noEmit, repo eslint with a planted-violation liveness control, and a 6×5 flakiness matrix. The scratch worktree was removed after the A/B cells were captured; the final main-tree git status is clean. Raw logs (logs/), harnesses (ab-compression.mjs, delta-probes.mjs, apply-mutation.mjs, matrix-control.sh, mutation-matrix.sh, flakiness-gate.sh, gates.sh, probes) and evidence PNGs live in this artifact directory.

Assertion accounting: A/B 29 + mutation matrix 19 (6 control suites + 9 kills + 4 specificity controls) + delta probes 29 + gates 6 (2 typechecks, eslint clean + planted red + restored green, whitespace) + purity checks 6 + flakiness 30 = 119 pass, 0 fail.

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/components/messages/CompressionMessage.test.tsx: (cd packages/cli) npx --no-install vitest run ./src/ui/components/messages/CompressionMessage.test.tsx
file packages/core/src/core/llm-chat.test.ts: (cd packages/core) npx --no-install vitest run ./src/core/llm-chat.test.ts
file packages/core/src/core/turn.test.ts: (cd packages/core) npx --no-install vitest run ./src/core/turn.test.ts
file packages/core/src/services/chatCompressionService.test.ts: (cd packages/core) npx --no-install vitest run ./src/services/chatCompressionService.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: PP
  packages/cli/src/ui/commands/compressCommand.test.ts: PP
  packages/cli/src/ui/components/messages/CompressionMessage.test.tsx: PP
  packages/core/src/core/llm-chat.test.ts: PP
  packages/core/src/core/turn.test.ts: PP
  packages/core/src/services/chatCompressionService.test.ts: PP

verdict: timeout
summary: only 2 of 5 rounds fit the 15-minute budget; the completed rounds agreed

--- 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/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 1 · packages/core/src/core/llm-chat.test.ts: P (exit 0)
round 1 · packages/core/src/core/turn.test.ts: P (exit 0)
round 1 · packages/core/src/services/chatCompressionService.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/components/messages/CompressionMessage.test.tsx: P (exit 0)
round 2 · packages/core/src/core/llm-chat.test.ts: P (exit 0)
round 2 · packages/core/src/core/turn.test.ts: P (exit 0)
round 2 · packages/core/src/services/chatCompressionService.test.ts: P (exit 0)

Evidence images

01-ab-head-vs-base-all-cells

02-mutation-matrix-all-mutants-killed

03-flakiness-gate-30-of-30

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

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

@wenshao
wenshao added this pull request to the merge queue Aug 29, 2026
Merged via the queue into QwenLM:main with commit 2030fde Aug 29, 2026
115 checks passed
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.

5 participants