⚠️ 学习声明:本文档基于 Claude Code 2.1.88 源码分析整理,仅供个人学习研究使用,不做任何商业用途。
一、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_NAME、SWARM_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>.json(TeamFile),所有 Agent 共享读取:
type TeamFile = { name: string description?: string createdAt: number leadAgentId: string leadSessionId?: string hiddenPaneIds?: string[] 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 的上下文。
关键设计
runWithTeammateContext(teammateContext, async () => { await runAgent() })
|
完整生命周期
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)
|
进度追踪
const progressTracker = createProgressTracker() updateProgressFromMessage(message, progressTracker)
|
六、权限同步协议(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 { const context = getDynamicTeamContext() if (!context) return undefined const teamFile = readTeamFile(teamName) const isLeader = !agentId return { teamName, teamFilePath, leadAgentId, isLeader, ... } }
|
关键设计:TeamContext 在 React 首次渲染之前同步计算(避免 useEffect 延迟),确保恢复的 Leader 能立即感知其团队状态。
十、面试要点
Q:Swarm 如何解决多 Agent 的权限一致性问题?
Worker 无法直接修改用户的权限配置,所有需要用户确认的权限都通过 permissionSync 协议路由给 Leader,由 Leader UI 统一呈现给用户。Leader 的决策(包括”永远允许”)可以通过 permissionSuggestions 持久化回 Worker 的 session 权限配置。
Q:同一进程运行多个 Agent Loop,如何避免状态污染?
使用 AsyncLocalStorage(runWithTeammateContext()),使 getAgentId()、getTeamName() 等全局函数在不同 async 调用链中自动返回各自 Teammate 的值,无需显式传参。
Q:Swarm 的通信为什么用文件系统邮箱而不是内存队列?
- 跨进程(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 主循环
export async function spawnInProcessTeammate(config, context) { const agentId = formatAgentId(name, teamName) const taskId = generateTaskId('in_process_teammate') const abortController = createAbortController() const teammateContext = createTeammateContext({...})
const taskState: InProcessTeammateTaskState = { type: 'in_process_teammate', status: 'running', identity, prompt, model, abortController, permissionMode: planModeRequired ? 'plan' : 'default', isIdle: false, pendingUserMessages: [], messages: [], ... } registerTask(taskState, setAppState) return { success: true, agentId, taskId, abortController, teammateContext } }
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)
const wrappedInitialPrompt = formatAsTeammateMessage('team-lead', prompt, undefined, description)
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 AbortController(
config.abortController):终止整个 Teammate,由 killInProcessTeammate() 触发
- per-turn AbortController(
currentWorkAbortController):只中断当前轮次,用户按 Escape 触发,Teammate 仍保持存活进入 Idle 等待
阶段 4:Idle 等待(waitForNextPromptOrShutdown)
const POLL_INTERVAL_MS = 500
while (!abortController.signal.aborted) { if (task.pendingUserMessages.length > 0) { return { type: 'new_message', message, from: 'user' } }
await sleep(POLL_INTERVAL_MS)
for (let i = 0; i < allMessages.length; i++) { const parsed = isShutdownRequest(msg.text) if (parsed) { return { type: 'shutdown_request', request: parsed, originalMessage } } }
let selectedIndex = allMessages.findIndex(m => !m.read && m.from === TEAM_LEAD_NAME) if (selectedIndex === -1) { selectedIndex = allMessages.findIndex(m => !m.read) }
const taskPrompt = await tryClaimNextTask(taskListId, identity.agentName) }
|
消息优先级(从高到低):
- Shutdown 请求(防止 peer 消息洪水导致 shutdown 饥饿)
- in-memory pendingUserMessages(来自 UI 的直接注入,无延迟)
- Leader 邮件(协调指令,代表用户意图)
- Peer 邮件(其他 Worker 的 DM)
- TaskList 任务认领(兜底,Worker 自主找活干)
阶段 5:Shutdown 处理(模型决策)
case 'shutdown_request': currentPrompt = formatAsTeammateMessage( waitResult.request?.from || 'team-lead', waitResult.originalMessage, ) break
|
这是反直觉的设计:模型有权拒绝 shutdown。若模型认为任务未完成,可以拒绝 Leader 的关闭请求,继续工作后再 approve。
阶段 6:终止(Completion / Failure)
updateTaskState(taskId, task => ({ ...task, status: 'completed', notified: true, endTime: Date.now(), messages: task.messages?.length ? [task.messages.at(-1)!] : undefined, pendingUserMessages: [], abortController: undefined, }), setAppState) void evictTaskOutput(taskId) evictTerminalTask(taskId, setAppState) emitTaskTerminatedSdk(taskId, 'completed', { toolUseId, summary: identity.agentId }) unregisterPerfettoAgent(identity.agentId)
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)) { const isolatedContext: ToolUseContext = { ...toolUseContext, readFileState: cloneFileStateCache(toolUseContext.readFileState), onCompactProgress: undefined, setStreamMode: undefined, } const compactedSummary = await compactConversation(allMessages, isolatedContext, ...) contextMessages = buildPostCompactMessages(compactedSummary) allMessages.length = 0 allMessages.push(...contextMessages) 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 数据结构
{ id: string, workerId: string, workerName: string, workerColor?: string, teamName: string, toolName: string, toolUseId: string, description: string, input: Record<string, unknown>, permissionSuggestions: unknown[], status: 'pending' | 'approved' | 'rejected', resolvedBy?: 'worker' | 'leader', resolvedAt?: number, feedback?: string, updatedInput?: Record<string, unknown>, 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 目录时使用目录级锁:
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 也使用相同的锁,保证读-改-写原子性:
- 加锁
- 读取 pending/{id}.json
- 写入 resolved/{id}.json(加入决策字段)
- 删除 pending/{id}.json
- 释放锁
潜在死锁风险:若进程在持有锁期间崩溃,锁文件不会自动释放。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.ts 用 module-level 单例实现 React 组件与非 React 代码之间的通信。
let registeredSetter: SetToolUseConfirmQueueFn | null = null let registeredPermissionContextSetter: SetToolPermissionContextFn | null = null
export function registerLeaderToolUseConfirmQueue(setter): void { registeredSetter = setter }
export function getLeaderToolUseConfirmQueue(): SetToolUseConfirmQueueFn | null { return registeredSetter }
|
13.2 快速路径 vs 邮箱回退
const setToolUseConfirmQueue = getLeaderToolUseConfirmQueue()
if (setToolUseConfirmQueue) { 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) { const setToolPermissionContext = getLeaderSetToolPermissionContext() if (setToolPermissionContext && permissionUpdates.length > 0) { const updatedContext = applyPermissionUpdates(...) setToolPermissionContext(updatedContext, { preserveMode: true }) } resolve({ behavior: 'allow', updatedInput, ... }) }, onReject(feedback) { resolve({ behavior: 'ask', message: SUBAGENT_REJECT_MESSAGE_WITH_REASON_PREFIX + feedback }) }, recheckPermission() { ... } }]) }) } else { ... }
|
13.3 权限模式隔离
setToolPermissionContext(updatedContext, { preserveMode: true })
|
这个细节极其重要:Leader 可能运行在 acceptEdits(自动接受编辑)模式,Worker 可能运行在 default 模式。若 Worker 的权限更新传回 Leader 时不 preserve mode,Leader 的 acceptEdits 会被覆盖为 default,导致 Leader 后续的编辑操作都需要确认。
十四、reconnection.ts — 会话恢复机制
14.1 两种启动场景
reconnection.ts 解决的核心问题:Teammate 进程崩溃后如何重新接入团队?
export function computeInitialTeamContext(): AppState['teamContext'] | undefined { const context = getDynamicTeamContext() if (!context?.teamName || !context?.agentName) return undefined
const teamFile = readTeamFile(teamName) const isLeader = !agentId
return { teamName, teamFilePath, leadAgentId: teamFile.leadAgentId, selfAgentId: agentId, selfAgentName: agentName, isLeader, teammates: {}, } }
export function initializeTeammateContextFromSession( setAppState, teamName, agentName ): void { const teamFile = readTeamFile(teamName) const member = teamFile.members.find(m => m.name === agentName) const agentId = member?.agentId
setAppState(prev => ({ ...prev, teamContext: { teamName, teamFilePath, leadAgentId: teamFile.leadAgentId, selfAgentId: agentId, selfAgentName: agentName, isLeader: false, teammates: {}, }, })) }
|
14.2 同步初始化的必要性
computeInitialTeamContext 是同步调用,在 React 首次渲染前执行。这避免了 useEffect 的延迟(至少 1 帧 = 16ms),确保:
- Heartbeat 功能从第一帧起就在正确的 teamContext 下运行
- 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 类型系统
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 创建锁:
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!) }
async createTeammatePaneInSwarmView(name, color) { const releaseLock = await acquirePaneCreationLock() try { return await this.createTeammatePaneWithLeader(name, color) } finally { releaseLock() } }
|
Shell 初始化延迟:
const PANE_SHELL_INIT_DELAY_MS = 200
await waitForPaneShellReady()
|
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 自动检测与选择
export async function isTmuxAvailable(): Promise<boolean> { ... } export async function isInsideTmux(): Promise<boolean> { ... }
|
十六、TeamFile 数据结构与并发控制
16.1 完整字段定义(teamHelpers.ts)
export type TeamFile = { name: string description?: string createdAt: number leadAgentId: string leadSessionId?: string hiddenPaneIds?: string[] teamAllowedPaths?: TeamAllowedPath[] members: Array<{ agentId: string name: string agentType?: string model?: string prompt?: string color?: string planModeRequired?: boolean joinedAt: number tmuxPaneId: string cwd: string worktreePath?: string sessionId?: string subscriptions: string[] backendType?: BackendType isActive?: boolean mode?: PermissionMode }> }
export type TeamAllowedPath = { path: string toolName: string addedBy: string addedAt: number }
|
16.2 读写并发控制策略
TeamFile 的并发控制是无锁的乐观策略:
readTeamFile(teamName) readTeamFileAsync(teamName)
writeTeamFile(teamName, teamFile) writeTeamFileAsync(teamName, teamFile)
|
为什么没有锁?
- 写入频率低:TeamFile 只在 spawn/shutdown/mode-change 时写,不是热路径
- 原子性假设:同一台机器上的文件写入在 OS 层面是原子的(单次 write syscall < 4KB)
- 冲突概率极低: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 text: string timestamp: string color?: string summary?: string read: boolean }
text = "Hello, please work on X"
text = JSON.stringify({ type: 'idle_notification', from: 'researcher', idleReason: 'available' | 'interrupted' | 'failed', summary?: string, completedTaskId?: string, completedStatus?: 'resolved' | 'blocked' | 'failed', failureReason?: string, })
text = JSON.stringify({ type: 'shutdown_request', from: 'team-lead', reason?: string, })
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: [...], })
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 优先于一切)
优先级覆盖顺序性:
这是有意的设计:协调一致性 > 消息顺序。Leader 的 shutdown 信号必须即时送达,不能因大量 peer 消息而延迟。
17.3 已读标记与幂等性
const allMessages = await readMailbox(agentName, teamName)
await markMessageAsReadByIndex(agentName, teamName, selectedIndex)
|
为什么用索引而非消息 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
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() task.onIdleCallbacks?.forEach(cb => cb())
killed = true return { ...prev, tasks: { ...prev.tasks, [taskId]: { ...task, status: 'killed', notified: true, endTime: Date.now(), ...clearFields }} } })
if (teamName && agentId) { removeMemberByAgentId(teamName, agentId) }
if (killed) { void evictTaskOutput(taskId) emitTaskTerminatedSdk(taskId, 'stopped', ...) 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() }) taskState.unregisterCleanup = unregisterCleanup
|
十九、深度面试题(含系统设计)
Q1:如果一个 Worker 卡死(无限循环),Leader 如何检测并恢复?
CC 当前没有主动心跳检测机制。Worker 卡死后,Leader 会一直等待 Worker 的 Idle 通知(永远不会到来)。
恢复路径:
- 用户手动:Leader UI 中点击 Worker 的”Stop”按钮 →
killInProcessTeammate() → abortController.abort() → Worker 主循环检测到 abort 退出
- Escape 中断:用户按 Escape →
currentWorkAbortController.abort() → runAgent 的 for-await 循环 break → Worker 回到 Idle 等待
- 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,会出现以下问题:
- Worker 可能正在进行文件写入的原子操作中途被终止,导致数据不一致
- Worker 可能有重要的清理工作(提交结果、更新任务状态)未完成
- 模型可以判断”任务是否真正完成”,人类(Leader 用户)可能提前发出 shutdown
让模型决策 shutdown 带来的收益:Worker 可以回复”我还有 2 个文件未完成修改,请给我 1 分钟”,Leader 收到后可以选择等待或强制中断(通过 lifecycle abort controller)。
强制终止的最终手段:killInProcessTeammate() 调用 abortController.abort(),这是用户通过 UI 触发的,绕过模型决策,直接中断。
涉及源文件
src/tasks/InProcessTeammateTask/
src/utils/swarm/
src/utils/teammate.ts
src/utils/teammateMailbox.ts