# 任务执行管道完全解析

本章解析 Claude Code 的后台任务管理系统——七种任务类型如何共享统一的生命周期模型，同时各自处理从本地 Shell 命令到远程云端 Agent 的不同执行语义。

## 概述

Claude Code 的任务系统管理着从本地 Shell 命令到远程云端 Agent 的所有后台工作。7 种任务类型共享统一的生命周期模型——注册、运行、完成/失败、通知、清理——同时各自处理截然不同的执行语义。

**技术比喻（OS 视角）**：任务管道就像操作系统的**进程管理器（process manager）**——负责注册、生命周期追踪和信号传递，而不是像进程调度器（scheduler）那样决定"谁先执行"（Claude Code 的任务系统没有时间片、优先级队列或抢占机制）。每种任务类型是一种进程类别（内核线程、用户进程、远程 RPC），它们共享相同的进程状态机（就绪→运行→终止→僵尸→回收），但内部的资源管理和信号处理各不相同。`isBackgrounded` 标志就像 Unix 的前台/后台进程组切换（`fg`/`bg`）。

> 💡 **通俗理解**：任务管道像物流中心——本地任务 = 同城快递（即时可控、直接通信），远程任务 = 跨城物流（有延迟、需要追踪号、可能丢件），Dream 任务 = 仓库夜间自动盘点（无人值守、自动运行、不需要签收回执）。而 `LocalMainSessionTask` 就像你在餐厅点了一道慢炖菜，厨师说要 30 分钟——你说"那先上其他菜，那道好了叫我"。慢炖菜继续在后厨做着（后台运行），你先吃别的（新提示符），做好了服务员通知你（notified）。

### 🌍 行业背景

后台任务管理是 AI 编程工具从"单轮对话"走向"持续工作伙伴"的关键能力。各工具的支持程度差异极大：

- **Cursor**：推出 Background Agents 后已具备后台任务能力——云端 VM 中的 Agent 可长时间并行执行，开发者通过后台面板监控进度。但其后台模型是"云端 VM 生命周期"而非 Claude Code 的"本地进程前后台切换"，两者在任务管理粒度上有显著差异。
- **Kimi Code**：据公开资料，其 Agent Swarm 架构下协调器可同时管理据宣称可达上百个并发子智能体的生命周期（具体上限与调度细节以 Moonshot 官方公告为准），在并行任务管理的规模上居于前列，但其任务模型面向云端集群调度而非终端用户的交互式前后台切换。
- **Codex（OpenAI）**：据公开资料，其 v0.118.x 版本（具体版本号以 OpenAI 官方发布为准）支持并行 Agent 工作流和邮箱通信机制，可对接 CI/CD 事件流实现无人值守修复，具备一定的后台任务能力。
- **Aider**：核心仍为纯同步模型——每次只能执行一个操作，没有后台任务、没有前后台切换。AiderDesk 通过 Vercel AI SDK 赋予了一定的并发能力。
- **Devin**：推出 Manage Devins 后，支持在多个隔离 VM 中并发运行子智能体（Managed Devins），配合 Human-in-the-loop 工作台实时监控。用户通过专用控制台查看每个子智能体的屏幕并直接干预执行流。
- **GitHub Copilot**：Agent Mode 已全面 GA，内置 Explore、Plan、Task 等专职智能体，支持自主解读问题意图并迭代修复代码。企业级 MCP 注册表机制支撑了异步任务流的集成。

Claude Code 的任务系统在终端工具中属于完善度最高的之一——7 种任务类型、统一的生命周期模型、前后台切换、远程任务轮询恢复。这种复杂度通常只出现在 IDE 级别的产品中，在 CLI 工具中实现到这个程度在行业中并不常见。

---

## 1. 任务类型全景

### 1.1 类型联合

系统定义了 7 种具体的任务状态类型：

```typescript
// 源码: src/tasks/types.ts (第12-19行)
export type TaskState =
  | LocalShellTaskState       // 本地 Shell 命令
  | LocalAgentTaskState       // 本地 Agent 子任务
  | RemoteAgentTaskState      // 远程云端 Agent
  | InProcessTeammateTaskState // 进程内团队成员
  | LocalWorkflowTaskState    // 本地工作流
  | MonitorMcpTaskState       // MCP 监控任务
  | DreamTaskState            // 记忆整合 Dream 任务
```

每种类型都是 `TaskStateBase` 的扩展，共享基础字段但携带特有的状态数据。

> 📌 **章节覆盖说明**：下文 §2–§7 深入解析其中 5 类最常用的具体任务态（LocalShell/LocalAgent/RemoteAgent/InProcessTeammate/Dream），外加 `LocalMainSessionTask`——它**不是第 8 种 concrete 类型**，而是 `type: 'local_agent'` + `agentType: 'main-session'` 的特化壳，用于"主会话后台化"场景。`LocalWorkflowTaskState`（本地工作流）与 `MonitorMcpTaskState`（MCP 监控任务）共享同一套生命周期/停止/底栏机制（见 §8–§9），差异主要在执行载体与触发方式，本章不单独展开，可在源码 `src/tasks/types.ts` 类型定义与对应消费点（`src/tasks.ts`、`src/components/tasks/BackgroundTasksDialog.tsx` 等）查阅。

### 1.2 后台任务判定

任务是否显示在底栏指示器中由一个统一的类型守卫决定：

```typescript
// 源码: src/tasks/types.ts (第37-46行)
export function isBackgroundTask(task: TaskState): task is BackgroundTaskState {
  if (task.status !== 'running' && task.status !== 'pending') {
    return false
  }
  // 前台任务 (isBackgrounded === false) 不算"后台任务"
  if ('isBackgrounded' in task && task.isBackgrounded === false) {
    return false
  }
  return true
}
```

关键语义：一个任务只有在"正在运行或等待中"且"已被后台化"时才是后台任务。前台运行的任务（用户正在观察其输出）不显示在后台指示器中。

> 📚 **课程关联**：任务的状态模型（pending→running→completed/failed/killed→notified→eviction）直接对应《操作系统》中的**进程状态机**（就绪→运行→终止→僵尸→回收）。`isBackgrounded` 标志等价于 Unix 的前台/后台进程组概念（`fg`/`bg` 命令），而 `notified` 标志的作用类似于 `wait()` 系统调用收割僵尸进程——只有父进程确认收到退出状态后，资源才能被回收。

---

## 2. LocalShellTask — 本地 Shell 命令

### 2.1 状态定义

```typescript
// 源码: src/tasks/LocalShellTask/guards.ts (第9-32行)
export type LocalShellTaskState = TaskStateBase & {
  type: 'local_bash'
  command: string                    // 执行的命令
  result?: { code: number; interrupted: boolean }  // 退出码和中断标志
  completionStatusSentInAttachment: boolean
  shellCommand: ShellCommand | null  // 底层 Shell 进程句柄
  unregisterCleanup?: () => void
  cleanupTimeoutId?: NodeJS.Timeout
  lastReportedTotalLines: number     // 用于计算输出增量
  isBackgrounded: boolean
  agentId?: AgentId                  // 生成此任务的 Agent
  kind?: BashTaskKind                // 'bash' | 'monitor'
}
```

注意 `type: 'local_bash'` 而非 `local_shell`——源码注释解释这是为了与持久化的会话状态**向后兼容**。重命名会导致恢复旧会话时任务类型无法识别。

其他字段的含义（便于读懂上面的类型定义）：

- `ShellCommand`：`src/tasks/LocalShellTask/` 下定义的底层 Shell 子进程句柄类型，内部封装了 `child_process.spawn` 返回对象、stdout/stderr 流、以及 pid / status 等元信息。本章只把它当成"一个可 kill / 可读输出的进程句柄"理解即可，具体字段结构见源码。
- `unregisterCleanup`：任务注册时会挂一个全局 process exit / abort cleanup，这个字段保存对应的反注册函数（任务正常结束时调用它，防止泄漏监听器）。
- `cleanupTimeoutId`：任务被标记 `notified` 之后延迟驱逐用的 `setTimeout` 句柄，`kill()` 或重新注册时用它做 `clearTimeout`。

### 2.2 任务变体

`kind` 字段区分两种用途：
- `'bash'`：标准 Shell 命令执行
- `'monitor'`：长期运行的监控命令（如 `tail -f`、`watch`）

监控任务在 UI 上显示描述而非命令，使用 "Monitor details" 对话框标题，有独立的状态栏徽标。

### 2.3 Agent 关联

`agentId` 字段追踪哪个 Agent 创建了此 Shell 任务。当 Agent 退出时，其创建的孤儿 Shell 任务会被自动终止（`killShellTasksForAgent`），防止资源泄漏。`undefined` 表示主线程创建的任务。

---

## 3. LocalAgentTask — 本地 Agent 子任务

### 3.1 状态定义

```typescript
// 源码: src/tasks/LocalAgentTask/LocalAgentTask.tsx (第116-148行)
export type LocalAgentTaskState = TaskStateBase & {
  type: 'local_agent'
  agentId: string
  prompt: string
  selectedAgent?: AgentDefinition
  agentType: string
  model?: string
  abortController?: AbortController
  error?: string
  result?: AgentToolResult
  progress?: AgentProgress
  retrieved: boolean
  messages?: Message[]
  lastReportedToolCount: number
  lastReportedTokenCount: number
  isBackgrounded: boolean
  pendingMessages: string[]     // SendMessage 排队的消息
  retain: boolean               // UI 是否持有此任务
  diskLoaded: boolean           // 是否已从磁盘加载历史
  evictAfter?: number           // 驱逐截止时间戳
}
```

### 3.2 进度追踪

Agent 进度通过 `ProgressTracker` 精确追踪 token 消耗：

```typescript
// 源码: src/tasks/LocalAgentTask/LocalAgentTask.tsx (第50-60行)
export type ProgressTracker = {
  toolUseCount: number
  latestInputTokens: number        // 最新值（API 中是累计的）
  cumulativeOutputTokens: number   // 累加值
  recentActivities: ToolActivity[]
}

export function getTokenCountFromTracker(tracker: ProgressTracker): number {
  return tracker.latestInputTokens + tracker.cumulativeOutputTokens
}
```

关键细节：Claude API 的 `input_tokens` 是每轮累计值（包含所有历史上下文），而 `output_tokens` 是每轮独立值。所以输入取最新值（`latestInputTokens`——定义见 `src/tasks/LocalAgentTask/LocalAgentTask.tsx` 第 52 行附近的 `ProgressTracker` 类型），输出做累加（`cumulativeOutputTokens`——同文件第 53 行附近）。如果都累加，输入 token 会被严重高估。

### 3.3 活动描述

每个工具调用都生成人类可读的活动描述：

```typescript
// 源码: src/tasks/LocalAgentTask/LocalAgentTask.tsx (第23-31行)
export type ToolActivity = {
  toolName: string
  input: Record<string, unknown>
  activityDescription?: string  // 预计算: "Reading src/foo.ts"
  isSearch?: boolean            // 预计算: Grep/Glob 等
  isRead?: boolean              // 预计算: Read/cat 等
}
```

活动描述在记录时预计算（而非显示时），通过 `ActivityDescriptionResolver` 查询工具定义获取。这避免了渲染时的重复计算。

### 3.4 面板生命周期

`retain`、`diskLoaded`、`evictAfter` 三个字段管理 UI 面板的精确生命周期：

- `retain = true`：用户正在查看此任务的面板，阻止任务被驱逐
- `diskLoaded`：面板打开时从磁盘 JSONL 文件加载完整历史（一次性操作）
- `evictAfter`：任务终止后设置的截止时间戳，过期后 GC 可以回收

这个三阶段模型确保了：打开面板 → 从磁盘恢复历史 → 实时追加流式消息 → 关闭面板 → 延迟驱逐。

---

## 4. RemoteAgentTask — 远程云端 Agent

### 4.1 状态定义

```typescript
// 源码: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx (第22-59行)
export type RemoteAgentTaskState = TaskStateBase & {
  type: 'remote_agent'
  remoteTaskType: RemoteTaskType
  remoteTaskMetadata?: RemoteTaskMetadata
  sessionId: string               // 远程会话 ID
  command: string
  title: string
  todoList: TodoList
  log: SDKMessage[]
  isLongRunning?: boolean         // 不会在首次结果后标记完成
  pollStartedAt: number           // 本地轮询开始时间
  isRemoteReview?: boolean        // 远程代码审查
  reviewProgress?: { stage?: string; bugsFound: number; bugsVerified: number; bugsRefuted: number }
  isUltraplan?: boolean           // Ultraplan 模式
  ultraplanPhase?: 'needs_input' | 'plan_ready'
}
```

### 4.2 远程任务类型

```typescript
// 源码: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx (第60-70行)
const REMOTE_TASK_TYPES = [
  'remote-agent',     // 通用远程 Agent
  'ultraplan',        // 超级计划模式
  'ultrareview',      // 远程代码审查
  'autofix-pr',       // 自动修复 PR
  'background-pr',    // 后台 PR 处理
] as const
export type RemoteTaskType = (typeof REMOTE_TASK_TYPES)[number]

// RemoteTaskMetadata 目前是单一形态（autofix-pr 专用字段）的别名，为将来扩展保留联合类型接口
export type AutofixPrRemoteTaskMetadata = { /* autofix-pr 专用字段 */ }
export type RemoteTaskMetadata = AutofixPrRemoteTaskMetadata
```

5 种远程任务类型都通过同一个 `remoteTaskType: RemoteTaskType` 字段归一识别，共享同一个 HTTP 轮询（`pollRemoteSessionEvents` 每秒 HTTP GET + 游标分页）和通知机制。上文状态定义里出现的 3 个具名类型字段（`isLongRunning` / `isRemoteReview` / `isUltraplan`）是对这 5 种类型的**行为维度标记**（不是另一层类型 union），便于 UI 差异化渲染。

### 4.3 完成检查器

远程任务支持可注册的完成检查器，在每次轮询时调用：

```typescript
// 源码: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx (第77-86行)
export type RemoteTaskCompletionChecker =
  (remoteTaskMetadata: RemoteTaskMetadata | undefined) => Promise<string | null>

const completionCheckers = new Map<RemoteTaskType, RemoteTaskCompletionChecker>()

export function registerCompletionChecker(
  remoteTaskType: RemoteTaskType,
  checker: RemoteTaskCompletionChecker,
): void {
  completionCheckers.set(remoteTaskType, checker)
}
```

返回非 null 字符串表示任务完成（字符串成为通知文本），返回 null 表示继续轮询。检查器应自行节流，因为每次轮询 tick 都会调用它。

> 📚 **课程关联**：远程任务的轮询模型（每秒 HTTP GET + 游标分页）vs 推送模型（WebSocket）的权衡，是《计算机网络》中**Pull vs Push** 架构的经典决策。轮询的优势是无状态、天然容错（符合 HTTP 的幂等语义）；推送的优势是低延迟。Claude Code 选择轮询，本质上是用 1 秒延迟换取了更简单的错误恢复——这与分布式系统中"最终一致性优先于强一致性"的设计哲学一致。

### 4.4 元数据持久化

远程任务的元数据持久化到会话边车文件，使 `--resume` 能恢复任务：

```typescript
// 源码: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx (第92-111行)
async function persistRemoteAgentMetadata(meta: RemoteAgentMetadata): Promise<void> {
  try {
    await writeRemoteAgentMetadata(meta.taskId, meta)
  } catch (e) {
    logForDebugging(`persistRemoteAgentMetadata failed: ${String(e)}`)
  }
}

// 任务完成/终止时清理
async function removeRemoteAgentMetadata(taskId: string): Promise<void> {
  try {
    await deleteRemoteAgentMetadata(taskId)
  } catch (e) {
    logForDebugging(`removeRemoteAgentMetadata failed: ${String(e)}`)
  }
}
```

持久化是 fire-and-forget——失败不阻塞任务注册。这保证了主流程的可靠性。

### 4.5 Ultraplan 阶段

Ultraplan 任务有额外的阶段跟踪——字段类型定义见上文第 4.1 节 (`ultraplanPhase?: 'needs_input' | 'plan_ready'`)：

- 未设置 / `undefined`（默认）：计划生成中（源码中没有 `'running'` 字面量，默认态就是 `undefined`，UI 底栏 switch 落到 default 分支）
- `needs_input`：远程 Agent 提出澄清问题，等待用户输入
- `plan_ready`：ExitPlanMode 等待浏览器审批

UI 底栏根据阶段显示不同的钻石符号和文案：

```typescript
// 源码: src/tasks/pillLabel.ts (第42-52行)
if (n === 1 && first.type === 'remote_agent' && first.isUltraplan) {
  switch (first.ultraplanPhase) {
    case 'plan_ready':   return `${DIAMOND_FILLED} ultraplan ready`
    case 'needs_input':  return `${DIAMOND_OPEN} ultraplan needs your input`
    default:             return `${DIAMOND_OPEN} ultraplan`
  }
}
```

`DIAMOND_FILLED` (◆) 表示完成等待审批，`DIAMOND_OPEN` (◇) 表示仍在运行或需要输入。

---

## 5. InProcessTeammateTask — 进程内团队成员

### 5.1 身份标识

团队成员任务携带丰富的身份信息：

```typescript
// 源码: src/tasks/InProcessTeammateTask/types.ts (第14-21行)
export type TeammateIdentity = {
  agentId: string      // "researcher@my-team"
  agentName: string    // "researcher"
  teamName: string
  color?: string
  planModeRequired: boolean
  parentSessionId: string  // 领导者的会话 ID
}
```

`agentId` 采用 `name@team` 格式，唯一标识团队中的成员。

### 5.2 消息容量管控：数据驱动的内存防线

这是全章最具工程洞察价值的设计决策——一个由生产环境真实数据驱动的内存治理方案。

```typescript
// 源码: src/tasks/InProcessTeammateTask/types.ts (第96-121行)
export const TEAMMATE_MESSAGES_UI_CAP = 50

export function appendCappedMessage<T>(prev: readonly T[] | undefined, item: T): T[] {
  // tail window: 始终保留最后 N-1 条历史 + 1 条新消息 = N 条，丢弃最旧的
  if (prev === undefined || prev.length === 0) return [item]
  if (prev.length >= TEAMMATE_MESSAGES_UI_CAP) {
    const next = prev.slice(-(TEAMMATE_MESSAGES_UI_CAP - 1))  // tail window
    next.push(item)
    return next
  }
  return [...prev, item]
}
```

**问题发现过程**。源码注释（`src/tasks/InProcessTeammateTask/types.ts:89-100`）记录了完整的决策背景：Anthropic 团队通过 BigQuery（BQ）分析生产遥测数据（2026-03-20，第 9 轮分析），测得**两个不同场景下的内存开销**——长对话稳态下每个 Agent 约 20MB RSS（500+ 轮），swarm 并发爆发下每个并发 Agent 约 125MB RSS。一个极端的"鲸鱼会话"（whale session `9a990de8`）在 2 分钟内启动了 292 个 Agent，实测总内存飙升到 36.8GB（即命中 swarm 爆发口径：292 × 125MB ≈ 36.5GB，与实测 36.8GB 吻合；单纯用 20MB 稳态值外推会低估约 6 倍，这正是两套数字需要并列标注的原因）。主要成本来源是 `task.messages` 字段——它为 UI 渲染持有每条消息的第二份完整拷贝（完整历史已保存在本地 `allMessages` 数组和磁盘 JSONL 文件中）。

**为什么是 50 条**。从用户角度看，任务面板的 UI 视窗通常只显示最近若干条消息，50 条足以覆盖用户回溯查看的典型深度。从内存角度看，50 条上限将每个 Agent 的 UI 消息内存从潜在的 20MB+ 降至可控范围；"降至原来约 1/10"是基于"500+ 轮 vs 50 轮"的线性外推估算（不是源码实测值），假设每条消息平均大小接近恒定。

**滑动窗口策略**。`appendCappedMessage` 实现的是尾部滑动窗口——始终保留最近 50 条，新消息进入时丢弃最旧的一条。完整对话历史不受影响，仍保存在磁盘 JSONL 文件中供后续恢复。

**工程文化启示**。这个案例揭示了 Anthropic 团队的一个重要工程实践：生产环境遥测 → BQ 数据分析 → 定量决策。"50"不是拍脑袋的魔法数字，而是基于真实的内存剖析数据做出的工程权衡。这种数据驱动的资源治理方式，对任何需要管理多 Agent 并发的系统都有参考价值。

### 5.3 空闲通知

团队成员支持高效的空闲等待机制：

```typescript
// 源码: src/tasks/InProcessTeammateTask/types.ts (第70-72行)
// Callbacks to notify when teammate becomes idle (runtime only)
// Used by leader to efficiently wait without polling
onIdleCallbacks?: Array<() => void>
```

领导者 Agent 等待成员完成时不需要轮询——注册回调，成员空闲时直接通知。这比 `setInterval` 检查状态高效得多。

### 5.4 计划模式审批

团队成员可以要求计划模式审批：

```typescript
awaitingPlanApproval: boolean   // 等待计划审批
permissionMode: PermissionMode  // 独立的权限模式（可在查看时切换）
```

每个团队成员有独立的权限模式，通过 `Shift+Tab` 在查看成员面板时循环切换。这比全局权限模式更灵活。

---

## 6. DreamTask — 记忆整合任务

### 6.1 任务定义

Dream 任务是最精简的任务类型，专门用于自动记忆整合：

```typescript
// 源码: src/tasks/DreamTask/DreamTask.ts (第12-41行)
const MAX_TURNS = 30               // 仅保留最近 N 轮用于实时展示

export type DreamTurn = {          // 单轮 assistant 输出（tool_use 被折叠为计数）
  text: string
  toolUseCount: number
}

export type DreamTaskState = TaskStateBase & {
  type: 'dream'
  phase: DreamPhase              // 'starting' | 'updating'
  sessionsReviewing: number      // 正在审查的会话数
  filesTouched: string[]         // 修改过的文件路径（不完全）
  turns: DreamTurn[]             // Assistant 文本响应
  abortController?: AbortController
  priorMtime: number             // 保存以便 kill 时回滚锁
}
```

### 6.2 阶段检测

Dream 的内部提示有 4 阶段结构（定向/收集/整合/剪枝），但代码不解析这些阶段——只区分 `starting` 和 `updating`：

```typescript
// 源码: src/tasks/DreamTask/DreamTask.ts (第76-104行)
export function addDreamTurn(taskId, turn, touchedPaths, setAppState): void {
  updateTaskState<DreamTaskState>(taskId, setAppState, task => {
    const seen = new Set(task.filesTouched)
    const newTouched = touchedPaths.filter(p => !seen.has(p) && seen.add(p))
    if (turn.text === '' && turn.toolUseCount === 0 && newTouched.length === 0) {
      return task  // 纯空操作跳过更新，避免无效重渲染
    }
    return {
      ...task,
      phase: newTouched.length > 0 ? 'updating' : task.phase,  // 首次文件修改 → updating
      filesTouched: newTouched.length > 0 ? [...task.filesTouched, ...newTouched] : task.filesTouched,
      turns: task.turns.slice(-(MAX_TURNS - 1)).concat(turn),   // 保留最近 30 轮
    }
  })
}
```

当首次检测到 Edit/Write 工具调用触碰文件时，阶段从 `starting` 翻转到 `updating`。

### 6.3 终止与锁回滚

Kill Dream 任务时需要回滚整合锁：

```typescript
// 源码: src/tasks/DreamTask/DreamTask.ts (第136-157行)
async kill(taskId, setAppState) {
  let priorMtime: number | undefined
  updateTaskState<DreamTaskState>(taskId, setAppState, task => {
    if (task.status !== 'running') return task
    task.abortController?.abort()
    priorMtime = task.priorMtime
    return { ...task, status: 'killed', endTime: Date.now(), notified: true }
  })
  if (priorMtime !== undefined) {
    await rollbackConsolidationLock(priorMtime)  // 回滚锁 mtime 让下次会话可以重试
  }
}
```

`priorMtime` 存储了整合开始前的锁文件修改时间。kill 时回滚这个时间戳，使下一次会话可以重新尝试整合。

### 6.4 通知差异

Dream 任务与其他任务的关键区别是——**没有模型面向的通知路径**：

```typescript
// 源码: src/tasks/DreamTask/DreamTask.ts (第106-120行)
export function completeDreamTask(taskId, setAppState): void {
  // notified: true 立即设置——dream 没有 model-facing 通知路径（纯 UI），
  // 驱逐需要 terminal + notified。内联的 appendSystemMessage 完成提示才是用户面。
  updateTaskState<DreamTaskState>(taskId, setAppState, task => ({
    ...task,
    status: 'completed',
    endTime: Date.now(),
    notified: true,  // 直接标记为已通知
    abortController: undefined,
  }))
}
```

---

## 7. LocalMainSessionTask — 主会话后台化

### 7.1 概念

当用户按 `Ctrl+B` 两次时，当前正在运行的主会话查询被"后台化"——查询继续在后台运行，UI 清空为新提示符。

```typescript
// 源码: src/tasks/LocalMainSessionTask.ts (第94-162行)
export function registerMainSessionTask(
  description: string,
  setAppState: SetAppState,
  mainThreadAgentDefinition?: AgentDefinition,
  existingAbortController?: AbortController,
): { taskId: string; abortSignal: AbortSignal } {
  const taskId = generateMainSessionTaskId()  // 's' 前缀区分于 Agent 的 'a' 前缀

  // 链接输出到独立的任务转录文件（不使用主会话的转录路径）
  void initTaskOutputAsSymlink(taskId, getAgentTranscriptPath(asAgentId(taskId)))

  // 复用现有的 AbortController（关键：确保 abort 任务能 abort 实际查询）
  const abortController = existingAbortController ?? createAbortController()

  const taskState: LocalMainSessionTaskState = {
    ...createTaskStateBase(taskId, 'local_agent', description),
    agentType: 'main-session',  // 特殊类型标记
    isBackgrounded: true,        // 创建即后台化
    // ...
  }
  registerTask(taskState, setAppState)
  return { taskId, abortSignal: abortController.signal }
}
```

### 7.2 ID 生成

主会话任务使用 `s` 前缀（session），Agent 任务使用 `a` 前缀：

```typescript
// 源码: src/tasks/LocalMainSessionTask.ts (第73-82行)
const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'

function generateMainSessionTaskId(): string {
  const bytes = randomBytes(8)
  let id = 's'
  for (let i = 0; i < 8; i++) {
    id += TASK_ID_ALPHABET[bytes[i]! % TASK_ID_ALPHABET.length]
  }
  return id
}
```

### 7.3 前台化

后台任务可以被拉回前台显示：

```typescript
// 源码: src/tasks/LocalMainSessionTask.ts (第270-302行，节选；省略错误处理与日志)
export function foregroundMainSessionTask(taskId, setAppState): Message[] | undefined {
  let taskMessages: Message[] | undefined
  setAppState(prev => {
    const task = prev.tasks[taskId]
    taskMessages = (task as LocalMainSessionTaskState).messages

    // 如果有之前被前台化的任务，恢复为后台
    const prevId = prev.foregroundedTaskId
    const prevTask = prevId ? prev.tasks[prevId] : undefined
    const restorePrev = prevId && prevId !== taskId && prevTask?.type === 'local_agent'

    return {
      ...prev,
      foregroundedTaskId: taskId,
      tasks: {
        ...prev.tasks,
        ...(restorePrev && { [prevId]: { ...prevTask, isBackgrounded: true } }),
        [taskId]: { ...task, isBackgrounded: false },
      },
    }
  })
  return taskMessages
}
```

前台化时会把之前前台化的其他任务推回后台——同一时间只有一个前台任务。

---

## 8. 统一任务操作

### 8.1 停止任务

多数任务类型可由 `TaskStopTool` / SDK `stopTask()` 入口统一停止（走 `getTaskByType(task.type).kill()` 派发到各自实现）；但 **InProcessTeammateTask 的 kill 路径** 有差异——领导者直接调用 `InProcessTeammateTask.kill()` 而不经 `stopTask.ts` 封装的那条路径（具体位置参见 `src/tasks/InProcessTeammateTask/` 目录），因此下面展示的 `stopTask()` 流程**不覆盖** teammate 的所有停止路径。"统一"是在 API 形态与状态机意义上，不是在调用链上。

```typescript
// 源码: src/tasks/stopTask.ts (第38-100行，节选；省略错误封装与遥测)
export async function stopTask(taskId, context): Promise<StopTaskResult> {
  const task = appState.tasks?.[taskId]

  if (!task) throw new StopTaskError(`No task found with ID: ${taskId}`, 'not_found')
  if (task.status !== 'running') throw new StopTaskError(`Task not running`, 'not_running')

  const taskImpl = getTaskByType(task.type)
  if (!taskImpl) throw new StopTaskError(`Unsupported task type`, 'unsupported_type')

  await taskImpl.kill(taskId, setAppState)

  // Shell 任务：抑制 "exit code 137" 通知（噪声）
  // Agent 任务：不抑制——AbortError 的 catch 发送携带 extractPartialResult 的通知
  let suppressed = false
  if (isLocalShellTask(task)) {
    setAppState(prev => {
      const prevTask = prev.tasks?.[taskId]
      if (!prevTask) return prev
      suppressed = true
      // 设置 notified 标志抑制通知
      return { ...prev, tasks: { ...prev.tasks, [taskId]: { ...prevTask, notified: true } } }
    })
    // 抑制 XML 通知也会抑制 print.ts 的 SDK 事件——直接发射
    if (suppressed) {
      emitTaskTerminatedSdk(taskId, 'stopped', { toolUseId: task.toolUseId, summary: task.description })
    }
  }
}
```

Shell 任务和 Agent 任务在 kill 后的通知处理不同：Shell 的 exit code 137（SIGKILL）是噪声需要抑制，Agent 的部分结果（`extractPartialResult`）是有价值的信息需要保留。

### 8.2 底栏徽标

底栏显示的文案根据任务类型和数量动态生成：

```typescript
// 源码: src/tasks/pillLabel.ts (第12-67行，节选；已省略 local_agent / local_workflow / monitor_mcp 分支)
export function getPillLabel(tasks: BackgroundTaskState[]): string {
  const n = tasks.length
  const allSameType = tasks.every(t => t.type === tasks[0]!.type)

  if (allSameType) {
    switch (tasks[0]!.type) {
      case 'local_bash': {
        const monitors = tasks.filter(t => t.type === 'local_bash' && t.kind === 'monitor').length
        const shells = n - monitors
        const parts: string[] = []
        if (shells > 0) parts.push(`${shells} shell${shells === 1 ? '' : 's'}`)
        if (monitors > 0) parts.push(`${monitors} monitor${monitors === 1 ? '' : 's'}`)
        // 例："2 shells, 1 monitor"
        return parts.join(', ')
      }
      case 'in_process_teammate': {
        const teamCount = new Set(tasks.map(t => t.identity.teamName)).size
        return teamCount === 1 ? '1 team' : `${teamCount} teams`
      }
      case 'remote_agent':
        return `${DIAMOND_OPEN} ${n} cloud sessions`
      case 'dream':
        return 'dreaming'
      // local_agent / local_workflow / monitor_mcp 分支参见源码原文
    }
  }
  return `${n} background ${n === 1 ? 'task' : 'tasks'}`
}
```

当所有任务类型相同时显示特定标签（如 "2 shells, 1 monitor"）；混合类型时退化为泛化标签（"3 background tasks"）。

---

## 9. 生命周期状态流

所有任务的统一生命周期：

```
                    register()
                       │
                       ▼
  ┌─────────┐     ┌─────────┐
  │ pending │────►│ running │
  └─────────┘     └────┬────┘
                       │
           ┌───────────┼───────────┐
           ▼           ▼           ▼
     ┌──────────┐ ┌─────────┐ ┌────────┐
     │completed │ │ failed  │ │ killed │
     └────┬─────┘ └────┬────┘ └───┬────┘
          │            │          │
          ▼            ▼          ▼
     ┌─────────────────────────────────┐
     │        notified = true          │  (通知已发送/抑制)
     └──────────────┬──────────────────┘
                    │
                    ▼
     ┌─────────────────────────────────┐
     │          eviction / GC          │  (UI 驱逐 + 磁盘清理)
     └─────────────────────────────────┘
```

`notified` 标志是驱逐的前置条件——确保通知已经发送或显式抑制后才能回收任务资源。

---

## 批判性分析

### 局限性

1. **内存压力**：InProcessTeammateTask 的消息历史是已知的内存瓶颈（292 个 Agent → 36.8GB）。50 条上限是权宜之计，不是根本解决方案——理想情况下应该使用磁盘为主内存为缓存的架构。

2. **无优先级调度**：所有后台任务平等竞争资源，没有优先级机制。Dream 任务（低优先级）可能与用户显式启动的 Agent 任务（高优先级）竞争 API 配额。

3. **Shell 任务的 type 命名**：`local_bash` 这个名称已经过时（不仅限于 bash），但因向后兼容无法修改。技术债的典型案例。

4. **通知去重依赖原子操作**：`notified` 标志的 check-and-set 通过 `setAppState` 的函数式更新实现原子性，但这依赖于 React 状态更新的串行化保证。如果底层调度模型改变，可能出现竞态条件。

### 设计权衡

1. **统一类型联合 vs. 多态类**：使用 TypeScript 联合类型（`TaskState`）而非 OOP 继承。优点是类型安全、穷举检查；缺点是新增任务类型需要修改联合定义和所有 switch 语句。

2. **前台/后台二值 vs. 优先级谱**：`isBackgrounded` 是布尔值而非优先级数字。对当前的 UI 模型（一个前台 + N 个后台）足够，但扩展性有限。

3. **Dream 的 filesTouched 不完全**：源码明确标注这是不完全的观察——只捕获 Edit/Write 工具调用，遗漏 bash 命令中的写操作。这是有意的权衡——完全追踪需要 strace 级别的监控，成本过高。

4. **RemoteAgentTask 的轮询模型**：远程任务使用轮询（`pollRemoteSessionEvents`）而非纯推送。这是因为 WebSocket 连接可能断开，轮询作为兜底确保最终一致性。代价是延迟和 API 负载。

### 整体评价

任务系统是 Claude Code 中复杂度最高的子系统之一，管理着从毫秒级 Shell 命令到小时级远程 Agent 的所有后台工作。统一的状态模型（`TaskStateBase` + 类型扩展）在保持一致性的同时允许每种任务类型的特化需求。内存管理（消息上限）、通知去重（notified 标志）和优雅终止（锁回滚）等细节展现了生产级系统的成熟度。主要改进方向是内存架构（磁盘为主）和优先级调度。
