@makerbi/remodex 1.3.8

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.
package/src/bridge.js ADDED
@@ -0,0 +1,2032 @@
1
+ // FILE: bridge.js
2
+ // Purpose: Runs Codex locally, bridges relay traffic, and coordinates desktop refreshes for Codex.app.
3
+ // Layer: CLI service
4
+ // Exports: startBridge
5
+ // Depends on: ws, crypto, os, ./codex-home, ./qr, ./codex-desktop-refresher, ./codex-transport, ./rollout-watch, ./voice-handler, ./ios-app-compatibility
6
+
7
+ const WebSocket = require("ws");
8
+ const { randomBytes } = require("crypto");
9
+ const { execFile, spawn } = require("child_process");
10
+ const path = require("path");
11
+ const os = require("os");
12
+ const { promisify } = require("util");
13
+ const {
14
+ CodexDesktopRefresher,
15
+ readBridgeConfig,
16
+ } = require("./codex-desktop-refresher");
17
+ const { createCodexTransport } = require("./codex-transport");
18
+ const { createThreadRolloutActivityWatcher } = require("./rollout-watch");
19
+ const { printQR } = require("./qr");
20
+ const { rememberActiveThread } = require("./session-state");
21
+ const { handleDesktopRequest } = require("./desktop-handler");
22
+ const { readDaemonConfig, writeDaemonConfig } = require("./daemon-state");
23
+ const { handleGitRequest } = require("./git-handler");
24
+ const { handleThreadContextRequest } = require("./thread-context-handler");
25
+ const { handleWorkspaceRequest } = require("./workspace-handler");
26
+ const { handleProjectRequest } = require("./project-handler");
27
+ const { createNotificationsHandler } = require("./notifications-handler");
28
+ const { createVoiceHandler, resolveVoiceAuth } = require("./voice-handler");
29
+ const {
30
+ composeSanitizedAuthStatusFromSettledResults,
31
+ } = require("./account-status");
32
+ const { createBridgePackageVersionStatusReader } = require("./package-version-status");
33
+ const { createPushNotificationServiceClient } = require("./push-notification-service-client");
34
+ const { createPushNotificationTracker } = require("./push-notification-tracker");
35
+ const { resolveCodexGeneratedImagesRoot } = require("./codex-home");
36
+ const {
37
+ loadOrCreateBridgeDeviceState,
38
+ rememberLastSeenPhoneAppVersion,
39
+ resolveBridgeRelaySession,
40
+ } = require("./secure-device-state");
41
+ const { createBridgeSecureTransport } = require("./secure-transport");
42
+ const { createRolloutLiveMirrorController } = require("./rollout-live-mirror");
43
+ const { version: bridgePackageVersion = "" } = require("../package.json");
44
+ const {
45
+ MINIMUM_SUPPORTED_IOS_APP_VERSION,
46
+ buildCachedIOSAppCompatibilityWarning,
47
+ buildIOSAppCompatibilitySnapshot,
48
+ normalizeVersionString,
49
+ } = require("./ios-app-compatibility");
50
+ const { createShortPairingCode, SHORT_PAIRING_CODE_LENGTH } = require("./qr");
51
+
52
+ const execFileAsync = promisify(execFile);
53
+ const RELAY_WATCHDOG_PING_INTERVAL_MS = 10_000;
54
+ const RELAY_WATCHDOG_STALE_AFTER_MS = 25_000;
55
+ const BRIDGE_STATUS_HEARTBEAT_INTERVAL_MS = 5_000;
56
+ const STALE_RELAY_STATUS_MESSAGE = "Relay heartbeat stalled; reconnect pending.";
57
+ const RELAY_HISTORY_IMAGE_REFERENCE_URL = "remodex://history-image-elided";
58
+ const RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES = 3 * 1024 * 1024;
59
+ const RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS = 24_000;
60
+
61
+ function buildRelayUserAgentHeader({ version = bridgePackageVersion } = {}) {
62
+ const normalizedVersion = typeof version === "string" && version.trim()
63
+ ? version.trim().replace(/\s+/g, "-")
64
+ : "dev";
65
+ return `RemodexBridge/${normalizedVersion}`;
66
+ }
67
+
68
+ function buildRelayAccessTokenHeaders(config = {}, env = process.env) {
69
+ const token = normalizeNonEmptyString(
70
+ config.relayAccessToken
71
+ || env.REMODEX_RELAY_ACCESS_TOKEN
72
+ || env.PHODEX_RELAY_ACCESS_TOKEN
73
+ );
74
+ return token
75
+ ? { "x-remodex-relay-token": token }
76
+ : {};
77
+ }
78
+
79
+ function startBridge({
80
+ config: explicitConfig = null,
81
+ printPairingQr = true,
82
+ onPairingSession = null,
83
+ onBridgeStatus = null,
84
+ } = {}) {
85
+ const config = explicitConfig || readBridgeConfig();
86
+ config.keepMacAwakeEnabled = config.keepMacAwakeEnabled === true;
87
+ const bridgeWakeAssertion = createMacOSBridgeWakeAssertion({
88
+ enabled: config.keepMacAwakeEnabled,
89
+ });
90
+ const relayBaseUrl = config.relayUrl.replace(/\/+$/, "");
91
+ if (!relayBaseUrl) {
92
+ console.error("[remodex] No relay URL configured.");
93
+ console.error("[remodex] In a source checkout, run ./run-local-remodex.sh or set REMODEX_RELAY.");
94
+ process.exit(1);
95
+ }
96
+
97
+ let deviceState;
98
+ try {
99
+ deviceState = loadOrCreateBridgeDeviceState();
100
+ } catch (error) {
101
+ console.error(`[remodex] ${(error && error.message) || "Failed to load the saved bridge pairing state."}`);
102
+ process.exit(1);
103
+ }
104
+ const relaySession = resolveBridgeRelaySession(deviceState);
105
+ deviceState = relaySession.deviceState;
106
+ let lastIOSAppCompatibilityWarning = "";
107
+ const cachedIOSAppCompatibilityWarning = buildCachedIOSAppCompatibilityWarning({
108
+ bridgeVersion: bridgePackageVersion,
109
+ iosAppVersion: deviceState.lastSeenPhoneAppVersion,
110
+ });
111
+ logIOSAppCompatibilityWarning(cachedIOSAppCompatibilityWarning);
112
+ const sessionId = relaySession.sessionId;
113
+ const relaySessionUrl = `${relayBaseUrl}/${sessionId}`;
114
+ const notificationSecret = randomBytes(24).toString("hex");
115
+ const desktopRefresher = new CodexDesktopRefresher({
116
+ enabled: config.refreshEnabled,
117
+ debounceMs: config.refreshDebounceMs,
118
+ refreshCommand: config.refreshCommand,
119
+ bundleId: config.codexBundleId,
120
+ appPath: config.codexAppPath,
121
+ });
122
+ const pushServiceClient = createPushNotificationServiceClient({
123
+ baseUrl: config.pushServiceUrl,
124
+ sessionId,
125
+ notificationSecret,
126
+ });
127
+ const notificationsHandler = createNotificationsHandler({
128
+ pushServiceClient,
129
+ });
130
+ const pushNotificationTracker = createPushNotificationTracker({
131
+ sessionId,
132
+ pushServiceClient,
133
+ previewMaxChars: config.pushPreviewMaxChars,
134
+ });
135
+ const readBridgePackageVersionStatus = createBridgePackageVersionStatusReader();
136
+
137
+ // Keep the local Codex runtime alive across transient relay disconnects.
138
+ let socket = null;
139
+ let isShuttingDown = false;
140
+ let reconnectAttempt = 0;
141
+ let reconnectTimer = null;
142
+ let relayWatchdogTimer = null;
143
+ let statusHeartbeatTimer = null;
144
+ let lastRelayActivityAt = 0;
145
+ let lastPublishedBridgeStatus = null;
146
+ let lastConnectionStatus = null;
147
+ let codexLaunchState = config.codexEndpoint ? "connected" : "starting";
148
+ let codexHandshakeState = config.codexEndpoint ? "warm" : "cold";
149
+ const forwardedInitializeRequestIds = new Set();
150
+ const bridgeManagedCodexRequestWaiters = new Map();
151
+ const forwardedRequestMethodsById = new Map();
152
+ const relaySanitizedResponseMethodsById = new Map();
153
+ const trackedForwardedRequestMethods = new Set([
154
+ "account/login/start",
155
+ "account/login/cancel",
156
+ "account/logout",
157
+ ]);
158
+ const relaySanitizedRequestMethods = new Set([
159
+ "thread/read",
160
+ "thread/resume",
161
+ ]);
162
+ const forwardedRequestMethodTTLms = 2 * 60_000;
163
+ const pendingAuthLogin = {
164
+ loginId: null,
165
+ authUrl: null,
166
+ requestId: null,
167
+ startedAt: 0,
168
+ };
169
+ const secureTransport = createBridgeSecureTransport({
170
+ sessionId,
171
+ relayUrl: relayBaseUrl,
172
+ deviceState,
173
+ onTrustedPhoneUpdate(nextDeviceState) {
174
+ deviceState = nextDeviceState;
175
+ sendRelayRegistrationUpdate(nextDeviceState);
176
+ },
177
+ });
178
+ // Keeps one stable sender identity across reconnects so buffered replay state
179
+ // reflects what actually made it onto the current relay socket.
180
+ function sendRelayWireMessage(wireMessage) {
181
+ if (socket?.readyState !== WebSocket.OPEN) {
182
+ return false;
183
+ }
184
+
185
+ socket.send(wireMessage);
186
+ return true;
187
+ }
188
+ // Only the spawned local runtime needs rollout mirroring; a real endpoint
189
+ // already provides the authoritative live stream for resumed threads.
190
+ const rolloutLiveMirror = !config.codexEndpoint
191
+ ? createRolloutLiveMirrorController({
192
+ sendApplicationResponse,
193
+ })
194
+ : null;
195
+ let contextUsageWatcher = null;
196
+ let watchedContextUsageKey = null;
197
+
198
+ const codex = createCodexTransport({
199
+ endpoint: config.codexEndpoint,
200
+ env: process.env,
201
+ appPath: config.codexAppPath,
202
+ logPrefix: "[remodex]",
203
+ });
204
+ const voiceHandler = createVoiceHandler({
205
+ sendCodexRequest,
206
+ logPrefix: "[remodex]",
207
+ });
208
+ startBridgeStatusHeartbeat();
209
+ publishBridgeStatus({
210
+ state: "starting",
211
+ connectionStatus: "starting",
212
+ pid: process.pid,
213
+ lastError: "",
214
+ });
215
+
216
+ codex.onError((error) => {
217
+ codexLaunchState = "error";
218
+ publishBridgeStatus({
219
+ state: "error",
220
+ connectionStatus: "error",
221
+ pid: process.pid,
222
+ lastError: error.message,
223
+ });
224
+ if (config.codexEndpoint) {
225
+ console.error(`[remodex] Failed to connect to Codex endpoint: ${config.codexEndpoint}`);
226
+ } else {
227
+ console.error("[remodex] Failed to start `codex app-server`.");
228
+ console.error(`[remodex] Launch command: ${codex.describe()}`);
229
+ console.error("[remodex] Make sure the Codex CLI is installed and that the launcher works on this OS.");
230
+ }
231
+ console.error(error.message);
232
+ process.exit(1);
233
+ });
234
+ // Marks the local Codex runtime as launchable before relay/network recovery updates.
235
+ codex.onStarted(() => {
236
+ codexLaunchState = "connected";
237
+ if (!lastPublishedBridgeStatus) {
238
+ return;
239
+ }
240
+
241
+ publishBridgeStatus(lastPublishedBridgeStatus);
242
+ });
243
+
244
+ function clearReconnectTimer() {
245
+ if (!reconnectTimer) {
246
+ return;
247
+ }
248
+
249
+ clearTimeout(reconnectTimer);
250
+ reconnectTimer = null;
251
+ }
252
+
253
+ // Periodically rewrites the latest bridge snapshot so CLI status does not stay frozen.
254
+ function startBridgeStatusHeartbeat() {
255
+ if (statusHeartbeatTimer) {
256
+ return;
257
+ }
258
+
259
+ statusHeartbeatTimer = setInterval(() => {
260
+ if (!lastPublishedBridgeStatus || isShuttingDown) {
261
+ return;
262
+ }
263
+
264
+ onBridgeStatus?.(buildHeartbeatBridgeStatus(lastPublishedBridgeStatus, lastRelayActivityAt));
265
+ }, BRIDGE_STATUS_HEARTBEAT_INTERVAL_MS);
266
+ statusHeartbeatTimer.unref?.();
267
+ }
268
+
269
+ function clearBridgeStatusHeartbeat() {
270
+ if (!statusHeartbeatTimer) {
271
+ return;
272
+ }
273
+
274
+ clearInterval(statusHeartbeatTimer);
275
+ statusHeartbeatTimer = null;
276
+ }
277
+
278
+ // Tracks relay liveness locally so sleep/wake zombie sockets can be force-reconnected.
279
+ function markRelayActivity() {
280
+ lastRelayActivityAt = Date.now();
281
+ }
282
+
283
+ function clearRelayWatchdog() {
284
+ if (!relayWatchdogTimer) {
285
+ return;
286
+ }
287
+
288
+ clearInterval(relayWatchdogTimer);
289
+ relayWatchdogTimer = null;
290
+ }
291
+
292
+ function startRelayWatchdog(trackedSocket) {
293
+ clearRelayWatchdog();
294
+ markRelayActivity();
295
+
296
+ relayWatchdogTimer = setInterval(() => {
297
+ if (isShuttingDown || socket !== trackedSocket) {
298
+ clearRelayWatchdog();
299
+ return;
300
+ }
301
+
302
+ if (trackedSocket.readyState !== WebSocket.OPEN) {
303
+ return;
304
+ }
305
+
306
+ if (hasRelayConnectionGoneStale(lastRelayActivityAt)) {
307
+ console.warn("[remodex] relay heartbeat stalled; forcing reconnect");
308
+ logConnectionStatus("disconnected");
309
+ trackedSocket.terminate();
310
+ return;
311
+ }
312
+
313
+ try {
314
+ trackedSocket.ping();
315
+ } catch {
316
+ trackedSocket.terminate();
317
+ }
318
+ }, RELAY_WATCHDOG_PING_INTERVAL_MS);
319
+ relayWatchdogTimer.unref?.();
320
+ }
321
+
322
+ // Keeps npm start output compact by emitting only high-signal connection states.
323
+ function logConnectionStatus(status) {
324
+ if (lastConnectionStatus === status) {
325
+ return;
326
+ }
327
+
328
+ lastConnectionStatus = status;
329
+ publishBridgeStatus({
330
+ state: "running",
331
+ connectionStatus: status,
332
+ pid: process.pid,
333
+ lastError: "",
334
+ });
335
+ console.log(`[remodex] ${status}`);
336
+ }
337
+
338
+ // Retries the relay socket while preserving the active Codex process and session id.
339
+ function scheduleRelayReconnect(closeCode) {
340
+ if (isShuttingDown) {
341
+ return;
342
+ }
343
+
344
+ if (closeCode === 4000 || closeCode === 4001) {
345
+ logConnectionStatus("disconnected");
346
+ shutdown(codex, () => socket, () => {
347
+ isShuttingDown = true;
348
+ bridgeWakeAssertion.stop();
349
+ clearReconnectTimer();
350
+ clearRelayWatchdog();
351
+ clearBridgeStatusHeartbeat();
352
+ });
353
+ return;
354
+ }
355
+
356
+ if (reconnectTimer) {
357
+ return;
358
+ }
359
+
360
+ reconnectAttempt += 1;
361
+ const delayMs = Math.min(1_000 * reconnectAttempt, 5_000);
362
+ logConnectionStatus("connecting");
363
+ reconnectTimer = setTimeout(() => {
364
+ reconnectTimer = null;
365
+ connectRelay();
366
+ }, delayMs);
367
+ }
368
+
369
+ function connectRelay() {
370
+ if (isShuttingDown) {
371
+ return;
372
+ }
373
+
374
+ logConnectionStatus("connecting");
375
+ const nextSocket = new WebSocket(relaySessionUrl, {
376
+ // The relay uses this per-session secret to authenticate the first push registration.
377
+ headers: {
378
+ "User-Agent": buildRelayUserAgentHeader(),
379
+ "x-role": "mac",
380
+ "x-notification-secret": notificationSecret,
381
+ ...buildRelayAccessTokenHeaders(config),
382
+ ...buildMacRegistrationHeaders(deviceState, pairingSession),
383
+ },
384
+ });
385
+ socket = nextSocket;
386
+
387
+ nextSocket.on("open", () => {
388
+ markRelayActivity();
389
+ clearReconnectTimer();
390
+ reconnectAttempt = 0;
391
+ startRelayWatchdog(nextSocket);
392
+ logConnectionStatus("connected");
393
+ secureTransport.bindLiveSendWireMessage(sendRelayWireMessage);
394
+ sendRelayRegistrationUpdate(deviceState);
395
+ });
396
+
397
+ nextSocket.on("message", (data) => {
398
+ markRelayActivity();
399
+ const message = typeof data === "string" ? data : data.toString("utf8");
400
+ if (secureTransport.handleIncomingWireMessage(message, {
401
+ sendControlMessage(controlMessage) {
402
+ if (nextSocket.readyState === WebSocket.OPEN) {
403
+ nextSocket.send(JSON.stringify(controlMessage));
404
+ }
405
+ },
406
+ onApplicationMessage(plaintextMessage) {
407
+ handleApplicationMessage(plaintextMessage);
408
+ },
409
+ })) {
410
+ return;
411
+ }
412
+ });
413
+
414
+ nextSocket.on("ping", () => {
415
+ markRelayActivity();
416
+ });
417
+
418
+ nextSocket.on("pong", () => {
419
+ markRelayActivity();
420
+ });
421
+
422
+ nextSocket.on("close", (code) => {
423
+ if (socket === nextSocket) {
424
+ clearRelayWatchdog();
425
+ }
426
+ logConnectionStatus("disconnected");
427
+ if (socket === nextSocket) {
428
+ socket = null;
429
+ }
430
+ stopContextUsageWatcher();
431
+ rolloutLiveMirror?.stopAll();
432
+ desktopRefresher.handleTransportReset();
433
+ scheduleRelayReconnect(code);
434
+ });
435
+
436
+ nextSocket.on("error", () => {
437
+ if (socket === nextSocket) {
438
+ clearRelayWatchdog();
439
+ }
440
+ logConnectionStatus("disconnected");
441
+ });
442
+ }
443
+
444
+ const pairingPayload = secureTransport.createPairingPayload();
445
+ const pairingSession = {
446
+ pairingPayload,
447
+ pairingCode: createShortPairingCode({ length: SHORT_PAIRING_CODE_LENGTH }),
448
+ };
449
+ onPairingSession?.(pairingSession);
450
+ if (printPairingQr) {
451
+ printQR(pairingSession);
452
+ }
453
+ pushServiceClient.logUnavailable();
454
+ connectRelay();
455
+
456
+ codex.onMessage((message) => {
457
+ if (handleBridgeManagedCodexResponse(message)) {
458
+ return;
459
+ }
460
+ updatePendingAuthLoginFromCodexMessage(message);
461
+ trackCodexHandshakeState(message);
462
+ desktopRefresher.handleOutbound(message);
463
+ pushNotificationTracker.handleOutbound(message);
464
+ rememberThreadFromMessage("codex", message);
465
+ secureTransport.queueOutboundApplicationMessage(
466
+ sanitizeRelayBoundCodexMessage(message),
467
+ sendRelayWireMessage
468
+ );
469
+ });
470
+
471
+ codex.onClose(() => {
472
+ clearRelayWatchdog();
473
+ clearBridgeStatusHeartbeat();
474
+ logConnectionStatus("disconnected");
475
+ publishBridgeStatus({
476
+ state: "stopped",
477
+ connectionStatus: "disconnected",
478
+ pid: process.pid,
479
+ lastError: "",
480
+ });
481
+ isShuttingDown = true;
482
+ bridgeWakeAssertion.stop();
483
+ clearReconnectTimer();
484
+ stopContextUsageWatcher();
485
+ rolloutLiveMirror?.stopAll();
486
+ desktopRefresher.handleTransportReset();
487
+ failBridgeManagedCodexRequests(new Error("Codex transport closed before the bridge request completed."));
488
+ forwardedRequestMethodsById.clear();
489
+ if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) {
490
+ socket.close();
491
+ }
492
+ });
493
+
494
+ process.on("SIGINT", () => shutdown(codex, () => socket, () => {
495
+ isShuttingDown = true;
496
+ bridgeWakeAssertion.stop();
497
+ clearReconnectTimer();
498
+ clearRelayWatchdog();
499
+ clearBridgeStatusHeartbeat();
500
+ }));
501
+ process.on("SIGTERM", () => shutdown(codex, () => socket, () => {
502
+ isShuttingDown = true;
503
+ bridgeWakeAssertion.stop();
504
+ clearReconnectTimer();
505
+ clearRelayWatchdog();
506
+ clearBridgeStatusHeartbeat();
507
+ }));
508
+
509
+ // Routes decrypted app payloads through the same bridge handlers as before.
510
+ function handleApplicationMessage(rawMessage) {
511
+ if (handleBridgeManagedHandshakeMessage(rawMessage)) {
512
+ return;
513
+ }
514
+ if (handleBridgeManagedAccountRequest(rawMessage, sendApplicationResponse)) {
515
+ return;
516
+ }
517
+ if (voiceHandler.handleVoiceRequest(rawMessage, sendApplicationResponse)) {
518
+ return;
519
+ }
520
+ if (handleThreadContextRequest(rawMessage, sendApplicationResponse)) {
521
+ return;
522
+ }
523
+ if (handleWorkspaceRequest(rawMessage, sendApplicationResponse)) {
524
+ return;
525
+ }
526
+ if (handleProjectRequest(rawMessage, sendApplicationResponse)) {
527
+ return;
528
+ }
529
+ if (notificationsHandler.handleNotificationsRequest(rawMessage, sendApplicationResponse)) {
530
+ return;
531
+ }
532
+ if (handleDesktopRequest(rawMessage, sendApplicationResponse, {
533
+ bundleId: config.codexBundleId,
534
+ appPath: config.codexAppPath,
535
+ readBridgePreferences,
536
+ updateBridgePreferences,
537
+ })) {
538
+ return;
539
+ }
540
+ if (handleGitRequest(rawMessage, sendApplicationResponse, {
541
+ codexAppPath: config.codexAppPath,
542
+ onThreadNameSet: sendThreadNameUpdatedNotification,
543
+ })) {
544
+ return;
545
+ }
546
+ desktopRefresher.handleInbound(rawMessage);
547
+ rolloutLiveMirror?.observeInbound(rawMessage);
548
+ rememberForwardedRequestMethod(rawMessage);
549
+ rememberThreadFromMessage("phone", rawMessage);
550
+ codex.send(rawMessage);
551
+ }
552
+
553
+ // Encrypts bridge-generated responses instead of letting the relay see plaintext.
554
+ function sendApplicationResponse(rawMessage) {
555
+ secureTransport.queueOutboundApplicationMessage(
556
+ sanitizeRelayBoundCodexMessage(rawMessage),
557
+ sendRelayWireMessage
558
+ );
559
+ }
560
+
561
+ // Mirrors accepted local renames back to the phone using the existing push-event shape.
562
+ function sendThreadNameUpdatedNotification(result) {
563
+ const threadId = readString(result?.threadId || result?.thread_id);
564
+ const name = readString(result?.name || result?.title);
565
+ if (!threadId || !name) {
566
+ return;
567
+ }
568
+
569
+ sendApplicationResponse(JSON.stringify({
570
+ method: "thread/name/updated",
571
+ params: {
572
+ threadId,
573
+ thread_id: threadId,
574
+ name,
575
+ title: name,
576
+ },
577
+ }));
578
+ }
579
+
580
+ // ─── Bridge-owned auth snapshot ─────────────────────────────
581
+
582
+ // Handles the bridge-owned auth status wrappers without exposing tokens to the phone.
583
+ // This dispatcher stays synchronous so non-account messages can continue down the normal routing chain.
584
+ function handleBridgeManagedAccountRequest(rawMessage, sendResponse) {
585
+ let parsed = null;
586
+ try {
587
+ parsed = JSON.parse(rawMessage);
588
+ } catch {
589
+ return false;
590
+ }
591
+
592
+ const method = typeof parsed?.method === "string" ? parsed.method.trim() : "";
593
+ if (method !== "account/status/read"
594
+ && method !== "getAuthStatus"
595
+ && method !== "account/login/openOnMac"
596
+ && method !== "voice/resolveAuth") {
597
+ return false;
598
+ }
599
+
600
+ const requestId = parsed.id;
601
+ const shouldRespond = requestId != null;
602
+ readBridgeManagedAccountResult(method, parsed.params || {})
603
+ .then((result) => {
604
+ if (shouldRespond) {
605
+ sendResponse(JSON.stringify({ id: requestId, result }));
606
+ }
607
+ })
608
+ .catch((error) => {
609
+ if (shouldRespond) {
610
+ sendResponse(createJsonRpcErrorResponse(requestId, error, "auth_status_failed"));
611
+ }
612
+ });
613
+
614
+ return true;
615
+ }
616
+
617
+ // Resolves bridge-owned account helpers like status reads and Mac-side browser opening.
618
+ async function readBridgeManagedAccountResult(method, params) {
619
+ switch (method) {
620
+ case "account/status/read":
621
+ case "getAuthStatus":
622
+ return readSanitizedAuthStatus();
623
+ case "account/login/openOnMac":
624
+ return openPendingAuthLoginOnMac(params);
625
+ case "voice/resolveAuth":
626
+ return resolveVoiceAuth(sendCodexRequest);
627
+ default:
628
+ throw new Error(`Unsupported bridge-managed account method: ${method}`);
629
+ }
630
+ }
631
+
632
+ // Combines account/read + getAuthStatus into one safe snapshot for the phone UI.
633
+ // The two RPCs are settled independently so one transient failure does not hide the other.
634
+ async function readSanitizedAuthStatus() {
635
+ const [accountReadResult, authStatusResult, bridgeVersionInfoResult] = await Promise.allSettled([
636
+ sendCodexRequest("account/read", {
637
+ refreshToken: false,
638
+ }),
639
+ sendCodexRequest("getAuthStatus", {
640
+ includeToken: true,
641
+ refreshToken: true,
642
+ }),
643
+ readBridgePackageVersionStatus(),
644
+ ]);
645
+
646
+ return composeSanitizedAuthStatusFromSettledResults({
647
+ accountReadResult: accountReadResult.status === "fulfilled"
648
+ ? {
649
+ status: "fulfilled",
650
+ value: normalizeAccountRead(accountReadResult.value),
651
+ }
652
+ : accountReadResult,
653
+ authStatusResult,
654
+ loginInFlight: Boolean(pendingAuthLogin.loginId),
655
+ bridgeVersionInfo: bridgeVersionInfoResult.status === "fulfilled"
656
+ ? bridgeVersionInfoResult.value
657
+ : null,
658
+ transportMode: codex.mode,
659
+ hostPlatform: process.platform,
660
+ });
661
+ }
662
+
663
+ // Opens the ChatGPT sign-in URL in the default browser on the bridge Mac.
664
+ async function openPendingAuthLoginOnMac(params) {
665
+ if (process.platform !== "darwin") {
666
+ const error = new Error("Opening ChatGPT sign-in on the bridge is only supported on macOS.");
667
+ error.errorCode = "unsupported_platform";
668
+ throw error;
669
+ }
670
+
671
+ const authUrl = readString(params?.authUrl) || pendingAuthLogin.authUrl;
672
+ if (!authUrl) {
673
+ const error = new Error("No pending ChatGPT sign-in URL is available on this bridge.");
674
+ error.errorCode = "missing_auth_url";
675
+ throw error;
676
+ }
677
+
678
+ await execFileAsync("open", [authUrl], { timeout: 15_000 });
679
+ return {
680
+ success: true,
681
+ openedOnMac: true,
682
+ };
683
+ }
684
+
685
+ function normalizeAccountRead(payload) {
686
+ if (!payload || typeof payload !== "object") {
687
+ return {
688
+ account: null,
689
+ requiresOpenaiAuth: true,
690
+ };
691
+ }
692
+
693
+ return {
694
+ account: payload.account && typeof payload.account === "object" ? payload.account : null,
695
+ requiresOpenaiAuth: Boolean(payload.requiresOpenaiAuth),
696
+ };
697
+ }
698
+
699
+ function createJsonRpcErrorResponse(requestId, error, defaultErrorCode) {
700
+ return JSON.stringify({
701
+ id: requestId,
702
+ error: {
703
+ code: -32000,
704
+ message: error?.userMessage || error?.message || "Bridge request failed.",
705
+ data: {
706
+ errorCode: error?.errorCode || defaultErrorCode,
707
+ },
708
+ },
709
+ });
710
+ }
711
+
712
+ function rememberForwardedRequestMethod(rawMessage) {
713
+ const parsed = safeParseJSON(rawMessage);
714
+ const method = typeof parsed?.method === "string" ? parsed.method.trim() : "";
715
+ const requestId = parsed?.id;
716
+ if (!method || requestId == null) {
717
+ return;
718
+ }
719
+
720
+ pruneExpiredForwardedRequestMethods();
721
+ if (trackedForwardedRequestMethods.has(method)) {
722
+ forwardedRequestMethodsById.set(String(requestId), {
723
+ method,
724
+ createdAt: Date.now(),
725
+ });
726
+ }
727
+ if (relaySanitizedRequestMethods.has(method)) {
728
+ relaySanitizedResponseMethodsById.set(String(requestId), {
729
+ method,
730
+ createdAt: Date.now(),
731
+ });
732
+ }
733
+ }
734
+
735
+ // Replaces huge inline desktop-history images with lightweight references before relay encryption.
736
+ function sanitizeRelayBoundCodexMessage(rawMessage) {
737
+ pruneExpiredForwardedRequestMethods();
738
+ const parsed = safeParseJSON(rawMessage);
739
+ const responseId = parsed?.id;
740
+ if (responseId == null) {
741
+ return sanitizeLiveGeneratedImageMessageForRelay(rawMessage);
742
+ }
743
+
744
+ const trackedRequest = relaySanitizedResponseMethodsById.get(String(responseId));
745
+ if (!trackedRequest) {
746
+ return rawMessage;
747
+ }
748
+ relaySanitizedResponseMethodsById.delete(String(responseId));
749
+
750
+ return sanitizeThreadHistoryImagesForRelay(rawMessage, trackedRequest.method);
751
+ }
752
+
753
+ function updatePendingAuthLoginFromCodexMessage(rawMessage) {
754
+ pruneExpiredForwardedRequestMethods();
755
+ const parsed = safeParseJSON(rawMessage);
756
+ const responseId = parsed?.id;
757
+ if (responseId != null) {
758
+ const trackedRequest = forwardedRequestMethodsById.get(String(responseId));
759
+ if (trackedRequest) {
760
+ forwardedRequestMethodsById.delete(String(responseId));
761
+ const requestMethod = trackedRequest.method;
762
+
763
+ if (requestMethod === "account/login/start") {
764
+ const loginId = readString(parsed?.result?.loginId);
765
+ const authUrl = readString(parsed?.result?.authUrl);
766
+ if (!loginId || !authUrl) {
767
+ clearPendingAuthLogin();
768
+ return;
769
+ }
770
+ pendingAuthLogin.loginId = loginId || null;
771
+ pendingAuthLogin.authUrl = authUrl || null;
772
+ pendingAuthLogin.requestId = String(responseId);
773
+ pendingAuthLogin.startedAt = Date.now();
774
+ return;
775
+ }
776
+
777
+ if (requestMethod === "account/login/cancel" || requestMethod === "account/logout") {
778
+ clearPendingAuthLogin();
779
+ return;
780
+ }
781
+ }
782
+ }
783
+
784
+ const method = typeof parsed?.method === "string" ? parsed.method.trim() : "";
785
+ if (method === "account/login/completed") {
786
+ clearPendingAuthLogin();
787
+ return;
788
+ }
789
+
790
+ if (method === "account/updated") {
791
+ clearPendingAuthLogin();
792
+ }
793
+ }
794
+
795
+ function clearPendingAuthLogin() {
796
+ pendingAuthLogin.loginId = null;
797
+ pendingAuthLogin.authUrl = null;
798
+ pendingAuthLogin.requestId = null;
799
+ pendingAuthLogin.startedAt = 0;
800
+ }
801
+
802
+ function pruneExpiredForwardedRequestMethods(now = Date.now()) {
803
+ for (const [requestId, trackedRequest] of forwardedRequestMethodsById.entries()) {
804
+ if (!trackedRequest || (now - trackedRequest.createdAt) >= forwardedRequestMethodTTLms) {
805
+ forwardedRequestMethodsById.delete(requestId);
806
+ }
807
+ }
808
+ for (const [requestId, trackedRequest] of relaySanitizedResponseMethodsById.entries()) {
809
+ if (!trackedRequest || (now - trackedRequest.createdAt) >= forwardedRequestMethodTTLms) {
810
+ relaySanitizedResponseMethodsById.delete(requestId);
811
+ }
812
+ }
813
+ }
814
+
815
+ function safeParseJSON(value) {
816
+ try {
817
+ return JSON.parse(value);
818
+ } catch {
819
+ return null;
820
+ }
821
+ }
822
+
823
+ function rememberThreadFromMessage(source, rawMessage) {
824
+ const context = extractBridgeMessageContext(rawMessage);
825
+ if (!context.threadId) {
826
+ return;
827
+ }
828
+
829
+ rememberActiveThread(context.threadId, source);
830
+ if (shouldStartContextUsageWatcher(context)) {
831
+ ensureContextUsageWatcher(context);
832
+ }
833
+ }
834
+
835
+ // Mirrors CodexMonitor's persisted token_count fallback so the phone keeps
836
+ // receiving context-window usage even when the runtime omits live thread usage.
837
+ function ensureContextUsageWatcher({ threadId, turnId }) {
838
+ const normalizedThreadId = readString(threadId);
839
+ const normalizedTurnId = readString(turnId);
840
+ if (!normalizedThreadId) {
841
+ return;
842
+ }
843
+
844
+ const nextWatcherKey = `${normalizedThreadId}|${normalizedTurnId || "pending-turn"}`;
845
+ if (watchedContextUsageKey === nextWatcherKey && contextUsageWatcher) {
846
+ return;
847
+ }
848
+
849
+ stopContextUsageWatcher();
850
+ watchedContextUsageKey = nextWatcherKey;
851
+ contextUsageWatcher = createThreadRolloutActivityWatcher({
852
+ threadId: normalizedThreadId,
853
+ turnId: normalizedTurnId,
854
+ onUsage: ({ threadId: usageThreadId, usage }) => {
855
+ sendContextUsageNotification(usageThreadId, usage);
856
+ },
857
+ onIdle: () => {
858
+ if (watchedContextUsageKey === nextWatcherKey) {
859
+ stopContextUsageWatcher();
860
+ }
861
+ },
862
+ onTimeout: () => {
863
+ if (watchedContextUsageKey === nextWatcherKey) {
864
+ stopContextUsageWatcher();
865
+ }
866
+ },
867
+ onError: () => {
868
+ if (watchedContextUsageKey === nextWatcherKey) {
869
+ stopContextUsageWatcher();
870
+ }
871
+ },
872
+ });
873
+ }
874
+
875
+ function stopContextUsageWatcher() {
876
+ if (contextUsageWatcher) {
877
+ contextUsageWatcher.stop();
878
+ }
879
+
880
+ contextUsageWatcher = null;
881
+ watchedContextUsageKey = null;
882
+ }
883
+
884
+ function sendContextUsageNotification(threadId, usage) {
885
+ if (!threadId || !usage) {
886
+ return;
887
+ }
888
+
889
+ sendApplicationResponse(JSON.stringify({
890
+ method: "thread/tokenUsage/updated",
891
+ params: {
892
+ threadId,
893
+ usage,
894
+ },
895
+ }));
896
+ }
897
+
898
+ // The spawned/shared Codex app-server stays warm across phone reconnects.
899
+ // When iPhone reconnects it sends initialize again, but forwarding that to the
900
+ // already-initialized Codex transport only produces "Already initialized".
901
+ function handleBridgeManagedHandshakeMessage(rawMessage) {
902
+ let parsed = null;
903
+ try {
904
+ parsed = JSON.parse(rawMessage);
905
+ } catch {
906
+ return false;
907
+ }
908
+
909
+ const method = typeof parsed?.method === "string" ? parsed.method.trim() : "";
910
+ if (!method) {
911
+ return false;
912
+ }
913
+
914
+ if (method === "initialize" && parsed.id != null) {
915
+ const compatibilityError = bridgeManagedInitializeCompatibilityError(parsed.params || {});
916
+ if (compatibilityError) {
917
+ sendApplicationResponse(JSON.stringify({
918
+ id: parsed.id,
919
+ error: compatibilityError,
920
+ }));
921
+ return true;
922
+ }
923
+
924
+ if (codexHandshakeState !== "warm") {
925
+ forwardedInitializeRequestIds.add(String(parsed.id));
926
+ return false;
927
+ }
928
+
929
+ sendApplicationResponse(JSON.stringify({
930
+ id: parsed.id,
931
+ result: {
932
+ bridgeManaged: true,
933
+ },
934
+ }));
935
+ return true;
936
+ }
937
+
938
+ if (method === "initialized") {
939
+ return codexHandshakeState === "warm";
940
+ }
941
+
942
+ return false;
943
+ }
944
+
945
+ // Blocks bridge/app version skew before the phone starts calling newer bridge APIs.
946
+ function bridgeManagedInitializeCompatibilityError(params) {
947
+ const clientInfo = params && typeof params === "object" ? params.clientInfo : null;
948
+ const clientName = normalizeNonEmptyString(clientInfo?.name);
949
+ if (clientName !== "codexmobile_ios") {
950
+ return null;
951
+ }
952
+
953
+ const clientVersion = normalizeVersionString(clientInfo?.version);
954
+ if (clientVersion) {
955
+ deviceState = rememberLastSeenPhoneAppVersion(deviceState, clientVersion);
956
+ }
957
+
958
+ const compatibility = buildIOSAppCompatibilitySnapshot({
959
+ bridgeVersion: bridgePackageVersion,
960
+ iosAppVersion: clientVersion,
961
+ });
962
+ if (!compatibility.requiresAppUpdate) {
963
+ return null;
964
+ }
965
+
966
+ logIOSAppCompatibilityWarning(buildCachedIOSAppCompatibilityWarning({
967
+ bridgeVersion: bridgePackageVersion,
968
+ iosAppVersion: clientVersion,
969
+ }));
970
+
971
+ return {
972
+ code: -32001,
973
+ message: compatibility.message,
974
+ data: {
975
+ errorCode: "ios_app_update_required",
976
+ minimumSupportedAppVersion: MINIMUM_SUPPORTED_IOS_APP_VERSION,
977
+ bridgeVersion: normalizeVersionString(bridgePackageVersion) || null,
978
+ clientVersion,
979
+ compatibleBridgeVersion: compatibility.legacyBridgeVersion,
980
+ downgradeCommand: compatibility.downgradeCommand,
981
+ },
982
+ };
983
+ }
984
+
985
+ function logIOSAppCompatibilityWarning(warning) {
986
+ const normalizedWarning = typeof warning === "string" ? warning.trim() : "";
987
+ if (!normalizedWarning || normalizedWarning === lastIOSAppCompatibilityWarning) {
988
+ return;
989
+ }
990
+
991
+ lastIOSAppCompatibilityWarning = normalizedWarning;
992
+ console.warn(normalizedWarning);
993
+ }
994
+
995
+ // Learns whether the underlying Codex transport has already completed its own MCP handshake.
996
+ function trackCodexHandshakeState(rawMessage) {
997
+ let parsed = null;
998
+ try {
999
+ parsed = JSON.parse(rawMessage);
1000
+ } catch {
1001
+ return;
1002
+ }
1003
+
1004
+ const responseId = parsed?.id;
1005
+ if (responseId == null) {
1006
+ return;
1007
+ }
1008
+
1009
+ const responseKey = String(responseId);
1010
+ if (!forwardedInitializeRequestIds.has(responseKey)) {
1011
+ return;
1012
+ }
1013
+
1014
+ forwardedInitializeRequestIds.delete(responseKey);
1015
+
1016
+ if (parsed?.result != null) {
1017
+ codexHandshakeState = "warm";
1018
+ return;
1019
+ }
1020
+
1021
+ const errorMessage = typeof parsed?.error?.message === "string"
1022
+ ? parsed.error.message.toLowerCase()
1023
+ : "";
1024
+ if (errorMessage.includes("already initialized")) {
1025
+ codexHandshakeState = "warm";
1026
+ }
1027
+ }
1028
+
1029
+ // Runs bridge-private JSON-RPC calls against the local app-server so token-bearing responses
1030
+ // can power bridge features like transcription without ever reaching the phone.
1031
+ function sendCodexRequest(method, params) {
1032
+ const requestId = `bridge-managed-${randomBytes(12).toString("hex")}`;
1033
+ const payload = JSON.stringify({
1034
+ id: requestId,
1035
+ method,
1036
+ params,
1037
+ });
1038
+
1039
+ return new Promise((resolve, reject) => {
1040
+ const timeout = setTimeout(() => {
1041
+ bridgeManagedCodexRequestWaiters.delete(requestId);
1042
+ reject(new Error(`Codex request timed out: ${method}`));
1043
+ }, 20_000);
1044
+
1045
+ bridgeManagedCodexRequestWaiters.set(requestId, {
1046
+ method,
1047
+ resolve,
1048
+ reject,
1049
+ timeout,
1050
+ });
1051
+
1052
+ try {
1053
+ codex.send(payload);
1054
+ } catch (error) {
1055
+ clearTimeout(timeout);
1056
+ bridgeManagedCodexRequestWaiters.delete(requestId);
1057
+ reject(error);
1058
+ }
1059
+ });
1060
+ }
1061
+
1062
+ // Intercepts responses for bridge-private requests so only user-visible app-server traffic
1063
+ // is forwarded back through secure transport.
1064
+ function handleBridgeManagedCodexResponse(rawMessage) {
1065
+ let parsed = null;
1066
+ try {
1067
+ parsed = JSON.parse(rawMessage);
1068
+ } catch {
1069
+ return false;
1070
+ }
1071
+
1072
+ const responseId = typeof parsed?.id === "string" ? parsed.id : null;
1073
+ if (!responseId) {
1074
+ return false;
1075
+ }
1076
+
1077
+ const waiter = bridgeManagedCodexRequestWaiters.get(responseId);
1078
+ if (!waiter) {
1079
+ return false;
1080
+ }
1081
+
1082
+ bridgeManagedCodexRequestWaiters.delete(responseId);
1083
+ clearTimeout(waiter.timeout);
1084
+
1085
+ if (parsed.error) {
1086
+ const error = new Error(parsed.error.message || `Codex request failed: ${waiter.method}`);
1087
+ error.code = parsed.error.code;
1088
+ error.data = parsed.error.data;
1089
+ waiter.reject(error);
1090
+ return true;
1091
+ }
1092
+
1093
+ waiter.resolve(parsed.result ?? null);
1094
+ return true;
1095
+ }
1096
+
1097
+ function failBridgeManagedCodexRequests(error) {
1098
+ for (const waiter of bridgeManagedCodexRequestWaiters.values()) {
1099
+ clearTimeout(waiter.timeout);
1100
+ waiter.reject(error);
1101
+ }
1102
+ bridgeManagedCodexRequestWaiters.clear();
1103
+ }
1104
+
1105
+ function publishBridgeStatus(status) {
1106
+ const nextStatus = {
1107
+ ...status,
1108
+ codexLaunchState,
1109
+ };
1110
+ lastPublishedBridgeStatus = nextStatus;
1111
+ onBridgeStatus?.(nextStatus);
1112
+ }
1113
+
1114
+ // Refreshes the relay's trusted-mac index after the QR bootstrap locks in a phone identity.
1115
+ function sendRelayRegistrationUpdate(nextDeviceState) {
1116
+ deviceState = nextDeviceState;
1117
+ if (socket?.readyState !== WebSocket.OPEN) {
1118
+ return;
1119
+ }
1120
+
1121
+ socket.send(JSON.stringify({
1122
+ kind: "relayMacRegistration",
1123
+ registration: buildMacRegistration(nextDeviceState, pairingSession),
1124
+ }));
1125
+ }
1126
+
1127
+ function readBridgePreferences() {
1128
+ return {
1129
+ success: true,
1130
+ preferences: {
1131
+ keepMacAwake: config.keepMacAwakeEnabled !== false,
1132
+ },
1133
+ applied: bridgeWakeAssertion.active,
1134
+ };
1135
+ }
1136
+
1137
+ function updateBridgePreferences(preferences = {}) {
1138
+ const nextKeepMacAwakeEnabled = preferences.keepMacAwake !== false;
1139
+ config.keepMacAwakeEnabled = nextKeepMacAwakeEnabled;
1140
+ bridgeWakeAssertion.setEnabled?.(nextKeepMacAwakeEnabled);
1141
+
1142
+ try {
1143
+ persistBridgePreferences({
1144
+ keepMacAwakeEnabled: nextKeepMacAwakeEnabled,
1145
+ });
1146
+ } catch (error) {
1147
+ const nextError = new Error("Could not save the bridge preference on this Mac.");
1148
+ nextError.errorCode = "bridge_preferences_persist_failed";
1149
+ nextError.userMessage = nextError.message;
1150
+ nextError.cause = error;
1151
+ throw nextError;
1152
+ }
1153
+
1154
+ return readBridgePreferences();
1155
+ }
1156
+ }
1157
+
1158
+ // Holds a single macOS idle-sleep assertion for as long as the bridge process stays alive.
1159
+ function createMacOSBridgeWakeAssertion({
1160
+ platform = process.platform,
1161
+ pid = process.pid,
1162
+ spawnImpl = spawn,
1163
+ consoleImpl = console,
1164
+ enabled = true,
1165
+ } = {}) {
1166
+ if (platform !== "darwin") {
1167
+ return {
1168
+ active: false,
1169
+ enabled: false,
1170
+ setEnabled() {
1171
+ return { active: false, enabled: false };
1172
+ },
1173
+ stop() {},
1174
+ };
1175
+ }
1176
+
1177
+ let desiredEnabled = Boolean(enabled);
1178
+ let child = null;
1179
+
1180
+ function stop() {
1181
+ if (!child || child.killed || typeof child.kill !== "function") {
1182
+ child = null;
1183
+ return;
1184
+ }
1185
+
1186
+ try {
1187
+ child.kill();
1188
+ } catch {}
1189
+ child = null;
1190
+ }
1191
+
1192
+ function start() {
1193
+ if (!desiredEnabled || child) {
1194
+ return;
1195
+ }
1196
+
1197
+ try {
1198
+ const nextChild = spawnImpl("/usr/bin/caffeinate", ["-i", "-w", String(pid)], {
1199
+ stdio: "ignore",
1200
+ });
1201
+
1202
+ nextChild.on?.("error", (error) => {
1203
+ consoleImpl.warn(`[remodex] Failed to hold the Mac awake while the bridge is active: ${error.message}`);
1204
+ });
1205
+ nextChild.on?.("exit", () => {
1206
+ if (child === nextChild) {
1207
+ child = null;
1208
+ }
1209
+ });
1210
+ nextChild.unref?.();
1211
+ child = nextChild;
1212
+ } catch (error) {
1213
+ consoleImpl.warn(
1214
+ `[remodex] Failed to start the bridge wake assertion: ${(error && error.message) || "unknown error"}`
1215
+ );
1216
+ child = null;
1217
+ }
1218
+ }
1219
+
1220
+ function setEnabled(nextEnabled) {
1221
+ desiredEnabled = Boolean(nextEnabled);
1222
+ if (desiredEnabled) {
1223
+ start();
1224
+ } else {
1225
+ stop();
1226
+ }
1227
+
1228
+ return {
1229
+ active: Boolean(child && !child.killed),
1230
+ enabled: desiredEnabled,
1231
+ };
1232
+ }
1233
+
1234
+ start();
1235
+
1236
+ return {
1237
+ get active() {
1238
+ return Boolean(child && !child.killed);
1239
+ },
1240
+ get enabled() {
1241
+ return desiredEnabled;
1242
+ },
1243
+ setEnabled,
1244
+ stop,
1245
+ };
1246
+ }
1247
+
1248
+ // Registers the canonical Mac identity and the one trusted iPhone allowed for auto-resolve.
1249
+ function buildMacRegistrationHeaders(deviceState, pairingSession) {
1250
+ const registration = buildMacRegistration(deviceState, pairingSession);
1251
+ const headers = {
1252
+ "x-mac-device-id": registration.macDeviceId,
1253
+ "x-mac-identity-public-key": registration.macIdentityPublicKey,
1254
+ "x-machine-name": registration.displayName,
1255
+ "x-pairing-code": registration.pairingCode,
1256
+ "x-pairing-version": registration.pairingVersion ? String(registration.pairingVersion) : "",
1257
+ "x-pairing-expires-at": registration.pairingExpiresAt ? String(registration.pairingExpiresAt) : "",
1258
+ };
1259
+ if (registration.trustedPhoneDeviceId && registration.trustedPhonePublicKey) {
1260
+ headers["x-trusted-phone-device-id"] = registration.trustedPhoneDeviceId;
1261
+ headers["x-trusted-phone-public-key"] = registration.trustedPhonePublicKey;
1262
+ }
1263
+ return headers;
1264
+ }
1265
+
1266
+ function buildMacRegistration(deviceState, pairingSession) {
1267
+ const trustedPhoneEntry = Object.entries(deviceState?.trustedPhones || {})[0] || null;
1268
+ return {
1269
+ macDeviceId: normalizeNonEmptyString(deviceState?.macDeviceId),
1270
+ macIdentityPublicKey: normalizeNonEmptyString(deviceState?.macIdentityPublicKey),
1271
+ displayName: normalizeNonEmptyString(os.hostname()),
1272
+ trustedPhoneDeviceId: normalizeNonEmptyString(trustedPhoneEntry?.[0]),
1273
+ trustedPhonePublicKey: normalizeNonEmptyString(trustedPhoneEntry?.[1]),
1274
+ pairingCode: normalizeNonEmptyString(pairingSession?.pairingCode),
1275
+ pairingVersion: Number.isInteger(pairingSession?.pairingPayload?.v) ? pairingSession.pairingPayload.v : 0,
1276
+ pairingExpiresAt: Number.isFinite(pairingSession?.pairingPayload?.expiresAt)
1277
+ ? pairingSession.pairingPayload.expiresAt
1278
+ : 0,
1279
+ };
1280
+ }
1281
+
1282
+ function shutdown(codex, getSocket, beforeExit = () => {}) {
1283
+ beforeExit();
1284
+
1285
+ const socket = getSocket();
1286
+ if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) {
1287
+ socket.close();
1288
+ }
1289
+
1290
+ codex.shutdown();
1291
+
1292
+ setTimeout(() => process.exit(0), 100);
1293
+ }
1294
+
1295
+ function extractBridgeMessageContext(rawMessage) {
1296
+ let parsed = null;
1297
+ try {
1298
+ parsed = JSON.parse(rawMessage);
1299
+ } catch {
1300
+ return { method: "", threadId: null, turnId: null };
1301
+ }
1302
+
1303
+ const method = parsed?.method;
1304
+ const params = parsed?.params;
1305
+ const threadId = extractThreadId(method, params);
1306
+ const turnId = extractTurnId(method, params);
1307
+
1308
+ return {
1309
+ method: typeof method === "string" ? method : "",
1310
+ threadId,
1311
+ turnId,
1312
+ };
1313
+ }
1314
+
1315
+ function shouldStartContextUsageWatcher(context) {
1316
+ if (!context?.threadId) {
1317
+ return false;
1318
+ }
1319
+
1320
+ return context.method === "turn/start"
1321
+ || context.method === "turn/started";
1322
+ }
1323
+
1324
+ function extractThreadId(method, params) {
1325
+ if (method === "turn/start" || method === "turn/started") {
1326
+ return (
1327
+ readString(params?.threadId)
1328
+ || readString(params?.thread_id)
1329
+ || readString(params?.turn?.threadId)
1330
+ || readString(params?.turn?.thread_id)
1331
+ );
1332
+ }
1333
+
1334
+ if (method === "thread/start" || method === "thread/started") {
1335
+ return (
1336
+ readString(params?.threadId)
1337
+ || readString(params?.thread_id)
1338
+ || readString(params?.thread?.id)
1339
+ || readString(params?.thread?.threadId)
1340
+ || readString(params?.thread?.thread_id)
1341
+ );
1342
+ }
1343
+
1344
+ if (method === "turn/completed") {
1345
+ return (
1346
+ readString(params?.threadId)
1347
+ || readString(params?.thread_id)
1348
+ || readString(params?.turn?.threadId)
1349
+ || readString(params?.turn?.thread_id)
1350
+ );
1351
+ }
1352
+
1353
+ return null;
1354
+ }
1355
+
1356
+ function extractTurnId(method, params) {
1357
+ if (method === "turn/started" || method === "turn/completed") {
1358
+ return (
1359
+ readString(params?.turnId)
1360
+ || readString(params?.turn_id)
1361
+ || readString(params?.id)
1362
+ || readString(params?.turn?.id)
1363
+ || readString(params?.turn?.turnId)
1364
+ || readString(params?.turn?.turn_id)
1365
+ );
1366
+ }
1367
+
1368
+ return null;
1369
+ }
1370
+
1371
+ function readString(value) {
1372
+ return typeof value === "string" && value ? value : null;
1373
+ }
1374
+
1375
+ function normalizeNonEmptyString(value) {
1376
+ return typeof value === "string" && value.trim() ? value.trim() : "";
1377
+ }
1378
+
1379
+ // Shrinks `thread/read` and `thread/resume` snapshots by eliding bulky history payloads
1380
+ // that the iPhone does not render directly (inline images, compaction replacement history).
1381
+ function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
1382
+ if (requestMethod !== "thread/read" && requestMethod !== "thread/resume") {
1383
+ return rawMessage;
1384
+ }
1385
+
1386
+ const parsed = parseBridgeJSON(rawMessage);
1387
+ const thread = parsed?.result?.thread;
1388
+ if (!thread || typeof thread !== "object" || !Array.isArray(thread.turns)) {
1389
+ return rawMessage;
1390
+ }
1391
+
1392
+ let didSanitize = false;
1393
+ const sanitizedTurns = thread.turns.map((turn) => {
1394
+ if (!turn || typeof turn !== "object" || !Array.isArray(turn.items)) {
1395
+ return turn;
1396
+ }
1397
+
1398
+ let turnDidChange = false;
1399
+ const threadId = normalizeNonEmptyString(thread.id)
1400
+ || normalizeNonEmptyString(thread.threadId)
1401
+ || normalizeNonEmptyString(thread.thread_id);
1402
+
1403
+ const sanitizedItems = turn.items.map((item) => {
1404
+ if (!item || typeof item !== "object") {
1405
+ return item;
1406
+ }
1407
+
1408
+ let itemDidChange = false;
1409
+ let sanitizedItem = annotateImageGenerationHistoryItem(item, threadId);
1410
+ if (sanitizedItem !== item) {
1411
+ itemDidChange = true;
1412
+ }
1413
+
1414
+ if (Array.isArray(item.content)) {
1415
+ const sanitizedContent = item.content.map((contentItem) => {
1416
+ const sanitizedEntry = sanitizeInlineHistoryImageContentItem(contentItem);
1417
+ if (sanitizedEntry !== contentItem) {
1418
+ itemDidChange = true;
1419
+ }
1420
+ return sanitizedEntry;
1421
+ });
1422
+
1423
+ if (itemDidChange) {
1424
+ sanitizedItem = {
1425
+ ...sanitizedItem,
1426
+ content: sanitizedContent,
1427
+ };
1428
+ }
1429
+ }
1430
+
1431
+ const sanitizedCompactionItem = sanitizeCompactionHistoryItem(sanitizedItem);
1432
+ if (sanitizedCompactionItem !== sanitizedItem) {
1433
+ sanitizedItem = sanitizedCompactionItem;
1434
+ itemDidChange = true;
1435
+ }
1436
+
1437
+ if (itemDidChange) {
1438
+ turnDidChange = true;
1439
+ }
1440
+
1441
+ return itemDidChange ? sanitizedItem : item;
1442
+ });
1443
+
1444
+ if (!turnDidChange) {
1445
+ return turn;
1446
+ }
1447
+
1448
+ didSanitize = true;
1449
+ return {
1450
+ ...turn,
1451
+ items: sanitizedItems,
1452
+ };
1453
+ });
1454
+
1455
+ if (!didSanitize) {
1456
+ const trimmedPayload = trimThreadPayloadForRelay(parsed, thread);
1457
+ return trimmedPayload == null ? rawMessage : trimmedPayload;
1458
+ }
1459
+
1460
+ const sanitizedPayload = JSON.stringify({
1461
+ ...parsed,
1462
+ result: {
1463
+ ...parsed.result,
1464
+ thread: {
1465
+ ...thread,
1466
+ turns: sanitizedTurns,
1467
+ },
1468
+ },
1469
+ });
1470
+
1471
+ return trimThreadPayloadForRelay(parseBridgeJSON(sanitizedPayload), null) ?? sanitizedPayload;
1472
+ }
1473
+
1474
+ // Annotates live image-generation notifications so the phone can render a local-file
1475
+ // preview and does not receive the bulky inline base64 result over the relay.
1476
+ function sanitizeLiveGeneratedImageMessageForRelay(rawMessage) {
1477
+ const parsed = parseBridgeJSON(rawMessage);
1478
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
1479
+ return rawMessage;
1480
+ }
1481
+
1482
+ const params = parsed.params;
1483
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
1484
+ return rawMessage;
1485
+ }
1486
+
1487
+ const sanitizedParams = sanitizeLiveGeneratedImageParams(params);
1488
+ if (sanitizedParams === params) {
1489
+ return rawMessage;
1490
+ }
1491
+
1492
+ return JSON.stringify({
1493
+ ...parsed,
1494
+ params: sanitizedParams,
1495
+ });
1496
+ }
1497
+
1498
+ function sanitizeLiveGeneratedImageParams(params) {
1499
+ const threadId = liveGeneratedImageThreadId(params);
1500
+ let nextParams = params;
1501
+ let didChange = false;
1502
+
1503
+ const item = params.item;
1504
+ if (item && typeof item === "object" && !Array.isArray(item)) {
1505
+ const sanitizedItem = annotateImageGenerationPayload(item, threadId);
1506
+ if (sanitizedItem !== item) {
1507
+ nextParams = { ...nextParams, item: sanitizedItem };
1508
+ didChange = true;
1509
+ }
1510
+ }
1511
+
1512
+ const event = params.event;
1513
+ if (event && typeof event === "object" && !Array.isArray(event)) {
1514
+ const sanitizedEvent = sanitizeNestedGeneratedImagePayloads(event, threadId);
1515
+ if (sanitizedEvent !== event) {
1516
+ nextParams = { ...nextParams, event: sanitizedEvent };
1517
+ didChange = true;
1518
+ }
1519
+ }
1520
+
1521
+ const sanitizedDirectParams = annotateImageGenerationPayload(nextParams, threadId);
1522
+ if (sanitizedDirectParams !== nextParams) {
1523
+ nextParams = sanitizedDirectParams;
1524
+ didChange = true;
1525
+ }
1526
+
1527
+ return didChange ? nextParams : params;
1528
+ }
1529
+
1530
+ function sanitizeNestedGeneratedImagePayloads(value, threadId) {
1531
+ let nextValue = annotateImageGenerationPayload(value, threadId);
1532
+ let didChange = nextValue !== value;
1533
+
1534
+ for (const key of ["item", "payload", "data"]) {
1535
+ const nested = nextValue?.[key];
1536
+ if (!nested || typeof nested !== "object" || Array.isArray(nested)) {
1537
+ continue;
1538
+ }
1539
+ const sanitizedNested = sanitizeNestedGeneratedImagePayloads(nested, threadId);
1540
+ if (sanitizedNested !== nested) {
1541
+ if (!didChange) {
1542
+ nextValue = { ...nextValue };
1543
+ didChange = true;
1544
+ }
1545
+ nextValue[key] = sanitizedNested;
1546
+ }
1547
+ }
1548
+
1549
+ return didChange ? nextValue : value;
1550
+ }
1551
+
1552
+ // Drops huge replacement-history blobs from compaction items because the phone only needs
1553
+ // the compacted marker itself, not the entire pre-compaction transcript snapshot.
1554
+ function sanitizeCompactionHistoryItem(item) {
1555
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
1556
+ return item;
1557
+ }
1558
+
1559
+ let sanitizedItem = omitCompactionReplacementHistory(item);
1560
+ const payload = sanitizedItem.payload;
1561
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) {
1562
+ const sanitizedPayload = omitCompactionReplacementHistory(payload);
1563
+ if (sanitizedPayload !== payload) {
1564
+ sanitizedItem = {
1565
+ ...sanitizedItem,
1566
+ payload: sanitizedPayload,
1567
+ };
1568
+ }
1569
+ }
1570
+
1571
+ return sanitizedItem;
1572
+ }
1573
+
1574
+ function omitCompactionReplacementHistory(value) {
1575
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1576
+ return value;
1577
+ }
1578
+
1579
+ let nextValue = value;
1580
+ let didChange = false;
1581
+ for (const key of ["replacement_history", "replacementHistory"]) {
1582
+ if (Object.prototype.hasOwnProperty.call(nextValue, key)) {
1583
+ if (!didChange) {
1584
+ nextValue = { ...nextValue };
1585
+ didChange = true;
1586
+ }
1587
+ delete nextValue[key];
1588
+ }
1589
+ }
1590
+
1591
+ return didChange ? nextValue : value;
1592
+ }
1593
+
1594
+ function annotateImageGenerationHistoryItem(item, threadId) {
1595
+ if (!item || typeof item !== "object") {
1596
+ return item;
1597
+ }
1598
+
1599
+ const normalizedType = normalizeRelayHistoryContentType(item.type);
1600
+ if (!isGeneratedImageRelayType(normalizedType)) {
1601
+ return item;
1602
+ }
1603
+
1604
+ return annotateImageGenerationPayload(item, threadId);
1605
+ }
1606
+
1607
+ function annotateImageGenerationPayload(item, threadId) {
1608
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
1609
+ return item;
1610
+ }
1611
+
1612
+ const normalizedType = normalizeRelayHistoryContentType(item.type);
1613
+ if (!isGeneratedImageRelayType(normalizedType)) {
1614
+ return item;
1615
+ }
1616
+
1617
+ let nextItem = item;
1618
+ let didChange = false;
1619
+ const existingPath = normalizeNonEmptyString(item.saved_path)
1620
+ || normalizeNonEmptyString(item.savedPath)
1621
+ || normalizeNonEmptyString(item.path)
1622
+ || normalizeNonEmptyString(item.file_path);
1623
+ const generatedPath = existingPath || generatedImagePathForHistoryItem(item, threadId);
1624
+ if (generatedPath && !existingPath) {
1625
+ nextItem = {
1626
+ ...nextItem,
1627
+ saved_path: generatedPath,
1628
+ };
1629
+ didChange = true;
1630
+ }
1631
+
1632
+ if (typeof nextItem.result === "string" && nextItem.result.length > 0) {
1633
+ const {
1634
+ result: _result,
1635
+ ...withoutInlineResult
1636
+ } = nextItem;
1637
+ nextItem = {
1638
+ ...withoutInlineResult,
1639
+ result_elided_for_relay: true,
1640
+ };
1641
+ didChange = true;
1642
+ }
1643
+
1644
+ return didChange ? nextItem : item;
1645
+ }
1646
+
1647
+ function generatedImagePathForHistoryItem(item, threadId) {
1648
+ const resolvedThreadId = normalizeNonEmptyString(threadId);
1649
+ const normalizedType = normalizeRelayHistoryContentType(item.type);
1650
+ const callId = normalizedType === "imagegenerationend"
1651
+ ? normalizeNonEmptyString(item.call_id)
1652
+ || normalizeNonEmptyString(item.callId)
1653
+ || normalizeNonEmptyString(item.itemId)
1654
+ || normalizeNonEmptyString(item.item_id)
1655
+ || normalizeNonEmptyString(item.id)
1656
+ : normalizeNonEmptyString(item.id)
1657
+ || normalizeNonEmptyString(item.call_id)
1658
+ || normalizeNonEmptyString(item.callId)
1659
+ || normalizeNonEmptyString(item.itemId)
1660
+ || normalizeNonEmptyString(item.item_id);
1661
+ if (!resolvedThreadId || !callId) {
1662
+ return "";
1663
+ }
1664
+
1665
+ return path.join(resolveCodexGeneratedImagesRoot(), resolvedThreadId, `${callId}.png`);
1666
+ }
1667
+
1668
+ function isGeneratedImageRelayType(normalizedType) {
1669
+ return normalizedType === "imagegeneration"
1670
+ || normalizedType === "imagegenerationcall"
1671
+ || normalizedType === "imagegenerationend"
1672
+ || normalizedType === "imageview";
1673
+ }
1674
+
1675
+ function liveGeneratedImageThreadId(params) {
1676
+ const event = params?.event && typeof params.event === "object" && !Array.isArray(params.event)
1677
+ ? params.event
1678
+ : null;
1679
+ const item = params?.item && typeof params.item === "object" && !Array.isArray(params.item)
1680
+ ? params.item
1681
+ : null;
1682
+
1683
+ return normalizeNonEmptyString(params?.threadId)
1684
+ || normalizeNonEmptyString(params?.thread_id)
1685
+ || normalizeNonEmptyString(params?.conversationId)
1686
+ || normalizeNonEmptyString(params?.conversation_id)
1687
+ || normalizeNonEmptyString(event?.threadId)
1688
+ || normalizeNonEmptyString(event?.thread_id)
1689
+ || normalizeNonEmptyString(event?.conversationId)
1690
+ || normalizeNonEmptyString(event?.conversation_id)
1691
+ || normalizeNonEmptyString(item?.threadId)
1692
+ || normalizeNonEmptyString(item?.thread_id)
1693
+ || "";
1694
+ }
1695
+
1696
+ // Converts `data:image/...` history content into a tiny placeholder the iPhone can render safely.
1697
+ function sanitizeInlineHistoryImageContentItem(contentItem) {
1698
+ if (!contentItem || typeof contentItem !== "object") {
1699
+ return contentItem;
1700
+ }
1701
+
1702
+ const normalizedType = normalizeRelayHistoryContentType(contentItem.type);
1703
+ if (!isRelayHistoryImageContentType(normalizedType)) {
1704
+ return contentItem;
1705
+ }
1706
+
1707
+ const hasInlineUrl = hasInlineHistoryImageDataURL(contentItem.url)
1708
+ || hasInlineHistoryImageDataURL(contentItem.image_url)
1709
+ || hasInlineHistoryImageDataURL(contentItem.path);
1710
+ if (!hasInlineUrl) {
1711
+ return contentItem;
1712
+ }
1713
+
1714
+ const {
1715
+ url: _url,
1716
+ image_url: _imageUrl,
1717
+ path: _path,
1718
+ ...rest
1719
+ } = contentItem;
1720
+
1721
+ return {
1722
+ ...rest,
1723
+ url: RELAY_HISTORY_IMAGE_REFERENCE_URL,
1724
+ };
1725
+ }
1726
+
1727
+ function normalizeRelayHistoryContentType(value) {
1728
+ return typeof value === "string"
1729
+ ? value.toLowerCase().replace(/[\s_-]+/g, "")
1730
+ : "";
1731
+ }
1732
+
1733
+ // Covers Codex history variants such as image, local_image, and input_image.
1734
+ function isRelayHistoryImageContentType(normalizedType) {
1735
+ return normalizedType === "image"
1736
+ || normalizedType === "localimage"
1737
+ || normalizedType === "inputimage"
1738
+ || normalizedType === "outputimage";
1739
+ }
1740
+
1741
+ function hasInlineHistoryImageDataURL(value) {
1742
+ if (typeof value === "string") {
1743
+ return value.toLowerCase().startsWith("data:image");
1744
+ }
1745
+
1746
+ if (Array.isArray(value)) {
1747
+ return value.some(hasInlineHistoryImageDataURL);
1748
+ }
1749
+
1750
+ if (value && typeof value === "object") {
1751
+ return Object.values(value).some(hasInlineHistoryImageDataURL);
1752
+ }
1753
+
1754
+ return false;
1755
+ }
1756
+
1757
+ function parseBridgeJSON(value) {
1758
+ try {
1759
+ return JSON.parse(value);
1760
+ } catch {
1761
+ return null;
1762
+ }
1763
+ }
1764
+
1765
+ function trimThreadPayloadForRelay(parsed, explicitThread = undefined) {
1766
+ const thread = explicitThread ?? parsed?.result?.thread;
1767
+ if (!parsed || !thread || typeof thread !== "object" || !Array.isArray(thread.turns)) {
1768
+ return null;
1769
+ }
1770
+
1771
+ let workingThread = thread;
1772
+ let encoded = encodeRelayThreadPayload(parsed, workingThread);
1773
+ if (encoded == null) {
1774
+ return null;
1775
+ }
1776
+
1777
+ if (Buffer.byteLength(encoded, "utf8") <= RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES) {
1778
+ return explicitThread === undefined ? null : encoded;
1779
+ }
1780
+
1781
+ const turns = thread.turns;
1782
+ let trimmedTurns = turns.slice();
1783
+ while (trimmedTurns.length > 1) {
1784
+ trimmedTurns = trimmedTurns.slice(1);
1785
+ const candidateThread = {
1786
+ ...thread,
1787
+ turns: trimmedTurns,
1788
+ historyTailTruncatedForRelay: true,
1789
+ };
1790
+ encoded = encodeRelayThreadPayload(parsed, candidateThread);
1791
+ if (encoded != null && Buffer.byteLength(encoded, "utf8") <= RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES) {
1792
+ return encoded;
1793
+ }
1794
+ workingThread = candidateThread;
1795
+ }
1796
+
1797
+ const newestTurn = trimmedTurns[0];
1798
+ if (!newestTurn || typeof newestTurn !== "object" || !Array.isArray(newestTurn.items)) {
1799
+ return encodeRelayThreadPayload(parsed, workingThread);
1800
+ }
1801
+
1802
+ let trimmedItems = newestTurn.items.slice();
1803
+ while (trimmedItems.length > 1) {
1804
+ trimmedItems = trimmedItems.slice(1);
1805
+ const candidateThread = {
1806
+ ...thread,
1807
+ turns: [{
1808
+ ...newestTurn,
1809
+ items: trimmedItems,
1810
+ }],
1811
+ historyTailTruncatedForRelay: true,
1812
+ };
1813
+ encoded = encodeRelayThreadPayload(parsed, candidateThread);
1814
+ if (encoded != null && Buffer.byteLength(encoded, "utf8") <= RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES) {
1815
+ return encoded;
1816
+ }
1817
+ workingThread = candidateThread;
1818
+ }
1819
+
1820
+ const mostRecentItem = trimmedItems[0];
1821
+ if (!mostRecentItem || typeof mostRecentItem !== "object") {
1822
+ return encodeRelayThreadPayload(parsed, workingThread);
1823
+ }
1824
+
1825
+ const truncatedItem = truncateHistoryItemTextForRelay(
1826
+ mostRecentItem,
1827
+ RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS
1828
+ );
1829
+ let candidateThread = {
1830
+ ...thread,
1831
+ turns: [{
1832
+ ...newestTurn,
1833
+ items: [truncatedItem],
1834
+ }],
1835
+ historyTailTruncatedForRelay: true,
1836
+ };
1837
+ encoded = encodeRelayThreadPayload(parsed, candidateThread);
1838
+ if (encoded != null && Buffer.byteLength(encoded, "utf8") <= RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES) {
1839
+ return encoded;
1840
+ }
1841
+
1842
+ candidateThread = {
1843
+ ...thread,
1844
+ turns: [{
1845
+ ...newestTurn,
1846
+ items: [compactHistoryItemForRelay(mostRecentItem, RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS)],
1847
+ }],
1848
+ historyTailTruncatedForRelay: true,
1849
+ };
1850
+ return encodeRelayThreadPayload(parsed, candidateThread);
1851
+ }
1852
+
1853
+ function encodeRelayThreadPayload(parsed, thread) {
1854
+ try {
1855
+ return JSON.stringify({
1856
+ ...parsed,
1857
+ result: {
1858
+ ...parsed.result,
1859
+ thread,
1860
+ },
1861
+ });
1862
+ } catch {
1863
+ return null;
1864
+ }
1865
+ }
1866
+
1867
+ function truncateHistoryItemTextForRelay(item, maxChars) {
1868
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
1869
+ return item;
1870
+ }
1871
+
1872
+ let didChange = false;
1873
+ let nextItem = item;
1874
+ const textKeys = ["text", "message", "summary", "output", "outputText", "output_text"];
1875
+
1876
+ for (const key of textKeys) {
1877
+ if (typeof item[key] === "string" && item[key].length > maxChars) {
1878
+ nextItem = {
1879
+ ...nextItem,
1880
+ [key]: truncateRelayTextTail(item[key], maxChars),
1881
+ };
1882
+ didChange = true;
1883
+ }
1884
+ }
1885
+
1886
+ if (Array.isArray(item.content)) {
1887
+ const nextContent = item.content.map((entry) => {
1888
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1889
+ return entry;
1890
+ }
1891
+
1892
+ const truncatedEntry = truncateHistoryItemTextForRelay(entry, maxChars);
1893
+ if (truncatedEntry !== entry) {
1894
+ didChange = true;
1895
+ }
1896
+ return truncatedEntry;
1897
+ });
1898
+
1899
+ if (didChange) {
1900
+ nextItem = {
1901
+ ...nextItem,
1902
+ content: nextContent,
1903
+ };
1904
+ }
1905
+ }
1906
+
1907
+ return didChange
1908
+ ? {
1909
+ ...nextItem,
1910
+ relayTextTailTruncated: true,
1911
+ }
1912
+ : item;
1913
+ }
1914
+
1915
+ function compactHistoryItemForRelay(item, maxChars) {
1916
+ const compactItem = {
1917
+ id: typeof item?.id === "string" ? item.id : undefined,
1918
+ type: typeof item?.type === "string" ? item.type : "relay_truncated_item",
1919
+ role: typeof item?.role === "string" ? item.role : undefined,
1920
+ itemId: typeof item?.itemId === "string" ? item.itemId : undefined,
1921
+ relayPayloadTruncated: true,
1922
+ };
1923
+ const tailText = firstRelayTextTail(item, maxChars);
1924
+ if (tailText) {
1925
+ compactItem.text = tailText;
1926
+ }
1927
+
1928
+ return Object.fromEntries(
1929
+ Object.entries(compactItem).filter(([, value]) => value !== undefined)
1930
+ );
1931
+ }
1932
+
1933
+ function firstRelayTextTail(value, maxChars) {
1934
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1935
+ return "";
1936
+ }
1937
+
1938
+ for (const key of ["text", "message", "summary", "output", "outputText", "output_text"]) {
1939
+ if (typeof value[key] === "string" && value[key].trim()) {
1940
+ return truncateRelayTextTail(value[key], maxChars);
1941
+ }
1942
+ }
1943
+
1944
+ if (Array.isArray(value.content)) {
1945
+ for (const entry of value.content) {
1946
+ const tail = firstRelayTextTail(entry, maxChars);
1947
+ if (tail) {
1948
+ return tail;
1949
+ }
1950
+ }
1951
+ }
1952
+
1953
+ return "";
1954
+ }
1955
+
1956
+ function truncateRelayTextTail(value, maxChars) {
1957
+ if (typeof value !== "string" || value.length <= maxChars) {
1958
+ return value;
1959
+ }
1960
+
1961
+ const tail = value.slice(-maxChars).trimStart();
1962
+ return `…\n${tail}`;
1963
+ }
1964
+
1965
+ // Treats silent relay sockets as stale so the daemon can self-heal after sleep/wake.
1966
+ function hasRelayConnectionGoneStale(
1967
+ lastActivityAt,
1968
+ {
1969
+ now = Date.now(),
1970
+ staleAfterMs = RELAY_WATCHDOG_STALE_AFTER_MS,
1971
+ } = {}
1972
+ ) {
1973
+ return Number.isFinite(lastActivityAt)
1974
+ && Number.isFinite(now)
1975
+ && now - lastActivityAt >= staleAfterMs;
1976
+ }
1977
+
1978
+ // Keeps persisted daemon status honest by downgrading stale "connected" snapshots.
1979
+ function buildHeartbeatBridgeStatus(
1980
+ status,
1981
+ lastActivityAt,
1982
+ {
1983
+ now = Date.now(),
1984
+ staleAfterMs = RELAY_WATCHDOG_STALE_AFTER_MS,
1985
+ staleMessage = STALE_RELAY_STATUS_MESSAGE,
1986
+ } = {}
1987
+ ) {
1988
+ if (!status || typeof status !== "object") {
1989
+ return status;
1990
+ }
1991
+
1992
+ if (status.connectionStatus !== "connected") {
1993
+ return status;
1994
+ }
1995
+
1996
+ if (!hasRelayConnectionGoneStale(lastActivityAt, { now, staleAfterMs })) {
1997
+ return status;
1998
+ }
1999
+
2000
+ return {
2001
+ ...status,
2002
+ connectionStatus: "disconnected",
2003
+ lastError: staleMessage,
2004
+ };
2005
+ }
2006
+
2007
+ function persistBridgePreferences(
2008
+ {
2009
+ keepMacAwakeEnabled,
2010
+ },
2011
+ {
2012
+ readDaemonConfigImpl = readDaemonConfig,
2013
+ writeDaemonConfigImpl = writeDaemonConfig,
2014
+ } = {}
2015
+ ) {
2016
+ writeDaemonConfigImpl({
2017
+ ...(readDaemonConfigImpl() || {}),
2018
+ keepMacAwakeEnabled,
2019
+ });
2020
+ }
2021
+
2022
+ module.exports = {
2023
+ buildHeartbeatBridgeStatus,
2024
+ buildRelayAccessTokenHeaders,
2025
+ buildRelayUserAgentHeader,
2026
+ createMacOSBridgeWakeAssertion,
2027
+ hasRelayConnectionGoneStale,
2028
+ persistBridgePreferences,
2029
+ sanitizeLiveGeneratedImageMessageForRelay,
2030
+ sanitizeThreadHistoryImagesForRelay,
2031
+ startBridge,
2032
+ };