Skip to content

fix(core): preserve rewind parents after resume - #5923

Merged
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/rewind-resumed-parent-chain
Jun 27, 2026
Merged

fix(core): preserve rewind parents after resume#5923
wenshao merged 1 commit into
QwenLM:mainfrom
tt-a1i:fix/rewind-resumed-parent-chain

Conversation

@tt-a1i

@tt-a1i tt-a1i commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

What this PR does

Preserves persisted parent UUID edges when rebuilding rewind turn boundaries after a session resume. The CLI resume path already rebuilt those boundaries; this PR makes that rebuild use each resumed record's stored parentUuid instead of inferring from the adjacent item in the reconstructed list, and applies the same rebuild step to ACP session resume paths used by VS Code and daemon clients.

Why it's needed

Issue #5920 reports resumed session history disappearing after an automatic rewind/edit flow. The recorder stores a tree of records, and the rewind marker must be attached to the record that was the parent of the target user turn. If the boundary is rebuilt as null, the next resume can stop walking the parent chain too early and drop earlier conversation history.

Reviewer Test Plan

How to verify

Run the focused tests and confirm that resumed rewind boundaries preserve the stored parent edge, and that both ACP resume entry points rebuild recorder boundaries from resumed conversation records.

Commands run locally:

npx vitest run packages/core/src/services/chatRecordingService.test.ts packages/cli/src/acp-integration/acpAgent.test.ts --coverage=false
npx eslint packages/core/src/services/chatRecordingService.ts packages/core/src/services/chatRecordingService.test.ts packages/cli/src/acp-integration/acpAgent.ts packages/cli/src/acp-integration/acpAgent.test.ts
npx prettier --check packages/core/src/services/chatRecordingService.ts packages/core/src/services/chatRecordingService.test.ts packages/cli/src/acp-integration/acpAgent.ts packages/cli/src/acp-integration/acpAgent.test.ts
git diff --check
npm run typecheck --workspace=packages/core
npm run build --workspace=packages/core
npm run build --workspace=packages/cli
npm run typecheck --workspace=packages/cli

Evidence (Before & After)

Before: rebuildTurnBoundaries() inferred the first resumed user turn boundary from local list traversal, so a sliced reconstructed history could lose the persisted parent edge and a later rewind marker could be written with the wrong parentUuid.

After: chatRecordingService.test.ts verifies that a resumed user turn with a stored parent writes the rewind marker under that same parent. acpAgent.test.ts verifies that both loadSession and unstable_resumeSession rebuild recorder boundaries from resumed conversation records.

Tested on

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

Environment (optional)

Local Node/npm workspace on macOS.

Risk & Scope

  • Main risk or tradeoff: this changes how rewind boundaries are rebuilt after resume, but uses the persisted tree edge already stored on each ChatRecord instead of deriving a new edge from list position.
  • Not validated / out of scope: this does not change compression-aware rewind mapping or the broader /rewind UI/API model handled by fix(cli): map rewind turns after compression #4242.
  • Breaking changes / migration notes: none.

Linked Issues

Fixes #5920

AI Assistance Disclosure

I used Codex to review the changes, sanity-check the implementation against existing patterns, and help spot potential edge cases.

中文说明

What this PR does

这个 PR 在 session resume 后重建 rewind turn boundaries 时保留持久化记录里的 parent UUID 边。CLI resume 路径之前已经会重建这些 boundaries;这个 PR 让重建逻辑使用每条 resumed record 自己保存的 parentUuid,而不是从 reconstructed list 中相邻元素推断,并且把同样的重建步骤应用到 VS Code 和 daemon clients 使用的 ACP session resume 路径。

Why it's needed

#5920 报告了恢复会话后,自动 rewind/edit 流程导致历史消失的问题。recorder 存的是一棵 record tree,rewind marker 必须挂到目标 user turn 原本的 parent record 下。如果 boundary 被重建成 null,下一次 resume 沿 parent chain 回溯时可能过早停止,并丢失更早的对话历史。

Reviewer Test Plan

How to verify

运行聚焦测试,确认 resumed rewind boundaries 会保留存储的 parent edge,并确认两个 ACP resume 入口都会从 resumed conversation records 重建 recorder boundaries。

本地运行过的命令:

npx vitest run packages/core/src/services/chatRecordingService.test.ts packages/cli/src/acp-integration/acpAgent.test.ts --coverage=false
npx eslint packages/core/src/services/chatRecordingService.ts packages/core/src/services/chatRecordingService.test.ts packages/cli/src/acp-integration/acpAgent.ts packages/cli/src/acp-integration/acpAgent.test.ts
npx prettier --check packages/core/src/services/chatRecordingService.ts packages/core/src/services/chatRecordingService.test.ts packages/cli/src/acp-integration/acpAgent.ts packages/cli/src/acp-integration/acpAgent.test.ts
git diff --check
npm run typecheck --workspace=packages/core
npm run build --workspace=packages/core
npm run build --workspace=packages/cli
npm run typecheck --workspace=packages/cli

Evidence (Before & After)

Before:rebuildTurnBoundaries() 会根据本地列表遍历推断第一个 resumed user turn 的 boundary;如果 reconstructed history 是一个被裁剪过的链,就可能丢失持久化的 parent edge,导致后续 rewind marker 写入错误的 parentUuid

After:chatRecordingService.test.ts 验证带有已存储 parent 的 resumed user turn 会把 rewind marker 写到同一个 parent 下。acpAgent.test.ts 验证 loadSessionunstable_resumeSession 都会从 resumed conversation records 重建 recorder boundaries。

Tested on

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

Environment (optional)

macOS 上的本地 Node/npm workspace。

Risk & Scope

  • Main risk or tradeoff:这个 PR 改变了 resume 后 rewind boundaries 的重建方式,但它使用的是每条 ChatRecord 已经持久化保存的 tree edge,而不是根据列表位置重新推导 edge。
  • Not validated / out of scope:这个 PR 不改 compression-aware rewind mapping,也不处理 fix(cli): map rewind turns after compression #4242 覆盖的更大 /rewind UI/API 模型。
  • Breaking changes / migration notes:无。

Linked Issues

Fixes #5920

AI Assistance Disclosure

我使用 Codex 审查变更、对照现有模式做 sanity check,并帮助发现潜在边界情况。

@tt-a1i
tt-a1i marked this pull request as ready for review June 27, 2026 05:34
@wenshao

wenshao commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for the PR! Template looks good ✓

On direction: this is a clear bug fix for #5920 — session history disappearing after resume + rewind due to parentUuid being set to null. Session management and history integrity are core. Aligned.

On approach: the fix is minimal and correct. Using each record's persisted parentUuid instead of inferring from list position is the right call — the stored tree edge is the source of truth, and the reconstructed list can start mid-chain. Extending rebuildTurnBoundaries to the ACP resume path (VS Code / daemon) mirrors what useResumeCommand.ts already does for the CLI path — consistent and focused. No scope creep, no drive-by changes.

Claude Code CHANGELOG has /rewind support for resuming from before /clear but no direct reference to turn-boundary parent preservation on resume. The area is relevant regardless.

Moving on to code review. 🔍

中文说明

感谢贡献!模板完整 ✓

方向:这是 #5920 的明确 bug 修复——resume 后 rewind 导致 parentUuidnull,会话历史丢失。Session 管理和历史完整性属于核心功能,方向对齐。

方案:修复最小化且正确。使用每条 record 持久化的 parentUuid 而不是从列表位置推断,是对的——存储的 tree edge 才是真实来源,重建的列表可能从链中间开始。把 rebuildTurnBoundaries 扩展到 ACP resume 路径(VS Code / daemon)与 CLI 路径一致。无范围蔓延。

Claude Code CHANGELOG 有 /rewind 支持从 /clear 前恢复的相关记录,但没有直接涉及 turn boundary parent 保留。领域相关。

进入代码审查 🔍

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code Review

My independent proposal before reading the diff: fix rebuildTurnBoundaries() to use each ChatRecord's stored parentUuid (the persisted tree edge) instead of inferring parent from list position, and add the same call to the ACP resume paths. The PR does exactly this — approach matches.

No critical blockers. No AGENTS.md violations. The change is minimal and well-scoped.

One note on the old code: the prevUuid variable was initialized to null for resumed sessions (when lastCompletedUuid exists), which meant the first user turn after resume always got parentUuid: null. The fix correctly replaces this with record.parentUuid ?? null — using the actual stored edge. This is semantically right because the reconstructed list can start mid-chain and the previous-list-item is not necessarily the parent.

The ACP path addition mirrors useResumeCommand.ts:156 which already calls rebuildTurnBoundaries for CLI resume — consistent extension.

Testing

Before (old source, new assertions)

 FAIL  src/services/chatRecordingService.test.ts > ChatRecordingService > rewindRecording > preserves a resumed user turn parent when rebuilding rewind boundaries
AssertionError: expected null to be 'pre-resume-parent' // Object.is equality

- Expected:
"pre-resume-parent"

+ Received:
null

 ❯ src/services/chatRecordingService.test.ts:209:33
    207|       const rewind = vi.mocked(jsonl.writeLine).mock.calls[0][1] as Ch…
    208|       expect(rewind.subtype).toBe('rewind');
    209|       expect(rewind.parentUuid).toBe('pre-resume-parent');
       |                                 ^
    210|     });

 Test Files  1 failed (1)
      Tests  1 failed | 34 skipped (35)
 Test Files  1 failed (1)
      Tests  1 failed | 155 skipped (156)
     Errors  1 error

(ACP test: rebuildTurnBoundaries not called in the old code → assertion fails.)

After (this PR)

 ✓ src/services/chatRecordingService.test.ts (35 tests) 32ms

 Test Files  1 passed (1)
      Tests  35 passed (35)
 ✓ src/acp-integration/acpAgent.test.ts (156 tests) 6573ms

 Test Files  1 passed (1)
      Tests  156 passed (156)

All 191 tests pass, zero regressions.

中文说明

代码审查

独立方案:使用 ChatRecordparentUuid(持久化的 tree edge)替代从列表位置推断,并在 ACP resume 路径加入同样的调用。PR 完全吻合。

无关键阻塞项,无 AGENTS.md 违规。旧代码中 prevUuid 在 resumed session 被初始化为 null,导致 resume 后第一个 user turn 的 parent 总是 null。修复使用 record.parentUuid ?? null,语义正确。

测试

修复前:两个新测试均失败(parentUuid 为 null 而非预期值,ACP 路径未调用 rebuildTurnBoundaries)。修复后:191 个测试全部通过,无回归。

Qwen Code · qwen3.7-max

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

This is a clean, well-reasoned bug fix. The old rebuildTurnBoundaries() tried to reconstruct parent edges from list position — a heuristic that broke whenever the reconstructed history was a slice starting mid-chain. The PR replaces that heuristic with the actual persisted tree edge (record.parentUuid), which is simpler and correct. The code is also smaller after the fix (the conditional prevUuid initialization and per-iteration tracking are gone).

The ACP path extension is a natural follow-through — useResumeCommand.ts already called rebuildTurnBoundaries for CLI resume, and the VS Code / daemon paths were missing it. Consistent fix.

Before/after testing confirms: new assertions fail against the old code (parentUuid is null), pass with the fix applied, and all 191 existing tests continue to pass. No regressions.

Ships the fix cleanly. ✅

中文说明

这是一个干净、合理的 bug 修复。旧的 rebuildTurnBoundaries() 尝试从列表位置推断 parent edge——当重建的历史是从链中间开始的切片时就会出错。PR 用实际持久化的 tree edge(record.parentUuid)替代了这个启发式方法,更简单也更正确。修复后代码反而更简洁了(去掉了条件初始化 prevUuid 和每次迭代的追踪)。

ACP 路径扩展是自然的补全——CLI resume 已经在调用 rebuildTurnBoundaries,VS Code / daemon 路径之前缺失。一致的修复。

前后测试确认:新断言在旧代码下失败,修复后通过,191 个现有测试无回归。✅

Qwen Code · qwen3.7-max

@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 Jun 27, 2026

Copy link
Copy Markdown
Collaborator

✅ Local real-runtime verification (maintainer)

Built the real core + cli packages at this PR head (6b2e628) and verified the rewind-parent fix three ways: the PR's full test plan, a non-vacuity mutation on both the core logic and the ACP wiring, and a real-service A/B that captures the actual rewind marker written by ChatRecordingService. After a resume the rewind marker now keeps its persisted parent edge instead of null, and both ACP resume entry points rebuild the boundaries. LGTM from a verification standpoint.

What the fix does: rebuildTurnBoundaries() used to infer each user turn's boundary from the previous item in the reconstructed list (prevUuid, which starts null after a resume) — so the first resumed user turn got a null parent, the rewind marker was written with parentUuid: null, and the next resume stopped walking the parent chain too early and dropped earlier history (#5920). The PR pushes each record's persisted record.parentUuid instead, and (in acpAgent.ts) calls rebuildTurnBoundaries() on the ACP resume paths (loadSession + unstable_resumeSession, used by VS Code / daemon clients), which previously only the CLI path did.

Environment: macOS (Darwin), Node/npm workspace, Vitest 3.2.4. Worktree on 6b2e628, npm ci + build.


1. PR test plan — green

Check Result
core · chatRecordingService.test.ts 35 / 35 pass
cli · acpAgent.test.ts 156 / 156 pass
typecheck (core + cli) 0 errors each
eslint (4 files) ✅ clean
prettier --check (4 files) ✅ clean
git diff --check ✅ clean

2. Mutation — both halves of the fix are non-vacuous

# Mutation Result
M1 — core revert rebuildTurnBoundaries() to base (list-position inference) preserves a resumed user turn parent… fails: expected null to be 'pre-resume-parent'
M2 — cli revert the acpAgent.ts rebuildTurnBoundaries() call both ACP resume tests failloadSession and unstable_resumeSession, each rebuildTurnBoundaries Number of calls: 0

So the core test pins the persisted-parent behavior, and the two ACP tests pin that both resume entry points rebuild boundaries — exactly the two things the PR adds.

3. Real-service A/B — the actual rewind marker written by ChatRecordingService

Driving the real ChatRecordingService through getResumedSessionData()rebuildTurnBoundaries([user-1{parentUuid:'pre-resume-parent'}, assistant-1])rewindRecording(0)flush(), and dumping the marker actually written:

Source rewind marker written
PR (fixed) {subtype:"rewind", parentUuid:"pre-resume-parent"} ✅ keeps the persisted edge
base {subtype:"rewind", parentUuid:null} ❌ parent edge dropped → chain breaks on the next resume

That null is the #5920 root cause: the rewind marker detaches from the record that was the real parent of the resumed turn, so a later resume walking the parent chain stops there and loses everything before it. The PR re-anchors the marker to pre-resume-parent, keeping earlier history reachable.


Notes / scope honesty

  • This is a recording-tree data-integrity fix. The user-visible symptom (history disappears after a resume + auto rewind/edit) is a downstream consequence of the null boundary breaking the next resume's parent-chain walk; the authoritative fix point — the rewind marker's parentUuid — is verified directly with the real service. A full multi-resume TUI repro isn't deterministically forceable, so verification sits at the real-service level where the bug and fix live.
  • The fix correctly uses record.parentUuid ?? null, so a genuinely root record (no parent) still gets null — only the spurious null from list-position inference is removed.
  • Verified on macOS only (matches the PR's Tested on table).
  • Merge is gated on REVIEW_REQUIRED (needs a maintainer approval) — orthogonal to the code.
🇨🇳 中文版(完整对应)

✅ 本地真实运行时验证(维护者)

在本 PR head(6b2e628)构建了真实 core + cli 包,从三个角度验证 rewind 父边修复:PR 完整测试计划、对核心逻辑和 ACP 接线两半都做的非空过变异,以及捕获 ChatRecordingService 真正写出的 rewind marker 的真实服务 A/B。resume 之后 rewind marker 现在会保留持久化的 parent edge 而不是 null,且两个 ACP resume 入口都会重建 boundaries。从验证角度 LGTM。

这个修复做了什么: rebuildTurnBoundaries() 原来从重建列表里的前一项(prevUuid,resume 后从 null 开始)推断每个 user turn 的 boundary —— 所以第一个 resumed user turn 拿到 null 父,rewind marker 被写成 parentUuid: null,下一次 resume 沿 parent chain 回溯时过早停止、丢失更早历史(#5920)。本 PR 改成 push 每条记录持久化的 record.parentUuid,并(在 acpAgent.ts 里)在 ACP resume 路径(loadSession + unstable_resumeSession,VS Code / daemon 客户端用)上调用 rebuildTurnBoundaries() —— 这一步之前只有 CLI 路径做了。

环境: macOS,Node/npm workspace,Vitest 3.2.4。worktree 在 6b2e628,npm ci + 构建。

1. PR 测试计划 — 全绿

检查 结果
core · chatRecordingService.test.ts 35 / 35 通过
cli · acpAgent.test.ts 156 / 156 通过
typecheck(core + cli) 0 错误
eslint(4 文件) ✅ 干净
prettier --check(4 文件) ✅ 干净
git diff --check ✅ 干净

2. 变异测试 —— 修复的两半都非空过

# 变异 结果
M1 — core rebuildTurnBoundaries() 还原成 base(按列表位置推断) preserves a resumed user turn parent… 失败:expected null to be 'pre-resume-parent'
M2 — cli 去掉 acpAgent.tsrebuildTurnBoundaries() 调用 两个 ACP resume 测试都失败 —— loadSessionunstable_resumeSession,各自 rebuildTurnBoundaries Number of calls: 0

也就是说:核心测试钉住"用持久化父"的行为,两个 ACP 测试钉住"两个 resume 入口都重建 boundaries" —— 正是 PR 新增的两件事。

3. 真实服务 A/B —— ChatRecordingService 真正写出的 rewind marker

真实 ChatRecordingServicegetResumedSessionData()rebuildTurnBoundaries([user-1{parentUuid:'pre-resume-parent'}, assistant-1])rewindRecording(0)flush(),并 dump 真正写出的 marker:

写出的 rewind marker
PR(已修复) {subtype:"rewind", parentUuid:"pre-resume-parent"} ✅ 保留持久化边
base {subtype:"rewind", parentUuid:null} ❌ 父边丢失 → 下一次 resume 时链断

那个 null 就是 #5920 的根因:rewind marker 与 resumed turn 真正的父记录脱钩,于是后续 resume 沿 parent chain 回溯就停在这里、丢掉之前的一切。本 PR 把 marker 重新锚到 pre-resume-parent,让更早的历史仍可达。

备注 / 范围诚实说明

  • 这是 recording-tree 的数据完整性修复。用户可见症状(resume + 自动 rewind/edit 后历史消失)是 null boundary 破坏下一次 resume 父链回溯的下游后果;而权威修复点 —— rewind marker 的 parentUuid —— 已用真实服务直接验证。完整的多次-resume TUI 复现无法确定性触发,所以验证落在 bug/修复所在的真实服务层。
  • 修复用的是 record.parentUuid ?? null,所以真正的根记录(无父)仍得 null —— 只去掉了按列表位置推断出来的 null
  • 仅在 macOS 验证(与 PR Tested on 表一致)。
  • 合并被 REVIEW_REQUIRED 卡住(需维护者 approve)—— 与代码正交。

@wenshao
wenshao added this pull request to the merge queue Jun 27, 2026
Merged via the queue into QwenLM:main with commit 56cbb1e Jun 27, 2026
38 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.

/rewind records have parentUuid: null instead of the correct turn parent, breaking conversation history on resume

3 participants