Skip to content

feat(serve): expose Workflow tasks and controls - #9546

Closed
qqqys wants to merge 27 commits into
QwenLM:mainfrom
qqqys:codex/issue-9033-workflow-daemon-api
Closed

feat(serve): expose Workflow tasks and controls#9546
qqqys wants to merge 27 commits into
QwenLM:mainfrom
qqqys:codex/issue-9033-workflow-daemon-api

Conversation

@qqqys

@qqqys qqqys commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR exposes Workflow execution through the daemon as an explicit opt-in extension of the existing session task contract. Opted-in clients can inspect live and persisted runs with phase, dispatch, token, log, approval, lineage, and terminal-state data; control active runs; delete persisted history safely; and start saved definitions. The TypeScript SDK and shared WebUI daemon adapter expose separate Workflow-aware methods while their existing task methods retain the legacy agent, shell, and monitor contract.

It also advertises Workflow availability and saved definitions at the workspace/session boundary, and applies the same enabled, non-bare, trusted-session gate to capability reporting and every Workflow mutation. The existing visual task surfaces remain unchanged.

Why it's needed

The core runtime now has structured Workflow execution state, but daemon consumers cannot observe or control it without this transport and client layer. An opt-in boundary lets new clients use that state without widening legacy task unions or forcing incomplete Workflow semantics into existing task UIs.

Reviewer Test Plan

How to verify

  1. In a trusted Workflow-enabled workspace, compare the default session task response with the opt-in response. The default response should contain only legacy task kinds; the opt-in response should additionally contain live and historical Workflow runs with their structured execution fields.
  2. Exercise cancel, pause, resume, retry, rerun, saved-definition start, and history deletion through a session owned by the calling client. Valid transitions should update the reported run; invalid actions, stale ownership, and controls against unsupported or historical-only states should fail closed.
  3. Repeat capability and control requests with Workflow disabled, in bare mode, and in an untrusted workspace. Workflow controls and built-in Workflow commands should not be advertised or executed, while an unrelated user-defined command with the same name remains compatible when the feature itself is disabled.
  4. Confirm existing SDK task calls issue no opt-in query and retain the legacy return type, while the explicit Workflow-aware calls include Workflow runs. Confirm the shared WebUI adapter follows the same separation and the current Web Shell still builds without adding visual Workflow handling.

Evidence (Before & After)

N/A — this PR adds daemon, SDK, and adapter contracts without a visual UI change.

Tested on

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

Environment (optional)

Local Node.js workspace with focused ACP, daemon, SDK, WebUI, and CLI tests, followed by the full repository build and typecheck.

Risk & Scope

  • Main risk or tradeoff: The additive Workflow snapshot is larger than the legacy task response, so it is returned only when explicitly requested and the SDK browser bundle budget is adjusted by 1 KiB.
  • Not validated / out of scope: The visual Workflow experience remains tracked by feat(web-shell): visualize and manage dynamic workflow runs #8941; Windows and Linux behavior rely on CI.
  • Breaking changes / migration notes: None. Existing task endpoints and SDK methods retain the legacy response and type unless callers use the new explicit opt-in methods.

Linked Issues

Closes #9033

Depends on #9034

中文说明

本 PR 做了什么

本 PR 通过显式 opt-in 的方式,在现有会话任务契约上扩展 daemon Workflow 执行能力。选择加入的客户端可以查看实时和持久化运行,包括阶段、调度、token、日志、审批、血缘和终态数据;可以控制活动运行、安全删除持久化历史并启动已保存的定义。TypeScript SDK 与共享 WebUI daemon adapter 提供独立的 Workflow 感知方法,现有任务方法继续保持 agent、shell、monitor 的旧契约。

同时,本 PR 在 workspace/session 边界声明 Workflow 可用性和已保存定义,并对能力声明及所有 Workflow 变更统一执行“功能启用、非 bare、可信会话”门禁。现有可视化任务界面保持不变。

为什么需要

Core runtime 现在已经具备结构化 Workflow 执行状态,但 daemon 消费端缺少观察和控制它的传输与客户端层。使用 opt-in 边界后,新客户端可以使用这些状态,同时不会扩大旧任务联合类型,也不会迫使现有任务 UI 提前承载不完整的 Workflow 语义。

Reviewer 测试计划

如何验证

  1. 在可信且启用 Workflow 的 workspace 中,对比默认会话任务响应和 opt-in 响应。默认响应应只包含旧任务类型;opt-in 响应应额外包含实时和历史 Workflow 运行及其结构化执行字段。
  2. 通过调用客户端拥有的会话执行 cancel、pause、resume、retry、rerun、启动已保存定义和删除历史。有效转换应更新运行状态;无效 action、过期 ownership、对不支持或仅历史状态的控制应 fail closed。
  3. 分别在 Workflow 关闭、bare mode 和不可信 workspace 中重复能力与控制请求。不得声明或执行 Workflow 控制和内建 Workflow 命令;当功能本身关闭时,同名但无关的用户自定义命令仍应保持兼容。
  4. 确认现有 SDK task 调用不发送 opt-in query 并保留旧返回类型,显式 Workflow 方法则包含 Workflow 运行。确认共享 WebUI adapter 采用同样的分离方式,当前 Web Shell 无需新增可视化 Workflow 处理也能正常构建。

证据(前后对比)

N/A — 本 PR 新增 daemon、SDK 和 adapter 契约,不包含可视化 UI 变化。

已测试平台

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

环境(可选)

本地 Node.js workspace,执行 ACP、daemon、SDK、WebUI 和 CLI 聚焦测试,随后执行完整仓库 build 与 typecheck。

风险与范围

  • 主要风险或权衡:新增 Workflow snapshot 比旧任务响应更大,因此只在显式请求时返回,并将 SDK browser bundle 预算增加 1 KiB。
  • 未验证或不在范围内:可视化 Workflow 体验继续由 feat(web-shell): visualize and manage dynamic workflow runs #8941 跟踪;Windows 和 Linux 行为依赖 CI。
  • 破坏性变化或迁移说明:无。除非调用方使用新的显式 opt-in 方法,否则现有任务 endpoint 和 SDK 方法继续保持旧响应与类型。

关联 Issue

Closes #9033

Depends on #9034

@github-actions github-actions Bot added the review/self-reported The linked issue was opened by the PR author (self-reported) label Aug 20, 2026
@qwen-code-ci-bot

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

Screenshots · before / after

⚠️ No preview: one or more scenarios failed to render on this head — see the workflow run. This is not "no visual change" — a scenario that times out or throws produces no image. Fix the failing scenario (or a genuine regression it caught) and the preview returns on the next push.

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

Qwen Code · web-shell visuals

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

🩺 serve daemon A/B

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

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

Qwen Code · serve A/B

@qqqys
qqqys marked this pull request as ready for review August 23, 2026 13:17
@qwen-code-ci-bot

qwen-code-ci-bot commented Aug 23, 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

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

Copy link
Copy Markdown
Collaborator

Re-run of the gate at the updated head — the verdict doesn't change, the footer does.

  • Template: complete ✓ — all required sections present, bilingual body, honest N/A on visual evidence (this is a contract PR, no UI change).
  • Problem: real and tracked, not theoretical. Issue feat(serve): expose Workflow tasks and controls #9033 (open, priority/P2, daemon, roadmap labels) asks for daemon-side observability and control of Workflow execution; the runtime half (feat(core): expose workflow execution state #9034) is already merged into main, and without this PR daemon consumers have no transport for it. A maintainer has since driven the surface on a real local stack and confirmed it behaves as described (see Stage 2).
  • Direction: aligned. Workflow is tracked roadmap work (visuals under feat(web-shell): visualize and manage dynamic workflow runs #8941), and this is its transport/client layer — not a solution looking for a problem. The opt-in boundary keeps the legacy task contract untouched; the repo's own Serve A/B check reports zero response changes against the PR base at the current head.
  • Size: core-touching feat. ~1,503 production lines vs ~2,836 test lines vs 0 generated/schema lines, spanning packages/core, cli, acp-bridge, sdk-typescript, webui. Above the 500-line maintainer-awareness bar and the 1,000-line large-PR advisory. It reads as one coherent contract end-to-end (route → dispatch → bridge → child handler → SDK → adapter), so splitting feels artificial — but flagging per policy; the size is why this PR gets a human sign-off rather than an auto-approve.
  • Approach: right scope. Every layer in the diff is needed for the stated goal; no drive-by refactors or unrelated churn found. Since the last gate pass, two fix commits landed on top: 3497d0d (the maintainer's trust-gate finding) and c25a5f9 (cross-session deletion races) — both reviewed in Stage 2.
  • Risk: Stage 1e match — packages/cli/src/acp-integration/** is a high-risk path (correlated with post-merge reverts in this repo's history). Full-depth review and CI evidence below; no elevated-risk conclusion, just where the attention went.

Moving on to code review. 🔍

中文说明

在更新后的 head 上重跑门禁——结论不变,页脚更新。

  • 模板:完整 ✓ —— 必填小节齐全,正文双语,可视化证据诚实地写了 N/A(这是契约 PR,无 UI 变化)。
  • 问题:真实且已有跟踪,不是理论问题。Issue feat(serve): expose Workflow tasks and controls #9033(开放中,priority/P2daemon、roadmap 标签)要求 daemon 侧可观测并控制 Workflow 执行;运行时部分(feat(core): expose workflow execution state #9034)已合入 main,没有本 PR,daemon 消费端就没有对应的传输层。此后维护者已在真实本地栈上驱动该面并确认行为与描述一致(见 Stage 2)。
  • 方向:对齐。Workflow 是有跟踪的路线图工作(可视化在 feat(web-shell): visualize and manage dynamic workflow runs #8941),本 PR 是它的传输/客户端层——不是"为方案找问题"。opt-in 边界保持旧任务契约不变;仓库自己的 Serve A/B 检查在当前 head 上报告相对 PR base 零响应变化。
  • 规模:触及核心的 feat。约 1,503 行生产代码、约 2,836 行测试代码、0 行生成/schema 代码,横跨 packages/corecliacp-bridgesdk-typescriptwebui。超过 500 行维护者关注线和 1,000 行大 PR 提示线。它读起来是一个端到端的完整契约(route → dispatch → bridge → child handler → SDK → adapter),拆分显得刻意——但按政策标记;这个规模正是本 PR 需要人工签核而非自动批准的原因。
  • 方案:范围合理。diff 里每一层都是目标所必需;未发现顺手重构或无关改动。上次门禁后新增两个修复提交:3497d0d(维护者的信任门禁问题)和 c25a5f9(跨会话删除竞态)——均在 Stage 2 中审查。
  • 风险:Stage 1e 命中 —— packages/cli/src/acp-integration/** 是高风险路径(与本仓库合并后回滚的历史相关)。下方是完整深度的审查与 CI 证据;没有升级风险的结论,只是注意力集中在这里。

进入代码审查。🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review — re-run on e80f785, focused on what landed since the last pass

The shape of this PR matches my independent proposal from the first run (opt-in flag on the existing session-tasks surface, one control route, separate Workflow-aware SDK/adapter methods, a single enabled+non-bare+trusted predicate), so this pass concentrates on the two commits that landed after my last review and after @wenshao's real-stack verification: the trust-gate fix (3497d0d) and the deletion-race fix (c25a5f9).

The maintainer's Finding 1 (untrusted workspace still advertised and executed workflows) is fixed the way the verification suggested. The daemon boundary now enforces its own trust verdict on every Workflow surface, in both transports:

  • REST (routes/session.ts): /supported-commands is redacted (workflowsEnabled:false, savedWorkflows:[], built-in /workflows removed, other commands untouched); tasks?includeWorkflows=true is coerced to false before it reaches the bridge; workflow-action answers {changed:false}; cancel kind:workflow answers {cancelled:false, reason:'disabled'} — all keyed on runtime.trusted, checked before any bridge call.
  • ACP-HTTP (acp-http/dispatch.ts): the same four methods gated on isWorkspaceTrusted(), which is wired to runtime.trusted at every real construction site (server.ts, all three run-qwen-serve.ts runtime paths) — the same predicate family /capabilities already uses, so advertised capability and session gate can no longer diverge on trust.
  • The child-side canUseWorkflowControls trust clause stays as defense in depth for direct qwen --acp use; its production blind spot (folder-trust feature never set in packages/cli) no longer matters through the daemon, because the boundary redacts first.
  • Regression tests pin the exact production shape on both transports (server.test.ts, acp-http/transport.test.ts): an untrusted daemon runtime yields the fail-closed responses above and proves the mutation and the includeWorkflows opt-in never reach the child at all. That is the test the verification asked for.

c25a5f9 closes the cross-session deletion races. All sessions share one snapshot store but keep private registries, so deletion now (1) refuses while any sibling registry owns a live or still-settling run (isWorkflowRunLiveOutsideSession), (2) drains the owner handle before deleting, (3) drops sibling terminal entries on success — which also resolves the maintainer's non-blocking observation 3 — and (4) retires the unpersisted history cache only once writeWorkflowSnapshot reports success, so a sibling deletion can no longer resurrect a run. A dozen new tests in Session.test.ts name each race individually.

Everything I could only read statically last time now carries live evidence. @wenshao drove a real daemon stack at e139338 (the commit just before these two fixes): 44/44 control-lifecycle checks, fail-closed inputs, active-work hold, restart/history semantics, SDK-to-base backward compatibility, bare mode, settings reload. The commits since then touch only the gate and the deletion path; the repo's own Serve A/B check at the current head reports no response changes against the PR base across 12 scenarios, so the legacy contract still holds at e80f785. The maintainer's remaining non-blocking observations (label filled ~100 ms after run-saved, optional X-Qwen-Client-Id header, concurrent rerun semantics, completion-notification visibility) remain as noted; none are leaks or correctness issues.

One residual note from reading the gate: the child's sessionTasks ext-method honors includeWorkflows without its own gate check. Through the daemon that is fine — the boundary already coerces it. On direct qwen --acp it only reads the invoking user's own snapshot store, so no boundary is crossed; naming it so nobody later "fixes" the wrong layer.

Files changed (27 of 48 shown — production files)
File What changed
packages/cli/src/serve/workflow-session-gate.ts New: daemon-boundary redaction helper for supported-commands (the trust fix)
packages/cli/src/serve/routes/session.ts Gates supported-commands, the tasks opt-in, cancel and the new workflow-action route on runtime.trusted
packages/cli/src/serve/acp-http/dispatch.ts Same four gates for the ACP-HTTP methods; registers the two new vendor methods
packages/cli/src/serve/routes/capabilities.ts Advertises per-workspace workflowsEnabled (enabled, trusted, kill-switch aware)
packages/cli/src/serve/run-qwen-serve.ts Carries tools.workflowsEnabled into runtime env metadata, including settings reload
packages/cli/src/serve/server.ts Wires daemon env into the capabilities route
packages/cli/src/serve/types.ts workflowsEnabled on the capabilities envelope
packages/cli/src/serve/server/telemetry.ts Registers the new route in the telemetry catalog (drift guard updated)
packages/cli/src/serve/workspace-registry.ts workflowsEnabledBySettings metadata field
packages/cli/src/acp-integration/acpAgent.ts Child handlers: control actions, run-saved, retry/rerun lineage, child gate, cross-session liveness
packages/cli/src/acp-integration/session/Session.ts History cache, race-safe deletion, workflow holds and completion notifications
packages/cli/src/acp-integration/session/tasksSnapshot.ts Projects live and persisted workflow runs only when opted in
packages/core/src/agents/runtime/workflow-runner.ts Notifies the registry when a terminal snapshot is persisted
packages/core/src/agents/workflow-run-registry.ts removeTerminal and the snapshot-persisted callback
packages/core/src/agents/workflow-snapshot.ts writeWorkflowSnapshot reports whether it persisted
packages/core/src/tools/workflow/workflow.ts buildSessionOwnedBackground for daemon-started runs
packages/acp-bridge/src/bridge.ts includeWorkflows opt-in and the workflow-action bridge method, clientId checks
packages/acp-bridge/src/bridgeTypes.ts Contract types; workflow joins the active-work hold categories
packages/acp-bridge/src/status.ts Workflow status/event/approval types and the new control ext-method
packages/sdk-typescript/src/daemon/DaemonClient.ts sessionWorkflowTasks and sessionWorkflowTaskAction; legacy methods unchanged
packages/sdk-typescript/src/daemon/DaemonSessionClient.ts workflowTasks and controlWorkflowTask session helpers
packages/sdk-typescript/src/daemon/acpRouteTable.ts Maps the cancel and workflow-action routes and the includeWorkflows query
packages/sdk-typescript/src/daemon/types.ts Workflow types; the legacy DaemonSessionTaskStatus union is untouched
packages/sdk-typescript/scripts/build.js Browser bundle budget +1 KiB for the added surface
packages/webui/src/daemon/session/actions.ts Adapter actions: getWorkflowTasks, controlWorkflowTask, runSavedWorkflow
packages/webui/src/daemon/session/types.ts Action contract and notice operations
…and 21 more files Collocated tests covering every layer above

Test evidence

This is an unattended CI run — I did not build or execute any PR code. The evidence below is the PR's own CI at the reviewed commit, fetched via the API, plus the bot signals that corroborate it.

Check Conclusion
Qwen Code CI · Test (ubuntu-latest, Node 22.x) ✅ success
Qwen Code CI · Test (macos-latest / windows-latest, Node 22.x) ⏭️ skipped
Qwen Code CI · Integration Tests (CLI, No Sandbox) ⏭️ skipped
Serve A/B (ubuntu-latest, Node 22.x) ✅ success — 0 response changes vs PR base, 12 scenarios
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Desktop Shell (ubuntu-22.04 / windows-2022) ✅ success
Live Host (macos-latest) ✅ success
Security Checks · Dependency CVE audit · Secret scan (TruffleHog) · precheck-pr ✅ success
SDK Java · Java 11 and Java 21 legs (ubuntu / macos / windows) ✅ success
SDK Java · ubuntu-latest / Java 17 ⚠️ cancelled
Real daemon E2E / Java 11 ✅ success

The skips are not this PR's doing: the macOS/Windows unit legs and the no-sandbox integration leg are skipped identically on recently merged PRs (compared against #10059) — that is this repo's standard CI shape. The Java 17 cancellation died during runner setup ("A task was canceled" while downloading actions, before any checkout); its Java 11 and Java 21 siblings in the same workflow are green, so I classify it as runner-preemption noise rather than PR-caused.

Not verified: a live re-run of the untrusted-workspace arm against e80f785. The maintainer's stack verification proved every behavior at e139338 except the one that was broken there, and the fix commits since are pinned only by the new automated tests. Sandboxed verification would settle it: @qwen-code /verify — that the untrusted workspace now fails closed live (capability, supported-commands, all control actions, and the includeWorkflows read path), which the diff and the unit suite argue for but nobody has executed end-to-end on this head.

中文说明

代码审查 —— 在 e80f785 上重跑,聚焦上次审查之后落地的改动

本 PR 的形态与我第一轮独立提出的方案一致(在现有会话任务面上 opt-in、一条控制路由、独立的 Workflow 感知 SDK/adapter 方法、单一的"启用+非 bare+可信"谓词),因此本轮集中在上次审查之后、也是在 @wenshao 真实栈验证之后落地的两个提交:信任门禁修复(3497d0d)与删除竞态修复(c25a5f9)。

维护者的问题 1(不受信工作区仍宣告并执行 workflow)已按验证评论建议的方式修复。 daemon 边界现在用自己的信任判定强制管控每个 Workflow 面,两种传输都覆盖:

  • REST(routes/session.ts):/supported-commands 被脱敏(workflowsEnabled:falsesavedWorkflows:[]、移除内建 /workflows、其余命令不受影响);tasks?includeWorkflows=true 在到达 bridge 前被强制改为 falseworkflow-action 返回 {changed:false}cancel kind:workflow 返回 {cancelled:false, reason:'disabled'} —— 全部基于 runtime.trusted,且在任何 bridge 调用之前检查。
  • ACP-HTTP(acp-http/dispatch.ts):同样四个方法以 isWorkspaceTrusted() 做门,该谓词在所有真实构造点(server.tsrun-qwen-serve.ts 的三条 runtime 路径)都接到 runtime.trusted —— 与 /capabilities 用的是同一谓词族,因此"宣告的能力"与"会话门"不会在信任上再分歧。
  • 子进程侧 canUseWorkflowControls 的信任子句保留为直连 qwen --acp 场景的纵深防御;它在生产中的盲点(packages/cli 从不设置 folder-trust feature)经由 daemon 已不再重要,因为边界先行脱敏。
  • 回归测试在两种传输上钉住了生产真实形态(server.test.tsacp-http/transport.test.ts):不受信的 daemon runtime 得到上述失败关闭响应,并且证明变更请求与 includeWorkflows opt-in 根本没有到达子进程。这正是验证评论要求补的测试。

c25a5f9 关闭了跨会话删除竞态。 所有会话共享同一个快照存储但各自持有私有 registry,因此删除现在:(1) 任一兄弟 registry 仍持有活动或未落盘运行则拒绝(isWorkflowRunLiveOutsideSession);(2) 删除前排空属主 handle;(3) 成功后移除兄弟终态条目——这同时解决了维护者的非阻塞观察 3;(4) 仅在 writeWorkflowSnapshot 报告成功后才清掉未持久化的历史缓存,兄弟删除不会再"复活"运行。Session.test.ts 里十几个新测试逐一钉住每个竞态。

上次只能静态阅读的部分现在有了实证。 @wenshaoe139338(这两个修复之前的那个提交)上驱动了真实 daemon 栈:44/44 控制生命周期检查、失败关闭输入、活动工作保持、重启/历史语义、SDK 对 base 的向后兼容、bare 模式、设置热加载。此后的提交只触及门禁与删除路径;仓库自己的 Serve A/B 检查在当前 head 上报告相对 PR base 零响应变化,即旧契约在 e80f785 依然成立。维护者其余非阻塞观察(run-saved 后约 100 ms 才填 label、X-Qwen-Client-Id 头可选、并发 rerun 语义、完成通知可见性)维持原议;均非泄漏或正确性问题。

阅读门禁时的一条遗留备注:子进程的 sessionTasks ext-method 不带自身门禁地接受 includeWorkflows。经 daemon 时没问题——边界已强制改写。直连 qwen --acp 时它只读取发起用户自己的快照存储,不跨越任何边界;写出来是为了防止以后有人"修"错层。

测试证据

这是无人值守的 CI 运行——我没有构建或执行任何 PR 代码。下方证据是审查提交上 PR 自己的 CI(经 API 获取),以及相互印证的 bot 信号。CI 表格见英文部分(机器可读区域,由 finalize 工作流在 CI 落定后就地更新)。

跳过的检查不是本 PR 造成的:macOS/Windows 单测腿与 no-sandbox 集成腿在最近已合并的 PR(对照 #10059)上同样被跳过——这是本仓库的标准 CI 形态。Java 17 的取消死于 runner 初始化阶段(下载 actions 时 "A task was canceled",尚在任何 checkout 之前);同一工作流的 Java 11 与 Java 21 腿为绿,故判定为 runner 抢占噪音而非 PR 所致。

未验证:在 e80f785 上实跑不受信工作区臂。 维护者的栈验证在 e139338 上证明了除当时坏掉的那一项之外的所有行为,而其后的修复提交目前只有新自动化测试钉住。沙箱验证可以定案:@qwen-code /verify —— 不受信工作区现在是否真的失败关闭(能力宣告、supported-commands、全部控制动作、includeWorkflows 读取路径),diff 与单测都支持这一结论,但还没有人在这个 head 上端到端执行过。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — the review itself is clean (the maintainer's one blocking finding is fixed exactly as prescribed and pinned by tests on both transports), but a core-touching feat at ~1,500 production lines is policy-escalated, so this gets a human sign-off rather than an auto-approve.

Stepping back over both runs: the approach is the one I would have chosen — opt-in on the existing task surface, one control route, separate SDK/adapter methods, one predicate at the daemon boundary. The maintainer's real-stack verification at e139338 is stronger evidence than anything this gate can produce on its own: 44/44 lifecycle checks, fail-closed inputs, hold semantics, history and restart behavior, SDK/base backward compatibility. The only thing broken there — the trust half of the gate — is now enforced at exactly the layer the verification named, with the exact response shapes it suggested, and with regression tests asserting the bridge is never reached on an untrusted runtime. The follow-up race fixes hold to the same standard: each race has a named test.

Why defer instead of approve, then:

  • Policy, not doubt. Stage 0 escalates core-touching features past 500 production lines to the maintainer; this one is ~3× that and spans five packages. The guardrail caps my confidence at 3/5 and withholds the bot approval regardless of how clean the stages look.
  • One live gap remains. Everything was executed end-to-end at e139338 except the fix itself, which is pinned by automated tests but not yet by a live untrusted-workspace run at e80f785. @qwen-code /verify would close that — Stage 2 names the exact claim.
  • The standing CHANGES_REQUESTED is this bot's own — a round-5 /review artifact from 2026-08-25 listing Suggestion- and probe-level coverage gaps, none called blocking in its own body. I'm not dismissing it (that belongs to the maintainer alongside the sign-off), and I'm not approving over it either.

@wenshao — you verified this stack on real daemons and named the one thing that needed fixing; that fix and its regression tests are now in. Ready for your sign-off (or a /verify run to settle the last live gap) whenever you are.

中文说明

置信度:3/5 —— 审查本身是干净的(维护者唯一的阻塞性问题已按其建议的方式修复,并由两种传输上的测试钉住),但触及核心、约 1,500 行生产代码的 feat 按政策需升级给维护者,因此走人工签核而非自动批准。

退后看两轮审查:方案正是我会选的那一个——在现有任务面上 opt-in、一条控制路由、独立的 SDK/adapter 方法、daemon 边界一个谓词。维护者在 e139338 上的真实栈验证比这个门禁自己能产生的任何证据都强:44/44 生命周期检查、失败关闭输入、hold 语义、历史与重启行为、SDK 对 base 的向后兼容。当时唯一坏掉的部分——门禁的信任那一半——现在恰好在验证点名的那一层强制执行,用的是它建议的响应形态,且回归测试断言不受信 runtime 下根本不会触达 bridge。后续的竞态修复维持同一标准:每个竞态都有点名的测试。

那为什么是暂缓而不是批准:

  • 是政策,不是疑虑。 Stage 0 将超过 500 行生产代码的核心 feat 升级给维护者;本 PR 约为其三倍且横跨五个包。护栏把我的置信度封顶在 3/5,无论各阶段看起来多干净都不给出 bot 批准。
  • 仍有一个实证缺口。 除修复本身外,一切行为都已在 e139338 上端到端执行过;修复目前只有自动化测试钉住,还没有在 e80f785 上实跑过不受信工作区场景。@qwen-code /verify 可以补上——Stage 2 写明了具体主张。
  • 挂起的 CHANGES_REQUESTED 来自本 bot 自己 —— 是 2026-08-25 第 5 轮 /review 的产物,列的是 Suggestion 与 probe 级别的覆盖缺口,其正文未称任何一项为阻塞。我不会撤销它(那与签核一起属于维护者的决定),也不会在它之上批准。

@wenshao —— 你在真实 daemon 上验证了这个栈并点名了唯一需要修的问题;该修复及其回归测试现已就位。随时可以给出你的签核(或跑一次 /verify 补上最后一个实证缺口)。

Qwen Code · qwen3.8-max

Reviewed at e80f78518601e27eec4408c577ffa21ff9bf144a · 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.

Partially reviewed — gaps disclosed.

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

中文说明

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

未审查: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.22.0)

Comment thread packages/cli/src/serve/run-qwen-serve.ts
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
Comment thread packages/cli/src/acp-integration/acpAgent.ts
});
});

it('restores persisted workflow runs as read-only task history', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] No test exercises the legacy persisted-snapshot shape (absent phaseVisits / dispatches / events) that serializeWorkflowSnapshot carries explicit ?? [] / ?.map guards for — every fixture in the new suite supplies all three fields (phaseVisits/dispatches guards are exercised by no test). Probe: removing the two ?? [] guards survives 14/14 tests. Shipped, one pre-graph-tracing <projectDir>/workflows/wf_*.json on disk passes isWorkflowSnapshot (which deliberately accepts the absent fields), reaches serializeWorkflowSnapshot, throws TypeError: Cannot read properties of undefined (reading 'map') inside the tasks array composition — and the entire session-tasks response fails for every includeWorkflows=true poller: agent, shell, and monitor tasks disappear with it. Part of a pattern: the workflow serializer/projection guards lack pinning tests.

Fix: in the new workflow graph block, pass a snapshot with the three keys deleted plus { includeWorkflows: true }, asserting it still serializes as a historical task with empty phaseVisits/dispatches and no events field.

中文说明

[Suggestion] 没有测试覆盖旧版持久化快照形态(缺少 phaseVisits / dispatches / events),而 serializeWorkflowSnapshot 明确带着 ?? [] / ?.map 守卫——新套件中的每个 fixture 都提供了全部三个字段(phaseVisits/dispatches 守卫没有任何测试执行到)。探针:移除两个 ?? [] 守卫后 14/14 个测试存活。若带病上线,磁盘上一个图形追踪之前的 <projectDir>/workflows/wf_*.json 会通过 isWorkflowSnapshot(它刻意接受缺失字段),进入 serializeWorkflowSnapshot,在 tasks 数组组装过程中抛出 TypeError: Cannot read properties of undefined (reading 'map')——整个 session-tasks 响应对每个 includeWorkflows=true 轮询者失败:agent、shell、monitor 任务一并消失。这是一个模式:workflow 序列化/投影守卫缺少锁定测试。

修复:在新的 workflow graph 块中,传入删除了这三个键的快照并附带 { includeWorkflows: true },断言它仍能序列化为历史任务:phaseVisits/dispatches 为空且没有 events 字段。

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

Comment on lines +7974 to +7979
expect(tasks).toMatchObject({
sessionId,
tasks: [
{
kind: 'workflow',
id: 'wf_saved',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This assertion uses loose toMatchObject while the fixture deliberately carries script: 'return 1;' — it cannot catch a leak of the snapshot's script/scriptPath fields, which serializeWorkflowSnapshot intentionally does not project. Probe: adding script/scriptPath to that serializer passes all 14 workflow tests — the full workflow source plus an absolute filesystem path would flow to every daemon/SDK/WebUI client calling sessionTasks with includeWorkflows=true, and this test stays green (toMatchObject ignores extra properties). The sibling capabilities assertion in the same test is strict toEqual (it would catch a scriptPath leak in savedWorkflows), and the file already has the house pattern for absence checks (expect(JSON.stringify(tasks)).not.toContain('abortController')). Part of a pattern: the workflow serializer/projection guards lack pinning tests.

Fix: after the toMatchObject block, add expect(JSON.stringify(tasks)).not.toContain('return 1;'); and expect(JSON.stringify(tasks)).not.toContain('scriptPath');.

中文说明

[Suggestion] 该断言使用宽松的 toMatchObject,而 fixture 特意携带了 script: 'return 1;'——它无法捕获快照 script/scriptPath 字段的泄漏,而 serializeWorkflowSnapshot 是刻意不投影这两个字段的。探针:把 script/scriptPath 加入该序列化器后,全部 14 个 workflow 测试通过——完整 workflow 源码加绝对文件系统路径会流向每个以 includeWorkflows=true 调用 sessionTasks 的 daemon/SDK/WebUI 客户端,而该测试保持绿色(toMatchObject 忽略多余属性)。同一测试中兄弟的 capabilities 断言是严格 toEqual(能捕获 savedWorkflows 中的 scriptPath 泄漏),且本文件已有"缺失性检查"的惯用写法(expect(JSON.stringify(tasks)).not.toContain('abortController'))。这是一个模式:workflow 序列化/投影守卫缺少锁定测试。

修复:在 toMatchObject 块之后增加 expect(JSON.stringify(tasks)).not.toContain('return 1;');expect(JSON.stringify(tasks)).not.toContain('scriptPath');

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

});

describe('buildSessionTasksStatus workflow graph', () => {
it('omits workflow tasks unless the caller opts in', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] The opt-out test only exercises the persisted-history branch: configWith([]) leaves the workflow registry empty, so the test cannot detect a regression that drops the includeWorkflows gate from the live-registry mapping spread in buildSessionTasksStatus. Probe: ungating the live-registry read survives 10/10 tests; giving this test a non-empty registry fails under the mutant and passes on this PR's code. Shipped, default GET /session/<id>/tasks pollers that never pass includeWorkflows (pre-feature SDK/webui consumers) start receiving live kind: 'workflow' tasks — an unknown task kind and unrequested payload — with no test in the repo failing. Part of a pattern: the workflow serializer/projection guards lack pinning tests.

Fix: give the opt-out test a non-empty registry as well — reuse the graph-test's live-task fixture alongside [workflowSnapshot()], keeping expect(snapshot.tasks).toEqual([]).

中文说明

[Suggestion] opt-out 测试只覆盖了持久化历史分支:configWith([]) 让 workflow registry 为空,因此该测试无法检测到"从 buildSessionTasksStatus 的实时 registry 映射展开中移除 includeWorkflows 门禁"的回归。探针:解除实时 registry 读取的门禁后 10/10 个测试存活;给该测试一个非空 registry 后,突变体下失败、本 PR 代码下通过。若带病上线,从不传 includeWorkflows 的默认 GET /session/<id>/tasks 轮询者(功能上线前的 SDK/webui 消费者)会开始收到实时的 kind: 'workflow' 任务——未知任务类型、未被请求的载荷——而仓库中没有任何测试失败。这是一个模式:workflow 序列化/投影守卫缺少锁定测试。

修复:让 opt-out 测试也带上非空 registry——复用 graph 测试的实时任务 fixture,与 [workflowSnapshot()] 并存,保持 expect(snapshot.tasks).toEqual([])

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

Comment on lines +4281 to 4282
workflowsEnabled: false,
kind: 'live',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This new workflowsEnabled assertion resolves against the host's process.env: the harness injects no daemonEnv dep and the fake runtime uses env: { mode: 'parent-process' }, so workflowsEnabledForRuntime falls back to deps.daemonEnv ?? process.env (server.ts). A developer or CI host exporting QWEN_CODE_ENABLE_WORKFLOWS=1 (dogfooding workflows, or a workflow-exercising CI job) flips the advertisement to true and this exact-equality assertion fails spuriously. Probe: clean env — passes; QWEN_CODE_ENABLE_WORKFLOWS=1 — fails with - "workflowsEnabled": false / + "workflowsEnabled": true. The same class affects the envelope assertions this PR added in multi-workspace-sessions.test.ts (makeHarness is likewise unshielded); the author already applied the shield elsewhere in this diff (daemonEnv: {} in the dynamic-workspace test, explicit daemonEnv in the new capabilities test).

Fix: pass daemonEnv: {} in the createServeApp deps here and in makeHarness in multi-workspace-sessions.test.ts.

中文说明

[Suggestion] 这个新增的 workflowsEnabled 断言依赖宿主机的 process.env:测试 harness 未注入 daemonEnv 依赖,且假运行时使用 env: { mode: 'parent-process' },因此 workflowsEnabledForRuntime 回退到 deps.daemonEnv ?? process.env(server.ts)。任何导出 QWEN_CODE_ENABLE_WORKFLOWS=1 的开发者或 CI 宿主(内测 workflows,或运行 workflow 相关套件的 CI 任务)都会把声明翻转为 true,使这个精确相等断言虚假失败。探针:干净环境——通过;QWEN_CODE_ENABLE_WORKFLOWS=1——失败,- "workflowsEnabled": false / + "workflowsEnabled": true。同类问题也影响本 PR 在 multi-workspace-sessions.test.ts 中新增的 envelope 断言(makeHarness 同样未屏蔽);作者已在本 diff 的其他地方应用了屏蔽(动态工作区测试中的 daemonEnv: {}、新 capabilities 测试中的显式 daemonEnv)。

修复:在此处与 multi-workspace-sessions.test.ts 的 makeHarness 中,给 createServeApp 的 deps 传入 daemonEnv: {}

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

Comment on lines +3151 to +3153
getWorkflowHistory(): readonly WorkflowSnapshot[] {
return this.workflowHistory;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] getWorkflowHistory() has zero production callers (grep across all packages: only the definition, Session.test.ts, and the acpAgent.test.ts session mock), and the live cache fed by #rememberWorkflowHistory is never observably consumed — every production read path re-derives from disk first (buildSessionTasksStatusrefreshWorkflowHistory(); deleteWorkflowHistory also refreshes before its membership check). Meanwhile every ACP session/new pays an unconditional listWorkflowSnapshots(config) preload — readdir plus sequential read + JSON.parse + schema validation of every retained snapshot — even when workflows are disabled; the new test pins that unconditionality with toHaveBeenCalledTimes(2) while running with isWorkflowsEnabled: false. The Session.test.ts cache test blesses live-cache behavior no production path can observe: a maintainer investigating "completed runs missing from history" sees green cache machinery and concludes live updates are wired, and future edits to the cache pass tests while changing nothing user-visible.

Fix: either route a production read through the cache (e.g. the tasks projection uses getWorkflowHistory() between refreshes) or drop the preload + constructor parameter + getWorkflowHistory() and derive purely from the disk listing; align the tests with whichever shape ships. If the accessor is intended for an imminent caller, name it in this PR.

中文说明

[Suggestion] getWorkflowHistory() 没有任何生产调用方(跨所有包 grep:只有定义、Session.test.ts 和 acpAgent.test.ts 的会话 mock),且 #rememberWorkflowHistory 维护的实时缓存从未被可观测地消费——每个生产读取路径都先从磁盘重新推导(buildSessionTasksStatusrefreshWorkflowHistory()deleteWorkflowHistory 在成员检查前也会 refresh)。与此同时,每个 ACP session/new 都会支付一次无条件的 listWorkflowSnapshots(config) 预加载——readdir 加逐个 read + JSON.parse + 每个保留快照的 schema 校验——即使 workflows 未启用;新测试还在 isWorkflowsEnabled: false 下以 toHaveBeenCalledTimes(2) 锁定了这种无条件性。Session.test.ts 的缓存测试为一条生产路径不可观测的实时缓存行为背书:维护者排查"已完成运行从历史中消失"时看到绿色的缓存机制会认为实时更新已接通;未来对缓存的改动能通过测试,却不改变任何用户可见行为。

修复:或者让某个生产读取走缓存(例如任务投影在两次 refresh 之间使用 getWorkflowHistory()),或者移除预加载 + 构造函数参数 + getWorkflowHistory()、完全从磁盘列表推导;让测试与实际落地的形态对齐。如果该访问器是为即将到来的调用方准备的,请在本 PR 中指明。

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

@qqqys
qqqys dismissed qwen-code-ci-bot’s stale review August 23, 2026 19:35

Superseded by subsequent commit; current head requires re-review.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

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

中文说明

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

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

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

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

Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
Comment on lines +11376 to +11377
const task = registry.get(taskId);
if (!task) return { changed: false };

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R2-3: retry/rerun resolve the target only from the in-memory registry, so snapshot-only runs (after a daemon restart or terminal eviction at 10 retained entries) are listed by includeWorkflows yet fail closed with a bare {changed:false} carrying no reason — indistinguishable from a disabled control or a precondition miss, while the sibling cancel path returns reason:'not_found'|'not_running' for the same kind. The refusal itself matches the PR's documented contract ("controls against historical-only states should fail closed") and WorkflowSnapshot has no args field, so the gap is only the missing reason code, not the refusal.

Concrete cost: after a daemon restart, a previously failed run appears in sessionTasksStatus(includeWorkflows=true) with status:'failed'; a client sending {action:'retry', taskId:'wf_x'} gets bare {changed:false} and cannot tell unknown/historical from precondition-not-met, while delete-history succeeds for the same run — an asymmetry visible inside this one endpoint. Fix: return {changed:false, reason:'not_found'} on the registry miss, mirroring the cancel path.

中文说明

retry/rerun 只从内存 registry 解析目标,因此仅有快照的运行(daemon 重启后或终态条目超过 10 条被逐出后)会被 includeWorkflows 列出,但控制请求只得到不带 reason 的 {changed:false}——与"功能被禁用"或"前置条件不满足"无法区分;而同为 workflow 的 cancel 分支会返回 reason:'not_found'|'not_running'。拒绝本身符合 PR 文档约定("对仅历史状态的控制应 fail closed"),且 WorkflowSnapshot 没有 args 字段,因此缺口只在缺少 reason 码,而不在于拒绝行为。修复:对 registry 未命中返回 {changed:false, reason:'not_found'},与 cancel 分支对齐。

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

Comment on lines +11342 to +11344
if (action === 'delete-history') {
return { changed: await session.deleteWorkflowHistory(taskId) };
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R2-4: A successful delete-history never removes the run's live WorkflowRunRegistry entry — the registry has no single-entry removal and retains up to 10 terminal entries — so the deleted run stays listed by includeWorkflows (serializeWorkflowTask applies no status filter over registry.list()), the retry preconditions still pass for it, and retry re-registers the same runId: WorkflowJournal.load tolerates the deleted journal (empty replay), and settlement re-persists the snapshot — the deleted run is resurrected.

Observed by probe: after simulated delete-history with empty persisted history, the terminal entry still appears in the includeWorkflows listing ([{"id":"wf_ffff0000","status":"failed"}]); after retry settlement the snapshot is back on disk. The residue is in-process only (self-heals on restart) and resurrection needs an explicit client retry, but the endpoint reports {changed:true} while the same server keeps serving the run. Fix: give the registry a guarded terminal-entry removal (only non-active entries with no handle) and call it from Session.deleteWorkflowHistory after the disk delete succeeds.

中文说明

delete-history 成功后不会移除运行在 WorkflowRunRegistry 中的存活条目——registry 没有单条移除能力且最多保留 10 条终态条目——因此被删除的运行仍会出现在 includeWorkflows 列表中(serializeWorkflowTaskregistry.list() 不做状态过滤),retry 前置条件对其仍然成立,retry 会用同一 runId 重新注册:WorkflowJournal.load 容忍已删除的 journal(空重放),结算时快照重新落盘——被删除的运行复活。

探针观察:模拟 delete-history(持久化历史为空)后,终态条目仍在 includeWorkflows 列表中;retry 结算后快照重新出现在磁盘上。残留仅在进程内(重启即自愈),复活也需要客户端显式 retry,但该端点在服务器仍在提供该运行的情况下返回了 {changed:true}。修复:为 registry 增加受保护的终态条目移除(仅限无 handle 的非活跃条目),并在磁盘删除成功后由 Session.deleteWorkflowHistory 调用。

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

Comment on lines +11294 to +11296
const handle = registry.getHandle(taskId);
registry.cancel(taskId, Date.now());
if (handle) await handle.completion;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R2-5: The cancel test resolves the mock completion promise independently of the cancel call, so nothing pins that production must call registry.cancel() BEFORE await handle.completion here. In production the ordering is load-bearing: a paused run never settles naturally — WorkflowRunRegistry.cancel() is what terminates it (it aborts the run's controller/handle).

Probe witness: swapping the two statements leaves all 471 existing acpAgent tests green; an ordering probe whose completion settles only when cancel fires deadlocks under the swap (15s timeout) and passes on clean code. Shipped, the swap would hang cancellation of paused workflows until client timeout. Fix: make completion causally depend on cancel in the test — resolve inside the mock (const cancel = vi.fn(() => { task.status = 'cancelled'; resolveCompletion(); });) and delete the standalone resolveCompletion() call; under the swapped order nothing resolves completion and the test fails.

中文说明

cancel 测试中 mock 的 completioncancel 调用相互独立地解决,因此没有任何测试钉住此处必须先调用 registry.cancel()await handle.completion 的顺序。生产中该顺序是承重的:paused 运行不会自然结束——只有 WorkflowRunRegistry.cancel()(中止其 controller/handle)能使其结算。

探针证据:交换这两条语句后现有 471 个 acpAgent 测试全部通过;一个"只有 cancel 触发才结算 completion"的顺序探针在交换后死锁(15s 超时),在正确代码下通过。若交换被合入,暂停态 workflow 的取消会挂起直到客户端超时。修复:让测试中的 completion 因果依赖于 cancel——在 mock 内部解决(见上),并删除独立的 resolveCompletion() 调用。

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

});
});

it('restores persisted workflow runs as read-only task history', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R1-21: Still stands at 135bf45 — tasksSnapshot.test.ts was not touched this round; no test exercises the legacy persisted-snapshot shape (absent phaseVisits / dispatches / events) that serializeWorkflowSnapshot carries explicit ?? [] / ?.map guards for — every fixture supplies the fields.

中文说明

R1-21 在 135bf45 仍然存在——本轮未改动 tasksSnapshot.test.ts;依旧没有测试覆盖旧版持久化快照形态(缺少 phaseVisits / dispatches / events),而 serializeWorkflowSnapshot 为此显式写了 ?? [] / ?.map 守卫——所有 fixture 都提供了这些字段。

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

tasks: [
{
kind: 'workflow',
id: 'wf_saved',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R1-22: Still stands at 135bf45 — the persisted-snapshot assertion still uses loose toMatchObject while the fixture deliberately carries script: 'return 1;'; it cannot catch a leak of the snapshot's script/scriptPath fields, which serializeWorkflowSnapshot intentionally omits.

中文说明

R1-22 在 135bf45 仍然存在——持久化快照断言依旧使用宽松的 toMatchObject,而 fixture 刻意携带 script: 'return 1;';它无法捕获快照 script/scriptPath 字段的泄漏,而 serializeWorkflowSnapshot 是刻意不输出它们的。

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

});

describe('buildSessionTasksStatus workflow graph', () => {
it('omits workflow tasks unless the caller opts in', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R1-23: Still stands at 135bf45 — the opt-out test still only exercises the persisted-history branch: configWith([]) leaves the workflow registry empty, so the test cannot detect a regression dropping the includeWorkflows gate from the live-registry mapping.

中文说明

R1-23 在 135bf45 仍然存在——opt-out 测试依旧只覆盖持久化历史分支:configWith([]) 使 workflow registry 为空,因此该测试无法发现从"实时 registry 映射"中移除 includeWorkflows 门禁的回归。

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

primary: false,
trusted: true,
workflowsEnabled: false,
kind: 'live',

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R1-24: Still stands at 135bf45 — server.test.ts was not touched this round; the workflowsEnabled assertion still resolves against the host's process.env (no daemonEnv dep injected; the fake runtime uses env: { mode: 'parent-process' }), so its truth depends on the runner environment.

中文说明

R1-24 在 135bf45 仍然存在——本轮未改动 server.test.ts;workflowsEnabled 断言依旧解析到宿主机的 process.env(未注入 daemonEnv 依赖;假运行时使用 env: { mode: 'parent-process' }),其真假取决于运行环境。

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


getWorkflowHistory(): readonly WorkflowSnapshot[] {
return this.workflowHistory;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R1-25: Still stands at 135bf45 — grep across all packages at HEAD confirms getWorkflowHistory() still has zero production callers (only the definition, Session.test.ts, and the acpAgent.test.ts session mock); the live cache fed by #rememberWorkflowHistory is never observed by the endpoint path (tasks status goes through refreshWorkflowHistory).

中文说明

R1-25 在 135bf45 仍然存在——在 HEAD 上跨包 grep 确认 getWorkflowHistory() 依旧没有生产调用方(只有定义、Session.test.ts 与 acpAgent.test.ts 的 session mock);由 #rememberWorkflowHistory 维护的活跃缓存从未被端点路径观察(任务状态走的是 refreshWorkflowHistory)。

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

@qqqys
qqqys dismissed qwen-code-ci-bot’s stale review August 23, 2026 23:26

Superseded by subsequent commit 4ab652a; the current head requires re-review.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed. Suggestions are inline.

1 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R2-5 cancel-ordering pin — dropped at acpAgent.test.ts:10262, overlaps existing comment 3839813863 (R1-10 re-post thread)

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

Deferred under the convergence posture (round 3, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/serve/multi-workspace-sessions.test.ts:1149 — [probe] New workflowsEnabled capabilities assertion computed from ambient process.env (fails when QWEN_CODE_ENABLE_WORKFLOWS=1 is exported)
  • packages/cli/src/serve/server.test.ts:9409 — [probe] Repurposed cancel test leaves legacy kinds (agent/monitor) without positive generic-route coverage; mutant ships green
  • packages/webui/src/daemon/session/actions.ts:1962 — [review] getWorkflowTasks reports failures under getTasks' identity (message, operation 'load_tasks', shared once-key)
  • packages/cli/src/acp-integration/session/Session.test.ts:1121 — [review] Workflow-hold test offers only 3 of 6 statuses; predicate-narrowing mutants pass green
  • packages/cli/src/acp-integration/session/Session.test.ts:2488 — [review] removeTerminal call in deleteWorkflowHistory (the R2-4 fix) pinned by no test; deletion resurrection mutant ships green
  • packages/cli/src/acp-integration/session/Session.ts:8345 — [review] todoStopGuard baseline keyed by runId; retry reuses the runId so retried attempts inherit baseline membership
  • packages/cli/src/serve/server.test.ts:4006 — [probe] Capabilities test pins no precedence combinations (settings+kill-switch, settings+untrusted); reorder mutants ship green
  • packages/cli/src/acp-integration/session/Session.test.ts:34033 — [probe] Baseline-classification test confounds baseline membership with todoWorkChainId presence; chain-id mutant passes
  • packages/cli/src/acp-integration/session/Session.test.ts:2297 — [probe] Failed workflow completions never exercised through the session queue; completed-only gating mutant passes
  • packages/cli/src/acp-integration/session/tasksSnapshot.test.ts:378 — [probe] Live-vs-persisted runId dedup test uses only a terminal live entry; the retry-shaped active variant is unpinned
  • packages/webui/src/daemon/session/actions.test.ts:1104 — [probe] runSavedWorkflow {changed:false} → started:false mapping unpinned; started:true mutant passes

Convergence: round 3 posted 35 inline comment(s), 1 of them reported for the first time; the previous round posted 38 (19 new). Findings keep coming back to the same files: packages/cli/src/acp-integration/acpAgent.test.ts (findings in rounds 1, 2; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. No Critical finding is open on this round, so merging and moving the remaining Suggestion threads to a follow-up issue is available as an ending — a merged pull request cannot diverge further. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

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

本轮确认的 1 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

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

收敛姿态下延后(第 3 轮,非阻断)——已记录,本轮不要求修改:共 11 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 3 轮发布了 35 条行内评论,其中 1 条是首次提出;上一轮发布了 38 条(其中 19 条首次提出)。发现反复回到同一批文件:packages/cli/src/acp-integration/acpAgent.test.ts(第 1、2 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。本轮没有未决的 Critical,因此"合入后把剩余 Suggestion 线程转到后续 issue"是一个可选的结束方式——已合入的 PR 不会继续发散。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment on lines +10721 to +10728
await expect(
agent.extMethod(SERVE_CONTROL_EXT_METHODS.sessionWorkflowTaskAction, {
sessionId,
taskId: task.runId,
action: 'retry',
}),
).resolves.toEqual({ changed: false, status: 'failed' });
expect(execute).not.toHaveBeenCalled();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] This new test pins the mutation-claim exclusion in only one direction (retry rejected while delete holds the claim). The delete-history branch's own re-entrancy guard (if (this.mutatingWorkflowTaskIds.has(mutationClaim)) return { changed: false };, acpAgent.ts:10363-10365) is exercised by no test with the claim already held by a retry/rerun. Mutant proof: removing that guard keeps all 13 workflow ext-method tests green (Tests 13 passed | 454 skipped), while a gated probe (retry holds the claim mid-execute, then delete-history for the same taskId) fails on the mutant with expected { changed: true } to deeply equal { changed: false } and passes with the guard restored. Without the guard, a delete-history arriving mid-retry descends into Session.deleteWorkflowHistory concurrently with the run restart — the same runId-reuse journal race from the delete side that the claim was added to close. Mirror this test with the roles swapped: block the retry execute mock on a gate, send delete-history, assert { changed: false } with deleteWorkflowHistory not called, then release and let the retry complete.

中文说明

这个新测试只锁定 mutation-claim 互斥的一个方向(delete 持有 claim 时 retry 被拒绝)。delete-history 分支自身的重入守卫(acpAgent.ts:10363-10365)没有任何测试在 retry/rerun 已持有 claim 时演练。变异证据:移除该守卫后 13 个 workflow ext-method 测试全绿(13 passed | 454 skipped),而门控探针(retry 在 execute 中持有 claim,随后对同一 taskId 发 delete-history)在变异体上失败(expected { changed: true } to deeply equal { changed: false }),恢复守卫后通过。没有该守卫时,retry 进行中的 delete-history 会与运行重启并发进入 Session.deleteWorkflowHistory——claim 本要堵住的 runId 复用 journal 竞态从 delete 侧重新打开。建议镜像本测试:门控 retry 的 execute,先发 delete-history 断言 { changed: false }deleteWorkflowHistory 未被调用,再放行让 retry 完成。

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

Comment on lines +4448 to +4451
const action = safeBody(req)['action'];
if (
action !== 'pause' &&
action !== 'resume' &&

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R1-7: Still stands at e1393380 — the invalid-action rejection path still has zero negative tests at every layer (REST route, ACP-HTTP dispatch, acpAgent ext method), and the six-value allow-list remains hand-maintained in three separate entrances. An unrecognized action that slips past any loosened check reaches the pause/resume fallthrough and silently becomes a resume. Add one invalid-action negative case per layer.

中文说明

仍然成立:非法 action 拒绝路径在每一层(REST 路由、ACP-HTTP dispatch、acpAgent ext method)依然没有任何负向测试,六值白名单仍在三处手工维护。任何放宽检查后漏过的未知 action 会落入 pause/resume 兜底,静默变成 resume。建议每层补一个非法 action 负向用例。

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

Comment on lines +10273 to +10277
it.each([
['workflows are disabled', false, false, false, true],
['bare mode is active', true, true, false, true],
['the workspace is untrusted', true, false, true, false],
])(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R1-8: Still stands at e1393380 — the gating it.each rows remain workflows-off, bare-mode, and untrusted (folderTrustFeature=true + folderTrust=false); no row enables getFolderTrustFeature with getFolderTrust=true, and none exercises feature-off + untrusted. Mutation (!getFolderTrustFeature() || getFolderTrust())(!getFolderTrustFeature() && getFolderTrust()) disables workflow controls for every user whose folder-trust feature is enabled in a trusted workspace, shipping green. Add rows for (feature=true, trust=true) expecting controls enabled, and (feature=false, trust=false).

中文说明

仍然成立:门禁 it.each 行仍只有功能关闭、bare 模式、不可信(folderTrustFeature=true + folderTrust=false);没有 getFolderTrustFeature=truegetFolderTrust=true 的行,也没有功能关闭+不可信的行。变异 (!A || B)(!A && B) 会令所有在可信 workspace 且启用 folder-trust 特性的用户失去 workflow 控制而测试全绿。建议补 (feature=true, trust=true) 与 (feature=false, trust=false) 两行。

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

await agentPromise;
});

it('retries a failed workflow with its original script, args, and journal', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R1-9: Still stands at e1393380 — retry (task.status === 'failed' && !registry.getHandle(taskId)) and rerun (terminal-status check) still have only happy-path and overlap-admission tests; no test drives a non-failed retry, a retry with a live handle, or a rerun of a running run. A mutation widening the retry precondition (e.g. dropping the !getHandle term) ships green while retries of active runs double-start. Add the three negative cases asserting {changed:false} with no execute call.

中文说明

仍然成立:retry(task.status === 'failed' && !registry.getHandle(taskId))与 rerun(终态检查)仍只有快乐路径与重叠准入测试;没有测试演练非失败态的 retry、带活跃 handle 的 retry、或对 running 运行的 rerun。放宽 retry 前置条件的变异(如去掉 !getHandle)会让活跃运行被重复启动而测试全绿。建议补三个断言 {changed:false} 且不触发 execute 的负向用例。

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

Comment on lines +10260 to +10261
await Promise.resolve();
expect(settled).toBe(false);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R1-10: Still stands at e1393380 — re-verified this round: expect(settled).toBe(false) after one await Promise.resolve() still passes whether or not the cancel handler awaits handle.completion, because both async layers resolve in the same microtask window. Removing if (handle) await handle.completion; from the workflow cancel branch keeps this test green while cancel returns before the run actually settles. Gate resolveCompletion behind a second microtask/timer so dropping the await fails the test.

中文说明

仍然成立:单次 await Promise.resolve() 后的 expect(settled).toBe(false) 无论 cancel 处理器是否 await handle.completion 都会通过(两个异步层在同一微任务窗口内解决)。移除 cancel 分支中的 if (handle) await handle.completion; 测试仍绿,而 cancel 会在运行真正结束前返回。建议把 resolveCompletion 放到第二个微任务/定时器之后,使移除 await 时测试失败。

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

Comment on lines +1222 to +1227
it('holds a queued workflow completion notification', async () => {
mockChat.sendMessageStream = vi
.fn()
.mockResolvedValue(createEmptyStream());
createReportingSession();
const releaseCloseGate = session.beginClose();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R2-15: Still stands at e1393380 — no test pins the in-flight continuation hold for workflow notifications (currentWorkflowNotificationTaskId). Once the queued item is dequeued for delivery, that field is the ONLY thing still holding active-work for the duration of the continuation turn; the queued-hold test stops at dequeue. Removing its set/clear in the drain path ships green while the session reports idle mid-delivery and can be reaped. Hold the drain mid-flight (gate sendMessageStream) and assert the hold persists until delivery settles.

中文说明

仍然成立:没有测试锁定 workflow 通知在途期间的持续 hold(currentWorkflowNotificationTaskId)。队列项出队投递后,该字段是续行 turn 期间唯一维持 active-work 的东西;排队 hold 测试在出队处就停了。在 drain 路径移除其 set/clear 会全绿,会话会在投递中途报告空闲并可能被回收。建议门控 sendMessageStream 使 drain 停在半路,断言投递完成前 hold 持续存在。

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

Comment on lines +2533 to +2535
const deletion = session.deleteWorkflowHistory(snapshot.runId);
await Promise.resolve();
expect(deleteWorkflowSnapshotSpy).not.toHaveBeenCalled();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R2-16: Still stands at e1393380 — 'waits for an active run owner to finish persistence before deletion' stays green if if (handle) await handle.completion; is removed from Session.deleteWorkflowHistory: reaching deleteWorkflowSnapshot then needs the mocked finishPersistence anyway, so the wait itself is unverified. Dropping the wait ships green while delete-history races an unsettled run's snapshot write. Make deleteWorkflowSnapshot reachable without finishPersistence and assert it is not called until the completion resolves.

中文说明

仍然成立:若在 Session.deleteWorkflowHistory 中移除 if (handle) await handle.completion;,'waits for an active run owner to finish persistence before deletion' 仍绿:到达 deleteWorkflowSnapshot 反正需要 mock 的 finishPersistence,等待本身未被验证。移除等待会全绿,delete-history 会与未结算运行的快照写入竞态。建议让 deleteWorkflowSnapshot 不依赖 finishPersistence 也可达,并断言完成前不被调用。

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

session.dispose();
});

it('holds each non-terminal workflow run', () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R2-17: Still stands at e1393380 — the workflow status-change callback's #activeWorkChanged() push (Session.ts ~8330) is pinned by no test, unlike the task- and shell-registry siblings which each have an explicit 'status change notifies the owner' test; without it, hold changes on workflow transitions never reach the bridge. Add the workflow analogue of that sibling test.

中文说明

仍然成立:workflow 状态变更回调里的 #activeWorkChanged() 推送(Session.ts ~8330)没有任何测试锁定,而 task 与 shell registry 的兄弟各自都有显式的"状态变更通知属主"测试;缺了它,workflow 状态转换的 hold 变化永远到不了 bridge。建议补该兄弟测试的 workflow 对应项。

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

Comment on lines +34033 to +34037
callback('baseline result', '<baseline-workflow />', {
runId: 'baseline-workflow',
status: 'completed',
todoWorkChainId: 'stale-chain',
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R2-18: Still stands at e1393380 — workflow completion todoWorkChainId propagation (enqueue → item.todoWorkChainIdstartAutomaticActiveTodoWorkChain(promptId, item.todoWorkChainId)) is pinned by zero tests; the baseline-classification test asserts only the continuesTodoStopGuardWorkChain flag, not the chain-id handoff. Dropping todoWorkChainId from the queued item ships green while workflow completions never resume their work chain. Assert the queued item carries the chain id and delivery passes it through.

中文说明

仍然成立:workflow 完成的 todoWorkChainId 传播(入队 → item.todoWorkChainIdstartAutomaticActiveTodoWorkChain(promptId, item.todoWorkChainId))没有任何测试锁定;基线分类测试只断言 continuesTodoStopGuardWorkChain 标志,不断言链 id 交接。从队列项丢弃 todoWorkChainId 会全绿,workflow 完成将永远无法续接其工作链。建议断言队列项携带链 id 且投递时透传。

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

Comment on lines +4453 to +4454
primaryRuntimeEnv.workflowsEnabledBySettings =
fresh.merged.tools?.workflowsEnabled === true;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] R2-19: Still stands at e1393380 — the reload FALLBACK path (env rebuild throws) updating workflowsEnabledBySettings is pinned by zero tests. The assignment deliberately runs before the buildRuntimeEnvironment try/catch — so the capabilities flag flips even when the reload otherwise fails — and no test exercises that branch. Moving the assignment inside the try (or dropping it on fallback) ships green while a failed reload leaves stale capability advertisement. Add a reload test where buildRuntimeEnvironment throws and assert the flag still updates.

中文说明

仍然成立:reload 回退路径(环境重建抛错)更新 workflowsEnabledBySettings 没有任何测试。该赋值故意放在 buildRuntimeEnvironment 的 try/catch 之前——即使 reload 其他部分失败,能力标志也会翻转——但没有测试演练该分支。把赋值移进 try(或回退时丢弃)会全绿,失败的 reload 会留下过期的能力声明。建议补一个 buildRuntimeEnvironment 抛错的 reload 测试,断言标志仍被更新。

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

@wenshao

wenshao commented Aug 24, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover from 6

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Aug 24, 2026
@qqqys

qqqys commented Aug 26, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover stop

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

14 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • dead getWorkflowHistory getter and preload — already reported (R1-25, comment 3839293583)
  • cancel completion-wait test vacuity — already reported (R1-10, comment 3839293554 and reposts)
  • SDK sessionTaskMutation wire-level coverage — already reported (R1-14, comment 3839293563)
  • ACP dispatch validation branches untested — already reported (R1-7 family, comment 3839293546; round-5 deferred dispatch.ts:3640)
  • retry getHandle guard unpinned — already reported (round-5/6 deferred: acpAgent.test.ts:10599, acpAgent.ts:10528)
  • multi-workspace workflow-action ownership test — already reported (R1-20, comment 3839293575)
  • cancel trust-gate kind-scoping pin — already reported (round-6 deferred server.test.ts:9591)
  • mutation-claim release unpinned — already reported (R2-7, comment 3839813819)
  • delete persistence-wait test vacuity — already reported (R2-16, comment 3839813833)
  • cancelTask stale-notice suppression — already reported (round-6 deferred actions.ts:1995; R1-18 comment 3839293570)
  • run-saved launch error collapses into bare {changed:false} — already reported (round-5 deferred acpAgent.ts:10476)
  • JSON-RPC tasks opt-in default unpinned — already reported (round-6 deferred transport.test.ts:8436)
  • stop-guard baseline keyed by runId misclassifies retried runs — already reported (round-6 deferred Session.ts:8389)
  • webui new-action error-notice positive pin — already reported (R1-18, comment 3839293570; round-6 deferred actions.ts:2030)

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

Not explored to full depth (tool budget reached): chunk 14: execute packages/cli/src/serve/workflow-session-gate.test.ts (and the two server.test.ts SSE tests) under vitest — the review worktree has no node_modules , ….

Deferred under the convergence posture (round 7, not a blocker) — recorded, not requested in this round:

  • packages/core/src/agents/workflow-snapshot.ts:140 — [probe] writeWorkflowSnapshot boolean contract unasserted; both mutants survive
  • packages/cli/src/serve/server.test.ts:939 — [probe] new fake's status?: string is a latent TS2322 behind the TODO(5691) exclude
  • packages/cli/src/serve/workflow-session-gate.test.ts:102 — [probe] liveJournal-only redaction branch unpinned; guard mutant ships green
  • packages/cli/src/acp-integration/acpAgent.test.ts:11637 — [review] sibling removeTerminal fan-out's if(changed) guard unpinned on the false path
  • packages/cli/src/acp-integration/session/Session.test.ts:3203 — [review] post-completion-wait isDeletable() re-check unpinned by any test
  • packages/cli/src/serve/server.test.ts:13884 — [review] redaction tests pin the inner array only; envelope-level regressions ship green

Convergence: round 7 posted 4 inline comment(s), 4 of them reported for the first time; the previous round posted 3 (3 new). Findings keep coming back to the same files: packages/cli/src/acp-integration/acpAgent.ts (findings in round 6; 2 more now). The rate of new findings is not falling. A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. Batching the remaining fixes and verifying them before the next push keeps the loop from re-deriving the same set; this PR's reviews already resolve to a critical posting floor. (Observation only — nothing was withheld from this review because of this observation.)

Residual risk: this loop is persistently critical — Criticals stood in the previous round's work-list and stand again this round (4 Critical(s)), the rate of first-time findings is not falling (this round 4, previous 3), and the standing Critical backlog is not shrinking. The severity floor will not converge it. Recommendation: land-with-residual-risk — the exit is a maintainer risk-acceptance decision (merge, carrying the residual risk), not another review round. Residual-risk inventory for that decision (maintainer to complete):

standing Critical attack surface attacker-dependency blast radius
(each standing Critical)

Advisory only — it does not block this review.

中文说明

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

本轮确认的 14 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

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

未探索到全部深度(达到工具调用预算):chunk 14:execute packages/cli/src/serve/workflow-session-gate.test.ts (and the two server.test.ts SSE tests) under vitest — the review worktree has no node_modules , …

收敛姿态下延后(第 7 轮,非阻断)——已记录,本轮不要求修改:共 6 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 7 轮发布了 4 条行内评论,其中 4 条是首次提出;上一轮发布了 3 条(其中 3 条首次提出)。发现反复回到同一批文件:packages/cli/src/acp-integration/acpAgent.ts(第 6 轮已出过发现,本轮又有 2 条)。新发现的产出速度没有下降。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。把剩余修复攒成一批、验证后再推送,可以避免循环反复推导同一组发现;本 PR 的评审已解析为 critical 发布下限。(仅为观察——本轮评审未因此扣留任何内容。)

残余风险:本循环处于 persistently-critical 形态——上一轮工作清单中的 Critical 本轮依然存在(本轮 4 条 Critical),首次发现的速率没有下降(本轮 4,上一轮 3),且未决 Critical 积压没有减少。severity floor 无法使其收敛。建议:land-with-residual-risk——出口是 maintainer 的风险接受决定(合入并承担残余风险),而非再开一轮评审。供该决定使用的残余风险清单(maintainer 填写):按每条未决 Critical 列出「攻击面 · 攻击者依赖性 · 影响范围」三栏。仅为建议——不阻断本次评审。

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

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/session/Session.ts
Comment thread packages/cli/src/acp-integration/acpAgent.ts
…oles

Addresses R5-9, R7-4, R7-5 and R7-10 — all four Criticals open on this PR.
They are one family: a run's history can be deleted, retried, or listed
from any session, and each gate had a different idea of which runs exist.

R5-9 — the mutation claim is task-global, not session-scoped. Keyed
`sessionId\0taskId`, it serialized nothing that mattered: every session
shares one snapshot store. A sibling's retry passed canStart (`failed`,
no handle), took its own per-session claim, then awaited journal
load/compile before `register()`; a delete-history landing in that
structural window found the run terminal and handle-less in every
registry, removed the journal directory and snapshot, and answered
`{changed: true}` — after which the retry re-registered and its
settlement re-persisted the history the user was told was deleted. The
claim is now keyed by taskId alone and taken by delete-history, retry,
rerun and run-saved. `run-saved` keys off a saved workflow's NAME, so it
claims in its own `saved\0` namespace rather than colliding with runIds.

R7-4 — deletion tests membership against the uncapped merged set.
`buildSessionTasksStatus` serializes every registry entry
unconditionally while `refreshWorkflowHistory` truncates to
MAX_RETAINED_SNAPSHOTS by startTime, so a long run that settled after
~30 newer ones started stayed listed via the registry but fell out of
the window — terminal, handle-free, live in no sibling, and permanently
undeletable. `refreshWorkflowHistory` now records the merged id set
before the cap, and deletion gates on that, the registry, or the
unpersisted cache. `deleteWorkflowSnapshot` already tolerates an absent
target, so the wider gate cannot delete what is not there.

R7-5 — snapshot retirement is a latch. The registry's dispatch-drain
callbacks emit status changes on TERMINAL entries with no status gate,
and in-flight dispatches keep draining across the snapshot write, so a
terminal emission routinely landed after `notifySnapshotPersisted` had
retired the cache entry — re-inserting the run as "never persisted".
A sibling's deletion was then undone by the next refresh, which reads
"absent on disk, present in cache" as a pending write and republishes.
Persistence is now remembered per runId and `#rememberWorkflowHistory`
returns early for members; the latch releases when the runId goes active
again, so a genuine re-run is still cached.

R7-10 — the liveness gate sees runs whose session is gone. It iterated
`this.sessions` only, but close/kill/shutdown use force semantics and a
background run owns a detached controller, so after
`removeStoredSessionEntry` a still-settling run was invisible to the
gate and unreachable by the delete handler's sibling `removeTerminal`
loop: a sibling delete-history removed the LIVE run's journal and
snapshot, and the orphan's settlement write recreated it. Two halves —
`Session.dispose()` now aborts its workflow registry the way it already
aborts the agent registry (before the callbacks are torn down), and the
registry of a removed session is retained here until its runs drain, so
the gate still answers across the settlement window an abort cannot
compress to zero. Retention is bookkeeping: a Config that cannot answer
is logged, never turned into a shutdown failure.

Regressions, each mutation-checked against its own repair:
- acpAgent: a sibling's parked retry makes delete-history answer
  `{changed: false}` without reaching the store, and the deletion goes
  through once the claim releases
- acpAgent: a live run's registry stays visible to the gate across its
  session's close, and is dropped once the handle is released
- Session: a status emission after `snapshotPersisted` no longer
  resurrects a sibling-deleted run (reverting the latch reproduces the
  reviewer's probe verbatim), and a re-registered runId is remembered
  again
- Session: a run with the oldest startTime behind 30 newer snapshots is
  deletable
- Session: dispose aborts the workflow registry before clearing its
  callbacks

Verification: Session.test.ts 733 passed, acpAgent.test.ts 514 passed,
`tsc --noEmit -p packages/cli` clean, eslint and prettier clean. The 14
`packages/cli/src/serve` failures (fast-path import boundary,
capabilities-docs contract, workspace fs/agents/memory, conversation
runtime ownership) reproduce identically on the unmodified head — base
skew, untouched by this change.

Claude-Session: https://claude.ai/code/session_01M7z4PccYfDPyyfg3oGr8V1
@qqqys

qqqys commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

b799ea63b1 addresses all four round-7 Criticals. They are one family — a run's history can be deleted, retried, or listed from any session, and each gate had a different idea of which runs exist — so they are fixed together rather than one patch per finding.

Finding Repair
R5-9 acpAgent.ts The mutation claim is keyed by taskId alone and taken by delete-history, retry, rerun and run-saved (which previously took none). run-saved keys off a saved workflow's NAME, so it claims in a separate saved\0 namespace instead of colliding with runIds.
R7-4 Session.ts refreshWorkflowHistory records the merged runId set before the MAX_RETAINED_SNAPSHOTS slice; deletion gates on that ∪ registry ∪ unpersisted cache. workflowHistory stays the capped display window.
R7-5 Session.ts Snapshot retirement is a latch (persistedWorkflowRunIds), released when the runId goes active again so a genuine re-run is still cached.
R7-10 acpAgent.ts + Session.ts Both halves: Session.dispose() aborts its workflow registry (before the callbacks are torn down, mirroring the agent registry two lines above), and a removed session's registry is retained in detachedWorkflowRegistries until its runs drain so the gate still answers across the settlement window.

On R7-10 specifically — both halves were needed, and your probe is why. A run cancelled via the abortAll shape still persists exactly one snapshot at settlement, so aborting alone cannot compress the window to zero; the gate has to keep answering while the handle lives. I used the retained-registry form rather than a parallel process-global runId set because the authoritative fact already lives in the registry (getHandle outlives the terminal transition until the write lands), and a second structure would need its own lifecycle and could drift from it. Retained registries are pruned as they drain, so this does not grow with every closed session.

One thing worth calling out from the repair: the first cut pushed a failed registry read in removeStoredSessionEntry into cleanupErrors, which turned 21 unrelated acpAgent.test.ts cases into AggregateError: Managed ACP shutdown failed. Retention is bookkeeping, not cleanup — a Config that cannot answer is now logged and skipped, never allowed to fail a successful close.

Regressions, each mutation-checked against its own repair:

  • acpAgent.test.ts — a sibling's parked retry makes delete-history answer {changed: false} without reaching the store; the deletion succeeds once the claim releases. Restoring the per-session key: expected { changed: true } to deeply equal { changed: false }.
  • acpAgent.test.ts — a live run's registry stays visible to the gate across its session's sessionClose, and is dropped once the handle is released.
  • Session.test.ts — a status emission after snapshotPersisted no longer resurrects a sibling-deleted run. Removing the latch reproduces your probe verbatim: expected [ { runId: 'wf_abcd', … } ] to deeply equal []. A companion test pins that the latch is not permanent.
  • Session.test.ts — a run with the oldest startTime behind 30 newer snapshots is deletable.
  • Session.test.ts — dispose aborts the workflow registry, before clearing its callbacks.

Verification: Session.test.ts 733 passed, acpAgent.test.ts 514 passed, tsc --noEmit -p packages/cli clean, eslint and prettier clean.

Disclosed gap: packages/cli/src/serve has 14 failures (fast-path import boundary, capabilities-docs contract, workspace fs/agents/memory, conversation runtime ownership). I re-ran those six files against the unmodified head and got the identical 14 — base skew, untouched by this change, and not something this PR introduced or can fix from here.

The 14 Suggestion-level findings this round re-confirmed are all previously deferred items in this PR's convergence posture; they remain deferred, and this round did not add to them.

中文说明

b799ea63b1 处理第 7 轮全部四条 Critical。它们本质是同一族问题——运行历史可以从任意会话删除、重试或列出,而各处门禁对「哪些运行存在」的判断彼此不一致——因此一并修复。

  • R5-9:互斥 claim 改为仅以 taskId 为键,并在 delete-history、retry、rerun 与 run-saved 四处获取(run-saved 此前完全没有获取)。run-saved 的 taskId 是保存的工作流名称而非 runId,因此使用独立的 saved\0 命名空间,避免与 runId 冲突。
  • R7-4refreshWorkflowHistoryMAX_RETAINED_SNAPSHOTS 截断之前记录合并后的 runId 集合;删除改为对「该集合 ∪ registry ∪ 未持久化缓存」做成员判定。workflowHistory 仍是带上限的展示窗口。
  • R7-5:快照退役改为闩锁(persistedWorkflowRunIds),并在该 runId 重新变为活动态时释放,确保真正的重跑仍会被缓存。
  • R7-10:两半都做了。Session.dispose() 现在会中止其 workflow registry(在清理回调之前,与上方 agent registry 的处理对齐);同时已移除会话的 registry 会保留在 detachedWorkflowRegistries 中直到其运行收尾完毕,使存活门禁在整个结算窗口内继续有效。

关于 R7-10:两半缺一不可,依据正是您的探针——以 abortAll 形态取消的运行在结算时仍会写入一份快照,因此仅靠中止无法把窗口压缩为零。选择「保留 registry」而非并行的进程级 runId 集合,是因为权威事实本就在 registry 中(getHandle 会一直存活到快照写入落盘),另建结构需要自己的生命周期且可能与之漂移。保留的 registry 会随收尾被剪除,不会随会话关闭数量增长。

修复过程中的一个插曲值得记录:第一版把 removeStoredSessionEntry 中 registry 读取失败推入了 cleanupErrors,导致 21 个无关的 acpAgent.test.ts 用例变成 AggregateError: Managed ACP shutdown failed。保留 registry 属于簿记而非清理,现在失败只记录日志并跳过,绝不让一次成功的关闭变成失败。

五条回归测试均做了变异校验。验证:Session.test.ts 733 通过,acpAgent.test.ts 514 通过,typecheck / eslint / prettier 干净。

已披露的缺口:packages/cli/src/serve 有 14 条失败(fast-path 导入边界、capabilities 文档契约、workspace fs/agents/memory、conversation 运行时归属)。我在未修改的 head 上重跑这六个文件得到完全相同的 14 条失败——属于 base skew,与本次改动无关。

qqqys added 2 commits August 27, 2026 14:32
Merging upstream/main (053f17b) clears the 9 `client.telemetrySwap`
failures this branch had from predating QwenLM#10220, but main carries its own
TS1117 on the same file: two commits added `getToolRegistry` to the same
object literal independently and neither saw the other.

  032b907 feat(serve): backfill session PR bindings ... (QwenLM#9729)
  8241905 test(core): give the telemetry-swap client mock a
             getToolRegistry (QwenLM#10220)

Checking that file out from upstream/main here and running
`tsc --noEmit -p packages/core` reproduces
`client.telemetrySwap.test.ts(103,5): error TS1117` verbatim, so taking
the merge unmodified would have traded 9 test failures for a build that
never reaches the tests at all.

Removed as part of the merge rather than left for a follow-up: keeps
QwenLM#10220's copy, which was added for this purpose and carries the
explanation, and drops QwenLM#9729's incidental one. main still needs the same
removal — this only keeps it out of the branch.

Verified after the merge: `tsc --noEmit` clean for both packages/core
and packages/cli; client.telemetrySwap 10 passed, Session.test.ts and
acpAgent.test.ts 1260 passed together.

Claude-Session: https://claude.ai/code/session_01M7z4PccYfDPyyfg3oGr8V1
qwen-code-ci-bot pushed a commit that referenced this pull request Aug 27, 2026
@qqqys

qqqys commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /takeover stop

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

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

Deferred under the convergence posture (round 8, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/acp-integration/acpAgent.ts:3386 — [review] detachedWorkflowRegistries pruned only on the delete-history path (single caller); accumulates without deletions
  • packages/cli/src/acp-integration/acpAgent.ts:10996 — [review] run-saved overlap-admission guard exercised by no test
  • packages/cli/src/acp-integration/acpAgent.test.ts:11639 — [probe] R7-10 test's prune assertion vacuous — prune removable, mutant ships green (514/514)
  • packages/cli/src/acp-integration/acpAgent.test.ts:11266 — [probe] claim release on error unpinned — a rejected execute strands the taskId claim until restart
  • packages/cli/src/serve/server.test.ts:9843 — [review] untrusted workflow-action gate tested for 1 of 6 actions (REST + ACP-HTTP twin)
  • packages/cli/src/acp-integration/session/Session.test.ts:32106 — [probe] dispose test doesn't pin the setSnapshotPersistedCallback(undefined) clear
  • packages/cli/src/acp-integration/acpAgent.ts:10978 — [probe] sibling cleanup clears registries but not history caches — deleted run re-displays in sibling (display-only)
  • packages/cli/src/acp-integration/session/Session.test.ts:3273 — [probe] R7-4 capped-window test's registry mock masks the fix — revert ships green (736/736)
  • packages/cli/src/serve/server.test.ts:4066 — [review] kill-switch vs workflowsEnabledBySettings precedence unpinned in capabilities tests

Convergence: round 8 posted 2 inline comment(s), 2 of them reported for the first time; the previous round posted 4 (4 new). Findings keep coming back to the same files: packages/cli/src/acp-integration/acpAgent.ts (findings in round 7; 2 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

中文说明

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

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

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

收敛姿态下延后(第 8 轮,非阻断)——已记录,本轮不要求修改:共 9 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 8 轮发布了 2 条行内评论,其中 2 条是首次提出;上一轮发布了 4 条(其中 4 条首次提出)。发现反复回到同一批文件:packages/cli/src/acp-integration/acpAgent.ts(第 7 轮已出过发现,本轮又有 2 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

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

Comment on lines +4054 to +4057
const registry = session.getConfig().getWorkflowRunRegistry?.();
if (registry && QwenAgent.isWorkflowRegistryDraining(registry)) {
this.detachedWorkflowRegistries.add(registry);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] R7-4: (fix-induced) The round-7 fix retains a removed session's workflow registry through a one-shot draining check at removal time, and that one-shot shape opens a new resurrection window at the same site. A retry/rerun/run-saved that is already past canStart and holding its task claim, but whose WorkflowRunner.start() is still awaiting journal.load() before register(), is not yet draining when removeStoredSessionEntry runs — so its registry is not retained, the session leaves this.sessions, and the run then registers into a registry that no liveness gate consults (isWorkflowRunLiveOutsideSession iterates only this.sessions + detachedWorkflowRegistries). A sibling delete-history for that runId passes every check (terminal and handle-less in every visible registry), removes the journal directory and snapshot, and answers {changed:true}; the orphan's settlement then runs writeWorkflowSnapshot and the history the user was told was deleted resurrects in every session — with the journal rm'd under a live run. The claim set does not help: session close never takes a claim.

Witness — probe against the real Session.deleteWorkflowHistory + real registry + real WorkflowRunner, journal load parked pre-register:

PR CODE: deleteWorkflowHistory -> changed=true, snapshotExistsAfterDelete=false,
         registryEntryAfterDelete=null; later registry.get(runId).status === 'completed'
         and the snapshot file exists again
FLIP (registry retained across the window): deleteWorkflowHistory -> changed=false,
         snapshot retained

Fix: fail closed at the start site — after execute(...) returns in the retry/rerun/run-saved handlers, check the session is still stored (this.sessions.get(sessionId) === session) and if not immediately registry.cancel(runId, Date.now()); alternatively serialize removeStoredSessionEntry against outstanding workflow-mutation claims for that session.

Please pin the fix with a test: park a retry inside its mocked execute (as the R5-9 test does), run sessionClose for that session, release the retry, and assert the started run is cancelled at once or a sibling delete-history for the runId keeps refusing {changed:false} while the handle lives — removing the fix must turn that test red.

中文说明

[Critical] R7-4:(修复引入)第 7 轮的修复通过在会话移除时刻做一次性的 draining 检查来保留被移除会话的 workflow registry,而这个一次性形态在同一位置打开了新的「复活」窗口。某个 retry/rerun/run-saved 已经通过 canStart 并持有任务申领,但其 WorkflowRunner.start() 仍停在 register() 之前的 journal.load() await 上——此时 removeStoredSessionEntry 执行时该 registry 还不算 draining,因此不会被保留;会话从 this.sessions 移除后,该运行随后注册进一个任何存活性门禁都不会查询的 registry(isWorkflowRunLiveOutsideSession 只遍历 this.sessions + detachedWorkflowRegistries)。兄弟会话对该 runId 的 delete-history 会通过所有检查(在所有可见 registry 中均为终态且无 handle),删除日志目录与快照并返回 {changed:true};孤立的结算随后执行 writeWorkflowSnapshot,用户刚被告知已删除的历史在所有会话中复活——且日志是在运行仍存活时被 rm 的。申领集合帮不上忙:会话关闭从不申领。

证据——对真实 Session.deleteWorkflowHistory + 真实 registry + 真实 WorkflowRunner 的探针(日志加载停靠在注册前):PR 代码返回 changed=true、快照被删、之后 registry.get(runId).status === 'completed' 且快照文件再次出现;翻转分支(跨窗口保留 registry)返回 changed=false、快照保留。

修复:在启动侧 fail closed——retry/rerun/run-saved 处理器在 execute(...) 返回后检查会话仍在存储中(this.sessions.get(sessionId) === session),否则立即 registry.cancel(runId, Date.now());或者让 removeStoredSessionEntry 与该会话未完成的 workflow 变更申领串行化。

请为修复补充测试:让一个 retry 停在 mock 的 execute 内(仿照 R5-9 测试),对该会话执行 sessionClose,释放 retry,断言启动的运行被立即取消、或兄弟会话的 delete-history 在 handle 存活期间持续拒绝 {changed:false}——移除修复必须使该测试变红。

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

Comment on lines +10995 to +10998
if (action === 'run-saved') {
if (this.mutatingWorkflowTaskIds.has(mutationClaim)) {
return { changed: false };
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Critical] The task-global mutation claim that closed the round-7 cross-session races is taken only by these control-plane branches (delete-history, retry/rerun, run-saved). The Workflow tool exposes a schema-documented resumeFromRunId parameter and is registered for daemon-session configs, so a model-invoked tool call re-registers an existing runId through WorkflowRunner.start() without ever consulting mutatingWorkflowTaskIds. During the resume's pre-register window (script compile + await journal.load() — nothing registered, no handle, no claim), a sibling delete-history for that runId passes every gate, rms the journal directory, unlinks the snapshot, and answers {changed:true}. WorkflowJournal.load() returns an empty replay on the missing file rather than throwing, so the resume proceeds, register() re-creates the entry, the R7-5 latch releases on the non-terminal emission, and settlement rewrites the snapshot — the deleted run resurrects through an entrance the claim does not cover. Additionally, Session.deleteWorkflowHistory ignores removeTerminal()'s false return (~line 3468), reporting success even when the entry could not be removed.

Witness — probe with the real Session + registry + runner + snapshot store, journal load parked pre-register:

PROBE[ungated] deleteWorkflowHistory -> changed=true, snapshotExistsAfterDelete=false,
               registryEntryAfterDelete=null; post-settlement: run 'completed' and the
               snapshot file exists again
PROBE[gated]   (pending start visible to the gate) -> changed=false, snapshot retained

Fix: route re-registration of an existing runId (WorkflowRunner.start with resumeFromRunId, or register() itself) through the same task-global claim the control plane uses; defensively treat removeTerminal() returning false in Session.deleteWorkflowHistory as a failed deletion (return false) so a live re-registered run is never reported deleted.

Please pin the fix with a test: extend the R5-9 shape to drive the concurrent re-registration through the Workflow tool / resumeFromRunId instead of the retry ext-method and assert delete-history answers {changed:false} (or the run is not re-persisted), plus a Session.test.ts case asserting deleteWorkflowHistory returns false when registry.removeTerminal returns false — removing either guard must turn the respective test red.

中文说明

[Critical] 闭合第 7 轮跨会话竞态的任务级互斥申领只由这些控制面分支(delete-history、retry/rerun、run-saved)获取。Workflow 工具暴露了 schema 文档化的 resumeFromRunId 参数,并且已注册到 daemon 会话配置,因此模型发起的工具调用会通过 WorkflowRunner.start() 重新注册已有 runId,而完全不查询 mutatingWorkflowTaskIds。在 resume 的注册前窗口(脚本编译 + await journal.load()——未注册、无 handle、无申领),兄弟会话对该 runId 的 delete-history 会通过全部门禁:rm 日志目录、删除快照并返回 {changed:true}WorkflowJournal.load() 在文件缺失时返回空回放而非抛错,因此 resume 继续执行,register() 重建条目,R7-5 锁存器在非终态发射时释放,结算重写快照——被删除的运行通过申领未覆盖的入口复活。此外,Session.deleteWorkflowHistory 忽略 removeTerminal()false 返回(约 3468 行),即使条目未能移除也报告删除成功。

证据——真实 Session + registry + runner + 快照存储的探针(日志加载停靠在注册前):未设防分支返回 changed=true、快照被删、结算后运行状态 completed 且快照文件再次出现;设防分支(待启动对门禁可见)返回 changed=false、快照保留。

修复:让已有 runId 的重新注册(带 resumeFromRunIdWorkflowRunner.start,或 register() 本身)走与控制面相同的任务级申领;防御性地在 Session.deleteWorkflowHistory 中把 removeTerminal() 返回 false 视为删除失败(返回 false),使存活的重新注册运行永远不会被报告为已删除。

请为修复补充测试:扩展 R5-9 形态,用 Workflow 工具 / resumeFromRunId(而非 retry ext 方法)驱动并发重新注册,断言 delete-history 返回 {changed:false}(或运行未被重新持久化);另加一个 Session.test.ts 用例,断言当 registry.removeTerminal 返回 falsedeleteWorkflowHistory 返回 false——移除任一防护必须使对应测试变红。

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

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

11 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • R9-6 vacuous cancel-wait assertion — already reported (R1-10, comment 3839293554)
  • R9-7 todoStopGuard true-direction test gap — already reported (R2-13, comment 3839813829)
  • R9-8 run-saved overlap claim untested — already recorded (round-8 deferred list, acpAgent.ts:10996)
  • R9-9 workflow cancel refusal branches untested — already reported (R2-8, comment 3839813821)
  • R9-10 detachedWorkflowRegistries pruned only on the delete-history path — already recorded (round-8 deferred list, acpAgent.ts:3386)
  • R9-11 vacuous handle-wait deletion test — already recorded (round-6 deferred list, Session.test.ts:2758)
  • R9-12 capped-window test registry stub masks the R7-4 fix — already recorded (round-8 deferred list, Session.test.ts:3273)
  • R9-13 retry canStart handle guard untested — already recorded (round-6 deferred list, acpAgent.ts:10528)
  • R9-14 run-saved not-found branch untested — already reported (R2-6, comment 3839813817)
  • R9-15 dispose setSnapshotPersistedCallback clear unpinned — already recorded (round-8 deferred list, Session.test.ts:32106)
  • R9-16 run-saved launch error collapsed into bare changed:false — already recorded (round-5 deferred list, acpAgent.ts:10476)

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

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

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

Not explored to full depth (tool budget reached): chunk 2: none — no check was cut short..

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.

Deferred under the convergence posture (round 9, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/acp-integration/session/Session.ts:3428 — [probe] stale in-flight refresh republishes a run deleted meanwhile
  • packages/core/src/agents/runtime/workflow-runner.test.ts:389 — [probe] loadSpy ordering witness vacuous (no storage in test config)
  • packages/core/src/agents/runtime/workflow-runner.ts:181 — [probe] abort-during-start throws a plain Error the tool catch cannot map
  • packages/cli/src/acp-integration/acpAgent.ts:10884 — [probe] sessionTaskCancel is blind to the starting window
  • packages/core/src/agents/workflow-run-registry.ts:594 — [review] register's JSDoc orphaned onto reserveStart
中文说明

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

本轮确认的 11 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

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

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

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

未探索到全部深度(达到工具调用预算):chunk 2:none — no check was cut short.

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

收敛姿态下延后(第 9 轮,非阻断)——已记录,本轮不要求修改:共 5 条(原文未翻译,列表见上方英文部分)。

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

Four of the behavioural items the review deferred across rounds 8-9; the
test-coverage-only entries stay deferred.

- Cancel during the start window (R9 acpAgent.ts:10884 + workflow-runner.ts:181).
  Between `reserveStart` and `register` the runner loads the script and
  replays the journal — seconds, for a resume of a large one — and the
  registry has no entry yet. `sessionTaskCancel` answered `not_found` for a
  run the client could see starting, and `registry.cancel` could not reach
  the reserved controller either. New `cancelStarting` aborts it (the
  reservation stays the runner's to release, as after `abortAll`), and the
  cancel handler routes there when the liveness gate would say "starting".
  Doing that exposed the second half: the runner threw a bare `Error` for
  an abort during start, and the tool's catch only recognised the CALLER's
  signal — a registry-side abort surfaced as an unexplained failure. It is
  now a typed `WorkflowStartCancelledError`, mapped to the same
  "cancelled before it could start" result.
- `detachedWorkflowRegistries` (R8 acpAgent.ts:3386) was pruned only inside
  the delete-history liveness check. A daemon that closes sessions mid-run
  and never deletes history retained every registry for its lifetime. Prune
  on session close as well.
- Refresh/delete race (R9 Session.ts:3428). `refreshWorkflowHistory` reads
  the directory and merges without a claim; a delete landing between the
  read and the merge was overwritten by the stale listing and the run
  reappeared until the next refresh. Deletions are now sequenced, and a
  refresh drops any run deleted after its read began — keyed by runId and
  compared against the refresh's own mark, so a later retry that reuses
  the id is not suppressed.
- The "Register a new run" JSDoc sat on `reserveStart` (R9 registry:594).

Mutation-verified, all four at once against the full suites: disabling the
starting-window branch, the prune-on-close, the deletion filter, and the
typed-error mapping reddens exactly the four new tests and nothing else.

Claude-Session: https://claude.ai/code/session_01VXsC4f71S6U6YkW82NRw7m
@qqqys

qqqys commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator Author

6c79490 picks up four of the behavioural items from the round-8/9 deferred lists. The coverage-only entries stay deferred, as the convergence note suggested.

Deferred item What changed
R9 acpAgent.ts:10884 — sessionTaskCancel blind to the starting window · R9 workflow-runner.ts:181 — abort-during-start throws a plain Error One fix, two halves. New WorkflowRunRegistry.cancelStarting(runId) aborts the reserved controller (the reservation stays the runner's to release, as after abortAll); the cancel handler routes there when isStarting is true instead of answering not_found. The runner now throws a typed WorkflowStartCancelledError, and the tool's catch maps it to the "cancelled before it could start" result even though the caller's signal is still live.
R8 acpAgent.ts:3386detachedWorkflowRegistries pruned only on the delete-history path pruneDrainedWorkflowRegistries() also runs on session close, before a new registry is retained.
R9 Session.ts:3428 — stale in-flight refresh republishes a run deleted meanwhile Deletions are sequenced; a refresh drops any run deleted after its disk read began. Keyed by runId against the refresh's own mark, so a retry that reuses the id is not suppressed.
R9 workflow-run-registry.ts:594 — register's JSDoc orphaned onto reserveStart Moved; reserveStart got its own.

Tests: cancelStarting contract (registry), typed error (runner), registry-side cancel during preflight (tool), starting-window cancel + not_found control (acpAgent), prune-on-close (acpAgent), refresh/delete race with a re-run control (Session). Mutation-verified with all four fixes disabled at once against the full suites: exactly the four new tests go red, nothing else.

中文说明

6c79490 处理了第 8/9 轮延后列表中的四条行为类条目;纯测试覆盖类条目按收敛提示继续延后。

  • 启动窗口内无法取消 + 启动期中止抛裸 Error(R9 acpAgent.ts:10884 / workflow-runner.ts:181):一个修复的两半。新增 WorkflowRunRegistry.cancelStarting(runId) 中止已预留的 controller(预留本身仍由 runner 在启动失败路径释放,与 abortAll 一致);取消处理器在 isStarting 为真时走这条路径,而不再回答 not_found。runner 现在抛出带类型的 WorkflowStartCancelledError,工具的 catch 在调用方 signal 仍存活的情况下也能把它映射为「启动前已取消」的结果。
  • detachedWorkflowRegistries 只在 delete-history 路径清理(R8 acpAgent.ts:3386):会话关闭时、保留新 registry 之前也执行一次 pruneDrainedWorkflowRegistries()
  • 刷新与删除竞争,已删 run 复活(R9 Session.ts:3428):删除操作带序号;刷新会丢弃在其磁盘读取开始之后被删除的 run。按 runId 与该次刷新自己的标记比较,因此复用同一 id 的重试不会被误压制。
  • JSDoc 挂错(R9 workflow-run-registry.ts:594):已移到 registerreserveStart 补了自己的注释。

测试:registry 的 cancelStarting 契约、runner 的类型化错误、工具在 preflight 期间的 registry 侧取消、acpAgent 的启动窗口取消(含 not_found 对照)、关闭时清理、Session 的刷新/删除竞争(含重跑对照)。变异验证:同时禁用四处修复并跑完整套件,恰好只有这四个新测试变红。

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

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

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

Not reviewed: reverse audit — did not converge within the reverse-audit round cap of 5.

Deferred under the convergence posture (round 10, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/acp-integration/session/Session.ts:3496 — [review] removeTerminal() false return ignored in deleteWorkflowHistory
  • packages/cli/src/acp-integration/session/Session.ts:3526 — [review] Comment names a nonexistent method on a concurrency latch
  • packages/cli/src/acp-integration/session/Session.ts:3088 — [probe] Todo-stop-guard workflow branch has no test
  • packages/cli/src/acp-integration/acpAgent.ts:10901 — [review] Workflow not_running cancel branch untested
  • packages/cli/src/acp-integration/acpAgent.ts:10943 — [review] Workflow action whitelist untested
  • packages/cli/src/acp-integration/session/Session.test.ts:32215 — [probe] Dispose test misses the setSnapshotPersistedCallback teardown
  • packages/core/src/agents/workflow-run-registry.ts:1380 — [probe] Blocking-work message cannot name a starting workflow
  • packages/cli/src/acp-integration/acpAgent.test.ts:10947 — [probe] Cancel settlement wait is pinned by no mutation-sensitive test
  • packages/cli/src/acp-integration/session/Session.test.ts:3338 — [probe] R7-4 capped-window test never exercises the membership gate
  • packages/cli/src/acp-integration/acpAgent.test.ts:11173 — [probe] Refused pause/resume (changed: false) untested
  • packages/cli/src/acp-integration/acpAgent.test.ts:11329 — [probe] run-saved overlap claim has no test

[Critical] R10-1 (inline comment withheld — an existing unresolved review thread already occupies Session.ts:3823): The workflow active-work hold skips the starting window — collectActiveWorkHolds() iterates registry.list(), which has no entry between reserveStart and register, so during a run-saved/retry start's pre-register window the session reports zero workflow holds and isIdle() is true. A daemon-initiated conditional close (onlyIfUnheld authorizes on collectActiveWorkHolds().length === 0) then proceeds, dispose() aborts the reserved starting controller, the start throws WorkflowStartCancelledError and the requested run never registers; the pending action resolves {changed: false}, indistinguishable from 'unknown saved workflow'. Meanwhile hasRunningEntries() (extended in this diff) and the delete/cancel liveness gates count isStarting as live. Probe-verified: PR code reports no hold in the starting window (AssertionError: expected [] to deeply equal [ 'wf-starting' ]); with the fix (expose reserved ids, push the hold) isIdle() is false. Fix: expose the reserved runIds on WorkflowRunRegistry (e.g. listStartingRunIds()) and push a workflow hold in collectActiveWorkHolds(); the hold self-releases (registration takes over with the per-entry running hold; failed/cancelled start removes it via releaseStart). Witness owed: Session.test.ts sibling of 'holds executing workflow runs but never paused ones' asserting a starting run yields a workflow hold — removing the guard must turn it red.

中文说明

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

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

未审查:反向审计——在 5 轮的反审轮数上限内未收敛。

收敛姿态下延后(第 10 轮,非阻断)——已记录,本轮不要求修改:共 11 条(原文未翻译,列表见上方英文部分)。

[Critical] R10-1 (inline comment withheld — an existing unresolved review thread already occupies Session.ts:3823): The workflow active-work hold skips the starting window — collectActiveWorkHolds() iterates registry.list(), which has no entry between reserveStart and register, so during a run-saved/retry start's pre-register window the session reports zero workflow holds and isIdle() is true. A daemon-initiated conditional close (onlyIfUnheld authorizes on collectActiveWorkHolds().length === 0) then proceeds, dispose() aborts the reserved starting controller, the start throws WorkflowStartCancelledError and the requested run never registers; the pending action resolves {changed: false}, indistinguishable from 'unknown saved workflow'. Meanwhile hasRunningEntries() (extended in this diff) and the delete/cancel liveness gates count isStarting as live. Probe-verified: PR code reports no hold in the starting window (AssertionError: expected [] to deeply equal [ 'wf-starting' ]); with the fix (expose reserved ids, push the hold) isIdle() is false. Fix: expose the reserved runIds on WorkflowRunRegistry (e.g. listStartingRunIds()) and push a workflow hold in collectActiveWorkHolds(); the hold self-releases (registration takes over with the per-entry running hold; failed/cancelled start removes it via releaseStart). Witness owed: Session.test.ts sibling of 'holds executing workflow runs but never paused ones' asserting a starting run yields a workflow hold — removing the guard must turn it red.

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

Comment thread packages/cli/src/acp-integration/session/Session.ts
Comment thread packages/cli/src/acp-integration/acpAgent.ts Outdated
Comment thread packages/cli/src/acp-integration/acpAgent.ts Outdated
Comment thread packages/core/src/agents/runtime/workflow-runner.ts Outdated
qqqys added 2 commits August 28, 2026 10:34
- Key the starting-window cancel on a live reservation rather than on the
  absence of an entry: a retry reuses its runId, so its terminal entry
  shadowed the reservation and cancel answered `not_running` about a run
  that was actively starting.
- Answer `changed: false` from retry when `execute()` reports a start that
  never registered (no `workflowRunId`), mirroring rerun and run-saved.
- Classify a registry-side abort of the reserved controller as a cancel in
  foreground starts too, not only background ones; the tool maps
  `WorkflowStartCancelledError` in either mode.
- Report reserved-but-unregistered runs as workflow active-work holds
  (`WorkflowRunRegistry.listStartingRunIds`), so a daemon conditional close
  cannot dispose the session under a start it just accepted.
- Propagate a successful history deletion into every sibling session's
  deletion marker and cached history, symmetric to the `removeTerminal`
  sweep, so a sibling refresh that had already read the directory cannot
  republish the deleted run.

Claude-Session: https://claude.ai/code/session_018dYE4LwSMeMPFchXk5UBdM
@qqqys

qqqys commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Round-10 R10-5 (body-only, inline withheld — starting-window active-work hold) is closed in f119607: WorkflowRunRegistry.listStartingRunIds() exposes the reserved ids and Session.collectActiveWorkHolds() pushes a workflow hold for each, so a daemon conditional close cannot dispose the session under a start it just accepted. The hold self-releases (registration takes over with the entry's running hold; a failed/cancelled start drops the reservation via releaseStart). Pinned by 'holds a workflow run that is reserved but not yet registered' in Session.test.ts; removing the loop turns it red.

The other four round-10 Criticals are answered in their threads. c58b3fd merges upstream/main (one conflict, sdk-typescript/scripts/build.js — took main's 215KB budget; the merged bundle builds within it).

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Qwen Code review did not complete successfully. The review pipeline failed before a review could be posted. A transient error is retried automatically; if you are seeing this, retry with @qwen-code /review. See workflow logs.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Partially reviewed — gaps disclosed.

15 Suggestion-level finding(s) this review confirmed are already reported on this PR and are not repeated:

  • vacuous completion-wait pin on 'cancels active workflow tasks' — already reported (R1-10, comment 3839293554)
  • workflow-action route has no cross-workspace dispatch test — already reported (R1-20, comment 3839293575)
  • legacy persisted-snapshot shape never exercised — already reported (R1-21, comment 3839293576)
  • opt-out test exercises only the persisted-history branch of the gate — already reported (R1-23, comment 3839293580)
  • new fake's status?: string is a latent TS2322 behind the TODO(5691) exclude — already recorded (round-7 deferred list, server.test.ts:939)
  • repurposed cancel test leaves legacy kinds without an untrusted-side success pin — already reported (R4-5, comment 3851196166; round-3/5/6 deferred lists)
  • kill-switch vs workflowsEnabledBySettings precedence unpinned in capabilities tests — already recorded (round-6 deferred list, server.test.ts:4066)
  • R7-4 capped-window test's registry mock masks the membership-gate fix — already recorded (round-8 deferred Session.test.ts:3273; round-10 deferred Session.test.ts:3338)
  • comment names nonexistent #forgetPersistedWorkflowRun — already recorded (round-10 deferred list, Session.ts:3526)
  • SDK wire-level tests missing for controlWorkflowTask/sessionWorkflowTaskAction — already reported (R1-14, comment 3839293563)
  • todo-stop-guard workflow branch has no test — already reported (R2-13, comment 3839813829)
  • invalid-action rejection path untested at every layer — already reported (R1-7, comment 3839293546)
  • writeWorkflowSnapshot boolean contract unasserted — already recorded (round-7 deferred list, workflow-snapshot.ts:140)
  • three new webui workflow actions have zero production call sites — already recorded (round-6 deferred list, actions.ts:2055)
  • delete persistence-wait test vacuity — already reported (R2-16, comment 3839813833)

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

Not explored to full depth (tool budget reached): "agent 1c": whether standalone-session restore is actually reachable for untrusted workspaces (standalone-session-service trust posture) — I could not settle this within bu…; chunk 11: running packages/cli vitest suites (tasksSnapshot.test.ts, transport.test.ts) — the review worktree has no node_modules and a full install+build exceeded the …; "agent reverse-audit (round 1)": none — all planned checks completed; the one unresolved verification (whether beginDrain tears down old SSE subscriptions) is disclosed inside the finding's f…; chunk 3: execute packages/cli vitest run of acpAgent.test.ts to confirm the new tests pass (no node_modules in this review worktree; dependency install + workspace….

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

Deferred under the convergence posture (round 11, not a blocker) — recorded, not requested in this round:

  • packages/cli/src/acp-integration/session/Session.test.ts:3001 — [review] noteExternalWorkflowDeletion's clearing of the observer's unpersistedWorkflowHistory is pinned by no test

Convergence: round 11 posted 2 inline comment(s), 2 of them reported for the first time; the previous round posted 4 (4 new). Findings keep coming back to the same files: packages/cli/src/acp-integration/acpAgent.ts (findings in round 10; 1 more now). A cluster that keeps producing siblings usually means the fixes are treating instances of a shared root cause — triaging that cause before the next round, or splitting an independent cluster into its own pull request, tends to end the loop faster than fixing them one at a time. (Observation only — nothing was withheld from this review because of this observation.)

Mechanism health: this round did not close cleanly, so it withholds the incremental anchor — and the round it recovered had no anchor this round could use either — none at all, one with no certifier, one certified by an identity other than the one this round runs under, or one this round's fetch refused or resolved to the head — so the next review re-reads the whole diff unless recovery grafts an earlier own anchor that the round running it can use onto the complete work list this round leaves behind, and keeps doing so until a round's marker carries an anchor again or a graft lands that the round running it can use. (Stated, not acted on — this changes nothing about what the round posts.)

中文说明

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

本轮确认的 15 条建议级发现已在 PR 上报告过,不再重复发布(列表见上方英文部分)。

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

未探索到全部深度(达到工具调用预算):"agent 1c"whether standalone-session restore is actually reachable for untrusted workspaces (standalone-session-service trust posture) — I could not settle this within bu…;chunk 11:running packages/cli vitest suites (tasksSnapshot.test.ts, transport.test.ts) — the review worktree has no node_modules and a full install+build exceeded the …"agent reverse-audit (round 1)"none — all planned checks completed; the one unresolved verification (whether beginDrain tears down old SSE subscriptions) is disclosed inside the finding's f…;chunk 3:execute packages/cli vitest run of acpAgent.test.ts to confirm the new tests pass (no node_modules in this review worktree; dependency install + workspace…

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

收敛姿态下延后(第 11 轮,非阻断)——已记录,本轮不要求修改:共 1 条(原文未翻译,列表见上方英文部分)。

收敛情况:第 11 轮发布了 2 条行内评论,其中 2 条是首次提出;上一轮发布了 4 条(其中 4 条首次提出)。发现反复回到同一批文件:packages/cli/src/acp-integration/acpAgent.ts(第 10 轮已出过发现,本轮又有 1 条)。一个不断再生兄弟发现的簇,通常意味着逐条修复只在处理同一根因的实例——先定位并处理该根因,或把独立的簇拆成单独的 PR,通常比逐条修复更快结束循环。(仅为观察——本轮评审未因此扣留任何内容。)

机制健康:本轮未能干净收尾,因而扣留了增量锚点,而它恢复到的那一轮也没有留下本轮可用的锚点——要么完全没有、要么没有认证者、要么由本轮运行身份之外的身份认证、要么被本轮的获取拒绝或解析为头提交——因此下一次评审将重读整个 diff,除非恢复流程把本轮能使用的更早自有锚点嫁接到本轮留下的完整工作清单上;并会一直如此,直到某一轮的标记重新带上锚点,或落地的嫁接能被运行该轮的评审使用。(仅陈述,不据此行动——这不改变本轮发布的任何内容。)

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

Comment thread packages/cli/src/acp-integration/acpAgent.ts Outdated
Comment thread packages/cli/src/serve/run-qwen-serve.ts
…ent across sessions

Two cross-session gaps in the workflow control surface:

- A retry consulted only the requesting session's registry. Every
  session shares one journal/snapshot store and the task-global claim is
  released as soon as the background start returns, so a sibling whose
  registry still showed the run `failed` started a second runner under
  the same runId. Retry now refuses while the runId is live in any
  session (or in its own starting window), checked synchronously beside
  canStart so the answer cannot go stale before the claim is taken.

- Workspace reload updated `tools.workflowsEnabled` for `/capabilities`
  but never told existing sessions; `Config.workflowsEnabled` was set
  once at construction. The reload's `tools` branch now propagates the
  flag and pushes an available-commands update when it flips.

Claude-Session: https://claude.ai/code/session_01NkW1J2aBKcsKS62dkPcWbT
@qqqys

qqqys commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator Author

Superseded by #10411 — same branch history plus the round-11 fixes, reopened on a fresh branch to reset the review thread history (~180 threads over 11 rounds). Closing this one.

@qqqys qqqys closed this Aug 28, 2026
pull Bot pushed a commit to Little-Star888/qwen-code that referenced this pull request Aug 31, 2026
* feat(serve): expose workflow tasks and controls

* fix(serve): align workflow capability gating

* fix(cli): reject live workflow history deletion

* fix(serve): start controlled workflows in background

* fix(serve): address workflow control review blockers

* fix(serve): make workflow history deletion race-safe

* fix(serve): enforce workspace trust on the daemon Workflow surfaces (QwenLM#9546)

* fix(serve): close cross-session workflow deletion races and untrusted task reads (QwenLM#9546)

- Session history merge now treats a persisted snapshot as authoritative
  over a stale callback cache, and retires the cache once the runner
  confirms the snapshot write (new registry snapshot-persisted hook), so
  a sibling's deletion is not resurrected on refresh.
- History deletion consults every sibling session's run registry (live
  entries and settling handles) before deleting from the shared store,
  and a successful deletion purges sibling terminal entries so retries
  cannot re-persist a deleted run.
- Workflow holds mirror the registry's hasRunningEntries: paused runs no
  longer pin the session indefinitely.
- The includeWorkflows opt-in is gated on workspace trust at the daemon
  boundary in both the ACP-HTTP dispatch and the REST tasks route,
  matching the fail-closed shape of the other workflow surfaces.

* fix(cli): close the four cross-session workflow-history consistency holes

Addresses R5-9, R7-4, R7-5 and R7-10 — all four Criticals open on this PR.
They are one family: a run's history can be deleted, retried, or listed
from any session, and each gate had a different idea of which runs exist.

R5-9 — the mutation claim is task-global, not session-scoped. Keyed
`sessionId\0taskId`, it serialized nothing that mattered: every session
shares one snapshot store. A sibling's retry passed canStart (`failed`,
no handle), took its own per-session claim, then awaited journal
load/compile before `register()`; a delete-history landing in that
structural window found the run terminal and handle-less in every
registry, removed the journal directory and snapshot, and answered
`{changed: true}` — after which the retry re-registered and its
settlement re-persisted the history the user was told was deleted. The
claim is now keyed by taskId alone and taken by delete-history, retry,
rerun and run-saved. `run-saved` keys off a saved workflow's NAME, so it
claims in its own `saved\0` namespace rather than colliding with runIds.

R7-4 — deletion tests membership against the uncapped merged set.
`buildSessionTasksStatus` serializes every registry entry
unconditionally while `refreshWorkflowHistory` truncates to
MAX_RETAINED_SNAPSHOTS by startTime, so a long run that settled after
~30 newer ones started stayed listed via the registry but fell out of
the window — terminal, handle-free, live in no sibling, and permanently
undeletable. `refreshWorkflowHistory` now records the merged id set
before the cap, and deletion gates on that, the registry, or the
unpersisted cache. `deleteWorkflowSnapshot` already tolerates an absent
target, so the wider gate cannot delete what is not there.

R7-5 — snapshot retirement is a latch. The registry's dispatch-drain
callbacks emit status changes on TERMINAL entries with no status gate,
and in-flight dispatches keep draining across the snapshot write, so a
terminal emission routinely landed after `notifySnapshotPersisted` had
retired the cache entry — re-inserting the run as "never persisted".
A sibling's deletion was then undone by the next refresh, which reads
"absent on disk, present in cache" as a pending write and republishes.
Persistence is now remembered per runId and `#rememberWorkflowHistory`
returns early for members; the latch releases when the runId goes active
again, so a genuine re-run is still cached.

R7-10 — the liveness gate sees runs whose session is gone. It iterated
`this.sessions` only, but close/kill/shutdown use force semantics and a
background run owns a detached controller, so after
`removeStoredSessionEntry` a still-settling run was invisible to the
gate and unreachable by the delete handler's sibling `removeTerminal`
loop: a sibling delete-history removed the LIVE run's journal and
snapshot, and the orphan's settlement write recreated it. Two halves —
`Session.dispose()` now aborts its workflow registry the way it already
aborts the agent registry (before the callbacks are torn down), and the
registry of a removed session is retained here until its runs drain, so
the gate still answers across the settlement window an abort cannot
compress to zero. Retention is bookkeeping: a Config that cannot answer
is logged, never turned into a shutdown failure.

Regressions, each mutation-checked against its own repair:
- acpAgent: a sibling's parked retry makes delete-history answer
  `{changed: false}` without reaching the store, and the deletion goes
  through once the claim releases
- acpAgent: a live run's registry stays visible to the gate across its
  session's close, and is dropped once the handle is released
- Session: a status emission after `snapshotPersisted` no longer
  resurrects a sibling-deleted run (reverting the latch reproduces the
  reviewer's probe verbatim), and a re-registered runId is remembered
  again
- Session: a run with the oldest startTime behind 30 newer snapshots is
  deletable
- Session: dispose aborts the workflow registry before clearing its
  callbacks

Verification: Session.test.ts 733 passed, acpAgent.test.ts 514 passed,
`tsc --noEmit -p packages/cli` clean, eslint and prettier clean. The 14
`packages/cli/src/serve` failures (fast-path import boundary,
capabilities-docs contract, workspace fs/agents/memory, conversation
runtime ownership) reproduce identically on the unmodified head — base
skew, untouched by this change.

Claude-Session: https://claude.ai/code/session_01M7z4PccYfDPyyfg3oGr8V1

* fix(core): drop the duplicated telemetry-swap mock property

Merging upstream/main (053f17b) clears the 9 `client.telemetrySwap`
failures this branch had from predating QwenLM#10220, but main carries its own
TS1117 on the same file: two commits added `getToolRegistry` to the same
object literal independently and neither saw the other.

  032b907 feat(serve): backfill session PR bindings ... (QwenLM#9729)
  8241905 test(core): give the telemetry-swap client mock a
             getToolRegistry (QwenLM#10220)

Checking that file out from upstream/main here and running
`tsc --noEmit -p packages/core` reproduces
`client.telemetrySwap.test.ts(103,5): error TS1117` verbatim, so taking
the merge unmodified would have traded 9 test failures for a build that
never reaches the tests at all.

Removed as part of the merge rather than left for a follow-up: keeps
QwenLM#10220's copy, which was added for this purpose and carries the
explanation, and drops QwenLM#9729's incidental one. main still needs the same
removal — this only keeps it out of the branch.

Verified after the merge: `tsc --noEmit` clean for both packages/core
and packages/cli; client.telemetrySwap 10 passed, Session.test.ts and
acpAgent.test.ts 1260 passed together.

Claude-Session: https://claude.ai/code/session_01M7z4PccYfDPyyfg3oGr8V1

* fix(workflows): guard pending run lifecycle

* fix(workflows): let a starting run be cancelled, and stop two slow leaks

Four of the behavioural items the review deferred across rounds 8-9; the
test-coverage-only entries stay deferred.

- Cancel during the start window (R9 acpAgent.ts:10884 + workflow-runner.ts:181).
  Between `reserveStart` and `register` the runner loads the script and
  replays the journal — seconds, for a resume of a large one — and the
  registry has no entry yet. `sessionTaskCancel` answered `not_found` for a
  run the client could see starting, and `registry.cancel` could not reach
  the reserved controller either. New `cancelStarting` aborts it (the
  reservation stays the runner's to release, as after `abortAll`), and the
  cancel handler routes there when the liveness gate would say "starting".
  Doing that exposed the second half: the runner threw a bare `Error` for
  an abort during start, and the tool's catch only recognised the CALLER's
  signal — a registry-side abort surfaced as an unexplained failure. It is
  now a typed `WorkflowStartCancelledError`, mapped to the same
  "cancelled before it could start" result.
- `detachedWorkflowRegistries` (R8 acpAgent.ts:3386) was pruned only inside
  the delete-history liveness check. A daemon that closes sessions mid-run
  and never deletes history retained every registry for its lifetime. Prune
  on session close as well.
- Refresh/delete race (R9 Session.ts:3428). `refreshWorkflowHistory` reads
  the directory and merges without a claim; a delete landing between the
  read and the merge was overwritten by the stale listing and the run
  reappeared until the next refresh. Deletions are now sequenced, and a
  refresh drops any run deleted after its read began — keyed by runId and
  compared against the refresh's own mark, so a later retry that reuses
  the id is not suppressed.
- The "Register a new run" JSDoc sat on `reserveStart` (R9 registry:594).

Mutation-verified, all four at once against the full suites: disabling the
starting-window branch, the prune-on-close, the deletion filter, and the
typed-error mapping reddens exactly the four new tests and nothing else.

Claude-Session: https://claude.ai/code/session_01VXsC4f71S6U6YkW82NRw7m

* fix(core): close the round-5 review findings on workflow task controls

- Key the starting-window cancel on a live reservation rather than on the
  absence of an entry: a retry reuses its runId, so its terminal entry
  shadowed the reservation and cancel answered `not_running` about a run
  that was actively starting.
- Answer `changed: false` from retry when `execute()` reports a start that
  never registered (no `workflowRunId`), mirroring rerun and run-saved.
- Classify a registry-side abort of the reserved controller as a cancel in
  foreground starts too, not only background ones; the tool maps
  `WorkflowStartCancelledError` in either mode.
- Report reserved-but-unregistered runs as workflow active-work holds
  (`WorkflowRunRegistry.listStartingRunIds`), so a daemon conditional close
  cannot dispose the session under a start it just accepted.
- Propagate a successful history deletion into every sibling session's
  deletion marker and cached history, symmetric to the `removeTerminal`
  sweep, so a sibling refresh that had already read the directory cannot
  republish the deleted run.

Claude-Session: https://claude.ai/code/session_018dYE4LwSMeMPFchXk5UBdM

* fix(serve): keep workflow retry and the workflowsEnabled flag consistent across sessions

Two cross-session gaps in the workflow control surface:

- A retry consulted only the requesting session's registry. Every
  session shares one journal/snapshot store and the task-global claim is
  released as soon as the background start returns, so a sibling whose
  registry still showed the run `failed` started a second runner under
  the same runId. Retry now refuses while the runId is live in any
  session (or in its own starting window), checked synchronously beside
  canStart so the answer cannot go stale before the claim is taken.

- Workspace reload updated `tools.workflowsEnabled` for `/capabilities`
  but never told existing sessions; `Config.workflowsEnabled` was set
  once at construction. The reload's `tools` branch now propagates the
  flag and pushes an available-commands update when it flips.

Claude-Session: https://claude.ai/code/session_01NkW1J2aBKcsKS62dkPcWbT

* fix(serve): refuse to report a workflow history deletion whose registry entry survived

`deleteWorkflowHistory` ignored `removeTerminal()`'s answer. The registry
refuses to remove a live or handle-held entry — its own last word on
whether the run is still active in this session — so a `false` for an
entry that exists meant the run re-registered under the deletion and
would re-persist the history the client was just told was gone. The
entry is now retired before the store is touched, and a refusal fails
the deletion; a persisted-only run has no entry and is unaffected.

Claude-Session: https://claude.ai/code/session_01NkW1J2aBKcsKS62dkPcWbT

* docs(serve): fix a stale method reference in the workflow history comment

The persisted latch is released inline at the top of #rememberWorkflowHistory;
no #forgetPersistedWorkflowRun exists.

Claude-Session: https://claude.ai/code/session_01NkW1J2aBKcsKS62dkPcWbT

* fix: report workflow lifecycle changes

* fix(core): repair the workflow test doubles and converge every eviction

The previous commit added a `listStartingRunIds()` call to
`describeBlockingBackgroundWork` without teaching the registry test
doubles about it, so seven tests across `clearCommand`,
`useBranchCommand` and `useResumeCommand` threw
`listStartingRunIds is not a function`. Add the reader to those stubs.

Also move the eviction's status emission from `releaseHandle` into
`evictTerminal` itself. Emitting only from `releaseHandle` left the
same convergence gap at the four sweeping call sites that already
emit: complete / fail / cancel / abortAll emit BEFORE they sweep, so a
consumer that re-reads on the callback observes the pre-eviction list
and keeps rendering a row that was just dropped. Emitting once after
the sweep closes all five paths, and a future eviction site inherits
the guarantee. New test pins the ordering; another pins that a release
which evicts nothing stays silent.

A starting reservation now reports `starting` rather than borrowing
`running`, and its line no longer repeats the run id the bracket
already carries.

Claude-Session: https://claude.ai/code/session_01VENc5rZYmMJDdqvjwBEyZd

* fix(cli): drop the duplicated mock keys and stop misdirecting a starting run

R3-2: the previous commit's stub plumbing re-inserted a `list:` line into
four `clearCommand.test.ts` registry mocks that already declared one.
Last-wins made it behaviour-neutral, and neither eslint (`no-dupe-keys`
is off here) nor tsc (the literal is not contextually typed, unlike the
`useResumeCommand` mocks where TS1117 did fire) caught it. Remove the
duplicates.

R3-1: when the only blocker is a reserved-but-unregistered run, the
blocked message ended with "Use /workflows to inspect them, then retry."
— but `/workflows` renders `registry.list()`, which a reservation has
not entered, so it named a surface that cannot show what is blocking.
Track inspectability separately and fall back to a plain retry hint when
nothing is listable; a registered run in the same set still points at
`/workflows`.

Claude-Session: https://claude.ai/code/session_01VENc5rZYmMJDdqvjwBEyZd

* fix(cli): preserve starting workflow guidance

* test(cli): pin background work hint guards

* fix(serve): redact untrusted standalone workflow replay

* test(web-shell): fix daemon SDK mock path

---------

Co-authored-by: qqqys <266654365+qqqys@users.noreply.github.com>
Co-authored-by: Qwen Code Autofix <qwen-code-autofix@users.noreply.github.com>
Co-authored-by: qwen-code-dev-bot <qwen-code-dev@service.alibaba.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

review/self-reported The linked issue was opened by the PR author (self-reported)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(serve): expose Workflow tasks and controls

4 participants