本章总览
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」。
建议学习步骤
- 阅读 RemoteAgentTaskState 与 REMOTE_TASK_TYPES
- 阅读 checkRemoteAgentEligibility 与 formatPreconditionError
- 阅读 registerRemoteAgentTask 与 persistRemoteAgentMetadata
- 阅读 startRemoteSessionPolling 主循环与 completion 分支
- 阅读 restoreRemoteAgentTasksImpl
- 阅读 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:
| 字段 | 含义 |
|---|---|
| remoteTaskType | remote-agent / ultraplan / ultrareview / autofix-pr / background-pr |
| sessionId | CCR API 会话 ID |
| command / title | 用户可见描述 |
| todoList | 从 log 末次 TodoWrite 提取 |
| log | 累积 SDKMessage[] |
| pollStartedAt | 恢复时重置,避免 review 30min 超时误杀 |
| isUltraplan / ultraplanPhase | Ultraplan pill 与 CTA |
| isRemoteReview / reviewProgress | bughunter heartbeat JSON |
| isLongRunning | 跳过 result 驱动 completion |
| remoteTaskMetadata | autofix-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_environment | claude.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 步骤:
- generateTaskId('remote_agent')
- initTaskOutput(taskId) — Remote 用 appendTaskOutput 而非 TaskOutput 组件
- createTaskStateBase + RemoteAgentTaskState 字段,status: running
- registerTask
- persistRemoteAgentMetadata(fire-and-forget writeRemoteAgentMetadata)
- 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):
- pollRemoteSessionEvents(sessionId, lastEventId)
- newEvents → accumulatedLog,appendTaskOutput 写磁盘
- sessionStatus === 'archived' → completed + enqueueRemoteNotification
- completionCheckers.get(remoteTaskType) 非 null → 自定义完成
- result 消息(非 ultraplan/longRunning)→ 标准 remote-agent 完成
- isRemoteReview:extractReviewTagFromLog 增量 + STABLE_IDLE_POLLS=5 debounce
- 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 之后调用:
- listRemoteAgentMetadata 读 sidecar
- fetchSession(sessionId) 取 live session_status
- 404 → removeRemoteAgentMetadata(会话已删)
- archived → removeRemoteAgentMetadata(离线期间已结束)
- 其他错误(401 等)→ skip 保留 sidecar 待 /login
- 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_agent | in_process_teammate |
|---|---|---|
| 执行位置 | CCR 云端 | 本地 Node 同进程 |
| 进度来源 | poll SDK events | runAgent assistant 流 |
| kill | archive + stop poll | killInProcessTeammate |
| pill 文案 | ◇ N cloud sessions | N teams |
| 持久化 | sidecar metadata | AppState + agent transcript |
| 典型入口 | /ultraplan、teleport | Agent(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。 继续学习: