You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Writing down what @tanzhenxin and I have aligned on for background tasks, so it's discoverable for anyone touching this area. Phase A merged (#3471 + #3488), with background agent resume/continuation extension merged at #3739 by @doudouOUC. Phase B merged + closed (#3642 + #3687 + #3720 + #3801 + doc cleanup #3808). Phase C merged + post-merge fix-ups all in at #3684 (Monitor tool + sleep interception + task_stop ↔ monitor all bundled), with the dialog-integration follow-up merged at #3791, Monitor(...) permission namespace merged at #3726 by @doudouOUC, Windows taskkill test fix merged at #3784, and token-bucket clock-drift / AST logging / UI routing fix-ups merged at #3792 by @doudouOUC. The kind framework now has four real consumers (agent / shell / monitor / dream) — kind framework promise fulfilled with the first system-initiated kind. Adjacent UX: MCP health pill merged at #3741 (parallel pill, not a kind extension). Phase D started + part (a) merged: #3809 (long-running foreground bash advisory) is in main; Phase D part (b) Ctrl+B promote design open at #3831. Beyond track first item merged at #3836 (auto-memory dream tasks → pill / dialog / task_stop 4th dispatch route, 2026-05-06, +1714 / -100). Foreground subagent → pill+dialog routing merged at #3768 by @tanzhenxin (2026-05-06, +1008 / -108) — first non-background consumer of the kind framework now in main. Phase D part (b) Ctrl+B promote — full sequence merged: PR-1 #3842 (signal.reason foundation, 2026-05-07, +947 / -56) + reviewer follow-up #3886 (Proxy-trap + PTY parity, 2026-05-07, +85 / -21) + PR-2 #3894 (shell.ts integration + lazy promote + design Q7 aborted:false, 2026-05-08, +935 / -15) + PR-3 #3969 (Ctrl+B keybind + AppContainer wire-up + E2E tests + docs, 2026-05-11, +308 / -15). #3831 design issue closed. Phase D (a)+(b) both done. No PRs open in this roadmap.
Background
Today qwen-code's shell tool can run a command in background by appending & (tools/shell.tsis_background: true), but the spawned process effectively leaks: there's no registry, no way to query status or read output afterwards, no way for the agent to terminate it later. For long-running things — dev servers, builds, file watchers, log tailing, polling jobs — this turns the agent into a poor manager: it can fire-and-forget, but it can't observe or steer. The user can't see what's running either.
The three subagent PRs (#3076 ✅ + #3471 ✅ + #3488 ✅) build the first complete answer to this problem for one kind of background work (subagents). The infrastructure they introduce — a background-task registry, a model-facing control surface, a user-facing pill + dialog — is naturally general. The notes below sketch how that frame extends to other kinds.
Phase A/B extension by @tanzhenxin (2026-05-06, +1008 / -108) — route foreground subagents through pill+dialog while running (flicker fix; reuses the pill+dialog rendering surface). First non-background consumer of the kind framework.
Phase B follow-up doc PR — point background-shell guidance at both /tasks AND the interactive Background tasks dialog (LLM-facing strings + 4 stale comments)
Phase D part (a) — long-running foreground bash advisory: hint to use is_background: true next time after a foreground command exceeds half its effective timeout
Beyond track — first landing (2026-05-06, +1714 / -100): surface and cancel auto-memory dream tasks (introduces dream as the 4th kind in the framework — agent / shell / monitor / dream); footer pill counts dreams, dialog list shows [dream] memory consolidation reviewing N sessions, per-kind detail body, dialog x cancel + task_stop 4th dispatch route, MemoryTaskStatus gains 'cancelled'. Two review-fix passes landed bugs in the same window: lockReleaseError same-session recovery (dreamLockReleaseFailed flag force-cleans the leaked lock on next scheduleDream), real cancelled-dream duration_ms for telemetry, AbortController-before-storeWith ordering (race fix), gating-metadata try/catch (no longer downgrades a successful dream to 'failed' on disk error), UI-surface lockReleaseError + metadataWriteError warnings. Out-of-scope (planned follow-up): live in-flight progress (phase flip / files-touched accumulation) — needs runForkedAgent's AgentPathParams extended with onAssistantMessage callback rippling through 4 call sites. Intentionally excludes extract tasks (too noisy; toast already covers; cancellation would interfere with the request loop).
Phase D part (b) PR-1 of 3 for #3831 (2026-05-07, +947 / -56 final). Defines a discriminated ShellAbortReason = { kind: 'cancel' } | { kind: 'background'; shellId? } union the AbortSignal carries; default behavior unchanged tree-kill, { kind: 'background' } is a takeover signal — execute() skips kill, drops child from active set, flushes output snapshot, resolves Promise immediately with promoted: true. ShellExecutionResult gains optional promoted?: boolean. Both abort handlers updated (executeWithPty + childProcessFallback); exhaustive switch with _exhaustive: never default; getShellAbortReasonKind helper exported with prototype-pollution defense. Pure plumbing, zero behavior change for existing callers. 5 review rounds + 1 self-audit pass → 22 review threads + 1 real bug found by reviewer (@lydell/node-pty exposes removeListener, not off alias). 70 / 70 tests pass.
PR-1 follow-up for #3842 (2026-05-07, +85 / -21). Addresses 2 of @tanzhenxin's approving-review notes: (1) real bug — Object.prototype.hasOwnProperty.call(reason, 'kind') was outside the helper's try/catch, so a Proxy with a throwing getOwnPropertyDescriptor trap would propagate past the helper, then through the abort handler's switch, then out of the (async) abort listener — leaving the shell process alive instead of being killed on cancel. Wrapped both the descriptor probe and the value read in the same try block + regression test. (2) PTY handoff-boundary test parity — re-invoke the production onData callback AFTER background-abort and assert onOutputEventMock.mock.calls.length === 0, exercising the listenersDetached guard in the chain callback. Note 1 (aborted: true + promoted: true shape — reviewer suggested aborted: false when promoted: true) deferred to PR-2 as design question 7 in #3831.
Phase D part (b) PR-2 of 3 for #3831 (2026-05-08, +935 / -15). shell.ts integration: foreground execute path detects result.promoted: true, snapshots output to bg_xxx.output under the project temp dir, registers a BackgroundShellEntry with the running pid + a fresh AbortController whose abort listener kills the still-running child via SIGTERM → 200ms → SIGKILL (matching the service-side cancel cascade), returns a model-facing ToolResult pointing the agent at /tasks / Background tasks dialog / task_stop. coreToolScheduler now stashes promoteAbortController on TrackedExecutingToolCall so PR-3's keybind can find it. Resolves PR-1 design question 7 (aborted: false when promoted: true so existing if (result.aborted) consumer branches fall through naturally). 6 review rounds → multiple Critical fixes (mkdir orphan, refused-promote race reported as timeout, commandToExecute co-author rewrite). 169 / 169 shell tests pass. Limitations deferred to PR-2.5: post-promote stream redirect (output snapshot frozen at promote time) + natural-exit registry settle (entry stays 'running' until task_stop / session-end).
Phase D part (b) PR-3 of 3 for #3831 (2026-05-11, +308 / -15). Command.PROMOTE_SHELL_TO_BACKGROUND keybind bound to Ctrl+B. useReactToolScheduler.ts projects promoteAbortController from core's ExecutingToolCall through TrackedExecutingToolCall (with compile-time extends keyof assertions to fail loud + local on a future core rename). AppContainer.handleGlobalKeypress adds the branch that walks pendingToolCallsRef.current, finds the executing shell tool call (gated on both tc.request.name === ToolNames.SHELL AND promoteAbortController !== undefined for defense-in-depth), calls .abort({ kind: 'background' }), returns early. No-op + fall-through when no foreground shell is executing — input layer's existing Ctrl+B (cursor-left) still fires. Docs: keyboard-shortcuts.md entry with the fall-through note. Tests: 4 keyMatchers + 3 AppContainer integration (promote / no-op / non-shell-tool guard). 60 / 60 tests pass. Closes the 3-PR sequence for #3831 (Phase D part b of #3634).
Together these provide a complete vertical slice for one background-task kind (subagents). The naming and shape (BackgroundAgentEntry, BackgroundTaskRegistry, "Background tasks" dialog title) already hint at the broader frame.
Direction we're taking
Background subagents are one task kind among several. The same registry, control tools, and UI naturally host:
Background subagents ✅ in flight via the three PRs above
Background shells — spawn npm run dev, pytest, build commands, watchers as managed processes with output capture, lifecycle queries, and explicit termination
Event monitors — long-running watchers (tail -F, inotifywait, polling loops) that push events back to the agent so it can react asynchronously
MCP server health surfacing — show MCP startup/reconnect state in the same dialog instead of being invisible
Auto-memory / consolidation tasks — system-initiated background work that surfaces to the user
Cloud-backed long tasks (longer-term) — tasks that can survive local restart
Every kind follows the same five elements:
A registry entry with lifecycle (running / completed / failed / cancelled)
A user-visible label in the status pill (counted by kind)
A row in the combined tasks dialog with a per-kind detail view
Cancellation via the same task_stop tool
(Where applicable) inbound steering via the same send_message tool
Each new kind is purely additive — that's the structural payoff of getting #3471 / #3488's interfaces right.
Architecture: current → target
Current — shell.ts background path is fully detached and invisible; subagent has its own agent-only registry/UI:
The shape change is small: keep the same BackgroundTaskRegistry already introduced for subagents, generalize its entry type with a kind discriminator, route shell + monitor + future kinds through the same gate. The user-facing surface (pill + dialog) and the model-facing surface (task_stop / send_message) become a small framework that hosts every kind.
Sequencing
flowchart LR
classDef done fill:#d4edda,stroke:#28a745,color:#155724
classDef inflight fill:#fff3cd,stroke:#ffc107,color:#856404
classDef next fill:#cfe2ff,stroke:#0d6efd,color:#084298
classDef later fill:#f8f9fa,stroke:#6c757d,color:#495057
A["<b>Phase A</b><br/>subagent slice<br/>(#3471 + #3488 merged)"]:::done
B["<b>Phase B</b><br/>bash bg pool<br/>(#3642 merged)"]:::done
C["<b>Phase C</b><br/>event monitor<br/>(#3684 merged)"]:::done
D["<b>Phase D</b><br/>auto-bg, sleep,<br/>Ctrl+B<br/>(#3809 + #3831 done)"]:::done
Beyond["<b>Beyond</b><br/>MCP / auto-mem /<br/>cloud / teams"]:::later
A --> B --> C --> D --> Beyond
Loading
The leftmost columns trace the planned rollout (what each phase was supposed to add); the rightmost Status (today) column shows the actual PRs that landed it and what's still pending.
✅ Done — #3836 (merged 2026-05-06, first Beyond-track PR). Brings system-initiated dream consolidation tasks into pill + dialog + task_stop. Successfully stress-tested the kind framework against the first non-user-initiated consumer.
Adjacent: MCP health pill (parallel pill, not a kind)
❌
❌
❌
❌
n/a
✅ Done — #3741 (deliberately not a kind extension; v1 visual only).
Phase A — finish the subagent slice cleanly(✅ merged: #3471 + #3488)
✅ shell.ts's legacy &-based path replaced with a managed registry entry (PID + output file + AbortController).
✅ Output streamed to a per-task file the agent can Read (under <projectTempDir>/background-shells/<sessionId>/).
✅ New entry kind shell; new /tasks slash command for text-mode inspection.
🟡 Pill / dialog integration + slash-command deprecation are post-merge follow-ups, see below.
Phase C — event monitor(✅ merged: #3684 by @doudouOUC — 2026-05-02)
New Monitor tool: long-running script whose stdout lines push events back to the agent (with sensible throttling).
New entry kind: monitor. Same pill / dialog reuse.
Pairs with a small ergonomics tweak in BashTool to suggest Monitor for tail -f / polling-loop intents.
Phase D — quality of life
✅ Long-running foreground bash advisory: merged at feat(core): hint to background long-running foreground bash commands #3809 (2026-05-04). When a foreground shell invocation runs past half its effective timeout (per-invocation, floored at 1000ms) and completes, both the LLM-facing tool result AND the user TUI get an advisory line suggesting is_background: true for similar long-running commands next time. Explicitly warns against re-running the just-completed command (matters for stateful operations like deploys / migrations / git push). Suppressed on cancel / timeout / external signal kill (their own messaging is enough). Hint also propagated through error.message (with a \n---\n divider for downstream parsers) so coreToolScheduler's error-branch routing doesn't drop it on slow-spawn-failure edges.
✅ Auto-memory / dream tasks surface + cancel: merged at feat(core,cli): surface and cancel auto-memory dream tasks #3836 (2026-05-06, +1714 / -100 across 16+ files; opened 2026-05-04). Brings system-initiated dream consolidation tasks (MemoryManager.scheduleDream) into the unified Background tasks UI. Footer pill counts dreams (e.g. 1 shell, 1 dream); combined dialog list shows [dream] memory consolidation reviewing N sessions; per-kind detail body shows sessions reviewing / progress text / topics touched / errors. Cancellation via dialog x keystroke + task_stop tool gets a 4th dispatch route. MemoryTaskStatus gains 'cancelled'; cancellation aborts the dream fork-agent and lets the existing runDreamfinally block release the consolidation lock as the agent unwinds. Behavior change for existing auto-memory users: previously dreams were invisible (only memory_saved toast on completion), now every fired dream surfaces in the footer pill — additive, no flag flip needed. Strategic significance: introduces dream as the 4th kind in the framework (after agent / shell / monitor) — the first system-initiated (not user-initiated) consumer, which the framework had to absorb without contract changes. Two review-fix passes in the same window addressed real bugs the integration surfaced: dreamLockReleaseFailed flag for same-session lock recovery (force-cleans the leaked lock on next scheduleDream); real cancelled-dream duration_ms for telemetry; AbortController-before-storeWith ordering (subscriber-during-storeWith race fix); gating-metadata write try/catch (no longer downgrades a successfully-completed dream to 'failed' on disk error); UI-surface lockReleaseError + metadataWriteError warnings on the DreamDetailBody; cancelled-dream telemetry event. Out of v1: live in-flight progress (phase flip / files-touched accumulation) — needs runForkedAgent's AgentPathParams extended with an onAssistantMessage callback rippling through 4 call sites; isolated as a separate PR for blast-radius. Intentionally excludes extract tasks (fire on every UserQuery → would flood pill; toast already covers; cancellation in the request loop would interfere with the user's own turn).
(still upstream of any PR) MCP health surfacing (already partially landed via feat(cli): add MCP health pill to footer #3741 as a parallel pill, not a kind), cloud-backed long tasks, multi-agent teaming — some intersect with existing Arena / MCP / headless work; tracked separately.
Parallel tracks
Not all remaining work blocks on #3471 / #3488. The non-integration core of each phase can start in parallel — only the "wire into unified pill / dialog / task_stop" pieces gate on Phase A.
flowchart LR
classDef indep fill:#d1ecf1,stroke:#0c5460,color:#0c5460
classDef gated fill:#f8d7da,stroke:#721c24,color:#721c24
subgraph Now["Independent — can start in parallel now"]
T1["Track 1<br/>bash bg pool<br/>feat/bash-bg-pool"]:::indep
T2["Track 2<br/>Monitor tool<br/>(#3666 in review)"]:::indep
T3["Track 3<br/>sleep interception<br/>BashTool only"]:::indep
end
subgraph Later["Gated by #3471 / #3488 merge"]
F1["shell + pill / dialog / task_stop"]:::gated
F2["monitor + pill / dialog"]:::gated
end
T1 --> F1
T2 --> F2
Loading
Track 1 — bash bg pool (Phase B core): ShellRegistry + spawn + per-task output file + /tasks slash command. Agent reads output via Read; no pill / dialog / task_stop integration in this PR. ✅ Merged: feat(core): managed background shell pool with /tasks command #3642.
When #3471 / #3488 land, each track spawns a small follow-up PR (~100-200 LOC) to wire shell/monitor entries into the unified pill / dialog / task_stop — purely additive once the renames in #3488 happen.
Aligned design points
These shape Phase A and everything built on top (full reasoning in the #3471 / #3488 review threads):
Unified entry type: BackgroundAgentEntry → BackgroundTaskEntry + kind discriminator. Renaming while consumers are few keeps future kind PRs purely additive.
Task-type-agnostic tools: task_stop / send_message description and errors aren't bound to agents from day one. Future shells / monitors / remote tasks cancel and steer through the same pair of tools.
Unified transcript path: <projectDir>/tasks/<sessionId>/<kind>-<id>.jsonl, so each new kind doesn't add a sibling directory.
Unified pill / dialog framework: getPillLabel is group-by-kind from Phase A; dialog's ListBody gains a section abstraction; DetailBody dispatches by kind to per-kind sub-components (AgentDetailBody / ShellDetailBody / ...).
Two-layer PR cleavage: each new task kind ships as "generic plumbing PR + per-kind extension follow-up."
Explicitly out of scope (v1)
These extensions look natural at a glance but are deliberately deferred — written down so contributors don't read silence as an open TODO. Each is an architecture-layer decision, not plumbing.
send_message → shell / monitor
Today task_stop covers all 3 kinds (agent / shell / monitor — see task-stop.ts:48-130 routing). send_message only routes to agents (send-message.ts:48 calls getBackgroundTaskRegistry() exclusively). PR#3684's "未做" follow-up list flagged monitor send_message as a remaining item, but on closer inspection this is architecture-layer, not a missed wire-up:
For agents: send_message queues a text instruction the agent picks up at the next tool-round boundary. Semantics are clear because agents are LLM-driven — they consume arbitrary text as a prompt-like instruction inside their reasoning loop.
For monitors / shells: there is no LLM in the receiver. A "text message" has no obvious consumption channel. The 3 plausible deliveries — write to the child's stdin / write to a sidecar .signals file the script polls / signal-based trigger — all require a receiver-side protocol the kind doesn't yet have.
Reference design — Claude Code (and what's actually transferable). SendMessage there doesn't bind to task type. From src-rust/crates/tools/src/send_message.rs:31:
A single global inbox keyed by recipient string. SendMessage doesn't know what kind the receiver is — it only writes to the inbox; receivers drain_inbox(my_id) themselves. This is the part that's relevant to the monitor / shell question — receiver-side abstraction is what would let non-agent kinds opt into receiving messages on their own terms.
The TS version also has a string | StructuredMessage payload (shutdown_request / shutdown_response / plan_approval_response, per spec/03_tools.md:902-909) routed through a useInboxPoller 1s-polling hook. However, this is not relevant to the monitor / shell question — the three StructuredMessage variants are agent ↔ agent / user ↔ agent protocol signals (gated by isAgentSwarmsEnabled()), driving Claude Code's multi-agent swarm coordination and plan-mode workflow. None is an agent ↔ non-LLM-receiver protocol. Conflating "structured payload" with "non-agent recipient" was an error in earlier drafts of this section — calling it out explicitly so future readers don't repeat it.
Two independent decisions if we ever extend send_message past agents (do not bundle them):
Receiver-side routing (the monitor / shell question) — adopt an inbox abstraction so kinds opt in by implementing acceptMessage(msg), instead of send_message fanning out via sender-side registry lookup. Refactor cost is real (touches all 3 registries + send_message); cost of Claude Code's specific design (1s polling) is real too — qwen-code is fully in-process so direct push would beat polling. The two cheaper alternatives stay open: (a) keep sender-side fan-out and define structured semantics per kind (monitor writes to a sidecar .signals file the script polls; shell writes to child stdin); (b) just say no.
Message shape (string vs string | StructuredMessage) — orthogonal to the routing question. Whether to take this on depends on whether qwen-code grows use cases analogous to Claude Code's swarm / plan-mode. Today none exist (no multi-agent swarm, no cross-process plan approval, no cross-machine session bridge), so introducing the union with a single { type: 'text', message } variant would be API-surface bloat with empty insides.
v1 decision: send_message stays agent-only at the runtime-routing layer, message stays string. This is consistent with — not in conflict with — aligned design point #2: that point commits to task-type-agnostic public API (send_message's description and errors don't say "agent", so future shell / monitor support is a non-breaking addition). What we're deferring is the actual runtime routing behind the API, where the receiver-side semantic for non-agent kinds is the unsettled question. task_stop is fully wired across all 3 kinds because its receiver-side semantic is unambiguous (terminate the entry); send_message's isn't.
The two decisions above only get made when concrete use cases appear (some that would unblock decision #1: "agent wants to nudge a long-running monitor to flush early", "agent wants a SIGUSR1-equivalent on a managed bash watcher", "agent wants the dev server to reload config without a restart"; decision #2 only gets made if/when qwen-code grows swarm / plan-mode / cross-process workflows of its own — copying the union for symmetry with Claude Code is the wrong reason). Don't pre-build on speculation.
Resume / continuation extension: ✅ Add background agent resume and continuation #3739 by @doudouOUC (2026-05-01, +4087 / -165 across 40 files). Adds persisted background-agent resume, paused-status entries for interrupted agents, and explicit resume / abandon flows (the dialog's r / x keybinds). Also rebuilds fork-agent resume on a transcript-first design so resumed forks preserve their original worker constraints.
Foreground subagents → pill+dialog routing: ✅ merged: feat(cli): route foreground subagents through pill+dialog while running #3768 by @tanzhenxin (2026-05-06, +1008 / -108). While a foreground (synchronous) Agent tool call is running, the inline AgentExecutionDisplay is suppressed and the run is surfaced through the existing pill + BackgroundTasksDialog. After the parent turn commits, the full frame appears in scrollback unchanged. Removes a flicker class entirely while reusing the pill+dialog rendering surface — first non-background consumer of the kind framework now in main.
Shell ↔ task_stop integration — ✅ merged: feat(core): wire background shells into the task_stop tool #3687. Threads BackgroundShellRegistry.requestCancel() into the unified task_stop tool's lookup so the model stops a bg_xxx shell the same way it stops a subagent. Cancel only aborts; spawn settle path records the real terminal state.
Shell entries in combined dialog — ✅ merged: feat(cli): wire background shells into combined Background tasks dialog #3720 (2026-04-29, +500 / -100 across 8 files). Wires shells into the combined pill + dialog: adds observer hooks (setStatusChangeCallback / setRegisterCallback) and requestCancel(id) to BackgroundShellRegistry; merges agent + shell entries via a kind discriminator (DialogEntry union); getPillLabel groups by kind ("2 shells, 1 local agent"); ListBody renders [shell] <command> rows; DetailBody dispatches by kind to AgentDetailBody (existing) or new ShellDetailBody; cancelSelected routes by kind. Includes a follow-up fix for the snapshot/registry shallow-copy that was detaching recentActivities between status changes — the dialog now re-resolves the selected agent from the registry on each render via a useMemo keyed on an activityTick, so recentActivities and stats stay live.
/tasks slash command repositioning — ✅ merged: feat(cli): include monitors in /tasks + add interactive-mode hint #3801 (2026-05-03). Of the three options the issue debated (delete / keep as fallback / add deprecation hint), the PR picks the keep + add-hint hybrid. Not deleted because /tasks is the only inspection path for non_interactive (-p), acp (IDE bridges), and SDK consumers — they have no TTY for the dialog. Reframed as a surface redirect: in interactive mode the output gets a Tip: focus the Background tasks pill in the footer (use ↓ from an empty composer) and press Enter ... line; in non-TTY modes the bare list is the canonical surface. Also bug-fixes a stealth gap from feat(core): event monitor tool with throttled stdout streaming (Phase C) #3684 / feat(cli): wire Monitor entries into combined Background tasks dialog #3791 — the command was missing monitors entirely. Output sanitization tightened (escapeAnsiCtrlCodes → stripUnsafeCharacters) per @doudouOUC's review to close the C0/C1 control-byte gap.
Phase C follow-up: ✅ merged at feat(cli): wire Monitor entries into combined Background tasks dialog #3791 (2026-05-03). Wires monitor entries into the combined Background tasks dialog (mirrors feat(cli): wire background shells into combined Background tasks dialog #3720 for the third kind). Adds setStatusChangeCallback to MonitorRegistry, extends the DialogEntry union with kind: 'monitor', gives the pill / dialog / cancelSelected a monitor branch, and a new MonitorDetailBody showing command / pid / event count / dropped lines / exit code / error reason. The kind framework now has three real consumers (agent / shell / monitor) — what turns "generic framework" from claim to evidence.
Token-bucket clock-drift + AST logging + UI routing dedupe: ✅ merged: fix(core): address post-merge monitor tool and UI routing issues #3792 by @doudouOUC (2026-05-04, +199 / -199 across 14 files). (a) Monitor's token bucket guards against Date.now() jumping backward after system suspend/resume (would otherwise make elapsed negative and starve the bucket). (b) AST parse-failure path in getConfirmationDetails was silent — added debugLogger.warn. (c) UI routing duplication fix from the agent tool display feature.
Adjacent UX (not part of the kind framework): MCP health pill ✅ merged at feat(cli): add MCP health pill to footer #3741 (2026-05-02). Surfaces MCP servers stuck in DISCONNECTED next to the Background tasks pill via a parallel pill (deliberately not a kind extension — MCP connection state has no terminal status, would dilute the registry contract). v1 visual indicator only; Down-arrow focus chain deferred.
Background
Today qwen-code's
shelltool can run a command in background by appending&(tools/shell.tsis_background: true), but the spawned process effectively leaks: there's no registry, no way to query status or read output afterwards, no way for the agent to terminate it later. For long-running things — dev servers, builds, file watchers, log tailing, polling jobs — this turns the agent into a poor manager: it can fire-and-forget, but it can't observe or steer. The user can't see what's running either.The three subagent PRs (#3076 ✅ + #3471 ✅ + #3488 ✅) build the first complete answer to this problem for one kind of background work (subagents). The infrastructure they introduce — a background-task registry, a model-facing control surface, a user-facing pill + dialog — is naturally general. The notes below sketch how that frame extends to other kinds.
Current state
Agenttool +run_in_background: true)task_stop,send_message, per-agent transcriptBackgroundShellRegistry,/taskscommandtask_stoptoolMonitor(...)permission namespace (the deferred item from #3684 review)kindextension)taskkillspawn assertion stdio option/tasksrepositioning: include monitors + interactive-mode hint pointing at the dialog (kept for non-TTY consumers, not deleted)/tasksAND the interactive Background tasks dialog (LLM-facing strings + 4 stale comments)is_background: truenext time after a foreground command exceeds half its effective timeoutdreamas the 4th kind in the framework — agent / shell / monitor / dream); footer pill counts dreams, dialog list shows[dream] memory consolidation reviewing N sessions, per-kind detail body, dialogxcancel +task_stop4th dispatch route,MemoryTaskStatusgains'cancelled'. Two review-fix passes landed bugs in the same window: lockReleaseError same-session recovery (dreamLockReleaseFailedflag force-cleans the leaked lock on next scheduleDream), real cancelled-dreamduration_msfor telemetry, AbortController-before-storeWith ordering (race fix), gating-metadata try/catch (no longer downgrades a successful dream to 'failed' on disk error), UI-surfacelockReleaseError+metadataWriteErrorwarnings. Out-of-scope (planned follow-up): live in-flight progress (phase flip / files-touched accumulation) — needsrunForkedAgent'sAgentPathParamsextended withonAssistantMessagecallback rippling through 4 call sites. Intentionally excludesextracttasks (too noisy; toast already covers; cancellation would interfere with the request loop).ShellAbortReason = { kind: 'cancel' } | { kind: 'background'; shellId? }union the AbortSignal carries; default behavior unchanged tree-kill,{ kind: 'background' }is a takeover signal —execute()skips kill, drops child from active set, flushes output snapshot, resolves Promise immediately withpromoted: true.ShellExecutionResultgains optionalpromoted?: boolean. Both abort handlers updated (executeWithPty+childProcessFallback); exhaustive switch with_exhaustive: neverdefault;getShellAbortReasonKindhelper exported with prototype-pollution defense. Pure plumbing, zero behavior change for existing callers. 5 review rounds + 1 self-audit pass → 22 review threads + 1 real bug found by reviewer (@lydell/node-ptyexposesremoveListener, notoffalias). 70 / 70 tests pass.Object.prototype.hasOwnProperty.call(reason, 'kind')was outside the helper's try/catch, so a Proxy with a throwinggetOwnPropertyDescriptortrap would propagate past the helper, then through the abort handler's switch, then out of the (async) abort listener — leaving the shell process alive instead of being killed on cancel. Wrapped both the descriptor probe and the value read in the same try block + regression test. (2) PTY handoff-boundary test parity — re-invoke the productiononDatacallback AFTER background-abort and assertonOutputEventMock.mock.calls.length === 0, exercising thelistenersDetachedguard in the chain callback. Note 1 (aborted: true + promoted: trueshape — reviewer suggestedaborted: falsewhenpromoted: true) deferred to PR-2 as design question 7 in #3831.shell.tsintegration: foreground execute path detectsresult.promoted: true, snapshots output tobg_xxx.outputunder the project temp dir, registers aBackgroundShellEntrywith the running pid + a freshAbortControllerwhose abort listener kills the still-running child via SIGTERM → 200ms → SIGKILL (matching the service-side cancel cascade), returns a model-facingToolResultpointing the agent at/tasks/ Background tasks dialog /task_stop.coreToolSchedulernow stashespromoteAbortControlleronTrackedExecutingToolCallso PR-3's keybind can find it. Resolves PR-1 design question 7 (aborted: falsewhenpromoted: trueso existingif (result.aborted)consumer branches fall through naturally). 6 review rounds → multiple Critical fixes (mkdir orphan, refused-promote race reported as timeout,commandToExecuteco-author rewrite). 169 / 169 shell tests pass. Limitations deferred to PR-2.5: post-promote stream redirect (output snapshot frozen at promote time) + natural-exit registry settle (entry stays'running'untiltask_stop/ session-end).Command.PROMOTE_SHELL_TO_BACKGROUNDkeybind bound to Ctrl+B.useReactToolScheduler.tsprojectspromoteAbortControllerfrom core'sExecutingToolCallthroughTrackedExecutingToolCall(with compile-timeextends keyofassertions to fail loud + local on a future core rename).AppContainer.handleGlobalKeypressadds the branch that walkspendingToolCallsRef.current, finds the executing shell tool call (gated on bothtc.request.name === ToolNames.SHELLANDpromoteAbortController !== undefinedfor defense-in-depth), calls.abort({ kind: 'background' }), returns early. No-op + fall-through when no foreground shell is executing — input layer's existing Ctrl+B (cursor-left) still fires. Docs:keyboard-shortcuts.mdentry with the fall-through note. Tests: 4 keyMatchers + 3 AppContainer integration (promote / no-op / non-shell-tool guard). 60 / 60 tests pass. Closes the 3-PR sequence for #3831 (Phase D part b of #3634).Together these provide a complete vertical slice for one background-task kind (subagents). The naming and shape (
BackgroundAgentEntry,BackgroundTaskRegistry, "Background tasks" dialog title) already hint at the broader frame.Direction we're taking
Background subagents are one task kind among several. The same registry, control tools, and UI naturally host:
npm run dev,pytest, build commands, watchers as managed processes with output capture, lifecycle queries, and explicit terminationtail -F,inotifywait, polling loops) that push events back to the agent so it can react asynchronouslyEvery kind follows the same five elements:
task_stoptoolsend_messagetoolEach new kind is purely additive — that's the structural payoff of getting #3471 / #3488's interfaces right.
Architecture: current → target
Current —
shell.tsbackground path is fully detached and invisible; subagent has its own agent-only registry/UI:flowchart LR classDef leak fill:#f8d7da,stroke:#721c24,color:#721c24 classDef ok fill:#d4edda,stroke:#155724,color:#155724 classDef partial fill:#fff3cd,stroke:#856404,color:#856404 Agent([LLM Agent]) ShellTool["shell.ts<br/>is_background: true<br/>+ '&' fork-detach"]:::leak AgentTool["Agent tool<br/>run_in_background: true<br/>(#3076 ✅)"]:::ok Void["output lost<br/>no kill / no observe"]:::leak Reg["BackgroundAgentRegistry<br/>agent-only"]:::partial Pill["status pill<br/>agent count only"]:::partial Dialog["dialog<br/>agents only"]:::partial Agent --> ShellTool Agent --> AgentTool ShellTool -.-> Void AgentTool --> Reg Reg --> Pill Reg --> DialogTarget — every background work routes through the same registry, surfaces in the same UI, controlled by the same tools:
flowchart LR classDef done fill:#d4edda,stroke:#155724,color:#155724 classDef new fill:#cfe2ff,stroke:#0d6efd,color:#084298 classDef later fill:#f8f9fa,stroke:#6c757d,color:#495057 Agent([LLM Agent]) Tools["task_stop / send_message<br/>(task-type-agnostic)"]:::done ShellTool["shell.ts<br/>(managed bg path)"]:::new MonitorTool["Monitor tool<br/>(new)"]:::new AgentTool["Agent tool"]:::done More["...mcp_health / dream /<br/>remote / teammate"]:::later Reg["<b>BackgroundTaskRegistry</b><br/>kind: shell / monitor / agent / ..."]:::done Pill["status pill<br/>per-kind counts"]:::done Dialog["combined dialog<br/>section per kind"]:::done Agent --> Tools Agent --> ShellTool Agent --> MonitorTool Agent --> AgentTool Tools --> Reg ShellTool --> Reg MonitorTool --> Reg AgentTool --> Reg More -.-> Reg Reg --> Pill Reg --> DialogThe shape change is small: keep the same
BackgroundTaskRegistryalready introduced for subagents, generalize its entry type with akinddiscriminator, route shell + monitor + future kinds through the same gate. The user-facing surface (pill + dialog) and the model-facing surface (task_stop/send_message) become a small framework that hosts every kind.Sequencing
flowchart LR classDef done fill:#d4edda,stroke:#28a745,color:#155724 classDef inflight fill:#fff3cd,stroke:#ffc107,color:#856404 classDef next fill:#cfe2ff,stroke:#0d6efd,color:#084298 classDef later fill:#f8f9fa,stroke:#6c757d,color:#495057 A["<b>Phase A</b><br/>subagent slice<br/>(#3471 + #3488 merged)"]:::done B["<b>Phase B</b><br/>bash bg pool<br/>(#3642 merged)"]:::done C["<b>Phase C</b><br/>event monitor<br/>(#3684 merged)"]:::done D["<b>Phase D</b><br/>auto-bg, sleep,<br/>Ctrl+B<br/>(#3809 + #3831 done)"]:::done Beyond["<b>Beyond</b><br/>MCP / auto-mem /<br/>cloud / teams"]:::later A --> B --> C --> D --> BeyondThe leftmost columns trace the planned rollout (what each phase was supposed to add); the rightmost Status (today) column shows the actual PRs that landed it and what's still pending.
&/tasks) + #3687 (task_stop↔ shell) + #3720 (shell ↔ dialog) + #3808 (doc cleanup).Monitor(...)permission namespace) + #3784 (Windowstaskkilltest fix) + #3792 (token-bucket clock-drift / AST log / UI dedupe).signal.reasonchannel. tmux-style: foreground shell mid-flight → Ctrl+B → child keeps running, becomes aBackgroundShellEntryvisible in/tasks/ dialog /task_stop. Limitations deferred to PR-2.5 (post-promote stream redirect + natural-exit registry settle).task_stop. Successfully stress-tested the kind framework against the first non-user-initiated consumer.kind)kindextension; v1 visual only).Phase A — finish the subagent slice cleanly (✅ merged: #3471 + #3488)
task_stop/send_message/ per-agent transcript with the task-type-agnostic generalizations from review.BackgroundAgent*→BackgroundTask*rename and dialog test coverage from review.Phase B — bash background pool (✅ merged: #3642)
shell.ts's legacy&-based path replaced with a managed registry entry (PID + output file + AbortController).Read(under<projectTempDir>/background-shells/<sessionId>/).shell; new/tasksslash command for text-mode inspection.Phase C — event monitor (✅ merged: #3684 by @doudouOUC — 2026-05-02)
Monitortool: long-running script whose stdout lines push events back to the agent (with sensible throttling).monitor. Same pill / dialog reuse.BashToolto suggestMonitorfortail -f/ polling-loop intents.Phase D — quality of life
shellinvocation runs past half its effective timeout (per-invocation, floored at 1000ms) and completes, both the LLM-facing tool result AND the user TUI get an advisory line suggestingis_background: truefor similar long-running commands next time. Explicitly warns against re-running the just-completed command (matters for stateful operations like deploys / migrations /git push). Suppressed on cancel / timeout / external signal kill (their own messaging is enough). Hint also propagated througherror.message(with a\n---\ndivider for downstream parsers) socoreToolScheduler's error-branch routing doesn't drop it on slow-spawn-failure edges.bg_xxx.output+BackgroundShellRegistryregistration with kill listener; resolves design Q7aborted:false when promoted:true; 2026-05-08, +935/-15) + PR-3 feat(cli): Ctrl+B promote keybind (#3831 PR-3 of 3) #3969 (Ctrl+B keybind + AppContainer wire-up + E2E tests +keyboard-shortcuts.mddocs; 2026-05-11, +308/-15). tmux-style: foreground shell mid-flight → Ctrl+B → child process keeps running (not killed), promoted to aBackgroundShellEntryvisible in/tasks/ dialog /task_stop. Limitations deferred to PR-2.5 (post-promote stream redirect — output snapshot frozen at promote time today; natural-exit registry settle — entry stays'running'untiltask_stop/ session-end).BashToolblocks baresleep Nand suggests the right alternative — already merged with feat(core): event monitor tool with throttled stdout streaming (Phase C) #3684 (sleep interception bundled into Phase C).Beyond — first item now in flight:
MemoryManager.scheduleDream) into the unified Background tasks UI. Footer pill counts dreams (e.g.1 shell, 1 dream); combined dialog list shows[dream] memory consolidation reviewing N sessions; per-kind detail body shows sessions reviewing / progress text / topics touched / errors. Cancellation via dialogxkeystroke +task_stoptool gets a 4th dispatch route.MemoryTaskStatusgains'cancelled'; cancellation aborts the dream fork-agent and lets the existingrunDreamfinallyblock release the consolidation lock as the agent unwinds. Behavior change for existing auto-memory users: previously dreams were invisible (onlymemory_savedtoast on completion), now every fired dream surfaces in the footer pill — additive, no flag flip needed. Strategic significance: introducesdreamas the 4th kind in the framework (after agent / shell / monitor) — the first system-initiated (not user-initiated) consumer, which the framework had to absorb without contract changes. Two review-fix passes in the same window addressed real bugs the integration surfaced:dreamLockReleaseFailedflag for same-session lock recovery (force-cleans the leaked lock on next scheduleDream); real cancelled-dreamduration_msfor telemetry; AbortController-before-storeWith ordering (subscriber-during-storeWith race fix); gating-metadata write try/catch (no longer downgrades a successfully-completed dream to 'failed' on disk error); UI-surfacelockReleaseError+metadataWriteErrorwarnings on the DreamDetailBody; cancelled-dream telemetry event. Out of v1: live in-flight progress (phase flip / files-touched accumulation) — needsrunForkedAgent'sAgentPathParamsextended with anonAssistantMessagecallback rippling through 4 call sites; isolated as a separate PR for blast-radius. Intentionally excludesextracttasks (fire on every UserQuery → would flood pill; toast already covers; cancellation in the request loop would interfere with the user's own turn).kind), cloud-backed long tasks, multi-agent teaming — some intersect with existing Arena / MCP / headless work; tracked separately.Parallel tracks
Not all remaining work blocks on #3471 / #3488. The non-integration core of each phase can start in parallel — only the "wire into unified pill / dialog /
task_stop" pieces gate on Phase A.flowchart LR classDef indep fill:#d1ecf1,stroke:#0c5460,color:#0c5460 classDef gated fill:#f8d7da,stroke:#721c24,color:#721c24 subgraph Now["Independent — can start in parallel now"] T1["Track 1<br/>bash bg pool<br/>feat/bash-bg-pool"]:::indep T2["Track 2<br/>Monitor tool<br/>(#3666 in review)"]:::indep T3["Track 3<br/>sleep interception<br/>BashTool only"]:::indep end subgraph Later["Gated by #3471 / #3488 merge"] F1["shell + pill / dialog / task_stop"]:::gated F2["monitor + pill / dialog"]:::gated end T1 --> F1 T2 --> F2ShellRegistry+spawn+ per-task output file +/tasksslash command. Agent reads output viaRead; no pill / dialog /task_stopintegration in this PR. ✅ Merged: feat(core): managed background shell pool with /tasks command #3642.Monitortool with throttling and self-stop. Push notifications already supported via feat: background subagents with headless and SDK support #3076'stask_notificationframework. ✅ Merged: feat(core): event monitor tool with throttled stdout streaming (Phase C) #3684 by @doudouOUC (+6297 / -147, includes sleep interception).detectBlockedSleepPatterncheck inBashToolplus a guidance message pointing torun_in_background: trueor the new Monitor. ✅ Merged together with feat(core): event monitor tool with throttled stdout streaming (Phase C) #3684 (Phase C).When #3471 / #3488 land, each track spawns a small follow-up PR (~100-200 LOC) to wire shell/monitor entries into the unified pill / dialog /
task_stop— purely additive once the renames in #3488 happen.Aligned design points
These shape Phase A and everything built on top (full reasoning in the #3471 / #3488 review threads):
BackgroundAgentEntry→BackgroundTaskEntry+kinddiscriminator. Renaming while consumers are few keeps future kind PRs purely additive.task_stop/send_messagedescription and errors aren't bound to agents from day one. Future shells / monitors / remote tasks cancel and steer through the same pair of tools.<projectDir>/tasks/<sessionId>/<kind>-<id>.jsonl, so each new kind doesn't add a sibling directory.getPillLabelis group-by-kind from Phase A; dialog'sListBodygains a section abstraction;DetailBodydispatches bykindto per-kind sub-components (AgentDetailBody/ShellDetailBody/ ...).Explicitly out of scope (v1)
These extensions look natural at a glance but are deliberately deferred — written down so contributors don't read silence as an open TODO. Each is an architecture-layer decision, not plumbing.
send_message→ shell / monitorToday
task_stopcovers all 3 kinds (agent / shell / monitor — seetask-stop.ts:48-130routing).send_messageonly routes to agents (send-message.ts:48callsgetBackgroundTaskRegistry()exclusively). PR#3684's "未做" follow-up list flagged monitorsend_messageas a remaining item, but on closer inspection this is architecture-layer, not a missed wire-up:send_messagequeues a text instruction the agent picks up at the next tool-round boundary. Semantics are clear because agents are LLM-driven — they consume arbitrary text as a prompt-like instruction inside their reasoning loop..signalsfile the script polls / signal-based trigger — all require a receiver-side protocol the kind doesn't yet have.Reference design — Claude Code (and what's actually transferable). SendMessage there doesn't bind to task type. From
src-rust/crates/tools/src/send_message.rs:31:A single global inbox keyed by recipient string. SendMessage doesn't know what kind the receiver is — it only writes to the inbox; receivers
drain_inbox(my_id)themselves. This is the part that's relevant to the monitor / shell question — receiver-side abstraction is what would let non-agent kinds opt into receiving messages on their own terms.The TS version also has a
string | StructuredMessagepayload (shutdown_request/shutdown_response/plan_approval_response, perspec/03_tools.md:902-909) routed through auseInboxPoller1s-polling hook. However, this is not relevant to the monitor / shell question — the threeStructuredMessagevariants are agent ↔ agent / user ↔ agent protocol signals (gated byisAgentSwarmsEnabled()), driving Claude Code's multi-agent swarm coordination and plan-mode workflow. None is an agent ↔ non-LLM-receiver protocol. Conflating "structured payload" with "non-agent recipient" was an error in earlier drafts of this section — calling it out explicitly so future readers don't repeat it.Two independent decisions if we ever extend
send_messagepast agents (do not bundle them):acceptMessage(msg), instead ofsend_messagefanning out via sender-side registry lookup. Refactor cost is real (touches all 3 registries +send_message); cost of Claude Code's specific design (1s polling) is real too — qwen-code is fully in-process so direct push would beat polling. The two cheaper alternatives stay open: (a) keep sender-side fan-out and define structured semantics per kind (monitor writes to a sidecar.signalsfile the script polls; shell writes to child stdin); (b) just say no.stringvsstring | StructuredMessage) — orthogonal to the routing question. Whether to take this on depends on whether qwen-code grows use cases analogous to Claude Code's swarm / plan-mode. Today none exist (no multi-agent swarm, no cross-process plan approval, no cross-machine session bridge), so introducing the union with a single{ type: 'text', message }variant would be API-surface bloat with empty insides.v1 decision:
send_messagestays agent-only at the runtime-routing layer, message stays string. This is consistent with — not in conflict with — aligned design point #2: that point commits to task-type-agnostic public API (send_message's description and errors don't say "agent", so future shell / monitor support is a non-breaking addition). What we're deferring is the actual runtime routing behind the API, where the receiver-side semantic for non-agent kinds is the unsettled question.task_stopis fully wired across all 3 kinds because its receiver-side semantic is unambiguous (terminate the entry);send_message's isn't.The two decisions above only get made when concrete use cases appear (some that would unblock decision #1: "agent wants to nudge a long-running monitor to flush early", "agent wants a SIGUSR1-equivalent on a managed bash watcher", "agent wants the dev server to reload config without a restart"; decision #2 only gets made if/when qwen-code grows swarm / plan-mode / cross-process workflows of its own — copying the union for symmetry with Claude Code is the wrong reason). Don't pre-build on speculation.
Next steps
ListBodysection abstraction,DetailBodyper-kind dispatch,getPillLabelgroup-by-kind,BackgroundAgent*→BackgroundTask*rename. Those land as part of Phase B follow-up Where is the config saved? #2 below, since that's the first PR that needs them.)r/xkeybinds). Also rebuilds fork-agent resume on a transcript-first design so resumed forks preserve their original worker constraints.AgentExecutionDisplayis suppressed and the run is surfaced through the existing pill +BackgroundTasksDialog. After the parent turn commits, the full frame appears in scrollback unchanged. Removes a flicker class entirely while reusing the pill+dialog rendering surface — first non-background consumer of the kind framework now in main.task_stopintegration — ✅ merged: feat(core): wire background shells into the task_stop tool #3687. ThreadsBackgroundShellRegistry.requestCancel()into the unifiedtask_stoptool's lookup so the model stops abg_xxxshell the same way it stops a subagent. Cancel only aborts; spawn settle path records the real terminal state.setStatusChangeCallback/setRegisterCallback) andrequestCancel(id)toBackgroundShellRegistry; merges agent + shell entries via akinddiscriminator (DialogEntryunion);getPillLabelgroups by kind ("2 shells, 1 local agent"); ListBody renders[shell] <command>rows; DetailBody dispatches by kind toAgentDetailBody(existing) or newShellDetailBody;cancelSelectedroutes by kind. Includes a follow-up fix for the snapshot/registry shallow-copy that was detachingrecentActivitiesbetween status changes — the dialog now re-resolves the selected agent from the registry on each render via auseMemokeyed on anactivityTick, sorecentActivitiesandstatsstay live./tasksslash command repositioning — ✅ merged: feat(cli): include monitors in /tasks + add interactive-mode hint #3801 (2026-05-03). Of the three options the issue debated (delete / keep as fallback / add deprecation hint), the PR picks the keep + add-hint hybrid. Not deleted because/tasksis the only inspection path fornon_interactive(-p),acp(IDE bridges), and SDK consumers — they have no TTY for the dialog. Reframed as a surface redirect: in interactive mode the output gets aTip: focus the Background tasks pill in the footer (use ↓ from an empty composer) and press Enter ...line; in non-TTY modes the bare list is the canonical surface. Also bug-fixes a stealth gap from feat(core): event monitor tool with throttled stdout streaming (Phase C) #3684 / feat(cli): wire Monitor entries into combined Background tasks dialog #3791 — the command was missing monitors entirely. Output sanitization tightened (escapeAnsiCtrlCodes → stripUnsafeCharacters) per @doudouOUC's review to close the C0/C1 control-byte gap.shell.ts(post-spawn) andtask-stop.ts(post-cancel) to mention BOTH/tasksAND the interactive Background tasks dialog as inspection paths. Also fixes 4 related stale comments / docstrings (including a real misnomer inmonitorRegistry.tsthat called the dialog/tasks dialog). Pure docs, +10 / -8.Monitortool with throttled stdout streaming, theBashToolsleep-interception heuristic, and supporting plumbing. Mirror follow-ups now unblocked:task_stopintegration — wireMonitorentries intotask_stoplookup so the model stops a monitor the same way it stops a subagent or shell. Mirrors feat(core): wire background shells into the task_stop tool #3687.DialogEntryunion with akind: 'monitor'variant and aMonitorDetailBody. Mirrors feat(cli): wire background shells into combined Background tasks dialog #3720; should be smaller since the kind framework is already in place.setStatusChangeCallbacktoMonitorRegistry, extends theDialogEntryunion withkind: 'monitor', gives the pill / dialog /cancelSelecteda monitor branch, and a newMonitorDetailBodyshowing command / pid / event count / dropped lines / exit code / error reason. The kind framework now has three real consumers (agent / shell / monitor) — what turns "generic framework" from claim to evidence.Monitor(...)permission namespace: ✅ feat(core): add Monitor(...) permission namespace #3726 by @doudouOUC (2026-04-29). The deferred item from the feat(core): event monitor tool with throttled stdout streaming (Phase C) #3684 review thread — Monitor was emittingBash(...)rules so "Always Allow" failed for future monitor invocations while leaking shell permission. Introduces a dedicatedMonitor(...)namespace +SHELL_LIKE_TOOLSset inpermission-manager.tsso all shell-like evaluation paths handle bothrun_shell_commandandmonitor.taskkilltest fix: ✅ fix(monitor): correct Windows taskkill spawn assertion to include stdio option #3784 by @Copilot (2026-05-02, +15 / -18). Trivial — implementation passesspawn('taskkill', [...], { stdio: 'ignore' })but tests asserted only two arguments → all Windows CI jobs failed exact-match.Date.now()jumping backward after system suspend/resume (would otherwise makeelapsednegative and starve the bucket). (b) AST parse-failure path ingetConfirmationDetailswas silent — addeddebugLogger.warn. (c) UI routing duplication fix from the agent tool display feature.DISCONNECTEDnext to the Background tasks pill via a parallel pill (deliberately not akindextension — MCP connection state has no terminal status, would dilute the registry contract). v1 visual indicator only; Down-arrow focus chain deferred.中文版
背景
当前 qwen-code 的
shell工具支持后台执行(tools/shell.ts里的is_background: true),实现方式是在命令尾加&,但 spawn 出去的进程基本上是泄漏的:没有 registry、没有办法事后查询状态或读取输出、agent 也无法之后终止它。对于长任务 — dev server、build、文件 watcher、日志 tail、轮询任务 — 这让 agent 变成一个糟糕的管理者:它能 fire-and-forget,但无法观察或控制。用户也看不到正在跑什么。三个 subagent PR(#3076 ✅ + #3471 ✅ + #3488 ✅)为一种后台工作(subagent)构建第一个完整的答案。它们引入的基础设施 — background-task registry、model-facing 控制面、user-facing pill + dialog — 天然是通用的。下面是把这个 frame 延伸到其他 kind 的笔记。
当前进展
Agent工具 +run_in_background: true)task_stop、send_message、per-agent transcriptBackgroundShellRegistry、/tasks命令task_stop工具kind扩展)/tasks重新定位:加 monitor + interactive-mode hint 指向 dialog(保留给非 TTY 消费者,不删)/tasks和交互式 Background tasks dialog(LLM 字符串 + 4 处 stale 注释)is_background: truedream作为框架第 4 种 kind — agent / shell / monitor / dream);footer pill 计 dream,dialog 显示[dream] memory consolidation reviewing N sessions,per-kind detail body,dialogxcancel +task_stop第 4 路 dispatch,MemoryTaskStatus加'cancelled'。同窗 2 轮 review-fix 提交解掉真 bug:dreamLockReleaseFailedflag 同 session 锁恢复(下次 scheduleDream 强制 clean 残留锁),cancelled dream 真实duration_ms给 telemetry,AbortController-before-storeWith ordering(race 修),gating-metadata try/catch(不再因磁盘错把成功 dream 降级为 'failed'),UI surfacelockReleaseError+metadataWriteErrorwarning。Out-of-scope(计划 follow-up):live in-flight progress(phase flip / files-touched 累积)— 需要扩runForkedAgent的AgentPathParams加onAssistantMessagecallback,涉及 4 个调用点。故意排除extract任务(每次 UserQuery fire,会洪 pill;toast 已覆盖;request loop 内 cancel 会干扰用户当前 turn)。ShellAbortReason = { kind: 'cancel' } | { kind: 'background'; shellId? }让 AbortSignal 携带;默认行为不变 tree-kill,{ kind: 'background' }是 takeover 信号 —execute()跳 kill、从 active 集合 delete child、flush output snapshot、立即 resolve Promise 带promoted: true。ShellExecutionResult加可选字段promoted?: boolean。两个 abort handler 都改(executeWithPty+childProcessFallback),用穷尽 switch +_exhaustive: never;导出getShellAbortReasonKindhelper 含 prototype-pollution 防御。纯 plumbing,对现有调用方零行为变化。5 轮 review + 1 自审 → 22 review threads + 1 真 bug 由 reviewer 抓(@lydell/node-pty暴露removeListener不是off)。70 / 70 测试 pass。Object.prototype.hasOwnProperty.call(reason, 'kind')在 helper 的 try/catch 之外,Proxy 带 throwinggetOwnPropertyDescriptortrap 会穿透 helper 经 abort handler switch 出 (async) abort listener — 导致 shell 进程留活而非被 kill。把 descriptor probe 跟 value read 都包进 try/catch + 加回归测试。(2) PTY handoff-boundary 测试 parity — abort 后再调 productiononDatacallback,断言onOutputEventMock.mock.calls.length === 0,exercise chain callback 内listenersDetachedguard。Note 1(aborted: true + promoted: trueshape — reviewer 建议promoted: true时aborted: false)延后到 PR-2,作为 #3831 design question 7。shell.ts集成:前台 execute 路径检测result.promoted: true,把输出 snapshot 到 project temp dir 下的bg_xxx.output,注册BackgroundShellEntry(带运行中 pid + 全新AbortController,其 abort listener 通过 SIGTERM → 200ms → SIGKILL 杀仍在跑的 child,对齐 service 侧 cancel cascade),返回 model 可见的ToolResult指引 agent 看/tasks/ Background tasks dialog /task_stop。coreToolScheduler把promoteAbortController挂在TrackedExecutingToolCall上让 PR-3 的 keybind 能拿到。解决 PR-1 design question 7(promoted: true时aborted: false让现有if (result.aborted)消费分支自然 fall through)。6 轮 review → 多个 Critical fix(mkdir orphan、refused-promote race 误报为 timeout、commandToExecuteco-author 改写)。169/169 shell 测试 pass。 限制 defer 到 PR-2.5:post-promote stream redirect(输出 snapshot 在 promote 时刻冻结)+ natural-exit registry settle(entry 维持'running'直到task_stop/ session-end)。Command.PROMOTE_SHELL_TO_BACKGROUND键位绑定 Ctrl+B。useReactToolScheduler.ts把 core 的ExecutingToolCall.promoteAbortController投影到TrackedExecutingToolCall(compile-timeextends keyof断言锁定,core 改名时 React 侧 build 立刻 fail)。AppContainer.handleGlobalKeypress加分支:从pendingToolCallsRef.current找正在执行的 shell tool call(同时 gatetc.request.name === ToolNames.SHELLANDpromoteAbortController !== undefined双重防御),调.abort({ kind: 'background' }),return 早退。没在执行 shell 时 no-op + fall-through —— 输入层既有 Ctrl+B(光标左移)继续生效。文档:keyboard-shortcuts.md加条目说明 fall-through 行为。 测试:4 个 keyMatchers + 3 个 AppContainer 集成(promote / no-op / 非 shell tool guard)。60/60 测试 pass。 闭合 #3831 三 PR 序列(#3634 Phase D 第 b 部分)。三者合起来为一种 background task kind(subagent)提供了完整垂直切片。命名和形状(
BackgroundAgentEntry、BackgroundTaskRegistry、dialog 标题 "Background tasks")已经隐含了更广的 framing。我们正在走的方向
把 background subagent 视为多种 task kind 中的一种。同一套 registry、控制工具、UI 自然地承载:
npm run dev、pytest、build 命令、watcher spawn 成被管理的进程,带输出捕获、生命周期查询、显式终止tail -F、inotifywait、轮询循环)把事件 push 回 agent,让 agent 可异步响应每种 kind 都遵循同样 5 个要素:
task_stop工具取消send_message工具向运行中的任务下指令每种新 kind 都是纯增量 — 这是把 #3471 / #3488 的接口现在做对的结构性收益。
架构演进:现状 → 目标
现状 ——
shell.ts后台路径完全 detach 不可见;subagent 有自己 agent-only 的 registry/UI:flowchart LR classDef leak fill:#f8d7da,stroke:#721c24,color:#721c24 classDef ok fill:#d4edda,stroke:#155724,color:#155724 classDef partial fill:#fff3cd,stroke:#856404,color:#856404 Agent([LLM Agent]) ShellTool["shell.ts<br/>is_background: true<br/>+ '&' fork-detach"]:::leak AgentTool["Agent 工具<br/>run_in_background: true<br/>(#3076 ✅)"]:::ok Void["输出丢失<br/>无法 kill / 无法观察"]:::leak Reg["BackgroundAgentRegistry<br/>仅 agent"]:::partial Pill["status pill<br/>agent count only"]:::partial Dialog["dialog<br/>仅 agent"]:::partial Agent --> ShellTool Agent --> AgentTool ShellTool -.-> Void AgentTool --> Reg Reg --> Pill Reg --> Dialog目标 —— 所有后台工作经同一 registry、汇入同一 UI、用同一对工具控制:
flowchart LR classDef done fill:#d4edda,stroke:#155724,color:#155724 classDef new fill:#cfe2ff,stroke:#0d6efd,color:#084298 classDef later fill:#f8f9fa,stroke:#6c757d,color:#495057 Agent([LLM Agent]) Tools["task_stop / send_message<br/>(task-type-agnostic)"]:::done ShellTool["shell.ts<br/>(受管后台路径)"]:::new MonitorTool["Monitor 工具<br/>(新)"]:::new AgentTool["Agent 工具"]:::done More["...mcp_health / dream /<br/>remote / teammate"]:::later Reg["<b>BackgroundTaskRegistry</b><br/>kind: shell / monitor / agent / ..."]:::done Pill["status pill<br/>per-kind counts"]:::done Dialog["combined dialog<br/>每个 kind 一个 section"]:::done Agent --> Tools Agent --> ShellTool Agent --> MonitorTool Agent --> AgentTool Tools --> Reg ShellTool --> Reg MonitorTool --> Reg AgentTool --> Reg More -.-> Reg Reg --> Pill Reg --> Dialog形状变化很小:沿用 subagent 已经引入的
BackgroundTaskRegistry,把 entry 类型用kinddiscriminator 一般化,让 shell + monitor + 未来 kind 都走同一道关。user-facing 面(pill + dialog)和 model-facing 面(task_stop/send_message)演化为一个小框架,承载每种 kind。节奏
flowchart LR classDef inflight fill:#fff3cd,stroke:#ffc107,color:#856404 classDef next fill:#cfe2ff,stroke:#0d6efd,color:#084298 classDef later fill:#f8f9fa,stroke:#6c757d,color:#495057 A["<b>Phase A</b><br/>subagent 切片<br/>(#3471 + #3488 已合)"]:::done B["<b>Phase B</b><br/>bash 后台池<br/>(#3642 已合)"]:::done C["<b>Phase C</b><br/>事件 monitor<br/>(#3684 已合)"]:::done D["<b>Phase D</b><br/>自动后台 / sleep /<br/>Ctrl+B<br/>(#3809 + #3831 已落)"]:::done Beyond["<b>Beyond</b><br/>MCP / auto-mem /<br/>云端 / teams"]:::later A --> B --> C --> D --> Beyond前几列描的是计划(每个 phase 设计上要加什么),最右今日状态列点出实际落地的 PR 和还在跑的项。
&泄漏/tasks)+ #3687(task_stop↔ shell)+ #3720(shell ↔ dialog)+ #3808(文案清理)。Monitor(...)权限 namespace)+ #3784(Windowstaskkill测试修)+ #3792(token-bucket 时钟漂移 / AST 日志 / UI 去重)。signal.reason通道。tmux 风格:前台 shell 跑一半 → Ctrl+B → 子进程继续跑,转成BackgroundShellEntry在/tasks/ dialog /task_stop里可见。限制 defer 到 PR-2.5(post-promote stream redirect + natural-exit registry settle)。task_stop。kind 框架成功扛过首个 non-user-initiated 消费者的真实压力测试。kind)kind扩展;v1 仅视觉指示器)。Phase A — 干净落地 subagent 切片 (✅ 已合: #3471 + #3488)
task_stop/send_message/ per-agent transcript,按 review 做了 task-type-agnostic 一般化。BackgroundAgent*→BackgroundTask*rename 和 dialog 测试覆盖。Phase B — bash 后台池 (✅ 已合: #3642)
shell.ts旧&路径替换为受管 registry entry(PID + 输出文件 + AbortController)。<projectTempDir>/background-shells/<sessionId>/),agent 可以Read。shell;新/tasksslash command(文本模式查看)。Phase C — 事件 monitor (✅ 已合: #3684 by @doudouOUC — 2026-05-02)
Monitor工具:长期脚本,stdout 行 push 事件回 agent(带合理节流)。monitor。同样复用 pill / dialog。BashTool的小改动 — 对tail -f/ 轮询循环意图建议改用Monitor。Phase D — 体验打磨
shell调用跑超过有效 timeout 一半(按调用计算,下限 1000ms)完成后,LLM 工具结果和用户 TUI 都追加建议下次用is_background: true。当前调用照样跑完,建议给 agent 下次决策。文案显式警告不要重跑已完成命令(DB 迁移 / 部署 /git push等有副作用操作要紧)。被 cancel / timeout / 外部信号路径压制(消息已够清晰)。Hint 也通过error.message走\n---\n分隔符传递给下游(coreToolScheduler错误分支只看 error.message 不看 llmContent,慢-spawn-失败边缘场景靠这个保住建议)。bg_xxx.output+BackgroundShellRegistry注册带 kill listener;解 design Q7promoted: true时aborted: false;2026-05-08,+935/-15)+ PR-3 feat(cli): Ctrl+B promote keybind (#3831 PR-3 of 3) #3969(Ctrl+B 键位 + AppContainer wire-up + E2E 测试 +keyboard-shortcuts.md文档;2026-05-11,+308/-15)。tmux 风格:前台 shell 跑一半 → Ctrl+B → 子进程继续跑不被 kill,转成BackgroundShellEntry在/tasks/ dialog /task_stop里可见。限制 defer 到 PR-2.5(post-promote stream redirect — 输出 snapshot 当前在 promote 时刻冻结;natural-exit registry settle — entry 维持'running'直到task_stop/ session-end)。BashTool拦截裸sleep N— 已经跟 feat(core): event monitor tool with throttled stdout streaming (Phase C) #3684 一并合入(sleep 拦截捆进了 Phase C)。Beyond — 第一项现在在跑:
MemoryManager.scheduleDream)接进统一 Background tasks UI。Footer pill 计 dream(如1 shell, 1 dream);combined dialog 显示[dream] memory consolidation reviewing N sessions;per-kind detail body 显示 sessions reviewing / progress text / topics touched / errors。Cancel 通过 dialogx键 +task_stop工具加第 4 路 dispatch。MemoryTaskStatus加'cancelled';cancel abort dream fork-agent 后让现有runDreamfinallyblock 在 agent unwind 时释放 consolidation 锁。对存量 auto-memory 用户的行为变化:之前 dream 不可见(仅完成时memory_savedtoast),现在每次 dream fire 都在 footer pill tick — 增量改动,无 flag。战略意义:引入dream作为框架第 4 种 kind(agent / shell / monitor 之后),首个 system-initiated(非 user/agent-initiated)消费者,框架不靠 contract 改动也扛住了。同窗 2 轮 review-fix 提交解掉集成时浮现的真 bug:dreamLockReleaseFailedflag 同 session 锁恢复(下次 scheduleDream 强制 clean 残留锁);cancelled dream 真实duration_ms给 telemetry;AbortController-before-storeWith ordering(subscriber-during-storeWith race 修);gating-metadata 写 try/catch(不再因磁盘错把成功 dream 降级为 'failed');DreamDetailBody 显示lockReleaseError+metadataWriteErrorwarning;cancelled dream telemetry 事件。v1 不含:live in-flight progress(phase flip / files-touched 累积)— 需扩runForkedAgent的AgentPathParams加onAssistantMessagecallback,4 个调用点,独立 PR 隔离 blast radius。故意排除extract任务(每次 UserQuery fire 会洪 pill;toast 已覆盖;request loop 内 cancel 会干扰用户当前 turn)。kind)、云端长任务、多 agent 协作 — 部分与现有 Arena / MCP / headless 工作交叉,单独跟进。可并行的轨道
不是所有剩余工作都阻塞在 #3471 / #3488 上。每个 phase 的非集成核心都可以并行起步 —— 只有"接入统一 pill / dialog /
task_stop"那部分卡 Phase A。flowchart LR classDef indep fill:#d1ecf1,stroke:#0c5460,color:#0c5460 classDef gated fill:#f8d7da,stroke:#721c24,color:#721c24 subgraph Now["独立 — 现在就能并行起步"] T1["轨道 1<br/>bash 后台池<br/>feat/bash-bg-pool"]:::indep T2["轨道 2<br/>Monitor 工具<br/>(#3666 review 中)"]:::indep T3["轨道 3<br/>sleep 拦截<br/>仅改 BashTool"]:::indep end subgraph Later["卡 #3471 / #3488 合入"] F1["shell + pill / dialog / task_stop"]:::gated F2["monitor + pill / dialog"]:::gated end T1 --> F1 T2 --> F2ShellRegistry+spawn+ per-task 输出文件 +/tasks命令。Agent 通过Read看输出,本 PR 不集成 pill / dialog /task_stop。✅ 已合:feat(core): managed background shell pool with /tasks command #3642。Monitor工具。Push 通知已经由 feat: background subagents with headless and SDK support #3076 的task_notification框架提供。✅ 已合:feat(core): event monitor tool with throttled stdout streaming (Phase C) #3684 by @doudouOUC(+6297 / -147,含 sleep 拦截一并落)。BashTool里加detectBlockedSleepPattern检查 + 引导消息(提示用run_in_background: true或新 Monitor)。✅ 跟 feat(core): event monitor tool with throttled stdout streaming (Phase C) #3684(Phase C)一起合入。#3471 / #3488 合入后,每条轨道派生一个小 follow-up PR(~100-200 LOC),把 shell / monitor entry 接入统一 pill / dialog /
task_stop—— 在 #3488 的 rename 完成后是纯增量。已对齐的设计要点
这些影响 Phase A 和其上所有工作(详细论证在 #3471 / #3488 的 review threads 里):
BackgroundAgentEntry→BackgroundTaskEntry+kinddiscriminator。趁 consumer 还少时改名,未来新 kind PR 才能纯增量。task_stop/send_message的 description 和错误从一开始就不绑 agent。后续 shell / monitor / 远端任务都通过同一对工具取消和下指令。<projectDir>/tasks/<sessionId>/<kind>-<id>.jsonl,避免后续每种 kind 加兄弟目录。getPillLabel从 Phase A 就 group-by-kind;dialog 的ListBody加 section 抽象;DetailBody按kind派发到不同子组件(AgentDetailBody/ShellDetailBody/ ...)。显式不在 v1 范围内
下面这些扩展乍一看像自然 follow-up,但故意延后 — 写下来避免 contributor 把"沉默"当成"open TODO"。每条都是架构层判断,不是漏写代码。
send_message→ shell / monitor今天
task_stop三 kind 都覆盖(agent / shell / monitor —task-stop.ts:48-130三分支齐)。send_message只接 agent(send-message.ts:48只调getBackgroundTaskRegistry())。PR#3684 的"未做"清单把 monitorsend_message列为待办,但仔细看这是架构层决定不是 plumbing 漏写:send_message把文本指令塞进 agent 的pendingMessages,下个 tool-round 边界 agent 当 prompt-like 指令消费。语义清晰因为 agent 是 LLM-driven,能接任意文本进 reasoning loop。.signals文件 / signal-based trigger — 都需要 receiver 端先约定协议,而 kind 现在还没这个协议。参考设计 — Claude Code(哪些可借鉴 / 哪些不可借鉴)。Claude Code 的 SendMessage 完全不绑 task type。Rust 实现
src-rust/crates/tools/src/send_message.rs:31:一个全局 inbox map,key 是 recipient 字符串。SendMessage 不知道 receiver 是哪种 kind — 只往 inbox 写;接收方自己
drain_inbox(my_id)。这部分跟 monitor / shell 集成问题相关 — receiver 端抽象正是非 agent kind 自己决定接不接消息的关键。TS 版还有
string | StructuredMessagepayload(shutdown_request/shutdown_response/plan_approval_response,spec03_tools.md:902-909),TUI 侧useInboxPoller1s 轮询按type派发。但这部分跟 monitor / shell 集成问题 无关 — 这三个StructuredMessagevariants 是 agent ↔ agent / user ↔ agent 协议信号(被isAgentSwarmsEnabled()gate),驱动 Claude Code 的 multi-agent swarm coordination 和 plan-mode workflow。没有一个是 agent ↔ non-LLM-receiver 协议。早前草稿把"结构化 payload"跟"非 agent receiver"混在一起描述是错的 — 显式写出来避免后续 reader 重复这个推论。如果以后要把
send_message扩到 agent 之外,两个独立决定(不要捆绑):acceptMessage(msg)自主决定接消息,替代send_message在 sender 端 fan-out 查 registry。改动成本真实(动 3 个 registry +send_message);Claude Code 那套设计的代价也真实(1s 轮询)— qwen-code 全 in-process,直接 push 比轮询高效。两条更便宜的退路保留:(a) 保 sender-side fan-out,每种 kind 定义结构化语义(monitor 写到 sidecar.signals文件让脚本 poll;shell 写到 child stdin);(b) 直接拒绝。stringvsstring | StructuredMessage) — 跟 routing 决定正交。是否引入 union 取决于 qwen-code 是否长出 Claude Code swarm / plan-mode 类的 use case。今天一个都没有(无 multi-agent swarm,无 cross-process plan approval,无 cross-machine session bridge),引入 union 但只放一个{ type: 'text', message }variant 是 API surface bloat 但内壳是空的。v1 决定:
send_message在运行时 routing 这一层保持 agent-only,message 形态保持 string。这跟已对齐设计点 #2 不冲突:那条承诺的是公共 API task-type-agnostic(send_message的 description 和错误不写"agent",所以未来加 shell / monitor 是非破坏性增量)。我们这里 defer 的是 API 背后的运行时 routing — receiver 端非 agent kind 的语义到底怎么定,那才是悬而未决的问题。task_stop三 kind 全接是因为它 receiver 端语义无歧义(终止该 entry);send_message的 receiver 端语义还没定。上面两个决定都得具体用例驱动才单开 design issue 讨论(解锁决定 1 的具体例子:"agent 想戳长跑 monitor 提早 flush"、"agent 想给受管 bash watcher 一个 SIGUSR1 等价信号"、"agent 想让 dev server 重新加载配置不重启";决定 2 只在 qwen-code 自己长出 swarm / plan-mode / cross-process workflow 时才做 — 为了跟 Claude Code 对称而 copy union 是错的理由)。不要凭推测预先建任一个。
Next steps
ListBodysection 抽象、DetailBody按 kind 派发、getPillLabelgroup-by-kind、BackgroundAgent*→BackgroundTask*rename。这些落到 Phase B follow-up Where is the config saved? #2 一起做,因为那是第一个真正需要它们的 PR。)r/x键)。fork-agent resume 改成 transcript-first 设计,让 resumed forks 保留原 worker 约束。AgentExecutionDisplay被压制,运行通过已有 pill +BackgroundTasksDialog呈现。父 turn commit 后,完整 frame 不变地落进 scrollback。整类去除 flicker 同时复用 pill+dialog 渲染面 — kind 框架第一个非后台消费者已入主线。task_stop集成 — ✅ 已合 feat(core): wire background shells into the task_stop tool #3687。把BackgroundShellRegistry.requestCancel()接到统一task_stop工具 lookup,让模型用同一个工具停bg_xxxshell。requestCancel仅 abort,spawn settle 路径记录真实终态。BackgroundShellRegistry加 observer hook(setStatusChangeCallback/setRegisterCallback)和requestCancel(id);用kinddiscriminator 把 agent / shell 合并(DialogEntry联合);getPillLabel按 kind 分组("2 shells, 1 local agent");ListBody 渲染[shell] <command>行;DetailBody 按 kind 派发到AgentDetailBody(已有)或新ShellDetailBody;cancelSelected按 kind 路由。还顺手修了 snapshot 与 registry 的 shallow-copy 把recentActivities在两次 statusChange 之间切断的 bug — 详情页在每次 render 时通过useMemo(deps 含activityTick)重新从 registry 取选中的 agent,recentActivities/stats即时刷新。/tasksslash command 重新定位 — ✅ 已合 feat(cli): include monitors in /tasks + add interactive-mode hint #3801(2026-05-03)。Issue 之前讨论的三个选项(删 / 留作 fallback / 加 deprecation hint),本 PR 选 keep + add-hint 混合。不删因为/tasks是non_interactive(-p)、acp(IDE 桥)、SDK 消费者唯一的查看方式 — 他们没 TTY 开不了对话框。重新框为 surface redirect:interactive 模式输出顶部加Tip: focus the Background tasks pill in the footer (use ↓ from an empty composer) and press Enter ...一行;非 TTY 模式裸列表是规范入口。顺手修了 feat(core): event monitor tool with throttled stdout streaming (Phase C) #3684 / feat(cli): wire Monitor entries into combined Background tasks dialog #3791 留下的隐蔽缺口 — 命令之前漏 monitor 完全没列。输出消毒按 @doudouOUC review 从 escapeAnsiCtrlCodes 切到 stripUnsafeCharacters,关闭 C0/C1 控制字节漏洞。shell.ts(spawn 后)和task-stop.ts(cancel 后)的 LLM 消息更新成同时点出/tasks和交互式 Background tasks dialog 两个查看路径。顺手修了 4 处 stale 注释 / docstring(含monitorRegistry.ts把 dialog 错称/tasks dialog的真错)。纯文档,+10 / -8。Monitor工具(带 throttled stdout 流)、BashTool的 sleep 拦截启发式,以及配套 plumbing。Mirror follow-up 现在解锁:task_stop集成 — 把Monitor接到task_stop的 lookup,让模型用同一个工具停 monitor、subagent、shell。镜像 feat(core): wire background shells into the task_stop tool #3687。DialogEntryunion 里加kind: 'monitor'变种 +MonitorDetailBody。镜像 feat(cli): wire background shells into combined Background tasks dialog #3720;因为 kind 框架已落,应该比 feat(cli): wire background shells into combined Background tasks dialog #3720 小。MonitorRegistry加setStatusChangeCallback,DialogEntry联合扩展kind: 'monitor',pill / dialog /cancelSelected加 monitor 分支,新MonitorDetailBody显示命令 / pid / 事件数 / dropped 行 / 退出码 / 错误原因。kind 框架现在有三个真实消费者(agent / shell / monitor)— 把"通用框架"从口号变成实证。Monitor(...)permission namespace:✅ feat(core): add Monitor(...) permission namespace #3726 by @doudouOUC(2026-04-29)。这是 feat(core): event monitor tool with throttled stdout streaming (Phase C) #3684 review thread 里 deferred 的项 — Monitor 之前发Bash(...)规则,"Always Allow" 对未来 monitor 调用失效同时漏放 shell 权限。引入专门的Monitor(...)namespace +permission-manager.ts里SHELL_LIKE_TOOLS集合,让所有 shell-like 评估路径(AST 分析、virtual ops、复合分割)同时处理run_shell_command和monitor。taskkill测试修:✅ fix(monitor): correct Windows taskkill spawn assertion to include stdio option #3784 by @Copilot(2026-05-02,+15 / -18)。琐细 — 实现传spawn('taskkill', [...], { stdio: 'ignore' })但测试只断言两个参数 → Windows CI 全 fail(vitest 严格匹配)。Date.now()在系统 suspend/resume 后回跳(否则elapsed变负 → bucket 饿死)。(b)getConfirmationDetails的 AST 解析失败之前完全静默,加debugLogger.warn。(c) agent 工具显示功能引入的 UI 路由重复修复。DISCONNECTED的 MCP 用平行 pill 显示在 Background tasks pill 旁边(故意不走kind扩展 — MCP 连接状态没有终态,会稀释 registry 契约)。v1 仅视觉指示器;Down 箭头焦点链延后。cc @tanzhenxin @doudouOUC