Claude Code 源码研究 09: Remote Control、Bridge 与 Multi-Agent

点击上方“慧响” 可以订阅哦!
本文字数: 14178字
阅读时间: 29分钟

本文回答十二个问题:
1. Claude Code 里的 remote-control、--remote、assistant、ssh 到底是不是同一套东西。
2. 为什么这个工程里“远程能力”不是单一路径,而是几种形态并存。
3. useReplBridge() 背后的 bridge,到底是在“把本地会话同步到 Web”,还是在“把 Web 会话接到本地”,还是两者都是。
4. claude remote-control 这个独立入口为什么不只是开一个 socket,而更像在注册一个可调度的本地执行环境。
5. bridgeMain.ts、replBridge.ts、initReplBridge.ts、sessionRunner.ts 这几层分别负责什么。
6. --remote / --teleport 这种 CCR remote session,和 bridge 模式相比,执行权、状态所有权、任务所有权分别落在哪一边。
7. claude assistant [sessionId] 为什么不是“第二个 REPL”,而是一个纯 viewer client。
8. claude ssh 为什么也是 remote mode,但它的结构和 CCR / bridge 都不同。
9. Claude Code 的 multi-agent 到底有几种实现形态,哪些是同步子代理,哪些是后台任务,哪些是真正意义上的 teammate。
10. AgentTool 为什么更像一个“agent routing plane”,而不是一个简单的工具函数。
11. task system 在 remote 与 multi-agent 设计里到底扮演什么角色。
12. 这一整套 remote + multi-agent 设计,最值得复用的工程模式和最明显的技术代价是什么。
如果上一章研究的是:
Claude Code 如何把能力扩展织进同一个运行时
那么这一章研究的就是:
Claude Code 如何把“执行”本身扩成一个分布式平面,让同一个 REPL 宿主能够接住本地执行、桥接执行、远程执行、SSH 执行,以及多代理协作执行。
这篇也必须很长。
因为到了这一层,已经不能再用“某个目录在做什么”来理解系统了。
remote-control 不只在 src/bridge/*。
remote session 不只在 src/remote/*。
multi-agent 不只在 src/tools/AgentTool/*。
它们一起横跨:
– src/entrypoints/cli.tsx– src/main.tsx– src/screens/REPL.tsx– src/bridge/– src/remote/– src/hooks/useReplBridge.tsx– src/hooks/useRemoteSession.ts– src/hooks/useSSHSession.ts– src/hooks/useAssistantHistory.ts– src/tools/AgentTool/– src/tasks/– src/utils/swarm/*
如果只盯着一个目录看,很容易得出几个错误结论:
1. 以为 remote-control 就是 claude.ai 控制本地 CLI 的那条桥。2. 以为 --remote 只是 remote-control 的另一个 UI 入口。3. 以为 multi-agent 只是在后台起几个 subagent。4. 以为 teammate 和 subagent 只是名字不同。5. 以为 task system 只是一个“进度列表”。
这些理解都不够准确。
更准确的说法是:
Claude Code 把执行拓扑做成了多形态并存的运行时体系。
在这个体系里:
– bridge 是一种“本地环境注册 + 会话桥接 + 控制请求转发”的机制。– CCR remote session 是一种“远程会话拥有执行权,本地 REPL 做客户端”的机制。– assistant viewer 是 CCR 机制的纯观察者形态。– SSH 是“本地 UI + 远程 CLI 执行 + 本地认证代理”的另一种 transport。– multi-agent 也不是单一路径,而是同步 subagent、本地后台 agent、远程隔离 agent、pane teammate、in-process teammate、fork subagent 几条路线并存。
所以这一章的一句话结论可以先写在最前面:
Claude Code 不是在一个本地 REPL 上外挂了几个 remote feature,而是在同一个 REPL 宿主之上,铺了一整张“执行拓扑层”。这个拓扑层既决定代码在哪跑,也决定消息从哪流、权限在哪确认、任务由谁持有、状态由谁恢复。
1. 先给结论
这一章最重要的结论有四个。
1.1 没有一个统一的 “remote mode”
Claude Code 里至少有五种要严格区分的 remote 形态:
1. 交互 REPL 上的 repl bridge 本地 REPL 正常运行,但后台维护一条 bridge 连接,让外部界面可以看到并接入这条会话。
2. 独立的 claude remote-control / claude bridge / claude rc 入口 不是简单开启某个桥接标志,而是显式注册一个本地环境,并根据工作项按需生成 Claude 子会话。
3. CCR remote session,也就是 --remote / --teleport 对应的云端会话 执行发生在远端,本地 REPL 只是 WebSocket + HTTP 客户端与交互宿主。
4. claude assistant [sessionId] viewer 模式 是 remote session 的只读/观察者变体,不拥有标题控制、不中断远端 agent,只是展示与补页历史。
5. claude ssh <host> 模式 UI 在本地,Claude CLI 在远端 Linux 主机执行,通过 SSH child process 和认证隧道协作。
这五种模式都涉及“远程”,但运行权、状态所有权、任务所有权、权限确认流都不一样。
1.2 没有一个统一的 “multi-agent”
Claude Code 至少实现了六种 agent 派生形态:
1. 同步 foreground subagent 当前 agent turn 内联调用 runAgent(),把结果同步返回给父 agent。
2. 本地异步后台 agentregisterAsyncAgent() 生成 local_agent task,异步执行并通过 task notification 回传。
3. 远程隔离 agentAgentTool 选择 isolation: "remote" 时,通过 remote session 启动,并登记成 remote_agent task。
4. worktree 隔离 agent 仍可能是本地 agent,但执行目录被切到新的 git worktree。
5. process-based teammate 通过 tmux / iTerm2 pane 或单独 Claude 进程启动,具有团队身份与消息通道。
6. in-process teammate 不起新系统进程,直接在同一 Node.js 进程内,以 AsyncLocalStorage 与 task state 做逻辑隔离。
再加上一个非常特别的分支:
7. fork subagent 不是一般意义上的“另一个 agent 类型”,而是带 forked context 语义的特化子代理路径。
所以,multi-agent 在 Claude Code 里不是一个 feature。
它是一整个 execution routing plane。
1.3 REPL 不是功能层,而是宿主层
src/screens/REPL.tsx 在这一章里会显得尤其关键。
因为无论是:
– 本地执行– repl bridge– remote session– assistant viewer– ssh session– direct connect– task panel– permission queue
最后都要收敛到 REPL 宿主之中。
它不是“聊天界面”。
它是一个能接纳多种 transport、多种消息源、多种任务宿主的运行时前端。
1.4 task system 是 remote 与 multi-agent 的共同抽象层
这一点非常重要。
如果没有 task system,Claude Code 会退化成一组互不相干的执行路径:
– 背景 agent 一套逻辑– 远端 agent 一套逻辑– teammate 一套逻辑– bridge 一套逻辑
task system 的作用就是把这些异构执行体,重新压回统一的用户表面:
– local_agent– remote_agent– in_process_teammate– 以及其它 shell / workflow / monitor 任务
这意味着 Claude Code 真正统一的并不是“底层怎么跑”,而是“跑完以后怎么在主会话里被管理、显示、通知、恢复、归档”。
2. 先把几个最容易混淆的 remote 概念拆开
如果这一章不先做去混淆,后面所有分析都会互相打架。
2.1 五种 remote 形态的总表
|
|
|
|
|
|
|
|---|---|---|---|---|---|
|
|
claude --remote-control
|
|
|
|
|
|
|
claude remote-control |
|
|
|
|
|
|
claude --remote
--teleport |
|
|
|
|
|
|
claude assistant [sessionId] |
|
|
|
|
|
|
claude ssh host |
|
|
|
|
表面看起来它们都叫“远程”。
但真正的差异点不在“网络有没有跨机器”,而在以下五个问题:
1. Claude 主执行循环到底在哪边。2. 谁持有主要 transcript。3. tool permission request 在哪边产生,又在哪边确认。4. 任务状态由哪边维护。5. 本地 REPL 是执行者、控制器、镜像端,还是纯 viewer。
2.2 不要把 bridge 和 remote session 混为一谈
这是最常见的误解。
从名字看,bridge 好像只是 remote session 的连接层。
但源码显示它们不是一个层级的东西。
bridge 更像:
– 让某个本地环境对外“可被接入、可被调度、可被绑定会话”的协议层与生命周期层。
而 CCR remote session 更像:
– 已经存在于远端基础设施中的会话,本地 REPL 只是它的客户端。
也就是说:
– bridge 重点是 把本地执行环境暴露出去。– remote session 重点是 把远端执行会话接进来。
一个是 outward bridge。
一个是 inward client。
两者都可以被 REPL 承载,但语义完全不同。
2.3 不要把 assistant viewer 当成另一个 remote mode
claude assistant [sessionId] 不是普通意义上的“再开一个远程 REPL”。
它的结构更接近:
– 对运行中的远端 agent 会话做 attach– 拉取最近历史– 向上滚动时增量补页– 实时订阅新事件
但它不会像普通 remote session 那样接管标题,不会像活跃控制端那样负责 interrupt,也不会把自己当作会话主人。
所以 assistant viewer 的关键语义不是 remote,而是 viewerOnly。
2.4 不要把 SSH 当成 bridge 的 transport
源码中 claude ssh 有自己独立的一整套路径。
它不是 bridge 的一个 transport backend。
它更像:
– 本地启动一个 SSH child process– 在远端检查 / 部署 Claude CLI– 建立 Unix socket 的反向代理,让远端 CLI 可以借本地认证– 本地 REPL 通过 SDK message 适配器消费远端输出
也就是说,SSH 模式不是把 bridge 搬到 SSH 上。
而是在做“远端执行 CLI,本地保留 UI 和权限交互”。
3. 从入口层看:remote 和 bridge 是一等公民,不是后挂功能
src/entrypoints/cli.tsx 和 src/main.tsx 给出的第一条证据就是:
这些模式都不是会话启动后才决定的“附加选项”。
它们在 argv 早期解析阶段就被识别、剥离、分流。
3.1 cli.tsx 的 fast-path 已经把 bridge 与 daemon 特判出来
src/entrypoints/cli.tsx 里有几个非常直白的 fast-path:
– --daemon-worker– claude remote-control– legacy alias: claude remote / claude sync / claude bridge– claude daemon
这说明:
1. bridge 不是普通 slash command。2. daemon 不是普通后台 feature。3. remote-control 已经被视作独立 entrypoint。
换句话说,在 Anthropic 的产品设计里,这些能力已经足够重要,重要到不应该经过完整主程序初始化后再决定。
3.2 main.tsx 对 assistant、ssh、--remote 的早期处理更说明问题
src/main.tsx 里还有几条很有代表性的早期 argv 逻辑:
– claude assistant [sessionId]– claude ssh <host> [dir]– --remote– --teleport– --remote-control / --rc
这些早期分流背后隐含着一个非常强的架构判断:
不同执行拓扑需要不同的初始化路径。
例如:
– claude ssh 需要在主 CLI 正常 argument handling 之前,先识别 host 与目标目录。– claude assistant 需要在普通 REPL 初始化前决定 viewerOnly 模式。– --remote 需要先创建远程会话,再决定本地 REPL 是不是带初始 prompt 的客户端。– --remote-control 需要在 AppState 初始化时就决定 replBridgeEnabled。
这不是 UI 选项层面的差异。
这是 runtime topology 层面的差异。
3.3 main.tsx 给不同 remote 路径打不同标签
源码里还能看到一些非常关键的环境与 client tagging:
– CLAUDE_CODE_ENTRYPOINT === 'remote'– CLAUDE_CODE_ENVIRONMENT_KIND === 'bridge'– session source tag remote-control
这些标记说明系统不只是“运行不同代码路径”,还在:
– 明确记录当前会话来自什么拓扑– 让后端服务知道它看到的是什么类型的 session– 让 analytics / policy / resume / reconnect 使用同一套拓扑识别信息
也就是说,remote / bridge 不是临时分支,而是产品级会话类型。
4. REPL bridge:正常本地会话上的“外连桥”
先看最容易被低估的一条线:useReplBridge()。
很多人看到这个 hook,第一反应会是:
“它是不是只是把聊天记录同步到某个外部界面?”
如果只看名字,很容易这么想。
但真实情况更复杂。
4.1 useReplBridge() 的职责不是单向镜像
src/hooks/useReplBridge.tsx 所做的事情,至少包含四层:
1. 当 AppState.replBridgeEnabled 为真时,初始化 bridge 连接。2. 把本地 REPL 新产生的 user / assistant 消息写入 bridge。3. 把 active bridge handle 发布成全局可访问对象。4. 在需要时接住远端控制请求。
这里最重要的是第 4 点。
如果只是单向镜像,它不需要成为一个全局 handle。
也不需要在 bridgeMessaging 里专门处理 control_request。
这说明 repl bridge 不是纯同步。
它是一条真正可交互的控制通道。
4.2 initReplBridge.ts 不是核心,而是 REPL 包装层
src/bridge/initReplBridge.ts 的意义在于:
– 它从 REPL / bootstrap 侧收集上下文– 处理 auth、gates、cwd、git context、title 等准备逻辑– 再把真正的核心初始化委托给 initBridgeCore()
这意味着 bridge 架构是分层的:
1. REPL wrapper 层 理解当前交互会话、当前工作目录、当前标题、当前 gate。
2. bridge core 层 负责环境注册、连接、控制消息、teardown、transport 生命周期。
这层分离的价值非常大。
因为一旦 core 不依赖完整 REPL bootstrap,它就能被别的宿主复用。
这正是后面 runBridgeHeadless() 可以复用它的原因之一。
4.3 replBridge.ts 里真正抽出来的是“bootstrap-free bridge core”
src/bridge/replBridge.ts 暴露出的不是简单的 UI helper。
它更像:
– 一个无需依赖 main.tsx 与交互入口的 bridge 运行核心
从设计上看,这一层已经把核心 bridge 生命周期抽出来了:
– 环境注册– 会话建立– ingress / transport 配置– control request / response– 消息写入– teardown
这一层甚至显式考虑了 daemon caller 的复用场景。
也就是说,作者清楚地知道:
bridge 不是 REPL 特性。
REPL 只是 bridge 的一种宿主。
4.4 bridge 的一条关键动作:system/init
useReplBridge() 成功连上 bridge 之后,会写一条很关键的初始化消息。
这条 system/init 并不是一个无关紧要的“hello”。
它本质上在对外声明当前本地会话的可控制面。
这里至少包含几个重要内容:
– 当前可用 slash commands,但会经过 isBridgeSafeCommand 过滤– 当前 skill 衍生出来的 command 集合– plugin 信息会做红化或裁剪– MCP clients / tools 也不会完整暴露
这告诉我们两个事实。
第一,bridge 的目标不是把整个本地运行时完全原封不动抛给外部客户端。
第二,bridge 对外暴露的是一个经过裁剪的控制面。
也就是说,remote-control 在产品语义上从一开始就不是“完全远程桌面”,而是“安全边界内的会话桥接”。
4.5 为什么 replBridgeHandle 要做成全局句柄
src/bridge/replBridgeHandle.ts 维护 active handle 的意义在于:
– bridge 相关逻辑并不都在 React 组件树内部– 某些工具、命令、后台逻辑也需要访问当前 bridge 会话
这本质上是一个经典运行时模式:
– React tree 负责宿主渲染与大部分状态订阅– 但某些 transport / command / tool path 必须能在树外部拿到 bridge 通道
一旦需要树外访问,就说明这条 bridge 已经不是 UI 附件。
它是全局运行时基础设施。
4.6 KAIROS / assistant mode 下的 perpetual bridge 特别能说明问题
源码里有个非常值得注意的注释:
– assistant mode 下 bridge 可以成为 perpetual session– claude.ai 可以跨 CLI 重启看到一条连续对话
这意味着 bridge 不只是“当前 CLI 生命周期里的镜像连接”。
它还在承担一种更长期的会话身份锚点。
这也是为什么源码里会涉及:
– bridge pointer– session ID publish– reconnect– persistent bridge state
换句话说,repl bridge 已经开始触及 session continuity,而不仅仅是 transport continuity。
5. standalone bridge:claude remote-control 本质上是在注册本地执行环境
如果说 useReplBridge() 代表的是:
在已有本地 REPL 上外挂一条可交互桥
那么 claude remote-control 代表的就是:
直接把本地环境作为一个 bridge host 启起来。
这两者看起来相关,但不是同一级抽象。
5.1 bridgeMain.ts 的主语不是“会话”,而是“环境”
src/bridge/bridgeMain.ts 给人的第一印象可能是很重。
原因正是因为它处理的不是单一会话,而是整个环境级别的生命周期。
从摘要里能看到它维护了很多环境级结构:
– active sessions map– start times– work IDs– compat session IDs– ingress tokens– timers– worktrees– titled sessions
这些东西如果只是为了同步一个本地会话,是不需要的。
它们之所以存在,是因为 standalone bridge 正在做更高一级的事情:
– 向外注册一个 environment– 接受工作分派– 为每个工作项生成或绑定 Claude session– 维护多个会话并发与回收
也就是说,remote-control 这套设计不是“把本地 Claude 暴露给网页”。
而更像:
把本地机器上的某个目录、repo、branch、认证上下文、容量与 spawn 策略,一起注册成一个可消费的执行环境。
5.2 BridgeConfig 揭示了 bridge 的真正对象模型
src/bridge/types.ts 非常关键。
因为 BridgeConfig 和 SpawnMode 基本直接定义了这个系统到底在抽象什么。
几个最重要的字段与类型是:
– SpawnMode = 'single-session' | 'worktree' | 'same-dir'– BridgeWorkerType = 'claude_code' | 'claude_code_assistant'– BridgeConfig 中包含: – cwd / dir – branch / repo – maxSessions – spawnMode – bridgeId – environmentId – API / session ingress URL – sessionTimeout
仅从这些字段就能看出,bridge 抽象的不是 socket。
bridge 抽象的是:
– 一个位置明确的工作环境– 一个带容量限制的会话池– 一个带 spawn 策略的执行宿主– 一个与后端服务协作的运行时实例
5.3 三种 spawn mode 说明 bridge 在管理“会话生产方式”
SpawnMode 的三个值特别说明问题:
1. single-session 一次只跑一个会话,结束即结束。更接近早期单会话桥。
2. same-dir 多个 session 共用当前目录。速度快,但容易互相踩工作树。
3. worktree 每个 session 进入独立 git worktree。隔离最好,但成本更高。
这里你会发现,bridge 的重点根本不在“消息怎么发”。
而在:
– 新会话如何诞生– 会话之间如何隔离– 环境容量怎么控制– 本地 repo 如何被多 session 安全消费
这已经是 job scheduler / execution fabric 的问题了。
5.4 bridgeMain.ts 的 first-run 交互暴露了产品判断
bridgeMain.ts 里关于 same-dir 和 worktree 的切换逻辑、首启选择逻辑、w 键切换逻辑非常有代表性。
它说明作者并没有把 spawn mode 当作内部实现细节。
相反,他们把它视为用户需要理解、也值得调参的运行时策略。
这背后的产品判断很明确:
– 多 session 已经不是边缘功能– 工作目录隔离会直接影响真实生产使用– 用户需要在效率与隔离之间选 tradeoff
如果只是一个“能让网页连上 CLI”的 feature,不会做到这个程度。
5.5 worktree 在 bridge 里的意义比在 agent 里更大
在 agent 场景里,worktree 更像一种隔离执行目录的技巧。
但在 bridge 场景里,worktree 其实承担的是:
– 环境级多租隔离
即使这不是传统 SaaS 语境下的多租户,它也已经在解决同类问题:
– 多个工作项同时落到同一 repo– 如何避免相互污染– 如何把“当前 shell 目录”与“实际执行 worktree 目录”区分开
源码里还有 bridgePointer 去扫描 worktree sibling 的逻辑。
这说明 bridge 系统已经把“同一 repo 下多个 worktree 都可能属于同一逻辑环境”的情况考虑进去了。
6. sessionRunner.ts:bridge 最关键的一层不是网络,而是子会话生成
standalone bridge 里最值得注意的,其实不是 poll loop,而是 sessionRunner.ts。
因为这里直接回答了一个核心问题:
bridge host 到底是怎么执行新会话的?
答案不是:
– bridge host 自己内嵌一个 query loop
而是:
– bridge host 再起一个 Claude CLI 子进程,并通过 SDK / stream-json 协议跟它对接。
6.1 createSessionSpawner() 说明 bridge 不是第二套执行引擎
src/bridge/sessionRunner.ts 里的 createSessionSpawner() 是 bridge 架构的关键锚点。
它揭示了一个非常重要的工程决策:
bridge 不自己重写 Claude 会话执行器。
它复用现有 CLI,可把子 session 当成标准化子进程来驱动。
启动参数里几个关键信号非常明显:
– --print– --sdk-url– --session-id– --input-format stream-json– --output-format stream-json– --replay-user-messages
这些参数组合说明 bridge host 的做法是:
1. 起一个标准 Claude CLI 子进程。2. 让它不要进入本地 TUI,而是进入 SDK / 流式 JSON 模式。3. 由 bridge host 负责把远端工作、权限控制、外部消息流,转换成对子进程的协议输入。4. 再把子进程产生的活动、工具申请、输出事件映射回 bridge 环境。
这非常重要。
因为它让 bridge 与普通 CLI 的执行语义尽量保持一致。
也意味着:
– 新功能一旦进主 CLI,bridge child session 就能继承更多能力– 不需要维护第二套 Claude 行为实现– 协议层与执行层分开演进
6.2 CLAUDE_CODE_ENVIRONMENT_KIND=bridge 暴露了会话自我认知
子进程环境变量里有一项非常关键:
– CLAUDE_CODE_ENVIRONMENT_KIND=bridge
这意味着被桥接生成出来的 Claude session 自己知道:
– 我不是一个普通本地交互式 CLI 会话– 我处在 bridge 环境之中
这个标签的意义至少有三层:
1. 让 session 上报或分析时可区分来源。2. 让某些逻辑知道当前权限确认、会话恢复、会话来源应该走 bridge 分支。3. 让后端与上层控制面可以识别这个 session 的出身。
6.3 control_request 证明 bridge 处理的是“权限交互穿透”
sessionRunner.ts 会专门检测 child NDJSON 中的 control_request。
这说明什么?
说明 bridge host 并不是把子会话纯粹当成黑盒命令执行器。
它必须理解某些高级协议事件,尤其是:
– 子 session 请求工具权限确认– 可能还有模型设置等控制请求
这说明 bridge 架构必须完成一件比“stdout 透传”高级得多的工作:
把子会话的交互式控制需求,提升为 bridge 层可以转发和响应的控制消息。
这也是为什么 bridgeMessaging.ts 专门围绕 control_request / control_response 设计。
6.4 replay-user-messages 是会话一致性的一个重要信号
启动参数里 --replay-user-messages 也很值得注意。
它表明 bridge 并不只是从某个瞬间开始实时接管。
它还关心:
– 子会话在建立时,是否需要重播之前用户消息以恢复上下文连贯性
这类设计通常只出现在把会话 continuity 当成一等问题的系统里。
如果是一次性短作业执行,其实完全不需要。
这再次说明 bridge 关注的是“可持续会话”,而不仅是“远程执行一个 prompt”。
7. runBridgeHeadless():bridge 的 daemon/headless 形态不是 REPL 的附属品
src/bridge/bridgeMain.ts 里还有一个非常关键的部分:
– runBridgeHeadless()
这部分几乎是在明说:
bridge 已经被做成一个可以脱离 TUI 的长期工作进程。
7.1 headless bridge 的定位很明确
源码注释里已经指出:
– 这是给 remoteControl daemon worker 的非交互入口– 不依赖 readline/TUI/process.exit– 配置来自 caller(例如 daemon.json)– auth 可以来自 supervisor IPC
这说明 headless bridge 不是一个“顺便支持”的隐藏模式。
它是 standalone bridge 的正式部署形态。
7.2 BridgeHeadlessPermanentError 暴露了 supervisor 协议思维
BridgeHeadlessPermanentError 的存在也很有意思。
因为它不是一个简单异常类型。
它实际上在表达一种 supervisor 协议:
– 有些错误不值得重试– 例如 trust 未接受、worktree 不可用、HTTP 非 HTTPS
这说明 headless bridge 的设计者已经把它当成一个会被 supervisor 管理、被重启、被看门狗照看的 worker 进程。
也就是说,这不是命令行工具的小技巧。
这是服务化思维。
7.3 headless bridge 同样构建 BridgeConfig
在 runBridgeHeadless() 里,仍然会构建完整的 BridgeConfig。
这意味着 daemon 形态和交互形态共享的是同一个 bridge 核心抽象,而不是两套平行实现。
这类“同一 core,多种宿主”的设计在整套 Claude Code 代码里反复出现:
– REPL 宿主– headless daemon 宿主– remote viewer 宿主– SSH 宿主
这也是为什么这个工程越来越像运行时平台,而不是终端 App。
8. CCR remote session:本地 REPL 作为远端会话的客户端
现在转到另一条必须严格区分的主线:--remote / --teleport。
8.1 这条线的本质不是 bridge,而是“远端 session 已经存在”
src/main.tsx 针对 --remote / --teleport 的处理逻辑非常清楚:
– 先创建或恢复远端会话– 再用 createRemoteSessionConfig(...) 构建配置– 本地 REPL 以 remote session client 身份启动
这里的关键差异是:
本地这边不再是执行宿主。
它只是:
– 展示宿主– 输入宿主– 权限确认宿主– 远端事件消费宿主
执行权落在远端会话。
8.2 RemoteSessionManager 的形态说明它是个完整客户端,不是 hook helper
src/remote/RemoteSessionManager.ts 的结构很值得注意。
它不是 React hook。
而是一个可独立使用的 session manager。
它负责至少这些事情:
– 建立 WebSocket 订阅– 通过 HTTP POST 发送用户消息– 接收并维护 permission request– 发送 permission response– 支持 interrupt– disconnect / reconnect
这说明在 CCR remote session 里,本地 REPL 与远端 session 之间不是一次性 RPC 关系。
而是一条长期双向控制连接。
8.3 SessionsWebSocket 是远端会话的事件主通道
src/remote/SessionsWebSocket.ts 则把 transport 层再单独抽掉。
这层的特征包括:
– 订阅 URL 明确以 session id 为中心– 每次连接都用新 token 鉴权– 有 reconnect delay / max reconnect attempts / session not found retry budget– 对某些 close code 做特殊处理,例如 unauthorized 或 compaction 期间的特殊状态
这些设计很像一个标准实时客户端 SDK。
而不像“给 REPL 用的临时封装”。
这说明 remote session 能力本身已经被抽象成一个可重用 transport client。
8.4 sdkMessageAdapter 揭示了本地 REPL 与远端协议并不是同一种消息体系
这一点非常关键。
src/remote/sdkMessageAdapter.ts 的存在说明:
– 远端 session 发来的,是 SDK 风格消息– 本地 REPL 消费的,是内部消息模型
所以中间必须有一层 adapter,把:
– assistant 消息– stream 事件– system / result / tool progress– compact 边界– viewer-only 需要显示的 tool_result / user text
重新映射到 REPL 能理解的事件世界。
这实际上透露了一个很深的架构事实:
REPL 并不是为远端协议原生设计的。
相反,是 remote layer 主动适配 REPL 的内部消息模型。
这也再次证明 REPL 是宿主,remote 是插件化接入层。
8.5 useRemoteSession() 把远端会话真正接到 REPL 里
src/hooks/useRemoteSession.ts 是 REPL 集成层。
这层最值得注意的点有几个。
第一,它会处理 echo filtering。
也就是说,本地刚发出去的用户消息,不能在远端回推回来时再原样重复显示。
这说明本地输入与远端广播之间存在典型的双写回流问题。
第二,它会处理 remote init。
即:
– 远端会话初始化消息可以带回 remote slash commands– 本地 REPL 再据此过滤本地 commands 表面
这很重要。
因为它说明在 remote session 模式下,本地 REPL 的用户命令表面也不是完全本地自治的。
它必须服从远端会话告诉它“这里能做什么”。
第三,它会维护 remoteBackgroundTaskCount。
这是一个非常关键的实现细节。
因为在 remote session 模式下,后台任务不在本地 AppState.tasks 里真正运行。
任务发生在远端 daemon / remote runtime 上。
所以本地只能根据事件,例如:
– task_started– task_notification
来近似维护一个远端任务计数或展示状态。
这和本地 task system 的对象持有方式完全不同。
第四,它会把 remote permission request 接回本地 ToolUseConfirm 队列。
也就是说,即使执行在远端,用户的权限确认体验仍然尽量落回本地 REPL。
这就是 Claude Code 在这条链路上的核心设计之一:
执行权可以远端化,但人机交互权尽量保持在本地。
9. claude assistant [sessionId]:远端会话的纯 viewer 客户端
如果说 --remote 是控制端。
那 claude assistant [sessionId] 更像旁路观察端。
9.1 viewerOnly 是这个模式的真正语义
RemoteSessionConfig 里有一个决定性的字段:
– viewerOnly?: boolean
在 main.tsx 里构造 assistant 模式时,会显式传入:
– viewerOnly = true
这个字段不是 UI 小开关。
它影响了很多核心行为。
9.2 viewerOnly 改变的不是显示,而是权责边界
从 useRemoteSession.ts 和 RemoteSessionManager.ts 的行为看,viewerOnly 至少意味着:
– 不负责 session title 更新– 不在 Ctrl+C / Escape 时向远端发 interrupt– 不启动某些 timeout warning– 更偏向把远端活动“展示出来”,而不是主导它
也就是说,viewerOnly 的本质不是“只读控件”。
而是:
不把自己当作该会话的主动控制端。
9.3 assistant viewer 的历史不是本地 transcript,而是 API 分页
src/hooks/useAssistantHistory.ts 与 src/assistant/sessionHistory.ts 组合起来,非常完整地说明了 assistant viewer 的历史机制:
– 首屏走 fetchLatestEvents(anchor_to_latest=true)– 向上滚动时走 fetchOlderEvents(before_id=...)– 每页返回 raw events,再通过 convertSDKMessage() 转成 REPL 消息
这非常重要。
它说明 assistant viewer 并不会像普通本地 session 那样依赖本地 transcript 文件或内存消息数组。
它是:
– 远端历史为真源– 本地按需分页拉取– 本地只做视图层合成
这进一步说明 assistant viewer 不是另一个 agent runtime。
它只是一个远端 runtime 的 viewer shell。
9.4 claude assistant 这条路径证明 REPL 可以退化成纯客户端
这点非常关键。
在前几章里,我们已经看到 REPL 作为宿主时非常重:
– 持状态– 承载 query– 承载 tasks– 承载 bridge– 承载 commands
但 assistant viewer 证明了另一件事:
REPL 也可以在极限情况下退化成一个展示壳。
这说明 REPL 的核心能力,不是“自己一定要跑 agent loop”,而是“能承载 agent loop 的交互表面”。
这就是平台型宿主与业务型页面的区别。
10. claude ssh:本地 UI,远端 CLI,认证从本地反向隧道
再看第三条很容易被误判的 remote 路线:SSH。
10.1 claude ssh 不是简单 remote shell
src/main.tsx 对 claude ssh <host> [dir] 的说明非常明确:
– 先 probe 远端– 如有需要部署 binary– 使用 SSH 启动远端 Claude– 建立 Unix socket 的 -R 反向转发,让 API auth 通过本地机器
这不是“ssh 到远端然后跑个命令”的朴素封装。
它更像一个有完整产品设计的远端执行模式。
10.2 SSH 模式的核心价值是“远端算力 / 文件,本地身份 / UI”
这条设计最漂亮的地方在于它切分了两个经常绑死在一起的东西:
1. 执行环境 远端 Linux 主机,远端文件系统,远端工具链。
2. 用户身份与交互表面 本地认证,本地 REPL,本地权限确认。
这意味着用户不需要:
– 在远端机器上重新登录 Claude– 在远端重建完整本地交互环境
只需要把执行搬过去。
10.3 useSSHSession() 说明 SSH 在 REPL 里也是外部会话接入
src/hooks/useSSHSession.ts 是一个很好的架构信号。
因为它和 useDirectConnect()、useRemoteSession() 是同一层级的 hook。
这说明 REPL 宿主看待 SSH 的方式不是:
– 进入一个特别模式后重写整个 UI
而是:
– 再接入一种新的外部会话 provider
在这一层里:
– 远端 child process 输出 SDK 消息– 本地通过 sdkMessageAdapter 转换– permission request 被转成与本地 UI 相兼容的确认流
这跟 CCR remote session 很像的一点是:
– 执行可以在外面– 但权限确认与交互体验仍尽量被收回本地
10.4 SSH 再次证明 remote 并不等于 cloud
Claude Code 里的 remote 不是单指“云端 Claude Code 服务”。
它也可以是:
– 你自己的 Linux 主机– 你自己的 repo– 你自己的远端工具链
这让整个 execution topology 的边界更广:
– cloud-hosted remote session– locally-bridged environment– ssh-hosted remote CLI
它们不是谁替代谁。
而是在覆盖不同的执行主权场景。
11. REPL 作为统一宿主:remote path 都是在往 REPL.tsx 插 provider
到这里就能更好理解 src/screens/REPL.tsx 的意义了。
11.1 REPL.tsx 同时接多种 remote provider
从代码结构看,REPL 同时接入:
– useRemoteSession(...)– useDirectConnect(...)– useSSHSession(...)– useAssistantHistory(...)– useReplBridge(...)
这非常像一个宿主应用在接多个 transport / runtime backend。
其中最关键的是:
– 它们不是彼此深度耦合– 它们大多通过 hook 返回的状态与回调接入 REPL 统一消息表面
所以 REPL 更像一个:
– message host– input host– state host– permission host– task host
11.2 activeRemote 这种设计说明 remote 是插槽,不是流程重写
REPL 会在:
– ssh remote– direct connect– remote session
之间选择活跃远程提供者。
这个选择模型非常说明问题。
如果 remote 只是一个布尔开关,根本不需要 provider 抽象。
只有当系统明确知道:
– 会有多种外部执行提供者– 它们共享部分 UI 合约– 但 transport 与生命周期不同
才会出现这种宿主层插槽设计。
11.3 handleRemoteInit() 说明 command surface 也会被远端协商
REPL 在收到 remote init 后,会根据远端会话给出的 slash commands 过滤本地命令表面。
这件事虽然看起来像 UX 细节,实际上透露出一条非常强的系统原则:
在外部执行模式下,本地 UI 展现的能力表面必须与远端 runtime 的真实能力对齐。
换句话说,本地 REPL 不能假装自己拥有一切。
这就是为什么 remote path 下很多 command / tool / permission surface 都会重新裁剪。
12. 从 remote 转到 multi-agent:Claude Code 不是只有“子代理”,而是一个 agent 派生体系
现在转向第二条主线:multi-agent。
如果只是看 AgentTool 的名字,容易误以为:
– 这是给模型调用的一个“帮我开个子 agent”工具
但源码展开以后,会发现它远比这复杂。
12.1 AgentTool 的输入字段已经暴露出它在做 routing
根据摘要,AgentTool 支持的输入字段包括:
– description– prompt– subagent_type– model– run_in_background– name– team_name– mode– isolation– cwd
其中最能说明问题的是这几项:
– run_in_background– name– team_name– mode– isolation
这些字段已经不是“开一个子 agent”所必需的最小输入。
它们对应的是不同执行拓扑:
– 后台还是前台– 子代理还是 teammate– 本地还是远端– 当前目录还是 worktree
也就是说,AgentTool 的本质不是 spawn。
而是 route。
12.2 一个工具里出现多条完全不同的 agent 路径,说明这里已经是 execution plane
根据 AgentTool.tsx 的主分支,至少有这些路线:
1. teammate spawn2. fork subagent3. remote isolation4. worktree isolation5. async local background6. sync foreground runAgent
如果一个工具内部只是在做参数解析,不会出现这种结构。
它之所以这么复杂,是因为它承担的不是能力本身,而是:
– 给模型提供一个统一入口– 再由运行时决定“这个请求应该变成哪种 agent 执行形态”
这非常像调度器入口,而不像普通工具。
13. 六类 agent 形态逐一拆开
这一节很关键。
因为如果不拆清楚,就会把后台 agent、remote agent、teammate 全部混成一锅。
13.1 同步 foreground subagent:最接近“传统子代理”
这是最容易理解的一类。
当没有选择后台、没有选 teammate、没有走 remote isolation、没有走特殊 fork 路径时,AgentTool 会直接调用 runAgent()。
这种模式的特点是:
– 当前父 agent 等待它完成– 子 agent 的结果同步返回– 执行仍在当前总体会话上下文之内
从产品语义上,这最像我们通常理解的:
– “帮我开一个子代理去做一件事,然后把结果带回来”
13.2 本地异步后台 agent:local_agent
当选择后台运行时,路线会切到:
– registerAsyncAgent(...)– 再通过生命周期逻辑异步执行
这一类的核心特点是:
– agent 不阻塞当前父 turn– 会被登记为 task– 完成后通过通知机制告诉主会话
这里非常重要的一点是:
它不是简单 setTimeout 或 fire-and-forget promise。
它是正式 Task:
– 有任务 ID– 有状态– 有 retain / evict / diskLoaded 等语义– 有通知格式– 可以与父任务/父 session 建立谱系关系
13.3 远程隔离 agent:remote_agent
当 isolation: "remote" 时,逻辑会进入另一条完全不同的路线:
– 先做 eligibility check– 通过 remote session / teleport 等机制启动远端 agent– 再登记成 remote_agent task
这类 agent 的几个核心特征是:
– 执行不在本地– 结果通过远端会话事件回流– task 展示是本地的,但 task 实体主要在远端推进
这类设计非常有意思。
因为它说明 multi-agent 已经和 remote execution 深度耦合。
在 Claude Code 里,agent 派生不是纯本地计算结构。
它也可以是分布式执行结构。
13.4 worktree 隔离 agent:本地 agent 的目录级隔离
createAgentWorktree(...) 这条路径说明:
即使不把 agent 发到远端,也可以给它一个隔离出来的 git worktree。
这类 agent 的价值主要有两个:
1. 避免同一 repo 上多个 agent 同时改同一 working tree。2. 让 agent 可以在更安全的环境里试验性修改文件。
需要特别注意的是:
worktree 不是一种 agent 类型。
它是一个与 agent 类型正交的隔离轴。
也就是说:
– 你可以有 foreground worktree agent– 也可以有 background worktree agent– 某些 fork path 也可能结合 worktree
13.5 process-based teammate:pane / child process 团队成员
这一类 agent 已经不再像普通 subagent。
它有更强的身份语义:
– name– team_name– 颜色– 团队上下文– 消息传递
spawnMultiAgent.ts 揭示了这套体系的几个特点:
– 可以使用 tmux backend– 可以使用 iTerm2 backend– 若不在 tmux 中,也可以建 claude-swarm session– 每个 teammate 都可以启动一个新的 Claude 进程或 pane– 启动命令会带: – --agent-id – --agent-name – --team-name – --agent-color – --parent-session-id – 可选 --plan-mode-required – 可选 --agent-type
这已经不是“开个子 agent”。
这是把 Claude Code 扩成一个 swarm 式的多实体协作界面。
13.6 in-process teammate:同进程多代理,而不是多进程多代理
这条线尤其值得单独强调。
因为它很容易被忽略。
src/utils/swarm/spawnInProcess.ts 与 src/tasks/InProcessTeammateTask/* 说明,Claude Code 并没有把 teammate 绑定死在 tmux / iTerm2 / 子进程模型上。
它还支持:
– 在同一个 Node.js 进程内创建 teammate– 使用 AsyncLocalStorage 与 teammate context 逻辑隔离– 把 teammate 作为 task 登记– 让它在同一进程里跑自己的连续 prompt loop
这非常重要。
因为它说明作者把“teammate”抽象成了逻辑实体,而不是 OS 进程实体。
进程只是实现之一。
13.7 fork subagent:一种特殊的上下文分叉语义
FORK_AGENT 路径说明 Claude Code 还支持另一种特殊子代理:
– 它不是普通 agent definition– 而是带 forked context / forked subagent memory 语义的一条路线
这类设计通常解决的问题是:
– 子代理需要共享父上下文的大部分内容– 但又要保留某种隔离、差异化或快照式的分叉执行语义
它本质上是“子代理”与“会话分叉”之间的折中模型。
14. runAgent():所有 agent 形态背后的通用执行内核
虽然 AgentTool 很像 routing plane,但真正执行 agent 的还是 runAgent()。
14.1 runAgent() 说明多代理不是另一套简化版 Claude
src/tools/AgentTool/runAgent.ts 支持的东西很多:
– agent-specific MCP server 初始化– parent / child context– forked subagent context– transcript 记录– hooks– allowedTools / permission mode
这意味着 subagent 并不是一个“比主 agent 弱很多的小号模型调用”。
相反,它基本也是完整 Claude 执行循环的一个变体。
14.2 agent 可以携带自己的 MCP server,这是非常强的设计
initializeAgentMcpServers() 尤其值得注意。
这意味着 agent definition 不只是 prompt 和 model 选择。
它还能带自己的工具世界。
这会带来几个非常强的效果:
1. 一个 agent 可以是领域特化执行体,而不只是 prompt persona。2. agent 可以按需引入额外外部能力,而不污染主会话工具池。3. inline MCP 定义还能在 agent 完成后清理掉,避免运行时泄露。
这让 Claude Code 的 agent 更像:
– 带专属能力上下文的临时执行角色
而不是简单的“换个系统 prompt 再问一次模型”。
14.3 transcript 与 lineage 说明 agent 被当成长期对象而不是函数调用
runAgent() 会记录 transcript、metadata、hook 状态,这说明在系统设计上,agent 不只是一次函数调用。
它还是:
– 可恢复的– 可观察的– 可归档的– 带 lineage 的执行实体
这正是后面 task system 能把它们统一管理的前提。
15. LocalAgentTask、RemoteAgentTask、InProcessTeammateTask:task system 如何把异构 agent 重新压平
前面说过,task system 是这章的共同抽象层。
现在展开讲。
15.1 LocalAgentTask:后台 agent 不是 promise,而是具名对象
src/tasks/LocalAgentTask/LocalAgentTask.tsx 的状态定义显示:
– 类型为 local_agent– 有进度 tracking– 有 queued messages– 有 retain / diskLoaded / evictAfter– 有 isBackgrounded
这些字段加在一起,说明本地后台 agent 被视为:
– 一个会长期存在、需要被 UI 管理、可能被恢复或驱逐的任务对象
而不是普通 JS 异步函数。
15.2 enqueueAgentNotification() 表明 task 不是内部对象,而是用户可见契约
本地 agent 完成时会生成结构化 <task-notification> 消息。
这意味着 task system 和主消息流之间不是隔离的。
任务会显式重新写回用户看得见的会话消息世界。
这正是为什么:
– 用户不用轮询后台 agent– 主会话也不需要一直阻塞等待
Claude Code 借 task-notification 机制,把“后台完成”重新映射回主线程叙事。
15.3 RemoteAgentTask:远端 agent 拥有自己的对象模型,不只是远端计数
src/tasks/RemoteAgentTask/RemoteAgentTask.tsx 很能说明 remote multi-agent 已经产品化了。
它并不是一个极简任务壳。
它包含:
– type: 'remote_agent'– sessionId– command– todoList– log– pollStartedAt– remoteTaskType– 甚至一些扩展类型: – ultraplan – ultrareview – autofix-pr – background-pr
这非常重要。
因为它说明 remote agent 不是单一 feature。
它已经成为远端任务平台的统一任务壳。
15.4 RemoteAgentTask 的 eligibility check 显示远端代理不是“想发就发”
摘要中提到的 precondition error 也很关键,例如:
– 未登录– 没有 remote environment– 不在 git repo– 没有 git remote– GitHub app 未安装– policy blocked
这些限制说明 remote agent 已经不是一个 purely technical execution choice。
它同时受:
– 产品权限– 仓库上下文– 账号绑定– 远端基础设施能力
共同约束。
这就是为什么远端 multi-agent 更像产品能力,而不是单纯工程实现。
15.5 InProcessTeammateTask:teammate 也是 task,而不是单纯 UI 视图
这点也特别重要。
即使是 in-process teammate,系统也不会把它只当作:
– 一个独立 message pane
而是把它显式登记成:
– in_process_teammate task
并为它提供:
– shutdown request– append message– inject user message– 根据 agentId 查找 teammate task
这说明 task system 不是“后台任务区”。
它实际上是整个多执行体系统的统一索引层。
16. spawnMultiAgent.ts:teammate 体系其实是另一套编排系统
如果说 AgentTool 管的是 agent routing。
那 spawnMultiAgent.ts 管的就是 teammate orchestration。
16.1 这里的第一原则是“后端可替换”
spawnMultiAgent.ts 会先做 backend 检测:
– tmux– iTerm2– in-process fallback
这说明 teammate 系统从一开始就没被绑死在某个 UI 平台上。
它要解决的是:
– 如何把多个协作 Claude 实体放出来
至于:
– 用 pane– 用进程– 用同进程逻辑实体
都是实现细节。
16.2 process-based teammate 保留了强烈的终端原生气质
tmux / iTerm2 backend 这一部分非常 Claude Code。
因为它不是在浏览器里做一个“多 agent 面板”。
而是延续了终端工作流:
– 当前 tmux window 里切 pane– 若不在 tmux 中,就建 claude-swarm session– iTerm2 原生分屏则走单独 backend
这和很多云端多 agent 产品完全不同。
它的立足点仍然是:
– 用户正在一个真实终端工作环境里写代码
16.3 in-process fallback 说明作者更看重“能力存在”,而不是“表现形态一致”
当 pane backend 不可用时,系统会回落到 in-process teammate。
这条 fallback 非常说明 Claude Code 的工程取向:
– 宁可牺牲一点“每个 teammate 都一个真实 pane”的可见性– 也要保住多 teammate 能力本身
也就是说,这个系统优先保护的是功能语义,而不是表现形式。
17. AgentTool 的几个关键路由判断值得单独讲
为了真正理解 multi-agent,这里要把 AgentTool 几个特别关键的判断讲透。
17.1 kairosEnabled 会强制 agent async
这条判断很有代表性。
assistant / kairos 模式下,agent 会被强制异步。
原因不是“体验更好”这种泛泛解释。
而是:
– daemon / assistant 输入队列必须保持响应– 如果 agent 同步阻塞,会卡住宿主输入流
这说明 agent 调度方式不是本地偏好问题。
它必须服从宿主 runtime 的响应性需求。
17.2 remote isolation 总是 backgrounded
这条规则也非常合理。
一旦 agent 被送到远端跑,它就天然更像 task,而不是当前 turn 内联子调用。
如果还强行做同步等待:
– UI 会变复杂– reconnect / detach / resume 语义会更难统一
所以 remote isolation 直接落成 task,是一个非常清醒的架构决定。
17.3 teammate spawn 不是子代理升级版,而是另一种协作结构
当存在 team_name && name 时,流程会走 spawnTeammate(...)。
这本质上说明:
– teammate 不是 anonymous subagent– 它有身份,有团队语义,有长期消息面,有可能长期存活
这和一次性 subagent 是两种不同对象。
17.4 fork path 说明“上下文克隆”是明确的一等需求
如果用户或模型没有指定特定 subagent type,而 fork gate 打开,就可能默认走 FORK_AGENT。
这透露出一个很重要的产品需求:
– 用户并不总是想选一个明确 agent 类型– 但系统希望提供一种“从当前上下文分叉出去做事”的捷径
这让 fork subagent 更像:
– 当前 agent 的分身
而 არა:
– 团队里的另一个固定角色
18. 从权限角度看:remote 与 multi-agent 都在做“执行权分离,但确认权回收”
这一节把前面几条线合在一起看,会更清楚。
18.1 bridge 中的 control_request
在 bridge child session 里,工具权限申请会变成 control_request。
这表示:
– 执行发生在子会话– 但权限确认权要抬升到 bridge 层
18.2 remote session 中的 permission request
在 CCR remote session 中,也是类似:
– 远端会话触发权限请求– 本地 RemoteSessionManager 与 useRemoteSession() 把它接回本地 UI– 用户在本地确认
18.3 SSH 中的 permission UI 仍尽量留在本地
SSH 模式也沿用同样模式:
– 远端 CLI 发出需要确认的动作– 本地 UI 以与本地工具近似的方式展示确认流
18.4 这是一条非常一致的产品原则
综合起来看,Claude Code 的产品原则很清楚:
– 执行权可以下沉、外移、远端化、子进程化– 但高风险确认权尽量收回到用户当前主交互表面
这条原则是整个分布式执行体系能成立的关键。
否则用户会很快丧失对系统的可控感。
19. 从状态角度看:remote 与 multi-agent 都在做“状态归属分层”
这一节继续从更抽象的角度总结。
19.1 本地 foreground session
这类最简单:
– transcript 在本地– tasks 在本地– title 在本地– commands / tools 在本地 runtime
19.2 repl bridge
这里开始出现分层:
– 主状态仍主要在本地– bridge 维护对外可接入会话身份– 外部客户端可以消费或协助控制该状态
19.3 standalone bridge
这里又提升一层:
– environment state 在 bridge host– child session state 在 Claude 子进程– 外部控制面通过 bridge API / ingress 间接操控
19.4 CCR remote session
这里状态归属更明显在远端:
– session state 在远端– 本地只保留 UI projection、权限对话状态、少量回显控制状态
19.5 assistant viewer
这里本地更进一步退化为:
– 历史分页缓存– 当前 UI 滚动与显示状态
19.6 remote agent
这里又有 task 级别分层:
– 真正执行状态在远端– 本地持一个映射后的 task 壳与通知表面
这个“状态归属分层”视角特别能帮助理解整个系统。
因为 Claude Code 从来没试图用一套状态模型平推所有执行形态。
它做的是:
– 允许状态真源在不同地方– 再靠 adapter / task / bridge / viewer,把这些状态投影回主交互表面
20. 这套设计最强的地方:Claude Code 把“在哪跑”与“怎么交互”解耦了
这一章到这里,可以开始总结优点。
我认为这套设计最强的点,不是 remote feature 多。
而是它系统地把两件常被绑死的事情解耦了:
1. 执行在哪里发生2. 用户主要在哪里交互
在很多工具里:
– 本地执行就本地 UI– 云端执行就网页 UI– SSH 执行就 SSH shell UI
Claude Code 没这么做。
它让:
– 本地 REPL 可以承载远端云会话– 本地 REPL 可以承载 SSH 会话– 外部客户端可以接入本地 bridge 会话– 多 agent 可以是本地、远端、同进程、异进程
一旦做出这种解耦,产品形态就会非常灵活。
21. 第二个强点:复用现有 CLI,而不是为 remote / multi-agent 重写第二套运行时
sessionRunner.ts 用 child CLI + stream-json,runAgent() 复用 agent 执行循环,SSH 也通过 SDK message 接回来。
这说明 Claude Code 一直在避免一件危险的事情:
– 为每个新拓扑重写一套 Claude 解释器
它更倾向于:
– 保持一个主 CLI / query runtime– 用不同 transport、adapter、宿主、task 壳,把它放到不同拓扑里
这是一个很成熟的工程选择。
因为它极大降低了行为漂移风险。
22. 第三个强点:task system 成为所有异步 / 分布式执行体的统一用户表面
如果没有 task system,这套设计会很快失控。
因为用户会看到:
– 有的 agent 在消息流里完成– 有的 agent 在远端列表里完成– 有的 teammate 在 pane 里跑– 有的后台任务没有统一入口
现在不是这样。
Claude Code 尽量把这些执行体统一还原成:
– 可查看– 可通知– 可恢复– 可归档– 可关联 lineage
的任务对象。
这也是为什么 task system 在这套产品里不是“附属功能”,而是中央整流器。
23. 代价一:概念边界非常多,学习成本高
这一章研究下来,最大的代价非常清楚。
就是概念边界真的很多:
– bridge– repl bridge– standalone bridge– remote session– assistant viewer– ssh session– direct connect– subagent– async agent– remote agent– teammate– in-process teammate– fork subagent– worktree isolation
这些名词不是 marketing 术语,而是源码里真实存在的不同拓扑。
这让系统很强。
但也让理解成本陡增。
23.1 许多 feature 名字彼此接近,但权责不同
例如:
– remote-control– --remote– assistant
都带 remote 感,但执行权完全不同。
对于读源码的人,如果不先建立拓扑脑图,很容易把几个分支误当成同一系统的变体。
24. 代价二:REPL 宿主越来越像总装配器
虽然前面一直说 REPL 是宿主,这是它的优点。
但反过来看,这也是一种集中复杂度。
因为 REPL 需要同时接住:
– 本地 query loop– 远端 session provider– ssh provider– assistant viewer history– repl bridge– permission queue– task panel– command filtering
这意味着 REPL 层天然会变得很重。
它必须知道很多“我当前到底是哪个拓扑”的判定。
这种集中化设计虽然能让用户体验统一,但也会让维护者面对一个越来越像 runtime orchestrator 的大组件。
25. 代价三:状态真源分散,恢复与一致性会变难
当系统允许:
– 本地 transcript 为真源– 远端 session 为真源– bridge environment 为真源– task sidecar 为真源
之后,恢复与一致性问题就会自然膨胀。
这就是为什么源码里会出现大量与这些主题相关的机制:
– reconnect– pointer– session id compat– lazy history fetch– remote background task count– task metadata restore
它们都在解决同一个根问题:
当执行不再只发生在一个进程里时,怎样仍然给用户一条连续可理解的会话叙事。
26. 代价四:权限确认流必须跨层穿透
分布式执行最难的地方之一,从来不是传消息,而是传“需要人确认”的消息。
Claude Code 在:
– bridge– remote session– ssh
里都显式处理了这一点。
这当然是优点。
但从工程成本看,这也意味着:
– 任何新的执行拓扑,只要想成为一等能力,就得把 permission request/response 流打通
也就是说,权限确认已经成为所有新 transport、新 agent mode 的硬依赖接口。
27. 这一章最值得复用的工程模式
如果把这一章抽象成可复用模式,我会选下面五条。
27.1 用宿主 + provider,而不是为每种执行拓扑复制 UI
REPL 承载多个 remote provider 的思路非常值得学。
27.2 用 child standard runtime,而不是为桥接重写第二套执行引擎
sessionRunner.ts 的 child CLI + stream-json 是很漂亮的复用方式。
27.3 允许状态真源分散,但强制把用户表面统一回 task / message host
这是这套系统能保持可用性的关键。
27.4 把高风险确认权回收到主交互表面
这对 remote AI coding 产品几乎是必须的。
27.5 把 teammate 抽象成逻辑实体,而不是进程实体
in-process teammate 的存在非常有启发性。
它说明多代理协作的核心不是“多窗口”,而是“多身份、多上下文、多通信通道”。
28. 这一章最值得警惕的技术债
如果从长期维护角度看,我会重点警惕三种技术债。
28.1 模式数量继续增长时,命令与能力裁剪矩阵会爆炸
现在已经有:
– local– bridge– remote– viewerOnly– ssh– assistant– kairos– daemon
一旦 commands / tools / permissions / tasks 的行为都依赖这些模式,就会逐步形成复杂矩阵。
28.2 远端事件适配层越多,消息语义漂移风险越高
有了:
– sdkMessageAdapter– bridge message layer– task notification mapping
之后,一旦内部消息协议进化,就必须保证多条 adapter 路线同步更新。
28.3 task system 会被越来越多职责吸附
现在 task system 已经不只是后台列表。
它承担的是多执行体统一表面。
随着产品功能继续扩张,task system 很容易继续膨胀成超大中心。
29. 回答开头的十二个问题
现在回到开头。
29.1 remote-control、--remote、assistant、ssh 不是同一套东西
它们共享的是:
– 本地 REPL 作为宿主– 远端或外部执行体需要被投影回主交互表面
但执行权、状态归属、transport、权限确认路径都不同。
29.2 远程能力是多形态并存,而不是单一路线
Claude Code 显然没有押注一种 remote 架构。
它同时支持:
– outward bridge– inward cloud session client– pure viewer– ssh execution
29.3 repl bridge 既是同步,也是控制入口
它不是单向镜像层。
它会处理初始化、安全裁剪、控制消息、全局 handle 与会话 continuity。
29.4 standalone bridge 是环境注册器 + 会话生成器
claude remote-control 的主语不是会话,而是本地环境。
29.5 bridge 核心层与 REPL 包装层是分开的
initReplBridge.ts 负责 REPL 上下文准备,replBridge.ts / core 负责真正 bridge 生命周期。
29.6 CCR remote session 的执行权在远端
本地 REPL 是客户端与 permission host。
29.7 assistant viewer 是纯观察者壳
它不拥有会话执行控制权,只负责展示与分页历史。
29.8 SSH 是本地 UI + 远端 CLI 执行
它与 bridge / remote session 都不同,是另一种 transport 设计。
29.9 multi-agent 不止一种
至少包括:
– sync subagent– async local agent– remote agent– worktree-isolated agent– process-based teammate– in-process teammate– fork subagent
29.10 AgentTool 是 execution routing plane
它并不只是在开子 agent。
它在决定 agent 该以哪种拓扑诞生。
29.11 task system 是异构执行体的统一用户表面
这是整个系统没有碎掉的原因。
29.12 这套设计的核心价值是“执行位置与交互位置解耦”
而核心代价是概念增殖、状态分层、一致性恢复复杂化。
30. 给这一章一个最终结论
如果要用一句最压缩的话概括这一章,我会写成:
Claude Code 并不是一个只能在本地终端里跑单一 agent 的工具,而是一个能够把同一套 agent runtime 投射到多种执行拓扑中的终端宿主。bridge 负责把本地环境暴露出去,remote session 负责把远端会话接进来,SSH 负责把执行搬到另一台机器,multi-agent 则把一次执行扩成一组协作执行体,而 task system 负责把这些异构执行体重新收敛成统一的用户表面。
这背后体现出的产品观非常明确:
– Claude Code 不是把“终端聊天”做强一点。– 它是在把“终端里的 agent runtime”做成一个可桥接、可远程、可团队化、可后台化的执行平台。
31. 下一篇应该看什么
这一章分析完以后,顺理成章的下一篇就应该进入:
– context budget– compact– snip– transcript– memory– session restore
也就是:
当执行拓扑已经被扩成这样以后,Claude Code 是怎么避免上下文爆炸、怎么维持会话连续性、怎么做 memory/session persistence 的。
所以下一篇应该进入:
10-memory-compaction-context-and-session-persistence.md
因为到了这一章以后,你会发现另一个核心问题已经浮出来了:
当系统允许本地、远端、桥接、后台 agent、viewer、SSH 全部共存时,
上下文和会话连续性到底如何不崩。
那正是下一篇的主题。

慧响精英荟开张啦~扫码加入,更有限时免费进入慧响星球的福利哦~(如二维码过期,请寻找最新文章底部或后台留言)
夜雨聆风