Task 系统与任务调度

📑 目录

在 Claude Code 的架构中,Task 系统是支撑多任务并发执行的核心基础设施。无论是本地子 Agent 的并行探索、远程云会话的长期监控,还是进程内队友(In-Process Teammate)的协作编排,乃至后台记忆整理的 Dream 任务,全部统一在 Task 抽象之下进行调度。本文将深入源码,解析 Claude Code Task 系统的完整实现。

一、Task 系统架构概览

1.1 目录结构与核心模块

Claude Code 的 Task 系统源码主要分布在 src/Task.tssrc/tasks.ts 以及 src/tasks/ 目录下:

src/
├── Task.ts                      # 基础类型定义
├── tasks.ts                     # 任务注册与分发
├── tasks/
│   ├── types.ts                 # TaskState 联合类型
│   ├── LocalAgentTask/          # 本地 Agent 任务
│   │   └── LocalAgentTask.tsx
│   ├── RemoteAgentTask/         # 远程 Agent 任务
│   │   └── RemoteAgentTask.tsx
│   ├── InProcessTeammateTask/   # 进程内队友任务
│   │   ├── InProcessTeammateTask.tsx
│   │   └── types.ts
│   ├── DreamTask/               # 后台记忆整理任务
│   │   └── DreamTask.ts
│   ├── LocalShellTask/          # 本地 Shell 任务
│   ├── LocalMainSessionTask.ts  # 主会话后台化
│   ├── LocalWorkflowTask/       # 工作流任务
│   ├── MonitorMcpTask/          # MCP 监控任务
│   ├── pillLabel.ts             # 底部 Pill 标签
│   └── stopTask.ts              # 通用停止逻辑
└── utils/task/
    ├── framework.ts             # 任务框架(注册、更新、GC)
    ├── diskOutput.ts            # 磁盘输出管理
    └── sdkProgress.ts           # SDK 进度上报

src/Task.ts 中可以看到,所有任务类型由一个联合类型 TaskType 统一定义(src/Task.ts,第 7–15 行):

export type TaskType =
  | 'local_bash'
  | 'local_agent'
  | 'remote_agent'
  | 'in_process_teammate'
  | 'local_workflow'
  | 'monitor_mcp'
  | 'dream'

任务生命周期状态 TaskStatus 则包含 pendingrunningcompletedfailedkilled 五种(src/Task.ts,第 17–23 行)。此外,isTerminalTaskStatus 辅助函数用于判断任务是否已进入终态,防止向已终止的任务注入消息或执行清理时产生竞态条件。

1.2 Task 抽象接口

Claude Code 采用了一个极简的 Task 接口来抽象所有任务类型(src/Task.ts,第 56–60 行):

export type Task = {
  name: string
  type: TaskType
  kill(taskId: string, setAppState: SetAppState): Promise<void>
}

在早期版本中,Task 接口还包含 spawnrender 方法,但在 #22546 提交中被移除,因为 "spawn/render were never called polymorphically"。当前只有 kill 操作需要多态分发——所有六种任务类型的终止逻辑都统一通过 getTaskByType(type) 查找对应实现后调用。

1.3 任务类型全景图

以下 Mermaid 图展示了 Claude Code 中七种任务类型的分类及其运行环境:

flowchart TD
    subgraph 本地进程任务
        LB[LocalShellTask
local_bash] LA[LocalAgentTask
local_agent] LM[LocalMainSessionTask
local_agent] LW[LocalWorkflowTask
local_workflow] IP[InProcessTeammateTask
in_process_teammate] end subgraph 远程任务 RA[RemoteAgentTask
remote_agent] end subgraph 后台系统任务 DT[DreamTask
dream] MM[MonitorMcpTask
monitor_mcp] end LB --> |子进程| Shell[ShellCommand 子进程] LA --> |子进程| AgentProc[独立 Agent 进程] LM --> |子进程| MainProc[主会话后台化] IP --> |同进程| AsyncLocal[AsyncLocalStorage 隔离] RA --> |HTTP 轮询| CCR[Claude Code Remote] DT --> |fork| DreamAgent[记忆整理子 Agent] MM --> |同进程| MCP[MCP 工具监控]

上图将任务按运行环境分为三类:

  • 本地进程任务:在本地 Node.js 进程内或派生子进程中运行,包括 Shell 命令、本地 Agent、工作流和进程内队友。
  • 远程任务:通过 HTTP API 与 Claude Code Remote(CCR)通信,本地仅维持轮询状态。
  • 后台系统任务:为特定系统功能服务,如 autoDream 记忆整理和 MCP 监控。

二、LocalAgentTask:本地 Agent 任务

LocalAgentTask 是 Claude Code 中最常用的后台任务类型,由 AgentTool 在调用时创建,用于启动独立的本地子 Agent 来并行处理子任务。

2.1 状态定义与核心字段

LocalAgentTaskStateLocalAgentTask.tsx 中定义(第 91–154 行),其扩展了 TaskStateBase,并添加了大量 Agent 特有的运行时字段:

export type LocalAgentTaskState = TaskStateBase & {
  type: 'local_agent'
  agentId: string
  prompt: string
  selectedAgent?: AgentDefinition
  agentType: string
  model?: string
  abortController?: AbortController
  unregisterCleanup?: () => void
  error?: string
  result?: AgentToolResult
  progress?: AgentProgress
  retrieved: boolean
  messages?: Message[]
  lastReportedToolCount: number
  lastReportedTokenCount: number
  isBackgrounded: boolean
  pendingMessages: string[]
  retain: boolean
  diskLoaded: boolean
  evictAfter?: number
}

其中几个关键字段的设计意图值得深入理解:

  • isBackgrounded:标记任务是否已转入后台。当 Agent 运行时间较长时,Claude Code 会显示 BackgroundHint,用户按 Ctrl+B 即可将前台运行的 Agent 转入后台。false 表示仍在"前台"运行(占据 UI 面板),true 表示已后台化。
  • retain:标记 UI 是否正在"持有"该任务。当用户通过 enterTeammateView 查看某个 Agent 的详细对话时,retain 置为 true,这会阻止垃圾回收(GC)清理该任务,并启用流式追加和磁盘引导。
  • diskLoaded:一次性标志,表示已读取 sidechain JSONL 并将内容合并到 messages。从 retaindiskLoaded 的转换只发生一次,之后由流式追加接管。
  • evictAfter:面板可见性截止时间。当任务进入终态且未被 retain 时,设置 evictAfter = Date.now() + PANEL_GRACE_MS(默认 30 秒), grace 期结束后可从 AppState 中驱逐。

2.2 任务创建:后台 Agent 与前台 Agent

LocalAgentTask 提供了两种注册路径。对于需要立即后台运行的 Agent,使用 registerAsyncAgentLocalAgentTask.tsx,第 433–490 行):

export function registerAsyncAgent({
  agentId, description, prompt, selectedAgent,
  setAppState, parentAbortController, toolUseId
}: {
  agentId: string;
  description: string;
  prompt: string;
  selectedAgent: AgentDefinition;
  setAppState: SetAppState;
  parentAbortController?: AbortController;
  toolUseId?: string;
}): LocalAgentTaskState {
  void initTaskOutputAsSymlink(agentId, getAgentTranscriptPath(asAgentId(agentId)));

  const abortController = parentAbortController
    ? createChildAbortController(parentAbortController)
    : createAbortController();

  const taskState: LocalAgentTaskState = {
    ...createTaskStateBase(agentId, 'local_agent', description, toolUseId),
    type: 'local_agent',
    status: 'running',
    agentId, prompt, selectedAgent,
    agentType: selectedAgent.agentType ?? 'general-purpose',
    abortController,
    retrieved: false,
    lastReportedToolCount: 0,
    lastReportedTokenCount: 0,
    isBackgrounded: true,
    pendingMessages: [],
    retain: false,
    diskLoaded: false
  };
  // ...
  registerTask(taskState, setAppState);
  return taskState;
}

这里有两个重要设计:

  1. 父子 AbortController 链:如果传入了 parentAbortController,则创建一个子 AbortController。当父任务(如 InProcessTeammate)被终止时,所有子 Agent 会自动级联中止,避免孤儿进程。
  2. 输出文件符号链接initTaskOutputAsSymlink 将任务的输出文件链接到 Agent 的 transcript 路径,使得任务输出和对话记录共享同一份存储。

对于可能先前台运行再后台化的 Agent,使用 registerAgentForegroundLocalAgentTask.tsx,第 530–610 行)。该方法返回一个 backgroundSignal Promise,当用户主动后台化或超时自动后台化时 resolve。autoBackgroundMs 参数支持配置自动后台化的超时时间。

2.3 进度追踪:AgentProgress

Claude Code 为每个本地 Agent 维护了细粒度的进度追踪器 ProgressTrackerLocalAgentTask.tsx,第 60–78 行):

export type ProgressTracker = {
  toolUseCount: number
  latestInputTokens: number
  cumulativeOutputTokens: number
  recentActivities: ToolActivity[]
}

updateProgressFromMessage 函数在每个 assistant 消息到达时更新追踪器,记录工具调用次数、token 消耗以及最近的工具活动(如 ReadGrepGlob 等)。MAX_RECENT_ACTIVITIES = 5 限制了保留的最近活动数量,避免内存无限增长。这些进度信息最终会渲染在底部状态栏的任务 Pill 中,让用户实时感知后台 Agent 的工作状态。

2.4 终止与清理

killAsyncAgent 实现了本地 Agent 的终止逻辑(LocalAgentTask.tsx,第 281–306 行):

export function killAsyncAgent(taskId: string, setAppState: SetAppState): void {
  let killed = false;
  updateTaskState<LocalAgentTaskState>(taskId, setAppState, task => {
    if (task.status !== 'running') {
      return task;
    }
    killed = true;
    task.abortController?.abort();
    task.unregisterCleanup?.();
    return {
      ...task,
      status: 'killed',
      endTime: Date.now(),
      evictAfter: task.retain ? undefined : Date.now() + PANEL_GRACE_MS,
      abortController: undefined,
      unregisterCleanup: undefined,
      selectedAgent: undefined
    };
  });
  if (killed) {
    void evictTaskOutput(taskId);
  }
}

终止时依次执行:

  1. 调用 abortController.abort():向 Agent 的异步执行链路发送中止信号。
  2. 注销清理回调:避免进程退出时重复 kill。
  3. 状态置为 killed:记录 endTimeevictAfter(若未被 retain)。
  4. 释放引用:将 abortControllerunregisterCleanupselectedAgent 设为 undefined,帮助 GC 回收内存。
  5. 驱逐输出文件evictTaskOutput 异步清理磁盘上的任务输出。

此外,killAllRunningAgentTasks 提供了批量终止能力,用于 Coordinator 模式下按 ESC 取消所有子 Agent。

三、RemoteAgentTask:远程 Agent 任务

RemoteAgentTask 是 Claude Code 与云端远程 Agent 交互的桥梁。它不负责直接执行 Agent 逻辑,而是通过 HTTP 轮询监控远程会话(CCR,Claude Code Remote)的状态变化。

3.1 远程任务状态定义

RemoteAgentTaskState 定义在 RemoteAgentTask.tsx 中(第 40–85 行),除了基础字段外,还包含远程特有的元数据:

export type RemoteAgentTaskState = TaskStateBase & {
  type: 'remote_agent'
  remoteTaskType: RemoteTaskType
  remoteTaskMetadata?: RemoteTaskMetadata
  sessionId: string
  command: string
  title: string
  todoList: TodoList
  log: SDKMessage[]
  isLongRunning?: boolean
  pollStartedAt: number
  isRemoteReview?: boolean
  reviewProgress?: { stage?: 'finding' | 'verifying' | 'synthesizing'; bugsFound: number; bugsVerified: number; bugsRefuted: number }
  isUltraplan?: boolean
  ultraplanPhase?: Exclude<UltraplanPhase, 'running'>
}

RemoteTaskType 支持七种类型(RemoteAgentTask.tsx,第 87–90 行):

const REMOTE_TASK_TYPES = ['remote-agent', 'ultraplan', 'ultrareview', 'autofix-pr', 'background-pr'] as const;

其中 ultraplanultrareview 是 Claude Code 的高级功能,分别对应云端计划模式和远程代码审查。

3.2 任务创建与会话持久化

spawnRemoteAgentTask 函数负责创建远程任务(RemoteAgentTask.tsx,第 400–460 行)。创建流程如下:

  1. 生成任务 ID:使用 generateTaskId('remote_agent') 生成以 r 为前缀的 ID。
  2. 初始化输出文件initTaskOutput(taskId) 创建空的任务输出文件。
  3. 构建状态对象:填充 RemoteAgentTaskState,状态设为 running
  4. 注册到 AppState:调用 registerTask
  5. 持久化元数据persistRemoteAgentMetadata 将任务信息写入 session sidecar。这是 --resume 功能的关键——当用户恢复会话时,Claude Code 可以从 sidecar 中重建远程任务并重新连接。
  6. 启动轮询startRemoteSessionPolling 开始每秒一次的远程会话事件轮询。

3.3 会话恢复:resume 机制

Claude Code 的 --resume 支持对远程任务的无缝恢复。restoreRemoteAgentTasksRemoteAgentTask.tsx,第 477–555 行)会扫描 sidecar 中的远程 Agent 元数据,向 CCR API fetchSession 查询每个会话的当前状态:

  • 若会话已归档(archived)或返回 404,则清理 sidecar 记录。
  • 若会话仍在运行,则在本地重建 RemoteAgentTaskState 并重启轮询。
  • 若遇到可恢复的错误(如 401 未登录),则跳过,等待用户重新登录后恢复。

这种设计确保了本地客户端重启后不会丢失对远程长期运行任务的跟踪。

3.4 轮询与状态机

远程任务的核心是 startRemoteSessionPolling 函数(RemoteAgentTask.tsx,第 560–750 行)。它以 POLL_INTERVAL_MS = 1000 为间隔,调用 pollRemoteSessionEvents 获取远程会话的新事件。轮询逻辑中包含多个精妙的状态转换规则:

稳定空闲检测:远程会话在每次工具调用之间会短暂变为 idle,单次 idle 不能作为完成信号。Claude Code 要求连续 STABLE_IDLE_POLLS = 5 次检测到 idle 且日志无增长,才认定为真正空闲。

结果提取:对于普通远程 Agent,从 accumulated log 中找到最后一个 result 消息来判断成功或失败。但对于 ultraplanisLongRunning 任务,result 会在每轮 CCR 交互后产生,不能驱动完成,因此跳过。

远程审查特殊处理isRemoteReview 任务通过解析 <remote-review> 标签来检测完成,同时从 <remote-review-progress> 心跳中提取 bug 统计信息。

3.5 终止与会话归档

RemoteAgentTaskkill 方法(RemoteAgentTask.tsx,第 811–850 行)除了更新本地状态外,还会调用 archiveRemoteSession 将远程会话归档:

async kill(taskId, setAppState) {
  let killed = false;
  let sessionId: string | undefined;
  updateTaskState<RemoteAgentTaskState>(taskId, setAppState, task => {
    if (task.status !== 'running') return task;
    killed = true;
    sessionId = task.sessionId;
    return { ...task, status: 'killed', endTime: Date.now(), notified: true };
  });
  if (killed && sessionId) {
    void archiveRemoteSession(sessionId);
    void removeRemoteAgentMetadata(taskId);
  }
}

归档后,远程会话的资源被释放,sidecar 中的元数据也被清除,确保恢复时不会重复连接已终止的任务。

四、InProcessTeammateTask:进程内队友任务

InProcessTeammateTask 是 Claude Code Swarm 模式的核心组件。与 LocalAgentTask 在独立进程中运行不同,进程内队友与主 Agent 共享同一个 Node.js 进程,通过 AsyncLocalStorage 实现执行上下文的隔离。

4.1 设计理念与差异

InProcessTeammateTask.tsx 文件头部的注释清晰地阐述了其设计定位(第 1–13 行):

Unlike LocalAgentTask (background agents), in-process teammates:

  1. Run in the same Node.js process using AsyncLocalStorage for isolation
  2. Have team-aware identity (agentName@teamName)
  3. Support plan mode approval flow
  4. Can be idle (waiting for work) or active (processing)

这种设计权衡带来了显著优势:启动延迟极低(无需 fork 新进程),内存占用更可控,** teammates 间通信无需 IPC**。代价是单个队友的崩溃或死循环可能影响整个进程。

4.2 队友身份与状态

InProcessTeammateTaskState 定义在 types.ts 中(第 16–85 行),核心字段包括:

export type InProcessTeammateTaskState = TaskStateBase & {
  type: 'in_process_teammate'
  identity: TeammateIdentity
  prompt: string
  model?: string
  selectedAgent?: AgentDefinition
  abortController?: AbortController
  currentWorkAbortController?: AbortController
  awaitingPlanApproval: boolean
  permissionMode: PermissionMode
  error?: string
  result?: AgentToolResult
  progress?: AgentProgress
  messages?: Message[]
  inProgressToolUseIDs?: Set<string>
  pendingUserMessages: string[]
  isIdle: boolean
  shutdownRequested: boolean
  onIdleCallbacks?: Array<() => void>
  lastReportedToolCount: number
  lastReportedTokenCount: number
}

TeammateIdentity 包含了 agentId(格式为 agentName@teamName)、agentNameteamNameplanModeRequiredparentSessionId,赋予队友明确的团队上下文和汇报关系。

currentWorkAbortController 是一个精妙的设计:它只中止当前工作轮次,而不会杀死整个队友。这允许队友在收到新任务时快速取消正在进行的思考,但保留自身生命周期。

4.3 消息注入与对话历史

用户可以通过 Zoomed View 查看队友的完整对话,并直接向队友发送消息。injectUserMessageToTeammate 函数实现了这一功能(InProcessTeammateTask.tsx,第 85–110 行):

export function injectUserMessageToTeammate(
  taskId: string, message: string, setAppState: SetAppState
): void {
  updateTaskState<InProcessTeammateTaskState>(taskId, setAppState, task => {
    if (isTerminalTaskStatus(task.status)) {
      logForDebugging(`Dropping message for teammate task ${taskId}: task status is "${task.status}"`);
      return task;
    }
    return {
      ...task,
      pendingUserMessages: [...task.pendingUserMessages, message],
      messages: appendCappedMessage(task.messages, createUserMessage({ content: message }))
    };
  });
}

注意守卫条件使用了 isTerminalTaskStatus,允许在 runningidle 状态下注入消息,只在终态时拒绝。注入的消息同时进入 pendingUserMessages 队列(等待实际交付)和 messages 数组(UI 即时显示)。

4.4 UI 消息上限与内存优化

进程内队友面临一个重要的内存挑战:BQ 分析显示,在 500+ 轮对话的会话中,每个 Agent 约占用 20MB RSS,在 Swarm 爆发期(如某次会话在 2 分钟内启动了 292 个 Agent)可达 36.8GB 内存。主要元凶是 task.messages 保存了对话的完整副本。

为此,Claude Code 引入了 TEAMMATE_MESSAGES_UI_CAP = 50types.ts,第 91–96 行),appendCappedMessage 函数在添加新消息时自动丢弃最旧的消息,将 UI 镜像限制在 50 条。完整的对话历史仍然保存在 inProcessRunner 的本地数组和磁盘 transcript 中,UI 只保留近期上下文用于展示。

4.5 生命周期管理

InProcessTeammateTaskkill 方法非常简洁(InProcessTeammateTask.tsx,第 28–35 行):

export const InProcessTeammateTask: Task = {
  name: 'InProcessTeammateTask',
  type: 'in_process_teammate',
  async kill(taskId, setAppState) {
    killInProcessTeammate(taskId, setAppState);
  }
};

实际的终止逻辑委托给 killInProcessTeammate,它会通过队友的运行时上下文发送中止信号。requestTeammateShutdown 则提供了一种"优雅关闭"机制:设置 shutdownRequested: true,让队友在完成当前工作后自行退出,而非强制中止。

五、DreamTask:autoDream 记忆整理任务

DreamTask 是 Claude Code 最具特色的任务类型之一,它将原本对用户不可见的 autoDream 后台记忆整理过程,通过 Task 系统"表面化"为 UI 可见的后台任务。

5.1 设计初衷

autoDream 是 Claude Code 的自动记忆整理系统,它在后台 fork 一个子 Agent,回顾最近的对话 session,将分散的记忆 consolidate(合并)到 memdir 中。这个子 Agent 原本对用户完全透明——它在后台默默运行,用户不知道它何时开始、何时结束、在做什么。

DreamTask 的设计目标是"pure UI surfacing via the existing task registry"(DreamTask.ts,第 5–7 行)。它不改变 dream agent 本身的任何逻辑,只是通过已有的任务注册机制,让 autoDream 变得可见:

  • 底部状态栏显示 "dreaming" pill
  • Shift+Down 对话框中列出正在整理的记忆
  • 用户可以看到 dream agent 处理的文件列表和对话轮次

5.2 状态定义

DreamTaskState 结构紧凑(DreamTask.ts,第 25–48 行):

export type DreamTaskState = TaskStateBase & {
  type: 'dream'
  phase: DreamPhase
  sessionsReviewing: number
  filesTouched: string[]
  turns: DreamTurn[]
  abortController?: AbortController
  priorMtime: number
}
  • phase'starting''updating',当首次检测到 Edit / Write 工具调用时从 starting 翻转到 updating
  • sessionsReviewing:正在审查的 session 数量。
  • filesTouched:通过 onMessage 观察到的 Edit / Write 工具调用中的文件路径。注释明确指出这是一个不完整的反映——它遗漏了通过 bash 命令间接写入的文件。
  • turns:最近的 assistant 对话轮次(最多保留 MAX_TURNS = 30 条),工具调用被折叠为计数。
  • priorMtime:锁文件修改时间,用于 kill 时回滚锁状态。

5.3 与 autoDream 锁机制的协作

autoDream 使用文件锁来避免多个进程同时执行记忆整理。锁文件位于 getAutoMemPath() 目录下的 .consolidate-lock,其 mtime 代表上一次整理的时间戳(src/services/autoDream/consolidationLock.ts)。

DreamTask 的终止逻辑需要与这个锁机制配合(DreamTask.ts,第 125–146 行):

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, abortController: undefined }
  })
  if (priorMtime !== undefined) {
    await rollbackConsolidationLock(priorMtime)
  }
}

当用户主动 kill DreamTask 时,rollbackConsolidationLock(priorMtime) 会将锁文件的 mtime 回滚到之前的状态,这样下一次启动时可以重新尝试整理。这与 autoDream 中 fork 失败时的处理路径完全一致。

5.4 与 memdir 的关联

DreamTask 本身不直接操作 memdir,它只是 autoDream 系统的 UI 层。真正的记忆整理逻辑在 src/services/autoDream/ 中执行,包括:

  • consolidationLock.ts:锁的获取、验证和回滚
  • autoDream.ts:fork dream agent、选择候选 session、触发整理流程
  • memdir 路径通过 getAutoMemPath() 获取(src/memdir/paths.js

DreamTask 的存在让用户能够感知到这一后台过程,并在必要时取消它——比如当用户知道即将进行大量对话,不想让整理过程干扰时。

六、任务调度器与生命周期管理

6.1 任务注册框架

src/utils/task/framework.ts 是 Task 系统的调度中枢,提供了任务注册、状态更新、垃圾回收和附件生成的核心逻辑。

registerTask 函数(framework.ts,第 65–96 行)负责将新任务加入 AppState.tasks

export function registerTask(task: TaskState, setAppState: SetAppState): void {
  let isReplacement = false
  setAppState(prev => {
    const existing = prev.tasks[task.id]
    isReplacement = existing !== undefined
    const merged = existing && 'retain' in existing
      ? { ...task, retain: existing.retain, startTime: existing.startTime,
          messages: existing.messages, diskLoaded: existing.diskLoaded }
      : task
    return { ...prev, tasks: { ...prev.tasks, [task.id]: merged } }
  })
  if (isReplacement) return
  enqueueSdkEvent({ type: 'system', subtype: 'task_started', task_id: task.id, ... })
}

当恢复 Agent(resume)时,registerTask 检测到已有同 ID 任务,会保留 UI 持有状态(retain)、对话历史(messages)和磁盘加载标志(diskLoaded),实现无缝切换。

6.2 状态更新与不可变性

updateTaskState 是一个泛型辅助函数(framework.ts,第 40–62 行),确保类型安全的状态更新:

export function updateTaskState<T extends TaskState>(
  taskId: string,
  setAppState: SetAppState,
  updater: (task: T) => T,
): void {
  setAppState(prev => {
    const task = prev.tasks?.[taskId] as T | undefined
    if (!task) return prev
    const updated = updater(task)
    if (updated === task) {
      return prev  // 跳过无意义的 spread
    }
    return { ...prev, tasks: { ...prev.tasks, [taskId]: updated } }
  })
}

这里有一个性能优化细节:如果 updater 返回了同一个引用(即 early-return no-op),updateTaskState 会跳过 spread 操作,避免触发 AppState.tasks 的 18 个订阅者(REPL、Spinner、PromptInput 等)不必要的重新渲染。

6.3 垃圾回收与驱逐策略

Task 系统实现了双层的任务垃圾回收机制。

显式驱逐evictTerminalTaskframework.ts,第 99–120 行)在任务进入终态(completed/failed/killed)且 notified = true 时,检查 evictAfter 截止时间。若 grace 期已过,立即从 AppState.tasks 中删除。

惰性 GCgenerateTaskAttachmentsframework.ts,第 135–195 行)在每次轮询时扫描所有任务,对已通知的终态任务执行驱逐:

for (const taskState of Object.values(tasks)) {
  if (taskState.notified) {
    switch (taskState.status) {
      case 'completed':
      case 'failed':
      case 'killed':
        evictedTaskIds.push(taskState.id)
        continue
      // ...
    }
  }
}

停止显示时间STOPPED_DISPLAY_MS = 3_000 控制已停止任务在 UI 中保留的最短时间,避免任务瞬间消失导致用户困惑。

Panel GracePANEL_GRACE_MS = 30_000 为本地 Agent 任务的面板提供 30 秒的保留期,让用户有时间查看最终结果。

6.4 通用停止逻辑

src/tasks/stopTask.ts 提供了统一的任务停止入口 stopTask(第 35–90 行)。它执行以下验证链:

  1. 通过 taskId 查找任务,不存在则抛出 StopTaskError('not_found')
  2. 检查任务状态是否为 running,否则抛出 not_running
  3. 通过 getTaskByType(task.type) 获取任务实现,不支持则抛出 unsupported_type
  4. 调用 taskImpl.kill(taskId, setAppState)
  5. 对于 Shell 任务,静默标记 notified = true(抑制 "exit code 137" 的噪音通知),但直接向 SDK 发送 task_terminated 事件
  6. 返回包含 taskIdtaskTypecommand 的结果

这种分层设计确保了 LLM 通过 TaskStopTool 调用、用户通过快捷键触发、SDK 通过控制请求三种停止路径共享同一套验证和清理逻辑。

6.5 底部 Pill 标签与状态展示

src/tasks/pillLabel.ts 将一组后台任务转换为紧凑的状态标签,显示在底部状态栏。不同类型的任务有不同的展示策略:

  • local_bash:区分普通 shell 和 monitor(如 1 shell, 2 monitors
  • local_agent1 local agent / N local agents
  • remote_agent:使用空心/实心钻石符号区分 ultraplan 的不同阶段(◇ ultraplan◆ ultraplan ready
  • in_process_teammate:按 team 聚合(1 team / N teams
  • dream:固定显示 dreaming

pillNeedsCta 函数判断是否需要显示 " · ↓ to view" 的行动号召(Call-to-Action),仅在 ultraplan 的 needs_inputplan_ready 两种需要用户注意的状态下触发。

6.6 任务调度全景图

以下 Mermaid 序列图展示了从任务创建到终止的完整调度流程:

sequenceDiagram
    actor User
    participant AgentTool as AgentTool / TeammateSpawn
    participant Framework as Task Framework
    participant AppState as AppState.tasks
    participant Poller as 轮询/GC 循环
    participant UI as UI 渲染

    User->>AgentTool: 触发子任务
    AgentTool->>Framework: registerTask(taskState)
    Framework->>AppState: 写入任务状态
    Framework->>UI: enqueueSdkEvent(task_started)
    UI->>User: 显示任务 Pill

    alt LocalAgentTask
        AgentTool->>AppState: 流式更新 messages/progress
        AppState->>UI: 重新渲染进度
    else RemoteAgentTask
        Poller->>AppState: 每秒 pollRemoteSessionEvents
        AppState->>UI: 更新 log/reviewProgress
    else InProcessTeammateTask
        AgentTool->>AppState: AsyncLocalStorage 隔离执行
        User->>AppState: injectUserMessageToTeammate
    else DreamTask
        autoDream->>AppState: addDreamTurn / 文件变更
    end

    alt 正常完成
        AgentTool->>Framework: completeAgentTask / completeDreamTask
        Framework->>AppState: status: completed, notified: true
    else 执行失败
        AgentTool->>Framework: failAgentTask / failDreamTask
        Framework->>AppState: status: failed, error
    else 用户终止
        User->>Framework: stopTask(taskId)
        Framework->>AppState: 查找任务实现
        AppState->>AgentTool: taskImpl.kill()
        AgentTool->>AppState: status: killed
    end

    Poller->>AppState: generateTaskAttachments()
    AppState->>AppState: evictTerminalTask (GC)
    AppState->>UI: 移除已终结任务
    UI->>User: Pill 消失或显示结果

七、总结

Claude Code 的 Task 系统通过一套统一的抽象接口,优雅地支撑了从本地 Shell 命令到远程云会话、从独立子进程到进程内协程、从用户触发的 Agent 到系统自动的 Dream 整理等多样化任务类型。

其核心设计亮点包括:

  1. 最小化抽象Task 接口仅要求 kill 方法,降低新增任务类型的成本。
  2. 精细化状态管理isBackgroundedretainevictAfter 等字段让 UI 状态和任务生命周期解耦,支持前台/后台切换、查看保持和延迟驱逐。
  3. 父子中止链AbortController 的父子关系确保任务树能够级联清理,避免孤儿进程。
  4. 远程会话恢复:sidecar 持久化 + --resume 重建让远程任务的本地代理可以跨越客户端重启而存活。
  5. 内存安全边界TEAMMATE_MESSAGES_UI_CAPMAX_TURNS 等上限防止 UI 镜像无限制增长。
  6. 可观察性设计DreamTask 将原本不可见的后台系统过程转化为用户可感知、可干预的 UI 元素。

理解 Task 系统的工作机制,对于扩展 Claude Code 的功能(如新增 MCP 监控任务类型)、优化大规模 Swarm 并发场景下的资源管理,以及构建更复杂的 Agent 编排逻辑,都具有重要的参考价值。