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

本章总览

remoteBridgeCore.ts(约 1008 行)实现 GrowthBook tengu_bridge_repl_v2 门控下的 env-less Remote Control:OAuth 直接 POST /v1/code/sessions 与 POST .../bridge 换取 worker JWT,无 Environments poll/ack。bridgeMain.ts(约 2999 行)是 claude remote-control 守护进程:注册环境、poll work、createSessionSpawner 拉起子 CLI、heartbeat、token 刷新。createSession.ts 提供 REPL 与 remote-control 共用的 createBridgeSession HTTP 封装。本章对比 env-based replBridge 与 standalone 多 session 架构。

学完本章你应该能

  • 区分 env-less、CCR v2 transport、bridgeMain 三概念
  • 说明 initEnvLessBridgeCore 的 /sessions + /bridge + transport 五步
  • 解释 runBridgeLoop 的 poll、spawn、heartbeat 与 backoff
  • 描述 createSessionSpawner 如何解析子进程 JSONL 与 permission_request
  • 理解 createBridgeSession 的 git source/outcome 与 events 包装

核心概念(先读懂这些)

env-less ≠ 没有 CCR

文件头注释:env-less 指去掉 Environments API 工作分派层;传输仍可用 CCR v2 /worker/ 端点。replBridge 在 v1 env 路径下也可通过 CLAUDE_CODE_USE_CCR_V2 使用 v2 transport,但仍有 poll。

bridgeMain 与子 CLI 的 OAuth 分工

守护进程用 environment_secret poll;子 session 经 work secret 获得 ingress JWT。v2 子进程不能用 OAuth 写 CCR(JWT 须含 session_id+worker role),故 heartbeat 401 时走 reconnectSession 触发服务端重新 dispatch。

createBridgeSession 懒加载 auth

函数体内 dynamic import auth、model、oauth——避免 bridge 模块 init 时拉取 commands 树;与 BridgeCoreParams 注入 createSession 的动机一致,但 REPL wrapper 仍用此文件 convenience API。

建议学习步骤

  1. 阅读源码块 A:remoteBridgeCore 文件头与 EnvLessBridgeParams
  2. 阅读源码块 B:initEnvLessBridgeCore 入口
  3. 阅读源码块 C:bridgeMain runBridgeLoop 启动
  4. 阅读源码块 D:heartbeat 与 token 刷新
  5. 阅读源码块 E:createSessionSpawner
  6. 阅读源码块 F:createBridgeSession POST 体

常见误区

注意

daemon/print 仍走 env-based initBridgeCore,非本章 env-less

注意

npm 安装下 spawn 必须带 scriptArgs,否则 node 把 --sdk-url 当 node 选项

注意

worktree spawn 模式不显示 bridge 级 branch(UI 误导)

注意

createBridgeSession 失败返回 null 非 throw,调用方须处理

在架构中的位置

Bridge 模块两条「进程级」入口:

┌─────────────────────────────────────┐
│ REPL 内嵌 (useReplBridge)           │
│  initReplBridge → core (v1 or v2)   │
│  单会话、共享 Message[]              │
└─────────────────────────────────────┘

┌─────────────────────────────────────┐
│ claude remote-control (bridgeMain)  │
│  runBridgeLoop → poll → spawn CLI   │
│  最多 N 并行 SessionHandle           │
└─────────────────────────────────────┘

remoteBridgeCore 服务第一种的 v2 分支;bridgeMain 仅第二种。二者共享 bridgeApi、bridgeMessaging、sessionRunner、createSession。

initEnvLessBridgeCore 流程

env-less 初始化(initEnvLessBridgeCore)精简为:

  1. POST /v1/code/sessions(OAuth,无 environment_id)→ session.id
  2. POST /v1/code/sessions/{id}/bridge → worker_jwt、expires_in、api_base_url、worker_epoch
  3. createV2ReplTransport(registerWorker 内嵌于构造)
  4. createTokenRefreshScheduler 在 JWT 过期前 5min proactive 调 /bridge
  5. SSE 401 → 用新 /bridge 凭证 rebuildTransport,携带 lastSequenceNum

无 pollForWork、acknowledgeWork、stopWork。FlushGate、BoundedUUIDSet、handleIngressMessage/handleServerControlRequest 与 v1 core 相同。

EnvLessBridgeParams 要求注入 toSDKMessages(无默认值);onUserMessage 标题 PATCH 策略由 initReplBridge wrapper 提供。

失败任一步返回 null,initReplBridge 向用户展示通用 initialization failed。

源码引用: src/bridge/remoteBridgeCore.ts · 第 1–29 行(共 1009 行)

   1| // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
   2| /**
   3|  * Env-less Remote Control bridge core.
   4|  *
   5|  * "Env-less" = no Environments API layer. Distinct from "CCR v2" (the
   6|  * /worker/* transport protocol) — the env-based path (replBridge.ts) can also
   7|  * use CCR v2 transport via CLAUDE_CODE_USE_CCR_V2. This file is about removing
   8|  * the poll/dispatch layer, not about which transport protocol is underneath.
   9|  *
  10|  * Unlike initBridgeCore (env-based, ~2400 lines), this connects directly
  11|  * to the session-ingress layer without the Environments API work-dispatch
  12|  * layer:
  13|  *
  14|  *   1. POST /v1/code/sessions              (OAuth, no env_id)  → session.id
  15|  *   2. POST /v1/code/sessions/{id}/bridge  (OAuth)             → {worker_jwt, expires_in, api_base_url, worker_epoch}
  16|  *      Each /bridge call bumps epoch — it IS the register. No separate /worker/register.
  17|  *   3. createV2ReplTransport(worker_jwt, worker_epoch)         → SSE + CCRClient
  18|  *   4. createTokenRefreshScheduler                             → proactive /bridge re-call (new JWT + new epoch)
  19|  *   5. 401 on SSE → rebuild transport with fresh /bridge credentials (same seq-num)
  20|  *
  21|  * No register/poll/ack/stop/heartbeat/deregister environment lifecycle.
  22|  * The Environments API historically existed because CCR's /worker/*
  23|  * endpoints required a session_id+role=worker JWT that only the work-dispatch
  24|  * layer could mint. Server PR #292605 (renamed in #293280) adds the /bridge endpoint as a direct
  25|  * OAuth→worker_jwt exchange, making the env layer optional for REPL sessions.
  26|  *
  27|  * Gated by `tengu_bridge_repl_v2` GrowthBook flag in initReplBridge.ts.
  28|  * REPL-only — daemon/print stay on env-based.
  29|  */

源码引用: src/bridge/remoteBridgeCore.ts · 第 89–131 行(共 1009 行)

  89| export type EnvLessBridgeParams = {
  90|   baseUrl: string
  91|   orgUUID: string
  92|   title: string
  93|   getAccessToken: () => string | undefined
  94|   onAuth401?: (staleAccessToken: string) => Promise<boolean>
  95|   /**
  96|    * Converts internal Message[] → SDKMessage[] for writeMessages() and the
  97|    * initial-flush/drain paths. Injected rather than imported — mappers.ts
  98|    * transitively pulls in src/commands.ts (entire command registry + React
  99|    * tree) which would bloat bundles that don't already have it.
 100|    */
 101|   toSDKMessages: (messages: Message[]) => SDKMessage[]
 102|   initialHistoryCap: number
 103|   initialMessages?: Message[]
 104|   onInboundMessage?: (msg: SDKMessage) => void | Promise<void>
 105|   /**
 106|    * Fired on each title-worthy user message seen in writeMessages() until
 107|    * the callback returns true (done). Mirrors replBridge.ts's onUserMessage —
 108|    * caller derives a title and PATCHes /v1/sessions/{id} so auto-started
 109|    * sessions don't stay at the generic fallback. The caller owns the
 110|    * derive-at-count-1-and-3 policy; the transport just keeps calling until
 111|    * told to stop. sessionId is the raw cse_* — updateBridgeSessionTitle
 112|    * retags internally.
 113|    */
 114|   onUserMessage?: (text: string, sessionId: string) => boolean
 115|   onPermissionResponse?: (response: SDKControlResponse) => void
 116|   onInterrupt?: () => void
 117|   onSetModel?: (model: string | undefined) => void
 118|   onSetMaxThinkingTokens?: (maxTokens: number | null) => void
 119|   onSetPermissionMode?: (
 120|     mode: PermissionMode,
 121|   ) => { ok: true } | { ok: false; error: string }
 122|   onStateChange?: (state: BridgeState, detail?: string) => void
 123|   /**
 124|    * When true, skip opening the SSE read stream — only the CCRClient write
 125|    * path is activated. Threaded to createV2ReplTransport and
 126|    * handleServerControlRequest.
 127|    */
 128|   outboundOnly?: boolean
 129|   /** Free-form tags for session categorization (e.g. ['ccr-mirror']). */
 130|   tags?: string[]
 131| }

源码引用: src/bridge/remoteBridgeCore.ts · 第 133–150 行(共 1009 行)

 133| /**
 134|  * Create a session, fetch a worker JWT, connect the v2 transport.
 135|  *
 136|  * Returns null on any pre-flight failure (session create failed, /bridge
 137|  * failed, transport setup failed). Caller (initReplBridge) surfaces this
 138|  * as a generic "initialization failed" state.
 139|  */
 140| export async function initEnvLessBridgeCore(
 141|   params: EnvLessBridgeParams,
 142| ): Promise<ReplBridgeHandle | null> {
 143|   const {
 144|     baseUrl,
 145|     orgUUID,
 146|     title,
 147|     getAccessToken,
 148|     onAuth401,
 149|     toSDKMessages,
 150|     initialHistoryCap,

bridgeMain runBridgeLoop

runBridgeLoop(bridgeMain.ts:141+)是 remote-control CLI 的主协程:

配置:BridgeConfig(dir、machineName、branch、maxSessions、spawnMode、workerType…)、BackoffConfig 连接/一般错误指数退避、getPollIntervalConfig。

注册:与 replBridge 相同 registerBridgeEnvironment,得到 environmentId + environmentSecret。

Poll 循环:pollForWork → 有 work 则 spawn 或 attach 已有 handle → acknowledgeWork。空 poll 递增计数,每 100 次打 debug。系统睡眠检测:poll 间隔 > 2× connCap 时重置错误预算。

多 session:GrowthBook tengu_ccr_bridge_multi_session 门控 --spawn/--capacity。isMultiSessionSpawnEnabled 用 blocking gate 避免冷启动误判。

spawnScriptArgs:bundled 二进制 execPath 即 claude;npm 安装须把 argv[1] cli.js 传给 spawn(#28334)。

shutdown:await pendingCleanups、SIGTERM grace、可选 resume 消息。

源码引用: src/bridge/bridgeMain.ts · 第 59–98 行(共 3000 行)

  59| export type BackoffConfig = {
  60|   connInitialMs: number
  61|   connCapMs: number
  62|   connGiveUpMs: number
  63|   generalInitialMs: number
  64|   generalCapMs: number
  65|   generalGiveUpMs: number
  66|   /** SIGTERM→SIGKILL grace period on shutdown. Default 30s. */
  67|   shutdownGraceMs?: number
  68|   /** stopWorkWithRetry base delay (1s/2s/4s backoff). Default 1000ms. */
  69|   stopWorkBaseDelayMs?: number
  70| }
  71| 
  72| const DEFAULT_BACKOFF: BackoffConfig = {
  73|   connInitialMs: 2_000,
  74|   connCapMs: 120_000, // 2 minutes
  75|   connGiveUpMs: 600_000, // 10 minutes
  76|   generalInitialMs: 500,
  77|   generalCapMs: 30_000,
  78|   generalGiveUpMs: 600_000, // 10 minutes
  79| }
  80| 
  81| /** Status update interval for the live display (ms). */
  82| const STATUS_UPDATE_INTERVAL_MS = 1_000
  83| const SPAWN_SESSIONS_DEFAULT = 32
  84| 
  85| /**
  86|  * GrowthBook gate for multi-session spawn modes (--spawn / --capacity / --create-session-in-dir).
  87|  * Sibling of tengu_ccr_bridge_multi_environment (multiple envs per host:dir) —
  88|  * this one enables multiple sessions per environment.
  89|  * Rollout staged via targeting rules: ants first, then gradual external.
  90|  *
  91|  * Uses the blocking gate check so a stale disk-cache miss doesn't unfairly
  92|  * deny access. The fast path (cache has true) is still instant; only the
  93|  * cold-start path awaits the server fetch, and that fetch also seeds the
  94|  * disk cache for next time.
  95|  */
  96| async function isMultiSessionSpawnEnabled(): Promise<boolean> {
  97|   return checkGate_CACHED_OR_BLOCKING('tengu_ccr_bridge_multi_session')
  98| }

源码引用: src/bridge/bridgeMain.ts · 第 111–118 行(共 3000 行)

 111| /**
 112|  * Returns the args that must precede CLI flags when spawning a child claude
 113|  * process. In compiled binaries, process.execPath is the claude binary itself
 114|  * and args go directly to it. In npm installs (node running cli.js),
 115|  * process.execPath is the node runtime — the child spawn must pass the script
 116|  * path as the first arg, otherwise node interprets --sdk-url as a node option
 117|  * and exits with "bad option: --sdk-url". See anthropics/claude-code#28334.
 118|  */

源码引用: src/bridge/bridgeMain.ts · 第 141–145 行(共 3000 行)

 141| export async function runBridgeLoop(
 142|   config: BridgeConfig,
 143|   environmentId: string,
 144|   environmentSecret: string,
 145|   api: BridgeApiClient,

heartbeat 与 token 刷新

多 session 模式下每个 active session 持有 workId + ingressToken。heartbeatActiveWorkItems 定期 POST heartbeat:

  • 401/403 → 记入 authFailedSessions,随后 reconnectSession 触发服务端重新 dispatch(CC-1263:否则 work 卡在 Redis PEL,poll 永远空)
  • 404/410 → fatal,环境过期
  • v2Sessions 集合标记的子会话:onRefresh 走 reconnect 而非直接 updateAccessToken

createTokenRefreshScheduler(jwtUtils.ts)在过期前回调;v1 child 更新 OAuth token,v2 child 触发 reconnect。

这与 REPL 内嵌桥的 proactive refresh(initReplBridge 2b)互补:守护进程须处理子进程 JWT 5h 级过期。

源码引用: src/bridge/bridgeMain.ts · 第 200–270 行(共 3000 行)

 200|    * poll delivers fresh work), or 'failed' if all failed for other reasons.
 201|    */
 202|   async function heartbeatActiveWorkItems(): Promise<
 203|     'ok' | 'auth_failed' | 'fatal' | 'failed'
 204|   > {
 205|     let anySuccess = false
 206|     let anyFatal = false
 207|     const authFailedSessions: string[] = []
 208|     for (const [sessionId] of activeSessions) {
 209|       const workId = sessionWorkIds.get(sessionId)
 210|       const ingressToken = sessionIngressTokens.get(sessionId)
 211|       if (!workId || !ingressToken) {
 212|         continue
 213|       }
 214|       try {
 215|         await api.heartbeatWork(environmentId, workId, ingressToken)
 216|         anySuccess = true
 217|       } catch (err) {
 218|         logForDebugging(
 219|           `[bridge:heartbeat] Failed for sessionId=${sessionId} workId=${workId}: ${errorMessage(err)}`,
 220|         )
 221|         if (err instanceof BridgeFatalError) {
 222|           logEvent('tengu_bridge_heartbeat_error', {
 223|             status:
 224|               err.status as unknown as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 225|             error_type: (err.status === 401 || err.status === 403
 226|               ? 'auth_failed'
 227|               : 'fatal') as AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS,
 228|           })
 229|           if (err.status === 401 || err.status === 403) {
 230|             authFailedSessions.push(sessionId)
 231|           } else {
 232|             // 404/410 = environment expired or deleted — no point retrying
 233|             anyFatal = true
 234|           }
 235|         }
 236|       }
 237|     }
 238|     // JWT expired → trigger server-side re-dispatch. Without this, work stays
 239|     // ACK'd out of the Redis PEL and poll returns empty forever (CC-1263).
 240|     // The existingHandle path below delivers the fresh token to the child.
 241|     // sessionId is already in the format /bridge/reconnect expects: it comes
 242|     // from work.data.id, which matches the server's EnvironmentInstance store
 243|     // (cse_* under the compat gate, session_* otherwise).
 244|     for (const sessionId of authFailedSessions) {
 245|       logger.logVerbose(
 246|         `Session ${sessionId} token expired — re-queuing via bridge/reconnect`,
 247|       )
 248|       try {
 249|         await api.reconnectSession(environmentId, sessionId)
 250|         logForDebugging(
 251|           `[bridge:heartbeat] Re-queued sessionId=${sessionId} via bridge/reconnect`,
 252|         )
 253|       } catch (err) {
 254|         logger.logError(
 255|           `Failed to refresh session ${sessionId} token: ${errorMessage(err)}`,
 256|         )
 257|         logForDebugging(
 258|           `[bridge:heartbeat] reconnectSession(${sessionId}) failed: ${errorMessage(err)}`,
 259|           { level: 'error' },
 260|         )
 261|       }
 262|     }
 263|     if (anyFatal) {
 264|       return 'fatal'
 265|     }
 266|     if (authFailedSessions.length > 0) {
 267|       return 'auth_failed'
 268|     }
 269|     return anySuccess ? 'ok' : 'failed'
 270|   }

源码引用: src/bridge/bridgeMain.ts · 第 272–313 行(共 3000 行)

 272|   // Sessions spawned with CCR v2 env vars. v2 children cannot use OAuth
 273|   // tokens (CCR worker endpoints validate the JWT's session_id claim,
 274|   // register_worker.go:32), so onRefresh triggers server re-dispatch
 275|   // instead — the next poll delivers fresh work with a new JWT via the
 276|   // existingHandle path below.
 277|   const v2Sessions = new Set<string>()
 278| 
 279|   // Proactive token refresh: schedules a timer 5min before the session
 280|   // ingress JWT expires. v1 delivers OAuth directly; v2 calls
 281|   // reconnectSession to trigger server re-dispatch (CC-1263: without
 282|   // this, v2 daemon sessions silently die at ~5h since the server does
 283|   // not auto-re-dispatch ACK'd work on lease expiry).
 284|   const tokenRefresh = getAccessToken
 285|     ? createTokenRefreshScheduler({
 286|         getAccessToken,
 287|         onRefresh: (sessionId, oauthToken) => {
 288|           const handle = activeSessions.get(sessionId)
 289|           if (!handle) {
 290|             return
 291|           }
 292|           if (v2Sessions.has(sessionId)) {
 293|             logger.logVerbose(
 294|               `Refreshing session ${sessionId} token via bridge/reconnect`,
 295|             )
 296|             void api
 297|               .reconnectSession(environmentId, sessionId)
 298|               .catch((err: unknown) => {
 299|                 logger.logError(
 300|                   `Failed to refresh session ${sessionId} token: ${errorMessage(err)}`,
 301|                 )
 302|                 logForDebugging(
 303|                   `[bridge:token] reconnectSession(${sessionId}) failed: ${errorMessage(err)}`,
 304|                   { level: 'error' },
 305|                 )
 306|               })
 307|           } else {
 308|             handle.updateAccessToken(oauthToken)
 309|           }
 310|         },
 311|         label: 'bridge',
 312|       })
 313|     : null

sessionRunner 子进程

createSessionSpawner(sessionRunner.ts:248)spawn 子 claude 进程,stdio 以 JSONL 协议通信:

PermissionRequest:子 CLI 发出 control_request subtype can_use_tool,bridge 转发到 server,用户在 claude.ai 批准。

SessionActivity:从 assistant tool_use 提取 toolSummary(TOOL_VERBS 映射 Read/Write/Bash…),供 bridgeUI 第二行状态显示。

safeFilenameId:剥离 session id 中路径危险字符,用于 debug 日志文件名。

MAX_ACTIVITIES / MAX_STDERR_LINES 限制内存;stderr 尾部保留供失败诊断。

子进程参数含 --sdk-url、work secret 解码后的 ingress URL、permission-mode 等;与 workSecret.ts buildSdkUrl/buildCCRv2SdkUrl 协作。

源码引用: src/bridge/sessionRunner.ts · 第 19–43 行(共 551 行)

  19| /**
  20|  * Sanitize a session ID for use in file names.
  21|  * Strips any characters that could cause path traversal (e.g. `../`, `/`)
  22|  * or other filesystem issues, replacing them with underscores.
  23|  */
  24| export function safeFilenameId(id: string): string {
  25|   return id.replace(/[^a-zA-Z0-9_-]/g, '_')
  26| }
  27| 
  28| /**
  29|  * A control_request emitted by the child CLI when it needs permission to
  30|  * execute a **specific** tool invocation (not a general capability check).
  31|  * The bridge forwards this to the server so the user can approve/deny.
  32|  */
  33| export type PermissionRequest = {
  34|   type: 'control_request'
  35|   request_id: string
  36|   request: {
  37|     /** Per-invocation permission check — "may I run this tool with these inputs?" */
  38|     subtype: 'can_use_tool'
  39|     tool_name: string
  40|     input: Record<string, unknown>
  41|     tool_use_id: string
  42|   }
  43| }

源码引用: src/bridge/sessionRunner.ts · 第 69–105 行(共 551 行)

  69| /** Map tool names to human-readable verbs for the status display. */
  70| const TOOL_VERBS: Record<string, string> = {
  71|   Read: 'Reading',
  72|   Write: 'Writing',
  73|   Edit: 'Editing',
  74|   MultiEdit: 'Editing',
  75|   Bash: 'Running',
  76|   Glob: 'Searching',
  77|   Grep: 'Searching',
  78|   WebFetch: 'Fetching',
  79|   WebSearch: 'Searching',
  80|   Task: 'Running task',
  81|   FileReadTool: 'Reading',
  82|   FileWriteTool: 'Writing',
  83|   FileEditTool: 'Editing',
  84|   GlobTool: 'Searching',
  85|   GrepTool: 'Searching',
  86|   BashTool: 'Running',
  87|   NotebookEditTool: 'Editing notebook',
  88|   LSP: 'LSP',
  89| }
  90| 
  91| function toolSummary(name: string, input: Record<string, unknown>): string {
  92|   const verb = TOOL_VERBS[name] ?? name
  93|   const target =
  94|     (input.file_path as string) ??
  95|     (input.filePath as string) ??
  96|     (input.pattern as string) ??
  97|     (input.command as string | undefined)?.slice(0, 60) ??
  98|     (input.url as string) ??
  99|     (input.query as string) ??
 100|     ''
 101|   if (target) {
 102|     return `${verb} ${target}`
 103|   }
 104|   return verb
 105| }

源码引用: src/bridge/sessionRunner.ts · 第 248–248 行(共 551 行)

 248| export function createSessionSpawner(deps: SessionSpawnerDeps): SessionSpawner {

createBridgeSession HTTP

createBridgeSession POST /v1/sessions(相对 org):

事件包装:每个 SDKMessage 包在 { type: 'event', data: sdk_message } 内,满足服务端 discriminated union。

Git 上下文:有 gitRepoUrl 时构建 git_source(https host/owner/name + revision)与 git_outcome(github repo + claude/branch 占位)。

懒加载依赖:accessToken、orgUUID、getMainLoopModel、axios 均在函数内 import。

返回值:成功 session id(compat 格式);失败 null + debug 日志,非致命 throw。

updateBridgeSessionTitle、archiveBridgeSession 同文件提供生命周期 PATCH/归档,供 initReplBridge onUserMessage 与 teardown 使用。

源码引用: src/bridge/createSession.ts · 第 18–54 行(共 385 行)

  18| // Events must be wrapped in { type: 'event', data: <sdk_message> } for the
  19| // POST /v1/sessions endpoint (discriminated union format).
  20| type SessionEvent = {
  21|   type: 'event'
  22|   data: SDKMessage
  23| }
  24| 
  25| /**
  26|  * Create a session on a bridge environment via POST /v1/sessions.
  27|  *
  28|  * Used by both `claude remote-control` (empty session so the user has somewhere to
  29|  * type immediately) and `/remote-control` (session pre-populated with conversation
  30|  * history).
  31|  *
  32|  * Returns the session ID on success, or null if creation fails (non-fatal).
  33|  */
  34| export async function createBridgeSession({
  35|   environmentId,
  36|   title,
  37|   events,
  38|   gitRepoUrl,
  39|   branch,
  40|   signal,
  41|   baseUrl: baseUrlOverride,
  42|   getAccessToken,
  43|   permissionMode,
  44| }: {
  45|   environmentId: string
  46|   title?: string
  47|   events: SessionEvent[]
  48|   gitRepoUrl: string | null
  49|   branch: string
  50|   signal: AbortSignal
  51|   baseUrl?: string
  52|   getAccessToken?: () => string | undefined
  53|   permissionMode?: string
  54| }): Promise<string | null> {

源码引用: src/bridge/createSession.ts · 第 77–120 行(共 385 行)

  77|   // Build git source and outcome context
  78|   let gitSource: GitSource | null = null
  79|   let gitOutcome: GitOutcome | null = null
  80| 
  81|   if (gitRepoUrl) {
  82|     const { parseGitRemote } = await import('../utils/detectRepository.js')
  83|     const parsed = parseGitRemote(gitRepoUrl)
  84|     if (parsed) {
  85|       const { host, owner, name } = parsed
  86|       const revision = branch || (await getDefaultBranch()) || undefined
  87|       gitSource = {
  88|         type: 'git_repository',
  89|         url: `https://${host}/${owner}/${name}`,
  90|         revision,
  91|       }
  92|       gitOutcome = {
  93|         type: 'git_repository',
  94|         git_info: {
  95|           type: 'github',
  96|           repo: `${owner}/${name}`,
  97|           branches: [`claude/${branch || 'task'}`],
  98|         },
  99|       }
 100|     } else {
 101|       // Fallback: try parseGitHubRepository for owner/repo format
 102|       const ownerRepo = parseGitHubRepository(gitRepoUrl)
 103|       if (ownerRepo) {
 104|         const [owner, name] = ownerRepo.split('/')
 105|         if (owner && name) {
 106|           const revision = branch || (await getDefaultBranch()) || undefined
 107|           gitSource = {
 108|             type: 'git_repository',
 109|             url: `https://github.com/${owner}/${name}`,
 110|             revision,
 111|           }
 112|           gitOutcome = {
 113|             type: 'git_repository',
 114|             git_info: {
 115|               type: 'github',
 116|               repo: `${owner}/${name}`,
 117|               branches: [`claude/${branch || 'task'}`],
 118|             },
 119|           }
 120|         }

runBridgeHeadless 与其它入口

runBridgeHeadless(bridgeMain 后部)供无需 TUI 的自动化场景:省略 QR、状态行,保留 poll/spawn 核心。

bridgeEnabled.ts 集中 GrowthBook/feature 门控;pollConfig.ts 可调 poll 间隔;capacityWake.ts 在容量可用时唤醒 loop。

codeSessionApi.ts 提供 daemon 瘦客户端 createBridgeSessionLean(调用方自带 orgUUID+model)。

选型建议:仅 REPL 远程控制读 initReplBridge;服务器侧长期驻留读 runBridgeLoop;Agent SDK 远程读 print.ts + initBridgeCore 注入参数。

源码引用: src/bridge/bridgeMain.ts · 第 2810–2812 行(共 3000 行)

2810| export async function runBridgeHeadless(
2811|   opts: HeadlessBridgeOpts,
2812|   signal: AbortSignal,

运维与调试

场景检查点
poll 永远空heartbeat 是否 401 未 reconnect;work 是否已 ack 未 stop
spawn 立即退出stderr 尾行;scriptArgs 是否缺失
多 session 不达标tengu_ccr_bridge_multi_session gate
env-less REPL 失败/bridge 403 trusted device;min_version config
子 session 工具不批PermissionRequest 是否到达 server

ant 用户:USER_TYPE===ant 时 bridgeMain 打印 debug 日志 glob 路径,与 sessionRunner 文件名一致,便于 tail -f。

本章小结与延伸

remote-bridge-core = 无 env poll 的 REPL 桥 + 多 session 守护进程。下一章 bridge-permissions-ui,读 API 客户端、TUI 与可信设备。 继续学习:

  • bridge-permissions-ui
  • repl-bridge
Prev
bridge-messaging · 桥消息路由与入站处理
Next
bridge-permissions-ui · 权限、API 与 TUI