第二十三篇:Tasks系统,Claude Code如何统一管理7种后台任务
本文是《Claude Code 源码分析 100 篇》系列的第二十三篇。为什么需要 Tasks 系统Claude Code 是一个高度并发的工作环境——一个主会话Leader可以同时调度多个后台任务用户按CtrlB把当前查询打入后台继续与主会话对话Agent 任务Teammates独立运行实时汇报进度Shell 脚本在后台默默执行随时被杀死MCP 工具持续监控资源状态Remote Agent 连接云端 claude.ai 会话Workflow 执行长流程脚本Dream 在空闲时生成测试建议这7种任务类型共存在同一个状态空间AppState.tasks中。Tasks 系统就是这个统一管理层。一、Task.ts类型骨架src/Task.ts定义了所有任务类型的基础类型系统~120行。1.1 七种任务类型exporttypeTaskType|local_bash// 本地 Shell 命令b前缀ID|local_agent// 本地 Agent 子进程a前缀ID|remote_agent// 远程 claude.ai 会话r前缀ID|in_process_teammate// 同进程 Teammatet前缀ID|local_workflow// 工作流脚本w前缀ID|monitor_mcp// MCP 资源监控m前缀ID|dream// 空闲时做梦d前缀ID每种类型有一个固定单字母前缀配合8位随机字符组成16字符任务IDba1b2c3d4e5f6g7h。1.2 任务基础状态exporttypeTaskStateBase{id:stringtype:TaskType status:pending|running|completed|failed|killeddescription:stringtoolUseId?:stringstartTime:numberendTime?:numbertotalPausedMs?:numberoutputFile:string// 磁盘输出文件路径outputOffset:number// 已读偏移断点续传notified:boolean// 用户通知已发送}outputFileoutputOffset是磁盘持久化核心——每个任务将输出写入独立文件UI 通过偏移量增量读取即使任务结束也能回顾完整输出。1.3 生命周期判断exportfunctionisTerminalTaskStatus(status:TaskStatus):boolean{returnstatuscompleted||statusfailed||statuskilled}用途防止向已死亡任务注入消息、触发任务清理、孤儿进程清理路径的前置守卫。二、types.ts任务状态联合类型src/tasks/types.ts是整个任务系统的类型交汇点exporttypeTaskState|LocalShellTaskState|LocalAgentTaskState|RemoteAgentTaskState|InProcessTeammateTaskState|LocalWorkflowTaskState|MonitorMcpTaskState|DreamTaskState每种具体状态都扩展了TaskStateBase添加各自特有的字段。后台任务过滤exportfunctionisBackgroundTask(task:TaskState):boolean{// 必须是 running/pendingif(task.status!runningtask.status!pending)returnfalse// 前景任务不算后台if(isBackgroundedintasktask.isBackgroundedfalse)returnfalsereturntrue}设计意图只有被用户主动后台化isBackgroundedtrue且在运行中的任务才出现在底栏 pill 指示器中。三、LocalAgentTask本地 Agent 子进程本地 Agent 通过 Bun 子进程运行执行 LLM 对话与用户自己的 Claude Code 并行。核心特性独立子进程不阻塞主对话通过abortController终止持有多轮对话消息历史汇报工具调用次数和 Token 计数后台化CtrlB × 2当用户按两次CtrlB时当前主会话被后台化// LocalMainSessionTask.tsexportfunctionregisterMainSessionTask(description:string,setAppState:SetAppState,mainThreadAgentDefinition?:AgentDefinition,existingAbortController?:AbortController,):{taskId:string;abortSignal:AbortSignal}{consttaskIdgenerateMainSessionTaskId()// 与子 agent 一样写入独立 transcript 文件voidinitTaskOutputAsSymlink(taskId,getAgentTranscriptPath(asAgentId(taskId)))consttaskState: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状态结构区别仅在于agentTypemain-session而非named-agent。输出写入独立文件不受/clear影响。完成通知exportfunctioncompleteMainSessionTask(taskId:string,success:boolean,setAppState:SetAppState,):void{updateTaskStateLocalMainSessionTaskState(taskId,setAppState,task{return{...task,status:success?completed:failed,endTime:Date.now(),messages:task.messages?.length?[task.messages.at(-1)!]:undefined,}})// 从 AppState 驱逐释放内存voidevictTaskOutput(taskId)}四、InProcessTeammateTask同进程 TeammateTeammate 是 Claude Code 的多 Agent 协作核心——Leader 可以派生子 AgentTeammates每个子 Agent 在同一进程内运行。Teammate 身份建模exporttypeTeammateIdentity{agentId:string// researchermy-teamagentName:string// researcherteamName:stringcolor?:stringplanModeRequired:booleanparentSessionId:string// Leader 的 session ID}为什么用 plain data 而不是引用TeammateContext运行时使用 AsyncLocalStorage 存储运行时上下文但 AppState 需要序列化所有状态以支持刷新恢复。所以 Identity 转为纯数据对象存储。Teammate 完整状态exporttypeInProcessTeammateTaskStateTaskStateBase{type:in_process_teammateidentity:TeammateIdentity// 执行prompt:stringmodel?:stringselectedAgent?:AgentDefinition// 终止信号abortController?:AbortController currentWorkAbortController?:AbortController// 只终止当前轮不杀进程// Plan mode 审批awaitingPlanApproval:boolean// 权限模式可独立于 Leader 通过 ShiftTab 切换permissionMode:PermissionMode// 状态error?:stringresult?:AgentToolResult progress?:AgentProgress// UI 镜象消息限最新50条节省内存messages?:Message[]// 生命周期isIdle:booleanshutdownRequested:boolean// 进度追踪计算通知差量lastReportedToolCount:numberlastReportedTokenCount:number}内存保护消息上限// BQ分析2026-03-20500轮对话时每 Agent 占用 ~20MB RSS// 300个 Agent 并发可达 36.8GBconstTEAMMATE_MESSAGES_UI_CAP50exportfunctionappendCappedMessageT(prev:readonlyT[]|undefined,item:T):T[]{if(prevundefined||prev.length0)return[item]if(prev.lengthTEAMMATE_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 注册任务exportfunctionregisterTask(task:TaskState,setAppState:SetAppState):void{setAppState(prev{constexistingprev.tasks[task.id]// 合并策略保留 UI 状态retain/messages/diskLoaded/pendingMessages// 这样恢复resumeAgentBackground不会丢失用户在 UI 中的状态constmergedexistingretaininexisting?{...task,retain:existing.retain,startTime:existing.startTime,messages:existing.messages,diskLoaded:existing.diskLoaded,pendingMessages:existing.pendingMessages,}:taskreturn{...prev,tasks:{...prev.tasks,[task.id]:merged}}})// 不是替换恢复才发送 SDK 事件if(!isReplacement){enqueueSdkEvent({type:system,subtype:task_started,...})}}合并策略的精妙之处恢复一个任务时UI 中的retain用户是否在查看和messages用户新追加的提示词还未落盘不能丢失。5.2 状态更新exportfunctionupdateTaskStateTextendsTaskState(taskId:string,setAppState:SetAppState,updater:(task:T)T,):void{setAppState(prev{consttaskprev.tasks?.[taskId]asT|undefinedif(!task)returnprevconstupdatedupdater(task)// 返回同一引用 → 跳过更新防止订阅者不必要重渲染if(updatedtask)returnprevreturn{...prev,tasks:{...prev.tasks,[taskId]:updated}}})}Object.is比较优化这是防止 React 不必要重渲染的关键——只有状态真正变化时才触发订阅者更新。5.3 驱逐终端任务exportfunctionevictTerminalTask(taskId:string,setAppState:SetAppState,):void{setAppState(prev{consttaskprev.tasks?.[taskId]if(!task)returnprev// 必须处于终态 用户已收到通知if(!isTerminalTaskStatus(task.status))returnprevif(!task.notified)returnprev// 面板宽限期30秒防止 UI 抖动if(retainintask(task.evictAfter??Infinity)Date.now())returnprevconst{[taskId]:_,...remaining}prev.tasksreturn{...prev,tasks:remaining}})}三层守卫终态 → 已通知 → 宽限期满或无宽限期→ 才从内存移除。六、stopTask.ts任务停止的统一入口无论 LLM 调用/stop工具还是 SDK 发来stop_task控制请求最终都走同一个stopTask()函数exportasyncfunctionstopTask(taskId:string,context:StopTaskContext,):PromiseStopTaskResult{const{getAppState,setAppState}contextconsttaskappState.tasks?.[taskId]// 1. 验证存在if(!task)thrownewStopTaskError(...,not_found)// 2. 验证运行中if(task.status!running)thrownewStopTaskError(...,not_running)// 3. 获取任务实现consttaskImplgetTaskByType(task.type)if(!taskImpl)thrownewStopTaskError(...,unsupported_type)// 4. 杀死任务多态调用awaittaskImpl.kill(taskId,setAppState)// 5. Shell 任务压制退出码 137 噪音通知// Agent 任务保留通知含有 partialResultif(isLocalShellTask(task)){setAppState(prev({...prev,tasks:{...prev.tasks,[taskId]:{...prev.tasks[taskId],notified:true}}}))}}多态 kill 模式tasks.ts中注册了每种任务类型的 kill 实现// src/tasks.tsexportfunctiongetTaskByType(type:TaskType):Task|undefined{returngetAllTasks().find(tt.typetype)}// Task { name, type, kill(taskId, setAppState): Promisevoid }七、pillLabel.ts底栏标签文本用户看到的1 team · ↓ to view这类文本由getPillLabel()生成exportfunctiongetPillLabel(tasks:BackgroundTaskState[]):string{constallSameTypetasks.every(tt.typetasks[0]!.type)if(allSameType){switch(tasks[0]!.type){caselocal_bash:constshellscount(tasks,tt.kind!monitor)constmonitorscount(tasks,tt.kindmonitor)returnshells0?${shells}shells:${monitors}monitorscasein_process_teammate:constteamCountnewSet(tasks.map(tt.identity.teamName)).sizereturnteamCount1?1 team:${teamCount}teamscaselocal_agent:returnn1?1 local agent:${n}local agentscaseremote_agent:if(n1tasks[0].isUltraplan){// Ultraplan 特判显示 ◇/◆ 钻石状态returnultraplanPhaseLabel(tasks[0].ultraplanPhase)}return${n}cloud sessionscasedream:returndreaming// 简洁有趣}}return${n}background tasks}细节dream任务显示 “dreaming”是唯一使用进行时态的任务类型——暗合 AI 在空闲时思考的隐喻。八、任务 ID 设计与安全性// 36进制8位随机 → 36^8 ≈ 2.8万亿组合constTASK_ID_ALPHABET0123456789abcdefghijklmnopqrstuvwxyzexportfunctiongenerateTaskId(type:TaskType):string{constprefixTASK_ID_PREFIXS[type]// b/a/r/t/w/m/dconstbytesrandomBytes(8)returnprefixbytes.map(bALPHABET[b%36]).join()}任务 ID 写入磁盘路径符号链接到 transcript 文件。8位随机 36进制提供了足够的熵来抵御符号链接攻击攻击者无法预测路径来覆盖系统文件。总结Tasks 系统的设计哲学维度设计选择统一状态7种任务共享AppState.tasks字典类型安全通过联合类型实现磁盘持久化每个任务输出写入独立文件 偏移量支持断点续读UI 镜象task.messages只保留最新50条防止内存膨胀多态 killTask接口统一所有任务类型的 kill 行为后台/前景分离isBackgrounded标志区分底栏指示器与前景任务驱逐策略三层守卫终态 → 已通知 → 宽限期满恢复保真任务替换时合并 UI 状态retain/messages不丢失用户上下文内存安全300并发 Agent → 36.8GB 上限通过消息上限和驱逐策略管控Tasks 系统完美体现了 Claude Code 的并发非阻塞哲学主会话永远可响应背景任务并行推进终止机制统一简洁。一套类型骨架 7种具体实现 1个通用框架 整系统的任务编排能力。下一篇预告第二十四篇我们将深入LocalShellTask分析 Claude Code 如何在安全的沙箱内执行 Shell 命令并实现命令超时、终止和输出流式回显。《Claude Code 源码分析 100 篇》系列本系列正在持续更新欢迎点赞、收藏、关注