# 后台任务与通知

> 后台 Bash、后台子 Agent 与工作流 run 怎样登记进同一张内存任务表，结束后怎样变成一条只给模型看的 task-notification 消息，在回合中途注入或在空闲时自动开一轮；单个任务的停止分派，以及桌面端后台面板怎样与运行时对齐。

- 作者：David（道雾轩）
- 专栏：ZCode 源码解读（https://daiw.net/manual/zcode.md）
- 最后更新：2026-09-21
- 原文：https://daiw.net/manual/zcode/background-tasks
- 转载与引用：请注明出处并附原文链接（https://daiw.net/about/copyright）

ZCode 里能放到后台跑的东西有三类：Bash 命令（显式带 `run_in_background`，或前台跑过期限后原地转后台，`apps/zcode-cli/packages/core/src/tool/handlers/bash.ts:172`）、后台运行的子 Agent，以及动态工作流的 run。它们各自怎么启动分别见 [Bash](https://daiw.net/manual/zcode/bash)、[子 Agent](https://daiw.net/manual/zcode/subagents) 与[动态工作流（三）](https://daiw.net/manual/zcode/dwf-tools)。这一篇讲它们共用的那一层：每个会话一张内存任务表 `RuntimeTaskRegistry`，任务结束时由运行时铸造一段 `<task-notification>` XML，作为一条只给模型看的用户消息送回主 Agent；停止走同一个分派函数，桌面端的后台面板读的是同一组事件。

| 位置 | 职责 |
| --- | --- |
| `apps/zcode-cli/packages/core/src/runtime-task/` | 任务表 `registry.ts`、通知格式器 `notification.ts`、工作流专属文案 `workflow-notification-copy.ts`、子 Agent 封口判定 `notification-policy.ts` |
| `core/src/tool/executor/background-task*.ts` | 工具返回“已转后台”之后的追踪器：登记、轮询、发事件、认领并投递通知 |
| `core/src/runtime/methods/background*.ts` | 运行时侧：通知入队与持久化、停止分派、子 Agent 收尾时的封口与清扫 |
| `core/src/runtime/command-queue.ts` 与 `methods/runtime-command-*.ts` | 通知搭乘的运行时命令队列（受理与引导的整体机制见[输入受理](https://daiw.net/manual/zcode/prompt-admission)） |
| `packages/shared/src/background-*.ts` | 桌面端与服务层共用的控制项解析、合并、通知 XML 解析、输出快照 schema |

## 任务种类与任务表

任务类型是一个五值联合，注释交代了为什么不把两种工作流合成一类（`apps/zcode-cli/packages/core/src/runtime-task/registry.ts:10`）：

```ts
// local_dynamic_workflow 与 local_workflow 刻意分开：后者是 legacy `Workflow` 工具（不可取消），
// 前者是 workflow run（经 DynamicWorkflowRunPort.cancel 可取消）。合成一个类型，取消分派就无法区分。
export type RuntimeTaskType =
  | "local_agent"
  | "local_bash"
  | "local_workflow"
  | "local_dynamic_workflow"
  | "monitor_mcp";
```

| 类型 | 谁往表里登记 | 当前版本 |
| --- | --- | --- |
| `local_bash` | 追踪器，Bash 返回 `backgrounded` 时（`apps/zcode-cli/packages/core/src/tool/executor/background-task-registry.ts:205`） | 在用 |
| `local_agent` | 子 Agent runner 自己登记（`apps/zcode-cli/packages/core/src/subagent/runner.ts:181`、`448`），追踪器只发事件 | 在用 |
| `local_dynamic_workflow` | 追踪器，`CreateWorkflow`、`AmendWorkflow`、`ResumeWorkflowRun` 三个工具名（`background-task-registry.ts:39`） | 在用 |
| `local_workflow` | 追踪器，旧 `Workflow` 工具（`background-task-registry.ts:207`） | 该工具的导入与注册条目都被注释掉了（`apps/zcode-cli/packages/core/src/tool/handlers/index.ts:70`、`136`） |
| `monitor_mcp` | 无 | 只有类型与名字映射，没有找到登记方 |

条目类型 `RuntimeTaskSnapshot` 扩展自子 Agent 的任务快照（`registry.ts:43`）。状态沿用 `SubagentTaskStatus` 的七个值（`apps/zcode-cli/packages/contracts/src/interfaces/subagent.port.ts:87`），除 `running` 外全算终态（`registry.ts:103`）。几个关键字段：

- `notified`：终态结果“已送达”的认领令牌（`subagent.port.ts:110`），下文细说。
- `isBackgrounded`：前台子 Agent 也登记在表里，但只有这一位为真、状态为 `running` 的条目才算“正在跑的后台任务”（`registry.ts:288`）。会话常驻池据此判断能不能回收会话（`apps/zcode-cli/packages/core/src/runtime/methods/residency.ts:24`）。
- `branchGeneration`：登记时盖上当前对话分支代数（`registry.ts:118`）。回退对话会把代数加一（`apps/zcode-cli/packages/core/src/runtime/methods/rewind-message.ts:581`），旧分支上任务的迟到结果随之作废，回退本身见[检查点、回退与分叉](https://daiw.net/manual/zcode/rewind-fork)。
- `pendingMessages`、`messageSink`：给运行中的子 Agent 转发消息用，归[子 Agent](https://daiw.net/manual/zcode/subagents)。
- `resultText`、`stopInitiator`：工作流 run 的产物文本，以及是谁请求的停止（`"user"` 或 `"model"`，`registry.ts:63`）。

实现 `InMemoryRuntimeTaskRegistry`（`registry.ts:112`）就是一个 `Map` 加两组等待者：`waitForTerminal` 等终态，`waitForBackgroundRequest` 等“转后台”，都接受 `AbortSignal`。每个 `AgentRuntime` 构造时新建一张（`apps/zcode-cli/packages/core/src/runtime/agent-runtime.ts:284`），不落盘，进程重启即清空。

前台子 Agent 转后台的入口有两个：`SubagentPort.backgroundTask` 调 `requestBackground`（`runner.ts:532`），或者 `subagents.autoBackgroundMs` 到时自动转（`runner.ts:660`）。当前版本里前者没有调用方，后者只在 core 的配置类型里声明（`apps/zcode-cli/packages/core/src/runtime/types.ts:142`），bootstrap 与桌面端都没有设置，这条路实际处于休眠状态。转入后台的子 Agent 会与父回合的中止信号脱钩（`runner.ts:319`），所以停掉前台回合不会连带停掉它。

## 生命周期

工具调用一结束，执行器在发出 `ToolCallResult` 事件之后调 `trackBackgroundTask`（`apps/zcode-cli/packages/core/src/tool/executor/call-runner.ts:543`）。它只认两种输出：`status` 为 `backgrounded`，或 Agent 工具返回的 `async_launched`（`apps/zcode-cli/packages/core/src/tool/executor/background-tasks.ts:805`）。之后的步骤：

1. 登记进任务表，发 `BackgroundTaskStarted` 事件（`background-tasks.ts:108`、`110`）。
2. 按工具名查一张“生命周期提供者”表，得到快照来源、终态等待者与能否取消（`background-tasks.ts:739`）。旧 `Workflow` 的 `cancellable` 写死为假（`background-tasks.ts:776`）。两种来源都没有时，直接判 `lost`（`background-tasks.ts:134`）。
3. 有快照来源就每 1000 毫秒轮询一次（`background-tasks.ts:353`），有终态等待者的话同时挂上（`background-tasks.ts:360`）。运行中的更新只在 pid、字节数或输出尾部变化时才发 `BackgroundTaskUpdated`（`background-tasks.ts:206`、`437`），这是唯一的“节流”，没有按时间合并。
4. 看到终态：更新任务表，认领并投递通知，发 `BackgroundTaskCompleted`（`background-tasks.ts:285`）。

执行端口的状态词在写进任务表时被折算：`cancelled` 与 `timed_out` 记为 `killed`，`spawn_error` 等其余值记为 `failed`（`background-task-registry.ts:233`）。子 Agent 运行时里的后台 Bash 另有一道上限，默认 3600000 毫秒（1 小时）到点就取消（`apps/zcode-cli/packages/core/src/runtime/helpers/runtime-tools.ts:25`，计时器在 `background-tasks.ts:177`）。工作流 run 用 `ResumeWorkflowRun` 恢复时沿用同一个 runId，登记函数会把上一轮的 `notified`、完成时间、错误等“结算面”整体复位，并重新盖分支代数，否则恢复后的终态通知会被当成已认领或旧分支残留吞掉（`background-task-registry.ts:78`）。

```mermaid
stateDiagram-v2
  direction LR
  state "前台运行" as FG
  state "后台运行 running" as BG
  state "终态 completed failed killed lost" as TERM
  state "已认领 notified" as CLAIMED
  state "task-notification 命令" as CMD
  [*] --> FG: 前台子 Agent
  [*] --> BG: 登记（Bash、工作流、后台子 Agent）
  FG --> BG: requestBackground（当前未接线）
  FG --> TERM: 前台跑完，结果直接是工具结果
  BG --> BG: 轮询，BackgroundTaskUpdated
  BG --> TERM: 结束、停止、超时或快照丢失
  TERM --> CLAIMED: 第一个认领者胜出
  CLAIMED --> [*]: TaskOutput 已读到结果
  CLAIMED --> CMD: 通知入队
  CMD --> [*]: 回合中注入，或空闲时新开一轮
```

同一个任务在各层用的是不同的状态词，读代码时容易混：

| 层 | 取值 | 出处 |
| --- | --- | --- |
| 任务表 | `running`、`completed`、`failed`、`cancelled`、`killed`、`stopped`、`lost` | `subagent.port.ts:87` |
| 执行端口与事件载荷 | `running`、`completed`、`failed`、`timed_out`、`cancelled`、`spawn_error`、`lost` | `apps/zcode-cli/packages/contracts/src/interfaces/session.port.ts:163` |
| 通知的 `<status>` | Bash 与工作流为 `completed`、`failed`、`killed`，子 Agent 为 `completed`、`failed`、`stopped`，动态工作流另有真实终态词 `completed`、`errored`、`stopped` | `background-tasks.ts:813`、`apps/zcode-cli/packages/core/src/subagent/completion-notification.ts:4`、`apps/zcode-cli/packages/core/src/runtime-task/notification.ts:53` |
| 桌面 v4 快照 | `running`、`resultPending`、`failed`、`cancelled` | `packages/shared/src/zcode-protocol-v4/snapshot.ts:371` |
| 旧投影栈控制项 | `pending`、`running`、`completed`、`failed`、`killed`、`lost` | `packages/shared/src/background-task-controls.ts:1` |

## 通知长什么样

`formatTaskNotification` 按任务类型分到三个格式器，其余类型走一个带缩进的通用格式（`notification.ts:91`）。Bash 的最精简（`notification.ts:129`）：

```ts
function formatLocalBashTaskNotification(input: TaskNotificationInput): string {
  const lines = ["<task-notification>", `<task-id>${escapeLocalBashXml(input.taskId)}</task-id>`];
  if (input.toolUseId) {
    lines.push(`<tool-use-id>${escapeLocalBashXml(input.toolUseId)}</tool-use-id>`);
  }
  if (input.outputFile) {
    lines.push(`<output-file>${escapeLocalBashXml(input.outputFile)}</output-file>`);
  }
  lines.push(`<status>${escapeLocalBashXml(input.status)}</status>`);
  lines.push(`<summary>${escapeLocalBashXml(input.summary)}</summary>`);
  lines.push("</task-notification>");

  return lines.join("\n");
}
```

Bash 通知不带输出正文，只给输出文件路径，摘要形如 `Background command "<描述>" completed (exit code 0)`（`background-tasks.ts:843`），模型要看输出得自己调 `TaskOutput` 或读文件。三种格式的差别：

| 类型 | 额外字段 | 转义与截断 |
| --- | --- | --- |
| Bash | 无 | 只转义 `<`、`>`、`&`，不截断（`notification.ts:404`） |
| 子 Agent | `<result>`（子 Agent 的最终回答）、`<error>`、`<usage>`（token、工具调用次数、耗时）（`notification.ts:144`） | 全量转义，总长超过 120000 字符截断并追加 `[truncated]`（`notification.ts:18`、`380`） |
| 工作流 run | `<stop-reason>`、`<result>`、`<reports>`、`<artifacts>`，XML 之后再追加一段“怎样把结果呈现给用户”的指引（`notification.ts:160`、`235`） | 同上；指引、产物清单排在最后，截断时先被砍掉 |

工作流指引按终态分支写死了下一步：用户停的 run 明确要求“不要自行恢复”（`notification.ts:264`），模型自己 `TaskStop` 的 run 提示用 `AmendWorkflow` 修订，出错的 run 指向修改脚本。动态工作流运行中的“停滞”与“升级提问”也走同一条通知管线（`apps/zcode-cli/packages/core/src/runtime/methods/dynamic-workflow-run-progress.ts:73`、`143`），细节见[动态工作流（三）](https://daiw.net/manual/zcode/dwf-tools)。

## 认领：一个终态只送达一次

`notified` 是一个先到先得的令牌。追踪器必须先认领成功才入队（`background-tasks.ts:536`），入队抛错就归还令牌（`background-tasks.ts:573`）；`TaskOutput` 读到终态结果时也会把它置真（`apps/zcode-cli/packages/core/src/tool/handlers/task-output.ts:60`），所以模型主动读过的任务不会再收到重复通知。子 Agent 的通知由 runner 自己投递并认领（`runner.ts:1848`），追踪器看到已认领的子 Agent 快照就只收尾不再通知（`background-tasks.ts:264`）。

即使认领成功，下面几种情况也不送达：

- 被 `AmendWorkflow` 替代的 run：发起修订的那次工具结果已经说明了一切，再来一条“你停了 run，去修订它”会把模型送进循环（`background-tasks.ts:487`）。
- 运行时正在关闭：直接丢弃，不再启动模型轮次（`apps/zcode-cli/packages/core/src/runtime/methods/background-notifications.ts:20`、`runtime-tools.ts:242`）。
- 分支代数对不上：入队时查一次（`background-notifications.ts:33`），出队、持久化前再查一次（`apps/zcode-cli/packages/core/src/runtime/methods/runtime-command-generation.ts:17`）。
- 子 Agent 已经结束：子 Agent 那一轮跑完（正常结束或被取消）后，其运行时把后台通知“封口”（`apps/zcode-cli/packages/core/src/runtime/methods/subagent.ts:409`），此后该子运行时里后台 Bash 的通知一律压下（`apps/zcode-cli/packages/core/src/runtime-task/notification-policy.ts:9`）；如果子 Agent 是被取消的，还会顺手取消它名下仍在跑的后台 Bash（`subagent.ts:414`）。

## 送回主 Agent

通知以一条运行时命令入队（`background-notifications.ts:44`）：

```ts
  const commandId = createRuntimeCommandId();
  this.enqueueRuntimeCommand({
    branchGeneration,
    createdAt: new Date(),
    id: commandId,
    mode: "task-notification",
    priority: "next",
    source: "background_task",
    originMeta: notification.originMeta,
    taskId: notification.taskId,
    text: notification.text,
    toolName: notification.toolName,
    traceContext: notification.traceContext,
  });
```

队列有 `now`、`next`、`later` 三档优先级（`apps/zcode-cli/packages/core/src/runtime/command-queue.ts:10`），但 core 里所有入队点写的都是 `next`（例如 `apps/zcode-cli/packages/core/src/runtime/methods/prompt-admission.ts:116`），实际就是先进先出。队列只在内存里，所以入队的同时还往会话输入账本写一条 `backgroundNotification`（`background-notifications.ts:61`）；进程崩溃后重启恢复时，残留的这类记录被收口为 `discarded`，原因 `session_resumed`（`apps/zcode-cli/packages/core/src/runtime/methods/steering.ts:1340`），因为后台子进程已随 CLI 一起退出。

之后分两条路：

**回合进行中**。回合循环每一轮开头、且已经完成过至少一次模型请求时，先把队列里的通知与子 Agent 回复取进本轮请求（`apps/zcode-cli/packages/core/src/runtime/methods/turn-loop.ts:55`，这段代码在[输入受理](https://daiw.net/manual/zcode/prompt-admission)里有全文）。也就是在一批工具结果之后、下一次模型请求之前注入；输出 token 截断后的续写期间不注入。每条通知各自持久化成一条消息，展示意图是 `task_notification_steer`（`apps/zcode-cli/packages/core/src/runtime/methods/runtime-command-active-loop.ts:76`）。遇到排队中的“配置”控制轮就停下，把后面的通知留给外层按序处理（`runtime-command-active-loop.ts:37`）。注入后重复工具调用检测清零，回合循环本身见[回合循环](https://daiw.net/manual/zcode/turn-loop)。

**回合之外**。入队后立即尝试排空队列（`apps/zcode-cli/packages/core/src/runtime/methods/runtime-command-queue.ts:34`）。出队时如果队首是通知，队列会把同优先级的所有通知一起带走，而不是一条一条出（`command-queue.ts:184`）。整批只写一条消息，各段 XML 以空行拼接（`background-notifications.ts:174`），展示标题取前 3 个任务名加“+N”（`background-notifications.ts:116`、`149`）；注释说过去逐条启动模型轮，请求数会随积压线性放大（`background-notifications.ts:192`）。然后以 `inputSource: "background_task"`、仅模型可见、跳过 `UserPromptSubmit` 钩子的方式开一轮（`runtime-command-queue.ts:345`）。

所以主会话空闲时，后台任务一结束就会自动起一轮，没有开关；只有“立即发送排队消息”持有的前台提升租约会让它稍等（`runtime-command-queue.ts:72`）。这一轮结束后如果会话有活动目标，还会接着跑目标续跑循环（`runtime-command-queue.ts:167`，见[目标模式](https://daiw.net/manual/zcode/goal-target)）。

这条消息的角色是 user：运行时把它当作直接注入的用户消息，而不是包成系统提醒（`apps/zcode-cli/packages/core/src/runtime/helpers/runtime-reminders.ts:99`），落库时标 `visibility: "model-only"`（`background-notifications.ts:184`）。桌面端把这样开出的一轮画成来源为 `backgroundResult` 的回合头（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/event-normalizer.ts:403`）。

```mermaid
flowchart TD
  A["工具结果：backgrounded 或 async_launched"] --> B["trackBackgroundTask"]
  B --> C["登记任务表，发 BackgroundTaskStarted"]
  C --> D{"到终态？"}
  D -->|"否"| E["1 秒轮询或等待终态"]
  E --> D
  D -->|"是"| F{"认领 notified"}
  F -->|"已被 TaskOutput 认领"| X["不再通知"]
  F -->|"认领成功"| G["enqueueBackgroundTaskNotification"]
  G --> L["会话输入账本 admitted"]
  G --> H["运行时命令队列：task-notification"]
  H --> I{"回合进行中？"}
  I -->|"是"| J["下一次模型请求前注入"]
  I -->|"否"| K["同批合并为一条消息，新开一轮"]
```

<Callout type="info">
  无头模式 `zcode -p` 只在观察到动态工作流活动时才等后台工作结算（`apps/zcode-cli/packages/cli/src/prompt-command.ts:315`）。只有后台 Bash 或后台子 Agent 时，第一回合一结束就进入收尾；会话关闭时执行端口会把仍在跑的后台 Bash 记为取消（`apps/zcode-cli/packages/adapters/src/exec/node-execution-adapter-base.ts:259`），它们的通知也不会再送达。
</Callout>

## 停止：一条分派，三个入口

| 入口 | 调用 | 发起者 | 已终结的任务 |
| --- | --- | --- | --- |
| 模型的 `TaskStop` | `backgroundTaskControlPort.stopBackgroundTask`，绑定到运行时的同名方法（`runtime-tools.ts:170`） | `model`，`strict: true`（`apps/zcode-cli/packages/core/src/tool/handlers/task-stop.ts:50`） | 工具报错“is not running” |
| 桌面端 Stop 按钮 | v4 `cancelBackgroundWork`，经 `cancelBackgroundTask` | `user`（`apps/zcode-cli/packages/core/src/runtime/methods/background.ts:50`） | 分派层宽松成功，`cancelBackgroundTask` 再补上 `background_task_not_running`（`background.ts:67`），网关据此回拒绝 |
| TUI 的 `/dwf cancel` | 同上 | `user` | 回一句“Could not cancel”，设计上只针对工作流 run |

三个入口最终都进 `stopBackgroundTask`：先从任务表与事件投影两处解析目标（`background.ts:132`），再按类型分派（`background.ts:112`）：

```ts
  if (target.taskType === "local_agent") {
    return stopLocalAgentBackgroundTask.call(this, target);
  }

  if (target.taskType === "local_bash") {
    return stopLocalBashBackgroundTask.call(this, target, traceContext);
  }

  if (target.taskType === "local_dynamic_workflow") {
    return stopDynamicWorkflowBackgroundTask.call(
      this,
      target,
      unsupportedBackgroundStopResult,
      options.initiator,
    );
  }

  return unsupportedBackgroundStopResult(target);
```

- **子 Agent**：调 `subagentPort.stopTask`，任务表记 `killed`，通知里的状态写 `stopped`，后台事件写 `cancelled`，子 Agent 事件写 `stopped`（这组词集中定义在 `runner.ts:1655`）；停止通知没能入队就把任务表恢复原状并报错（`runner.ts:1713`）。
- **Bash**：先补发一条带 `cancelRequestedAt`、`cancellable` 为假的 `BackgroundTaskUpdated`，让面板先把它标成不可再取消（`background.ts:215`），再调执行端口取消；端口已查不到这个任务就判 `lost`（`background.ts:231`）。
- **动态工作流**：先把发起者写进任务表的 `stopInitiator`，再调 `DynamicWorkflowRunPort.cancel`，顺序不能反，否则终态通知可能读到空值（`apps/zcode-cli/packages/core/src/runtime/methods/background-stop-dynamic-workflow.ts:45`）。
- **旧 `Workflow`、`monitor_mcp`**：返回 `background_task_cancel_not_supported`。

失败原因只有三个：`background_task_not_found`、`background_task_not_running`、`background_task_cancel_not_supported`（`apps/zcode-cli/packages/core/src/runtime/methods/background-stop-types.ts:15`）。

面向用户的“全部停止”在代码里没有找到，只有几处隐式清扫：子 Agent 被取消时停掉它名下在跑的后台 Bash（`background.ts:294`）；会话关闭时先 `beginShutdown` 置 `shuttingDown` 挡住通知（`agent-runtime.ts:327`），再停下本会话拥有的动态工作流 run，最后由执行端口把在跑的后台 Bash 记为取消（`apps/zcode-cli/packages/bootstrap/src/app/session-facade.ts:267`、`274`、`297`，`node-execution-adapter-base.ts:259`）。

## 桌面端怎样与运行时对齐

事实只从运行时的三个 `BackgroundTask*` 事件出。展示类别 `taskKind`（`bash`、`subagent`、`workflow`）在发事件处一次裁决（`background-tasks.ts:397`、`1157`），`cancellable` 取自生命周期提供者（`background-tasks.ts:402`）。v4 投影把事件归约成快照里的 `backgroundWorks`（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/product-projection.ts:4213`）：状态压成四个值，`completed` 记为 `resultPending`，`timed_out`、`spawn_error`、`lost` 都记为 `failed`（`product-projection.ts:4248`）；内容无变化不产出增量（`product-projection.ts:4288`）。`workId` 就是 `taskId`，对工作流 run 也等于 runId，全程不做身份翻译（`background-stop-dynamic-workflow.ts:24`）。

UI 这一侧：

- 输入框旁的徽标统计正在跑的 Bash、工作流 run 与子 Agent 数量，三类合计大于零才显示（`packages/ui/src/v4/composer/ConversationBackgroundWorkTrigger.tsx:16`、`65`）；点开的是状态面板还是工作流详情，由宿主决定（`ConversationBackgroundWorkTrigger.tsx:50`）。
- 状态面板只给仍在 `running` 的条目 Stop 按钮，已结束的条目没有可取消的东西（`packages/ui/src/v4/conversationStatusPanelModel.ts:226`）。
- Stop 发出 `cancelBackgroundWork {workId}`（`packages/ui/src/v4/SessionPane.tsx:2118`）。运行时明确回答“什么也没取消”时，网关以 `fault.command.backgroundWorkCancelRejected.` 加去掉 `background_task_` 前缀的原因作为 ACK 返回，而不是假装成功（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/commands/handlers/interaction-background.ts:187`、`packages/shared/src/zcode-protocol-v4/command.ts:289`）。
- 后台 Bash 的详情页经 `v4/conversation/backgroundBashOutput` 读一次文件快照（`packages/shared/src/zcode-protocol-v4/transport.ts:320`），最多 8192 字节（`packages/shared/src/background-bash-output.ts:3`）；找执行器时沿父会话链往上走，绝不为了看输出去恢复运行时（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol/background-work-owner.ts:5`）。
- 侧栏的“有后台活动”标记也由 `backgroundWorks` 里有无 `running` 条目决定（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol-v4/sessions-index-projection.ts:43`）。

`snapshot.ts:369` 的注释说 `resultPending` 条目在结果投递后消失，但投影里只看到把 `completed` 映射成 `resultPending`，没有找到移除条目的代码，从代码看条目会一直留在快照里。

`packages/shared` 下几个 `background-*` 文件服务的是旧投影栈：core 的事件归约维护 `projection.backgroundTasks`，旧协议把它映射成 `backgroundJobs`（`apps/zcode-cli/packages/bootstrap/src/zcode-protocol/session-mapper.ts:538`），服务层再用 `parseZCodeBackgroundTaskControlItems` 解析、按 `jobId` 合并（`packages/services/src/zcode-agent/zcodeTaskServiceAdapter.ts:1550`、`packages/shared/src/background-task-control-merge.ts:3`）。解析时 `jobId` 必须优先读 `taskId`，注释说落到 `toolCallId` 会让取消请求查无此任务（`background-task-controls.ts:300`）。同一层还会从用户消息里解析 `<task-notification>`，按 `tool-use-id` 回写原工具卡的状态（`packages/shared/src/background-task-notifications.ts:14`、`57`）；旧栈推导会话状态时只看活动回合、待审批与在跑的工具调用，后台任务不算在内（`packages/shared/src/zcode-session-task-status.ts:20`）。按运行 30 秒过滤可见任务的 `collectVisibleZCodeBackgroundTaskControlItems`（`background-task-controls.ts:391`）没有找到调用方，旧的 `session/cancelBackgroundTask` 协议方法也已标为废弃（`packages/shared/src/zcode-protocol/index.ts:3579`）。

TUI 没有后台任务面板。`apps/zcode-cli/packages/tui/src/types.ts:317` 声明了 `cancelBackgroundTask`，但没有调用方；唯一直接取消的入口是 `/dwf cancel`，缺省只从 `pending` 与 `running` 的 run 里挑（`apps/zcode-cli/packages/cli/src/command-center/handlers/dwf.ts:19`、`88`）。其余后台任务只能让模型调 `TaskStop`。

下一篇：[定时任务与闲时任务](https://daiw.net/manual/zcode/cron-offpeak)——谁来调度定时任务与闲时任务，任务定义存在哪里，自动化回合为什么要禁掉一部分工具。
