yzcode-cli 1.0.2 → 1.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/assistant/sessionHistory.ts +87 -0
  2. package/bootstrap/state.ts +1769 -0
  3. package/bridge/bridgeApi.ts +539 -0
  4. package/bridge/bridgeConfig.ts +48 -0
  5. package/bridge/bridgeDebug.ts +135 -0
  6. package/bridge/bridgeEnabled.ts +202 -0
  7. package/bridge/bridgeMain.ts +2999 -0
  8. package/bridge/bridgeMessaging.ts +461 -0
  9. package/bridge/bridgePermissionCallbacks.ts +43 -0
  10. package/bridge/bridgePointer.ts +210 -0
  11. package/bridge/bridgeStatusUtil.ts +163 -0
  12. package/bridge/bridgeUI.ts +530 -0
  13. package/bridge/capacityWake.ts +56 -0
  14. package/bridge/codeSessionApi.ts +168 -0
  15. package/bridge/createSession.ts +384 -0
  16. package/bridge/debugUtils.ts +141 -0
  17. package/bridge/envLessBridgeConfig.ts +165 -0
  18. package/bridge/flushGate.ts +71 -0
  19. package/bridge/inboundAttachments.ts +175 -0
  20. package/bridge/inboundMessages.ts +80 -0
  21. package/bridge/initReplBridge.ts +569 -0
  22. package/bridge/jwtUtils.ts +256 -0
  23. package/bridge/pollConfig.ts +110 -0
  24. package/bridge/pollConfigDefaults.ts +82 -0
  25. package/bridge/remoteBridgeCore.ts +1008 -0
  26. package/bridge/replBridge.ts +2406 -0
  27. package/bridge/replBridgeHandle.ts +36 -0
  28. package/bridge/replBridgeTransport.ts +370 -0
  29. package/bridge/sessionIdCompat.ts +57 -0
  30. package/bridge/sessionRunner.ts +550 -0
  31. package/bridge/trustedDevice.ts +210 -0
  32. package/bridge/types.ts +262 -0
  33. package/bridge/workSecret.ts +127 -0
  34. package/buddy/CompanionSprite.tsx +371 -0
  35. package/buddy/companion.ts +133 -0
  36. package/buddy/prompt.ts +36 -0
  37. package/buddy/sprites.ts +514 -0
  38. package/buddy/types.ts +148 -0
  39. package/buddy/useBuddyNotification.tsx +98 -0
  40. package/coordinator/coordinatorMode.ts +369 -0
  41. package/memdir/findRelevantMemories.ts +141 -0
  42. package/memdir/memdir.ts +507 -0
  43. package/memdir/memoryAge.ts +53 -0
  44. package/memdir/memoryScan.ts +94 -0
  45. package/memdir/memoryTypes.ts +271 -0
  46. package/memdir/paths.ts +278 -0
  47. package/memdir/teamMemPaths.ts +292 -0
  48. package/memdir/teamMemPrompts.ts +100 -0
  49. package/migrations/migrateAutoUpdatesToSettings.ts +61 -0
  50. package/migrations/migrateBypassPermissionsAcceptedToSettings.ts +40 -0
  51. package/migrations/migrateEnableAllProjectMcpServersToSettings.ts +118 -0
  52. package/migrations/migrateFennecToOpus.ts +45 -0
  53. package/migrations/migrateLegacyOpusToCurrent.ts +57 -0
  54. package/migrations/migrateOpusToOpus1m.ts +43 -0
  55. package/migrations/migrateReplBridgeEnabledToRemoteControlAtStartup.ts +22 -0
  56. package/migrations/migrateSonnet1mToSonnet45.ts +48 -0
  57. package/migrations/migrateSonnet45ToSonnet46.ts +67 -0
  58. package/migrations/resetAutoModeOptInForDefaultOffer.ts +51 -0
  59. package/migrations/resetProToOpusDefault.ts +51 -0
  60. package/native-ts/color-diff/index.ts +999 -0
  61. package/native-ts/file-index/index.ts +370 -0
  62. package/native-ts/yoga-layout/enums.ts +134 -0
  63. package/native-ts/yoga-layout/index.ts +2578 -0
  64. package/outputStyles/loadOutputStylesDir.ts +98 -0
  65. package/package.json +19 -2
  66. package/plugins/builtinPlugins.ts +159 -0
  67. package/plugins/bundled/index.ts +23 -0
  68. package/schemas/hooks.ts +222 -0
  69. package/screens/Doctor.tsx +575 -0
  70. package/screens/REPL.tsx +5006 -0
  71. package/screens/ResumeConversation.tsx +399 -0
  72. package/server/createDirectConnectSession.ts +88 -0
  73. package/server/directConnectManager.ts +213 -0
  74. package/server/types.ts +57 -0
  75. package/skills/bundled/batch.ts +124 -0
  76. package/skills/bundled/claudeApi.ts +196 -0
  77. package/skills/bundled/claudeApiContent.ts +75 -0
  78. package/skills/bundled/claudeInChrome.ts +34 -0
  79. package/skills/bundled/debug.ts +103 -0
  80. package/skills/bundled/index.ts +79 -0
  81. package/skills/bundled/keybindings.ts +339 -0
  82. package/skills/bundled/loop.ts +92 -0
  83. package/skills/bundled/loremIpsum.ts +282 -0
  84. package/skills/bundled/remember.ts +82 -0
  85. package/skills/bundled/scheduleRemoteAgents.ts +447 -0
  86. package/skills/bundled/simplify.ts +69 -0
  87. package/skills/bundled/skillify.ts +197 -0
  88. package/skills/bundled/stuck.ts +79 -0
  89. package/skills/bundled/updateConfig.ts +475 -0
  90. package/skills/bundled/verify/SKILL.md +3 -0
  91. package/skills/bundled/verify/examples/cli.md +3 -0
  92. package/skills/bundled/verify/examples/server.md +3 -0
  93. package/skills/bundled/verify.ts +30 -0
  94. package/skills/bundled/verifyContent.ts +13 -0
  95. package/skills/bundledSkills.ts +220 -0
  96. package/skills/loadSkillsDir.ts +1086 -0
  97. package/skills/mcpSkillBuilders.ts +44 -0
  98. package/tasks/DreamTask/DreamTask.ts +157 -0
  99. package/tasks/InProcessTeammateTask/InProcessTeammateTask.tsx +126 -0
  100. package/tasks/InProcessTeammateTask/types.ts +121 -0
  101. package/tasks/LocalAgentTask/LocalAgentTask.tsx +683 -0
  102. package/tasks/LocalMainSessionTask.ts +479 -0
  103. package/tasks/LocalShellTask/LocalShellTask.tsx +523 -0
  104. package/tasks/LocalShellTask/guards.ts +41 -0
  105. package/tasks/LocalShellTask/killShellTasks.ts +76 -0
  106. package/tasks/RemoteAgentTask/RemoteAgentTask.tsx +856 -0
  107. package/tasks/pillLabel.ts +82 -0
  108. package/tasks/stopTask.ts +100 -0
  109. package/tasks/types.ts +46 -0
  110. package/upstreamproxy/relay.ts +455 -0
  111. package/upstreamproxy/upstreamproxy.ts +285 -0
  112. package/vim/motions.ts +82 -0
  113. package/vim/operators.ts +556 -0
  114. package/vim/textObjects.ts +186 -0
  115. package/vim/transitions.ts +490 -0
  116. package/vim/types.ts +199 -0
  117. package/voice/voiceModeEnabled.ts +54 -0
@@ -0,0 +1,36 @@
1
+ import { updateSessionBridgeId } from '../utils/concurrentSessions.js'
2
+ import type { ReplBridgeHandle } from './replBridge.js'
3
+ import { toCompatSessionId } from './sessionIdCompat.js'
4
+
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
+ }
@@ -0,0 +1,370 @@
1
+ import type { StdoutMessage } from 'src/entrypoints/sdk/controlTypes.js'
2
+ import { CCRClient } from '../cli/transports/ccrClient.js'
3
+ import type { HybridTransport } from '../cli/transports/HybridTransport.js'
4
+ import { SSETransport } from '../cli/transports/SSETransport.js'
5
+ import { logForDebugging } from '../utils/debug.js'
6
+ import { errorMessage } from '../utils/errors.js'
7
+ import { updateSessionIngressAuthToken } from '../utils/sessionIngressAuth.js'
8
+ import type { SessionState } from '../utils/sessionState.js'
9
+ import { registerWorker } from './workSecret.js'
10
+
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
+ }
71
+
72
+ /**
73
+ * v1 adapter: HybridTransport already has the full surface (it extends
74
+ * WebSocketTransport which has setOnConnect + getStateLabel). This is a
75
+ * no-op wrapper that exists only so replBridge's `transport` variable
76
+ * has a single type.
77
+ */
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
+ }
104
+
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
+ */
119
+ export async function createV2ReplTransport(opts: {
120
+ sessionUrl: string
121
+ ingressToken: string
122
+ sessionId: string
123
+ /**
124
+ * SSE sequence-number high-water mark from the previous transport.
125
+ * Passed to the new SSETransport so its first connect() sends
126
+ * from_sequence_num / Last-Event-ID and the server resumes from where
127
+ * the old stream left off. Without this, every transport swap asks the
128
+ * server to replay the entire session history from seq 0.
129
+ */
130
+ initialSequenceNum?: number
131
+ /**
132
+ * Worker epoch from POST /bridge response. When provided, the server
133
+ * already bumped epoch (the /bridge call IS the register — see server
134
+ * PR #293280). When omitted (v1 CCR-v2 path via replBridge.ts poll loop),
135
+ * call registerWorker as before.
136
+ */
137
+ epoch?: number
138
+ /** CCRClient heartbeat interval. Defaults to 20s when omitted. */
139
+ heartbeatIntervalMs?: number
140
+ /** ±fraction per-beat jitter. Defaults to 0 (no jitter) when omitted. */
141
+ heartbeatJitterFraction?: number
142
+ /**
143
+ * When true, skip opening the SSE read stream — only the CCRClient write
144
+ * path is activated. Use for mirror-mode attachments that forward events
145
+ * but never receive inbound prompts or control requests.
146
+ */
147
+ outboundOnly?: boolean
148
+ /**
149
+ * Per-instance auth header source. When provided, CCRClient + SSETransport
150
+ * read auth from this closure instead of the process-wide
151
+ * CLAUDE_CODE_SESSION_ACCESS_TOKEN env var. Required for callers managing
152
+ * multiple concurrent sessions — the env-var path stomps across sessions.
153
+ * When omitted, falls back to the env var (single-session callers).
154
+ */
155
+ getAuthToken?: () => string | undefined
156
+ }): Promise<ReplBridgeTransport> {
157
+ const {
158
+ sessionUrl,
159
+ ingressToken,
160
+ sessionId,
161
+ initialSequenceNum,
162
+ getAuthToken,
163
+ } = opts
164
+
165
+ // Auth header builder. If getAuthToken is provided, read from it
166
+ // (per-instance, multi-session safe). Otherwise write ingressToken to
167
+ // the process-wide env var (legacy single-session path — CCRClient's
168
+ // default getAuthHeaders reads it via getSessionIngressAuthHeaders).
169
+ let getAuthHeaders: (() => Record<string, string>) | undefined
170
+ if (getAuthToken) {
171
+ getAuthHeaders = (): Record<string, string> => {
172
+ const token = getAuthToken()
173
+ if (!token) return {}
174
+ return { Authorization: `Bearer ${token}` }
175
+ }
176
+ } else {
177
+ // CCRClient.request() and SSETransport.connect() both read auth via
178
+ // getSessionIngressAuthHeaders() → this env var. Set it before either
179
+ // touches the network.
180
+ updateSessionIngressAuthToken(ingressToken)
181
+ }
182
+
183
+ const epoch = opts.epoch ?? (await registerWorker(sessionUrl, ingressToken))
184
+ logForDebugging(
185
+ `[bridge:repl] CCR v2: worker sessionId=${sessionId} epoch=${epoch}${opts.epoch !== undefined ? ' (from /bridge)' : ' (via registerWorker)'}`,
186
+ )
187
+
188
+ // Derive SSE stream URL. Same logic as transportUtils.ts:26-33 but
189
+ // starting from an http(s) base instead of a --sdk-url that might be ws://.
190
+ const sseUrl = new URL(sessionUrl)
191
+ sseUrl.pathname = sseUrl.pathname.replace(/\/$/, '') + '/worker/events/stream'
192
+
193
+ const sse = new SSETransport(
194
+ sseUrl,
195
+ {},
196
+ sessionId,
197
+ undefined,
198
+ initialSequenceNum,
199
+ getAuthHeaders,
200
+ )
201
+ let onCloseCb: ((closeCode?: number) => void) | undefined
202
+ const ccr = new CCRClient(sse, new URL(sessionUrl), {
203
+ getAuthHeaders,
204
+ heartbeatIntervalMs: opts.heartbeatIntervalMs,
205
+ heartbeatJitterFraction: opts.heartbeatJitterFraction,
206
+ // Default is process.exit(1) — correct for spawn-mode children. In-process,
207
+ // that kills the REPL. Close instead: replBridge's onClose wakes the poll
208
+ // loop, which picks up the server's re-dispatch (with fresh epoch).
209
+ onEpochMismatch: () => {
210
+ logForDebugging(
211
+ '[bridge:repl] CCR v2: epoch superseded (409) — closing for poll-loop recovery',
212
+ )
213
+ // Close resources in a try block so the throw always executes.
214
+ // If ccr.close() or sse.close() throw, we still need to unwind
215
+ // the caller (request()) — otherwise handleEpochMismatch's `never`
216
+ // return type is violated at runtime and control falls through.
217
+ try {
218
+ ccr.close()
219
+ sse.close()
220
+ onCloseCb?.(4090)
221
+ } catch (closeErr: unknown) {
222
+ logForDebugging(
223
+ `[bridge:repl] CCR v2: error during epoch-mismatch cleanup: ${errorMessage(closeErr)}`,
224
+ { level: 'error' },
225
+ )
226
+ }
227
+ // Don't return — the calling request() code continues after the 409
228
+ // branch, so callers see the logged warning and a false return. We
229
+ // throw to unwind; the uploaders catch it as a send failure.
230
+ throw new Error('epoch superseded')
231
+ },
232
+ })
233
+
234
+ // CCRClient's constructor wired sse.setOnEvent → reportDelivery('received').
235
+ // remoteIO.ts additionally sends 'processing'/'processed' via
236
+ // setCommandLifecycleListener, which the in-process query loop fires. This
237
+ // transport's only caller (replBridge/daemonBridge) has no such wiring — the
238
+ // daemon's agent child is a separate process (ProcessTransport), and its
239
+ // notifyCommandLifecycle calls fire with listener=null in its own module
240
+ // scope. So events stay at 'received' forever, and reconnectSession re-queues
241
+ // them on every daemon restart (observed: 21→24→25 phantom prompts as
242
+ // "user sent a new message while you were working" system-reminders).
243
+ //
244
+ // Fix: ACK 'processed' immediately alongside 'received'. The window between
245
+ // SSE receipt and transcript-write is narrow (queue → SDK → child stdin →
246
+ // model); a crash there loses one prompt vs. the observed N-prompt flood on
247
+ // every restart. Overwrite the constructor's wiring to do both — setOnEvent
248
+ // replaces, not appends (SSETransport.ts:658).
249
+ sse.setOnEvent(event => {
250
+ ccr.reportDelivery(event.event_id, 'received')
251
+ ccr.reportDelivery(event.event_id, 'processed')
252
+ })
253
+
254
+ // Both sse.connect() and ccr.initialize() are deferred to connect() below.
255
+ // replBridge's calling order is newTransport → setOnConnect → setOnData →
256
+ // setOnClose → connect(), and both calls need those callbacks wired first:
257
+ // sse.connect() opens the stream (events flow to onData/onClose immediately),
258
+ // and ccr.initialize().then() fires onConnectCb.
259
+ //
260
+ // onConnect fires once ccr.initialize() resolves. Writes go via
261
+ // CCRClient HTTP POST (SerialBatchEventUploader), not SSE, so the
262
+ // write path is ready the moment workerEpoch is set. SSE.connect()
263
+ // awaits its read loop and never resolves — don't gate on it.
264
+ // The SSE stream opens in parallel (~30ms) and starts delivering
265
+ // inbound events via setOnData; outbound doesn't need to wait for it.
266
+ let onConnectCb: (() => void) | undefined
267
+ let ccrInitialized = false
268
+ let closed = false
269
+
270
+ return {
271
+ write(msg) {
272
+ return ccr.writeEvent(msg)
273
+ },
274
+ async writeBatch(msgs) {
275
+ // SerialBatchEventUploader already batches internally (maxBatchSize=100);
276
+ // sequential enqueue preserves order and the uploader coalesces.
277
+ // Check closed between writes to avoid sending partial batches after
278
+ // transport teardown (epoch mismatch, SSE drop).
279
+ for (const m of msgs) {
280
+ if (closed) break
281
+ await ccr.writeEvent(m)
282
+ }
283
+ },
284
+ close() {
285
+ closed = true
286
+ ccr.close()
287
+ sse.close()
288
+ },
289
+ isConnectedStatus() {
290
+ // Write-readiness, not read-readiness — replBridge checks this
291
+ // before calling writeBatch. SSE open state is orthogonal.
292
+ return ccrInitialized
293
+ },
294
+ getStateLabel() {
295
+ // SSETransport doesn't expose its state string; synthesize from
296
+ // what we can observe. replBridge only uses this for debug logging.
297
+ if (sse.isClosedStatus()) return 'closed'
298
+ if (sse.isConnectedStatus()) return ccrInitialized ? 'connected' : 'init'
299
+ return 'connecting'
300
+ },
301
+ setOnData(cb) {
302
+ sse.setOnData(cb)
303
+ },
304
+ setOnClose(cb) {
305
+ onCloseCb = cb
306
+ // SSE reconnect-budget exhaustion fires onClose(undefined) — map to
307
+ // 4092 so ws_closed telemetry can distinguish it from HTTP-status
308
+ // closes (SSETransport:280 passes response.status). Stop CCRClient's
309
+ // heartbeat timer before notifying replBridge. (sse.close() doesn't
310
+ // invoke this, so the epoch-mismatch path above isn't double-firing.)
311
+ sse.setOnClose(code => {
312
+ ccr.close()
313
+ cb(code ?? 4092)
314
+ })
315
+ },
316
+ setOnConnect(cb) {
317
+ onConnectCb = cb
318
+ },
319
+ getLastSequenceNum() {
320
+ return sse.getLastSequenceNum()
321
+ },
322
+ // v2 write path (CCRClient) doesn't set maxConsecutiveFailures — no drops.
323
+ droppedBatchCount: 0,
324
+ reportState(state) {
325
+ ccr.reportState(state)
326
+ },
327
+ reportMetadata(metadata) {
328
+ ccr.reportMetadata(metadata)
329
+ },
330
+ reportDelivery(eventId, status) {
331
+ ccr.reportDelivery(eventId, status)
332
+ },
333
+ flush() {
334
+ return ccr.flush()
335
+ },
336
+ connect() {
337
+ // Outbound-only: skip the SSE read stream entirely — no inbound
338
+ // events to receive, no delivery ACKs to send. Only the CCRClient
339
+ // write path (POST /worker/events) and heartbeat are needed.
340
+ if (!opts.outboundOnly) {
341
+ // Fire-and-forget — SSETransport.connect() awaits readStream()
342
+ // (the read loop) and only resolves on stream close/error. The
343
+ // spawn-mode path in remoteIO.ts does the same void discard.
344
+ void sse.connect()
345
+ }
346
+ void ccr.initialize(epoch).then(
347
+ () => {
348
+ ccrInitialized = true
349
+ logForDebugging(
350
+ `[bridge:repl] v2 transport ready for writes (epoch=${epoch}, sse=${sse.isConnectedStatus() ? 'open' : 'opening'})`,
351
+ )
352
+ onConnectCb?.()
353
+ },
354
+ (err: unknown) => {
355
+ logForDebugging(
356
+ `[bridge:repl] CCR v2 initialize failed: ${errorMessage(err)}`,
357
+ { level: 'error' },
358
+ )
359
+ // Close transport resources and notify replBridge via onClose
360
+ // so the poll loop can retry on the next work dispatch.
361
+ // Without this callback, replBridge never learns the transport
362
+ // failed to initialize and sits with transport === null forever.
363
+ ccr.close()
364
+ sse.close()
365
+ onCloseCb?.(4091) // 4091 = init failure, distinguishable from 4090 epoch mismatch
366
+ },
367
+ )
368
+ },
369
+ }
370
+ }
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Session ID tag translation helpers for the CCR v2 compat layer.
3
+ *
4
+ * Lives in its own file (rather than workSecret.ts) so that sessionHandle.ts
5
+ * and replBridgeTransport.ts (bridge.mjs entry points) can import from
6
+ * workSecret.ts without pulling in these retag functions.
7
+ *
8
+ * The isCseShimEnabled kill switch is injected via setCseShimGate() to avoid
9
+ * a static import of bridgeEnabled.ts → growthbook.ts → config.ts — all
10
+ * banned from the sdk.mjs bundle (scripts/build-agent-sdk.sh). Callers that
11
+ * already import bridgeEnabled.ts register the gate; the SDK path never does,
12
+ * so the shim defaults to active (matching isCseShimEnabled()'s own default).
13
+ */
14
+
15
+ let _isCseShimEnabled: (() => boolean) | undefined
16
+
17
+ /**
18
+ * Register the GrowthBook gate for the cse_ shim. Called from bridge
19
+ * init code that already imports bridgeEnabled.ts.
20
+ */
21
+ export function setCseShimGate(gate: () => boolean): void {
22
+ _isCseShimEnabled = gate
23
+ }
24
+
25
+ /**
26
+ * Re-tag a `cse_*` session ID to `session_*` for use with the v1 compat API.
27
+ *
28
+ * Worker endpoints (/v1/code/sessions/{id}/worker/*) want `cse_*`; that's
29
+ * what the work poll delivers. Client-facing compat endpoints
30
+ * (/v1/sessions/{id}, /v1/sessions/{id}/archive, /v1/sessions/{id}/events)
31
+ * want `session_*` — compat/convert.go:27 validates TagSession. Same UUID,
32
+ * different costume. No-op for IDs that aren't `cse_*`.
33
+ *
34
+ * bridgeMain holds one sessionId variable for both worker registration and
35
+ * session-management calls. It arrives as `cse_*` from the work poll under
36
+ * the compat gate, so archiveSession/fetchSessionTitle need this re-tag.
37
+ */
38
+ export function toCompatSessionId(id: string): string {
39
+ if (!id.startsWith('cse_')) return id
40
+ if (_isCseShimEnabled && !_isCseShimEnabled()) return id
41
+ return 'session_' + id.slice('cse_'.length)
42
+ }
43
+
44
+ /**
45
+ * Re-tag a `session_*` session ID to `cse_*` for infrastructure-layer calls.
46
+ *
47
+ * Inverse of toCompatSessionId. POST /v1/environments/{id}/bridge/reconnect
48
+ * lives below the compat layer: once ccr_v2_compat_enabled is on server-side,
49
+ * it looks sessions up by their infra tag (`cse_*`). createBridgeSession still
50
+ * returns `session_*` (compat/convert.go:41) and that's what bridge-pointer
51
+ * stores — so perpetual reconnect passes the wrong costume and gets "Session
52
+ * not found" back. Same UUID, wrong tag. No-op for IDs that aren't `session_*`.
53
+ */
54
+ export function toInfraSessionId(id: string): string {
55
+ if (!id.startsWith('session_')) return id
56
+ return 'cse_' + id.slice('session_'.length)
57
+ }