Skip to content

feat: support fork from any conversation - #8817

Merged
water-in-stone merged 69 commits into
QwenLM:mainfrom
water-in-stone:feat/fork-from-any-conversation
Aug 15, 2026
Merged

feat: support fork from any conversation#8817
water-in-stone merged 69 commits into
QwenLM:mainfrom
water-in-stone:feat/fork-from-any-conversation

Conversation

@water-in-stone

Copy link
Copy Markdown
Collaborator

What this PR does

Previously, session branching used the latest active session state and could not reliably target an earlier Assistant response. Treating a visible message as a branch point is unsafe because tool calls, cancellations, metadata records, transcript pagination, rewinds, and concurrent transcript mutations can make the displayed response different from the authoritative active history.

Durable checkpoints provide a single source of truth shared by recording, replay, UI presentation, and Core validation. This lets users explore an alternative direction from an earlier answer without modifying the original session or exposing a partially created branch.

Why it's needed

Previously, session branching used the latest active session state and could not reliably target an earlier Assistant response. Treating a visible message as a branch point is unsafe because tool calls, cancellations, metadata records, transcript pagination, rewinds, and concurrent transcript mutations can make the displayed response different from the authoritative active history.

Durable checkpoints provide a single source of truth shared by recording, replay, UI presentation, and Core validation. This lets users explore an alternative direction from an earlier answer without modifying the original session or exposing a partially created branch.

Reviewer Test Plan

How to verify

  1. Start Web Shell in a disposable workspace and complete at least two successful interactive turns.
  2. Confirm that each eligible final Assistant response exposes a Branch action. Cancelled, errored, incomplete, legacy, or otherwise ineligible responses should not expose it.
  3. Select Branch on the first completed Assistant response.
  4. Confirm that Web Shell switches to a new session whose transcript ends at the selected response. The later source turn must not appear, and the original session must remain unchanged.
  5. Continue the conversation in the new session and confirm that it evolves independently from the source session.
  6. Verify that files changed after the selected response remain at their current state. Branching truncates conversation history; it does not rewind the working directory or Git state.
  7. For stale-checkpoint handling, display the same session in two clients, make the selected checkpoint inactive through a rewind in one client, and then use the old Branch action in the other. The request should be rejected, the transcript should refresh, and no partial session should be exposed.
  8. Confirm that branching without a historical checkpoint still branches from the latest session state.

Evidence (Before & After)

Before: Branching represented the latest active session state. Historical Assistant responses did not have an authoritative, durable branch point.

image

After: Eligible completed Assistant responses carry a recorded and replayable checkpoint. Selecting Branch creates a new session ending at the chosen turn, while stale checkpoints are rejected and trigger a transcript refresh.

image

Tested on

OS Status
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

Environment (optional)

  • macOS 26.0
  • Node.js v22.22.3
  • npm 10.9.8
  • Native local checkout without a sandbox
  • Vitest and jsdom for automated UI coverage

Risk & Scope

  • Main risk or tradeoff: The feature spans transcript topology, session concurrency, replay, protocol transport, persistence, and UI state. Historical branch publication also requires filesystem hard-link support to expose the completed transcript atomically. Unsupported filesystems fail the branch without exposing a partial session; there is intentionally no non-atomic copy fallback.
  • Not validated / out of scope: Real provider/model browser E2E, visual evidence, and local Windows/Linux execution were not performed. Working files and Git state are intentionally not rewound. Legacy transcripts without durable checkpoints are intentionally not historical-branchable.
  • Breaking changes / migration notes: No schema or API migration is required; the historical checkpoint is optional and existing latest-state branching remains supported. If restore or response delivery fails after either kind of branch is committed, the complete branch is intentionally retained and remains recoverable in the session picker because another client may already have discovered or attached it.

Linked Issues

Refs #8271

中文说明

Why it's needed

此前,会话分支只能使用源会话的最新活动状态,无法可靠地定位到更早的一条 Assistant 回复。直接把界面上可见的消息作为分支点并不安全,因为工具调用、取消、元数据记录、分页回放、会话回退和并发 transcript 写入都可能导致界面显示内容与权威活动历史不一致。

持久化 checkpoint 为录制、回放、界面展示和 Core 校验提供了统一的事实来源。用户因此可以从较早的回答探索另一条对话路径,同时不修改原始会话,也不会看到尚未完整创建的分支会话。

Reviewer Test Plan

How to verify

  1. 在一个可丢弃的工作区中启动 Web Shell,成功完成至少两个交互式回合。
  2. 确认每一条符合条件的最终 Assistant 回复都会显示 Branch 操作。已取消、发生错误、未完整结束、来自旧版 transcript 或其他不符合条件的回复不应显示该操作。
  3. 在第一个已完成的 Assistant 回复上选择 Branch。
  4. 确认 Web Shell 自动切换到一个新会话,并且新会话的 transcript 截止于所选回复。源会话中更晚的回合不应出现在新会话里,源会话本身应保持不变。
  5. 在新会话中继续对话,确认它可以独立于源会话继续发展。
  6. 确认在所选回复之后发生的文件修改仍保持当前状态。分支只截断对话历史,不会回退工作目录或 Git 状态。
  7. 验证过期 checkpoint:在两个客户端中打开同一个会话,在其中一个客户端通过 rewind 使所选 checkpoint 离开活动历史链,然后在另一个客户端使用旧的 Branch 操作。请求应被拒绝,transcript 应自动刷新,并且不应暴露任何未完整创建的会话。
  8. 确认不提供历史 checkpoint 时,系统仍能从会话最新状态创建分支。

Evidence (Before & After)

Before: 分支操作只能表示会话当前最新的活动状态。历史 Assistant 回复没有权威且持久化的分支点。

image

After: 符合条件的已完成 Assistant 回复带有可录制、可回放的 checkpoint。选择 Branch 后,新会话会准确截止于所选回合;过期 checkpoint 会被拒绝,并触发 transcript 刷新。

image

Tested on

OS Status
🍏 macOS ✅ tested
🪟 Windows ⚠️ 未在本地测试
🐧 Linux ⚠️ 未在本地测试

Environment (optional)

  • macOS 26.0
  • Node.js v22.22.3
  • npm 10.9.8
  • 本地原生环境,未使用 sandbox
  • UI 自动化覆盖使用 Vitest 和 jsdom

Risk & Scope

  • Main risk or tradeoff:该功能横跨 transcript 拓扑、会话并发、回放、协议传输、持久化和 UI 状态。历史分支发布还依赖文件系统支持硬链接,以原子方式暴露完整 transcript;不支持硬链接的文件系统会让分支失败,但不会暴露半成品,并且系统刻意不使用非原子的复制回退。
  • Not validated / out of scope:尚未执行真实 provider/model 的浏览器 E2E、视觉证据采集以及 Windows/Linux 本地验证。工作区文件和 Git 状态不会被回退。没有持久化 checkpoint 的旧版 transcript 不支持从历史回复分支。
  • Breaking changes / migration notes:不需要 schema、API 或数据迁移;历史 checkpoint 是可选参数,现有的最新状态分支行为仍然受支持。无论历史分支还是最新状态分支,如果在提交后恢复或响应交付失败,完整分支都会被保留并可从会话列表恢复,因为其他客户端可能已经发现或附加了该会话。

Linked Issues

Refs #8271

heyang.why and others added 30 commits August 1, 2026 16:12
Add durable response checkpoints so Web Shell sessions can branch from
eligible completed Assistant turns without mutating the source history.

- Record and validate checkpoints behind serialized topology fences
- Preserve historical anchors through replay, daemon, SDK, and UI layers
- Publish bounded forks with crash-safe ownership and referenced backups
- Serialize prompt, rewind, branch, automatic turn, and close mutations
- Cover stale anchors, replay pagination, cleanup, and pending UI states

Note: Responses recorded before this change remain non-branchable.

# Conflicts:
#	packages/acp-bridge/src/bridge.ts
#	packages/acp-bridge/src/bridgeTypes.ts
#	packages/cli/src/acp-integration/acpAgent.test.ts
#	packages/cli/src/acp-integration/acpAgent.ts
#	packages/cli/src/serve/routes/session.ts
#	packages/cli/src/serve/server.test.ts
#	packages/core/src/services/chatRecordingService.ts
#	packages/core/src/services/sessionService.test.ts
#	packages/core/src/services/sessionService.ts
#	packages/sdk-typescript/src/daemon/DaemonClient.ts
#	packages/web-shell/client/components/MessageItem.tsx
#	packages/web-shell/client/components/MessageList.tsx
Keep Assistant-response branching intact across the daemon stack after
rebases, including history serialization and persisted-session ownership.

- Forward durable checkpoint IDs through Bridge, SDK, and UI layers
- Serialize live history mutations and retain valid nested branch anchors
- Preserve persisted branches during generation cleanup
- Add cross-layer regression tests for replay and stale checkpoints
Keep the PR review report as a local ignored backup instead of
shipping it with the feature branch.

- Remove the generated PR comment evaluation from tracked files
- Preserve the report under the ignored analyze directory
Historical branch requests could outlive the client timeout during an
active turn, and interactive forks lacked the recorder's cross-process
writer-lease barrier.

- Hide Assistant Branch actions while a turn is active
- Run interactive fork creation inside the recorder write barrier
- Use the concrete checkpoint recorder contract in Session
- Document committed-session ownership and implemented design status
Build branch catalogs during the frozen index scan so the first history
page no longer reopens and materializes the complete active chain.

- Retain a compact projection for shared branch-point resolution
- Correlate live branch anchors with the completed prompt and final reply
- Complete recorder mocks required by the concrete Session contract
- Update the reviewed design with performance and correlation invariants
)

Add focused tests requested in PR review:
- branch catalog resolves checkpoints that fall on a later page
- accept a parallel tool batch closed within a single turn
- exercise the linkSync->copyFileSync fork backup fallback success path
- prove a remapped checkpoint stays usable via a nested fork
- isolate each branch-point validation conjunct across bridge and SDK
- Make the directory-fsync durability test platform-aware (skip on win32),
  since fsyncDirectoryBestEffort swallows the injected error on Windows and
  the rejection path is non-Windows by design.
- Reject atRecordId on the side-task fork path instead of silently discarding
  it, so the API surface no longer implies acceptance.
- Correct the design doc: name the real promptQueue FIFO (not the nonexistent
  historyMutationQueue) and describe filtered checkpoint boundaries as
  remapped to the nearest retained predecessor, not unconditionally null.
- Add focused tests: branch-point assistantRecordUuid mismatch rejection, and
  insight-block branchRecordId anchoring (insight-only block must not anchor
  onto the previous reply).
…nversation

# Conflicts:
#	packages/cli/src/acp-integration/acpAgent.ts
…t shapes (QwenLM#8274)

- Filter null/non-object part elements in the shared branch resolver so a
  transcript containing null parts no longer makes forkSession throw a
  TypeError for every checkpoint.
- Tag tool calls carried in from the pre-boundary prefix so a dangling call
  left by a crashed turn no longer permanently disables checkpoint
  recording; only calls issued inside the turn must close.
- Merge duplicate-uuid records first-wins for identity fields in the
  transcript reader, matching the byUuid index and fork aggregation, so the
  reader never advertises a branch marker the fork path must reject.
…nversation

# Conflicts:
#	packages/cli/src/acp-integration/acpAgent.ts
#	packages/cli/src/acp-integration/session/history-replay-page.test.ts
…nversation

# Conflicts:
#	packages/cli/src/acp-integration/session/history-replay-page.test.ts
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. Run review failed. See workflow logs for details. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review. See workflow logs.

…conversation

# Conflicts:
#	packages/acp-bridge/src/bridge.ts
#	packages/cli/src/acp-integration/acpAgent.ts
#	packages/cli/src/serve/routes/session.ts
@water-in-stone
water-in-stone dismissed qwen-code-ci-bot’s stale review August 14, 2026 07:42

All the critical comments have been resolved

yiliang114
yiliang114 previously approved these changes Aug 14, 2026

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

Re-reviewed head df26a9af (delta since a30d6196).

Delta composition: 5 merges of upstream/main (resolving the content conflicts @LaZzyMan flagged) plus two fork-specific commits:

  • 00ce316694 — restores the v1 branch-session contract: a branchSession call without atRecordId now restores the latest-state branch inside the bridge and returns the full restored session (DaemonBranchedSession extends DaemonRestoredSession again); calls with atRecordId stay persisted-only historical branches. Dispatch/route/SDK/webui call sites and tests updated accordingly.
  • a3b7c27728 — fixes R5-1: both branchSession admission gates now use isClosingOrAuthorizingClose(entry) (pre-queue and post-dispatch recheck), with regression tests for queued-branch-vs-close races. Also serializes the Goal continuation drain behind runExclusiveAutomaticHistoryMutation and skips branch checkpoints for scheduled goal turns.

Invariants re-verified at head: three-layer admission gating with the full close predicate; session_busySessionBusyError → 409+Retry-After mapping; narrow isTurnIdle() fail-fast for branch/rewind; normalized session-id lookups; checkpoint transaction keeps the topology fence, flush, and "transcript changed" guard via appendRecordStrict; REST fork route releases a live branch when the response is aborted; web-shell keeps the pending-branch request dedupe. No regression to source-session immutability or lineage handling observed in the conflict resolutions.

Feedback status: doudouOUC's v1-contract Critical and qwen-code-ci-bot's R5-1 are both addressed; the R1 items were already addressed at 7e1fdab199.

CI at df26a9af: green — Qwen Code CI, SDK Java, Serve A/B, web-shell visuals, Live Host, and Security Checks all succeeded (the cancelled suites are the duplicate runs from the double push; only the bot's own review run is still in progress).

…conversation

# Conflicts:
#	packages/webui/src/daemon/session/DaemonSessionProvider.test.tsx
#	packages/webui/src/daemon/session/actions.test.ts
#	packages/webui/src/daemon/session/actions.ts
yiliang114
yiliang114 previously approved these changes Aug 14, 2026

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

Re-review of delta df26a9af...bec0d959: this head is a merge of upstream main (7 main commits) with conflicts confined to webui/src/daemon/session/{actions.ts,actions.test.ts,DaemonSessionProvider.test.tsx}, resolved by re-adapting the fork wiring to #9129's revert of the transactional session-switching model.

Verified at this head:

  • Server side byte-identical to the previously reviewed head: no delta in sessionService.ts, branch-points.ts, chatRecordingService.ts, session-transcript-reader.ts, serve/routes/session.ts, acp-bridge, or sdk-typescript — source-session immutability, tmp+fsync+hardlink atomic commit, concurrency gates, the 409 branch_point_invalid contract, and lineage tracking all stand unchanged.
  • Conflict resolution is correct and tested: branchSession(name?, atRecordId?) gains a branchInFlight gate (InvalidStateError), a switchStarted generation check with detach cleanup for orphaned restored sessions, and stale-branch-point rethrow without a generic notice. Dropping the 30s action timeout in favor of the SDK-owned deadline is deliberate and pinned by a test. Provider-side branchPoint extraction from turn_complete is UUID-validated before flowing into assistant.done.
  • Per-message fork chain intact under the reverted model: branchRecordId flows transcript → messages → MessageList (button shown per completed assistant record, hidden while responding) → MessageItem bound handler → App.branchCurrentSession with request dedup map and session-identity-guarded stale recovery reload. New i18n keys present in both locales.
  • CI: earlier batch on this SHA was superseded (cancelled); current batch is green so far (Security, SDK Java, Live Host, Serve A/B, Web-shell Visuals) with the main suite still running and no failed check runs. Serve A/B reports no response changes vs the PR base across all 4 scenarios.

LaZzyMan's conflict note from 2026-08-13 is addressed by this merge. No new findings.

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

Reviewed the full diff at bec0d959 against merge-base 22bacfe2, focusing on the core recording/validation layer, forkSession atomicity, and the ACP bridge/agent plumbing. Two blocking issues, both reproduced with runtime tests against the real services; details inline.

  1. Cold-resumed session loses the branch checkpoint for its first completed turnchatRecordingService.ts projection-restore path.
  2. Forked file-history backups lose their mode, corrupting working-tree permissions on /rewindsessionService.ts backup copy loop.

The rest of the change checked out: topology-fence ordering, resolveBranchPoints fail-closed dedup, reader↔fork first-wins parity, publish/orphan atomicity, O_NOFOLLOW/dev-ino backup hardening, the v1 latest-state contract split, _meta['qwen.branchPoint'] plumbing across goal/cancel/side-task/channel turns, and the stale-checkpoint 409 (branch_point_invalid) mapping.

Non-blocking, for awareness only: the /branch route dropped its post-await generationGuard.assertOpen() check, so a latest-state branch committed while the runtime generation closes mid-flight now returns 201 with a clientId that is already dead. It is recoverable by re-restoring the persisted branch and appears intentional (a rewritten test now locks it in), so no change requested.

Comment thread packages/core/src/services/chatRecordingService.ts
Comment thread packages/core/src/services/sessionService.ts

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

Not reviewed: reverse audit — stopped at the 3-round cap without converging (the round 1-2 convergence pair and round 3 all reported findings; no two consecutive dry rounds).

Not explored to full depth (tool budget reached): "You are review agent reverse-audit — Reverse audit agent…": none — all checks above completed within budget.; "PR #8817 (feat: support fork from any conversation) adds…": none — all checks above completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": none — all planned checks completed within budget (~16 of ~37 calls used).; "You are review agent reverse-audit — Reverse audit agent…": none — all checks above completed within budget.; "You are review agent reverse-audit — Reverse audit agent…": none — all planned checks completed within budget., and 32 more.

中文说明

仅完成部分审查,审查缺口已披露。

未审查:reverse audit — stopped at the 3-round cap without converging (the round 1-2 convergence pair and round 3 all reported findings; no two consecutive dry rounds)。

未探索到全部深度(达到工具调用预算):"You are review agent reverse-audit — Reverse audit agent…"none — all checks above completed within budget."PR #8817 (feat: support fork from any conversation) adds…"none — all checks above completed within budget."You are review agent reverse-audit — Reverse audit agent…"none — all planned checks completed within budget (~16 of ~37 calls used)."You are review agent reverse-audit — Reverse audit agent…"none — all checks above completed within budget."You are review agent reverse-audit — Reverse audit agent…"none — all planned checks completed within budget.,另有 32 条。

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

Comment thread packages/core/src/services/chatRecordingService.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/core/src/services/sessionService.ts
Comment thread packages/acp-bridge/src/bridge.test.ts
Comment thread packages/acp-bridge/src/bridge.ts
Comment thread packages/cli/src/acp-integration/acpAgent.ts Outdated
Comment thread packages/web-shell/client/App.tsx
Keep branch checkpoints and file-history snapshots correct across resumed,
forked, and non-interactive session flows.

- Track the restored active-chain base before the first appended turn
- Preserve backup file modes during fork publication
- Exclude authenticated channel prompts from checkpoint recording
- Add regressions for all three review failures

@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 explored to full depth (tool budget reached): chunk 10: running the test file in vitest — worktree and parent checkout have no node_modules , so a full monorepo install would have been required; substituted by traci…; chunk 1: none — no checks left unfinished..

Not reviewed: reverse audit — stopped before round 3 by the review time budget.

中文说明

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

未探索到全部深度(达到工具调用预算):chunk 10:running the test file in vitest — worktree and parent checkout have no node_modules , so a full monorepo install would have been required; substituted by traci…;chunk 1:none — no checks left unfinished.

未审查:反向审计——评审时间预算不足,未能开始第 3 轮。

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

Comment thread packages/cli/src/acp-integration/acpAgent.ts Outdated
Comment thread packages/cli/src/acp-integration/acpAgent.ts Outdated
Comment thread packages/core/src/services/chatRecordingService.test.ts
Comment thread packages/cli/src/acp-integration/acpAgent.test.ts
Comment thread packages/core/src/services/sessionService.test.ts
Comment thread packages/acp-bridge/src/bridge.test.ts
Comment thread packages/acp-bridge/src/compactionEngine.test.ts Outdated
Comment thread packages/core/src/services/sessionService.test.ts Outdated
Comment thread packages/webui/src/daemon/session/actions.ts
@water-in-stone

Copy link
Copy Markdown
Collaborator Author

已完成 Review 4940270549 的全部 9 条行内 Suggestion(当前工作区,尚未提交/推送),并已逐条回复对应线程。验证结果:8 个受影响测试文件共 1,915 项通过;Web Shell、CLI、Core、ACP Bridge typecheck 通过;根目录 npm run build 通过;test-engineer post-fix 复验未发现剩余问题。

Handle the remaining branch and rewind review findings without widening
the feature contract.

- Ignore benign concurrent branch rejections in the Web Shell
- Validate rewind prompt IDs before using string operations
- Pin mutation ordering, cleanup, compaction, and checkpoint invariants
- Align sourced-fork fixtures with the canonical side_task value

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

Re-reviewed at 602bb082. Both Criticals from my prior review are fixed and independently verified:

  1. Cold-resumed checkpoint loss — fixed via the new activeBranchBaseUuid (seeded to the restored leaf in restoreProjectedState, consulted by the staleness guard when activeRecordCount === 0). My original repro now records a checkpoint on turn 1 rather than throwing.
  2. Forked-backup mode corruption — fixed via chmod(sourceStat.mode & 0o7777) on the staged copy; the added 0755 regression passes.

Both ship with targeted regression tests. I also reviewed the two follow-up commits end to end: the rewind promptId type-validation hardening (rejects non-string ids with -32602 instead of a downstream TypeError), and the Web Shell concurrent-branch swallow — it matches the exact branchInFlight DOMException producer in actions.ts, which is reset in a finally, and is message-discriminated so it won't mask the unrelated "Session switch is still preparing" InvalidStateError. Core suites pass locally (375 tests) and CI Test (ubuntu-latest, Node 22.x) is green at this head. No unresolved Criticals remain. LGTM.

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

Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally; this PR's only real-daemon E2E for the feature (integration-tests/cli/qwen-serve-streaming.test.ts) sits outside every npm workspace test command.

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI; OS-sensitive paths (fsync win32 branch, O_NOFOLLOW win32 exemption, chmod mode preservation) were verified on Linux only.

Not explored to full depth (tool budget reached): "agent reverse-audit (round 1)": none — I finished the walk within budget; nothing was cut short.; "agent reverse-audit (round 2)": full walk of the TranscriptIndex freeze construction in session-transcript-reader.ts (~1700–1790); I verified the resolver projection and both catalog consumers…; "agent reverse-audit (round 2)": Agent-side session_busy / isIdle() and history-mutation flag acquisition ordering in acpAgent.ts (§12.2) beyond what findings #2/#5 already cover.; "agent reverse-audit (round 3)": run AssistantMessage.test.tsx with the catch removed to confirm whether the runner itself fails on the leaked rejection (would settle the Suggestion's confidenc….

中文说明

仅完成部分审查,审查缺口已披露。

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally; this PR's only real-daemon E2E for the feature (integration-tests/cli/qwen-serve-streaming.test.ts) sits outside every npm workspace test command。

未审查:build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI; OS-sensitive paths (fsync win32 branch, O_NOFOLLOW win32 exemption, chmod mode preservation) were verified on Linux only。

未探索到全部深度(达到工具调用预算):"agent reverse-audit (round 1)"none — I finished the walk within budget; nothing was cut short."agent reverse-audit (round 2)"full walk of the TranscriptIndex freeze construction in session-transcript-reader.ts (~1700–1790); I verified the resolver projection and both catalog consumers…"agent reverse-audit (round 2)"Agent-side session_busy / isIdle() and history-mutation flag acquisition ordering in acpAgent.ts (§12.2) beyond what findings #2/#5 already cover."agent reverse-audit (round 3)"run AssistantMessage.test.tsx with the catch removed to confirm whether the runner itself fails on the leaked rejection (would settle the Suggestion's confidenc…

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

Comment on lines +10621 to +10622
const rewindResult = entry.promptQueue.then(async () => {
if (entry.closing) {

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] R9-1: This queued-rewind in-queue guard only checks entry.closing, omitting entry.activeWorkCloseInFlight and the byId.get(sessionId) !== entry identity re-check that the sibling branchSession queue guard (~8499-8505) performs — violating the bridge's own admission contract (isClosingOrAuthorizingClose, bridge.ts:2267: "closing is teardown already under way; activeWorkCloseInFlight is teardown authorized and being confirmed. Both must refuse admission").

Failure scenario: closeIfChildUnheld sets activeWorkCloseInFlight for the conditional-close round trip (up to ACTIVE_WORK_CLOSE_TIMEOUT_MS) before closeSessionImpl sets closing; a rewind queued behind earlier work passes this guard on dispatch and truncates history / restores workspace files for a session already authorized for destruction. If the id was re-registered to a different entry during the window (the hazard closeIfChildUnheld's comment documents), the stale queued rewind dispatches on the old entry's connection and truncates the newly restored session's history. The synchronous admission check cannot cover this: the window opens during the queue wait.

Witness (probe mirroring this PR's own queued-branch conditional-close test, for rewind): unmodified PR — the queued rewind was dispatched into the closing window: AssertionError: expected { code: -32603, …(2) } to be an instance of SessionNotFoundError (the child-side dispatch stub fired, proving dispatch); with the guard aligned as below: Test Files 1 passed, Tests 7 passed. The probe flips.

if (isClosingOrAuthorizingClose(entry) || byId.get(sessionId) !== entry) {
  throw new SessionNotFoundError(sessionId, 'The session is closing', 'session_closing');
}

Note: this is distinct from the existing thread on this gate about the missing 'session_closing' code argument — it is the missing flag/identity disjuncts (the exact shape R5-1 fixed for branch).

中文说明

[Critical] R9-1:rewind 的队列内复查门只检查 entry.closing,遗漏了兄弟 branchSession 队列门(~8499-8505)所执行的 entry.activeWorkCloseInFlightbyId.get(sessionId) !== entry 身份复查——违反 bridge 自身的准入契约(isClosingOrAuthorizingClose,bridge.ts:2267:"closing 是已在进行的拆除;activeWorkCloseInFlight 是已授权、正在确认的拆除。两者都必须拒绝准入")。

失败场景:closeIfChildUnheld 在条件关闭往返期间(最长 ACTIVE_WORK_CLOSE_TIMEOUT_MS)设置 activeWorkCloseInFlight,之后 closeSessionImpl 才设置 closing;排队中的 rewind 在该窗口内派发时通过此门,对已被授权销毁的会话执行历史截断/文件恢复。若窗口期间该 id 被重新注册到另一个 entry(closeIfChildUnheld 注释中记载的危险),过期的排队 rewind 会在旧 entry 的连接上派发,截断新恢复会话的历史。同步准入检查无法覆盖该窗口:窗口正是在队列等待期间打开的。

证据(以本 PR 自带的 queued-branch conditional-close 测试为模板、改写为 rewind 的探针):未修复代码——排队的 rewind 在关闭窗口内被派发:AssertionError: expected { code: -32603, …(2) } to be an instance of SessionNotFoundError(子侧派发桩被触发,证明派发发生);将门改为下述形式后:Test Files 1 passed, Tests 7 passed。探针双向翻转。

建议修复见英文部分代码块。注意:这与该门上已有的"缺少 'session_closing' code 参数"线程是不同的问题——这里是缺少的标志/身份析取项(正是 R5-1 为 branch 修复的形状)。

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

Comment on lines +3509 to 3515
if (this.historyMutationActive) {
throw RequestError.invalidParams(
undefined,
'Session history mutation is in progress',
);
}
if (admissionCancellation?.aborted) {

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] R9-2: This second historyMutationActive admission check has an await gap: between it and this.pendingPrompt = pendingSend (~3559) lies the goal-queue clearing loop's await runtime.releaseTurn(...) (~3538), during which no flag in #hasActiveTurn() is set — so a concurrent beginHistoryMutation() succeeds in that window, and nothing re-checks historyMutationActive before the turn runs.

Failure scenario: with an active goal holding queued continuations, an interactive prompt passes both historyMutationActive checks and suspends in the clearing loop; a branch/rewind admitted via a path not gated on the bridge pendingPromptCount (e.g. direct-ACP mode) calls beginHistoryMutation() successfully and rewrites history while the prompt then runs a full turn — including capturing its branch-checkpoint cursor — concurrently with the active mutation.

Witness (probe in the Session.test.ts harness; goal runtime mocked active with one queued continuation whose releaseTurn hangs on a gate): unmodified — PROBE_GAP {"pendingPromptSet":false,"mutationAdmitted":true,…,"promptOutcome":{"stopReason":"end_turn"}} (mutation admitted mid-admission, prompt ran a complete turn); with pendingPrompt installed before the goal loop — {"pendingPromptSet":true,"mutationAdmitted":false,"mutationError":"RequestError: Session is busy processing a turn"}, and the 7 existing history-mutation tests still pass. The probe flips.

Suggested fix: re-check the flag after the last await before admission commits (immediately before this.pendingPrompt = pendingSend, releasing the goal reservation and throwing the same error if set), or move this check down to that point.

中文说明

[Critical] R9-2:第二道 historyMutationActive 准入检查存在 await 间隙:在该检查与 this.pendingPrompt = pendingSend(~3559)之间是 goal 队列清理循环的 await runtime.releaseTurn(...)(~3538),期间 #hasActiveTurn() 中没有任何标志被置位——并发的 beginHistoryMutation() 可在该窗口内成功,且在回合继续执行前没有任何地方复查 historyMutationActive

失败场景:当活跃 goal 持有排队的续写时,交互式 prompt 通过两道 historyMutationActive 检查后挂起在清理循环中;经由不受 bridge pendingPromptCount 门控的路径(如 direct-ACP 模式)准入的 branch/rewind 成功调用 beginHistoryMutation() 并改写历史,而 prompt 随后仍会完整执行一个回合——包括捕获 branch-checkpoint 游标——与活动变更并发。

证据(Session.test.ts 测试台探针;goal runtime mock 为活跃且带一个挂起在门上的 releaseTurn 的排队续写):未修复——PROBE_GAP {"pendingPromptSet":false,"mutationAdmitted":true,…,"promptOutcome":{"stopReason":"end_turn"}}(变更在准入中途被接纳,prompt 完整执行);将 pendingPrompt 提前到 goal 循环之前安装后——{"pendingPromptSet":true,"mutationAdmitted":false,"mutationError":"RequestError: Session is busy processing a turn"},且既有 7 个 history-mutation 测试仍全部通过。探针双向翻转。

建议修复:在准入提交前的最后一个 await 之后(即 this.pendingPrompt = pendingSend 之前)复查该标志(若已置位则释放 goal 预留并抛出相同错误),或将该检查下移到该点。

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

Comment on lines +1751 to +1754
const branchPointsByAssistantUuid = new Map(
[
...resolveBranchPoints(
replayUuids.flatMap((uuid) => {

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] R9-4: Reader↔fork resolver divergence. The reader resolves branch points over the side-task-filtered chain (replayUuids = runtimeUuids.slice(sourceBoundary).filter(!inherited)), while forkSession validates over the FULL chain (resolveBranchPoints(currentActiveMessages), sessionService.ts:1915), and the recorder's incremental entry shares the full-chain semantics (carriedFromPrefix). A dangling tool call inherited from the side-task prefix that a later turn closes via name-only uniqueNameMatch yields a checkpoint the write/fork side accepts but the read side silently drops.

Failure scenario: a side-task session inherits a dangling call-1 from its prefix; a later turn's name-only functionResponse closes it (single-name match) and recordBranchCheckpointTransaction writes checkpoint c2. forkSession(c2) succeeds, but the reader's filtered chain has no pendingCallsAtBoundary entry for the carried call, treats the response as an orphan, and skips c2 — the replay/UI never advertises a branch anchor that fork validation honors. This defeats design Goal 6 ("eligibility … authoritative in Core and identical for recording, replay, and fork validation").

Witness (probe on a real on-disk side-task fixture run through the actual code): recorder={"startExclusiveRecordUuid":"c1",…,"assistantRecordUuid":"a4"} | forkSession(c2)=success copiedCount=11 | reader={"pageRecords":["ss1","u2","at1","tr0","a3","c1","u3","tr2","a4","c2"],"branchPointsByAssistantUuid":null} — the page contains assistant a4 and even the checkpoint record c2, yet no branch anchor is advertised, while fork validation honors the same checkpoint.

Suggested fix: make the interval scan's verdict independent of elidable prefix state — e.g. treat an unmatchable response as a no-op during resolution (only unclosed fresh calls veto), or pass the unfiltered active chain to resolveBranchPoints here and filter only the returned markers.

中文说明

[Critical] R9-4:读取侧与 fork 侧的解析器分歧。reader 在 side-task 过滤后的链上解析分支点(replayUuids = runtimeUuids.slice(sourceBoundary).filter(!inherited)),而 forkSession 在完整链上校验(resolveBranchPoints(currentActiveMessages),sessionService.ts:1915),recorder 的增量入口也共享完整链语义(carriedFromPrefix)。从 side-task 前缀继承的悬空工具调用若被后续回合以仅名称 uniqueNameMatch 闭合,会产生写入/fork 侧接受、读取侧静默丢弃的 checkpoint。

失败场景:side-task 会话从前缀继承悬空 call-1;后续回合的仅名称 functionResponse(唯一名称匹配)将其闭合,recordBranchCheckpointTransaction 写入 checkpoint c2。forkSession(c2) 成功,但 reader 的过滤链中 pendingCallsAtBoundary 没有该继承调用的条目,将响应视为孤儿并跳过 c2——回放/UI 永远不展示 fork 校验所承认的分支锚点。这违背设计 Goal 6(资格判定"在 Core 层权威,且录制、重放、fork 校验三者一致")。

证据(以真实磁盘 side-task fixture 驱动真实代码的探针):recorder={"startExclusiveRecordUuid":"c1",…,"assistantRecordUuid":"a4"} | forkSession(c2)=success copiedCount=11 | reader={"pageRecords":["ss1","u2","at1","tr0","a3","c1","u3","tr2","a4","c2"],"branchPointsByAssistantUuid":null}——页面包含 assistant a4 甚至 checkpoint 记录 c2,却没有展示任何分支锚点,而 fork 校验承认同一 checkpoint。

建议修复:让区间扫描的判定独立于可省略的前缀状态——例如解析时把无法匹配的响应视为无操作(只有未闭合的新调用才具否决权),或在此处把未过滤的活动链传给 resolveBranchPoints、只对返回的标记做过滤。

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

Comment on lines +11268 to +11271
if (!session.isTurnIdle()) {
throw new RequestError(
-32602,
'Cannot rewind while a prompt is running',

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] R9-5: The rewind fail-fast gate (isTurnIdle()) is blind to the queue-held/latch-free state. The branch body occupies this session's mutation queue but sets the latch only AFTER await sourceSession.assertCanStartTurn() (a write-barrier wait) and await computeUniqueBranchTitle(...) (~11578-11598). Barrier holders that keep the latch unset exist: side-task forks run runWithWriteBarrier(fork) without the agent queue or latch (~11632-11645), and assertCanStartTurn chains on operationTail behind them (chatRecordingService.ts:1335).

Failure scenario: a branch stalls at assertCanStartTurn behind a barrier holder; historyMutationActive is still false, so a well-formed rewind passes this gate and blocks unboundedly in runExclusiveHistoryMutation's await previous (the rewind caller passes no waitTimeoutMs). If the barrier holder plus branch fork exceed the client's deadline (SDK rewind uses the generic fetchTimeoutMs, default 30s), the caller is told the rewind failed and the destructive history truncation + file restores land afterwards — the exact hazard the adjacent fail-fast comment claims to close. rewindToTurn's in-gate re-check passes because the session is idle when the wait ends.

Witness (probe with the real agent queue + handler ordering; Session mocked to mirror latch↔isTurnIdle semantics): PR arm, while branch held the queue inside assertCanStartTurn{"rewindState":"pending","latchCallsAtAdmission":0}, then after release the rewind entered its mutation body (where a real Session truncates); fixed arm (latch acquired before the barrier probe) — {"rewindState":"rejected","rewindErrorKind":"session_busy","latchCallsAtAdmission":1}. The probe flips.

Suggested fix: acquire the latch before the async admission wait in the branch body (move beginHistoryMutation() ahead of assertCanStartTurn(), releasing on failure) so the queue is never held latch-free; or make the rewind's queue admission fail fast when the tail is not immediately free. Add a test that occupies the queue with the latch free and asserts a well-formed rewind rejects promptly.

中文说明

[Critical] R9-5:rewind 快速失败门(isTurnIdle()``)对"队列被占用但门闩未置位"的状态是盲的。branch 主体占用该会话的变更队列,但只在 await sourceSession.assertCanStartTurn()(写屏障等待)与 await computeUniqueBranchTitle(...)之后才置位门闩(~11578-11598)。存在保持门闩未置位的屏障持有者:side-task fork 不经 agent 队列与门闩直接runWithWriteBarrier(fork)(~11632-11645),而 assertCanStartTurn会排在它们之后的operationTail` 上(chatRecordingService.ts:1335)。

失败场景:branch 在屏障持有者之后阻塞于 assertCanStartTurn;此时 historyMutationActive 仍为 false,良构的 rewind 通过此门并在 runExclusiveHistoryMutationawait previous 上无界阻塞(rewind 调用方未传 waitTimeoutMs)。若屏障持有者加 branch fork 的时长超过客户端期限(SDK rewind 使用通用 fetchTimeoutMs,默认 30s),调用方被告知 rewind 失败,而破坏性的历史截断 + 文件恢复在其后才真正执行——正是相邻快速失败注释声称要消除的危险。rewindToTurn 的门内复查会通过,因为等待结束时会话已空闲。

证据(真实 agent 队列 + 处理器顺序的探针;Session mock 反映门闩↔isTurnIdle 语义):PR 侧,branch 在 assertCanStartTurn 内占用队列时——{"rewindState":"pending","latchCallsAtAdmission":0},释放后 rewind 进入其变更主体(真实 Session 在此截断);修复侧(屏障探测前先获取门闩)——{"rewindState":"rejected","rewindErrorKind":"session_busy","latchCallsAtAdmission":1}。探针双向翻转。

建议修复:在 branch 主体的异步准入等待之前获取门闩(把 beginHistoryMutation() 移到 assertCanStartTurn() 之前,失败时释放),使队列永远不会在门闩未置位时被占用;或让 rewind 的队列准入在尾不可立即获得时快速失败。补充一个"队列被占用但门闩空闲"的测试,断言良构 rewind 立即被拒绝。

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

Comment on lines +7580 to +7584
if (
error instanceof DOMException &&
error.name === 'InvalidStateError' &&
error.message === 'A branch request is already in progress'
) {

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] R9-6: This concurrent-branch swallow re-implements webui's DOMException contract as a three-clause literal match (instanceof + name + exact message), duplicated again in the App.test.tsx mock (~6598-6602), while this same PR establishes the exported-predicate pattern for exactly this class — isStaleBranchPointError, documented as living in the SDK "so that copies don't drift".

Failure scenario: rewording the throw site (webui/src/daemon/session/actions.ts:1632-1635, e.g. 'Branch request already in progress') silently breaks the match; the error then falls through to reportError(error, t('branch.failed')) and a benign same-session double-click surfaces a misleading "Failed to branch session." toast. web-shell already consumes webui's DaemonSessionActions, so an exported predicate next to the throw is a natural fit.

Suggested fix: export a shared predicate or typed error (e.g. isConcurrentBranchError(error) next to isStaleBranchPointError, or have the action layer throw a typed error) and use it in both webui and web-shell.

中文说明

[Suggestion] R9-6:该并发 branch 吞错逻辑把 webui 的 DOMException 契约重新实现为三小句字面匹配(instanceof + name + 精确 message),并在 App.test.tsx 的 mock 中再次复制(~6598-6602)——而本 PR 自己已为同类契约建立了导出谓词模式:isStaleBranchPointError,其注释明确说明放在 SDK 是为了"防止副本漂移"。

失败场景:修改抛出点措辞(webui/src/daemon/session/actions.ts:1632-1635,例如 'Branch request already in progress')会静默破坏匹配;错误随后落入 reportError(error, t('branch.failed')),同会话的良性双击会显示误导性的 "Failed to branch session." 提示。web-shell 本就消费 webui 的 DaemonSessionActions,在抛出点旁导出谓词是自然的方案。

建议修复:导出共享谓词或类型化错误(如在 isStaleBranchPointError 旁增加 isConcurrentBranchError(error),或让 action 层抛出类型化错误),webui 与 web-shell 统一使用。

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

await bridge.shutdown();
});

it('rejects a queued branch when a conditional close starts before dispatch', async () => {

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] R9-16: This new queued-branch conditional-close test pins only the activeWorkCloseInFlight disjunct of the branch queue guard; the identity re-check (byId.get(sessionId) !== entry, bridge.ts:8499-8502) is unpinned — the entry is never re-registered under the same id in this test's flow.

Failure scenario: branch1 in flight, branch2 queued, conditional close completes and removes the entry, another client loads the same persisted id (fresh entry under the same id — the hazard closeIfChildUnheld's comment documents), branch1 releases: a refactor dropping the identity clause (the exact shape the sibling rewind queue guard is already missing, see R9-1) dispatches the queued branch on the stale entry's connection, and this test stays green because its SessionNotFoundError fires via the flag disjunct alone.

Suggested fix: add a sibling test that re-registers the same session id (load/spawn of the same persisted id) after the conditional close completes but before the first branch is released, and asserts the queued branch rejects with SessionNotFoundError without reaching the channel.

中文说明

[Suggestion] R9-16:这个新的 queued-branch conditional-close 测试只固定了 branch 队列门的 activeWorkCloseInFlight 析取项;身份复查(byId.get(sessionId) !== entry,bridge.ts:8499-8502)未被固定——本测试流程中 entry 从未以相同 id 被重新注册。

失败场景:branch1 进行中,branch2 排队,conditional close 完成并移除 entry,另一客户端加载同一持久化 id(以相同 id 产生新 entry——closeIfChildUnheld 注释记载的危险),branch1 释放:若重构删除身份子句(兄弟 rewind 队列门已然缺失的形状,见 R9-1),排队的 branch 会在过期 entry 的连接上派发,而本测试仍为绿——因为其 SessionNotFoundError 仅由标志析取项触发。

建议修复:补充一个兄弟测试——在 conditional close 完成之后、首个 branch 释放之前,重新注册同一会话 id(加载/派生同一持久化 id),断言排队的 branch 以 SessionNotFoundError 被拒绝且未到达通道。

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

Comment on lines +230 to +231
startExclusiveRecordUuid: 'old-tail',
pendingCallsAtStart: [{ name: 'read_file' }],

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] R9-18: The carried-pending-call machinery's rebase recompute is unpinned: deleting this.pendingBranchToolCalls = collectPendingBranchToolCalls(this.activeBranchRecords) from updateActiveBranch's rebase branch (chatRecordingService.ts, rebase path) ships with all 88 existing tests green. (The rebuildTurnBoundaries rebuild, per-append updates, and the cursor snapshot ARE pinned — the genuinely unpinned site is this rebase recompute and the rewind→checkpoint interaction.)

Failure scenario (mutation-proven): after a rewind/branch-restore that rebases while a crashed turn's call is dangling, the cursor carries a stale pending list; when the crashed turn's tool result arrives in a later turn (the exact side-task scenario carriedFromPrefix was built for), closeToolCall sees an orphan response and rejects the candidate, so recordBranchCheckpointTransaction silently writes no checkpoint — branch affordances vanish for the rest of the session while every test stays green.

Witness: mutation (rebase recompute removed) — all 88 chatRecordingService tests pass, ships green; probe — pending tool calls after rewinding past the dangling call: expected [ { id: 'call-x', name: 'read_file' } ] to deeply equal [] (stale dangling call survives a rewind that truncates it); probe passes on PR code. The probe flips.

Suggested fix: add a ChatRecordingService-level test where getBranchCheckpointCursor is called for real on a chain containing a dangling call, rewind truncates it, and a subsequent turn's checkpoint transaction resolves correctly (or unit-test updateActiveBranch's rebase recompute directly).

中文说明

[Suggestion] R9-18:carried-pending-call 机制的 rebase 重算未被固定:从 updateActiveBranch 的 rebase 分支中删除 this.pendingBranchToolCalls = collectPendingBranchToolCalls(this.activeBranchRecords)(chatRecordingService.ts rebase 路径)后,既有 88 个测试全部为绿。(rebuildTurnBoundaries 的重建、逐条 append 更新、游标快照均有固定——真正未固定的是该 rebase 重算及 rewind→checkpoint 交互。)

失败场景(突变验证):在崩溃回合的调用仍悬空时发生 rewind/branch-restore 触发 rebase,游标携带过期的 pending 列表;当崩溃回合的工具结果在后续回合到达(正是 carriedFromPrefix 为之而生的 side-task 场景),closeToolCall 视为孤儿响应并否决候选,于是 recordBranchCheckpointTransaction 静默不写 checkpoint——会话余下时间的分支入口消失,而所有测试保持绿色。

证据:突变(移除 rebase 重算)——88 个 chatRecordingService 测试全部通过,静默通过;探针——pending tool calls after rewinding past the dangling call: expected [ { id: 'call-x', name: 'read_file' } ] to deeply equal [](过期悬空调用在截断它的 rewind 后仍然存在);探针对 PR 代码通过。探针双向翻转。

建议修复:新增 ChatRecordingService 级测试——对含悬空调用的链真实调用 getBranchCheckpointCursor,rewind 截断该调用,随后回合的 checkpoint 事务正确解析(或直接单测 updateActiveBranch 的 rebase 重算)。

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

Comment on lines +3520 to +3521
trackedFileBackups: Object.fromEntries(
backupNames.map((backupFileName, index) => [

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] R9-19: The basename clause of collectReferencedFileHistoryBackupNames (path.basename(backup.backupFileName) === backup.backupFileName, sessionService.ts:2609) has zero test coverage: the sibling dot-name test is caught entirely by the explicit !== '.'/!== '..' comparisons, because path.basename('.')/('..') return the name unchanged — the basename clause is never exercised.

Failure scenario (mutation-proven): a refactor dropping or inverting the basename clause ships green (the dot test still passes). Then a transcript whose snapshot references a /-containing backup name (hand-edited or corrupted JSONL — jsonl.read parses arbitrary records off disk) reaches validatedBackupPath inside copyFileHistoryBackupsToStaging, which throws Invalid file-history backup name uncaught — turning the intended graceful omit-and-warn degradation (the behavior the dot test's title pins: "instead of failing the fork") into a hard fork failure for the whole session.

Witness: Node check path.basename('sub/x') === 'x'. Mutation (basename clause removed) — all 31 forkSession tests pass including the dot test, ships green; probe flips from pass to promise rejected "Error: Invalid file-history backup name: sub/dir/backup" instead of resolving; clause restored → fork succeeds, only backup-ok copied.

Suggested fix: extend the dot-name test (or add a sibling) seeding a backup name like 'nested/backup' via the helper and assert the fork still resolves and omits it, e.g. appendFileHistorySnapshot(oldId, file, lines, ['nested/backup', 'backup-ok']) with expect(fs.readdirSync(targetBackupDir)).toEqual(['backup-ok']).

中文说明

[Suggestion] R9-19:collectReferencedFileHistoryBackupNames 的 basename 子句(path.basename(backup.backupFileName) === backup.backupFileName,sessionService.ts:2609)零测试覆盖:兄弟 dot-name 测试完全由显式 !== '.'/!== '..' 比较捕获,因为 path.basename('.')/('..') 原样返回名称——basename 子句从未被执行。

失败场景(突变验证):删除或反转 basename 子句的重构全绿通过(dot 测试仍然通过)。此后,快照引用含 / 备份名的 transcript(手改或损坏的 JSONL——jsonl.read 会解析磁盘上的任意记录)会到达 copyFileHistoryBackupsToStaging 内的 validatedBackupPath,其抛出的 Invalid file-history backup name 无人捕获——把预期的"优雅省略并告警"降级(dot 测试标题所固定的行为:"instead of failing the fork")变成整个会话的硬性 fork 失败。

证据:Node 检查 path.basename('sub/x') === 'x'。突变(移除 basename 子句)——31 个 forkSession 测试(含 dot 测试)全部通过,静默通过;探针由通过翻转为 promise rejected "Error: Invalid file-history backup name: sub/dir/backup" instead of resolving;恢复子句 → fork 成功,仅复制 backup-ok

建议修复:扩展 dot-name 测试(或新增兄弟测试),通过 helper 注入 'nested/backup' 之类的备份名,断言 fork 仍能解析并省略它,例如 appendFileHistorySnapshot(oldId, file, lines, ['nested/backup', 'backup-ok'])expect(fs.readdirSync(targetBackupDir)).toEqual(['backup-ok'])

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

Comment on lines +2017 to +2019
if (boundary !== null && !retainedUuids.has(boundary)) {
boundary = nearestRetainedPredecessor.get(boundary) ?? null;
}

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] R9-20: A checkpoint whose startExclusiveRecordUuid is absent from preFilterRecords entirely (not merely filtered out) is silently widened to null (whole chain): nearestRetainedPredecessor only contains entries for records present in preFilterRecords, so get(boundary) === undefined and ?? null coalesces the unknown-boundary case with the legitimate head-of-chain case — the exact widening the adjacent comment says this block prevents.

Failure scenario (probe-verified): the live writer cannot produce this (cursor validation keeps boundaries on-chain), but an imported, hand-edited, or corruption-repaired transcript can carry an on-chain checkpoint whose boundary points off the active chain: the source session's resolveBranchPoints skips it (dangling boundary → startIndex < 0), but forkSession rewrites the boundary to null; if whole-chain candidate resolution then matches the payload's assistantRecordUuid, the fork publishes a branch point the source never validated — branching from it rewinds the forked session to the very start instead of the intended boundary.

Witness: probe on a source transcript carrying a checkpoint with startExclusiveRecordUuid: 'ghost-uuid-not-on-chain' — source resolver skips it (has === false); a latest-state fork rewrote the payload to startExclusiveRecordUuid: null and resolved it (has === true); with the fix below the fork retains the ghost boundary, the resolver skips it, source/fork agree, and all 33 forkSession tests pass. The probe flips.

const remapped = nearestRetainedPredecessor.get(boundary);
if (remapped !== undefined) {
  boundary = remapped;
}
中文说明

[Suggestion] R9-20:startExclusiveRecordUuid 完全不在 preFilterRecords 中(而非仅被过滤掉)的 checkpoint 会被静默扩展为 null(整条链):nearestRetainedPredecessor 只包含 preFilterRecords 中存在记录的条目,因此 get(boundary) === undefined?? null 把未知边界与合法的链首两种情况合并——正是相邻注释声称本块要防止的扩展。

失败场景(探针验证):在线写入器不会产生该情况(游标校验保证边界在链上),但导入、手改或损坏修复后的 transcript 可能携带边界指向活动链之外的在链 checkpoint:源会话的 resolveBranchPoints 跳过它(悬空边界 → startIndex < 0),而 forkSession 把边界改写为 null;若全链候选解析随后匹配上 payload 的 assistantRecordUuid,fork 就发布了一个源会话从未校验过的分支点——从它分支会把 fork 会话回退到最开头而非预期边界。

证据:对携带 startExclusiveRecordUuid: 'ghost-uuid-not-on-chain' checkpoint 的源 transcript 运行探针——源解析器跳过(has === false);最新状态 fork 把 payload 改写为 startExclusiveRecordUuid: null 并成功解析(has === true);应用下述修复后 fork 保留幽灵边界、解析器跳过、源/fork 一致,且 33 个 forkSession 测试全部通过。探针双向翻转。

建议修复见英文部分代码块(区分"映射到链首"与"未映射")。

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

if (connectionRef.current.sessionId !== sourceSessionId) return;
pushToast(
'error',
t(refreshed ? 'branch.stale' : 'branch.staleRefreshFailed'),

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] R9-21: The stale-recovery refreshed === false arm (the branch.staleRefreshFailed toast) has zero positive test coverage: none of the six new branch tests makes reloadSession reject with a non-abort error while the session is unchanged (staleRefreshFailed appears only in App.tsx and i18n.tsx). The sibling branch.staleUnsupported arm is already tracked by an existing open comment and is not re-reported here.

Failure scenario: a regression dropping the failure arm, inverting the ternary, or always showing the success string ships green — on a daemon where the recovery reload genuinely fails (network error, 5xx), the user is told "The transcript has been refreshed" while the stale transcript is still on screen.

Suggested fix: add a test where mockSessionActions.reloadSession rejects with a non-abort Error (session unchanged) and assert onToast receives the branch.staleRefreshFailed text.

中文说明

[Suggestion] R9-21:stale 恢复的 refreshed === false 分支(branch.staleRefreshFailed 提示)零正向测试覆盖:六个新 branch 测试中没有一个让 reloadSession 在会话未切换时以非 abort 错误失败(staleRefreshFailed 只出现在 App.tsx 与 i18n.tsx 中)。兄弟分支 branch.staleUnsupported 已有未关闭的评论跟踪,此处不重复报告。

失败场景:删除失败分支、反转三元表达式、或总是显示成功文案的回归都能全绿通过——当恢复重载在某个 daemon 上真实失败(网络错误、5xx)时,用户会看到 "The transcript has been refreshed",而屏幕上仍是过期 transcript。

建议修复:新增测试——mockSessionActions.reloadSession 以非 abort 的 Error 失败(会话未变),断言 onToast 收到 branch.staleRefreshFailed 文案。

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

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

@/tmp/8817-review.md

@water-in-stone
water-in-stone dismissed qwen-code-ci-bot’s stale review August 15, 2026 10:01

All the comments have been resolved

@water-in-stone
water-in-stone added this pull request to the merge queue Aug 15, 2026
Merged via the queue into QwenLM:main with commit 9f8f65d Aug 15, 2026
281 of 282 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.13.

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.

7 participants