Claude Code 源码分析Claude Code 源码分析
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
首页
源码统计
系统架构
UML 图表
工具系统
CodeGraph
  • 概览

    • Claude Code 源码分析
    • 源码统计
    • CodeGraph 图谱
  • 架构

    • 系统架构
    • UML 图表索引
    • 查询引擎
    • 核心流程
    • 消息系统
    • 状态管理
  • 功能模块

    • 工具系统
    • 斜杠命令
    • 服务层
    • MCP 协议
    • Skills 技能
    • 子代理系统
  • 分层深度

    • 入口层
    • UI / Ink 层
    • utils 基础设施
    • 桥接 / 远程
    • 上下文压缩
  • 原理与安全

    • 底层原理
    • 技术难点
    • 权限与安全
    • 内部机制
    • 遥测与分析
  • 深度专题

    • Hooks 系统
    • 插件系统
    • 记忆系统
    • API 通信层
    • Ink 终端 UI
    • 认证系统
    • 构建与发布
    • 术语表
  • 调用分析

    • 调用链分析
    • 核心文件索引
  • 模块详解

    • utils

      • 模块: utils
      • messages · 消息工厂与规范化
      • session-storage · JSONL 会话持久化
      • permissions · 工具权限决策
      • shell-hooks · 用户 Shell Hook 系统
    • components

      • 模块: components
      • REPL · 主屏编排
      • messages · 消息行渲染
      • PermissionRequest · 权限弹窗
      • PromptInput · 底部输入
    • services

      • 模块: services
      • api-claude · Anthropic API 流式与重试
      • mcp-client · MCP 连接与工具调用
      • compact · 上下文压缩与自动触发
      • analytics · GrowthBook、Datadog 与 1P 事件
    • tools

      • 模块: tools
      • tool-interface · Tool 契约与注册表
      • bash-tool · Shell 执行与权限
      • streaming-executor · 流式工具并发调度
      • agent-tool · 子 Agent 委派
    • commands

      • 模块: commands
      • command-registry · commands.ts 注册与分派
      • model-command · /model 模型选择
      • mcp-commands · /mcp 服务器管理
      • compact-memory-commands · /compact 与 /memory
    • ink

      • 模块: ink
      • Ink 渲染管线 · Screen 与终端输出
      • 终端事件 · resize、paste、stdin
      • Ink Hooks · 输入、搜索、终端状态
      • Ink 组件 · Box、Text、ScrollBox 原语
    • hooks

      • 模块: hooks
      • useCanUseTool · 权限 UI 接缝
      • 输入与快捷键 Hook
      • 合并态 Hook(MCP + 本地)
      • notifs 通知 Hook
    • bridge

      • 模块: bridge
      • repl-bridge · REPL 桥初始化与传输
      • bridge-messaging · 桥消息路由与入站处理
      • remote-bridge-core · env-less 核心与守护主循环
      • bridge-permissions-ui · 权限、API 与 TUI
    • cli

      • 模块: cli
      • Structured IO · NDJSON SDK 协议
      • CLI Transports · Session Ingress 传输层
      • CLI Handlers · 子命令懒加载实现
      • Update & Upload · 自更新与串行上传原语
    • screens

      • 模块: screens
      • REPL 屏 · Screen 类型与顶层路由
      • ResumeConversation · 会话恢复选择器
      • Doctor · 安装诊断全屏
    • entrypoints

      • 模块: entrypoints
      • cli-entrypoint · Bootstrap 与快路径
      • sdk-types · core / control / runtime 类型体系
      • mcp-entrypoint · MCP stdio 服务器
      • sandbox-types · 沙箱配置单一真相源
    • skills

      • 模块: skills
      • skills-loading · 磁盘加载与 bundled 注册表
      • bundled-skills · 内置 skill 与 initBundledSkills
      • mcp-skills · MCP prompt 转 skill
      • skill-tool-integration · SkillTool 与命令注册
    • types

      • 模块: types
      • message-types · Message 联合与 content blocks
      • tool-permission-types · Tool、Permission、Command 类型
      • api-sdk-types · API 与 Hooks 协议类型
      • misc-types · ids、plugin、generated 与其余类型
    • tasks

      • 模块: tasks
      • local-agent-task · 本地 Agent 与主会话后台化
      • remote-agent-task · 远程 CCR 与 In-Process Teammate
      • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
      • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybindings

      • 模块: keybindings
      • keybinding-registry · 注册、Provider 与 useKeybinding
      • default-bindings · 默认键位表与平台差异
      • command-bindings · command:* 动态斜杠命令绑定
      • vim-bindings · Vim 模式与 keybindings 边界
    • memdir

      • 模块: memdir
      • memdir-core · 路径、加载与 MEMORY.md
      • memory-extraction · extractMemories 与 SessionMemory
      • memdir-commands · /memory、/remember 与命令集成
    • state

      • 模块: state
      • app-state-core · store、AppState 类型与 Provider
      • app-state-selectors · selectors 与 onChangeAppState
      • teammate-state · 队友视图与 swarm 状态
      • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query

      • 模块: query
      • query config 与 deps · 配置快照与依赖注入
      • query tokenBudget · +500k 自动续跑
      • query transitions · Continue / Terminal 状态机
      • query stopHooks · Stop 事件与 turn 结束编排
  • 模块详解(扩展)

    • messages · 消息工厂与规范化
    • session-storage · JSONL 会话持久化
    • permissions · 工具权限决策
    • shell-hooks · 用户 Shell Hook 系统
    • REPL · 主屏编排
    • messages · 消息行渲染
    • PermissionRequest · 权限弹窗
    • PromptInput · 底部输入
    • api-claude · Anthropic API 流式与重试
    • mcp-client · MCP 连接与工具调用
    • compact · 上下文压缩与自动触发
    • analytics · GrowthBook、Datadog 与 1P 事件
    • tool-interface · Tool 契约与注册表
    • bash-tool · Shell 执行与权限
    • streaming-executor · 流式工具并发调度
    • agent-tool · 子 Agent 委派
    • command-registry · commands.ts 注册与分派
    • model-command · /model 模型选择
    • mcp-commands · /mcp 服务器管理
    • compact-memory-commands · /compact 与 /memory
    • Ink 渲染管线 · Screen 与终端输出
    • 终端事件 · resize、paste、stdin
    • Ink Hooks · 输入、搜索、终端状态
    • Ink 组件 · Box、Text、ScrollBox 原语
    • useCanUseTool · 权限 UI 接缝
    • 输入与快捷键 Hook
    • 合并态 Hook(MCP + 本地)
    • notifs 通知 Hook
    • repl-bridge · REPL 桥初始化与传输
    • bridge-messaging · 桥消息路由与入站处理
    • remote-bridge-core · env-less 核心与守护主循环
    • bridge-permissions-ui · 权限、API 与 TUI
    • Structured IO · NDJSON SDK 协议
    • CLI Transports · Session Ingress 传输层
    • CLI Handlers · 子命令懒加载实现
    • Update & Upload · 自更新与串行上传原语
    • REPL 屏 · Screen 类型与顶层路由
    • ResumeConversation · 会话恢复选择器
    • Doctor · 安装诊断全屏
    • cli-entrypoint · Bootstrap 与快路径
    • sdk-types · core / control / runtime 类型体系
    • mcp-entrypoint · MCP stdio 服务器
    • sandbox-types · 沙箱配置单一真相源
    • skills-loading · 磁盘加载与 bundled 注册表
    • bundled-skills · 内置 skill 与 initBundledSkills
    • mcp-skills · MCP prompt 转 skill
    • skill-tool-integration · SkillTool 与命令注册
    • message-types · Message 联合与 content blocks
    • tool-permission-types · Tool、Permission、Command 类型
    • api-sdk-types · API 与 Hooks 协议类型
    • misc-types · ids、plugin、generated 与其余类型
    • local-agent-task · 本地 Agent 与主会话后台化
    • remote-agent-task · 远程 CCR 与 In-Process Teammate
    • shell-workflow-tasks · Bash 后台、Workflow 与 stopTask
    • dream-monitor-tasks · Dream、Monitor MCP 与 pill 文案
    • keybinding-registry · 注册、Provider 与 useKeybinding
    • default-bindings · 默认键位表与平台差异
    • command-bindings · command:* 动态斜杠命令绑定
    • vim-bindings · Vim 模式与 keybindings 边界
    • memdir-core · 路径、加载与 MEMORY.md
    • memory-extraction · extractMemories 与 SessionMemory
    • memdir-commands · /memory、/remember 与命令集成
    • app-state-core · store、AppState 类型与 Provider
    • app-state-selectors · selectors 与 onChangeAppState
    • teammate-state · 队友视图与 swarm 状态
    • state-boundaries · bootstrap、sessionStorage、FileStateCache
    • query config 与 deps · 配置快照与依赖注入
    • query tokenBudget · +500k 自动续跑
    • query transitions · Continue / Terminal 状态机
    • query stopHooks · Stop 事件与 turn 结束编排
  • 工具详解

    • tool-interface · Tool 契约与注册表
    • tool-permission-types · Tool、Permission、Command 类型
    • 工具: Bash
    • 工具: PowerShell
    • 工具: Agent
    • 工具: LSP
    • 工具: FileEdit
    • 工具: FileRead
    • 工具: Skill
    • 工具: WebFetch
    • 工具: MCP
    • 工具: SendMessage
    • 工具: FileWrite
    • 工具: Config
    • 工具: Grep
    • 工具: Brief
    • 工具: ExitPlanMode
    • 工具: ToolSearch
    • 工具: NotebookEdit
    • 工具: TaskOutput
    • 工具: WebSearch
    • 工具: ScheduleCron

本章总览

REPL Remote Control 由 initReplBridge.ts(约 569 行)做 bootstrap 门控与标题推导,再委托 replBridge.ts 的 initBridgeCore(约 2400 行)完成环境注册、会话创建、work poll、传输挂载与 teardown。replBridgeTransport.ts 把 v1 HybridTransport 与 v2 SSE+CCRClient 统一为 ReplBridgeTransport 接口;replBridgeHandle.ts 暴露进程级单例句柄供斜杠命令与工具调用。本章要求你能从 /remote-control 或 useReplBridge 自动启动,反查到 OAuth 刷新、perpetual 指针恢复与传输 swap 的完整链路。

学完本章你应该能

  • 说明 initReplBridge 与 initBridgeCore 的职责分界及 bundle 隔离动机
  • 解释 ReplBridgeHandle 与 BridgeCoreHandle 的方法语义
  • 描述 createV1ReplTransport 与 createV2ReplTransport 的读写路径差异
  • 理解 getReplBridgeHandle 与 updateSessionBridgeId 的并发去重
  • 能在 replBridge 中定位 poll 错误退避与 reconnect-in-place 策略

核心概念(先读懂这些)

initReplBridge 不 import initBridgeCore 的同文件

initReplBridge.ts 动态 import replBridge.js 的 initBridgeCore,自身可安全被 print.ts SDK 路径加载;文件头注释写明:getCurrentSessionTitle 经 sessionStorage 牵出整个 commands 树,故 daemon 应只 import 不含 sessionStorage 的 core 文件。

BridgeCoreParams 显式注入一切 bootstrap 状态

initBridgeCore 不读 bootstrap/state:dir、branch、gitRepoUrl、title、createSession、archiveSession、toSDKMessages、onAuth401 均由 wrapper 填入。Agent SDK daemon(PR 4)与 REPL 共用同一 core,仅参数来源不同。

v2 写路径不经 SSETransport.write

replBridgeTransport.ts 注释强调:CCR v2 写出走 CCRClient.writeEvent → SerialBatchEventUploader,SSETransport.write() 的 URL 形状面向 Session-Ingress,不能用于 v2。读侧仍用 SSE;getLastSequenceNum 在 transport swap 时携带,避免服务端从 seq 0 重放全量历史。

建议学习步骤

  1. 阅读源码块 A:InitBridgeOptions 与 initReplBridge 门控
  2. 阅读源码块 B:ReplBridgeHandle 与 BridgeCoreParams
  3. 阅读源码块 C:initBridgeCore 注册与环境创建
  4. 阅读源码块 D:ReplBridgeTransport 类型与 v1 适配器
  5. 阅读源码块 E:createV2ReplTransport 注册与 JWT
  6. 阅读源码块 F:replBridgeHandle 全局指针
  7. 对照 useReplBridge.tsx 的 setReplBridgeHandle 时机

常见误区

注意

不要把 replBridge.ts 与 remoteBridgeCore.ts 的 env-less 路径混淆

注意

perpetual 模式只复用 source:repl 的 bridgePointer,standalone 指针不可复用

注意

toSDKMessages 未注入时调用 writeMessages 会 throw

注意

v1 getLastSequenceNum 恒为 0,seq 去重逻辑对 v1 为 no-op

在架构中的位置

Remote Control 在 REPL 内的典型启动路径:

用户 /remote-control 或 GrowthBook 自动桥接
  → useReplBridge useEffect / print.ts enableRemoteControl
  → initReplBridge(options)
  → [gate] isBridgeEnabledBlocking / OAuth / policy / 版本
  → initBridgeCore(params) 或 initEnvLessBridgeCore(params)
  → createBridgeSession + registerBridgeEnvironment
  → poll loop 收到 work → 挂载 ReplBridgeTransport
  → writeMessages / handleIngressMessage 双向同步
  → teardown → archiveSession + clearBridgePointer

initReplBridge 负责「能否启动」与「标题/metadata」;initBridgeCore 负责「如何维持连接」。斜杠命令、BriefTool 等通过 getReplBridgeHandle() 调用 sendControlRequest、writeSdkMessages,不必经过 React 树。

与 bridgeMain.ts 的区别:后者是独立进程 claude remote-control,用 sessionRunner spawn 子 CLI,同一套 bridgeApi 但无 Ink REPL 消息数组。

initReplBridge 门控链

initReplBridge 在调用 core 前串行执行多层防护,失败时 onStateChange?.('failed', detail) 并返回 null:

步骤检查用户可见 hint
1isBridgeEnabledBlocking()功能未开放
2getBridgeAccessToken()/login
3isPolicyAllowed('allow_remote_control')组织策略禁用
2a-2c跨进程 OAuth 死亡计数、主动 refresh、过期且不可刷新/login

setCseShimGate(isCseShimEnabled) 在入口设置 session ID 兼容 shim,daemon 路径可跳过。

标题推导优先级:initialName → sessionStorage /rename → initialMessages 最后一条人类 user → remote-control-{slug}。hasExplicitTitle 阻止 count-1/3 自动覆盖;onUserMessage 回调在第 1、3 条有效 user 消息时 PATCH 标题(Haiku generateSessionTitle)。

v1/v2 分支:isEnvLessBridgeEnabled() 为真时走 initEnvLessBridgeCore(见 remote-bridge-core 章);否则 initBridgeCore 并分别做 checkBridgeMinVersion / checkEnvLessBridgeMinVersion。

源码引用: src/bridge/initReplBridge.ts · 第 75–108 行(共 570 行)

  75| export type InitBridgeOptions = {
  76|   onInboundMessage?: (msg: SDKMessage) => void | Promise<void>
  77|   onPermissionResponse?: (response: SDKControlResponse) => void
  78|   onInterrupt?: () => void
  79|   onSetModel?: (model: string | undefined) => void
  80|   onSetMaxThinkingTokens?: (maxTokens: number | null) => void
  81|   onSetPermissionMode?: (
  82|     mode: PermissionMode,
  83|   ) => { ok: true } | { ok: false; error: string }
  84|   onStateChange?: (state: BridgeState, detail?: string) => void
  85|   initialMessages?: Message[]
  86|   // Explicit session name from `/remote-control <name>`. When set, overrides
  87|   // the title derived from the conversation or /rename.
  88|   initialName?: string
  89|   // Fresh view of the full conversation at call time. Used by onUserMessage's
  90|   // count-3 derivation to call generateSessionTitle over the full conversation.
  91|   // Optional — print.ts's SDK enableRemoteControl path has no REPL message
  92|   // array; count-3 falls back to the single message text when absent.
  93|   getMessages?: () => Message[]
  94|   // UUIDs already flushed in a prior bridge session. Messages with these
  95|   // UUIDs are excluded from the initial flush to avoid poisoning the
  96|   // server (duplicate UUIDs across sessions cause the WS to be killed).
  97|   // Mutated in place — newly flushed UUIDs are added after each flush.
  98|   previouslyFlushedUUIDs?: Set<string>
  99|   /** See BridgeCoreParams.perpetual. */
 100|   perpetual?: boolean
 101|   /**
 102|    * When true, the bridge only forwards events outbound (no SSE inbound
 103|    * stream). Used by CCR mirror mode — local sessions visible on claude.ai
 104|    * without enabling inbound control.
 105|    */
 106|   outboundOnly?: boolean
 107|   tags?: string[]
 108| }

源码引用: src/bridge/initReplBridge.ts · 第 110–162 行(共 570 行)

 110| export async function initReplBridge(
 111|   options?: InitBridgeOptions,
 112| ): Promise<ReplBridgeHandle | null> {
 113|   const {
 114|     onInboundMessage,
 115|     onPermissionResponse,
 116|     onInterrupt,
 117|     onSetModel,
 118|     onSetMaxThinkingTokens,
 119|     onSetPermissionMode,
 120|     onStateChange,
 121|     initialMessages,
 122|     getMessages,
 123|     previouslyFlushedUUIDs,
 124|     initialName,
 125|     perpetual,
 126|     outboundOnly,
 127|     tags,
 128|   } = options ?? {}
 129| 
 130|   // Wire the cse_ shim kill switch so toCompatSessionId respects the
 131|   // GrowthBook gate. Daemon/SDK paths skip this — shim defaults to active.
 132|   setCseShimGate(isCseShimEnabled)
 133| 
 134|   // 1. Runtime gate
 135|   if (!(await isBridgeEnabledBlocking())) {
 136|     logBridgeSkip('not_enabled', '[bridge:repl] Skipping: bridge not enabled')
 137|     return null
 138|   }
 139| 
 140|   // 1b. Minimum version check — deferred to after the v1/v2 branch below,
 141|   // since each implementation has its own floor (tengu_bridge_min_version
 142|   // for v1, tengu_bridge_repl_v2_config.min_version for v2).
 143| 
 144|   // 2. Check OAuth — must be signed in with claude.ai. Runs before the
 145|   // policy check so console-auth users get the actionable "/login" hint
 146|   // instead of a misleading policy error from a stale/wrong-org cache.
 147|   if (!getBridgeAccessToken()) {
 148|     logBridgeSkip('no_oauth', '[bridge:repl] Skipping: no OAuth tokens')
 149|     onStateChange?.('failed', '/login')
 150|     return null
 151|   }
 152| 
 153|   // 3. Check organization policy — remote control may be disabled
 154|   await waitForPolicyLimitsToLoad()
 155|   if (!isPolicyAllowed('allow_remote_control')) {
 156|     logBridgeSkip(
 157|       'policy_denied',
 158|       '[bridge:repl] Skipping: allow_remote_control policy not allowed',
 159|     )
 160|     onStateChange?.('failed', "disabled by your organization's policy")
 161|     return null
 162|   }

源码引用: src/bridge/initReplBridge.ts · 第 243–300 行(共 570 行)

 243|   // 4. Compute baseUrl — needed by both v1 (env-based) and v2 (env-less)
 244|   // paths. Hoisted above the v2 gate so both can use it.
 245|   const baseUrl = getBridgeBaseUrl()
 246| 
 247|   // 5. Derive session title. Precedence: explicit initialName → /rename
 248|   // (session storage) → last meaningful user message → generated slug.
 249|   // Cosmetic only (claude.ai session list); the model never sees it.
 250|   // Two flags: `hasExplicitTitle` (initialName or /rename — never auto-
 251|   // overwrite) vs. `hasTitle` (any title, including auto-derived — blocks
 252|   // the count-1 re-derivation but not count-3). The onUserMessage callback
 253|   // (wired to both v1 and v2 below) derives from the 1st prompt and again
 254|   // from the 3rd so mobile/web show a title that reflects more context.
 255|   // The slug fallback (e.g. "remote-control-graceful-unicorn") makes
 256|   // auto-started sessions distinguishable in the claude.ai list before the
 257|   // first prompt.
 258|   let title = `remote-control-${generateShortWordSlug()}`
 259|   let hasTitle = false
 260|   let hasExplicitTitle = false
 261|   if (initialName) {
 262|     title = initialName
 263|     hasTitle = true
 264|     hasExplicitTitle = true
 265|   } else {
 266|     const sessionId = getSessionId()
 267|     const customTitle = sessionId
 268|       ? getCurrentSessionTitle(sessionId)
 269|       : undefined
 270|     if (customTitle) {
 271|       title = customTitle
 272|       hasTitle = true
 273|       hasExplicitTitle = true
 274|     } else if (initialMessages && initialMessages.length > 0) {
 275|       // Find the last user message that has meaningful content. Skip meta
 276|       // (nudges), tool results, compact summaries ("This session is being
 277|       // continued…"), non-human origins (task notifications, channel pushes),
 278|       // and synthetic interrupts ([Request interrupted by user]) — none are
 279|       // human-authored. Same filter as extractTitleText + isSyntheticMessage.
 280|       for (let i = initialMessages.length - 1; i >= 0; i--) {
 281|         const msg = initialMessages[i]!
 282|         if (
 283|           msg.type !== 'user' ||
 284|           msg.isMeta ||
 285|           msg.toolUseResult ||
 286|           msg.isCompactSummary ||
 287|           (msg.origin && msg.origin.kind !== 'human') ||
 288|           isSyntheticMessage(msg)
 289|         )
 290|           continue
 291|         const rawContent = getContentText(msg.message.content)
 292|         if (!rawContent) continue
 293|         const derived = deriveTitle(rawContent)
 294|         if (!derived) continue
 295|         title = derived
 296|         hasTitle = true
 297|         break
 298|       }
 299|     }
 300|   }

ReplBridgeHandle 与 BridgeCoreParams

ReplBridgeHandle(replBridge.ts 导出)是 REPL 与 claude.ai 交互的稳定面向对象 API:

方法用途
writeMessages(Message[])过滤 eligible 消息后转 SDKMessage 写出
writeSdkMessages(SDKMessage[])daemon/SDK 直写,不经 Message 映射
sendControlRequest/Response/Cancel与 web 端 permission、interrupt 协议对齐
sendResult()回合结束信号
teardown()关闭 transport、archive、清指针

BridgeCoreParams 扩展了 createSession/archiveSession 注入、perpetual 崩溃恢复、initialSSESequenceNum 跨进程 seq 延续、outboundOnly、onUserMessage 标题策略等。注释明确:createSession 注入是因为 createSession.ts 懒加载 auth/model 在 bun outfile 下仍会内联整个 REPL 树。

BridgeState 四态:ready → connected → reconnecting → failed,经 onStateChange 驱动 useReplBridge UI 指示器。

源码引用: src/bridge/replBridge.ts · 第 70–81 行(共 2407 行)

  70| export type ReplBridgeHandle = {
  71|   bridgeSessionId: string
  72|   environmentId: string
  73|   sessionIngressUrl: string
  74|   writeMessages(messages: Message[]): void
  75|   writeSdkMessages(messages: SDKMessage[]): void
  76|   sendControlRequest(request: SDKControlRequest): void
  77|   sendControlResponse(response: SDKControlResponse): void
  78|   sendControlCancelRequest(requestId: string): void
  79|   sendResult(): void
  80|   teardown(): Promise<void>
  81| }

源码引用: src/bridge/replBridge.ts · 第 91–150 行(共 2407 行)

  91| export type BridgeCoreParams = {
  92|   dir: string
  93|   machineName: string
  94|   branch: string
  95|   gitRepoUrl: string | null
  96|   title: string
  97|   baseUrl: string
  98|   sessionIngressUrl: string
  99|   /**
 100|    * Opaque string sent as metadata.worker_type. Use BridgeWorkerType for
 101|    * the two CLI-originated values; daemon callers may send any string the
 102|    * backend recognizes (it's just a filter key on the web side).
 103|    */
 104|   workerType: string
 105|   getAccessToken: () => string | undefined
 106|   /**
 107|    * POST /v1/sessions. Injected because `createSession.ts` lazy-loads
 108|    * `auth.ts`/`model.ts`/`oauth/client.ts` and `bun --outfile` inlines
 109|    * dynamic imports — the lazy-load doesn't help, the whole REPL tree ends
 110|    * up in the Agent SDK bundle.
 111|    *
 112|    * REPL wrapper passes `createBridgeSession` from `createSession.ts`.
 113|    * Daemon wrapper passes `createBridgeSessionLean` from `sessionApi.ts`
 114|    * (HTTP-only, orgUUID+model supplied by the daemon caller).
 115|    *
 116|    * Receives `gitRepoUrl`+`branch` so the REPL wrapper can build the git
 117|    * source/outcome for claude.ai's session card. Daemon ignores them.
 118|    */
 119|   createSession: (opts: {
 120|     environmentId: string
 121|     title: string
 122|     gitRepoUrl: string | null
 123|     branch: string
 124|     signal: AbortSignal
 125|   }) => Promise<string | null>
 126|   /**
 127|    * POST /v1/sessions/{id}/archive. Same injection rationale. Best-effort;
 128|    * the callback MUST NOT throw.
 129|    */
 130|   archiveSession: (sessionId: string) => Promise<void>
 131|   /**
 132|    * Invoked on reconnect-after-env-lost to refresh the title. REPL wrapper
 133|    * reads session storage (picks up /rename); daemon returns the static
 134|    * title. Defaults to () => title.
 135|    */
 136|   getCurrentTitle?: () => string
 137|   /**
 138|    * Converts internal Message[] → SDKMessage[] for writeMessages() and the
 139|    * initial-flush/drain paths. REPL wrapper passes the real toSDKMessages
 140|    * from utils/messages/mappers.ts. Daemon callers that only use
 141|    * writeSdkMessages() and pass no initialMessages can omit this — those
 142|    * code paths are unreachable.
 143|    *
 144|    * Injected rather than imported because mappers.ts transitively pulls in
 145|    * src/commands.ts via messages.ts → api.ts → prompts.ts, dragging the
 146|    * entire command registry + React tree into the Agent SDK bundle.
 147|    */
 148|   toSDKMessages?: (messages: Message[]) => SDKMessage[]
 149|   /**
 150|    * OAuth 401 refresh handler passed to createBridgeApiClient. REPL wrapper

源码引用: src/bridge/replBridge.ts · 第 83–84 行(共 2407 行)

  83| export type BridgeState = 'ready' | 'connected' | 'reconnecting' | 'failed'
  84| 

initBridgeCore 生命周期概要

initBridgeCore 是 env-based 路径的核心状态机(约 2400 行),关键阶段如下:

注册:createBridgeApiClient + optional wrapApiForFaultInjection(ant)→ registerBridgeEnvironment。reuseEnvironmentId 来自 perpetual bridgePointer(仅 source:repl)。注册失败且存在 stale pointer 时 clearBridgePointer。

会话:createSession({ environmentId, title, gitRepoUrl, branch }) 返回 bridge session id;写入 bridgePointer 供崩溃恢复。支持 tryReconnectInPlace:环境 ID 匹配时对已有 session 调 reconnectSession,避免重复创建。

初始 flush:eligible 历史消息经 HTTP 批量 POST;期间 FlushGate 排队新消息(bridge-messaging 章)。previouslyFlushedUUIDs 防止跨 session 重复 UUID 毒化服务端 WS。

Poll 循环:pollForWork → acknowledgeWork → onWorkReceived 建 transport。Poll 连续失败时指数退避(2s→60s cap,15min give-up)。系统休眠检测阈值 = 2× connCapMs。

Transport:按 feature 选 v1 Hybrid 或 v2;swap 时保存 getLastSequenceNum()、比较 droppedBatchCount 检测静默丢批。

Teardown:archiveSession、clearBridgePointer(非 perpetual)、registerCleanup 钩子。

源码引用: src/bridge/replBridge.ts · 第 251–259 行(共 2407 行)

 251| /**
 252|  * Bootstrap-free core: env registration → session creation → poll loop →
 253|  * ingress WS → teardown. Reads nothing from bootstrap/state or
 254|  * sessionStorage — all context comes from params. Caller (initReplBridge
 255|  * below, or a daemon in PR 4) has already passed entitlement gates and
 256|  * gathered git/auth/title.
 257|  *
 258|  * Returns null on registration or session-creation failure.
 259|  */

源码引用: src/bridge/replBridge.ts · 第 260–296 行(共 2407 行)

 260| export async function initBridgeCore(
 261|   params: BridgeCoreParams,
 262| ): Promise<BridgeCoreHandle | null> {
 263|   const {
 264|     dir,
 265|     machineName,
 266|     branch,
 267|     gitRepoUrl,
 268|     title,
 269|     baseUrl,
 270|     sessionIngressUrl,
 271|     workerType,
 272|     getAccessToken,
 273|     createSession,
 274|     archiveSession,
 275|     getCurrentTitle = () => title,
 276|     toSDKMessages = () => {
 277|       throw new Error(
 278|         'BridgeCoreParams.toSDKMessages not provided. Pass it if you use writeMessages() or initialMessages — daemon callers that only use writeSdkMessages() never hit this path.',
 279|       )
 280|     },
 281|     onAuth401,
 282|     getPollIntervalConfig = () => DEFAULT_POLL_CONFIG,
 283|     initialHistoryCap = 200,
 284|     initialMessages,
 285|     previouslyFlushedUUIDs,
 286|     onInboundMessage,
 287|     onPermissionResponse,
 288|     onInterrupt,
 289|     onSetModel,
 290|     onSetMaxThinkingTokens,
 291|     onSetPermissionMode,
 292|     onStateChange,
 293|     onUserMessage,
 294|     perpetual,
 295|     initialSSESequenceNum = 0,
 296|   } = params

源码引用: src/bridge/replBridge.ts · 第 318–370 行(共 2407 行)

 318|   // 5. Register bridge environment
 319|   const rawApi = createBridgeApiClient({
 320|     baseUrl,
 321|     getAccessToken,
 322|     runnerVersion: MACRO.VERSION,
 323|     onDebug: logForDebugging,
 324|     onAuth401,
 325|     getTrustedDeviceToken,
 326|   })
 327|   // Ant-only: interpose so /bridge-kick can inject poll/register/heartbeat
 328|   // failures. Zero cost in external builds (rawApi passes through unchanged).
 329|   const api =
 330|     process.env.USER_TYPE === 'ant' ? wrapApiForFaultInjection(rawApi) : rawApi
 331| 
 332|   const bridgeConfig: BridgeConfig = {
 333|     dir,
 334|     machineName,
 335|     branch,
 336|     gitRepoUrl,
 337|     maxSessions: 1,
 338|     spawnMode: 'single-session',
 339|     verbose: false,
 340|     sandbox: false,
 341|     bridgeId: randomUUID(),
 342|     workerType,
 343|     environmentId: randomUUID(),
 344|     reuseEnvironmentId: prior?.environmentId,
 345|     apiBaseUrl: baseUrl,
 346|     sessionIngressUrl,
 347|   }
 348| 
 349|   let environmentId: string
 350|   let environmentSecret: string
 351|   try {
 352|     const reg = await api.registerBridgeEnvironment(bridgeConfig)
 353|     environmentId = reg.environment_id
 354|     environmentSecret = reg.environment_secret
 355|   } catch (err) {
 356|     logBridgeSkip(
 357|       'registration_failed',
 358|       `[bridge:repl] Environment registration failed: ${errorMessage(err)}`,
 359|     )
 360|     // Stale pointer may be the cause (expired/deleted env) — clear it so
 361|     // the next start doesn't retry the same dead ID.
 362|     if (prior) {
 363|       await clearBridgePointer(dir)
 364|     }
 365|     onStateChange?.('failed', errorMessage(err))
 366|     return null
 367|   }
 368| 
 369|   logForDebugging(`[bridge:repl] Environment registered: ${environmentId}`)
 370|   logForDiagnosticsNoPII('info', 'bridge_repl_env_registered')

源码引用: src/bridge/replBridge.ts · 第 244–246 行(共 2407 行)

 244| const POLL_ERROR_INITIAL_DELAY_MS = 2_000
 245| const POLL_ERROR_MAX_DELAY_MS = 60_000
 246| const POLL_ERROR_GIVE_UP_MS = 15 * 60 * 1000

ReplBridgeTransport 抽象

ReplBridgeTransport 把 replBridge 对底层传输的依赖收敛为单一接口,便于 v1/v2 互换而不改 2000+ 行核心逻辑。

读路径:setOnData 收到 JSON 行 → handleIngressMessage;setOnClose 触发重连;connect() 启动。

写路径:write / writeBatch 发送 StdoutMessage 形状事件;v2 在 flush() 里 drain 队列再 close。

可观测性:getStateLabel、isConnectedStatus、droppedBatchCount(v1 Hybrid 在 maxConsecutiveFailures 时递增)。

v2 专属:reportState(requires_action 指示 web 等待权限)、reportMetadata、reportDelivery(CCR 处理时间戳列)。

createV1ReplTransport:对 HybridTransport 的薄包装,getLastSequenceNum 固定返回 0。

createV2ReplTransport:异步 registerWorker,JWT 的 session_id claim 与 worker role 由服务端校验;OAuth token 不能直接用于 v2 端点(与 v1 故意用 OAuth 相反)。

源码引用: src/bridge/replBridgeTransport.ts · 第 11–70 行(共 371 行)

  11| /**
  12|  * Transport abstraction for replBridge. Covers exactly the surface that
  13|  * replBridge.ts uses against HybridTransport so the v1/v2 choice is
  14|  * confined to the construction site.
  15|  *
  16|  * - v1: HybridTransport (WS reads + POST writes to Session-Ingress)
  17|  * - v2: SSETransport (reads) + CCRClient (writes to CCR v2 /worker/*)
  18|  *
  19|  * The v2 write path goes through CCRClient.writeEvent → SerialBatchEventUploader,
  20|  * NOT through SSETransport.write() — SSETransport.write() targets the
  21|  * Session-Ingress POST URL shape, which is wrong for CCR v2.
  22|  */
  23| export type ReplBridgeTransport = {
  24|   write(message: StdoutMessage): Promise<void>
  25|   writeBatch(messages: StdoutMessage[]): Promise<void>
  26|   close(): void
  27|   isConnectedStatus(): boolean
  28|   getStateLabel(): string
  29|   setOnData(callback: (data: string) => void): void
  30|   setOnClose(callback: (closeCode?: number) => void): void
  31|   setOnConnect(callback: () => void): void
  32|   connect(): void
  33|   /**
  34|    * High-water mark of the underlying read stream's event sequence numbers.
  35|    * replBridge reads this before swapping transports so the new one can
  36|    * resume from where the old one left off (otherwise the server replays
  37|    * the entire session history from seq 0).
  38|    *
  39|    * v1 returns 0 — Session-Ingress WS doesn't use SSE sequence numbers;
  40|    * replay-on-reconnect is handled by the server-side message cursor.
  41|    */
  42|   getLastSequenceNum(): number
  43|   /**
  44|    * Monotonic count of batches dropped via maxConsecutiveFailures.
  45|    * Snapshot before writeBatch() and compare after to detect silent drops
  46|    * (writeBatch() resolves normally even when batches were dropped).
  47|    * v2 returns 0 — the v2 write path doesn't set maxConsecutiveFailures.
  48|    */
  49|   readonly droppedBatchCount: number
  50|   /**
  51|    * PUT /worker state (v2 only; v1 is a no-op). `requires_action` tells
  52|    * the backend a permission prompt is pending — claude.ai shows the
  53|    * "waiting for input" indicator. REPL/daemon callers don't need this
  54|    * (user watches the REPL locally); multi-session worker callers do.
  55|    */
  56|   reportState(state: SessionState): void
  57|   /** PUT /worker external_metadata (v2 only; v1 is a no-op). */
  58|   reportMetadata(metadata: Record<string, unknown>): void
  59|   /**
  60|    * POST /worker/events/{id}/delivery (v2 only; v1 is a no-op). Populates
  61|    * CCR's processing_at/processed_at columns. `received` is auto-fired by
  62|    * CCRClient on every SSE frame and is not exposed here.
  63|    */
  64|   reportDelivery(eventId: string, status: 'processing' | 'processed'): void
  65|   /**
  66|    * Drain the write queue before close() (v2 only; v1 resolves
  67|    * immediately — HybridTransport POSTs are already awaited per-write).
  68|    */
  69|   flush(): Promise<void>
  70| }

源码引用: src/bridge/replBridgeTransport.ts · 第 78–103 行(共 371 行)

  78| export function createV1ReplTransport(
  79|   hybrid: HybridTransport,
  80| ): ReplBridgeTransport {
  81|   return {
  82|     write: msg => hybrid.write(msg),
  83|     writeBatch: msgs => hybrid.writeBatch(msgs),
  84|     close: () => hybrid.close(),
  85|     isConnectedStatus: () => hybrid.isConnectedStatus(),
  86|     getStateLabel: () => hybrid.getStateLabel(),
  87|     setOnData: cb => hybrid.setOnData(cb),
  88|     setOnClose: cb => hybrid.setOnClose(cb),
  89|     setOnConnect: cb => hybrid.setOnConnect(cb),
  90|     connect: () => void hybrid.connect(),
  91|     // v1 Session-Ingress WS doesn't use SSE sequence numbers; replay
  92|     // semantics are different. Always return 0 so the seq-num carryover
  93|     // logic in replBridge is a no-op for v1.
  94|     getLastSequenceNum: () => 0,
  95|     get droppedBatchCount() {
  96|       return hybrid.droppedBatchCount
  97|     },
  98|     reportState: () => {},
  99|     reportMetadata: () => {},
 100|     reportDelivery: () => {},
 101|     flush: () => Promise.resolve(),
 102|   }
 103| }

源码引用: src/bridge/replBridgeTransport.ts · 第 105–118 行(共 371 行)

 105| /**
 106|  * v2 adapter: wrap SSETransport (reads) + CCRClient (writes, heartbeat,
 107|  * state, delivery tracking).
 108|  *
 109|  * Auth: v2 endpoints validate the JWT's session_id claim (register_worker.go:32)
 110|  * and worker role (environment_auth.py:856). OAuth tokens have neither.
 111|  * This is the inverse of the v1 replBridge path, which deliberately uses OAuth.
 112|  * The JWT is refreshed when the poll loop re-dispatches work — the caller
 113|  * invokes createV2ReplTransport again with the fresh token.
 114|  *
 115|  * Registration happens here (not in the caller) so the entire v2 handshake
 116|  * is one async step. registerWorker failure propagates — replBridge will
 117|  * catch it and stay on the poll loop.
 118|  */

全局句柄 replBridgeHandle

replBridgeHandle.ts 维护进程级 ReplBridgeHandle | null:

  • setReplBridgeHandle(h):init 完成时由 useReplBridge 设置;teardown 时清 null。
  • getReplBridgeHandle():工具、斜杠命令读取。
  • getSelfBridgeCompatId():toCompatSessionId(h.bridgeSessionId) 写入 session 记录,供 peerSessions 本地优先去重。

设计理由与 bridgeDebug.ts 相同:句柄闭包捕获创建时的 sessionId 与 getAccessToken,外部自行拼 token 可能导致 staging/prod 漂移。

updateSessionBridgeId 在 set/clear 时异步调用,失败静默 catch——不阻塞主路径。

源码引用: src/bridge/replBridgeHandle.ts · 第 5–36 行(共 37 行)

   5| /**
   6|  * Global pointer to the active REPL bridge handle, so callers outside
   7|  * useReplBridge's React tree (tools, slash commands) can invoke handle methods
   8|  * like subscribePR. Same one-bridge-per-process justification as bridgeDebug.ts
   9|  * — the handle's closure captures the sessionId and getAccessToken that created
  10|  * the session, and re-deriving those independently (BriefTool/upload.ts pattern)
  11|  * risks staging/prod token divergence.
  12|  *
  13|  * Set from useReplBridge.tsx when init completes; cleared on teardown.
  14|  */
  15| 
  16| let handle: ReplBridgeHandle | null = null
  17| 
  18| export function setReplBridgeHandle(h: ReplBridgeHandle | null): void {
  19|   handle = h
  20|   // Publish (or clear) our bridge session ID in the session record so other
  21|   // local peers can dedup us out of their bridge list — local is preferred.
  22|   void updateSessionBridgeId(getSelfBridgeCompatId() ?? null).catch(() => {})
  23| }
  24| 
  25| export function getReplBridgeHandle(): ReplBridgeHandle | null {
  26|   return handle
  27| }
  28| 
  29| /**
  30|  * Our own bridge session ID in the session_* compat format the API returns
  31|  * in /v1/sessions responses — or undefined if bridge isn't connected.
  32|  */
  33| export function getSelfBridgeCompatId(): string | undefined {
  34|   const h = getReplBridgeHandle()
  35|   return h ? toCompatSessionId(h.bridgeSessionId) : undefined
  36| }

与 commands / hooks 的衔接

commands.ts 中 BRIDGE_SAFE_COMMANDS 与 isBridgeSafeCommand 决定手机端能否触发本地斜杠:local-jsx(如 /model)被阻断,prompt 可展开,local 须在白名单(compact、clear、cost 等)。

/remote-control 命令(commands/bridge/)调用 initReplBridge,传入 initialMessages、onInboundMessage 等回调,把 web 输入注入 REPL 队列。

useReplBridge(hooks)在 mount 时 dynamic import initReplBridge,连接 onSetModel、onSetPermissionMode 等到 AppState;失败次数达 MAX_CONSECUTIVE_INIT_FAILURES 后停止重试。

print.ts SDK enableRemoteControl 路径无完整 messages 数组,count-3 标题推导退化为单条文本;仍可 outboundOnly 镜像会话到 claude.ai。

源码引用: src/bridge/initReplBridge.ts · 第 1–14 行(共 570 行)

   1| /**
   2|  * REPL-specific wrapper around initBridgeCore. Owns the parts that read
   3|  * bootstrap state — gates, cwd, session ID, git context, OAuth, title
   4|  * derivation — then delegates to the bootstrap-free core.
   5|  *
   6|  * Split out of replBridge.ts because the sessionStorage import
   7|  * (getCurrentSessionTitle) transitively pulls in src/commands.ts → the
   8|  * entire slash command + React component tree (~1300 modules). Keeping
   9|  * initBridgeCore in a file that doesn't touch sessionStorage lets
  10|  * daemonBridge.ts import the core without bloating the Agent SDK bundle.
  11|  *
  12|  * Called via dynamic import by useReplBridge (auto-start) and print.ts
  13|  * (SDK -p mode via query.enableRemoteControl).
  14|  */

源码引用: src/bridge/replBridge.ts · 第 1–39 行(共 2407 行)

   1| // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
   2| import { randomUUID } from 'crypto'
   3| import {
   4|   createBridgeApiClient,
   5|   BridgeFatalError,
   6|   isExpiredErrorType,
   7|   isSuppressible403,
   8| } from './bridgeApi.js'
   9| import type { BridgeConfig, BridgeApiClient } from './types.js'
  10| import { logForDebugging } from '../utils/debug.js'
  11| import { logForDiagnosticsNoPII } from '../utils/diagLogs.js'
  12| import {
  13|   type AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
  14|   logEvent,
  15| } from '../services/analytics/index.js'
  16| import { registerCleanup } from '../utils/cleanupRegistry.js'
  17| import {
  18|   handleIngressMessage,
  19|   handleServerControlRequest,
  20|   makeResultMessage,
  21|   isEligibleBridgeMessage,
  22|   extractTitleText,
  23|   BoundedUUIDSet,
  24| } from './bridgeMessaging.js'
  25| import {
  26|   decodeWorkSecret,
  27|   buildSdkUrl,
  28|   buildCCRv2SdkUrl,
  29|   sameSessionId,
  30| } from './workSecret.js'
  31| import { toCompatSessionId, toInfraSessionId } from './sessionIdCompat.js'
  32| import { updateSessionBridgeId } from '../utils/concurrentSessions.js'
  33| import { getTrustedDeviceToken } from './trustedDevice.js'
  34| import { HybridTransport } from '../cli/transports/HybridTransport.js'
  35| import {
  36|   type ReplBridgeTransport,
  37|   createV1ReplTransport,
  38|   createV2ReplTransport,
  39| } from './replBridgeTransport.js'

调试清单

现象优先查
桥从不启动initReplBridge 各 logBridgeSkip 原因;GrowthBook bridge flags
连上后立刻 failedregisterBridgeEnvironment 4xx;BridgeFatalError.errorType
手机消息重复recentInboundUUIDs / SSE seq 未携带
历史 flush 后乱序FlushGate 是否在 initial flush 期间 enqueue
401 风暴checkAndRefreshOAuthTokenIfNeeded;bridgeOauthDeadExpiresAt 计数
v2 切换失败createV2ReplTransport registerWorker;trusted device 头

日志前缀:[bridge:repl]、[bridge:api]、[bridge:transport]。Analytics:tengu_bridge_repl_env_registered、tengu_bridge_message_received 等。

本章小结与延伸

repl-bridge = REPL 侧远程控制的发动机。下一章 bridge-messaging,读 ingress 解析、出站过滤与 FlushGate 排队。 继续学习:

  • bridge-messaging
  • remote-bridge-core
Prev
模块: bridge
Next
bridge-messaging · 桥消息路由与入站处理