Claude Code 源码分析第 20 课 · 第 06
第 20 课 课 · 交通架构

Bridge 与 Remote

Claude Code 如何将本地 CLI 会话转变为双向云连接环境 - 从两阶段环境 API 到无环境 CCR v2 路径、SSE/WebSocket 混合、权限桥接和 claude remote-control command.

架构概述

远程控制将本地 Claude Code REPL(或无头桥接服务器)连接到 claude.ai Web 前端。 这种联系有两个不同的方面:

  • 出站(本地 → 云): Claude 的消息、工具活动和结果事件会流出到 CCR(Cloud Code Runner)会话,以便 claude.ai 可以实时渲染它们。
  • 入站(云 → 本地): 在 claude.ai 上输入的用户提示、权限批准/拒绝决策、中断信号和控制消息流回正在运行的 Claude 进程。
┌──────────────────────────────┐ │ Claude Code REPL / 桥 │ └────────────┬─────────────────┘ │ OAuth / JWT 授权 ┌──────────────────────────────┐ │ 环境 API(仅限 v1) │ ← 注册环境、轮询工作、心跳 └────────────┬─────────────────┘ │ WorkSecret (base64url JWT) ┌──────────────────────────────┐ │ Session-Ingress / CCR 层 │ ← 会话 ID、会话入口令牌 │ ┌─────────┐ ┌────────────┐ │ │ │ SSE 中 │ │ HTTP POST │ │ ← v2:SSE 读取 + CCRClient 写入 │ │ WS 输入/ │ │ 输出(v1) │ │ ← v1:混合传输 WS+POST │ └─────────┘ └────────────┘ │ └──────────────────────────────┘ │ WS订阅 ┌──────────────────────────────┐ │ claude.ai 网络应用程序 │ /v1/sessions/ws/{id}/subscribe └──────────────────────────────┘

身份验证边界至关重要:所有桥 API 调用都使用用户的 claude.ai OAuth 令牌。 CCR 工作端点还验证一个短暂的 JWT,该 JWT 编码了 session_id and role=worker 声明 — OAuth 令牌将被拒绝。

Bridge v1 与 v2 — 基于环境与无环境

桥接系统的根本区别是会话是否通过 环境API (v1) 或使用新的工作 JWT(v2,“env-less”)直接连接到 CCR。

v1 — 基于环境(initBridgeCore)

  • 通过注册环境 POST /v1/environments
  • 工作投票通过 GET /v1/environments/{id}/work
  • 解码一个 WorkSecret (base64url JSON) 获取会话 JWT
  • 确认、心跳和停止工作项目
  • 支持永久模式(通过崩溃恢复 bridge-pointer.json)
  • 支持多会话生成(--spawn,工作树模式)
  • Transport: HybridTransport (WebSocket 读取 + HTTP POST)
  • 使用者:REPL、守护进程、 claude remote-control 服务器模式

v2 — 无环境 (initEnvLessBridgeCore)

  • 无环境 API — 跳过注册/轮询/确认/心跳
  • POST /v1/code/sessions → 会话 ID
  • POST /v1/code/sessions/{id}/bridge → 工人 JWT + 纪元
  • Each /bridge 打电话是工人登记
  • Transport: SSETransport (读)+ CCRClient (writes)
  • 门控由 tengu_bridge_repl_v2 成长书旗帜
  • REPL-only — 守护进程/打印保留在 v1 上
  • 令牌刷新调用 /bridge 再次(新 JWT + 新纪元)
WorkSecret 解码 — v1 握手令牌内的内容

当环境 API 交付工作时,它会附加一个不透明的 secret 字段是一个 base64url 编码的 JSON blob。 decodeWorkSecret() in bridge/workSecret.ts 解压它:

export function decodeWorkSecret(secret: string): WorkSecret {
  const json = Buffer.from(secret, 'base64url').toString('utf-8')
  const parsed: unknown = jsonParse(json)
  // validates version === 1, session_ingress_token, api_base_url
  return parsed as WorkSecret
}

type WorkSecret = {
  version: 1
  session_ingress_token: string   // JWT for CCR worker endpoints
  api_base_url: string
  sources: Array<{ type: string; git_info?: ... }>
  auth: Array<{ type: string; token: string }>
  use_code_sessions?: boolean    // server-driven CCR v2 selector
}

The session_ingress_token 是授权工作层操作的短暂 JWT。 OAuth 令牌不能在其位置使用 - CCR 验证 JWT session_id and role=worker 直接索赔。

门逻辑输入 initReplBridge

v1/v2 分支已解决 bridge/initReplBridge.ts 所有身份验证检查通过后:

// tengu_bridge_repl_v2 enables env-less (v2) path for the REPL.
// perpetual=true falls back to v1 — bridge-pointer not yet wired to v2.
if (isEnvLessBridgeEnabled() && !perpetual) {
  const versionError = await checkEnvLessBridgeMinVersion()
  if (versionError) {
    onStateChange?.('failed', 'run `claude update` to upgrade')
    return null
  }
  const { initEnvLessBridgeCore } = await import('./remoteBridgeCore.js')
  return initEnvLessBridgeCore({ baseUrl, orgUUID, title, ... })
}

// v1 path: env-based register/poll/ack/heartbeat
return initBridgeCore({ dir, machineName, branch, gitRepoUrl, ... })

有独立版本楼层: tengu_bridge_min_version 门 v1; tengu_bridge_repl_v2_config.min_version 盖茨 v2. 两者都是 GrowthBook 动态配置 - 操作可以通过将下限降低到 0.0.0.

会话 ID 兼容 — cse_* vs session_*

CCR v2 兼容层造成分裂:基础设施端点分发 cse_* ID,而 claude.ai 前端路由 session_*。 两者都是相同的 UUID,但具有不同的前缀。

// bridge/sessionIdCompat.ts
export function toCompatSessionId(id: string): string {
  if (!id.startsWith('cse_')) return id
  if (_isCseShimEnabled && !_isCseShimEnabled()) return id
  return 'session_' + id.slice('cse_'.length)
}

export function toInfraSessionId(id: string): string {
  if (!id.startsWith('session_')) return id
  return 'cse_' + id.slice('session_'.length)
}

// sameSessionId() ignores prefix so the poll loop doesn't
// reject its own session as "foreign" when the compat gate is on
export function sameSessionId(a: string, b: string): boolean {
  const aBody = a.slice(a.lastIndexOf('_') + 1)
  const bBody = b.slice(b.lastIndexOf('_') + 1)
  return aBody.length >= 4 && aBody === bBody
}

The isCseShimEnabled 终止开关通过注入 setCseShimGate() 以避免将 GrowthBook 导入 Agent SDK 捆绑包。

传输层 - WebSocket、SSE 和混合

传输抽象存在于 bridge/replBridgeTransport.ts。 它定义了一个单一的 ReplBridgeTransport 接口和两个工厂函数(一个用于 v1,一个用于 v2),因此桥接代码的其余部分永远不知道下面是哪个协议。

ReplBridgeTransport接口

export type ReplBridgeTransport = {
  write(message: StdoutMessage): Promise<void>
  writeBatch(messages: StdoutMessage[]): Promise<void>
  close(): void
  isConnectedStatus(): boolean
  getStateLabel(): string
  setOnData(cb: (data: string) => void): void
  setOnClose(cb: (closeCode?: number) => void): void
  setOnConnect(cb: () => void): void
  connect(): void
  getLastSequenceNum(): number  // v1 always returns 0
  readonly droppedBatchCount: number
  reportState(state: SessionState): void    // v2 only; v1 no-op
  reportMetadata(m: Record<string, unknown>): void  // v2 only
  reportDelivery(id: string, s: 'processing'|'processed'): void
  flush(): Promise<void>  // v2 drains queue; v1 resolves immediately
}
v1 运输 — HybridTransport adapter

createV1ReplTransport() 是一个薄的传递,包裹 HybridTransport。 HybridTransport 为入站消息打开到 Session-Ingress 的 WebSocket,并为出站消息使用 HTTP POST。 v1 从不使用 SSE 序列号——服务器端游标处理重播。

export function createV1ReplTransport(
  hybrid: HybridTransport,
): ReplBridgeTransport {
  return {
    write: msg => hybrid.write(msg),
    writeBatch: msgs => hybrid.writeBatch(msgs),
    close: () => hybrid.close(),
    isConnectedStatus: () => hybrid.isConnectedStatus(),
    getLastSequenceNum: () => 0,     // WS replay != SSE seq nums
    reportState: () => {},              // no-op
    reportMetadata: () => {},
    reportDelivery: () => {},
    flush: () => Promise.resolve(),     // POSTs are awaited per-write
    // ... other pass-throughs
  }
}
v2 传输 — SSETransport + CCRClient

v2 传输是不对称的: reads 过来SSE(服务器发送事件); writes 经过 CCRClient 哪个帖子到 /worker/events via SerialBatchEventUploader。 这种分割是有意为之的——入站 SSE 流可以暂停,而 CCRClient 的心跳和写入路径保持活动状态。

export async function createV2ReplTransport(opts: {
  sessionUrl: string
  ingressToken: string
  sessionId: string
  initialSequenceNum?: number  // resume from this SSE seq on reconnect
  epoch?: number               // skip registerWorker if provided by /bridge
  getAuthToken?: () => string | undefined  // multi-session safe
  outboundOnly?: boolean       // skip SSE read (mirror mode)
}): Promise<ReplBridgeTransport> {

  // registerWorker returns worker_epoch — required for CCRClient
  const epoch = opts.epoch ?? (await registerWorker(sessionUrl, ingressToken))

  const sse = new SSETransport(sseUrl, {}, sessionId, ...)
  const ccr = new CCRClient(sse, new URL(sessionUrl), {
    getAuthHeaders,
    onEpochMismatch: () => {
      // 409 from server: our epoch was superseded by another worker
      ccr.close(); sse.close()
      onCloseCb?.(4090)  // poll-loop recovery code
      throw new Error('epoch superseded')
    },
  })
  // ACK 'processed' immediately alongside 'received' to prevent
  // phantom prompt floods on daemon restart (CC-1263)
  sse.setOnEvent(event => {
    ccr.reportDelivery(event.event_id, 'received')
    ccr.reportDelivery(event.event_id, 'processed')
  })
  return { write: msg => ccr.writeEvent(msg), ... }
}
纪元不匹配 (409) 当服务器检测到第二个工作程序已注册同一会话(例如,网桥重新启动)时,它会向旧工作程序的下一个心跳或写入发送 409。 旧的传输会自行关闭(代码 4090),轮询循环会在新的纪元中开始新的工作。
FlushGate — 防止历史交错 + 实时消息

当桥接会话启动时,它会 POST 刷新历史对话。 在该刷新期间到达的任何新消息都将与服务器上的历史记录交错。 FlushGate 将它们排队直到刷新完成:

class FlushGate<T> {
  start(): void            // mark flush in-progress, enqueue() starts queuing
  end(): T[]               // flush done; return queued items for draining
  enqueue(...items: T[]): boolean  // true if active (queued), false if pass-through
  drop(): number           // discard queue (transport closed permanently)
  deactivate(): void       // transport swapped — new one will drain
}

deactivate() 当传输被替换时被调用(例如,环境丢失后重新连接)——项目被保留用于新的传输 end() call.

重新连接时的 SSE 序列号 在 v2 上, getLastSequenceNum() 返回上证所高水位线。 当交换传输(纪元不匹配、401、SSE 丢弃)时,新的 SSETransport 是用创建的 initialSequenceNum 所以它发送 from_sequence_num 并且服务器在不重播完整历史记录的情况下恢复。 v1 始终返回 0,因为 WS 重播是基于游标的服务器端。

许可桥

远程控制通过界面显示工具使用权限提示 控制请求/控制响应 协议。 当 Claude 想要运行潜在危险的工具时,REPL 通常会询问本地用户。 在远程控制模式下,这个问题会通过网桥传送到 claude.ai。

控制消息类型

  • control_request — Bridge 问 claude.ai“这个工具可以运行吗?”
  • control_response — claude.ai 回答允许/拒绝,可选择更新输入
  • control_cancel_request — 服务器取消待处理的提示(例如,会话结束)
权限响应类型和类型保护
// bridge/bridgePermissionCallbacks.ts
type BridgePermissionResponse = {
  behavior: 'allow' | 'deny'
  updatedInput?: Record<string, unknown>
  updatedPermissions?: PermissionUpdate[]
  message?: string
}

function isBridgePermissionResponse(value: unknown): value is BridgePermissionResponse {
  if (!value || typeof value !== 'object') return false
  return (
    'behavior' in value &&
    (value.behavior === 'allow' || value.behavior === 'deny')
  )
}
RemoteSessionManager — 权限请求/响应流程

remote/RemoteSessionManager.ts 协调本地 CLI 正在查看的 CCR 托管会话的客户端。 它通过 WebSocket 接收来自 CCR 的权限请求并保留它们直到用户响应:

class RemoteSessionManager {
  private pendingPermissionRequests = new Map<string, SDKControlPermissionRequest>()

  private handleControlRequest(req: SDKControlRequest): void {
    const { request_id, request: inner } = req
    if (inner.subtype === 'can_use_tool') {
      this.pendingPermissionRequests.set(request_id, inner)
      this.callbacks.onPermissionRequest(inner, request_id)
    } else {
      // Unsupported subtype: send error response immediately so server doesn't hang
      this.websocket?.sendControlResponse({ type: 'control_response', response: {
        subtype: 'error', request_id, error: 'Unsupported subtype'
      }})
    }
  }

  respondToPermissionRequest(requestId: string, result: RemotePermissionResponse): void {
    this.pendingPermissionRequests.delete(requestId)
    this.websocket?.sendControlResponse({
      type: 'control_response',
      response: {
        subtype: 'success', request_id: requestId,
        response: {
          behavior: result.behavior,
          ...(result.behavior === 'allow'
            ? { updatedInput: result.updatedInput }
            : { message: result.message }),
        },
      },
    })
  }
}
用于远程权限提示的合成 AssistantMessage

REPL 的权限 UI 需要真实的 AssistantMessage 与工具使用块。 当权限请求来自远程 CCR 容器时,本地不存在此类消息。 remote/remotePermissionBridge.ts 制作一个:

export function createSyntheticAssistantMessage(
  request: SDKControlPermissionRequest,
  requestId: string,
): AssistantMessage {
  return {
    type: 'assistant',
    uuid: randomUUID(),
    message: {
      id: `remote-${requestId}`,
      type: 'message',
      role: 'assistant',
      content: [{
        type: 'tool_use',
        id: request.tool_use_id,
        name: request.tool_name,
        input: request.input,
      }],
      // zero-usage stub fields ...
    } as AssistantMessage['message'],
    requestId: undefined,
    timestamp: new Date().toISOString(),
  }
}

// For tools not known locally (e.g. MCP tools on the remote container):
export function createToolStub(toolName: string): Tool {
  return {
    name: toolName,
    isEnabled: () => true,
    needsPermissions: () => true,
    // ... renders first 3 input key:value pairs for display
    call: async () => ({ data: '' }),
  } as unknown as Tool
}
独立网桥权限流程 (bridgeMain.ts) In claude remote-control 服务器模式,权限请求来自 child 克劳德进程被拦截 sessionRunner.ts,通过转发到服务器 api.sendPermissionResponseEvent(),并将响应写回子级的标准输入。

远程控制命令 — /remote-control

The /remote-control 斜杠命令位于 commands/bridge/bridge.tsx。 它是在 REPL 的 Ink 终端 UI 内呈现的 React 组件。

它的作用

  • 检查网桥是否已连接 — 如果是,则显示带有会话 URL 和 QR 代码选项的断开连接对话框。
  • 如果未连接,则运行飞行前检查(checkBridgePrerequisites)然后设置 replBridgeEnabled: true in AppState.
  • useReplBridge in REPL.tsx watches replBridgeEnabled 并打电话 initReplBridge().
  • The name 争论 (/remote-control my-session) 设置明确的会话标题。
BridgeToggle组件逻辑(已编译React)
// commands/bridge/bridge.tsx (compiled)
function BridgeToggle({ onDone, name }) {
  const replBridgeConnected = useAppState(s => s.replBridgeConnected)
  const replBridgeEnabled   = useAppState(s => s.replBridgeEnabled)
  const [showDisconnectDialog, setShow] = useState(false)

  useEffect(() => {
    if ((replBridgeConnected || replBridgeEnabled) && !replBridgeOutboundOnly) {
      setShow(true)  // already connected → show dialog
      return
    }
    (async () => {
      const error = await checkBridgePrerequisites()
      if (error) { onDone(error, { display: 'system' }); return }
      setAppState(prev => ({
        ...prev,
        replBridgeEnabled: true,
        replBridgeExplicit: true,
        replBridgeOutboundOnly: false,
        replBridgeInitialName: name,
      }))
      onDone('Remote Control connecting…', { display: 'system' })
    })()
  }, [])  // fires once on mount
}
权利门——谁可以使用远程控制

之前五项检查必须全部通过 initReplBridge proceeds:

  1. 运行时门: isBridgeEnabledBlocking() — 需要 tengu_ccr_bridge GrowthBook 标志和 claude.ai OAuth 订阅(无 Bedrock/Vertex/API 密钥身份验证)。
  2. OAuth 存在令牌: getBridgeAccessToken() 必须返回一个值。
  3. 组织方针: isPolicyAllowed('allow_remote_control') — 企业管理员可以为所有组织成员禁用 RC。
  4. 代币新鲜度:主动刷新 + 如果过期且不可刷新则跳过(避免保证 401 循环)。
  5. 最低版本: checkBridgeMinVersion() 对于 v1, checkEnvLessBridgeMinVersion() 对于 v2 — 操作员可以强制整个机队进行升级。

如果任何一扇门出现故障, onStateChange?.('failed', reason) 被调用并且函数返回 null.

CCR 镜像模式(outboundOnly)

When isCcrMirrorEnabled() 是真的(环境变量 CLAUDE_CODE_CCR_MIRROR 或 GrowthBook 标志),每个本地会话都会启动一个仅出站的桥。 SSE 读取流被跳过——桥仅将事件流式传输到 claude.ai,而不接受入站提示。 该会话以只读视图的形式显示在 claude.ai 会话列表中。

会话标题派生 标题分两个阶段设置:count-1 使用快速占位符(第一条用户消息的第一句话,被截断为 50 个字符); count-3 发射俳句 (generateSessionTitle)覆盖完整的对话文本。 明确的标题来自 /remote-control <name> or /rename 永远不会被自动覆盖。

CCR 集成 — 云代码运行器

CCR(云代码运行器)是服务器端执行环境,用于处理从 claude.ai 请求的会话,而无需本地 CLI 存在。 这 local 桥连接到 CCR 的会话入口层,以呈现输出并处理最初远程创建的会话的权限。

SessionsWebSocket — 订阅 CCR 会话

remote/SessionsWebSocket.ts 连接到 wss://api.anthropic.com/v1/sessions/ws/{sessionId}/subscribe 接收活动 CCR 会话的实时事件流。

连接和重新连接逻辑
const RECONNECT_DELAY_MS = 2000
const MAX_RECONNECT_ATTEMPTS = 5
const MAX_SESSION_NOT_FOUND_RETRIES = 3  // 4001 can be transient during compaction

const PERMANENT_CLOSE_CODES = new Set([
  4003,  // unauthorized — stop immediately
])

private handleClose(closeCode: number): void {
  if (PERMANENT_CLOSE_CODES.has(closeCode)) {
    this.callbacks.onClose?.()
    return
  }
  if (closeCode === 4001) {
    // session not found — retry up to 3 times with linear backoff
    this.sessionNotFoundRetries++
    if (this.sessionNotFoundRetries > MAX_SESSION_NOT_FOUND_RETRIES) {
      this.callbacks.onClose?.()
      return
    }
    this.scheduleReconnect(RECONNECT_DELAY_MS * this.sessionNotFoundRetries, ...)
    return
  }
  if (previousState === 'connected' && this.reconnectAttempts < MAX_RECONNECT_ATTEMPTS) {
    this.reconnectAttempts++
    this.scheduleReconnect(RECONNECT_DELAY_MS, ...)
  }
}

Bun 的本机 WebSocket 通过标头传递身份验证; 节点使用 ws 具有相同身份验证标头的包(订阅端点不需要连接后身份验证消息)。

SDK消息适配器

CCR 发送 SDK 格式消息(assistant, stream_event, result, system, tool_progress, ETC。)。 remote/sdkMessageAdapter.ts 将它们转换为 REPL 的内部 Message 用于本地渲染的类型。

消息转换表
function convertSDKMessage(msg: SDKMessage, opts?: ConvertOptions): ConvertedMessage {
  switch (msg.type) {
    case 'assistant':
      return { type: 'message', message: convertAssistantMessage(msg) }

    case 'stream_event':
      return { type: 'stream_event', event: convertStreamEvent(msg) }

    case 'result':
      // Only show errors — success results are noise in multi-turn
      return msg.subtype !== 'success'
        ? { type: 'message', message: convertResultMessage(msg) }
        : { type: 'ignored' }

    case 'system':
      if (msg.subtype === 'init')            return { type: 'message', message: convertInitMessage(msg) }
      if (msg.subtype === 'status')          return { ... }  // 'compacting' → banner
      if (msg.subtype === 'compact_boundary') return { ... }  // marks compaction point
      return { type: 'ignored' }

    case 'tool_progress':  return { type: 'message', message: convertToolProgressMessage(msg) }
    case 'user':          return { type: 'ignored' }  // already added locally by REPL
    case 'auth_status':   return { type: 'ignored' }
    default:             return { type: 'ignored' }  // forward-compat: unknown types silently dropped
  }
}
用户消息被故意忽略 在实时 WS 模式下,REPL 在将用户键入的消息发送到 CCR 之前已在本地添加。 如果适配器转换入站用户消息,它们会出现两次。 convertUserTextMessages: true 仅在重播历史事件时设置。

独立桥 — claude remote-control 服务器模式

bridge/bridgeMain.ts 实施 runBridgeLoop() 使用的函数 claude remote-control 作为持久服务器。 与 REPL 桥(一个会话,内联)不同,独立桥管理一组并发子 Claude 进程。

关键概念

  • SpawnMode: single-session (一个会话,桥退出), worktree (每个会话都有一个独立的 git 工作树), same-dir (会话共享 cwd — 可以互相踩踏)。
  • maxSessions:可配置的池大小(默认32); 网桥在达到容量时暂停轮询并使用 capacityWake 会话完成后立即恢复。
  • 令牌刷新:v1 会话通过以下方式接收更新的 OAuth 令牌 handle.updateAccessToken(); v2 会话调用 reconnectSession 使用新的 JWT 触发服务器重新调度(OAuth 令牌不能在 CCR 工作端点中使用)。
轮询循环退避和重新连接策略
const DEFAULT_BACKOFF: BackoffConfig = {
  connInitialMs:    2_000,
  connCapMs:      120_000,  // 2 min
  connGiveUpMs:   600_000,  // 10 min
  generalInitialMs:   500,
  generalCapMs:    30_000,
  generalGiveUpMs:600_000,
}

// Sleep detection: if a poll tick is delayed by >2× the cap,
// the machine probably slept — reset error budget and reconnect immediately
function pollSleepDetectionThresholdMs(b: BackoffConfig): number {
  return b.connCapMs * 2  // 240_000ms — above max backoff cap
}

连接错误和一般轮询错误具有独立的退避预算。 连接错误(注册/WebSocket 失败)在 10 分钟后放弃。 一般错误(工作轮询中的 HTTP 500)也会在 10 分钟后放弃——服务器是会话活跃度的权威。

子进程生成和会话跟踪
// bridgeMain.ts — activeSessions map tracks all running sessions
const activeSessions = new Map<string, SessionHandle>()
const sessionStartTimes = new Map<string, number>()
const sessionIngressTokens = new Map<string, string>()
const sessionTimers = new Map<string, ReturnType<typeof setTimeout>>()

// Per-session timeout watchdog (default 24h)
const DEFAULT_SESSION_TIMEOUT_MS = 24 * 60 * 60 * 1000

// SessionHandle exposes kill()/forceKill(), writeStdin(), activities ring buffer
type SessionHandle = {
  sessionId: string
  done: Promise<SessionDoneStatus>
  kill(): void
  forceKill(): void
  activities: SessionActivity[]      // ring buffer (last 10)
  currentActivity: SessionActivity | null
  accessToken: string
  lastStderr: string[]              // ring buffer (last 10 lines)
  writeStdin(data: string): void
  updateAccessToken(token: string): void
}
心跳和 JWT 过期恢复

活动工作项按照 GrowthBook 配置的时间间隔进行检测。 心跳使用会话入口 JWT(不是 OAuth) SessionIngressAuth — 轻量级的无数据库 JWT 验证。 在 401/403(JWT 过期)上,网桥调用 reconnectSession 重新排队工作,以便下一次轮询提供新的凭据:

async function heartbeatActiveWorkItems() {
  for (const [sessionId] of activeSessions) {
    const ingressToken = sessionIngressTokens.get(sessionId)
    try {
      await api.heartbeatWork(environmentId, workId, ingressToken)
    } catch (err) {
      if (err.status === 401 || err.status === 403) {
        // JWT expired — re-dispatch so next poll delivers fresh token
        await api.reconnectSession(environmentId, sessionId)
      }
    }
  }
}

主动令牌刷新调度程序也会在到期前 5 分钟触发。 v1 会话直接接收新的 OAuth 令牌; v2 会话经过 reconnectSession 因为 CCR 工作端点拒绝 OAuth 令牌。

BridgeWorkerType

每个环境注册都包含一个 worker_type 字符串发送为 metadata.worker_type。 Web UI 使用它来过滤其会话选择器中的会话:

  • "claude_code" — 标准 REPL 会话
  • "claude_code_assistant" — 助手模式(KAIROS 功能标志)
  • "cowork" — Desktop Cowork(由 claude.ai 桌面应用程序发送,不是此代码库)

要点

  1. 两种桥接架构共存: v1(基于 env 的 poll/ack/heartbeat 循环)和 v2(“无环境” — 直接 OAuth → 工作 JWT 通过 POST /bridge)。 成长书标志 tengu_bridge_repl_v2 控制 REPL 采用的路径。
  2. v2 中的传输是不对称的: 入站使用 SSE(带有用于无缝重新连接的序列号),出站使用 CCRClient 发布到 /worker/events。 v1 使用 WebSocket 进行双向传输 HybridTransport.
  3. The FlushGate 防止历史/实时消息交错: 历史消息在连接时作为一批 HTTP 刷新; 在该窗口期间到达的任何实时消息都会在刷新完成后排队并排出。
  4. 权限流在同一传输上是双向的: control_request 从 Claude → 服务器 → claude.ai 出发; control_response (允许/拒绝)返回。 远程工具存根是在本地合成客户端不知道的工具的。
  5. 会话 ID 有两种外观: 基础设施层使用 cse_*,compat/面向客户端的 API 使用 session_*. sameSessionId() 通过 UUID 主体进行比较,因此轮询循环不会拒绝其自己的会话。 toCompatSessionId() 通过 GrowthBook 进行终止切换。
  6. 工作端点的身份验证需要 JWT,而不是 OAuth: CCR 验证 JWT session_id 索赔和 role=worker。 v2 会话的令牌刷新会触发服务器重新调度(reconnectSession)而不是将新的 OAuth 令牌推送到正在运行的进程。
  7. claude remote-control 服务器模式 支持多会话并发执行 single-session, worktree, 和 same-dir 生成模式、可配置的池大小和 24 小时每个会话超时看门狗。

知识检查

1. 在v2(env-less)桥接路径中,什么是 POST /v1/code/sessions/{id}/bridge 返回取代整个环境 API 轮询工作流程?

A. 以 base64url 编码的 WorkSecret,其中包含 OAuth 刷新令牌
B. 一个worker JWT(短暂的)和一个授权CCR工作层操作的worker epoch
C. 与 v1 HybridTransport 类似的 WebSocket URL 和 session_ingress_token
D. 用于轮询排队工作的environment_id 和environment_secret

2.为什么v2传输立即ACK processed (不仅 received) 收到 SSE 事件后?

A. CCRClient 规范要求两个事件在单个 HTTP 往返中发送
B. 服务器仅使用 processed 用于速率限制; received 单独触发重复投递
C、没有 processed,事件保留在服务器的重新队列中,并在每次守护进程重新启动时用幻象提示淹没会话
D. 上交所交通 setOnEvent 只为 processed- 类型交付确认

3.A cse_abc123 会话 ID 来自工作轮询。 哪个函数将其转换为与 client-facing 会话 API(/v1/sessions/{id}/archive)?

A. toInfraSessionId() — 重新标记为 cse_*
B. toCompatSessionId() — 重新标记为 session_*
C. sameSessionId() — 比较 UUID 实体并选择匹配的 ID
D. 不需要转换——存档端点接受两个前缀

4. 目的是什么 FlushGate.deactivate() (相对于 drop())?

A. 它立即停止所有排队并丢弃待处理的项目,因为传输永远不会重新连接
B. 它将刷新标记为完成并返回排队的项目以供调用者排出
C. 它清除活动标志但保留排队项目,以便 replacement 运输可以在下次冲洗时耗尽它们
D. 它暂停排队而不清除挂起的项目,并在重新连接退避后安排重试

5. 在 claude remote-control 服务器模式(独立桥),当 v2 会话的 JWT 在心跳期间过期时会发生什么?

A. handle.updateAccessToken(newOAuthToken) 被调用以将新的凭据推送到正在运行的子进程
B. 会话被终止并使用下一次民意调查中的新 JWT 重新生成
C. api.reconnectSession() 被调用以触发服务器重新调度,以便下一次轮询使用新的 JWT 交付新的工作
D. 在宣布会话死亡之前,桥接器以指数退避重试心跳最多 5 次

6. createToolStub(toolName) in remote/remotePermissionBridge.ts 存在是因为:

A. MCP 工具在转发之前需要一个存根来绕过 CCRClient 的架构验证
B. 远程 CCR 容器可能具有本地 CLI 不知道的 MCP 工具,因此存根路由到 FallbackPermissionRequest 进行显示
C. 所有工具都需要一个用于合成 AssistantMessage 的存根,因为真正的工具定义永远不会通过网络进行序列化
D. 仅当以下情况时才需要存根 outboundOnly 为 true 并且没有入站工具消息可以到达