Skip to content

feat(workflows): add cooperative pause and resume - #8320

Merged
wenshao merged 35 commits into
QwenLM:mainfrom
qqqys:codex/issue-8105-workflow-pause-resume
Aug 8, 2026
Merged

feat(workflows): add cooperative pause and resume#8320
wenshao merged 35 commits into
QwenLM:mainfrom
qqqys:codex/issue-8105-workflow-pause-resume

Conversation

@qqqys

@qqqys qqqys commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

What this PR does

This PR adds whole-run cooperative pause and resume to Dynamic Workflows. A pause-aware per-run scheduler stops dequeuing new agent dispatches, lets already in-flight work converge, and holds fulfilled or rejected results at a gate until the run resumes. Cancellation rejects queued work and wakes gated waiters while preserving exactly-once dispatch settlement.

The workflow lifecycle now distinguishes running, pausing, and paused. Background Tasks exposes p to pause or resume and keeps all active states stoppable, while /workflows p <runId> provides the same process-local control in the interactive TUI. Text, pill, sorting, snapshot, shutdown, and session-switch consumers now treat all three states consistently.

Why it's needed

Opt-in background workflows can continue after the parent turn, but before this change operators could only observe or stop an entire run. Cooperative pause provides a truthful control boundary: no new agents start, queued work remains ordered, in-flight agents may finish, and the workflow script does not consume their results until resume.

This is the next stage of the Dynamic Workflows roadmap. It does not add durable cross-process resume, change the journal format, freeze arbitrary JavaScript or external promises, or introduce per-agent controls.

Reviewer Test Plan

How to verify

  1. Start an opt-in background workflow with concurrency one and at least three parallel agent calls, then pause it while the first agent is in flight. Expect the UI to move through Pausing to Paused, with the second and third agents still queued.
  2. Close Background Tasks and run /workflows. Expect the paused run to remain in the Active bucket. Run /workflows p <runId> and expect the queued agents to continue in FIFO order and the workflow to emit exactly one completion notification.
  3. Repeat with a paused workflow and stop it instead of resuming. Expect queued agents never to start, no success notification to be emitted, and the parent session to remain usable for a subsequent prompt.
  4. Confirm foreground workflows retain their existing synchronous result and failure behavior, and confirm non-interactive and ACP modes reject pause control clearly.

Evidence (Before & After)

Before: a background workflow could be observed or stopped, but there was no pause state, pause gate, or resume control.

After: real terminal testing against the production bundle observed Running → Pausing → Paused → Running, kept queued dispatches out of the provider while paused, resumed them in A, B, C order through /workflows p <runId>, emitted one completion notification after resume, and emitted none after stopping a paused run.

Local verification: 251 focused core tests and 128 focused CLI tests passed; full lint, build, typecheck, and bundle passed; the real terminal pause/resume and pause/cancel scenarios passed.

Tested on

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

Environment (optional)

macOS with Node.js v24.14.1, the production dist/cli.js bundle, node-pty, headless xterm, Ink, and the repository fake OpenAI server.

Risk & Scope

  • Main risk or tradeoff: pause is cooperative rather than a hard process suspension; already in-flight agents may finish before the run reaches Paused.
  • Not validated / out of scope: real provider authentication, network latency, durable cross-process resume, journal durability, per-agent controls, and freezing ordinary JavaScript or arbitrary external promises.
  • Breaking changes / migration notes: none; background execution remains opt-in and the journal format is unchanged.

Linked Issues

Part of #8105

中文说明

本 PR 做了什么

本 PR 为 Dynamic Workflows 增加整次运行级别的协作式暂停与恢复。每次运行拥有一个可感知暂停状态的调度器:暂停时停止取出新的 Agent 调度,允许已经在飞的任务自然收敛,并把成功或失败结果阻塞在结果门之后,直到运行恢复。取消会拒绝队列中的工作、唤醒等待结果门的调用,同时保持每次调度只结算一次。

Workflow 生命周期现在明确区分 runningpausingpaused。Background Tasks 使用 p 暂停或恢复,并允许停止所有 active 状态;交互式 TUI 中的 /workflows p <runId> 提供同等的进程内控制。文本输出、footer pill、排序、snapshot、shutdown 和 session switch 等消费者也统一处理这三个状态。

为什么需要

Opt-in 后台 Workflow 可以在父 turn 返回后继续执行,但本次改动前,操作者只能观察或停止整次运行。协作式暂停提供了与事实一致的控制边界:不再启动新 Agent、队列顺序保持稳定、在飞 Agent 可以完成,而 Workflow 脚本在恢复前不会消费其结果。

这是 Dynamic Workflows 路线图的下一阶段。本 PR 不增加跨进程持久恢复,不改变 journal 格式,不冻结任意 JavaScript 或外部 Promise,也不引入逐 Agent 控制。

Reviewer 测试计划

如何验证

  1. 启动一个并发度为一、至少包含三个并行 Agent 调用的 opt-in 后台 Workflow,在第一个 Agent 在飞时暂停。预期 UI 从 Pausing 进入 Paused,第二、第三个 Agent 仍保留在队列中。
  2. 关闭 Background Tasks 并运行 /workflows。预期暂停中的运行仍位于 Active 分组。执行 /workflows p <runId>,预期队列中的 Agent 按 FIFO 顺序继续,并且 Workflow 只产生一次完成通知。
  3. 再启动一个场景,在暂停后停止而不是恢复。预期队列中的 Agent 永不启动、不产生成功通知,并且父会话仍能正常回答下一条消息。
  4. 确认 foreground Workflow 保持原有同步结果与失败语义,并确认 non-interactive 和 ACP 模式会明确拒绝暂停控制。

证据(Before & After)

Before:后台 Workflow 可以被观察或停止,但没有暂停状态、暂停 gate 或恢复控制。

After:基于生产 bundle 的真实终端测试观察到 Running → Pausing → Paused → Running;暂停期间队列任务没有进入 provider;通过 /workflows p <runId> 恢复后,调度顺序为 A, B, C;恢复完成后只产生一次通知,停止暂停中的运行则不产生完成通知。

本地验证:251 个 core focused tests 与 128 个 CLI focused tests 通过;全量 lint、build、typecheck 和 bundle 通过;真实终端 pause/resume 与 pause/cancel 场景均通过。

测试系统

OS 状态
🍏 macOS
🪟 Windows ⚠️
🐧 Linux ⚠️

环境(可选)

macOS、Node.js v24.14.1、生产 dist/cli.js bundle、node-pty、headless xterm、Ink,以及仓库 fake OpenAI server。

风险与范围

  • 主要风险或取舍:暂停是协作式,而不是进程级硬挂起;已经在飞的 Agent 可能在运行进入 Paused 前完成。
  • 未验证 / 范围外:真实 provider 认证与网络延迟、跨进程持久恢复、journal durability、逐 Agent 控制,以及冻结普通 JavaScript 或任意外部 Promise。
  • Breaking changes / 迁移说明:无;后台执行仍为 opt-in,journal 格式未改变。

关联 Issue

Part of #8105

@qqqys

qqqys commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

E2E evidence / E2E 证据

Validated the production bundle in a real terminal harness using node-pty, headless xterm, and Ink on macOS with Node.js v24.14.1. The model transport was the repository fake OpenAI server.

  • Pause/resume: observed Running → Pausing → Paused; only agent A reached the provider before pause; /workflows kept the paused run in Active; /workflows p <runId> resumed the run; dispatch order was exactly A, B, C; completion notification count was exactly 1.
  • Pause/cancel: only agent A reached the provider; stopping the paused run left B/C undispatched; completion notification count was 0; a subsequent ordinary prompt received a normal response.
  • Static verification: core focused tests 251/251, CLI focused tests 128/128, full lint, build, typecheck, and bundle all passed.

限制:该 E2E 使用仓库 fake OpenAI server,没有覆盖真实 provider 的认证、网络和延迟;暂停是 cooperative,不冻结普通 JavaScript 或任意外部 Promise。

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

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

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Re-run at the new head — round-13 review fixes have landed since the last full pass (2026-08-01), and a maintainer approval arrived at this exact head. Re-checking the gate from the top.

Template ✓ — all required sections present, bilingual body, and a concrete reviewer test plan with before/after expectations.

Problem: this is roadmap feature work, not a bugfix — the Dynamic Workflows staged roadmap (#8105) carries the repo's roadmap/background-automation and roadmap/subagents-tools labels, and this is PR3 of that train (predecessors #8107, #8240, #8303 already merged). The stated gap is real and observable: background workflow runs could only be observed or stopped outright, with no intermediate control between the two.

Direction: aligned — a direct next stage of a roadmap the maintainers are actively shipping. No direction concerns.

Size: core paths are touched (packages/core/src/agents/**, packages/core/src/tools/**, packages/core/src/utils/**). Per the gate count: ~2,006 production-logic lines (core/CLI logic 1,908 + i18n keys 81 + docs 17) vs ~3,602 test lines across 42 files. This is a feat, so no hard block — but it is far past the 500-line maintainer-awareness threshold and the 1,000-line large-PR advisory. Flagging for maintainer awareness (already engaged — see the approval at this head). The train structure already splits the roadmap across PRs, and this stage is one coherent unit (scheduler + result gate + lifecycle + UI surfaces), so I would not push for further splitting.

Approach: the scope feels right. Every piece ties to the stated goal — the pause-aware scheduler replaces the concurrency limiter (whose only consumer was the orchestrator), the result gate hangs off the existing settlement paths, and the UI changes are the same lifecycle surfaced on existing controls. No unrelated changes or drive-by refactors spotted.

Risk: no high-risk-path signal from the revert-history check.

Moving on to code review. 🔍

中文说明

在新 head 上重跑——自上次完整审查(2026-08-01)以来第 13 轮 review 修复已合入,且 maintainer 已在当前 head 上批准。从头重新过 gate。

模板 ✓ —— 必填章节齐全,正文双语,reviewer 测试计划包含具体的 before/after 预期。

**问题:**这是路线图功能开发,不是 bugfix —— Dynamic Workflows 分阶段路线图(#8105)带有仓库的 roadmap/background-automationroadmap/subagents-tools 标签,本 PR 是该系列第 3 个(前置 #8107#8240#8303 已合并)。所述缺口真实可见:后台 workflow 运行此前只能被观察或整体停止,两者之间没有中间控制。

**方向:**对齐 —— 是维护者正在推进的路线图的直接下一阶段。无方向性顾虑。

**规模:**触及核心路径(packages/core/src/agents/**packages/core/src/tools/**packages/core/src/utils/**)。按 gate 口径统计:约 2,006 行生产逻辑(core/CLI 逻辑 1,908 + i18n 键 81 + 文档 17),约 3,602 行测试,共 42 个文件。这是 feat,不硬阻断——但远超 500 行维护者知会阈值与 1,000 行大 PR 提示线。提请维护者知会(维护者已介入——当前 head 已有批准)。系列 PR 结构已将路线图拆分,本阶段是一个内聚单元(调度器 + 结果门 + 生命周期 + UI 面),不建议进一步拆分。

**方案:**范围合理。每一部分都服务于既定目标——可感知暂停的调度器替换并发限制器(其唯一消费者是 orchestrator),结果门挂在既有结算路径上,UI 改动是同一生命周期在既有控件上的呈现。未发现无关改动或顺手重构。

**风险:**revert 历史高风险路径检查无命中。

进入代码审查 🔍

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Code review

Reviewed the full diff at 750133f4, with a hunk-by-hunk pass over the round-13 delta since the last reviewed head (89b78039).

My independent proposal for cooperative pause would be: a pause-aware state machine at the dispatch layer (where concurrency is already enforced), a result gate so in-flight results aren't consumed by the script while paused, lifecycle widening in the registry, and consistent surfacing on the existing controls. This PR matches that shape almost exactly:

  • The new WorkflowDispatchScheduler replaces createConcurrencyLimiter (sole consumer updated, old module and its lint allowlist entry removed — clean removal). pause()/resume() both guard on the abort signal, settlement is exactly-once, and abortPending() rejects queued work and gate waiters while letting in-flight dispatches drain — the cooperative contract the PR promises.
  • The pause gate (waitUntilRunning()) is chained onto every settlement path I looked for: cache hit, successful dispatch, failed dispatch, budget-gate refusal, and agent-cap refusal. Both gate arms resolve successes and re-throw errors rather than manufacturing teardown rejections — the comment explaining why a held success must not turn into an unhandledRejection on a correctly-cancelled run is exactly the right reasoning.
  • Registry transitions are strictly guarded (pausing only from running, paused only from pausing, running only from paused, terminal checked first), and hasRunningEntries() deliberately excludes paused with a matching teardown fix — paused runs are aborted before the session-switch reset instead of leaking a gated script and its vm context.
  • The round-13 delta is disciplined: every fix cites the finding it pins (R11-*/R12), the settlement guard generalizes from cancelled to any terminal status, the watchdog's banked-budget accounting moves to the monotonic clock, snapshot and telemetry projections are captured before the first await, and the new unhandledRejection escape hook is installed/removed symmetrically around the run body. Test-to-production ratio is ~1.8:1, and findings from thirteen review rounds are pinned by name in the tests.

No critical blockers at this head. One honest reservation: the unconsumed-rejection mirror machinery in workflow-sandbox.ts (ObservedPromise interception, adoption detection via native-toString sniffing, cross-root suppression, the escape hook) is the most intricate code in the PR. It has had thirteen review rounds plus A/B sandbox verification directed at it, and the documented failure mode of the adoption heuristic errs toward more visibility, not less — but it is the part I would least want to claim I traced every edge of. That reservation feeds the confidence score below, not a change request.

The standing CHANGES_REQUESTED on this PR is from the round-11 review at an earlier head; the review workflow is re-reviewing this head now. I did not find blocking issues here, so I am not re-issuing it.

The pause/resume control flow, since it spans five components:

sequenceDiagram
    participant P1 as User
    participant P2 as Dialog and slash command
    participant P3 as WorkflowRunRegistry
    participant P4 as Run handle
    participant P5 as Dispatch scheduler
    participant P6 as Result gate
    P1->>P2: press p or run p subcommand
    P2->>P3: pause or resume the runId
    P3->>P4: delegate when the state allows
    P4->>P5: pause or resume
    P5->>P5: running to pausing, paused when drained
    P5->>P6: hold results while not running
    P6-->>P5: wake waiters on resume, reject on abort
Loading
Files changed (20 of 42 shown)
File What changed
packages/core/src/agents/runtime/workflow-dispatch-scheduler.ts New pause-aware dispatch scheduler with the result gate, replaces the concurrency limiter
packages/core/src/agents/runtime/workflow-orchestrator.ts Every settlement path routed through the pause gate, nested sandbox logs merged into the parent run
packages/core/src/agents/runtime/workflow-runner.ts Scheduler wiring into the handle, settlement guard generalized to all terminal states, telemetry projection captured pre-await
packages/core/src/agents/runtime/workflow-sandbox.ts Pause-aware wall-clock watchdog on a monotonic clock, unconsumed-rejection mirror hardening round
packages/core/src/agents/workflow-run-registry.ts Pausing and paused lifecycle, pause and resume API, counters decoupled from status gating
packages/core/src/agents/workflow-snapshot.ts Snapshot projection taken before the first fs await
packages/core/src/agents/tasks/types.ts TaskBase made generic over the status union
packages/core/src/tools/workflow/workflow.ts Tool description and return text mention the new controls
packages/core/src/utils/concurrencyLimiter.ts Deleted, superseded by the scheduler
packages/cli/src/ui/commands/workflowsCommand.ts New p subcommand with execution-mode, usage, terminal, and foreground guards
packages/cli/src/ui/components/background-view/BackgroundTasksDialog.tsx p keybinding in list and detail, pause hints, pausing and paused status presentation, armed-confirm clears on every detail exit
packages/cli/src/ui/components/background-view/BackgroundTasksPill.tsx Pill counts pausing and paused workflows as live
packages/cli/src/ui/contexts/BackgroundTaskViewContext.tsx toggleSelectedWorkflowPause action with a tri-state verdict
packages/cli/src/ui/hooks/useBackgroundTaskView.ts Sort buckets treat pausing as active
packages/cli/src/ui/utils/backgroundWorkUtils.ts Session switch aborts paused runs before registry reset
docs/users/reference/keyboard-shortcuts.md Background tasks dialog shortcut table added
docs/users/features/commands.md Command reference row updated
packages/cli/src/i18n/locales/en.js (+8 more locales) New pause and resume strings in nine locales
packages/web-shell/client/i18n.tsx Command description string updated
…and 22 more files Test files pinning the new lifecycle and every R11 finding, plus the eslint allowlist entry removal

Testing evidence

Unattended run — no local tmux drive on the CI path; the evidence below is the PR's own CI, fetched via the API for the reviewed commit.

Check Conclusion
Test (ubuntu-latest, Node 22.x) ✅ success
Desktop Shell (ubuntu-22.04) ✅ success
web-shell E2E Smoke (ubuntu-latest, Node 22.x) ✅ success
Capture web-shell visuals (ubuntu-latest, Node 22.x) ✅ success
precheck-pr / precheck ✅ success
Classify PR ✅ success
Test (macos-latest, Node 22.x) ⏭️ skipped — merge queue only
Test (windows-latest, Node 22.x) ⏭️ skipped — merge queue only
Integration Tests (CLI, No Sandbox) ⏭️ skipped — merge queue only
review-pr (bot orchestration) 🔄 in progress

The three skipped jobs are skipped by repository design, for every PR: macOS/Windows test jobs and the CLI integration suite are gated on merge_group in .github/workflows/ci.yml ("slowest/costliest runners — run only in the merge queue; ubuntu is the fast PR signal"), and the integration job additionally predates merge queue being enabled. The round-11 review's blocking line ("Integration Tests was skipped in CI") asked for a suite the CI design never runs on PR pushes — that block is resolved by design, not by this PR. What a PR gets today is the ubuntu unit suite (the repo's PR signal), and it is green at this head along with the Desktop Shell and web-shell suites. Not verified: macOS/Windows behavior — those run only once the PR enters the merge queue.

The author reports 251 focused core tests and 128 CLI tests passing locally plus real-terminal pause/resume runs — that is the author's claim, not independently re-run here; the CI suite above is the evidence this review carries.

Sandboxed verification would settle the remaining gap: @qwen-code /verify — the A/B load-bearing proof for the pause gate (649/649 assertions, gate-removed control flipping exactly the gate-dependent assertions) was taken at fd8a6ed7, two commits before this head. Rounds 12–13 landed ~450 production lines after it — the adoption-escape mirror, the generalized settlement guard, the monotonic watchdog. The green ubuntu suite shows the new tests pass; it does not show the earlier guarantees survive that delta. A fresh run at this head would close that.

中文说明

代码审查

750133f4 审查了完整 diff,并对上次审查 head(89b78039)以来的第 13 轮增量做了逐 hunk 检查。

我对"协作式暂停"的独立方案是:在派发层(并发控制已有之处)做可感知暂停的状态机、加一道结果门使在飞结果在暂停期间不被脚本消费、注册表生命周期扩展、既有控件统一呈现。本 PR 与该形状几乎完全一致:

  • 新的 WorkflowDispatchScheduler 替换 createConcurrencyLimiter(唯一消费者已更新,旧模块与 lint 白名单条目一并移除——干净)。pause()/resume() 均检查 abort 信号,结算恰好一次,abortPending() 拒绝队列工作与门等待者、允许在飞派发排空——正是 PR 承诺的协作式语义。
  • 暂停门(waitUntilRunning())挂在我能找到的每一条结算路径上:缓存命中、成功派发、失败派发、预算门拒绝、agent 上限拒绝。两个门分支都是"成功仍 resolve、错误重新抛出",而不是制造拆除期 rejection——关于"被扣留的成功结果在正确取消的运行上不得变成 unhandledRejection"的注释理由完全正确。
  • 注册表状态迁移有严格守卫(pausing 仅来自 runningpaused 仅来自 pausingrunning 仅来自 paused,终态先判),且 hasRunningEntries() 有意不计 paused 并配套拆除修复——会话切换 reset 前先 abort 暂停中的运行,避免泄漏被门阻塞的脚本及其 vm 上下文。
  • 第 13 轮增量纪律良好:每处修复都注明所钉住的 finding(R11-*/R12),结算守卫从 cancelled 推广到所有终态,看门狗预算核算改用单调时钟,snapshot 与遥测投影在首个 await 之前捕获,新的 unhandledRejection 逃逸钩子在运行体前后对称安装/移除。测试与生产代码比约 1.8:1,十三轮 review 的 findings 均在测试中点名钉住。

当前 head 无关键阻断项。 一个诚实的保留意见:workflow-sandbox.ts 中未消费 rejection 镜像机制(ObservedPromise 拦截、以 native-toString 嗅探识别 adoption、跨 root 抑制、逃逸钩子)是全 PR 最复杂的部分。它已经被十三轮 review 与 A/B 沙箱验证反复打磨,且 adoption 启发式的文档化失败模式偏向"更多可见性"而非更少——但这是我最不敢声称穷尽每条边界的部分。该保留意见计入下文的置信分,不构成 change request。

本 PR 上现存的 CHANGES_REQUESTED 来自更早 head 的第 11 轮 review;review 工作流正在重新审查当前 head。本轮未发现阻断问题,故不重新提交。

测试证据

无人值守运行——CI 路径不做本地 tmux 实测;以上证据为该 PR 自身 CI,经 API 在受审 commit 上取得。

三个 skipped 作业是仓库设计使然、对所有 PR 一致:macOS/Windows 测试与 CLI 集成套件在 .github/workflows/ci.yml 中仅在 merge_group 触发("最慢/最贵的 runner 只在合并队列跑;ubuntu 是 PR 快速信号")。第 11 轮 review 的阻断行("Integration Tests 在 CI 被跳过")要求的是 CI 设计从不在 PR 推送上运行的套件——该阻断是设计层面解除的,而非本 PR 解除的。PR 当下能拿到的是 ubuntu 单测套件(仓库的 PR 信号),在当前 head 为绿,Desktop Shell 与 web-shell 套件同样为绿。未验证:macOS/Windows 行为——仅当 PR 进入合并队列时运行。

作者自述本地通过 251 个 core focused tests、128 个 CLI tests 以及真实终端 pause/resume 场景——这是作者的陈述,本审查未独立复跑;本审查携带的证据是上述 CI 套件。

沙箱验证可以补齐剩余缺口:@qwen-code /verify —— 暂停门的 A/B 承重证明(649/649 断言,摘门对照组恰好翻转门依赖断言)取自 fd8a6ed7,比当前 head 早两个提交。第 12–13 轮在其后新增约 450 行生产代码——adoption 逃逸镜像、推广的结算守卫、单调时钟看门狗。ubuntu 套件为绿说明新测试通过,不能说明既有保证在该增量后仍然成立。在当前 head 上重跑一次即可闭环。

Qwen Code · qwen3.8-max

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

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Confidence: 3/5 — clean review at this head, including the round-13 fixes, and @wenshao has approved at exactly this commit; the 3 is the Stage-0 policy cap (a feat touching core at ≥500 production-logic lines — this one carries ~2,000), not doubt.

Stepping back: the implementation matches my independent proposal almost line for line — pause-aware scheduler at the dispatch layer, a result gate chained onto every settlement path, lifecycle widening in the registry, and the same lifecycle surfaced consistently across the dialog, the pill, sorting, snapshots, shutdown, and session switch. Nothing in the diff reads like it is trying too hard; the scheduler is 159 lines and does exactly one thing, and the old limiter was removed rather than left parallel. The round-13 delta is the kind of fix round that raises trust rather than patience — every change names the finding it answers, and each lands with a test that would go red if the fix regressed.

Two reservations, both stated plainly above and neither a blocker: the sandbox's unconsumed-rejection mirror machinery is genuinely intricate (thirteen rounds and an A/B verification have been at it, and I still would not claim to have traced every edge), and macOS/Windows plus integration coverage only lands in the merge queue by repo design — true for every PR, but worth naming so nobody reads the green table as full-platform evidence.

On the verdict: all three stages came out clean at this head, and the standing CHANGES_REQUESTED (round 11, earlier head) does not reflect what I see now — the review workflow is re-reviewing this head and will replace it. But this PR sits squarely in the Stage-0 large-core-feat escalation, so the bot's approval is withheld by policy regardless of how clean the passes looked. @wenshao's approval at this exact head and a fresh @qwen-code /verify over the rounds 12–13 delta are the two signals standing between this PR and merge. ⏸️ Deferring to @wenshao for the final call — no blocking issues found at this head; the defer is policy, not doubt.

中文说明

置信:3/5 —— 当前 head(含第 13 轮修复)审查干净,且 @wenshao 已在同一 commit 上批准;3 分是 Stage-0 政策上限(触及核心路径且生产逻辑 ≥500 行的 feat——本 PR 约 2,000 行),并非存疑。

退一步看:实现与我的独立方案几乎逐条吻合——派发层的可感知暂停调度器、挂在每条结算路径上的结果门、注册表生命周期扩展,以及对话框、pill、排序、snapshot、shutdown、session switch 各面统一呈现同一生命周期。diff 中没有过度设计的痕迹:调度器 159 行、只做一件事,旧 limiter 被移除而非并行保留。第 13 轮修复是提升信任而非消耗耐心的那种——每处改动都点名其回应的问题,且各自带着"修复回归即变红"的测试。

两点保留意见,上文已明说,均非阻断:sandbox 的未消费 rejection 镜像机制确实复杂(十三轮 review 加一次 A/B 验证反复打磨,我仍不敢声称穷尽每条边界);macOS/Windows 与集成覆盖按仓库设计只在合并队列落地——对每个 PR 都如此,但值得点名,避免把为绿的表格误读为全平台证据。

关于结论:三个阶段在当前 head 均为干净,现存的 CHANGES_REQUESTED(第 11 轮、更早 head)与我本轮所见不符——review 工作流正在重审当前 head,会将其替换。但本 PR 正处 Stage-0 大型核心 feat 升级区,无论各阶段多干净,bot 的批准都按政策保留。@wenshao 在当前 head 的批准与针对第 12–13 轮增量的一次新的 @qwen-code /verify,是本 PR 走向合并所剩的两项信号。⏸️ 交由 @wenshao 做最终决定——当前 head 未发现阻断问题;此次 defer 是政策使然,并非存疑。

Qwen Code · qwen3.8-max

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

@wenshao

wenshao commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Review: feat(workflows): add cooperative pause and resume

Overview

This PR introduces WorkflowDispatchScheduler, a per-run pause-aware scheduler that replaces the plain concurrency limiter, and threads a running → pausing → paused → running lifecycle through the registry, /workflows, Background Tasks, the pill, sorting, snapshots, and shutdown paths. The design is sound: pause stops dequeuing, in-flight work converges, and settled results (fulfilled and rejected, including journal-cache hits and nested workflow() results) are held at a waitUntilRunning() gate until resume; cancellation rejects queued jobs and wakes gated waiters.

Particularly good:

  • The scheduler is a small, self-contained state machine; pump() only dequeues in running, and the pausing → paused transition on inFlight === 0 is race-free because the result promise settles (in .then(job.resolve)) before the .finally state flip, and 'pausing' also gates waitUntilRunning().
  • onDispatchStateChange validates every transition (pausing only from running, etc.) and ignores terminal entries, so a late scheduler callback can't resurrect a cancelled run.
  • The emitCompletion latch keeps agentsCompleted exactly-once across the three settlement paths (dispatch success, dispatch/slot failure, queued-job abort that never ran its thunk).
  • Test coverage is thorough and targets the real races: journal-id assignment while paused, result-append before the gate opens, cancel-of-paused without deadlock, resume-replay behind the gate, detail-view reorder exits.

Findings

1. Foreground workflows are pausable from the Background Tasks dialog, silently blocking the parent turn (medium)

WorkflowRunner.start registers and attachHandles foreground runs too, and registry.pause() only checks status === 'running' + handle presence — it never consults entry.isBackgrounded. Foreground workflow rows appear in the Background Tasks dialog (useBackgroundTaskView lists the registry unfiltered), which is reachable mid-turn, so pressing p pauses a foreground run: handle.completion is then held at the gate and the active turn hangs with only a spinner, while the dialog shows "Paused". This contradicts the PR body's "foreground workflows retain their existing synchronous result and failure behavior" and the tool description that frames pause as a background-run control. It's recoverable (resume, x stop, or Esc-cancel of the turn aborts the child controller), but it's an easy foot-gun. Suggest gating pause()/the p hint on entry.isBackgrounded (the field already exists), or explicitly documenting that pausing a foreground run suspends the current turn.

2. A pause request cannot be withdrawn while pausing (low)

resume() requires state === 'paused', and both the dialog and /workflows p no-op during pausing. Since an in-flight agent can take minutes to converge, a user who fat-fingers p can only wait or stop the run. Consider allowing pausing → running directly (drain the would-be pause before any gate has actually held anything).

3. scheduler.setState invokes onStateChange unguarded (low)

A throwing listener propagates out of pause()/resume() (called from UI key handlers) and, worse, out of pump()'s .finally, where it becomes an unhandled rejection. The only current consumer (registry.onDispatchStateChange → guarded emitStatusChange) is safe, but a try/catch in setState would keep the scheduler robust against future listeners, matching the defensive pattern used for every emitter callback in the orchestrator.

4. agentsCompleted now means "settled", including never-started dispatches (low / semantic)

On cancel of a paused run, queued jobs that never ran their thunk are counted via the abort-rejection emitCompletion(error) path — the new runner test asserts agentsDispatched: 2, agentsCompleted: 2 for a run where only one agent executed. The agentsCompleted >= agentsDispatched clamp plus the 'cancelled' carve-out in onAgentCompleted make this deliberate, but a 2/2 agents display on a cancelled run where one agent ran is mildly misleading. Worth a comment on the counter (or a UI distinction) so future readers don't "fix" it.

5. Docs not updated (low)

The new p keybinding in Background Tasks and the /workflows p <runId> subcommand aren't reflected in docs/users/features/commands.md / docs/users/reference/keyboard-shortcuts.md. The tool-schema description was updated; the user docs should follow.

Minor notes

  • /workflows p acts by current status (pause when running, resume when paused). The registry re-validates, so it's race-safe, but explicit pause/resume verbs would be less surprising than a state-dependent toggle if this surface grows.
  • New user-facing strings in workflowsCommand's pause branch aren't t()-wrapped; the file is already inconsistent about this (existing errors are unwrapped, tips are wrapped), so fine to leave, just noting for a future i18n sweep.
  • The detail-mode auto-exit rework (exit when the selected id changes under a reorder, not just on active → terminal) fixes a real stale-index bug and is well covered by the two new dialog tests.

Verdict

Core scheduler and lifecycle plumbing look correct; tests are strong and target the actual race windows. Finding 1 (foreground runs pausable from the dialog) is the one thing I'd want resolved or explicitly decided before merge; the rest are polish.

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

中文说明

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

— qwen3.8-max-preview via Qwen Code /review

@qqqys

qqqys commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

已修复:前台 workflow 现在在 registry 与 Background Tasks UI 两层拒绝 pause,仅后台 run 显示并响应 p pause

验证证据:

  • cd packages/core && npx vitest run src/agents/workflow-run-registry.test.ts src/agents/runtime/workflow-runner.test.ts:74/74 通过
  • cd packages/cli && npx vitest run src/ui/components/background-view/BackgroundTasksDialog.test.tsx:66/66 通过
  • npm run typecheck:通过
  • 修改文件 ESLint:通过
  • npm run build:通过
  • commit:b84c14492e

@qqqys

qqqys commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator Author

已修复:前台 workflow 现在在 registry 与 Background Tasks UI 两层拒绝 pause,仅后台 run 显示并响应 。\n\n验证证据:\n-
RUN v3.2.4 /Users/qqqys/Desktop/qys/qwen-code-worktrees/dynamic-workflows-pr3/packages/core
Coverage enabled with v8

✓ src/agents/workflow-run-registry.test.ts (62 tests) 68ms
✓ src/agents/runtime/workflow-runner.test.ts (12 tests) 872ms

Test Files 2 passed (2)
Tests 74 passed (74)
Start at 00:13:14
Duration 4.36s (transform 1.40s, setup 15ms, collect 2.25s, tests 940ms, environment 0ms, prepare 80ms)

JUNIT report written to /Users/qqqys/Desktop/qys/qwen-code-worktrees/dynamic-workflows-pr3/packages/core/junit.xml
% Coverage report from v8:74/74 通过\n-
RUN v3.2.4 /Users/qqqys/Desktop/qys/qwen-code-worktrees/dynamic-workflows-pr3/packages/cli
Coverage enabled with v8

✓ src/ui/components/background-view/BackgroundTasksDialog.test.tsx (66 tests) 8614ms

Test Files 1 passed (1)
Tests 66 passed (66)
Start at 00:13:21
Duration 16.34s (transform 1.66s, setup 92ms, collect 2.70s, tests 8.61s, environment 281ms, prepare 39ms)

JUNIT report written to /Users/qqqys/Desktop/qys/qwen-code-worktrees/dynamic-workflows-pr3/packages/cli/junit.xml
% Coverage report from v8:66/66 通过\n-

@qwen-code/qwen-code@0.21.2 typecheck
npm run typecheck --workspaces --if-present

@qwen-code/acp-bridge@0.21.2 typecheck
tsc --noEmit

@qwen-code/audio-capture@0.21.2 typecheck
tsc --noEmit

@qwen-code/chrome-bridge@0.21.2 typecheck
tsc --noEmit

@qwen-code/qwen-code@0.21.2 typecheck
tsc --noEmit

@qwen-code/qwen-code-core@0.21.2 typecheck
tsc --noEmit

@qwen-code/sdk@0.1.8 typecheck
tsc --noEmit

@qwen-code/web-shell@0.21.2 typecheck
tsc -p tsconfig.json --noEmit

@qwen-code/webui@0.21.2 typecheck
tsc --noEmit

@qwen-code/external-context@0.20.1 typecheck
tsc --noEmit:通过\n- 修改文件 ESLint:通过\n-
@qwen-code/qwen-code@0.21.2 build
cross-env NODE_OPTIONS="--max-old-space-size=3072" node scripts/build.js

@qwen-code/qwen-code@0.21.2 generate
node scripts/generate-git-commit-info.js

@qwen-code/qwen-code-core@0.21.2 build
node ../../scripts/build_package.js

Successfully copied files.

@qwen-code/web-templates@0.21.2 build
node build.mjs && tsc --build --clean && tsc

Building web-templates...
Building insight assets with Vite...
vite v5.4.21 building for production...
transforming...
✓ 8 modules transformed.
rendering chunks...
computing gzip size...
dist/main.css 17.77 kB │ gzip: 4.28 kB
dist/main.js 32.80 kB │ gzip: 9.04 kB
✓ built in 161ms
Reading generated files...
Successfully generated /Users/qqqys/Desktop/qys/qwen-code-worktrees/dynamic-workflows-pr3/packages/web-templates/src/generated/insightTemplate.ts
Successfully built all web-templates.

@qwen-code/channel-base@0.21.2 build
tsc --build

@qwen-code/channel-telegram@0.21.2 build
tsc --build

@qwen-code/channel-weixin@0.21.2 build
tsc --build

@qwen-code/channel-dingtalk@0.21.2 build
tsc --build

@qwen-code/channel-wecom@0.21.2 build
tsc --build

@qwen-code/channel-feishu@0.21.2 build
tsc --build

@qwen-code/channel-qqbot@0.21.2 build
tsc --build

@qwen-code/channel-github@0.21.2 build
tsc --build

@qwen-code/channel-plugin-example@0.21.2 build
tsc --build

@qwen-code/audio-capture@0.21.2 build:ts
tsc --build

@qwen-code/acp-bridge@0.21.2 build
node ../../scripts/build_package.js

Successfully copied files.

@qwen-code/sdk@0.1.8 build
node scripts/build.js

Compiling input files...
Processing src/index.ts
Writing src/index.ts -> dist/index.d.ts
Checking generated files...
Done in 10.82s
Compiling input files...
Processing src/daemon/transcript.ts
Writing src/daemon/transcript.ts -> dist/daemon/transcript.d.ts
Checking generated files...
Done in 1.30s

@qwen-code/qwen-code@0.21.2 build
node ../../scripts/build_package.js

Successfully copied files.
Generated settings JSON Schema at: /Users/qqqys/Desktop/qys/qwen-code-worktrees/dynamic-workflows-pr3/packages/vscode-ide-companion/schemas/settings.schema.json

@qwen-code/webui@0.21.2 build
vite build

vite v5.4.21 building for production...
transforming...
✓ 198 modules transformed.
rendering chunks...

[vite:dts] Start generate declaration files...
computing gzip size...
dist/styles.css 83.30 kB │ gzip: 15.52 kB
dist/toolNames-kewwpc1v.js 0.30 kB │ gzip: 0.23 kB │ map: 1.00 kB
dist/daemon-react-sdk.js 229.17 kB │ gzip: 40.43 kB │ map: 435.51 kB
dist/index.js 393.02 kB │ gzip: 98.55 kB │ map: 842.77 kB
[vite:dts] Start rollup declaration files...
Analysis will use the bundled TypeScript version 5.8.2
Analysis will use the bundled TypeScript version 5.8.2
[vite:dts] Declaration files built in 5159ms.

dist/styles.css 83.30 kB │ gzip: 15.52 kB
dist/toolNames-C9pS8yq9.cjs 0.33 kB │ gzip: 0.24 kB │ map: 1.00 kB
dist/daemon-react-sdk.cjs 230.64 kB │ gzip: 40.48 kB │ map: 437.23 kB
dist/index.cjs 402.45 kB │ gzip: 99.29 kB │ map: 847.85 kB
✓ built in 6.00s

@qwen-code/web-shell@0.21.2 build
vite build && vite build --config vite.lib.config.ts && tsc -p tsconfig.lib.json

vite v5.4.21 building for production...
transforming...
✓ 5473 modules transformed.
rendering chunks...
computing gzip size...
../dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff 4.42 kB
../dist/index.html 4.87 kB │ gzip: 2.32 kB
../dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 4.93 kB
../dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 5.21 kB
../dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 5.47 kB
../dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff 5.98 kB
../dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff 6.19 kB
../dist/assets/KaTeX_Size1-Regular-C195tn64.woff 6.50 kB
../dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 6.91 kB
../dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 6.91 kB
../dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf 7.59 kB
../dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff 7.66 kB
../dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff 7.72 kB
../dist/assets/default-aEtf0G7-.svg 8.90 kB │ gzip: 2.64 kB
../dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 9.64 kB
../dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 10.34 kB
../dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf 10.36 kB
../dist/assets/KaTeX_Script-Regular-D5yQViql.woff 10.59 kB
../dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 11.32 kB
../dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 11.35 kB
../dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf 11.51 kB
../dist/assets/queue-DWpnpFgW.svg 11.56 kB │ gzip: 2.42 kB
../dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 12.03 kB
../dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 12.22 kB
../dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf 12.23 kB
../dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff 12.32 kB
../dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf 12.34 kB
../dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf 12.37 kB
../dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff 13.21 kB
../dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff 13.30 kB
../dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 13.57 kB
../dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff 14.11 kB
../dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff 14.41 kB
../dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff 16.03 kB
../dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 16.40 kB
../dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 16.44 kB
../dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf 16.65 kB
../dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 16.78 kB
../dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 16.99 kB
../dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff 18.67 kB
../dist/assets/KaTeX_Math-Italic-DA0__PXp.woff 18.75 kB
../dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff 19.41 kB
../dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf 19.44 kB
../dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf 19.57 kB
../dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf 19.58 kB
../dist/assets/KaTeX_Main-Italic-BMLOBm91.woff 19.68 kB
../dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf 22.36 kB
../dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf 24.50 kB
../dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 25.32 kB
../dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 26.27 kB
../dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf 27.56 kB
../dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 28.08 kB
../dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff 29.91 kB
../dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff 30.77 kB
../dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf 31.20 kB
../dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf 31.31 kB
../dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf 32.97 kB
../dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff 33.52 kB
../dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf 33.58 kB
../dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf 51.34 kB
../dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf 53.58 kB
../dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf 63.63 kB
../dist/assets/index-Bi1dP2mU.css 446.40 kB │ gzip: 77.32 kB
../dist/assets/channel-A434sWK5.js 0.11 kB │ gzip: 0.13 kB
../dist/assets/init-Gi6I4Gst.js 0.15 kB │ gzip: 0.13 kB
../dist/assets/chunk-QZHKN3VN-CqgBf-95.js 0.19 kB │ gzip: 0.16 kB
../dist/assets/chunk-55IACEB6-Co15vq5x.js 0.24 kB │ gzip: 0.21 kB
../dist/assets/chunk-4BX2VUAB-DRF-egfm.js 0.30 kB │ gzip: 0.20 kB
../dist/assets/chunk-FMBD7UC4-DXbPTnqF.js 0.37 kB │ gzip: 0.27 kB
../dist/assets/stateDiagram-v2-BHNVJYJU-CCxQt56w.js 0.39 kB │ gzip: 0.29 kB
../dist/assets/classDiagram-4FO5ZUOK-CwPuhldB.js 0.47 kB │ gzip: 0.32 kB
../dist/assets/classDiagram-v2-Q7XG4LA2-CwPuhldB.js 0.47 kB │ gzip: 0.32 kB
../dist/assets/chunk-2J33WTMH-Dzz03hk6.js 0.53 kB │ gzip: 0.37 kB
../dist/assets/codeowners-Bp6g37R7.js 0.55 kB │ gzip: 0.32 kB
../dist/assets/infoDiagram-5YYISTIA-DXZGO8qB.js 0.59 kB │ gzip: 0.40 kB
../dist/assets/shellsession-C_rIy8kc.js 0.72 kB │ gzip: 0.43 kB
../dist/assets/tsv-B_m7g4N7.js 0.74 kB │ gzip: 0.34 kB
../dist/assets/html-derivative-CSfWNPLT.js 0.97 kB │ gzip: 0.53 kB
../dist/assets/git-rebase-B-v9cOL2.js 0.98 kB │ gzip: 0.44 kB
../dist/assets/qmldir-C8lEn-DE.js 1.00 kB │ gzip: 0.45 kB
../dist/assets/fortran-fixed-form-TqA4NnZg.js 1.11 kB │ gzip: 0.54 kB
../dist/assets/ordinal-Cboi1Yqb.js 1.19 kB │ gzip: 0.57 kB
../dist/assets/csv-B0qRVHPH.js 1.22 kB │ gzip: 0.37 kB
../dist/assets/xsl-Dd0NUgwM.js 1.39 kB │ gzip: 0.52 kB
../dist/assets/sparql-bYkjHRlG.js 1.51 kB │ gzip: 0.83 kB
../dist/assets/ini-BjABl1g7.js 1.53 kB │ gzip: 0.50 kB
../dist/assets/git-commit-i4q6IMui.js 1.55 kB │ gzip: 0.67 kB
../dist/assets/dotenv-BjQB5zDj.js 1.70 kB │ gzip: 0.63 kB
../dist/assets/wenyan-7A4Fjokl.js 1.71 kB │ gzip: 1.08 kB
../dist/assets/docker-COcR7UxN.js 1.77 kB │ gzip: 0.60 kB
../dist/assets/hxml-TIA70rKU.js 1.83 kB │ gzip: 0.89 kB
../dist/assets/chunk-ND2GUHAM-BleQdi4d.js 1.86 kB │ gzip: 0.82 kB
../dist/assets/desktop-DEIpsLCJ.js 2.03 kB │ gzip: 0.79 kB
../dist/assets/edge-D5gP-w-T.js 2.36 kB │ gzip: 0.70 kB
../dist/assets/reg-5LuOXUq_.js 2.37 kB │ gzip: 0.71 kB
../dist/assets/berry-3xVqZejG.js 2.45 kB │ gzip: 0.79 kB
../dist/assets/erb-BYTLMnw6.js 2.61 kB │ gzip: 0.84 kB
../dist/assets/diff-BgYniUM_.js 2.64 kB │ gzip: 0.74 kB
../dist/assets/gleam-B430Bg39.js 2.69 kB │ gzip: 0.85 kB
../dist/assets/diagram-5GNKFQAL-D_Hy-L64.js 2.75 kB │ gzip: 1.37 kB
../dist/assets/hy-BMj5Y0dO.js 2.82 kB │ gzip: 1.19 kB
../dist/assets/json-BQoSv7ci.js 2.92 kB │ gzip: 0.81 kB
../dist/assets/cairo--RitsXJZ.js 2.95 kB │ gzip: 0.81 kB
../dist/assets/log-Cc5clBb7.js 2.97 kB │ gzip: 0.88 kB
../dist/assets/jssm-P4WzXJd0.js 2.97 kB │ gzip: 0.70 kB
../dist/assets/jsonl-DREVFZK8.js 3.10 kB │ gzip: 0.82 kB
../dist/assets/jsonc-TU54ms6u.js 3.20 kB │ gzip: 0.83 kB
../dist/assets/logo-IuBKFhSY.js 3.21 kB │ gzip: 1.49 kB
../dist/assets/genie-ajMbGru0.js 3.38 kB │ gzip: 1.21 kB
../dist/assets/po-BFLt1xDp.js 3.38 kB │ gzip: 0.99 kB
../dist/assets/tasl-CQjiPCtT.js 3.39 kB │ gzip: 0.88 kB
../dist/assets/vala-BFOHcciG.js 3.40 kB │ gzip: 1.19 kB
../dist/assets/mipsasm-BC5c_5Pe.js 3.41 kB │ gzip: 1.28 kB
../dist/assets/arc-Bd7d781i.js 3.42 kB │ gzip: 1.45 kB
../dist/assets/rel-DJlmqQ1C.js 3.43 kB │ gzip: 1.10 kB
../dist/assets/json5-w8dY5SsB.js 3.54 kB │ gzip: 0.94 kB
../dist/assets/ssh-config-BknIz3MU.js 3.61 kB │ gzip: 1.59 kB
../dist/assets/fluent-Dayu4EKP.js 3.62 kB │ gzip: 0.89 kB
../dist/assets/jsonnet-BfivnA6A.js 3.63 kB │ gzip: 1.07 kB
../dist/assets/narrat-DLbgOhZU.js 3.69 kB │ gzip: 1.12 kB
../dist/assets/turtle-BMR_PYu6.js 3.71 kB │ gzip: 0.98 kB
../dist/assets/splunk-Cf8iN4DR.js 3.92 kB │ gzip: 1.67 kB
../dist/assets/glsl-DBO2IWDn.js 3.94 kB │ gzip: 1.45 kB
../dist/assets/nextflow-B0XVJmRM.js 3.96 kB │ gzip: 1.09 kB
../dist/assets/sdbl-BLhTXw86.js 4.14 kB │ gzip: 2.08 kB
../dist/assets/pascal-JqZropPD.js 4.18 kB │ gzip: 1.69 kB
../dist/assets/smalltalk-DkLiglaE.js 4.20 kB │ gzip: 1.25 kB
../dist/assets/diagram-LMA3HP47-DzSluD4F.js 4.24 kB │ gzip: 1.84 kB
../dist/assets/lean-XBlWyCtg.js 4.29 kB │ gzip: 1.33 kB
../dist/assets/zenscript-HnGAYVZD.js 4.40 kB │ gzip: 1.42 kB
../dist/assets/bicep-DHo0CJ0O.js 4.45 kB │ gzip: 1.08 kB
../dist/assets/http-FRrOvY1W.js 4.65 kB │ gzip: 1.14 kB
../dist/assets/polar-DKykz6zU.js 4.68 kB │ gzip: 1.17 kB
../dist/assets/defaultLocale-DX6XiGOO.js 4.69 kB │ gzip: 2.17 kB
../dist/assets/fennel-bCA53EVm.js 4.82 kB │ gzip: 1.57 kB
../dist/assets/tcl-DQ1-QYvQ.js 5.02 kB │ gzip: 1.68 kB
../dist/assets/bibtex-xW4inM5L.js 5.08 kB │ gzip: 0.81 kB
../dist/assets/pieDiagram-4H26LBE5-BZ0wQaKN.js 5.32 kB │ gzip: 2.36 kB
../dist/assets/fish-w-ucz2PV.js 5.39 kB │ gzip: 1.69 kB
../dist/assets/xml-e3z08dGr.js 5.41 kB │ gzip: 1.22 kB
../dist/assets/qml-D8XfuvdV.js 5.42 kB │ gzip: 1.39 kB
../dist/assets/zig-BVz_zdnA.js 5.46 kB │ gzip: 1.59 kB
../dist/assets/gdresource-BHYsBjWJ.js 5.48 kB │ gzip: 1.37 kB
../dist/assets/awk-eg146-Ew.js 5.49 kB │ gzip: 1.38 kB
../dist/assets/dax-ClGRhx96.js 5.53 kB │ gzip: 2.25 kB
../dist/assets/linear-C63F0tyh.js 5.66 kB │ gzip: 2.32 kB
../dist/assets/jinja-DGy0s7-h.js 5.72 kB │ gzip: 1.42 kB
../dist/assets/powerquery-CSHBycmS.js 5.93 kB │ gzip: 1.52 kB
../dist/assets/verilog-CJaU5se_.js 5.94 kB │ gzip: 1.90 kB
../dist/assets/diagram-2AECGRRQ-DaYAjbdK.js 5.97 kB │ gzip: 2.50 kB
../dist/assets/coq-Dsg_Bt_b.js 5.99 kB │ gzip: 2.03 kB
../dist/assets/vb-CdO5JTpU.js 6.26 kB │ gzip: 2.35 kB
../dist/assets/red-bN70gL4F.js 6.26 kB │ gzip: 1.60 kB
../dist/assets/shaderlab-B7qAK45m.js 6.27 kB │ gzip: 2.10 kB
../dist/assets/min-dark-CafNBF8u.js 6.29 kB │ gzip: 1.72 kB
../dist/assets/gdshader-SKMF96pI.js 6.37 kB │ gzip: 1.75 kB
../dist/assets/prisma-B48N-Iqd.js 6.39 kB │ gzip: 1.40 kB
../dist/assets/solarized-light-L9t79GZl.js 6.48 kB │ gzip: 1.74 kB
../dist/assets/wgsl-CB0Krxn9.js 6.49 kB │ gzip: 1.69 kB
../dist/assets/postcss-B3ZDOciz.js 6.52 kB │ gzip: 1.93 kB
../dist/assets/toml-CB2ApiWb.js 6.53 kB │ gzip: 1.30 kB
../dist/assets/proto-zocC4JxJ.js 6.57 kB │ gzip: 1.42 kB
../dist/assets/talonscript-C1XDQQGZ.js 6.71 kB │ gzip: 1.47 kB
../dist/assets/cypher-m2LEI-9-.js 6.81 kB │ gzip: 1.83 kB
../dist/assets/solarized-dark-DXbdFlpD.js 6.85 kB │ gzip: 1.81 kB
../dist/assets/soy-C-lX7w71.js 6.95 kB │ gzip: 1.67 kB
../dist/assets/min-light-CTRr51gU.js 6.97 kB │ gzip: 1.90 kB
../dist/assets/clojure-DxSadP1t.js 7.08 kB │ gzip: 1.48 kB
../dist/assets/ara-7O62HKoU.js 7.25 kB │ gzip: 2.11 kB
../dist/assets/hlsl-ifBTmRxC.js 7.61 kB │ gzip: 2.24 kB
../dist/assets/riscv-QhoSD0DR.js 7.70 kB │ gzip: 2.21 kB
../dist/assets/qss-DhMKtDLN.js 7.82 kB │ gzip: 2.62 kB
../dist/assets/monokai-D4h5O-jR.js 7.88 kB │ gzip: 1.92 kB
../dist/assets/dart-B9wLZaAG.js 7.89 kB │ gzip: 1.93 kB
../dist/assets/systemd-CUnW07Te.js 8.00 kB │ gzip: 2.56 kB
../dist/assets/regexp-DWJ3fJO_.js 8.07 kB │ gzip: 1.46 kB
../dist/assets/haml-B2EZWmdv.js 8.49 kB │ gzip: 1.87 kB
../dist/assets/typst-BVUVsWT6.js 8.55 kB │ gzip: 1.70 kB
../dist/assets/plsql-LKU2TuZ1.js 8.57 kB │ gzip: 3.03 kB
../dist/assets/vue-html-xdeiXROB.js 8.70 kB │ gzip: 1.77 kB
../dist/assets/scheme-BJGe-b2p.js 8.72 kB │ gzip: 2.61 kB
../dist/assets/kotlin-B5lbUyaz.js 8.82 kB │ gzip: 2.14 kB
../dist/assets/andromeeda-C3khCPGq.js 8.86 kB │ gzip: 2.31 kB
../dist/assets/make-Bvotw-X0.js 9.01 kB │ gzip: 1.77 kB
../dist/assets/dark-plus-C3mMm8J8.js 9.10 kB │ gzip: 2.10 kB
../dist/assets/slack-dark-BthQWCQV.js 9.12 kB │ gzip: 1.98 kB
../dist/assets/ts-tags-CipyTH0X.js 9.18 kB │ gzip: 1.24 kB
../dist/assets/plastic-3e1v2bzS.js 9.30 kB │ gzip: 1.99 kB
../dist/assets/tex-rYs2v40G.js 9.37 kB │ gzip: 2.97 kB
../dist/assets/sass-BJ4Li9vH.js 9.41 kB │ gzip: 2.50 kB
../dist/assets/slack-ochin-DqwNpetd.js 9.43 kB │ gzip: 2.11 kB
../dist/assets/jison-BqZprYcd.js 9.72 kB │ gzip: 1.86 kB
../dist/assets/sas-BmTFh92c.js 9.79 kB │ gzip: 4.03 kB
../dist/assets/light-plus-B7mTdjB0.js 9.94 kB │ gzip: 2.29 kB
../dist/assets/gherkin--30QC5Em.js 10.12 kB │ gzip: 5.02 kB
../dist/assets/stateDiagram-AJRCARHV-Dt9zUwmZ.js 10.36 kB │ gzip: 3.62 kB
../dist/assets/cmake-DbXoA79R.js 10.50 kB │ gzip: 3.57 kB
../dist/assets/dream-maker-C-nORZOA.js 10.61 kB │ gzip: 2.30 kB
../dist/assets/raku-B1bQXN8T.js 10.61 kB │ gzip: 2.99 kB
../dist/assets/rst-4NLicBqY.js 10.65 kB │ gzip: 2.42 kB
../dist/assets/beancount-BwXTMy5W.js 10.76 kB │ gzip: 1.52 kB
../dist/assets/yaml-CVw76BM1.js 10.79 kB │ gzip: 2.29 kB
../dist/assets/diagram-KO2AKTUF-dB06cZyD.js 10.82 kB │ gzip: 4.18 kB
../dist/assets/cadence-DNquZEk8.js 11.03 kB │ gzip: 2.32 kB
../dist/assets/github-light-DAi9KRSo.js 11.18 kB │ gzip: 2.52 kB
../dist/assets/elm-CmHSxxaM.js 11.29 kB │ gzip: 2.21 kB
../dist/assets/dagre-BM42HDAG-v_55ki8o.js 11.40 kB │ gzip: 4.21 kB
../dist/assets/github-dark-DHJKELXO.js 11.41 kB │ gzip: 2.56 kB
../dist/assets/prolog-BY-TUvya.js 11.44 kB │ gzip: 3.87 kB
../dist/assets/laserwave-DUszq2jm.js 11.50 kB │ gzip: 2.60 kB
../dist/assets/puppet-Cza_XSSt.js 11.75 kB │ gzip: 2.23 kB
../dist/assets/hcl-HzYwdGDm.js 11.97 kB │ gzip: 2.51 kB
../dist/assets/handlebars-BQGss363.js 12.27 kB │ gzip: 2.39 kB
../dist/assets/hjson-T-Tgc4AT.js 12.33 kB │ gzip: 1.68 kB
../dist/assets/vesper-BEBZ7ncR.js 12.66 kB │ gzip: 1.98 kB
../dist/assets/luau-Du5NY7AG.js 12.92 kB │ gzip: 3.12 kB
../dist/assets/bat-fje9CFhw.js 12.99 kB │ gzip: 3.25 kB
../dist/assets/apache-Dn00JSTd.js 13.21 kB │ gzip: 3.79 kB
../dist/assets/terraform-BbSNqyBO.js 13.38 kB │ gzip: 3.11 kB
../dist/assets/vitesse-light-CVO1_9PV.js 13.62 kB │ gzip: 3.06 kB
../dist/assets/aurora-x-D-2ljcwZ.js 13.66 kB │ gzip: 2.30 kB
../dist/assets/vitesse-black-Bkuqu6BP.js 13.68 kB │ gzip: 3.08 kB
../dist/assets/v-CAQ2eGtk.js 13.74 kB │ gzip: 2.85 kB
../dist/assets/vitesse-dark-D0r3Knsf.js 13.76 kB │ gzip: 3.08 kB
../dist/assets/synthwave-84-CbfX1IO0.js 14.04 kB │ gzip: 2.88 kB
../dist/assets/github-light-default-D7oLnXFd.js 14.16 kB │ gzip: 3.06 kB
../dist/assets/github-light-high-contrast-BfjtVDDH.js 14.28 kB │ gzip: 3.04 kB
../dist/assets/github-dark-dimmed-DH5Ifo-i.js 14.43 kB │ gzip: 3.14 kB
../dist/assets/github-dark-default-Cuk6v7N8.js 14.44 kB │ gzip: 3.14 kB
../dist/assets/clarity-BHOwM8T6.js 14.55 kB │ gzip: 2.48 kB
../dist/assets/actionscript-3-D_z4Izcz.js 14.56 kB │ gzip: 2.79 kB
../dist/assets/github-dark-high-contrast-E3gJ1_iC.js 14.60 kB │ gzip: 3.10 kB
../dist/assets/pug-CM9l7STV.js 14.89 kB │ gzip: 2.90 kB
../dist/assets/gnuplot-CM8KxXT1.js 14.91 kB │ gzip: 3.31 kB
../dist/assets/ayu-dark-Cv9koXgw.js 14.95 kB │ gzip: 3.09 kB
../dist/assets/nix-shcSOmrb.js 15.55 kB │ gzip: 2.36 kB
../dist/assets/lua-CvWAzNxB.js 15.62 kB │ gzip: 3.16 kB
../dist/assets/diagram-OG6HWLK6-Cvb6CeXQ.js 15.92 kB │ gzip: 5.69 kB
../dist/assets/wasm-C6j12Q_x.js 15.93 kB │ gzip: 2.89 kB
../dist/assets/solidity-C1w2a3ep.js 16.23 kB │ gzip: 3.13 kB
../dist/assets/svelte-MSaWC3Je.js 16.89 kB │ gzip: 2.96 kB
../dist/assets/purescript-Bg-kzb6g.js 17.07 kB │ gzip: 2.64 kB
../dist/assets/kanagawa-wave-DWedfzmr.js 17.12 kB │ gzip: 2.93 kB
../dist/assets/kanagawa-lotus-CfQXZHmo.js 17.13 kB │ gzip: 2.94 kB
../dist/assets/kanagawa-dragon-CkXjmgJE.js 17.13 kB │ gzip: 2.95 kB
../dist/assets/liquid-D3W5UaiH.js 17.17 kB │ gzip: 3.08 kB
../dist/assets/cue-DtFQj3wx.js 17.35 kB │ gzip: 2.08 kB
../dist/assets/ishikawaDiagram-YF4QCWOH-g7gbuH2w.js 17.56 kB │ gzip: 6.63 kB
../dist/assets/rust-Be6lgOlo.js 17.60 kB │ gzip: 3.37 kB
../dist/assets/angular-html-LfdN0zeE.js 17.66 kB │ gzip: 3.51 kB
../dist/assets/graphql-cDcHW_If.js 18.20 kB │ gzip: 2.65 kB
../dist/assets/elixir-CLiX3zqd.js 18.30 kB │ gzip: 3.22 kB
../dist/assets/material-theme-D5KoaKCx.js 18.62 kB │ gzip: 3.13 kB
../dist/assets/material-theme-darker-BfHTSMKl.js 18.63 kB │ gzip: 3.12 kB
../dist/assets/material-theme-ocean-CyktbL80.js 18.63 kB │ gzip: 3.15 kB
../dist/assets/material-theme-lighter-B0m2ddpp.js 18.63 kB │ gzip: 3.13 kB
../dist/assets/material-theme-palenight-Csfq5Kiy.js 18.64 kB │ gzip: 3.14 kB
../dist/assets/gdscript-DfxzS6Rs.js 18.65 kB │ gzip: 3.72 kB
../dist/assets/abap-DsBKuouk.js 18.82 kB │ gzip: 6.20 kB
../dist/assets/marko-z0MBrx5-.js 19.33 kB │ gzip: 3.21 kB
../dist/assets/groovy-DkBy-JyN.js 19.65 kB │ gzip: 3.77 kB
../dist/assets/mdc-DB_EDNY_.js 19.70 kB │ gzip: 6.62 kB
../dist/assets/nushell-D4Tzg5kh.js 19.75 kB │ gzip: 4.95 kB
../dist/assets/matlab-D9-PGadD.js 20.14 kB │ gzip: 3.85 kB
../dist/assets/move-DB_GagMm.js 20.24 kB │ gzip: 4.03 kB
../dist/assets/glimmer-js-D-cwc0-E.js 20.56 kB │ gzip: 2.99 kB
../dist/assets/glimmer-ts-pgjy16dm.js 20.56 kB │ gzip: 2.99 kB
../dist/assets/kusto-mebxcVVE.js 20.63 kB │ gzip: 4.58 kB
../dist/assets/kanban-definition-UN3LZRKU-Dws8-mjG.js 20.70 kB │ gzip: 7.22 kB
../dist/assets/snazzy-light-Bw305WKR.js 20.77 kB │ gzip: 3.85 kB
../dist/assets/viml-m4uW47V2.js 21.02 kB │ gzip: 7.24 kB
../dist/assets/dracula-BzJJZx-M.js 21.07 kB │ gzip: 4.03 kB
../dist/assets/dracula-soft-BXkSAIEj.js 21.08 kB │ gzip: 4.07 kB
../dist/assets/vue-BuYVFjOK.js 21.48 kB │ gzip: 2.88 kB
../dist/assets/rose-pine-CmCqftbK.js 21.76 kB │ gzip: 3.89 kB
../dist/assets/rose-pine-moon-CjDtw9vr.js 21.77 kB │ gzip: 3.91 kB
../dist/assets/rose-pine-dawn-Ds-gbosJ.js 21.77 kB │ gzip: 3.91 kB
../dist/assets/powershell-BIEUsx6d.js 22.26 kB │ gzip: 4.70 kB
../dist/assets/apl-BBq3IX1j.js 22.84 kB │ gzip: 4.18 kB
../dist/assets/twig-NC5TFiHP.js 23.01 kB │ gzip: 4.15 kB
../dist/assets/mindmap-definition-RKZ34NQL-qRkydfT-.js 23.36 kB │ gzip: 7.81 kB
../dist/assets/sankeyDiagram-5OEKKPKP-VcXX6PVZ.js 23.39 kB │ gzip: 8.54 kB
../dist/assets/journeyDiagram-JHISSGLW-euefICGn.js 23.58 kB │ gzip: 8.31 kB
../dist/assets/nim-ZlGxZxc3.js 23.63 kB │ gzip: 3.51 kB
../dist/assets/vhdl-DYoNaHQp.js 23.78 kB │ gzip: 3.88 kB
../dist/assets/graph-qsFoMdT2.js 23.94 kB │ gzip: 8.30 kB
../dist/assets/templ-dwX3ZSMB.js 23.97 kB │ gzip: 5.40 kB
../dist/assets/astro-CqkE3fuf.js 24.07 kB │ gzip: 7.48 kB
../dist/assets/sql-COK4E0Yg.js 24.32 kB │ gzip: 7.70 kB
../dist/assets/one-light-PoHY5YXO.js 25.30 kB │ gzip: 3.68 kB
../dist/assets/razor-CNLDkMZG.js 25.81 kB │ gzip: 3.47 kB
../dist/assets/fsharp-XplgxFYe.js 25.83 kB │ gzip: 4.23 kB
../dist/assets/wardleyDiagram-YWT4CUSO-BddN8oK8.js 26.10 kB │ gzip: 6.89 kB
../dist/assets/nord-Ddv68eIx.js 26.72 kB │ gzip: 4.41 kB
../dist/assets/system-verilog-C7L56vO4.js 26.77 kB │ gzip: 4.88 kB
../dist/assets/erDiagram-TEJ5UH35-7eB2Hmov.js 26.97 kB │ gzip: 9.29 kB
../dist/assets/bsl-Dgyn0ogV.js 27.44 kB │ gzip: 8.88 kB
../dist/assets/java-xI-RfyKK.js 27.49 kB │ gzip: 4.33 kB
../dist/assets/coffee-dyiR41kL.js 27.75 kB │ gzip: 6.41 kB
../dist/assets/scss-C31hgJw-.js 28.13 kB │ gzip: 4.43 kB
../dist/assets/typespec-BpWG_bgh.js 28.19 kB │ gzip: 2.83 kB
../dist/assets/index-B_1Z0Mgr.js 28.32 kB │ gzip: 9.86 kB
../dist/assets/common-lisp-C7gG9l05.js 28.50 kB │ gzip: 6.74 kB
../dist/assets/night-owl-C39BiMTA.js 28.91 kB │ gzip: 5.18 kB
../dist/assets/julia-BBuGR-5E.js 29.45 kB │ gzip: 5.88 kB
../dist/assets/gitGraphDiagram-PVQCEYII-DQLurQ2i.js 29.94 kB │ gzip: 8.85 kB
../dist/assets/scala-DQVVAn-B.js 30.49 kB │ gzip: 4.30 kB
../dist/assets/applescript-Bu5BbsvL.js 30.78 kB │ gzip: 6.38 kB
../dist/assets/requirementDiagram-4Y6WPE33-CqWvjLHU.js 31.20 kB │ gzip: 9.69 kB
../dist/assets/timeline-definition-PNZ67QCA-DQwlNRws.js 31.36 kB │ gzip: 10.37 kB
../dist/assets/stylus-BeQkCIfX.js 31.56 kB │ gzip: 8.14 kB
../dist/assets/mermaid-Ci6OQyBP.js 33.06 kB │ gzip: 4.65 kB
../dist/assets/poimandres-CS3Unz2-.js 33.49 kB │ gzip: 5.53 kB
../dist/assets/codeql-sacFqUAJ.js 33.59 kB │ gzip: 4.00 kB
../dist/assets/one-dark-pro-GBQ2dnAY.js 33.74 kB │ gzip: 5.53 kB
../dist/assets/crystal-DtDmRg-F.js 33.75 kB │ gzip: 5.58 kB
../dist/assets/tokyo-night-DBQeEorK.js 34.36 kB │ gzip: 5.99 kB
../dist/assets/quadrantDiagram-W4KKPZXB-Fjq6GbSZ.js 34.50 kB │ gzip: 10.02 kB
../dist/assets/haxe-C5wWYbrZ.js 35.30 kB │ gzip: 5.94 kB
../dist/assets/houston-DnULxvSX.js 35.42 kB │ gzip: 5.80 kB
../dist/assets/nginx-D_VnBJ67.js 35.73 kB │ gzip: 4.53 kB
../dist/assets/layout-CNYaLpI5.js 35.84 kB │ gzip: 12.85 kB
../dist/assets/erlang-B-DoSBHF.js 36.31 kB │ gzip: 4.43 kB
../dist/assets/r-CwjWoCRV.js 36.87 kB │ gzip: 11.26 kB
../dist/assets/chunk-AQP2D5EJ-Br8u_E2R.js 37.55 kB │ gzip: 12.11 kB
../dist/assets/xychartDiagram-2RQKCTM6-BUNuBrRJ.js 40.39 kB │ gzip: 11.47 kB
../dist/assets/cobol-PTqiYgYu.js 40.58 kB │ gzip: 11.07 kB
../dist/assets/asm-Dhn9LcZ4.js 40.85 kB │ gzip: 8.20 kB
../dist/assets/vennDiagram-CIIHVFJN-DwxjUEtQ.js 41.83 kB │ gzip: 15.44 kB
../dist/assets/shellscript-atvbtKCR.js 42.71 kB │ gzip: 6.35 kB
../dist/assets/d-BoXegm-a.js 43.01 kB │ gzip: 8.35 kB
../dist/assets/haskell-BILxekzW.js 43.21 kB │ gzip: 7.00 kB
../dist/assets/perl-CHQXSrWU.js 44.54 kB │ gzip: 4.86 kB
../dist/assets/catppuccin-mocha-LGGdnPYs.js 45.61 kB │ gzip: 7.76 kB
../dist/assets/catppuccin-latte-DRW-0cLl.js 45.61 kB │ gzip: 7.77 kB
../dist/assets/catppuccin-frappe-CD_QflpE.js 45.62 kB │ gzip: 7.78 kB
../dist/assets/catppuccin-macchiato-C-shW-Y.js 45.62 kB │ gzip: 7.77 kB
../dist/assets/apex-COJ4H7py.js 46.81 kB │ gzip: 6.79 kB
../dist/assets/ada-727ZlQH0.js 48.53 kB │ gzip: 6.11 kB
../dist/assets/chunk-727SXJPM-DHSUk1J7.js 49.06 kB │ gzip: 15.55 kB
../dist/assets/ruby-DeZ3UC14.js 51.47 kB │ gzip: 7.12 kB
../dist/assets/go-B1SYOhNW.js 52.41 kB │ gzip: 6.43 kB
../dist/assets/imba-bv_oIlVt.js 53.46 kB │ gzip: 10.23 kB
../dist/assets/everforest-dark-BgDCqdQA.js 53.75 kB │ gzip: 8.46 kB
../dist/assets/everforest-light-C8M2exoo.js 53.75 kB │ gzip: 8.46 kB
../dist/assets/css-BPhBrDlE.js 53.80 kB │ gzip: 13.46 kB
../dist/assets/wikitext-DCE3LsBG.js 56.62 kB │ gzip: 4.77 kB
../dist/assets/markdown-UIAJJxZW.js 56.99 kB │ gzip: 5.61 kB
../dist/assets/latex-C-cWTeAZ.js 59.42 kB │ gzip: 6.01 kB
../dist/assets/stata-DorPZHa4.js 61.01 kB │ gzip: 13.69 kB
../dist/assets/flowDiagram-I6XJVG4X-B08iGCw
.js 61.17 kB │ gzip: 19.24 kB
../dist/assets/html-C2L_23MC.js 61.33 kB │ gzip: 12.05 kB
../dist/assets/ballerina-Du268qiB.js 61.63 kB │ gzip: 8.36 kB
../dist/assets/ocaml-BNioltXt.js 65.87 kB │ gzip: 5.19 kB
../dist/assets/ganttDiagram-6RSMTGT7-BgsrCHq-.js 69.29 kB │ gzip: 23.09 kB
../dist/assets/c4Diagram-AAUBKEIU-BWT75l82.js 69.95 kB │ gzip: 19.48 kB
../dist/assets/mojo-Tz6hzZYG.js 72.57 kB │ gzip: 10.36 kB
../dist/assets/python-DhUJRlN_.js 73.53 kB │ gzip: 10.44 kB
../dist/assets/c-C3t2pwGQ.js 73.97 kB │ gzip: 10.62 kB
../dist/assets/blockDiagram-GPEHLZMM-NxVTiWnk.js 74.92 kB │ gzip: 21.39 kB
../dist/assets/vyper-nyqBNV6O.js 78.23 kB │ gzip: 12.05 kB
../dist/assets/cose-bilkent-S5V4N54A-WS7b13Yg.js 81.55 kB │ gzip: 22.35 kB
../dist/assets/hack-D1yCygmZ.js 84.64 kB │ gzip: 27.44 kB
../dist/assets/csharp-D9R-vmeu.js 87.17 kB │ gzip: 10.44 kB
../dist/assets/swift-BSxZ-RaX.js 90.99 kB │ gzip: 14.51 kB
../dist/assets/asciidoc-BPT9niGB.js 93.51 kB │ gzip: 7.40 kB
../dist/assets/racket-CzouJOBO.js 97.61 kB │ gzip: 15.68 kB
../dist/assets/fortran-free-form-DKXYxT9g.js 98.19 kB │ gzip: 12.40 kB
../dist/assets/less-BfCpw3nA.js 102.46 kB │ gzip: 15.22 kB
../dist/assets/objective-c-Deuh7S70.js 107.79 kB │ gzip: 23.27 kB
../dist/assets/blade-a8OxSdnT.js 108.00 kB │ gzip: 28.54 kB
../dist/assets/php-B5ebYQev.js 113.76 kB │ gzip: 28.74 kB
../dist/assets/sequenceDiagram-3UESZ5HK-BJSRtxFr.js 117.29 kB │ gzip: 30.92 kB
../dist/assets/mdx-sdHcTMYB.js 140.36 kB │ gzip: 23.83 kB
../dist/assets/architectureDiagram-3BPJPVTR-CpnOJqNE.js 149.54 kB │ gzip: 41.97 kB
../dist/assets/objective-cpp-BUEGK8hf.js 175.60 kB │ gzip: 30.79 kB
../dist/assets/javascript-ySlJ1b_l.js 198.03 kB │ gzip: 17.58 kB
../dist/assets/tsx-B6W0miNI.js 198.75 kB │ gzip: 17.59 kB
../dist/assets/jsx-BAng5TT0.js 201.01 kB │ gzip: 17.70 kB
../dist/assets/typescript-Dj6nwHGl.js 209.02 kB │ gzip: 17.23 kB
../dist/assets/angular-ts-CKsD7JZE.js 211.78 kB │ gzip: 17.82 kB
../dist/assets/wolfram-C3FkfJm5.js 268.60 kB │ gzip: 77.00 kB
../dist/assets/cytoscape.esm-CUqq0XTU.js 443.69 kB │ gzip: 141.74 kB
../dist/assets/wardley-L42UT6IY-D3QdMSpJ.js 615.19 kB │ gzip: 147.94 kB
../dist/assets/mermaid.core-fLrhp_4-.js 620.55 kB │ gzip: 148.41 kB
../dist/assets/wasm-CG6Dc4jp.js 622.34 kB │ gzip: 231.16 kB
../dist/assets/cpp-BksuvNSY.js 697.52 kB │ gzip: 50.37 kB
../dist/assets/emacs-lisp-BX77sIaO.js 804.67 kB │ gzip: 197.40 kB
../dist/assets/index-C2GE3DAF.js 1,128.18 kB │ gzip: 374.41 kB
../dist/assets/index-DJ-z1Oba.js 3,149.02 kB │ gzip: 938.48 kB
✓ built in 8.03s
vite v5.4.21 building for production...
transforming...
✓ 322 modules transformed.
rendering chunks...
computing gzip size...
dist/index.js 3,482.94 kB │ gzip: 610.84 kB
✓ built in 1.94s

qwen-code-vscode-ide-companion@0.21.2 build
npm run build:dev

qwen-code-vscode-ide-companion@0.21.2 build:dev
npm run check-types && npm run lint && node esbuild.js

qwen-code-vscode-ide-companion@0.21.2 check-types
tsc --noEmit

qwen-code-vscode-ide-companion@0.21.2 lint
eslint src

@qwen-code/chrome-bridge@0.21.2 build
node scripts/sync-extension.js && node config/esbuild.background.config.js --production

Static assets synced -> dist/extension
Background/content build complete!

@qwen-code/external-context@0.20.1 build
npm run clean && tsc --build

@qwen-code/external-context@0.20.1 clean
node -e "const fs=require('node:fs'); fs.rmSync('dist',{recursive:true,force:true}); fs.rmSync('tsconfig.tsbuildinfo',{force:true})":通过\n- commit:

@wenshao

wenshao commented Aug 1, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification — real-environment run of the reviewer test plan ✅

Verified this PR locally against the production dist/cli.js bundle in a real terminal (tmux, 120×36 pty), with the repository's fake OpenAI server as the model backend so agent timing could be controlled deterministically (agent A of each run blocks on a release file, which makes "pause while the first agent is in flight" exactly reproducible).

Environment: macOS (Darwin 25.6.0), Node v24.18.1, PR head b88fdacbf, QWEN_CODE_ENABLE_WORKFLOWS=1, QWEN_CODE_MAX_WORKFLOW_CONCURRENCY=1, --yolo, isolated $HOME + workspace settings. PR head merges cleanly into current main (checked with git merge-tree — no conflicts).

Static checks: npm run build + npm run bundle pass. Focused tests pass and match the PR's claim exactly: core 251/251 (scheduler, orchestrator, runner, registry, snapshot, workflow tool) and CLI 128/128 (workflowsCommand, BackgroundTasksDialog, BackgroundTasksPill, useBackgroundTaskView).

Scenario 1 — pause while agent A in flight, then resume

Workflow: one parallel([A, B, C]) with concurrency 1, run_in_background: true.

Step Expected Observed
p in Background Tasks while A in flight Running → Pausing ✅ hint flips to cooperative pause pending; detail shows … Pausing + "Pause is cooperative; in-flight work may finish…"
/workflows p <runId> while still pausing clear rejection ✅ warning: "still pausing; wait until it reaches paused before resuming."
A finishes while pausing Pausing → Paused, B/C stay queued ⏸ Paused · 1/3 agents; server log shows zero B/C requests while paused
/workflows after closing dialog paused run stays in Active bucket Active: wf_… paused · 1/3 agents
/workflows p <runId> resume, FIFO order, one completion notification ✅ server log: A 16:23:11 → resume 16:26:09 → B 16:26:09.9 → C 16:26:11.5 (strict A→B→C FIFO); exactly one completion-notification turn

Pausing (in-flight A may finish) → Paused (queued work held):

/workflows keeps the paused run in Active; resume replays the queue and emits exactly one completion notification:

Scenario 2 — pause, then stop instead of resuming

Expected Observed
queued agents never start ✅ server log has zero requests for agents B/C of this run
no success notification ✅ completion-notification count stayed at 1 (scenario 1's only)
parent session stays usable ✅ next prompt answered normally after the stop

Scenario 3 — foreground unchanged + non-interactive rejection

  • Foreground workflow (no run_in_background) still returns its result synchronously through the tool-result channel. ✅
  • qwen -p "/workflows p wf_…" (non-interactive) → Workflow pause controls are available only in the interactive TUI. ✅ (ACP mode not exercised in this run.)
More screenshots (dialog list with pause hint, foreground run)

Observations (non-blocking)

  1. "3/3 agents" on a stopped run — stopping a paused run rejects the two queued dispatches, and those rejections count as settled, so the terminal detail reads ✖ Stopped · 3/3 agents even though B/C never started. Consistent with exactly-once settlement, but slightly misleading at a glance; a possible follow-up is to render it as 1/3 or 3/3 settled.
  2. A pause request can't be withdrawn during pausingresume is only valid from paused, so with a long-running in-flight agent the operator must wait for convergence (or stop). This matches the documented design; just noting it as the observable UX.
  3. Harness artifact, not a PR issue: an agent request held >60 s hits the OpenAI client's streaming retry, so the fake server saw a duplicate request for agent A. Scheduler-level dispatch remained exactly-once; the duplicate came from the HTTP retry layer and would occur on main as well for any model call that stalls that long.

Conclusion: all four items of the reviewer test plan reproduce on macOS with the production bundle; state transitions, queue holding, FIFO resume, notification exactly-once, stop-while-paused, and the non-interactive guard all behave as described. LGTM from the real-environment verification standpoint.

中文版本(Chinese version)

维护者验证 — 真实环境跑通 Reviewer 测试计划 ✅

真实终端(tmux 120×36 pty)中运行生产 dist/cli.js bundle 完成本地验证,模型后端使用仓库自带的 fake OpenAI server,以便确定性控制 agent 时序(每个 run 的 agent A 阻塞在 release 文件上,可以精确复现"第一个 agent 在飞时暂停")。

环境:macOS(Darwin 25.6.0)、Node v24.18.1、PR head b88fdacbfQWEN_CODE_ENABLE_WORKFLOWS=1QWEN_CODE_MAX_WORKFLOW_CONCURRENCY=1--yolo、隔离的 $HOME 与 workspace settings。PR head 与当前 main 合并无冲突(git merge-tree 验证)。

静态检查npm run build + npm run bundle 通过。聚焦测试全部通过,与 PR 描述一致:core 251/251CLI 128/128

场景一 — A 在飞时暂停,然后恢复

Workflow:parallel([A, B, C]),并发度 1,run_in_background: true

  • A 在飞时在 Background Tasks 中按 p:✅ 进入 Pausing,提示 cooperative pause pending,detail 显示协作暂停说明。
  • pausing 期间执行 /workflows p <runId>:✅ 明确警告"仍在 pausing,需等待 paused"。
  • A 完成后:✅ 进入 ⏸ Paused · 1/3 agents;server 日志确认暂停期间 B/C 零请求
  • /workflows:✅ 暂停中的 run 保留在 Active 分组。
  • /workflows p <runId> 恢复:✅ server 日志时间线 A 16:23:11 → resume 16:26:09 → B 16:26:09.9 → C 16:26:11.5,严格 A→B→C FIFO;完成通知恰好一次

场景二 — 暂停后停止

  • ✅ 该 run 的 B/C 从未向模型发出请求(队列中的 agent 从未启动)。
  • ✅ 无成功通知(完成通知计数保持为场景一的 1 次)。
  • ✅ 停止后父会话可以正常继续对话。

场景三 — 前台行为不变 + 非交互拒绝

  • ✅ 前台 workflow(不带 run_in_background)仍同步返回结果。
  • ✅ 非交互 -p "/workflows p wf_…" 明确拒绝:"available only in the interactive TUI"。(本次未覆盖 ACP 模式。)

观察项(不阻塞合并)

  1. 停止后的 run 显示 "3/3 agents" — 停止暂停中的 run 会拒绝两个队列中的调度,拒绝也计为 settled,因此终态显示 ✖ Stopped · 3/3 agents,虽然 B/C 从未启动。与 exactly-once 结算语义一致,但乍看有些误导,可考虑后续微调展示。
  2. pausing 期间无法撤回暂停请求resume 仅在 paused 态有效,遇到长时间在飞的 agent 只能等待收敛或停止。与文档设计一致,仅记录为可观察的 UX。
  3. Harness 现象、非本 PR 问题:agent 请求被 hold 超过 60 秒会触发 OpenAI client 的流式重试,fake server 因此看到 agent A 的重复请求。调度器层面 dispatch 保持 exactly-once;该重复来自 HTTP 重试层,任何模型调用 hang 超过 60 秒在 main 上同样会发生。

结论:Reviewer 测试计划四项在 macOS 生产 bundle 上全部复现;状态迁移、队列保持、FIFO 恢复、通知 exactly-once、暂停后停止、非交互拒绝均符合描述。从真实环境验证角度 LGTM。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

中文说明

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

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

const idx = windowStart + visibleIdx;
const isSelected = idx === selectedIndex;
const terminal = terminalStatusPresentation(entry.status);
const terminal = statusPresentation(entry.status);

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 local variable is still named terminal after terminalStatusPresentation was renamed to statusPresentation. That function now also returns a presentation for the new active states (pausing/paused), so const terminal = statusPresentation(entry.status) is a misnomer — the value is no longer terminal-only. Concrete cost: a future contributor adding a presentation for another active state reads const terminal and concludes the result is only consumed for terminal states, potentially adding a redundant isTerminal guard that silently drops the new styling. Rename the local (e.g. presentation) at all six call sites (lines 403, 523, 696, 1001, 1082, 1173).

Suggested change
const terminal = statusPresentation(entry.status);
const presentation = statusPresentation(entry.status);
中文说明

函数 terminalStatusPresentation 已重命名为 statusPresentation(现在也会为新的 active 状态 pausing/paused 返回值),但六个调用处仍把结果绑定到名为 terminal 的局部变量。这个名字现在具有误导性——该值不再只针对 terminal 状态。具体代价:未来若为其它 active 状态新增 presentation,贡献者读到 const terminal 可能误以为该结果只用于 terminal 状态,进而加上多余的 isTerminal 判断,悄悄丢掉新状态的样式。建议在全部六个调用处(第 403、523、696、1001、1082、1173 行)将局部变量改名为 presentation

— qwen3.8-max-preview via Qwen Code /review

Comment on lines +214 to +215
if (target.status === 'running') {
return registry.pause(runId)

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 registry.pause() / registry.resume() false-return branches (the "…could not be paused/resumed because its state changed" errors) have no test coverage. Every test in workflowsCommand.test.ts stubs pauseMock / resumeMock with .mockReturnValue(true), so the false arm is never exercised. Concrete cost: a regression that makes registry.pause return false for a valid running backgrounded run — or a mutation flipping this ternary — would surface the wrong or missing message, and no test goes red. Add cases with pauseMock.mockReturnValue(false) / resumeMock.mockReturnValue(false) asserting the messageType: 'error' "state changed" content for a running and a paused target respectively.

中文说明

registry.pause() / registry.resume() 返回 false 的分支(即 "…could not be paused/resumed because its state changed" 错误)没有任何测试覆盖。workflowsCommand.test.ts 中所有测试都用 .mockReturnValue(true) 打桩 pauseMock / resumeMock,因此 false 分支从未被执行。具体代价:如果某个回归使 registry.pause 对一个合法的 running 后台 run 返回 false,或有人误翻转了这个三元表达式,将输出错误或缺失的消息,而没有任何测试会变红。建议新增 pauseMock.mockReturnValue(false) / resumeMock.mockReturnValue(false) 的用例,分别对 runningpaused 目标断言 messageType: 'error' 的 "state changed" 文案。

— qwen3.8-max-preview via Qwen Code /review

if (
!entry ||
(!isActiveWorkflowStatus(entry.status) && entry.status !== 'cancelled') ||
entry.agentsCompleted >= entry.agentsDispatched

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 new over-count cap entry.agentsCompleted >= entry.agentsDispatched is mutation-vacuous: no test drives onAgentCompleted past the dispatched count, so deleting this cap keeps the whole suite green. The only test touching this path lands exactly at agentsCompleted: 2, agentsDispatched: 2, where the cap never fires. Concrete cost: this cap is the registry-level backstop for the orchestrator's exactly-once emitCompletion latch; if that latch ever regressed (double-emitting on the abort/error arm), the pill/dialog completion count could exceed dispatched and nothing would catch it. Add a registry unit test that calls onAgentDispatched once then onAgentCompleted twice (in both an active and a cancelled state) and asserts agentsCompleted stays at 1. The cap itself is correct and defensive — this finding is about the missing test, not the cap.

中文说明

新增的超额上限 entry.agentsCompleted >= entry.agentsDispatched 是「变异真空」的:没有任何测试把 onAgentCompleted 驱动到超过 dispatched 数量,因此删掉这个上限整个测试套件仍然全绿。唯一触及该路径的测试恰好停在 agentsCompleted: 2, agentsDispatched: 2,此时上限从不触发。具体代价:该上限是 orchestrator 的 exactly-once emitCompletion 锁存在 registry 层的兜底;一旦那个锁发生回归(在 abort/error 分支重复触发),pill/dialog 的完成计数可能超过 dispatched 而无人发现。建议新增一个 registry 单测:调用一次 onAgentDispatched 再调用两次 onAgentCompleted(分别在 active 状态和 cancelled 状态下),断言 agentsCompleted 保持为 1。上限本身正确且是防御性的——本条指的是缺失的测试,而非上限本身。

— qwen3.8-max-preview via Qwen Code /review

: {
type: 'message' as const,
messageType: 'error' as const,
content: `Workflow ${runId} could not be paused because its state changed.`,

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] registry.pause() returns false for three distinct reasons — a foreground run (!isBackgrounded), a status that is no longer running, or a missing handle — but this branch always reports "could not be paused because its state changed". Foreground runs are the default and ARE listed by /workflows, and the target.status === 'running' pre-check above does not filter them out, so /workflows p <foreground-runId> on a healthy running foreground run prints a misleading "state changed" message when the real reason is that foreground runs are intentionally not pausable. The pause is correctly refused — only the diagnostic is wrong. Concrete cost: a user is told a race occurred when nothing did, and may retry or doubt the pause. Pre-check target.isBackgrounded and return a distinct message (e.g. "Foreground workflow runs cannot be paused; only background runs support cooperative pause"), reserving "state changed" for a genuine status race. The resume arm below has the same shape.

中文说明

registry.pause() 返回 false 有三种不同原因——前台 run(!isBackgrounded)、状态已不是 running、或缺少 handle——但这个分支一律报告 "could not be paused because its state changed"。前台 run 是默认形态,且确实会被 /workflows 列出;上方的 target.status === 'running' 预检查并不会把它们过滤掉。因此对一个正常的 running 前台 run 执行 /workflows p <foreground-runId> 时,会输出一条误导性的 "state changed" 消息,而真正的原因是前台 run 本就不允许暂停。暂停确实被正确拒绝了——只是诊断信息错了。具体代价:用户被告知发生了竞态(其实并没有),可能重试或怀疑暂停是否生效。建议预先检查 target.isBackgrounded 并返回一条独立消息(例如 "Foreground workflow runs cannot be paused; only background runs support cooperative pause"),把 "state changed" 保留给真正的状态竞态。下方的 resume 分支同理。

— qwen3.8-max-preview via Qwen Code /review

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed. Suggestions are inline. Not reviewed: reverse audit — an auditor ran and opened its brief, but no agent was launched with the prompt the CLI built — the launch was written by hand, and what the agent was actually asked is not what this skill certifies.

— qwen3.7-max via Qwen Code /review

Comment on lines +298 to +308
const toggleSelectedWorkflowPause = useCallback(() => {
if (!config) return;
const target = entries[selectedIndex];
if (!target || target.kind !== 'workflow' || !target.isBackgrounded) return;
const registry = config.getWorkflowRunRegistry();
if (target.status === 'running') {
registry.pause(target.runId);
} else if (target.status === 'paused') {
registry.resume(target.runId);
}
}, [config, entries, selectedIndex]);

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 unit test covers toggleSelectedWorkflowPause, the primary new user-facing action in this PR. The Background Tasks dialog integration tests exercise this indirectly through keypress simulation, but a direct unit test would catch future regressions in the callback itself (e.g., swapped status checks, missing isBackgrounded guard, dependency array changes). — Failure scenario: a refactor that changes the callback's early-return conditions or status dispatch would only be caught by the heavier dialog integration tests, not at the unit level.

— qwen3.7-max via Qwen Code /review

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.

Declined — the callback is a thin delegation to registry.pause()/registry.resume() gated by status checks. It is already exercised end-to-end by the BackgroundTasksDialog integration tests (66 tests simulating the p keypress), and the registry-level gating is independently tested in workflow-run-registry.test.ts (65 tests, including the new pausing-state approval and over-count cap tests from this round). A direct unit test would require mocking the entire context provider infrastructure for minimal additional coverage beyond what the integration and registry tests already provide.

中文说明

拒绝 — 该回调是对 registry.pause()/registry.resume() 的薄委托,由状态检查门控。BackgroundTasksDialog 集成测试(66 个测试,模拟 p 按键)已端到端覆盖该路径;registry 层门控由 workflow-run-registry.test.ts(65 个测试,包括本轮新增的 pausing 状态 approval 和超额上限测试)独立测试。直接单元测试需要 mock 整个 context provider 基础设施,额外覆盖有限。

Comment on lines +214 to +225
if (target.status === 'running') {
return registry.pause(runId)
? {
type: 'message' as const,
messageType: 'info' as const,
content: `Cooperative pause requested for workflow ${runId}.`,
}
: {
type: 'message' as const,
messageType: 'error' as const,
content: `Workflow ${runId} could not be paused because its state changed.`,
};

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 error message "could not be paused because its state changed" is misleading when registry.pause() returns false because the workflow is not backgrounded. The command checks target.status === 'running' before calling pause(), but the registry also gates on entry.isBackgrounded — so a foreground workflow with status === 'running' passes the command's check but fails the registry's, and the user sees a state-change error that doesn't apply. — Failure scenario: user types /workflows p <runId> on a foreground workflow and receives "could not be paused because its state changed" when the actual reason is that the workflow isn't running in the background.

Suggested change
if (target.status === 'running') {
return registry.pause(runId)
? {
type: 'message' as const,
messageType: 'info' as const,
content: `Cooperative pause requested for workflow ${runId}.`,
}
: {
type: 'message' as const,
messageType: 'error' as const,
content: `Workflow ${runId} could not be paused because its state changed.`,
};
if (target.status === 'running') {
if (!target.isBackgrounded) {
return {
type: 'message' as const,
messageType: 'error' as const,
content: `Workflow ${runId} is not running in the background and cannot be paused.`,
};
}
return registry.pause(runId)
? {
type: 'message' as const,
messageType: 'info' as const,
content: `Cooperative pause requested for workflow ${runId}.`,
}
: {
type: 'message' as const,
messageType: 'error' as const,
content: `Workflow ${runId} could not be paused because its state changed.`,
};

— qwen3.7-max via Qwen Code /review

Comment on lines +1580 to +1582
return scheduler.waitUntilRunning().then(() => {
throw error;
});

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] waitUntilRunning() in the error path can reject with AbortError, replacing the original dispatch error that the handler intended to re-throw. If the scheduler is paused when a dispatch fails, and the user then cancels (triggering abortPending() which rejects gate waiters), the AbortError from the gate replaces the real dispatch error (e.g., rate limit, budget exceeded) in the caller's rejection chain. — Failure scenario: dispatch fails with a diagnostic error → waitUntilRunning() creates a gate waiter → user cancels → abortPending() rejects the waiter with AbortError.then(() => { throw error }) never executes → the workflow run outcome carries AbortError instead of the dispatch-level diagnostic.

Suggested change
return scheduler.waitUntilRunning().then(() => {
throw error;
});
return scheduler.waitUntilRunning().then(
() => {
throw error;
},
() => {
throw error;
},
);

— qwen3.7-max via Qwen Code /review

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

已修复。验证:cd packages/core && npx vitest run src/agents/runtime/workflow-orchestrator.test.ts(124/124),根目录 npm run build && npm run typecheck、修改文件 ESLint 与 git diff --check 均通过。

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

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

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

中文说明

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

— qwen3.8-max-preview via Qwen Code /review

Comment on lines +462 to +464
onDispatchStateChange(runId: string, state: WorkflowDispatchState): void {
const entry = this.entries.get(runId);
if (!entry || isTerminalWorkflowStatus(entry.status)) return;

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 terminal-state guard in onDispatchStateChange is the only thing that stops a late scheduler callback from resurrecting a settled run, yet no test covers it. — Failure scenario: cancel a background run while its scheduler is pausing with one dispatch in flight (inFlight=1). abortPending() rejects queued jobs and gate waiters but leaves the scheduler state at pausing; when the in-flight thunk then rejects on the abort signal, pump()'s finally sees state === 'pausing' && inFlight === 0 and calls setState('paused'), firing onDispatchStateChange('paused') against an entry that is now cancelled. This guard discards that callback. If it regressed, the cancelled run would flip back to paused: counted active in hasRunningEntries() (blocking /clear and session shutdown), escaping terminal eviction, and rendering as paused in the Background Tasks dialog. No existing test drives onDispatchStateChange after a terminal transition.

// Suggested registry unit test
const r = createRegistry();
r.register({ runId: 'wf_x', isBackgrounded: true, /* ... */ });
r.onDispatchStateChange('wf_x', 'pausing');
r.cancel('wf_x', Date.now());
r.onDispatchStateChange('wf_x', 'paused'); // late callback
r.onDispatchStateChange('wf_x', 'running'); // late callback
expect(r.get('wf_x')!.status).toBe('cancelled');
中文说明

onDispatchStateChange 中的终态守卫是阻止调度器迟到回调把已结束的运行「复活」的唯一防线,但目前没有测试覆盖它。失败场景:在一个后台运行的调度器处于 pausing 且有一个在飞调度(inFlight=1)时取消该运行。abortPending() 会拒绝队列中的任务与结果门等待者,但不改变调度器状态(仍为 pausing);当在飞 thunk 随后因 abort 信号 reject 时,pump()finally 看到 state === 'pausing' && inFlight === 0,调用 setState('paused'),对此时已是 cancelled 的 entry 触发 onDispatchStateChange('paused')。该守卫丢弃这个迟到回调。若发生回归,被取消的运行会被翻回 paused:在 hasRunningEntries() 中被计为活跃(阻塞 /clear 与会话关闭)、逃过终态驱逐,并在 Background Tasks 对话框中显示为已暂停。现有测试没有任何一个在终态转换之后再调用 onDispatchStateChange。建议补充上方代码块中的 registry 单测。

— qwen3.8-max-preview via Qwen Code /review

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.

已修复。验证:cd packages/core && npx vitest run src/agents/workflow-run-registry.test.ts(63/63)与 git diff --check 均通过。

import { toModelVisibleSubagentResult } from '../subagent-result.js';
import { SUBAGENT_PLAN_LIFECYCLE_TOOLS } from './subagent-plan-tool-policy.js';
import { runWithAgentContext } from './agent-context.js';
import { WorkflowDispatchScheduler } from './workflow-dispatch-scheduler.js';

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 import swap removes the only production consumer of createConcurrencyLimiter, leaving packages/core/src/utils/concurrencyLimiter.ts and its ~115-line test file as dead code. — Concrete cost: the module is not exported from the core index.ts and no other production file imports it, yet CI keeps running its test suite on every change, and future maintainers may preserve its API contract assuming it is still in use. WorkflowDispatchScheduler fully subsumes it (concurrency limiting and abort draining, plus the new pause/resume gate).

git rm packages/core/src/utils/concurrencyLimiter.ts packages/core/src/utils/concurrencyLimiter.test.ts
中文说明

这次 import 替换移除了 createConcurrencyLimiter 唯一的生产消费者,使 packages/core/src/utils/concurrencyLimiter.ts 及其约 115 行测试文件成为死代码。具体代价:该模块没有从 core 的 index.ts 导出,也没有任何其他生产文件 import 它,但 CI 仍会在每次变更时运行它的测试套件,未来的维护者可能会以为它仍在使用而刻意维护其 API 契约。WorkflowDispatchScheduler 已完全覆盖它的能力(并发限制与 abort 排空,外加新的暂停/恢复门)。建议在本 PR 或后续 PR 中删除这两个文件(见上方命令)。

— qwen3.8-max-preview via Qwen Code /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.

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

中文说明

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

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

@wenshao

wenshao commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Maintainer re-verification at latest head 552cb77 — follow-up fixes confirmed ✅

My previous verification ran against the initial commit b88fdacbf. Since then 4 commits landed addressing review findings (b84c144 restrict pause to background runs, 2337d58 clarify foreground pause errors, beba054 preserve dispatch errors across cancellation, 552cb77 late-state-callback tests), so I re-ran the full verification against the current head 552cb7707 in a real environment: production dist/cli.js bundle, real terminal (tmux 120×42 pty), the repository's fake OpenAI server as the model backend (agent requests block on release files, making "pause while agent A is in flight" exactly reproducible), isolated $HOME + fixture workspace.

Environment: macOS (Darwin 25.6.0), Node v24.18.1, QWEN_CODE_ENABLE_WORKFLOWS=1, QWEN_CODE_MAX_WORKFLOW_CONCURRENCY=1, --yolo.

Static checks (all at 552cb77): npm run lint ✅ · npm run typecheck ✅ · npm run build + npm run bundle ✅. Focused tests — core 254/254 across the 6 touched core files (scheduler, orchestrator, runner, registry, snapshot, workflow tool) and CLI 132/132 across the 4 touched CLI files (workflowsCommand, BackgroundTasksDialog, BackgroundTasksPill, useBackgroundTaskView); counts are up from 251/128 at the first head because the fix commits added tests. GitHub reports the branch mergeable against main.

Scenario 1 — pause while agent A in flight, then resume (test-plan step 1–2)

Workflow: parallel([A, B, C]), concurrency 1, run_in_background: true. Agent A's provider request is held open by the test gate.

Step Expected Observed
p in Background Tasks while A in flight Running → Pausing ✅ detail shows … Pausing + "Pause is cooperative; in-flight work may finish before the workflow is paused."; hint flips to cooperative pause pending
A's request released Pausing → Paused, B/C stay queued ⏸ Paused · 1/3 agents, hint p resume (cooperative)
While paused no new provider traffic ✅ provider request log frozen for the entire paused window (> 3 min) — zero requests for B or C
/workflows after closing the dialog paused run stays in Active bucket Active: wf_fed71499c61f2912 paused
/workflows p <runId> FIFO resume, one completion notification ✅ B dispatched at 02:08:50.461, C at 02:08:50.471 (A→B→C order), exactly one task-notification model turn at 02:08:50.496, "Background workflow "pause-verify" completed."

Provider request log (the hard evidence — every model request the CLI actually issued):

02:03:16 launch-turn        (workflow tool call returned)
02:03:16 agent-alpha        (dispatch #1 in flight, held open by the test gate)
02:04:16 agent-alpha        (60s client retry of the held request — harness artifact, see note)
02:05:16 agent-alpha        (retry; gate released → ALPHA_DONE; run converges to Paused)
         --- paused window: no bravo / no charlie / no notification ---
02:08:50.461 agent-bravo    (resume requested via /workflows p)
02:08:50.471 agent-charlie  (FIFO after bravo)
02:08:50.496 notification-turn   (exactly one completion notification)

Note: the duplicate agent-alpha lines are the provider client's 60s streaming-timeout retries against my deliberately stalling fake server — in Scenario 2, where the gate opens within 60s, agent A issues exactly one request. Not PR behavior.

Pausing (A in flight) Paused (1/3, B/C queued)
pausing paused

Full sequence — /workflows Active bucket while paused, resume via /workflows p, single completion notification:

resumed and completed

Scenario 2 — pause, then stop instead of resume (test-plan step 3)

Step Expected Observed
pause → release A → Paused as scenario 1 ✅ (A issued exactly one provider request this time)
x, x to confirm stop queued agents never start ✅ request log ends at A — zero B/C requests, ever
after stop no success notification zero task-notification turns; detail shows ✖ Stopped
next prompt parent session usable ✅ follow-up prompt round-trips normally (PONG_OK)
Stopped from paused Session still usable
stopped alive

Scenario 3 — foreground runs reject pause (verifies the b84c144 fix; test-plan step 4)

Started the same workflow without run_in_background and opened the Background Tasks dialog mid-turn while agent A was in flight:

  • The foreground workflow row shows no p pause hint (only x stop), and pressing p is a no-op — status stays running, no Pausing transition. This is the review finding from the first round, now fixed and verified in the real TUI.
  • After releasing the gates the run completed synchronously through the normal tool-result channel with zero background notifications — foreground semantics unchanged.

foreground no pause

Guard rails (real TUI)

/workflows p wf_deadbeef00000000Unknown live workflow runId; /workflows p <completed runId>Workflow … is completed and cannot be paused or resumed. Non-interactive/ACP rejection and the foreground /workflows p error path are covered by the passing unit tests (workflowsCommand.test.ts).

More screenshots (running dialog with p hint · /workflows paused listing · guard errors)

running dialog
workflows list paused
guards

Observations (non-blocking)

  1. A stopped-from-paused run displays 3/3 agents although only agent A ever executed — queued dispatches settled as rejected by cancellation are counted into agentsCompleted (deliberate per beba054's exactly-once settlement; review finding 4). Fine as-is, but a UI distinction between "ran" and "settled by cancellation" would avoid misreading.
  2. User docs (docs/users/reference/keyboard-shortcuts.md, commands doc) still don't mention the p key / /workflows p (review finding 5). Suggest a small follow-up.

Verdict

All four reviewer-test-plan scenarios pass against the production bundle at the latest head, the pause gate provably keeps queued dispatches out of the provider, resume preserves FIFO order with exactly one completion notification, stop-from-paused leaks nothing, and the first-round review findings 1 (foreground pause) has been fixed and re-verified. LGTM from the verification standpoint — merge-ready, with the docs follow-up tracked as a nice-to-have.

中文版本(Chinese version)

维护者复验 — 最新 head 552cb77,后续修复已确认 ✅

上一轮验证针对的是首个提交 b88fdacbf。此后作者推送了 4 个针对评审意见的修复提交(b84c144 限制仅后台运行可暂停、2337d58 澄清前台暂停错误提示、beba054 取消时保留 dispatch 错误、552cb77 补充迟到状态回调测试),因此本轮在当前 head 552cb7707 上重跑了全部验证:生产 dist/cli.js bundle、真实终端(tmux 120×42 pty)、仓库自带 fake OpenAI server 作为模型后端(agent 请求阻塞在释放文件上,可精确复现"第一个 agent 在飞时暂停")、隔离的 $HOME 与 fixture 工作区。

环境:macOS (Darwin 25.6.0)、Node v24.18.1、QWEN_CODE_ENABLE_WORKFLOWS=1QWEN_CODE_MAX_WORKFLOW_CONCURRENCY=1--yolo

静态检查(均在 552cb77):lint ✅ · typecheck ✅ · build + bundle ✅。Focused 测试:core 254/254(6 个被改动的 core 文件)、CLI 132/132(4 个被改动的 CLI 文件);比首个 head 的 251/128 多,是因为修复提交新增了测试。GitHub 显示分支可合并到 main

场景 1 — agent A 在飞时暂停,随后恢复(测试计划步骤 1–2)

Workflow:parallel([A, B, C]),并发度 1,run_in_background: true,A 的 provider 请求被测试门挡住。

  • Background Tasks 中按 p(A 在飞)→ ✅ 详情页显示 … Pausing 与"Pause is cooperative…"提示,快捷键提示变为 cooperative pause pending
  • 放行 A 的请求 → ✅ ⏸ Paused · 1/3 agents,提示 p resume (cooperative)
  • 暂停期间 → ✅ provider 请求日志在整个暂停窗口(> 3 分钟)完全静止,零条 B/C 请求
  • 关闭对话框后 /workflows → ✅ 暂停中的运行保留在 Active 分组(paused)
  • /workflows p <runId> 恢复 → ✅ B 于 02:08:50.461 下发、C 于 .471 下发(A→B→C FIFO),恰好一条完成通知(02:08:50.496),提示 "Background workflow "pause-verify" completed."

(请求日志摘录见英文部分;重复的 agent-alpha 行是 provider 客户端对被故意挂起请求的 60 秒流式超时重试——场景 2 中 60 秒内放行时 A 只发出一条请求,与 PR 行为无关。)

场景 2 — 暂停后停止而非恢复(测试计划步骤 3)

  • 暂停 → 放行 A → Paused(本次 A 恰好只有一条请求)→ 按 xx 确认停止
  • ✅ 请求日志止于 A——B/C 从未下发;✅ 零条完成通知;详情显示 ✖ Stopped;✅ 后续消息正常往返(PONG_OK),父会话可用

场景 3 — 前台运行拒绝暂停(验证 b84c144 修复;测试计划步骤 4)

不带 run_in_background 启动同一 workflow,turn 进行中打开 Background Tasks:前台 workflow 行没有 p pause 提示(仅 x stop),按 p无操作——状态保持 running。这正是第一轮评审的发现 1,现已修复并在真实 TUI 中复验。放行后运行通过正常工具结果通道同步完成,零条后台通知——前台语义未变。

护栏(真实 TUI)

/workflows p wf_deadbeef00000000Unknown live workflow runId;对已完成运行执行 → is completed and cannot be paused or resumed.。非交互 / ACP 拒绝与前台 /workflows p 错误路径由通过的单测覆盖。

观察(不阻塞合并)

  1. 从暂停停止的运行显示 3/3 agents,但实际只有 A 执行过——被取消拒绝的排队 dispatch 也计入 agentsCompleted(beba054 的 exactly-once 结算语义,评审发现 4)。现状可接受,但 UI 上区分"执行过"与"被取消结算"更不易误读。
  2. 用户文档(快捷键、命令文档)仍未提及 p 键 / /workflows p(评审发现 5),建议小型后续 PR 补上。

结论

四个评审测试计划场景在最新 head 的生产 bundle 上全部通过:暂停门可证明地把排队 dispatch 挡在 provider 之外;恢复保持 FIFO 且恰好一条完成通知;暂停后停止无泄漏;第一轮评审发现 1(前台可被暂停)已修复并复验。从验证角度 LGTM,可合并;文档补充作为 nice-to-have 跟进。

wenshao
wenshao previously approved these changes Aug 2, 2026
@wenshao

wenshao commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 458 passed · 0 failed · 458 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:458 通过 · 0 失败 · 458 总计

Verification report

PR #8320 Deep Verification — feat(workflows): add cooperative pause and resume

Verdict: merge-ready — 458/458 scripted assertions met their expectation, 0 unexpected failures.
Verified head: 552cb7707fe80abbf6d03b2b248c133dd5cd26cd (git rev-parse HEAD^2); A/B base: 184365390320639be94cc13b66748fd8328cb2f1 (HEAD^1).

中文摘要
  • 结论merge-ready。458/458 脚本化断言全部符合预期,无意外失败。
  • A/B 结论:核心改动(结果门 waitUntilRunning())经证明是承重的。在真实编译出的 WorkflowDispatchScheduler 上跑 mock-free harness:HEAD 32/32 通过;把结果门摘除的对照组 31/32,唯一翻转的断言正是「暂停期间结果被扣留在门后」(对照组里该结果在暂停时立即送达 value=A)。其余性质(泵门阻止新调度、FIFO 恢复、exactly-once 结算、abort 瞬间排空队列)不依赖结果门,来自调度器自身——翻转面恰好隔离出本 PR 的贡献。
  • 空转检验:把 orchestrator 活路径的结果门 hunk 还原后,核心测试 keeps a nested agent result behind the shared pause gate 在其目标断言 expect(settled).toBe(false) 上变红(expected false, received true),证明该测试非空转、确实由结果门钉住。
  • 端到端 wire-oracle:驱动真实 orchestrator + scheduler + vm sandbox + 计数 provider,证明 Reviewer 测试计划场景——暂停时排队的 B/C 不进入 provider、在飞 A 的结果扣留在门后;恢复后 provider 按 A,B,C FIFO 调用、run 恰好结算一次;停止暂停中的 run 时 B/C 永不启动且 run reject(无成功结算)。
  • 注册表状态机探针:21/21,严格转移(running→pausing→paused→running)、paused 可 cancel 并锁定终态、完成计数 exactly-once 上限、pause/resume 门控、前台 run 不可暂停。
  • 定向门禁:core 受影响 6 个测试文件 254/254、CLI 受影响 4 个测试文件 132/132,全绿。
  • Findings:无阻塞性问题。两条非阻塞观察见下(调度器 abort 后状态停留 pausedp 处于 pausing 时返回 warning——均为预期行为)。
  • 未覆盖:跨进程持久恢复、journal durability、真实 provider 认证(PR 自述范围外);逐 commit 归因(CI 浅克隆 depth 2 不可达);repo 级 lint/typecheck(CI 已覆盖,未重跑)。

Scope selection

Central claim: cooperative pause holds fulfilled/rejected dispatch results at a gate (waitUntilRunning()) until resume, stops new dispatches from starting while paused, and cancellation rejects queued work + wakes gated waiters while preserving exactly-once settlement.

Secondary claims: (1) the registry run lifecycle distinguishes running/pausing/paused, allows cancel from any active state, and keeps the dispatch/completion counters exactly-once; (2) CLI /workflows p <runId> and Background Tasks p provide the control interactively and reject foreground / non-interactive pause clearly.

Budget went to: an A/B load-bearing proof of the central claim (~half), two mock-free wire-oracle harnesses (scheduler unit + orchestrator integration), a registry state-machine probe, a vacuity check on the central test, and targeted gates on the affected workspaces.

Central claim — A/B load-bearing proof

The pause gate is WorkflowDispatchScheduler.waitUntilRunning(), which the orchestrator chains onto both result paths (cache-hit and live dispatch): a result is only delivered once the scheduler is running. I drove the real compiled scheduler (packages/core/dist/.../workflow-dispatch-scheduler.js) through pause/resume/cancel/abort scenarios with a harness that mirrors the orchestrator's exact gating pattern, then ran the identical harness with the gate removed (the control: waitUntilRunning() resolves immediately — i.e. the orchestrator hunk reverted).

cell environment observable oracle result
HEAD (gate active) real compiled scheduler, limit=1, deferred in-flight A, queued B/C result held while paused; FIFO resume; exactly-once; abort drains queue 32/32 pass
CONTROL (gate removed) same scheduler, waitUntilRunning() short-circuited same 32 assertions 31/32 — exactly one flip

The single flip is the load-bearing property: S1 A result HELD at gate while paused goes PASS → FAIL (state=fulfilled value=A) — with the gate gone, the in-flight result is delivered immediately even though the run is paused. Every other assertion (queued B/C never start while paused, FIFO A,B,C resume, exactly-once settlement, abort draining a 50-job backlog in <50 ms, concurrency window) is unchanged across the two cells, proving those properties live in the scheduler's pump/abort logic while "hold results until resume" is contributed specifically by the waitUntilRunning() chaining — the orchestrator's load-bearing hunk. Witness: 01-ab-scheduler-head-vs-gateless.png.

Vacuity check (the gate hunk pins the central test)

Reverted only the live-dispatch success-path gate in workflow-orchestrator.ts ((result) => scheduler.waitUntilRunning().then(() => result)(result) => result, interface-preserving) and ran the central test keeps a nested agent result behind the shared pause gate:

  • Unmutated: green (Tests 1 passed).
  • Mutated: red on the intended assertion — AssertionError: expected true to be false at line 836 expect(settled).toBe(false) (the run settles while paused because the result is no longer held). Source restored afterward; git status clean.

The revert breaks the behavior the test exists to catch (not the import/compile/fixture), so the test is non-vacuous and is pinned by exactly this hunk. Witness: 02-vacuity-gate-hunk.png.

End-to-end wire-oracle (orchestrator + scheduler + vm sandbox)

Drove the real WorkflowOrchestrator with a counting, controllable fake provider (the dispatch seam the orchestrator is designed to accept; real vm sandbox, real scheduler) through the Reviewer Test Plan scenario (concurrency 1, parallel([agent('A'),agent('B'),agent('C')])):

  • E1 pause/resume: only A reaches the provider initially; after pause() and A settling, state is paused, the provider still saw only A (B/C held out), and the run is not settled (A's result held at the gate). After resume(), the provider is called A,B,C in FIFO order and the run settles exactly once with ['A','B','C'].
  • E2 stop a paused run: with B/C queued and A held, controller.abort() → B/C never reach the provider and the run rejects (no success settlement).

Both pass (2/2). Witness: 04-e2e-pause-resume-cancel.png.

Harness-fidelity note (recorded so the next reader does not repeat it): my first E2 run showed the run resolving on abort. That was my harness omitting abortOnTimeout, which left parallel's internal signal undefined so settleToNullArray's if (signal?.aborted) throw never fired. Production threads the same controller through abortOnTimeout (workflow-runner.ts line ~187); mirroring that, E2 rejects as designed. This is a fake-peer-semantics bug in my harness, not a defect in the PR.

Registry state-machine probe (secondary claim 1)

Drove the real compiled WorkflowRunRegistry (no mocks): 21/21 pass. Witness: 03-registry-state-machine.png.

  • Transitions are strict: running→paused and pausing→running are rejected; only running→pausing→paused→running is admitted.
  • Cancel from pausedcancelled, and the entry is then terminal: subsequent onDispatchStateChange, complete(), and fail() are all no-ops (this is what suppresses a success notification on stop — the contract behind Reviewer step 3).
  • Completion counter is exactly-once: 5 onAgentCompleted calls against 2 dispatched cap agentsCompleted at 2.
  • pause()/resume() gating: pause() needs backgrounded + running + attached handle; resume() needs paused + handle (false while running or pausing); foreground runs cannot be paused; a paused run still counts as active for re-registration.

Targeted gates (affected workspaces)

workspace files result
core workflow-dispatch-scheduler, workflow-orchestrator, workflow-runner, workflow-run-registry, workflow-snapshot, tools/workflow/workflow 6 files, 254/254 pass
cli workflowsCommand, BackgroundTasksDialog, BackgroundTasksPill, useBackgroundTaskView 4 files, 132/132 pass

The gate is live: the vacuity check above is the positive control (the central test goes red when the gate hunk is reverted), so the green suite is shown to actually pin the behavior. The CLI p handler was read directly: it rejects non-interactive use, malformed usage, unknown runId, and foreground runs with clear messages, and returns a warning (not a silent no-op) when p is pressed during pausing.

Findings

No blocking findings. Two non-blocking observations, both judged to be intended behavior:

  1. (nit) Scheduler state stays paused after abort. abortPending() rejects queued jobs and gate waiters but does not transition state out of paused. This is harmless: terminal status is tracked by the registry (proven to lock out further transitions in R2), and the per-run scheduler is discarded with the run. No assertion depends on a post-abort scheduler state.
  2. (nit, already handled) p during pausing. Neither pause() (needs running) nor resume() (needs paused) acts while a run is pausing; the CLI surfaces this as an explicit warning ("still pausing; wait until it reaches paused"), so it is a deliberate cooperative-pause boundary with clear feedback, not a dead control.

Not covered

  • Durable cross-process resume, journal durability, real provider auth/network, per-agent controls — explicitly out of scope per the PR description; not probed.
  • Per-commit attribution — the CI checkout is depth 2 (merge commit + base tip + PR head only); the five commits in the metadata snapshot are not individually reachable, so claims were verified against the aggregate HEAD^1..HEAD diff. git rev-parse --is-shallow-repository = true.
  • Repo-wide lint / typecheck / bundle — covered by the PR's own CI; not re-run (my A/B used the pre-built dist and vitest-on-source, neither of which required a rebuild).
  • Standalone node execution of the orchestrator harness — importing the orchestrator as an entry module trips a pre-existing circular-init TDZ between worktreeCleanup.ts and gitWorktreeService.ts. Proven environmental (A/A): both files are byte-identical on base and head and untouched by this PR. The same orchestrator loads cleanly under the vitest load order, which is how the e2e wire-oracle was run.
  • Windows / Linux real-terminal pause/resume — the PR marks these ⚠️ (untested by the author); this Linux container ran the unit/integration layers, not a TUI session.

Methodology

Environment: CI verify container (node:22-bookworm, Node v22.23.2), working tree at refs/pull/8320/merge, npm ci + npm run build pre-completed at HEAD. All harnesses are mock-free with respect to the unit under test — they import the real compiled dist modules (scheduler, registry) or drive the real orchestrator + vm sandbox through the dispatch seam (the only fake is the provider, the seam the orchestrator is designed to accept). The A/B control differs from HEAD by nothing else than the gate hunk (short-circuited waitUntilRunning() in the harness; reverted hunk in the source for the vacuity check). Raw per-cell logs live in logs/ (01-scheduler-head.txt, 02-scheduler-gateless.txt, 03-vacuity-green.txt, 04-vacuity-mutated.txt, 05-core-gate.txt, 06-cli-gate.txt, 07-registry-probe.txt, 08-e2e-orchestrator.txt); rerunnable harnesses are scheduler-harness.mjs, registry-harness.mjs, e2e-orchestrator.mjs, and e2e-orchestrator.harness.test.ts (the last must run under the vitest load order — see Not covered). Assertion tally: scheduler 32 + control differential 1 + registry 21 + e2e 16 + vacuity 2 + targeted gates 386 = 458, all met expectation; the single intermediate e2e red was a diagnosed harness-fidelity bug (corrected), so unexpected fail = 0.

Evidence images

01-ab-scheduler-head-vs-gateless

02-vacuity-gate-hunk

03-registry-state-machine

04-e2e-pause-resume-cancel

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot's only review on 552cb7707fe80abbf6d03b2b248c133dd5cd26cd is a COMMENTED one, which carries no vote — so it has no verdict of its own on this commit, and main needs two approving reviews: an approval left by another account is a separate vote and does not count as the bot's own. Two different things look like this, and the stage-3 comment above says which: the triage skill deferring on purpose at 3/5 — a fork refactor hitting the approval guardrail, or a core change escalated for maintainer awareness, both normal outcomes — or an earlier approval that a push dismissed, leaving only the comment behind, which needs a fresh review.

⚠️ 机器人在 552cb7707fe80abbf6d03b2b248c133dd5cd26cd 上唯一的评审是 COMMENTED不带票 —— 因此它在该 commit 上没有自己的裁决,而 main 需要两个批准(其他账号的批准是另一张票)。有两种情况长这样,上方的 stage-3 评论会说明是哪一种:triage skill 在 3/5 时有意 defer(fork refactor 命中审批护栏,或核心改动被升级交由维护者把关,两者都是正常结果);或者更早的批准被一次推送作废、只剩下这条评论,此时需要重新评审。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

wenshao added a commit that referenced this pull request Aug 7, 2026
@wenshao

wenshao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

Maintainer re-verification at head 750133f4 — real terminal, real bundle, on Linux

My previous passes ran at b88fdacbf, 552cb77 and fd8a6ed77. Rounds 11–13 have landed since, including another large pass over workflow-sandbox.ts (+446/−147 in round 13 alone), a change to what blocks a session switch, and new wording for /workflows p. I rebuilt the harness from scratch at this head and re-ran the whole reviewer test plan plus five scenarios aimed specifically at what changed after my last pass.

Verdict: merge-ready. Every reviewer test-plan item reproduced. Three non-blocking notes below — one of them retires a claim made in the round-11 summary, so it is worth reading before anyone treats that claim as verified.

The PR's "Tested on" table marks 🐧 Linux as ⚠️. This run is Linux (Debian 13, kernel 6.12.63, Node v22.22.2), so that cell can be upgraded.

Setup

Isolated git worktree at 750133f4007ce1a5e5e8534e4debbbe298186849, full npm run build + npm run bundle; everything below drives the production dist/cli.js, not the dev entrypoint and not a unit-test harness.

  • Model transport: the repository's own integration-tests/fake-openai-server.ts, wrapped so each agent request blocks on a release file. That is what makes "agent A is in flight, B and C are queued" a deterministic state instead of a race.
  • UI: a real pty via tmux new-session -x 130 -y 42, isolated QWEN_HOME, QWEN_CODE_ENABLE_WORKFLOWS=1, QWEN_CODE_MAX_WORKFLOW_CONCURRENCY=1.
  • Script under test: parallel([agent A, agent B, agent C]) behind a concurrency-1 window.
  • Every provider request is timestamped into requests.jsonl, so "did not reach the provider" is an observation, not an inference.
  • Scenarios 1–3, the /clear A/B and the pausing-feedback check were each run twice end-to-end; timings and conclusions reproduce.

Harness note for anyone rebuilding this: the fake server must route on the last user message only, and the tool must be called by its registered name workflow (not the display name Workflow). The main conversation embeds the script text inside assistant tool-call arguments, so a whole-body match misclassifies main turns as agent turns and deadlocks them on the gate.

Reviewer test plan

# Test-plan item Result
1 Pause while agent A is in flight → PausingPaused, B and C still queued
2 Paused run stays in the /workflows Active bucket; /workflows p <runId> resumes in FIFO order; exactly one completion notification
3 Stop a paused run instead of resuming → queued agents never start, no success notification, session stays usable
4 Foreground keeps synchronous semantics; the dialog refuses pause on a foreground run ✅ (see note C)
5 Pause-aware wall-clock watchdog: paused time is not charged, backstop still armed while running ✅ re-confirmed

1 · Running → Pausing → Paused

lifecycle

p was pressed while agent A was still gated at the provider. The run went to … Pausing · 0/3 agents with the honest banner "Pause is cooperative; in-flight work may finish before the workflow is paused", and only reached ⏸ Paused · 1/3 agents after A settled. The footer hint tracks the state exactly: p pause while running, no p at all while pausing, p resume once paused — you cannot ask for a transition the state does not accept.

2 · Paused stays Active, and resumes from the slash command

resume

/workflows listed the run under Active as paused (not dropped into the terminal bucket), and /workflows p <runId> — the process-local control path, not the dialog — resumed it.

The provider-side timeline is the load-bearing evidence:

timeline

77.2 seconds of pause with zero requests for B and C, then B and C in script order after resume, then exactly one <kind>workflow</kind> completion carrying {"out":["marker-A-done","marker-B-done","marker-C-done"]} — the position-aligned array survives the pause. The first run of this scenario produced the same shape with a 77.0 s window.

3 · Pause → stop

stop

Stopping the paused run recorded it as cancelled, B and C never reached the provider (1 agent request total, for A), zero completion notifications were emitted, and the parent session answered the very next prompt. Process stderr stayed clean across every scenario in this report — no unhandledRejection from cancelled gate waiters.

5 · Wall-clock watchdog (re-checked because round 13 rewrote the sandbox)

wallclock

  • Held paused for 52 s on a 25 s budget → the run was not killed mid-pause, resumed cleanly, and completed. Paused time genuinely is not charged.
  • Control, same 25 s budget, never paused → aborted at exactly 25 s with Workflow execution exceeded 25000 ms of active time (paused time is not counted). The backstop is suspended, not removed.

New since my last pass — rounds 11–13

R12 · a paused run no longer blocks /clear

This is the behaviour change with the most operator-visible consequence, so I ran it as an A/B.

clear

  • Running/clear still refused: Stop the current session's running background tasks before starting a new session.
  • Paused/clear went through.
  • The paused run was genuinely aborted, not leaked: over 20 s of quiet afterwards, zero further provider requests, zero completion notifications, and an on-disk snapshot recording status: "cancelled". The abortAll()-before-reset() ordering does what its comment claims.

I agree with the call — a paused-and-forgotten run has its watchdog suspended, so if it blocked the switch there would be no backstop to release it. But it does mean a session switch silently cancels a paused run, and that is not mentioned in the PR description's Risk & Scope section. Worth one line there.

R12 · p during Pausing now answers instead of looking stuck

flash

A second p while the run is still pausing lights Pause/resume was rejected; the workflow state changed. Try again. and the flash clears itself ~3 s later. Since pausing can last a whole subagent dispatch, this is a real improvement over the previous silent no-op.

R11 / R13 · /workflows p on a run the registry no longer holds

wordings

After a session switch emptied the registry, /workflows p <runId> for a run that is still listed from its snapshot answers Workflow <id> is cancelled and cannot be paused or resumed. — not the contradictory "Unknown live workflow runId" it used to give. An id present in neither source still gets Unknown live workflow runId. A completed run gets the same terminal wording. Both halves behave as described.

Tests and hygiene at this head

  • packages/core focused: 438 passed / 7 files (workflow-dispatch-scheduler, workflow-orchestrator, workflow-runner, workflow-sandbox, workflow-run-registry, workflow-snapshot, tools/workflow) — up from 402 at my last pass.
  • packages/cli focused: 188 passed / 6 files (workflowsCommand, BackgroundTasksDialog, BackgroundTasksPill, useBackgroundTaskView, backgroundWorkUtils, useBranchCommand).
  • npm run build, npm run bundle, npm run typecheck (0 TS errors), npm run lint — all clean. (Lint reported 26 problems, all of them in my own harness files under an untracked harness/ directory, none in the PR's files.)
  • The deleted utils/concurrencyLimiter.ts still has no remaining references anywhere under packages/.

Three non-blocking notes

A. Round 11's stated hazard for Promise.all does not reproduce — the fix improves log wording, not crash-safety

Round 11 (R10-9) justified wrapping Promise.all / race / any by saying that without it, a fire-and-forget aggregate holding a failed dispatch "fires a process-level unhandledRejection (the interactive CRITICAL banner, or Node's default --unhandled-rejections=throw termination in headless hosts)". I A/B'd that claim: I disabled just the three static wrappers, rebundled, and re-ran two fire-and-forget shapes (a bare rejecting aggregate, a .then() derived off one, and a nested aggregate), using the per-run agent cap to produce a deterministic dispatch rejection.

Neither build crashed. No CRITICAL banner, no unhandledRejection on stderr, process alive in both. What actually changed is the mirrored log line:

script with the wrap (this PR) wrap disabled
fire-and-forget Promise.all + .finally() on a rejected dispatch result not consumed + rejection not handled rejection not handled ×2
.then() derived off a rejecting aggregate + a nested aggregate rejection not handled + result not consumed rejection not handled ×2

(Both lines are prefixed dispatch failed (…): Workflow exceeded the maximum of N agent() calls per run.; I am not claiming which line maps to which shape, only the classification each build produced.)

Both builds emit the same number of lines. The host-side adoption-escape hook — which the R11-16 comment itself mentions — is the real backstop; the aggregate wrap refines the classification. That is a legitimate improvement and I am not asking for it to be reverted. But the round-11 summary overstates the risk it retired, and nobody should treat "would otherwise terminate the process" as verified.

B. The persisted agent counter for a terminated paused run depends on how it was terminated

Same script, same outcome — only agent A ever reached the provider — but:

how the paused run ended live /workflows on-disk snapshot /workflows after a restart
x from Background Tasks 3/3 agents dispatched=3 completed=3 3/3 agents
/clear while paused dispatched=3 completed=1 1/3 agents

The /clear path calls abortAll() then reset(), so the draining dispatches' onAgentCompleted() calls find no entry and stop counting; the dialog path keeps the entry and the drain lands before the snapshot write. Both numbers are defensible under "settled" versus "executed", and R11-10 (widening the drain past cancelled to completed/failed) and R13 (freezing the snapshot projection before the first await) each make sense on their own — they just pull in opposite directions. The visible result is that a run where one agent executed persists as either 1/3 or 3/3 depending on how it was killed. I also hit the failed face of this in the watchdog control run: a run aborted at 25 s with a single dispatch to the provider reports failed · 3/3 agents.

This is the concrete second face of the cosmetic note I raised last round. Still cosmetic, still non-blocking — flagging it because it is now reachable on completed and failed runs, not just cancelled ones.

C. The live-foreground /workflows p wording is effectively unreachable from the TUI

A foreground workflow blocks the composer for its entire lifetime, so /workflows p <runId> typed during one is queued and only submits after the run has already settled — at which point round 13's new terminal check answers first with the terminal wording. I confirmed the terminal wording at this head; I could not reach the foreground wording through the TUI this round (I did observe it at fd8a6ed77). The branch is still correct defensive code and the dialog-level foreground gate is re-confirmed here — a foreground run offers x stop but no p pause, and pressing p is a no-op with no flash. Just noting that its user-facing string is close to theoretical.

Not covered by this run

Real provider authentication and network latency, durable cross-process resume, journal durability, per-agent controls, macOS and Windows. Gated agent requests would hit the provider client's stream timeout if a pause were held for minutes; every pause window here stayed well under it, so no retry noise contaminated the timelines.

中文版本

维护者在最新 head 750133f4 上的复验 —— 真实终端 + 生产 bundle,运行于 Linux

我此前三轮验证分别跑在 b88fdacbf552cb77fd8a6ed77 上。之后 round 11–13 陆续合入,其中包括对 workflow-sandbox.ts 的又一次大改(仅 round 13 就是 +446/−147)、对「什么会阻塞 session 切换」的调整,以及 /workflows p 的新文案。我在当前 head 上从零重建 harness,重跑了完整的 reviewer 测试计划,并新增五个专门针对上次之后改动的场景。

结论:可以合并。 reviewer 测试计划每一项都复现。下面三条不阻塞的说明——其中一条推翻了 round 11 总结中的一个说法,在把该说法当作「已验证」之前值得一读。

PR 的「Tested on」表格中 🐧 Linux 标为 ⚠️本轮就是 Linux(Debian 13、内核 6.12.63、Node v22.22.2),该项可以升级。

环境

在隔离的 git worktree 中检出 750133f4007ce1a5e5e8534e4debbbe298186849,完整执行 npm run build + npm run bundle;以下全部操作驱动的都是生产 dist/cli.js,不是 dev 入口,也不是单测 harness。

  • 模型侧:仓库自带的 integration-tests/fake-openai-server.ts,外面包一层,使每个 agent 请求阻塞在释放文件上。正因如此,「agent A 在飞、B 与 C 排队」是确定状态,而非竞态。
  • UI:tmux new-session -x 130 -y 42 的真实 pty,隔离 QWEN_HOMEQWEN_CODE_ENABLE_WORKFLOWS=1QWEN_CODE_MAX_WORKFLOW_CONCURRENCY=1
  • 被测脚本:parallel([agent A, agent B, agent C]),并发窗口为 1。
  • 每个 provider 请求都带时间戳写入 requests.jsonl,所以**「没有到达 provider」是观测结果,不是推断**。
  • 场景 1–3、/clear A/B 与 pausing 反馈检查各完整跑了两遍,时间线与结论均可复现。

给想重建此 harness 的人两个坑:fake server 必须只按最后一条 user 消息路由;工具要用注册名 workflow 调用(不是显示名 Workflow)。主对话历史把脚本正文嵌在 assistant 的 tool-call 参数里,全文匹配会把主 turn 误判成 agent 请求并卡死在门上。

Reviewer 测试计划

# 测试项 结果
1 agent A 在飞时暂停 → PausingPaused,B、C 仍在队列
2 暂停中的 run 保留在 /workflowsActive 分组;/workflows p <runId> 按 FIFO 恢复;恰好一次完成通知
3 暂停后选择停止而非恢复 → 队列 agent 永不启动、无成功通知、父会话仍可用
4 foreground 保持同步语义;对话框拒绝对 foreground run 暂停 ✅(见说明 C)
5 可感知暂停的 wall-clock watchdog:暂停时间不计费,运行期间 backstop 仍武装 ✅ 再次确认

1 · Running → Pausing → Paused

在 agent A 仍被 provider 侧门挡住时按下 p:状态进入 … Pausing · 0/3 agents,并给出与事实一致的提示「Pause is cooperative; in-flight work may finish before the workflow is paused」;只有在 A 结算之后才到达 ⏸ Paused · 1/3 agents。底部提示与状态严格对应:running 时是 p pause,pausing 时完全不出现 p,paused 后变成 p resume —— 不会让用户请求一个当前状态不接受的转换。

2 · 暂停中的 run 留在 Active,并可从 slash command 恢复

/workflows 把该 run 列在 Active 分组、状态 paused(没有被丢进终态分组);/workflows p <runId>(进程内控制路径,而非对话框按键)成功恢复。

provider 侧时间线是关键证据:暂停期间 77.2 秒内 B、C 的请求数为 0;恢复后 B、C 按脚本顺序依次派发;随后恰好一条 <kind>workflow</kind> 完成通知,携带 {"out":["marker-A-done","marker-B-done","marker-C-done"]} —— 位置对齐的数组在暂停后依然正确。该场景的第一次运行得到同样形状,窗口为 77.0 秒。

3 · 暂停 → 停止

停止暂停中的 run 后记录为 cancelled,B、C 从未到达 provider(全程只有 A 一次 agent 请求),没有任何完成通知,父会话随即正常回答了下一条消息。本报告涉及的所有场景,进程 stderr 均干净——没有来自被取消的 gate waiter 的 unhandledRejection

5 · wall-clock watchdog(因 round 13 重写 sandbox 而重测)

  • 在 25 秒预算下暂停了 52 秒 → run 没有在暂停期间被杀掉,恢复后正常完成。暂停时间确实没有计费。
  • 对照组,同样 25 秒预算、不暂停 → 恰好在 25 秒中止:Workflow execution exceeded 25000 ms of active time (paused time is not counted). backstop 是被挂起,不是被移除。

上次之后的新增内容 —— round 11–13

R12 · 暂停中的 run 不再阻塞 /clear

这是本轮对操作者可见影响最大的行为变化,所以我做了 A/B。

  • running/clear 仍被拒绝:Stop the current session's running background tasks before starting a new session.
  • paused/clear 放行。
  • 暂停中的 run 确实是被中止而非泄漏:之后 20 秒安静期内零 provider 请求、零完成通知,磁盘快照记录 status: "cancelled"abortAll() 先于 reset() 的顺序确实达成了注释所述效果。

我认同这个取舍——暂停中的 run watchdog 被挂起,若它还能阻塞切换,就没有任何 backstop 能把它释放。但这也意味着一次 session 切换会静默取消暂停中的 run,而 PR 描述的 Risk & Scope 一节并未提及。建议在那里补一行。

R12 · Pausing 期间再按 p 会给出反馈,而不是看起来卡住

在 run 仍处于 pausing 时再按一次 p,会亮起 Pause/resume was rejected; the workflow state changed. Try again.,约 3 秒后自动消失。由于 pausing 可能持续一整个子代理调度,这比之前的静默无响应是实质改进。

R11 / R13 · 对注册表已不再持有的 run 执行 /workflows p

session 切换清空注册表后,对一个仍能从快照列出的 run 执行 /workflows p <runId>,返回 Workflow <id> is cancelled and cannot be paused or resumed. —— 而不是过去那句自相矛盾的「Unknown live workflow runId」。两个来源都查不到的 id 仍然返回 Unknown live workflow runIdcompleted 的 run 得到同样的终态文案。两侧行为都与描述一致。

该 head 上的测试与卫生检查

  • packages/core 定向用例:438 通过 / 7 个文件(workflow-dispatch-schedulerworkflow-orchestratorworkflow-runnerworkflow-sandboxworkflow-run-registryworkflow-snapshottools/workflow)—— 上次是 402。
  • packages/cli 定向用例:188 通过 / 6 个文件(workflowsCommandBackgroundTasksDialogBackgroundTasksPilluseBackgroundTaskViewbackgroundWorkUtilsuseBranchCommand)。
  • npm run buildnpm run bundlenpm run typecheck(0 个 TS 错误)、npm run lint 全部通过。(lint 报了 26 条问题,全部位于我自己未纳入版本控制的 harness/ 目录,PR 自身文件零问题。)
  • 被删除的 utils/concurrencyLimiter.tspackages/仍无任何引用

三条不阻塞的说明

A. round 11 为 Promise.all 声称的风险无法复现——该修复改善的是日志分类,而非崩溃安全性

round 11(R10-9)为包装 Promise.all / race / any 给出的理由是:不包装的话,持有失败 dispatch 的即发即弃聚合会「触发进程级 unhandledRejection(交互式下的 CRITICAL 横幅,或 headless 宿主中 Node 默认 --unhandled-rejections=throw 导致的进程终止)」。我对这个说法做了 A/B:只禁用那三个静态方法的包装,重新打包,再跑两类即发即弃形状(裸的拒绝聚合、由聚合派生的 .then()、以及嵌套聚合),用每次运行的 agent 上限来制造确定性的 dispatch 拒绝。

两个构建都没有崩溃。 没有 CRITICAL 横幅,stderr 没有 unhandledRejection,进程都存活。真正变化的是镜像日志行:

脚本 有包装(本 PR) 禁用包装
即发即弃 Promise.all + 对被拒绝 dispatch 调用 .finally() result not consumed + rejection not handled rejection not handled ×2
由拒绝聚合派生的 .then() + 嵌套聚合 rejection not handled + result not consumed rejection not handled ×2

(两种行的完整前缀都是 dispatch failed (…): Workflow exceeded the maximum of N agent() calls per run.;我不声称哪一行对应哪种形状,只陈述各构建产生的分类组合。)

两个构建产生的行数相同。真正的兜底是宿主侧的 adoption-escape hook —— R11-16 的注释本身也提到了它;聚合包装细化的是分类。这本身是合理的改进,我不建议回退。但 round 11 总结夸大了它所消除的风险,任何人都不应把「否则会终止进程」当作已验证的结论。

B. 被终止的暂停 run,其持久化 agent 计数取决于以何种方式终止

同一个脚本、同样的结果——只有 agent A 到达过 provider——但:

暂停 run 的终止方式 实时 /workflows 磁盘快照 重启后 /workflows
Background Tasks 中按 x 3/3 agents dispatched=3 completed=3 3/3 agents
暂停期间 /clear dispatched=3 completed=1 1/3 agents

/clear 路径先 abortAll()reset(),于是正在收敛的 dispatch 调用 onAgentCompleted() 时已找不到 entry,计数停止;对话框路径保留 entry,收敛发生在快照写入之前。在「已结算」与「已执行」两种定义下两个数字都说得通,R11-10(把收敛从 cancelled 扩展到 completed/failed)与 R13(把快照投影冻结在第一个 await 之前)各自也都合理——只是方向相反。可见结果是:只有一个 agent 执行过的 run,会因终止方式不同而持久化为 1/33/3。watchdog 对照组还让我撞上了它的 failed 形态:一个只向 provider 发出过一次 dispatch、在 25 秒被中止的 run,显示为 failed · 3/3 agents

这是我上轮那条「外观性」说明的具体第二副面孔。仍属外观问题、不阻塞——之所以提出,是因为它现在在 completedfailed 的 run 上也能出现,不再只限于 cancelled

C. 「live foreground」下 /workflows p 的文案在 TUI 中几乎不可达

foreground workflow 会在其整个生命周期内占用输入框,因此期间键入的 /workflows p <runId> 会被排队,只有在该 run 已经落为终态之后才提交——而此时 round 13 新增的终态检查会先一步作答,返回终态文案。我在当前 head 确认了终态文案;本轮无法通过 TUI 触达 foreground 文案(我在 fd8a6ed77 上曾观测到它)。该分支仍是正确的防御性代码,且对话框层的 foreground 护栏在本 head 再次确认:foreground run 只提供 x stop、没有 p pause,按 p 无任何效果也不出现闪烁。这里只是说明:它面向用户的那句文案基本停留在理论层面。

本轮未覆盖

真实 provider 认证与网络延迟、跨进程持久恢复、journal durability、逐 agent 控制、macOS 与 Windows。被门挡住的 agent 请求如果暂停达到分钟级会触发 provider 客户端的流式超时;本轮所有暂停窗口都远低于该阈值,因此时间线中没有重试噪声。

wenshao
wenshao previously approved these changes Aug 7, 2026
@wenshao

wenshao commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-ci-bot

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

Copy link
Copy Markdown
Collaborator

Sandboxed verification: ✅ passed — merge-ready (agent verdict) - workflow run

Ran the PR in an isolated, token-free container: A/B against the base build, mock-free harness assertions, targeted gates. Advisory evidence for human reviewers — not a review, an approval, or a CI check.

Scripted assertions: 736 passed · 0 failed · 736 total

中文 — 判定:✅ 通过 · 可合入(agent 判定)

沙箱验证在隔离、无凭证的容器中执行了该 PR 的代码(与 base 构建 A/B 对照、无 mock harness 断言、定向门禁)。仅作为评审证据,不构成评审、批准或 CI 检查

脚本断言:736 通过 · 0 失败 · 736 总计

Verification report

PR #8320 Deep Verification (follow-up round) — feat(workflows): add cooperative pause and resume

Verdict: merge-ready — 736/736 scripted assertions met their expectation, 0 unexpected failures.
Verified head: 750133f4007ce1a5e5e8534e4debbbe298186849 (git rev-parse HEAD^2); A/B base tip: 20b9504276839c06d1bf238fa9a32fab6375ce3d (HEAD^1).

中文摘要
  • 本轮性质:跟进轮。head 自上轮(fd8a6ed77…)推进至 750133f40…(review 第 11–13 轮修复),base 推进至 20b950427…。输入闭包已变,不适用「proven-identical input closure」捷径——所有继承测量均在新 head 上重新执行;旧 head 在 depth-2 浅克隆下不可达(git cat-file 失败),故 delta(R11–R13)以聚合 diff + 代码中的轮次标记定界,逐 commit 归因列入未覆盖。
  • 结论merge-ready。736/736 脚本化断言全部符合预期,无意外失败。
  • A/B 结论:承重门(waitUntilRunning() 链在四条结果路径上:cache 命中、成功臂、错误臂、以及 R12 新增的入口门拒绝 rejectThroughPauseGate;base orchestrator 中 waitUntilRunning 出现 0 次)经证明仍承重:真实编译 scheduler 上 HEAD 25/25;摘门对照组恰好翻转 4 条「暂停期间结果被扣留」族断言(S1/S2 成功形、S18 abort 唤醒前未结算、S19 错误形),其余 21 条(泵停新调度、FIFO 恢复、exactly-once、abort 排空 50 队列、guard rails、listener 语义)两 cell 不变;脚本化差分 11/11。
  • 空转检验:成功臂门 hunk 还原为 (result) => result 后,核心测试 keeps a nested agent result behind the shared pause gate 在其目标断言 L868 expect(settled).toBe(false) 上变红(expected true to be false),未变异为绿;源码已还原,git status 干净。
  • 端到端 wire-oracle:真实 orchestrator + vm sandbox + 计数 dispatch:暂停时 B/C 不进 provider、在飞 A 结果扣门;恢复后 A,B,C FIFO、run 恰好结算一次;停止暂停中的 run 时 B/C 永不启动且 run reject;R12 delta 三 cell(预算门拒绝扣门至 resume、agent 上限拒绝扣门、abort 保持入口门拒绝原错误)全过。5/5。
  • 探针:registry 状态机 33/33(含 R12 hasRunningEntries() 不计 paused、严格转移、终态锁、cancel 无完成通知、exactly-once 上限);nit1 复测 11/11;暂停感知挂钟 watchdog 3/3(W2 为「不暂停必被杀」阳性对照)。
  • 定向门禁:core 受影响 7 文件 438/438、CLI 受影响 5 文件 162/162,全绿(较上轮 +36/+19,即 R11–R13 新增测试)。
  • Findings:无阻塞项。上轮两条 nit 复测后仍然成立(状态表)。另有一条预存在观察(非本 PR 引入):终端铃通知槽对 foreground run 同样触发(base 的 emitNotification 即未门控),模型向 <task-notification> 仍正确门控于 backgrounded——记录为观察,非缺陷。
  • 未覆盖:跨进程持久恢复、journal durability、真实 provider 认证(PR 自述范围外);逐 commit 归因(浅克隆不可达);repo 级 lint/typecheck/bundle(PR CI 覆盖);独立 node 入口加载 orchestrator(预存在循环初始化 TDZ,e2e 沿用 vitest 加载序);Windows/macOS 真实终端与交互式 TUI 会话。

Previous-finding status (follow-up round)

Head moved fd8a6ed77…750133f40… (review-fix rounds 11–13) and base moved too; the input closure changed, so every measurement below was re-run fresh at the new head. The prior head is unreachable locally (depth-2 checkout), so the delta is verified as the aggregate HEAD^1..HEAD diff, with new probes scoped to the round-marked delta (R11 sandbox promise handling, R12 rejectThroughPauseGate + hasRunningEntries, R13 test-only).

# Prior finding Severity Status at this head How re-measured
V Verdict merge-ready, 649/649 stands (re-measured) Central A/B, vacuity, registry, nit1, watchdog, e2e, and both gates re-run fresh → 736/736 (count differs by harness granularity; the load-bearing isolation is the same shape: control flips exactly the gate-dependent assertions).
1 Scheduler state stays in the pause band after abortPending() (never terminal) nit stands (re-measured) Fresh nit1-probe.mjs (11/11) on the real compiled scheduler: pause with in-flight → pausing; abort → no transition; late resolve lands in paused; pause/resume/run all refuse after abort. Harmless by construction: terminal status is the registry's job (re-confirmed in T12/T13: cancel locks cancelled, late complete/fail/onDispatchStateChange are no-ops).
2 p pressed during pausing returns an explicit warning nit (already handled) stands (re-measured) CLI gate 162/162 includes the parameterized ['pausing', 'still pausing', 'warning'] case (workflowsCommand.test.ts L278) and the foreground-rejection case; handler shape unchanged.

No prior finding worsened; none was declined.

Scope selection

Central claim: cooperative pause holds fulfilled/rejected dispatch results at a gate (waitUntilRunning() chained onto all result paths — cache-hit, live-dispatch success, error arm, and, new in the R12 delta, entry-gate rejections via rejectThroughPauseGate) until resume; the pump stops new dispatches while paused; cancellation rejects queued work and wakes gated waiters while preserving exactly-once settlement.

Secondary claims: (1) the registry lifecycle distinguishes running/pausing/paused, allows cancel from any active state, locks terminal state, keeps counters exactly-once, and (R12) hasRunningEntries() excludes paused; (2) /workflows p <runId> and Background Tasks p provide the control and reject foreground / non-interactive / terminal / still-pausing cases clearly.

Budget went to: fresh A/B load-bearing proof with scripted differential (~half), fresh vacuity check, fresh e2e wire-oracle with R12 delta cells, fresh registry/nit1/watchdog probes, fresh targeted gates.

Central claim — A/B load-bearing proof (re-measured fresh)

The gate is WorkflowDispatchScheduler.waitUntilRunning(), chained by the orchestrator onto four result paths: entry-gate rejections (rejectThroughPauseGate, workflow-orchestrator.ts L1360, new in the R12 delta), cache-hit (L1433), live-dispatch success (L1603), and the error arm (L1612) — each resolving/rejecting with the original value whether the gate resolves or rejects (so a cancelled run never surfaces an unhandled rejection). Base has zero occurrences of waitUntilRunning (git show HEAD^1:…workflow-orchestrator.ts | grep -c = 0), so the gate is entirely this PR's. I drove the real compiled scheduler (packages/core/dist/…/workflow-dispatch-scheduler.js) through pause/resume/cancel/abort scenarios via a harness mirroring the orchestrator's exact gate-chain pattern, then ran the identical harness with the gate short-circuited (control = the orchestrator gate hunks reverted, nothing else), and scripted the differential.

cell environment observable oracle result
HEAD (gate active) real compiled scheduler, limit=1, deferred in-flight A, queued B/C, error shapes, 50-job backlog result/error held while paused; FIFO resume; exactly-once; abort drains queue + wakes gate waiter 25/25 pass
CONTROL (gate removed) same scheduler, gate chain short-circuited in the harness same 25 assertions 21/25 — exactly four flips, all in the "held while paused" family
differential (scripted) ab-differential.mjs parses both cells HEAD all green; control flips exactly {S1-held-at-gate, S2-zero-deliveries-while-paused, S18-held-before-abort, S19-error-held-while-paused}; nothing else moves 11/11 pass

The four flips are the load-bearing property: with the gate gone, a fulfilled result is delivered while the run is paused (S1/S2), a held result settles before abort instead of being woken by it (S18), and a rejected dispatch settles during pause instead of being held (S19). Every other assertion — queued B/C never start while paused, FIFO A,B,C resume, exactly-once delivery, abort draining a 50-job backlog with AbortError in <500 ms, pause/resume guard rails, pre-aborted-signal refusal, invalid-limit throw, double-transition listener semantics (pausing then paused) — is unchanged across the two cells: those live in the scheduler's pump/abort logic; "hold results until resume" is contributed specifically by the waitUntilRunning() chaining. Witness: 01-ab-scheduler-head-vs-gateless.png (image keeps the tail — control cell + differential; the head cell's PASS lines are in the raw log); logs 01-scheduler-head.txt, 02-scheduler-gateless.txt, 09-ab-differential.txt.

Vacuity check (the gate hunk pins the central test) — re-measured fresh

Reverted only the live-dispatch success-path gate in workflow-orchestrator.ts ((result) => scheduler.waitUntilRunning().then(() => result, () => result)(result) => result, interface-preserving) and ran the central test keeps a nested agent result behind the shared pause gate:

  • Unmutated: green (1 passed | 132 skipped), log 03-vacuity-green.txt.
  • Mutated: red on the intended assertion — AssertionError: expected true to be false at L868 expect(settled).toBe(false) (the run settles while paused because the result is no longer held), log 04-vacuity-mutated.txt. Source restored afterward; git status --porcelain empty (byte-identical cmp).

Witness: 02-vacuity-gate-hunk.png. The revert breaks the behavior the test exists to catch (not the import/compile/fixture), so the test is non-vacuous and pinned by exactly this hunk.

End-to-end wire-oracle (real orchestrator + vm sandbox + counting dispatch) — re-measured fresh

Drove the real WorkflowOrchestrator (real vm sandbox, real injected scheduler) with a counting, controllable fake dispatch — the seam the orchestrator's constructor is designed to accept — through the Reviewer Test Plan scenario (parallel([() => agent('A'), () => agent('B'), () => agent('C')]) at concurrency 1, abortOnTimeout threaded exactly as the runner does). 5 tests / 51 scripted assertions, all green:

  • E1 pause/resume (18): only A reaches the dispatch initially; after pause() + A settling, state is paused, the dispatch still saw only A, queued=2, and the run is not settled (result held at the gate). After resume(), dispatches arrive A,B,C in FIFO order, the transition band is exactly pausing,paused,running, the run settles exactly once with ['resA','resB','resC'].
  • E2 stop a paused run (11): with B/C queued and A held, controller.abort() → B/C never reach the dispatch, the run rejects (message carries "abort"), settlement count is exactly 1 and rejection-shaped.
  • E3 (R12 delta, 8): a budget-gate rejection issued while paused is held (20 ms tick, no settlement, zero dispatch calls) and delivered on resume with exceeded the token budget.
  • E4 (R12 delta, 9): with QWEN_CODE_MAX_WORKFLOW_AGENTS=1, the cap rejection for the second agent() is held while paused; on resume the first (cap-passing) dispatch fires and the script observes both the cap error and its result.
  • E5 (R12 delta, 5): aborting a held entry-gate rejection preserves the original error through the gate's abort arm (script catches exceeded the token budget), settling exactly once.

Witness: 04-e2e-pause-resume-cancel.png; log 08-e2e-orchestrator.txt.

Registry state-machine probe (secondary claim 1) — re-measured fresh

Drove the real compiled WorkflowRunRegistry (no mocks; handle stubs mirror WorkflowRunHandle's pause/resume/abort shape): 33/33 pass. Witness: 03-registry-state-machine.png; log 07-registry-probe.txt.

  • Transitions are strict: running→pausing→paused→running admitted; the running→paused skip, pausing→running, and paused→pausing are all rejected (T4–T7).
  • Cancel from pausedcancelled, aborting through the attached handle; the entry is then terminal: late complete(), fail(), onDispatchStateChange() are no-ops, and no completion <task-notification> or bell notification fires for cancel (the contract behind Reviewer step 3).
  • Completion counter is exactly-once: 5 onAgentCompleted calls against 2 dispatched cap agentsCompleted at 2 (T16).
  • pause() needs backgrounded + running + attached handle; resume() needs paused; foreground runs cannot be paused; duplicate active register throws; attachHandle is ignored for terminal entries; abortAll() cancels running and paused entries, leaves terminal ones, and fires statusChange exactly once; reset() clears.
  • R12 delta (T18): hasRunningEntries() is false for a lone paused entry, true for running and pausing — the property that keeps a paused-and-forgotten run from blocking /clear and session switch, with the CLI's useBranchCommand/backgroundWorkUtils tests (R12) pinning the consumer side inside the gates.

nit1 probe (prior finding 1) + pause-aware wall-clock watchdog — re-measured fresh

nit1-probe.mjs on the real compiled scheduler (11/11): after abortPending() the scheduler never self-terminates — state rests in the pause band (pausing when in-flight never drained, paused when it had), the state listener sees no post-abort transition, and a late in-flight resolve keeps the state in the pause band; pause()/resume()/run() all refuse after abort and waitUntilRunning() rejects with AbortError. Harmless: terminal status is the registry's job (T12/T13 above).

watchdog-probe.mjs on the real compiled createWorkflowSandbox (3/3): with a 150 ms maxWallClockMs cap, a run that parks 400 ms in paused completes with done:ok (W1 — paused time neither burns budget nor kills the run), while the identical shape without pausing is killed by the cap with exceeded … active time (paused time is not counted) (W2 — the positive control proving W1's green is the suspension, not a disabled watchdog). Logs 10-nit1-probe.txt, 11-watchdog-probe.txt; witness 06-nit1-watchdog-probes.png.

Targeted gates (affected workspaces) — re-measured fresh

workspace files result
core workflow-dispatch-scheduler, workflow-orchestrator, workflow-runner, workflow-sandbox, workflow-run-registry, workflow-snapshot, tools/workflow/workflow 7 files, 438/438 pass
cli workflowsCommand, BackgroundTasksDialog, BackgroundTasksPill, useBackgroundTaskView, backgroundWorkUtils 5 files, 162/162 pass

Witness: 05-targeted-gates-core-cli.png; logs 05-core-gate.txt, 06-cli-gate.txt. Counts rose vs the prior round (402 → 438 core, 143 → 162 cli) — the delta is the R11–R13 test additions (e.g. the sandbox R11 promise-adoption suite, the R12 entry-gate and hasRunningEntries tests), all green inside these gates. The gate is live: the vacuity check above is the positive control (the central test goes red when the gate hunk is reverted). The deleted concurrencyLimiter module leaves no remaining references in packages/, scripts/, or integration-tests/.

Corrections

None. The prior report's mechanism description (gate = waitUntilRunning() chained on the result paths; terminal lockout in the registry suppresses the stop notification) was re-verified against the new head's source and stands; the R12 delta adds a fourth gated path (rejectThroughPauseGate) and the hasRunningEntries exclusion, both now covered above.

Findings

No blocking findings. The two carried-forward nits (status table rows 1–2) remain the only PR-attributable observations, both non-blocking and judged intended behavior.

One pre-existing, non-PR observation (not a finding): the terminal-bell notification slot (emitNotification) fires for foreground runs too — emitNotification is ungated at HEAD^1 as well (only the model-facing emitCompletion checks isBackgrounded), so this predates the PR; my initial probe assertion encoding the opposite was a harness assumption error, corrected in the final 33/33 registry run.

Not covered

  • Durable cross-process resume, journal durability, real provider auth/network, per-agent controls — explicitly out of scope per the PR description; not probed.
  • Per-commit attribution — the CI checkout is depth 2 (merge commit + base tip + PR head only); git rev-parse --is-shallow-repository = true, git cat-file -t fd8a6ed77… (prior head) fails, and the metadata snapshot lists 31 commits while only 1 is locally reachable. Claims were verified against the aggregate HEAD^1..HEAD diff; the delta since the prior round (rounds 11–13) is scoped via the round markers in source (R11 sandbox, R12 orchestrator/registry) and covered by the fresh gates + probes, not attributed per commit.
  • Repo-wide lint / typecheck / bundle — covered by the PR's own CI; not re-run. My A/B used the pre-built dist (timestamps precede this round) and vitest-on-source.
  • Standalone node execution of the orchestrator harness — importing the orchestrator as an entry module trips a pre-existing circular-init TDZ (carried forward as environmental from the prior round); the e2e wire-oracle therefore ran under the vitest load order via a temporary in-package test file, removed afterward (git status clean).
  • Windows / macOS real-terminal pause/resume and interactive TUI sessions — the PR marks these ⚠️ (untested by the author); this Linux container ran the unit/integration layers, not a TUI session.

Methodology

Environment: CI verify container (node:22-bookworm, Node v22.23.2), working tree at refs/pull/8320/merge, npm ci + npm run build pre-completed at HEAD. Follow-up round with a changed head and base: every measurement re-run fresh; the prior head is unreachable locally so the delta is the aggregate diff. All harnesses are mock-free with respect to the unit under test — they import the real compiled dist modules (scheduler, registry, sandbox) or drive the real orchestrator + vm sandbox through the dispatch seam (the only fake is the dispatch, the seam the orchestrator is designed to accept). The A/B control differs from HEAD by nothing else than the gate chain (short-circuited in the harness; reverted hunk in the source for the vacuity check). Raw per-cell logs live in logs/ (01-scheduler-head.txt, 02-scheduler-gateless.txt, 03-vacuity-green.txt, 04-vacuity-mutated.txt, 05-core-gate.txt, 06-cli-gate.txt, 07-registry-probe.txt, 08-e2e-orchestrator.txt, 09-ab-differential.txt, 10-nit1-probe.txt, 11-watchdog-probe.txt); rerunnable harnesses are scheduler-harness.mjs, ab-differential.mjs, registry-harness.mjs, nit1-probe.mjs, watchdog-probe.mjs. Assertion tally: scheduler HEAD 25 + differential 11 + vacuity 2 + e2e 51 + registry 33 + nit1 11 + watchdog 3 + gates 600 (438 core + 162 cli) = 736, all met expectation; the control cell's four reds are the predicted base-arm failures, scripted as passes via ab-differential.mjs, so unexpected fail = 0.

Evidence images

01-ab-scheduler-head-vs-gateless

02-vacuity-gate-hunk

03-registry-state-machine

04-e2e-pause-resume-cancel

05-targeted-gates-core-cli

06-nit1-watchdog-probes

Harness scripts and raw logs are in the workflow run artifacts (7-day retention).

Qwen Code · sandboxed verification

@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Triage re-run completed without a new review.

⚠️ The bot has neither a verdict nor a deferral on 750133f4007ce1a5e5e8534e4debbbe298186849 — no APPROVED, CHANGES_REQUESTED, or COMMENTED review of its own. A DISMISSED one does not count: dismiss_stale_reviews voids the bot's approval on every push, which is exactly when a fresh one is needed. If this re-run was meant to review or approve, it did not, and an approval left by another account is a separate vote that does not count as the bot's own.

⚠️ 机器人在 750133f4007ce1a5e5e8534e4debbbe298186849既没有裁决也没有 defer —— 没有属于它自己的 APPROVEDCHANGES_REQUESTEDCOMMENTED 评审。DISMISSED 不算:dismiss_stale_reviews 会在每次推送时作废机器人的批准,而那恰恰是需要一次新批准的时刻。如果这次重跑本应评审或批准,那么它没有做到;而其他账号留下的批准是另一张票,不能算作机器人自己的。

The stage comments above were updated with the latest result. View workflow run.

上方各阶段评论已更新为最新结果。查看工作流运行

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Round 14 — response to the maintainer re-verification at 750133f4

Thank you for the deep Linux re-verification and the merge-ready verdict. One comment-only commit this round (fix(workflows): address round-14 review findings (#8320)); every other point is dispositioned below.

Changed

R14-A (Note A) — corrected the Promise.all/race/any wrap rationale in the code itself. Your A/B disproved the round-11 claim that an unwrapped fire-and-forget aggregate "fires a process-level unhandledRejection (the interactive CRITICAL banner, or Node's default --unhandled-rejections=throw termination)". That claim was still living in the comment above the wrappers in workflow-sandbox.ts, so the comment now states the verified picture instead: while the run is live, the host-side adoption-escape hook (R11-3) is the real backstop — it catches and mirrors the escape, but only with the coarse (rejection not handled) wording; the wrap gives each aggregate its own observer so the run-log mirror itself classifies the rejection, and once the run's hook is detached nothing in the sandbox catches the escape (no crash outcome asserted — per your caveat, that was never verified). Comment-only change; per your verdict the wrappers themselves stay. Note the original overstatement lives in the round-11 PR comment, which this bot cannot edit — the repo-side record is now correct.

Agreed, but needs a maintainer's hand: PR-description edits

This bot mode holds no GitHub credentials and has no path to edit the PR body, so both of your description suggestions need a quick maintainer edit:

  1. "Tested on" table — upgrade the 🐧 Linux cell from ⚠️ to ✅ (your round-14 run: Debian 13, kernel 6.12.63, Node v22.22.2).
  2. Risk & Scope — add that a session switch silently cancels a paused run. Suggested one line:

    Switching sessions (/clear or starting a new session) cancels any paused workflow run: a paused run has its watchdog suspended, so nothing else could ever release it — it cannot keep blocking the switch. The run is recorded as cancelled, its snapshot persisted, and queued agents never start.

Declined (reasons recorded)

Note B — persisted agent counter depends on how the paused run was terminated (3/3 via Background Tasks x vs 1/3 via /clear, and the failed · 3/3 watchdog face). Cosmetic per your own classification, on a merge-ready verdict. The divergence is two deliberate prior-round fixes pulling in opposite directions: R11-10 removed the status gate from onAgentCompleted so in-flight dispatches drain into the live counter for every terminal status (fixing completed/failed runs frozen at 3/5), while R13 freezes the snapshot projection before the first await (fixing snapshots frozen at an fs-timing-dependent point mid-drain) — and on the /clear path reset() drops the entry before the draining onAgentCompleted() calls can land. Unifying the persisted number means choosing between "settled" and "executed" semantics — a product call — and rolling back either fix would resurrect the bug it fixed. That trade is not worth it on a PR this heavily verified; it belongs in a follow-up issue that settles the counting semantics for all terminal statuses in one deliberate change.

Note C — the live-foreground /workflows p wording is effectively unreachable from the TUI. Informational; you confirmed the branch is still correct defensive code and the dialog-level foreground gate re-verified. Keeping the wording: it still guards non-TUI control paths and costs nothing — removing it would be churn for a string that is merely quiet, not wrong.

Note A, the wrap itself — kept, per your explicit "not asking for it to be reverted".

Verification

  • npm run build — passed
  • npm run typecheck — passed (0 TS errors)
  • npm run lint — passed (0 problems)
  • packages/core focused Vitest, 7 workflow files (workflow-sandbox, workflow-dispatch-scheduler, workflow-orchestrator, workflow-runner, workflow-run-registry, workflow-snapshot, tools/workflow) — 438/438 passed, identical to the maintainer's round-14 baseline at this head
  • No settings source changed → npm run generate:settings-schema not applicable
  • Comment-only diff, no bundled-CLI-only behavior → no integration run needed
  • Base-conflict: --conflict false, no merge performed
中文说明

第 14 轮 —— 对维护者在 750133f4 上复验的回应

感谢这次深入的 Linux 复验以及「可以合并」的结论。本轮只有一个纯注释提交(fix(workflows): address round-14 review findings (#8320)),其余各点逐条处理如下。

已改动

R14-A(说明 A)—— 在代码中纠正了 Promise.all/race/any 包装的动机描述。 你的 A/B 推翻了 round 11 的说法:未包装的即发即弃聚合并不会「触发进程级 unhandledRejection(交互式 CRITICAL 横幅,或 headless 宿主上 Node 默认 --unhandled-rejections=throw 的进程终止)」。该说法仍留在 workflow-sandbox.ts 中包装方法上方的注释里,现已改为与验证结果一致的表述:run 存活期间,真正的兜底是宿主侧的 adoption-escape hook(R11-3)——它会捕获并镜像该逃逸,但只给出粗粒度的 (rejection not handled) 文案;包装让每个聚合拥有自己的 observer,使 run 日志镜像本身能对拒绝做分类;而在 run 结束、hook 被移除之后,沙箱内不再有任何东西捕获该逃逸(不再断言崩溃后果——按你的提醒,这一点从未被验证过)。纯注释改动;按你的结论,包装本身保留。注意:原先夸大的说法出自 round 11 的 PR 评论,本 bot 无法编辑该评论——仓库内的记录现在是准确的。

认同、但需要维护者动手:PR 描述修改

本 bot 模式没有 GitHub 凭据,也没有修改 PR 正文的通道,因此你对描述的两处建议需要维护者快速手动修改:

  1. 「Tested on」表格 —— 将 🐧 Linux 一栏从 ⚠️ 升级为 ✅(你的第 14 轮验证环境:Debian 13、内核 6.12.63、Node v22.22.2)。
  2. Risk & Scope —— 补充「session 切换会静默取消暂停中的 run」。建议措辞:

    切换 session(/clear 或开启新会话)会取消所有暂停中的 workflow run:暂停中的 run 其 watchdog 已被挂起,没有任何其他机制能释放它,因此它不能继续阻塞切换。该 run 会被记录为 cancelled,快照落盘,排队中的 agent 永不启动。

已拒绝(附理由)

说明 B —— 持久化的 agent 计数取决于暂停 run 的终止方式(Background Tasks 中按 x 得到 3/3/clear 得到 1/3,以及 watchdog 对照组中的 failed · 3/3 形态)。按你自己的分类属外观问题,且 PR 已是 merge-ready。该分歧源于前两轮两个刻意的修复方向相反:R11-10 移除了 onAgentCompleted 的状态门,让在飞 dispatch 在所有终态下都能收敛进实时计数(修复了 completed/failed run 永远停在 3/5 的问题);而 R13 把快照投影冻结在第一个 await 之前(修复了快照冻结时机依赖 fs 时序的问题)——/clear 路径中 reset() 会在收敛中的 onAgentCompleted() 调用落地之前删掉 entry。统一持久化数字意味着在「已结算」与「已执行」两种语义之间做选择——这是产品决策——而回退任一修复都会让当年修的 bug 复活。在一个验证得如此充分的 PR 上,这笔交易不划算;应作为后续 issue 处理,在一次刻意的改动中为所有终态确定计数语义。

说明 C —— 「live foreground」下 /workflows p 的文案在 TUI 中几乎不可达。 信息性说明;你已确认该分支仍是正确的防御性代码,对话框层的 foreground 护栏也再次验证通过。文案保留:它仍然守护非 TUI 控制路径,且零成本——删掉它只是对一句「只是安静、并非错误」的文案做无谓改动。

说明 A 中的包装本身 —— 保留,按你明确的「不建议回退」。

验证

  • npm run build —— 通过
  • npm run typecheck —— 通过(0 个 TS 错误)
  • npm run lint —— 通过(0 个问题)
  • packages/core 定向 Vitest,7 个 workflow 文件(workflow-sandboxworkflow-dispatch-schedulerworkflow-orchestratorworkflow-runnerworkflow-run-registryworkflow-snapshottools/workflow)—— 438/438 通过,与维护者在该 head 上的第 14 轮基线完全一致
  • 未改动 settings 源 → 无需 npm run generate:settings-schema
  • 纯注释 diff,不涉及仅经 bundled CLI 暴露的行为 → 无需集成测试
  • 基分支冲突:--conflict false,未执行任何合并

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

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


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

wenshao
wenshao previously approved these changes Aug 8, 2026

@doudouOUC doudouOUC left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Not approving — escalating to a maintainer. Reviewed at 444b52bb49. This is a review comment, not a block: I found no Critical regression, but this PR is outside what I can sign off on under this repo's own gate, and the reason is scale, not a specific defect.

Why I am not casting an approval

AGENTS.md puts core modules (packages/core/src/**, cross-package changes) behind a two-tier gate for non-maintainer PRs. Small-scope changes may be gated, but only at 100% confidence — any doubt escalates. This PR is +5087/−525 across 42 files, roughly 1200 lines of production logic in packages/core (excluding tests), spanning workflow-sandbox, workflow-orchestrator, workflow-runner, workflow-dispatch-scheduler, workflow-run-registry, workflow-snapshot, plus CLI UI and web-shell. It is feat-type, so not hard-blocked on size — but it crosses the 1000-line advisory threshold and is on review round 14 with 9 Critical and 91 Suggestion threads still open. I sampled and verified the 9 Criticals and a handful of Suggestions; I did not audit 5000 lines line-by-line, so I cannot honestly claim the 100% bar. Handing this to the maintainer is the correct outcome, and per AGENTS.md a large core feat escalates for awareness regardless.

What I did verify — 8 of the 9 Criticals are genuinely fixed

None of the 9 has an author reply, so I checked each against the code at this head rather than the thread flags:

Thread Verdict at 444b52bb49
workflow-runner.ts:209 (R11-1) — settlement guard only special-cased 'cancelled', so an externally-failed entry could settle ok: true fixed — now isTerminalWorkflowStatus(entry.status)ok: false with the entry's message, exactly the suggested widening
workflow-sandbox.ts:627 (R11-2) — pause-banking mixed Date.now() with a monotonic setTimeout deadline fixed — both armedAt and the banking subtraction use performance.now(), with a comment naming the divergence
workflow-orchestrator.ts:1352 — pause did not suspend the wall-clock watchdog, so a paused run died at the cap and could never resume fixedWallClockWatchdog now clears the timer and banks the remainder on pause, re-arms on resume
workflow-sandbox.ts:1256 — cancelling a paused run whose script hangs in ungated code orphaned it permanently addressed — an abort listener re-arms the suspended watchdog, so the Promise.race settles via the banked remainder. Now bounded rather than permanent; see the note below
workflow-sandbox.ts:914 (R8-7) — derived .then() chains got no rejection observer fixed — probe: 0 process-level escapes, failure mirrored as dispatch failed (rejection not handled)
workflow-sandbox.ts:978 (R10) — teardown suppression keyed only on AbortError, but in-flight cancel rejects with a plain Error fixed — suppression is now `readFlag(err,'__wfAbort')
workflow-orchestrator.ts:1607 / :1619 — pause-gate success/cached/error arms turned a cancelled run into an unobserved rejection fixed — probe: the teardown AbortError shape now yields 0 escapes and logs nothing, which is the intended contract

The 9th (R11-3) partially stands — but it is not a regression

await / Promise.resolve adoption of an ObservedPromise still lets the rejection reach Node as a process-level unhandledRejection. Probed at this head with a failing dispatch:

Script shape this PR main (merge-base 650e085f)
async function step(){ await agent('x'); } step(); unhandled=1, logged unhandled=1, no log
[1,2].map(async i => { await agent('x'+i); }) unhandled=2, logged ×2 unhandled=2, no log
Promise.resolve(agent('x')) unhandled=1, logged unhandled=1, no log
bare agent('x') (control) unhandled=0, logged unhandled=1, no log
agent('x').then(v => …) unhandled=0, logged unhandled=1, no log
plain-Error CANCELLED unhandled=0 unhandled=1, no log
teardown AbortError unhandled=0, silent unhandled=1, no log

The A/B is what matters here: main escapes on all seven shapes and logs nothing on any of them. This PR takes four shapes to zero escapes and, critically, gives every shape a run-log entry. The three await-adoption shapes still escape — so R11-3's "the rejection reaches Node" half is real — but its "leaves no log, alarm, or telemetry" half is fixed, and the escape itself is pre-existing behaviour that this PR narrows rather than introduces. Treating it as a blocker on this PR would be wrong; it belongs in a follow-up. (These shapes are also a genuine forgotten-await bug in the user's workflow script, which makes the product-level "CRITICAL … file a bug report" banner the more objectionable part than the rejection itself.)

Other evidence

  • Tests: all 7 core workflow suites pass at this head — workflow-sandbox 148, workflow-orchestrator 133, workflow-run-registry 76, workflow.test 39, workflow-runner 15, workflow-snapshot 14, workflow-dispatch-scheduler 13 = 438 passed, 0 failed. CI's ubuntu leg is green on 444b52bb49.
  • i18n: the 9 new keys are present in all 9 locale files.
  • concurrencyLimiter.ts and its test are deleted (−111/−117), replaced by workflow-dispatch-scheduler.ts (+159) — a real consumer swap, not dead code left behind.

Suggestion for how to land this

The blocker is process, not correctness: 91 open Suggestions at round 14 is well past the ~5-round guidance in AGENTS.md, which says to land only Critical fixes at this point and defer the rest. I would ask the maintainer to (a) take the sign-off on the core surface, and (b) triage the 91 Suggestions into "must-fix" versus a follow-up issue, so the diff stops widening. Every further round has been adding production lines to a change that already crossed the advisory threshold.

中文说明

不予批准 —— 上交维护者决策。 审查提交 444b52bb49。这是一条评审意见而非阻断:我没有发现 Critical 回归,但本 PR 超出了我在本仓库自身门禁下可以签署的范围,原因是规模而非某个具体缺陷。

为何不投批准票AGENTS.md 对非维护者的 core 改动设有两级门禁,小范围改动「必须 100% 确信,任何疑虑即上交」。本 PR 为 +5087/−525、42 文件,packages/core 中约 1200 行生产逻辑(不含测试),横跨 sandbox/orchestrator/runner/dispatch-scheduler/run-registry/snapshot 以及 CLI UI 与 web-shell;类型为 feat 故不因体积硬阻断,但已越过 1000 行提示线,且处于第 14 轮评审、仍有 9 个 Critical 与 91 个 Suggestion 未解决。我抽样核验了 9 个 Critical 与部分 Suggestion,但未逐行审计 5000 行,因此无法诚实地宣称达到 100% 标准。按 AGENTS.md,大型 core feat 本身也应上交维护者。

已核验:9 个 Critical 中 8 个确已修复(全部无作者回复,故按代码而非线程标记判定):R11-1 结算守卫已改为 isTerminalWorkflowStatus;R11-2 已全部改用 performance.now();暂停未挂起 wall-clock 看门狗已修(暂停清零并寄存余量、恢复时重新装载);「取消已暂停且脚本悬挂的运行会永久孤立」已改为在 abort 时重新装载看门狗,从永久变为有界;R8-7 派生 .then 链已修(探针 0 次逃逸并写入日志);R10 已加入 isRunAborted() 析取覆盖纯 Error 取消路径;暂停闩锁的成功/缓存/错误三臂已修(teardown AbortError 探针 0 逃逸且不写日志)。

第 9 个(R11-3)部分成立,但不是回归await / Promise.resolve 采纳 ObservedPromise 时,拒绝仍会以进程级 unhandledRejection 逃逸。关键是 A/B 对照:main 上全部 7 种形态都逃逸且都不写日志;本 PR 将其中 4 种降为 0 逃逸,并让全部 7 种都写入运行日志。因此「无日志、无告警、无遥测」这一半已修复,「拒绝到达 Node」这一半是本 PR 收窄而非引入的既有行为——把它当作本 PR 的阻断项并不恰当,应转为后续跟进。

其他证据:本 head 上 7 个 core workflow 套件 438 全部通过;CI ubuntu 绿;9 个新 i18n key 在 9 个语言包中齐全;concurrencyLimiter 及其测试被 workflow-dispatch-scheduler 真实替换,未留死代码。

落地建议:瓶颈在流程而非正确性。第 14 轮仍有 91 条未解决 Suggestion,已远超 AGENTS.md 的约 5 轮指引(此后只应合入 Critical 修复)。建议由维护者(a)承接 core 面的签署,(b)把 91 条 Suggestion 分为「必修」与「后续 issue」,以止住 diff 继续扩张——每一轮都在给一个已越过提示线的改动继续添加生产代码。

@yiliang114

Copy link
Copy Markdown
Collaborator

Review: the cooperative pause/resume state machine is well-constructed and well-tested — scheduler states running->pausing->paused->running with strict one-step guards, registry mirrors transitions with explicit validation (running->paused skip rejected, terminal entries ignore late events), pause() gated to background+running, resume() gated to paused, both refuse after abort. Pause never interrupts mid-tool-call (dispatch-boundary pause; in-flight dispatches run to completion under 'pausing', queued ones never start, completed results held behind waitUntilRunning gates until resume), and resume pumps the untouched queue without re-running completed work (journal started-ids assigned before dequeue, results appended before the gate opens). All race pairs are test-pinned (double-pause, resume-before-paused, pause-then-natural-finish, cancel-during-pause), all 9 locales carry the 5 new keys, docs match. No P0/P1. Holding approval for two P2s worth addressing before merge:

P2 — paused runs are silently cancelled by /clear, /branch, and session switch. paused is deliberately excluded from hasRunningEntries() (so it doesn't block switching), and session switch calls abortAll() before reset() — so a user who pauses a long workflow then clears/switches sessions loses the run, with only a cancelled snapshot as trace and nothing in the normal /clear confirm mentioning it. Suggest naming paused runs in the blocking-gate confirmation, or adding a line to the dialog's Paused explainer ('session switch cancels paused runs').

P2 — pause parked on a pending tool approval can burn the budget and die. The watchdog suspends only on 'paused'. If pause lands while an in-flight dispatch is parked waiting on tool approval, state stays 'pausing', the wall-clock budget keeps burning (default 30 min), and resume() returns false from 'pausing' — the run dies with a timeout while the UI shows 'Pausing', and the user can't rescue it except by cancelling. Acknowledged in source comments, but it's a real trap: consider suspending on 'pausing' when the entry has pendingApprovals (or surfacing remaining budget), and at minimum note it in the Pausing explainer, not just code comments.

P3s: 'p' on a 'pausing' row routes to pause() which always refuses and flashes 'state changed — Try again', which can never succeed until paused (distinct wording suggested); the ~10 new dynamic p-branch messages are raw English not t()-wrapped (matches the file's existing dynamic-runId pattern, but the 5 new static keys ARE localized — optionally wrap static portions); the PR bundles three features (pause/resume, a watchdog wall-clock->active-time rewrite with a user-visible timeout-message change, and an unconsumed-rejection mirror + nested-log merge) — splitting would ease review/revert and the active-time semantics change deserves a release note; the 3s rejection flash can overwrite the two-step 'x again to confirm stop' hint if both fire on one row.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Round 15 summary — PR #8320

Addressed the two P2 findings from @yiliang114's review (approval held on
both). @doudouOUC's review was a process escalation with no code change
requested. Commit: e41fb76623 on codex/issue-8105-workflow-pause-resume.

@yiliang114 — P2: paused runs are silently cancelled by /clear, /branch, and session switch — FIXED

Added one sentence to the dialog's Paused explainer, per the reviewer's
option (b): "/clear, /branch, and switching sessions cancel paused runs."
The statement is accurate: paused is an active status, so
WorkflowRunRegistry.abortAll() cancels paused entries, and all three paths
(/clear, /branch, /resume session switch) call
resetBackgroundStateForSessionSwitch() which aborts before resetting.
Option (a) ("name paused runs in the blocking-gate confirmation") was not
viable: these commands have no confirmation dialog — the gate is a hard-block
error that only fires for running/pausing runs — and making paused runs
block the switch would resurrect the exact "paused-and-forgotten run blocks
/clear forever" problem that round 12 deliberately removed. The Paused
explainer is the surface where the paused state is already explained.

@yiliang114 — P2: pause parked on a pending tool approval burns the budget and dies — FIXED (reviewer's "at minimum")

The Pausing explainer now surfaces the trap at the moment the user sees
the Pausing status: "An agent call waiting on a tool approval keeps the run
in this state and still counts against the active-time limit until the
approval is answered."
Declined, with evidence, the two stronger variants:

  • Suspend the watchdog on pausing when approvals are pending: with a
    concurrency window > 1, another in-flight dispatch can be genuinely
    executing while one is parked on an approval; suspending the watchdog would
    remove the 0-token-hang backstop for exactly the window it exists to cover.
    A correct version needs dynamic re-arm on every approval resolution, plumbed
    from the registry into both the top-level and nested sandboxes' watchdogs —
    new race surface in a PR this late, for a trap that is now user-visible. The
    tradeoff stays documented in the SandboxOptions.scheduler docs.
  • Surface remaining budget: requires plumbing watchdog remaining time into
    the registry/UI; the explainer already warns proactively at the point where
    the user is looking.

@yiliang114 — P3 items — DEFERRED (recorded, not dropped)

Past the ~5-round guidance in AGENTS.md (land Critical fixes, defer the
rest); none of these is a correctness defect:

  1. Distinct wording for p pressed on a pausing row: the refusal is correct
    (pause only transitions runningpausing; the run is already pausing).
    The /workflows p <runId> command path already emits a dedicated
    "still pausing" warning; aligning the dialog flash needs a new i18n key in
    9 locales. Follow-up.
  2. t()-wrapping the ~10 dynamic p-branch messages: matches the file's
    existing dynamic-runId pattern (acknowledged in the review). Follow-up.
  3. Splitting the PR into its three features: a maintainer/process judgment,
    not one this bot should make — left with the maintainer alongside the
    escalation below; splitting at round 15 would be pure churn.
  4. The 3s rejection flash can overwrite the two-step stop hint on one row:
    UI polish. Follow-up.

@doudouOUC — COMMENTED review (escalation to maintainer) — NO CODE CHANGE

The review verified 8 of 9 Criticals fixed at the previous head and explicitly
recommends the 9th (R11-3, await-adoption unhandled rejections) "belongs in
a follow-up", since main escapes on all seven probe shapes with no logging
while this PR narrows the escapes and adds run-log entries on every shape.
Agreed — no change this round. The sign-off/triage-of-open-suggestions request
is a maintainer process step, echoed by the deferrals above.

Files changed

  • packages/cli/src/ui/components/background-view/BackgroundTasksDialog.tsx — extended the Pausing and Paused explainers
  • packages/cli/src/i18n/locales/{ca,de,en,fr,ja,pt,ru,zh,zh-TW}.js — updated both keys in all 9 locales
  • packages/cli/src/ui/components/background-view/BackgroundTasksDialog.test.tsx — pinned the two new warning sentences

No conflict (--conflict false); no merge performed.

Verification

Commands actually run on the final tree (commit e41fb76623):

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • npx prettier --check (the 11 changed files) — passed
  • npm run check-i18n — passed (key parity across all 9 locales intact)
  • vitest src/ui/components/background-view/BackgroundTasksDialog.test.tsx (packages/cli) — 83 passed
  • vitest src/ui/components/background-view/ (packages/cli) — 178 passed
  • vitest src/i18n/ (packages/cli) — 31 passed
  • Integration tests after npm run bundle — not applicable: the change is dialog copy covered by the component tests above, not behavior exercised only through the bundled CLI
  • npm run generate:settings-schema — not applicable: no settings source changed
中文说明

第 15 轮总结 — PR #8320

已处理 @yiliang114 评审中的两个 P2 问题(其批准以这两项为前提)。@doudouOUC 的评审属于流程性上交,未要求代码改动。提交:codex/issue-8105-workflow-pause-resume 分支上的 e41fb76623

@yiliang114 — P2:已暂停(paused)的运行会被 /clear、/branch 和会话切换静默取消 — 已修复

按评审者给出的方案 (b),在对话框的 Paused 说明文案中新增一句:"/clear、/branch 以及切换会话会取消已暂停的运行。"该表述准确无误:paused 属于活跃状态,因此 WorkflowRunRegistry.abortAll() 会取消已暂停的条目,且上述三条路径(/clear/branch/resume 会话切换)都会调用 resetBackgroundStateForSessionSwitch(),先 abort 再 reset。方案 (a)("在阻断门确认中点名已暂停的运行")不可行:这些命令没有确认对话框——阻断门是一个仅对 running/pausing 运行生效的硬阻断错误——而且让已暂停的运行阻断切换会重新引入第 12 轮刻意移除的"暂停后被遗忘的运行永远阻塞 /clear"问题。Paused 说明文案正是解释暂停状态的位置。

@yiliang114 — P2:暂停时卡在待处理工具审批上会耗尽时长预算并导致运行死亡 — 已修复(采用评审者的"至少"方案)

Pausing 说明文案现在会在用户看到 Pausing 状态时直接揭示该陷阱:"等待工具审批的 agent 调用会让运行保持在此状态,且在审批得到响应前仍会计入活跃时间上限。"以下两个更强的方案经评估后拒绝,附证据:

  • 在有挂起审批时于 pausing 状态挂起看门狗:当并发窗口大于 1 时,可能另一个在途派发正在真实执行、而其中一个卡在审批上;此时挂起看门狗会恰好在其存在的意义(0-token 死循环兜底)所覆盖的窗口内移除兜底。正确的实现需要在每次审批解决时动态重新装载,并从 registry 接线到顶层与嵌套两个沙箱的看门狗——在如此后期的 PR 中为一个现已对用户可见的陷阱引入新的竞态面,不值得。该权衡保留在 SandboxOptions.scheduler 的文档注释中。
  • 展示剩余预算:需要把看门狗剩余时间接入 registry/UI;说明文案已在用户注视的位置提前给出警告。

@yiliang114 — P3 各项 — 延后处理(记录在案,不会丢弃)

已超过 AGENTS.md 的约 5 轮指引(此后仅合入 Critical 修复,其余延后);以下均非正确性缺陷:

  1. pausing 行按 p 时使用不同措辞:拒绝行为本身是正确的(pause 仅允许 runningpausing 转换;该运行本就在暂停中)。/workflows p <runId> 命令路径已有专门的"仍在暂停中"警告;对齐对话框闪烁提示需要在 9 个语言包中新增 i18n key。转入后续跟进。
  2. 将约 10 条动态 p 分支消息包进 t():与文件既有的动态 runId 模式一致(评审中也已确认)。转入后续跟进。
  3. 将 PR 拆分为三个功能:属于维护者/流程判断,不应由本机器人决定——与下面的上交一并留给维护者;在第 15 轮拆分只会带来纯粹的反复劳动。
  4. 3 秒的拒绝闪烁可能覆盖同一行上的两步停止提示:UI 细节打磨。转入后续跟进。

@doudouOUC — COMMENTED 评审(上交维护者) — 无代码改动

该评审核验了上一个 head 上 9 个 Critical 中的 8 个已修复,并明确建议第 9 个(R11-3,await 采纳导致的未处理拒绝)"转入后续跟进"——因为 main 在全部七种探针形态上都会逃逸且无任何日志,而本 PR 收窄了逃逸并在每种形态上都写入了运行日志。同意——本轮不做改动。签署/分流未解决建议的请求属于维护者流程步骤,与上述延后项一并转达。

变更文件

  • packages/cli/src/ui/components/background-view/BackgroundTasksDialog.tsx — 扩展 Pausing 与 Paused 说明文案
  • packages/cli/src/i18n/locales/{ca,de,en,fr,ja,pt,ru,zh,zh-TW}.js — 在全部 9 个语言包中更新两个 key
  • packages/cli/src/ui/components/background-view/BackgroundTasksDialog.test.tsx — 固化(pin)两条新警告语句

无冲突(--conflict false);未执行合并。

验证

在最终代码树(提交 e41fb76623)上实际执行的命令:

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • npx prettier --check(11 个变更文件) — 通过
  • npm run check-i18n — 通过(9 个语言包的 key 一致性保持完好)
  • vitest src/ui/components/background-view/BackgroundTasksDialog.test.tsx(packages/cli) — 83 通过
  • vitest src/ui/components/background-view/(packages/cli) — 178 通过
  • vitest src/i18n/(packages/cli) — 31 通过
  • npm run bundle 后的集成测试 — 不适用:本次变更为对话框文案,已由上述组件测试覆盖,并非仅能经由打包后的 CLI 验证的行为
  • npm run generate:settings-schema — 不适用:未改动任何 settings 源文件

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

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


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

@yiliang114 yiliang114 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approving per maintainer — the P2s below are accepted as non-blocking follow-ups. The cooperative pause/resume core is solid: running->pausing->paused->running with strict one-step guards, registry transition validation, pause gated to background+running / resume gated to paused / both refuse after abort, pause never interrupts mid-tool-call (in-flight dispatches finish under 'pausing', queued never start, completed results held behind gates until resume), and resume doesn't re-run completed work. Race pairs all test-pinned, all 9 locales carry the new keys. Follow-ups (non-blocking): (1) paused runs are silently cancelled by /clear//branch/session switch — worth naming them in the blocking-gate confirm or the Paused explainer; (2) pause parked on a pending tool approval stays 'pausing', budget burns, resume() refuses — consider suspending on 'pausing' when there are pendingApprovals or noting it in the Pausing explainer. Minor: distinct flash wording for 'pausing' (not 'Try again'), optionally t()-wrap the dynamic messages, and release-note the watchdog active-time semantics change.

@wenshao
wenshao added this pull request to the merge queue Aug 8, 2026
Merged via the queue into QwenLM:main with commit 88a325b Aug 8, 2026
32 checks passed
qqqys added a commit to qqqys/qwen-code that referenced this pull request Aug 8, 2026
Resolves the conflict in packages/core/src/tools/workflow/workflow.ts.
main's QwenLM#8320 edited the inline constructor description this branch is
replacing, so the two sides touched the same argument:

- Kept this branch's `WORKFLOW_TOOL_DESCRIPTION` constant — extracting
  that description is the whole point of the PR.
- Ported QwenLM#8320's fact into the constant. The extracted text still listed
  the `/workflows` dialog controls as "live phase tree, token usage,
  cancel"; taking our side verbatim would have dropped cooperative
  pause/resume from the description the model actually reads.
- Pinned that capability list in the existing description test. Nothing
  else asserted it, so the same silent drop could recur on the next base
  merge.

Verified: npm run build, npm run typecheck, eslint on both changed files,
and packages/core src/tools/workflow/workflow.test.ts (40 passed).
@qwen-code-ci-bot

Copy link
Copy Markdown
Collaborator

Released in v0.21.8.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants