feat: visualize ordinary-session plan execution - #7580
Conversation
gwinthis
left a comment
There was a problem hiding this comment.
Architecture Review: COMMENT (C=0)
Summary
Large feature (+3266/-192) connecting Todo plans to live subagent executions via layered dependency DAG visualization in Web Shell. The design keeps Todo as source of truth for business completion while treating Agent task state as execution evidence.
Architecture Assessment
Strengths:
- Todo as single source of truth — plan nodes own completion state; agent executions are evidence, not completion triggers. Failed/cancelled executions draw attention without completing a Todo.
- No new engine — uses existing Todo and task data paths, native SVG/CSS. No workflow engine, scheduler, or graph dependency added.
- Backward compatible — legacy Todo lists without dependency metadata render as lists; unlinked Agent calls appear as unassigned executions.
- Edge-count cap — dense plans fall back without materializing excessive SVG edges. Resize observation and scale normalization bound browser layout.
- Stable identity across replays — Todo snapshots retain stable plan/node identity + dependency metadata across live ACP updates and history replay.
Scope observations (non-blocking):
- Graph edges depend on measured browser layout — this is a known tradeoff of SVG-based graph rendering vs. canvas/WebGL
- Automatic dependency scheduling, retries, completion propagation are explicitly out of scope — correct for Phase 1
Pattern
Execution evidence vs. completion authority: Separate the "what should be done" (plan/Todo) from "what is being done" (agent execution). Plan nodes own completion; executions provide evidence. This prevents a failed execution from falsely completing a plan item, and allows independent retry without unlocking dependent work.
中文说明
架构评审:COMMENT (C=0)
概要
大型功能(+3266/-192):将 Todo 计划连接到实时子代理执行,在 Web Shell 中展示分层依赖 DAG 可视化。
架构评估
- Todo 为唯一事实来源 — 计划节点拥有完成状态;代理执行是证据
- 无新引擎 — 复用现有 Todo 和任务数据路径,原生 SVG/CSS
- 向后兼容 — 旧 Todo 列表仍为列表,未关联 Agent 调用显示为未分配执行
模式
执行证据 vs. 完成权威: 将"应做什么"(计划/Todo)与"正在做什么"(代理执行)分离。计划节点拥有完成权;执行提供证据。
— qwen3.7-max via Qwen Code /review
# Conflicts: # packages/cli/src/acp-integration/session/Session.test.ts
E2E Test ReportResult: PASS Tested a real daemon-backed Plan Mode session with a four-node fork/join workflow:
Final cold-recovery evidence: Environment: macOS, Node.js 22, local daemon/Web Shell development build, persisted session |
🖼️ web-shell visual previewRendered against a mock daemon (no real backend): the PR base vs this PR head Screenshots · before / afterℹ️ No screenshot changed against the PR base — but this PR edits 11 render-shaping files:
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 Full-resolution recordings (.webm) are attached to the workflow run. — Qwen Code · web-shell visuals |
🩺 serve daemon A/BBuilt the PR base vs this PR head ✅ No response changes against the PR base across 4 scenario(s). — Qwen Code · serve A/B |
SubAgentTracker: the emitter now guards subagent TodoWrite results (tool-call-emitter emitResult early-returns on subagentMeta), so a subagent todo no longer promotes into a session-level plan. Flip the stale assertion to expect no plan emission, mirroring the dedicated guard test in tool-call-emitter.test.ts. HistoricalPlanExecution: the pagination fixture's onLoadOlderHistory returned Promise<void>, but PlanExecutionHistoryProvider requires Promise<boolean> and throws 'Unable to load earlier session history' on a falsy resolution. Production wires loadOlderHistory (resolves true after layout); return true in the fixture to model a successful load.
# Conflicts: # packages/cli/src/acp-integration/session/Session.test.ts # packages/cli/src/acp-integration/session/Session.ts # packages/web-shell/client/App.test.tsx # packages/web-shell/client/App.tsx # packages/web-shell/client/components/ChatPane.tsx # packages/web-shell/client/components/MessageList.tsx # packages/web-shell/client/components/messages/TasksStatusMessage.test.tsx # packages/web-shell/client/components/messages/TasksStatusMessage.tsx
# Conflicts: # packages/core/src/tools/todoWrite.test.ts # packages/core/src/tools/todoWrite.ts # packages/sdk-typescript/scripts/build.js # packages/web-shell/client/App.test.tsx # packages/web-shell/client/components/ChatPane.tsx # packages/web-shell/client/components/messages/ToolApproval.test.tsx
|
Re-running triage on Template looks good ✓ — all sections present, with before/after screenshots and a filled-in test matrix. Problem: a roadmap feature request, not theoretical hardening. The linked issue (#7525) is labeled Direction: aligned. Visualizing the plan→execution relationship is squarely within the multi-agent direction the roadmap labels point to, and the PR follows the issue's key constraint — it joins the existing Todo / task-registry / transcript streams for presentation and adds no workflow scheduler, graph store, or graph endpoint. That restraint is the right call. Size: ~1,910 production-logic lines (≈1,537 TS/TSX + 373 CSS) across six packages, ~214 of them in Approach: large but cohesive — one feature spanning the data contract ( Risk: elevated-review-depth signal. The diff touches Moving on to code review. 🔍 中文说明应 模板完整 ✓——各节齐全,附 before/after 截图,测试矩阵也已填写。 问题:路线图上的 feature request,不是理论性加固。关联 issue(#7525)带 方向:对齐。把"计划→执行"关系可视化正属于路线图标签所指的多智能体方向;PR 也遵循了 issue 的关键约束——只关联展示现有 Todo / task-registry / transcript 数据流,不新增 workflow 调度器、图存储或图接口。这种克制是对的。 规模:约 1,910 行生产逻辑(约 1,537 行 TS/TSX + 373 行 CSS),跨六个 package,其中约 214 行位于 方案:范围虽大但内聚——一个 feature 贯穿数据契约( 风险:review 深度升级信号。diff 触及 进入代码审查 🔍 — Qwen Code · qwen3.8-max-preview Reviewed at |
Code reviewI re-read the production diff across all six packages at the current head. No correctness, security, or regression blockers — the design is coherent and the implementation is careful. The shape: Things done well:
I independently re-verified the findings
My own earlier notes still stand and are likewise non-blocking: the sequenceDiagram
participant U as User
participant M as Model
participant T as todo_write tool
participant A as ACP plan stream
participant R as Task registry
participant W as Web Shell
M->>T: write todos (id, blockedBy)
T->>T: validate DAG, assign planId
T->>A: plan update with qwenTodo metadata
A->>W: plan entries with stable ids
M->>R: launch Agent with todo_id
R->>W: live status by toolUseId
W->>W: join plan nodes to executions
W-->>U: render workflow DAG
Files changed (22 of 53 shown — production logic; tests and snapshots folded)
Test evidenceUnattended CI run — I did not build or drive the product myself; the signal below is the PR's own CI on the reviewed commit, fetched via the API. CI is fully green on
The macOS/Windows unit legs and the CLI integration tests are skipped on this trigger, so the unit suite's only running leg was ubuntu/Node 22 — now green. The web-shell visuals capture and the web-shell E2E smoke both passed, the most relevant completed signals for a UI-heavy PR. The verification gap my last pass flagged is now closed by independent evidence. After my previous review, 中文说明代码审查我在当前 head 上重新通读了全部六个 package 的生产代码 diff。没有正确性、安全性或回归层面的阻塞问题——设计自洽,实现细致。整体形态: 做得好的地方:
我独立对照 diff 复核了上方
我此前的备注同样成立,也都非阻塞: (时序图见英文正文,描述计划发布与实时关联的关键路径。) 测试证据无人值守 CI 运行——我没有在本地构建或驱动产品;下方信号是该 PR 自身在受审 commit 上的 CI,通过 API 获取。CI 在 (CI 表格见英文正文区域标记内。) macOS/Windows 单测分支与 CLI 集成测试在此触发下被跳过,因此单测套件唯一在跑的分支是 ubuntu/Node 22——现已绿。web-shell 视觉截图捕获与 web-shell E2E smoke 均通过,对这个 UI 占比高的 PR 是最相关的已完成信号。 我上次标注的验证缺口现已由独立证据弥合。在我上一轮审查之后, — Qwen Code · qwen3.8-max-preview Reviewed at |
|
Confidence: 4/5 — a solid, well-tested feature that does exactly what the roadmap issue asked; the verification gap from my last pass is now closed by a maintainer's end-to-end A/B run, and what remains is non-blocking nits (named in the code-review comment) plus a description that should be aligned with the diff. Stepping back: this is genuinely good work, and it's now genuinely verified. The independent read I formed before digging into the diff — give What changed since my last pass: the one thing holding this at 4/5 then was the verification gap — the integrated behaviour (a real session restored after a daemon restart, fork/join edges, nested-Agent detail) rested on the author's macOS-only testing and wasn't observable from the diff. What keeps it at 4/5 rather than an unreserved 5/5 is no longer doubt about whether it works — it's the loose ends a maintainer should track before/after merge, all non-blocking:
None of these change the verdict; they're the kind of thing to fold into a fast-follow or address in a quick description edit. On authorship and the gate: the author ( On the approval itself: my prior run already posted an 中文说明置信度:4/5——一个扎实、测试充分的 feature,完全做到了路线图 issue 的要求;我上次留下的验证缺口已由一位维护者的端到端 A/B 运行弥合,剩下的只有非阻塞的小问题(已在代码审查评论中点名),以及一份应与 diff 对齐的描述。 退一步看:这确实是好活,而且现在确实被验证过了。我在深入 diff 之前形成的独立判断——给 自上次审查以来的变化:上次把本 PR 压在 4/5 的唯一原因,是验证缺口——集成层面的行为(daemon 重启后恢复真实 session、fork/join 连线、嵌套 Agent 详情)依赖作者在 macOS 上的本地测试,无法从 diff 观察到。 之所以仍是 4/5 而非毫无保留的 5/5,已不再是怀疑它能不能用——而是维护者在合并前后应当跟进的几个收尾项,全部非阻塞:
这些都不改变结论;属于可以放进快速跟进、或用一次描述编辑解决的范畴。 关于作者与门禁:作者( 关于批准本身:我上一轮运行已经针对恰好这个 commit( — Qwen Code · qwen3.8-max-preview Reviewed at |
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
LGTM, looks ready to ship — CI landed green after the review. ✅
qwen-code-ci-bot
left a comment
There was a problem hiding this comment.
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-preview via Qwen Code /review
| // Skip tool_call_update event for TodoWriteTool | ||
| // Still log and return function response for LLM | ||
| } else { | ||
| } else if (!isTodoWriteTool) { |
There was a problem hiding this comment.
[Suggestion] Tautological guard: else if (!isTodoWriteTool) is the direct alternative to if (isTodoWriteTool), and isTodoWriteTool is a const (line 7420), so on entry to this branch it is provably false — exactly equivalent to a plain else. — Concrete cost: the redundant negation implies a code path where isTodoWriteTool could be true here, which cannot exist; a maintainer may waste time hunting the imagined third case or "correct" surrounding logic under a false assumption about the control flow.
| } else if (!isTodoWriteTool) { | |
| } else { |
中文说明
冗余判断:else if (!isTodoWriteTool) 是 if (isTodoWriteTool) 的直接分支,而 isTodoWriteTool 是 const(第 7420 行),进入此分支时它必然为 false,完全等价于普通的 else。具体代价:这个多余的否定暗示此处可能存在 isTodoWriteTool 为 true 的代码路径,而该路径并不存在;维护者可能浪费时间寻找这个想象中的第三种情况,或在错误理解控制流的前提下“修正”周边逻辑。
— qwen3.8-max-preview via Qwen Code /review
| const blocked = (todo.blockedBy ?? []).some( | ||
| (id) => todosById.get(id)?.status !== 'completed', | ||
| ); |
There was a problem hiding this comment.
[Suggestion] A blockedBy reference to a non-existent todo makes a node permanently "blocked". todosById.get(id)?.status !== 'completed' evaluates undefined !== 'completed' → true for an unknown id, so the node is badged blocked forever. But layerPlanTodos (~line 56, byId.has(dependencyId)) and the topology/edge-drawing (~line 331, knownIds.has(dependencyId)) both filter out unknown ids — so the node is laid out as unblocked (correct layer, no incoming edge) yet labelled blocked, a contradiction the user cannot resolve. — Failure scenario: a plan is revised and todo "3" removed while todo "5" still carries blockedBy: ["3"]; the layout ignores the stale ref but the status stays "blocked" permanently. Filter to known ids first, consistent with the other two sites:
| const blocked = (todo.blockedBy ?? []).some( | |
| (id) => todosById.get(id)?.status !== 'completed', | |
| ); | |
| const blocked = (todo.blockedBy ?? []).some( | |
| (id) => todosById.has(id) && todosById.get(id)!.status !== 'completed', | |
| ); |
中文说明
指向不存在 todo 的 blockedBy 引用会让节点永久处于 “blocked” 状态。对未知 id,todosById.get(id)?.status !== 'completed' 会得到 undefined !== 'completed' → true,因此该节点会一直被标记为阻塞。但 layerPlanTodos(约第 56 行,byId.has(dependencyId))与拓扑/连线绘制(约第 331 行,knownIds.has(dependencyId))都会过滤掉未知 id——所以该节点在布局上显示为未阻塞(正确的层级、无入边),却被标记为阻塞,形成用户无法解决的矛盾。失败场景:计划被修订、todo “3” 被删除,而 todo “5” 仍带有 blockedBy: ["3"];布局忽略了这个过期引用,但状态会永久停留在 “blocked”。建议与其它两处保持一致,先过滤到已知 id。
— qwen3.8-max-preview via Qwen Code /review
| 'planExecution.stepDetails': 'Step details', | ||
| 'planExecution.subagents': 'Subagents', | ||
| 'planExecution.openDetails': 'Open subagent details', | ||
| 'planExecution.view': 'View plan execution', |
There was a problem hiding this comment.
[Suggestion] Dead i18n key: planExecution.view is declared in both EN (here) and ZH (~line 4646) but referenced by no t(...) call site anywhere — a repo-wide grep finds only the two definitions, and no dynamic key construction reaches it. The only new element that opens the plan view (the TodoPanel progress button) labels itself via summaryAriaLabel ("Step X / Y"), not this string. — Concrete cost: Messages is an untyped Record<string, MessageValue>, so nothing flags the dead key; it must be translated and kept in sync across locales indefinitely and misleads readers into thinking a "View plan execution" affordance exists. Remove it from both locales (or wire it into the TodoPanel button if an action label was intended):
// remove from both EN (~2165) and ZH (~4646):
'planExecution.view': 'View plan execution',
'planExecution.view': '查看计划执行',中文说明
死 i18n key:planExecution.view 在英文(此处)和中文(约第 4646 行)都有声明,但整个仓库没有任何 t(...) 调用引用它——全仓搜索只找到这两处定义,也没有动态拼接 key 能到达它。唯一会打开计划视图的新元素(TodoPanel 进度按钮)用 summaryAriaLabel(“Step X / Y”)作为标签,并非这个字符串。具体代价:Messages 是无类型的 Record<string, MessageValue>,因此没有任何机制能标记这个死 key;它必须被翻译并在各语言间长期保持同步,还会误导读者以为存在一个 “View plan execution” 入口。建议从两种语言中删除(或如果本想用一个操作标签,则将其接到 TodoPanel 按钮上)。
— qwen3.8-max-preview via Qwen Code /review
Review —
|
Local verification report — real build, real daemon, real browserI verified this PR end-to-end on Linux with a fresh Verdict: the shipped behaviour works and is solid. The PR description, however, still describes an earlier revision — see F1. Nothing here blocks merging the code; F1 asks for a description/test-plan correction (or the missing feature back). Environment & method
A/B method. In one worktree: Scenario. One prompt produces a diamond plan — 1. Unit suites at PR head — all green
2. The contract actually reaches the modelCaptured from the real request body the daemon sent (not from source):
Persisted transcript (durable side) carries the identity too: 3. Live workflow — before vs after
The fork/join topology, the layering, and the After — live DAG with per-node executions and the unassigned bucket: Before (merge base) — same session, same plan: a flat list, no dependencies, no linkage, not clickable: Scrolled right — the join edges 4. Plan-mode gate (Reviewer Test Plan steps 1–2)Switching the composer to Plan and letting the model call 5. Durability (Reviewer Test Plan steps 4–5, live half)
FindingsF1 — The PR description (and Reviewer Test Plan step 5) describes code that is not in this revisionThe body promises a post-completion history story:
None of that exists in Behaviourally, the entry point this PR adds is gated on the plan being active: // App.tsx
const nextTodoPanelMode =
connection.catchingUp || floatingTodos.length === 0 || floatingTodosAllCompleted
? 'hidden' : 'active';
…
onOpen={showFloatingTodos ? openTasksPanel : undefined}Measured on a real session that runs the plan to completion:
Consequence: Reviewer Test Plan step 5 is not reproducible at this head, and neither are the two "After" screenshots in the description (both show a restored completed session). Suggested resolution — either is fine, but please pick one before merging:
F2 —
|
| PR head | 2f0b685353 |
| Merge base | 07c832ce37(真实范围:packages/ 下 51 个文件) |
| Node | v22.22.2,Linux |
| 构建 | 独立 worktree 中 npm ci + npm run build(不使用软链 node_modules) |
| Daemon | node packages/cli/dist/index.js serve --port … --workspace … --no-open |
| UI | 生产版 packages/web-shell/dist,Playwright 1.58.2 / Chromium 驱动 |
| 模型 | 本地 mock OpenAI SSE 端点(按需产出 agent、todo_write、exit_plan_mode) |
A/B 方法。 同一个 worktree 内:git checkout <merge-base> -- packages docs 并删除新增的三个 PlanExecutionView.* 文件 → npm run build → 同一个 daemon、同一个 mock、同一份脚本跑一遍;之后 git checkout HEAD -- packages docs 再重建。两侧数据来自完全相同的驱动脚本。每次都通过 grep 构建产物里的特征串(Plan execution、data-plan-workflow 等)确认当前生效的是哪一版。
场景。 一次 prompt 产出菱形计划 —— t1 → (t2, t3) → t4 → t5,其中 t1 已完成、t2/t3 进行中、t4/t5 待办。随后发起三个顶层后台 agent:两个分别带 todo_id: t2 / todo_id: t3,另一个刻意不带 todo_id。
1. PR head 上的单测 —— 全绿
| 包 | 文件 | 用例 |
|---|---|---|
core — todoWrite、agent、agent-core、config.workflow-registration、prompts |
5 | 422 |
cli — PlanEmitter、tool-call-emitter、history-replayer、Session、SubAgentTracker |
5 | 649 |
web-shell — PlanExecutionView、todos、ToolApproval、TodoPanel、TasksStatusMessage、transcriptAdapter、ChatPane |
7 | 203 |
sdk-typescript — daemonUi |
1 | 279 |
acp-bridge — transcript-replay |
1 | 17 |
webui — DaemonSessionProvider.subagent、selectors |
2 | 11 |
| 合计 | 21 | 1581 通过,0 失败 |
2. 新契约确实到达了模型
以下取自 daemon 真实发出的请求体(不是从源码推断):
| 本 PR | Merge base | |
|---|---|---|
todo_write 条目字段 |
content, status, id, blockedBy |
content, status, id |
工具描述里的 blockedBy 指引 |
有 | 无 |
agent.todo_id 声明 |
{type: string, maxLength: 500} |
未声明 |
agent 描述里的 todo_id 指引 |
1 行 | 0 行 |
持久化 transcript(durable 侧)同样带上了计划身份:
"resultDisplay":{"type":"todo_list","planId":"6ada526b-…","todos":[
{"id":"t1","content":"Design the release data schema","status":"completed"},
{"id":"t2","content":"Implement the API layer","status":"in_progress","blockedBy":["t1"]}, …
"functionCall":{"name":"agent","args":{…,"run_in_background":true,"todo_id":"t2"}}
3. 运行期 workflow —— 改动前后对比
| 断言(同脚本、同 mock) | Merge base | 本 PR |
|---|---|---|
section[aria-label="Plan execution"] |
0 | 1 |
[data-plan-node-id] |
0 | 5(t1…t5) |
path[data-plan-edge] |
0 | 5 —— t1→t2, t1→t3, t2→t4, t3→t4, t4→t5 |
| 节点状态 | — | t1 completed、t2 running、t3 running、t4 blocked、t5 blocked |
| Todo 面板可点击 | 否(普通 <span>) |
是(Step 2 / 5) |
| 执行挂在对应节点下 | — | t2、t3 各自显示其实时 agent |
没有 todo_id 的 agent |
— | 落到 Unassigned executions |
fork/join 拓扑、分层、以及 blocked 的推导(只要有任一 blockedBy 目标未 completed 即为阻塞)全部正确;实时 task 状态也正确覆盖了 Todo 状态(t2/t3 显示 Running 而不是「进行中」,因为它们的 agent 在跑)。
改动后 —— 带节点内执行与未关联执行的实时 DAG:
改动前(merge base)—— 同一会话、同一计划:一个扁平列表,没有依赖、没有关联、不可点击:
右滚后 —— 汇聚边 t2→t4、t3→t4、t4→t5:
4. Plan Mode 门禁(Reviewer Test Plan 第 1–2 步)
把 composer 切到 Plan,让模型先 todo_write 再 exit_plan_mode,得到的正是设计中的形态:同一个审批面板里同时呈现提交的计划正文与依赖图,原有的 Reject / 放行选项不受影响。在审批面板内点击节点会选中它并展示 Step details,且不会被审批的键盘/确认逻辑吞掉(data-plan-interactive 守卫生效):审批面板保持打开(approvalStillOpen: true),没有任何选项被确认。
5. 持久性(Reviewer Test Plan 第 4–5 步的运行期部分)
| 步骤 | 结果 |
|---|---|
| 浏览器刷新 + transcript 回放 | 5 个节点、5 条边不变,执行仍挂在节点下 |
| daemon 完全重启(从磁盘冷回放) | 5 个节点、5 条边不变;三个 agent 恢复为 Paused,节点也正确从 Running 降级为 Paused |
| 点击节点内的执行 | 打开真实的 Agent 详情面板(prompt、running 标记、Stop) |
问题清单
F1 —— PR 描述(以及 Reviewer Test Plan 第 5 步)描述的是本次未包含的代码
描述里承诺了「完成后可回看」的能力:
Completed sessions keep a collapsed workflow entry beside each persisted Todo snapshot. Expanding it lazily rebuilds root Agent executions from the parent transcript and lightweight descendant lineage from existing Agent sidecars […] Historical indexes are runtime-scoped, bounded, short-lived, and explicitly report partial lineage when a safety limit is reached […]
但 git diff 07c832ce37 2f0b685353 -- packages/ 里没有这些代码:没有 sidecar 层级加载器、没有有界/短时的历史索引、也没有截断提示。(diff 中唯一的 sidecar 命中,是 agent-core.ts 里关于 Todo sidecar 文件 的一句注释。)这看起来是 f251be909 refactor: simplify session plan execution workflow 之前留下的文案。
从行为上看,本 PR 新增的入口是以计划处于活跃状态为前提的:
// App.tsx
const nextTodoPanelMode =
connection.catchingUp || floatingTodos.length === 0 || floatingTodosAllCompleted
? 'hidden' : 'active';
…
onOpen={showFloatingTodos ? openTasksPanel : undefined}在一个真实跑到计划全部完成的会话上实测:
计划运行中的 Step N / M 按钮 |
存在 |
所有 todo 变为 completed 之后 |
消失 |
此外,只要最后一次快照之后出现了新的用户消息,getFloatingTodos() 就返回空,planTodos 变成 [],PlanExecutionView 从此渲染为 null。通过既有的其它入口仍可打开 tasks 弹窗,并在 floatingTodos 非空期间看到图,但 diff 里没有任何「按快照回看」的历史入口。
结论:Reviewer Test Plan 第 5 步在当前 head 上无法复现,描述里那两张「After」截图(都展示的是恢复后的已完成会话)同样无法复现。
建议二选一,合入前确定一个:
- 把描述与测试计划收敛到实际交付的范围(执行前门禁 + 运行期 + 活跃状态下的刷新/重启恢复),并替换那两张过期截图;或
- 把历史快照入口补回来。
F2 —— todo_id 是子智能体投影里唯一没有长度上限的字段(nit)
packages/webui/src/daemon/session/DaemonSessionProvider.tsx:
const subagentType = boundedString(rawInput?.['subagent_type'], 120);
const prompt = boundedString(rawInput?.['prompt'], 240);
const description = boundedString(rawInput?.['description'], 240);
const todoId = typeof rawInput?.['todo_id'] === 'string' ? rawInput['todo_id'] : undefined;AgentTool.validateToolParams 会把 todo_id 限制在 500 字符,但这个投影同样会处理未经该校验的 tool-call 事件。建议改成 boundedString(rawInput?.['todo_id'], 500),与相邻字段保持一致。
F3 —— 固定宽度弹窗里 DAG 打开即被裁切(UX)
.dagViewport 是 overflow-x: auto,所以内容不会丢失;但弹窗不会随窗口变宽。在 2400px 视口下实测:clientWidth: 688、scrollWidth: 1148。因此 5 层的计划打开时 t4/t5 在屏幕外,而汇聚边 —— 图里信息量最大的部分 —— 恰恰最先被藏起来。可以考虑在 hasDependencies 时放宽弹窗,或对画布做自适应缩放。
F4 —— exit_plan_mode 审批里出现两个「Yes, allow once」(既有问题,非本 PR 引入)
上面的截图可见。本 PR 没有改动 ToolApproval 的选项列表,所以这来自 daemon 为 exit_plan_mode 下发的权限选项;提出来只是因为本 PR 把这个面板放到了很显眼的位置。
我特意尝试攻击但没能攻破的点
- 环 / 自依赖 / 未知依赖 / 重复 ID —— 新的
validateTodos是标准的 Kahn 拓扑排序,全部会被拒绝且报错清晰;layerPlanTodos对遗留的成环数据也能安全降级(环上的节点被推到末层,不会死循环)。 - 子智能体污染父计划 ——
TODO_WRITE现已对 subagent 与 teammate 屏蔽,emitResult也会在params.subagentMeta时提前返回,子任务无法再覆盖父会话的可见计划。 - 失败的
todo_write—— 不再产生幽灵计划更新(if (!params.success) return,以及extractPlan(returnDisplay, succeeded ? args : undefined))。 - 没有
todo_id的agent,以及指向不存在节点的todo_id—— 都会降级到 Unassigned executions,而不是悄悄消失。
整体做得很好 —— 执行前与运行期这两块确实扎实,「每类数据一个事实来源、不新增图存储」的设计在 daemon 冷重启下也站得住。
|
@qwen-code /triage |
|
Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check. Scripted assertions: 40 passed · 0 failed · 40 total 中文 — 判定:✅ 通过 · 可合入(agent 判定)沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查。 脚本断言:40 通过 · 0 失败 · 40 总计 Verification reportPR #7580 Deep Verification —
|
| behavior | head (PR) | base (HEAD^1) |
flip? |
|---|---|---|---|
cycle a→b→a |
rejected "must not contain a cycle" | accepted | ✅ |
self-dependency a→a |
rejected "must not depend on itself" | accepted | ✅ |
unknown dependency a→ghost |
rejected "references unknown dependency" | accepted | ✅ |
| id > 500 chars | rejected "at most 500 characters" | accepted | ✅ |
| blockedBy item > 500 chars | rejected "at most 500 characters" | accepted | ✅ |
| duplicate id (control) | rejected | rejected | — (both; proves the harness discriminates) |
| valid independent plan | accepted | accepted | — (no over-rejection) |
| empty todos | accepted | accepted | — |
planId in result display |
f93d0936-… (UUID) |
absent | ✅ |
planId persisted to file |
same UUID | absent | ✅ |
planId stable across same-plan update |
f93d… == f93d… |
n/a | ✅ |
planId renewed after all-completed plan |
a75b… != f93d… |
n/a | ✅ |
Head 12/12, base 12/12. Witness: evidence/01-ab-todowrite-validation-planid.png.
A/B #2 — wire oracle: the ACP SessionUpdate the Web Shell receives
node ab-wire.mjs <dist> <head|base> drives the real compiled transcript-replay module —
the code that produces the actual plan SessionUpdate sent over the wire.
Head payload (captured live):
{"sessionUpdate":"plan","entries":[
{"content":"Task A","status":"completed","_meta":{"qwenTodo":{"id":"a"}}},
{"content":"Task B","status":"pending","_meta":{"qwenTodo":{"id":"b","blockedBy":["a"]}}}],
"_meta":{"qwenTodoPlan":{"id":"plan-XYZ"},"qwenTranscript":{"planToolCallId":"call-1"}}}Base payload (captured live):
{"sessionUpdate":"plan","entries":[
{"content":"Task A","status":"completed"},
{"content":"Task B","status":"pending"}],
"_meta":{"qwenTranscript":{"planToolCallId":"call-1"}}}| check | head | base |
|---|---|---|
wire carries qwenTodoPlan.id |
✅ present | absent |
wire carries per-entry qwenTodo.blockedBy |
✅ ["a"] |
absent |
extract round-trips planId / blockedBy |
✅ | drops both (base returns a bare array) |
non-todo resultDisplay precedence |
✅ returns null (no args fallback) | falls back to args |
Head 6/6, base 6/6. This is the smoking gun for the whole feature: on head the dependency
edge, node ids, and plan identity ride the wire; on base the entries are bare, so no DAG
could be drawn. Witness: evidence/02-ab-wire-payload-blockedby-planid.png.
Findings
No blocking findings. The central and secondary claims hold under adversarial input, and
every gate is green. Non-blocking observations a reviewer may note:
- (informational) Failed
todo_writeno longer emits a plan update.tool-call-emitter.ts
addsif (!params.success) return;. Base emitted an empty plan when a failed write's
args still held todos; head emits nothing, which preserves the last-good plan in the UI
rather than wiping it. The failure itself still surfaces through the tool result
(Todo list modification failed with error: …), so no information is lost — only an
arguably-incorrect empty-plan emission is suppressed. Covered by the 62/62 emitter suite,
which includesdoes not promote a subagent TodoWrite as the session plan(the
if (params.subagentMeta) return;guard) andshould not emit anything for TodoWriteTool with empty/no extractable todos. - (informational) The PR body cites "43 and 479 tests" for the Todo and Web Shell suites.
The 43 matchespackages/core/src/tools/todoWrite.test.tsexactly; the full
packages/web-shellsuite is actually 2690 tests / 163 files, all green — a stronger
result than the number quoted, not a discrepancy worth correcting in code.
Vacuity check — the new tests are load-bearing
Finest-grained mutation: in validateTodos, change the cycle guard
if (queue.length !== todos.length) → if (queue.length !== todos.length && false),
disabling only cycle detection while leaving every earlier guard (self-dep, unknown-dep,
duplicate-dep, oversize) intact. Result against the real source via vitest:
- mutated:
1 failed | 42 passed— exactlyshould reject a 'cycle'fails, with
validateToolParamsreturningnullwhere the test expectstoContain('must not contain a cycle'). The intended assertion fails on the behavioral mismatch (not an import/compile
break). - restored:
43/43 passed(positive control — the suite is green unmutated, so the
harness can both pass and fail).
The single-test kill with 42 siblings surviving is the signature of a correctly-pinned guard:
the cycle test asserts exactly the clause this PR added. Witness:
evidence/03-vacuity-cycle-mutation.png.
Defense-in-depth — renderer cycle-safety
The core validator rejects cycles, but the Web Shell also renders replayed / legacy
transcripts the validator never saw. I probed the real exported layerPlanTodos
(PlanExecutionView.tsx) with hostile graph shapes via a scratch vitest probe (added, run,
removed; tree confirmed clean afterward):
- 2-cycle
a↔b→ terminates, both nodes bucketed into the fallback layer. - self-loop
a→a→ ignored (filtered bydependencyId !== todo.id). - dangling ref
a→ghost→ ignored (filtered bybyId.has(...)). - 5000-node ring → layered in 9 ms (iterative Kahn's algorithm; cyclic nodes land at
maxDepth + 1, never an infinite loop).
4/4 probe assertions passed. Witness: evidence/04-cycle-safety-layerplantodos.png.
Targeted gates (all green)
| suite | result |
|---|---|
packages/core todoWrite.test.ts |
43/43 |
packages/cli PlanEmitter.test.ts + tool-call-emitter.test.ts |
82/82 (20 + 62) |
packages/acp-bridge transcript-replay.test.ts |
17/17 |
packages/web-shell utils/todos.test.ts |
77/77 |
packages/cli Session + SubAgentTracker + history-replayer |
567/567 |
packages/webui selectors + DaemonSessionProvider.subagent |
11/11 |
packages/web-shell full package (incl. 797-line PlanExecutionView.test.tsx) |
2690/2690, 163 files |
Not covered
- Per-commit attribution. The checkout is depth-2 (
git rev-parse --is-shallow-repository
= true);git rev-list --count HEAD^1..HEAD^2returns the shallow-boundary artifact1
while the metadata snapshot lists 16 commits, so the intermediate commits are unreachable.
I verified the aggregateHEAD^1..HEADdiff and did not exercise each commit's claim
individually. - Full
typecheck/lint. Covered by the PR's own CI; not re-run here. My base-side
rebuilds emitted JS despite unrelated type-declaration resolution errors from the worktree
context (@lydell/node-pty,mime/lite,fdir,ignore,ajv) — an environment artifact
of building inside a nested worktree, not a defect in the PR; the emitted base dist was
confirmed genuine base (0blockedBy/planId/qwenTodoPlanreferences vs head's 10/7/1)
before use. - Live-daemon end-to-end and the Plan Mode approval panel at the interaction level
(Reviewer Test Plan steps 1–5: approval/rejection keeping the session in Plan Mode, live
concurrent node states across a running daemon, daemon-restart recovery of a real session).
These are exercised here only at the jsdom/unit level via the web-shell suite; the reviewer
test plan's GUI flow was not driven against a real daemon. - The base-side wire replay calibration for a full historical transcript (the A/B proved
the per-update wire shape, not a byte-for-byte replay of a persisted session). - Windows / Linux rendering (PR reports macOS only).
Methodology
Environment: the CI verify sandbox (node:22-bookworm, Node v22.23.2), merge-ref checkout
at depth 2 (HEAD = merge, HEAD^1 = base tip, HEAD^2 = verified PR head). npm ci and
npm run build had completed at head before the round. A/B controls were built in a scratch
worktree at tmp/base-tree (git worktree add tmp/base-tree HEAD^1), rebuilding only
packages/core and packages/acp-bridge; the root node_modules was reused (lockfile
unchanged) with the nested packages/core/node_modules and a base-local
@qwen-code/qwen-code-core symlink added so the base dist resolved base code and the correct
ajv@8 (root ajv is v6 and lacks dist/2020.js). Harnesses (ab-todowrite.mjs,
ab-wire.mjs) import each tree's compiled dist/ by absolute path and assert both the
head-presence and base-absence of each behavior; the cycle probe and vacuity mutation ran the
real source through vitest. Raw per-cell stdout/stderr and build logs are in logs/;
harness scripts are in this directory; image witnesses are in evidence/.
Evidence images
Harness scripts and raw logs are in the workflow run artifacts (7-day retention).
— Qwen Code · sandboxed verification
|
Triage re-run completed without a new review. The bot already has a review of its own on 机器人在 The stage comments above were updated with the latest result. View workflow run. 上方各阶段评论已更新为最新结果。查看工作流运行。 |
|
Released in v0.21.3. |












What this PR does
This PR adds a Session Workflow view for ordinary daemon-backed sessions by projecting the existing Todo plan, Agent executions, and persisted transcript into one dependency graph. Todo snapshots retain a stable plan ID, stable node IDs, and optional
blockedByedges across live ACP updates and replay. Top-level Agent calls may identify the Todo node they implement throughtodo_id, while existing task call IDs provide the deterministic live-status join.Plan Mode now provides the opt-in pre-execution gate: when
exit_plan_moderequests approval, the existing approval surface shows the submitted plan together with the current Todo workflow. Approval keeps the existing permission lifecycle and starts execution; rejection leaves the session in Plan Mode. Sessions that do not use Plan Mode continue normally.During execution, the Web Shell layers dependency nodes, overlays live task state, keeps nested subagents under their root execution, and lets users inspect each step and open the existing Agent detail view. Completed sessions keep a collapsed workflow entry beside each persisted Todo snapshot. Expanding it lazily rebuilds root Agent executions from the parent transcript and lightweight descendant lineage from existing Agent sidecars; clicking a root or nested Agent loads its exact persisted virtual session, including prompt, progress, tool calls, and final output.
The implementation keeps one source of truth per concern and adds no workflow scheduler, graph store, or graph endpoint. Todo remains the business-state source, the task registry remains the live execution source, transcripts and Agent sidecars remain the durable history source, and the Web Shell only joins those streams for presentation. Historical indexes are runtime-scoped, bounded, short-lived, and explicitly report partial lineage when a safety limit is reached.
Why it's needed
Before this change, ordinary-session Todo plans, live tasks, and completed Agent details were separate views. Users could not review a dependency workflow before execution, see which Agent implemented a plan node, understand fork/join relationships while work was running, or return to the same workflow and nested Agent output after the session completed or the daemon restarted.
This preserves the existing best-effort behavior of ordinary sessions while making the plan observable before, during, and after execution. Todo completion remains authoritative; Agent state is execution evidence and never schedules, completes, retries, or unlocks Todo nodes automatically.
Reviewer Test Plan
How to verify
blockedBydependencies, and callexit_plan_modebefore launching Agents or modifying files.todo_idvalues and confirm concurrent running states appear on the corresponding nodes, nested Agents remain under their root, and incomplete dependencies display as blocked.Evidence (Before & After)
Before: Todo, Tasks, and Agent history were independent surfaces with no dependency graph, pre-execution workflow review, durable node-to-execution relationship, or completed-session workflow entry.
After — a real completed session restored after a daemon restart, including fork/join edges and nested Agent hierarchy:
After — clicking the completed nested Agent still opens its persisted prompt, metrics, progress, and final output:
Tested on
Environment (optional)
Node.js 22, local daemon and Web Shell development build, real persisted session
2c91c2b4-7896-4906-85b0-c409aa7934fe. Verified pre-execution approval, parallel and nested Agent linkage, terminal completion, daemon restart, cold transcript recovery, and completed Agent detail reopening. After merging the latestmain, full workspace lint, build, and post-build typecheck pass. The affected Todo and Web Shell suites also pass (43 and 479 tests).Risk & Scope
todo_id; durable lineage is loaded lazily and bounded, and the UI labels truncated or failed history hydration instead of presenting partial data as complete.Linked Issues
Closes #7525
中文说明
这个 PR 做了什么
这个 PR 为普通 daemon Session 增加 Session Workflow 视图,把现有 Todo 计划、Agent 执行和持久化 transcript 投影为同一张依赖图。Todo 快照在实时 ACP 更新与历史回放中保留稳定的计划 ID、节点 ID 和可选
blockedBy边。顶层 Agent 可以通过todo_id声明自己实现的 Todo 节点,现有 task call ID 则用于确定性关联实时状态。Plan Mode 现在提供可选的执行前门禁:当
exit_plan_mode请求审批时,现有审批区域会同时展示提交的计划正文和当前 Todo workflow。批准后沿用现有权限生命周期并开始执行;拒绝后仍停留在 Plan Mode。不使用 Plan Mode 的 Session 继续按原有方式执行。执行过程中,Web Shell 会对依赖节点分层、叠加实时 task 状态、把 nested subagent 保留在根执行之下,并允许用户查看每个步骤以及打开现有 Agent 详情。Session 完成后,每个持久化 Todo 快照旁仍保留一个折叠的 workflow 入口。展开时会按需从父 transcript 重建根 Agent 执行,并从现有 Agent sidecar 读取轻量后代层级;点击根 Agent 或 nested Agent 时,再加载它对应的精确持久化虚拟 Session,包括 prompt、进展、工具调用和最终输出。
实现对每类数据只保留一个事实来源,不新增 workflow 调度器、图存储或图接口。Todo 仍是业务状态来源,task registry 仍是实时执行来源,transcript 与 Agent sidecar 仍是持久历史来源,Web Shell 只负责把这些数据流关联展示。历史索引按 runtime 隔离、有界、短时缓存;达到安全上限时会明确标记只展示部分层级。
为什么需要
改动前,普通 Session 的 Todo 计划、实时 Tasks 和完成后的 Agent 详情是彼此独立的视图。用户无法在执行前审阅依赖 workflow,也无法在执行过程中确认哪个 Agent 对应哪个计划节点、理解 fork/join 关系,或者在 Session 完成与 daemon 重启后重新查看同一张 workflow 和 nested Agent 输出。
这个实现保留普通 Session 原有的 best-effort 行为,同时让计划在执行前、执行中和执行后都可观察。Todo 完成状态仍然权威;Agent 状态只是执行证据,不会自动调度、完成、重试或解锁 Todo 节点。
Reviewer Test Plan
如何验证
blockedBy依赖的 Todo 计划,并在启动 Agent 或修改文件前调用exit_plan_mode。todo_id启动互相独立的顶层 Agent,确认并发运行状态出现在对应节点,nested Agent 保留在根 Agent 下方,依赖未完成的节点显示为阻塞。证据(改动前后)
改动前:Todo、Tasks 和 Agent 历史是三个独立区域,没有依赖图、执行前 workflow 审阅、持久的节点与执行关联,也没有完成后可回看的 workflow 入口。
改动后——真实 Session 在 daemon 重启后恢复,包含 fork/join 连线与 nested Agent 层级:
改动后——点击已完成的 nested Agent,仍可打开持久化的 prompt、指标、进展和最终输出:
测试环境
环境(可选)
Node.js 22,本地 daemon 与 Web Shell 开发构建,真实持久化 Session
2c91c2b4-7896-4906-85b0-c409aa7934fe。已验证执行前审批、并行与 nested Agent 关联、终态完成、daemon 重启、冷 transcript 恢复,以及完成后重新打开 Agent 详情。合并最新main后,完整 workspace lint、build 与 build 后 typecheck 均通过;受影响的 Todo 与 Web Shell 测试也分别通过 43 与 479 个用例。风险与范围
todo_id时,Agent 与 Todo 的关联按设计保持 best effort;持久层级按需加载并设有上限,发生截断或历史加载失败时,UI 会明确提示,不会把部分数据伪装成完整结果。关联 Issue
Closes #7525