目录
  1. 1. 一、Swarm 是什么
  2. 2. 二、核心文件职责
  3. 3. 三、TeamFile:协调的数据中心
  4. 4. 四、Mailbox:Agent 间通信机制
  5. 5. 五、inProcessRunner:Teammate 执行引擎
    1. 5.1. 关键设计
    2. 5.2. 完整生命周期
    3. 5.3. 进度追踪
  6. 6. 六、权限同步协议(permissionSync)
  7. 7. 七、Backend 抽象:多种派生方式
  8. 8. 八、Plan Mode Required
  9. 9. 九、会话恢复(reconnection.ts)
  10. 10. 十、面试要点
  11. 11. 十一、inProcessRunner.ts — Teammate 完整执行引擎
    1. 11.1. 11.1 整体架构:单文件覆盖完整生命周期
    2. 11.2. 11.2 Teammate 完整生命周期
      1. 11.2.1. 阶段 1:初始化(Spawn)
      2. 11.2.2. 阶段 2:首次运行(Initial Prompt)
      3. 11.2.3. 阶段 3:运行 Agent Loop(核心)
      4. 11.2.4. 阶段 4:Idle 等待(waitForNextPromptOrShutdown)
      5. 11.2.5. 阶段 5:Shutdown 处理(模型决策)
      6. 11.2.6. 阶段 6:终止(Completion / Failure)
    3. 11.3. 11.3 自动 Compaction:防止 Token 膨胀
  12. 12. 十二、permissionSync.ts — 权限同步协议深度解析
    1. 12.1. 12.1 文件系统目录结构
    2. 12.2. 12.2 完整 SwarmPermissionRequest 数据结构
    3. 12.3. 12.3 双轨权限通道
    4. 12.4. 12.4 文件锁机制(lockfile)
    5. 12.5. 12.5 Sandbox 权限扩展
  13. 13. 十三、leaderPermissionBridge.ts — 内存桥接
    1. 13.1. 13.1 为什么需要这个模块
    2. 13.2. 13.2 快速路径 vs 邮箱回退
    3. 13.3. 13.3 权限模式隔离
  14. 14. 十四、reconnection.ts — 会话恢复机制
    1. 14.1. 14.1 两种启动场景
    2. 14.2. 14.2 同步初始化的必要性
    3. 14.3. 14.3 “Reconnection”名称的误导性
  15. 15. 十五、Backend 抽象层深度解析
    1. 15.1. 15.1 Backend 类型系统
    2. 15.2. 15.2 TmuxBackend — 外部进程 Teammate
    3. 15.3. 15.3 InProcess vs TmuxBackend 对比
    4. 15.4. 15.4 Backend 自动检测与选择
  16. 16. 十六、TeamFile 数据结构与并发控制
    1. 16.1. 16.1 完整字段定义(teamHelpers.ts)
    2. 16.2. 16.2 读写并发控制策略
    3. 16.3. 16.3 Leader 如何用 TeamFile 协调 Worker
  17. 17. 十七、Mailbox 通信协议深度分析
    1. 17.1. 17.1 消息格式(teammates/mailbox.ts 层)
    2. 17.2. 17.2 投递保证与顺序性
    3. 17.3. 17.3 已读标记与幂等性
  18. 18. 十八、spawnInProcess.ts — 状态注册与清理
    1. 18.1. 18.1 killInProcessTeammate:原子状态转换
    2. 18.2. 18.2 Cleanup Registry
  19. 19. 十九、深度面试题(含系统设计)
【Claude Code源码剖析】20-Swarm 多 Agent 协调系统

源码路径:src/utils/swarm/(14个文件 + backends/ 子目录,约4500行)
关联:src/tasks/InProcessTeammateTask/src/utils/teammateMailbox.tssrc/utils/teammate.ts


一、Swarm 是什么

Swarm 是 Claude Code 的多 Agent 并行执行框架。当单个 Agent 无法高效完成复杂任务时(如同时修改多个子系统、并行运行测试),Swarm 允许一个 Leader Agent 派生多个 Worker Agent(Teammate),各自独立运行 Agentic Loop,通过邮箱(Mailbox)通信协调。

                   ┌─────────────────────┐
│ Leader Agent │
│ (主 REPL 会话) │
│ TeamCreateTool ──┐ │
└──────────────────┼──┘
│ 派生
┌──────────────────────────┼─────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Teammate A │ │ Teammate B │ │ Teammate C │
│ (in-process) │ │ (tmux pane) │ │ (tmux pane) │
│ 独立 AgentLoop │ │ 独立 AgentLoop │ │ 独立 AgentLoop │
└────────┬────────┘ └────────┬────────┘ └────────┬────────┘
│ Mailbox │ Mailbox │ Mailbox
└────────────────────────┴──────────────────────┘

权限请求/响应
空闲通知
直接消息(DM)

二、核心文件职责

文件 职责
constants.ts 常量:TEAM_LEAD_NAMESWARM_SESSION_NAME、环境变量名
teamHelpers.ts Team 文件(~/.claude/teams/<name>.json)的 CRUD、成员管理
inProcessRunner.ts 核心:在同一进程内运行 Teammate 的 runAgent() 循环(1552行)
permissionSync.ts Worker↔Leader 权限请求/响应协议(Zod Schema + 邮箱读写)
leaderPermissionBridge.ts Leader 的 React UI 与非 React 权限代码之间的桥接
teammateInit.ts Teammate 初始化:注册 Stop Hook、同步 Team 级路径权限
reconnection.ts 计算 TeamContext:区分全新派生 vs 会话恢复
spawnInProcess.ts 进程内启动 Teammate 的入口(spawn 流程)
spawnUtils.ts spawn 共用工具函数
teammateLayoutManager.ts tmux pane 布局管理
teammateModel.ts Teammate 模型选择逻辑
teammatePromptAddendum.ts Teammate System Prompt 追加内容
It2SetupPrompt.tsx iTerm2 集成设置提示 UI
backends/ 不同启动后端(tmux pane、in-process、hidden)的抽象(9个文件)

三、TeamFile:协调的数据中心

Team 的元数据存储在 ~/.claude/teams/<teamName>.jsonTeamFile),所有 Agent 共享读取:

type TeamFile = {
name: string
description?: string
createdAt: number
leadAgentId: string // Leader 的 Agent ID
leadSessionId?: string // Leader 的 Session UUID(用于发现)
hiddenPaneIds?: string[] // 当前隐藏的 tmux pane
teamAllowedPaths?: TeamAllowedPath[] // 团队共享的路径权限
members: Array<{
agentId: string
name: string
agentType?: string
model?: string
prompt?: string
color?: string // 终端彩色标识
planModeRequired?: boolean // 是否要求先规划再执行
joinedAt: number
// ...
}>
}

关键设计:TeamFile 是 Leader-owned 的配置文件,Teammates 只读;权限变更通过 permissionSync 协议而非直接修改文件。


四、Mailbox:Agent 间通信机制

每个 Agent 有一个专属邮箱目录~/.claude/teams/<teamName>/mailbox/<agentId>/),通过文件系统实现异步消息传递(用 lockfile 保证原子性)。

消息类型:

消息类型 方向 用途
idle_notification Worker → Leader Worker 完成任务,通知 Leader
permission_request Worker → Leader Worker 遇到权限问题,请求 Leader 审批
permission_response Leader → Worker Leader 回传审批结果(approved/rejected)
shutdown_request Leader → Worker Leader 要求 Worker 停止
direct_message (DM) 任意 → 任意 自由文本通信(SendMessageTool
sandbox_permission_request Worker → Leader 沙箱环境下的权限请求
Worker A 需要权限

▼ writeToMailbox(leader, permission_request{id, toolName, input, ...})
Leader 轮询邮箱(useSwarmPermissionPoller hook)

▼ 用户在 Leader UI 审批

▼ writeToMailbox(workerA, permission_response{id, approved, updatedInput?})
Worker A 轮询邮箱,收到响应,继续执行

五、inProcessRunner:Teammate 执行引擎

inProcessRunner.ts 是 Swarm 最核心的文件(1553行),负责在同一 Node.js 进程内运行多个 Agent Loop,通过 AsyncLocalStorage 隔离各 Teammate 的上下文。

关键设计

// AsyncLocalStorage 隔离不同 Teammate 的上下文
// 使 teammate.ts 中的 getAgentId()/getTeamName() 等函数
// 在同一进程的不同 Teammate 中返回各自正确的值
runWithTeammateContext(teammateContext, async () => {
await runAgent(/* 完整的 AgentLoop */)
})

完整生命周期

1. 权限初始化
└─ applyPermissionUpdates():应用 Team 级共享路径权限

2. System Prompt 构建
└─ 注入 Teammate 身份信息、Team 名称、Leader 信息

3. runAgent() 执行
└─ 标准 Agentic Loop:LLM → Tool → LLM → ...
└─ 支持 Plan Mode(PLAN_MODE_REQUIRED_ENV_VAR)
└─ 支持自动压缩(autoCompact)

4. 完成/中止
├─ 发送 idle_notification 到 Leader 邮箱
├─ 持久化权限变更(persistPermissionUpdates)
└─ 清理资源(cleanupRegistry)

进度追踪

// Leader 可以实时看到每个 Teammate 的进度
const progressTracker = createProgressTracker()
updateProgressFromMessage(message, progressTracker)
// → InProcessTeammateTaskState.progressUpdate
// → Leader UI 中显示 Worker 的当前活动

六、权限同步协议(permissionSync)

这是 Swarm 最复杂的工程挑战:Worker 遇到需要用户确认的操作,但用户只在 Leader 的终端

SwarmPermissionRequestSchema:
{
id: string, // 请求唯一ID
workerId: string, // 哪个 Worker 发起
toolName: string, // 需要哪个工具的权限
toolUseId: string, // 对应的 LLM tool_use block ID
input: object, // 工具输入(可被 Leader 修改后回传)
permissionSuggestions: [], // 建议添加的规则
status: 'pending' | 'approved' | 'rejected',
feedback?: string, // 拒绝时的反馈(发回给 Worker 的 LLM)
updatedInput?: object // Leader 修改了输入
}

设计亮点:Leader 不仅能批准/拒绝,还能修改 Worker 的工具输入updatedInput),实现细粒度控制。


七、Backend 抽象:多种派生方式

type BackendType = 'tmux' | 'in-process' | 'hidden'
后端 特点 适用场景
tmux 在 tmux 新 pane 启动独立进程 交互式,用户可见每个 Teammate
in-process 同进程 AsyncLocalStorage 隔离 轻量,适合短任务
hidden 独立进程但不显示 UI 后台任务

八、Plan Mode Required

Teammate 可以设置 planModeRequired: true,要求在执行前先规划、后实施

PLAN_MODE_REQUIRED_ENV_VAR=true

Teammate 先进入 Plan Mode(只读 + 思考)

生成计划后请求 Leader 审批

Leader 批准后 Teammate 才开始写代码

这是对多 Agent 并行执行的关键安全约束——防止 Worker 在没有充分规划的情况下直接修改代码。


九、会话恢复(reconnection.ts)

当 Leader 会话因网络断开或用户操作中断后恢复时:

function computeInitialTeamContext(): AppState['teamContext'] | undefined {
// 区分:全新派生(从 CLI args)vs 会话恢复(从 transcript)
const context = getDynamicTeamContext() // CLI args 优先
if (!context) return undefined

const teamFile = readTeamFile(teamName) // 从磁盘读取 TeamFile
const isLeader = !agentId // 无 agentId → 是 Leader

return { teamName, teamFilePath, leadAgentId, isLeader, ... }
}

关键设计:TeamContext 在 React 首次渲染之前同步计算(避免 useEffect 延迟),确保恢复的 Leader 能立即感知其团队状态。


十、面试要点

Q:Swarm 如何解决多 Agent 的权限一致性问题?

Worker 无法直接修改用户的权限配置,所有需要用户确认的权限都通过 permissionSync 协议路由给 Leader,由 Leader UI 统一呈现给用户。Leader 的决策(包括”永远允许”)可以通过 permissionSuggestions 持久化回 Worker 的 session 权限配置。

Q:同一进程运行多个 Agent Loop,如何避免状态污染?

使用 AsyncLocalStoragerunWithTeammateContext()),使 getAgentId()getTeamName() 等全局函数在不同 async 调用链中自动返回各自 Teammate 的值,无需显式传参。

Q:Swarm 的通信为什么用文件系统邮箱而不是内存队列?

  1. 跨进程(tmux backend 中 Teammate 是独立进程);2. 持久化(崩溃恢复后邮件不丢失);3. 进程间隔离(Teammate 不能直接访问 Leader 的内存)。对于 in-process 后端也使用相同机制,保持协议统一。

十一、inProcessRunner.ts — Teammate 完整执行引擎

11.1 整体架构:单文件覆盖完整生命周期

inProcessRunner.ts(1553 行)是 Swarm 子系统中最复杂的单文件。它的职责是:在同一个 Node.js 进程中运行多个独立的 Agent Loop,同时保证各 Agent 的状态完全隔离。

核心入口有两个:

  • spawnInProcessTeammate(config, context):在 spawnInProcess.ts 中注册 AppState 任务,返回 taskId/abortController
  • startInProcessTeammate(config):调用 runInProcessTeammate(config),以 fire-and-forget 方式启动 Agent 主循环
// spawnInProcess.ts — 负责注册,不负责执行
export async function spawnInProcessTeammate(config, context) {
const agentId = formatAgentId(name, teamName) // "researcher@my-team"
const taskId = generateTaskId('in_process_teammate')
const abortController = createAbortController() // 独立的 AbortController
const teammateContext = createTeammateContext({...}) // AsyncLocalStorage 容器

const taskState: InProcessTeammateTaskState = {
type: 'in_process_teammate',
status: 'running',
identity, prompt, model, abortController,
permissionMode: planModeRequired ? 'plan' : 'default',
isIdle: false,
pendingUserMessages: [],
messages: [],
...
}
registerTask(taskState, setAppState) // 写入 AppState.tasks[taskId]
return { success: true, agentId, taskId, abortController, teammateContext }
}

// inProcessRunner.ts — 负责执行
export function startInProcessTeammate(config) {
const agentId = config.identity.agentId
void runInProcessTeammate(config).catch(error => {
logForDebugging(`[inProcessRunner] Unhandled error in ${agentId}: ${error}`)
})
}

设计要点:Spawn 与 Execute 分离spawnInProcess.ts 只负责在 AppState 注册元数据,inProcessRunner.ts 才是真正驱动 Agent 运行的引擎,两者通过 taskId 关联。

11.2 Teammate 完整生命周期

阶段 1:初始化(Spawn)

用户触发 TeamCreate → SpawnTeamTool → spawnInProcessTeammate()

创建 AbortController(独立于 Leader)
创建 TeammateContext(AsyncLocalStorage 容器)
生成 AgentId = "name@teamName"
注册 InProcessTeammateTaskState 到 AppState.tasks
注册 cleanupRegistry(进程退出时自动 abort)

返回 { taskId, abortController, teammateContext }

InProcessTeammateTask 组件接管,调用
startInProcessTeammate(config)

阶段 2:首次运行(Initial Prompt)

// 主循环开始前:尝试抢占任务(提前显示活动状态)
await tryClaimNextTask(identity.parentSessionId, identity.agentName)

// 将初始 prompt 包装为 XML 格式
const wrappedInitialPrompt = formatAsTeammateMessage('team-lead', prompt, undefined, description)
// → <teammate-message teammate_id="team-lead" summary="...">...</teammate-message>

// 将 prompt 写入 task.messages 供 UI 显示
updateTaskState(taskId, task => ({
...task,
messages: appendCappedMessage(task.messages, createUserMessage({ content: wrappedInitialPrompt })),
}), setAppState)

阶段 3:运行 Agent Loop(核心)

while (!abortController.signal.aborted && !shouldExit) {
┌─────────────────────────────────────────────────┐
│ 检查是否需要 compaction(token 超阈值) │
│ → 复制 ToolUseContext(隔离,不污染主会话) │
│ → compactConversation() 摘要历史 │
│ → 替换 allMessages + 重置 replacementState │
└─────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────┐
│ 创建 per-turn AbortController │
│ (Escape 只中断当前轮次,不终止 Teammate 进程) │
└─────────────────────────────────────────────────┘

runWithTeammateContext(ctx, () =>
runWithAgentContext(agentCtx, () =>
for await (message of runAgent({...})) {
// 逐条处理消息
updateProgressFromMessage(tracker, message, ...)
updateTaskState(taskId, task => ({
...task,
progress,
messages: appendCappedMessage(task.messages, message),
inProgressToolUseIDs: ... // 动画进度追踪
}), setAppState)
}
)
)

Mark task.isIdle = true
sendIdleNotification() → 写入 Leader 邮箱

waitForNextPromptOrShutdown() ← 进入 Idle 轮询
}

关键设计:双层 AbortController

  • lifecycle AbortControllerconfig.abortController):终止整个 Teammate,由 killInProcessTeammate() 触发
  • per-turn AbortControllercurrentWorkAbortController):只中断当前轮次,用户按 Escape 触发,Teammate 仍保持存活进入 Idle 等待

阶段 4:Idle 等待(waitForNextPromptOrShutdown)

// 500ms 轮询间隔
const POLL_INTERVAL_MS = 500

while (!abortController.signal.aborted) {
// 优先级 1:检查 AppState.pendingUserMessages(in-memory,来自 transcript 视图)
if (task.pendingUserMessages.length > 0) {
return { type: 'new_message', message, from: 'user' }
}

await sleep(POLL_INTERVAL_MS)

// 优先级 2:检查邮箱中的 shutdown 请求(最高优先级,防止消息洪水饥饿)
for (let i = 0; i < allMessages.length; i++) {
const parsed = isShutdownRequest(msg.text)
if (parsed) {
// 标记为已读,立即返回 shutdown
return { type: 'shutdown_request', request: parsed, originalMessage }
}
}

// 优先级 3:Leader 消息 > Peer 消息(防止 peer 消息饥饿 Leader 的协调指令)
let selectedIndex = allMessages.findIndex(m => !m.read && m.from === TEAM_LEAD_NAME)
if (selectedIndex === -1) {
selectedIndex = allMessages.findIndex(m => !m.read)
}

// 优先级 4:检查 TaskList 中是否有可认领的任务
const taskPrompt = await tryClaimNextTask(taskListId, identity.agentName)
}

消息优先级(从高到低):

  1. Shutdown 请求(防止 peer 消息洪水导致 shutdown 饥饿)
  2. in-memory pendingUserMessages(来自 UI 的直接注入,无延迟)
  3. Leader 邮件(协调指令,代表用户意图)
  4. Peer 邮件(其他 Worker 的 DM)
  5. TaskList 任务认领(兜底,Worker 自主找活干)

阶段 5:Shutdown 处理(模型决策)

case 'shutdown_request':
// 注意:CC 不自动 approve shutdown!
// shutdown 请求被包装成 XML 消息,传给模型,模型自己决定
currentPrompt = formatAsTeammateMessage(
waitResult.request?.from || 'team-lead',
waitResult.originalMessage, // 原始 shutdown JSON
)
// 模型收到后,可以调用 approveShutdown 或 rejectShutdown 工具
break

这是反直觉的设计:模型有权拒绝 shutdown。若模型认为任务未完成,可以拒绝 Leader 的关闭请求,继续工作后再 approve。

阶段 6:终止(Completion / Failure)

// 正常退出(shouldExit=true 或 abort)
updateTaskState(taskId, task => ({
...task,
status: 'completed',
notified: true, // 阻止 SDK 重复通知
endTime: Date.now(),
messages: task.messages?.length ? [task.messages.at(-1)!] : undefined, // 只保留最后一条
pendingUserMessages: [],
abortController: undefined,
}), setAppState)
void evictTaskOutput(taskId)
evictTerminalTask(taskId, setAppState) // 从 AppState 急速驱逐
emitTaskTerminatedSdk(taskId, 'completed', { toolUseId, summary: identity.agentId })
unregisterPerfettoAgent(identity.agentId)

// 异常退出(catch 块)
await sendIdleNotification(agentName, color, teamName, {
idleReason: 'failed',
completedStatus: 'failed',
failureReason: errorMessage,
})

11.3 自动 Compaction:防止 Token 膨胀

in-process Teammate 是长生命周期的,一个 Teammate 可能在多个轮次中积累大量 token。CC 在每轮开始前检测:

const tokenCount = tokenCountWithEstimation(allMessages)
if (tokenCount > getAutoCompactThreshold(mainLoopModel)) {
// 创建隔离的 ToolUseContext(不污染主会话的 readFileState)
const isolatedContext: ToolUseContext = {
...toolUseContext,
readFileState: cloneFileStateCache(toolUseContext.readFileState),
onCompactProgress: undefined,
setStreamMode: undefined,
}
const compactedSummary = await compactConversation(allMessages, isolatedContext, ...)
contextMessages = buildPostCompactMessages(compactedSummary)

// 原地替换 allMessages(保持引用不变)
allMessages.length = 0
allMessages.push(...contextMessages)

// 重置 contentReplacementState(旧 tool_use_id 不再有效)
teammateReplacementState = createContentReplacementState()
resetMicrocompactState()
}

设计细节:contentReplacementState 跨轮次持久化(防止缓存 miss),但 compact 后必须重置(旧 tool ID 消失)。


十二、permissionSync.ts — 权限同步协议深度解析

12.1 文件系统目录结构

~/.claude/teams/{team-name}/
permissions/
pending/
perm-1748000000-abc123.json ← Worker 写入,等待 Leader 处理
.lock ← 目录级锁文件
resolved/
perm-1748000000-abc123.json ← Leader 处理后移到此

12.2 完整 SwarmPermissionRequest 数据结构

// Zod schema 定义的完整字段
{
id: string, // "perm-{timestamp}-{random7}" 全局唯一
workerId: string, // CLAUDE_CODE_AGENT_ID (e.g., "researcher@my-team")
workerName: string, // 显示名(e.g., "researcher")
workerColor?: string, // UI 颜色标识
teamName: string, // 路由用
toolName: string, // "Bash" | "Edit" | "Write" 等
toolUseId: string, // Anthropic API 的 tool_use_id
description: string, // 人类可读描述(展示给用户)
input: Record<string, unknown>, // 序列化工具入参
permissionSuggestions: unknown[], // 权限建议规则
status: 'pending' | 'approved' | 'rejected',
resolvedBy?: 'worker' | 'leader',
resolvedAt?: number,
feedback?: string, // 拒绝时的原因
updatedInput?: Record<string, unknown>, // Leader 可修改入参
permissionUpdates?: unknown[], // "永远允许"规则
createdAt: number,
}

12.3 双轨权限通道

permissionSync 实际维护了两套机制,在演进中共存:

机制 路径 使用场景
文件系统 pending/resolved ~/.claude/teams/.../permissions/ 旧的轮询模型(保留兼容)
Mailbox 消息 ~/.claude/teams/.../mailbox/ 新的事件驱动模型(主路径)

主路径的数据流:

Worker:
createPermissionRequest() → SwarmPermissionRequest 对象
sendPermissionRequestViaMailbox(request)
→ 读取 TeamFile 找到 leaderName
→ createPermissionRequestMessage({ request_id, tool_name, input, ... })
→ writeToMailbox(leaderName, { from: workerName, text: JSON.stringify(msg) }, teamName)

Leader (useSwarmPermissionPoller.ts 轮询):
读取自己邮箱 → 识别 permission_request 类型消息
→ 在 UI 渲染 ToolUseConfirm 对话框
→ 用户决策后 → sendPermissionResponseViaMailbox(workerName, resolution, requestId)
→ createPermissionResponseMessage({ request_id, subtype: 'success'/'error', ... })
→ writeToMailbox(workerName, ...)

Worker (inProcessRunner.ts 中的 pollInterval):
每 500ms 读取自己邮箱
→ isPermissionResponse(msg.text) 识别
→ processMailboxPermissionResponse({ requestId, decision, updatedInput, permissionUpdates })
→ 回调注册表中的 onAllow / onReject → resolve Promise

12.4 文件锁机制(lockfile)

写入 pending 目录时使用目录级锁:

// 锁文件路径:~/.claude/teams/{team}/permissions/pending/.lock
const lockFilePath = join(pendingDir, '.lock')
await writeFile(lockFilePath, '', 'utf-8') // 确保锁文件存在

let release: (() => Promise<void>) | undefined
try {
release = await lockfile.lock(lockFilePath) // 获取排他锁
await writeFile(pendingPath, jsonStringify(request, null, 2), 'utf-8')
return request
} finally {
if (release) await release() // 无论成功失败都释放
}

resolvePermission 也使用相同的锁,保证读-改-写原子性:

  1. 加锁
  2. 读取 pending/{id}.json
  3. 写入 resolved/{id}.json(加入决策字段)
  4. 删除 pending/{id}.json
  5. 释放锁

潜在死锁风险:若进程在持有锁期间崩溃,锁文件不会自动释放。lockfile 库通过 stale lock detection(检测 PID 是否存活)解决,但在 Node.js 层面若崩溃 PID 被复用,可能短暂误判。

12.5 Sandbox 权限扩展

permissionSync 还处理 sandbox 网络访问权限(与工具权限独立):

Worker sandbox runtime → sendSandboxPermissionRequestViaMailbox(host, requestId)
Leader → 渲染网络访问确认弹窗
Leader → sendSandboxPermissionResponseViaMailbox(workerName, requestId, host, allow)
Worker → 读取邮箱中 sandbox_permission_response → 恢复网络连接

十三、leaderPermissionBridge.ts — 内存桥接

13.1 为什么需要这个模块

in-process Teammate 运行在同一进程,因此可以直接调用 Leader UI 的权限弹窗,绕过邮箱获得更低延迟的权限体验。leaderPermissionBridge.tsmodule-level 单例实现 React 组件与非 React 代码之间的通信。

// 模块级单例(跨越 React 树的边界)
let registeredSetter: SetToolUseConfirmQueueFn | null = null
let registeredPermissionContextSetter: SetToolPermissionContextFn | null = null

// REPL 组件在 mount 时注册
export function registerLeaderToolUseConfirmQueue(setter): void {
registeredSetter = setter
}

// inProcessRunner 获取并调用
export function getLeaderToolUseConfirmQueue(): SetToolUseConfirmQueueFn | null {
return registeredSetter
}

13.2 快速路径 vs 邮箱回退

// createInProcessCanUseTool 内部逻辑
const setToolUseConfirmQueue = getLeaderToolUseConfirmQueue()

if (setToolUseConfirmQueue) {
// 快速路径:直接推送到 Leader 的 React 状态队列
// 零延迟,用户在 Leader UI 看到弹窗
return new Promise<PermissionDecision>(resolve => {
setToolUseConfirmQueue(queue => [...queue, {
assistantMessage, tool, description, input, toolUseID,
workerBadge: identity.color
? { name: identity.agentName, color: identity.color }
: undefined,
onAllow(updatedInput, permissionUpdates, feedback, contentBlocks) {
// 权限更新写回 Leader 的 toolPermissionContext
const setToolPermissionContext = getLeaderSetToolPermissionContext()
if (setToolPermissionContext && permissionUpdates.length > 0) {
const updatedContext = applyPermissionUpdates(...)
setToolPermissionContext(updatedContext, { preserveMode: true })
// preserveMode: 防止 Worker 的 acceptEdits 模式污染 Leader 的协调器模式
}
resolve({ behavior: 'allow', updatedInput, ... })
},
onReject(feedback) {
resolve({ behavior: 'ask', message: SUBAGENT_REJECT_MESSAGE_WITH_REASON_PREFIX + feedback })
},
recheckPermission() {
// 后台重新检查,若已自动允许则直接 resolve
...
}
}])
})
} else {
// 邮箱回退:Leader UI 不可用时(如 Leader 在后台)
// 通过 permissionSync.sendPermissionRequestViaMailbox 发送
// 500ms 轮询 Worker 邮箱等待响应
...
}

13.3 权限模式隔离

// setToolPermissionContext 时传入 preserveMode: true
setToolPermissionContext(updatedContext, { preserveMode: true })

这个细节极其重要:Leader 可能运行在 acceptEdits(自动接受编辑)模式,Worker 可能运行在 default 模式。若 Worker 的权限更新传回 Leader 时不 preserve mode,Leader 的 acceptEdits 会被覆盖为 default,导致 Leader 后续的编辑操作都需要确认。


十四、reconnection.ts — 会话恢复机制

14.1 两种启动场景

reconnection.ts 解决的核心问题:Teammate 进程崩溃后如何重新接入团队?

// 场景 1:全新 Spawn(CLI args 路径)
// main.tsx 在首次渲染前调用 computeInitialTeamContext()
export function computeInitialTeamContext(): AppState['teamContext'] | undefined {
const context = getDynamicTeamContext() // 读取 CLI args 中的环境变量
if (!context?.teamName || !context?.agentName) return undefined

const teamFile = readTeamFile(teamName) // 同步读取(React 渲染前)
const isLeader = !agentId // 无 agentId = Leader

return {
teamName, teamFilePath,
leadAgentId: teamFile.leadAgentId,
selfAgentId: agentId,
selfAgentName: agentName,
isLeader,
teammates: {},
}
}

// 场景 2:会话恢复(从 transcript 恢复)
export function initializeTeammateContextFromSession(
setAppState, teamName, agentName
): void {
const teamFile = readTeamFile(teamName)
const member = teamFile.members.find(m => m.name === agentName)
const agentId = member?.agentId // 从 TeamFile 恢复 agentId

setAppState(prev => ({
...prev,
teamContext: {
teamName, teamFilePath,
leadAgentId: teamFile.leadAgentId,
selfAgentId: agentId,
selfAgentName: agentName,
isLeader: false, // 恢复的 Teammate 不可能是 Leader
teammates: {},
},
}))
}

14.2 同步初始化的必要性

computeInitialTeamContext同步调用,在 React 首次渲染前执行。这避免了 useEffect 的延迟(至少 1 帧 = 16ms),确保:

  1. Heartbeat 功能从第一帧起就在正确的 teamContext 下运行
  2. Leader 的邮件轮询器不会因 teamContext 为 undefined 而跳过第一批消息

14.3 “Reconnection”名称的误导性

注意:reconnection.ts 并不处理网络重连(Swarm 没有网络连接)。它处理的是会话恢复(session resume):

  • Tmux pane 关闭后重新打开 Claude Code
  • 从 transcript 文件恢复上下文
  • 将 TeamFile 中的成员信息重新注入 AppState

实际的 Teammate”断线”场景(如 tmux pane 崩溃)由 cleanupSessionTeams() 处理(注册为进程退出钩子),不由 reconnection.ts 处理。


十五、Backend 抽象层深度解析

15.1 Backend 类型系统

// backends/types.ts
export type BackendType = 'tmux' | 'iterm2' | 'in-process'

export interface PaneBackend {
readonly type: BackendType
readonly displayName: string
readonly supportsHideShow: boolean

isAvailable(): Promise<boolean>
isRunningInside(): Promise<boolean>
createTeammatePaneInSwarmView(name, color): Promise<CreatePaneResult>
sendCommandToPane(paneId, command, useExternalSession?): Promise<void>
killPane(paneId, useExternalSession?): Promise<boolean>
hidePane?(paneId, useExternalSession?): Promise<boolean>
showPane?(paneId, targetWindowOrPane, useExternalSession?): Promise<boolean>
}

15.2 TmuxBackend — 外部进程 Teammate

TmuxBackend 实现了真正的多进程 Swarm,每个 Teammate 是独立的 Claude Code 进程,运行在独立的 tmux pane 中。

两种拓扑结构

拓扑 1:Leader 在 tmux 中(insideTmux=true)
┌─────────────────────────────────────────────┐
│ tmux window │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Leader │ │ Worker1 │ │ Worker2 │ │
│ │ (30%) │ │ (35%) │ │ (35%) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
第一个 Worker:split-window -h -l 70%(从 Leader 右分割)
后续 Worker:在已有 Teammate pane 中交替水平/垂直分割

拓扑 2:Leader 在普通终端中(insideTmux=false)
Leader 进程在普通终端,通过 -L socket 控制单独的 claude-swarm session
┌─────────────────────────────────────────────┐
│ claude-swarm session (hidden) │
│ swarm-view window │
│ ┌──────────┐ ┌──────────┐ │
│ │ Worker1 │ │ Worker2 │ │
│ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────┘
布局:tiled(均等分配,无 Leader 占位)

Pane 创建锁

// 模块级顺序锁(防止并发 split-window 导致布局混乱)
let paneCreationLock: Promise<void> = Promise.resolve()

function acquirePaneCreationLock(): Promise<() => void> {
let release: () => void
const newLock = new Promise<void>(resolve => { release = resolve })
const previousLock = paneCreationLock
paneCreationLock = newLock
return previousLock.then(() => release!) // 等待前一个完成后返回 release
}

async createTeammatePaneInSwarmView(name, color) {
const releaseLock = await acquirePaneCreationLock()
try {
// 串行执行,防止多个 split-window 同时发出导致布局计算错误
return await this.createTeammatePaneWithLeader(name, color)
} finally {
releaseLock()
}
}

Shell 初始化延迟

const PANE_SHELL_INIT_DELAY_MS = 200
// split-window 后 shell 需要加载 rc 文件(.bashrc, starship, oh-my-zsh 等)
// 若立即 send-keys 注入命令,命令可能在 shell ready 之前到达导致丢失
await waitForPaneShellReady() // sleep(200)

15.3 InProcess vs TmuxBackend 对比

维度 InProcess Backend TmuxBackend
进程隔离 同进程,AsyncLocalStorage 隔离 独立进程,OS 级隔离
通信延迟 权限:0ms(bridge 直接调用);消息:500ms 轮询 权限:500ms 轮询;消息:500ms 轮询
内存共享 共享进程堆(readFileState 需 clone) 完全独立
崩溃影响 Worker 崩溃可能影响 Leader Worker 崩溃对 Leader 透明
调试 可断点调试(同进程) 需 attach 到另一个进程
UI 呈现 Leader UI 内嵌 Teammate 进度 独立 pane
权限弹窗 推送到 Leader React 队列 发送到 Leader 邮箱
适用场景 轻量任务、API 使用、测试 重型并发、隔离性要求高

15.4 Backend 自动检测与选择

// backends/detection.ts
export async function isTmuxAvailable(): Promise<boolean> { ... }
export async function isInsideTmux(): Promise<boolean> { ... }

// 选择逻辑(简化):
// 1. 用户显式指定 --backend=in-process → InProcessBackend
// 2. isTmuxAvailable() = true → TmuxBackend(支持 insideTmux 和 external 两种拓扑)
// 3. isIterm2Available() = true → ITerm2Backend
// 4. 兜底 → InProcessBackend

十六、TeamFile 数据结构与并发控制

16.1 完整字段定义(teamHelpers.ts)

export type TeamFile = {
name: string // 团队名称
description?: string // 团队描述
createdAt: number // 创建时间戳(ms)
leadAgentId: string // Leader 的 agentId
leadSessionId?: string // Leader 的 sessionId(用于发现)
hiddenPaneIds?: string[] // 当前被隐藏的 pane ID 列表
teamAllowedPaths?: TeamAllowedPath[] // 所有 Worker 无需询问即可编辑的路径
members: Array<{
agentId: string // "name@team"(全局唯一)
name: string // 显示名
agentType?: string // 自定义 agent 类型
model?: string // 模型覆盖
prompt?: string // 初始 prompt
color?: string // UI 颜色
planModeRequired?: boolean // 是否强制 plan mode
joinedAt: number // 加入时间
tmuxPaneId: string // tmux pane ID(in-process 时为占位符)
cwd: string // 工作目录
worktreePath?: string // git worktree 路径
sessionId?: string // Claude Code session ID
subscriptions: string[] // 订阅的主题
backendType?: BackendType // 'tmux' | 'in-process' | 'iterm2'
isActive?: boolean // false=idle,undefined/true=active
mode?: PermissionMode // 当前权限模式(Leader 可远程修改)
}>
}

export type TeamAllowedPath = {
path: string // 绝对路径
toolName: string // "Edit" | "Write" 等
addedBy: string // 添加者 agentName
addedAt: number // 添加时间
}

16.2 读写并发控制策略

TeamFile 的并发控制是无锁的乐观策略

// 读:有同步和异步两个版本
readTeamFile(teamName) // sync:React 渲染路径(readFileSync)
readTeamFileAsync(teamName) // async:工具处理器(await readFile)

// 写:直接覆盖,无锁
writeTeamFile(teamName, teamFile) // sync
writeTeamFileAsync(teamName, teamFile) // async

为什么没有锁?

  1. 写入频率低:TeamFile 只在 spawn/shutdown/mode-change 时写,不是热路径
  2. 原子性假设:同一台机器上的文件写入在 OS 层面是原子的(单次 write syscall < 4KB)
  3. 冲突概率极低:Leader 是唯一的协调者,Worker 只在特定场景写(如 syncTeammateMode)

但有一个并发原子操作:setMultipleMemberModes,在单次文件读写中更新多个成员的 mode,避免多次写导致的覆盖:

export function setMultipleMemberModes(teamName, modeUpdates): boolean {
const teamFile = readTeamFile(teamName) // 一次读
const updateMap = new Map(modeUpdates.map(u => [u.memberName, u.mode]))
const updatedMembers = teamFile.members.map(member => {
const newMode = updateMap.get(member.name)
return newMode !== undefined && member.mode !== newMode
? { ...member, mode: newMode }
: member
})
if (anyChanged) writeTeamFile(teamName, { ...teamFile, members: updatedMembers }) // 一次写
}

16.3 Leader 如何用 TeamFile 协调 Worker

Leader 读 TeamFile.members → 获取所有 Worker 的 agentId
Leader 发送 shutdown 到 Worker.mailbox → Worker 处理后 removeMemberByAgentId()
Leader 修改 Worker mode → setMemberMode() → Worker 下一轮读取 task.permissionMode
Leader 查看 isActive → UI 显示 Worker 状态(绿色=active,灰色=idle)

十七、Mailbox 通信协议深度分析

17.1 消息格式(teammates/mailbox.ts 层)

// 写入邮箱的基础格式
type MailboxMessage = {
from: string // 发送者 agentName
text: string // 消息正文(可以是 JSON 字符串)
timestamp: string // ISO 8601
color?: string // 发送者颜色(UI 显示)
summary?: string // 摘要(显示在 peer DM 通知中)
read: boolean // 是否已读(Leader 维护此状态)
}

// 特殊消息类型(通过 text 字段的 JSON 结构区分)
// 1. 普通文本消息
text = "Hello, please work on X"

// 2. Idle 通知(Worker → Leader)
text = JSON.stringify({
type: 'idle_notification',
from: 'researcher',
idleReason: 'available' | 'interrupted' | 'failed',
summary?: string, // 最后一条 peer DM 摘要
completedTaskId?: string,
completedStatus?: 'resolved' | 'blocked' | 'failed',
failureReason?: string,
})

// 3. Shutdown 请求(Leader → Worker)
text = JSON.stringify({
type: 'shutdown_request',
from: 'team-lead',
reason?: string,
})

// 4. 权限请求(Worker → Leader mailbox)
text = JSON.stringify({
type: 'permission_request',
request_id: 'perm-...',
agent_id: 'researcher',
tool_name: 'Bash',
tool_use_id: 'toolu_xxx',
description: 'Run: npm test',
input: { command: 'npm test' },
permission_suggestions: [...],
})

// 5. 权限响应(Leader → Worker mailbox)
text = JSON.stringify({
type: 'permission_response',
request_id: 'perm-...',
subtype: 'success' | 'error',
error?: string,
updated_input?: {...},
permission_updates?: [...],
})

17.2 投递保证与顺序性

投递保证:At-Least-Once

  • 写入邮箱 = 写入文件系统(持久化)
  • 接收方通过 markMessageAsReadByIndex 标记为已读(幂等操作)
  • 若接收方在标记前崩溃,消息会被重新处理

顺序性:FIFO(同发送者)

  • 同一发送者的消息按时间戳顺序处理
  • 不同发送者之间无全局顺序(但 shutdown 优先于一切)

优先级覆盖顺序性

// waitForNextPromptOrShutdown 中:
// 1. 全局扫描找 shutdown(打破 FIFO)
// 2. 优先选 team-lead 消息(打破 FIFO)
// 3. 其余 FIFO

这是有意的设计:协调一致性 > 消息顺序。Leader 的 shutdown 信号必须即时送达,不能因大量 peer 消息而延迟。

17.3 已读标记与幂等性

// 读邮箱(返回所有消息,含 read 标志)
const allMessages = await readMailbox(agentName, teamName)

// 标记特定索引消息为已读(通过索引而非 ID,避免内容解析)
await markMessageAsReadByIndex(agentName, teamName, selectedIndex)

// 这是幂等操作:若消息已是 read=true,再次标记无副作用

为什么用索引而非消息 ID?

  • 避免对消息 text 进行解析(可能是任意 JSON)
  • 索引在单次 readMailbox 调用中稳定
  • 性能更好(无需哈希查找)

十八、spawnInProcess.ts — 状态注册与清理

18.1 killInProcessTeammate:原子状态转换

killInProcessTeammate 的实现展示了如何在 React 状态更新中安全地执行副作用:

export function killInProcessTeammate(taskId, setAppState): boolean {
let killed = false
let teamName: string | null = null
let agentId: string | null = null

// 在 state updater 中:仅做状态判断和内存操作
setAppState((prev: AppState) => {
const task = prev.tasks[taskId]
if (task.status !== 'running') return prev // 幂等检查

teamName = task.identity.teamName // 保存到外部变量
agentId = task.identity.agentId
task.abortController?.abort() // 触发 runAgent 退出
task.onIdleCallbacks?.forEach(cb => cb()) // 解锁等待者

killed = true
return {
...prev,
tasks: { ...prev.tasks, [taskId]: {
...task, status: 'killed', notified: true,
endTime: Date.now(), ...clearFields
}}
}
})

// 在 state updater 外:执行 I/O 副作用
if (teamName && agentId) {
removeMemberByAgentId(teamName, agentId) // 写 TeamFile(文件 I/O)
}

if (killed) {
void evictTaskOutput(taskId)
emitTaskTerminatedSdk(taskId, 'stopped', ...) // SDK 事件
setTimeout(evictTerminalTask.bind(null, taskId, setAppState), STOPPED_DISPLAY_MS)
}
}

为什么文件 I/O 在 state updater 外?React state updater 必须是纯函数(无副作用),且可能被 React 调用多次(严格模式)。CC 将所有 I/O 推到 updater 外部执行,用捕获变量传递必要信息。

18.2 Cleanup Registry

const unregisterCleanup = registerCleanup(async () => {
abortController.abort()
// 注意:不需要写 TeamFile,shutdown 时 cleanupSessionTeams() 会删除整个目录
})
taskState.unregisterCleanup = unregisterCleanup

// 进程正常退出时(SIGINT/SIGTERM):
// gracefulShutdown → 遍历所有注册的 cleanup → abortController.abort()
// → inProcessRunner 检测到 abort → 退出主循环 → 进入 finally 清理

十九、深度面试题(含系统设计)

Q1:如果一个 Worker 卡死(无限循环),Leader 如何检测并恢复?

CC 当前没有主动心跳检测机制。Worker 卡死后,Leader 会一直等待 Worker 的 Idle 通知(永远不会到来)。

恢复路径:

  1. 用户手动:Leader UI 中点击 Worker 的”Stop”按钮 → killInProcessTeammate()abortController.abort() → Worker 主循环检测到 abort 退出
  2. Escape 中断:用户按 Escape → currentWorkAbortController.abort()runAgent 的 for-await 循环 break → Worker 回到 Idle 等待
  3. Tmux Backend:可以直接 tmux kill-pane,OS 杀死进程

改进方向:增加 Worker→Leader 的定时心跳(如每 30s 写一次 isActive 到 TeamFile),Leader 侧检测超时(如 2min 无心跳则标记为 failed)。

Q2:permissionSync 的死锁风险分析

场景:两个 Worker(W1 和 W2)同时向 Leader 请求权限,Leader 正在等待其中一个的结果。

当前设计的安全性:两个权限请求是完全独立的 Promise,不会相互阻塞。Leader UI 的 ToolUseConfirmQueue 是一个数组,支持同时显示多个待确认项(按 Worker badge 区分)。

真正的死锁风险:文件系统锁。若 Worker 在持有 permissions/pending/.lock 时崩溃,进程 PID 释放,lockfile 库会通过过期检测(stale lock)解锁。但若 PID 在操作系统层面被复用(极罕见),可能出现误判,导致两个进程同时持有锁。

实际风险评级:低。因为 CC 的 lockfile 操作极快(微秒级),窗口极小。

Q3:为什么 inProcessRunner 要 clone ToolUseContext 再做 compaction?

compactConversation 内部会修改 readFileState(文件状态缓存),若直接用 Leader 的 toolUseContext,compaction 会清空 Leader 的文件缓存,导致 Leader 下次读文件时缓存 miss,性能下降,甚至行为异常。Clone 后,compaction 在隔离副本上操作,Leader 的缓存不受影响。

Q4:系统设计题 — 设计一个 10-Worker 的 Swarm,最小化权限请求延迟

问题分析:10 个 Worker 并发请求权限,每个请求需要用户交互,串行处理延迟 O(n)。

方案 A(现有设计的优化)

  • in-process backend:通过 leaderPermissionBridge 直接推送到 Leader React 队列,Leader UI 批量展示(已实现)
  • tmux backend:每个 Worker 维护独立的 500ms 轮询,Leader 批量渲染权限卡片(Worker badge 区分)

方案 B(理想设计)

  • 引入权限分级:将工具分为”沙箱安全”(只读命令)/ “需确认”(写操作)/ “危险”(删除/网络)
  • “沙箱安全”工具自动 approve,无需 Leader 交互
  • “需确认”工具批量合并(如 5s 内的相同类型权限合并为一个确认)
  • Leader 设置”团队信任级别”,信任级别高时 Worker 自主决策

CC 实际的 Bash Classifier:正是方案 B 的部分实现(feature flag BASH_CLASSIFIER),对 Bash 命令用分类器预判安全性,通过则自动 approve,省去 Leader 交互。

Q5:Teammate shutdown 为什么要让模型决策而不是直接终止?

这是 Swarm 协调哲学的核心:Agent 自治性。若 Leader 可以强制终止 Worker,会出现以下问题:

  1. Worker 可能正在进行文件写入的原子操作中途被终止,导致数据不一致
  2. Worker 可能有重要的清理工作(提交结果、更新任务状态)未完成
  3. 模型可以判断”任务是否真正完成”,人类(Leader 用户)可能提前发出 shutdown

让模型决策 shutdown 带来的收益:Worker 可以回复”我还有 2 个文件未完成修改,请给我 1 分钟”,Leader 收到后可以选择等待或强制中断(通过 lifecycle abort controller)。

强制终止的最终手段:killInProcessTeammate() 调用 abortController.abort(),这是用户通过 UI 触发的,绕过模型决策,直接中断。

打赏
  • 微信
  • 支付宝

评论