本章总览
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。
建议学习步骤
- 阅读源码块 A:remoteBridgeCore 文件头与 EnvLessBridgeParams
- 阅读源码块 B:initEnvLessBridgeCore 入口
- 阅读源码块 C:bridgeMain runBridgeLoop 启动
- 阅读源码块 D:heartbeat 与 token 刷新
- 阅读源码块 E:createSessionSpawner
- 阅读源码块 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)精简为:
- POST /v1/code/sessions(OAuth,无 environment_id)→ session.id
- POST /v1/code/sessions/{id}/bridge → worker_jwt、expires_in、api_base_url、worker_epoch
- createV2ReplTransport(registerWorker 内嵌于构造)
- createTokenRefreshScheduler 在 JWT 过期前 5min proactive 调 /bridge
- 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 与可信设备。 继续学习: