fix(cli): prevent /rewind from dropping conversation history after /compress-fast - #9331
Conversation
computeApiTruncationIndex treated every successful compression marker as a summarizing boundary. /compress-fast emits the same marker but removes no user prompts from the API history and inserts no summary prefix, so rewinding to a post-marker turn collapsed the truncation anchor to the startup context and silently dropped the whole pre-marker conversation. Tag compression UI items with the path that produced them (CompressionProps.compressionKind) and make the rewind mapping ignore 'fast' markers as boundaries. Legacy markers without a kind keep prior behavior. Fixes #9320
|
Thanks for the continued work — this re-run covers the push through Template still good ✓ Problem: unchanged and solid — #9320 is an observed bug with measured evidence (llama.cpp prefill logs, ~87k tokens resumed as ~27k). The scope growth since the first pass is review-driven: each increment answers a named finding from the /review rounds (placeholder count drift, marker-less auto-compaction), not scope creep. The structural residue of those findings is tracked in #9437 rather than folded in here — the right split for a bugfix PR. Direction: aligned, as before. Silent context loss in Size: core paths are touched now, so correcting this section from the first pass. Breakdown: ~109 production-logic lines — core 30 (placeholder predicate + export + a resume-hazard note) and cli 79 (mapping guard, kind tags, type field; the ACP Session.ts delta is comment-only) — vs ~444 test lines. Well under the 500-line maintainer-awareness bar; the Tier-2 100%-confidence bar for core changes applies and is addressed in the code review below. Approach: every edit earns its place. The placeholder predicate is derived from the producer constant so the two cannot drift, the entrance-3 guard converts a silent-truncation corner into a loud abort, and the exact-match collision corner is documented and pinned as fail-safe rather than hidden. Legacy kind-less markers keep today's behavior — disclosed in the PR body. The increments land in the one function where the loss originates, and the diff carries nothing else. Risk: Stage 1e matches Moving on to code review. 🔍 中文说明感谢持续投入——本次 re-run 覆盖到 模板完整 ✓ 问题:不变且扎实——#9320 是已观测、有量化证据的 bug(llama.cpp prefill 日志:约 87k token 恢复成约 27k)。首轮之后的范围增长由评审驱动:每个增量都对应 /review 轮次中的具名发现(占位符计数漂移、无标记自动压缩),不是范围蔓延;结构性残余记录在 #9437 而非并入本 PR——对 bugfix PR 而言拆分合理。 方向:同前,对齐。/rewind 的静默上下文丢失是核心功能的可靠性漏洞。 规模:现已触及核心路径,此处更正首轮的这一节。拆分:约 109 行生产逻辑——core 30(占位符谓词 + 导出 + resume 风险注释)、cli 79(映射守卫、kind 标记、类型字段;ACP Session.ts 增量仅为注释)——对约 444 行测试。远低于 500 行维护者关注线;核心改动的 Tier-2 百分百信心标准适用,并在下方代码审查中落实。 方案:每处改动都有必要。占位符谓词从产生方常量推导,两者不会漂移;entrance-3 守卫把静默截断角落变成响亮中止;精确匹配碰撞角落被记录并钉为失败安全形态而非掩盖。无 kind 的旧标记保持现状行为——PR 正文已披露。增量都落在丢失发生的那一个函数里,diff 没有夹带其他内容。 风险:Stage 1e 命中 进入代码审查 🔍 — Qwen Code · qwen3.8-max Reviewed at |
Code reviewMy independent proposal for the increments pushed since the first pass: for the placeholder hazard, match the FULL generated shape with a pattern derived from the producer's own constant (a prefix match would miscount pasted placeholders), and for the marker-less auto-compaction corner, fail loud in the Verified along the way (on
No correctness, security, or convention blockers. One non-blocking observation, which round 8 already recorded as a deferred probe: the placeholder regex's interior class admits Files changed (13 of 13 shown)
TestingUnattended run — no PR code executed locally. Evidence is the PR's own CI on the reviewed commit, fetched via the API; all checks have completed. Final CI results for
Beyond CI, two evidence sources with honest attribution:
Not verified by this run: wire-level behavior of the two increments added after the verified head — that is the gap the lane below names. Sandboxed verification would settle this: 中文说明代码审查首轮之后新增增量的独立思路:占位符隐患应匹配完整生成形态、且模式从产生方常量推导(前缀匹配会误数粘贴进来的占位符);无标记自动压缩角落应在 核实过程(基于 未发现正确性、安全或规范阻断项。一条非阻断观察(round 8 已记录为延后探针):占位符正则内部字符类接受产生方会剥离的 测试无人值守运行——本地未执行任何 PR 代码。证据为被审 commit 上 PR 自身的 CI(经 API 获取):全部检查已完成且无失败;运行新增 historyMapping 与 microcompact 测试的是 CI 之外的两个证据来源(如实标注):8 月 17 日的沙箱 沙箱验证可以一锤定音: — Qwen Code · qwen3.8-max Reviewed at |
|
Confidence: 4/5 — the observed bug is fixed and A/B-proven, every review-round increment earned its place and closes a named hazard; the two reservations below are non-blocking. Stepping back: this is the fix the gate should wave through, and the eight review rounds did their job without burying it. The problem was real and measured (#9320's ~87k-becomes-~27k prefill logs), the core fix is the explicit one and carries a genuine A/B load-bearing proof from the sandboxed run, and each increment pushed afterwards answers a named finding — the placeholder exclusion keeps the UI-turn and API-prompt counts aligned, and the entrance-3 guard turns round 8's silent-loss scenario into a loud abort. I re-derived the guard's trigger condition against Two reservations, both named, neither blocking:
CI is fully green on the reviewed head (no pending 中文说明置信度:4/5 —— 已观测的 bug 已修复且有 A/B 承重证明,评审轮次的每个增量都名副其实、关闭了具名隐患;以下两点保留意见均为非阻断。 整体来看:这是门禁应当放行的修复,八轮评审也尽到了职责而没有把它拖死。问题真实且可量化(#9320 约 87k 变约 27k 的 prefill 日志),核心修法是显式的并带沙箱运行给出的真正 A/B 承重证明,其后推送的每个增量都对应一条具名发现——占位符排除保持 UI 轮次与 API prompt 计数对齐,entrance-3 守卫把 round 8 的静默丢失场景变成响亮中止。我对照 两点保留意见,均已点名、均非阻断:
被审 head 的 CI 已全绿(无未完成的 pull_request 运行),批准护栏不适用(内部分支、fix 类型),作者是维护者——现在批准,并钉在被审 commit 上。 — Qwen Code · qwen3.8-max Reviewed at |
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
|
@qwen-code /verify |
|
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: 37 passed · 0 failed · 37 total 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:37 通过 · 0 失败 · 37 总计 Verification reportVerification report — PR #9331Verdict: 中文摘要
Central claim + A/BCentral claim: after Harness:
The base-side reds are encoded as passing assertions (expected control failures), per the counting rule. Mutation/vacuity check (PR's own test files run against base source in the worktree): Secondary claims verified:
CorrectionsNone — the PR description's mechanism account (fast compression removes no user prompts, inserts no summary prefix) was checked against Findings (non-blocking)
Not covered
MethodologyEnvironment: CI verify container ( Evidence imagesHarness scripts and raw logs are in the workflow run artifacts (7-day retention). — Qwen Code · sandboxed verification |
|
@qwen-code /review --timeout=360 |
|
Qwen Code review request accepted. Review is queued in workflow run. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "agent 1c": could not verify whether the TUI submit path permits an image-only prompt with empty text (reachability of the media-clearing count desync above).; "agent 6a": verify whether any submission path (TUI image paste with empty text, ACP content blocks) can produce a user-role Content with top-level media and no text part —…; "agent 6a": verify whether UI compression marker items (and compressionKind ) are restored on session resume — if markers are reconstructed from the chat_compression sys….
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"agent 1c":could not verify whether the TUI submit path permits an image-only prompt with empty text (reachability of the media-clearing count desync above).;"agent 6a":verify whether any submission path (TUI image paste with empty text, ACP content blocks) can produce a user-role Content with top-level media and no text part —…;"agent 6a":verify whether UI compression marker items (and compressionKind ) are restored on session resume — if markers are reconstructed from the chat_compression sys…。
— qwen3.8-max via Qwen Code /review (v0.21.13)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "agent 1c": confirm whether the TUI can submit a user turn whose API Content has no text part (image-only paste / bare @image.png mention), which decides reachability o….
Deferred under the convergence posture (round 2, not a blocker) — recorded, not requested in this round:
packages/cli/src/ui/types.ts:112 — [review] R1-3 compressionKind stamped only on ephemeral UI items; persistence discards it — still stands, deferred (convergence posture)packages/cli/src/ui/commands/compressFastCommand.ts:141 — [review] D2-1 CompressionMessage renders fast and summarize markers identically; nothing user-visible distinguishes the two pathspackages/cli/src/ui/utils/historyMapping.test.ts:355 — [probe] D2-2 no test pins the summarize->fast marker ordering; a plausible variant passes all 31 existing tests yet breaks it
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"agent 1c":confirm whether the TUI can submit a user turn whose API Content has no text part (image-only paste / bare @image.png mention), which decides reachability o…。
收敛姿态下延后(第 2 轮,非阻断)——已记录,本轮不要求修改:共 3 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
…count Microcompaction's media clearing replaces the top-level inlineData/fileData parts of user entries with text placeholders. A media-only user entry (e.g. an image-only ACP prompt) never produced a UI user turn, but once cleared it satisfied isUserTextContent's naive text check, desynchronizing the API prompt count from the UI turn count. Rewinding across a fast marker then returned one prompt early and truncateHistory silently dropped a turn the UI still shows — the same hazard this PR fixes, newly reachable through the cross-fast-marker rewinds it enables. Exclude placeholder-prefixed text parts from the prompt count (entries mixing placeholders with real prompt text still count), and mirror the compressionKind persistence caveat onto ChatCompressionRecordPayload. Addresses review R1-1 (Critical) and R1-3 (minimal option).
Local E2E verification (macOS) — before / afterRan the manual repro from the Reviewer Test Plan against two bundled builds ( Scenario: 3 short turns →
Both builds render an identical TUI throughout the flow; the loss surfaces only in the request body, which is why the bug stays silent until the next request — consistent with the #9320 report. This also covers the macOS column: ✅ verified end-to-end on macOS. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed. Suggestions are inline.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
中文说明
仅完成部分审查,审查缺口已披露。 建议见行内评论。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
— qwen3.8-max via Qwen Code /review (v0.21.13)
- R3-1: exclude media-clear placeholders by the full generated shape (`[Old inline media cleared: <mime>]`) instead of the bare prefix, so a genuine user prompt that merely begins with the prefix still counts as a prompt. Adds a shared isClearedMediaPlaceholder() helper next to the producer constant and regression tests for both the truncation-late and spurious-compressed-error witness cases. - R3-2: document why the ACP session's private #isUserTextContent twin deliberately keeps the prefix check (ACP rewind maps against per-prompt file-history snapshots, which include media-only prompts). - R3-3: drop the zero-consumer MICROCOMPACT_CLEARED_MESSAGE and MICROCOMPACT_CLEARED_IMAGE_PREFIX barrel exports added by the prior commit; export only the new isClearedMediaPlaceholder() helper.
|
Closeout for the R3 review round (pushed 6dd5b6b):
Verification: core typecheck clean, touched files lint-clean, microcompact (79) and historyMapping (35) suites green; local review converged with no evidence-backed findings. All three threads resolved. |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "agent 1a": run the PR's new vitest suites (historyMapping/compress/microcompact) — the review worktree has no node_modules installed and no parent checkout node_modules to….
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"agent 1a":run the PR's new vitest suites (historyMapping/compress/microcompact) — the review worktree has no node_modules installed and no parent checkout node_modules to…。
— qwen3.8-max via Qwen Code /review (v0.21.13)
sanitizeMimeForPlaceholder returns '' for empty/whitespace-only/ bracket-only mimeTypes and the producer's ?? fallback only covers null/undefined, so microcompaction can emit `[Old inline media cleared: ]`. The consumer regex required one-or-more mime chars and rejected that shape, leaving a cleared media-only entry counted as a genuine prompt and desynchronizing the rewind prompt count. Accept the empty-mime shape and pin the producer/consumer round-trip for degenerate mimeTypes. Also correct the ACP-twin note in historyMapping.ts (the twin keeps the bare text-presence check, not a prefix check) and document the deliberate divergence reciprocally on Session.ts #isUserTextContent, with a Session.test.ts pin that cleared placeholders stay rewindable on the ACP side.
|
Patrol closeout for the new review findings on 6dd5b6b (fixed in 5298003):
Verification: microcompact 81/81, historyMapping 35/35, Session |
…nd mapping Records the known exact-match collision corner next to the media-clear placeholder exclusion and pins its fail-safe behavior with a test: a genuine prompt whose entire text equals a generated placeholder is excluded from the API prompt count, so later rewind targets abort loudly (-1) instead of truncating against a misaligned count. A durable fix needs a structural sentinel on cleared parts, which changes the persisted API history shape and stays out of scope for this PR.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Deferred under the convergence posture (round 5, not a blocker) — recorded, not requested in this round:
packages/cli/src/ui/utils/historyMapping.ts:111 — [probe] /restore checkpoints reinstate kind-less legacy fast markers misclassified as summarizing boundaries — silent-loss walk reproduced for restored sessions (probe: returns 1, expected 3…
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
收敛姿态下延后(第 5 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
Address the round-5 review thread within the scope that is not pending the maintainer structural decision: - The media-clear placeholder regex no longer accepts \r/\n/\t interiors: sanitizeMimeForPlaceholder normalizes them to spaces, so the producer can never emit them, and accepting them let multi-line user text that merely starts with the prefix be misclassified as a cleared-media placeholder. Every producible shape (including the empty-mime one) still matches. - The exact-match collision doc no longer claims "never silent history loss": mid-history, rewinding TO a colliding turn truncates one turn late (its prompt+response stays in model context while the UI removes the turn - under-deletion, not loss of context the UI keeps). The mid-history shape is now pinned next to the first-turn pin so a future structural sentinel updates both expectations.
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Deferred under the convergence posture (round 6, not a blocker) — recorded, not requested in this round:
packages/core/src/services/microcompaction/microcompact.ts:29 — [probe] CLEARED_MEDIA_PLACEHOLDER_RE's leading ^ anchor is pinned by no test — the anchor-removal mutant survives all 82+37 predicate/walk testspackages/cli/src/ui/utils/historyMapping.ts:111 — [probe] /restore checkpoints reinstate kind-less legacy fast markers misclassified as summarizing boundaries — silent-loss walk reproduced for restored sessions (round-5 deferral, still stan…
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
收敛姿态下延后(第 6 轮,非阻断)——已记录,本轮不要求修改:共 2 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
|
@qwen-code /review --timeout=360 |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Deferred under the convergence posture (round 7, not a blocker) — recorded, not requested in this round:
packages/core/src/services/microcompaction/microcompact.ts:29 — [probe] CLEARED_MEDIA_PLACEHOLDER_RE interior accepts '[' / leading-whitespace / >128-char interiors the producer cannot emit — collision corner wider than the documented emitt…
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
收敛姿态下延后(第 7 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Not explored to full depth (tool budget reached): "agent 6b": verify whether extractTextFromParts (resumeHistoryUtils.ts:50) strips <system-reminder> blocks — decides whether a [reminder, media] record rebuilds with …; "agent 6b": confirm the TUI session picker offers ACP-originated sessions for resume (cross-origin reachability for Finding 2).
Deferred under the convergence posture (round 8, not a blocker) — recorded, not requested in this round:
packages/core/src/services/microcompaction/microcompact.ts:33 — [probe] CLEARED_MEDIA_PLACEHOLDER_RE accepts '[' / >128-char / untrimmed interiors the producer cannot emit — collision surface wider than the documented limitationpackages/core/src/services/chatRecordingService.ts:484 — [review] new NOTE's 'exists only on ephemeral UI items today' claim is wrong — compression markers persist via outputHistoryItems and survive resume with kind intactpackages/cli/src/ui/utils/historyMapping.test.ts:355 — [probe] no test pins the summarize→fast marker ordering; a last-marker-of-any-kind mutant passes all 37 existing testspackages/core/src/services/microcompaction/microcompact.test.ts:171 — [probe] the ^ anchor and \] exclusion of CLEARED_MEDIA_PLACEHOLDER_RE are pinned by no negative test; two one-line mutants survive the suite
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
未探索到全部深度(达到工具调用预算):"agent 6b":verify whether extractTextFromParts (resumeHistoryUtils.ts:50) strips <system-reminder> blocks — decides whether a [reminder, media] record rebuilds with …;"agent 6b":confirm the TUI session picker offers ACP-originated sessions for resume (cross-origin reachability for Finding 2)。
收敛姿态下延后(第 8 轮,非阻断)——已记录,本轮不要求修改:共 4 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.13)
… compressed prefix (#9437 entrance 3)
|
R5-1 entrance (3) is now closed in |
|
@qwen-code /triage |
|
Sandboxed verification: The verification job did not complete (checkout, runner, or setup error) and produced no report. See the workflow run for details. 中文 — 判定:
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
Partially reviewed — gaps disclosed.
Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.
Deferred under the convergence posture (round 9, not a blocker) — recorded, not requested in this round:
packages/core/src/services/microcompaction/microcompact.ts:30 — [probe] CLEARED_MEDIA_PLACEHOLDER_RE accepts '[' and unbounded interiors the producer can never emitpackages/core/src/services/microcompaction/microcompact.test.ts:171 — [probe] the regex's leading ^ anchor is pinned by no negative test; the anchor-removal mutant survives the suitepackages/cli/src/ui/utils/historyMapping.ts:177 — [review] the new guard's -1 surfaces as the generic 'turn that was compressed' message, misleading for marker-less auto-compactionpackages/cli/src/acp-integration/session/Session.ts:3589 — [probe] the ACP twin's turn-0 shortcut has no counterpart to the new TUI fail-loud guard; probe-verified silent truncation
中文说明
仅完成部分审查,审查缺口已披露。
未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。
收敛姿态下延后(第 9 轮,非阻断)——已记录,本轮不要求修改:共 4 条(原文未翻译,列表见上方英文部分)。
— qwen3.8-max via Qwen Code /review (v0.21.14)
| item.compression.compressionStatus === CompressionStatus.COMPRESSED && | ||
| item.compression.compressionKind !== 'fast', |
There was a problem hiding this comment.
[Critical] R5-1: Round-9 status of the class finding — the rewind mapping still re-derives UI/API alignment from two independent representations (turn count vs prompt count, marker boundaries, text-shape matching), and the surface closes only structurally. The new commit acbfaf2 closed entrance (3)'s first-turn variant — the uiUserTurnCount === 0 shortcut now fails loud when a compressed prefix has no UI boundary, and the added test pins it — but three entrances remain open at this commit, each probe-verified this round:
(1) The same marker-less auto-compaction hazard on the sibling walk branch — the new guard covers only the shortcut. After auto-compaction absorbs M turns without a UI marker, the user sends K ≥ 2 new prompts and rewinds to an absorbed turn j (2 ≤ j ≤ K): the walk returns a silent index ≥ 0, so the UI truncates to the old turns while the model keeps [prelude, summary, ack, new prompts] — context silently diverges from everything displayed.
(2) Resumed cross-surface sessions — a media-only prompt cleared by /compress-fast is rebuilt by TUI resume as a counted [User message with attachments] turn while the rebuilt API entry is the placeholder now excluded from the walk, so the rewind mapping desyncs by one: rewinding to a later turn truncates one turn late (keeping prompt+response the UI removed), and rewinding to the last turn aborts with a spurious "was compressed" error.
(3) Restored kind-less legacy fast markers — resume reinstates recorded slash-command outputHistoryItems verbatim, so pre-PR /compress-fast markers (shipped since v0.18.0) come back without compressionKind and act as summarizing boundaries: rewinding a post-marker turn silently truncates early (observed index 0 where 2 is expected — dropping everything while the UI still shows the turns), and the kind-less marker bypasses the new guard because compressionIndex ≠ -1. The NOTE added on ChatCompressionRecordPayload warns about a reconstruction that already happens through this path.
Witness (probes against the real code at this commit):
(1) rewind to second absorbed turn (walk branch, unguarded):
AssertionError: expected 5 to be -1
hoisting the guard above the shortcut branch: 41/41 tests pass
(2) rewind to first text turn: 4 (pre-PR counting model: 2)
rewind to last turn: -1 (pre-PR: 4)
(3) restored marker: {compressionStatus: 1, no compressionKind}
first post-marker rewind: 0 (expected 2)
second post-marker rewind: 2 (expected 4)
Rounds 1–9 have each surfaced a state of this mechanism; per-entrance patches do not converge it. The structural closure (single authoritative source / stable-identity mapping) is tracked in #9437. If the rework stays out of scope, the probes validate per-entrance stopgaps: hoist the marker-less guard above the uiUserTurnCount === 0 branch so the walk fails loud too; exclude rebuilt attachment-only turns (no promptId) from the rewind count or mirror the placeholder exclusion on the UI side; treat a kind-less marker as a boundary only when the API history actually carries a compressed summary prefix.
中文说明
[严重] R5-1:类发现的第 9 轮状态——回退映射仍然从两个独立表示重新推导 UI/API 对齐(轮次计数对 prompt 计数、标记边界、文本形态匹配),该表面只能从结构上关闭。新提交 acbfaf2 关闭了入口 (3) 的第一轮变体——当存在压缩前缀而 UI 无摘要边界时,uiUserTurnCount === 0 捷径现在响亮失败,新增测试已钉住——但本提交上仍有三个入口敞开,均在本轮经探针验证:
(1) 同一"无标记自动压缩"危害存在于同函数的遍历分支——新守护只覆盖捷径。自动压缩吸收 M 轮(无 UI 标记)后,用户发送 K ≥ 2 个新 prompt 并回退到被吸收的轮次 j(2 ≤ j ≤ K):遍历返回静默的 ≥ 0 索引,UI 截断到旧轮次,而模型保留 [prelude, summary, ack, 新 prompt]——上下文与界面显示静默背离。
(2) 恢复的跨表面会话——被 /compress-fast 清理的纯媒体 prompt 在 TUI 恢复时被重建为计数的 [User message with attachments] 轮次,而重建后的 API 条目是被本次排除出遍历的占位符,回退映射因此错位一轮:回退到更晚轮次时延迟一轮截断(保留 UI 已移除的 prompt+回复),回退到最后一轮时报出虚假的"已压缩"错误。
(3) 还原的无 kind 旧版 fast 标记——会话恢复会原样还原记录的 slash 命令 outputHistoryItems,因此 PR 之前产生的 /compress-fast 标记(自 v0.18.0 发布)还原后不带 compressionKind,被当作摘要式边界:回退到标记后轮次时静默提前截断(实测索引 0,期望 2——UI 仍显示这些轮次时丢弃全部内容),且无 kind 标记因 compressionIndex ≠ -1 绕过新守护。ChatCompressionRecordPayload 上新增的 NOTE 警告的重建其实已经通过该路径发生。
证据(在本提交上对真实代码的探针):(1) 回退到第二个被吸收轮次(遍历分支,无守护):AssertionError: expected 5 to be -1;把守护提升到捷径分支之上后 41/41 测试通过。(2) 回退到第一个文本轮次:4(改动前计数模型为 2);回退到最后一轮:-1(改动前为 4)。(3) 还原的标记:{compressionStatus: 1, 无 compressionKind};标记后第一轮回退:0(期望 2);标记后第二轮回退:2(期望 4)。
第 1–9 轮每轮都发现该机制的一种状态;逐入口修补并不收敛。结构性关闭(单一权威来源/稳定标识映射)已在 #9437 跟踪。若重构暂不在范围内,探针已验证各入口的缓解措施:把无标记守护提升到 uiUserTurnCount === 0 分支之上使遍历也响亮失败;把重建出的仅附件轮次(无 promptId)排除出回退计数,或在 UI 侧镜像占位符排除;仅当 API 历史确实携带压缩摘要前缀时才把无 kind 标记当作边界。
— qwen3.8-max via Qwen Code /review (v0.21.14)
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
doudouOUC
left a comment
There was a problem hiding this comment.
Review summary
14 review agents examined the full diff (13 files, +551/-2, 217 source lines). Coverage: 3/3 chunks reviewed, all 14 agents completed work, no budget gaps.
Findings
No Critical findings. All code paths are correctly implemented, well-tested, and follow existing conventions. The two Critical findings from Agent 6a (attacker mindset) were verified against the producer code and determined to be false positives: microcompaction keeps the functionResponse part structure rather than replacing it with a text part, so the hasFunctionResponse guard correctly catches cleared tool result entries.
Verification
- Build (packages/core): Completed successfully
- Build (packages/cli): Blocked by pre-existing audio-capture node-gyp failure (missing Python) — not a PR defect
- Tests: Not run due to the pre-existing build chain blockage
Verdict
This PR correctly fixes the root cause of #9320 (silent context loss after compress-fast + rewind). The remaining structural class finding about the dual-representation rewind mapping is a known larger rework deferred to #9437.
|
Released in v0.21.15. |




What this PR does
/rewindmaps the selected UI turn to an API-history truncation point viacomputeApiTruncationIndex, and it treats every successful compression marker as a summarizing boundary./compress-fastemits the sameCompressionStatus.COMPRESSEDmarker, but it is purely rule-based: it removes no user prompts from the API history and inserts no summary prefix. So rewinding to any turn after a/compress-fastmarker collapsed the truncation anchor to just the startup context entry and silently dropped the entire pre-marker conversation — the UI history still rendered everything, so nothing looked wrong until the next request.This PR tags compression UI items with the path that produced them (
CompressionProps.compressionKind: 'summarize' | 'fast', set by/compressand/compress-fastrespectively) and makes the rewind mapping ignore'fast'markers as boundaries — turns are then mapped against the full history exactly as if no compression happened. Legacy markers without a kind keep the previous behavior.Why it's needed
Silent context loss. After
/compress-fast, rewinding to a post-marker turn truncates the model history to the startup context plus the re-entered prompt; everything before is gone without any warning. Reported in #9320 with llama.cpp prefill logs: resumed context was ~27k tokens instead of the expected ~87k.Reviewer Test Plan
How to verify
cd packages/cli && npx vitest run src/ui/utils/historyMapping.test.ts— the newwith fast (non-summarizing) compression markersblock covers the reported shape (rewind to first/later post-marker turn keeps the full pre-marker history, pre-marker turns stay reachable, a later summarizing compression still absorbs turns, legacy markers behave as before). Mutation check: reverting only thehistoryMapping.tspredicate makes 3 of the new tests fail.compressCommand.test.ts/compressFastCommand.test.tsassert the newcompressionKindon the added compression item./compress-fast→/rewindto the first turn after the compression entry → before this fix the next request carries only startup context + the new prompt; after it, the full pre-marker history is retained.Evidence (Before & After)
N/A — no rendering change; the compression banner and rewind selector UI are untouched. Behavior verified at the mapping function, which is where the loss originates.
Tested on
Environment (optional)
Unit tests only (
vitest runonpackages/cli).Risk & Scope
compressionKindand keep being treated as summarizing boundaries (today's behavior) — only markers created by this build onward get the corrected mapping.Session.#computeApiTruncationIndexForUserTurn) maps purely from API history and is not affected; file-history rewind interplay is unchanged.Linked Issues
Fixes #9320
Related: #9309 (same reporter; diagnosed in-thread as expected estimator-vs-API banner scaling, not context loss — the rewind interaction fixed here was the actual loss path).
中文说明
本 PR 做了什么
/rewind通过computeApiTruncationIndex把选中的 UI 轮次映射为 API 历史的截断点,并把每一个成功的压缩标记都当作"摘要式压缩"的边界。/compress-fast会发出相同的CompressionStatus.COMPRESSED标记,但它是纯规则压缩:既不会从 API 历史中移除任何用户 prompt,也不会插入摘要前缀。因此,回退到/compress-fast标记之后的任意轮次时,截断锚点会坍缩到只剩启动上下文条目,整段标记前的对话被静默丢弃——UI 历史仍然完整渲染,所以在下一次请求之前看不出任何异常。本 PR 为压缩 UI 条目打上产生来源标记(
CompressionProps.compressionKind: 'summarize' | 'fast',分别由/compress与/compress-fast设置),并让回退映射忽略'fast'标记的边界作用——轮次映射将基于完整历史进行,等同于没有发生过压缩。没有该字段的旧标记保持原有行为。为什么需要
静默上下文丢失。
/compress-fast之后回退到标记后的轮次,会把模型历史截断到只剩启动上下文 + 重新输入的 prompt,之前的全部内容无声丢失。#9320 附带 llama.cpp prefill 日志:恢复后的上下文只有约 27k token,而预期是约 87k。评审验证计划
如何验证
cd packages/cli && npx vitest run src/ui/utils/historyMapping.test.ts——新增的with fast (non-summarizing) compression markers用例覆盖报告中的形态(回退到标记后第一/后续轮次时保留完整标记前历史、标记前轮次仍可回退、其后的摘要式压缩仍会吸收轮次、旧标记行为不变)。变异验证:仅还原historyMapping.ts中的判定条件,新增用例中有 3 个会失败。compressCommand.test.ts/compressFastCommand.test.ts断言了新增的compressionKind字段。/compress-fast→/rewind选择压缩条目后的第一个轮次 → 修复前下一次请求只带启动上下文 + 新 prompt;修复后完整保留标记前历史。前后证据
N/A——无渲染变化;压缩横幅与回退选择器 UI 均未改动。行为在映射函数层验证,丢失正是发生在该处。
测试环境
环境(可选)
仅单测(
packages/cli下vitest run)。风险与范围
compressionKind,仍按摘要式边界处理(即现状行为)——只有本版本之后产生的标记才获得修正后的映射。Session.#computeApiTruncationIndexForUserTurn)纯粹基于 API 历史映射,不受影响;文件历史回退的交互未改动。关联 Issue
Fixes #9320
相关:#9309(同一 reporter;线程中已诊断为估算值与 API 计数的口径差异,并非上下文丢失——本 PR 修复的回退交互才是真正的丢失路径)。