feat(core): wire background shells into the task_stop tool#3687
Conversation
Phase B follow-up #1 from #3634, unblocked by #3471 (control plane) merging in. The model can now cancel a managed background shell with the same `task_stop` tool it uses for subagents — no more falling back to `kill <pid>` via BashTool. Lookup order: subagent registry first (existing behavior), then the background shell registry as a fallback. Agent IDs follow `<subagentName>-<suffix>` and shell IDs follow `bg_<8 hex chars>`, so the two namespaces cannot collide in practice; the order is fixed for determinism (a defensive test pins agent-wins-over-shell). The shell cancel path resolves through the entry's own AbortController (which `BackgroundShellRegistry.cancel` triggers); the child process exit handler then settles the registry to `cancelled` and the on-disk output file is preserved for inspection via `/tasks` or a direct `Read`. This matches Phase B's "registry's own AbortController is the cancellation source of truth" design without needing the in-flight notification framework that subagents use. Tests: 7 task-stop tests (was 4) — added cancel-shell happy path, NOT_RUNNING for already-exited shell, and a defensive agent-takes-precedence-on-id-collision case.
Code Coverage Summary
CLI Package - Full Text ReportCore Package - Full Text ReportFor detailed HTML reports, please see the 'coverage-reports-22.x-ubuntu-latest' artifact from the main CI run. |
|
No issues found. LGTM! ✅ — gpt-5.5 via Qwen Code /review |
@doudouOUC noticed that the previous task_stop path called `BackgroundShellRegistry.cancel(id, Date.now())`, which marked the entry `cancelled` immediately. The spawn handler's settle path only records real exit info via cancel/complete/fail when the entry is still `running`, so the cancel-vs-exit race could permanently hide a real completed/failed result and `/tasks` would show a terminal endTime while the process was still draining. Add a `requestCancel(id)` method to `BackgroundShellRegistry` that triggers the entry's AbortController only; status stays `running` until the settle path observes the abort and records the real terminal state. The immediate-mark `cancel(id, endTime)` is reserved for `abortAll()` / shutdown, where the CLI process is tearing down anyway and there is no settle handler to wait for. Tests updated: - `task-stop.test.ts` cancel-shell happy path now asserts the entry stays `running` with `endTime` undefined post-stop, and the abort signal fires (the settle path's contract, not task_stop's, is the one that flips status). - 3 new `requestCancel` tests in `backgroundShellRegistry.test.ts`: running → abort+still-running, terminal entry no-op, unknown id no-op.
|
谢谢 catch — 真问题,push 到 按你提议的两条路径里第一条走的(defer terminal transition until result handler runs):
测试调整:
cancel-vs-exit race 现在 settle path 单一权威:先到的 也考虑过你提议的第二条路径("cancellation requested" 中间状态对齐 background-agent finalize),但当前 settle path 已有 if-running guard,加新中间 status 会让所有 status 检查代码都要更新而没真增益 — 选了第一条 minimal API 路径。如果你觉得需要中间状态以便 dialog 区分"cancelling"和"cancelled",可以再开 follow-up 加(dialog PR |
tanzhenxin
left a comment
There was a problem hiding this comment.
Review
Clean, well-scoped wiring change: task_stop consults the subagent registry first, then falls back to BackgroundShellRegistry, with cancellation flowing through the entry's existing AbortController and the on-disk output path surfaced so the model can still inspect captured output post-cancel. No schema changes, no new error types, and subagent behavior is unchanged.
LGTM.
…og (#3720) * feat(cli): wire background shells into combined Background tasks dialog Phase B follow-up #2: surface managed background shells in the same overlay that already shows local subagents, so users get one unified view instead of having to remember /tasks for shells. - BackgroundShellRegistry: add setRegisterCallback/setStatusChangeCallback and requestCancel(id), mirroring BackgroundTaskRegistry's contract. register() also fires statusChange so subscribers see the lifecycle start, not just transitions. - useBackgroundTaskView: subscribe to both registries, merge entries by startTime, attach a `kind` discriminator (DialogEntry union) so renderers can dispatch on agent vs shell. - BackgroundTasksPill: group running counts by kind ("2 shells, 1 local agent"); when all entries are terminal, collapse to "N task(s) done". - BackgroundTasksDialog: replace per-kind section header with a single "Background tasks" header; ListBody renders shell rows as "[shell] <command>"; DetailBody dispatches to AgentDetailBody (the original) or a new ShellDetailBody (cwd / output file / pid / exit). - Context cancelSelected switches by kind: agents go through cancel(), shells through requestCancel() — only aborts, lets the spawn settle path record the real terminal state (mirrors task_stop in #3687). Tests: 8 pill cases (singular/plural per kind, mixed, terminal-only), 4 dialog cases (auto-fallback on running→terminal, cancel flow, already-terminal stays in detail, selectedIndex clamp); shell registry gains 5 callback tests + 3 requestCancel tests. * fix(cli): refresh detail-body agent fields between status changes useBackgroundTaskView shallow-copies agent entries into DialogEntry so each entry can carry a `kind` discriminator. The copy detaches `recentActivities` from the registry: BackgroundTaskRegistry.appendActivity mutates `entry.recentActivities = next` on the registry object and emits `activityChange`, but the dialog's activity callback only bumps a local counter — so the snapshot's `recentActivities` reference goes stale and the Progress block keeps rendering the old array until the next status-driven refresh. Resolve `selectedEntry` against the registry on each render when the selected entry is an agent, with `activityTick` as a useMemo dep so it recomputes on every activity callback. Snapshot remains the source of truth for the list (no churn on the pill / AppContainer); only the detail body re-reads live. Also rename the non-empty list section header from "Local agents" to "Background tasks" to match the empty-state branch and the unified multi-kind contents. --------- Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
Captures current state of the bg-agent subsystem in the fork (what's already wired, what is not), maps the upstream qwen-code PRs we have not yet ported (QwenLM#3076 → QwenLM#3739), and sketches three phases to close the gap: - Phase A: model-facing agent control + event monitor (QwenLM#3471, QwenLM#3684, QwenLM#3687) - Phase B: TUI surface + /tasks command (QwenLM#3488, QwenLM#3642) - Phase C: cross-session resume (QwenLM#3739) Also calls out cross-cutting decisions we should make before Phase A lands: settings layout, stop-tool naming, persistence shape, gateway validation. This is a planning doc, not a spec. Per-phase code-level designs come later. Co-authored-by: Automaker <automaker@localhost> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The original Phase A/B/C plan was written before we committed to making proto a workstacean fleet agent. This update revises the roadmap to match current priorities: - Track 1 (active): SDK surface for workstacean (drives protoCLI#223) - Track 2 (deferred): Phase A reduced — model-facing agent control, in two PRs (housekeeping first, then content port) - Track 3 (dropped): Phase B TUI surface — workstacean owns the dashboard; only /tasks remains as an optional spike - Track 4 (dropped): Phase C cross-session resume — workstacean is the long-running session, not proto Notable concrete decisions captured: - Drop our task-stop.ts and adopt upstream's (Arena/Team unused). - Adopt upstream's tools/agent/ nested layout in PR-1. - Skip QwenLM#3687 — covered by PR-1's reconciliation. - Persistence stays single-JSON since Track 4 is dropped. Adds a 2026-05-03 update banner at the top so readers don't trust the older sections without context. Section numbering preserved. Co-authored-by: Automaker <automaker@localhost> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(core): wire background shells into the task_stop tool Phase B follow-up QwenLM#1 from QwenLM#3634, unblocked by QwenLM#3471 (control plane) merging in. The model can now cancel a managed background shell with the same `task_stop` tool it uses for subagents — no more falling back to `kill <pid>` via BashTool. Lookup order: subagent registry first (existing behavior), then the background shell registry as a fallback. Agent IDs follow `<subagentName>-<suffix>` and shell IDs follow `bg_<8 hex chars>`, so the two namespaces cannot collide in practice; the order is fixed for determinism (a defensive test pins agent-wins-over-shell). The shell cancel path resolves through the entry's own AbortController (which `BackgroundShellRegistry.cancel` triggers); the child process exit handler then settles the registry to `cancelled` and the on-disk output file is preserved for inspection via `/tasks` or a direct `Read`. This matches Phase B's "registry's own AbortController is the cancellation source of truth" design without needing the in-flight notification framework that subagents use. Tests: 7 task-stop tests (was 4) — added cancel-shell happy path, NOT_RUNNING for already-exited shell, and a defensive agent-takes-precedence-on-id-collision case. * fix(core): defer shell terminal transition until spawn handler settles @doudouOUC noticed that the previous task_stop path called `BackgroundShellRegistry.cancel(id, Date.now())`, which marked the entry `cancelled` immediately. The spawn handler's settle path only records real exit info via cancel/complete/fail when the entry is still `running`, so the cancel-vs-exit race could permanently hide a real completed/failed result and `/tasks` would show a terminal endTime while the process was still draining. Add a `requestCancel(id)` method to `BackgroundShellRegistry` that triggers the entry's AbortController only; status stays `running` until the settle path observes the abort and records the real terminal state. The immediate-mark `cancel(id, endTime)` is reserved for `abortAll()` / shutdown, where the CLI process is tearing down anyway and there is no settle handler to wait for. Tests updated: - `task-stop.test.ts` cancel-shell happy path now asserts the entry stays `running` with `endTime` undefined post-stop, and the abort signal fires (the settle path's contract, not task_stop's, is the one that flips status). - 3 new `requestCancel` tests in `backgroundShellRegistry.test.ts`: running → abort+still-running, terminal entry no-op, unknown id no-op. --------- Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
…og (QwenLM#3720) * feat(cli): wire background shells into combined Background tasks dialog Phase B follow-up QwenLM#2: surface managed background shells in the same overlay that already shows local subagents, so users get one unified view instead of having to remember /tasks for shells. - BackgroundShellRegistry: add setRegisterCallback/setStatusChangeCallback and requestCancel(id), mirroring BackgroundTaskRegistry's contract. register() also fires statusChange so subscribers see the lifecycle start, not just transitions. - useBackgroundTaskView: subscribe to both registries, merge entries by startTime, attach a `kind` discriminator (DialogEntry union) so renderers can dispatch on agent vs shell. - BackgroundTasksPill: group running counts by kind ("2 shells, 1 local agent"); when all entries are terminal, collapse to "N task(s) done". - BackgroundTasksDialog: replace per-kind section header with a single "Background tasks" header; ListBody renders shell rows as "[shell] <command>"; DetailBody dispatches to AgentDetailBody (the original) or a new ShellDetailBody (cwd / output file / pid / exit). - Context cancelSelected switches by kind: agents go through cancel(), shells through requestCancel() — only aborts, lets the spawn settle path record the real terminal state (mirrors task_stop in QwenLM#3687). Tests: 8 pill cases (singular/plural per kind, mixed, terminal-only), 4 dialog cases (auto-fallback on running→terminal, cancel flow, already-terminal stays in detail, selectedIndex clamp); shell registry gains 5 callback tests + 3 requestCancel tests. * fix(cli): refresh detail-body agent fields between status changes useBackgroundTaskView shallow-copies agent entries into DialogEntry so each entry can carry a `kind` discriminator. The copy detaches `recentActivities` from the registry: BackgroundTaskRegistry.appendActivity mutates `entry.recentActivities = next` on the registry object and emits `activityChange`, but the dialog's activity callback only bumps a local counter — so the snapshot's `recentActivities` reference goes stale and the Progress block keeps rendering the old array until the next status-driven refresh. Resolve `selectedEntry` against the registry on each render when the selected entry is an agent, with `activityTick` as a useMemo dep so it recomputes on every activity callback. Snapshot remains the source of truth for the list (no churn on the pill / AppContainer); only the detail body re-reads live. Also rename the non-empty list section header from "Local agents" to "Background tasks" to match the empty-state branch and the unified multi-kind contents. --------- Co-authored-by: wenshao <wenshao@U-K7F6PQY3-2157.local>
Summary
Phase B follow-up #1 from #3634, unblocked by #3471 (control plane) merging in. The model can now cancel a managed background shell with the same
task_stoptool it uses for subagents — no more falling back tokill <pid>viaBashTool.Why
#3642 landed
BackgroundShellRegistrywith its ownAbortControlleras the cancellation source of truth, buttask_stoponly knew about the subagent registry. To stop abg_xxxshell the model had to spawn another bash and callkill, which is awkward, leaks a foreground bash, and bypasses the registry's lifecycle hooks. @tanzhenxin called this out as the obvious post-merge follow-up in his #3642 review (no blockers, follow-up).Before / After (model-facing)
The shell ID
bg_a1b2c3d4came fromBash(npm run dev, is_background: true)'s response.Before:
After:
Subagents see no behavior change — same lookup, same description, same final task-notification semantics.
What changes
task_stoplookup: subagent registry first (existing behavior), thenBackgroundShellRegistryas a fallback. Agent IDs follow<subagentName>-<suffix>and shell IDs followbg_<8 hex chars>, so the two namespaces cannot collide in practice. A defensive test pins agent-wins-over-shell determinism in case they ever do.AbortController(whichBackgroundShellRegistry.canceltriggers), then the child-process exit handler settles the registry tocancelled. The on-disk output file is preserved for inspection via/tasksor a directRead— message wording explicitly tells the agent where to find it.TASK_STOP_NOT_FOUND/TASK_STOP_NOT_RUNNING(both already kind-agnostic from feat(core): model-facing agent control (task_stop, send_message, per-agent transcript) #3471's review) cover both registries; only the human-facing message changes wording (background agentvsbackground shell).No behavior change for subagents.
What this PR doesn't do
feat/shell-in-dialog)./tasksslash-command deprecation — gated on follow-up Where is the config saved? #2 landing.Test plan
task-stop.test.ts: cancel-shell happy path,NOT_RUNNINGfor exited shell, agent-precedence-on-id-collision.task-stoptests pass.background-tasks.test.tsorbackgroundShellRegistry.test.ts.Related: #3634 (roadmap), #3642 (Phase B), #3471 (Phase A control plane).
中文版
Summary
来自 #3634 的 Phase B follow-up #1,在 #3471(control plane)合入后解锁。模型现在可以用和取消 subagent 同一个
task_stop工具取消受管的 background shell — 不再需要通过BashTool跑kill <pid>。为什么
#3642 落地了
BackgroundShellRegistry,自己的AbortController作为 cancellation 单一源,但task_stop只认识 subagent registry。要停一个bg_xxxshell 时模型必须再 spawn 一个 bash 调kill—— 很别扭、泄漏一个前台 bash、绕过 registry 的 lifecycle hook。@tanzhenxin 在 #3642 review 里把这个标为明显的 post-merge follow-up(不阻塞,follow-up 处理)。Before / After(模型视角)
shell ID
bg_a1b2c3d4来自之前Bash(npm run dev, is_background: true)的返回。Before:
After:
subagent 行为完全不变 — 同样的 lookup、同样的 description、同样的最终 task-notification 语义。
改动
task_stop查找顺序:subagent registry 优先(保留原行为),再 fallback 到BackgroundShellRegistry。agent ID 是<subagentName>-<suffix>格式,shell ID 是bg_<8 个 hex>,两个命名空间实践中不可能撞。一个 defensive test 钉住"撞了就 agent 优先"的确定性行为。AbortController(被BackgroundShellRegistry.cancel触发),然后子进程 exit handler 把 registry 状态 settle 成cancelled。磁盘上的输出文件保留,可通过/tasks或直接Read查看 —— 返回消息明确告诉 agent 文件位置。TASK_STOP_NOT_FOUND/TASK_STOP_NOT_RUNNING(在 feat(core): model-facing agent control (task_stop, send_message, per-agent transcript) #3471 review 时已经改成 kind-agnostic)覆盖两个 registry;只是人类可读消息按 kind 区分(background agentvsbackground shell)。subagent 行为零变化。
本 PR 不做的
feat/shell-in-dialog)。/tasksslash command 的 deprecate —— 卡 follow-up Where is the config saved? #2 合入。测试
task-stop.test.ts加 3 个新测试:cancel-shell happy path、对已退出 shell 返回NOT_RUNNING、ID 撞车时 agent 优先。task-stop测试全过。background-tasks.test.ts和backgroundShellRegistry.test.ts无 regression。相关:#3634(roadmap)、#3642(Phase B)、#3471(Phase A 控制面)。