@makerbi/remodex 1.5.3 → 1.5.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 CHANGED
@@ -2,11 +2,12 @@
2
2
  // Purpose: Runs Codex locally, bridges relay traffic, and coordinates desktop refreshes for Codex.app.
3
3
  // Layer: CLI service
4
4
  // Exports: startBridge
5
- // Depends on: ws, crypto, os, ./codex-home, ./qr, ./codex-desktop-refresher, ./codex-transport, ./rollout-watch, ./voice-handler, ./ios-app-compatibility
5
+ // Depends on: ws, crypto, os, ./bridge-status, ./codex-desktop-refresher, ./codex-transport, ./rollout-watch, ./voice-handler
6
6
 
7
7
  const WebSocket = require("ws");
8
8
  const { randomBytes, randomUUID } = require("crypto");
9
9
  const { execFile, spawn } = require("child_process");
10
+ const fs = require("fs");
10
11
  const path = require("path");
11
12
  const os = require("os");
12
13
  const { promisify } = require("util");
@@ -14,6 +15,11 @@ const {
14
15
  CodexDesktopRefresher,
15
16
  readBridgeConfig,
16
17
  } = require("./codex-desktop-refresher");
18
+ const {
19
+ buildHeartbeatBridgeStatus,
20
+ createBridgeStatusPublisher,
21
+ hasRelayConnectionGoneStale,
22
+ } = require("./bridge-status");
17
23
  const { createCodexTransport } = require("./codex-transport");
18
24
  const {
19
25
  createThreadRolloutActivityWatcher,
@@ -47,6 +53,7 @@ const { createBridgeSecureTransport } = require("./secure-transport");
47
53
  const { createRolloutLiveMirrorController } = require("./rollout-live-mirror");
48
54
  const {
49
55
  createDesktopIpcActionFollower,
56
+ seedConversationStateFromThreadRead,
50
57
  } = require("./desktop-ipc-action-follower");
51
58
  const { version: bridgePackageVersion = "" } = require("../package.json");
52
59
  const {
@@ -57,15 +64,14 @@ const {
57
64
  } = require("./ios-app-compatibility");
58
65
  const { createShortPairingCode, SHORT_PAIRING_CODE_LENGTH } = require("./qr");
59
66
  const {
67
+ parseSessionJsonlMetadata,
68
+ parseSessionJsonlTurns,
60
69
  readThreadTurnsListPageFromSessionJsonl,
61
70
  } = require("./session-jsonl-history");
71
+ const { buildApplyPatchFileChangeItem } = require("./apply-patch-changes");
62
72
 
63
73
  const execFileAsync = promisify(execFile);
64
74
  const RELAY_WATCHDOG_PING_INTERVAL_MS = 10_000;
65
- // Keep the watchdog above the relay heartbeat cadence so quiet healthy sockets survive idle gaps.
66
- const RELAY_WATCHDOG_STALE_AFTER_MS = 70_000;
67
- const BRIDGE_STATUS_HEARTBEAT_INTERVAL_MS = 5_000;
68
- const STALE_RELAY_STATUS_MESSAGE = "Relay heartbeat stalled; reconnect pending.";
69
75
  const CLOSE_CODE_INVALID_RELAY_REQUEST = 4000;
70
76
  const CLOSE_CODE_REPLACED_BY_NEW_MAC = 4001;
71
77
  const CLOSE_CODE_MAC_UNAUTHORIZED = 4005;
@@ -77,6 +83,15 @@ const RELAY_TURNS_LIST_TARGET_BUDGET_MS = 5_500;
77
83
  const RELAY_TURNS_LIST_BUDGET_RESERVE_MS = 1_000;
78
84
  const RELAY_TURNS_LIST_MAX_INITIAL_LIMIT = 5;
79
85
  const RELAY_TURNS_LIST_SAFE_RETRY_LIMIT = 5;
86
+ const RELAY_JSONL_TURNS_LIST_CACHE_TTL_MS = 30_000;
87
+ const RELAY_JSONL_ARTIFACT_CACHE_TTL_MS = 2_000;
88
+ const RELAY_JSONL_ARTIFACT_CACHE_MAX_ENTRIES = 128;
89
+ const BRIDGE_PACKAGE_UPDATE_COMMAND = "npm install -g @makerbi/remodex@latest";
90
+ const BRIDGE_PACKAGE_UPDATE_TIMEOUT_MS = 180_000;
91
+ const BRIDGE_RESTART_AFTER_UPDATE_DELAY_MS = 750;
92
+ const MODELS_WITHOUT_REASONING_SUMMARY = new Set([
93
+ "gpt-5.3-codex-spark",
94
+ ]);
80
95
  const RELAY_TURNS_LIST_RESULT_KEYS = ["data", "items", "turns"];
81
96
  const RELAY_TURNS_LIST_PAGINATION_RESULT_KEYS = [
82
97
  "nextCursor",
@@ -93,6 +108,9 @@ const RELAY_TURNS_LIST_PAGINATION_RESULT_KEYS = [
93
108
  "previousCursor",
94
109
  "previous_cursor",
95
110
  ];
111
+ const jsonlArtifactItemsCacheByThread = new Map();
112
+ const FORWARDED_REQUEST_METHODS_MAX_SIZE = 500;
113
+ const JSONL_ROLLOUT_PATH_CACHE_MAX_SIZE = 200;
96
114
 
97
115
  function buildRelayUserAgentHeader({ version = bridgePackageVersion } = {}) {
98
116
  const normalizedVersion = typeof version === "string" && version.trim()
@@ -112,6 +130,18 @@ function buildRelayAccessTokenHeaders(config = {}, env = process.env) {
112
130
  : {};
113
131
  }
114
132
 
133
+ function evictOldestEntries(map, maxSize) {
134
+ if (map.size <= maxSize) {
135
+ return;
136
+ }
137
+ const excess = map.size - maxSize;
138
+ const iterator = map.keys();
139
+ for (let i = 0; i < excess; i += 1) {
140
+ const key = iterator.next().value;
141
+ map.delete(key);
142
+ }
143
+ }
144
+
115
145
  function startBridge({
116
146
  config: explicitConfig = null,
117
147
  printPairingQr = true,
@@ -176,9 +206,7 @@ function startBridge({
176
206
  let reconnectAttempt = 0;
177
207
  let reconnectTimer = null;
178
208
  let relayWatchdogTimer = null;
179
- let statusHeartbeatTimer = null;
180
209
  let lastRelayActivityAt = 0;
181
- let lastPublishedBridgeStatus = null;
182
210
  let lastConnectionStatus = null;
183
211
  let lastConnectionError = "";
184
212
  let codexLaunchState = config.codexEndpoint ? "connected" : "starting";
@@ -190,6 +218,8 @@ function startBridge({
190
218
  const relayChannels = [];
191
219
  const codexResponseRoutesById = new Map();
192
220
  const extraRelaySessionCount = readExtraRelaySessionCount(process.env);
221
+ const jsonlTurnsListRolloutCacheByThread = new Map();
222
+ const jsonlTurnsListRolloutMissCacheByThread = new Map();
193
223
  const trackedForwardedRequestMethods = new Set([
194
224
  "account/login/start",
195
225
  "account/login/cancel",
@@ -212,6 +242,7 @@ function startBridge({
212
242
  sessionId,
213
243
  relayUrl: relayBaseUrl,
214
244
  deviceState,
245
+ displayName: os.hostname(),
215
246
  onTrustedPhoneUpdate(nextDeviceState) {
216
247
  deviceState = nextDeviceState;
217
248
  sendRelayRegistrationUpdate(nextDeviceState);
@@ -238,6 +269,9 @@ function startBridge({
238
269
  const desktopIpcActionFollower = !config.codexEndpoint
239
270
  ? createDesktopIpcActionFollower({
240
271
  sendApplicationResponse,
272
+ readConversationState: async (threadId) => seedConversationStateFromThreadRead(
273
+ await sendCodexRequest("thread/read", { threadId })
274
+ ),
241
275
  socketPath: config.desktopIpcSocketPath || undefined,
242
276
  })
243
277
  : null;
@@ -254,7 +288,14 @@ function startBridge({
254
288
  sendCodexRequest,
255
289
  logPrefix: "[remodex]",
256
290
  });
257
- startBridgeStatusHeartbeat();
291
+ const bridgeStatusPublisher = createBridgeStatusPublisher({
292
+ onBridgeStatus,
293
+ getCodexLaunchState: () => codexLaunchState,
294
+ });
295
+ bridgeStatusPublisher.startHeartbeat({
296
+ shouldPublish: () => !isShuttingDown,
297
+ getLastRelayActivityAt: () => lastRelayActivityAt,
298
+ });
258
299
  publishBridgeStatus({
259
300
  state: "starting",
260
301
  connectionStatus: "starting",
@@ -275,7 +316,7 @@ function startBridge({
275
316
  } else {
276
317
  console.error("[remodex] Failed to start `codex app-server`.");
277
318
  console.error(`[remodex] Launch command: ${codex.describe()}`);
278
- console.error("[remodex] Make sure the Codex CLI is installed and that the launcher works on this OS.");
319
+ console.error("[remodex] Make sure the Codex CLI is installed, authenticated, and launchable on this OS.");
279
320
  }
280
321
  console.error(error.message);
281
322
  process.exit(1);
@@ -283,6 +324,7 @@ function startBridge({
283
324
  // Marks the local Codex runtime as launchable before relay/network recovery updates.
284
325
  codex.onStarted(() => {
285
326
  codexLaunchState = "connected";
327
+ const lastPublishedBridgeStatus = bridgeStatusPublisher.latest();
286
328
  if (!lastPublishedBridgeStatus) {
287
329
  return;
288
330
  }
@@ -299,31 +341,6 @@ function startBridge({
299
341
  reconnectTimer = null;
300
342
  }
301
343
 
302
- // Periodically rewrites the latest bridge snapshot so CLI status does not stay frozen.
303
- function startBridgeStatusHeartbeat() {
304
- if (statusHeartbeatTimer) {
305
- return;
306
- }
307
-
308
- statusHeartbeatTimer = setInterval(() => {
309
- if (!lastPublishedBridgeStatus || isShuttingDown) {
310
- return;
311
- }
312
-
313
- onBridgeStatus?.(buildHeartbeatBridgeStatus(lastPublishedBridgeStatus, lastRelayActivityAt));
314
- }, BRIDGE_STATUS_HEARTBEAT_INTERVAL_MS);
315
- statusHeartbeatTimer.unref?.();
316
- }
317
-
318
- function clearBridgeStatusHeartbeat() {
319
- if (!statusHeartbeatTimer) {
320
- return;
321
- }
322
-
323
- clearInterval(statusHeartbeatTimer);
324
- statusHeartbeatTimer = null;
325
- }
326
-
327
344
  // Tracks relay liveness locally so sleep/wake zombie sockets can be force-reconnected.
328
345
  function markRelayActivity() {
329
346
  lastRelayActivityAt = Date.now();
@@ -338,6 +355,33 @@ function startBridge({
338
355
  relayWatchdogTimer = null;
339
356
  }
340
357
 
358
+ function prepareBridgeShutdown() {
359
+ isShuttingDown = true;
360
+ bridgeWakeAssertion.stop();
361
+ clearReconnectTimer();
362
+ clearRelayWatchdog();
363
+ bridgeStatusPublisher.stopHeartbeat();
364
+ stopContextUsageWatcher();
365
+ rolloutLiveMirror?.stopAll();
366
+ desktopIpcActionFollower?.stopAll();
367
+ }
368
+
369
+ function stopBridge() {
370
+ if (isShuttingDown) {
371
+ return;
372
+ }
373
+
374
+ prepareBridgeShutdown();
375
+ desktopRefresher.handleTransportReset();
376
+ failBridgeManagedCodexRequests(new Error("Bridge stopped before the request completed."));
377
+ forwardedRequestMethodsById.clear();
378
+
379
+ if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) {
380
+ socket.close();
381
+ }
382
+ codex.shutdown();
383
+ }
384
+
341
385
  function startRelayWatchdog(trackedSocket) {
342
386
  clearRelayWatchdog();
343
387
  markRelayActivity();
@@ -394,18 +438,9 @@ function startBridge({
394
438
  return;
395
439
  }
396
440
 
397
- if (isTerminalRelayCloseCode(closeCode)) {
398
- const lastError = buildRelayCloseStatusError(closeCode, closeReason);
399
- logConnectionStatus("disconnected", lastError);
400
- shutdown(codex, () => socket, () => {
401
- isShuttingDown = true;
402
- bridgeWakeAssertion.stop();
403
- clearReconnectTimer();
404
- clearRelayWatchdog();
405
- clearBridgeStatusHeartbeat();
406
- }, {
407
- exitCode: closeCode === CLOSE_CODE_MAC_UNAUTHORIZED ? 1 : 0,
408
- });
441
+ if (closeCode === 4000 || closeCode === 4001) {
442
+ logConnectionStatus("disconnected");
443
+ shutdown(codex, () => socket, prepareBridgeShutdown);
409
444
  return;
410
445
  }
411
446
 
@@ -414,7 +449,9 @@ function startBridge({
414
449
  }
415
450
 
416
451
  reconnectAttempt += 1;
417
- const delayMs = Math.min(1_000 * reconnectAttempt, 5_000);
452
+ const baseDelayMs = Math.min(1_000 * reconnectAttempt, 5_000);
453
+ const jitterMs = Math.floor(Math.random() * Math.min(baseDelayMs, 2_000));
454
+ const delayMs = baseDelayMs + jitterMs;
418
455
  logConnectionStatus("connecting");
419
456
  reconnectTimer = setTimeout(() => {
420
457
  reconnectTimer = null;
@@ -429,6 +466,11 @@ function startBridge({
429
466
 
430
467
  logConnectionStatus("connecting");
431
468
  const nextSocket = new WebSocket(relaySessionUrl, {
469
+ perMessageDeflate: {
470
+ zlibDeflateOptions: { level: 6 },
471
+ threshold: 256,
472
+ concurrencyLimit: 4,
473
+ },
432
474
  // The relay uses this per-session secret to authenticate the first push registration.
433
475
  headers: {
434
476
  "User-Agent": buildRelayUserAgentHeader(),
@@ -485,8 +527,8 @@ function startBridge({
485
527
  socket = null;
486
528
  }
487
529
  stopContextUsageWatcher();
488
- rolloutLiveMirror?.stopAll();
489
- desktopIpcActionFollower?.stopAll();
530
+ // Relay reconnects are transport-only: keep local live observers running
531
+ // so their output can enter secure replay and catch up on the next resume.
490
532
  desktopRefresher.handleTransportReset();
491
533
  scheduleRelayReconnect(code, closeReason);
492
534
  });
@@ -582,8 +624,10 @@ function startBridge({
582
624
 
583
625
  const nextSocket = new WebSocket(extraRelaySessionUrl, {
584
626
  headers: {
627
+ "User-Agent": buildRelayUserAgentHeader(),
585
628
  "x-role": "mac",
586
629
  "x-notification-secret": notificationSecret,
630
+ ...buildRelayAccessTokenHeaders(config),
587
631
  ...buildMacRegistrationHeaders(deviceState, extraPairingSession),
588
632
  },
589
633
  });
@@ -675,22 +719,24 @@ function startBridge({
675
719
  });
676
720
 
677
721
  codex.onClose(() => {
722
+ const wasShuttingDown = isShuttingDown;
678
723
  clearRelayWatchdog();
679
- clearBridgeStatusHeartbeat();
680
- const lastError = lastConnectionError || "";
724
+ bridgeStatusPublisher.stopHeartbeat();
725
+ const lastError = wasShuttingDown
726
+ ? ""
727
+ : (lastConnectionError || "Codex transport closed unexpectedly.");
681
728
  logConnectionStatus("disconnected", lastError);
682
729
  publishBridgeStatus({
683
- state: "stopped",
730
+ state: wasShuttingDown ? "stopped" : "error",
684
731
  connectionStatus: "disconnected",
685
732
  pid: process.pid,
686
733
  lastError,
687
734
  });
688
- isShuttingDown = true;
689
- bridgeWakeAssertion.stop();
690
- clearReconnectTimer();
691
- stopContextUsageWatcher();
692
- rolloutLiveMirror?.stopAll();
693
- desktopIpcActionFollower?.stopAll();
735
+ if (!wasShuttingDown) {
736
+ console.error(`[remodex] ${lastError}`);
737
+ process.exitCode = 1;
738
+ }
739
+ prepareBridgeShutdown();
694
740
  desktopRefresher.handleTransportReset();
695
741
  failBridgeManagedCodexRequests(new Error("Codex transport closed before the bridge request completed."));
696
742
  forwardedRequestMethodsById.clear();
@@ -701,22 +747,8 @@ function startBridge({
701
747
  closeExtraRelayChannels();
702
748
  });
703
749
 
704
- process.on("SIGINT", () => shutdown(codex, () => socket, () => {
705
- isShuttingDown = true;
706
- bridgeWakeAssertion.stop();
707
- clearReconnectTimer();
708
- clearRelayWatchdog();
709
- clearBridgeStatusHeartbeat();
710
- closeExtraRelayChannels();
711
- }));
712
- process.on("SIGTERM", () => shutdown(codex, () => socket, () => {
713
- isShuttingDown = true;
714
- bridgeWakeAssertion.stop();
715
- clearReconnectTimer();
716
- clearRelayWatchdog();
717
- clearBridgeStatusHeartbeat();
718
- closeExtraRelayChannels();
719
- }));
750
+ process.on("SIGINT", () => shutdown(codex, () => socket, prepareBridgeShutdown));
751
+ process.on("SIGTERM", () => shutdown(codex, () => socket, prepareBridgeShutdown));
720
752
 
721
753
  // Routes decrypted app payloads through the same bridge handlers as before.
722
754
  function handleApplicationMessage(rawMessage, relayChannel = primaryRelayChannel) {
@@ -750,6 +782,7 @@ function startBridge({
750
782
  appPath: config.codexAppPath,
751
783
  readBridgePreferences,
752
784
  updateBridgePreferences,
785
+ updateBridgePackageAndRestart,
753
786
  })) {
754
787
  return;
755
788
  }
@@ -767,9 +800,10 @@ function startBridge({
767
800
  if (handleBridgeManagedThreadTurnsListRequest(rawMessage, sendResponse)) {
768
801
  return;
769
802
  }
770
- const codexMessage = prepareCodexForwardMessage(rawMessage, relayChannel);
803
+ const codexRequest = disableUnsupportedReasoningSummaryForTurnStart(rawMessage);
804
+ const codexMessage = prepareCodexForwardMessage(codexRequest, relayChannel);
771
805
  rememberForwardedRequestMethod(codexMessage);
772
- rememberThreadFromMessage("phone", rawMessage);
806
+ rememberThreadFromMessage("phone", codexMessage);
773
807
  mirrorUserMessageToPeerDevices(rawMessage, relayChannel);
774
808
  codex.send(codexMessage);
775
809
  }
@@ -978,12 +1012,19 @@ function startBridge({
978
1012
  const response = await fetchAdaptiveThreadTurnsListForRelay(request, {
979
1013
  fetchPage: (params) => sendCodexRequest("thread/turns/list", params),
980
1014
  });
981
- const fallbackResponse = maybeBuildJsonlThreadTurnsListFallback(request, response);
1015
+ const jsonlFallback = maybeBuildJsonlThreadTurnsListFallback(request, response);
1016
+ const responsePayload = jsonlFallback?.response ?? response;
1017
+ const finalSanitizeContext = buildThreadTurnsListRelaySanitizeContext(request);
982
1018
  relaySanitizedResponseMethodsById.set(String(request.id), {
983
1019
  method: "thread/turns/list",
1020
+ ...finalSanitizeContext,
984
1021
  createdAt: Date.now(),
985
1022
  });
986
- sendResponse(JSON.stringify(fallbackResponse ?? response));
1023
+ sendResponse(sanitizeThreadHistoryImagesForRelay(
1024
+ JSON.stringify(responsePayload),
1025
+ "thread/turns/list",
1026
+ finalSanitizeContext
1027
+ ));
987
1028
  } catch (error) {
988
1029
  sendResponse(createJsonRpcErrorResponse(
989
1030
  request.id,
@@ -997,10 +1038,6 @@ function startBridge({
997
1038
  }
998
1039
 
999
1040
  function maybeBuildJsonlThreadTurnsListFallback(request, response) {
1000
- if (!isEmptyTurnsListResponse(response)) {
1001
- return null;
1002
- }
1003
-
1004
1041
  const params = request?.params || {};
1005
1042
  const threadId = normalizeNonEmptyString(params.threadId)
1006
1043
  || normalizeNonEmptyString(params.thread_id);
@@ -1009,10 +1046,17 @@ function startBridge({
1009
1046
  }
1010
1047
 
1011
1048
  try {
1012
- const rolloutPath = findRecentRolloutFileForContextRead(resolveSessionsRoot(), { threadId });
1049
+ const responseIsEmpty = isEmptyTurnsListResponse(response);
1050
+ const rolloutPath = resolveJsonlTurnsListRolloutPathForFallback({
1051
+ threadId,
1052
+ responseIsEmpty,
1053
+ readCachedPath: readCachedJsonlTurnsListRolloutPath,
1054
+ findAndCachePath: findAndCacheJsonlTurnsListRolloutPath,
1055
+ });
1013
1056
  if (!rolloutPath) {
1014
1057
  return null;
1015
1058
  }
1059
+
1016
1060
  const result = readThreadTurnsListPageFromSessionJsonl(rolloutPath, {
1017
1061
  threadId,
1018
1062
  limit: params.limit,
@@ -1024,16 +1068,69 @@ function startBridge({
1024
1068
  return null;
1025
1069
  }
1026
1070
 
1071
+ if (!responseIsEmpty) {
1072
+ const mergedResponse = maybeMergeLatestJsonlTurnIntoTurnsListResponse(request, response, result, params);
1073
+ return mergedResponse ? { response: mergedResponse, usesJsonl: true } : null;
1074
+ }
1075
+
1027
1076
  return {
1028
- id: request.id,
1029
- result,
1077
+ response: {
1078
+ id: request.id,
1079
+ result,
1080
+ },
1081
+ usesJsonl: true,
1030
1082
  };
1031
1083
  } catch (error) {
1084
+ jsonlTurnsListRolloutCacheByThread.delete(threadId);
1032
1085
  console.warn(`[remodex] thread/turns/list jsonl fallback failed: ${error.message}`);
1033
1086
  return null;
1034
1087
  }
1035
1088
  }
1036
1089
 
1090
+ function findAndCacheJsonlTurnsListRolloutPath(threadId) {
1091
+ if (hasFreshJsonlTurnsListRolloutMiss(threadId)) {
1092
+ return "";
1093
+ }
1094
+
1095
+ const rolloutPath = findRecentRolloutFileForContextRead(resolveSessionsRoot(), { threadId });
1096
+ if (rolloutPath) {
1097
+ jsonlTurnsListRolloutMissCacheByThread.delete(threadId);
1098
+ jsonlTurnsListRolloutCacheByThread.set(threadId, {
1099
+ rolloutPath,
1100
+ cachedAt: Date.now(),
1101
+ });
1102
+ } else {
1103
+ jsonlTurnsListRolloutMissCacheByThread.set(threadId, Date.now());
1104
+ }
1105
+ return rolloutPath;
1106
+ }
1107
+
1108
+ function readCachedJsonlTurnsListRolloutPath(threadId) {
1109
+ const cached = jsonlTurnsListRolloutCacheByThread.get(threadId);
1110
+ if (!cached) {
1111
+ return "";
1112
+ }
1113
+ if (Date.now() - cached.cachedAt > RELAY_JSONL_TURNS_LIST_CACHE_TTL_MS) {
1114
+ jsonlTurnsListRolloutCacheByThread.delete(threadId);
1115
+ return "";
1116
+ }
1117
+ // Non-empty app-server pages only consult this positive cache to avoid
1118
+ // walking the sessions tree during ordinary pagination.
1119
+ return cached.rolloutPath;
1120
+ }
1121
+
1122
+ function hasFreshJsonlTurnsListRolloutMiss(threadId) {
1123
+ const missedAt = jsonlTurnsListRolloutMissCacheByThread.get(threadId);
1124
+ if (!missedAt) {
1125
+ return false;
1126
+ }
1127
+ if (Date.now() - missedAt <= RELAY_JSONL_TURNS_LIST_CACHE_TTL_MS) {
1128
+ return true;
1129
+ }
1130
+ jsonlTurnsListRolloutMissCacheByThread.delete(threadId);
1131
+ return false;
1132
+ }
1133
+
1037
1134
  // ─── Bridge-owned auth snapshot ─────────────────────────────
1038
1135
 
1039
1136
  // Handles the bridge-owned auth status wrappers without exposing tokens to the phone.
@@ -1182,10 +1279,17 @@ function startBridge({
1182
1279
  });
1183
1280
  }
1184
1281
  if (relaySanitizedRequestMethods.has(method)) {
1185
- relaySanitizedResponseMethodsById.set(String(requestId), {
1282
+ const trackedRequest = {
1186
1283
  method,
1284
+ threadId: method === "thread/turns/list" || method === "thread/read" || method === "thread/resume"
1285
+ ? threadIdFromRequestParams(parsed.params)
1286
+ : "",
1187
1287
  createdAt: Date.now(),
1188
- });
1288
+ };
1289
+ if (method === "thread/turns/list") {
1290
+ trackedRequest.skipJsonlArtifactAugmentation = false;
1291
+ }
1292
+ relaySanitizedResponseMethodsById.set(String(requestId), trackedRequest);
1189
1293
  }
1190
1294
  }
1191
1295
 
@@ -1211,7 +1315,7 @@ function startBridge({
1211
1315
  }
1212
1316
  relaySanitizedResponseMethodsById.delete(String(responseId));
1213
1317
 
1214
- return sanitizeThreadHistoryImagesForRelay(normalizedMessage, trackedRequest.method);
1318
+ return sanitizeThreadHistoryImagesForRelay(normalizedMessage, trackedRequest.method, trackedRequest);
1215
1319
  }
1216
1320
 
1217
1321
  function updatePendingAuthLoginFromCodexMessage(rawMessage) {
@@ -1264,16 +1368,31 @@ function startBridge({
1264
1368
  }
1265
1369
 
1266
1370
  function pruneExpiredForwardedRequestMethods(now = Date.now()) {
1371
+ const expiredForwarded = [];
1267
1372
  for (const [requestId, trackedRequest] of forwardedRequestMethodsById.entries()) {
1268
1373
  if (!trackedRequest || (now - trackedRequest.createdAt) >= forwardedRequestMethodTTLms) {
1269
- forwardedRequestMethodsById.delete(requestId);
1374
+ expiredForwarded.push(requestId);
1270
1375
  }
1271
1376
  }
1377
+ for (const id of expiredForwarded) {
1378
+ forwardedRequestMethodsById.delete(id);
1379
+ }
1380
+
1381
+ const expiredSanitized = [];
1272
1382
  for (const [requestId, trackedRequest] of relaySanitizedResponseMethodsById.entries()) {
1273
1383
  if (!trackedRequest || (now - trackedRequest.createdAt) >= forwardedRequestMethodTTLms) {
1274
- relaySanitizedResponseMethodsById.delete(requestId);
1384
+ expiredSanitized.push(requestId);
1275
1385
  }
1276
1386
  }
1387
+ for (const id of expiredSanitized) {
1388
+ relaySanitizedResponseMethodsById.delete(id);
1389
+ }
1390
+
1391
+ evictOldestEntries(forwardedRequestMethodsById, FORWARDED_REQUEST_METHODS_MAX_SIZE);
1392
+ evictOldestEntries(relaySanitizedResponseMethodsById, FORWARDED_REQUEST_METHODS_MAX_SIZE);
1393
+ evictOldestEntries(jsonlArtifactItemsCacheByThread, RELAY_JSONL_ARTIFACT_CACHE_MAX_ENTRIES);
1394
+ evictOldestEntries(jsonlTurnsListRolloutCacheByThread, JSONL_ROLLOUT_PATH_CACHE_MAX_SIZE);
1395
+ evictOldestEntries(jsonlTurnsListRolloutMissCacheByThread, JSONL_ROLLOUT_PATH_CACHE_MAX_SIZE);
1277
1396
  }
1278
1397
 
1279
1398
  function safeParseJSON(value) {
@@ -1578,12 +1697,7 @@ function startBridge({
1578
1697
  }
1579
1698
 
1580
1699
  function publishBridgeStatus(status) {
1581
- const nextStatus = {
1582
- ...status,
1583
- codexLaunchState,
1584
- };
1585
- lastPublishedBridgeStatus = nextStatus;
1586
- onBridgeStatus?.(nextStatus);
1700
+ bridgeStatusPublisher.publish(status);
1587
1701
  }
1588
1702
 
1589
1703
  // Refreshes the relay's trusted-mac index after the QR bootstrap locks in a phone identity.
@@ -1638,6 +1752,64 @@ function startBridge({
1638
1752
 
1639
1753
  return readBridgePreferences();
1640
1754
  }
1755
+
1756
+ async function updateBridgePackageAndRestart() {
1757
+ if (process.platform !== "darwin") {
1758
+ const error = new Error("Bridge self-update is available only for the macOS bridge service.");
1759
+ error.errorCode = "unsupported_platform";
1760
+ error.userMessage = error.message;
1761
+ throw error;
1762
+ }
1763
+
1764
+ try {
1765
+ await execFileAsync("/bin/zsh", [
1766
+ "-lc",
1767
+ [
1768
+ "export TERM=dumb",
1769
+ "source ~/.zshrc >/dev/null 2>/dev/null || true",
1770
+ BRIDGE_PACKAGE_UPDATE_COMMAND,
1771
+ ].join("; "),
1772
+ ], {
1773
+ timeout: BRIDGE_PACKAGE_UPDATE_TIMEOUT_MS,
1774
+ maxBuffer: 2 * 1024 * 1024,
1775
+ });
1776
+ } catch (error) {
1777
+ const nextError = new Error(
1778
+ truncateCommandOutput(error?.stderr || error?.stdout || error?.message)
1779
+ || "Could not update the Remodex bridge package on this Mac."
1780
+ );
1781
+ nextError.errorCode = "bridge_update_failed";
1782
+ nextError.userMessage = nextError.message;
1783
+ nextError.cause = error;
1784
+ throw nextError;
1785
+ }
1786
+
1787
+ scheduleBridgeServiceRestartAfterUpdate();
1788
+ return {
1789
+ success: true,
1790
+ command: BRIDGE_PACKAGE_UPDATE_COMMAND,
1791
+ restartScheduled: true,
1792
+ restartDelayMs: BRIDGE_RESTART_AFTER_UPDATE_DELAY_MS,
1793
+ };
1794
+ }
1795
+
1796
+ // Restarts after the RPC response has crossed the encrypted phone channel.
1797
+ function scheduleBridgeServiceRestartAfterUpdate() {
1798
+ const restartTimer = setTimeout(() => {
1799
+ const cliPath = path.join(__dirname, "..", "bin", "remodex.js");
1800
+ const child = spawn(process.execPath, [cliPath, "restart"], {
1801
+ detached: true,
1802
+ stdio: "ignore",
1803
+ env: process.env,
1804
+ });
1805
+ child.unref?.();
1806
+ }, BRIDGE_RESTART_AFTER_UPDATE_DELAY_MS);
1807
+ restartTimer.unref?.();
1808
+ }
1809
+
1810
+ return {
1811
+ stop: stopBridge,
1812
+ };
1641
1813
  }
1642
1814
 
1643
1815
  // Holds a single macOS idle-sleep assertion for as long as the bridge process stays alive.
@@ -1825,6 +1997,41 @@ function buildRelayCloseStatusError(closeCode, closeReason = "") {
1825
1997
  : `Relay closed the connection (${closeCode}).`;
1826
1998
  }
1827
1999
 
2000
+ // Forces app-server summary generation off for models whose Responses API calls
2001
+ // reject reasoning.summary, while leaving the phone-facing runtime choice intact.
2002
+ function disableUnsupportedReasoningSummaryForTurnStart(rawMessage) {
2003
+ const parsed = parseBridgeJSON(rawMessage);
2004
+ if (!parsed || parsed.method !== "turn/start") {
2005
+ return rawMessage;
2006
+ }
2007
+
2008
+ const params = parsed.params && typeof parsed.params === "object" && !Array.isArray(parsed.params)
2009
+ ? parsed.params
2010
+ : null;
2011
+ if (!params || params.summary === "none") {
2012
+ return rawMessage;
2013
+ }
2014
+
2015
+ const model = readTurnStartModel(params);
2016
+ if (!MODELS_WITHOUT_REASONING_SUMMARY.has(model)) {
2017
+ return rawMessage;
2018
+ }
2019
+
2020
+ return JSON.stringify({
2021
+ ...parsed,
2022
+ params: {
2023
+ ...params,
2024
+ summary: "none",
2025
+ },
2026
+ });
2027
+ }
2028
+
2029
+ function readTurnStartModel(params) {
2030
+ return normalizeNonEmptyString(params?.model).toLowerCase()
2031
+ || normalizeNonEmptyString(params?.collaborationMode?.settings?.model).toLowerCase()
2032
+ || normalizeNonEmptyString(params?.collaboration_mode?.settings?.model).toLowerCase();
2033
+ }
2034
+
1828
2035
  function extractBridgeMessageContext(rawMessage) {
1829
2036
  let parsed = null;
1830
2037
  try {
@@ -1909,6 +2116,14 @@ function normalizeNonEmptyString(value) {
1909
2116
  return typeof value === "string" && value.trim() ? value.trim() : "";
1910
2117
  }
1911
2118
 
2119
+ function truncateCommandOutput(value, maxChars = 1_200) {
2120
+ const normalized = normalizeNonEmptyString(value);
2121
+ if (!normalized || normalized.length <= maxChars) {
2122
+ return normalized;
2123
+ }
2124
+ return `...${normalized.slice(-maxChars)}`;
2125
+ }
2126
+
1912
2127
  function parseAdaptiveThreadTurnsListRequest(rawMessage) {
1913
2128
  const parsed = parseBridgeJSON(rawMessage);
1914
2129
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
@@ -1935,6 +2150,22 @@ function parseAdaptiveThreadTurnsListRequest(rawMessage) {
1935
2150
  return parsed;
1936
2151
  }
1937
2152
 
2153
+ function threadIdFromRequestParams(params) {
2154
+ return normalizeNonEmptyString(params?.threadId)
2155
+ || normalizeNonEmptyString(params?.thread_id)
2156
+ || normalizeNonEmptyString(params?.id)
2157
+ || "";
2158
+ }
2159
+
2160
+ function buildThreadTurnsListRelaySanitizeContext(request, {
2161
+ skipJsonlArtifactAugmentation = false,
2162
+ } = {}) {
2163
+ return {
2164
+ threadId: threadIdFromRequestParams(request?.params || {}),
2165
+ skipJsonlArtifactAugmentation,
2166
+ };
2167
+ }
2168
+
1938
2169
  async function fetchAdaptiveThreadTurnsListForRelay(request, {
1939
2170
  fetchPage,
1940
2171
  now = Date.now,
@@ -1952,6 +2183,9 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
1952
2183
  const requestedLimit = Number.isInteger(params?.limit) && params.limit > 0
1953
2184
  ? Math.min(params.limit, RELAY_TURNS_LIST_MAX_INITIAL_LIMIT)
1954
2185
  : 1;
2186
+ const sanitizeContext = buildThreadTurnsListRelaySanitizeContext(request, {
2187
+ skipJsonlArtifactAugmentation: true,
2188
+ });
1955
2189
  const startedAt = now();
1956
2190
  let nextCursor = params?.cursor;
1957
2191
  let turnsKey = null;
@@ -1976,6 +2210,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
1976
2210
  fetchPage,
1977
2211
  now,
1978
2212
  sanitizeForRelay,
2213
+ sanitizeContext,
1979
2214
  payloadSoftLimitBytes,
1980
2215
  });
1981
2216
  }
@@ -1988,6 +2223,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
1988
2223
  fetchPage,
1989
2224
  now,
1990
2225
  sanitizeForRelay,
2226
+ sanitizeContext,
1991
2227
  payloadSoftLimitBytes,
1992
2228
  });
1993
2229
  }
@@ -2006,7 +2242,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
2006
2242
  combinedTurns = combinedTurns.concat(pageTurns);
2007
2243
  response = buildSafeTurnsListResponse(request.id, firstResult, lastResult, turnsKey, combinedTurns);
2008
2244
 
2009
- if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) >= payloadSoftLimitBytes) {
2245
+ if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, sanitizeContext) >= payloadSoftLimitBytes) {
2010
2246
  response = buildLargestSafeTurnsListResponse({
2011
2247
  requestId: request.id,
2012
2248
  firstResult,
@@ -2015,6 +2251,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
2015
2251
  turns: combinedTurns,
2016
2252
  maxTurns: RELAY_TURNS_LIST_SAFE_RETRY_LIMIT,
2017
2253
  sanitizeForRelay,
2254
+ sanitizeContext,
2018
2255
  payloadSoftLimitBytes,
2019
2256
  }) ?? buildEmptyTurnsListResponse(request);
2020
2257
  break;
@@ -2026,7 +2263,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
2026
2263
  }
2027
2264
 
2028
2265
  const rawPageBytes = jsonByteLength(pageResult);
2029
- const sanitizedResponseBytes = measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay);
2266
+ const sanitizedResponseBytes = measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, sanitizeContext);
2030
2267
  const elapsedMs = Math.max(0, now() - startedAt);
2031
2268
  const remainingBudgetMs = Math.max(0, targetBudgetMs - elapsedMs);
2032
2269
  if (
@@ -2062,10 +2299,87 @@ function isEmptyTurnsListResponse(response) {
2062
2299
  return Boolean(turnsKey) && response.result[turnsKey].length === 0;
2063
2300
  }
2064
2301
 
2302
+ // Non-empty app-server pages can be stale for Mac-started runs, so the first page
2303
+ // still gets one JSONL lookup when the positive rollout cache is cold.
2304
+ function resolveJsonlTurnsListRolloutPathForFallback({
2305
+ threadId,
2306
+ responseIsEmpty,
2307
+ readCachedPath,
2308
+ findAndCachePath,
2309
+ }) {
2310
+ if (!threadId || typeof findAndCachePath !== "function") {
2311
+ return "";
2312
+ }
2313
+
2314
+ if (responseIsEmpty) {
2315
+ return findAndCachePath(threadId);
2316
+ }
2317
+
2318
+ return typeof readCachedPath === "function"
2319
+ ? readCachedPath(threadId) || findAndCachePath(threadId)
2320
+ : findAndCachePath(threadId);
2321
+ }
2322
+
2323
+ function maybeMergeLatestJsonlTurnIntoTurnsListResponse(request, response, jsonlResult, params = {}) {
2324
+ const responseResult = response?.result;
2325
+ const responseTurnsKey = findTurnsListResultKey(responseResult);
2326
+ const jsonlTurnsKey = findTurnsListResultKey(jsonlResult);
2327
+ if (!responseTurnsKey || !jsonlTurnsKey) {
2328
+ return null;
2329
+ }
2330
+
2331
+ const responseTurns = responseResult[responseTurnsKey];
2332
+ const jsonlTurn = jsonlResult[jsonlTurnsKey]?.[0];
2333
+ const jsonlTurnId = turnListTurnIdentifier(jsonlTurn);
2334
+ if (!jsonlTurnId || responseTurns.some((turn) => turnListTurnIdentifier(turn) === jsonlTurnId)) {
2335
+ return null;
2336
+ }
2337
+
2338
+ if (!shouldMergeLatestJsonlTurn(jsonlTurn)) {
2339
+ return null;
2340
+ }
2341
+
2342
+ const requestedLimit = Number.isInteger(params?.limit) && params.limit > 0
2343
+ ? params.limit
2344
+ : responseTurns.length + 1;
2345
+ const mergedTurns = [jsonlTurn, ...responseTurns].slice(0, requestedLimit);
2346
+ return {
2347
+ id: request.id,
2348
+ result: {
2349
+ ...responseResult,
2350
+ [responseTurnsKey]: mergedTurns,
2351
+ remodexJsonlMergedLatest: true,
2352
+ },
2353
+ };
2354
+ }
2355
+
2356
+ function shouldMergeLatestJsonlTurn(turn) {
2357
+ if (!turn || typeof turn !== "object") {
2358
+ return false;
2359
+ }
2360
+
2361
+ const status = normalizeHistoryItemToken(turn.status);
2362
+ if (status === "running" || status === "inprogress" || status === "active") {
2363
+ return true;
2364
+ }
2365
+
2366
+ return Array.isArray(turn.items) && turn.items.some((item) => {
2367
+ const type = normalizeHistoryItemToken(item?.type);
2368
+ return type === "plan" || type === "filechange";
2369
+ });
2370
+ }
2371
+
2372
+ function turnListTurnIdentifier(turn) {
2373
+ return normalizeNonEmptyString(turn?.id)
2374
+ || normalizeNonEmptyString(turn?.turnId)
2375
+ || normalizeNonEmptyString(turn?.turn_id);
2376
+ }
2377
+
2065
2378
  async function fetchSafeThreadTurnsListFallback(request, {
2066
2379
  fetchPage,
2067
2380
  now,
2068
2381
  sanitizeForRelay,
2382
+ sanitizeContext = {},
2069
2383
  payloadSoftLimitBytes,
2070
2384
  }) {
2071
2385
  const params = request?.params;
@@ -2093,6 +2407,7 @@ async function fetchSafeThreadTurnsListFallback(request, {
2093
2407
  turns: pageResult[turnsKey],
2094
2408
  maxTurns: safeLimit,
2095
2409
  sanitizeForRelay,
2410
+ sanitizeContext,
2096
2411
  payloadSoftLimitBytes,
2097
2412
  });
2098
2413
  if (response) {
@@ -2161,6 +2476,7 @@ function buildLargestSafeTurnsListResponse({
2161
2476
  turns,
2162
2477
  maxTurns,
2163
2478
  sanitizeForRelay,
2479
+ sanitizeContext = {},
2164
2480
  payloadSoftLimitBytes,
2165
2481
  }) {
2166
2482
  const sliceLimit = Math.min(turns.length, maxTurns);
@@ -2172,7 +2488,7 @@ function buildLargestSafeTurnsListResponse({
2172
2488
  turnsKey,
2173
2489
  turns.slice(0, count)
2174
2490
  );
2175
- if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) < payloadSoftLimitBytes) {
2491
+ if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, sanitizeContext) < payloadSoftLimitBytes) {
2176
2492
  return response;
2177
2493
  }
2178
2494
  }
@@ -2182,6 +2498,7 @@ function buildLargestSafeTurnsListResponse({
2182
2498
  turnsKey,
2183
2499
  turn: turns[0],
2184
2500
  sanitizeForRelay,
2501
+ sanitizeContext,
2185
2502
  payloadSoftLimitBytes,
2186
2503
  });
2187
2504
  }
@@ -2192,6 +2509,7 @@ function buildEmergencySingleTurnResponse({
2192
2509
  turnsKey,
2193
2510
  turn,
2194
2511
  sanitizeForRelay,
2512
+ sanitizeContext = {},
2195
2513
  payloadSoftLimitBytes,
2196
2514
  }) {
2197
2515
  if (!turn || typeof turn !== "object" || Array.isArray(turn)) {
@@ -2214,7 +2532,7 @@ function buildEmergencySingleTurnResponse({
2214
2532
  remodexEmergencySingleTurnForRelay: true,
2215
2533
  },
2216
2534
  };
2217
- if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) < payloadSoftLimitBytes) {
2535
+ if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, sanitizeContext) < payloadSoftLimitBytes) {
2218
2536
  return response;
2219
2537
  }
2220
2538
  }
@@ -2292,10 +2610,10 @@ function jsonByteLength(value) {
2292
2610
  }
2293
2611
  }
2294
2612
 
2295
- function measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) {
2613
+ function measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, requestContext = {}) {
2296
2614
  try {
2297
2615
  const rawResponse = JSON.stringify(response);
2298
- const sanitizedResponse = sanitizeForRelay(rawResponse, "thread/turns/list");
2616
+ const sanitizedResponse = sanitizeForRelay(rawResponse, "thread/turns/list", requestContext);
2299
2617
  return Buffer.byteLength(sanitizedResponse, "utf8");
2300
2618
  } catch {
2301
2619
  return Number.POSITIVE_INFINITY;
@@ -2406,9 +2724,9 @@ function isRelayBoundServerRequestMethod(method) {
2406
2724
 
2407
2725
  // Shrinks thread history snapshots/pages for mobile relay delivery.
2408
2726
  // This elides bulky blobs and replaces oversized older history with a compact marker.
2409
- function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
2727
+ function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod, requestContext = {}) {
2410
2728
  if (requestMethod === "thread/turns/list") {
2411
- return sanitizeThreadTurnsListForRelay(rawMessage);
2729
+ return sanitizeThreadTurnsListForRelay(rawMessage, requestContext);
2412
2730
  }
2413
2731
 
2414
2732
  if (requestMethod !== "thread/read" && requestMethod !== "thread/resume") {
@@ -2421,12 +2739,15 @@ function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
2421
2739
  return rawMessage;
2422
2740
  }
2423
2741
 
2424
- const threadId = normalizeNonEmptyString(thread.id)
2742
+ const threadId = normalizeNonEmptyString(requestContext?.threadId)
2743
+ || normalizeNonEmptyString(thread.id)
2425
2744
  || normalizeNonEmptyString(thread.threadId)
2426
2745
  || normalizeNonEmptyString(thread.thread_id);
2427
2746
  const { turns: sanitizedTurns, didSanitize } = sanitizeRelayHistoryTurns(thread.turns, threadId);
2747
+ const { thread: threadWithJsonlMetadata, didAugment: didAugmentThreadMetadata } = augmentRelayThreadWithJsonlMetadata(thread, threadId);
2748
+ const { turns: augmentedTurns, didAugment } = augmentRelayHistoryTurnsWithJsonlArtifacts(sanitizedTurns, threadId);
2428
2749
 
2429
- if (!didSanitize) {
2750
+ if (!didSanitize && !didAugment && !didAugmentThreadMetadata) {
2430
2751
  const trimmedPayload = trimThreadPayloadForRelay(parsed, thread);
2431
2752
  return trimmedPayload == null ? rawMessage : trimmedPayload;
2432
2753
  }
@@ -2436,8 +2757,8 @@ function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
2436
2757
  result: {
2437
2758
  ...parsed.result,
2438
2759
  thread: {
2439
- ...thread,
2440
- turns: sanitizedTurns,
2760
+ ...threadWithJsonlMetadata,
2761
+ turns: augmentedTurns,
2441
2762
  },
2442
2763
  },
2443
2764
  });
@@ -2445,7 +2766,7 @@ function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
2445
2766
  return trimThreadPayloadForRelay(parseBridgeJSON(sanitizedPayload), null) ?? sanitizedPayload;
2446
2767
  }
2447
2768
 
2448
- function sanitizeThreadTurnsListForRelay(rawMessage) {
2769
+ function sanitizeThreadTurnsListForRelay(rawMessage, requestContext = {}) {
2449
2770
  const parsed = parseBridgeJSON(rawMessage);
2450
2771
  const result = parsed?.result;
2451
2772
  if (!result || typeof result !== "object" || Array.isArray(result)) {
@@ -2457,23 +2778,372 @@ function sanitizeThreadTurnsListForRelay(rawMessage) {
2457
2778
  return rawMessage;
2458
2779
  }
2459
2780
 
2460
- const threadId = normalizeNonEmptyString(result.threadId)
2781
+ const threadId = normalizeNonEmptyString(requestContext?.threadId)
2782
+ || normalizeNonEmptyString(result.threadId)
2461
2783
  || normalizeNonEmptyString(result.thread_id)
2462
2784
  || normalizeNonEmptyString(result.thread?.id)
2463
2785
  || normalizeNonEmptyString(result.thread?.threadId)
2464
- || normalizeNonEmptyString(result.thread?.thread_id);
2786
+ || normalizeNonEmptyString(result.thread?.thread_id)
2787
+ || inferThreadIdFromTurns(result[turnsKey]);
2465
2788
  const { turns: sanitizedTurns, didSanitize } = sanitizeRelayHistoryTurns(result[turnsKey], threadId);
2466
- const sanitizedParsed = didSanitize
2789
+ const shouldAugmentJsonlArtifacts = requestContext?.skipJsonlArtifactAugmentation !== true;
2790
+ const { turns: augmentedTurns, didAugment } = shouldAugmentJsonlArtifacts
2791
+ ? augmentRelayHistoryTurnsWithJsonlArtifacts(sanitizedTurns, threadId)
2792
+ : { turns: sanitizedTurns, didAugment: false };
2793
+ const didChange = didSanitize || didAugment;
2794
+ const sanitizedParsed = didChange
2467
2795
  ? {
2468
2796
  ...parsed,
2469
2797
  result: {
2470
2798
  ...result,
2471
- [turnsKey]: sanitizedTurns,
2799
+ [turnsKey]: augmentedTurns,
2472
2800
  },
2473
2801
  }
2474
2802
  : parsed;
2475
2803
 
2476
- return trimTurnsListPayloadForRelay(sanitizedParsed, turnsKey, didSanitize ? null : rawMessage);
2804
+ return trimTurnsListPayloadForRelay(sanitizedParsed, turnsKey, didChange ? null : rawMessage);
2805
+ }
2806
+
2807
+ function augmentRelayThreadWithJsonlMetadata(thread, threadId = "") {
2808
+ const cwd = readJsonlThreadCwd(threadId);
2809
+ if (!cwd || !thread || typeof thread !== "object") {
2810
+ return { thread, didAugment: false };
2811
+ }
2812
+
2813
+ if (normalizeNonEmptyString(thread.cwd) === cwd
2814
+ && normalizeNonEmptyString(thread.current_working_directory) === cwd) {
2815
+ return { thread, didAugment: false };
2816
+ }
2817
+
2818
+ return {
2819
+ thread: {
2820
+ ...thread,
2821
+ cwd,
2822
+ current_working_directory: cwd,
2823
+ },
2824
+ didAugment: true,
2825
+ };
2826
+ }
2827
+
2828
+ function readJsonlThreadCwd(threadId) {
2829
+ const normalizedThreadId = normalizeNonEmptyString(threadId);
2830
+ if (!normalizedThreadId) {
2831
+ return "";
2832
+ }
2833
+
2834
+ try {
2835
+ const rolloutPath = findRecentRolloutFileForContextRead(resolveSessionsRoot(), { threadId: normalizedThreadId });
2836
+ if (!rolloutPath) {
2837
+ return "";
2838
+ }
2839
+
2840
+ const metadata = parseSessionJsonlMetadata(fs.readFileSync(rolloutPath, "utf8"));
2841
+ const cwd = normalizeNonEmptyString(metadata?.cwd);
2842
+ return cwd && path.isAbsolute(cwd) ? cwd : "";
2843
+ } catch {
2844
+ return "";
2845
+ }
2846
+ }
2847
+
2848
+ function augmentRelayHistoryTurnsWithJsonlArtifacts(turns, threadId = "") {
2849
+ const normalizedThreadId = normalizeNonEmptyString(threadId);
2850
+ if (!normalizedThreadId || !Array.isArray(turns) || turns.length === 0) {
2851
+ return { turns, didAugment: false };
2852
+ }
2853
+
2854
+ const jsonlArtifactsByTurnId = readJsonlArtifactItemsByTurnId(normalizedThreadId);
2855
+ if (jsonlArtifactsByTurnId.size === 0) {
2856
+ return { turns, didAugment: false };
2857
+ }
2858
+
2859
+ let didAugment = false;
2860
+ const augmentedTurns = turns.map((turn) => {
2861
+ const turnId = normalizeNonEmptyString(turn?.id)
2862
+ || normalizeNonEmptyString(turn?.turnId)
2863
+ || normalizeNonEmptyString(turn?.turn_id);
2864
+ const artifacts = turnId ? jsonlArtifactsByTurnId.get(turnId) : null;
2865
+ if (!artifacts || !turn || typeof turn !== "object") {
2866
+ return turn;
2867
+ }
2868
+
2869
+ const items = Array.isArray(turn.items) ? turn.items : [];
2870
+ let nextItems = items;
2871
+ if (artifacts.fileChangeItem && !hasEquivalentFileChangeItem(nextItems, artifacts.fileChangeItem)) {
2872
+ nextItems = nextItems === items ? [...items] : nextItems;
2873
+ nextItems.push(artifacts.fileChangeItem);
2874
+ }
2875
+ for (const imageViewItem of artifacts.imageViewItems || []) {
2876
+ if (hasEquivalentImageViewItem(nextItems, imageViewItem)) {
2877
+ continue;
2878
+ }
2879
+ nextItems = nextItems === items ? [...items] : nextItems;
2880
+ nextItems.push(imageViewItem);
2881
+ }
2882
+ if (artifacts.progressPlanItem && !hasEquivalentProgressPlanItem(nextItems, artifacts.progressPlanItem)) {
2883
+ nextItems = nextItems === items ? [...items] : nextItems;
2884
+ nextItems.push(artifacts.progressPlanItem);
2885
+ }
2886
+
2887
+ if (nextItems === items) {
2888
+ return turn;
2889
+ }
2890
+
2891
+ didAugment = true;
2892
+ return {
2893
+ ...turn,
2894
+ items: nextItems,
2895
+ };
2896
+ });
2897
+
2898
+ return { turns: didAugment ? augmentedTurns : turns, didAugment };
2899
+ }
2900
+
2901
+ function readJsonlArtifactItemsByTurnId(threadId) {
2902
+ const emptyArtifactsByTurnId = new Map();
2903
+ const normalizedThreadId = normalizeNonEmptyString(threadId);
2904
+ if (!normalizedThreadId) {
2905
+ return emptyArtifactsByTurnId;
2906
+ }
2907
+
2908
+ const sessionsRoot = resolveSessionsRoot();
2909
+ const cacheKey = buildJsonlArtifactItemsCacheKey(sessionsRoot, normalizedThreadId);
2910
+ const cachedArtifacts = readCachedJsonlArtifactItems(cacheKey, normalizedThreadId);
2911
+ if (cachedArtifacts) {
2912
+ return cachedArtifacts;
2913
+ }
2914
+
2915
+ try {
2916
+ const rolloutPath = findRecentRolloutFileForContextRead(sessionsRoot, { threadId: normalizedThreadId });
2917
+ if (!rolloutPath) {
2918
+ jsonlArtifactItemsCacheByThread.delete(cacheKey);
2919
+ return emptyArtifactsByTurnId;
2920
+ }
2921
+
2922
+ return readAndCacheJsonlArtifactItems(cacheKey, rolloutPath, normalizedThreadId);
2923
+ } catch (error) {
2924
+ jsonlArtifactItemsCacheByThread.delete(cacheKey);
2925
+ console.warn(`[remodex] history jsonl artifact augmentation failed for ${normalizedThreadId}: ${error.message}`);
2926
+ }
2927
+
2928
+ return emptyArtifactsByTurnId;
2929
+ }
2930
+
2931
+ function buildJsonlArtifactItemsCacheKey(sessionsRoot, threadId) {
2932
+ return `${sessionsRoot}\0${threadId}`;
2933
+ }
2934
+
2935
+ function readCachedJsonlArtifactItems(cacheKey, threadId) {
2936
+ const cached = jsonlArtifactItemsCacheByThread.get(cacheKey);
2937
+ if (!cached) {
2938
+ return null;
2939
+ }
2940
+
2941
+ const stat = statJsonlArtifactRollout(cached.rolloutPath);
2942
+ if (!stat) {
2943
+ jsonlArtifactItemsCacheByThread.delete(cacheKey);
2944
+ return null;
2945
+ }
2946
+
2947
+ if (stat.mtimeMs !== cached.mtimeMs || stat.size !== cached.size) {
2948
+ try {
2949
+ return readAndCacheJsonlArtifactItems(cacheKey, cached.rolloutPath, threadId, stat);
2950
+ } catch (error) {
2951
+ jsonlArtifactItemsCacheByThread.delete(cacheKey);
2952
+ console.warn(`[remodex] history jsonl artifact cache refresh failed for ${threadId}: ${error.message}`);
2953
+ return null;
2954
+ }
2955
+ }
2956
+
2957
+ const now = Date.now();
2958
+ if (now - cached.checkedAt <= RELAY_JSONL_ARTIFACT_CACHE_TTL_MS) {
2959
+ return cached.artifactsByTurnId;
2960
+ }
2961
+
2962
+ cached.checkedAt = now;
2963
+ return null;
2964
+ }
2965
+
2966
+ function readAndCacheJsonlArtifactItems(cacheKey, rolloutPath, threadId, stat = null) {
2967
+ const rolloutStat = stat || fs.statSync(rolloutPath);
2968
+ const artifactsByTurnId = new Map();
2969
+ try {
2970
+ const turns = parseSessionJsonlTurns(fs.readFileSync(rolloutPath, "utf8"), { threadId });
2971
+ for (const turn of turns) {
2972
+ const turnId = normalizeNonEmptyString(turn?.id);
2973
+ const turnItems = Array.isArray(turn?.items) ? turn.items : [];
2974
+ if (!turnId || turnItems.length === 0) {
2975
+ continue;
2976
+ }
2977
+
2978
+ const fileChanges = turnItems.filter((item) => normalizeHistoryItemToken(item?.type) === "filechange");
2979
+ const progressPlan = turnItems.find((item) => (
2980
+ normalizeHistoryItemToken(item?.type) === "plan"
2981
+ && item?.remodexJsonlProgressPlan === true
2982
+ ));
2983
+ const artifacts = {
2984
+ fileChangeItem: null,
2985
+ imageViewItems: [],
2986
+ progressPlanItem: null,
2987
+ };
2988
+
2989
+ const changes = [];
2990
+ for (const item of fileChanges) {
2991
+ if (Array.isArray(item.changes)) {
2992
+ changes.push(...item.changes);
2993
+ }
2994
+ }
2995
+ if (changes.length > 0) {
2996
+ artifacts.fileChangeItem = {
2997
+ id: `remodex-jsonl-file-change-${turnId}`,
2998
+ type: "fileChange",
2999
+ status: "completed",
3000
+ changes,
3001
+ remodexJsonlFileChangeAggregate: true,
3002
+ };
3003
+ }
3004
+ if (progressPlan) {
3005
+ artifacts.progressPlanItem = {
3006
+ ...progressPlan,
3007
+ id: normalizeNonEmptyString(progressPlan.id) || `remodex-jsonl-progress-plan-${turnId}`,
3008
+ };
3009
+ }
3010
+ artifacts.imageViewItems = turnItems
3011
+ .filter((item) => normalizeHistoryItemToken(item?.type) === "imageview")
3012
+ .map((item, index) => ({
3013
+ ...item,
3014
+ id: normalizeNonEmptyString(item.id) || `remodex-jsonl-image-view-${turnId}-${index + 1}`,
3015
+ }));
3016
+
3017
+ if (artifacts.fileChangeItem || artifacts.progressPlanItem || artifacts.imageViewItems.length > 0) {
3018
+ artifactsByTurnId.set(turnId, artifacts);
3019
+ }
3020
+ }
3021
+ } catch (error) {
3022
+ jsonlArtifactItemsCacheByThread.delete(cacheKey);
3023
+ throw error;
3024
+ }
3025
+
3026
+ rememberJsonlArtifactItemsCache(cacheKey, {
3027
+ rolloutPath,
3028
+ mtimeMs: rolloutStat.mtimeMs,
3029
+ size: rolloutStat.size,
3030
+ checkedAt: Date.now(),
3031
+ artifactsByTurnId,
3032
+ });
3033
+ return artifactsByTurnId;
3034
+ }
3035
+
3036
+ function statJsonlArtifactRollout(rolloutPath) {
3037
+ try {
3038
+ return fs.statSync(rolloutPath);
3039
+ } catch {
3040
+ return null;
3041
+ }
3042
+ }
3043
+
3044
+ function rememberJsonlArtifactItemsCache(cacheKey, entry) {
3045
+ jsonlArtifactItemsCacheByThread.set(cacheKey, entry);
3046
+ while (jsonlArtifactItemsCacheByThread.size > RELAY_JSONL_ARTIFACT_CACHE_MAX_ENTRIES) {
3047
+ const oldestKey = jsonlArtifactItemsCacheByThread.keys().next().value;
3048
+ if (oldestKey == null) {
3049
+ break;
3050
+ }
3051
+ jsonlArtifactItemsCacheByThread.delete(oldestKey);
3052
+ }
3053
+ }
3054
+
3055
+ function hasEquivalentFileChangeItem(items, incomingItem) {
3056
+ const incomingId = normalizeNonEmptyString(incomingItem?.id);
3057
+ const incomingPaths = fileChangePathSet(incomingItem);
3058
+ return items.some((item) => {
3059
+ if (normalizeHistoryItemToken(item?.type) !== "filechange") {
3060
+ return false;
3061
+ }
3062
+ if (incomingId && normalizeNonEmptyString(item.id) === incomingId) {
3063
+ return true;
3064
+ }
3065
+ if (item.remodexJsonlFileChangeAggregate === true) {
3066
+ return true;
3067
+ }
3068
+
3069
+ const existingPaths = fileChangePathSet(item);
3070
+ if (incomingPaths.size === 0 || existingPaths.size === 0) {
3071
+ return false;
3072
+ }
3073
+ for (const pathKey of incomingPaths) {
3074
+ if (!existingPaths.has(pathKey)) {
3075
+ return false;
3076
+ }
3077
+ }
3078
+ return true;
3079
+ });
3080
+ }
3081
+
3082
+ function hasEquivalentProgressPlanItem(items, incomingItem) {
3083
+ const incomingId = normalizeNonEmptyString(incomingItem?.id);
3084
+ return items.some((item) => {
3085
+ if (normalizeHistoryItemToken(item?.type) !== "plan") {
3086
+ return false;
3087
+ }
3088
+ return item.remodexJsonlProgressPlan === true
3089
+ || (incomingId && normalizeNonEmptyString(item.id) === incomingId);
3090
+ });
3091
+ }
3092
+
3093
+ function hasEquivalentImageViewItem(items, incomingItem) {
3094
+ const incomingId = normalizeNonEmptyString(incomingItem?.id);
3095
+ const incomingPath = normalizeImageViewPathKey(incomingItem);
3096
+ return items.some((item) => {
3097
+ if (normalizeHistoryItemToken(item?.type) !== "imageview") {
3098
+ return false;
3099
+ }
3100
+ const itemId = normalizeNonEmptyString(item.id);
3101
+ if (incomingId && itemId === incomingId) {
3102
+ return true;
3103
+ }
3104
+ return incomingPath && normalizeImageViewPathKey(item) === incomingPath;
3105
+ });
3106
+ }
3107
+
3108
+ function normalizeImageViewPathKey(item) {
3109
+ return normalizeNonEmptyString(item?.path)
3110
+ || normalizeNonEmptyString(item?.saved_path)
3111
+ || normalizeNonEmptyString(item?.savedPath)
3112
+ || normalizeNonEmptyString(item?.file_path)
3113
+ || normalizeNonEmptyString(item?.filePath);
3114
+ }
3115
+
3116
+ function fileChangePathSet(item) {
3117
+ const paths = new Set();
3118
+ const changes = Array.isArray(item?.changes) ? item.changes : [];
3119
+ for (const change of changes) {
3120
+ const pathKey = normalizeFileChangePathKey(change?.path || change?.file || change?.filePath || change?.file_path);
3121
+ if (pathKey) {
3122
+ paths.add(pathKey);
3123
+ }
3124
+ }
3125
+ return paths;
3126
+ }
3127
+
3128
+ function normalizeFileChangePathKey(value) {
3129
+ return normalizeNonEmptyString(value).replace(/\\/g, "/").replace(/^\/+/, "").toLowerCase();
3130
+ }
3131
+
3132
+ function inferThreadIdFromTurns(turns) {
3133
+ if (!Array.isArray(turns)) {
3134
+ return "";
3135
+ }
3136
+ for (const turn of turns) {
3137
+ const threadId = normalizeNonEmptyString(turn?.threadId)
3138
+ || normalizeNonEmptyString(turn?.thread_id)
3139
+ || normalizeNonEmptyString(turn?.thread?.id)
3140
+ || normalizeNonEmptyString(turn?.thread?.threadId)
3141
+ || normalizeNonEmptyString(turn?.thread?.thread_id);
3142
+ if (threadId) {
3143
+ return threadId;
3144
+ }
3145
+ }
3146
+ return "";
2477
3147
  }
2478
3148
 
2479
3149
  function sanitizeRelayHistoryTurns(turns, threadId = "") {
@@ -2504,7 +3174,12 @@ function sanitizeRelayHistoryTurn(turn, threadId = "") {
2504
3174
  }
2505
3175
 
2506
3176
  let itemDidChange = false;
2507
- let sanitizedItem = annotateImageGenerationHistoryItem(item, turnThreadId);
3177
+ let sanitizedItem = convertApplyPatchHistoryItem(item) || item;
3178
+ if (sanitizedItem !== item) {
3179
+ itemDidChange = true;
3180
+ }
3181
+
3182
+ sanitizedItem = annotateImageGenerationHistoryItem(sanitizedItem, turnThreadId);
2508
3183
  if (sanitizedItem !== item) {
2509
3184
  itemDidChange = true;
2510
3185
  }
@@ -2547,6 +3222,26 @@ function sanitizeRelayHistoryTurn(turn, threadId = "") {
2547
3222
  : turn;
2548
3223
  }
2549
3224
 
3225
+ function convertApplyPatchHistoryItem(item) {
3226
+ const itemType = normalizeHistoryItemToken(item?.type);
3227
+ const toolName = normalizeNonEmptyString(item?.name);
3228
+ if (toolName !== "apply_patch" || itemType !== "customtoolcall") {
3229
+ return null;
3230
+ }
3231
+
3232
+ const fileChangeItem = buildApplyPatchFileChangeItem({
3233
+ callId: normalizeNonEmptyString(item.call_id) || normalizeNonEmptyString(item.callId) || normalizeNonEmptyString(item.id),
3234
+ patch: normalizeNonEmptyString(item.input),
3235
+ status: normalizeNonEmptyString(item.status) || "completed",
3236
+ idFallback: normalizeNonEmptyString(item.id) || "history-apply-patch-file-change",
3237
+ });
3238
+ return fileChangeItem ? { ...item, ...fileChangeItem } : null;
3239
+ }
3240
+
3241
+ function normalizeHistoryItemToken(value) {
3242
+ return normalizeNonEmptyString(value).toLowerCase().replace(/[\s_-]+/g, "");
3243
+ }
3244
+
2550
3245
  // Annotates live image-generation notifications so the phone can render a local-file
2551
3246
  // preview and does not receive the bulky inline base64 result over the relay.
2552
3247
  function sanitizeLiveGeneratedImageMessageForRelay(rawMessage) {
@@ -3123,6 +3818,12 @@ function compactHistoryItemForRelay(item, maxChars) {
3123
3818
  type: typeof item?.type === "string" ? item.type : "relay_truncated_item",
3124
3819
  role: typeof item?.role === "string" ? item.role : undefined,
3125
3820
  itemId: typeof item?.itemId === "string" ? item.itemId : undefined,
3821
+ turnId: typeof item?.turnId === "string" ? item.turnId : undefined,
3822
+ turn_id: typeof item?.turn_id === "string" ? item.turn_id : undefined,
3823
+ createdAt: relayScalarHistoryMetadata(item?.createdAt),
3824
+ created_at: relayScalarHistoryMetadata(item?.created_at),
3825
+ timestamp: relayScalarHistoryMetadata(item?.timestamp),
3826
+ time: relayScalarHistoryMetadata(item?.time),
3126
3827
  relayPayloadTruncated: true,
3127
3828
  };
3128
3829
  const tailText = maxChars > 0 ? firstRelayTextTail(item, maxChars) : "";
@@ -3135,6 +3836,10 @@ function compactHistoryItemForRelay(item, maxChars) {
3135
3836
  );
3136
3837
  }
3137
3838
 
3839
+ function relayScalarHistoryMetadata(value) {
3840
+ return typeof value === "string" || typeof value === "number" ? value : undefined;
3841
+ }
3842
+
3138
3843
  function firstRelayTextTail(value, maxChars) {
3139
3844
  if (!value || typeof value !== "object" || Array.isArray(value)) {
3140
3845
  return "";
@@ -3167,48 +3872,6 @@ function truncateRelayTextTail(value, maxChars) {
3167
3872
  return `…\n${tail}`;
3168
3873
  }
3169
3874
 
3170
- // Treats silent relay sockets as stale so the daemon can self-heal after sleep/wake.
3171
- function hasRelayConnectionGoneStale(
3172
- lastActivityAt,
3173
- {
3174
- now = Date.now(),
3175
- staleAfterMs = RELAY_WATCHDOG_STALE_AFTER_MS,
3176
- } = {}
3177
- ) {
3178
- return Number.isFinite(lastActivityAt)
3179
- && Number.isFinite(now)
3180
- && now - lastActivityAt >= staleAfterMs;
3181
- }
3182
-
3183
- // Keeps persisted daemon status honest by downgrading stale "connected" snapshots.
3184
- function buildHeartbeatBridgeStatus(
3185
- status,
3186
- lastActivityAt,
3187
- {
3188
- now = Date.now(),
3189
- staleAfterMs = RELAY_WATCHDOG_STALE_AFTER_MS,
3190
- staleMessage = STALE_RELAY_STATUS_MESSAGE,
3191
- } = {}
3192
- ) {
3193
- if (!status || typeof status !== "object") {
3194
- return status;
3195
- }
3196
-
3197
- if (status.connectionStatus !== "connected") {
3198
- return status;
3199
- }
3200
-
3201
- if (!hasRelayConnectionGoneStale(lastActivityAt, { now, staleAfterMs })) {
3202
- return status;
3203
- }
3204
-
3205
- return {
3206
- ...status,
3207
- connectionStatus: "disconnected",
3208
- lastError: staleMessage,
3209
- };
3210
- }
3211
-
3212
3875
  function persistBridgePreferences(
3213
3876
  {
3214
3877
  keepMacAwakeEnabled,
@@ -3225,16 +3888,19 @@ function persistBridgePreferences(
3225
3888
  }
3226
3889
 
3227
3890
  module.exports = {
3891
+ buildThreadTurnsListRelaySanitizeContext,
3228
3892
  buildHeartbeatBridgeStatus,
3229
3893
  buildRelayCloseStatusError,
3230
3894
  buildRelayAccessTokenHeaders,
3231
3895
  buildRelayUserAgentHeader,
3232
3896
  createMacOSBridgeWakeAssertion,
3897
+ disableUnsupportedReasoningSummaryForTurnStart,
3233
3898
  fetchAdaptiveThreadTurnsListForRelay,
3234
3899
  hasRelayConnectionGoneStale,
3235
3900
  isTerminalRelayCloseCode,
3236
3901
  normalizeRelayBoundJsonRpcMessage,
3237
3902
  persistBridgePreferences,
3903
+ resolveJsonlTurnsListRolloutPathForFallback,
3238
3904
  sanitizeLiveGeneratedImageMessageForRelay,
3239
3905
  sanitizeThreadHistoryImagesForRelay,
3240
3906
  startBridge,