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

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 生命周期现在明确区分 running、pausing 和 paused。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。

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

@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/[email protected] typecheck
npm run typecheck --workspaces --if-present

@qwen-code/[email protected] typecheck
tsc --noEmit

@qwen-code/[email protected] typecheck
tsc --noEmit

@qwen-code/[email protected] typecheck
tsc --noEmit

@qwen-code/[email protected] typecheck
tsc --noEmit

@qwen-code/[email protected] typecheck
tsc --noEmit

@qwen-code/[email protected] typecheck
tsc --noEmit

@qwen-code/[email protected] typecheck
tsc -p tsconfig.json --noEmit

@qwen-code/[email protected] typecheck
tsc --noEmit

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

@qwen-code/[email protected] generate
node scripts/generate-git-commit-info.js

@qwen-code/[email protected] build
node ../../scripts/build_package.js

Successfully copied files.

@qwen-code/[email protected] 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/[email protected] build
tsc --build

@qwen-code/[email protected] build
tsc --build

@qwen-code/[email protected] build
tsc --build

@qwen-code/[email protected] build
tsc --build

@qwen-code/[email protected] build
tsc --build

@qwen-code/[email protected] build
tsc --build

@qwen-code/[email protected] build
tsc --build

@qwen-code/[email protected] build
tsc --build

@qwen-code/[email protected] build
tsc --build

@qwen-code/[email protected] build:ts
tsc --build

@qwen-code/[email protected] build
node ../../scripts/build_package.js

Successfully copied files.

@qwen-code/[email protected] 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/[email protected] 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/[email protected] 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/[email protected] 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

[email protected] build
npm run build:dev

[email protected] build:dev
npm run check-types && npm run lint && node esbuild.js

[email protected] check-types
tsc --noEmit

[email protected] lint
eslint src

@qwen-code/[email protected] build
node scripts/sync-extension.js && node config/esbuild.background.config.js --production

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

@qwen-code/[email protected] build
npm run clean && tsc --build

@qwen-code/[email protected] 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 pausing — resume 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 b88fdacbf、QWEN_CODE_ENABLE_WORKFLOWS=1、QWEN_CODE_MAX_WORKFLOW_CONCURRENCY=1、--yolo、隔离的 $HOME 与 workspace settings。PR head 与当前 main 合并无冲突(git merge-tree 验证)。

静态检查:npm run build + npm run bundle 通过。聚焦测试全部通过,与 PR 描述一致:core 251/251、CLI 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。

@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 均通过。

@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_deadbeef00000000 → Unknown 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=1、QWEN_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 恰好只有一条请求)→ 按 x、x 确认停止
  • ✅ 请求日志止于 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_deadbeef00000000 → Unknown 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

@wenshao

wenshao commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /review

@github-actions

github-actions Bot commented Aug 2, 2026

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

@wenshao

wenshao commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@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 at 552cb77. Two of my three earlier findings are fixed; the third stands. More importantly, I ran mutation probes rather than take the outstanding vacuity claims on trust, and all three I tested survived — so the guards this PR adds to the registry are not pinned by any test in the suite.

My earlier findings, at this HEAD

  • Fixed — the misleading pause error. workflowsCommand.ts now has an explicit if (!target.isBackgrounded) branch before the status branches, with its own accurate message ("Foreground workflow runs cannot be paused or resumed…"), so "could not be paused because its state changed" is now reachable only for the genuine race. This also closes the ci-bot's near-duplicate at :232.
  • Fixed — the dispatch error being replaced by AbortError. workflow-orchestrator.ts:1580-1587 now re-throws the original error from both arms of the waitUntilRunning() settlement, so a gate rejection can no longer overwrite the cause the handler meant to surface.
  • Stands — toggleSelectedWorkflowPause still has no direct unit test. git grep finds it only in BackgroundTasksDialog.tsx and BackgroundTaskViewContext.tsx; no .test. file references it, so the primary new user-facing action is covered only indirectly through dialog keypress simulation.

Mutation probes — three guards, three survivors

Baseline first: the six workflow suites are 254/254 green at this HEAD (workflow-run-registry, workflow-orchestrator, workflow-runner, workflow-dispatch-scheduler, workflow-snapshot, workflow). Then, one mutation at a time, restoring in between and confirming the file byte-identical to HEAD afterwards:

mutation result
delete the over-count cap || entry.agentsCompleted >= entry.agentsDispatched (workflow-run-registry.ts:711) survived — suite still green
revert the approval guard !isActiveWorkflowStatus(entry.status) → entry.status !== 'running' (:588) survived — suite still green
delete the terminal-state guard || isTerminalWorkflowStatus(entry.status) from onDispatchStateChange (:464) survived — suite still green

So the three ci-bot vacuity findings on this file are confirmed by probe, not merely asserted. That matters for what the guards are for: :464 is the only thing stopping a late scheduler callback from moving a settled run back to pausing/paused, :588 is what admits pausing to the approval path (the widening this PR exists to make), and :711 is what keeps agentsCompleted from passing agentsDispatched once cancellation and gate-release race. Each is a new invariant introduced by cooperative pause, and none of them would notice being removed.

I did not probe the remaining claims, but two are checkable by inspection and hold: workflowsCommand.test.ts:135 asserts toContain('p') — a single character that the fixture runIds wf_pausing/wf_paused and the status column supply on their own, so it cannot fail while those strings are present; and the createConcurrencyLimiter import swap in workflow-orchestrator.ts:49 does leave utils/concurrencyLimiter.ts with no production consumer.

Assessment

The implementation reads coherent and the three-state lifecycle is threaded through the consumers consistently — I am not disputing the design, and the two fixes above were the right ones. But this is a concurrency feature in packages/core/src/agents/** whose new invariants are, measurably, untested: three separate guards can each be deleted without a single test noticing. Per AGENTS.md a missing test is a Suggestion rather than a Critical, and I am not blocking — a maintainer has already approved at this exact commit. I am recording the probe results because "the suite is green" is currently weak evidence for this diff specifically, and the cheapest way to make it strong is one test per guard: drive onAgentCompleted past the dispatched count, transition an entry to pausing before an approval, and deliver a scheduler state change after a run has settled.

Process note for the record, not a request: this is a feat (not refactor) from a non-maintainer touching core, at roughly 335 production lines in packages/core — under the 500-line hard block and under the 1000-line advisory, so the two-tier gate is satisfied by wenshao's approval rather than needing escalation. Flagging it only because AGENTS.md asks for awareness on core-touching external PRs.

All local runs at this HEAD; the registry file was restored and verified byte-identical after probing. CI green.

@wenshao

wenshao commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover

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

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This is a fork PR, so the first round comes from the next scheduled scan (usually within minutes). Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本 PR 来自 fork,首轮处理将由下一次定时扫描执行(通常几分钟内)。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Aug 2, 2026 •

Copy link
Copy Markdown
Collaborator

✅ AutoFix round 7 finished — view run. See this round's report below.

中文说明

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

- Rename misleading `terminal` local to `presentation` in BackgroundTasksDialog
- Fix vacuous `toContain('p')` assertion to `toContain('Background tasks + p')`
- Fix vacuous gate assertion with macrotask yield in scheduler test
- Add over-count cap test for `onAgentCompleted` past dispatched count
- Add pausing-state approval parking test
- Remove dead `concurrencyLimiter` module (no production consumers)
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Address review summary — PR #8320

Feedback points and dispositions

Implemented this round

rc ID File Finding Action
rc:3696231060 BackgroundTasksDialog.tsx const terminal misnomer after statusPresentation rename Renamed to const presentation at all 6 call sites + downstream refs
rc:3696231071 workflow-run-registry.ts:711 Over-count cap is mutation-vacuous Added test: onAgentDispatched once → onAgentCompleted twice → asserts cap at 1; also tests cancelled state
rc:3696604980 workflow-orchestrator.ts:49 Dead concurrencyLimiter.ts (no production consumers) Removed concurrencyLimiter.ts + concurrencyLimiter.test.ts
rc:3697803401 workflow-dispatch-scheduler.test.ts:66 Vacuous gateSettled assertion (microtask ordering) Replaced await Promise.resolve() with await new Promise((resolve) => setTimeout(resolve, 0))
rc:3697803412 workflowsCommand.test.ts:135 toContain('p') is vacuous Changed to toContain('Background tasks + p')
rc:3697803416 workflow-run-registry.ts:589 Approval guard widening to pausing is mutation-vacuous Added test: transition to pausing, emit approval, assert parked (not rejected)

Verified as already fixed (earlier commits)

rc ID File Finding Fixed in
rc:3696231072 workflowsCommand.ts:232 Foreground run gets misleading "state changed" error 2337d584e — explicit !target.isBackgrounded branch
rc:3696310829 workflowsCommand.ts:233 Same as above (duplicate finding) 2337d584e
rc:3696310837 workflow-orchestrator.ts AbortError replaces dispatch error beba05493 — both arms re-throw original error
rc:3696403394 workflow-orchestrator.ts Same as above (author confirmed fix) beba05493
rc:3696604976 workflow-run-registry.ts:464 Terminal-state guard has no test 552cb7707 — "ignores late dispatch state changes after cancellation"
rc:3696231068 workflowsCommand.ts:223 pause()/resume() false-return branches untested Already covered by it.each at line 205–226 with mockReturnValue(false)

Declined with evidence

rc ID File Finding Reason
rc:3696310827 BackgroundTaskViewContext.tsx:308 No unit test for toggleSelectedWorkflowPause 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 p keypress). The registry-level gating is independently tested in workflow-run-registry.test.ts (65 tests). A direct unit test would require mocking the entire context provider infrastructure for minimal additional coverage.

Conflict notes

No conflicts (--conflict false).

Verification

  • npm run build — passed
  • npm run typecheck — passed
  • npm run lint — passed
  • cd packages/core && npx vitest run src/agents/workflow-run-registry.test.ts src/agents/runtime/workflow-dispatch-scheduler.test.ts — 68 passed (68)
  • cd packages/cli && npx vitest run src/ui/commands/workflowsCommand.test.ts src/ui/components/background-view/BackgroundTasksDialog.test.tsx — 92 passed (92)
中文说明

评审意见处理总结 — PR #8320

反馈要点与处理

本轮实施

rc ID 文件 发现 操作
rc:3696231060 BackgroundTasksDialog.tsx const terminal 在 statusPresentation 重命名后成为误称 在全部 6 个调用处及下游引用重命名为 const presentation
rc:3696231071 workflow-run-registry.ts:711 超额上限是突变真空的 新增测试:onAgentDispatched 一次 → onAgentCompleted 两次 → 断言上限为 1;同时测试 cancelled 状态
rc:3696604980 workflow-orchestrator.ts:49 死代码 concurrencyLimiter.ts(无生产消费者) 删除 concurrencyLimiter.ts + concurrencyLimiter.test.ts
rc:3697803401 workflow-dispatch-scheduler.test.ts:66 空转的 gateSettled 断言(微任务排序) 将 await Promise.resolve() 替换为 await new Promise((resolve) => setTimeout(resolve, 0))
rc:3697803412 workflowsCommand.test.ts:135 toContain('p') 是空转的 改为 toContain('Background tasks + p')
rc:3697803416 workflow-run-registry.ts:589 approval 守卫放宽到 pausing 是突变真空的 新增测试:转到 pausing,发出 approval 事件,断言被暂存(而非拒绝)

已验证为先前提交已修复

rc ID 文件 发现 修复于
rc:3696231072 workflowsCommand.ts:232 前台 run 收到误导性 "state changed" 错误 2337d584e — 显式 !target.isBackgrounded 分支
rc:3696310829 workflowsCommand.ts:233 同上(重复发现) 2337d584e
rc:3696310837 workflow-orchestrator.ts AbortError 替换了 dispatch 错误 beba05493 — 两个分支都重新抛出原始错误
rc:3696403394 workflow-orchestrator.ts 同上(作者确认已修复) beba05493
rc:3696604976 workflow-run-registry.ts:464 终态守卫无测试 552cb7707 — "ignores late dispatch state changes after cancellation"
rc:3696231068 workflowsCommand.ts:223 pause()/resume() 返回 false 的分支未测试 已由第 205–226 行的 it.each 覆盖,使用 mockReturnValue(false)

附证据拒绝

rc ID 文件 发现 原因
rc:3696310827 BackgroundTaskViewContext.tsx:308 toggleSelectedWorkflowPause 无单元测试 该回调是对 registry.pause()/registry.resume() 的薄委托,由状态检查门控。BackgroundTasksDialog 集成测试(66 个测试,模拟 p 按键)已端到端覆盖。registry 层门控由 workflow-run-registry.test.ts(65 个测试)独立测试。直接单元测试需要 mock 整个 context provider 基础设施,额外覆盖有限。

冲突说明

无冲突(--conflict false)。

验证

  • npm run build — 通过
  • npm run typecheck — 通过
  • npm run lint — 通过
  • cd packages/core && npx vitest run src/agents/workflow-run-registry.test.ts src/agents/runtime/workflow-dispatch-scheduler.test.ts — 68 通过 (68)
  • cd packages/cli && npx vitest run src/ui/commands/workflowsCommand.test.ts src/ui/components/background-view/BackgroundTasksDialog.test.tsx — 92 通过 (92)

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-preview

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Round-12 review feedback — summary

All findings were verified against the live head 5c117fa6 (the reviewer's
corrected assessment), then addressed in one commit: 89b78039fd.

Fixed

1. Budget gate and agent-cap rejections bypass the pause gate (Medium, standing)

In countedDispatch, the budget gate and the agent cap returned bare
Promise.reject(...), while the other three settlement paths (cache-hit,
success, failure) all settle through scheduler.waitUntilRunning(). While a
run was paused, a script that caught one of those rejections kept executing
— pause was silently ineffective on that path. Both entry-gate rejections now
route through a shared rejectThroughPauseGate() helper with the same
two-arm shape as the failure path: the error is delivered on resume and
preserved when cancellation aborts the gate. Three new orchestrator tests
pin this: a budget rejection held while paused and delivered on resume; an
agent-cap rejection held while paused and delivered on resume; and the
entry-gate error preserved when cancellation aborts the gate waiter.

2. A paused run blocked /clear and session switching indefinitely (Medium, standing)

Implemented the reviewer's predicate-split option:

  • WorkflowRunRegistry.hasRunningEntries() no longer counts paused (only
    running / pausing). A paused run has drained its dispatches, executes
    nothing, and its wall-clock watchdog is suspended — counting it as blocking
    let a paused-and-forgotten run block /clear and session switching forever
    with no backstop to release it. This aligns with the sibling
    BackgroundTaskRegistry.hasRunningTasks(), which also excludes paused
    agents from the switch gate.
  • resetBackgroundStateForSessionSwitch() now calls
    workflowRunRegistry.abortAll() before reset(), so paused runs are
    cancelled cleanly on session switch instead of leaking (a bare reset would
    drop their entries without aborting the controllers, leaving the gated
    script and its vm context alive). The runner's finally still writes the
    terminal snapshot and telemetry via its own entry reference.
    Running / pausing runs still block the switch exactly as before.
  • Registry and backgroundWorkUtils tests updated for the new semantics;
    the branch-command test mock gained the abortAll member.

3. Pressing p during pausing was completely silent (smaller)

pausing can last a full subagent dispatch (up to ten minutes), so a silent
keypress reasonably read as a stuck UI. The keypress is now routed to
registry.pause(), which refuses the transition (false) and lights the
existing "Pause/resume was rejected; the workflow state changed. Try again."
flash. Genuinely not-applicable keypresses (non-workflow rows, foreground
runs) keep the null verdict and still never flash, so the R10-4 invariant
is preserved. The dialog test was updated to assert the new behavior.

4. Stale concurrencyLimiter allowlist entry (smaller)

Removed from eslint.legacy-filenames.mjs; the file was deleted by this PR
and was never exported from the core package index, so nothing else changes.

Verified already fixed (round 11) — no action needed

  • High — false "dispatch failed" on cancellation (retracted by the
    reviewer): at this HEAD mapDispatchError marks teardown on
    isAbort || isRunAborted(), covering the plain-Error cancel paths.
  • Observer exception safety: the isAbort computation is wrapped in
    try/catch, so a throwing rejection value cannot make the observer reject.
  • Promise.all / ObservedPromise.then coverage comment (marked
    "unchecked rather than standing" by the reviewer) — verified at this HEAD:
    the Promise.all/race/any statics are wrapped explicitly
    (observeAggregate), finally is implemented through the observed then
    with a marked rethrow combinator, and the comment now accurately
    distinguishes observed elements from native aggregates.
  • unconsumedSettled latch: the per-run bookkeeping is reset at the top
    of every run() (R10-7).
  • onAgentCompleted guard asymmetry: the status gate was removed
    entirely; late settlements drain for every terminal status, with the cap
    as the only guard.

Declined

  • registry.resume missing the isBackgrounded guard — unreachable:
    isBackgrounded is assigned once at register time and never mutated, and
    pause() (the only path into paused) already requires it. Adding the
    check would be error handling for an impossible state, which AGENTS.md's
    Simplicity First rules out. Recorded here so the deferral is visible.

For the maintainer (no code action)

  • Splitting the PR — the reviewer's suggestion to split this change
    three ways (scheduler swap as a pure refactor / rejection observation and
    pausable watchdog / the three-state pause feature) is a scope tradeoff for
    the maintainer to decide; it is surfaced here rather than settled
    autonomously.

Verification

  • npm run build — passed
  • npm run typecheck — passed (0 errors)
  • npm run lint — passed (full repo, 0 errors/warnings)
  • npx prettier --check on every changed file — passed
  • vitest (core) workflow-run-registry.test.ts + workflow-orchestrator.test.ts — 208 passed (includes the 3 new pause-gate tests)
  • vitest (core) workflow-sandbox / workflow-runner / workflow-dispatch-scheduler / workflow-snapshot / tools/workflow — 209 passed
  • vitest (core) goalHook.test.ts + goalLoop.integration.test.ts + background-tasks.test.ts — passed
  • vitest (cli) backgroundWorkUtils + BackgroundTasksDialog + useBackgroundTaskView + workflowsCommand + clearCommand — 151 passed
  • vitest (cli) useResumeCommand + useBranchCommand — passed
  • vitest (cli) useGeminiStream + permissionController + BackgroundTasksPill — passed
  • Integration tests after npm run bundle — not run: the changed behavior
    is exercised by the unit suites above, not only through the bundled CLI or
    the integration harness.
  • Settings schema — no settings source changed, so no regeneration was
    needed.
中文说明

第 12 轮评审反馈 — 总结

所有发现均已在最新的 head 5c117fa6(评审者更正后的评估)上逐一核实,并在一次提交中处理完毕:89b78039fd。

已修复

1. 预算门禁与 agent 上限的拒绝绕过了暂停门(Medium,仍然成立)

在 countedDispatch 中,预算门禁和 agent 数量上限此前直接返回
Promise.reject(...),而该函数中其余三条结算路径(缓存命中、成功、失败)都经过
scheduler.waitUntilRunning() 结算。当运行处于 paused 状态时,脚本如果捕获了这两个
拒绝就可以继续执行 —— 暂停在这条路径上被悄悄绕过。现在这两个入口门禁的拒绝都通过共享的
rejectThroughPauseGate() 辅助函数走门,其形状与失败路径一致的两个分支相同:恢复时投递
错误,取消中止门时同样保留原始错误。新增三个编排器测试锁定该行为:预算拒绝在暂停期间被
挂起、恢复后投递;agent 上限拒绝在暂停期间被挂起、恢复后投递;以及取消中止门等待者时
入口门禁错误仍被保留。

2. 暂停的运行无限期阻塞 /clear 与会话切换(Medium,仍然成立)

采用评审者给出的"拆分谓词"方案:

  • WorkflowRunRegistry.hasRunningEntries() 不再把 paused 计入(只计
    running / pausing)。暂停的运行已经排空了所有派发、不执行任何工作,其墙钟看门狗
    也处于挂起状态 —— 若把它算作阻塞项,一个被暂停后遗忘的运行将永远阻塞 /clear 和
    会话切换,且没有任何兜底机制能解除阻塞。这与同级的
    BackgroundTaskRegistry.hasRunningTasks() 保持一致(后者同样不将被暂停的后台 agent
    计入切换门禁)。
  • resetBackgroundStateForSessionSwitch() 现在会在 reset() 之前调用
    workflowRunRegistry.abortAll(),使被暂停的运行在会话切换时被干净地取消,而不是泄漏
    (单纯 reset 会丢弃条目但不中止控制器,被门挂起的脚本及其 vm 上下文将一直存活)。
    运行器自身的 finally 仍会通过其持有的条目引用写入终态快照与遥测。
    running / pausing 状态的运行仍像以前一样阻塞切换。
  • 更新了注册表与 backgroundWorkUtils 的测试以匹配新语义;branch 命令的测试 mock
    补充了 abortAll 成员。

3. 在 pausing 期间按 p 完全无反馈(较小问题)

pausing 可能持续一整次子代理派发的时间(最长十分钟),此时按键毫无反馈会让用户合理地
认为界面卡死。现在该按键会转交给 registry.pause(),注册表拒绝该转换(返回
false),从而点亮既有的"暂停/恢复被拒绝;工作流状态已变化,请重试。"提示。真正不适用
的按键(非工作流行、前台运行)仍返回 null、不闪烁,因此 R10-4 的不变量得以保留。
对话框测试已更新为断言新行为。

4. 过期的 concurrencyLimiter 白名单条目(较小问题)

已从 eslint.legacy-filenames.mjs 中移除;该文件已被本 PR 删除,且从未从 core 包的
索引导出,因此没有其他影响。

已核实第 11 轮已修复 — 无需处理

  • High — 取消时误报 "dispatch failed"(评审者已撤回):当前 HEAD 上
    mapDispatchError 以 isAbort || isRunAborted() 判定 teardown,覆盖了抛出普通
    Error 的取消路径。
  • 观察者的异常安全:isAbort 的计算已包在 try/catch 中,会抛异常的拒绝值无法再
    让观察者自身 reject。
  • Promise.all / ObservedPromise.then 覆盖度注释(评审者标注为"未复核,不视为
    仍然成立")— 已在当前 HEAD 核实:Promise.all/race/any 静态方法已被显式包装
    (observeAggregate),finally 通过带"重新抛出"标记的组合器走被观察的 then
    实现,注释现在也准确区分了被观察的元素与原生聚合 Promise。
  • unconsumedSettled 锁存:按运行计数的簿记在每次 run() 开始时重置(R10-7)。
  • onAgentCompleted 守卫不对称:状态门已被整体移除;所有终态下的迟到结算都能继续
    排空,唯一守卫是上限。

已拒绝

  • registry.resume 缺少 isBackgrounded 守卫 — 不可达:isBackgrounded 只在
    注册时赋值一次、之后从不被修改,而 pause()(进入 paused 的唯一路径)已要求该
    字段。补这个检查等于为不可能发生的状态做错误处理,违反 AGENTS.md 的"简单优先"原则。
    在此记录拒绝原因以保证可见性。

交由维护者决定(不做代码改动)

  • 拆分 PR — 评审者建议把本改动拆成三部分(调度器替换作为纯重构 / 拒绝观察与可暂停
    看门狗 / 三态暂停特性),这属于由维护者权衡的范围取舍;在此如实呈现,而不自行拍板。

验证

  • npm run build — 通过
  • npm run typecheck — 通过(0 个错误)
  • npm run lint — 通过(全仓库,0 个错误/警告)
  • 对所有改动文件执行 npx prettier --check — 通过
  • vitest(core)workflow-run-registry.test.ts + workflow-orchestrator.test.ts — 208 个通过(含 3 个新的暂停门测试)
  • vitest(core)workflow-sandbox / workflow-runner / workflow-dispatch-scheduler / workflow-snapshot / tools/workflow — 209 个通过
  • vitest(core)goalHook.test.ts + goalLoop.integration.test.ts + background-tasks.test.ts — 通过
  • vitest(cli)backgroundWorkUtils + BackgroundTasksDialog + useBackgroundTaskView + workflowsCommand + clearCommand — 151 个通过
  • vitest(cli)useResumeCommand + useBranchCommand — 通过
  • vitest(cli)useGeminiStream + permissionController + BackgroundTasksPill — 通过
  • npm run bundle 之后的集成测试 — 未运行:本次改动的行为已由上述单元测试覆盖,并非
    只能通过打包后的 CLI 或集成测试框架验证。
  • 设置 schema — 未改动任何设置源,无需重新生成。

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

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


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

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Round 13 — review feedback addressed

All 34 inline findings (3 Critical, 31 Suggestion) were verified against the current code and addressed in one batch (750133f400). No feedback was declined; nothing required a maintainer decision. There were no conflicts (--conflict false), and no merge was performed.

Critical findings

R11-1 — settlement guard only special-cased 'cancelled'. Widened the guard in WorkflowRunner to every terminal status via isTerminalWorkflowStatus; a non-cancelled terminal entry now settles with entry.error ?? 'Workflow run failed.'. New runner test reproduces the probe shape: registry.fail() + handle.abort() while a held successful dispatch still lets the script complete — the handle now reports ok: false with the entry's error instead of ok: true.

R11-2 — watchdog pause-banking used Date.now() against a setTimeout deadline. Both the elapsed deduction in pause() and the re-arm in arm() now use the monotonic performance.now(), so system suspend / NTP steps can no longer corrupt the banked remainder in either direction. The existing budget-window tests pin the behavior.

R11-3 — adoption escapes disarmed the mirror (forgotten-await failure class). Design decision, implemented as suggested (consumption ≠ handling) plus a host-side escape hook, because the adopting promise (typically an async wrapper's implicit promise) is a plain vm-realm Promise the mirror cannot observe, and nothing inside the sandbox can reach it:

  • The observed then detects adoption attaches (native capability functions, identified by the [native code] toString signature). Adoption consumes (clears a recorded verdict, like a delayed await) and marks a new per-root adoptedOut state, but never sets rejectionHandled.
  • The flush and the immediate-mirror path suppress forwarded dispatch-failure entries for adopted-out roots: handled by the adopting chain they need no signal; forgotten there the escape hook provides it.
  • New host-side adoptionEscapeHook is installed per run() on process unhandledRejection, matches only this run's stamped rejections (__wfDispatchFailed + new __wfRunId stamp, both added in vmAsync), stays silent for roots with script-visible handlers, and dedupes against the flush via a shared (rootId, msg) key — surfacing forgotten transfers as dispatch failed (rejection not handled): … in the run log.
  • Regression tests cover the probe shapes: forgotten await in an async wrapper, forgotten fan-out (one line per failure), top-level return await (run error, no mirror line), try { await } catch (no mirror line), and late adoption clearing a recorded verdict.
  • Documented residual limits (code comments + here): a rejection landing after the run's flush still escapes as before; the process event itself cannot be cancelled from inside the sandbox, so a host with its own unhandledRejection listener (e.g. the CLI banner) still observes it. Note the sibling adoptedOut state also fixed a regression the naive "adoption never marks anything" approach introduced against the existing try { await p } catch + side-branch test (caught during this round's test run).

Sandbox mirror Suggestions

  • R11-4 — observeDispatch now derives dispatchFailed from an AggregateError's .errors markers (all-rejected Promise.any → dispatch attribution). Test added.
  • R11-10 — run() restructured: the flush now shares the single finally wrapping the entire run body (hook install → meta extraction → vm.Script → runInContext → watchdog → race), so sync throws before the race no longer discard queued mirror entries. Companion test pins flush-on-sync-throw via a script-level throw; the 30s sync vm-timeout member of this path carries the same code path but is impractical to exercise inside unit-test budgets.
  • R11-11 — flush dedupes by (rootId, msg) (same set shared with the escape hook): one failed dispatch fanned into N unconsumed branches now yields exactly one line. Test added.
  • R11-14 — suppression in both the flush and the immediate-mirror copy now keys on rejectionHandled alone (dropped the dispatchFailed conjunct). Test added for a handled root with an unconsumed side branch, including the post-settlement shape.
  • R11-15 — cross-root suppression: vmAsync stamps __wfRootId on every dispatch rejection; the observer suppresses forwarded failures whose originating element root the script handled (covers Promise.all/race forwarding and Promise.any via .errors), via new wfIsRootHandled bridge function. Probe shape pinned by a test (Promise.all([a]) + a.catch(handle)).
  • R11-16 — Promise.all/race/any wrappers bypass observation for non-default receivers (species contract preserved); elements still funnel through the observed then, and escapes via such aggregates are still caught by the hook through the stamped markers. Test added.
  • R11-20 — test added pinning the immediate-path suppression gate (slow dispatch rejected after flush, root caught + side branch).
  • R11-21 — Promise.race aggregate observation pinned by a new test (fire-and-forget race holding a failed dispatch: one mirror line, no unhandledRejection); Promise.any observation pinned by the R11-4/R11-30 tests.
  • R11-23 — script-handler-failure wording changed to script handler failed (rejection not handled): … (the root was attached in every shape reaching that branch). Two existing assertions updated.
  • R11-26 — handler introspection is guarded (try/catch before any state mutation), so an exotic rejection handler (revoked Proxy wrapping a function) can no longer make .then() throw synchronously. Test added.
  • R11-30 — the observer also consults isRunAborted(), so teardown rejections of a correctly-cancelled run wrapped in Promise.any (unmarked native AggregateError) stay silent. Test added.

Pause/resume surface Suggestions

  • R11-5 — two dialog tests added: armed-confirm clearing when the viewed entry disappears (setEntries([])), and on the roster-drift exit; both assert a single Esc then closes the dialog.
  • R11-12 — dialog test added for the 'failed' settle arm (running → failed exits detail to list).
  • R11-13 — two scheduler tests pinning resume()/pause() refusal once the abort signal fired (paused → stays paused; running → stays running).
  • R11-17 — /workflows p now checks terminal status BEFORE the foreground gate, so a retained terminal foreground run gets the same terminal wording as a snapshot-only hit. Test added.
  • R11-18 — abort re-arm test gained the lower bound (> 40ms) complementing the existing upper bound, distinguishing the banked remainder from a zero-remainder re-arm.
  • R11-19 — new multi-cycle watchdog test: pause → resume → second pause must suspend again (pins resume() clearing paused), and the run settles only after the second resume.
  • R11-24 (1/3) — the pausing/paused detail test now also asserts the Pausing/Paused status line renders.
  • R11-24 (2/3) — the allows an active %s workflow to be stopped tests now assert the x stop footer hint before the keypress.
  • R11-24 (3/3) — new fake-timer test: a running workflow detail re-renders on the 1s tick.
  • R11-25 (1/3, 2/3, 3/3) — the three single-armed pause-gate hold tests (keeps a nested agent result behind the shared pause gate, appends an in-flight result before the paused result gate opens, P6 cached-prefix) now arm both .then arms so a rejection-shaped regression fails the assertion instead of escaping as an unhandledRejection.
  • R11-28 — executionMode gate test now pins the exact Workflow pause controls are available only in the interactive TUI. wording.
  • R11-29 — active-bucket test now pins oldest-first ordering (wf_paused before wf_pausing); the fixture registers them in inverse order so a flipped/dropped sort cannot pass.

Registry / persistence Suggestions

  • R11-22 — nested workflow logs are merged into the parent run's logs at nested settlement (new WorkflowSandbox.appendLog; orchestrator reads nestedSandbox.getLogs() in a finally after the nested flush), so nested mirror lines and script logs are no longer discarded. Orchestrator test added. Both sandboxes also receive the new runId option for escape-hook attribution.
  • R11-27 — writeWorkflowSnapshot captures toSnapshot(task) before its first await, and the runner captures the telemetry projection before the snapshot write's await — both are now genuinely frozen at settlement (the no-yield finally path), making snapshot and telemetry consistent. Snapshot test added (fs yield simulated via a spying mkdir that mutates the live entry mid-write); the onBudgetUpdated comment was refreshed to match the now-true invariant.

Docs Suggestions

  • R11-6 — s row now reads "finished (completed, failed, or cancelled) workflow run".
  • R11-7 — dialog intro now documents the live-agent panel / Arena tab bar traversal and the dream entries.

Review-level concern (integration suite not run)

The round-12 review's CHANGES_REQUESTED ("Integration Tests (CLI, No Sandbox) were skipped in CI, and the suite was not run locally") is addressed by actually running the suite locally this round — see Verification. Result: 182 passed / 1 failed / 18 skipped. The single failure is environmental, not code:

  • Failing test: cli/qwen-config-dir.test.ts > 1d: CLI functions normally when QWEN_HOME is not set.
  • Error: EACCES: permission denied, mkdir '/home/github-runner/.qwen' in writeOutputLanguageFile / initializeLlmOutputLanguage.
  • Evidence: /home/github-runner is root:root drwxr-xr-x and the tests run as node (uid 1000); running mkdir /home/github-runner/.qwen directly as this user fails with the identical Permission denied. The failing module (packages/cli/src/utils/languageUtils.ts, gemini.tsx) is not touched by this PR (git diff origin/main...HEAD --name-only confirms). This failure reproduces identically regardless of any code change and passes on CI where $HOME is writable.

Verification

All commands actually run, in the final state (commit 750133f400):

  • npm run build — passed (exit 0)
  • npm run typecheck — passed (exit 0)
  • npm run lint — passed (exit 0; one prefer-const error found mid-round and fixed)
  • npx vitest run src/agents/runtime/workflow-sandbox.test.ts (packages/core) — 148 passed
  • npx vitest run orchestrator + scheduler + runner + snapshot + registry suites (packages/core) — 250 passed, then orchestrator re-run after a refactor — 133 passed
  • npx vitest run src/ui/commands/workflowsCommand.test.ts src/ui/components/background-view/BackgroundTasksDialog.test.tsx (packages/cli) — 114 passed
  • npm run bundle — passed (exit 0)
  • npm run test:integration:cli:sandbox:none — 182 passed / 1 failed / 18 skipped; the 1 failure is the environmental EACCES issue documented above (unrelated code path, reproduced as an OS-permission failure outside the CLI)
中文说明

第 13 轮 —— 评审反馈处理情况

全部 34 条行内发现(3 条 Critical、31 条 Suggestion)均已对照当前代码核实,并在一个提交批次(750133f400)中处理完毕。没有拒绝任何反馈,也没有需要维护者决策的事项。本轮无冲突(--conflict false),未执行任何合并。

Critical 发现

R11-1 —— 结算守卫只特判 'cancelled'。 已将 WorkflowRunner 中的守卫通过 isTerminalWorkflowStatus 扩展到所有终态;非 cancelled 的终态条目现在以 entry.error ?? 'Workflow run failed.' 结算。新增的 runner 测试复现了探针场景:在挂起的成功 dispatch 仍会让脚本正常完成的情况下执行 registry.fail() + handle.abort() —— handle 现在报告 ok: false 并携带条目的错误信息,而不是 ok: true。

R11-2 —— 看门狗暂停记账使用 Date.now() 对比 setTimeout 截止时间。 pause() 中的已用时间扣减和 arm() 中的重新计时现在都使用单调时钟 performance.now(),系统挂起 / NTP 校时不再能把剩余的预算时间往任一方向算错。既有的预算窗口测试钉住了该行为。

R11-3 —— 收养(adoption)逃逸使镜像失效(忘写 await 的失败类别)。 按建议方向(消费 ≠ 处理)实现,并辅以宿主侧逃逸钩子 —— 因为收养方 promise(通常是 async 包装器的隐式 promise)是镜像无法观察的普通 vm-realm Promise,且沙箱内部无法触及它:

  • 被观察的 then 检测收养挂接(原生 capability 函数,通过 [native code] toString 签名识别)。收养视为消费(清除已记录的裁决,等价于延迟 await),并标记新的 per-root adoptedOut 状态,但绝不设置 rejectionHandled。
  • flush 与立即镜像路径对"已被收养出去的根"上转发来的 dispatch 失败条目予以抑制:若被收养链处理了该拒绝则本无需信号;若被遗忘,则由逃逸钩子提供信号。
  • 新增宿主侧 adoptionEscapeHook:按 run() 粒度安装到 process 的 unhandledRejection 上,只匹配本 run 打标的拒绝(__wfDispatchFailed + 新增 __wfRunId 标记,两者均在 vmAsync 中打上),对脚本可见 handler 处理过的根保持沉默,并通过与 flush 共享的 (rootId, msg) 键去重 —— 把被遗忘的转移以 dispatch failed (rejection not handled): … 呈现到运行日志中。
  • 回归测试覆盖探针场景:async 包装器中的忘写 await、忘写 await 的扇出(每个失败一行)、顶层 return await(运行报错、无镜像行)、try { await } catch(无镜像行)、以及延迟收养清除已记录裁决。
  • 已在代码注释与本文档中记录的残留限制:在 run 的 flush 之后才到达的拒绝仍会像从前一样逃逸;进程级事件本身无法在沙箱内部取消,因此拥有自己的 unhandledRejection 监听器的宿主(例如 CLI 的横幅)仍会看到该事件。另外,adoptedOut 状态还修复了朴素的"收养不做任何标记"方案对既有 try { await p } catch + 旁支测试引入的回归(在本轮测试运行中被发现)。

沙箱镜像相关 Suggestion

  • R11-4 —— observeDispatch 现在从 AggregateError 的 .errors 标记中推导 dispatchFailed(全部拒绝的 Promise.any → dispatch 归因)。已加测试。
  • R11-10 —— 重构 run():flush 现在与包裹整个运行体的唯一 finally 共享(钩子安装 → meta 提取 → vm.Script → runInContext → 看门狗 → race),race 之前的同步抛出不再丢弃已排队的镜像条目。伴随测试通过脚本级抛出钉住"同步抛出时仍 flush";该路径中的 30 秒同步 vm 超时成员走同一代码路径,但在单元测试预算内不便演练。
  • R11-11 —— flush 按 (rootId, msg) 去重(与逃逸钩子共享同一集合):一个失败的 dispatch 扇出到 N 个未消费分支时,现在恰好产出一行日志。已加测试。
  • R11-14 —— flush 与立即镜像副本中的抑制现在只以 rejectionHandled 为键(去掉 dispatchFailed 合取条件)。新增测试覆盖"根已被处理但存在未消费旁支"的场景,包括结算后的形态。
  • R11-15 —— 跨根抑制:vmAsync 在每个 dispatch 拒绝上打 __wfRootId 标记;观察者对"源元素根已被脚本处理"的转发失败予以抑制(覆盖 Promise.all/race 的转发与 Promise.any 经 .errors 的形态),通过新的 wfIsRootHandled 桥接函数实现。探针场景已由测试钉住(Promise.all([a]) + a.catch(handle))。
  • R11-16 —— Promise.all/race/any 包装器对非默认接收者跳过观察(保留 species 契约);元素仍经由被观察的 then 进入观察,且经此类聚合一并逃逸的 dispatch 失败仍会被钩子通过打标捕获。已加测试。
  • R11-20 —— 新增测试钉住立即路径的抑制门(慢 dispatch 在 flush 之后才拒绝,根被 catch + 旁支形态)。
  • R11-21 —— 新增测试钉住 Promise.race 聚合的观察(fire-and-forget 的 race 中含失败 dispatch:一行镜像日志、无 unhandledRejection);Promise.any 的观察由 R11-4/R11-30 测试钉住。
  • R11-23 —— 脚本 handler 失败的措辞改为 script handler failed (rejection not handled): …(到达该分支的所有形态中根都曾被挂接)。两处既有断言同步更新。
  • R11-26 —— handler 内省加了防护(任何状态变更前先 try/catch),异常 rejection handler(包着函数的已撤销 Proxy)不再能让 .then() 同步抛错。已加测试。
  • R11-30 —— 观察者同时查询 isRunAborted(),因此被正确取消的 run 中包在 Promise.any 里的拆除期拒绝(无标记的原生 AggregateError)保持沉默。已加测试。

暂停/恢复界面相关 Suggestion

  • R11-5 —— 新增两个 dialog 测试:查看中的条目消失(setEntries([]))时清除 armed-confirm;以及 roster 漂移退出时清除;两者都断言此后单次 Esc 即可关闭对话框。
  • R11-12 —— 新增 dialog 测试覆盖 'failed' 结算分支(running → failed 时详情视图退回列表)。
  • R11-13 —— 新增两个 scheduler 测试,钉住 abort 信号触发后 resume()/pause() 的拒绝(paused → 保持 paused;running → 保持 running)。
  • R11-17 —— /workflows p 现在在前台运行门禁之前检查终态,因此仍保留在注册表中的终态前台 run 与仅命中快照的情况得到相同的终态措辞。已加测试。
  • R11-18 —— abort 重新计时的测试补上了下界(> 40ms),与既有上界配合,把"剩余预算"与"零剩余重新计时"区分开。
  • R11-19 —— 新增多周期看门狗测试:暂停 → 恢复 → 第二次暂停必须再次挂起(钉住 resume() 清除 paused 标志),且 run 只在第二次恢复后才结算。
  • R11-24 (1/3) —— pausing/paused 详情测试现在同时断言渲染出 Pausing/Paused 状态行。
  • R11-24 (2/3) —— allows an active %s workflow to be stopped 测试现在在按键之前断言页脚的 x stop 提示。
  • R11-24 (3/3) —— 新增假定时器测试:running 状态的 workflow 详情按 1 秒节拍重渲染。
  • R11-25 (1/3、2/3、3/3) —— 三个单臂的暂停门保持测试(keeps a nested agent result behind the shared pause gate、appends an in-flight result before the paused result gate opens、P6 cached-prefix)现在为 .then 装上双臂,使拒绝形态的回归让断言失败,而不是作为 unhandledRejection 逃逸。
  • R11-28 —— executionMode 门禁测试现在钉住完整措辞 Workflow pause controls are available only in the interactive TUI.。
  • R11-29 —— Active 分组测试现在钉住"最旧在前"的排序(wf_paused 在 wf_pausing 之前);夹具按相反顺序注册,因此排序被翻转或丢弃都无法通过。

注册表 / 持久化相关 Suggestion

  • R11-22 —— 嵌套 workflow 的日志在嵌套结算时并入父运行的日志(新增 WorkflowSandbox.appendLog;orchestrator 在嵌套 flush 之后的 finally 中读取 nestedSandbox.getLogs()),嵌套镜像行与脚本日志不再被丢弃。已加 orchestrator 测试。两个沙箱还接收新的 runId 选项用于逃逸钩子的归属判定。
  • R11-27 —— writeWorkflowSnapshot 在第一个 await 之前捕获 toSnapshot(task),runner 也在快照写入的 await 之前捕获遥测投影 —— 两者现在真正冻结在结算时刻(无 yield 的 finally 路径),快照与遥测彼此一致。已加快照测试(用间谍 mkdir 模拟写入期间的 drain 修改存活条目);onBudgetUpdated 的注释同步刷新,与其现已成立的不变量一致。

文档相关 Suggestion

  • R11-6 —— s 行改为 "finished (completed, failed, or cancelled) workflow run"。
  • R11-7 —— 对话框引言现在说明需先经过 live-agent 面板 / Arena 标签栏,并列出 dream 条目。

评审级关注点(集成测试套件未运行)

第 12 轮评审的 CHANGES_REQUESTED("Integration Tests (CLI, No Sandbox) 在 CI 中被跳过,且该套件未在本地运行")已通过本轮实际本地运行该套件来处理 —— 见下方验证清单。结果:182 通过 / 1 失败 / 18 跳过。唯一失败为环境问题,与代码无关:

  • 失败测试:cli/qwen-config-dir.test.ts > 1d: CLI functions normally when QWEN_HOME is not set。
  • 错误:writeOutputLanguageFile / initializeLlmOutputLanguage 中 EACCES: permission denied, mkdir '/home/github-runner/.qwen'。
  • 证据:/home/github-runner 属主为 root:root drwxr-xr-x,而测试以 node(uid 1000)运行;以当前用户直接执行 mkdir /home/github-runner/.qwen 得到完全相同的 Permission denied。失败模块(packages/cli/src/utils/languageUtils.ts、gemini.tsx)不在本 PR 的改动范围内(git diff origin/main...HEAD --name-only 证实)。该失败在任何代码改动下都会同样复现,而在 $HOME 可写的 CI 上通过。

验证(Verification)

最终状态(提交 750133f400)下实际执行的命令:

  • npm run build —— 通过(exit 0)
  • npm run typecheck —— 通过(exit 0)
  • npm run lint —— 通过(exit 0;本轮中途发现并修复了一处 prefer-const 错误)
  • npx vitest run src/agents/runtime/workflow-sandbox.test.ts(packages/core)—— 148 通过
  • npx vitest run orchestrator + scheduler + runner + snapshot + registry 套件(packages/core)—— 250 通过;重构后复跑 orchestrator —— 133 通过
  • npx vitest run src/ui/commands/workflowsCommand.test.ts src/ui/components/background-view/BackgroundTasksDialog.test.tsx(packages/cli)—— 114 通过
  • npm run bundle —— 通过(exit 0)
  • npm run test:integration:cli:sandbox:none —— 182 通过 / 1 失败 / 18 跳过;唯一失败为上文记录的环境性 EACCES 问题(与本 PR 无关的代码路径,已作为操作系统权限问题在 CLI 之外直接复现)

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

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


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

wenshao added a commit that referenced this pull request Aug 7, 2026
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 → Pausing → Paused, 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 ✅

我此前三轮验证分别跑在 b88fdacbf、552cb77 和 fd8a6ed77 上。之后 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_HOME,QWEN_CODE_ENABLE_WORKFLOWS=1,QWEN_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 在飞时暂停 → Pausing → Paused,B、C 仍在队列 ✅
2 暂停中的 run 保留在 /workflows 的 Active 分组;/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 runId。completed 的 run 得到同样的终态文案。两侧行为都与描述一致。

该 head 上的测试与卫生检查

  • packages/core 定向用例:438 通过 / 7 个文件(workflow-dispatch-scheduler、workflow-orchestrator、workflow-runner、workflow-sandbox、workflow-run-registry、workflow-snapshot、tools/workflow)—— 上次是 402。
  • packages/cli 定向用例:188 通过 / 6 个文件(workflowsCommand、BackgroundTasksDialog、BackgroundTasksPill、useBackgroundTaskView、backgroundWorkUtils、useBranchCommand)。
  • npm run build、npm run bundle、npm run typecheck(0 个 TS 错误)、npm run lint 全部通过。(lint 报了 26 条问题,全部位于我自己未纳入版本控制的 harness/ 目录,PR 自身文件零问题。)
  • 被删除的 utils/concurrencyLimiter.ts 在 packages/ 下仍无任何引用。

三条不阻塞的说明

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/3 或 3/3。watchdog 对照组还让我撞上了它的 failed 形态:一个只向 provider 发出过一次 dispatch、在 25 秒被中止的 run,显示为 failed · 3/3 agents。

这是我上轮那条「外观性」说明的具体第二副面孔。仍属外观问题、不阻塞——之所以提出,是因为它现在在 completed 和 failed 的 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-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-sandbox、workflow-dispatch-scheduler、workflow-orchestrator、workflow-runner、workflow-run-registry、workflow-snapshot、tools/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
@wenshao
wenshao dismissed a stale review August 8, 2026 01:53

fixed

@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 fixed — WallClockWatchdog 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 running → pausing; 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 仅允许 running → pausing 转换;该运行本就在暂停中)。/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).
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.

5 participants