From 2567e77d37966327a42c8d823702e68a1209512c Mon Sep 17 00:00:00 2001 From: Slayer Date: Tue, 9 Jun 2026 11:50:46 +0800 Subject: [PATCH] sub agents docs (#1266) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * docs: 添加 JSONL transcript 会话机制文档 * docs: 重构多 Agent 编排机制文档 --- docs.json | 1 + docs/agent/coordinator-and-swarm.mdx | 776 ++++++++++++---- .../session-transcript-persistence.md | 828 ++++++++++++++++++ 3 files changed, 1417 insertions(+), 188 deletions(-) create mode 100644 docs/internals/session-transcript-persistence.md diff --git a/docs.json b/docs.json index 9350d9105..fbe92988b 100644 --- a/docs.json +++ b/docs.json @@ -87,6 +87,7 @@ "docs/internals/sentry-setup", "docs/internals/hidden-features", "docs/internals/ant-only-world", + "docs/internals/session-transcript-persistence", "docs/features/debug-mode", "docs/features/buddy" ] diff --git a/docs/agent/coordinator-and-swarm.mdx b/docs/agent/coordinator-and-swarm.mdx index da15c98a2..db269a10c 100644 --- a/docs/agent/coordinator-and-swarm.mdx +++ b/docs/agent/coordinator-and-swarm.mdx @@ -1,86 +1,216 @@ --- -title: "协调者与蜂群模式 - 多 Agent 高级编排" -description: "从源码角度解析 Claude Code 多 Agent 协作:Coordinator Mode 的 System Prompt 设计、Worker 生命周期、Task 通信协议和 Swarm 蜂群的任务分配机制。" -keywords: ["协调者模式", "蜂群模式", "Agent Swarm", "多 Agent 协作", "任务编排"] +title: "协调者与蜂群模式:多 Agent 编排机制" +description: "从源码角度拆解 Claude Code 的 Coordinator Mode、Agent Teams / Swarm、subagent、teammate、Mailbox、Task 工具、runtime task、状态恢复与排障路径。" +keywords: ["协调者模式", "蜂群模式", "Agent Swarm", "Agent Teams", "多 Agent 协作", "任务编排", "Mailbox", "Subagent"] --- -{/* 本章目标:从源码角度揭示 Coordinator Mode 和 Agent Swarms 的架构设计 */} +Claude Code 里有很多看起来都叫“多 Agent”的东西:`Agent` 工具、fork agent、Coordinator Mode、Agent Teams / Swarm、remote agent、后台 runtime task、`TaskCreate` 任务白板。它们共享部分底层设施,但不是同一个抽象。 -## 两种协作模式的架构差异 +这篇文档解决的是跨机制理解问题:当你看到一个任务被“派出去”、一个 teammate 变成 idle、一个 `` 回到主线程、一个 team 目录还在但 teammate 不跑了,应该知道它属于哪套机制、状态放在哪里、通信走哪条路、哪些东西能恢复。 -| 维度 | Coordinator Mode | Agent Swarms | -|------|-----------------|--------------| -| **门控** | `feature('COORDINATOR_MODE')` + `CLAUDE_CODE_COORDINATOR_MODE=1` | `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` 环境变量 | -| **拓扑** | 星型:Coordinator 居中,Worker 外围 | 星型+P2P 混合:Team Lead 协调,Teammate 间可直接通信 | -| **角色** | 明确分工:Coordinator 编排、Worker 执行 | Team Lead 协调 + Teammate 自主认领任务 | -| **通信** | `SendMessage` 定向通信 + `` | Mailbox 消息系统(message / broadcast) | -| **适用** | 需要集中决策的复杂任务 | 并行度高、需要 Teammate 间直接协作的任务 | +## 全局心智模型 -两者不是互斥的——理论上 Coordinator Mode 可以在 Agent Teams 架构之上运行(概念层叠加,非嵌套团队),将 Coordinator 作为特殊的 Team Lead,但这部分集成(`workerAgent.ts` 中的 `getCoordinatorAgents`)目前为 stub 实现,尚未完整落地。 +最短心智模型是: -## Coordinator Mode:星型编排架构 - -### 激活机制 - -```typescript -// src/coordinator/coordinatorMode.ts:36 -export function isCoordinatorMode(): boolean { - if (feature('COORDINATOR_MODE')) { - return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE) - } - return false // 外部构建始终 false -} +```text +Agent 是派人干活。 +TaskCreate 是往白板上贴任务卡。 +Runtime Task 是正在跑的人或远端人影。 +Coordinator 是星型编排器。 +Swarm 是有成员、有邮箱、有任务白板的团队。 ``` -Coordinator Mode 需要双重门控:构建时 `feature('COORDINATOR_MODE')` 和运行时环境变量。`matchSessionMode()` 在会话恢复时自动同步模式状态——如果恢复的会话是 coordinator 模式,它会翻转环境变量以确保一致性。 +先把几个词压平: -### Coordinator 的工具集 +| 概念 | 本质 | 入口 | 状态位置 | 结果回路 | +|---|---|---|---|---| +| 普通 sync subagent | 一次性前台 `Agent` tool call | `Agent({ subagent_type })` | foreground `LocalAgentTask` | 当前 turn 的 `tool_result` | +| 普通 async subagent | 一次性后台 agent | `Agent({ subagent_type, async: true })` 或自动后台化 | `AppState.tasks` + sidechain | `async_launched` + `` | +| fork agent | 继承父上下文和 exact tools 的后台分支 | 省略 `subagent_type` 且 fork gate 满足 | `LocalAgentTask` + `.meta.json` | `` | +| coordinator worker | Coordinator 派出的 `worker` async subagent | Coordinator 调 `Agent({ subagent_type: "worker" })` | `LocalAgentTask` | `` + `SendMessage(to: agentId)` | +| swarm teammate | 长生命周期团队成员 | `Agent({ name, team_name?, prompt })` | `InProcessTeammateTask` 或 pane member | mailbox by name,可 idle 后继续 | +| remote agent | 远端执行体的本地镜像 | `Agent(..., isolation: "remote")` | `RemoteAgentTask` + remote sidecar | CCR events / polling | +| work item task | 共享任务白板条目 | `TaskCreate/Update/List/Get` | `~/.claude/tasks//*.json` | teammate / lead 认领和更新 | +| runtime task | 正在运行或曾运行的后台执行体 | agent、shell、workflow、remote 等入口 | `AppState.tasks` | UI、spinner、resume、kill | -Coordinator 被剥夺了所有"动手"工具,只保留编排能力: +## 系统分层 -| 工具 | 用途 | -|------|------| -| **Agent** | 启动新 Worker(`subagent_type: "worker"`) | -| **SendMessage** | 向已有 Worker 发送后续指令 | -| **TaskStop** | 中途停止走错方向的 Worker | -| **subscribe_pr_activity** | 订阅 GitHub PR 事件(review comments、CI 结果) | +多 Agent 系统可以看成五层,每层回答一个问题: -Coordinator **不写代码、不读文件、不执行命令**——它的核心职责是:理解需求、分配任务、综合结果,以及在无需工具时直接回答用户问题。 +| 层 | 回答的问题 | 典型对象 | +|---|---|---| +| 入口层 | 用户或模型通过什么工具启动动作 | `/coordinator`、`AgentTool`、`TeamCreate`、`SendMessage`、`TaskUpdate` | +| 编排层 | 谁负责拆解、派发、控制和综合 | Coordinator、Team Lead、AgentTool routing | +| 运行层 | 谁真正执行或代表执行状态 | `LocalAgentTask`、`InProcessTeammateTask`、`RemoteAgentTask` | +| 通信层 | 结果和控制信号如何回流 | `tool_result`、``、mailbox、CCR events | +| 持久化层 | 进程重启后还能看见什么 | session JSONL、sidechain、team config、task files、inbox、sidecar meta | -### Worker 的工具权限 - -Worker 的可用工具由 `getCoordinatorUserContext()`(`coordinatorMode.ts:80`)动态注入到 System Prompt: - -```typescript -// 简化模式下:只有 Bash + Read + Edit -const workerTools = isEnvTruthy(process.env.CLAUDE_CODE_SIMPLE) - ? [BASH_TOOL_NAME, FILE_READ_TOOL_NAME, FILE_EDIT_TOOL_NAME] - : Array.from(ASYNC_AGENT_ALLOWED_TOOLS) - .filter(name => !INTERNAL_WORKER_TOOLS.has(name)) +```mermaid +flowchart TD + A["入口层
slash command / AgentTool / Team tools / SendMessage"] --> B["编排层
Coordinator / Team Lead / AgentTool routing"] + B --> C["运行层
LocalAgentTask / RemoteAgentTask / InProcessTeammateTask"] + C --> D["通信层
tool_result / task-notification / mailbox / CCR events"] + D --> E["持久化层
session JSONL / sidechain / team config / tasks / inboxes / sidecar meta"] ``` -`INTERNAL_WORKER_TOOLS`(TeamCreate、TeamDelete、SendMessage、SyntheticOutput)被显式排除——Worker 不能嵌套创建团队或发送消息,防止不可控的递归。 +这五层不是一一对应关系。Coordinator worker 在运行层是 `LocalAgentTask`,通信层靠 `` 和 `SendMessage(to: agentId)`;Swarm teammate 在运行层可能是 `InProcessTeammateTask`,通信层靠 mailbox;remote agent 在运行层是本地 `RemoteAgentTask` 镜像,真实执行状态来自 CCR。 -### Scratchpad:跨 Worker 的共享知识库 +## 什么时候用哪套机制 -当 `isScratchpadGateEnabled()`(内部检查 `tengu_scratch` feature gate)启用时,Workers 获得一个 Scratchpad 目录,Coordinator 通过其系统上下文知晓该目录的存在: +| 场景 | 推荐机制 | 为什么 | +|---|---|---| +| 需要一个主脑拆解、派发、综合、纠偏 | Coordinator Mode | 主线程被限制为编排器,减少直接上手乱改。 | +| 多个任务相对独立,需要长期队友持续领任务 | Agent Teams / Swarm | 有 team config、mailbox、shared task list。 | +| 只想派一个专家研究或修改 | 普通 subagent | 成本低、模型路径短、结果直接回当前 turn 或后台通知。 | +| 想复制当前上下文做并行探索 | fork agent | 继承父上下文和 exact tools,适合分支探索。 | +| 想把工作放到远端环境执行 | remote agent | 本地只保留 `RemoteAgentTask` 镜像,执行在 CCR。 | -``` -Scratchpad 目录: - - Workers 可自由读写,无需权限审批 - - 用于持久化的跨 Worker 知识 - - 结构由 Coordinator 决定(无固定格式) +两个常见误判: + +| 误判 | 更好的选择 | +|---|---| +| “我要并行,所以一定用 Swarm” | 如果只是一次性研究/验证,用 async subagent 或 Coordinator worker 更轻。 | +| “我要团队,所以 Coordinator 就够了” | 如果需要成员持续认领共享任务、互相发消息、保留 team 状态,用 Swarm。 | + +## 两种多 Agent 拓扑 + +Coordinator 和 Swarm 都是多 Agent,但控制权和状态模型完全不同。 + +```mermaid +flowchart LR + subgraph CoordinatorMode["Coordinator Mode"] + U1["用户"] --> C["Coordinator 主 Claude"] + C -->|Agent worker| W1["worker A
LocalAgentTask"] + C -->|Agent worker| W2["worker B
LocalAgentTask"] + W1 -->|task-notification| C + W2 -->|task-notification| C + C -->|SendMessage to agentId| W1 + end + + subgraph SwarmMode["Agent Teams / Swarm"] + U2["用户"] --> L["Team Lead"] + L --> TF["TeamFile config.json"] + L --> TB["Shared TaskList"] + L -->|Agent name| T1["teammate researcher"] + L -->|Agent name| T2["teammate tester"] + T1 <--> M1["Mailbox inbox JSON"] + T2 <--> M2["Mailbox inbox JSON"] + T1 --> TB + T2 --> TB + end ``` -这是一个关键的协作原语——Worker A 的研究结果可以写入 Scratchpad,Worker B 直接读取,无需通过 Coordinator 中转。 +| 维度 | Coordinator Mode | Agent Teams / Swarm | +|---|---|---| +| 拓扑 | 星型:Coordinator 居中,worker 外围 | 团队型:Team Lead + named teammates + mailbox + task list | +| 主 Claude 角色 | 只编排,不直接执行 | 可以直接执行,也可以作为 team lead 管理团队 | +| 执行者 | built-in `worker` async subagent | teammate,可能是 in-process,也可能是 pane-based | +| 通信方式 | ``,必要时 `SendMessage(to: agentId)` | mailbox by name,支持 P2P、broadcast、structured protocol | +| 任务协作 | 不以 `TeamCreate/TaskList` 为核心 | `TeamFile` + shared task list + mailbox | +| 恢复模型 | mode 在主 transcript,worker 是 local agent sidechain | team/task/inbox 文件可保留;in-process runner 不完整恢复 | -### `` 通信协议 +Coordinator Mode 不是 Swarm 的特殊 Team Lead。它共享 `AgentTool`、`LocalAgentTask`、`SendMessage` 等设施,但不使用 `TeamCreate/TeamDelete/TaskList/TaskUpdate` 作为核心团队协作机制。 -Worker 完成后,Coordinator 收到 XML 格式的通知: +## Coordinator Mode 五段状态机 + +Coordinator Mode 的核心设计是把主 Claude 降级为编排器:主线程不直接 `Read/Edit/Bash`,而是拆任务、派 worker、综合结果、必要时停止或继续 worker。 + +### 1. 启用状态机 + +```mermaid +flowchart TD + A["feature COORDINATOR_MODE?"] -->|no| B["Coordinator unavailable"] + A -->|yes| C["/coordinator command"] + C --> D{"target mode?"} + D -->|enable| E["set CLAUDE_CODE_COORDINATOR_MODE=1"] + D -->|disable| F["delete CLAUDE_CODE_COORDINATOR_MODE"] + E --> G["save mode metadata"] + F --> G + G --> H["inject mode reminder"] +``` + +两层条件都满足才算进入 Coordinator: + +| 条件 | 作用 | +|---|---| +| `feature("COORDINATOR_MODE")` | 构建/运行 feature gate。 | +| `CLAUDE_CODE_COORDINATOR_MODE=1` | 当前进程实际进入 coordinator。 | + +### 2. 恢复状态机 + +Coordinator mode 是会话属性,写在主 session JSONL 的 `mode` entry 中: + +```jsonl +{"type":"mode","sessionId":"...","mode":"coordinator"} +``` + +resume 时会把当前环境和 transcript 中的 mode 对齐: + +```mermaid +flowchart TD + A["load transcript mode metadata"] --> B{"env matches transcript mode?"} + B -->|yes| C["continue"] + B -->|no, transcript=coordinator| D["set CLAUDE_CODE_COORDINATOR_MODE=1"] + B -->|no, transcript=normal| E["delete CLAUDE_CODE_COORDINATOR_MODE"] + D --> F["emit warning + refresh agent definitions"] + E --> F +``` + +这避免用户在 normal 环境恢复 coordinator 会话,或反过来把普通会话误当 coordinator 运行。 + +### 3. Prompt 状态机 + +Coordinator prompt 不是只看 env。交互 REPL 侧大致优先级是: + +| 优先级 | 来源 | 说明 | +|---|---|---| +| 1 | override system prompt | 最高优先级。 | +| 2 | coordinator prompt | `isCoordinatorMode()` 且没有 `mainThreadAgentDefinition` 时使用。 | +| 3 | main-thread agent prompt | `--agent` / settings agent。 | +| 4 | custom/default prompt | 普通主线程 prompt。 | +| 5 | append prompt | 追加型补充。 | + +风险点是 `--agent` 和 Coordinator 混用:可能出现工具池已经按 coordinator 过滤,但 system prompt 不是 coordinator 的不一致。 + +Headless 也要单独看。当前 headless 路径明确做了 coordinator 工具过滤,并注入 coordinator user context;但 system prompt 组装路径和交互 REPL 不完全相同,应把它当成需要复核的边界,而不是默认等同交互路径。 + +### 4. 工具过滤状态机 + +Coordinator 主线程和 worker 的工具池不同: + +| 角色 | 工具池 | 设计目的 | +|---|---|---| +| Coordinator 主线程 | `Agent`、`SendMessage`、`TaskStop`、`SyntheticOutput`、PR activity 订阅类 MCP 工具 | 只编排,不直接执行。 | +| worker | `ASYNC_AGENT_ALLOWED_TOOLS`,排除 `TeamCreate`、`TeamDelete`、`SendMessage`、`SyntheticOutput` | 执行任务,但不能继续嵌套编排。 | +| simple mode worker | `Bash`、`Read`、`Edit` | 降低工具面,适合简单执行路径。 | +| MCP 工具 | 按已连接 server 注入 worker context | 让 worker 能使用外部能力,但由工具池控制边界。 | +| scratchpad | gate 开启时提供 scratchpad 目录 | 允许跨 worker 共享临时知识。 | + +交互路径主要走 `mergeAndFilterTools()`;headless 路径会在主入口直接应用 coordinator 工具过滤;worker 工具池由 `AgentTool` 独立组装,不继承主线程被过滤后的工具池。 + +### 5. Worker lifecycle + +Coordinator 下 `Agent(worker)` 会被强制异步: + +```mermaid +flowchart TD + A["Coordinator calls Agent(worker)"] --> B["AgentTool marks shouldRunAsync"] + B --> C["registerAsyncAgent"] + C --> D["runAsyncAgentLifecycle"] + D --> E{"final status"} + E -->|completed| F["enqueue completed task-notification"] + E -->|failed| G["enqueue failed task-notification"] + E -->|killed| H["enqueue killed task-notification"] + F --> I["command queue injects into next turn"] + G --> I + H --> I +``` + +`` 是 user-role message,但不是用户输入。Coordinator prompt 必须把它当成 worker 结果信号: ```xml - agent-a1b ← Worker 的 agentId + agent-a1b completed|failed|killed Agent "Investigate auth bug" completed Found null pointer in src/auth/validate.ts:42... @@ -92,160 +222,430 @@ Worker 完成后,Coordinator 收到 XML 格式的通知: ``` -通知以 `user-role message` 形式送达,Coordinator 通过 `` 标签区分它和用户消息。`` 用于 `SendMessage` 的 `to` 参数,实现定向续传。 +Coordinator 的关键约束是“综合而不是转发”。worker 看不到用户和 coordinator 的完整对话,所以 prompt 必须自包含: -### Coordinator 的核心职责:综合(Synthesis) - -Coordinator System Prompt(`coordinatorMode.ts:111-369`,约 260 行)明确要求 Coordinator **不能懒惰地委派理解**: - -``` -反模式(禁止): - "Based on your findings, fix the auth bug" - → 把理解的责任推给了 Worker - -正确做法: - "Fix the null pointer in src/auth/validate.ts:42. - The user field on Session (src/auth/types.ts:15) is - undefined when sessions expire but the token remains cached. - Add a null check before user.id access." - → Coordinator 自己理解了问题,给出精确指令 +```text +Fix the null pointer in src/auth/validate.ts:42. +Session.user can be undefined when the session expires but the token remains cached. +Add a null check before user.id access; if null, return 401 with "Session expired". +Run validate.test.ts and report the commit hash. ``` -这是 Coordinator Mode 最核心的设计约束:Coordinator 必须先理解,再分配。 +反模式是: -## Agent Teams (Swarm):蜂群式协作 - -Swarm 模式基于任务系统 V2(详见[任务管理](../tools/task-management.mdx)),核心机制是**共享任务列表 + 竞争认领 + Mailbox 消息系统**: - -### 团队初始化 - -``` -Team Lead 创建团队(TeamCreateTool) - ↓ -设置 teamName → setLeaderTeamName() - ↓ -所有 Teammate 自动获得相同的 taskListId - ↓ -Teammate 启动时: - 1. CLAUDE_CODE_TASK_LIST_ID 环境变量(显式覆盖) - 2. Teammate 上下文的 teamName(共享 Lead 的任务列表) - 3. CLAUDE_CODE_TEAM_NAME 环境变量 - 4. Lead 设置的 teamName - 5. getSessionId()(兜底) +```text +Based on your findings, fix it. ``` -多级优先级确保了 Team Lead 和所有 Teammate 指向同一个任务列表,无需额外协调。 +### Coordinator 边界与排错 -### 架构组件 +| 现象 | 可能原因 | 处理方式 | +|---|---|---| +| Coordinator 主线程不能读文件或跑命令 | 工具池被过滤,这是预期行为 | 派 `worker`,把文件、错误、验收标准写入 worker prompt。 | +| `--agent` 后 coordinator 行为不一致 | agent prompt 优先级压过 coordinator prompt,但工具仍可能被过滤 | 避免混用,或确认当前 system prompt 来源。 | +| worker 还在跑但方向错 | runtime task 仍是 `running` | 用 `TaskStop` 停止;会产生 `killed` notification。 | +| worker 完成但结论不够 | 已经结束的一次性 async agent | 更推荐 fresh worker;只有需要保留 sidechain 时才 `SendMessage` 续跑。 | +| `SendMessage` 失败 | 找不到 agent、缺 sidechain transcript、message 缺 `summary` | 查 agentId/name、sidechain `.jsonl/.meta.json`,plain text message 记得带 `summary`。 | +| coordinator 下没有 `worker` | non-interactive 下禁用了 built-in agents | 检查 `CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS`。 | -官方 Agent Teams 架构定义了四个核心组件: +## Swarm 完整状态机 -| 组件 | 角色 | -|------|------| -| **Team Lead** | 创建团队、分配任务、综合结果的主 Claude Code 会话 | -| **Teammate** | 独立的 Claude Code 实例,各自拥有独立的上下文窗口 | -| **Task List** | 共享的任务列表,Teammate 竞争认领和完成 | -| **Mailbox** | 消息系统,支持 Teammate 间直接通信 | +Swarm 的核心是团队,而不是一次 `Agent` 调用。`TeamCreate` 建 team,`Agent({ name })` 加 teammate,`TaskCreate/Update/List/Get` 提供任务白板,`SendMessage` 和 mailbox 提供通信与控制。 -### Mailbox 消息系统 +当前实现默认启用 Agent Teams;设置 `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS_DISABLED` 才会关闭。 -官方架构中的 Mailbox 是 Teammate 间通信的核心原语,支持两种消息模式(`broadcast` 模式来自源码推断,官方文档未明确细分): +### 团队生命周期 -| 模式 | 作用 | 场景 | -|------|------|------| -| **message** | 定向发送给指定 Teammate | 传递具体指令、请求协作 | -| **broadcast** | 广播给所有 Teammate | 全局通知、状态同步 | - -Mailbox 的关键特性: -- **自动投递**:消息自动送达目标 Teammate 的对话上下文 -- **空闲通知**(TeammateIdle):Teammate 完成当前任务进入空闲时,自动通过 Mailbox 通知 Team Lead -- **直接通信**:与 Coordinator Mode 不同,Teammate 之间可以直接通信,无需经过 Lead 中转 - -### Hook 事件 - -Agent Teams 提供三个关键 Hook 事件,用于在团队生命周期中注入自定义逻辑: - -| Hook | 触发时机 | 典型用途 | -|------|---------|---------| -| **TaskCreated** | 新任务添加到任务列表时 | 自动分配、优先级排序 | -| **TaskCompleted** | 任务标记为完成时 | 结果通知、依赖解锁 | -| **TeammateIdle** | Teammate 完成所有任务进入空闲时 | Lead 重新分配、动态扩缩容 | - -### 限制 - -当前 Agent Teams 实现的限制: -- **不支持嵌套团队**:Teammate 不能再创建子团队 -- **每 session 一个团队**:一个会话只能属于一个团队 -- **Lead 固定**:Team Lead 创建后不可更换 -- **不支持 in-process Teammate 的会话恢复**:进程重启后 in-process 类型 Teammate 的状态丢失 - -### 持久化存储 - -团队状态通过文件系统持久化,确保进程重启后可恢复: - -``` -~/.claude/teams/{team-name}/config.json ← 团队配置 -~/.claude/tasks/{team-name}/ ← 共享任务列表(文件锁保护) +```mermaid +flowchart TD + A["NoTeam"] -->|TeamCreate| B["TeamReady leader"] + B -->|AgentTool name + team| C["SpawnResolving"] + C --> D{"backend"} + D -->|in-process| E["InProcessTeammateTask registered"] + D -->|pane-based| F["terminal pane spawned"] + E --> G["TeamMemberRegistered"] + F --> G + G --> H["TeammateRunning"] + H -->|turn complete| I["IdleNotification"] + I --> J["TeammateIdle"] + J -->|mailbox message| H + J -->|unowned unblocked task| K["claim task + TaskUpdate in_progress"] + K --> H + H -->|shutdown_request| L["model approves or rejects"] + J -->|shutdown_request| L + L -->|approved| M["cleanup member / unassign task"] + L -->|rejected| J + B -->|TeamDelete| N["request active teammate shutdown"] + N --> O["wait optional wait_ms"] + O --> P["cleanup team dir / task dir / AppState"] + P --> A ``` -### 任务认领与竞争 +关键不变量: -`claimTask()` 是 Agent Teams 的核心并发原语: +| 不变量 | 含义 | +|---|---| +| roster 扁平 | teammate 内禁止再 spawn teammate,避免团队嵌套。 | +| mailbox 按 name 寻址 | inbox 路径是 `teamName + agentName`,不是 agentId。 | +| task list 是共享白板 | `TaskCreate` 只写 pending task,不启动执行体。 | +| shutdown 不是强杀 | shutdown request 会交给模型处理,approve 后才 graceful shutdown。 | +| TeamFile 是跨进程事实源 | `AppState.teamContext` 是 leader UI 的投影。 | -``` -Teammate A 调用 TaskList → 发现 task #3 是 pending -Teammate B 同时发现 task #3 是 pending - ↓ -两者同时尝试 TaskUpdate(task #3, {status: "in_progress"}) - ↓ -文件锁保证原子性: - - 第一个写入者获得 owner 锁定 - - 第二个写入者收到 already_claimed 错误 - ↓ -获得任务的 teammate 执行工作 - ↓ -完成后 TaskUpdate(task #3, {status: "completed"}) - → 依赖此任务的其他任务自动解锁 - → tool_result 提示 "Call TaskList to find your next task" +### 存储拓扑 + +Swarm 的核心状态在 `~/.claude/teams` 和 `~/.claude/tasks`: + +```text +~/.claude/ + teams/ + / + config.json + inboxes/ + .json + tasks/ + / + .highwatermark + 1.json + 2.json + ... ``` -### Teammate 的生命周期管理 +| 文件或结构 | 内容 | +|---|---| +| `TeamFile` | `name`、`leadAgentId`、`leadSessionId`、`hiddenPaneIds`、`teamAllowedPaths`、`members[]`。 | +| `TeamFile.members[]` | `agentId`、`name`、`agentType`、`model`、`color`、`backendType`、`isActive`、`mode`、`worktreePath`、`sessionId`。 | +| task JSON | `id`、`subject`、`description`、`activeForm`、`owner`、`status`、`blocks`、`blockedBy`、`metadata`。 | +| mailbox JSON | 普通消息、协议消息、已读状态、颜色和摘要等。 | -``` -Teammate 异常退出 - ↓ -unassignTeammateTasks() - → 扫描任务列表,找到 owner === teammateName 的未完成任务 - → 重置为 pending + owner=undefined - ↓ -Team Lead 感知途径: - 1. 任务状态变化(pending 重置)—— 通过共享任务列表 - 2. Mailbox 空闲通知(TeammateIdle hook)—— Teammate 停止时自动通知 Lead - ↓ -Team Lead 重新分配任务或创建新 Teammate +### TeamCreate 到 teammate 的链路 + +```mermaid +sequenceDiagram + participant L as TeamLead + participant TC as TeamCreate + participant TF as TeamFile + participant TL as TaskList + participant A as AgentTool + participant B as Backend + participant M as Mailbox + + L->>TC: create team + TC->>TF: write config with lead member + TC->>TL: reset task list + TC->>L: set leader team context + L->>A: Agent with teammate name + A->>B: spawn in-process or pane + B->>TF: append member + B->>M: write initial prompt if needed + B->>L: teammate spawned ``` -## 任务类型全景 +`TeamCreate` 不只是写 `config.json`。它还会注册 session cleanup、重置 team 对应 task list、设置 `leaderTeamName`,并把 leader 投影到 `AppState.teamContext`。 -支撑多 Agent 协作的是 7 种任务类型(`src/tasks/types.ts`): +`AgentTool` 遇到 `team_name/current teamContext + name` 时走 teammate spawn 分支,不走普通 `runAgent()`。`spawnTeammate()` 会解析 team、唯一化 name、选择 backend、更新 `AppState.teamContext.teammates`,再追加 `TeamFile.members`。 -| 任务类型 | 运行位置 | 状态管理 | 适用场景 | -|----------|---------|---------|---------| -| **LocalAgentTask** | 本地子进程 | `LocalAgentTaskState` | 标准子 Agent 任务 | -| **LocalShellTask** | 本地 shell | `LocalShellTaskState` | 后台 shell 命令 | -| **InProcessTeammateTask** | 同进程内 | `InProcessTeammateTaskState` | 轻量级进程内队友 | -| **RemoteAgentTask** | 远程服务器 | `RemoteAgentTaskState` | 分布式 Agent(CCR) | -| **DreamTask** | 后台静默 | `DreamTaskState` | 后台自主整理记忆 | -| **LocalWorkflowTask** | 本地 | `LocalWorkflowTaskState` | 工作流编排 | -| **MonitorMcpTask** | 本地 | `MonitorMcpTaskState` | MCP 监控任务 | +### in-process vs pane-based teammate -`InProcessTeammateTask` 与 `LocalAgentTask` 的关键差异:前者共享进程的内存空间和基础设施状态(如 MCP 连接池),但有独立的对话上下文和工具权限;后者是完全隔离的子进程,启动开销更大但更安全。 +| 维度 | in-process teammate | pane-based teammate | +|---|---|---| +| 运行位置 | leader 同进程 | 独立终端 pane / CLI 进程 | +| 启动方式 | 注册 `InProcessTeammateTask`,启动 `runInProcessTeammate()` | 创建 tmux / iTerm2 / Windows Terminal pane | +| 消息消费 | runner 自己约 500ms poll mailbox | leader / teammate 侧 `useInboxPoller()` 约 1s poll | +| 输入路径 | teammate view 输入进入 `pendingUserMessages` | 普通 mailbox prompt 进入 teammate 进程 | +| 处理优先级 | shutdown > team-lead message > peer message > unowned task claim | poller 按消息类型路由,空闲时自动开一轮 | +| UI | spinner tree、footer pills、detail dialog、teammate transcript view | footer TeamStatus、TeamsDialog、pane 状态 | +| 恢复 | runner、AbortController、pending queue 在内存,进程重启不能完整恢复 | pane 进程可能还在;leader 侧 backend map 不持久化,恢复是 best-effort | +| 删除 | 需要当前 AppState task / AbortController | 通过 backend 写 shutdown request,等待 teammate approve / cleanup | -## Coordinator vs Agent Teams 的选择 +## AgentTool 分流决策树 -| 场景 | 推荐模式 | 原因 | -|------|---------|------| -| "重构认证系统,需要多模块协调" | Coordinator | 需要集中决策,Worker 间有依赖 | -| "修复 10 个独立的 lint 警告" | Agent Teams | 任务独立,Teammate 可完全并行 | -| "研究方案 A 和方案 B,然后选一个实现" | Coordinator | 先并行研究,再集中决策 | -| "在大仓库中搜索所有 TODO 并分类" | Agent Teams | 无依赖,各自领任务即可 | +`AgentTool.call()` 是多 Agent 入口最复杂的分叉点。同一个 `Agent` 工具会根据参数和上下文走不同运行时: + +```mermaid +flowchart TD + A["AgentTool.call"] --> B{"name + team context?"} + B -->|yes| C["spawnTeammate"] + B -->|no| D{"isolation=remote?"} + D -->|yes| E["registerRemoteAgentTask"] + D -->|no| F{"fork route?"} + F -->|yes| G["register async LocalAgentTask as fork"] + F -->|no| H{"shouldRunAsync?"} + H -->|yes| I["register async LocalAgentTask"] + H -->|no| J["foreground LocalAgentTask + tool_result"] +``` + +| 路由 | 触发条件 | 结果 | +|---|---|---| +| teammate | 有 `name`,且存在 `team_name` 或当前 `teamContext` | `spawnTeammate()`,返回 `teammate_spawned`。 | +| remote | `isolation: "remote"` | 注册 `RemoteAgentTask`,本地保存 remote sidecar。 | +| fork | 省略 `subagent_type` 且 fork gate/上下文允许 | 强制后台 local agent,继承父上下文和 exact tools。 | +| async local | 显式 async、Coordinator worker、或自动后台条件满足 | 返回 `async_launched`,完成后注入 ``。 | +| sync local | 默认前台一次性 subagent | 当前 tool call 返回 `tool_result`。 | + +所以文档里不能把“Agent”写成一个单一概念:同一个工具入口下面至少有五条运行路径。 + +## 通信路径对照 + +多 Agent 的通信路径决定了结果是否进入当前 turn、是否持久化、能不能 resume。 + +| 通信路径 | 发送者 | 接收者 | 用途 | 持久化/恢复 | +|---|---|---|---|---| +| `tool_result` | sync subagent | 当前 assistant turn | 一次性前台结果 | 写入主 transcript。 | +| `` | async local agent / coordinator worker | 主线程下一 turn | 后台完成/失败/被杀通知 | 来自 `LocalAgentTask` lifecycle 和 sidechain。 | +| `SendMessage(to: agentId)` | Coordinator 或用户 | local agent task | 继续 running/stopped worker | running 时排队;stopped 时尝试 sidechain resume。 | +| `SendMessage(to: teammateName)` | lead / teammate | teammate mailbox | Swarm 普通通信 | 写 inbox JSON,按 name 寻址。 | +| `SendMessage(to: "*")` | lead / teammate | team members | Swarm broadcast | 写多个 inbox;structured message 不能 broadcast。 | +| structured mailbox protocol | lead / teammate / runtime | 特定 teammate 或 lead | permission、plan、shutdown、mode、task assignment | 保持 unread 给 poller 路由,不应被普通 attachment 吞掉。 | +| CCR events / polling | remote runtime | `RemoteAgentTask` | remote agent 状态和结果 | 本地 sidecar + 远端 session 状态。 | + +### SendMessage 路由 + +```mermaid +flowchart TD + A["SendMessage(to)"] --> B{"cross-session scheme?"} + B -->|yes| C["UDS / LAN / bridge plain text"] + B -->|no| D{"matches LocalAgentTask?"} + D -->|running| E["queuePendingMessage"] + D -->|stopped or evicted| F["resumeAgentBackground from sidechain"] + D -->|no| G{"to == * ?"} + G -->|yes| H["broadcast team mailbox"] + G -->|no| I{"structured protocol?"} + I -->|yes| J["write protocol message"] + I -->|no| K["write teammate mailbox"] +``` + +plain text `SendMessage` 要带 `summary`。structured message 不能 broadcast,也不能跨 `uds/bridge/tcp` session。单 session 下 teammate name 是裸 name,`to` 不应写成含 `@` 的跨域地址。 + +## Mailbox 协议表 + +Mailbox 路径是: + +```text +~/.claude/teams//inboxes/.json +``` + +它有 lock、原子 rename、大小上限和压缩策略: + +| 限制 | 值 | +|---|---| +| 单条 text | 64KB | +| mailbox 文件 | 4MB | +| retained bytes | 2MB | +| 普通 message 保留 | 最多 1000 条 | +| read message 保留 | 最多 200 条 | +| unread protocol message 保留 | 最多 2000 条 | + +协议消息不只是“聊天”: + +| 消息类型 | 典型发送者 | 典型接收者 | 消费者 | 是否应进入普通 LLM context | +|---|---|---|---|---| +| plain text | lead / teammate | teammate / lead | mailbox attachment 或 prompt handler | 是 | +| broadcast | lead / teammate | team members | mailbox attachment 或 prompt handler | 是 | +| `task_assignment` | `TaskUpdate` | new owner | teammate poller / runner | 通常作为任务触发,不应当成普通闲聊 | +| `permission_request/response` | teammate / lead | lead / teammate | `useInboxPoller` + permission UI queue | 否 | +| `sandbox_permission_request/response` | teammate / sandbox host | lead / teammate | permission sync | 否 | +| `plan_approval_request/response` | teammate / lead | lead / teammate | plan approval path | 否 | +| `shutdown_request/approved/rejected` | lead / teammate | teammate / lead | backend / runner / poller | 否 | +| `mode_set_request` | lead | teammate | permission mode sync | 否 | +| `team_permission_update` | lead | team members | permission sync | 否 | +| idle notification | teammate runner | lead | UI / lead poller | 通常否 | + +一个重要边界:mailbox attachment 只消费非结构化消息;结构化协议消息应保持 unread,交给 `useInboxPoller` 或 in-process runner 路由。否则权限、plan、shutdown 可能被当成普通上下文吞掉。 + +## Task 不是 Runtime Task + +`TaskCreate` 的 task 和 `LocalAgentTask` 的 task 是两套模型。 + +| 名称 | 源码类型 | 存储 | 状态 | 谁消费 | +|---|---|---|---|---| +| work item task | `src/utils/tasks.ts` 的 `Task` | `~/.claude/tasks//.json` | `pending/in_progress/completed` | Task tools、TaskList UI、teammate 认领 | +| runtime task | `TaskStateBase` 子类型 | `AppState.tasks`,部分有 sidecar/output | `running/completed/failed/killed` 等 | UI、spinner、background selector、kill/resume | + +共享任务生命周期: + +```mermaid +flowchart TD + A["TaskCreate"] --> B["pending task JSON"] + B --> C["TaskList"] + C --> D["Teammate chooses work"] + D --> E["TaskUpdate status=in_progress owner=me"] + E --> F["execute work"] + F --> G["TaskUpdate status=completed"] + G --> H["TaskCompleted hooks"] + G --> I["tool_result hints: call TaskList for next task"] +``` + +`TaskUpdate` 在 Swarm 下有增强: + +| 行为 | 说明 | +|---|---| +| teammate 标记 `in_progress` 且 owner 为空 | 自动把 owner 设为当前 teammate name。 | +| owner 变化 | 写 `task_assignment` 到新 owner mailbox。 | +| status -> `completed` | 执行 TaskCompleted hooks。 | +| teammate 完成任务 | tool result 追加提示:立刻 `TaskList` 找下一项。 | +| 主线程完成 3+ 任务且没有 verification | 在 feature gate 下追加 verification nudge。 | + +runtime task 类型包括: + +| 类型 | 运行位置 | 典型场景 | +|---|---|---| +| `LocalAgentTask` | 本地子 agent | 普通后台 agent、fork、coordinator worker。 | +| `InProcessTeammateTask` | 同进程 runner | in-process teammate。 | +| `RemoteAgentTask` | CCR remote session | remote agent。 | +| `LocalShellTask` | 本地 shell | 后台 shell。 | +| `LocalWorkflowTask` | 本地 workflow | workflow 编排。 | +| `DreamTask` | 后台静默 | memory dream。 | +| `MonitorMcpTask` | 本地监控 | MCP monitor。 | + +## 持久化与恢复矩阵 + +恢复能力取决于状态放在哪里。最重要的区别是:能看到状态不等于能继续运行。 + +| 机制 | 持久化 | resume 后能看到 | resume 后能继续跑 | 边界 | +|---|---|---|---|---| +| main session | 主 session JSONL | 对话链、metadata、mode | 是,按主会话恢复 | 受 compact/branch/leaf 影响。 | +| coordinator mode | 主 session JSONL 的 `mode` entry | 当前会话模式 | 是,`matchSessionMode()` 会切 env | prompt/tool 状态仍受当前启动参数影响。 | +| coordinator worker | local agent sidechain + `.meta.json` | agent task 身份和历史 | 通常可 `resumeAgentBackground()` | 缺 sidechain/meta 或工具定义变化会失败。 | +| ordinary/fork subagent | local agent sidechain + `.meta.json` | agent 历史 | 可恢复,fork 依赖 `agentType:"fork"` | fork 恢复需要 metadata 正确。 | +| remote agent | `remote-agents/remote-agent-.meta.json` + CCR | remote task 镜像 | 取决于 CCR session 状态 | 404/archive 会删除 sidecar。 | +| team config | `~/.claude/teams//config.json` | team/member roster | 不代表 teammate runner 还活 | `TeamFile` 是事实源,`AppState` 是投影。 | +| mailbox | `~/.claude/teams//inboxes/*.json` | 未读普通/协议消息 | 可继续投递 | structured message 需要 poller/runner 正确消费。 | +| shared tasks | `~/.claude/tasks//*.json` | task list / owner / status | 可继续认领/更新 | owner 可能指向已经不活跃的 teammate。 | +| in-process teammate runner | leader 进程内存 | 不能完整看到 runner 内态 | 不能完整跨进程恢复 | AbortController、pending queue、recent messages 都在内存。 | +| pane-based teammate | 外部 pane + transcript + team file | 可能仍可见 | best-effort | leader 侧 backend map 不持久化,active/kill 依赖 pane 状态。 | + +调试时可以按这个顺序问: + +1. 文件还在吗? +2. `AppState` 投影还在吗? +3. runtime task 还在 `running` 吗? +4. 通信通道还可用吗? +5. sidechain / inbox / remote sidecar 是否足够恢复? + +## 用户可见状态如何投影 + +UI 展示的是不同状态源的投影,不是单一真相。 + +| UI | 数据源 | 能说明什么 | 不能说明什么 | +|---|---|---|---| +| TaskListV2 | task files + `teamContext` | work item task、owner、状态 | owner 对应 teammate 一定还活。 | +| TeammateSpinnerTree | running in-process teammates | 当前 leader 进程内的 teammate 活动 | pane-based teammate 或历史 teammate 全部状态。 | +| TeammateSpinnerLine | `InProcessTeammateTaskState` | idle、approval、stopping、tool/token、最近消息 | 完整 transcript。 | +| BackgroundAgentSelector | backgrounded `LocalAgentTask` | 可选择的本地后台 agent | remote/shell/workflow/in-process teammate。 | +| agent transcript view | `viewingAgentTaskId` | local agent 或 in-process teammate 的可视化对话 | pane teammate 的完整外部进程状态。 | +| TeamsDialog / TeamStatus | `AppState.teamContext` + team file | 团队成员展示、管理、kill/shutdown/mode | runner 一定可恢复。 | + +pane-based team 主要通过 footer TeamStatus 和 TeamsDialog 管理:Enter 查看,`k` kill,`s` shutdown,`p` prune idle,Shift+Tab 切 permission mode。in-process teammate 的 transcript view 输入会进 `pendingUserMessages`,不是写 mailbox。 + +## 两条端到端场景 + +### 复杂 bug 用 Coordinator + +| 步骤 | 发生了什么 | 运行体 | 通信 | 持久化 | +|---|---|---|---|---| +| 1 | 用户提出复杂 bug | 主会话 | user message | main JSONL | +| 2 | Coordinator 拆成调查、实现、验证 | Coordinator 主线程 | `Agent(worker)` | main JSONL + task state | +| 3 | worker 异步执行 | `LocalAgentTask` | tool calls | sidechain JSONL | +| 4 | worker 完成 | `LocalAgentTask` | `` | notification queue / main turn | +| 5 | Coordinator 综合 root cause | 主线程 | assistant reasoning | main JSONL | +| 6 | 需要修正方向 | 同一个或新 worker | `SendMessage(to: agentId, summary, message)` 或 fresh `Agent` | sidechain / new sidechain | +| 7 | 汇总给用户 | 主线程 | assistant message | main JSONL | + +这个流程没有 `TeamCreate`,也不依赖 shared task list。 + +### 长期并行任务用 Swarm + +| 步骤 | 发生了什么 | 状态源 | 通信 | +|---|---|---|---| +| 1 | `TeamCreate({ team_name })` | `teams//config.json` + `tasks/` | tool result | +| 2 | `TaskCreate` 多个工作项 | task JSON | Task tools | +| 3 | `Agent({ name: "researcher" })` | TeamFile member + backend task/pane | initial prompt | +| 4 | teammate 认领任务 | task JSON owner/status | `TaskUpdate` | +| 5 | lead 发消息 | inbox JSON | `SendMessage(to: teammateName)` | +| 6 | teammate 完成一轮 | runner/poller 状态 | idle notification | +| 7 | teammate 继续领任务 | task list | `TaskList` / claim | +| 8 | `TeamDelete({ wait_ms })` | team/task dirs cleanup | shutdown request / response | + +这个流程里 team、task list 和 mailbox 是核心。teammate 输出不会自动给 lead;需要 `SendMessage` 或明确的协议消息。 + +## 失败与排障矩阵 + +| 现象 | 先查什么 | 常见原因 | 处理 | +|---|---|---|---| +| Coordinator worker 结果没回来 | `AppState.tasks[agentId]`、notification queue、sidechain | worker 仍 running、failed、被 killed、notification 尚未进入下一 turn | 等下一 turn;或看 sidechain / task status。 | +| `SendMessage(to: agentId)` 找不到 worker | agentId/name、sidechain `.jsonl/.meta.json` | agent 被 evict、metadata 缺失、传了 teammate name | 用正确 raw agentId;必要时新开 worker。 | +| `SendMessage(to: teammate)` 失败 | teamContext、team file、inbox path | teammate name 拼错、当前 session 无 team、用了含 `@` 地址 | 用当前 team 内裸 teammate name。 | +| plain text `SendMessage` 校验失败 | 参数 | 缺 `summary` | 补 `summary`。 | +| structured message 没生效 | inbox read 状态、poller | 被当普通 attachment 标 read,或 consumer 没跑 | 确认 structured message 保持 unread,poller/runner 活着。 | +| 任务不显示 | `leaderTeamName`、`getTaskListId()`、tasks dir | lead/teammate 指向不同 task list | 查 env/teamName/sessionId 优先级。 | +| task 被认领但没人执行 | task owner、team member active、runner/pane | owner teammate 不活跃或 runner 丢失 | 重新分配 owner,或重启 teammate。 | +| TeamDelete 拒绝清理 | `TeamFile.members[].isActive` | 仍有 active teammate | 先 graceful shutdown,或确认后手动清理。 | +| resume 后 team 在但 teammate 不跑 | team file、runner/pane 状态 | in-process runner 在旧进程内,不能恢复 | 重新 spawn teammate 或用现有 mailbox/task 重新编排。 | +| pane teammate 似乎还在但 UI 不准 | paneId、backendType、backend map | leader 侧 `spawnedTeammates` map 不持久化 | 以 TeamFile + pane 实际状态为准,best-effort 管理。 | +| permission/plan 卡住 | leader inbox、permission UI queue、protocol response | leader poller 没消费,或 response 没写回 | 查 `useInboxPoller` 和对应 inbox。 | +| remote agent resume 失败 | remote sidecar、CCR session | session 404 / archived | 接受 sidecar 清理,重新创建 remote agent。 | + +## 常见误区 + +| 误区 | 正确理解 | +|---|---| +| Coordinator 就是 Swarm 的 Team Lead | 不是。Coordinator worker 是 async subagent,不是 teammate。 | +| Swarm 必须设置 `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1` | 当前实现默认启用;用 `CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS_DISABLED` 关闭。 | +| `TaskCreate` 创建了一个运行中的 agent | 它只创建 work item JSON;运行体是 `LocalAgentTask` / `InProcessTeammateTask` 等。 | +| teammate 完成一轮后结果自动给 lead | 不一定。teammate 需要通过 `SendMessage` 沟通;runner 也会发送 idle notification。 | +| mailbox 按 agentId 寻址 | Swarm mailbox 按 teammate name 寻址。 | +| BackgroundAgentSelector 会列出所有后台任务 | 它只列 backgrounded `LocalAgentTask`,不列 remote/shell/workflow/in-process teammate。 | +| `TeamUpdate` 是一个工具 | 当前源码没有独立 `TeamUpdateTool`;团队成员更新分散在 spawn、teamHelpers、dialogs 中。 | +| `SyntheticOutput` 是 Swarm 内部通信工具 | 它主要用于结构化输出,不是 Team 协作核心。 | +| shutdown request 是强杀 | 不是,它是模型处理的 graceful shutdown 协议。 | +| in-process teammate 可以像 local agent 一样跨进程 resume | 不行,runner 运行态在内存中,进程重启后不能完整恢复。 | + +## 延伸阅读 + +这篇文档是跨机制总览。需要深入某条链路时,优先看专题文档: + +| 想深入 | 阅读 | +|---|---| +| `AgentTool` 参数、sync/async/fork、通知队列 | `docs/agent/sub-agents.mdx` | +| Task V2 数据模型、锁、高水位、owner、hooks | `docs/tools/task-management.mdx` | +| JSONL transcript、sidechain、compact、resume、remote sidecar | `docs/internals/session-transcript-persistence.md` | +| Coordinator feature 的单独说明 | `docs/features/coordinator-mode.md` | +| worktree 隔离 | `docs/agent/worktree-isolation.mdx` | + +## 源码入口索引 + +| 问题 | 从这里看 | +|---|---| +| coordinator mode 检测、恢复、prompt、context | `src/coordinator/coordinatorMode.ts` | +| `/coordinator` 命令 | `src/commands/coordinator.ts` | +| coordinator worker 定义 | `src/coordinator/workerAgent.ts` | +| system prompt 选择 | `src/utils/systemPrompt.ts` | +| coordinator 工具过滤 | `src/utils/toolPool.ts` | +| coordinator mode 持久化 | `src/utils/sessionStorage.ts` 的 `mode` entry / `saveMode()` | +| AgentTool 路由 | `packages/builtin-tools/src/tools/AgentTool/AgentTool.tsx` | +| subagent query loop | `packages/builtin-tools/src/tools/AgentTool/runAgent.ts` | +| async local agent lifecycle | `packages/builtin-tools/src/tools/AgentTool/agentToolUtils.ts` | +| local agent runtime task | `src/tasks/LocalAgentTask/LocalAgentTask.tsx` | +| remote agent runtime task | `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx` | +| agent resume | `packages/builtin-tools/src/tools/AgentTool/resumeAgent.ts` | +| task stop | `packages/builtin-tools/src/tools/TaskStopTool/TaskStopTool.ts`、`src/tasks/stopTask.ts` | +| team gate | `src/utils/agentSwarmsEnabled.ts` | +| team file helpers | `src/utils/swarm/teamHelpers.ts` | +| TeamCreate | `packages/builtin-tools/src/tools/TeamCreateTool/TeamCreateTool.ts` | +| TeamDelete | `packages/builtin-tools/src/tools/TeamDeleteTool/TeamDeleteTool.ts` | +| spawn teammate | `packages/builtin-tools/src/tools/shared/spawnMultiAgent.ts` | +| in-process teammate spawn | `src/utils/swarm/spawnInProcess.ts` | +| in-process teammate runner | `src/utils/swarm/inProcessRunner.ts` | +| pane backend | `src/utils/swarm/backends/PaneBackendExecutor.ts` | +| teammate AsyncLocalStorage identity | `src/utils/teammateContext.ts` | +| mailbox | `src/utils/teammateMailbox.ts` | +| permission sync | `src/utils/swarm/permissionSync.ts` | +| SendMessage routing | `packages/builtin-tools/src/tools/SendMessageTool/SendMessageTool.ts` | +| shared task list | `src/utils/tasks.ts` | +| Task tools | `packages/builtin-tools/src/tools/TaskCreateTool`、`TaskUpdateTool`、`TaskListTool`、`TaskGetTool` | +| inbox polling | `src/hooks/useInboxPoller.ts` | +| swarm initialization | `src/hooks/useSwarmInitialization.ts` | +| teammate view | `src/state/teammateViewHelpers.ts`、`src/screens/REPL.tsx` | +| teammate spinner | `src/components/Spinner/TeammateSpinnerTree.tsx`、`TeammateSpinnerLine.tsx` | +| team dialog/status | `src/components/teams/TeamsDialog.tsx`、`src/components/teams/TeamStatus.tsx` | +| background local agent selector | `src/hooks/useBackgroundAgentTasks.ts`、`src/components/tasks/BackgroundAgentSelector.tsx` | diff --git a/docs/internals/session-transcript-persistence.md b/docs/internals/session-transcript-persistence.md new file mode 100644 index 000000000..9ed0c386c --- /dev/null +++ b/docs/internals/session-transcript-persistence.md @@ -0,0 +1,828 @@ +# JSONL Transcript 会话持久化与恢复机制 + +本文梳理 Claude Code 基于 JSONL transcript 的会话持久化、恢复、错误恢复、上下文压缩、分支、subagent、fork agent 和 remote agent 逻辑。 + +这不是按文件罗列的源码笔记,而是一份机制手册:先建立心智模型,再看数据结构、生命周期、异常路径和源码入口。 + +## 怎么读 + +| 如果你想看 | 建议先读 | +|---|---| +| 为什么 resume 能恢复到正确位置 | `总览`、`读取与链路重建`、`恢复入口` | +| 为什么 compact 后历史还在但模型看不到 | `上下文视图`、`Compact 与投影` | +| 为什么 subagent 不污染主会话 | `存储拓扑`、`Subagent 与 Fork Agent` | +| `/branch`、`--fork-session`、`/fork` 有什么区别 | `分支与 Fork 对比` | +| 崩溃、超限、取消后如何恢复 | `错误恢复矩阵` | + +## 总览 + +Claude Code 的本地会话核心是 append-only JSONL。每一行是一个 `Entry`,但恢复时不会按文件顺序重放整个文件,而是: + +1. 把 transcript message 放入 `uuid -> message` map。 +2. 把 metadata entry 放入各自 map 或数组。 +3. 选择最新 leaf。 +4. 从 leaf 沿 `parentUuid` 回溯,得到当前有效链。 +5. 应用 compact、snip、preserved segment、content replacement 等投影。 +6. 恢复 sessionId、worktree、mode、agent setting、任务状态等内存状态。 + +核心不变量: + +| 不变量 | 含义 | +|---|---| +| JSONL 尽量 append-only | compact、branch、sidechain 都优先追加新 entry,不直接改旧历史。 | +| `uuid/parentUuid` 决定世界线 | 文件顺序只说明写入顺序,真正恢复靠链路回溯。 | +| metadata 不参与主链 | title、tag、worktree、content replacement 等通过 sessionId/messageId/agentId 合并。 | +| compact 不删除历史 | 它追加 boundary,模型视图从最后一个 boundary 后开始。 | +| subagent 是 sidechain | 子 agent 的完整对话在独立 JSONL,父会话只看到 Agent tool 的结果/通知。 | +| remote agent 不是 sidechain | remote agent 本地只保存 sidecar 身份,执行状态来自 CCR。 | + +### 系统分层 + +```mermaid +flowchart TD + A[磁盘层
append-only JSONL + sidecar metadata] --> B[链路层
uuid / parentUuid / leaf] + B --> C[投影层
compact / snip / tool_result budget / context-collapse] + C --> D[恢复层
deserialize / interrupt detection / metadata restore] + D --> E[运行层
REPL / QueryEngine / AgentTask / RemoteTask] +``` + +### 存储拓扑 + +```text +~/.claude/projects// + .jsonl + / + subagents/ + agent-.jsonl + agent-.meta.json + / + agent-.jsonl + agent-.meta.json + remote-agents/ + remote-agent-.meta.json +``` + +| 文件 | 生成函数 | 用途 | +|---|---|---| +| `.jsonl` | `getTranscriptPath()` | 主会话 transcript。 | +| `subagents/agent-.jsonl` | `getAgentTranscriptPath(agentId)` | 本地 subagent / fork agent sidechain。 | +| `subagents/agent-.meta.json` | `getAgentMetadataPath(agentId)` | agentType、worktreePath、description。 | +| `remote-agents/remote-agent-.meta.json` | `getRemoteAgentMetadataPath(taskId)` | remote CCR session 身份,用于恢复 polling。 | + +## 核心源码地图 + +| 机制 | 主要文件 | +|---|---| +| Entry 类型 | `src/types/logs.ts` | +| 路径、写入、读取、链路重建 | `src/utils/sessionStorage.ts` | +| 大文件流式读取 | `src/utils/sessionStoragePortable.ts` | +| CLI resume 加载和中断检测 | `src/utils/conversationRecovery.ts` | +| session 切换和状态恢复 | `src/utils/sessionRestore.ts` | +| SDK/headless query 写 transcript | `src/QueryEngine.ts` | +| API query loop、compact、错误恢复 | `src/query.ts` | +| compact 实现 | `src/services/compact/*` | +| context-collapse stub 与持久化接口 | `src/services/contextCollapse/*` | +| `/branch` | `src/commands/branch/branch.ts` | +| `/fork` | `src/commands/fork/fork.tsx` | +| AgentTool 和 subagent | `packages/builtin-tools/src/tools/AgentTool/*` | +| 通用 forked side query | `src/utils/forkedAgent.ts` | +| remote agent task | `src/tasks/RemoteAgentTask/RemoteAgentTask.tsx` | + +## 数据模型 + +`Entry` 定义在 `src/types/logs.ts`,可以分为三大类。 + +| 类别 | 典型 type | 是否进入 `parentUuid` 链 | key | 恢复用途 | +|---|---|---:|---|---| +| transcript message | `user`、`assistant`、`attachment`、`system` | 是 | `uuid` | 重建对话链、模型上下文、UI scrollback。 | +| session metadata | `custom-title`、`tag`、`mode`、`worktree-state`、`pr-link`、`agent-setting` | 否 | `sessionId` | 恢复标题、标签、模式、worktree、PR、agent 设置。 | +| message metadata | `file-history-snapshot`、`attribution-snapshot`、`summary` | 否 | `messageId` 或 `leafUuid` | 恢复文件历史、归因、摘要。 | +| replacement metadata | `content-replacement` | 否 | `sessionId` + optional `agentId` | 恢复大 tool_result 的替换决策。 | +| context-collapse metadata | `marble-origami-commit`、`marble-origami-snapshot` | 否 | `sessionId` | 预留 context-collapse 恢复接口;当前实现为 stub。 | +| queue/task metadata | `queue-operation`、`task-summary`、`speculation-accept` | 否 | 各自字段 | 恢复队列、任务摘要、推测接受统计。 | + +### TranscriptMessage 字段 + +真正参与链路的是 `TranscriptMessage`: + +| 字段 | 含义 | +|---|---| +| `uuid` | 当前消息 ID。 | +| `parentUuid` | 链路父节点,恢复时沿它回溯。 | +| `logicalParentUuid` | compact boundary 等断链场景保留逻辑父节点。 | +| `sessionId` | 所属主 session。 | +| `cwd` | 写入时工作目录。 | +| `timestamp` | 写入时间。 | +| `version` | CLI 版本。 | +| `gitBranch` | 写入时 git 分支。 | +| `isSidechain` | 是否是 subagent sidechain。 | +| `agentId` | sidechain 所属 agent。 | +| `teamName/agentName/agentColor` | swarm / teammate 展示元数据。 | + +### JSONL 示例 + +主会话消息: + +```jsonl +{"type":"user","uuid":"u1","parentUuid":null,"sessionId":"s1","isSidechain":false,"cwd":"D:\\vibe\\claude-code","message":{"role":"user","content":"修复测试"}} +{"type":"assistant","uuid":"a1","parentUuid":"u1","sessionId":"s1","isSidechain":false,"message":{"role":"assistant","content":[{"type":"text","text":"我来检查。"}]}} +``` + +sidechain 消息: + +```jsonl +{"type":"user","uuid":"u2","parentUuid":null,"sessionId":"s1","isSidechain":true,"agentId":"ag1","message":{"role":"user","content":"分析 compact 路径"}} +``` + +agent 的 `content-replacement`: + +```jsonl +{"type":"content-replacement","sessionId":"s1","agentId":"ag1","replacements":[{"messageUuid":"u2","toolUseId":"toolu_...","blockIndex":0,"kind":"persisted"}]} +``` + +compact boundary: + +```jsonl +{"type":"system","subtype":"compact_boundary","uuid":"b1","parentUuid":"a9","logicalParentUuid":"a9","sessionId":"s1","compactMetadata":{"trigger":"auto","preTokens":182000,"messagesSummarized":94}} +``` + +## 写入生命周期 + +### 总流程 + +```mermaid +sequenceDiagram + participant User + participant QE as QueryEngine + participant SS as sessionStorage.Project + participant FS as JSONL + participant API as query()/API + + User->>QE: ask(messages) + QE->>SS: recordTranscript(user messages) + SS->>SS: clean + dedup + insertMessageChain + SS->>SS: appendEntry / enqueueWrite + SS-->>FS: drain queue append JSONL + QE->>API: start query loop + API-->>QE: assistant/user/system compact_boundary + QE->>SS: recordTranscript(streamed messages) + QE->>SS: flushSessionStorage before result when needed +``` + +关键点: + +| 设计 | 为什么 | +|---|---| +| 用户输入先写 transcript,再进 API | 进程在 API 前崩溃时,resume 仍能看到用户 prompt。 | +| assistant streaming 写入多为 fire-and-forget | 不阻塞 token streaming。 | +| result 前按需 flush | 避免 SDK/桌面端拿到 result 后立即杀进程导致尾部丢失。 | +| `progress` 不参与链路 | 高频 progress tick 不应该制造分叉或膨胀 transcript。 | + +### 主会话写入 + +入口:`recordTranscript(messages, teamInfo?, startingParentUuidHint?, allMessages?)`。 + +流程: + +1. `cleanMessagesForLogging()` 过滤 UI-only 或不应持久化的消息。 +2. `getSessionMessages(sessionId)` 读取当前 session 已有 UUID set。 +3. 对未写过的消息调用 `insertMessageChain()`。 +4. `insertMessageChain()` 补 `parentUuid/sessionId/cwd/timestamp/version/gitBranch/isSidechain`。 +5. `appendEntry()` 进入 per-file queue。 + +去重不是简单丢弃所有重复:如果 prefix 中某些消息已写过,写入器会推进 `startingParentUuid`,确保后续新消息接在正确父节点后。 + +### 写队列、materialize 和 flush + +`Project` 内部维护 per-file queue: + +| 机制 | 细节 | +|---|---| +| `writeQueues` | `Map`,按文件聚合写入。 | +| drain timer | 默认 100ms;CCR/remote persistence 场景约 10ms。 | +| queue 上限 | 单队列超过 1000 条会丢弃最老 queued entry 并 resolve,防止内存无限增长。 | +| chunk 上限 | 单次 JSONL append chunk 约 100MB。 | +| `flushSessionStorage()` | 取消 timer,等待 active drain 和 tracked writes。 | + +`sessionFile` 初始为 `null`。这时 title、tag、mode、worktree 等 metadata 先存在内存或 `pendingEntries` 中。第一次出现 `user` 或 `assistant` 时,`materializeSessionFile()` 才创建 session 文件,然后: + +1. 写入缓存 metadata。 +2. 回放 pending entries。 +3. 之后所有 entry 正常 append。 + +这样可以避免“只打开 CLI 没说话”也产生 metadata-only session,污染 `/resume` 列表。 + +### sidechain 写入 + +subagent 使用 `recordSidechainTranscript(messages, agentId, startingParentUuid?)`。 + +它底层仍走 `insertMessageChain()`,但写入字段不同: + +```ts +isSidechain: true +agentId: agentId +``` + +`appendEntry()` 遇到 `isSidechain && agentId` 的 transcript message,会把它路由到: + +```text +//subagents/agent-.jsonl +``` + +如果 `content-replacement` 带 `agentId`,也会路由到该 agent 的 sidechain JSONL,而不是主 session JSONL。 + +一个很重要的例外:sidechain 写入不会用主 session UUID set 做去重。fork agent 会复用父会话消息 UUID 来继承上下文;如果按主 session 去重,会把继承上下文从 sidechain 中误删,导致 agent resume 时只剩子 prompt。 + +## 读取与链路重建 + +### 从 JSONL 到有效链 + +```mermaid +flowchart TD + A[loadTranscriptFile(file)] --> B[readTranscriptForLoad
大文件按 chunk 读] + B --> C[parseJSONL Entry] + C --> D[messages Map uuid->TranscriptMessage] + C --> E[metadata maps/arrays] + D --> F[progress bridge / preserved relink / snip removal] + F --> G[select leaf] + G --> H[buildConversationChain] + H --> I[recoverOrphanedParallelToolResults] + I --> J[LogOption or agent transcript] +``` + +`loadTranscriptFile(filePath, opts?)` 产出: + +| 输出 | 用途 | +|---|---| +| `messages` | `uuid -> TranscriptMessage`。 | +| `leafUuids` | 候选 leaf。 | +| title/tag/mode/worktree/PR maps | session metadata。 | +| `fileHistorySnapshots` / `attributionSnapshots` | 文件状态恢复。 | +| `contentReplacements` | 主线程 replacement records。 | +| `agentContentReplacements` | `agentId -> replacement records`。 | +| `contextCollapseCommits` / `contextCollapseSnapshot` | context-collapse 恢复输入。 | + +### leaf 与 parent 链 + +`buildConversationChain(messages, leaf)`: + +1. 从 leaf 开始。 +2. 读取 `parentUuid`。 +3. 找到父消息并继续回溯。 +4. 检测 parent cycle,避免无限循环。 +5. reverse 成正序 transcript。 +6. 补回并行 tool_use 形成的 DAG 分支。 + +一个简化例子: + +```text +u1 <- a1 <- u2 <- a2 + ^ + leaf + +恢复链: a2 -> u2 -> a1 -> u1 +正序链: u1, a1, u2, a2 +``` + +文件顺序不等于有效链。branch、rewind、streaming fallback 都可能让 JSONL 里有死分支;恢复只选择当前 leaf 所在世界线。 + +### metadata 合并规则 + +| metadata | 合并方式 | 说明 | +|---|---|---| +| `custom-title`、`tag`、`mode`、`worktree-state`、`pr-link`、`agent-setting` | sessionId keyed,通常 last-wins | 恢复最新 session 状态。 | +| `file-history-snapshot`、`attribution-snapshot` | messageId keyed / array | 恢复文件历史与归因。 | +| `content-replacement` | append array | 多轮 replacement 决策都要保留。 | +| `agentContentReplacements` | agentId keyed + append array | agent resume 重建 sidechain replacement state。 | +| `marble-origami-commit` | ordered array | 顺序有语义,后一个 commit 可能引用前一个 summary。 | +| `marble-origami-snapshot` | last-wins | staged snapshot 只恢复最新状态。 | + +### 大文件读取优化 + +transcript 可增长到几百 MB 甚至 GB,读取路径有几层防护。 + +| 优化 | 位置 | 目的 | +|---|---|---| +| chunk 读取 | `readTranscriptForLoad()` | 避免一次性读爆内存。 | +| fd 层跳过大 metadata | `readTranscriptForLoad()` | `attribution-snapshot` 等大 entry 不进入 buffer。 | +| compact 前缀跳过 | `readTranscriptForLoad()` | 遇到非 preserved compact boundary 后,只保留 boundary 后内容。 | +| pre-boundary metadata scan | `scanPreBoundaryMetadata()` | compact 前被跳过时,仍保留 title/tag/mode/worktree/PR 等展示信息。 | +| byte-level dead branch 裁剪 | `walkChainBeforeParse()` | JSON.parse 前只拼 active chain 和 metadata,跳过 dead fork/rewind branch。 | +| lite read 限制 | `MAX_TRANSCRIPT_READ_BYTES` | 直接读 raw transcript 的调用超过约 50MB 要避开。 | + +`walkChainBeforeParse()` 只有预计能丢掉至少一半 buffer 时才做 concat,避免优化本身变成额外成本。 + +### preserved segment 与 snip + +compact boundary 可以带 `compactMetadata.preservedSegment`。恢复时 `applyPreservedSegmentRelinks()` 会: + +1. 验证 `tailUuid -> headUuid` 链是否完整。 +2. 把 preserved segment 的 head 接到 compact anchor 后。 +3. 把 anchor 的其他 children 接到 preserved tail。 +4. 删除最后一个 boundary 前且不属于 preserved segment 的旧消息。 +5. 清零 preserved assistant 的 usage,避免恢复后马上又触发 autocompact。 + +示意: + +```text +compact 前: old... -> anchor -> head -> ... -> tail -> next +compact 后: boundary/summary -> head -> ... -> tail -> next +``` + +`snip` 和 compact 不同:compact 截断前缀,snip 删除中段。JSONL 不能真的删除旧行,所以 `applySnipRemovals()` 在内存 map 中删除 `removedUuids`,再把 dangling `parentUuid` 重连到最近未删除祖先。 + +### 旧链路修复 + +| 问题 | 修复 | +|---|---| +| legacy `progress` 曾进入 parent 链 | `progressBridge` 把指向 progress 的 parent 改回 progress 的真实父节点。 | +| parent cycle | `buildConversationChain()` 检测 cycle,记录并返回 partial chain。 | +| 并行 tool_use 形成 DAG | `recoverOrphanedParallelToolResults()` 按 assistant `message.id` 和 tool_result parent 关系补回 sibling。 | +| streaming fallback 孤儿尾巴 | tombstone 触发 `removeTranscriptMessage(uuid)` 删除失败 attempt。 | + +## 恢复入口 + +### 入口矩阵 + +| 入口 | 加载源 | 是否复用原 sessionId | 是否 adopt 原 JSONL | 特点 | +|---|---|---:|---:|---| +| `--continue` | 当前目录最近 session | 是 | 是 | 跳过仍 live 的 bg/daemon 非 interactive session。 | +| `--resume ` | 指定 session | 是 | 是 | 也支持 custom title / 搜索词 / picker。 | +| `--resume ` | 指定 JSONL 文件 | 是 | 是 | Ant 内部/print path 支持。 | +| `--fork-session` + resume | 旧 session messages | 否 | 否 | 保持新 sessionId,把旧消息作为新 session 初始内容。 | +| `--resume-session-at ` | print/headless resume | 取决于 resume | 取决于 resume | 截断到指定 assistant message。 | +| REPL `/resume` | picker / log option | 是或 fork | 是或否 | 会跑 SessionEnd/SessionStart hooks,切换 UI state。 | + +### CLI resume 流程 + +```mermaid +flowchart TD + A[main.tsx --continue/--resume] --> B[loadConversationForResume] + B --> C[load log or transcript] + C --> D[deserializeMessagesWithInterruptDetection] + D --> E[processSessionStartHooks] + E --> F[processResumedConversation] + F --> G{fork session?} + G -- no --> H[switchSession + adoptResumedSessionFile] + G -- yes --> I[keep fresh sessionId + seed content replacement] + H --> J[restore mode/worktree/agent/context-collapse/cost] + I --> J + J --> K[start REPL or print] +``` + +核心函数: + +| 函数 | 责任 | +|---|---| +| `loadConversationForResume()` | 统一加载最近 session、sessionId、LogOption 或 JSONL path;补 lite log;复制 plan/file history;做 consistency check;反序列化和中断检测;返回 metadata。 | +| `processResumedConversation()` | CLI interactive 启动恢复;切换或 fork session;恢复 cost、worktree、mode、agent setting、context-collapse、attribution。 | +| `restoreSessionStateFromLog()` | 恢复 AppState 侧状态:file history、attribution、context-collapse、TodoWrite todos。 | + +### REPL `/resume` + +REPL 内 resume 比 CLI 启动路径多了“从当前 session 切换到另一个 session”的工作: + +1. 清理目标 log messages。 +2. 当前 session 跑 SessionEnd hooks。 +3. 目标 session 跑 SessionStart resume hooks。 +4. 保存当前 session cost,恢复目标 session cost。 +5. `switchSession(sessionId, dirname(fullPath))` 原子切换 sessionId + project dir。 +6. `resetSessionFilePointer()` 并恢复 metadata cache。 +7. 非 fork 时退出上一次 worktree,恢复目标 worktree,`adoptResumedSessionFile()`。 +8. fork 时不接管原 transcript,不退出当前 worktree。 +9. 重建 content replacement state。 +10. 恢复 remote/local task 状态。 +11. 替换 messages、清 tool JSX、清输入框。 + +### 中断检测矩阵 + +`deserializeMessagesWithInterruptDetection()` 会先清理历史消息: + +| 清理 | 目的 | +|---|---| +| legacy attachment 迁移 | 兼容旧 transcript。 | +| 非法 `permissionMode` 删除 | 防止跨 build 的无效枚举进入运行态。 | +| unresolved tool_use 过滤 | 避免 API 报 tool_use/tool_result 不配对。 | +| orphaned thinking-only assistant 过滤 | 避免中断 streaming 留下孤儿 thinking block。 | +| whitespace-only assistant 过滤 | 避免取消时留下空白 assistant。 | + +然后看最后一个 turn-relevant message: + +| 最后有效消息 | 结果 | 额外动作 | +|---|---|---| +| assistant | `none` | streaming 持久化里 stop_reason 常为 null,不能靠它判断未完成。 | +| 普通 user | `interrupted_prompt` | 插入 `NO_RESPONSE_REQUESTED` sentinel 保持 API-valid。 | +| meta user / compact summary user | `none` | 不把内部控制消息当用户新请求。 | +| tool_result user | 通常 `interrupted_turn` | 例外:Brief/SendUserMessage/SendUserFile terminal tool_result 视为完成。 | +| attachment | `interrupted_turn` | 追加 meta user:`Continue from where you left off.` | +| system/progress/API error assistant | 跳过 | 不作为 turn 完成判断依据。 | + +`interrupted_turn` 会统一转换为 `interrupted_prompt`,让上层只处理一种“需要续跑”的状态。 + +## 错误恢复矩阵 + +| 场景 | 处理策略 | transcript 影响 | +|---|---|---| +| API 前进程崩溃 | 用户 prompt 已由 `QueryEngine.ask()` 先写入。 | resume 看到普通 user,触发 `interrupted_prompt`。 | +| streaming fallback 产生孤儿 assistant | yield tombstone,REPL 移除 UI message 并调用 `removeTranscriptMessage(uuid)`。 | 优先只改 JSONL 尾部 64KB;大文件目标不在尾部时跳过慢 rewrite。 | +| prompt-too-long / media-too-large | streaming 阶段先 withheld;先 context-collapse drain,再 reactive compact;失败才暴露错误。 | compact 成功则写 boundary/summary 并重试;失败才写 API error message。 | +| max_output_tokens | 先提高 max output override;仍失败则注入内部 recovery prompt 续写;耗尽才暴露错误。 | 内部 retry prompt 不一定成为普通 transcript,取决于是否 yield 到外层。 | +| auto compact 关闭但到 blocking limit | 直接 yield prompt-too-long 风格 API error。 | 保留用户手动 `/compact` 空间。 | +| abort during streaming/tools | 补齐缺失 tool_result,必要时 yield user interruption message。 | `reason === interrupt` 时跳过 interruption message,因为后续 queued user message 已提供上下文。 | +| stop hook blocking | 把 hook blocking error 加入 state 后重试。 | 有 reactive compact guard,避免 hook/error/compact 无限循环。 | +| compact boundary 指向未落盘 tail | QueryEngine 写 boundary 前强制补写 preserved tail 前的消息。 | 避免恢复时 boundary 引用不存在 UUID。 | +| subagent transcript 尾部不完整 | `resumeAgentBackground()` 再次过滤 unresolved tool_use、orphan thinking、空白 assistant。 | 避免恢复 agent 后 API 请求非法。 | + +## 上下文视图 + +同一份消息在系统里有四种视图,不要混在一起: + +| 视图 | 内容 | 谁使用 | +|---|---|---| +| Raw transcript | JSONL 中所有 entry,包括旧历史、dead branch、metadata、sidechain。 | 磁盘持久化和审计。 | +| UI scrollback | REPL 当前展示的消息,可能保留 compact 前历史和 collapsed UI group。 | 终端 UI。 | +| Active query view | `getMessagesAfterCompactBoundary()` 后的消息,默认再投影 snip。 | `query.ts` 上下文管理。 | +| API wire view | `normalizeMessagesForAPI()` 后,过滤 system boundary、修复 tool pairing、插入 cache edits。 | Anthropic/OpenAI/Gemini 等 API client。 | + +每轮 query 的 active context 顺序: + +1. `getMessagesAfterCompactBoundary(messages)`:取最近 compact boundary 之后的 active slice,默认叠加 snip 投影。 +2. 删除旧 `toolUseResult` 原始 payload,只保留 API 需要的 `message.content`。 +3. `applyToolResultBudget()`:过大的 tool_result 替换为 preview/stub,并写 `content-replacement`。 +4. `snipCompactIfNeeded()`:`HISTORY_SNIP` 下删除中段历史。 +5. `microcompactMessages()`:time-based microcompact,再 cached microcompact。 +6. `contextCollapse.applyCollapsesIfNeeded()`:当前为 identity stub。 +7. `autoCompactIfNeeded()`:主动 compact,优先 session memory compact。 +8. predictive autocompact:API 前估算本 turn 增长,必要时提前 compact。 +9. API 真实超限后:context-collapse drain,再 reactive compact。 + +## Compact 与投影 + +### Compact 类型对比 + +| 类型 | 触发 | 摘要来源 | 是否调用 compact API | 是否保留尾段 | 失败策略 | +|---|---|---|---:|---:|---| +| manual compact | `/compact` | compact summary API 或 session memory | 取决于路径 | 取决于 full/partial/SM | 显示失败或回退传统 compact。 | +| auto compact | token 阈值 | 先 session memory,后 summary API | 取决于路径 | 取决于路径 | 连续失败 circuit breaker,默认 3 次后停止自动 compact。 | +| predictive compact | API 前估算增长 | 同 auto compact | 取决于路径 | 取决于路径 | 失败则继续原请求或走后续错误恢复。 | +| reactive compact | API 真实 413/media error 后 | `compactConversation()` | 是 | 当前 wrapper 取决于 compact 实现 | `hasAttemptedReactiveCompact` 防循环。 | +| session memory compact | manual/auto 前置尝试 | session memory 文件 | 否 | 是 | 若 post-compact 仍超阈值,放弃并回退传统 compact。 | +| microcompact | time/cached 小型压缩 | 局部清理或 API cache edit | 不一定 | 不适用 | 通常不改变 JSONL 主历史。 | +| snip | `HISTORY_SNIP` | 删除中段 | 否 | 保留前后上下文 | 通过 snip metadata 投影,不物理删旧行。 | + +### Compact 结果形态 + +传统 compact 会生成: + +1. `compact_boundary` system message。 +2. compact summary user message。 +3. post-compact attachments,例如当前文件、计划模式、技能、MCP/tool schema delta、hook 结果。 + +简化 before/after: + +```text +Raw/UI: + u1, a1, u2, a2, ... u99, a99, + system:compact_boundary, + user:compact summary, + attachment:current files, + u100 + +Active query view: + system:compact_boundary, + user:compact summary, + attachment:current files, + u100 + +API wire view: + user:compact summary, + attachment/content, + u100 +``` + +boundary 本身是 system message,最后会被 API normalization 过滤;它的价值主要在本地投影、恢复和统计。 + +### Boundary metadata + +`createCompactBoundaryMessage()` 写: + +| 字段 | 含义 | +|---|---| +| `compactMetadata.trigger` | `manual` 或 `auto`。 | +| `compactMetadata.preTokens` | compact 前 token 数。 | +| `compactMetadata.userContext` | 用户手动 compact 的额外说明。 | +| `compactMetadata.messagesSummarized` | 被总结消息数量。 | +| `logicalParentUuid` | compact 前最后消息,用于逻辑追踪。 | + +后续路径还会补: + +| 字段 | 来源 | 作用 | +|---|---|---| +| `preCompactDiscoveredTools` | traditional/SM compact | 恢复 deferred tool schema 可见性。 | +| `preservedSegment.{headUuid,anchorUuid,tailUuid}` | partial/SM compact | 恢复时把保留尾段接到 boundary 后。 | + +### Tool result budget 与 content replacement + +大 tool_result 不一定直接进入后续上下文。`applyToolResultBudget()` 会按 API-level user message 聚合预算,必要时把大块内容持久化并替换成较小 preview/stub。 + +关键点: + +| 点 | 说明 | +|---|---| +| replacement decision 会落 JSONL | `recordContentReplacement()` 写 `content-replacement`。 | +| 主线程和 agent 分开 | 无 `agentId` 写主 JSONL;有 `agentId` 写 sidechain JSONL。 | +| resume 会重建 replacement state | 避免恢复后同一大结果又变回完整内容,导致 token 暴涨或 prompt cache 失配。 | +| `--fork-session` 会 seed records | fork 新 session 时复制 replacement 决策到新 session。 | + +### Session memory compact + +`sessionMemoryCompact.ts` 是传统 summary compact 前的实验路径。流程: + +1. 等待 session memory extraction 完成。 +2. 读取 session memory 文件。 +3. 有 `lastSummarizedMessageId` 时,从其后保留安全尾段;否则把 resumed session 视为已有 memory summary。 +4. 调整切点,避免断开 tool_use/tool_result 或 thinking blocks。 +5. 创建标准 `compact_boundary` + summary user message。 +6. 若 post-compact token count 仍超过阈值,放弃并回退传统 compact。 + +因为产物仍是标准 `CompactionResult`,下游写 transcript 和恢复逻辑与传统 compact 共用。 + +### Context-collapse 当前状态 + +本仓库保留了 context-collapse 的持久化接口,但核心实现是 stub: + +| 模块 | 当前行为 | +|---|---| +| `contextCollapse/index.ts` | `applyCollapsesIfNeeded()` 返回原 messages;`recoverFromOverflow()` 返回 committed=0;`isWithheldPromptTooLong()` 恒 false。 | +| `contextCollapse/operations.ts` | `projectView()` 是 identity。 | +| `contextCollapse/persist.ts` | `restoreFromEntries()` 是 no-op。 | + +已预留 JSONL entry: + +| Entry | 写入接口 | 内容 | +|---|---|---| +| `marble-origami-commit` | `recordContextCollapseCommit()` | `collapseId`、summary UUID/content、archived span 边界。 | +| `marble-origami-snapshot` | `recordContextCollapseSnapshot()` | staged spans、armed、lastSpawnTokens。 | + +loader 会收集这些 entry;遇到 compact boundary 时会清空旧 commits/snapshot,避免它们引用已被 compact 丢弃的 UUID。 + +所以当前真实生效的上下文缩减主要是 compact、session memory compact、tool_result budget、microcompact 和 snip;context-collapse 只是接口已接好。 + +### Compact 后清理 + +`runPostCompactCleanup(querySource)` 总是清: + +- microcompact state。 +- system prompt sections。 +- classifier approvals。 +- speculative bash checks。 +- beta tracing。 +- session messages memo cache。 +- compact cleanup callbacks。 +- `COMMIT_ATTRIBUTION` 下异步 sweep file-content cache。 + +只在主线程 compact 清: + +- context-collapse store。 +- `getUserContext` cache。 +- memory files cache。 + +原因:subagent 和主线程同进程,共享模块级状态。`agent:*` compact 如果清主线程 context-collapse 或 memory cache,会破坏父会话状态。 + +它明确不清 `resetSentSkillNames()`,避免 compact 后重新注入完整 skill listing,浪费 token 和 prompt cache。 + +## 分支与 Fork 对比 + +| 入口 | 本质 | 是否新主 session | 是否 subagent | 持久化位置 | 父会话看到什么 | 恢复方式 | +|---|---|---:|---:|---|---|---| +| `/branch` | 复制当前主 transcript 成新 JSONL | 是 | 否 | `.jsonl` | 直接切到新分支会话 | 普通 session resume。 | +| `--fork-session` | resume/continue 时把旧消息作为新 session 初始消息 | 是 | 否 | 新 session 首次写入时 materialize | 启动即在新 session 中继续 | 新 session resume。 | +| `/fork ` | slash wrapper,调用 AgentTool fork | 否 | 是 | `subagents/agent-.jsonl` + `.meta.json` | fork started + task notification | `resumeAgentBackground()`。 | +| `AgentTool({ fork: true })` | Tool 层 fork 子 agent | 否 | 是 | `subagents/agent-.jsonl` + `.meta.json` | sync final tool_result 或 async notification | `resumeAgentBackground()`。 | +| 普通 AgentTool async | 后台本地 subagent | 否 | 是 | `subagents/agent-.jsonl` + `.meta.json` | `async_launched` + task notification | `resumeAgentBackground()`。 | +| remote AgentTool | CCR remote session | 否 | 远端 | `remote-agents/*.meta.json` | remote task output/notification | `restoreRemoteAgentTasks()` + CCR。 | + +### `/branch` + +`/branch` 创建新 session 文件,不是在原 JSONL 里追加 branch marker。 + +流程: + +1. 生成新的 sessionId。 +2. 读取当前 transcript 文件。 +3. 过滤主会话消息,排除 `isSidechain` 和非 transcript entry。 +4. 复制消息并重写 `sessionId`。 +5. 重新串 `parentUuid`。 +6. 添加 `forkedFrom: { sessionId, messageUuid }`。 +7. 复制原 session 的 `content-replacement` entry 并改成新 sessionId。 +8. 写入 `.jsonl`。 +9. 构造 `LogOption` 并让 REPL resume 到新分支。 + +### `--fork-session` + +`--fork-session` 只改变 resume 的 ownership: + +| 非 fork resume | fork-session resume | +|---|---| +| 切到旧 sessionId。 | 保持启动时 fresh sessionId。 | +| `adoptResumedSessionFile()` 接管旧 JSONL。 | 不接管旧 JSONL。 | +| 后续继续 append 到旧 transcript。 | 后续 materialize 成新 transcript。 | +| 原 session 继续增长。 | 原 session 不被写入。 | + +如果旧 session 有 `content-replacement`,会先把 records seed 到新 session,避免大 tool_result 的替换状态丢失。 + +## Subagent 与 Fork Agent + +### 普通 subagent + +普通 AgentTool subagent 最终走 `runAgent()`: + +```mermaid +sequenceDiagram + participant Parent as 父会话 + participant Tool as AgentTool + participant Agent as runAgent + participant Side as sidechain JSONL + participant Task as LocalAgentTask + + Parent->>Tool: assistant tool_use Agent + Tool->>Agent: start sync or async + Agent->>Side: record initialMessages + Agent->>Side: record assistant/user/progress/compact_boundary + alt sync foreground + Agent-->>Tool: final result + Tool-->>Parent: Agent tool_result + else async/background + Tool-->>Parent: async_launched tool_result + Agent-->>Task: complete + Task-->>Parent: + end +``` + +父会话通常只记录: + +- Agent tool_use。 +- Agent tool_result。 +- async launch result。 +- task notification。 +- 必要 progress。 + +完整子 agent 内部工具调用和消息在 sidechain JSONL 中,不会混进主会话 active context。 + +### Fork agent + +fork agent 是 AgentTool 的一种特殊 subagent。它继承父上下文、system prompt、tools、model 和 thinking config,目标是让多个子 agent 共享尽可能长的 byte-identical prompt cache prefix。 + +关键实现: + +| 继承内容 | 实现 | +|---|---| +| system prompt | 优先使用 `toolUseContext.renderedSystemPrompt`,没有才 fallback 重建。 | +| tools | 使用父 `toolUseContext.options.tools`,`useExactTools: true`。 | +| model | `FORK_AGENT.model = "inherit"`。 | +| thinking/non-interactive | 通过 exact tool/options 继承,避免 cache key 分叉。 | +| messages | `forkContextMessages = toolUseContext.messages`。 | + +`buildForkedMessages()` 负责构造 cache-friendly 尾部: + +```text +parent history... +assistant: [text/thinking/tool_use A/tool_use B/...] +user: + tool_result for A = "Fork started — processing in background" + tool_result for B = "Fork started — processing in background" + directive = "" +``` + +多个 fork child 的长前缀相同,只有最后 directive 不同。 + +限制: + +| 限制 | 原因 | +|---|---| +| 需要 `FORK_SUBAGENT` feature。 | 功能门控。 | +| coordinator mode 禁用。 | coordinator 已有自己的编排模型。 | +| non-interactive session 禁用。 | fork subagent 偏交互式后台任务模型。 | +| fork child 禁止递归 fork。 | 防止无限 fork;通过 querySource 和 boilerplate tag 检测。 | +| resume fork agent 不再传 `forkContextMessages`。 | sidechain 已包含父上下文切片,重复传会造成重复 tool_use id。 | + +### `runForkedAgent()` 不是 AgentTool fork + +`src/utils/forkedAgent.ts` 的 `runForkedAgent()` 是内部 cache-safe side query 工具,用于 session memory、prompt suggestion、summary 等。它复用父 system/user/system context、tools、messages,可选 `skipTranscript`,但默认不写 AgentTool metadata,也不是用户可继续对话的 AgentTool fork。 + +## Agent 恢复 + +本地 agent 恢复入口是 `resumeAgentBackground()`。 + +流程: + +```mermaid +flowchart TD + A[user continues agent] --> B[getAgentTranscript(agentId)] + B --> C[load sidechain JSONL + build chain] + C --> D[readAgentMetadata(agentId)] + D --> E[filter unresolved tool_use/thinking/blank assistant] + E --> F[reconstruct content replacement state] + F --> G{metadata.worktreePath exists?} + G -- yes --> H[runWithCwdOverride(worktreePath)] + G -- no --> I[parent cwd] + H --> J[register async LocalAgentTask] + I --> J + J --> K[continue query loop] +``` + +恢复时: + +| 状态 | 来源 | +|---|---| +| agent transcript | `agent-.jsonl`。 | +| agent type | `agent-.meta.json`。 | +| fork/general agent 选择 | metadata `agentType`。 | +| worktree cwd | metadata `worktreePath`,目录不存在则回退父 cwd。 | +| content replacement | sidechain records + parent live state gap-fill。 | +| task UI | 重新注册 async task。 | + +## Remote Agent 恢复 + +remote CCR agent 不靠本地 sidechain 继续执行。 + +```mermaid +sequenceDiagram + participant Tool as AgentTool + participant R as RemoteAgentTask + participant Sidecar as remote-agents meta + participant CCR as CCR session + participant REPL as REPL resume + + Tool->>CCR: teleportToRemote() + Tool->>R: registerRemoteAgentTask() + R->>Sidecar: write remote-agent-.meta.json + REPL->>Sidecar: restoreRemoteAgentTasks() + REPL->>CCR: fetchSession(sessionId) + alt running + REPL->>R: rebuild RemoteAgentTaskState + polling + else 404/archive + REPL->>Sidecar: delete sidecar + end +``` + +差异: + +| 本地 subagent | remote agent | +|---|---| +| 有完整 sidechain JSONL。 | 没有本地执行 transcript。 | +| resume 可继续 API 对话。 | resume 只恢复 polling。 | +| 状态来自 JSONL + `.meta.json`。 | 状态来自 CCR session + local sidecar。 | +| 完成后本地 sidechain 仍可审计。 | 完成/archived 后 sidecar 会删除。 | + +## 常见误区 + +| 误区 | 正确理解 | +|---|---| +| JSONL 顺序就是会话顺序 | 恢复靠 leaf + `parentUuid`,不是简单顺序 replay。 | +| compact 删除了旧历史 | compact 追加 boundary;旧历史仍在 raw transcript。 | +| boundary 会发给模型 | boundary 是本地 system marker,API normalization 会过滤。 | +| `/branch` 和 `/fork` 都是 fork | `/branch` 是新主 session;`/fork` 是 fork subagent sidechain。 | +| `--fork-session` 等于 `/branch` | 它不是复制文件命令,而是 resume 时保持 fresh session ownership。 | +| subagent 消息会进入主上下文 | 父会话只看到 Agent tool result/notification,完整内部消息在 sidechain。 | +| remote agent 有本地 sidechain | remote 只有 sidecar 身份,执行状态来自 CCR。 | +| context-collapse 已经真实压缩上下文 | 当前仓库中 context-collapse 核心实现是 stub。 | + +## 源码入口索引 + +| 问题 | 从这里看 | +|---|---| +| Entry union 有哪些类型 | `src/types/logs.ts` 的 `Entry`。 | +| 主 transcript 路径 | `src/utils/sessionStorage.ts` 的 `getTranscriptPath()`。 | +| subagent transcript 路径 | `getAgentTranscriptPath(agentId)`。 | +| remote sidecar 路径 | `getRemoteAgentsDir()` / `getRemoteAgentMetadataPath()`。 | +| 主写入 | `recordTranscript()`。 | +| sidechain 写入 | `recordSidechainTranscript()`。 | +| write queue | `Project.enqueueWrite()` / `drainWriteQueue()` / `flush()`。 | +| lazy materialize | `Project.materializeSessionFile()`。 | +| tombstone 删除 | `removeTranscriptMessage()` / `Project.removeMessageByUuid()`。 | +| 读取 transcript | `loadTranscriptFile()`。 | +| 大文件读取 | `readTranscriptForLoad()` in `sessionStoragePortable.ts`。 | +| dead branch 裁剪 | `walkChainBeforeParse()`。 | +| parent 链重建 | `buildConversationChain()`。 | +| parallel tool_result 补回 | `recoverOrphanedParallelToolResults()`。 | +| preserved segment | `applyPreservedSegmentRelinks()`。 | +| snip removal | `applySnipRemovals()`。 | +| CLI resume 加载 | `loadConversationForResume()`。 | +| resume 状态切换 | `processResumedConversation()`。 | +| AppState 恢复 | `restoreSessionStateFromLog()`。 | +| 中断检测 | `deserializeMessagesWithInterruptDetection()`。 | +| active context | `getMessagesAfterCompactBoundary()`。 | +| query context pipeline | `src/query.ts`。 | +| compact boundary | `createCompactBoundaryMessage()`。 | +| auto compact | `autoCompactIfNeeded()` / `shouldAutoCompact()`。 | +| session memory compact | `src/services/compact/sessionMemoryCompact.ts`。 | +| reactive compact | `src/services/compact/reactiveCompact.ts`。 | +| post compact cleanup | `runPostCompactCleanup()`。 | +| context-collapse stub | `src/services/contextCollapse/*`。 | +| `/branch` | `src/commands/branch/branch.ts`。 | +| `/fork` | `src/commands/fork/fork.tsx`。 | +| AgentTool fork | `AgentTool.tsx` + `forkSubagent.ts`。 | +| 普通 subagent 运行 | `runAgent.ts`。 | +| agent resume | `resumeAgent.ts`。 | +| remote task restore | `restoreRemoteAgentTasks()`。 |