Skip to content

feat: fork from any conversation - #8274

Closed
water-in-stone wants to merge 45 commits into
QwenLM:mainfrom
water-in-stone:feat/fork-from-one-conversation
Closed

feat: fork from any conversation#8274
water-in-stone wants to merge 45 commits into
QwenLM:mainfrom
water-in-stone:feat/fork-from-one-conversation

Conversation

@water-in-stone

@water-in-stone water-in-stone commented Aug 1, 2026

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

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 1, 2026
@water-in-stone water-in-stone changed the title feat:fork from one conversation feat: fork from any conversation Aug 1, 2026
@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Thanks for this — it's a substantial piece of work and the design doc is genuinely helpful.

Template: complete ✓ — all sections present, with a real reviewer test plan and before/after evidence.

Problem: this is a feature request (#8271), self-reported and on the roadmap/session-management track (P3). The gap is real and user-facing — today you can only branch from the latest session state, so exploring an alternative direction from an earlier Assistant response means manually recreating context. That's a legitimate problem, not theoretical hardening. Worth noting it's self-reported and low priority, so the bar is "is this the right thing to build and is it safe to land," not "is this urgent."

Direction: aligned. Conversation forking is an active area in the reference product (claude-code ships /fork, fork-session lineage, and checkpoint pruning), and this extends the idea in a sensible direction — durable checkpoints as a single source of truth shared by recording, replay, UI, and Core validation, rather than trusting a visible message as the branch point. The issue deliberately splits the work; this PR is Part 1 (branch from any eligible response), with worktree isolation (Part 2) deferred. One linking nit: the body says Closes #8271, which would close the whole issue including the not-yet-implemented Part 2 — a plain reference (or scoping the close) would avoid that.

Size: this is the thing a maintainer needs to see. ~2,099 production-logic lines (additions+deletions, excluding ~1,796 test lines and the ~791-line design doc) across 48 files and 6 packages, with the weight in core session infrastructure (packages/core/src/services/sessionService.ts alone is ~637 changed lines, plus a new branch-points.ts service). Per the core-module gate, a feat of this size touching core is not blocked, but it does cross both the 500-line maintainer-awareness threshold and the 1,000-line large-PR advisory. Flagging it here so it gets a maintainer's architectural sign-off rather than sliding through on review quality alone. Splitting isn't obviously feasible given how tightly the durability, crash-safety, and concurrency pieces couple, but I'll leave that judgment to the maintainer.

Approach: the scope feels earned rather than padded — each layer maps to a stated requirement (durable checkpoint, crash-safe atomic creation, stale-checkpoint rejection, UI surface). I don't see a materially simpler path that still meets the "never expose a partially created session" constraint. Implementation-level observations are in the next comment.

Risk: elevated. The diff touches packages/cli/src/acp-integration/ (acpAgent.ts, session/Session.ts, history-replay-page.ts), which is on the high-revert-correlation path list for this repo. That doesn't block anything, but it means the code review below goes to full depth and the CI evidence is load-bearing before any approval.

Moving on to code review. 🔍

中文说明

感谢贡献,工作量很大,设计文档也很有帮助。

模板: 完整 ✓ —— 各节齐全,有真实的 reviewer 测试计划和 before/after 证据。

问题: 这是一个 feature request(#8271),由作者自己提出,挂在 roadmap/session-management 路线上(P3)。缺口是真实且面向用户的——目前只能从会话最新状态分支,想从更早的一条 Assistant 回复探索另一条路径,只能手动重建上下文。这是合理的问题,不是理论性加固。需要说明:它是自我提出且低优先级的,所以标准是"该不该做、能否安全落地",而不是"是否紧急"。

方向: 对齐。会话 fork 在参考产品里是活跃方向(claude-code 已有 /fork、fork 会话谱系、checkpoint 清理),本 PR 把思路往合理方向延伸——用持久化 checkpoint 作为录制、回放、UI 和 Core 校验共享的唯一事实来源,而不是把界面上可见的消息当作分支点。issue 有意拆分了工作;本 PR 是 Part 1(从任意符合条件的回复分支),worktree 隔离(Part 2)延后。一个关联小问题:正文写了 Closes #8271,会连同尚未实现的 Part 2 一起关掉——改成普通引用(或限定关闭范围)更稳妥。

规模: 这是维护者需要看到的重点。约 2,099 行生产逻辑(增+删,不含约 1,796 行测试和 791 行设计文档),跨 48 个文件、6 个 package,重量集中在核心会话基础设施(仅 packages/core/src/services/sessionService.ts 就改动约 637 行,外加新增的 branch-points.ts 服务)。按核心模块门禁,这种规模的 feat 触及核心不会被拦截,但确实同时越过 500 行维护者关注阈值和 1,000 行大 PR 提示线。在此标出,以便获得维护者的架构层面确认,而不是仅凭 review 质量通过。鉴于持久化、崩溃安全、并发这几块耦合紧密,拆分未必可行,这个判断留给维护者。

方案: 范围是"挣来的"而非堆砌——每一层都对应一个明确需求(持久化 checkpoint、崩溃安全的原子创建、过期 checkpoint 拒绝、UI 层面)。在满足"绝不暴露部分创建的会话"这一约束的前提下,我没有看到明显更简的路径。实现层面的观察见下一条评论。

风险: 升级。diff 触及 packages/cli/src/acp-integration/acpAgent.tssession/Session.tshistory-replay-page.ts),这是本仓库高回滚相关路径之一。这不拦截任何东西,但意味着下面的代码审查要走完整深度,且 CI 证据在任何批准前都是关键依据。

进入代码审查。🔍

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

I wrote my independent proposal first (a versioned checkpoint record appended to the transcript, validated against the reconstructed active chain, with branch creation that truncates the raw JSONL at the checkpoint and refuses stale points), then read the diff. The PR matches or exceeds it — and goes further than I would have on crash safety.

The core design is sound. branch-points.ts treats a checkpoint as valid only when the turn is genuinely complete: it replays tool-call/tool-response pairing across the chain, bails on any unbalanced call, and requires exactly one final visible-text Assistant record after the last tool result. The payload is versioned (v: 1) and defensively parsed. resolveBranchPoints de-duplicates by assistant UUID and drops ambiguous points rather than guessing.

Two pieces are notably careful:

  • Concurrency. ChatRecordingService grows a "topology fence" so async metadata writers (titles, artifacts) that land during a checkpoint transaction are buffered and re-parented onto the checkpoint instead of becoming its siblings — this is what keeps the recorded topology authoritative. In acpAgent.ts, a new per-session promise-chain mutex (runExclusiveHistoryMutation) serializes prompt / rewind / close / branch, with a regression test asserting a close can't finalize the source recorder mid-branch. This directly answers the issue's "concurrent transcript mutations" concern.
  • Crash safety. forkSession now stages through a claim file + staging dir + owner tokens, publishes the transcript via an atomic hard link, fsyncs along the way, validates the branch point twice (before staging and after building the forked records), and on failure deliberately preserves ambiguous resources for a stale-creation GC rather than risk deleting something it doesn't own. The GC (cleanupStaleBranchCreations) is conservative in the same direction — it refuses to touch anything with an owner-marker mismatch. Path-traversal is guarded (validatedBackupPath). This is the right instinct for "never expose a partially created session."

The serve route validates atRecordId (400 on non-string) and maps branch_point_invalid → 409; the ACP layer maps it to -32009. compactionEngine now merges nested qwenTranscript meta so checkpoints survive compaction instead of being overwritten. Conventions look clean (ESM, kebab-case core files, collocated tests), and test coverage is heavy (~1,796 test lines, including concurrency and stale-checkpoint cases).

No critical blockers found in static review. Two non-blocking notes:

  1. The body says Closes #8271, but the PR implements only Part 1 of that issue (branch from any response); Part 2 (worktree isolation) is not here. Worth scoping the close so Part 2 isn't silently closed.
  2. This is a static review only — I did not build or run the PR (CI path). The behavioural claims below are not independently exercised here.
sequenceDiagram
    participant P1 as Web Shell UI
    participant P2 as Serve route
    participant P3 as Bridge and Agent
    participant P4 as SessionService
    participant P5 as Filesystem
    P1->>P2: POST branch with atRecordId
    P2->>P3: branchSession atRecordId
    P3->>P3: serialize via history mutex
    P3->>P4: forkSession atRecordId
    P4->>P4: resolveBranchPoints then validate
    P4->>P5: claim then stage transcript and backups
    P4->>P5: atomic link publish
    P4-->>P3: new session path
    P3-->>P2: attached session
    P2-->>P1: 201 created
Loading
Files changed (28 of 48 shown)
File What changed
packages/core/src/services/branch-points.ts New service: versioned checkpoint payload, completed-turn validation, branch-point resolution with de-dup
packages/core/src/services/chatRecordingService.ts Topology fence buffering concurrent writes; recordBranchCheckpointTransaction; new branch_checkpoint subtype
packages/core/src/services/sessionService.ts forkSession gains atRecordId; crash-safe claim/stage/publish; stale-creation GC; backup path guards
packages/core/src/services/session-transcript-reader.ts Projects validated branch points per transcript page
packages/core/src/index.ts Export new branch symbols
packages/acp-bridge/src/bridge.ts branchSession forwards atRecordId; error mapping
packages/acp-bridge/src/compactionEngine.ts Merge nested qwenTranscript meta so checkpoints survive compaction
packages/acp-bridge/src/bridgeTypes.ts atRecordId option type
packages/cli/src/acp-integration/acpAgent.ts Per-session history-mutation mutex; branch handler maps BranchPointInvalidError to -32009 (high-risk path)
packages/cli/src/acp-integration/session/Session.ts Accepts the mutation serializer; branch plumbing (high-risk path)
packages/cli/src/acp-integration/session/history-replay-page.ts Expose branch points in the replay page (high-risk path)
packages/cli/src/serve/routes/session.ts POST branch accepts atRecordId; 400 on non-string; 409 on invalid point
packages/cli/src/serve/server/error-response.ts branch_point_invalid error kind
packages/sdk-typescript/src/daemon/DaemonClient.ts branchSession atRecordId parameter
packages/sdk-typescript/src/daemon/ui/transcript.ts Branch points in UI transcript model
packages/sdk-typescript/src/daemon/ui/normalizer.ts Normalize branch points from events
packages/sdk-typescript/src/daemon/ui/types.ts Branch point UI types
packages/sdk-typescript/src/daemon/types.ts Daemon event types for branch points
packages/web-shell/client/App.tsx Branch action handler wiring
packages/web-shell/client/components/messages/AssistantMessage.tsx Branch action on eligible Assistant responses
packages/web-shell/client/components/MessageList.tsx Thread branch points to messages
packages/web-shell/client/adapters/transcriptToMessages.ts Carry branch points into message view model
packages/web-shell/client/i18n.tsx Branch action strings
packages/webui/src/daemon/session/DaemonSessionProvider.tsx Branch action provider wiring
packages/webui/src/daemon/session/actions.ts Branch action plumbing
docs/design/web-shell/assistant-response-session-branching.md New design doc (791 lines)
packages/core/src/services/sessionService.test.ts +616 lines: fork-at-record, crash-safety, GC, stale-point cases
packages/core/src/services/branch-points.test.ts +263 lines: completed-turn validation and resolution
…and 20 more files Mostly collocated tests (acpAgent, bridge, server, web-shell/webui components) plus small type plumbing

Test evidence (PR's own CI)

CI on the reviewed commit is still running — no failures so far, but the load-bearing suites have not completed. I fetched this once via the API and did not poll; the finalize job updates the table below when CI settles. The macOS/Windows test jobs are skipped (gated behind the Ubuntu suite), so cross-platform coverage is not yet established either.

Final CI results for ed32544 (auto-updated by the triage finalize job after CI completed):

Check Conclusion
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Classify PR ✅ success
macos-latest / Java 21 ✅ success
Real daemon E2E / Java 11 ✅ success
Serve A/B (ubuntu-latest, Node 22.x) ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
ubuntu-latest / Java 11 ✅ success
ubuntu-latest / Java 17 ✅ success
ubuntu-latest / Java 21 ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
windows-latest / Java 21 ✅ success

One row per check name (latest run); skipped checks omitted; failures sort first. / 每个检查名一行(取最新一次运行),省略 skipped,失败项排在最前。

Sandboxed verification would settle the behavioural core here: @qwen-code /verify — that forkSession(atRecordId) actually produces a session whose transcript ends at the chosen Assistant turn (and that a stale/inactive checkpoint is rejected with branch_point_invalid rather than silently falling back to latest state) is not observable from the diff, and a green suite could pass with that truncation logic weakened. @qwen-code /tmux would additionally confirm the Web Shell surface — that the Branch action appears only on eligible completed responses and that selecting it switches to the truncated session. The author has write access, so both lanes are available directly; given the high-risk acp-integration paths, running at least /verify before merge is the least-optional step.

中文说明

代码审查

我先独立写下自己的方案(一个带版本的 checkpoint 记录追加到 transcript,针对重建出的活跃链做校验,分支创建时在 checkpoint 处截断原始 JSONL 并拒绝过期点),然后再读 diff。PR 达到甚至超过了这个方案——而且在崩溃安全方面做得比我预期的更多。

核心设计是合理的。branch-points.ts 只在一个回合真正完整时才认为 checkpoint 有效:它回放整条链上的工具调用/响应配对,任何未闭合的调用就退出,并要求最后一个工具结果之后恰好有一条可见文本的 Assistant 记录。payload 带版本(v: 1)且防御性解析。resolveBranchPoints 按 assistant UUID 去重,对歧义点直接丢弃而不是猜测。

有两处特别用心:

  • 并发。 ChatRecordingService 引入"拓扑栅栏(topology fence)",使 checkpoint 事务期间落地的异步元数据写入(标题、产物)被缓冲并重新挂到 checkpoint 之下,而不是成为它的兄弟节点——这正是保证记录的拓扑权威的关键。acpAgent.ts 里新增了一个按会话的 promise 链互斥锁(runExclusiveHistoryMutation),串行化 prompt / rewind / close / branch,并有回归测试断言 close 不能在分支进行中途 finalize 源记录器。这直接回应了 issue 里"并发 transcript 写入"的担忧。
  • 崩溃安全。 forkSession 现在通过 claim 文件 + 暂存目录 + owner token 来暂存,用原子硬链接发布 transcript,沿途 fsync,两次校验分支点(暂存前和构建 fork 记录后各一次),失败时刻意保留歧义资源交给过期创建 GC,而不是冒险删除不属于自己的东西。GC(cleanupStaleBranchCreations)同样保守——owner marker 不匹配就拒绝触碰。路径穿越也有防护(validatedBackupPath)。对于"绝不暴露部分创建的会话"这个要求,这是正确的取向。

serve 路由校验 atRecordId(非字符串返回 400),把 branch_point_invalid 映射为 409;ACP 层映射为 -32009compactionEngine 现在会合并嵌套的 qwenTranscript 元数据,使 checkpoint 在压缩后存活而不是被覆盖。约定看起来干净(ESM、core 文件 kebab-case、测试就近放置),测试覆盖很重(约 1,796 行测试,含并发和过期 checkpoint 用例)。

静态审查未发现关键阻塞项。两条非阻塞提示:

  1. 正文写了 Closes #8271,但本 PR 只实现了该 issue 的 Part 1(从任意回复分支);Part 2(worktree 隔离)不在这里。建议限定关闭范围,避免 Part 2 被静默关掉。
  2. 这只是静态审查——我没有构建或运行 PR(CI 路径)。下面的行为性结论在此未被独立验证。

(时序图见上方英文部分:Web Shell UI → serve 路由 → bridge/agent(经历史互斥锁串行化)→ SessionService(resolveBranchPoints 校验 → claim/暂存 → 原子链接发布)→ 文件系统。)

测试证据(PR 自身的 CI)

所审提交上的 CI 仍在运行——目前没有失败,但关键的几个套件还没跑完。我通过 API 只取了一次,没有轮询;CI 结束后 finalize 任务会更新上方表格。macOS/Windows 测试任务为 skipped(门控在 Ubuntu 套件之后),所以跨平台覆盖也尚未建立。

沙盒化验证可以落实这里的行为性核心:@qwen-code /verify——forkSession(atRecordId) 是否真的产出一个 transcript 止于所选 Assistant 回合的会话(以及过期/非活跃 checkpoint 是否被以 branch_point_invalid 拒绝、而不是静默回退到最新状态)从 diff 看不出来,而且即使截断逻辑被削弱,绿色套件也可能照样通过。@qwen-code /tmux 还能额外确认 Web Shell 层面——Branch 操作只出现在符合条件的已完成回复上,且选中后会切换到截断后的会话。作者有写权限,两条通道都可直接使用;鉴于触及高风险的 acp-integration 路径,合并前至少跑一次 /verify 是最不该省略的一步。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review and genuinely strong engineering, but this is a large feat touching core session infrastructure, so the core-module gate caps it here and it needs a maintainer's architectural sign-off rather than an auto-approval. The 3/5 is policy, not doubt about the code.

Stepping back: this is better work than most of what lands in the queue. My independent proposal was a fraction of this — a versioned checkpoint plus truncation at the branch point — and the PR goes well past it on the two things that are actually hard here, concurrency and crash safety. The topology fence, the per-session history mutex (with a test that pins the exact race), and the claim/stage/atomic-publish/GC dance in forkSession are the work of someone who has thought about how this fails, not just how it succeeds. The conservative "preserve ambiguous resources for GC" instinct is the right one. Test coverage is heavy and aimed at the right risks (stale checkpoints, concurrent mutation, crash cleanup).

The reason I'm not approving isn't a defect — it's scope and trust boundary. ~2,099 production lines across six packages, with the weight in packages/core/src/services/, is exactly the surface the core-module gate exists to slow down. It's also self-reported and P3: a real, roadmap-aligned feature, but not urgent, so the question for a maintainer is less "is it correct" (the review and tests make a strong case) and more "do we want this much new core machinery now, and is the durable-checkpoint contract the one we want to commit to long-term." That's a judgment call I shouldn't make unilaterally.

Two things I'd want settled before merge, neither blocking the review:

  1. Behavioural proof. The central claims — a branch at a historical checkpoint ends the new session at exactly that turn, and a stale checkpoint is rejected rather than silently falling back to latest state — are not observable from the diff, and CI is still running. @qwen-code /verify (and /tmux for the Web Shell surface) would close that; the author has write access, so both are available. Given the high-risk acp-integration paths, I'd treat at least /verify as a precondition.
  2. Issue scoping. Closes #8271 would close Part 2 (worktree isolation) too, which isn't implemented here.

⏸️ Deferring to @wenshao @tanzhenxin @yiliang114 @LaZzyMan (core CODEOWNERS) — no blocking defects found; this needs a maintainer call on whether the scope and the durable-checkpoint contract are what we want, plus green CI and ideally a /verify run before merge.

中文说明

置信度:3/5 —— 审查干净、工程质量确实很高,但这是一个触及核心会话基础设施的大型 feat,核心模块门禁在此把它封顶,需要维护者的架构层面确认,而不是自动批准。这个 3/5 是策略所致,并非对代码有疑虑。

退一步看:这比队列里大多数 PR 都要好。我独立的方案只是它的一小部分——一个带版本的 checkpoint 加上在分支点截断——而 PR 在真正难的两点上远超我的方案:并发与崩溃安全。拓扑栅栏、按会话的历史互斥锁(附带一个精确钉住该竞态的测试)、以及 forkSession 里的 claim/暂存/原子发布/GC 流程,都出自一个认真思考过"它会怎么失败"而不只是"它怎么成功"的人。保守的"把歧义资源留给 GC"取向是正确的。测试覆盖很重,且对准了正确的风险(过期 checkpoint、并发写入、崩溃清理)。

我不批准的原因不是缺陷——而是范围与信任边界。约 2,099 行生产代码跨六个 package,重量集中在 packages/core/src/services/,这正是核心模块门禁要放慢的那类改动。它也是自我提出且 P3 的:一个真实、与路线图对齐的功能,但不紧急,所以维护者要回答的问题与其说是"它是否正确"(审查和测试已给出有力论证),不如说是"我们现在是否想要这么多新的核心机制,以及持久化 checkpoint 契约是否是我们想长期承诺的那个"。这是我不该独自做出的判断。

合并前我希望落实两件事,都不阻塞审查:

  1. 行为性证明。 核心结论——在历史 checkpoint 处分支会使新会话恰好止于该回合,且过期 checkpoint 会被拒绝而非静默回退到最新状态——从 diff 看不出来,而且 CI 还在跑。@qwen-code /verify(以及针对 Web Shell 层面的 /tmux)可以闭环;作者有写权限,两者都可用。鉴于高风险的 acp-integration 路径,我会把至少跑一次 /verify 视为前置条件。
  2. issue 范围。 Closes #8271 会连同 Part 2(worktree 隔离)一起关掉,而它并未在此实现。

⏸️ 转交 @wenshao @tanzhenxin @yiliang114 @LaZzyMan(core CODEOWNERS)—— 未发现阻塞性缺陷;需要维护者就范围与持久化 checkpoint 契约是否符合预期做出决定,并在合并前等 CI 转绿、最好再跑一次 /verify

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🖼️ web-shell visual preview

Rendered against a mock daemon (no real backend): the PR base vs this PR head 60494e3. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

assistant-response-branch-dark before/after

assistant-response-branch-light before/after

Full-resolution recordings (.webm) are attached to the workflow run.

Qwen Code · web-shell visuals

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

Built the PR base vs this PR head 60494e3, drove a fixed endpoint set against each, and diffed the JSON responses. Only fields that changed are shown.

No response changes against the PR base across 4 scenario(s).

Qwen Code · serve A/B

@wenshao

wenshao commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Review — feat: fork from any conversation

Reviewed the full diff (48 files, +4236/−450) plus surrounding code in this checkout.

Overview

The PR replaces "branch from latest session state" with a durable, replayable branch point:

  • Core — a new system/branch_checkpoint record written at end_turn inside a topology fence that buffers concurrent metadata writers, so the checkpoint is guaranteed to be the immediate child of the turn's last record. branch-points.ts is the single resolver used by the recorder, the transcript reader, and fork validation.
  • Transport — the checkpoint rides the turn-complete _meta through bridge → SDK → webui → web-shell; historical turns get it from branchPointsByAssistantUuid on the transcript page and via replay _meta.
  • PersistenceforkSession gained an atRecordId/title path and a claim + staging + link-commit protocol so a partially-built branch is never discoverable, plus an activity-triggered GC for abandoned staging.
  • Concurrency — a per-session runExclusiveHistoryMutation mutex in QwenAgent now serializes prompt / close / rewind / branch / cron / notification turns; rewind additionally chains onto entry.promptQueue.
  • UI — the Branch affordance moves from "last completed assistant" to "any assistant block carrying a branchRecordId", with a pending state and a 409 stale-checkpoint path that refreshes the transcript.

The layering is clean and the single-resolver decision is the right call — recording, replay, reader, and validation all agreeing removes a whole class of drift. Design doc is thorough. Test coverage is genuinely broad (contract tests at every layer plus concurrency cases).

Findings below, ordered by severity.


High

1. Branching now hard-fails when a referenced file-history backup is gone.
copyFileHistoryBackupsToStaging throws Missing file-history backup: ${name} when a referenced backup is not a regular file, and that propagates out of forkSession. The previous copyFileHistoryBackups logged a warning per failed entry and let the fork succeed.

This is reachable on the ordinary "branch from latest" path, not just the historical one: cleanupOldFileHistoryBackups sweeps <globalQwenDir>/file-history/<sessionId>/ purely by directory mtime, while the transcript with its file_history_snapshot records lives on indefinitely. Any session older than the cleanup period therefore has snapshot records pointing at deleted backups — and branching it now fails outright instead of producing a branch without rewind material.

Suggest treating a missing backup as a warning (skip the name, keep it out of the manifest) and reserving the throw for cases where the file exists but cannot be linked/copied.

2. A checkpoint write failure turns a completed turn into a failed turn.
In Session.prompt, recordBranchCheckpointTransaction is awaited inside the main try, before releasePendingSend(). It can throw in several ways:

  • this.writeFailure rethrown (set by an earlier async write failure)
  • SessionWriterUnavailableError when state !== 'active'
  • appendRecordStrict rejecting
  • a plain Error('Unable to load active transcript for session ...') from readActiveTranscriptChain when loadSession returns undefined

The first three are SessionWriterErrors, so the existing catch converts them into a RequestError — the client gets a turn error for a turn whose response already streamed successfully. The fourth is a bare Error and escapes uncaught. In all cases the success-path work after it (#maybeEmitFollowupSuggestion, #scheduleChannelDelivery, the eager cron/notification drains) is skipped.

The checkpoint is an optional convenience; it should never be able to fail the turn. Suggest wrapping the call in try/catch, logging, and falling back to undefined (no branch point) — which the whole downstream chain already handles.


Medium

3. Full transcript re-read on every completed turn.
recordBranchCheckpointTransactionreadActiveTranscriptChain()flush() + loadSession(), which readAllRecords + reconstructHistory over the entire JSONL. That now runs once per end_turn, and it runs while the topology fence is blocking every other transcript writer. Cost scales with session size, so the longest sessions — exactly the ones where branching is most useful — pay the most per turn.

Since the recorder already knows lastRecordUuid and the turn's start cursor, the candidate resolution only needs records from startExclusiveRecordUuid forward. A tail-bounded read (or caching the active chain across turns) would remove the O(session) per-turn cost.

4. resolveBranchPoints is O(n²) in active-chain length.
For each branch_checkpoint it calls resolveCompletedTurnBranchCandidate with activeChain.slice(0, index), which allocates a fresh array and rescans from record 0. With one checkpoint per turn that is ~turns × records work per invocation. It is called from SessionTranscriptReader (memoized on the index — fine) and from forkSession, where validateTargetBranchPoint() runs it twice and reconstructHistory(forked) runs twice on top of the initial source resolve. A single forward pass carrying per-turn pending-tool state would make this linear; at minimum, validateTargetBranchPoint() could be computed once and reused.

5. Synchronous filesystem GC in the SessionService constructor.
maybeCleanupStaleBranchCreations() runs readdirSync / statSync / readFileSync / existsSync / rmSync({recursive}) on the constructor path. SessionService is constructed inside daemon request handling (createWorkspaceRuntimeSessionService), so this stalls the event loop — at most hourly per chats dir, but the recursive rmSync on a stale staging dir is unbounded in duration. Deferring to setImmediate/a microtask, or triggering GC only from listSessions/forkSession (which already call it) rather than from the constructor, would keep construction cheap.

Related nit: SessionService.branchGcLastRunAt is a static Map keyed by chats dir with no eviction — bounded by workspace count, but it never shrinks.

6. Rewind changed from fail-fast to unbounded queueing.
sessionRewind now runs inside entry.promptQueue.then(...). Previously a rewind during an active prompt surfaced session_busy promptly; now the request waits for the prompt to drain first. withTimeout(initTimeoutMs, ...) only wraps the ext-method call after dequeue, so the queue wait itself is unbounded — an HTTP rewind can hang for the length of a long agentic turn. If the intent is to make rewind wait rather than fail, please confirm and consider bounding the queue wait so the client gets a deterministic error.

7. Transcript commit relies on hardlink support.
fs.linkSync(stagedTranscriptPath, targetPath) is the commit step. Hardlinks are unavailable on exFAT/FAT32 and some network/container mounts, so forkSession would fail there where the previous writeFileSync succeeded. The backup staging next to it already commits with renameSync — using renameSync for the transcript too would be atomic, portable, and would also make the staging cleanup a no-op instead of an unlink.

8. Aborted branches now leave a persisted session behind.
routes/session.ts swapped deleteDaemonSessionIfOrphan(...) for killSession(..., { requireZeroAttaches: true }) on both the generation-guard and dead-socket paths, and the test now asserts removeSpy is never called. So when the response never reaches the client, a fully committed branch session stays in the user's session list — created by an action they saw fail. That is defensible under the new "never expose a partial session, never delete a complete one" model, but it is a user-visible behavior change that isn't called out in the PR description and isn't explained at the call site. Worth a comment either way.


Low / nits

  • Fail-closed prefix scan can permanently disable checkpoints. resolveCompletedTurnBranchCandidate seeds pendingCalls from activeChain.slice(0, startIndex + 1) and bails if anything is still open at the end. A single functionCall anywhere earlier in the session that never received a matching functionResponse leaves a permanent resident in pendingCalls, so every subsequent turn silently loses its checkpoint with no diagnostic. Restricting the pending set to the branch interval, or logging when the prefix is what blocked the candidate, would make this debuggable.
  • releaseTopologyFence reparents side-artifact records. It assigns intent.record.parentUuid = this.lastRecordUuid for every buffered append, including the { updateActiveTail: false } writers (recordSessionArtifactEvent / recordSessionArtifactSnapshot) that deliberately do not advance the tail. Reparenting those onto the checkpoint changes side-chain topology. If that is intended (it looks like it is — it's the point of the fence), a one-line comment would save the next reader the trace.
  • No fallback affordance when a checkpoint is absent. showAssistantBranch is now strictly branchRecordId !== undefined, and the lastCompletedAssistantId fallback was deleted. For legacy transcripts, or any turn where the candidate resolver bails (see above), the message list has no Branch button at all — even though forkSession still supports branching from latest. Consider keeping the old fallback on the final completed assistant when no checkpoint exists.
  • new AbortController().signal in App.tsx's stale-branch handler creates a signal that can never fire. If reloadSession is expected to be cancellable here, thread a real controller; otherwise a shared never-aborting constant makes the intent explicit.
  • setBranchPending(false) in finally (AssistantMessage.tsx) runs after a successful branch has likely unmounted the component. Harmless under React 18, but a mounted guard is worth it if that's the pattern elsewhere in this package.
  • DaemonClient.branchSession sends atRecordId: req.atRecordId unconditionally while name uses conditional spread. JSON.stringify drops undefined, so it works — just inconsistent with the line above it.
  • Duplicated UUID regex. The strict RFC-4122 pattern ([1-5] version, [89ab] variant) is inlined in both bridge.ts and DaemonClient.ts. Worth a shared constant; also note that any record whose uuid isn't strict v4 silently loses its branch point at the transport boundary.
  • parseBranchCreationManifest validates ownerToken via SESSION_FILE_PATTERN.test(\${ownerToken}.jsonl`)`. It works, but reusing a filename pattern to validate a token reads as accidental; a direct UUID check would state the intent.

Security

No issues found. Specifically checked and satisfied:

  • validatedBackupPath rejects non-basename names and verifies the resolved parent, so manifest-driven names can't escape the backup directory.
  • atRecordId is only ever a map key / uuid comparison — it never reaches a path, a shell, or a query.
  • Claim, staging, and owner-marker files are created with wx and 0o600 / 0o700, so no pre-existing-file hijack and no world-readable transcript staging.
  • The GC refuses to delete anything whose .branch-owner marker doesn't match the manifest, and re-reads the claim before removing it — the ambiguous cases are preserved rather than reclaimed.

One operational note: staging directories are created under <globalQwenDir>/file-history/ as .<sessionId>.<token>.staging, which cleanupOldFileHistoryBackups will sweep by mtime like any other child directory. Harmless for abandoned staging, but a housekeeping sweep landing during a very slow fork could delete the directory out from under it.

Tests

Coverage is strong and layered — branch-points.test.ts, fork staging/GC/concurrency cases in sessionService.test.ts, bridge and route contract tests (forwarding, 400, 409), compaction meta-merge, replay-page anchoring, SDK normalizer/transcript, web-shell adapter + DOM, and the webui provider. Gaps worth closing:

  • A fork whose referenced file-history backup is missing on disk (finding 1) — currently the tests lock in the failing behavior.
  • A turn where recordBranchCheckpointTransaction rejects, asserting the turn still returns end_turn (finding 2).
  • Interleaved prompt / close / branch through runExclusiveHistoryMutation asserting ordering and no lost release.

Style

Matches house conventions — ESM, no any in the new code, kebab-case.ts in core, PascalCase.tsx for components, collocated tests, comments reserved for non-obvious why. packages/core/src/index.ts re-exports branch-points.js with no name collisions.

@github-actions github-actions Bot removed the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 1, 2026
@water-in-stone

Copy link
Copy Markdown
Collaborator Author

Thanks @wenshao for the thorough review. I evaluated every finding and pushed the accepted fixes in a4c31c9c5; the complete triage (including the original human and bot comments) is in pr-8274-comment-evaluation.md.

Addressed:

  • Missing or non-regular file-history backups are now warned and omitted, and the claim manifest is narrowed to the files actually staged. An access/copy failure for an existing regular backup remains fatal and leaves no visible target.
  • A checkpoint transaction failure is logged and no longer turns an already-completed Assistant response into a failed turn or skips normal post-turn work.
  • Branch-point replay now indexes UUIDs and captures prefix tool state in one forward pass instead of slicing/rescanning the active-chain prefix for every checkpoint. A regression test fails if prefix slicing returns.
  • Branch GC no longer runs in the request-scoped SessionService constructor; list/fork activity retains the bounded hourly trigger.
  • Added the requested commit-boundary and side-artifact-topology comments, plus missing-backup, copy-failure, checkpoint-rejection, GC-deferral, and visual scenario coverage.
  • Changed both Closes #8271 references to Refs #8271, so the worktree-isolation part remains open.

Intentionally not changed / follow-up needed:

  • The full transcript read under the topology fence is a real scaling concern, but a safe bounded cache/cursor has to define ownership across restore, rewind, writer failure, and automatic appends. I propose benchmarking long transcripts and designing that cursor contract separately rather than adding an unreviewed cache here.
  • Rewind queueing is the reviewed v6 ordering contract. A timeout without cancellable queue admission could return an error and still execute the rewind later, so bounding it needs a product decision plus cancellation support.
  • The transcript hard link preserves both atomic visibility and no-clobber publication. POSIX rename can overwrite, while exclusive copy exposes partial contents; unsupported filesystems therefore continue to fail safely before publication.
  • Prefix pending-tool state remains fail-closed to avoid publishing malformed provider history. The legacy last-message fallback also remains intentionally absent because it could present an unanchored latest-state fork as a historical fork.
  • The AbortController, unmount reset, optional JSON field, UUID/token regex, static GC map, and one-hour staging-race notes do not show a concrete defect large enough to justify extra abstraction or cross-package churn in this PR.
  • I did not duplicate the prompt/close/branch test: Bridge tests already pin FIFO/close admission and the Agent test pins recorder finalization during a branch mutation.

Validation after the fixes:

  • Core: 270 focused tests passed.
  • CLI: 474 Session tests and 360 ACP Agent tests passed. In the combined CLI run, 3 unrelated serve tests flaked under concurrent load; all 3 passed independently.
  • Web Shell: 492 focused tests passed; the new dark/light Chromium visual scenario passed (2 cases).
  • Targeted ESLint, repository build, repository typecheck, and git diff --check passed.

The post-commit persisted-session behavior remains deliberate: once Core publishes a complete branch, transport cleanup releases only live ownership and the session stays recoverable in the picker.

@water-in-stone

Copy link
Copy Markdown
Collaborator Author

@qwen-code /verify

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ⚠️ not run — skipped - workflow run

Skipped because the PR has merge conflicts, so refs/pull/8274/merge is unavailable — resolve conflicts and re-run.

中文 — 判定:⚠️ 未运行 · 已跳过

跳过原因:the PR has merge conflicts, so refs/pull/8274/merge is unavailable — resolve conflicts and re-run。

Qwen Code · sandboxed verification

heyang.why added 3 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
@water-in-stone
water-in-stone force-pushed the feat/fork-from-one-conversation branch from a4c31c9 to 870af37 Compare August 1, 2026 08:30
@github-actions

github-actions Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Please do not rebase or force-push to an active PR as it invalidates existing review comments. Note for future reference, the bots always squash all changes into a single commit automatically as part of the integration.

中文

请勿对活跃的 PR 执行 rebase 或 force-push,因为这会使已有的评审评论失效。另外,供日后参考:作为集成流程的一部分,机器人始终会自动将所有改动压缩(squash)为单个提交。

water-in-stone and others added 2 commits August 1, 2026 16:36
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
@water-in-stone

Copy link
Copy Markdown
Collaborator Author

@yiliang114 @wenshao Please take a look

@wenshao

wenshao commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Overview

This PR adds durable branch_checkpoint records so a Web Shell user can branch from any completed Assistant response, not just the latest session state. The mechanism is coherent and the layering is clean:

  • Core — new services/branch-points.ts derives/validates branch candidates from the active chain; ChatRecordingService.recordBranchCheckpointTransaction() writes the checkpoint under a new topology fence that buffers concurrent metadata writers so they can't become siblings; forkSession() gains atRecordId/title and a claim + staging + hard-link commit protocol with a GC pass for stale creations.
  • TransportatRecordId threaded through ACP ext-method → bridge → POST /session/:id/branch → SDK; branchPoint returned on turn_complete; branch_point_invalid mapped to HTTP 409.
  • Replay/UIbranchRecordId attached to Assistant updates on replay and live, surfaced as DaemonUiEvent.branchRecordId → transcript block → DaemonAssistantMessage → per-message Branch button; stale checkpoints trigger a transcript refresh with a dedicated toast.
  • ConcurrencyrunExclusiveHistoryMutation in QwenAgent serializes prompt / rewind / branch / close / automatic-queue drains per session, and bridge rewindSession now chains onto entry.promptQueue like branch already did.

Test coverage is genuinely good: cross-layer contract tests, serialization ordering tests, crash/cleanup ownership tests in sessionService.test.ts, and DOM tests for button gating. CI is green.

The concerns below are mostly about cost and one UX regression, not about the core design.


Issues

1. Branch button is now shown during an active turn — 30 s client timeout (MessageList.tsx:3702)

The old gate was lastCompletedAssistantId, computed with if (isResponding) return null. The new gate is just branchRecordId !== undefined, so every anchored Assistant message keeps its Branch button while a later turn is streaming.

Clicking it then goes: bridge.branchSessionentry.promptQueue.then(...), i.e. it queues behind the in-flight prompt. Meanwhile the client wraps the call in withActionTimeout(..., 'Branch session timed out') with DEFAULT_ACTION_TIMEOUT_MS = 30_000 (packages/webui/src/daemon/timing.ts). Any turn longer than ~30 s produces:

  • user sees a "Branch session timed out" error,
  • the Branch button stays disabled (branchPending never clears until the promise settles),
  • the fork still commits server-side afterwards, leaving a session the user was never switched to.

Suggest re-introducing an isResponding gate on showAssistantBranch (or disabling the button with a tooltip while responding). Cheapest fix and it preserves the previous contract.

2. Full transcript reload on every completed turn (chatRecordingService.ts:1304)

recordBranchCheckpointTransaction is awaited inline in Session.prompt for every end_turn, and calls readActiveTranscriptChain()SessionService.loadSession(), which reads and JSON.parses the entire session JSONL and reconstructs history. Before this PR that path only ran when a goal was active (goal-runtime.ts) or on rewind.

So turn-completion latency now grows linearly with session length, making total cost quadratic over a session's lifetime — and it happens while the topology fence is held, so every other transcript writer is blocked for the duration. On a multi-MB transcript this is seconds of added tail latency per turn.

The validation only needs the records after startExclusiveRecordUuid. Is there a reason it can't read a bounded tail (the reader already supports paged/backward reads) instead of the whole chain?

3. Pagination is defeated for branch-point projection (session-transcript-reader.ts:1138)

const activeRecords = await readAggregatedRecords(index, index.activeUuids);
index.branchPointsByAssistantUuid = new Map(...resolveBranchPoints(activeRecords)...);

readPage exists so clients don't have to materialize a whole transcript; this materializes all active records to build the checkpoint map. It's memoized on the index, but the index is rebuilt as the session grows, so an actively-used long session pays it repeatedly. Same question as #2: can this be scoped to the page's record window plus the enclosing turn boundary?

4. forkSession is now fully synchronous I/O on the daemon event loop (sessionService.ts:403, ~2130-2250)

copyFileHistoryBackups was fs/promises + Promise.all; it's now copyFileHistoryBackupsToStaging with lstatSync / linkSync / copyFileSync / fsyncPath(target) per backup file, plus several fsyncDirectoryBestEffort calls and two readFileSync of the claim. All synchronous, all on the daemon's single thread, while a live session is held under runExclusiveHistoryMutation.

The durability rationale is clear, but a fork with many tracked-file backups will visibly stall the whole daemon (all sessions, not just this one). Worth converting the copy loop to fs/promises + filehandle.sync(), or at minimum documenting the expected bound on backup count.

5. Aborted branches now leak persisted sessions (routes/session.ts:2446, :2458)

Both cleanup arms changed from deleteDaemonSessionIfOrphan(...) to bridge.killSession(..., { requireZeroAttaches: true }), and the test was flipped to expect(removeSpy).not.toHaveBeenCalled(). That matches the design ("the complete persisted session remains recoverable"), but it means a generation-guard trip or a dead response socket now leaves a fully-committed branch session in the picker that the user never asked to keep — with no GC (the .branch-claims GC only reaps incomplete creations, and this one is complete by construction).

Intentional? If so, a short comment on why an orphaned-but-complete branch is preferable to deletion would help the next reader; the existing comment explains the mechanism but not the product choice.

6. Write barrier dropped on the interactive branch path (acpAgent.ts:10252)

Old code: if (isSideTask && recording) await recording.runWithWriteBarrier(fork); else await fork();
New code: the non-side-task path does if (recording) await recording.flush(); then forkSession(...) inside runExclusiveHistoryMutationno runWithWriteBarrier, so lease.assertOwnedAndUnchanged() never runs around the read+copy. The side-task path below still uses the barrier.

runExclusiveHistoryMutation is an in-process lock; the lease assertion is the cross-process guard. Rewind still takes the barrier (recording.runWithWriteBarrier(loadAuthoritative)). Is the asymmetry deliberate, or should the interactive branch also run under the barrier?


Smaller notes

  • Session.ts:2508typeof recording?.getTranscriptCursor === 'function' / typeof recording?.recordBranchCheckpointTransaction === 'function'. These are concrete methods on ChatRecordingService; the duck-typing only exists to tolerate partial test mocks, and it will silently disable checkpointing if either method is ever renamed. Prefer completing the mocks and calling the methods directly.
  • sessionService.tsprivate static readonly branchGcLastRunAt = new Map<string, number>() keyed by chats dir is never evicted. Unbounded (slowly) in a long-lived daemon serving many workspaces.
  • branch-points.ts:resolveBranchPoints — on a duplicate record uuid it does return points, relying on points still being empty at that stage. return new Map() states the intent directly and is robust to future reordering.
  • DaemonClient.ts:2470atRecordId: req.atRecordId is emitted unconditionally while name uses a conditional spread; it works only because JSON.stringify drops undefined. Match the surrounding style.
  • MessageList.tsx:3693onBranchSession && branchRecordId ? () => onBranchSession(branchRecordId) : undefined allocates a new closure per anchored message per render, defeating memo on MessageItem (MessageItem.tsx:45) for every branchable message. In a long transcript that's a lot of avoidable re-renders; a useCallback'd handler receiving the id from a data- attribute, or a per-id memo cache, would keep the identity stable.
  • docs/design/web-shell/assistant-response-session-branching.md — still Status: Proposed while the implementation ships. Worth flipping to Accepted/Implemented.

Security / correctness spot-checks (no issues found)

  • validatedBackupPath correctly rejects traversal (path.basename(name) !== name and a resolved-dirname check); the claim manifest re-validates backupNames on read.
  • Owner-marker matching before any destructive rmSync in both the finally cleanup and the GC pass looks right — mismatches preserve resources and warn rather than deleting.
  • UUID validation on branchPoint metadata is applied in both bridge.ts and matchTurnEvent, and stopReason === 'end_turn' is required in both — good defense in depth.
  • releaseTopologyFence re-parents buffered records to this.lastRecordUuid, and appendRecordStrict updates lastRecordUuid synchronously before awaiting, so sequential buffered records chain correctly rather than becoming siblings. Verified.
  • runExclusiveHistoryMutation's chain can't poison itself: current = previous.then(() => gate) and gate never rejects, so the tail never rejects and the queue can't wedge.

Overall this is careful, well-tested work. #1 is the one I'd want fixed before merge; #2#4 are performance questions worth answering even if the answer is "acceptable for now."

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
@water-in-stone

Copy link
Copy Markdown
Collaborator Author

Thanks for the detailed follow-up. I re-evaluated every point against the current implementation and pushed the accepted fixes in ed2e41972.

Fixed in this revision:

  • Issue 1: historical Branch actions are hidden while any turn is active. The DOM regression test rerenders the same list from idle to responding and verifies the action disappears.
  • Issue 5: the post-commit cleanup comment now states the product reason: another client may already have discovered or attached the persisted session, so deleting it can cause cross-client data loss.
  • Issue 6: the complete interactive forkSession read/validation/publication operation now runs inside the recorder's runWithWriteBarrier, matching the cross-process writer-lease contract. A rejected-barrier test proves that forkSession is never invoked.
  • The concrete Session path no longer duck-types recorder methods; the shared recorder mock now implements the complete checkpoint contract.
  • The design is marked Implemented and records the active-turn UI gate and writer-barrier amendments.

The three performance concerns are valid, but I am deferring them to measured follow-ups rather than making partial changes in this already broad correctness PR:

  • Issue 2: a bounded tail read is not equivalent yet because checkpoint validation carries unresolved tool-call state across the turn boundary and must remain authoritative across restore, rewind, writer loss, and topology-fence buffering. The next step should be a long-transcript benchmark followed by an ownership-fenced incremental index/cursor design.
  • Issue 3: a page-local scan cannot prove the current contract when the Assistant and checkpoint fall on different pages or prior tool-call state affects validity. A safe optimization should add a same-snapshot checkpoint catalog to the transcript index after measuring the current cost.
  • Issue 4: replacing only the backup-copy loop would leave synchronous claim, staging, fsync, publish, ownership-check, and cleanup steps. This needs an end-to-end async persistence transaction that preserves publish-last, no-clobber, and crash-cleanup invariants.

For the remaining smaller notes: the process-lifetime GC map has no demonstrated practical growth path; duplicate validation occurs before checkpoint collection so returning points is behaviorally equivalent to an empty map; undefined is deterministically omitted from the JSON request; and virtualized rendering bounds mounted Branch callback allocations. I left those unchanged to avoid unmeasured or style-only churn.

Validation completed locally:

  • Web Shell MessageList.dom.test.tsx: 67/67 passed
  • CLI acpAgent.test.ts: 360/360 passed serially
  • CLI Session.test.ts: 483/483 passed
  • Focused independent branching regression run: 7/7 passed
  • npm run build && npm run typecheck && npm run lint: passed

qwen-code-ci-bot pushed a commit that referenced this pull request Aug 1, 2026

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

Not reviewed: build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and did not run locally.

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

中文说明

未审查:build-and-test — Test (macos-latest, Node 22.x) and Test (windows-latest, Node 22.x) were skipped in CI and did not run locally。

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

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

Comment thread packages/acp-bridge/src/bridge.ts Outdated
Comment on lines +8308 to +8311
response = (await Promise.race([
withTimeout(
entry.connection.extMethod(
SERVE_CONTROL_EXT_METHODS.sessionRewind,

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] R12-1: A dispatched rewind is still wrapped in withTimeout(initTimeoutMs) — a race-only Promise.race that cannot cancel the dispatched extMethod — while this same PR removed that wrapper for branches because "a caller never sees a timeout followed by an unobserved committed session". The queue tail (entry.promptQueue = rewindResult.then(() => undefined, () => undefined)) also swallows the timeout rejection and advances immediately. — Failure scenario: a rewind whose agent-side execution exceeds initTimeoutMs (10s default; file rewinds restore many files by default) → the caller receives BridgeTimeoutError while the rewind commits history truncation + file restores unobserved, and the next queued branch/rewind is dispatched to an agent still mid-rewind — probe-verified on this commit (a queued branch was dispatched during an in-flight rewind; awaiting the real outcome flipped the ordering). Pre-PR had the timeout but no queue, so this overlap is created by this diff.

Suggested fix — treat a dispatched rewind like a dispatched branch:

response = (await Promise.race([
  entry.connection.extMethod(
    SERVE_CONTROL_EXT_METHODS.sessionRewind,
    { sessionId, promptId: req.promptId, rewindFiles: req.rewindFiles !== false },
  ),
  getTransportClosedReject(entry),
])) as Record<string, unknown>;

and add a rewind analogue of waits for a dispatched branch instead of timing out and committing later.

中文说明

[Critical] R12-1:已派发的 rewind 仍然包在 withTimeout(initTimeoutMs) 里 —— 这是一个只能竞速、无法取消已派发 extMethod 的纯 Promise.race —— 而本 PR 对 branch 恰恰移除了这个包装,理由是"调用方永远不会先看到超时、随后又出现一个未被观察到的已提交会话"。队列尾部(entry.promptQueue = rewindResult.then(() => undefined, () => undefined))还会吞掉超时拒绝并立即前进。— 失败场景:agent 侧执行超过 initTimeoutMs(默认 10 秒;文件回退默认恢复大量文件)的 rewind → 调用方收到 BridgeTimeoutError,而 rewind 在未被观察的情况下提交了历史截断 + 文件恢复,并且排队的下一个 branch/rewind 会被派发到仍在执行 rewind 的 agent —— 已在本 commit 上用探针验证(排队的 branch 在 rewind 执行期间被派发;改为等待真实结果后顺序翻转)。此 diff 之前有超时但没有队列,因此该重叠是本 diff 新引入的。

建议修复:像对待已派发的 branch 一样对待已派发的 rewind —— 直接等待真实结果(保留 getTransportClosedReject(entry) 作为竞速对手),并补充一个类似 "waits for a dispatched branch instead of timing out" 的 rewind 版回归测试。

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

Comment thread packages/acp-bridge/src/bridge.ts Outdated
// ACP cannot cancel a branch after dispatch. Keep the queue and
// reservation until its real outcome is known so a caller never sees
// a timeout followed by an unobserved committed session.
const result = (await mutation) as {

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] R12-2: The branch/side-task mutation awaits entry.connection.extMethod(...) with neither the withTimeout this diff removed nor a getTransportClosedReject(entry) race, but the ACP SDK never rejects pending requests when the transport dies — every other extMethod call site in this file races getTransportClosedReject for exactly this hazard. — Failure scenario: the channel's child process dies mid-branch (OOM/segfault — branching a large session is memory/IO-heavy): the pending extMethod promise never settles, so the caller's branchSession hangs forever (the SDK fetch timeout is disabled for this route, see the sibling finding on DaemonClient.ts), and finally { releaseAdmissionOnce(); } never runs — permanently leaking the reservation's inFlight slot in createTotalSessionAdmissionController; repeated channel deaths exhaust the daemon-wide session budget (TotalSessionLimitExceededError for all new sessions). Probe-verified: crash(SIGKILL) → branch still pending 300ms later, 0 admission releases; adding Promise.race([mutation, getTransportClosedReject(entry)]) flips both.

Suggested change
const result = (await mutation) as {
const result = (await Promise.race([
mutation,
getTransportClosedReject(entry),
])) as {

The race fires only when the channel exits (a branch whose channel died cannot be observed or delivered anyway), never on a slow-but-alive fork, so the "unobserved committed session" rationale for removing the timeout is unaffected.

中文说明

[Critical] R12-2:branch/side-task 变更直接 await entry.connection.extMethod(...),既没有本 diff 移除的 withTimeout,也没有与 getTransportClosedReject(entry) 竞速 —— 但 ACP SDK 在传输层死亡时从不拒绝挂起的请求;本文件中其他所有 extMethod 调用点都与 getTransportClosedReject 竞速,正是为了防这一风险。— 失败场景:branch 执行期间 channel 子进程死亡(OOM/段错误 —— 大会话分支正是内存/IO 密集操作):挂起的 extMethod promise 永不落定,调用方的 branchSession 永久挂起(该路由的 SDK fetch 超时已被禁用,见 DaemonClient.ts 上的同族发现),且 finally { releaseAdmissionOnce(); } 永不执行 —— 永久泄漏 createTotalSessionAdmissionController 中预订的 inFlight 槽位;channel 反复死亡会耗尽 daemon 级会话配额(所有新会话报 TotalSessionLimitExceededError)。已用探针验证:crash(SIGKILL) → 300ms 后 branch 仍挂起、0 次 admission 释放;加上 Promise.race([mutation, getTransportClosedReject(entry)]) 后两者均翻转。

该竞速只在 channel 退出时触发(channel 已死的 branch 本来也无法被观察或交付),对缓慢但存活的 fork 从不触发,因此移除超时的"避免未观察到的已提交会话"理由不受影响。

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

Comment on lines +4791 to +4793
const realOpen = fs.promises.open;
vi.spyOn(fs.promises, 'open').mockImplementation(async (...args) => {
if (String(args[0]).includes(`${realPath.sep}.branch-staging`)) {

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] R12-3: The rewritten failure injection in removes a partially written target when fork creation fails now throws at the staging-file open — before any byte is written — so the test no longer sets up the scenario its name describes, and its sole assertion (expect(fs.existsSync(targetPath)).toBe(false)) is trivially true. — Concrete cost: the old injection failed mid-write (after fs.writeSync(file, 'partial')); the new one throws for any .branch-staging open, which happens before transcriptStagingCreated is set and before anything exists on disk, so the assertion holds for ANY pre-link error regardless of cleanup — deleting all failure-path cleanup from forkSession would still pass (only claim removal is actually exercised). The mid-write cleanup path (staged writeFile failure) is now unexercised, and the test name misleads a maintainer into believing partial-write coverage exists.

Suggested fix: inject the failure after a partial write (e.g. have the open spy return a real FileHandle whose writeFile/sync throws after some bytes), then assert both that targetPath is absent and .branch-staging is empty; or rename the test to what it now verifies ("propagates staging-open failures and leaves no target file behind").

中文说明

[Suggestion] R12-3:重写后的 removes a partially written target when fork creation fails 故障注入现在在 staging 文件的 open 处抛出 —— 此时尚未写入任何字节 —— 因此该测试不再构造其名称所描述的场景,唯一断言(expect(fs.existsSync(targetPath)).toBe(false))恒为真。— 具体代价:旧注入在写入中途失败(fs.writeSync(file, 'partial') 之后);新注入对任何 .branch-staging 的 open 都抛出,而这发生在 transcriptStagingCreated 置位之前、磁盘上尚无任何内容之时,因此无论清理逻辑是否存在,该断言对任何 link 之前的错误都成立 —— 即使删除 forkSession 中全部失败路径清理,测试仍会通过(实际只验证了 claim 删除)。写入中途的清理路径(staged writeFile 失败)现在完全未被覆盖,且测试名称会误导维护者以为存在部分写入覆盖。

建议修复:在部分写入之后注入故障(例如让 open spy 返回一个真实 FileHandle,在其写入若干字节后的 writeFile/sync 中抛出),然后同时断言 targetPath 不存在且 .branch-staging 为空;或者把测试改名为它实际验证的行为。

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

ownerToken,
transcriptStagingName,
backupStagingName,
backupNames: [...backupNames].sort(),

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] R12-4: backupNames is written into every branch-claim manifest and validated on parse but has no production read site — GC and the in-method cleanup operate on whole directories, never per-name — and it is stale by construction (the manifest is written before staging; the post-copy narrowing at line 2416 is not reflected in the claim). — Concrete cost: the manifest grows O(referenced file-history backups) (~24 bytes/name in JSON); a forked transcript referencing roughly 11k backup names pushes the claim past BRANCH_GC_MAX_MANIFEST_BYTES (256KB). If that fork then crashes mid-creation, readBranchCreationManifestBounded returns undefined for the oversized claim, GC logs "invalid manifest" and continues — so the orphaned claim, staged transcript, staged backups, and any published backup dir are retained forever instead of being reclaimed. A future GC enhancement trusting this field would also flag valid forks as corrupt, because the persisted list can overstate what was actually staged.

Suggested change
backupNames: [...backupNames].sort(),

Drop the field from BranchCreationManifestV1 and parseBranchCreationManifest's validation too; if a future GC pass needs per-name data, derive it from the staged/published directories.

中文说明

[Suggestion] R12-4:backupNames 被写入每个 branch-claim 清单并在解析时校验,但没有任何生产读取点 —— GC 与方法内清理都按整目录操作,从不按名操作 —— 且它天生是过期的(清单在 staging 之前写入;第 2416 行拷贝后的收窄不会反映到 claim 中)。— 具体代价:清单随引用的 file-history 备份数线性增长(JSON 中约 24 字节/名);引用约 1.1 万个备份名的 fork 会把 claim 推过 BRANCH_GC_MAX_MANIFEST_BYTES(256KB)。若该 fork 在创建中途崩溃,readBranchCreationManifestBounded 对超大 claim 返回 undefined,GC 记录 "invalid manifest" 并 continue —— 于是孤立的 claim、staged transcript、staged 备份以及已发布的备份目录将被永久保留而无法回收。未来若有 GC 改进信任该字段,还会把有效 fork 误判为损坏,因为持久化的列表可能多于实际 staged 的内容。

建议同时从 BranchCreationManifestV1parseBranchCreationManifest 的校验中移除该字段;若未来 GC 需要按名数据,从 staged/已发布目录推导。

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

Comment on lines +2515 to +2517
return (await res.json()) as DaemonBranchedSession;
},
0,

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] R12-5: Bare positional 0 as fetchWithTimeout's fourth argument (perCallTimeoutMs) silently disables the client-side fetch timeout for branchSession, with no comment pinning that intent and no caller-facing timeoutMs knob. — Concrete cost: per fetchWithTimeout's own JSDoc, 0 means "disable the timeout entirely" (bare fetch, no AbortController). The no-timeout behavior is deliberate and tested, but if the daemon stalls mid-branch (blocked restoring a large session, or a hung-but-alive daemon over a half-open connection) the promise never settles and the UI caller is stuck in its "branching" state with no way for any caller to impose a cap. An unannotated positional 0 also reads like a retry count, so the opt-out is easy to misread or clobber in a future refactor; sibling long-running routes (restartMcpServer, reload) expose a timeoutMs option instead.

Suggested fix:

        return (await res.json()) as DaemonBranchedSession;
      },
      // 0 = no per-call timeout: branch restore can exceed fetchTimeoutMs on large sessions
      0,

or follow the restartMcpServer pattern and accept an optional timeoutMs defaulting to a large finite value.

中文说明

[Suggestion] R12-5:fetchWithTimeout 的第四个位置参数(perCallTimeoutMs)传入裸 0,静默禁用了 branchSession 的客户端 fetch 超时 —— 既没有注释固定该意图,也没有面向调用方的 timeoutMs 开关。— 具体代价:按 fetchWithTimeout 自身的 JSDoc,0 表示"完全禁用超时"(裸 fetch,无 AbortController)。禁用超时是有意且经过测试的行为,但如果 daemon 在 branch 中途卡住(恢复大会话时阻塞,或半开连接上假死但存活的 daemon),promise 永不落定,UI 调用方会一直卡在 "branching" 状态且无法强制上限。未加注释的位置参数 0 看起来像重试次数,未来重构时容易被误读或误改;同族的长耗时路由(restartMcpServerreload)都改为暴露 timeoutMs 选项。

建议修复:加注释说明(见上方代码),或仿照 restartMcpServer 接受可选 timeoutMs,默认取一个较大的有限值。

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

Comment on lines +4978 to +4982
const readFileSpy = vi.spyOn(fs.promises, 'readFile');

try {
await service['cleanupStaleBranchCreations']();
expect(readFileSpy).not.toHaveBeenCalled();

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] R12-16: The oversized-manifest test's "without reading it unbounded" guard spies fs.promises.readFile — an API the GC's manifest reader never calls (readBranchCreationManifestBounded uses open + handle.stat + handle.read) — so the assertion is trivially true and pins nothing about read boundedness. — Failure scenario: probe-verified four-arm cycle — deleting the before.size > BRANCH_GC_MAX_MANIFEST_BYTES guard and rewriting the reader to read the whole file through handle.read leaves this test GREEN (the unbounded-read regression ships undetected), while the proposed fix (tracking fs.promises.open's handle.read) catches it (expected [ 262145 ] reads to deeply equal []). A planted or corrupt multi-gigabyte .claim file would then be read fully into memory inside cleanupStaleBranchCreations, which runs inline in the daemon's listSessions path — the exact unbounded read the test name promises to prevent.

Suggested fix: spy the API the reader actually uses so the stat-size guard is proven to fire before any byte is read — wrap fs.promises.open to return a handle whose read is tracked, then for the oversized claim assert read was never called.

中文说明

[Suggestion] R12-16:超大清单测试的"不无界读取"守卫 spy 的是 fs.promises.readFile —— 而 GC 的清单读取器从不调用该 API(readBranchCreationManifestBounded 使用 open + handle.stat + handle.read)—— 因此该断言恒为真,对读取是否有界什么也没固定。— 失败场景:已用四臂探针循环验证 —— 删除 before.size > BRANCH_GC_MAX_MANIFEST_BYTES 守卫并把读取器改写为经 handle.read 读取整个文件后,该测试仍为绿色(无界读取的回归在未被察觉的情况下合入);而建议的修复(跟踪 fs.promises.open 返回句柄的 read)能捕获它(expected [ 262145 ] reads to deeply equal [])。届时一个被植入或损坏的数 GB .claim 文件会在 cleanupStaleBranchCreations 中被整个读入内存 —— 它内联运行在 daemon 的 listSessions 路径上 —— 正是该测试名称承诺要防止的无界读取。

建议修复:spy 读取器实际使用的 API,证明 stat 大小守卫在任何字节被读取之前生效 —— 包装 fs.promises.open 使其返回一个 read 被跟踪的句柄,然后对超大 claim 断言 read 从未被调用。

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

Comment on lines +5521 to +5524
const syncSpies = [
vi.spyOn(fs, 'readdirSync'),
vi.spyOn(fs, 'statSync'),
vi.spyOn(fs, 'openSync'),

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] R12-17 (pattern, occurrence 1/3): Async-purity guard test with an incomplete sync-API spy list — uses only asynchronous filesystem APIs for the collision scan spies 5 sync APIs but misses existsSync/accessSync/readFileSync/lstatSync. Same pattern also at sessionService.test.ts:4929 (branch-GC test omits accessSync/opendirSync) and sessionStorageUtils.test.ts:795 (title-pair test spies only 4). — Failure scenario: a follow-up introducing a sync call through any unspied API (an existsSync fast-path pre-check in findSessionTitlesByPrefix, readFileSync in the head-window fallback) blocks the daemon event loop per file — on every branch, on every stale-claim scan — while the guard test named for preventing exactly that stays green. Probe-verified for the title-pair instance: injecting if (!fs.existsSync(filePath)) return emptyResult; left all 55 tests passing until the spy was added. The sibling fork-publication guard spies 17 sync APIs, showing the convention these three fall short of.

Suggested fix: extend syncSpies with existsSync, accessSync, readFileSync, lstatSync, realpathSync — and opendirSync for the GC test.

中文说明

[Suggestion] R12-17(模式问题,第 1/3 处):异步纯净性守卫测试的 sync-API spy 列表不完整 —— uses only asynchronous filesystem APIs for the collision scan 只 spy 了 5 个同步 API,漏掉 existsSync/accessSync/readFileSync/lstatSync。同一模式还出现在 sessionService.test.ts:4929(branch-GC 测试漏 accessSync/opendirSync)和 sessionStorageUtils.test.ts:795(标题对测试只 spy 4 个)。— 失败场景:后续改动若经由任一未被 spy 的 API 引入同步调用(findSessionTitlesByPrefix 中的 existsSync 快速预检、head-window 回退中的 readFileSync),会在每次分支、每次陈旧 claim 扫描时按文件阻塞 daemon 事件循环 —— 而名义上防止此事的守卫测试仍是绿色。标题对实例已用探针验证:注入 if (!fs.existsSync(filePath)) return emptyResult; 后,在补上 spy 之前全部 55 个测试照常通过。同族的 fork 发布守卫 spy 了 17 个同步 API,说明这三处未达到既有惯例。

建议修复:为 syncSpies 补上 existsSyncaccessSyncreadFileSynclstatSyncrealpathSync —— GC 测试再补 opendirSync

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

Comment on lines +4929 to +4932
const syncSpies = [
vi.spyOn(fs, 'readFileSync'),
vi.spyOn(fs, 'readdirSync'),
vi.spyOn(fs, 'statSync'),

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] R12-17 (pattern, occurrence 2/3): The branch-GC async-purity test spies 6 sync APIs but omits accessSync and opendirSync — the sync counterparts of the exact async calls GC makes (fs.promises.access via pathExists, fs.promises.opendir for the claims listing). See occurrence 1/3 (sessionService.test.ts:5521) for the shared pattern, failure scenario, and fix. — A regression to either of those blocks listSessions/forkSession (GC runs inline there) while this test stays green.

Suggested fix: add vi.spyOn(fs, 'accessSync') and vi.spyOn(fs, 'opendirSync') (optionally mkdirSync/renameSync/copyFileSync) to syncSpies.

中文说明

[Suggestion] R12-17(模式问题,第 2/3 处):branch-GC 异步纯净性测试 spy 了 6 个同步 API,但漏掉了 accessSyncopendirSync —— 它们正是 GC 实际异步调用的同步对应物(pathExistsfs.promises.access、claims 列表的 fs.promises.opendir)。共享的模式、失败场景与修复见第 1/3 处(sessionService.test.ts:5521)。— 对这两者之一的回归会阻塞 listSessions/forkSession(GC 在其中内联运行),而该测试仍是绿色。

建议修复:为 syncSpies 补上 vi.spyOn(fs, 'accessSync')vi.spyOn(fs, 'opendirSync')(可选 mkdirSync/renameSync/copyFileSync)。

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

Comment on lines +795 to +797
const syncSpies = [
vi.spyOn(fs, 'statSync'),
vi.spyOn(fs, 'openSync'),

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] R12-17 (pattern, occurrence 3/3): The async-purity guard for readLastJsonStringFieldsAsync spies only four sync APIs (statSync/openSync/readSync/closeSync); a regression through any other sync API keeps the test green while defeating the property the test name asserts. See occurrence 1/3 (sessionService.test.ts:5521) for the shared pattern. — Failure scenario: the function's sole call chain (computeUniqueBranchTitlefindSessionTitlesByPrefix, inline from the daemon branch path, scanning up to MAX_FILES_TO_PROCESS files) would block the daemon event loop per file, on every branch. Probe-verified: injecting an fs.existsSync pre-check left all 55 tests passing until the spy was added.

Suggested fix: extend syncSpies with readFileSync, existsSync, accessSync, lstatSync, realpathSync.

中文说明

[Suggestion] R12-17(模式问题,第 3/3 处):readLastJsonStringFieldsAsync 的异步纯净性守卫只 spy 了 4 个同步 API(statSync/openSync/readSync/closeSync);经由任何其他同步 API 的回归都会在该测试保持绿色的同时破坏测试名称所断言的性质。共享模式见第 1/3 处(sessionService.test.ts:5521)。— 失败场景:该函数的唯一调用链(computeUniqueBranchTitlefindSessionTitlesByPrefix,内联于 daemon 分支路径,最多扫描 MAX_FILES_TO_PROCESS 个文件)会在每次分支时按文件阻塞 daemon 事件循环。已用探针验证:注入 fs.existsSync 预检后,在补上 spy 之前全部 55 个测试照常通过。

建议修复:为 syncSpies 补上 readFileSyncexistsSyncaccessSynclstatSyncrealpathSync

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

Comment on lines +95 to +101
expect(state.blocks).toMatchObject([
{
kind: 'assistant',
text: 'historical answer',
branchRecordId: 'checkpoint-record',
},
]);

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] R12-18: The only replay-path reducer test feeds a single chunk, pinning the block-CREATION attachment (transcript.ts:713-714); the MERGE-path assignment (transcript.ts:692-693) that production replay actually depends on has zero coverage. — Failure scenario: the daemon contract attaches branchRecordId only to the LAST visible assistant chunk of a checkpointed record (pinned by history-replay-page's own multi-chunk test); for any record replayed as 2+ chunks (e.g. text/thought/text) the block is created by an earlier chunk without the id, and canMergeTextDelta admits the decorated final chunk into the existing block — so attachment rides entirely on the merge-path assignment. Probe-verified: removing that assignment drops branchRecordId from the merged block while all 298 existing tests in daemonUi.test.ts + daemon-ui-transcript.test.ts stay green — every restored/paginated session would silently lose the Branch button on all multi-chunk checkpointed messages.

Suggested fix: extend the test with a two-chunk replay sequence — first agent_message_chunk without _meta.qwenTranscript.branchRecordId, second carrying it, both sharing sourceRecordIds — and assert the single merged block ends with branchRecordId: 'checkpoint-record'; this exercises transcript.ts:692-693 directly.

中文说明

[Suggestion] R12-18:唯一的回放路径 reducer 测试只喂了单个 chunk,固定的是块创建时的挂载(transcript.ts:713-714);而生产回放实际依赖的合并路径赋值(transcript.ts:692-693)完全没有覆盖。— 失败场景:daemon 契约只把 branchRecordId 挂到带 checkpoint 记录的最后一个可见 assistant chunk 上(history-replay-page 自己的多 chunk 测试已固定);任何以 2 个以上 chunk 回放的记录(如 text/thought/text),其块由不带 id 的较早 chunk 创建,canMergeTextDelta 允许带装饰的最后 chunk 并入已有块 —— 因此挂载完全依赖合并路径赋值。已用探针验证:移除该赋值后合并块丢失 branchRecordId,而 daemonUi.test.ts + daemon-ui-transcript.test.ts 中全部 298 个既有测试仍是绿色 —— 所有恢复/分页会话都会在多 chunk 的带 checkpoint 消息上静默丢失 Branch 按钮。

建议修复:为该测试补一个双 chunk 回放序列 —— 第一个 agent_message_chunk 不带 _meta.qwenTranscript.branchRecordId,第二个带上,两者共享 sourceRecordIds —— 并断言合并后的单个块最终带有 branchRecordId: 'checkpoint-record';这直接覆盖 transcript.ts:692-693

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

…nversation

# Conflicts:
#	packages/cli/src/acp-integration/session/Session.ts
#	packages/web-shell/client/components/MessageList.dom.test.tsx
#	packages/web-shell/client/e2e/visuals/screenshots.spec.ts
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 18/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 18/100 轮)。改动内容与我反驳保留之处如下:

Round-12 review feedback — address summary (PR #8274)

Conflict resolution (--conflict true)

Merged origin/main (845d6cf77e) into feat/fork-from-one-conversation (merge commit 32617dbf89). Three content conflicts, all resolved by keeping both sides' intent:

  1. packages/cli/src/acp-integration/session/Session.ts — main exported DaemonToolLoopState (needed by main's Session.test.ts import) while this branch added the HistoryMutationRunner type above it. Kept both.
  2. packages/web-shell/client/components/MessageList.dom.test.tsx — main wrapped the test mount in TranscriptRenderModeProvider; this branch added the onBranchSession prop. Combined both in the mount helper.
  3. packages/web-shell/client/e2e/visuals/screenshots.spec.ts — both sides added independent visual tests at the same location (Assistant response branch action here, parallel agents group on main). Kept both tests in sequence.

One auto-merge artifact fixed before committing the merge: MessageList.tsx had a duplicated isResponding entry in a useCallback dependency array (added by both branches), which tripped the pre-commit react-hooks/exhaustive-deps gate.

Feedback dispositions

Critical findings — fixed

  • R12-1 (rc:3737429631) — RESOLVED. bridge.ts rewind dispatch: removed the withTimeout(initTimeoutMs) race-only wrapper; the dispatched sessionRewind ext-method is now awaited against getTransportClosedReject(entry) only, matching the branch path's "never a timeout followed by an unobserved commit" semantics. Added regression test waits for a dispatched rewind instead of timing out and committing later (mirror of the branch analogue).
  • R12-2 (rc:3737429641) — RESOLVED. bridge.ts branch/side-task mutation: await mutation now races getTransportClosedReject(entry), so a channel death mid-branch rejects the caller and the finally { releaseAdmissionOnce(); } runs instead of leaking the admission slot forever. The race only fires on channel exit, so slow-but-alive forks still wait for the real outcome.

Suggestions — addressed

  • R12-3 (rc:3737429667) — RESOLVED. removes a partially written target when fork creation fails now injects the failure AFTER a partial write lands on disk (staging writeFile writes bytes then throws) and asserts both that the target is absent and .branch-staging is empty. Probe-verified discriminating: disabling the staging cleanup makes the test fail.
  • R12-4 (rc:3737429672) — RESOLVED. Dropped the dead, stale-by-construction backupNames field from BranchCreationManifestV1, parseBranchCreationManifest validation, and the claim write site; updated the GC test fixture. Existing claims with the extra field still parse (JSON.parse ignores unknown fields).
  • R12-5 (rc:3737429674) — RESOLVED. Annotated the bare positional 0 in DaemonClient.branchSession with a comment pinning the intent (per-call timeout disabled because server-side branch restore can exceed fetchTimeoutMs, and a client-side abort could leave a committed branch unobserved). Kept the behavior — it is deliberate and tested; adding a timeoutMs knob would grow the API surface speculatively.
  • R12-6 (rc:3737429683) — RESOLVED. does not finalize the source recorder while a branch mutation is active now advances the close to the mutation queue (vi.waitFor on liveWaitForActiveTurnsToSettle + 20 microtask yields) before asserting, so without the gate finalize would already have been called and the assertion discriminates.
  • R12-7 (rc:3737429684) — RESOLVED. Session.beginHistoryMutation()'s busy throw now carries data: { errorKind: 'session_busy' } at the source, so sessionBranch callers see the same contract the rewind endpoint already maps. Added handler-level regression test surfaces session_busy errorKind when a branch hits a busy session.
  • R12-10 (rc:3737429701) — RESOLVED. Added the rewind mirror of rejects a normal branch as soon as a prompt is admittedrejects rewind as soon as a prompt is admitted pins the pendingPromptCount half of the admission guard and asserts sessionRewind never reaches the agent after the prompt completes.
  • R12-11 (rc:3737429706) — RESOLVED. The channel-overlap test now pins the second half of the invariant: the branched session's loadSession restore lands on the fresh channel B and NOT on the dying source channel A.
  • R12-12 (rc:3737429714) — RESOLVED. rejects a branch at the agent gate when assertCanStartTurn fails now also asserts liveBeginHistoryMutation was NOT called, pinning the assert-before-begin ordering whose violation would strand the busy latch.
  • R12-13 (rc:3737429717) — RESOLVED. Hoisted the rewind promptId prefix/suffix format checks and the Number.isInteger(turnIndex) check above runExclusiveHistoryMutation; only the snapshot-index resolution stays inside the gate. Malformed rewinds now fail synchronously with a deterministic invalid_rewind_target / invalid-params instead of queueing behind an active mutation and surfacing as a timeout. Added regression test rewindSession rejects malformed requests without waiting on a held gate.
  • R12-14 (rc:3737429720) — RESOLVED. Both sides of the catalog boundary hardened: the reader builds pageBranchPoints with Object.create(null) (fixes the silent '__proto__' write drop), and the replay page checks Object.hasOwn(branchPoints, recordId) (fixes the prototype-chain read leak).
  • R12-15 (rc:3737429726) — RESOLVED. Added rolls back a published backup when transcript publication fails with %s (ENOTSUP/EPERM/EXDEV): seeds a real file-history backup so backupPublished becomes true, fails the transcript link with the typed codes, and asserts the published file-history/<newId> directory is rolled back plus claims/staging are empty.
  • R12-16 (rc:3737429730) — RESOLVED. The oversized-manifest test now spies the API the reader actually uses: fs.promises.open is wrapped so each handle's read is tracked, and the test asserts zero reads of the oversized claim (the previous readFile spy pinned nothing).
  • R12-17 (rc:3737429732, rc:3737429736, rc:3737429741) — RESOLVED (all 3 occurrences). Extended the async-purity guard spy lists: collision-scan test gains existsSync/accessSync/readFileSync/lstatSync/realpathSync; branch-GC test gains accessSync/opendirSync; readLastJsonStringFieldsAsync test gains readFileSync/existsSync/accessSync/lstatSync/realpathSync.
  • R12-18 (rc:3737429745) — RESOLVED. Added attaches branchRecordId when the decorated chunk merges into an existing block: a two-chunk replay sequence sharing sourceRecordIds where only the second chunk carries branchRecordId; asserts the single merged block ends with the id. Probe-verified discriminating: removing the merge-path assignment (transcript.ts:692-693) makes the test fail.

Suggestions — not resolved (replies posted, threads left open)

  • R12-8 (rc:3737429689) — DEFERRED to a follow-up issue. The fix requires changing where checkpoint validation derives its state (recorder-maintained pending-tool-call snapshots surviving resume/re-anchor, or a new bounded reader reusing the index machinery) — a redesign of correctness-critical branching validation, not a safe minimal change this late. Current behavior is functionally correct; the cost is O(transcript-size) latency at end_turn. Already tracked as Medium-3 in the maintainer review. Reply posted on the thread.
  • R11-9 (rc:3737429679) — ESCALATED for maintainer follow-up. CI gating (ensuring the integration_cli job runs and is required) is CI/branch-protection configuration owned by the repository workflows and maintainers, not PR code — the finding itself notes "no code change needed". As additional evidence the gate works when it runs: this round executed integration-tests/cli/qwen-serve-streaming.test.ts against the fresh bundle locally — 6/6 passed. Reply posted on the thread.

Review-level note

The CHANGES_REQUESTED review ("Not reviewed: build-and-test — macos/windows Test jobs skipped in CI") describes skipped CI jobs, not a code defect — there is nothing addressable in code; the workflow owns CI job scheduling. All locally runnable required checks are listed below and pass.

Verification

All commands actually run and their results:

  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0, zero error TS)
  • npm run lint — passed (exit 0, zero errors/warnings)
  • npm run bundle — passed (exit 0)
  • vitest packages/acp-bridge src/bridge.test.ts (touched) — 497 passed
  • vitest packages/core sessionService.test.ts + sessionStorageUtils.test.ts + session-transcript-reader.test.ts + chatRecordingService.test.ts (touched) — 4 files, 376 passed
  • vitest packages/cli acpAgent.test.ts + session/Session.test.ts + session/history-replay-page.test.ts (touched) — 3 files, 967 passed
  • vitest packages/sdk-typescript daemonUi.test.ts + DaemonClient.test.ts (touched) — 594 passed; test/daemon-ui-transcript.test.ts — 14 passed
  • vitest packages/web-shell MessageList.dom.test.tsx + AssistantMessage.test.tsx (merge-affected) — 2 files, 107 passed
  • integration: cd integration-tests && QWEN_SANDBOX=false npx vitest run cli/qwen-serve-streaming.test.ts after npm run bundle — 6 passed
  • Probe checks (reverted after): disabling fork staging cleanup fails the R12-3 test; removing the transcript merge-path branchRecordId assignment fails the R12-18 test
  • Settings schema regeneration not needed (no settings source changed)

Commits this round: 32617dbf89 (merge of origin/main with conflict resolution) and 20bf878e6d (feedback fixes).

中文说明

第 12 轮评审反馈处理汇总(PR #8274

冲突解决(--conflict true

已将 origin/main845d6cf77e)合入 feat/fork-from-one-conversation(合并提交 32617dbf89)。共三处内容冲突,均按保留双方意图解决:

  1. packages/cli/src/acp-integration/session/Session.ts —— main 将 DaemonToolLoopState 改为导出(main 的 Session.test.ts import 需要),本分支在其上方新增了 HistoryMutationRunner 类型。两者均保留。
  2. packages/web-shell/client/components/MessageList.dom.test.tsx —— main 在测试挂载外层包了 TranscriptRenderModeProvider;本分支新增了 onBranchSession prop。在挂载辅助函数中合并两者。
  3. packages/web-shell/client/e2e/visuals/screenshots.spec.ts —— 双方在同一位置各自新增了独立的视觉测试(本分支的 Assistant response branch action、main 的 parallel agents group)。两个测试按顺序全部保留。

提交合并前修复了一处自动合并产物:MessageList.tsx 中某个 useCallback 依赖数组里出现了重复的 isResponding 条目(两个分支各自添加),会触发 pre-commit 的 react-hooks/exhaustive-deps 门禁。

反馈处理结论

Critical 发现 —— 已修复

  • R12-1(rc:3737429631)—— 已解决。 bridge.ts rewind 派发:移除了 withTimeout(initTimeoutMs) 这个只能竞速、无法取消的包装;已派发的 sessionRewind ext-method 现在只与 getTransportClosedReject(entry) 竞速等待真实结果,与 branch 路径"绝不出现超时之后紧跟一个未被观察到的提交"的语义一致。新增回归测试 waits for a dispatched rewind instead of timing out and committing later(branch 版本的镜像)。
  • R12-2(rc:3737429641)—— 已解决。 bridge.ts branch/side-task 变更:await mutation 现在与 getTransportClosedReject(entry) 竞速,channel 在 branch 过程中死亡时会拒绝调用方,finally { releaseAdmissionOnce(); } 得以执行,不再永久泄漏准入槽位。该竞速只在 channel 退出时触发,缓慢但存活的 fork 仍然等待真实结果。

Suggestion —— 已处理

  • R12-3(rc:3737429667)—— 已解决。 removes a partially written target when fork creation fails 现在在部分写入落盘之后注入失败(staging writeFile 写入若干字节后抛出),并同时断言目标文件不存在、.branch-staging 为空。已用探针验证具有区分力:禁用 staging 清理后该测试失败。
  • R12-4(rc:3737429672)—— 已解决。BranchCreationManifestV1parseBranchCreationManifest 校验与 claim 写入点移除了无生产读取点、天生过期的 backupNames 字段;同步更新了 GC 测试夹具。携带该字段的旧 claim 仍可解析(JSON.parse 忽略未知字段)。
  • R12-5(rc:3737429674)—— 已解决。DaemonClient.branchSession 中裸的位置参数 0 添加注释固定意图(禁用单次调用超时,因为服务端 branch 恢复可能超过 fetchTimeoutMs,且客户端中止可能留下未被观察到的已提交 branch)。行为保持不变 —— 该行为是有意且经过测试的;新增 timeoutMs 开关属于投机性地扩大 API 面。
  • R12-6(rc:3737429683)—— 已解决。 does not finalize the source recorder while a branch mutation is active 现在先把 close 推进到 mutation 队列(vi.waitFor 断言 liveWaitForActiveTurnsToSettle 已被调用 + 20 次微任务让出)再做断言;若没有门控,finalize 此时应已被调用,断言因而具有区分力。
  • R12-7(rc:3737429684)—— 已解决。 Session.beginHistoryMutation() 的忙抛出在源头携带 data: { errorKind: 'session_busy' },使 sessionBranch 调用方获得与 rewind 端点一致的契约。新增 handler 级回归测试 surfaces session_busy errorKind when a branch hits a busy session
  • R12-10(rc:3737429701)—— 已解决。 新增 rewind 版镜像测试 rejects rewind as soon as a prompt is admitted,固定准入守卫中 pendingPromptCount 这一半,并断言 prompt 完成后 sessionRewind 从未到达 agent。
  • R12-11(rc:3737429706)—— 已解决。 channel 重叠测试现在固定不变量的后一半:分支会话的 loadSession restore 落在新 channel B 上,而不是垂死的源 channel A 上。
  • R12-12(rc:3737429714)—— 已解决。 rejects a branch at the agent gate when assertCanStartTurn fails 新增断言 liveBeginHistoryMutation 未被调用,固定"先 assert 后 begin"的顺序 —— 违反该顺序会让忙锁永久置位且无释放。
  • R12-13(rc:3737429717)—— 已解决。 将 rewind 的 promptId 前缀/后缀格式校验与 Number.isInteger(turnIndex) 检查提升到 runExclusiveHistoryMutation 之上;门控内只保留快照索引解析。格式错误的 rewind 现在同步以确定性的 invalid_rewind_target / invalid-params 失败,而不是排在活跃 mutation 之后最终以超时呈现。新增回归测试 rewindSession rejects malformed requests without waiting on a held gate
  • R12-14(rc:3737429720)—— 已解决。 目录边界两侧都做了加固:读取器用 Object.create(null) 构建 pageBranchPoints(修复 '__proto__' 写入被静默丢弃),回放页用 Object.hasOwn(branchPoints, recordId) 判断(修复经原型链的读取泄漏)。
  • R12-15(rc:3737429726)—— 已解决。 新增 rolls back a published backup when transcript publication fails with %s(ENOTSUP/EPERM/EXDEV):播种真实 file-history 备份使 backupPublished 置位,用类型化错误码使 transcript link 失败,断言已发布的 file-history/<newId> 目录被回滚、claims/staging 为空。
  • R12-16(rc:3737429730)—— 已解决。 超大清单测试现在 spy 读取器实际使用的 API:包装 fs.promises.open 使每个句柄的 read 被跟踪,断言对超大 claim 零读取(原先的 readFile spy 什么也没固定)。
  • R12-17(rc:3737429732、rc:3737429736、rc:3737429741)—— 已解决(3 处全部)。 扩展异步纯净性守卫的 spy 列表:碰撞扫描测试补 existsSync/accessSync/readFileSync/lstatSync/realpathSync;branch-GC 测试补 accessSync/opendirSyncreadLastJsonStringFieldsAsync 测试补 readFileSync/existsSync/accessSync/lstatSync/realpathSync
  • R12-18(rc:3737429745)—— 已解决。 新增 attaches branchRecordId when the decorated chunk merges into an existing block:共享 sourceRecordIds 的双 chunk 回放序列,仅第二个 chunk 携带 branchRecordId,断言合并后的单个块最终带有该 id。已用探针验证具有区分力:移除合并路径赋值(transcript.ts:692-693)后该测试失败。

Suggestion —— 未解决(已回帖,线程保持开放)

  • R12-8(rc:3737429689)—— 推迟到后续 issue。 修复需要改变 checkpoint 校验的状态来源(记录器维护跨 resume/重新锚定仍正确的挂起工具调用快照,或新增复用索引机制的有界读取器)—— 这是对分支子系统正确性关键校验的重新设计,在 PR 的这个阶段不属于安全的最小改动。当前行为功能正确,代价是 end_turn 时 O(transcript 大小) 的延迟。维护者评审已作为 Medium-3 跟踪。已在对应线程回帖。
  • R11-9(rc:3737429679)—— 升级给维护者跟进。 CI 门禁(确保 integration_cli job 运行且为必需)属于仓库工作流与维护者负责的 CI 配置/分支保护,不是 PR 代码 —— 该发现本身也注明"无需代码改动"。作为门禁可用性的补充证据:本轮在重新 bundle 后本地运行了 integration-tests/cli/qwen-serve-streaming.test.ts,6/6 通过。已在对应线程回帖。

评审层面说明

CHANGES_REQUESTED 评审("Not reviewed: build-and-test —— macos/windows Test jobs 在 CI 中被跳过")描述的是被跳过的 CI job,而非代码缺陷 —— 代码层面无可处理项;CI job 调度由工作流负责。所有本地可运行的必需检查均已列出且通过(见下)。

验证(Verification)

实际运行的命令及结果:

  • npm run build —— 通过(exit 0)
  • npm run typecheck —— 通过(exit 0,零 error TS
  • npm run lint —— 通过(exit 0,零 error/warning)
  • npm run bundle —— 通过(exit 0)
  • vitest packages/acp-bridge src/bridge.test.ts(触及)—— 497 通过
  • vitest packages/core sessionService.test.ts + sessionStorageUtils.test.ts + session-transcript-reader.test.ts + chatRecordingService.test.ts(触及)—— 4 文件 376 通过
  • vitest packages/cli acpAgent.test.ts + session/Session.test.ts + session/history-replay-page.test.ts(触及)—— 3 文件 967 通过
  • vitest packages/sdk-typescript daemonUi.test.ts + DaemonClient.test.ts(触及)—— 594 通过;test/daemon-ui-transcript.test.ts —— 14 通过
  • vitest packages/web-shell MessageList.dom.test.tsx + AssistantMessage.test.tsx(合并波及)—— 2 文件 107 通过
  • 集成测试:npm run bundlecd integration-tests && QWEN_SANDBOX=false npx vitest run cli/qwen-serve-streaming.test.ts —— 6 通过
  • 探针检查(验证后已还原):禁用 fork staging 清理会使 R12-3 测试失败;移除 transcript 合并路径的 branchRecordId 赋值会使 R12-18 测试失败
  • 无需重新生成 settings schema(未改动 settings 源)

本轮提交:32617dbf89(合入 origin/main 并解决冲突)、20bf878e6d(反馈修复)。

Deferred non-Critical feedback

Critical-only mode is active after 10 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 10 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

qwen-code-ci-bot pushed a commit that referenced this pull request Aug 7, 2026
@wenshao

wenshao commented Aug 8, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /review

@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown
Contributor
_Qwen Code review request accepted. Review is queued in [workflow run](https://github.com/QwenLM/qwen-code/actions/runs/31243158316)._

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 19/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 19/100 轮)。改动内容与我反驳保留之处如下:

Round summary — PR #8274 (base-conflict resolution)

Feedback triage

No actionable feedback this round: feedback.md contained no reviews, inline
comments, issue-level comments, or failed checks. Critical-only mode is active
after 10 change-producing rounds; the Deferred non-Critical feedback section
is an empty audit record and was not worked, per the mode rules.

Conflict resolution (--conflict true, base main)

Merged origin/main (tip 59b750fc4d) into the PR branch. Two content
conflicts plus one semantic auto-merge breakage, all around the Session
constructor where both sides independently added a trailing parameter:

  1. packages/cli/src/acp-integration/session/Session.ts — our branch added
    runExclusiveAutomaticHistoryMutation: HistoryMutationRunner; main added
    onActiveWorkChanged?: () => void (active-work reporting from feat(serve): Expose active work state #8588).
    Resolution: keep both parameters — ours first (existing tests pass it
    positionally as the 5th argument), onActiveWorkChanged after it, JSDoc
    preserved verbatim from main.
  2. packages/cli/src/acp-integration/acpAgent.ts — the new Session(...)
    call in createAndStoreSession passed each side's callback alone.
    Resolution: pass both, in the order matching the constructor — the
    history-mutation runner, then () => this.activeWorkReporter?.notifyChanged().
  3. packages/cli/src/acp-integration/session/Session.test.ts — auto-merged
    without markers but broken: main's createReportingSession helper passed
    the notify callback as the 5th positional argument, which is now the
    history-mutation-runner slot (TS2345: Type 'void' is not assignable to type 'Promise<T>', caught by the build). Fix: pass undefined for the
    runner so the callback lands in the 6th slot; the runner's default
    (run inline) applies.

The two features are orthogonal — serialization of automatic history
mutations vs. active-work heartbeat reporting — so no behavioral interaction
beyond the shared constructor signature.

Verification

  • npm run build — passed (after the Session.test.ts fix; initially failed
    with the TS2345 above)
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --check on the three touched files — passed
  • vitest (packages/cli, touched):
    • Session.test.ts — 557 passed (covers both merged feature sets:
      history-mutation gating + active-work holds)
    • acpAgent.test.ts — 400 passed
    • Session.review-lease.test.ts, Session.worktree.test.ts,
      active-work-reporter.test.ts — 21 passed
  • vitest (packages/core, merged files): chatRecordingService.test.ts +
    transcript-records.test.ts — 95 passed
  • vitest (packages/acp-bridge, merged files): bridge.test.ts,
    transcript-replay.test.ts, spawnChannel.test.ts — 576 passed
  • vitest (packages/sdk-typescript, merged file): DaemonClient.test.ts
    311 passed

Committed as merge 40c50aad58 with parents 20bf878e6d (prior PR head) and
59b750fc4d (origin/main).

中文说明

轮次总结 — PR #8274(基分支冲突解决)

反馈分诊

本轮没有可处理的反馈:feedback.md 中没有任何评审、行内评论、issue 级评论或失败的检查。在 10 个产生改动的轮次之后已进入仅处理 Critical 的模式;Deferred non-Critical feedback(延后的非 Critical 反馈)部分是一条空的审计记录,按该模式规则不做处理。

冲突解决(--conflict true,基分支 main

origin/main(尖端 59b750fc4d)合并进 PR 分支。共两处内容冲突,外加一处自动合并造成的语义破坏,全部集中在 Session 构造函数附近——两侧各自独立地在末尾新增了一个参数:

  1. packages/cli/src/acp-integration/session/Session.ts — 我们的分支新增了 runExclusiveAutomaticHistoryMutation: HistoryMutationRunnermain 新增了 onActiveWorkChanged?: () => void(来自 feat(serve): Expose active work state #8588 的活跃工作状态上报)。解决方案:两个参数都保留——我们的在前(已有测试按位置将其作为第 5 个参数传入),onActiveWorkChanged 在后,其 JSDoc 注释原样保留自 main
  2. packages/cli/src/acp-integration/acpAgent.tscreateAndStoreSession 中的 new Session(...) 调用各自只传了本侧的回调。解决方案:两个都传,顺序与构造函数一致——先传历史变更串行化 runner,再传 () => this.activeWorkReporter?.notifyChanged()
  3. packages/cli/src/acp-integration/session/Session.test.ts — 自动合并时没有冲突标记但实际已损坏:maincreateReportingSession 辅助函数把通知回调作为第 5 个位置参数传入,而该位置现在是历史变更 runner 的槽位(TS2345: Type 'void' is not assignable to type 'Promise<T>',由构建捕获)。修复:为 runner 显式传 undefined,使回调落到第 6 个槽位;runner 使用其默认值(直接内联执行)。

两个功能彼此正交——一个是自动历史变更的串行化,一个是活跃工作心跳上报——除了共享的构造函数签名外没有任何行为上的交互。

验证

  • npm run build — 通过(在修复 Session.test.ts 之后;最初因上述 TS2345 失败)
  • npm run typecheck — 通过
  • npm run lint — 通过
  • 对三个改动文件执行 npx prettier --check — 通过
  • vitest(packages/cli,涉及文件):
    • Session.test.ts — 557 通过(覆盖合并进来的两套功能:历史变更门控 + 活跃工作持有)
    • acpAgent.test.ts — 400 通过
    • Session.review-lease.test.tsSession.worktree.test.tsactive-work-reporter.test.ts — 21 通过
  • vitest(packages/core,合并涉及文件):chatRecordingService.test.ts + transcript-records.test.ts — 95 通过
  • vitest(packages/acp-bridge,合并涉及文件):bridge.test.tstranscript-replay.test.tsspawnChannel.test.ts — 576 通过
  • vitest(packages/sdk-typescript,合并涉及文件):DaemonClient.test.ts — 311 通过

以合并提交 40c50aad58 提交,父提交为 20bf878e6d(之前的 PR head)与 59b750fc4d(origin/main)。

Deferred non-Critical feedback

Critical-only mode is active after 10 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 10 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: conflicted with main — resolved in this push. · 与 main 有冲突——已在本次推送中解决。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

qwen-code-ci-bot pushed a commit that referenced this pull request Aug 8, 2026
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 21600 seconds (of the 360-minute budget). This run already used the maximum 360 minute timeout. See workflow logs.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

No action taken this round (PR #8274)

Critical-only mode is active after 10 change-producing rounds. This round's actionable sections (reviews, inline comments, issue-level comments) are empty — there is no new feedback to address. The only actionable signal is the failed review-pr check, which was diagnosed below as reviewer budget exhaustion with no in-PR fix.

Feedback triage

  • Reviews since the last evaluation (2026-08-07T17:01Z): none.
  • Inline comments since the last evaluation: none.
  • Issue-level comments since the last evaluation: none.
  • Deferred non-Critical feedback: one bot notification comment recording that Critical-only mode is active. Per the Critical-only rules this is an audit record, not work — no code change, thread resolution, or reply was made for it.

Failed check review-pr — reviewer budget exhaustion, no fix available in this branch

The only failed check is review-pr (workflow "🧐 Qwen Pull Request Review", job 93081872987). Evidence:

  • The job ran from 2026-08-08T08:39:17Z to 2026-08-08T14:39:55Z — 360m 38s. This equals the job-level cap timeout-minutes: ${{ fromJSON(vars.QWEN_REVIEW_JOB_TIMEOUT_MINUTES) }} (default 360, per the workflow's own comments), i.e. GitHub Actions cancelled the job at its timeout cap rather than any step failing on its own.
  • review-pr is the automated reviewer's own job: it checks out the base branch and reviews the PR diff. It does not build or test this PR's code, so its timeout is not evidence of a defect in the branch.
  • All quality gates that actually exercise this PR's code in the same CI run passed: Test (ubuntu-latest, Node 22.x), Desktop Shell (ubuntu-22.04 and windows-2022), Serve A/B, web-shell E2E Smoke, Web-shell Visuals, SDK Java (all five platform/Java combinations). The macOS/Windows Test jobs were skipped, not failed, consistent with prior rounds.
  • This PR contains no changes under .github/ — the review workflow and its timeout configuration (repository variables) are maintainer-owned infrastructure that this branch does not touch.
  • The previous completed review (2026-08-07T17:01Z) already reported being "stopped before round 4 by the review time budget". This PR is unusually large (62 files, +9729/−830, 163 inline comments across 10+ review rounds) and repeatedly exhausts the review budget.

There is therefore no code-level change this branch can make to clear this check. Suggested maintainer next steps:

  1. Re-run the review (@qwen-code /retry starts a fresh counting window) — a retry may complete if the budget overrun was marginal.
  2. Raise the QWEN_REVIEW_JOB_TIMEOUT_MINUTES / QWEN_REVIEW_MAX_TIMEOUT_MINUTES repository variables so an unusually large PR fits within the review budget.
  3. If review cost stays prohibitive, consider splitting the PR — a scope call for the maintainer, not a decision this bot can make.

Verification

No code changes were made this round, so no build/typecheck/lint/test commands were run and no commit was created. The working tree remains at the PR head (40c50aad58).

中文说明

本轮未做任何改动(PR #8274

在经历 10 个产生改动的轮次后,仅处理 Critical 的模式已激活。本轮可执行区域(reviews、inline comments、issue-level comments)均为空 —— 没有新反馈需要处理。唯一的可执行信号是失败的 review-pr 检查,下面将其诊断为评审预算耗尽,且本分支内没有可用的修复手段。

反馈分类

  • 自上次评估(2026-08-07T17:01Z)以来的 Reviews:
  • 自上次评估以来的 Inline comments:
  • 自上次评估以来的 Issue 级评论:
  • 延后的非 Critical 反馈:一条机器人通知评论,记录仅处理 Critical 模式已激活。按照 Critical-only 规则,这是一条审计记录,不是工作项 —— 未对其做任何代码改动、线程 resolve 或回复。

失败的检查 review-pr —— 评审预算耗尽,本分支无法修复

唯一失败的检查是 review-pr(工作流 “🧐 Qwen Pull Request Review”,作业 93081872987)。证据如下:

  • 该作业从 2026-08-08T08:39:17Z 运行到 2026-08-08T14:39:55Z,共 360 分 38 秒。这正好等于作业级上限 timeout-minutes: ${{ fromJSON(vars.QWEN_REVIEW_JOB_TIMEOUT_MINUTES) }}(按工作流自身注释,默认值为 360),也就是说该作业是被 GitHub Actions 在超时上限处取消的,而不是某个步骤自行失败。
  • review-pr 是自动评审机器人自己的作业:它检出 base 分支并对 PR diff 进行评审,并不会构建或测试本 PR 的代码,因此它的超时并不能说明本分支存在缺陷。
  • 同一次 CI 运行中,所有真正执行本 PR 代码的质量门禁均通过:Test (ubuntu-latest, Node 22.x)、Desktop Shell(ubuntu-22.04 和 windows-2022)、Serve A/B、web-shell E2E Smoke、Web-shell Visuals、SDK Java(全部五个平台/Java 组合)。macOS/Windows 的 Test 作业是被跳过而非失败,与此前各轮一致。
  • 本 PR 不包含 .github/ 下的任何改动 —— 评审工作流及其超时配置(仓库变量)属于维护者管理的基础设施,本分支并未触碰。
  • 上一次完成的评审(2026-08-07T17:01Z)已经报告 “stopped before round 4 by the review time budget”(在第 4 轮之前因评审时间预算而中止)。本 PR 体量异常大(62 个文件,+9729/−830,历经 10 多轮评审、163 条 inline 评论),屡次耗尽评审预算。

因此,本分支没有任何代码层面的改动可以清掉这个检查。建议维护者后续采取的措施:

  1. 重新运行评审(@qwen-code /retry 会开启一个新的计数窗口)—— 如果预算超出的幅度不大,重试可能会完成。
  2. 调高仓库变量 QWEN_REVIEW_JOB_TIMEOUT_MINUTES / QWEN_REVIEW_MAX_TIMEOUT_MINUTES,让异常大的 PR 能在评审预算内完成。
  3. 如果评审成本始终过高,可以考虑拆分 PR —— 这属于维护者的范围决策,不是本机器人可以做出的决定。

验证

本轮没有任何代码改动,因此没有运行任何 build/typecheck/lint/测试命令,也没有创建任何提交。工作树保持在 PR head(40c50aad58)。

Deferred non-Critical feedback

Critical-only mode is active after 10 change-producing rounds. The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

完成 10 个产生改动的轮次后进入仅处理 Critical 的模式。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [review-pr] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [review-pr] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

qwen-code-ci-bot pushed a commit that referenced this pull request Aug 8, 2026
Simplify historical session branching around the minimum persistence,
recording, and navigation invariants required by the Web Shell flow.

- Replace branch claims and garbage collection with staged publication
- Validate completed turns incrementally instead of reloading transcripts
- Separate persisted branch creation from live session restoration
- Bound SDK waits and prevent late results from replacing navigation
- Remove unused checkpoint prompt IDs while reading legacy records

Note: A pre-commit crash may leave hidden staging or orphan backups.
qwen-code-ci-bot pushed a commit that referenced this pull request Aug 9, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [Test (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [Test (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

qwen-code-ci-bot pushed a commit that referenced this pull request Aug 9, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix ran out of time before finishing (timeout (7200000ms)) (attempt 20/100) — it will retry on the next scan.

What I found before stopping:
Qwen failed during address-review: timeout (7200000ms).

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31298406145


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix stopped: this counting window now contains 3 time-budget exhaustions (pushed rounds in between included; this round itself may have failed differently). That is 3 full agent runs that pushed nothing. A human should split or reduce the PR (or raise the agent time budget AND its step backstop together), then comment @qwen-code /retry to re-arm. Until then future scans will skip this PR.

What I found before stopping:
Qwen failed during address-review: timeout (7200000ms).

See the Qwen Autofix agent step logs for model/tool output.

Run log: https://github.com/QwenLM/qwen-code/actions/runs/31303345287


🧠 Handled by Qwen Code · model/模型 qwen3.8-max

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

⏸️ Takeover paused: this PR reached its round cap (100/100). Comment @qwen-code /takeover to re-arm a fresh window and continue management, or @qwen-code /takeover stop to release.

中文说明

⏸️ 托管已暂停:本 PR 达到轮次上限(100/100)。评论 @qwen-code /takeover 可重新武装、开启新窗口继续托管;或评论 @qwen-code /takeover stop 释放。

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review timed out. Qwen review timed out after 21600 seconds (of the 360-minute budget). This run already used the maximum 360 minute timeout. See workflow logs.

…conversation

# Conflicts:
#	packages/cli/src/acp-integration/acpAgent.ts
qwen-code-ci-bot pushed a commit that referenced this pull request Aug 9, 2026
pull Bot pushed a commit to mcx/qwen-code that referenced this pull request Aug 15, 2026
* feat(web-shell): branch from completed assistant responses

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

* fix(session): preserve historical branch checkpoints

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

* fix(web-shell): harden response session branching

* chore: remove PR comment evaluation artifact

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

* fix(web-shell): guard historical branch mutations

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

* perf(core): index historical branch points during transcript scan

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

* fix(core): address review findings — dead code, boundary remap, promptId guard, stale toast (QwenLM#8274)

* fix(core): address review findings — dead code, boundary remap, promptId guard, stale toast (QwenLM#8274)

* fix(core): address review findings — archived GC, subtype registration, UUID validation, dead code (QwenLM#8274)

* test: strengthen branch-point and fork coverage from review (QwenLM#8274)

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

* fix: address round-4 review feedback for session branching (QwenLM#8274)

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

* fix: address round-5 review feedback for session branching (QwenLM#8274)

* fix: address round-6 review feedback for session branching (QwenLM#8274)

* fix: address round-7 review feedback for session branching (QwenLM#8274)

* fix(core): harden branch-point resolution against malformed transcript 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.

* fix: address round-8 review feedback for session branching (QwenLM#8274)

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix: address round-9 review feedback for session branching (QwenLM#8274)

* test(core): pin branch GC isolation from throwing warning callbacks (QwenLM#8274)

* fix(acp-bridge): reject rewind at admission while a prompt is active (QwenLM#8274)

* fix(web-shell): harden session branch publication

Preserve direct ACP prompt preemption while fencing branch and rewind history mutations at the Session boundary. Convert branch publication, backup staging, cleanup, and stale-claim GC to asynchronous filesystem APIs, and surface unsupported hard-link commits as typed ACP and HTTP errors. Expand regression coverage and update the reviewed design contract.

* fix(web-shell): harden historical response branching

Reject branches during prompt admission and keep dispatched mutations
owned until their real outcome is known.

- remove detached timeouts across ACP, SDK, and WebUI
- bound branch cleanup and make title scans asynchronous
- avoid full branch-point scans during transcript pagination
- add regression coverage from Core through the real daemon and browser

* fix: address round-11 review feedback for session branching (QwenLM#8274)

* fix: address round-12 review feedback for session branching (QwenLM#8274)

* refactor(branching): remove branch-specific overdesign

Simplify historical session branching around the minimum persistence,
recording, and navigation invariants required by the Web Shell flow.

- Replace branch claims and garbage collection with staged publication
- Validate completed turns incrementally instead of reloading transcripts
- Separate persisted branch creation from live session restoration
- Bound SDK waits and prevent late results from replacing navigation
- Remove unused checkpoint prompt IDs while reading legacy records

Note: A pre-commit crash may leave hidden staging or orphan backups.

* chore(sdk): update browser bundle budget

Account for the combined historical branching and transcript projection APIs after merging main while keeping the browser bundle size guard narrowly bounded.

* fix(branching): address review lifecycle gaps

Harden historical session branching against cancellation, observer,
navigation, and shutdown races found during review.

- Normalize cancellation keys and bound close-time mutation waits
- Preserve anchors after observer completion and load persisted forks
- Report success only when the guarded session switch starts
- Cover recorder cursors, fork cleanup, admission, and rollback
- Align daemon events and branch errors with runtime behavior

* refactor(session): simplify branching safeguards

Reduce the session branching surface after review while preserving the
critical concurrency, durability, and ownership guarantees.

- Remove the unused full-chain resolver and test production entry points
- Copy backups from verified open handles instead of using hard links
- Reuse the bounded title scan instead of maintaining an async mirror
- Deduplicate UI branch requests and fail fast for busy automatic turns
- Consolidate repeated mutation tests and retain critical race coverage
- Document the retained invariants and rejected overdesign explicitly

* test(branching): simplify regression coverage

Reduce duplicated branching tests while retaining regression coverage for
the safety, concurrency, and lifecycle fixes introduced by this feature.

- Consolidate symmetric bridge and agent scenarios with table-driven cases
- Remove repeated cross-layer assertions and brittle implementation spies
- Drop redundant UI permutations and branch-only visual snapshots

* fix(serve): handle branch busy admission

* fix(sdk): preserve v1 branch session contract

Keep existing latest-state branch callers source- and wire-compatible while
retaining the persisted-only behavior for historical checkpoint branches.

- Restore no-anchor branches before returning their live client identity
- Add a separate typed result for persisted historical branch requests
- Clean up restored attachments on stale navigation and disconnect races
- Cover immediate continuation and historical persistence independently

* fix(daemon): guard branching history mutations

Prevent branch creation and automatic Goal turns from racing session
teardown or interactive history mutations.

- Reject branch admission while a conditional close is authorized
- Serialize Goal continuations behind the history mutation gate
- Limit branch checkpoints to interactive prompts
- Add regressions for close and Goal scheduling races

* fix(branching): preserve fork and checkpoint semantics

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

* fix(branching): harden branch and rewind behavior

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

---------

Co-authored-by: heyang.why <heyang.why@alibaba-inc.com>
Co-authored-by: Shaojin Wen <shaojin.wensj@alibaba-inc.com>
Co-authored-by: 易良 <1204183885@qq.com>
Co-authored-by: qwen-code-ci-bot <qwen-code-ci-bot@users.noreply.github.com>
Co-authored-by: qwen-code-bot <qwen-code-bot@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants