Claude Code 源码分析Claude Code 源码分析
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
  • 概览

    • Claude Code 源码分析
    • 源码统计
    • CodeGraph 图谱
  • 架构

    • 系统架构
    • UML 图表索引
    • 查询引擎
    • 核心流程
    • 消息系统
    • 状态管理
  • 功能模块

    • 工具系统
    • 斜杠命令
    • 服务层
    • MCP 协议
    • Skills 技能
    • 子代理系统
  • 分层深度

    • 入口层
    • UI / Ink 层
    • utils 基础设施
    • 桥接 / 远程
    • 上下文压缩
  • 原理与安全

    • 底层原理
    • 技术难点
    • 权限与安全
    • 内部机制
    • 遥测与分析
  • 深度专题

    • Hooks 系统
    • 插件系统
    • 记忆系统
    • API 通信层
    • Ink 终端 UI
    • 认证系统
    • 构建与发布
    • 术语表
  • 调用分析

    • 调用链分析
    • 核心文件索引
  • 模块详解

    • utils

      • 模块: utils
      • messages · 消息工厂与规范化
      • session-storage · JSONL 会话持久化
      • permissions · 工具权限决策
      • shell-hooks · 用户 Shell Hook 系统
    • components

      • 模块: components
      • REPL · 主屏编排
      • messages · 消息行渲染
      • PermissionRequest · 权限弹窗
      • PromptInput · 底部输入
    • services

      • 模块: services
      • api-claude · Anthropic API 流式与重试
      • mcp-client · MCP 连接与工具调用
      • compact · 上下文压缩与自动触发
      • analytics · GrowthBook、Datadog 与 1P 事件
    • tools

      • 模块: tools
      • tool-interface · Tool 契约与注册表
      • bash-tool · Shell 执行与权限
      • streaming-executor · 流式工具并发调度
      • agent-tool · 子 Agent 委派
    • commands

      • 模块: commands
      • command-registry · commands.ts 注册与分派
      • model-command · /model 模型选择
      • mcp-commands · /mcp 服务器管理
      • compact-memory-commands · /compact 与 /memory
    • ink

      • 模块: ink
      • Ink 渲染管线 · Screen 与终端输出
      • 终端事件 · resize、paste、stdin
      • Ink Hooks · 输入、搜索、终端状态
      • Ink 组件 · Box、Text、ScrollBox 原语
    • hooks

      • 模块: hooks
      • useCanUseTool · 权限 UI 接缝
      • 输入与快捷键 Hook
      • 合并态 Hook(MCP + 本地)
      • notifs 通知 Hook
    • bridge

      • 模块: bridge
      • repl-bridge · REPL 桥初始化与传输
      • bridge-messaging · 桥消息路由与入站处理
      • remote-bridge-core · env-less 核心与守护主循环
      • bridge-permissions-ui · 权限、API 与 TUI
    • cli

      • 模块: cli
      • Structured IO · NDJSON SDK 协议
      • CLI Transports · Session Ingress 传输层
      • CLI Handlers · 子命令懒加载实现
      • Update & Upload · 自更新与串行上传原语
    • screens

      • 模块: screens
      • REPL 屏 · Screen 类型与顶层路由
      • ResumeConversation · 会话恢复选择器
      • Doctor · 安装诊断全屏
    • entrypoints

      • 模块: entrypoints
      • cli-entrypoint · Bootstrap 与快路径
      • sdk-types · core / control / runtime 类型体系
      • mcp-entrypoint · MCP stdio 服务器
      • sandbox-types · 沙箱配置单一真相源
    • skills

      • 模块: skills
      • skills-loading · 磁盘加载与 bundled 注册表
      • bundled-skills · 内置 skill 与 initBundledSkills
      • mcp-skills · MCP prompt 转 skill
      • skill-tool-integration · SkillTool 与命令注册
    • types

      • 模块: types
      • message-types · Message 联合与 content blocks
      • tool-permission-types · Tool、Permission、Command 类型
      • api-sdk-types · API 与 Hooks 协议类型
      • misc-types · ids、plugin、generated 与其余类型
    • tasks

      • 模块: tasks
      • local-agent-task · 本地 Agent 与主会话后台化
      • remote-agent-task · 远程 CCR 与 In-Process Teammate
      • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
      • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybindings

      • 模块: keybindings
      • keybinding-registry · 注册、Provider 与 useKeybinding
      • default-bindings · 默认键位表与平台差异
      • command-bindings · command:* 动态斜杠命令绑定
      • vim-bindings · Vim 模式与 keybindings 边界
    • memdir

      • 模块: memdir
      • memdir-core · 路径、加载与 MEMORY.md
      • memory-extraction · extractMemories 与 SessionMemory
      • memdir-commands · /memory、/remember 与命令集成
    • state

      • 模块: state
      • app-state-core · store、AppState 类型与 Provider
      • app-state-selectors · selectors 与 onChangeAppState
      • teammate-state · 队友视图与 swarm 状态
      • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query

      • 模块: query
      • query config 与 deps · 配置快照与依赖注入
      • query tokenBudget · +500k 自动续跑
      • query transitions · Continue / Terminal 状态机
      • query stopHooks · Stop 事件与 turn 结束编排
  • 模块详解(扩展)

    • messages · 消息工厂与规范化
    • session-storage · JSONL 会话持久化
    • permissions · 工具权限决策
    • shell-hooks · 用户 Shell Hook 系统
    • REPL · 主屏编排
    • messages · 消息行渲染
    • PermissionRequest · 权限弹窗
    • PromptInput · 底部输入
    • api-claude · Anthropic API 流式与重试
    • mcp-client · MCP 连接与工具调用
    • compact · 上下文压缩与自动触发
    • analytics · GrowthBook、Datadog 与 1P 事件
    • tool-interface · Tool 契约与注册表
    • bash-tool · Shell 执行与权限
    • streaming-executor · 流式工具并发调度
    • agent-tool · 子 Agent 委派
    • command-registry · commands.ts 注册与分派
    • model-command · /model 模型选择
    • mcp-commands · /mcp 服务器管理
    • compact-memory-commands · /compact 与 /memory
    • Ink 渲染管线 · Screen 与终端输出
    • 终端事件 · resize、paste、stdin
    • Ink Hooks · 输入、搜索、终端状态
    • Ink 组件 · Box、Text、ScrollBox 原语
    • useCanUseTool · 权限 UI 接缝
    • 输入与快捷键 Hook
    • 合并态 Hook(MCP + 本地)
    • notifs 通知 Hook
    • repl-bridge · REPL 桥初始化与传输
    • bridge-messaging · 桥消息路由与入站处理
    • remote-bridge-core · env-less 核心与守护主循环
    • bridge-permissions-ui · 权限、API 与 TUI
    • Structured IO · NDJSON SDK 协议
    • CLI Transports · Session Ingress 传输层
    • CLI Handlers · 子命令懒加载实现
    • Update & Upload · 自更新与串行上传原语
    • REPL 屏 · Screen 类型与顶层路由
    • ResumeConversation · 会话恢复选择器
    • Doctor · 安装诊断全屏
    • cli-entrypoint · Bootstrap 与快路径
    • sdk-types · core / control / runtime 类型体系
    • mcp-entrypoint · MCP stdio 服务器
    • sandbox-types · 沙箱配置单一真相源
    • skills-loading · 磁盘加载与 bundled 注册表
    • bundled-skills · 内置 skill 与 initBundledSkills
    • mcp-skills · MCP prompt 转 skill
    • skill-tool-integration · SkillTool 与命令注册
    • message-types · Message 联合与 content blocks
    • tool-permission-types · Tool、Permission、Command 类型
    • api-sdk-types · API 与 Hooks 协议类型
    • misc-types · ids、plugin、generated 与其余类型
    • local-agent-task · 本地 Agent 与主会话后台化
    • remote-agent-task · 远程 CCR 与 In-Process Teammate
    • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
    • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybinding-registry · 注册、Provider 与 useKeybinding
    • default-bindings · 默认键位表与平台差异
    • command-bindings · command:* 动态斜杠命令绑定
    • vim-bindings · Vim 模式与 keybindings 边界
    • memdir-core · 路径、加载与 MEMORY.md
    • memory-extraction · extractMemories 与 SessionMemory
    • memdir-commands · /memory、/remember 与命令集成
    • app-state-core · store、AppState 类型与 Provider
    • app-state-selectors · selectors 与 onChangeAppState
    • teammate-state · 队友视图与 swarm 状态
    • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query config 与 deps · 配置快照与依赖注入
    • query tokenBudget · +500k 自动续跑
    • query transitions · Continue / Terminal 状态机
    • query stopHooks · Stop 事件与 turn 结束编排
  • 工具详解

    • tool-interface · Tool 契约与注册表
    • tool-permission-types · Tool、Permission、Command 类型
    • 工具: Bash
    • 工具: PowerShell
    • 工具: Agent
    • 工具: LSP
    • 工具: FileEdit
    • 工具: FileRead
    • 工具: Skill
    • 工具: WebFetch
    • 工具: MCP
    • 工具: SendMessage
    • 工具: FileWrite
    • 工具: Config
    • 工具: Grep
    • 工具: Brief
    • 工具: ExitPlanMode
    • 工具: ToolSearch
    • 工具: NotebookEdit
    • 工具: TaskOutput
    • 工具: WebSearch
    • 工具: ScheduleCron

本章总览

tasks/RemoteAgentTask/(约 850 行)管理 Claude Code Remote(CCR)云会话:registerRemoteAgentTask 创建 type=remote_agent 状态、initTaskOutput 预建磁盘文件、sidecar 持久化 metadata,startRemoteSessionPolling 每秒 pollRemoteSessionEvents 增量写入 log 与 appendTaskOutput。Ultraplan / Ultrareview / autofix-pr 等 remoteTaskType 通过 completionCheckers 与专用 notification 路径分支。InProcessTeammateTask/(约 125 行 + types 122 行)则代表同进程 multi-agent:AsyncLocalStorage 隔离 runtime,AppState 存 TeammateIdentity 与 capped messages。本章要求你能从 registerRemoteAgentTask 追踪到 poll 完成分支,以及从 spawnInProcess teammate 追踪到 injectUserMessageToTeammate。

学完本章你应该能

  • 说明 RemoteAgentTaskState 字段(remoteTaskType、ultraplanPhase、reviewProgress)
  • 解释 checkRemoteAgentEligibility 前置条件与 formatPreconditionError
  • 理解 restoreRemoteAgentTasks 在 --resume 时如何重建 polling
  • 掌握 InProcessTeammateTaskState 与 TeammateIdentity 的 team 寻址
  • 区分 remote_agent pill 文案(◇ cloud session)与 in_process_teammate(N teams)

核心概念(先读懂这些)

远程任务 = 本地 poller + CCR session

本地 CLI 不执行 remote query;它 poll CCR 事件流、镜像 log 到 task.log 与磁盘 output、在 archived/result/checker 满足时 terminal 并 enqueueRemoteNotification。sessionId 是 API 主键;taskId 是本地 AppState 键。kill 时 archiveRemoteSession 可选,Ultrareview 故意保留 session 供用户回访 URL。

In-process teammate ≠ LocalAgentTask

Teammate 用 type=in_process_teammate,identity.agentId 形如 researcher@my-team。runAgent 在同进程 AsyncLocalStorage 内执行,kill 委托 killInProcessTeammate。messages 数组仅 UI 镜像(TEAMMATE_MESSAGES_UI_CAP=50),完整 transcript 在 runner 与 disk。planModeRequired 驱动 awaitingPlanApproval 流。

Ultraplan phase 驱动 pill CTA

remote_agent.isUltraplan + ultraplanPhase(needs_input / plan_ready)由 ExitPlanModeScanner 写入。pillLabel 用 DIAMOND_OPEN/FILLED 区分;pillNeedsCta 仅在单任务 attention 状态显示「↓ to view」。

建议学习步骤

  1. 阅读 RemoteAgentTaskState 与 REMOTE_TASK_TYPES
  2. 阅读 checkRemoteAgentEligibility 与 formatPreconditionError
  3. 阅读 registerRemoteAgentTask 与 persistRemoteAgentMetadata
  4. 阅读 startRemoteSessionPolling 主循环与 completion 分支
  5. 阅读 restoreRemoteAgentTasksImpl
  6. 阅读 InProcessTeammateTaskState 与 injectUserMessageToTeammate

常见误区

注意

Ultraplan / isLongRunning 必须跳过 result 消息驱动 completion

注意

Remote review 用 extractReviewTagFromLog 增量扫描,避免 premature complete

注意

404 vs 401:restore 时仅 404 删 sidecar,auth 错误可 /login 恢复

注意

in-process teammate 禁止 nested teammate 与 background agent spawn(AgentTool 层检查)

RemoteAgentTaskState 核心字段

RemoteAgentTaskState 扩展 TaskStateBase:

字段含义
remoteTaskTyperemote-agent / ultraplan / ultrareview / autofix-pr / background-pr
sessionIdCCR API 会话 ID
command / title用户可见描述
todoList从 log 末次 TodoWrite 提取
log累积 SDKMessage[]
pollStartedAt恢复时重置,避免 review 30min 超时误杀
isUltraplan / ultraplanPhaseUltraplan pill 与 CTA
isRemoteReview / reviewProgressbughunter heartbeat JSON
isLongRunning跳过 result 驱动 completion
remoteTaskMetadataautofix-pr 的 owner/repo/prNumber

registerCompletionChecker 允许 product 代码注册 per-type 外部完成检测(如 PR merged)。

源码引用: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx · 第 22–86 行(共 1103 行)

  22|   TaskContext,
  23|   TaskStateBase,
  24| } from '../../Task.js'
  25| import { createTaskStateBase, generateTaskId } from '../../Task.js'
  26| import { TodoWriteTool } from '../../tools/TodoWriteTool/TodoWriteTool.js'
  27| import {
  28|   type BackgroundRemoteSessionPrecondition,
  29|   checkBackgroundRemoteSessionEligibility,
  30| } from '../../utils/background/remote/remoteSession.js'
  31| import { logForDebugging } from '../../utils/debug.js'
  32| import { logError } from '../../utils/log.js'
  33| import { enqueuePendingNotification } from '../../utils/messageQueueManager.js'
  34| import { extractTag, extractTextContent } from '../../utils/messages.js'
  35| import { emitTaskTerminatedSdk } from '../../utils/sdkEventQueue.js'
  36| import {
  37|   deleteRemoteAgentMetadata,
  38|   listRemoteAgentMetadata,
  39|   type RemoteAgentMetadata,
  40|   writeRemoteAgentMetadata,
  41| } from '../../utils/sessionStorage.js'
  42| import { jsonStringify } from '../../utils/slowOperations.js'
  43| import {
  44|   appendTaskOutput,
  45|   evictTaskOutput,
  46|   getTaskOutputPath,
  47|   initTaskOutput,
  48| } from '../../utils/task/diskOutput.js'
  49| import { registerTask, updateTaskState } from '../../utils/task/framework.js'
  50| import { fetchSession } from '../../utils/teleport/api.js'
  51| import {
  52|   archiveRemoteSession,
  53|   pollRemoteSessionEvents,
  54| } from '../../utils/teleport.js'
  55| import type { TodoList } from '../../utils/todo/types.js'
  56| import type { UltraplanPhase } from '../../utils/ultraplan/ccrSession.js'
  57| 
  58| export type RemoteAgentTaskState = TaskStateBase & {
  59|   type: 'remote_agent'
  60|   remoteTaskType: RemoteTaskType
  61|   /** Task-specific metadata (PR number, repo, etc.). */
  62|   remoteTaskMetadata?: RemoteTaskMetadata
  63|   sessionId: string // Original session ID for API calls
  64|   command: string
  65|   title: string
  66|   todoList: TodoList
  67|   log: SDKMessage[]
  68|   /**
  69|    * Long-running agent that will not be marked as complete after the first `result`.
  70|    */
  71|   isLongRunning?: boolean
  72|   /**
  73|    * When the local poller started watching this task (at spawn or on restore).
  74|    * Review timeout clocks from here so a restore doesn't immediately time out
  75|    * a task spawned >30min ago.
  76|    */
  77|   pollStartedAt: number
  78|   /** True when this task was created by a teleported /ultrareview command. */
  79|   isRemoteReview?: boolean
  80|   /** Parsed from the orchestrator's <remote-review-progress> heartbeat echoes. */
  81|   reviewProgress?: {
  82|     stage?: 'finding' | 'verifying' | 'synthesizing'
  83|     bugsFound: number
  84|     bugsVerified: number
  85|     bugsRefuted: number
  86|   }

Eligibility 与 precondition 错误

checkRemoteAgentEligibility 委托 checkBackgroundRemoteSessionEligibility:

error.type用户提示
not_logged_in/login Claude.ai OAuth
no_remote_environmentclaude.ai/code/onboarding env-setup
not_in_git_repo需 git 仓库
no_git_remote需 GitHub remote
github_app_not_installed安装 Claude GitHub App
policy_blocked组织策略禁用 remote sessions

formatPreconditionError 将结构化错误转为可读字符串,Background 命令与 /ultraplan 入口共用。skipBundle 用于已上传 bundle 的重试路径。

源码引用: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx · 第 124–161 行(共 1103 行)

 124|   remoteTaskMetadata: RemoteTaskMetadata | undefined,
 125| ) => Promise<string | null>
 126| 
 127| const completionCheckers = new Map<
 128|   RemoteTaskType,
 129|   RemoteTaskCompletionChecker
 130| >()
 131| 
 132| /**
 133|  * Register a completion checker for a remote task type. Invoked on every poll
 134|  * tick; survives --resume via the sidecar's remoteTaskType + remoteTaskMetadata.
 135|  */
 136| export function registerCompletionChecker(
 137|   remoteTaskType: RemoteTaskType,
 138|   checker: RemoteTaskCompletionChecker,
 139| ): void {
 140|   completionCheckers.set(remoteTaskType, checker)
 141| }
 142| 
 143| /**
 144|  * Persist a remote-agent metadata entry to the session sidecar.
 145|  * Fire-and-forget — persistence failures must not block task registration.
 146|  */
 147| async function persistRemoteAgentMetadata(
 148|   meta: RemoteAgentMetadata,
 149| ): Promise<void> {
 150|   try {
 151|     await writeRemoteAgentMetadata(meta.taskId, meta)
 152|   } catch (e) {
 153|     logForDebugging(`persistRemoteAgentMetadata failed: ${String(e)}`)
 154|   }
 155| }
 156| 
 157| /**
 158|  * Remove a remote-agent metadata entry from the session sidecar.
 159|  * Called on task completion/kill so restored sessions don't resurrect
 160|  * tasks that already finished.
 161|  */

registerRemoteAgentTask 与 sidecar

registerRemoteAgentTask 步骤:

  1. generateTaskId('remote_agent')
  2. initTaskOutput(taskId) — Remote 用 appendTaskOutput 而非 TaskOutput 组件
  3. createTaskStateBase + RemoteAgentTaskState 字段,status: running
  4. registerTask
  5. persistRemoteAgentMetadata(fire-and-forget writeRemoteAgentMetadata)
  6. startRemoteSessionPolling → 返回 cleanup stopPolling

callers(ultraplan.tsx、teleport 命令)负责 git dialog、transcript upload 等前置 UI;register 只处理统一 task 框架部分。

removeRemoteAgentMetadata 在 terminal/kill 时调用,防止 --resume 复活已完成任务。

源码引用: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx · 第 386–466 行(共 1103 行)

 386|       (msg.subtype === 'hook_progress' || msg.subtype === 'hook_response')
 387|     ) {
 388|       const tagged = extractTag(msg.stdout, REMOTE_REVIEW_TAG)
 389|       if (tagged?.trim()) return tagged.trim()
 390|     }
 391|   }
 392| 
 393|   // assistant text per-message scan (prompt mode)
 394|   for (let i = log.length - 1; i >= 0; i--) {
 395|     const msg = log[i]
 396|     if (msg?.type !== 'assistant') continue
 397|     const fullText = extractTextContent(msg.message.content, '\n')
 398|     const tagged = extractTag(fullText, REMOTE_REVIEW_TAG)
 399|     if (tagged?.trim()) return tagged.trim()
 400|   }
 401| 
 402|   // Hook-stdout concat fallback for split tags
 403|   const hookStdout = log
 404|     .filter(
 405|       msg =>
 406|         msg.type === 'system' &&
 407|         (msg.subtype === 'hook_progress' || msg.subtype === 'hook_response'),
 408|     )
 409|     .map(msg => msg.stdout)
 410|     .join('')
 411|   const hookTagged = extractTag(hookStdout, REMOTE_REVIEW_TAG)
 412|   if (hookTagged?.trim()) return hookTagged.trim()
 413| 
 414|   return null
 415| }
 416| 
 417| /**
 418|  * Enqueue a remote-review completion notification. Injects the review text
 419|  * directly into the message queue so the local model receives it on the next
 420|  * turn — no file indirection, no mode change. Session is kept alive so the
 421|  * claude.ai URL stays a durable record the user can revisit; TTL handles cleanup.
 422|  */
 423| function enqueueRemoteReviewNotification(
 424|   taskId: string,
 425|   reviewContent: string,
 426|   setAppState: SetAppState,
 427| ): void {
 428|   if (!markTaskNotified(taskId, setAppState)) return
 429| 
 430|   const message = `<${TASK_NOTIFICATION_TAG}>
 431| <${TASK_ID_TAG}>${taskId}</${TASK_ID_TAG}>
 432| <${TASK_TYPE_TAG}>remote_agent</${TASK_TYPE_TAG}>
 433| <${STATUS_TAG}>completed</${STATUS_TAG}>
 434| <${SUMMARY_TAG}>Remote review completed</${SUMMARY_TAG}>
 435| </${TASK_NOTIFICATION_TAG}>
 436| The remote review produced the following findings:
 437| 
 438| ${reviewContent}`
 439| 
 440|   enqueuePendingNotification({ value: message, mode: 'task-notification' })
 441| }
 442| 
 443| /**
 444|  * Enqueue a remote-review failure notification.
 445|  */
 446| function enqueueRemoteReviewFailureNotification(
 447|   taskId: string,
 448|   reason: string,
 449|   setAppState: SetAppState,
 450| ): void {
 451|   if (!markTaskNotified(taskId, setAppState)) return
 452| 
 453|   const message = `<${TASK_NOTIFICATION_TAG}>
 454| <${TASK_ID_TAG}>${taskId}</${TASK_ID_TAG}>
 455| <${TASK_TYPE_TAG}>remote_agent</${TASK_TYPE_TAG}>
 456| <${STATUS_TAG}>failed</${STATUS_TAG}>
 457| <${SUMMARY_TAG}>Remote review failed: ${reason}</${SUMMARY_TAG}>
 458| </${TASK_NOTIFICATION_TAG}>
 459| Remote review did not produce output (${reason}). Tell the user to retry /ultrareview, or use /review for a local review instead.`
 460| 
 461|   enqueuePendingNotification({ value: message, mode: 'task-notification' })
 462| }
 463| 
 464| /**
 465|  * Extract todo list from SDK messages (finds last TodoWrite tool use).
 466|  */

源码引用: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx · 第 166–183 行(共 1103 行)

 166|     logForDebugging(`removeRemoteAgentMetadata failed: ${String(e)}`)
 167|   }
 168| }
 169| 
 170| // Precondition error result
 171| export type RemoteAgentPreconditionResult =
 172|   | {
 173|       eligible: true
 174|     }
 175|   | {
 176|       eligible: false
 177|       errors: BackgroundRemoteSessionPrecondition[]
 178|     }
 179| 
 180| /**
 181|  * Check eligibility for creating a remote agent session.
 182|  */
 183| export async function checkRemoteAgentEligibility({

Polling 循环与 completion 分支

startRemoteSessionPolling(POLL_INTERVAL_MS=1000):

  1. pollRemoteSessionEvents(sessionId, lastEventId)
  2. newEvents → accumulatedLog,appendTaskOutput 写磁盘
  3. sessionStatus === 'archived' → completed + enqueueRemoteNotification
  4. completionCheckers.get(remoteTaskType) 非 null → 自定义完成
  5. result 消息(非 ultraplan/longRunning)→ 标准 remote-agent 完成
  6. isRemoteReview:extractReviewTagFromLog 增量 + STABLE_IDLE_POLLS=5 debounce
  7. update todoList、ultraplanPhase、reviewProgress 到 AppState

extractPlanFromLog / extractReviewFromLog 支持 assistant 文本与 hook_progress stdout 双 producer。enqueueUltraplanFailureNotification 不指向 raw JSONL output file。

RemoteAgentTask Task 实现 kill 时 stopPolling + archive + removeRemoteAgentMetadata。

源码引用: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx · 第 208–218 行(共 1103 行)

 208|     case 'no_git_remote':
 209|       return 'Background tasks require a GitHub remote. Add one with `git remote add origin REPO_URL`.'
 210|     case 'github_app_not_installed':
 211|       return 'The Claude GitHub app must be installed on this repository first.\nhttps://github.com/apps/claude/installations/new'
 212|     case 'policy_blocked':
 213|       return "Remote sessions are disabled by your organization's policy. Contact your organization admin to enable them."
 214|   }
 215| }
 216| 
 217| /**
 218|  * Enqueue a remote task notification to the message queue.

源码引用: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx · 第 538–610 行(共 1103 行)

 538|     sessionId: session.id,
 539|     command,
 540|     title: session.title,
 541|     todoList: [],
 542|     log: [],
 543|     isRemoteReview,
 544|     isUltraplan,
 545|     isLongRunning,
 546|     pollStartedAt: Date.now(),
 547|     remoteTaskMetadata,
 548|   }
 549| 
 550|   registerTask(taskState, context.setAppState)
 551| 
 552|   // Persist identity to the session sidecar so --resume can reconnect to
 553|   // still-running remote sessions. Status is not stored — it's fetched
 554|   // fresh from CCR on restore.
 555|   void persistRemoteAgentMetadata({
 556|     taskId,
 557|     remoteTaskType,
 558|     sessionId: session.id,
 559|     title: session.title,
 560|     command,
 561|     spawnedAt: Date.now(),
 562|     toolUseId,
 563|     isUltraplan,
 564|     isRemoteReview,
 565|     isLongRunning,
 566|     remoteTaskMetadata,
 567|   })
 568| 
 569|   // Ultraplan lifecycle is owned by startDetachedPoll in ultraplan.tsx. Generic
 570|   // polling still runs so session.log populates for the detail view's progress
 571|   // counts; the result-lookup guard below prevents early completion.
 572|   // TODO(#23985): fold ExitPlanModeScanner into this poller, drop startDetachedPoll.
 573|   const stopPolling = startRemoteSessionPolling(taskId, context)
 574| 
 575|   return {
 576|     taskId,
 577|     sessionId: session.id,
 578|     cleanup: stopPolling,
 579|   }
 580| }
 581| 
 582| /**
 583|  * Restore remote-agent tasks from the session sidecar on --resume.
 584|  *
 585|  * Scans remote-agents/, fetches live CCR status for each, reconstructs
 586|  * RemoteAgentTaskState into AppState.tasks, and restarts polling for sessions
 587|  * still running. Sessions that are archived or 404 have their sidecar file
 588|  * removed. Must run after switchSession() so getSessionId() points at the
 589|  * resumed session's sidecar directory.
 590|  */
 591| export async function restoreRemoteAgentTasks(
 592|   context: TaskContext,
 593| ): Promise<void> {
 594|   try {
 595|     await restoreRemoteAgentTasksImpl(context)
 596|   } catch (e) {
 597|     logForDebugging(`restoreRemoteAgentTasks failed: ${String(e)}`)
 598|   }
 599| }
 600| 
 601| async function restoreRemoteAgentTasksImpl(
 602|   context: TaskContext,
 603| ): Promise<void> {
 604|   const persisted = await listRemoteAgentMetadata()
 605|   if (persisted.length === 0) return
 606| 
 607|   for (const meta of persisted) {
 608|     let remoteStatus: string
 609|     try {
 610|       const session = await fetchSession(meta.sessionId)

源码引用: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx · 第 254–283 行(共 1103 行)

 254|  * Atomically mark a task as notified. Returns true if this call flipped the
 255|  * flag (caller should enqueue), false if already notified (caller should skip).
 256|  */
 257| function markTaskNotified(taskId: string, setAppState: SetAppState): boolean {
 258|   let shouldEnqueue = false
 259|   updateTaskState(taskId, setAppState, task => {
 260|     if (task.notified) {
 261|       return task
 262|     }
 263|     shouldEnqueue = true
 264|     return { ...task, notified: true }
 265|   })
 266|   return shouldEnqueue
 267| }
 268| 
 269| /**
 270|  * Extract the plan content from the remote session log.
 271|  * Searches all assistant messages for <ultraplan>...</ultraplan> tags.
 272|  */
 273| export function extractPlanFromLog(log: SDKMessage[]): string | null {
 274|   // Walk backwards through assistant messages to find <ultraplan> content
 275|   for (let i = log.length - 1; i >= 0; i--) {
 276|     const msg = log[i]
 277|     if (msg?.type !== 'assistant') continue
 278|     const fullText = extractTextContent(msg.message.content, '\n')
 279|     const plan = extractTag(fullText, ULTRAPLAN_TAG)
 280|     if (plan?.trim()) return plan.trim()
 281|   }
 282|   return null
 283| }

--resume 恢复

restoreRemoteAgentTasks 在 switchSession 之后调用:

  1. listRemoteAgentMetadata 读 sidecar
  2. fetchSession(sessionId) 取 live session_status
  3. 404 → removeRemoteAgentMetadata(会话已删)
  4. archived → removeRemoteAgentMetadata(离线期间已结束)
  5. 其他错误(401 等)→ skip 保留 sidecar 待 /login
  6. running → 重建 RemoteAgentTaskState,initTaskOutput,startRemoteSessionPolling

pollStartedAt: Date.now() 重置 review 超时时钟,避免恢复后立即 timeout。

remoteTaskType 非法值 fallback 为 'remote-agent'。

源码引用: src/tasks/RemoteAgentTask/RemoteAgentTask.tsx · 第 477–532 行(共 1103 行)

 477|   }
 478| 
 479|   const input = todoListMessage.message.content.find(
 480|     (block): block is ToolUseBlock =>
 481|       block.type === 'tool_use' && block.name === TodoWriteTool.name,
 482|   )?.input
 483|   if (!input) {
 484|     return []
 485|   }
 486| 
 487|   const parsedInput = TodoWriteTool.inputSchema.safeParse(input)
 488|   if (!parsedInput.success) {
 489|     return []
 490|   }
 491| 
 492|   return parsedInput.data.todos
 493| }
 494| 
 495| /**
 496|  * Register a remote agent task in the unified task framework.
 497|  * Bundles task ID generation, output init, state creation, registration, and polling.
 498|  * Callers remain responsible for custom pre-registration logic (git dialogs, transcript upload, teleport options).
 499|  */
 500| export function registerRemoteAgentTask(options: {
 501|   remoteTaskType: RemoteTaskType
 502|   session: { id: string; title: string }
 503|   command: string
 504|   context: TaskContext
 505|   toolUseId?: string
 506|   isRemoteReview?: boolean
 507|   isUltraplan?: boolean
 508|   isLongRunning?: boolean
 509|   remoteTaskMetadata?: RemoteTaskMetadata
 510| }): {
 511|   taskId: string
 512|   sessionId: string
 513|   cleanup: () => void
 514| } {
 515|   const {
 516|     remoteTaskType,
 517|     session,
 518|     command,
 519|     context,
 520|     toolUseId,
 521|     isRemoteReview,
 522|     isUltraplan,
 523|     isLongRunning,
 524|     remoteTaskMetadata,
 525|   } = options
 526|   const taskId = generateTaskId('remote_agent')
 527| 
 528|   // Create the output file before registering the task.
 529|   // RemoteAgentTask uses appendTaskOutput() (not TaskOutput), so
 530|   // the file must exist for readers before any output arrives.
 531|   void initTaskOutput(taskId)
 532| 

InProcessTeammateTaskState 与 types

TeammateIdentity 存 agentId、agentName、teamName、color、planModeRequired、parentSessionId。

InProcessTeammateTaskState 关键字段:

  • identity — 与 TeammateContext 同形,但 plain data 可序列化
  • abortController — 杀整个 teammate;currentWorkAbortController — 仅 abort 当前 turn
  • awaitingPlanApproval — plan mode 审批 UI
  • permissionMode — Shift+Tab 独立切换
  • pendingUserMessages — zoom 视图 typed 输入队列
  • isIdle / shutdownRequested — 生命周期与优雅关闭
  • TEAMMATE_MESSAGES_UI_CAP=50 — appendCappedMessage 防 RSS 爆炸

BQ 分析引用:292 agent burst 达 36.8GB,messages 镜像为 dominant cost。

源码引用: src/tasks/InProcessTeammateTask/types.ts · 第 13–76 行(共 122 行)

  13| export type TeammateIdentity = {
  14|   agentId: string // e.g., "researcher@my-team"
  15|   agentName: string // e.g., "researcher"
  16|   teamName: string
  17|   color?: string
  18|   planModeRequired: boolean
  19|   parentSessionId: string // Leader's session ID
  20| }
  21| 
  22| export type InProcessTeammateTaskState = TaskStateBase & {
  23|   type: 'in_process_teammate'
  24| 
  25|   // Identity as sub-object (matches TeammateContext shape for consistency)
  26|   // Stored as plain data in AppState, NOT a reference to AsyncLocalStorage
  27|   identity: TeammateIdentity
  28| 
  29|   // Execution
  30|   prompt: string
  31|   // Optional model override for this teammate
  32|   model?: string
  33|   // Optional: Only set if teammate uses a specific agent definition
  34|   // Many teammates run as general-purpose agents without a predefined definition
  35|   selectedAgent?: AgentDefinition
  36|   abortController?: AbortController // Runtime only, not serialized to disk - kills WHOLE teammate
  37|   currentWorkAbortController?: AbortController // Runtime only - aborts current turn without killing teammate
  38|   unregisterCleanup?: () => void // Runtime only
  39| 
  40|   // Plan mode approval tracking (planModeRequired is in identity)
  41|   awaitingPlanApproval: boolean
  42| 
  43|   // Permission mode for this teammate (cycled independently via Shift+Tab when viewing)
  44|   permissionMode: PermissionMode
  45| 
  46|   // State
  47|   error?: string
  48|   result?: AgentToolResult // Reuse existing type since teammates run via runAgent()
  49|   progress?: AgentProgress
  50| 
  51|   // Conversation history for zoomed view (NOT mailbox messages)
  52|   // Mailbox messages are stored separately in teamContext.inProcessMailboxes
  53|   messages?: Message[]
  54| 
  55|   // Tool use IDs currently being executed (for animation in transcript view)
  56|   inProgressToolUseIDs?: Set<string>
  57| 
  58|   // Queue of user messages to deliver when viewing teammate transcript
  59|   pendingUserMessages: string[]
  60| 
  61|   // UI: random spinner verbs (stable across re-renders, shared between components)
  62|   spinnerVerb?: string
  63|   pastTenseVerb?: string
  64| 
  65|   // Lifecycle
  66|   isIdle: boolean
  67|   shutdownRequested: boolean
  68| 
  69|   // Callbacks to notify when teammate becomes idle (runtime only)
  70|   // Used by leader to efficiently wait without polling
  71|   onIdleCallbacks?: Array<() => void>
  72| 
  73|   // Progress tracking (for computing deltas in notifications)
  74|   lastReportedToolCount: number
  75|   lastReportedTokenCount: number
  76| }

源码引用: src/tasks/InProcessTeammateTask/types.ts · 第 101–121 行(共 122 行)

 101| export const TEAMMATE_MESSAGES_UI_CAP = 50
 102| 
 103| /**
 104|  * Append an item to a message array, capping the result at
 105|  * TEAMMATE_MESSAGES_UI_CAP entries by dropping the oldest. Always returns
 106|  * a new array (AppState immutability).
 107|  */
 108| export function appendCappedMessage<T>(
 109|   prev: readonly T[] | undefined,
 110|   item: T,
 111| ): T[] {
 112|   if (prev === undefined || prev.length === 0) {
 113|     return [item]
 114|   }
 115|   if (prev.length >= TEAMMATE_MESSAGES_UI_CAP) {
 116|     const next = prev.slice(-(TEAMMATE_MESSAGES_UI_CAP - 1))
 117|     next.push(item)
 118|     return next
 119|   }
 120|   return [...prev, item]
 121| }

InProcessTeammateTask API

InProcessTeammateTask Task 实现仅 export kill → killInProcessTeammate。

辅助函数:

  • requestTeammateShutdown — shutdownRequested=true,runner 优雅退出
  • appendTeammateMessage — running 时 capped append 到 messages
  • injectUserMessageToTeammate — pendingUserMessages + 即时 createUserMessage 显示
  • findTeammateTaskByAgentId — 优先 running 匹配
  • getRunningTeammatesSorted — agentName localeCompare,与 TeammateSpinnerTree / PromptInput footer 共享排序

Teammate 与 LocalAgentTask 协作:spawn 时 registerAsyncAgent 可传 parentAbortController;SendMessage 走 queuePendingMessage / injectUserMessageToTeammate 双路径。

源码引用: src/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx · 第 24–30 行(共 157 行)

  24| import { appendCappedMessage, isInProcessTeammateTask } from './types.js'
  25| 
  26| /**
  27|  * InProcessTeammateTask - Handles in-process teammate execution.
  28|  */
  29| export const InProcessTeammateTask: Task = {
  30|   name: 'InProcessTeammateTask',

源码引用: src/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx · 第 68–84 行(共 157 行)

  68|     }
  69| 
  70|     return {
  71|       ...task,
  72|       messages: appendCappedMessage(task.messages, message),
  73|     }
  74|   })
  75| }
  76| 
  77| /**
  78|  * Inject a user message to a teammate's pending queue.
  79|  * Used when viewing a teammate's transcript to send typed messages to them.
  80|  * Also adds the message to task.messages so it appears immediately in the transcript.
  81|  */
  82| export function injectUserMessageToTeammate(
  83|   taskId: string,
  84|   message: string,

源码引用: src/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx · 第 92–125 行(共 157 行)

  92|         `Dropping message for teammate task ${taskId}: task status is "${task.status}"`,
  93|       )
  94|       return task
  95|     }
  96| 
  97|     return {
  98|       ...task,
  99|       pendingUserMessages: [...task.pendingUserMessages, message],
 100|       messages: appendCappedMessage(
 101|         task.messages,
 102|         createUserMessage({ content: message }),
 103|       ),
 104|     }
 105|   })
 106| }
 107| 
 108| /**
 109|  * Get teammate task by agent ID from AppState.
 110|  * Prefers running tasks over killed/completed ones in case multiple tasks
 111|  * with the same agentId exist.
 112|  * Returns undefined if not found.
 113|  */
 114| export function findTeammateTaskByAgentId(
 115|   agentId: string,
 116|   tasks: Record<string, TaskStateBase>,
 117| ): InProcessTeammateTaskState | undefined {
 118|   let fallback: InProcessTeammateTaskState | undefined
 119|   for (const task of Object.values(tasks)) {
 120|     if (isInProcessTeammateTask(task) && task.identity.agentId === agentId) {
 121|       // Prefer running tasks in case old killed tasks still exist in AppState
 122|       // alongside new running ones with the same agentId
 123|       if (task.status === 'running') {
 124|         return task
 125|       }

Remote vs Teammate 对比

维度remote_agentin_process_teammate
执行位置CCR 云端本地 Node 同进程
进度来源poll SDK eventsrunAgent assistant 流
killarchive + stop pollkillInProcessTeammate
pill 文案◇ N cloud sessionsN teams
持久化sidecar metadataAppState + agent transcript
典型入口/ultraplan、teleportAgent(name, team_name)

调试 remote pill 不更新:查 poll 是否仍在运行、task.status 是否仍为 running、ultraplanPhase 是否写入。调试 teammate 消息丢失:查 isTerminalTaskStatus 与 pendingUserMessages drain。

本章小结与延伸

RemoteAgentTask = CCR 会话的本地镜像与 poller。InProcessTeammateTask = 同进程 swarm 的 Task 适配层。下一章 shell-workflow-tasks 读 Bash 后台与 stopTask。 继续学习:

  • shell-workflow-tasks
  • dream-monitor-tasks
Prev
local-agent-task · 本地 Agent 与主会话后台化
Next
shell-workflow-tasks · Bash 后台、Workflow 与 stopTask