Skip to content

Background task management: roadmap and next steps #3634

@wenshao

Description

@wenshao

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.ts is_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

PR Status What it lands
#3076 ✅ merged Background subagent launch (Agent tool + run_in_background: true)
#3471 ✅ merged Model-facing control: task_stop, send_message, per-agent transcript
#3488 ✅ merged User-facing UI: status pill, combined tasks dialog, per-entry detail view
#3642 ✅ merged Phase B — bash background pool, BackgroundShellRegistry, /tasks command
#3687 ✅ merged Phase B follow-up #1 — wire shells into task_stop tool
#3720 ✅ merged Phase B follow-up #2 — shells in combined Background tasks dialog
#3684 ✅ merged Phase C — event monitor tool with throttled stdout streaming + sleep interception
#3726 ✅ merged Phase C follow-up by @doudouOUCMonitor(...) permission namespace (the deferred item from #3684 review)
#3739 ✅ merged Phase A/B extension by @doudouOUC — background agent resume / continuation with paused status
#3741 ✅ merged Adjacent UX — MCP health pill in Footer (parallel pill, not a kind extension)
#3784 ✅ merged Phase C post-merge fix by @Copilot — Windows taskkill spawn assertion stdio option
#3791 ✅ merged Phase C follow-up — Monitor entries in combined Background tasks dialog
#3792 ✅ merged Phase C post-merge fix-up by @doudouOUC — token-bucket clock-drift guard + AST-parse failure logging + UI routing dedupe
#3768 ✅ merged 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.
#3801 ✅ merged Phase B closure — /tasks repositioning: include monitors + interactive-mode hint pointing at the dialog (kept for non-TTY consumers, not deleted)
#3808 ✅ merged 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)
#3809 ✅ merged 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
#3836 ✅ merged 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).
#3842 ✅ merged 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.
#3886 ✅ merged PR-1 follow-up for #3842 (2026-05-07, +85 / -21). Addresses 2 of @tanzhenxin's approving-review notes: (1) real bugObject.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.
#3894 ✅ merged 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).
#3969 ✅ merged 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:

  1. A registry entry with lifecycle (running / completed / failed / cancelled)
  2. A user-visible label in the status pill (counted by kind)
  3. A row in the combined tasks dialog with a per-kind detail view
  4. Cancellation via the same task_stop tool
  5. (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

Currentshell.ts background 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/>+ '&amp;' 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 --> Dialog
Loading

Target — 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 --> Dialog
Loading

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.

Capability Now After A After B After C After D Status (today)
Background subagent (launch / control / UI) partial (#3076) ✅ generic-ized Done#3076 #3471 #3488 #3739 (resume / continuation) + #3768 (foreground subagent → pill+dialog routing, merged 2026-05-06 by @tanzhenxin — first non-background consumer of the kind framework).
Background shell (managed PID + output + cancel) ❌ leaks via & Done#3642 (registry + /tasks) + #3687 (task_stop ↔ shell) + #3720 (shell ↔ dialog) + #3808 (doc cleanup).
Event monitor (push events to agent) Done#3684 (Monitor tool + sleep interception bundled) + #3791 (Monitor ↔ dialog) + #3726 (Monitor(...) permission namespace) + #3784 (Windows taskkill test fix) + #3792 (token-bucket clock-drift / AST log / UI dedupe).
Unified pill / dialog (multi-kind) ✅ +shell ✅ +monitor Done#3488 (framework) + #3720 (+shell) + #3791 (+monitor) + #3836 (+dream, merged 2026-05-06). Four real consumers (agent / shell / monitor / dream) — kind framework promise fulfilled with the first system-initiated kind.
Long-running foreground bash advisory Done#3809 (Phase D part a).
Sleep interception + Monitor suggestion Done — bundled inside #3684 (Phase C).
Ctrl+B promote running shell to background Done#3831 (Phase D part b), full 3-PR sequence merged: PR-1 #3842 (signal.reason foundation, 2026-05-07, +947/-56) + follow-up #3886 + PR-2 #3894 (shell.ts integration, 2026-05-08, +935/-15) + PR-3 #3969 (Ctrl+B keybind + UI + E2E + docs, 2026-05-11, +308/-15). Architecture as designed: lazy promote + signal.reason channel. tmux-style: foreground shell mid-flight → Ctrl+B → child keeps running, becomes a BackgroundShellEntry visible in /tasks / dialog / task_stop. Limitations deferred to PR-2.5 (post-promote stream redirect + natural-exit registry settle).
Beyond — auto-memory / dream task surface + cancel n/a 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)

Phase B — bash background pool (✅ merged: #3642)

  • 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

Beyond — first item now in flight:

  • 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 runDream finally 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

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):

  1. Unified entry type: BackgroundAgentEntryBackgroundTaskEntry + kind discriminator. Renaming while consumers are few keeps future kind PRs purely additive.
  2. 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.
  3. Unified transcript path: <projectDir>/tasks/<sessionId>/<kind>-<id>.jsonl, so each new kind doesn't add a sibling directory.
  4. 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 / ...).
  5. 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:

static INBOX: Lazy<DashMap<String, Vec<AgentMessage>>> = Lazy::new(DashMap::new);

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):

  1. 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.
  2. 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.

Next steps

中文版

把和 @tanzhenxin 已经对齐的方向写下来,方便后续接触这块的人能看到。Phase A 已合#3471 + #3488),background agent resume / continuation 扩展由 @doudouOUC#3739 落地。Phase B 已合 + 收尾完成#3642 + #3687 + #3720 + #3801 + 文案清理 #3808)。Phase C 已合 + 合后修补全部入 #3684(Monitor 工具 + sleep 拦截 + task_stop ↔ monitor 一并落),dialog 集成 follow-up 已合 #3791Monitor(...) permission namespace 已合 #3726 by @doudouOUC,Windows taskkill 测试修复已合 #3784,token-bucket 时钟漂移 / AST 日志 / UI 路由修补已合 #3792 by @doudouOUC。kind 框架现在有四个真实消费者(agent / shell / monitor / dream)— kind 框架承诺由首个 system-initiated kind 兑现。相邻 UX:MCP health pill 已合 #3741(平行 pill,不走 kind 扩展)。Phase D 起步 + (a) 已落#3809(前台长跑 bash 后台化建议)已合主;Phase D (b) Ctrl+B 提升 design issue 已开 #3831Beyond 轨道第一项已合 #3836(auto-memory dream 任务接 pill / dialog / task_stop 第 4 路 dispatch,2026-05-06,+1714 / -100)。前台 subagent → pill+dialog 路由已合 #3768 by @tanzhenxin(2026-05-06,+1008 / -108)— kind 框架第一个非后台消费者已入主线。Phase D 第 (b) 部分 Ctrl+B promote — 三 PR 全部合入:PR-1 #3842signal.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 集成 + lazy promote + design Q7 aborted:false,2026-05-08,+935/-15)+ PR-3 #3969(Ctrl+B 键位 + AppContainer wire-up + E2E + docs,2026-05-11,+308/-15)。#3831 design issue 已关闭。Phase D (a)+(b) 全部完成。本路线无 open PR。

背景

当前 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 的笔记。

当前进展

PR 状态 落地内容
#3076 ✅ merged 后台 subagent 启动(Agent 工具 + run_in_background: true
#3471 ✅ merged model-facing 控制:task_stopsend_message、per-agent transcript
#3488 ✅ merged user-facing UI:status pill、combined tasks dialog、详情视图
#3642 ✅ merged Phase B — bash 后台池、BackgroundShellRegistry/tasks 命令
#3687 ✅ merged Phase B follow-up #1 — 把 shell 接入 task_stop 工具
#3720 ✅ merged Phase B follow-up #2 — shell 接入 combined Background tasks dialog
#3684 ✅ merged Phase C — event monitor 工具 + throttled stdout 流 + sleep 拦截
#3741 ✅ merged 相邻 UX — Footer 加 MCP health pill(平行 pill,不走 kind 扩展)
#3791 ✅ merged Phase C follow-up — Monitor 接 combined Background tasks dialog
#3792 ✅ merged Phase C 合后修补 by @doudouOUC — token-bucket clock-drift 防御 + AST parse failure 日志 + UI routing 去重
#3801 ✅ merged Phase B 收尾 — /tasks 重新定位:加 monitor + interactive-mode hint 指向 dialog(保留给非 TTY 消费者,不删)
#3808 ✅ merged Phase B follow-up 文档 PR — 把后台 shell 引导文案同时指向 /tasks 和交互式 Background tasks dialog(LLM 字符串 + 4 处 stale 注释)
#3809 ✅ merged Phase D 第 (a) 部分 — 前台长跑 bash 建议:跑超过有效 timeout 一半后建议下次用 is_background: true
#3836 ✅ merged Beyond 轨道首落地(2026-05-06,+1714 / -100)— auto-memory dream 任务接 pill + dialog + cancel(引入 dream 作为框架第 4 种 kind — agent / shell / monitor / dream);footer pill 计 dream,dialog 显示 [dream] memory consolidation reviewing N sessions,per-kind detail body,dialog x cancel + task_stop 第 4 路 dispatch,MemoryTaskStatus'cancelled'。同窗 2 轮 review-fix 提交解掉真 bug:dreamLockReleaseFailed flag 同 session 锁恢复(下次 scheduleDream 强制 clean 残留锁),cancelled dream 真实 duration_ms 给 telemetry,AbortController-before-storeWith ordering(race 修),gating-metadata try/catch(不再因磁盘错把成功 dream 降级为 'failed'),UI surface lockReleaseError + metadataWriteError warning。Out-of-scope(计划 follow-up):live in-flight progress(phase flip / files-touched 累积)— 需要扩 runForkedAgentAgentPathParamsonAssistantMessage callback,涉及 4 个调用点。故意排除 extract 任务(每次 UserQuery fire,会洪 pill;toast 已覆盖;request loop 内 cancel 会干扰用户当前 turn)。
#3842 ✅ merged Phase D 第 (b) 部分 PR-1 of 3 for #3831(2026-05-07,+947 / -56 final)。定义 discriminated union ShellAbortReason = { kind: 'cancel' } | { kind: 'background'; shellId? } 让 AbortSignal 携带;默认行为不变 tree-kill,{ kind: 'background' } 是 takeover 信号 — execute() 跳 kill、从 active 集合 delete child、flush output snapshot、立即 resolve Promise 带 promoted: trueShellExecutionResult 加可选字段 promoted?: boolean。两个 abort handler 都改(executeWithPty + childProcessFallback),用穷尽 switch + _exhaustive: never;导出 getShellAbortReasonKind helper 含 prototype-pollution 防御。纯 plumbing,对现有调用方零行为变化。5 轮 review + 1 自审 → 22 review threads + 1 真 bug 由 reviewer 抓(@lydell/node-pty 暴露 removeListener 不是 off)。70 / 70 测试 pass。
#3886 ✅ merged PR-1 follow-up for #3842(2026-05-07,+85 / -21)。处理 @tanzhenxin approving review 的 2 个 note:(1) 真 bugObject.prototype.hasOwnProperty.call(reason, 'kind') 在 helper 的 try/catch 之外,Proxy 带 throwing getOwnPropertyDescriptor trap 会穿透 helper 经 abort handler switch 出 (async) abort listener — 导致 shell 进程留活而非被 kill。把 descriptor probe 跟 value read 都包进 try/catch + 加回归测试。(2) PTY handoff-boundary 测试 parity — abort 后再调 production onData callback,断言 onOutputEventMock.mock.calls.length === 0,exercise chain callback 内 listenersDetached guard。Note 1(aborted: true + promoted: true shape — reviewer 建议 promoted: trueaborted: false)延后到 PR-2,作为 #3831 design question 7。
#3894 ✅ merged Phase D 第 (b) 部分 PR-2 of 3 for #3831(2026-05-08,+935 / -15)。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_stopcoreToolSchedulerpromoteAbortController 挂在 TrackedExecutingToolCall 上让 PR-3 的 keybind 能拿到。解决 PR-1 design question 7(promoted: trueaborted: false 让现有 if (result.aborted) 消费分支自然 fall through)。6 轮 review → 多个 Critical fix(mkdir orphan、refused-promote race 误报为 timeout、commandToExecute co-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)。
#3969 ✅ merged Phase D 第 (b) 部分 PR-3 of 3 for #3831(2026-05-11,+308 / -15)。Command.PROMOTE_SHELL_TO_BACKGROUND 键位绑定 Ctrl+BuseReactToolScheduler.ts 把 core 的 ExecutingToolCall.promoteAbortController 投影到 TrackedExecutingToolCall(compile-time extends keyof 断言锁定,core 改名时 React 侧 build 立刻 fail)。AppContainer.handleGlobalKeypress 加分支:从 pendingToolCallsRef.current 找正在执行的 shell tool call(同时 gate tc.request.name === ToolNames.SHELL AND promoteAbortController !== 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)提供了完整垂直切片。命名和形状(BackgroundAgentEntryBackgroundTaskRegistry、dialog 标题 "Background tasks")已经隐含了更广的 framing。

我们正在走的方向

把 background subagent 视为多种 task kind 中的一种。同一套 registry、控制工具、UI 自然地承载:

  • 后台 subagent ✅ 三个 PR 进行中
  • 后台 shell — 把 npm run devpytest、build 命令、watcher spawn 成被管理的进程,带输出捕获、生命周期查询、显式终止
  • 事件 monitor — 长期 watcher(tail -Finotifywait、轮询循环)把事件 push 回 agent,让 agent 可异步响应
  • MCP server 健康呈现 — 把 MCP 启动 / 重连状态显示在同一 dialog 里,而不是不可见
  • auto-memory / 整理类任务 — 系统发起的后台工作呈现给用户
  • 云端长任务(远期)— 可在本地重启后存活的任务

每种 kind 都遵循同样 5 个要素:

  1. 带生命周期的 registry entry(running / completed / failed / cancelled)
  2. 状态条 pill 里按 kind 计数的可见标签
  3. 组合 tasks dialog 里的一行 + per-kind 详情视图
  4. 通过同一个 task_stop 工具取消
  5. (适用时)通过同一个 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/>+ '&amp;' 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
Loading

目标 —— 所有后台工作经同一 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
Loading

形状变化很小:沿用 subagent 已经引入的 BackgroundTaskRegistry,把 entry 类型用 kind discriminator 一般化,让 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
Loading

前几列描的是计划(每个 phase 设计上要加什么),最右今日状态列点出实际落地的 PR 和还在跑的项。

能力 现状 After A After B After C After D 今日状态
后台 subagent(启动 / 控制 / UI) partial (#3076) ✅ generic 化 已完成#3076 #3471 #3488 #3739(resume / continuation)+ #3768(前台 subagent → pill+dialog 路由,2026-05-06 合 by @tanzhenxin — kind 框架第一个非后台消费者)。
后台 shell(PID + 输出 + cancel) & 泄漏 已完成#3642(registry + /tasks)+ #3687task_stop ↔ shell)+ #3720(shell ↔ dialog)+ #3808(文案清理)。
事件 monitor(push 事件到 agent) 已完成#3684(Monitor 工具 + sleep 拦截一并落)+ #3791(Monitor ↔ dialog)+ #3726Monitor(...) 权限 namespace)+ #3784(Windows taskkill 测试修)+ #3792(token-bucket 时钟漂移 / AST 日志 / UI 去重)。
统一 pill / dialog(多 kind 共享) ✅ +shell ✅ +monitor 已完成#3488(框架)+ #3720(+shell)+ #3791(+monitor)+ #3836(+dream,2026-05-06 合)4 个真实消费者(agent / shell / monitor / dream)— kind 框架承诺由首个 system-initiated kind 兑现。
长前台 bash 后台化建议 已完成#3809(Phase D 第 a 部分)。
Sleep 拦截 + Monitor 引导 已完成 — 跟 #3684 一起合(Phase C 捆进来)。
Ctrl+B 把运行中的 shell 甩到后台 已完成#3831(Phase D 第 b 部分),三 PR 序列全部合入:PR-1 #3842(signal.reason foundation,2026-05-07,+947/-56)+ follow-up #3886 + PR-2 #3894(shell.ts 集成,2026-05-08,+935/-15)+ PR-3 #3969(Ctrl+B 键位 + UI + E2E + docs,2026-05-11,+308/-15)。架构按设计:懒 promote + signal.reason 通道。tmux 风格:前台 shell 跑一半 → Ctrl+B → 子进程继续跑,转成 BackgroundShellEntry/tasks / dialog / task_stop 里可见。限制 defer 到 PR-2.5(post-promote stream redirect + natural-exit registry settle)。
Beyond — auto-memory / dream 任务呈现 + cancel n/a 已完成#3836(2026-05-06 合,Beyond 轨道首个 PR)。把 system-initiated dream consolidation 任务接进 pill + dialog + task_stop。kind 框架成功扛过首个 non-user-initiated 消费者的真实压力测试。
相邻 UX:MCP health pill(平行 pill,非 kind n/a 已完成#3741(故意不走 kind 扩展;v1 仅视觉指示器)。

Phase A — 干净落地 subagent 切片 (✅ 已合: #3471 + #3488)

Phase B — bash 后台池 (✅ 已合: #3642)

  • shell.ts& 路径替换为受管 registry entry(PID + 输出文件 + AbortController)。
  • ✅ 输出流式落到 per-task 文件(<projectTempDir>/background-shells/<sessionId>/),agent 可以 Read
  • ✅ 新 entry kind shell;新 /tasks slash command(文本模式查看)。
  • 🟡 pill / dialog 集成 + slash command deprecate 是 post-merge follow-up,见下方。

Phase C — 事件 monitor (✅ 已合: #3684 by @doudouOUC — 2026-05-02)

  • Monitor 工具:长期脚本,stdout 行 push 事件回 agent(带合理节流)。
  • 新 entry kind: monitor。同样复用 pill / dialog。
  • 配合 BashTool 的小改动 — 对 tail -f / 轮询循环意图建议改用 Monitor

Phase D — 体验打磨

Beyond — 第一项现在在跑:

  • Auto-memory / dream 任务呈现 + cancel:已合 feat(core,cli): surface and cancel auto-memory dream tasks #3836(2026-05-06,+1714 / -100,16+ 文件;2026-05-04 开)。把 system-initiated dream consolidation 任务(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 通过 dialog x 键 + task_stop 工具加第 4 路 dispatch。MemoryTaskStatus'cancelled';cancel abort dream fork-agent 后让现有 runDream finally block 在 agent unwind 时释放 consolidation 锁。对存量 auto-memory 用户的行为变化:之前 dream 不可见(仅完成时 memory_saved toast),现在每次 dream fire 都在 footer pill tick — 增量改动,无 flag。战略意义:引入 dream 作为框架第 4 种 kind(agent / shell / monitor 之后),首个 system-initiated(非 user/agent-initiated)消费者,框架不靠 contract 改动也扛住了。同窗 2 轮 review-fix 提交解掉集成时浮现的真 bug:dreamLockReleaseFailed flag 同 session 锁恢复(下次 scheduleDream 强制 clean 残留锁);cancelled dream 真实 duration_ms 给 telemetry;AbortController-before-storeWith ordering(subscriber-during-storeWith race 修);gating-metadata 写 try/catch(不再因磁盘错把成功 dream 降级为 'failed');DreamDetailBody 显示 lockReleaseError + metadataWriteError warning;cancelled dream telemetry 事件。v1 不含:live in-flight progress(phase flip / files-touched 累积)— 需扩 runForkedAgentAgentPathParamsonAssistantMessage callback,4 个调用点,独立 PR 隔离 blast radius。故意排除 extract 任务(每次 UserQuery fire 会洪 pill;toast 已覆盖;request loop 内 cancel 会干扰用户当前 turn)。
  • (还在任何 PR 上游)MCP 健康呈现(已部分通过 feat(cli): add MCP health pill to footer #3741 平行 pill 落地,非 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 --> F2
Loading

#3471 / #3488 合入后,每条轨道派生一个小 follow-up PR(~100-200 LOC),把 shell / monitor entry 接入统一 pill / dialog / task_stop —— 在 #3488 的 rename 完成后是纯增量。

已对齐的设计要点

这些影响 Phase A 和其上所有工作(详细论证在 #3471 / #3488 的 review threads 里):

  1. 统一 entry 类型BackgroundAgentEntryBackgroundTaskEntry + kind discriminator。趁 consumer 还少时改名,未来新 kind PR 才能纯增量。
  2. task-type-agnostic 工具task_stop / send_message 的 description 和错误从一开始就不绑 agent。后续 shell / monitor / 远端任务都通过同一对工具取消和下指令。
  3. 统一 transcript 路径<projectDir>/tasks/<sessionId>/<kind>-<id>.jsonl,避免后续每种 kind 加兄弟目录。
  4. 统一 pill / dialog 框架getPillLabel 从 Phase A 就 group-by-kind;dialog 的 ListBody 加 section 抽象;DetailBodykind 派发到不同子组件(AgentDetailBody / ShellDetailBody / ...)。
  5. PR 拆分两层 cleavage:每个新 task kind 切成"通用 plumbing 一个 PR + per-kind 扩展 follow-up"。

显式不在 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 的"未做"清单把 monitor send_message 列为待办,但仔细看这是架构层决定不是 plumbing 漏写:

  • agentsend_message 把文本指令塞进 agent 的 pendingMessages,下个 tool-round 边界 agent 当 prompt-like 指令消费。语义清晰因为 agent 是 LLM-driven,能接任意文本进 reasoning loop。
  • monitor / shell:receiver 没有 LLM。"文本消息"没有显然的消费通道。三种可能交付方式 — 写到子进程 stdin / 写到 monitor 脚本 poll 的 sidecar .signals 文件 / signal-based trigger — 都需要 receiver 端先约定协议,而 kind 现在还没这个协议。

参考设计 — Claude Code(哪些可借鉴 / 哪些不可借鉴)。Claude Code 的 SendMessage 完全不绑 task type。Rust 实现 src-rust/crates/tools/src/send_message.rs:31

static INBOX: Lazy<DashMap<String, Vec<AgentMessage>>> = Lazy::new(DashMap::new);

一个全局 inbox map,key 是 recipient 字符串。SendMessage 不知道 receiver 是哪种 kind — 只往 inbox 写;接收方自己 drain_inbox(my_id)这部分跟 monitor / shell 集成问题相关 — receiver 端抽象正是非 agent kind 自己决定接不接消息的关键。

TS 版还有 string | StructuredMessage payload(shutdown_request / shutdown_response / plan_approval_response,spec 03_tools.md:902-909),TUI 侧 useInboxPoller 1s 轮询按 type 派发。但这部分跟 monitor / shell 集成问题 无关 — 这三个 StructuredMessage variants 是 agent ↔ agent / user ↔ agent 协议信号(被 isAgentSwarmsEnabled() gate),驱动 Claude Code 的 multi-agent swarm coordinationplan-mode workflow。没有一个是 agent ↔ non-LLM-receiver 协议。早前草稿把"结构化 payload"跟"非 agent receiver"混在一起描述是错的 — 显式写出来避免后续 reader 重复这个推论。

如果以后要把 send_message 扩到 agent 之外,两个独立决定(不要捆绑):

  1. Receiver 端 routing(monitor / shell 问题) — 引入 inbox 抽象,让 kind 通过实现 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) 直接拒绝。
  2. Message 形态(string vs string | 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


cc @tanzhenxin @doudouOUC

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions