第二十三篇:Tasks系统,Claude Code如何统一管理7种后台任务

本文是《Claude Code 源码分析 100 篇》系列的第二十三篇。


为什么需要 Tasks 系统

Claude Code 是一个高度并发的工作环境------一个主会话(Leader)可以同时调度多个后台任务:

  • 用户按 Ctrl+B 把当前查询打入后台,继续与主会话对话
  • Agent 任务(Teammates)独立运行,实时汇报进度
  • Shell 脚本在后台默默执行,随时被杀死
  • MCP 工具持续监控资源状态
  • Remote Agent 连接云端 claude.ai 会话
  • Workflow 执行长流程脚本
  • Dream 在空闲时生成测试建议

这7种任务类型共存在同一个状态空间 AppState.tasks 中。Tasks 系统就是这个统一管理层。


一、Task.ts:类型骨架

src/Task.ts 定义了所有任务类型的基础类型系统(~120行)。

1.1 七种任务类型

typescript 复制代码
export type TaskType =
  | 'local_bash'       // 本地 Shell 命令(b前缀ID)
  | 'local_agent'      // 本地 Agent 子进程(a前缀ID)
  | 'remote_agent'     // 远程 claude.ai 会话(r前缀ID)
  | 'in_process_teammate' // 同进程 Teammate(t前缀ID)
  | 'local_workflow'   // 工作流脚本(w前缀ID)
  | 'monitor_mcp'      // MCP 资源监控(m前缀ID)
  | 'dream'            // 空闲时做梦(d前缀ID)

每种类型有一个固定单字母前缀,配合8位随机字符组成16字符任务ID(b + a1b2c3d4e5f6g7h)。

1.2 任务基础状态

typescript 复制代码
export type TaskStateBase = {
  id: string
  type: TaskType
  status: 'pending' | 'running' | 'completed' | 'failed' | 'killed'
  description: string
  toolUseId?: string
  startTime: number
  endTime?: number
  totalPausedMs?: number
  outputFile: string    // 磁盘输出文件路径
  outputOffset: number  // 已读偏移(断点续传)
  notified: boolean      // 用户通知已发送
}

outputFile + outputOffset磁盘持久化核心------每个任务将输出写入独立文件,UI 通过偏移量增量读取,即使任务结束也能回顾完整输出。

1.3 生命周期判断

typescript 复制代码
export function isTerminalTaskStatus(status: TaskStatus): boolean {
  return status === 'completed' || status === 'failed' || status === 'killed'
}

用途:防止向已死亡任务注入消息、触发任务清理、孤儿进程清理路径的前置守卫。


二、types.ts:任务状态联合类型

src/tasks/types.ts 是整个任务系统的类型交汇点

typescript 复制代码
export type TaskState =
  | LocalShellTaskState
  | LocalAgentTaskState
  | RemoteAgentTaskState
  | InProcessTeammateTaskState
  | LocalWorkflowTaskState
  | MonitorMcpTaskState
  | DreamTaskState

每种具体状态都扩展了 TaskStateBase,添加各自特有的字段。

后台任务过滤

typescript 复制代码
export function isBackgroundTask(task: TaskState): boolean {
  // 必须是 running/pending
  if (task.status !== 'running' && task.status !== 'pending') return false
  // 前景任务不算后台
  if ('isBackgrounded' in task && task.isBackgrounded === false) return false
  return true
}

设计意图 :只有被用户主动后台化(isBackgrounded=true)且在运行中的任务才出现在底栏 pill 指示器中。


三、LocalAgentTask:本地 Agent 子进程

本地 Agent 通过 Bun 子进程运行,执行 LLM 对话(与用户自己的 Claude Code 并行)。

核心特性

  • 独立子进程,不阻塞主对话
  • 通过 abortController 终止
  • 持有多轮对话消息历史
  • 汇报工具调用次数和 Token 计数

后台化(Ctrl+B × 2)

当用户按两次 Ctrl+B 时,当前主会话被"后台化":

typescript 复制代码
// LocalMainSessionTask.ts
export function registerMainSessionTask(
  description: string,
  setAppState: SetAppState,
  mainThreadAgentDefinition?: AgentDefinition,
  existingAbortController?: AbortController,
): { taskId: string; abortSignal: AbortSignal } {
  const taskId = generateMainSessionTaskId()

  // 与子 agent 一样,写入独立 transcript 文件
  void initTaskOutputAsSymlink(taskId, getAgentTranscriptPath(asAgentId(taskId)))

  const taskState: LocalMainSessionTaskState = {
    ...createTaskStateBase(taskId, 'local_agent', description),
    type: 'local_agent',
    status: 'running',
    isBackgrounded: true, // 关键:标记为后台
    retain: false,
    pendingMessages: [],
    // ...
  }

  registerTask(taskState, setAppState)
  return { taskId, abortSignal: abortController.signal }
}

语义 :后台化的主会话使用与子 Agent 相同的 local_agent 状态结构,区别仅在于 agentType='main-session' 而非 'named-agent'。输出写入独立文件 ,不受 /clear 影响。

完成通知

typescript 复制代码
export function completeMainSessionTask(
  taskId: string,
  success: boolean,
  setAppState: SetAppState,
): void {
  updateTaskState<LocalMainSessionTaskState>(taskId, setAppState, task => {
    return {
      ...task,
      status: success ? 'completed' : 'failed',
      endTime: Date.now(),
      messages: task.messages?.length ? [task.messages.at(-1)!] : undefined,
    }
  })

  // 从 AppState 驱逐,释放内存
  void evictTaskOutput(taskId)
}

四、InProcessTeammateTask:同进程 Teammate

Teammate 是 Claude Code 的多 Agent 协作 核心------Leader 可以派生子 Agent(Teammates),每个子 Agent 在同一进程内运行。

Teammate 身份建模

typescript 复制代码
export type TeammateIdentity = {
  agentId: string       // "researcher@my-team"
  agentName: string     // "researcher"
  teamName: string
  color?: string
  planModeRequired: boolean
  parentSessionId: string  // Leader 的 session ID
}

为什么用 plain data 而不是引用? TeammateContext(运行时)使用 AsyncLocalStorage 存储运行时上下文,但 AppState 需要序列化所有状态以支持刷新恢复。所以 Identity 转为纯数据对象存储。

Teammate 完整状态

typescript 复制代码
export type InProcessTeammateTaskState = TaskStateBase & {
  type: 'in_process_teammate'
  identity: TeammateIdentity

  // 执行
  prompt: string
  model?: string
  selectedAgent?: AgentDefinition

  // 终止信号
  abortController?: AbortController
  currentWorkAbortController?: AbortController  // 只终止当前轮,不杀进程

  // Plan mode 审批
  awaitingPlanApproval: boolean

  // 权限模式(可独立于 Leader 通过 Shift+Tab 切换)
  permissionMode: PermissionMode

  // 状态
  error?: string
  result?: AgentToolResult
  progress?: AgentProgress

  // UI 镜象消息(限最新50条,节省内存)
  messages?: Message[]

  // 生命周期
  isIdle: boolean
  shutdownRequested: boolean

  // 进度追踪(计算通知差量)
  lastReportedToolCount: number
  lastReportedTokenCount: number
}

内存保护:消息上限

typescript 复制代码
// BQ分析(2026-03-20):500+轮对话时每 Agent 占用 ~20MB RSS
// 300个 Agent 并发可达 36.8GB
const TEAMMATE_MESSAGES_UI_CAP = 50

export function appendCappedMessage<T>(prev: readonly T[] | undefined, item: T): T[] {
  if (prev === undefined || prev.length === 0) return [item]
  if (prev.length >= TEAMMATE_MESSAGES_UI_CAP) {
    return [...prev.slice(-(TEAMMATE_MESSAGES_UI_CAP - 1)), item]
  }
  return [...prev, item]
}

task.messages 是 AppState 中的 UI 镜象 ------真正的完整对话在磁盘文件 getAgentTranscriptPath(agentId) 中。50条上限防止 UI 镜象无限膨胀,而磁盘文件才是真正的历史记录。


五、framework.ts:任务注册与状态更新

src/utils/task/framework.ts 是任务状态操作的工具库(~200行)。

5.1 注册任务

typescript 复制代码
export function registerTask(task: TaskState, setAppState: SetAppState): void {
  setAppState(prev => {
    const existing = prev.tasks[task.id]
    // 合并策略:保留 UI 状态(retain/messages/diskLoaded/pendingMessages)
    // 这样恢复(resumeAgentBackground)不会丢失用户在 UI 中的状态
    const merged = existing && 'retain' in existing
      ? {
          ...task,
          retain: existing.retain,
          startTime: existing.startTime,
          messages: existing.messages,
          diskLoaded: existing.diskLoaded,
          pendingMessages: existing.pendingMessages,
        }
      : task
    return { ...prev, tasks: { ...prev.tasks, [task.id]: merged } }
  })

  // 不是替换(恢复)才发送 SDK 事件
  if (!isReplacement) {
    enqueueSdkEvent({ type: 'system', subtype: 'task_started', ... })
  }
}

合并策略的精妙之处 :恢复一个任务时,UI 中的 retain(用户是否在查看)和 messages(用户新追加的提示词还未落盘)不能丢失。

5.2 状态更新

typescript 复制代码
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
    return { ...prev, tasks: { ...prev.tasks, [taskId]: updated } }
  })
}

Object.is 比较优化:这是防止 React 不必要重渲染的关键------只有状态真正变化时才触发订阅者更新。

5.3 驱逐终端任务

typescript 复制代码
export function evictTerminalTask(
  taskId: string,
  setAppState: SetAppState,
): void {
  setAppState(prev => {
    const task = prev.tasks?.[taskId]
    if (!task) return prev
    // 必须处于终态 + 用户已收到通知
    if (!isTerminalTaskStatus(task.status)) return prev
    if (!task.notified) return prev
    // 面板宽限期(30秒),防止 UI 抖动
    if ('retain' in task && (task.evictAfter ?? Infinity) > Date.now()) return prev
    const { [taskId]: _, ...remaining } = prev.tasks
    return { ...prev, tasks: remaining }
  })
}

三层守卫:终态 → 已通知 → 宽限期满(或无宽限期)→ 才从内存移除。


六、stopTask.ts:任务停止的统一入口

无论 LLM 调用 /stop 工具还是 SDK 发来 stop_task 控制请求,最终都走同一个 stopTask() 函数:

typescript 复制代码
export async function stopTask(
  taskId: string,
  context: StopTaskContext,
): Promise<StopTaskResult> {
  const { getAppState, setAppState } = context
  const task = appState.tasks?.[taskId]

  // 1. 验证存在
  if (!task) throw new StopTaskError(..., 'not_found')
  // 2. 验证运行中
  if (task.status !== 'running') throw new StopTaskError(..., 'not_running')
  // 3. 获取任务实现
  const taskImpl = getTaskByType(task.type)
  if (!taskImpl) throw new StopTaskError(..., 'unsupported_type')

  // 4. 杀死任务(多态调用)
  await taskImpl.kill(taskId, setAppState)

  // 5. Shell 任务:压制退出码 137 噪音通知
  //    Agent 任务:保留通知(含有 partialResult)
  if (isLocalShellTask(task)) {
    setAppState(prev => ({
      ...prev,
      tasks: { ...prev.tasks, [taskId]: { ...prev.tasks[taskId], notified: true } }
    }))
  }
}

多态 kill 模式tasks.ts 中注册了每种任务类型的 kill 实现:

typescript 复制代码
// src/tasks.ts
export function getTaskByType(type: TaskType): Task | undefined {
  return getAllTasks().find(t => t.type === type)
}
// Task = { name, type, kill(taskId, setAppState): Promise<void> }

七、pillLabel.ts:底栏标签文本

用户看到的"1 team · ↓ to view"这类文本由 getPillLabel() 生成:

typescript 复制代码
export function getPillLabel(tasks: BackgroundTaskState[]): string {
  const allSameType = tasks.every(t => t.type === tasks[0]!.type)

  if (allSameType) {
    switch (tasks[0]!.type) {
      case 'local_bash':
        const shells = count(tasks, t => t.kind !== 'monitor')
        const monitors = count(tasks, t => t.kind === 'monitor')
        return shells > 0 ? `${shells} shells` : `${monitors} monitors`
      case 'in_process_teammate':
        const teamCount = new Set(tasks.map(t => t.identity.teamName)).size
        return teamCount === 1 ? '1 team' : `${teamCount} teams`
      case 'local_agent':
        return n === 1 ? '1 local agent' : `${n} local agents`
      case 'remote_agent':
        if (n === 1 && tasks[0].isUltraplan) {
          // Ultraplan 特判:显示 ◇/◆ 钻石状态
          return ultraplanPhaseLabel(tasks[0].ultraplanPhase)
        }
        return `${n} cloud sessions`
      case 'dream':
        return 'dreaming'  // 简洁有趣
    }
  }

  return `${n} background tasks`
}

细节dream 任务显示 "dreaming",是唯一使用进行时态的任务类型------暗合 AI 在"空闲时思考"的隐喻。


八、任务 ID 设计与安全性

typescript 复制代码
// 36进制,8位随机 → 36^8 ≈ 2.8万亿组合
const TASK_ID_ALPHABET = '0123456789abcdefghijklmnopqrstuvwxyz'

export function generateTaskId(type: TaskType): string {
  const prefix = TASK_ID_PREFIXS[type]  // 'b'/'a'/'r'/'t'/'w'/'m'/'d'
  const bytes = randomBytes(8)
  return prefix + bytes.map(b => ALPHABET[b % 36]).join('')
}

任务 ID 写入磁盘路径(符号链接到 transcript 文件)。8位随机 + 36进制提供了足够的熵来抵御符号链接攻击(攻击者无法预测路径来覆盖系统文件)。


总结:Tasks 系统的设计哲学

维度 设计选择
统一状态 7种任务共享 AppState.tasks 字典,类型安全通过联合类型实现
磁盘持久化 每个任务输出写入独立文件 + 偏移量,支持断点续读
UI 镜象 task.messages 只保留最新50条,防止内存膨胀
多态 kill Task 接口统一所有任务类型的 kill 行为
后台/前景分离 isBackgrounded 标志区分底栏指示器与前景任务
驱逐策略 三层守卫:终态 → 已通知 → 宽限期满
恢复保真 任务替换时合并 UI 状态(retain/messages),不丢失用户上下文
内存安全 300并发 Agent → 36.8GB 上限通过消息上限和驱逐策略管控

Tasks 系统完美体现了 Claude Code 的并发非阻塞哲学:主会话永远可响应,背景任务并行推进,终止机制统一简洁。一套类型骨架 + 7种具体实现 + 1个通用框架 = 整系统的任务编排能力。


下一篇预告 :第二十四篇我们将深入 LocalShellTask,分析 Claude Code 如何在安全的沙箱内执行 Shell 命令,并实现命令超时、终止和输出流式回显。


📚 《Claude Code 源码分析 100 篇》系列

本系列正在持续更新,欢迎点赞、收藏、关注!

相关推荐
独隅1 小时前
IntelliJ IDEA 接入多种AI大模型插件终极指南(2026.1 企业合规版)
java·人工智能·intellij-idea
有Li1 小时前
EvoMDT:用于多癌种结构化临床决策的自进化多智能体系统文献速递/医学智能体前沿
人工智能·学习·分类·文献·医学生
小刘BlandNew1 小时前
AI核心概念大串联
人工智能
墨染天姬1 小时前
【AI】自驱动智能体
人工智能
D2aZXN3FhrDa7e2122 小时前
佛山乐从低预算实体店如何选择?看美诚AI自动化获客方案
运维·人工智能·自动化·佛山美诚科技有限公司
林泽毅2 小时前
PyTRIO:当强化学习不再需要本地GPU
人工智能·python·深度学习·机器学习
颜酱2 小时前
# 02 | 搭骨架:用 LangGraph 编排 12 步工作流(思路)
前端·人工智能·后端
颜酱2 小时前
02 | 搭骨架:用 LangGraph 编排 12 步工作流
前端·人工智能·后端
码上解惑2 小时前
从 Dify 工作流说起:常用节点怎么选、怎样组合?
java·人工智能·ai·agent·dify·智能体·spring ai