fix(core): allow rewind after compressed history - #6358
Conversation
|
Thanks for the PR, @yiliang114! Template looks good ✓ — all required sections present with bilingual body. Problem: observed bug with evidence. Fixes #6318 — a user reports that after Direction: aligned, minimal. The fix is in the right place — Approach: tight. The diff is 108 production lines / 262 test lines across 5 files. Production changes are in Core-module scope: 108 production lines total, well under the 500-line threshold. Downstream consumers ( Moving on to code review and testing. 🔍 中文说明感谢 @yiliang114 的 PR! 模板完整 ✓ — 所有必填部分齐全,含双语正文。 问题:有证据的真实 bug。 修复 #6318 —— 用户报告 方向:对齐且最小化。 修复位置正确—— 方案:紧凑。 5 个文件,108 行生产代码 / 262 行测试。生产代码改动位于 Core 模块范围:共 108 行生产代码,远低于 500 行阈值。下游消费者 进入代码审查与测试 🔍 — Qwen Code · qwen3.7-max |
Code ReviewThe revised diff is clean and focused. Two improvements over the initial version stand out: 1. Dual-sentinel corroboration. 2. UI-side compression boundary guard. Reuse check: No correctness bugs, security issues, or regressions. No drive-by refactors. TestingUnit tests — all pass on this branch (worktree verification): The 10 new The 3 new Real-scenario tmux test — not run in CI. The However, the author @yiliang114 provided comprehensive tmux verification in their follow-up comment using a deterministic fake OpenAI-compatible server. Key observations from their terminal output: Pre-compression rewind is correctly rejected. And: Post-compression rewind succeeds and pre-fills the prompt. The resend after rewind also works: This covers the three behaviors the fix introduces: pre-compression targets rejected, post-compression targets rewindable, and post-rewind resend functional. Screenshots are attached in the author's comment. 中文说明代码审查修订后的 diff 干净且聚焦。相比初始版本有两个显著改进: 1. 双 sentinel 验证。 2. UI 侧压缩边界守卫。 复用检查: 初始评审中标记的跨 package 耦合问题在修订版中通过正向识别方式( 无正确性 bug、安全问题或回归。无 drive-by refactor。 测试单元测试 —— 在该分支上全部通过(worktree 验证):
真实场景 tmux 测试 —— CI 中未执行。 但作者 @yiliang114 在后续评论中使用确定性 fake OpenAI 兼容服务器提供了完整的 tmux 验证。其终端输出中的关键观察: 压缩前 rewind 被正确拒绝。而: 压缩后 rewind 成功并预填 prompt。rewind 后的重发也正常工作。这覆盖了修复引入的三种行为:压缩前目标被拒绝、压缩后目标可 rewind、rewind 后重发正常。截图附在作者的评论中。 — Qwen Code · qwen3.7-max |
|
Re-triage after author pushed review fixes. Template ✓ Problem: observed bug — #6318 reports Direction: aligned and minimal. Extending Size: 94 production lines (Session.ts +8/-2, historyMapping.ts +4/-1, environmentContext.ts +82/-6) + 180 test lines. Not applicable for core-module thresholds. Approach: tight. The review feedback from @wenshao was addressed thoughtfully:
Moving on to code review. 🔍 中文说明作者推送了评审修复后的重新 triage。 模板 ✓ 问题: 已观察到的 bug — #6318 报告 方向: 对齐且最小。通过 opt-in 的 规模: 94 行生产代码(Session.ts +8/-2,historyMapping.ts +4/-1,environmentContext.ts +82/-6)+ 180 行测试。不触及核心模块阈值。 方案: 紧凑。@wenshao 的评审反馈被认真处理:冲突调用方通过 进入代码审查 🔍 — Qwen Code · qwen3.7-max |
|
This PR does exactly what it says and does it well. The problem is real (linked issue #6318 with screenshot and reproduction), the fix is at the right abstraction level ( My independent proposal matched the PR's approach: opt-in compressed-prefix detection on The test suite has teeth — 12 new tests covering the happy path, degraded fallback, NOOP compression, and negative cases (wrong sentinel, missing resume trailer). The new tests don't compile against base source, proving they actually exercise the new code. Two independent real-build TUI verifications (maintainer + author) confirm the fix works end-to-end: pre-compression targets are blocked, post-compression targets rewind correctly, and the re-sent prompt continues normally. The A/B matrix across different pre-compression turn counts (P=1, P≥2) shows the fix is correct regardless of history depth. Non-blocking note (echoing @wenshao): the dual-sentinel detection couples to hardcoded English strings. Correct today, but if either string is reworded or localized, the mapping silently regresses. A structural marker on the Approving. ✅ 中文说明这个 PR 准确解决了问题,做得很好。 问题是真实的(关联 issue #6318 附截图和复现),修复在正确的抽象层级( 我的独立方案与 PR 方案一致:在 测试有效——12 个新增测试覆盖正常路径、degraded fallback、NOOP 压缩和反面用例(错误 sentinel、缺少 resume trailer)。新测试在 base 源码上无法编译,证明确实在测试新代码。 两次独立的真实构建 TUI 验证(maintainer + 作者)确认修复端到端有效:压缩前目标被拦截,压缩后目标正确 rewind,重新发送的 prompt 正常继续。不同压缩前轮次数(P=1, P≥2)的 A/B 矩阵显示修复在任何历史深度下都正确。 非阻塞建议(与 @wenshao 一致):双重 sentinel 检测耦合到硬编码英文字符串。目前正确,但如果字符串被改写或本地化,映射会静默回退。长期来看,在 批准 ✅ — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Suggestions — commit
|
| File | Issue | Suggested fix |
|---|---|---|
packages/core/src/utils/environmentContext.ts:617 |
Comment references isUserTextContent but the actual function introduced is isUserTextContextEntry. These are different functions — isUserTextContent in historyMapping.ts also excludes <system-reminder> entries. |
Change isUserTextContent to isUserTextContextEntry in the comment. |
packages/core/src/core/client.test.ts |
No test covers refreshStartupContextReminder or restoreStartupContextAfterCompaction with a compressed-history prefix — these are the paths where the Critical regressions manifest. |
Add test cases for both functions with a compressed prefix in currentHistory. |
packages/core/src/utils/environmentContext.test.ts |
isUserTextContextEntry has 4 logical branches but only 2-3 are tested. Missing: model role at history[2], empty parts at history[2], mixed text+functionResponse parts. |
Add 2-3 short test cases in the getStartupContextLength describe block. |
packages/core/src/utils/environmentContext.ts:621 |
Sentinel 'Got it. Thanks for the additional context!' hardcoded in 3 files (postCompactAttachments.ts, chatCompressionService.ts, environmentContext.ts) with no shared constant. |
Extract to a named constant (e.g., COMPACT_HISTORY_ACK) in a shared module. |
packages/core/src/utils/environmentContext.ts:618 |
The sentinel is checked only on history[1] with no corroboration from history[0]. A false-positive match would cause the same data loss as the Critical findings. |
Check that history[0].parts[0].text ends with RESUME_TRAILER (which postProcessSummary always appends) before accepting the match. |
— qwen3.7-max via Qwen Code /review
|
Review follow-up summary:
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No high-confidence review issues found. The fix is well-targeted and the includeCompressed opt-in cleanly separates rewind-specific detection from startup-context refresh/restore paths.
Needs human review (low confidence):
historyMapping.test.ts: No integration test forcomputeApiTruncationIndexwith compressed-history API array.Session.test.ts: No integration test forgetRewindableUserTurnCount/rewindToTurnwith compressed-history prefix.environmentContext.ts:636:RESUME_TRAILERinpostCompactAttachments.tsis file-private whiledetectCompressedPrefixLengthuses a substring match — if the trailer text changes, tests still pass but detection breaks silently.
— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
Code ReviewIndependent proposal: To fix Comparison with the diff: the PR matches this approach exactly and adds good defense-in-depth. Key observations:
Reuse check: no violations. The new helpers are appropriately scoped to the rewind path. No cross-package coupling issues. No correctness bugs, security issues, or regressions found. No drive-by refactors or scope creep. TestingUnit tests — all pass on the PR branch: Typecheck — clean ( Real-build TUI verification — both @wenshao (maintainer) and @yiliang114 (author) independently verified this fix with real CLI builds driven through tmux against deterministic mock providers. Key results from @wenshao's A/B matrix (P = pre-compression user turns): @yiliang114's TUI evidence: Both verifications confirm: pre-compression targets are correctly blocked, post-compression targets rewind correctly, and the re-sent prompt continues normally. 中文说明代码审查独立方案: 要修复 与 diff 对比:PR 完全匹配此方案,并增加了良好的纵深防御。 关键观察:
复用检查: 无违规。新 helper 适当限定在 rewind 路径内。 未发现正确性 bug、安全问题或回退。无顺手重构或范围蔓延。 测试单测 — PR 分支全部通过: 类型检查 — 通过( 真实构建 TUI 验证 — @wenshao(maintainer)和 @yiliang114(作者)均独立使用真实 CLI 构建 + tmux + 确定性 mock provider 验证了此修复。关键结果:P ≥ 2 时 rewind 到最新压缩后 prompt 在 main 上被拦截(#6318),在当前 head 上成功。pre-compression 目标被正确拦截。 — Qwen Code · qwen3.7-max |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅
Verification report — real
|
| rewind to | main |
this PR |
|---|---|---|
| 1st post‑compress prompt | 3 ✅ correct |
5 ❌ keeps a turn you asked to drop |
| 2nd post‑compress prompt | 5 ✅ correct |
7 ❌ keeps a turn you asked to drop |
| 3rd post‑compress prompt (last) | 7 ✅ correct |
-1 ❌ "Cannot rewind" |
Live confirmation, same P=1 scenario, Restore conversation only:
| build | rewind to the last post‑compress prompt (the #6318 action) |
|---|---|
main |
● Conversation rewound. Edit your prompt and press Enter to continue. ✅ |
| this PR | ✕ Cannot rewind to a turn that was compressed. Try a more recent turn. ❌ |
So for the very common "one prompt → /compress → a few more prompts" flow, main rewinds correctly to any later prompt; this PR truncates to the wrong turn for the earlier ones and errors on the last.
Finding 2 — #6318's own scenario (≥2 prompts before /compress) is still broken
P = 2, three prompts after /compress:
| rewind to | main |
this PR |
|---|---|---|
| 1st post‑compress prompt | 5 (off by one) |
7 (off by two) |
| 2nd post‑compress prompt | 7 (off by one) |
-1 ❌ |
| 3rd post‑compress prompt (last) | -1 ❌ |
-1 ❌ |
#6318's layout (some conversation → /compress → prompt 1/2/3 → rewind to prompt 3) is P ≥ 2, and rewinding to the last prompt still returns -1 after this PR — I reproduced that error live at P=3 too. The reported symptom is unchanged.
Root cause
computeApiTruncationIndex compares a UI‑derived count (uiUserTurnCount, which includes the retained pre‑compression turns) against an API walk from startIndex. On main, startIndex skipped only the startup reminder and the summary was counted as one API user prompt — which offsets exactly one collapsed pre‑compression turn, so P=1 lined up perfectly. This PR makes startIndex also skip the summary, so it stops offsetting any pre‑compression UI turn; the walk then lands one turn late per pre‑compression turn (P=1 → off‑by‑one / -1, P≥2 → worse). It is only self‑consistent when P=0, or when the count is purely API‑derived — which is exactly the ACP Session.ts case (getRewindableUserTurnCount / #computeApiTruncationIndexForUserTurn both count from apiHistory only), or when the UI actually collapses pre‑compression turns (collapseOnResume, off by default).
What is fine
- The added
getStartupContextLengthunit tests are correct and load‑bearing: with the fix they pass (13); revertingdetectCompressedPrefixLengthfails exactly the 5 compressed‑prefix cases. P = 0improves (rewinding to the first post‑compression prompt now keeps the summary instead of dropping it:main1→ this PR3).- The two ACP call sites in
Session.tslook correct — that path counts turns only from the API history, so skipping the compressed prefix keeps its count and mapping consistent. (Not exercised end‑to‑end this session — assessment is from code + the same helper behavior.)
Suggestion
The real TUI fix needs both sides to agree: if the API collapses N pre‑compression turns into one summary, the UI turn count used for rewind must treat those N turns as non‑rewindable (collapse them) — otherwise computeApiTruncationIndex is always off by N. As written, the historyMapping.ts change moves the CLI the wrong way for the default live‑session flow. Consider splitting the ACP/helper change (good) from the CLI computeApiTruncationIndex change and reworking the latter, or gating post‑compression targets in the rewind selector so the user gets a coherent result instead of a wrong/erroring one.
Reproduce
Clean worktree, npm ci; launch node scripts/cli-entry.js with empty HOME and OPENAI_API_KEY/OPENAI_BASE_URL/OPENAI_MODEL pointed at any mock that (a) echoes normal turns and (b) returns a non‑empty <state_snapshot> for the /compress side‑query (identify it by the kick‑off message First, reason in your <analysis> block…). Then: one prompt → /compress → a few prompts → /rewind → pick a post‑compression prompt. main rewinds; this PR shows Cannot rewind to a turn that was compressed.
中文版(供合并参考)
验证报告 —— 真实 tmux 端到端 + 确定性回放
我用 tmux 驱动真实交互式 TUI(干净 HOME、OPENAI_* 指向 mock、真实 /compress side‑query、真实 /rewind 选择器)验证本 PR;并把从这次真实会话逐字抓取的 history 喂给真实编译产物里的 computeApiTruncationIndex 做确定性回放。全部跑在本 PR 的构建上(在 fix/6318-rewind-after-compress 干净 worktree 里 npm ci;fix 已在 packages/core/dist、packages/cli/dist 和 dist/ bundle 中)。“改动前/后”只通过还原 detectCompressedPrefixLength 得到——这会让 getStartupContextLength(h,{includeCompressed:true}) 的行为与 main 完全一致。
结论:Session.ts)改动看起来没问题;问题集中在 packages/cli/src/ui/utils/historyMapping.ts(computeApiTruncationIndex)。
原因:UI 保留压缩前轮次,API 把它们折叠了
真实 /compress 之后,压缩前轮次仍留在 UI history 里(compress 只追加 type:'compression' 标记;collapseOnResume 默认 false),所以 computeApiTruncationIndex 的 uiUserTurnCount 仍会数它们。它索引的 API history 如下(逐字抓取,/compress 前 1 条、后 1 条):
[0] user "<system-reminder> …tools…" ← 压缩后 startChat 重新前置的 startup reminder
[1] user "<state_snapshot>… Resume the prior task …" ← summary
[2] model "Got it. Thanks for the additional context!" ← ack
[3] user "<system-reminder>…date… | beta post-compress turn" ← 要 rewind 到的压缩后 prompt
[4] model "REPLY[…]"
getStartupContextLength 结果:main = 1,本 PR = 3(跳过了 summary+ack)。但 UI 仍会数压缩前那条 prompt,于是两边相差“压缩前轮次数”。
发现 1 —— 回退常见用例:一条 prompt → /compress → 再几条 prompt(P=1)
P = /compress 前的真实用户轮次数。把抓取到的 history(按真实抓取的轮次结构克隆扩展到 3 条压缩后 prompt)喂给真实 computeApiTruncationIndex——返回值是“保留多少条 Content”;数值不对会静默截断到错误的轮次,-1 会报错:
P = 1,/compress 后 3 条 prompt:
| rewind 到 | main |
本 PR |
|---|---|---|
| 第 1 条压缩后 prompt | 3 ✅ 正确 |
5 ❌ 保留了本该丢弃的轮次 |
| 第 2 条压缩后 prompt | 5 ✅ 正确 |
7 ❌ 保留了本该丢弃的轮次 |
| 第 3 条压缩后 prompt(最后一条) | 7 ✅ 正确 |
-1 ❌ “Cannot rewind” |
真实 TUI 确认,相同 P=1 场景,Restore conversation only:
| 构建 | rewind 到最后一条压缩后 prompt(即 #6318 的操作) |
|---|---|
main |
● Conversation rewound. Edit your prompt and press Enter to continue. ✅ |
| 本 PR | ✕ Cannot rewind to a turn that was compressed. Try a more recent turn. ❌ |
所以对“一条 prompt → /compress → 再几条 prompt”这个很常见的流程,main 能正确 rewind 到之后的任意一条;本 PR 对靠前的会截断到错误轮次,对最后一条直接报错。
发现 2 —— #6318 自己的场景(/compress 前 ≥2 条)仍然是坏的
P = 2,/compress 后 3 条 prompt:
| rewind 到 | main |
本 PR |
|---|---|---|
| 第 1 条压缩后 prompt | 5(偏一条) |
7(偏两条) |
| 第 2 条压缩后 prompt | 7(偏一条) |
-1 ❌ |
| 第 3 条压缩后 prompt(最后一条) | -1 ❌ |
-1 ❌ |
#6318 的布局(some conversation → /compress → prompt 1/2/3 → rewind 到 prompt 3)是 P ≥ 2,本 PR 之后 rewind 到最后一条仍返回 -1——我在 P=3 下也真实复现了该报错。报告的症状没有变化。
根因
computeApiTruncationIndex 拿一个基于 UI 的计数(uiUserTurnCount,含被保留的压缩前轮次)与一个从 startIndex 开始的 API 遍历对比。在 main 上,startIndex 只跳过 startup reminder,summary 被当成 1 条 API 用户 prompt 计数——恰好抵消一条被折叠的压缩前轮次,所以 P=1 完全对齐。本 PR 让 startIndex 也跳过 summary,于是它不再抵消任何压缩前 UI 轮次;遍历会按“每条压缩前轮次”偏晚一条(P=1 → 偏一条 / -1;P≥2 → 更糟)。只有在 P=0、或计数完全来自 API 时才自洽——后者正是 ACP Session.ts 的情形(getRewindableUserTurnCount / #computeApiTruncationIndexForUserTurn 都只从 apiHistory 计数),或 UI 真的折叠了压缩前轮次(collapseOnResume,默认关闭)。
没问题的部分
- 新增的
getStartupContextLength单测正确且有效:带 fix 全过(13 个),还原detectCompressedPrefixLength后正好挂掉 5 个压缩前缀用例。 P = 0变好了(rewind 到压缩后第一条现在会保留 summary:main1→ 本 PR3)。Session.ts里两个 ACP 调用点看起来是对的——该路径只从 API history 计数,跳过压缩前缀能让其计数与映射保持一致。(本次没端到端跑 ACP/serve 路径,此判断来自代码 + 同一 helper 行为。)
建议
真正的 TUI 修复需要两边一致:如果 API 把 N 条压缩前轮次折叠成一个 summary,UI 用于 rewind 的轮次计数也必须把这 N 条视为不可 rewind(折叠掉),否则 computeApiTruncationIndex 永远偏 N。当前写法让 CLI 在默认实时会话流程里走反了方向。建议把 ACP/helper 改动(好的部分)与 CLI 的 computeApiTruncationIndex 改动拆开并重做后者;或在 rewind 选择器里屏蔽压缩后的目标,让用户得到一致结果而不是错误/报错。
复现
干净 worktree + npm ci;用空 HOME 和指向任意 mock 的 OPENAI_API_KEY/OPENAI_BASE_URL/OPENAI_MODEL 启动 node scripts/cli-entry.js,mock 需要:(a) 回显普通轮次,(b) 对 /compress 的 side‑query(以 kick‑off 消息 First, reason in your <analysis> block… 识别)返回非空 <state_snapshot>。然后:一条 prompt → /compress → 再几条 prompt → /rewind → 选压缩后的某条。main 能 rewind,本 PR 报 Cannot rewind to a turn that was compressed。
|
Addressed the TUI rewind regression from the verification report. What changed:
Local verification:
|
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. |
Follow-up verification report — real
|
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
No new high-confidence issues found on independent review. The includeCompressed opt-in cleanly separates rewind-specific detection from startup refresh/restore paths, and the compression-boundary guard in computeApiTruncationIndex correctly blocks pre-compression rewind targets. Downgraded from Approve to Comment: CI still running.
— qwen3.7-max via Qwen Code /review
|
@qwen-code /triage |
✅ Verification report (current head
|
base 5c8af1a1f (main) |
pre‑fix 8e8e7a87e (earlier report's head) |
current 09f144e04 |
|
|---|---|---|---|
| P = 1 | 7 → rewound ✅ |
-1 → blocked ❌ |
7 → rewound ✅ |
| P ≥ 2 (the #6318 layout) | -1 → blocked (#6318) ❌ |
-1 → blocked ❌ |
7 → rewound ✅ |
- main: works at P=1, broken at P≥2 → this is Unable to /rewind after /compress even when just rewinding to non-compressed position. #6318.
8e8e7a87e: the API‑side‑only fix additionally broke P=1 (the regression the earlier report correctly caught), still broken at P≥2.09f144e04(current): thefindLastSuccessfulCompressionIndexchange makes the UI turn count start after the compression marker, so both sides agree again → every case correct.
The post‑/compress API history is P‑independent (the pre‑compression turns genuinely collapse into a single summary+ack), which is why the current head is correct regardless of P. On that same captured history the current head also maps the earlier post‑compress prompts correctly (POST‑1 → 3, POST‑2 → 5) and still correctly blocks rewinding into the compressed region (-1).
Real‑TUI screenshots — current head, same driven session, P = 3
/rewind opens this identical selector (newest turn pre‑selected):
Rewind Conversation (6 turns)
#1 First pre-compress question alpha ┐
#2 Second pre-compress question bravo │ collapsed into the summary by /compress
#3 Third pre-compress question charlie ┘
#4 Post-compress prompt delta
#5 Post-compress prompt echo
› #6 Post-compress prompt foxtrot GAMMA-LATEST ← newest, pre-selected
Choosing it → "Restore conversation only":
base 5c8af1a1f ❌
> /rewind
✕ Cannot rewind to a turn that was compressed. Try a more recent turn.
PR 09f144e04 ✅
● Conversation rewound. Edit your prompt and press Enter to continue.
* Post-compress prompt foxtrot GAMMA-LATEST ← prompt pre-filled, ready to edit
Tests (on PR head)
| suite | result |
|---|---|
core/src/utils/environmentContext.test.ts |
59 / 59 pass |
cli/src/ui/utils/historyMapping.test.ts |
26 / 26 pass |
The new tests have teeth — against base source they don't even compile (TS2554: Expected 1 arguments, but got 2).
Minor, non‑blocking note
Prefix detection is coupled to two hard‑coded English sentinels — firstText.includes('Resume the prior task') and the exact ack 'Got it. Thanks for the additional context!'. Correct for today's composePostCompactHistory output, but if either string is ever reworded/localized the mapping silently regresses to #6318; a structural marker would be sturdier.
Real CLI built from 5c8af1a1f, 8e8e7a87e, 09f144e04 · interactive TUI via tmux · deterministic mock OpenAI provider · computeApiTruncationIndex call site instrumented to dump real inputs/output · isolated git workspace, file‑checkpointing on (default).
🀄 中文版(供 merge 参考)
✅ 验证报告(当前 head 09f144e04)—— 修复 #6318,且无回退
结论:在当前 head 上,本 PR 正确修复了 #6318,且没有回退“压缩前只有一条 prompt”的情况。建议合并。
关于本 PR 上更早那条
⚠️ 报告 —— 那条 review 发布于2026‑07‑06 15:17Z,当时 head 是8e8e7a87e。在那个 commit 上只有API 侧的跳过(getStartupContextLength({includeCompressed})),而computeApiTruncationIndex仍然会数被保留的压缩前 UI 轮次,两侧因此不一致——它的结论对那个 commit 是正确的。之后的 commit09f144e04“align rewind mapping after compression”(约 11 小时后推送,2026‑07‑07 02:34Z)正好补上了它建议的computeApiTruncationIndex改动。我在真实构建上重新验证了这三个状态,所以更早那条报告现在是已被解决 / 过期,而非被推翻。
方法:分别用每个 commit 真实编译出 CLI,通过 tmux 驱动真实交互式 TUI,对接确定性 mock OpenAI provider(真实 /compress side‑query、真实 /rewind 选择器)。我在真实的 computeApiTruncationIndex 调用点插桩,dump 出 getHistoryShallow() 和返回的 index,因此下表每个数值都是真实会话里的真实值,不是重写复现。P = /compress 之前的真实用户轮次数。
权威真实构建 A/B —— rewind 到最新的压缩后 prompt(即 #6318 的操作)
base 5c8af1a1f(main) |
修复前 8e8e7a87e (更早报告的 head) |
当前 09f144e04 |
|
|---|---|---|---|
| P = 1 | 7 → 成功 ✅ |
-1 → 拦截 ❌ |
7 → 成功 ✅ |
| P ≥ 2 (#6318 的布局) | -1 → 拦截(#6318) ❌ |
-1 → 拦截 ❌ |
7 → 成功 ✅ |
- main:P=1 正常,P≥2 坏 → 即 Unable to /rewind after /compress even when just rewinding to non-compressed position. #6318。
8e8e7a87e:只改 API 侧导致 P=1 也坏了(更早报告正确抓到的回退),P≥2 仍坏。09f144e04(当前):findLastSuccessfulCompressionIndex让 UI 轮次计数从压缩标记之后开始,两侧重新一致 → 所有情况都正确。
/compress 之后的 API history 是与 P 无关的(压缩前轮次确实被折叠成单条 summary+ack),这正是当前 head 无论 P 多少都正确的原因。在同一份抓取到的 history 上,当前 head 对更靠前的压缩后 prompt 也映射正确(POST‑1 → 3、POST‑2 → 5),并且仍然正确拦截“rewind 进入已压缩区域”(-1)。
真实 TUI 截图 —— 当前 head,同一驱动会话,P = 3
/rewind 打开的选择器(最新 turn 默认选中):
Rewind Conversation (6 turns)
#1 First pre-compress question alpha ┐
#2 Second pre-compress question bravo │ 被 /compress 折叠进 summary
#3 Third pre-compress question charlie ┘
#4 Post-compress prompt delta
#5 Post-compress prompt echo
› #6 Post-compress prompt foxtrot GAMMA-LATEST ← 最新,默认选中
选中它 → “Restore conversation only”:
base 5c8af1a1f ❌
> /rewind
✕ Cannot rewind to a turn that was compressed. Try a more recent turn.
PR 09f144e04 ✅
● Conversation rewound. Edit your prompt and press Enter to continue.
* Post-compress prompt foxtrot GAMMA-LATEST ← prompt 已回填,可直接编辑
测试(PR head 上)
| 套件 | 结果 |
|---|---|
core/src/utils/environmentContext.test.ts |
59 / 59 通过 |
cli/src/ui/utils/historyMapping.test.ts |
26 / 26 通过 |
新测试是有效的:在 base 源码上它们甚至无法编译(TS2554: Expected 1 arguments, but got 2)。
一点小建议(不阻塞合并)
前缀检测依赖两个硬编码英文 sentinel —— firstText.includes('Resume the prior task') 和精确的 ack 'Got it. Thanks for the additional context!'。对当前 composePostCompactHistory 的输出是正确的;但一旦这两个字符串被改写 / 本地化,映射会静默退回 #6318,换成结构化标记会更稳。
真实 CLI 分别构建自 5c8af1a1f、8e8e7a87e、09f144e04 · tmux 驱动交互式 TUI · 确定性 mock OpenAI provider · 对 computeApiTruncationIndex 调用点插桩 dump 真实输入/输出 · 隔离 git 工作区,file‑checkpointing 默认开启。
|
@qwen-code /triage |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship. ✅




What this PR does
This PR teaches the rewind turn-counting path to treat the synthetic
/compresssummary prefix as startup context, so real user prompts after compression remain rewindable. It recognizes the compression ack pair and skips the optional post-compression attachment entry only when that third entry is user text, not a tool/function response.Why it's needed
Fixes #6318. After
/compress, the saved summary is plain user-shaped content, so the existing rewind mapping counted it as a real prompt and could reject rewinding to later user prompts. This keeps the fix bounded to compressed-history prefix detection and preserves degraded tool-call fallback behavior.Reviewer Test Plan
How to verify
Reviewers can confirm that histories beginning with a compressed summary plus
Got it. Thanks for the additional context!no longer contribute synthetic turns to/rewindindexing. The added tests cover compressed history without attachments, compressed history with post-compact text/reminder attachments, and a degraded fallback where a function response follows the compression ack and must not be skipped as an attachment entry.Evidence (Before & After)
N/A for UI evidence. Local verification passed:
cd packages/core && npx vitest run src/utils/environmentContext.test.ts,cd packages/cli && npx vitest run src/ui/utils/historyMapping.test.ts, andnpm run typecheck.npm installalso completed successfully and ran the repository prepare/build/bundle lifecycle in this clean worktree.Tested on
Environment (optional)
macOS local worktree, Node/npm workspace dependencies installed with
npm install.Risk & Scope
Linked Issues
Fixes #6318
中文说明
What this PR does
这个 PR 让
/rewind的用户轮次计数逻辑把/compress生成的摘要前缀视为结构性上下文,因此压缩之后真实输入的用户 prompt 仍然可以被 rewind。它会识别压缩专用的 ack pair,并且只有第三条记录是用户文本时才跳过可选的压缩后附件记录;如果第三条是 tool/function response,则不会误判为附件记录。Why it's needed
Fixes #6318。
/compress之后保存的摘要是普通 user 形态的内容,原来的 rewind mapping 会把它当成真实用户 prompt 计数,从而可能拒绝 rewind 到后续用户 prompt。这个修复只限定在压缩历史前缀识别上,并保留 degraded tool-call fallback 的行为。Reviewer Test Plan
How to verify
Reviewer 可以确认以压缩摘要和
Got it. Thanks for the additional context!开头的历史不再把这些 synthetic turns 计入/rewind索引。新增测试覆盖了无附件的压缩历史、有压缩后文本/提醒附件的压缩历史,以及 compression ack 后跟 function response 的 degraded fallback,后者不能被跳过为附件 entry。Evidence (Before & After)
非 UI 变更,无截图。已通过本地验证:
cd packages/core && npx vitest run src/utils/environmentContext.test.ts、cd packages/cli && npx vitest run src/ui/utils/historyMapping.test.ts、npm run typecheck。在这个干净 worktree 中npm install也执行成功,并跑完了仓库的 prepare/build/bundle 生命周期。Tested on
Environment (optional)
macOS 本地 worktree,使用
npm install安装 Node/npm workspace 依赖。Risk & Scope
Linked Issues
Fixes #6318