Skip to content

feat(web-shell): bind plan approval to its Todo revision - #8393

Merged
yiliang114 merged 14 commits into
QwenLM:mainfrom
yiliang114:codex/8389-plan-revision-approval
Aug 4, 2026
Merged

feat(web-shell): bind plan approval to its Todo revision#8393
yiliang114 merged 14 commits into
QwenLM:mainfrom
yiliang114:codex/8389-plan-revision-approval

Conversation

@yiliang114

@yiliang114 yiliang114 commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

Binds each exit_plan_mode approval request to the exact structured Todo revision that was submitted for review. The daemon carries the Todo plan ID and source tool-call ID in permission metadata, and WebShell resolves the approval DAG only when both values match the transcript snapshot.

Live plan updates establish the revision through the existing session-update path. History replay, rewind, and fresh transitions into Plan mode invalidate the binding so stale snapshots from completed cycles cannot bind a later approval. A resumed session without a new live todo_write safely falls back to the existing text-only approval; the next valid live Todo update establishes a new binding. Once an approval request is created, later Todo updates cannot replace its DAG, and an all-completed snapshot remains visible for review.

The structured Todo DAG is shown only when the experimental Session Workflow setting from #8391 is enabled. With the setting disabled, Plan mode keeps the existing text-only approval presentation.

This PR is stacked on #8391. Until #8391 merges, GitHub will show both phases in this PR's diff.

Why it's needed

The approval dialog previously selected the latest active Todo list, which could differ from the plan the user was actually approving. Binding approval to a stable revision makes the pre-execution DAG deterministic without introducing another Workflow store or scheduler.

Reviewer Test Plan

How to verify

  1. Enable the experimental Session Workflow setting from feat(web-shell): gate Session Workflow behind an experimental setting #8391, enter Plan & Review, produce a structured Todo plan, and submit exit_plan_mode; confirm the approval dialog shows that Todo DAG before execution begins.
  2. Add a newer Todo snapshot with the same plan ID after the permission request is created; confirm the dialog still shows the snapshot named by the approval's source call.
  3. Resume a Plan-mode session with a persisted Todo snapshot and submit exit_plan_mode without a new live todo_write; confirm the approval uses the existing text-only presentation. Then produce a new structured Todo update and submit again; confirm the exact new DAG is shown.
  4. Disable the experimental Session Workflow setting, produce a structured Todo plan, and submit exit_plan_mode; confirm Plan mode keeps the existing text-only approval presentation.
  5. Remove or corrupt the revision metadata; confirm approval safely uses the existing text-only presentation.

Evidence (Before & After)

Before: exit-plan approval used whichever active Todo snapshot appeared latest in the WebShell transcript.

After: with Session Workflow enabled, exit-plan approval resolves only the exact {planId, sourceCallId} pair carried by the daemon permission request. Replay, rewind, and Plan-mode re-entry invalidate stale bindings; sessions without a fresh live Todo revision fall back to text-only approval.

Automated checks passed on macOS: repository lint, full typecheck, full build, 149 focused WebShell tests, and focused daemon approval tests.

Tested on

OS Status
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Environment (optional)

Node.js 22, local daemon WebShell build.

Risk & Scope

  • Main risk or tradeoff: The revision identity is carried in Qwen-specific ACP _meta; clients that ignore it keep the existing text approval behavior. Replayed and rewound sessions also use text-only approval until a new live Todo update establishes a binding.
  • Feature availability: The structured Todo DAG is disabled by default and requires the experimental Session Workflow setting.
  • Not validated / out of scope: Automatic dependency scheduling, retries, cross-session cockpit storage, and broad cross-browser manual UI testing.
  • Breaking changes / migration notes: None. No new protocol mode or persistent store is introduced.

Linked Issues

Related to #8389. Depends on #8391.

中文说明

本 PR 做了什么

将每一次 exit_plan_mode 审批请求绑定到本次实际提交审阅的结构化 Todo 版本。daemon 在权限元数据中携带 Todo 的计划 ID 与来源工具调用 ID,WebShell 只有在两者都与 transcript 快照精确匹配时才展示审批 DAG。

实时 plan update 会通过现有 session update 路径建立 revision。历史回放、rewind 和重新进入 Plan 模式会使绑定失效,避免已完成周期中的过期快照绑定后续审批。恢复后的 session 如果没有新的 live todo_write,会安全降级为现有纯文本审批;下一条有效的实时 Todo update 会重新建立绑定。审批请求创建后,后续 Todo 更新不能替换正在审批的 DAG;即使快照中的节点都已 completed,审批 DAG 仍会保留。

结构化 Todo DAG 仅在开启 #8391 提供的实验性 Session Workflow 设置后展示。关闭该设置时,Plan 模式保持现有纯文本审批界面。

本 PR 叠加在 #8391 之上。在 #8391 合并前,GitHub 会暂时在本 PR 中显示两个阶段的累计 diff。

为什么需要

此前审批框会选择 WebShell transcript 中最新的 active Todo,它可能并不是用户本次实际审批的计划。绑定稳定版本后,执行前 DAG 可以保持确定,同时不需要新增 Workflow store 或调度器。

Reviewer Test Plan

如何验证

  1. 开启 feat(web-shell): gate Session Workflow behind an experimental setting #8391 提供的实验性 Session Workflow 设置,进入 Plan & Review,生成结构化 Todo 后提交 exit_plan_mode;确认执行开始前审批框展示该 Todo DAG。
  2. 在权限请求创建后增加同一计划 ID 的较新 Todo 快照;确认审批框仍展示审批元数据指定的来源调用所对应的快照。
  3. 恢复一个包含持久化 Todo 快照的 Plan Mode session,在没有新 live todo_write 的情况下提交 exit_plan_mode;确认使用现有纯文本审批。随后生成新的结构化 Todo update 并再次提交;确认展示精确绑定的新 DAG。
  4. 关闭实验性 Session Workflow 设置,生成结构化 Todo 后提交 exit_plan_mode;确认 Plan 模式保持现有纯文本审批界面。
  5. 删除或破坏 revision 元数据;确认审批安全降级为现有纯文本展示。

前后对比证据

之前:退出 Plan 的审批会使用 WebShell transcript 中最后出现的 active Todo 快照。

之后:开启 Session Workflow 时,退出 Plan 的审批只解析 daemon 权限请求携带的准确 {planId, sourceCallId}。历史回放、rewind 和重新进入 Plan 模式会使过期绑定失效;没有新的实时 Todo revision 时,session 会降级为纯文本审批。

已在 macOS 通过仓库 lint、全量 typecheck、全量 build、149 项 WebShell 相关测试以及 daemon 审批定向测试。

测试环境

OS 状态
🍏 macOS
🪟 Windows N/A
🐧 Linux N/A

Node.js 22,本地 daemon WebShell 构建。

风险与范围

  • 主要风险或取舍:revision identity 通过 Qwen 专用 ACP _meta 传递;忽略该字段的客户端继续使用现有纯文本审批。历史回放或 rewind 后的 session 也会使用纯文本审批,直到新的实时 Todo update 建立绑定。
  • 功能可用性:结构化 Todo DAG 默认关闭,需要开启实验性 Session Workflow 设置。
  • 未验证或不在范围内:自动依赖调度、重试、跨 Session 驾驶舱存储,以及广泛的跨浏览器人工 UI 测试。
  • 破坏性变更与迁移:无。没有新增协议模式或持久化存储。

关联 Issue

关联 #8389。依赖 #8391

@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 2, 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 a3a6ece. Only screenshots that changed are shown (flows below, if any, are head-only) — refreshes on every push.

Screenshots · before / after

ℹ️ No screenshot changed against the PR base — but this PR edits 3 render-shaping files:

  • packages/web-shell/client/App.tsx
  • packages/web-shell/client/components/ChatPane.tsx
  • packages/web-shell/client/components/messages/ToolApproval.tsx

Either the change has no visual effect (logic, plumbing, a state the scenarios never reach), or no scenario renders this UI — in which case the preview cannot see it, and an empty result is a coverage gap rather than a clean bill of health. To make it visible, add a scenario to packages/web-shell/client/e2e/visuals/screenshots.spec.ts that seeds whatever state the UI is gated on; it then appears here as a head-only (NEW) capture.

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

Qwen Code · web-shell visuals

@yiliang114
yiliang114 marked this pull request as ready for review August 3, 2026 02:03
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 3, 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 the PR!

Template: complete ✓

Problem: real and documented. Open issue #8389 (status/in-progress) lists "bind the approval dialog to the exact planId and Todo revision/source call submitted for approval" as an explicit goal, following the merged workflow-visualization work (#7525, #7580). This PR is "PR 2: Revision-bound pre-execution approval" from that issue's delivery plan — a planned increment, not a speculative change.

Direction: aligned. The Plan & Review experience is experimental, default-off, and scoped to the daemon WebShell; binding the approval to the submitted revision is exactly what makes the pre-execution DAG deterministic. The claude-code CHANGELOG has no direct reference to revision binding, but plan-approval dialog correctness is an actively maintained area there (fixes for stale-snapshot labeling and empty plan-approval modals) — a signal this UX deserves the care taken here.

Size: small. Phase 2 (this PR's own commit): ~91 production-logic lines + ~178 test lines. Cumulative with stacked #8391: ~154 production / ~232 test / 5 schema / ~54 docs lines. The only Stage 0 core-path match is packages/cli/src/config/settingsSchema.ts (+10, additive experimental setting, phase-1 scope). Well under every threshold.

Approach: minimal. The revision identity ({planId, sourceCallId}) rides in Qwen-specific ACP _meta over the already-existing plan lineage and session-update paths — the daemon already emits this meta (PlanEmitter) and re-emits it on history replay (transcript-replay), so this PR captures and consumes it rather than adding a store or scheduler. Clients ignoring the meta keep today's text approval. I don't see a materially simpler way to make the approval deterministic.

Stacking: the branch carries #8391's commit, so until #8391 merges GitHub shows both phases in this diff. The review below focuses on the phase-2 commit (985a87b).

Risk: packages/cli/src/acp-integration/session/Session.ts matches a revert-correlated path pattern (acp-integration) — flagged for reviewer focus; reviewed at full depth with CI evidence in the next stage.

Moving on to code review. 🔍

中文说明

感谢贡献!

模板:完整 ✓

问题:真实且有据可查。开放 issue #8389status/in-progress)明确将"审批框绑定到实际提交审批的 planId 与 Todo 版本/source call"列为目标,是已合并的 Workflow 可视化工作(#7525#7580)的后续。本 PR 正是该 issue 交付计划中的"PR 2:版本绑定的执行前审批"——计划内的增量,不是临时起意。

方向:对齐。Plan & Review 是实验性、默认关闭、仅限 daemon WebShell 的能力;把审批绑定到提交的版本正是让执行前 DAG 确定化的关键。claude-code 的 CHANGELOG 没有直接对应"版本绑定"的条目,但 plan 审批框的正确性在那里是活跃维护区域(修复过快照过期标注、空审批弹窗等问题)——说明这块 UX 值得细致打磨。

规模:小。阶段 2(本 PR 自有 commit)约 91 行生产逻辑 + 178 行测试;与叠加的 #8391 累计约 154 生产行 / 232 测试行 / 5 行 schema / 54 行文档。唯一命中 Stage 0 核心路径的是 packages/cli/src/config/settingsSchema.ts(+10,纯新增实验设置,属阶段 1)。远低于所有阈值。

方案:最小化。版本身份({planId, sourceCallId})通过 Qwen 专用 ACP _meta 搭载在既有的 plan lineage 与 session-update 路径上——daemon 本来就会发出这些 meta(PlanEmitter),历史回放也会重放(transcript-replay),本 PR 只是捕获并使用它,没有新增 store 或调度器。忽略该 meta 的客户端保持现有文字审批。没有更简单的路径能让审批确定化。

叠加关系:分支包含 #8391 的 commit,在 #8391 合并前 GitHub 会显示两个阶段的累计 diff。下一阶段的代码审查聚焦阶段 2 的 commit(985a87b)。

风险packages/cli/src/acp-integration/session/Session.ts 命中与 revert 相关的路径模式(acp-integration)——标记提醒 reviewer 关注;已按完整深度审查,CI 证据见下一阶段。

进入代码审查 🔍

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Code review — no blockers found. I proposed the solution independently before reading the diff (carry a stable revision identity in the permission _meta, resolve by exact match, fall back to text) and the PR lands on the same mechanism with nothing simpler missed. What I verified:

  • Daemon capture & resets: sendUpdate stores {planId, sourceCallId} from plan updates carrying the existing qwenTodoPlan / qwenTranscript.planToolCallId meta, and clears it on meta-less or empty updates. Both entries into Plan mode reset a stale revision — the setApprovalMode RPC transition and successful enter_plan_mode execution — so a revision can't leak across plan cycles.
  • Additive protocol surface: qwenTodoApproval is attached to the exit_plan_mode permission request only when a revision is known; clients ignoring it keep the current text approval.
  • Strict resolution: WebShell matches on both ids (first match wins). No meta / no match → empty list → ToolApproval's showsPlanWorkflow gate falls back to text-only. I traced that fallback end to end.
  • Intentional behavior change: an all-completed bound snapshot stays visible for review, where the old latest-active filter would have dropped it — asserted in tests, matches the PR description.
  • Tests pin the change: Session.test drives the real sendUpdate capture path and asserts the permission frame carries the binding; App.test sets up two snapshots under the same planId and asserts the dialog renders the bound (older) one, not the newer; todos.test covers mismatch rejection and terminal-snapshot preservation.

One non-blocking observation: legacy text-parsed plan messages (parsePlanTodosrole: 'plan' in transcriptToMessages) no longer feed the approval DAG — such transcripts now get text-only approval. That's consistent with the stated design (binding requires structured revision identity) and the flow is opt-in, but it is a behavior delta versus the old latest-active selection.

The binding handshake:

sequenceDiagram
    participant P1 as daemon Session
    participant P2 as WebShell adapter
    participant P3 as todos resolver
    participant P4 as Approval dialog
    P1->>P1: capture activeTodoPlanRevision from plan update meta
    P1->>P2: exit_plan_mode permission request with qwenTodoApproval (planId, sourceCallId)
    P2->>P3: getActiveTodosForPlanRevision(messages, revision)
    P3-->>P4: bound snapshot todos, or empty for text-only fallback
Loading
Files changed (phase 2 — 12 of 22 shown)

The cumulative diff also carries the 10 phase-1 files tracked in PR 8391 (experimental setting, entry points, i18n, schemas).

File What changed
packages/cli/src/acp-integration/session/Session.ts Captures the latest plan revision from plan-update meta, attaches it to exit-plan permission requests, resets on both Plan-mode entries
packages/cli/src/acp-integration/session/Session.test.ts Asserts the permission frame carries qwenTodoApproval after a plan update
packages/web-shell/client/utils/todos.ts getLatestActiveTodos replaced by strict getActiveTodosForPlanRevision
packages/web-shell/client/utils/todos.test.ts Binding, mismatch rejection, terminal-snapshot preservation
packages/web-shell/client/adapters/transcriptAdapter.ts Extracts todoPlan from permission meta with string validation
packages/web-shell/client/adapters/transcriptAdapter.test.ts Covers the extraction
packages/web-shell/client/adapters/types.ts PermissionRequest gains optional todoPlan
packages/web-shell/client/App.tsx Floating overlay resolves todos only for exit-plan approvals
packages/web-shell/client/App.test.tsx Bound snapshot wins over a newer one when the setting is on
packages/web-shell/client/components/ChatPane.tsx Same resolution for the pane-level approval
packages/web-shell/client/components/ChatPane.test.tsx Covers the pane wiring
docs/design/experimental-session-plan-review.md Design doc wording aligned with the lineage-reuse approach

Testing evidence — this is an unattended CI run, so no PR code was built or executed here; the evidence below is the PR's own CI on the reviewed commit, fetched via API. The author's macOS manual verification in the PR body is a self-reported claim, not independently re-run. All three pull_request workflow runs (Qwen Code CI, SDK Java, Web-shell Visuals) completed successfully; the skipped legs are merge-queue-only by design (ci.yml runs macOS/Windows and the CLI integration job only in the merge queue — Ubuntu is the PR signal).

Check Conclusion
Classify PR ✅ success
precheck-pr / precheck ✅ success
Test (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
Real daemon E2E / Java 11 ✅ success
SDK Java (5 matrix legs) ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped (merge queue only)
Test (windows-latest, Node 22.x) ⏭️ skipped (merge queue only)
Integration Tests (CLI, No Sandbox) ⏭️ skipped (merge queue only)

The unit tests pin the binding logic on both sides, but the end-to-end path (a live daemon session producing the meta and the real WebShell dialog rendering it, including cold-resume replay) rests on the author's macOS run. Sandboxed verification would settle it: @qwen-code /tmux — drive Plan & Review end to end and confirm the approval dialog shows the bound DAG, still shows it after a newer same-plan snapshot lands, and restores it on session resume.

中文说明

代码审查——未发现阻塞问题。我在读 diff 前先独立提出了方案(在 permission _meta 中携带稳定版本身份、精确匹配解析、失败回退文字),PR 采用了相同机制,没有遗漏更简单的路径。已核实:

  • daemon 捕获与重置sendUpdate 从携带既有 qwenTodoPlan / qwenTranscript.planToolCallId meta 的 plan 更新中保存 {planId, sourceCallId},meta 缺失或条目为空时清除。两条进入 Plan Mode 的路径都会重置旧版本——setApprovalMode RPC 切换与 enter_plan_mode 成功执行——版本不会跨计划周期泄漏。
  • 协议面纯增量:只有在版本已知时才给 exit_plan_mode 权限请求附加 qwenTodoApproval;忽略它的客户端保持现有文字审批。
  • 严格解析:WebShell 同时匹配两个 id(首个匹配生效)。无 meta 或不匹配 → 空列表 → ToolApprovalshowsPlanWorkflow 开关回退为纯文字。该回退路径已端到端确认。
  • 有意的行为变化:全部 completed 的绑定快照仍会保留供审阅(旧的 latest-active 过滤会丢弃它)——有测试断言,与 PR 描述一致。
  • 测试钉住了变更Session.test 驱动真实 sendUpdate 捕获路径并断言权限帧携带绑定;App.test 构造同一 planId 下的两个快照,断言对话框渲染绑定的(较早的)快照而非较新的;todos.test 覆盖不匹配拒绝与终态快照保留。

一个非阻塞观察:旧的文字解析 plan 消息(transcriptToMessagesparsePlanTodosrole: 'plan')不再进入审批 DAG——这类 transcript 现在走纯文字审批。这与声明的设计一致(绑定需要结构化版本身份),且该流程是 opt-in,但相对旧的 latest-active 选择确实是行为差异。

测试证据——这是无人值守 CI 运行,此处未构建或执行任何 PR 代码;以上证据是通过 API 获取的该 commit 自身 CI 结果。作者在 PR 正文中的 macOS 手工验证为自述声明,未独立复跑。三个 pull_request 工作流(Qwen Code CI、SDK Java、Web-shell Visuals)全部成功完成;跳过的任务是 merge queue 专属(ci.yml 仅在 merge queue 运行 macOS/Windows 与 CLI 集成任务——Ubuntu 是 PR 阶段信号)。

单元测试在两端钉住了绑定逻辑,但端到端路径(真实 daemon 会话产生 meta、真实 WebShell 对话框渲染、冷启动回放恢复)目前依据作者的 macOS 运行。沙箱验证可以补齐:@qwen-code /tmux——完整走一遍 Plan & Review,确认审批框展示绑定 DAG、同 plan 较新快照出现后仍展示绑定快照、会话恢复后能还原。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Confidence: 4/5 — clean, minimal, well-tested increment of a planned feature; the only gap is that the end-to-end daemon→dialog path is verified by the author's self-reported macOS run plus unit tests, not an independent live run.

Stepping back: this is exactly the change the linked issue asked for, implemented the simplest way that works. My independent proposal before reading the diff was the same mechanism — carry the revision identity in the permission _meta, resolve by exact match, degrade to text — and I found no simpler path the PR missed. ~91 production lines for phase 2, no drive-by edits, no new store or protocol mode, and every piece is exercised by tests that would fail without the change. The two things I'd want a maintainer to keep in mind are both process-level, not code-level: this branch carries #8391's commit (merge #8391 first and this diff shrinks to phase 2), and Session.ts sits in a revert-prone area, which is why the CI evidence and the optional /tmux lane were called out. If I had to maintain this in six months, the explicit {planId, sourceCallId} binding and the strict-match-with-fallback resolver would read clearly. Approving — not 5/5 only because the live end-to-end path hasn't been independently exercised.

CI is fully settled and green on the reviewed commit (macOS/Windows/integration legs are merge-queue-only by design), the fork-refactor guardrail does not apply (feat-type PR), and no maintainer escalation was raised — approval is pinned to the reviewed commit and submitted right after this comment.

中文说明

置信度:4/5——干净、最小化、测试充分的计划内增量;唯一缺口是 daemon 到对话框的端到端路径只有作者自述的 macOS 运行加单元测试佐证,没有独立的真实运行。

整体来看:这正是关联 issue 要求的变更,且用了最简单可行的实现。我在读 diff 前独立提出的方案就是同一机制——版本身份放进 permission _meta、精确匹配解析、降级为文字——没有发现 PR 遗漏的更简路径。阶段 2 约 91 行生产代码,无顺手改动,无新增 store 或协议模式,每个部分都有"去掉改动就会失败"的测试覆盖。需要维护者留意的两点都是流程层面而非代码层面:本分支携带 #8391 的 commit(先合并 #8391,本 PR 的 diff 会收缩到阶段 2);Session.ts 位于 revert 高发区域,因此前面强调了 CI 证据与可选的 /tmux 验证通道。若六个月后维护这段代码,显式的 {planId, sourceCallId} 绑定与"严格匹配+回退"的解析逻辑依然清晰可读。予以批准——未给 5/5 仅因端到端真实路径未被独立演练。

CI 在受审 commit 上已全部完成且为绿色(macOS/Windows/集成任务按设计仅在 merge queue 运行),fork-refactor 护栏不适用(feat 类型 PR),也没有触发维护者升级——批准锚定在受审 commit 上,随本条评论之后提交。

Qwen Code · qwen3.8-max-preview

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

@qwen-code-ci-bot qwen-code-ci-bot left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, looks ready to ship. ✅

chiga0
chiga0 previously approved these changes Aug 3, 2026

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

Phase 1 (blind) + Phase 2 (cross-check) complete.

The revision-binding flow is clean: the daemon captures the latest {planId, sourceCallId} from the plan update metadata, injects it into the exit_plan_mode permission request, and the web-shell resolves the exact Todo snapshot via getActiveTodosForPlanRevision instead of the latest active list. Gating the Workflow UI on experimental.sessionWorkflow keeps the feature opt-in and preserves existing behavior when disabled. Tests cover the metadata injection, adapter extraction, revision matching, and setting gating.

A couple of non-blocking observations:

  • getActiveTodosForPlanRevision returns the matched snapshot regardless of todo status (including fully completed ones). This is consistent with the updated unit tests, but differs slightly from the design doc phrase "nodes remain pending before approval." Consider whether the approval UI should filter or warn on terminal snapshots.
  • After a successful exit_plan_mode, activeTodoPlanRevision is not cleared in the post-execution path (the clear only runs when the new mode is PLAN, i.e., on enter_plan_mode). It is reset on the next setMode/enter_plan_mode/plan-update, so it's not a correctness issue, but clearing it on exit would make the lifecycle more explicit.

No Critical issues. Approving.

@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 — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

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

Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
Comment thread packages/web-shell/client/utils/todos.test.ts Outdated
Comment thread packages/web-shell/client/components/ChatPane.test.tsx
Comment thread packages/web-shell/client/App.tsx
Comment thread packages/web-shell/client/components/ChatEditor.tsx
Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
Comment thread packages/web-shell/client/components/artifacts/SideTaskPanel.tsx

@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 — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

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

Comment thread packages/cli/src/acp-integration/acpAgent.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
Comment thread packages/cli/src/acp-integration/acpAgent.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
Comment thread packages/web-shell/client/App.tsx Outdated
Comment thread packages/web-shell/client/utils/todos.test.ts
Comment thread packages/cli/src/acp-integration/acpAgent.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts
@yiliang114

Copy link
Copy Markdown
Collaborator Author

Addressed the two stale-revision defects found in review: bulk replay no longer restores an old Todo plan revision when the loaded session is already in Plan mode, and rewind now clears any active Todo plan revision after truncating history.

Verified with focused loadSession and rewind/exit_plan_mode tests, targeted ESLint, Prettier, and git diff checks. I intentionally did not add the suggested helper abstractions, duplicate parser extraction, extra logging, or additional mutation-only coverage in this round because they would expand this PR without fixing a current stale-approval bug.

@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 — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

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

Comment thread packages/cli/src/acp-integration/acpAgent.ts Outdated
Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
@yiliang114 yiliang114 added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 3, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 3, 2026

Copy link
Copy Markdown
Collaborator

AutoFix round 6 finishedview run. See this round's report below.

中文说明

AutoFix 第 6 轮已完成 —— 查看运行。本轮报告见下方。

History replay re-sends stale plan updates through Session.sendUpdate,
re-stamping activeTodoPlanRevision from finished plan cycles. Clear the
revision after every replay path (cold replayHistory and live non-bulk
loadSession) so a replayed snapshot can never bind a later exit_plan_mode
approval; reloaded sessions fall back to text-only approval until the next
live todo_write re-establishes the binding. Also drop the bulk-load restore
that could never be read before a plan-mode transition cleared it, and pin
the workflow gates and mode-entry clears with negative tests.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review round — PR #8393

Feedback dispositions

Critical — fixed

  • rc:3705214954 (R2-1 remainder): stale plan revision re-stamped by non-bulk replay. Fixed at both surviving vectors. History replay re-sends stale plan updates through Session.sendUpdate, which re-stamps activeTodoPlanRevision from a finished plan cycle:
    • Session.replayHistory now clears the revision after replay — covers cold non-bulk loads (Web Shell session branch/fork, channels/base bridge, IDE-companion and desktop loads).
    • The live-session non-bulk loadSession path now clears the revision after its sendUpdate loop — covers direct ACP clients.
    • The dead bulk-restore branch was removed (below), so no load path can seed a revision from history.
    • Regression coverage: the parameterized Session approval test gained a history replay arm that replays a real prior-cycle todo_write result record and asserts the next exit_plan_mode approval carries no qwenTodoApproval; a new acpAgent test asserts the live-session load clears the revision. Both were mutation-verified (removing either clear fails the tests).

Suggestions — implemented

  • rc:3705214965: guarded bulk-load restore was dead state. Adopted the reviewer's option (b): the non-plan restore could never be read (approvals only fire in plan mode, and every transition into plan clears the revision), so restoreTodoPlanRevisionFromReplay, its call site, and the false-confidence bulk replay restore test arm were removed. Cold reload now uniformly falls back to text-only approval until the first live todo_write re-establishes the binding.
  • rc:3703300851: plan→plan no-clear guard untested. Extended the acpAgent sessionApprovalMode ext-method test with a plan→plan re-selection asserting the clear is NOT re-invoked (the Web Shell /plan path), and added Session setMode tests for both directions (default→plan clears, plan→plan preserves). Mutation-verified.
  • rc:3703300858: post-enter_plan_mode clear untested. New Session test: capture a revision, run an enter_plan_mode execution that switches DEFAULT→PLAN, assert the revision is dropped.
  • rc:3703300828: three copies of the exit-plan wire-name check. Added shared isExitPlanModeToolName beside isTodoWriteToolName in web-shell utils/todos.ts and used it at all three sites (App, ChatPane, ToolApproval), preserving each site's additional toolKind === 'switch_mode' condition.
  • rc:3703300837: text-only fallback only asserted against an empty transcript. Added a non-empty-transcript + undefined revision → [] assertion. Mutation-verified against the pre-PR latest-wins behavior.
  • rc:3701445192: ChatPane flag-off negative test. Added: with the flag off, the exit-plan approval receives no plan todos. Mutation-verified.
  • rc:3701445195: App Tasks-dialog flag-off negative test. Added: floating todos + agent tool seeded, setting unset — asserts planTodos and agentTools are empty and the dialog title stays tasks.title (the DialogShell test mock now exposes the title). Mutation-verified.
  • rc:3701445199: ChatEditor mode-rename coverage. Added a dropdown test (plan entry renamed, other entries keep their labels) and a mode-chip test asserted through the always-rendered toolbar measure replicas.
  • rc:3701445203: ApprovalModeDialog "renames only the plan entry". Now also asserts the default and yolo rows keep their labels with the flag on.
  • rc:3701445209: SideTaskPanel pass-through. Added: a connected side-task pane forwards sessionWorkflowEnabled to its ChatPane. Mutation-verified.

Verified previously fixed (no change needed)

  • rc:3701445189 (round-1 Critical): verified fixed in 47cca94 — the ext-method path clears the revision on plan entry, and this round's new plan→plan tests pin the guard on both the ext-method and setMode paths.
  • rc:3701445190: verified fixed in 9c5b6fb — the multi-revision test binds the approval to the second snapshot's call id, so planId-only matching fails.
  • rc:3701445207: verified — the parameterized Session test asserts the failed-replacement and mode-transition fallbacks (no qwenTodoApproval), and the daemon entry tests assert the clears are invoked. Extended this round with the replay / setMode / enter_plan_mode / live-load coverage above.
  • rc:3703300873: verified fixed in 7994dabrewindToTurn clears the revision, asserted by the rewind test.
  • rc:3703300804 (round-2 Critical): fully resolved — the bulk path by the earlier guard commit, the non-bulk paths by this round's fix.

Declined (replied on their threads, left open)

  • rc:3703300813 / rc:3703300818 — consolidate the mode-entry clears into one choke point (Config.setApprovalMode transition or a shared Session method). Declined for this PR: every clear site is now individually pinned by tests added this round (setMode both directions, ext-method default→plan + plan→plan, settings reload, post-enter_plan_mode execution, history replay), so a dropped site turns a test red. Routing the policy through Config.setApprovalMode is a cross-package architectural change into packages/core that exceeds a bugfix PR's scope; reasonable follow-up refactor.
  • rc:3703300823 — shared plan-identity reader in acp-bridge. Declined: it would change three packages' public API surface, and the two readers intentionally answer missing identity differently (daemon clears the revision → text-only fallback; SDK normalizer synthesizes DAEMON_PLAN_TOOL_CALL_ID). Unifying those semantics is a design decision beyond this PR; follow-up material.
  • rc:3703300864 — debug logging for the silent text-only fallback. Declined: failing closed to text-only approval is the chain's designed behavior; adding logging across two packages adds surface without changing behavior. Worth revisiting in a diagnostics pass if field debugging demands it.

Notes

  • --conflict false: no merge of origin/main performed; no conflict work.
  • The issue-level web-shell-visuals bot comment is advisory: this round's changes are flag-gated logic and tests with no visual delta at default settings. Adding a flag-on scenario to screenshots.spec.ts is reasonable follow-up, not part of this round's findings.

Verification

Commands actually run (after the final state of the change set):

  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0)
  • npx prettier --check on all 14 changed files — passed
  • npx vitest run src/acp-integration (packages/cli) — 26 files, 1193 tests passed
  • npx vitest run (packages/web-shell, full package) — 164 files, 2741 tests passed
  • Mutation checks (temporary mutants, each reverted afterwards): dropping the replayHistory clear, the live-load clear, the ext-method plan→plan guard, the ChatPane workflow gate, the App tasks-dialog title gate, the !revision → [] fallback, or the SideTaskPanel pass-through each failed the corresponding new test.
  • Integration suites were not run: the changed behavior (ACP daemon session load/replay and the web-shell client) is exercised by the acp-integration Vitest suites above, not by the bundled-CLI integration harness.
中文说明

Autofix 审查轮次 — PR #8393

反馈处理结论

Critical — 已修复

  • rc:3705214954(R2-1 遗留):非 bulk 回放会重新写入过期的计划版本(revision)。 两条幸存路径均已修复。历史回放会通过 Session.sendUpdate 重发过期的 plan update,从而把已结束计划周期的 revision 重新写入 activeTodoPlanRevision
    • Session.replayHistory 现在在回放结束后清空 revision —— 覆盖冷启动非 bulk 加载(Web Shell 会话分支/fork、channels/base bridge、IDE companion 与 desktop 加载)。
    • 存活会话的非 bulk loadSession 路径现在在其 sendUpdate 循环之后清空 revision —— 覆盖直连 ACP 客户端。
    • 已移除死状态的 bulk 恢复分支(见下),任何加载路径都不会再从历史中种入 revision。
    • 回归覆盖:参数化的 Session 审批测试新增 history replay 分支 —— 回放一条真实的上一周期 todo_write 结果记录,断言下一次 exit_plan_mode 审批不携带 qwenTodoApproval;新增 acpAgent 测试断言存活会话加载会清空 revision。两者均经过变异验证(删除任一清空都会导致测试失败)。

Suggestion — 已实现

  • rc:3705214965:带守卫的 bulk 加载恢复是死状态。 采纳评审者的方案 (b):非 plan 模式下的恢复永远不可能被读取(审批只在 plan 模式触发,而所有进入 plan 的切换都会清空 revision),因此移除了 restoreTodoPlanRevisionFromReplay、其调用点、以及给出虚假信心的 bulk replay restore 测试分支。冷重载现在统一降级为文字审批,直到第一个实时 todo_write 重新建立绑定。
  • rc:3703300851:plan→plan 不清空守卫无测试。 扩展 acpAgent sessionApprovalMode ext-method 测试:plan→plan 重选时断言清空不会被再次调用(即 Web Shell /plan 路径);并为 Session setMode 补充双向测试(default→plan 清空、plan→plan 保留)。已变异验证。
  • rc:3703300858:enter_plan_mode 执行后的清空无测试。 新增 Session 测试:先捕获 revision,执行一次使模式 DEFAULT→PLAN 的 enter_plan_mode,断言 revision 被清空。
  • rc:3703300828:exit-plan 线上名称判断存在三份拷贝。 在 web-shell utils/todos.tsisTodoWriteToolName 旁新增共享谓词 isExitPlanModeToolName,并在全部三处(App、ChatPane、ToolApproval)使用,同时保留各处额外的 toolKind === 'switch_mode' 条件。
  • rc:3703300837:文字审批降级只用空 transcript 断言过。 补充"非空 transcript + undefined revision → []"断言,并已对 PR 前的"取最新快照"行为做变异验证。
  • rc:3701445192:ChatPane flag 关闭方向负向测试。 已补充:flag 关闭时 exit-plan 审批不接收任何 plan todos。已变异验证。
  • rc:3701445195:App 任务对话框 flag 关闭方向负向测试。 已补充:构造浮动 todos 与 agent 工具、不设置该 setting,断言 planTodosagentTools 为空且对话框标题保持 tasks.title(DialogShell 测试 mock 现在暴露标题)。已变异验证。
  • rc:3701445199:ChatEditor 模式改名覆盖。 新增下拉测试(plan 项改名、其余项保持原文案)与模式 chip 测试(通过始终渲染的 toolbar 测量副本断言)。
  • rc:3701445203:ApprovalModeDialog "只改 plan 一项"。 现在同时断言 flag 开启时 default 与 yolo 行保持原文案。
  • rc:3701445209:SideTaskPanel 透传。 已补充:连接会话的侧边任务窗格会把 sessionWorkflowEnabled 透传给其 ChatPane。已变异验证。

已核实此前修复(无需改动)

  • rc:3701445189(第 1 轮 Critical): 已核实 47cca94 修复 —— ext-method 路径在进入 plan 时清空 revision,本轮新增的 plan→plan 测试在 ext-method 与 setMode 两条路径上钉住了该守卫。
  • rc:3701445190: 已核实 9c5b6fb 修复 —— 多版本测试把审批绑定到第二个快照的 call id,只按 planId 匹配会失败。
  • rc:3701445207: 已核实 —— 参数化 Session 测试断言了替换失败与模式切换两种降级(不携带 qwenTodoApproval),daemon 入口测试断言清空被调用。本轮又补充了回放 / setMode / enter_plan_mode / 存活加载覆盖。
  • rc:3703300873: 已核实 7994dab 修复 —— rewindToTurn 清空 revision,rewind 测试有断言。
  • rc:3703300804(第 2 轮 Critical): 已完全解决 —— bulk 路径由先前守卫提交覆盖,非 bulk 路径由本轮修复覆盖。

已拒绝(已在对应线程回复,线程保持打开)

  • rc:3703300813 / rc:3703300818 —— 把各模式入口的清空收敛到单一收口点(Config.setApprovalMode 切换或共享 Session 方法)。 本 PR 拒绝:本轮新增测试已逐一钉住每个清空点(setMode 双向、ext-method default→plan + plan→plan、settings 热加载、enter_plan_mode 执行后、历史回放),删除任何一处都会有测试变红。把策略收口到 Config.setApprovalMode 是跨包改动 packages/core 的架构级变更,超出 bugfix PR 的范围;适合作为后续重构。
  • rc:3703300823 —— 在 acp-bridge 中共享计划身份读取方。 拒绝:这会改动三个包的公开 API,且两个读取方对"缺失身份"的处理是有意不同的(daemon 清空 revision → 文字降级;SDK normalizer 回退到合成的 DAEMON_PLAN_TOOL_CALL_ID)。统一这些语义属于设计决策,超出本 PR 范围;留作后续。
  • rc:3703300864 —— 为静默文字降级加调试日志。 拒绝:失败关闭降级为文字审批是这条链路的设计行为;跨两个包加日志只增加面积、不改变行为。若现场排查需要,可在专门的诊断轮次中再考虑。

说明

  • --conflict false:未合并 origin/main,无冲突处理。
  • issue 级别的 web-shell 视觉预览机器人评论属于提示性质:本轮改动是 flag 门控的逻辑与测试,在默认设置下没有视觉差异。为 screenshots.spec.ts 增加 flag 开启的场景属于合理的后续工作,不属于本轮发现项。

验证

实际执行的命令(针对改动集的最终状态):

  • npm run build — 通过(exit 0)
  • npm run typecheck — 通过(exit 0)
  • npm run lint — 通过(exit 0)
  • 对全部 14 个改动文件执行 npx prettier --check — 通过
  • npx vitest run src/acp-integration(packages/cli)— 26 个文件、1193 个测试通过
  • npx vitest run(packages/web-shell 全包)— 164 个文件、2741 个测试通过
  • 变异检查(临时变异、随后全部还原):删除 replayHistory 清空、存活加载清空、ext-method plan→plan 守卫、ChatPane 工作流门控、App 任务对话框标题门控、!revision → [] 降级、或 SideTaskPanel 透传,均会使对应的新增测试失败。
  • 未运行集成测试套件:被改动的行为(ACP daemon 会话加载/回放与 web-shell 客户端)由上述 acp-integration Vitest 套件覆盖,而非打包 CLI 集成测试框架所执行的路径。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 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 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.

Reviewed. Suggestions are inline. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

已审查。 建议见行内评论。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

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

Comment thread packages/web-shell/client/components/ChatPane.test.tsx
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Address-review summary — PR #8393 (round 4)

Feedback points and dispositions

  • [rc:3707765792] Suggestion — ChatPane.test.tsx:1008: revision-binding test cannot catch a latest-wins regressionAddressed.
    The finding is correct and was verified against the code: the fixture for
    passes this pane workflow to its exit-plan approval contained a single
    todo_write snapshot (todo-call-1) that was simultaneously the pinned
    revision (sourceCallId: 'todo-call-1') and the latest snapshot for
    plan-1. If the ChatPane planTodos memo (ChatPane.tsx) regressed from
    getActiveTodosForPlanRevision back to latest-wins semantics (the removed
    getLatestActiveTodos), the pinned ["prepare","ship"] assertion would
    still pass. ChatPane renders its own ToolApproval for split panes and
    restored side tasks, so this guards a real, separate surface from App.tsx.

    Change: added a second, newer tool_group snapshot
    (plan-update-newer / todo-call-2, single pending entry ship-v2) under
    the same plan-1, mirroring the discriminating fixture already used in
    App.test.tsx. The pinned approval still references todo-call-1. Test-only
    change; no production code touched.

    Mutation probe: temporarily mutated getActiveTodosForPlanRevision
    back to latest-wins semantics (last snapshot wins, filtered by active
    todos). With the strengthened fixture the test now fails
    (Expected: ["prepare","ship"], Received: ["ship-v2"]); the mutation was
    reverted and the test passes again with the real implementation. Before the
    fixture change the same mutant stayed green on this test, matching the
    reviewer's report.

  • Review-level note ("Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally")No action required. This is an informational note about reviewer coverage, not a finding. This round's change is a web-shell test fixture only; the touched behavior is not exercised through the bundled CLI or the integration harness, so no integration run is required by the trusted-command rules.

Conflict notes

--conflict false: no merge of origin/main performed; branch applied cleanly.

Verification

Commands actually run this round (all in the PR checkout):

  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint (full: eslint . --ext .ts,.tsx + integration-tests) — passed (exit 0)
  • npx vitest run client/components/ChatPane.test.tsx (packages/web-shell, touched) — 66 passed, 0 failed
  • Mutation probe (temporary latest-wins mutant in todos.ts): target test failed as expected; mutant reverted, tree restored, test green again
  • Integration tests / npm run bundle — not run (change is a web-shell test fixture only, not exercised through the bundled CLI or integration harness)
  • npm run generate:settings-schema — not needed (no settings source changed)

Commit: b05227c73 test(web-shell): pin older plan revision in ChatPane approval test (#8393) — one file, +21 lines.

中文说明

处理审查意见总结 — PR #8393(第 4 轮)

反馈点及处理结果

  • [rc:3707765792] 建议 — ChatPane.test.tsx:1008:revision 绑定测试无法捕获 latest-wins(最新快照优先)回退已处理。
    该发现正确,且已对照代码核实:passes this pane workflow to its exit-plan approval 的 fixture 只包含一个 todo_write 快照(todo-call-1),它既是被锁定的 revision(sourceCallId: 'todo-call-1'),又是 plan-1 下最新的快照。如果 ChatPane 的 planTodos memo(ChatPane.tsx)从 getActiveTodosForPlanRevision 退回到 latest-wins 语义(即已删除的 getLatestActiveTodos),锁定的 ["prepare","ship"] 断言依然会通过。ChatPane 会为分屏和恢复的 side task 渲染自己的 ToolApproval,因此这守护的是一个与 App.tsx 相互独立且真实的渲染面。

    改动: 在同一 plan-1 下新增了第二个更新的 tool_group 快照(plan-update-newer / todo-call-2,单个 pending 条目 ship-v2),与 App.test.tsx 中已有的具有区分力的 fixture 保持一致。被锁定的审批仍然引用 todo-call-1。仅改动测试,未触碰任何生产代码。

    变异探针: 临时将 getActiveTodosForPlanRevision 变异回 latest-wins 语义(最后一个快照优先,并按 active todos 过滤)。在使用强化后的 fixture 时该测试失败(Expected: ["prepare","ship"]Received: ["ship-v2"]);变异体已还原,真实实现下测试恢复通过。在 fixture 改动之前,同一变异体在该测试下保持绿色,与审查者的报告一致。

  • 审查级备注("Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally")无需处理。 这是关于审查覆盖范围的信息性说明,并非发现项。本轮改动仅为 web-shell 测试 fixture;被触动的行为并不通过打包后的 CLI 或集成测试框架运行,因此按可信命令规则无需运行集成测试。

冲突说明

--conflict false:未合并 origin/main,分支可干净应用。

验证

本轮实际运行的命令(均在 PR 检出目录中):

  • npm run build — 通过(exit 0)
  • npm run typecheck — 通过(exit 0)
  • npm run lint(完整:eslint . --ext .ts,.tsx + integration-tests)— 通过(exit 0)
  • npx vitest run client/components/ChatPane.test.tsx(packages/web-shell,被触动包)— 66 通过,0 失败
  • 变异探针(todos.ts 中的临时 latest-wins 变异体):目标测试按预期失败;变异体已还原,工作树恢复,测试重新变绿
  • 集成测试 / npm run bundle — 未运行(改动仅为 web-shell 测试 fixture,不通过打包后的 CLI 或集成测试框架运行)
  • npm run generate:settings-schema — 无需运行(未改动任何 settings 源)

提交:b05227c73 test(web-shell): pin older plan revision in ChatPane approval test (#8393) — 1 个文件,+21 行。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 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 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.

Reviewed. Suggestions are inline. 1 Suggestion-level finding(s) could not be anchored to a changed line and were dropped; nothing further to act on here. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

已审查。 建议见行内评论。 1 条建议级发现无法锚定到改动行,已丢弃;此处无需进一步处理。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

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

Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
…nLM#8393)

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

Copy link
Copy Markdown
Collaborator

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

Autofix review round summary — PR #8393

Feedback dispositions

  • [rc:3708953525] #captureTodoPlanRevision rejection branches have no test (Suggestion, packages/cli/src/acp-integration/session/Session.ts:4466) — Resolved in code. Added two variants to the existing keeps exit_plan_mode approval revision correct after %s it.each in Session.test.ts:

    • empty plan update — a successful sendUpdate of a plan update with valid qwenTodoPlan/qwenTranscript meta but entries: [] (the production shape emitted when a todo_write result clears the list).
    • plan update without identity — a successful sendUpdate of a plan update with entries but no _meta identity at all.

    Both assert that the subsequent exit_plan_mode requestPermission call carries no qwenTodoApproval (expect.not.objectContaining({ qwenTodoApproval: expect.anything() })), pinning the "successful-but-unbindable plan update ⇒ text-only approval" contract for the live path, which previously only covered replay/cleared/failed-send. Both rejection guards (entries.length > 0 and the typeof planId/sourceCallId === 'string' checks) were proven load-bearing via temporary mutation checks: flipping > to >= fails only the empty-entries variant, and dropping the typeof guards fails only the missing-identity variant. Session.ts was restored afterward; the committed diff is test-only.

No other actionable feedback this round. No conflict (--conflict false); origin/main was not merged. No declined or escalated items.

Changes

  • packages/cli/src/acp-integration/session/Session.test.ts — +9 lines: two new it.each rows and their two sendUpdate branches (commit d0ed76fa0).

Verification

  • npx vitest run src/acp-integration/session/Session.test.ts -t "keeps exit_plan_mode approval revision correct" (packages/cli) — 6 passed (4 existing + 2 new variants)
  • Mutation check: entries.length > 0>= 0 in Session.ts — focused test failed as expected (1 failed | 5 passed); restored
  • Mutation check: dropped typeof planId/sourceCallId === 'string' guards in Session.ts — focused test failed as expected (1 failed | 5 passed); restored; git status confirmed only the test file differs from the commit
  • npx vitest run src/acp-integration/session/Session.test.ts (packages/cli, full file) — 492 passed (492)
  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0)
  • npx prettier --check packages/cli/src/acp-integration/session/Session.test.ts — passed
  • Integration tests after npm run bundle — not run: the change adds unit tests against Session with mocked ACP client and does not touch behavior only exercised through the bundled CLI or integration harness
  • npm run generate:settings-schema — not applicable: no settings source changed
中文说明

Autofix 审查轮次总结 — PR #8393

反馈处理

  • [rc:3708953525] #captureTodoPlanRevision 的拒绝分支没有测试(建议级,packages/cli/src/acp-integration/session/Session.ts:4466)— 已在代码中解决。Session.test.ts 现有的 keeps exit_plan_mode approval revision correct after %s it.each 中新增两个变体:

    • empty plan update —— 成功 sendUpdate 一条携带有效 qwenTodoPlan/qwenTranscript meta 但 entries: [] 的 plan update(即 todo_write 结果清空列表时生产环境发出的形态)。
    • plan update without identity —— 成功 sendUpdate 一条有 entries 但完全没有 _meta 身份信息的 plan update。

    两者均断言随后的 exit_plan_mode requestPermission 调用携带 qwenTodoApprovalexpect.not.objectContaining({ qwenTodoApproval: expect.anything() })),从而在 live 路径上钉住「成功送达但无法绑定的 plan update ⇒ 降级为文字审批」这一契约 —— 此前该契约只覆盖 replay/cleared/failed-send 路径。两个拒绝守卫(entries.length > 0typeof planId/sourceCallId === 'string' 检查)均通过临时变异测试证明有效:将 > 翻转为 >= 只会让 empty-entries 变体失败;删除 typeof 守卫只会让 missing-identity 变体失败。随后 Session.ts 已还原;提交的 diff 仅含测试。

本轮无其他可执行反馈。无冲突(--conflict false),未合并 origin/main。无拒绝或升级项。

改动

  • packages/cli/src/acp-integration/session/Session.test.ts —— +9 行:两个新的 it.each 行及其对应的两个 sendUpdate 分支(提交 d0ed76fa0)。

验证

  • npx vitest run src/acp-integration/session/Session.test.ts -t "keeps exit_plan_mode approval revision correct"(packages/cli)— 6 通过(4 个既有 + 2 个新变体)
  • 变异检查:将 Session.tsentries.length > 0 改为 >= 0 — 聚焦测试按预期失败(1 失败 | 5 通过);已还原
  • 变异检查:删除 Session.tstypeof planId/sourceCallId === 'string' 守卫 — 聚焦测试按预期失败(1 失败 | 5 通过);已还原;git status 确认相对该提交只有测试文件有差异
  • npx vitest run src/acp-integration/session/Session.test.ts(packages/cli,全文件)— 492 通过(492)
  • npm run build — 通过(exit 0)
  • npm run typecheck — 通过(exit 0)
  • npm run lint — 通过(exit 0)
  • npx prettier --check packages/cli/src/acp-integration/session/Session.test.ts — 通过
  • npm run bundle 后的集成测试 — 未运行:本次改动是针对 Session 并使用 mock ACP client 的单元测试新增,不涉及仅通过捆绑 CLI 或集成测试框架才能验证的行为
  • npm run generate:settings-schema — 不适用:未改动任何 settings 源文件

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 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-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind main, so it merged current main in via update-branch and will retry on the next scan. A stale base (a dependency or symbol main already changed) can fail the build without being the fix's fault; if it still fails once current, it hands off to a human.

What I found before stopping:

Blocked: failing ubuntu-latest / Java 11 check is not attributable to this PR's diff

Blocker

The only unresolved feedback on PR #8393 is the failed check ubuntu-latest / Java 11
(workflow "SDK Java", run 30877410569, job 91891563064). The failing step is
"Run Java SDK tests" (mvn --batch-mode --no-transfer-progress clean test), which
exited 1 after ~23s on the self-hosted runner ecs-qwen-runner-sg-22. No code change
in this PR can have caused it, and the exact failing test cannot be identified without
authenticated log access, which this workflow does not have.

Evidence the failure is unrelated to the PR diff

  1. The failing step runs pure Maven/Java unit tests. The job has no Node build steps
    before it, and nothing in packages/sdk-java/qwencode/src/test references Node,
    TypeScript files, or anything outside the Java module (verified by search).
  2. The PR contains zero Java changes. The only commit added since the branch's last
    fully green CI run (30860224703 at head b05227c73) is d0ed76fa0, which adds
    9 lines to packages/cli/src/acp-integration/session/Session.test.ts — a vitest
    file that never enters the Java build.
  3. The identical branch code passed the full SDK Java workflow 8 consecutive times
    before this run, most recently 2026-08-03T22:51Z.
  4. In the same run, ubuntu-latest / Java 17, ubuntu-latest / Java 21,
    macos-latest / Java 21, and windows-latest / Java 21 all passed on the same
    sources (the pom pins `m

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


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

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Review follow-up: I rechecked the four remaining threads against the current head. The two replay/bulk-restore findings no longer apply after dc61cc1, which clears revisions after the non-bulk replay paths and removes the dead bulk restore. The ChatPane and Session test suggestions were already addressed by b05227c and d0ed76f. I resolved the corresponding threads; no additional code change is needed for these comments.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Autofix review round — no code changes

The only feedback this round was a failed check; there were no review comments or inline findings. After an evidence-based investigation, no code change is warranted for this PR. Details below.

Failed check investigated

SDK Java / ubuntu-latest / Java 11 — FAILURE.

Using the GitHub Actions job-steps API, the failing step was identified precisely:

  • Failed step: Run Java SDK tests (mvn --batch-mode --no-transfer-progress clean test), which ran ~23 seconds before exiting non-zero. Run Java SDK Checkstyle and Build release artifacts were skipped.
  • This job failed on both runs of this PR (04:22 UTC and 05:27 UTC), with the same failing step and similar timing.

Why this is not caused by this PR

  1. This PR touches nothing the failing step can see. The diff is limited to a design doc, packages/cli/src/acp-integration/** (TypeScript), and packages/web-shell/**. It makes zero changes to packages/sdk-java/, any pom.xml, or any Maven input (verified by diff).
  2. The Java unit suite cannot observe the TypeScript diff. The Java tests never spawn the Node CLI or reference the JS bundle, and the matrix job does not even install Node.js.
  3. The identical Java tree is green elsewhere. The same job passed on main at 03:48 UTC the same day (21s test phase), and other PRs passed it in the same time window.
  4. Every related job passed on this PR, twice: ubuntu-latest / Java 17, ubuntu-latest / Java 21, macos-latest / Java 21, windows-latest / Java 21, and Real daemon E2E / Java 11 — the last one compiles the same Java sources with Maven on Java 11 and exercises the real daemon built from this PR's TypeScript changes end-to-end.

Why no fix was attempted

  • GitHub Actions job logs require authenticated access, which this workflow does not have, so the specific failing test cannot be identified from the available public API data (annotations carry only the generic "Process completed with exit code 1.").
  • This environment has no Java/Maven toolchain, so no Java-side change could be reproduced or verified here.
  • The suite contains wall-clock-bound assertions (e.g. elapsed-time caps in the daemon session client tests), which are plausible flake candidates on the self-hosted ECS pool — but hardening any specific test without evidence of which test failed would be a guess, and per project policy (Simplicity First; diagnose from evidence, not guesses) speculative test edits are not acceptable.

Conclusion

The failure is a flaky or environment-specific unit-test failure in the Java SDK suite, uncorrelated with this PR's content. The appropriate remedy is re-running the failed job (or relying on the repository's flaky-rerun automation); no source change is made by this round.

中文说明

Autofix 审查轮次 —— 无代码改动

本轮唯一的反馈是一个失败的 CI 检查;没有任何审查评论或行内意见。经过基于证据的排查,本 PR 无需任何代码改动。详情如下。

排查的失败检查

SDK Java / ubuntu-latest / Java 11 —— 失败(FAILURE)。

通过 GitHub Actions 的 job-steps API 精确定位了失败步骤:

  • 失败步骤:Run Java SDK tests(即 mvn --batch-mode --no-transfer-progress clean test),运行约 23 秒后以非零状态退出。Run Java SDK CheckstyleBuild release artifacts 均被跳过。
  • 本 PR 的两次运行(04:22 UTC 与 05:27 UTC)该 job 均失败,失败步骤相同、耗时相近。

为什么该失败与本 PR 无关

  1. 本 PR 未触碰该失败步骤所能看到的任何内容。 差异仅限于一份设计文档、packages/cli/src/acp-integration/**(TypeScript)与 packages/web-shell/**;对 packages/sdk-java/、任何 pom.xml 或任何 Maven 输入均无任何改动(已用 diff 验证)。
  2. Java 单元测试无法感知 TypeScript 差异。 Java 测试从不启动 Node CLI,也不引用 JS bundle;该矩阵 job 甚至不安装 Node.js。
  3. 相同的 Java 代码树在其他位置是绿色的。 同一天 03:48 UTC,main 分支上同一 job 通过(测试阶段 21 秒);同一时间窗口内其他 PR 也通过了该 job。
  4. 本 PR 上所有相关 job 均两次通过:ubuntu-latest / Java 17ubuntu-latest / Java 21macos-latest / Java 21windows-latest / Java 21,以及 Real daemon E2E / Java 11 —— 最后一个 job 会用 Maven 在 Java 11 下编译相同的 Java 源码,并对由本 PR 的 TypeScript 改动构建出的真实守护进程做端到端验证。

为什么没有尝试修复

  • GitHub Actions 的 job 日志需要鉴权访问,而本工作流没有该权限,因此无法从可用的公开 API 数据中确定具体失败的测试(注解仅包含通用的 "Process completed with exit code 1.")。
  • 本环境没有 Java/Maven 工具链,任何 Java 侧改动都无法在此复现或验证。
  • 该测试套件包含基于墙钟的断言(例如 daemon session client 测试中的耗时上限断言),这些是自托管 ECS 资源池上合理的 flake 候选 —— 但在没有证据表明是哪个测试失败的情况下去加固任何具体测试都属于猜测;按照项目准则(简单优先;基于证据而非猜测进行诊断),推测性的测试修改不可接受。

结论

该失败是 Java SDK 套件中一次 flaky 或环境相关的单元测试失败,与本 PR 的内容无关。合适的处理方式是重跑失败的 job(或依赖仓库已有的 flaky 自动重跑机制);本轮不做任何源码改动。

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


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

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed. Suggestions are inline. Not reviewed: the linked-issue fidelity pass, the line-by-line correctness pass, the security pass, the reuse and duplication pass, the altitude and abstraction pass, the consistency and clarity pass, the performance pass, the test-coverage pass, the open-ended audit (attacker mindset), the open-ended audit (oncall mindset), the open-ended audit (maintainer mindset), the removed-behavior audit, the cross-file consistency pass, the build-and-test check — its prompt was built, but no agent on record was launched with it.

中文说明

已审查。 建议见行内评论。 未审查:关联 issue 一致性检查、逐行正确性检查、安全检查、复用与重复代码检查、修复层次与抽象合理性检查、一致性与可读性检查、性能检查、测试覆盖检查、开放式审计(攻击者视角)、开放式审计(值班排障视角)、开放式审计(后续维护者视角)、删除行为审计、跨文件一致性检查、构建与测试验证——它的 prompt 已构建,但没有任何 agent 有记录用它启动过。

— qwen3.7-max via Qwen Code /review (v0.21.4)

Comment thread packages/web-shell/client/adapters/transcriptAdapter.ts
chiga0
chiga0 previously approved these changes Aug 4, 2026

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

Code Review Overview (AI Generated)

PR: #8393 feat(web-shell): bind plan approval to its Todo revision
Type: New feature / bug fix(实验性 Session Workflow)
Change size: +793/-142 across 18 files
HEAD reviewed: 8ecb774517a216c9d67a630de633a0deabe8e151

Findings Summary

  • Critical: 0
  • Major: 0
  • Minor: 0
  • Nit / Suggestion: 0 条新的独立发现;现有 reviewer 的 Suggestion 已在当前 HEAD 处理或明确 declined

Key Observations

本 PR 将 exit_plan_mode 审批绑定到 daemon 通过 _meta.qwenTodoApproval 携带的精确 {planId, sourceCallId} 快照,避免审批框误用最新的 active Todo。核心实现分布在 Session.tsacpAgent.tsweb-shell/client/utils/todos.ts。我在 HEAD 独立核查了 revision 生命周期与匹配逻辑,确认之前的 Critical 泄漏路径均已得到清理。

Cross-Validation

Finding Other Reviewer My Assessment
Plan mode 实际使用路径上的 stale plan-revision 泄漏(进入 plan mode 时未清理) qwen-code-ci-bot (Critical) 已确认修复 — Session.ts:6515 在切换至 plan mode 后清理 activeTodoPlanRevision
Session cold-load / history replay 会重新植入过期 revision qwen-code-ci-bot (Critical) 已确认修复 — acpAgent.ts:4546 在非 bulk loadSession replay 结束后清理;Session.ts:2216/2257replayHistory 后清理
rewindToTurn 截断历史未使 revision 失效 qwen-code-ci-bot (Suggestion) 已确认修复 — Session.ts:4464 在 rewind 后清理
requestPermissionexit_plan_mode 注入 _meta.qwenTodoApproval 缺少 revision 有效性校验 qwen-code-ci-bot (Suggestion) 已确认修复 — Session.ts:8259 仅在 isExitPlanModeTool && this.activeTodoPlanRevision 时注入
getLatestActiveTodos 误用最新快照而非审批对应快照 qwen-code-ci-bot (Suggestion) 已确认修复 — 已替换为 getActiveTodosForPlanRevision(messages, revision),按 callId + planId 精确匹配
sessionApprovalMode ext-method 与 settings-reload 切换至 plan mode 未清理 revision qwen-code-ci-bot (Suggestion) 已确认修复 — acpAgent.ts:9168/10648 在切换时清理
exit_plan_mode 字符串比较多处重复 qwen-code-ci-bot / doudouOUC (Suggestion) 已确认 todos.ts 提供 isExitPlanModeToolName 共享辅助函数
getString 等辅助函数重复 doudouOUC (Suggestion) 作者已在 inline reply 中说明超出本 PR 范围,建议后续统一提取
Integration Tests (CLI, No Sandbox) 未在 CI 中运行 qwen-code-ci-bot 作者已说明该 suite 被 skip;非代码问题,建议后续在 CI matrix 中补齐

Additional Audit Coverage

  • 在 HEAD 直接读取 Session.tsacpAgent.tstodos.ts,列出 activeTodoPlanRevision 所有读写位置,确认清理路径覆盖:plan mode 进入、history replay、rewind、非 bulk loadSession replay、sessionApprovalMode 切换、settings-reload 切换、enter_plan_mode 执行后。
  • 核对 getActiveTodosForPlanRevision 的匹配条件为 tool.callId === revision.sourceCallId && getTodoPlanId(tool) === revision.planId,实现 PR 描述的精确绑定。
  • 确认 text-only fallback:当 revision 缺失时返回 [],与关闭实验性 Session Workflow 的行为一致。

Final Verdict

APPROVE。审批 revision 绑定逻辑在 HEAD 已覆盖主要生命周期清理路径,测试也针对 stale revision 场景进行了 negative 覆盖。剩余的 Suggestion 级别问题(如辅助函数提取、特定分支的测试覆盖)作者已处理或明确标记为后续跟进,不构成合并阻塞。


This review was generated by QoderWork AI

@yiliang114

Copy link
Copy Markdown
Collaborator Author

Local verification at 8ecb774517a (isolated worktree on top of latest main):

Check Result
npm run lint
npm run typecheck (all workspaces)
vitest run src/acp-integration (packages/cli) ✅ 26 files / 1221 tests
npm test (packages/web-shell) ✅ 166 files / 2780 tests
npm run build + npm run bundle
npm run test:e2e:smoke (Playwright) 25/26 ✅ — see note

The one smoke failure (grows long text to the responsive composer cap at 600px) passes when re-run in isolation and is green in this PR's CI. The full-suite run showed ECONNREFUSED 127.0.0.1:4170 vite proxy errors under parallel load, and the test covers composer sizing — unrelated to this change — so I read it as a local flake, not a regression.

UI evidence for the revision-bound approval. The CI visuals artifact only captures the generic prompt-stream / model-switch videos, so I added a throwaway Playwright capture on this branch using the repo's mock-daemon harness (real browser, real WebShell client and SSE pipeline; daemon mocked at the HTTP boundary):

  1. Session Workflow on + matching qwenTodoApproval binding → the structured Todo DAG renders inside the approval panel, with statuses and dependency edges:

approval panel with bound Todo DAG

  1. Session Workflow on + stale/mismatched binding → safely falls back to the text-only approval, no DAG:

approval panel with unbound revision

  1. Session Workflow off → text-only approval, unchanged:

approval panel with workflow off

(Full-page view of case 1: 2-full-view-workflow-on-bound.png)

The binding behaves as described in the PR: the DAG shows only when both planId and sourceCallId match the transcript snapshot, and the fallback paths are unchanged. Nothing blocking from my side.

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

Reviewed. Suggestions are inline. Not reviewed: build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally.

中文说明

已审查。 建议见行内评论。 未审查:build-and-test — Integration Tests (CLI, No Sandbox) was skipped in CI and its suite did not run locally。

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

Comment thread packages/cli/src/acp-integration/acpAgent.test.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
restoreHistory was the one history-resetting path that kept
activeTodoPlanRevision, so a restored snapshot could let a stale
revision bind the next exit_plan_mode approval. Clear it like the
sibling reset paths, pin the behavior with a test, and pin the
live-load clear ordering after the replayed updates.
@wenshao

wenshao commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Review: bind plan approval to its Todo revision

Reviewed at 8ecb7745. Scope: daemon stamps {planId, sourceCallId} into the exit_plan_mode permission _meta, the web-shell resolves the approval DAG by exact match instead of "latest active snapshot", and every history-resetting / plan-mode-entering path invalidates the binding.

What works well

  • The identity round-trips correctly. PlanEmitter.emitPlancreateTranscriptPlanUpdate({planToolCallId, todoPlanId})normalizePlanUpdate (toolCallId: planCallId, rawOutput.plan.id) → getActiveTodosForPlanRevision(tool.callId, getTodoPlanId(tool)). ToolCallEmitter.emitResult deliberately skips the tool_call_update for todo_write, so the plan update is the only writer of that block's rawOutput — no clobbering race on the shared callId.
  • Fail-closed everywhere. Missing/partial _meta, empty entries, a sessionUpdate rejection, a sub-agent todo write, and a snapshot with no planId all leave the binding undefined and fall back to text-only. The it.each in Session.test.ts covers live / replay / failed / cleared / empty / missing-meta, which is the right matrix.
  • Dropping the role === 'plan' branch from the revision lookup is safe — those messages only come from the parsePlanTodos(statusBlock.text) diagnostic path and carry no callId/planId, so they could never match a revision anyway. getFloatingTodos still handles them.
  • getLatestActiveTodos has no remaining callers; the rename is complete.
  • The two replaced web-shell tests are the right regression tests — both put a newer same-planId snapshot after the bound one and assert the older DAG still renders.

Findings

1. [Suggestion] The previous !== 'plan' guard silently changes Todo Stop Guard behavior, not just the revision.
Session.ts setMode and acpAgent.ts sessionApprovalMode previously called clearTodoStopGuardTrust() on every transition into plan, including a redundant plan→plan re-select. The new guard suppresses it, so a Web Shell /plan re-select no longer runs #clearTodoStopGuardTrustAndDrainAutomaticQueues() — meaning todoStopGuard.blockUntilOrdinaryPromptStarts() is skipped and cron/notification queues are no longer drained, and a hasTrustedUnfinishedState guard keeps its trust across the re-select. The new tests (toHaveBeenCalledOnce after the second plan) lock this in, so it looks deliberate, but nothing in the PR body explains why the stop guard should change. Only the revision reset needs the guard:

this.config.setApprovalMode(approvalMode);
if (approvalMode === ApprovalMode.PLAN) {
  if (previousApprovalMode !== ApprovalMode.PLAN) {
    this.activeTodoPlanRevision = undefined;
  }
  this.clearTodoStopGuardTrust();
}

If the stop-guard change is intended, it deserves its own sentence in the description — it is a separate user-visible behavior from plan-approval binding.

2. [Suggestion] The invalidation clears aren't failure-safe.
Both Session.replayHistory and the live-load loop in acpAgent.ts put the clear on the success path:

for (const update of replay.updates) {
  await liveSession.sendUpdate(update);   // a transport rejection here...
}
liveSession.clearActiveTodoPlanRevision(); // ...skips this

If sendUpdate rejects part-way (client disconnect, SSE fanout failure) after a replayed plan update has already re-stamped the revision, the stale replayed binding survives on a live session — exactly the case the PR is guarding against. Same shape in replayHistory if historyReplayer.replay throws. try { … } finally { this.activeTodoPlanRevision = undefined; } closes both.

3. [Suggestion] restoreHistory() is the one reset path that doesn't clear the revision (already open as R6-3; re-verified at this commit — still stands). Session.ts:2311-2324 calls #clearTodoStopGuardTrustAndDrainAutomaticQueues() but leaves activeTodoPlanRevision intact, while replayHistory, rewindToTurn, the live-load loop and mode transitions all clear it. A wholesale history restore is at least as strong an invalidation signal as a rewind. One line next to the existing clear.

4. [Nit] Live-session re-attach loses a legitimately current binding, asymmetrically.
Non-bulk loadSession on an already-live session now wipes the revision; the bulk (historyReplay: 'response') path doesn't need to, so it keeps it. Both are safe (worst case is text-only), but the two branches now disagree about whether re-attaching invalidates a live plan cycle. Low impact today since the web-shell uses bulk mode and non-bulk clients don't render the DAG — worth a comment noting the asymmetry is intentional.

5. [Nit] The extracted predicate only de-duplicates half the check.
isExitPlanModeToolName fixes the wire-name drift, but toolKind === 'switch_mode' is still duplicated in ChatPane.tsx and ToolApproval.tsx, and App.tsx's approvalPlanTodos omits it entirely (harmless — ToolApproval re-checks — but it means App and ChatPane compute the same thing under different predicates). A single isExitPlanApproval(request) in utils/todos.ts would carry the whole rule.

6. [Nit] The pre-clear in sendUpdate has no comment, and it's the non-obvious half.

if (update.sessionUpdate === 'plan') {
  this.activeTodoPlanRevision = undefined;
}
await this.client.sessionUpdate(params);
this.#captureTodoPlanRevision(update);

#captureTodoPlanRevision already assigns undefined on the invalid branch, so the only reason the pre-clear exists is "a plan update the client never received must not stay bound" — which is exactly the 'failed' case in the it.each and is worth one line of comment. (The helper is also called for every non-plan update just to no-op; folding the whole thing into the existing if would read better.)

7. [Nit] Four new Session.test.ts assertions poke the private field via (session as unknown as { activeTodoPlanRevision?: unknown }). The it.each block already proves the same behavior through the observable _meta.qwenTodoApproval on the permission request; the private-field assertions will break on any refactor of the field without catching anything the observable assertions miss. The enter_plan_mode and rewindToTurn cases could assert through a follow-up exit_plan_mode request instead.

8. [Suggestion] R6-1 (order-blind live-load test) also still stands — toHaveBeenCalledOnce on a passive sendUpdate: vi.fn() mock can't distinguish clear-before-replay from clear-after-replay, and the ordering is the whole point.

Risk / other

  • Security: no new surface. qwenTodoApproval is agent-authored metadata, matched by exact string equality with non-empty validation in getString; no rendering of it.
  • Performance: approvalPlanTodos now depends on pendingToolApproval as well as messages, but pendingApproval already churns per blocks change (useShallowMemo compares options/content element identity, which extractPendingPermission reallocates). No new re-render pressure; the scan itself early-returns on the first callId match and only runs while an exit-plan approval is pending.
  • Protocol: _meta.qwenTodoApproval is additive and ignorable; no versioning concern.
  • Automated checks: CI is green on this commit (I did not re-run the suites locally — findings above are static, verified against the reviewed blobs).

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Address-review summary — PR #8393

Commit: a3a6ecec4fix(cli): clear Todo plan revision on history restore (#8393) (3 files, +26 lines). No conflict handling (--conflict false, no merge).

Feedback points and decisions

rc:3711145984 — getString helper is a 3rd copy (Suggestion, @doudouOUC / qwen3.7-max)

Decline (maintainer decision upheld, no code change). The duplication is real, but extracting a shared module would change three consumers and expand this PR far beyond its revision-binding scope. The maintainer already declined this in the thread (see rc:3711184522) and deferred it to a follow-up, which also matches the repo's round-count rule (past ~5 review rounds, only Critical fixes land; suggestions defer to follow-ups). Because that decline reply is already visible in the thread, no duplicate reply was posted this round; the thread stays open with its recorded reason.

rc:3711184522 — in-thread decline reply (@yiliang114)

Not a finding — it is the recorded decline for rc:3711145984 above. No action.

rc:3712081519 — R6-1: live-session load test is order-blind (Suggestion, @qwen-code-ci-bot / qwen3.8-max)

Resolved in code. Added the ordering assertion to clears the replayed Todo plan revision on a live-session load (packages/cli/src/acp-integration/acpAgent.test.ts): the test now requires clearActiveTodoPlanRevision's invocation order to be greater than the last replayed sendUpdate call, matching the production order in the non-bulk load path. Mutation-verified: moving liveSession.clearActiveTodoPlanRevision() above the replay loop in acpAgent.ts makes the test fail (expected 35 to be greater than 36); with the fix it passes.

rc:3712081534 — R6-3: restoreHistory() keeps activeTodoPlanRevision (Suggestion, @qwen-code-ci-bot / qwen3.8-max)

Resolved in code. Verified at HEAD: restoreHistory() was the only history-resetting path without the clear (siblings: replayHistory, rewindToTurn, the live-load replay loop, PLAN-mode transitions). Added this.activeTodoPlanRevision = undefined; after setHistory(...) in restoreHistory (Session.ts), same ordering as rewindToTurn, plus a pinning test in Session.test.ts that stamps a revision via a live plan update, asserts the stamped value, calls restoreHistory, and asserts the revision is undefined. Mutation-verified: dropping the new clear makes the test fail (expected { planId: 'old-plan', … } to be undefined); with the fix it passes. The sole production caller (restoreSessionHistory ext-method) discards the cycle the revision refers to, so clearing is the intended semantics.

Issue-level comments (@yiliang114)

Informational only — prior thread cleanup confirmation and local verification/UI evidence for the revision-bound approval. No action required.

Verification

All commands run on the committed tree (a3a6ecec4); the pre-commit hook (lint-staged/prettier) reformatted one assertion line without semantic change, and the gates were re-run after the commit:

  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0, tsc --noEmit across all workspaces)
  • npm run lint — passed (exit 0, no errors/warnings)
  • npx vitest run src/acp-integration/session/Session.test.ts src/acp-integration/acpAgent.test.ts (packages/cli) — passed, 2 files / 874 tests
  • Mutation checks for both new assertions — each fails against the deliberately broken code and passes against the fix (details above)
  • npm run generate:settings-schema — not run (no settings source changed)
  • Integration tests after npm run bundle — not run (changed behavior is exercised directly by the unit tests above, not only through the bundled CLI/integration harness)
中文说明

处理审查意见总结 — PR #8393

提交:a3a6ecec4fix(cli): clear Todo plan revision on history restore (#8393)(3 个文件,+26 行)。无冲突处理(--conflict false,未做合并)。

反馈点与决定

rc:3711145984 — getString 辅助函数是第 3 份副本(Suggestion,@doudouOUC / qwen3.7-max)

拒绝(维持 maintainer 决定,不改代码)。 重复确实存在,但抽取共享模块会改动三个使用方,使本 PR 超出其 revision 绑定的范围。maintainer 已在该 thread 中拒绝(见 rc:3711184522)并推迟到后续 PR 处理,这也符合仓库的轮次规则(超过约 5 轮审查后只落地 Critical 修复,建议类意见推迟到后续 PR)。由于该拒绝回复已在 thread 中可见,本轮不再重复回复;thread 保持打开,其中已记录拒绝理由。

rc:3711184522 — thread 内的拒绝回复(@yiliang114

不是审查发现 —— 它是上述 rc:3711145984 的已记录拒绝回复。无需处理。

rc:3712081519 — R6-1:存活会话加载测试与顺序无关(Suggestion,@qwen-code-ci-bot / qwen3.8-max)

已在代码中解决。clears the replayed Todo plan revision on a live-session load(packages/cli/src/acp-integration/acpAgent.test.ts)中补充了顺序断言:测试现在要求 clearActiveTodoPlanRevision 的调用顺序大于最后一次回放的 sendUpdate 调用,与生产代码中非批量加载路径的顺序一致。已通过变异验证:把 acpAgent.ts 中的 liveSession.clearActiveTodoPlanRevision() 移到回放循环上方,测试失败(expected 35 to be greater than 36);修复后测试通过。

rc:3712081534 — R6-3:restoreHistory() 保留 activeTodoPlanRevision(Suggestion,@qwen-code-ci-bot / qwen3.8-max)

已在代码中解决。 已在 HEAD 上核实:restoreHistory() 是唯一没有清空 revision 的历史重置路径(兄弟路径:replayHistoryrewindToTurn、存活加载回放循环、PLAN 模式切换)。已在 restoreHistory(Session.ts)的 setHistory(...) 之后补上 this.activeTodoPlanRevision = undefined;,顺序与 rewindToTurn 一致,并在 Session.test.ts 中新增固定测试:先通过 live plan update 写入 revision 并断言其值,再调用 restoreHistory,断言 revision 为 undefined。已通过变异验证:移除新增的清空会让测试失败(expected { planId: 'old-plan', … } to be undefined);修复后测试通过。唯一的生产调用方(restoreSessionHistory ext-method)会丢弃该 revision 所对应的轮次,因此清空是预期语义。

Issue 级评论(@yiliang114

仅为信息性内容 —— 对此前 thread 清理的确认,以及针对 revision 绑定审批的本地验证与 UI 证据。无需处理。

验证

以下命令均在已提交的代码树(a3a6ecec4)上运行;pre-commit 钩子(lint-staged/prettier)将一行断言重新排版,无语义变化,提交后已重新运行所有检查:

  • npm run build — 通过(exit 0)
  • npm run typecheck — 通过(exit 0,全部 workspace 执行 tsc --noEmit
  • npm run lint — 通过(exit 0,无 error/warning)
  • npx vitest run src/acp-integration/session/Session.test.ts src/acp-integration/acpAgent.test.ts(packages/cli)— 通过,2 个文件 / 874 个测试
  • 两条新断言的变异检查 —— 各自在人为破坏的代码上失败、在修复后通过(详见上文)
  • npm run generate:settings-schema — 未运行(未改动 settings 源)
  • npm run bundle 后的集成测试 — 未运行(改动的行为已由上述单元测试直接覆盖,并非只通过打包 CLI/集成测试框架执行)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 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

The previous-mode guard added for the revision binding also skipped the
Todo Stop Guard trust clear on a redundant plan re-select; scope the
guard to the revision reset so every transition into plan clears the
stop guard as before. The replay-time revision clears now run in
finally blocks so a transport failure part-way through a replay cannot
leave a replayed binding on the live session, and the web-shell
exit-plan approval rule is unified in one predicate. Revision tests
assert through the observable qwenTodoApproval approval metadata
instead of the private field.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Review feedback addressed — PR #8393

Commit: fc032c5a5 on codex/8389-plan-revision-approval (base: main, no merge needed — no conflict reported).

Findings and dispositions

1. [Suggestion] The previous !== 'plan' guard silently changed Todo Stop Guard behavior — FIXED.
The guard now only skips the revision reset. Session.setMode and the sessionApprovalMode ext-method call clearTodoStopGuardTrust() on every transition into plan again (including a redundant plan→plan re-select), exactly as before this PR; only activeTodoPlanRevision is preserved across a re-select. The settings-reload path needed no change (it already only runs on a real mode change). The acpAgent test that locked in the changed behavior now expects the stop-guard clear twice (once per transition into plan).

2. [Suggestion] The invalidation clears aren't failure-safe — FIXED.
Both Session.replayHistory and the live-load replay loop in loadSession now clear the revision in a finally block, so a sendUpdate/transport rejection part-way through a replay can no longer leave a re-stamped replayed binding on the live session. The replayError throw ordering is unchanged.

3. [Suggestion] restoreHistory() doesn't clear the revision — ALREADY FIXED in a3a6ecec4 (the commit after this review was taken): restoreHistory clears activeTodoPlanRevision next to the stop-guard clear, pinned by a test. Re-verified in the current diff.

4. [Nit] Live-session re-attach asymmetry — FIXED.
The non-bulk loadSession branch now carries a comment recording that the clear is intentional there (worst case text-only) and that the bulk path keeps a live binding on purpose because it hands the updates to the client instead of replaying them through the session.

5. [Nit] Predicate only de-duplicated half the check — FIXED.
isExitPlanApprovalRequest(request) in web-shell/client/utils/todos.ts now carries the whole rule (toolKind === 'switch_mode' + exit-plan wire name) and is used by App.tsx, ChatPane.tsx, and ToolApproval.tsx — so App also gates on the frame kind, matching the other two surfaces. The now-unused isExitPlanModeToolName helper was removed rather than left as dead surface; unit tests for the new predicate added.

6. [Nit] Pre-clear in sendUpdate had no comment — FIXED.
Added the one-line "why" (a plan update the client never receives must not stay bound; the capture re-stamps only after delivery succeeds) and folded the capture call into the plan-update guard. #captureTodoPlanRevision now takes the narrowed plan variant and no longer no-ops on every non-plan update.

7. [Nit] Tests poked the private activeTodoPlanRevision field — FIXED.
All five private-field assertions (setMode transition, setMode re-select, rewindToTurn, restoreHistory, enter_plan_mode) now assert through the observable _meta.qwenTodoApproval on a follow-up exit_plan_mode permission request, via a shared harness mirroring the existing it.each block. The restoreHistory test additionally proves the stamp was live before the restore.

8. [Suggestion] R6-1 order-blind live-load test — ALREADY FIXED in a3a6ecec4: the live-load test asserts clearActiveTodoPlanRevision ran after the last replayed sendUpdate via mock.invocationCallOrder (still holds under the new try/finally). The Session.replayHistory ordering remains pinned by the it.each "history replay" case, which would fail if the clear ran before the replayed updates.

Notes

  • No defer-to-human items; no unresolved findings.
  • No inline review threads existed this round (all feedback arrived as an issue-level comment), so there is no resolved-comments.txt / comment-replies.json.

Verification

Commands actually run (after the final code state):

  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0)
  • npx prettier --check on touched files — clean after --write on two files
  • cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts src/acp-integration/acpAgent.test.ts — 874 passed (515 Session + 359 acpAgent)
  • cd packages/web-shell && npx vitest run client/utils/todos.test.ts client/components/ChatPane.test.tsx client/components/messages/ToolApproval.test.tsx client/App.test.tsx — 480 passed
  • cd packages/web-shell && npx tsc --noEmit — passed

Integration tests were not run: the touched behavior (ACP session internals and web-shell client rendering) is exercised by the unit suites above, not only through the bundled CLI or integration harness. Settings sources were not changed, so no schema regeneration was needed.

中文说明

已处理的评审反馈 — PR #8393

提交:codex/8389-plan-revision-approval 分支上的 fc032c5a5(基线:main,无需合并——未报告冲突)。

各条反馈的处理结果

1. [建议] previous !== 'plan' 守卫悄悄改变了 Todo Stop Guard 行为 — 已修复。
该守卫现在只跳过 revision(修订版)的重置。Session.setModesessionApprovalMode 扩展方法在每次转入 plan 模式时(包括多余的 plan→plan 重选)重新调用 clearTodoStopGuardTrust(),与本 PR 之前的行为完全一致;只有 activeTodoPlanRevision 会在重选时保留。settings 热重载路径无需改动(它本来就只在模式真正变化时执行)。原本锁定新行为的 acpAgent 测试现在期望 stop-guard 清理被调用两次(每次转入 plan 各一次)。

2. [建议] 失效清理不具备失败安全性 — 已修复。
Session.replayHistoryloadSession 中的实时加载回放循环现在都在 finally 块中清理 revision,因此回放过程中途发生 sendUpdate/传输失败时,不会再让重新盖章的回放绑定残留在活跃会话上。replayError 的抛出顺序保持不变。

3. [建议] restoreHistory() 不清理 revision — 已在 a3a6ecec4 中修复(即本次评审取样之后提交的那笔):restoreHistory 已在 stop-guard 清理旁一并清理 activeTodoPlanRevision,并有测试锁定。已在当前 diff 中复核确认。

4. [Nit] 活跃会话重新挂接的不对称性 — 已修复。
loadSession 的非 bulk 分支现在带有注释,说明该处清理是有意为之(最坏情况是仅显示文本),并说明 bulk 路径刻意保留活跃绑定——因为它把更新交给客户端自行回放,而不是经由该会话重放。

5. [Nit] 谓词只去重了一半检查 — 已修复。
web-shell/client/utils/todos.ts 中的 isExitPlanApprovalRequest(request) 现在承载完整规则(toolKind === 'switch_mode' + exit-plan 线名),并被 App.tsxChatPane.tsxToolApproval.tsx 共同使用——因此 App 也会按帧类型进行门控,与另外两处保持一致。原本不再被使用的 isExitPlanModeToolName 助手函数被直接移除,避免留下无用的导出;新谓词已补充单元测试。

6. [Nit] sendUpdate 中的预清理缺少注释 — 已修复。
补充了一行“为什么”的注释(客户端从未收到的 plan 更新不得保持绑定;只有投递成功后才由 capture 重新盖章),并把 capture 调用收进 plan 更新的守卫分支内。#captureTodoPlanRevision 现在接收收窄后的 plan 变体,不再对每条非 plan 更新空调用。

7. [Nit] 测试直接探测私有字段 activeTodoPlanRevision — 已修复。
全部五处私有字段断言(setMode 转入、setMode 重选、rewindToTurnrestoreHistoryenter_plan_mode)现在都改为通过后续 exit_plan_mode 权限请求上可观测的 _meta.qwenTodoApproval 进行断言,使用与现有 it.each 块相同的共享测试脚手架。restoreHistory 测试还额外证明了恢复之前绑定确实处于生效状态。

8. [建议] R6-1 实时加载测试对顺序不敏感 — 已在 a3a6ecec4 中修复:实时加载测试通过 mock.invocationCallOrder 断言 clearActiveTodoPlanRevision 在最后一次回放 sendUpdate 之后执行(在新的 try/finally 下依然成立)。Session.replayHistory 的顺序仍由 it.each 的 “history replay” 用例锁定——如果清理发生在回放更新之前,该用例会失败。

说明

  • defer-to-human(需人工决策)事项;无未解决的反馈。
  • 本轮没有行内评审线程(所有反馈以 issue 级评论形式到达),因此没有 resolved-comments.txt / comment-replies.json

验证

实际执行的命令(针对最终代码状态):

  • npm run build — 通过(退出码 0)
  • npm run typecheck — 通过(退出码 0)
  • npm run lint — 通过(退出码 0)
  • npx prettier --check(针对改动文件)— 对两个文件执行 --write 后通过
  • cd packages/cli && npx vitest run src/acp-integration/session/Session.test.ts src/acp-integration/acpAgent.test.ts — 874 个测试通过(Session 515 + acpAgent 359)
  • cd packages/web-shell && npx vitest run client/utils/todos.test.ts client/components/ChatPane.test.tsx client/components/messages/ToolApproval.test.tsx client/App.test.tsx — 480 个测试通过
  • cd packages/web-shell && npx tsc --noEmit — 通过

未运行集成测试:本次改动的行为(ACP 会话内部逻辑与 web-shell 客户端渲染)由上述单元测试套件覆盖,并非只能通过打包后的 CLI 或集成测试框架验证。未改动 settings 源文件,因此无需重新生成 schema。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 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

@wenshao

wenshao commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Follow-up review: bind plan approval to its Todo revision

Re-reviewed at a3a6ecec, delta since my last pass at 8ecb7745 (one commit: fix(cli): clear Todo plan revision on history restore). CI is green on this head.

Verified fixed

  • F3 — restoreHistory() didn't clear the revision. Session.ts:2323 now clears it next to #clearTodoStopGuardTrustAndDrainAutomaticQueues(), so all five reset paths (replayHistory, rewindToTurn, restoreHistory, mode transition, plan-lifecycle tool execution) agree. The added test pins it.
  • F8 — order-blind live-load test. acpAgent.test.ts now asserts clearActiveTodoPlanRevision.mock.invocationCallOrder[0] > sendUpdate.mock.invocationCallOrder.at(-1), which is the ordering that actually matters (a clear before the replay loop would be re-stamped by the replayed plan update). Good fix — that assertion is exactly the one that catches the regression.

Nothing else moved, so the remaining findings below are re-stated only as far as needed; all were re-read against the blobs at this commit.

Still open

1. [Suggestion] previous !== 'plan' changes Todo Stop Guard behavior, not just the revisionSession.ts:6510-6518, acpAgent.ts:9167-9170. A plan→plan re-select (the Web Shell /plan path) now skips clearTodoStopGuardTrust() entirely, so blockUntilOrdinaryPromptStarts() doesn't run, the cron/notification queues aren't drained, and hasTrustedUnfinishedState survives the re-select. Only the revision reset needs the transition guard:

if (approvalMode === ApprovalMode.PLAN) {
  if (previousApprovalMode !== ApprovalMode.PLAN) {
    this.activeTodoPlanRevision = undefined;
  }
  this.clearTodoStopGuardTrust();
}

Impact is bounded — the prompt loop clears trust on every turn while in PLAN (Session.ts:3498) — so this is a Suggestion, not a blocker. But it is a second, unrelated behavior change riding along, and the new toHaveBeenCalledOnce assertions lock it in. Either split it out or give it a sentence in the description.

2. [Suggestion] The invalidation clears still aren't failure-safeSession.ts:2213 and acpAgent.ts:4543. Both put the clear on the success path:

for (const update of replay.updates) {
  await liveSession.sendUpdate(update);   // rejection here...
}
liveSession.clearActiveTodoPlanRevision(); // ...skips this

A transport rejection part-way through a replay (client disconnect, SSE fanout failure) leaves a replayed plan update's re-stamped revision bound on a live session — the exact case the PR guards against. try { … } finally { … } closes both. Same shape if historyReplayer.replay throws.

4. [Nit] Live re-attach asymmetry. Non-bulk loadSession on an already-live session wipes a still-current binding; the bulk (LOAD_REPLAY_META_KEY) branch never re-stamps and so keeps it. Both are safe (worst case text-only) and the earlier rounds settled on removing the bulk restore deliberately — a one-line comment at acpAgent.ts:4543 saying the two branches differ because only the non-bulk one re-stamps would stop the next reader from "fixing" it.

5. [Nit] The extracted predicate only de-duplicates half the check. isExitPlanModeToolName fixes the wire-name drift, but toolKind === 'switch_mode' is still inline in ChatPane.tsx:353 and ToolApproval.tsx:377, and App.tsx:3239 omits it — so App and ChatPane compute the same DAG under different predicates (harmless today, ToolApproval re-checks). A single isExitPlanApproval(request) in utils/todos.ts would carry the whole rule, which is what the helper was extracted for.

6. [Nit] The pre-clear in sendUpdate is the non-obvious half and has no commentSession.ts:4464-4470. #captureTodoPlanRevision already assigns undefined on every invalid branch, so the pre-clear exists solely so a plan update the client never received can't stay bound. That's the 'failed' row of the it.each, and it's worth one line in the source. Folding the helper into the existing if (update.sessionUpdate === 'plan') would also stop calling it as a no-op on every non-plan update.

7. [Nit, slightly regressed] Private-field assertions. The new restoreHistory test adds a sixth (session as unknown as { activeTodoPlanRevision?: unknown }) probe. The it.each block already proves the same property through the observable _meta.qwenTodoApproval on the permission request; the private-field probes will break on any rename of the field without catching anything the observable assertions miss. Asserting through a follow-up exit_plan_mode request would be equally short and refactor-proof.

Verdict

The mechanism is right and the identity round-trips cleanly (PlanEmitter.emitPlancreateTranscriptPlanUpdatenormalizePlanUpdategetActiveTodosForPlanRevision), with ToolCallEmitter.emitResult skipping the tool_call_update for todo_write so nothing else writes that block's rawOutput. Every failure mode degrades to the existing text-only approval. No Criticals; nothing here blocks merge. F2 is the one I'd still take before landing — it's a two-line finally and it protects the invariant the PR is built on.

@chiga0 chiga0 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@yiliang114
yiliang114 added this pull request to the merge queue Aug 4, 2026
Merged via the queue into QwenLM:main with commit 52e0d1b Aug 4, 2026
44 of 46 checks passed
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.6.

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