@makerbi/remodex 1.5.4 → 2.0.0
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/bin/remodex.js +77 -13
- package/package.json +1 -1
- package/src/account-status.js +7 -1
- package/src/apply-patch-changes.js +185 -0
- package/src/bootstrap-codex-cli.js +1 -1
- package/src/bridge-status.js +3 -2
- package/src/bridge.js +837 -73
- package/src/codex-transport.js +10 -10
- package/src/desktop-handler.js +14 -1
- package/src/desktop-ipc-action-follower.js +129 -0
- package/src/git-handler.js +92 -28
- package/src/index.js +4 -2
- package/src/ios-app-compatibility.js +7 -7
- package/src/macos-launch-agent.js +132 -2
- package/src/project-handler.js +162 -1
- package/src/push-notification-service-client.js +85 -37
- package/src/push-notification-tracker.js +15 -0
- package/src/qr.js +2 -5
- package/src/rollout-live-mirror.js +331 -20
- package/src/rollout-watch.js +5 -1
- package/src/secure-device-state.js +66 -2
- package/src/secure-transport.js +47 -12
- package/src/session-jsonl-history.js +850 -16
- package/src/voice-handler.js +162 -65
- package/src/workspace-handler.js +327 -14
package/src/bridge.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
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
|
-
const { randomBytes, randomUUID } = require("crypto");
|
|
8
|
+
const { createHash, 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");
|
|
@@ -45,6 +46,7 @@ const { createPushNotificationTracker } = require("./push-notification-tracker")
|
|
|
45
46
|
const { resolveCodexGeneratedImagesRoot } = require("./codex-home");
|
|
46
47
|
const {
|
|
47
48
|
loadOrCreateBridgeDeviceState,
|
|
49
|
+
rememberLastSeenClientDeviceKind,
|
|
48
50
|
rememberLastSeenPhoneAppVersion,
|
|
49
51
|
resolveBridgeRelaySession,
|
|
50
52
|
} = require("./secure-device-state");
|
|
@@ -52,6 +54,7 @@ const { createBridgeSecureTransport } = require("./secure-transport");
|
|
|
52
54
|
const { createRolloutLiveMirrorController } = require("./rollout-live-mirror");
|
|
53
55
|
const {
|
|
54
56
|
createDesktopIpcActionFollower,
|
|
57
|
+
seedConversationStateFromThreadRead,
|
|
55
58
|
} = require("./desktop-ipc-action-follower");
|
|
56
59
|
const { version: bridgePackageVersion = "" } = require("../package.json");
|
|
57
60
|
const {
|
|
@@ -62,8 +65,11 @@ const {
|
|
|
62
65
|
} = require("./ios-app-compatibility");
|
|
63
66
|
const { createShortPairingCode, SHORT_PAIRING_CODE_LENGTH } = require("./qr");
|
|
64
67
|
const {
|
|
68
|
+
parseSessionJsonlMetadata,
|
|
69
|
+
parseSessionJsonlTurns,
|
|
65
70
|
readThreadTurnsListPageFromSessionJsonl,
|
|
66
71
|
} = require("./session-jsonl-history");
|
|
72
|
+
const { buildApplyPatchFileChangeItem } = require("./apply-patch-changes");
|
|
67
73
|
|
|
68
74
|
const execFileAsync = promisify(execFile);
|
|
69
75
|
const RELAY_WATCHDOG_PING_INTERVAL_MS = 10_000;
|
|
@@ -78,6 +84,12 @@ const RELAY_TURNS_LIST_TARGET_BUDGET_MS = 5_500;
|
|
|
78
84
|
const RELAY_TURNS_LIST_BUDGET_RESERVE_MS = 1_000;
|
|
79
85
|
const RELAY_TURNS_LIST_MAX_INITIAL_LIMIT = 5;
|
|
80
86
|
const RELAY_TURNS_LIST_SAFE_RETRY_LIMIT = 5;
|
|
87
|
+
const RELAY_JSONL_TURNS_LIST_CACHE_TTL_MS = 30_000;
|
|
88
|
+
const RELAY_JSONL_ARTIFACT_CACHE_TTL_MS = 2_000;
|
|
89
|
+
const RELAY_JSONL_ARTIFACT_CACHE_MAX_ENTRIES = 128;
|
|
90
|
+
const BRIDGE_PACKAGE_UPDATE_COMMAND = "npm install -g @makerbi/remodex@latest";
|
|
91
|
+
const BRIDGE_PACKAGE_UPDATE_TIMEOUT_MS = 180_000;
|
|
92
|
+
const BRIDGE_RESTART_AFTER_UPDATE_DELAY_MS = 750;
|
|
81
93
|
const MODELS_WITHOUT_REASONING_SUMMARY = new Set([
|
|
82
94
|
"gpt-5.3-codex-spark",
|
|
83
95
|
]);
|
|
@@ -97,6 +109,9 @@ const RELAY_TURNS_LIST_PAGINATION_RESULT_KEYS = [
|
|
|
97
109
|
"previousCursor",
|
|
98
110
|
"previous_cursor",
|
|
99
111
|
];
|
|
112
|
+
const jsonlArtifactItemsCacheByThread = new Map();
|
|
113
|
+
const FORWARDED_REQUEST_METHODS_MAX_SIZE = 500;
|
|
114
|
+
const JSONL_ROLLOUT_PATH_CACHE_MAX_SIZE = 200;
|
|
100
115
|
|
|
101
116
|
function buildRelayUserAgentHeader({ version = bridgePackageVersion } = {}) {
|
|
102
117
|
const normalizedVersion = typeof version === "string" && version.trim()
|
|
@@ -116,6 +131,18 @@ function buildRelayAccessTokenHeaders(config = {}, env = process.env) {
|
|
|
116
131
|
: {};
|
|
117
132
|
}
|
|
118
133
|
|
|
134
|
+
function evictOldestEntries(map, maxSize) {
|
|
135
|
+
if (map.size <= maxSize) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
const excess = map.size - maxSize;
|
|
139
|
+
const iterator = map.keys();
|
|
140
|
+
for (let i = 0; i < excess; i += 1) {
|
|
141
|
+
const key = iterator.next().value;
|
|
142
|
+
map.delete(key);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
119
146
|
function startBridge({
|
|
120
147
|
config: explicitConfig = null,
|
|
121
148
|
printPairingQr = true,
|
|
@@ -192,6 +219,8 @@ function startBridge({
|
|
|
192
219
|
const relayChannels = [];
|
|
193
220
|
const codexResponseRoutesById = new Map();
|
|
194
221
|
const extraRelaySessionCount = readExtraRelaySessionCount(process.env);
|
|
222
|
+
const jsonlTurnsListRolloutCacheByThread = new Map();
|
|
223
|
+
const jsonlTurnsListRolloutMissCacheByThread = new Map();
|
|
195
224
|
const trackedForwardedRequestMethods = new Set([
|
|
196
225
|
"account/login/start",
|
|
197
226
|
"account/login/cancel",
|
|
@@ -210,14 +239,23 @@ function startBridge({
|
|
|
210
239
|
requestId: null,
|
|
211
240
|
startedAt: 0,
|
|
212
241
|
};
|
|
242
|
+
let activePhoneSummary = null;
|
|
213
243
|
const secureTransport = createBridgeSecureTransport({
|
|
214
244
|
sessionId,
|
|
215
245
|
relayUrl: relayBaseUrl,
|
|
216
246
|
deviceState,
|
|
247
|
+
displayName: os.hostname(),
|
|
217
248
|
onTrustedPhoneUpdate(nextDeviceState) {
|
|
218
249
|
deviceState = nextDeviceState;
|
|
219
250
|
sendRelayRegistrationUpdate(nextDeviceState);
|
|
220
251
|
},
|
|
252
|
+
onSecureSessionReady(session) {
|
|
253
|
+
activePhoneSummary = buildActivePhoneSummary(session, deviceState);
|
|
254
|
+
const lastPublishedBridgeStatus = bridgeStatusPublisher.latest();
|
|
255
|
+
if (lastPublishedBridgeStatus) {
|
|
256
|
+
publishBridgeStatus(lastPublishedBridgeStatus);
|
|
257
|
+
}
|
|
258
|
+
},
|
|
221
259
|
});
|
|
222
260
|
let primaryRelayChannel = null;
|
|
223
261
|
// Keeps one stable sender identity across reconnects so buffered replay state
|
|
@@ -240,6 +278,9 @@ function startBridge({
|
|
|
240
278
|
const desktopIpcActionFollower = !config.codexEndpoint
|
|
241
279
|
? createDesktopIpcActionFollower({
|
|
242
280
|
sendApplicationResponse,
|
|
281
|
+
readConversationState: async (threadId) => seedConversationStateFromThreadRead(
|
|
282
|
+
await sendCodexRequest("thread/read", { threadId })
|
|
283
|
+
),
|
|
243
284
|
socketPath: config.desktopIpcSocketPath || undefined,
|
|
244
285
|
})
|
|
245
286
|
: null;
|
|
@@ -323,6 +364,33 @@ function startBridge({
|
|
|
323
364
|
relayWatchdogTimer = null;
|
|
324
365
|
}
|
|
325
366
|
|
|
367
|
+
function prepareBridgeShutdown() {
|
|
368
|
+
isShuttingDown = true;
|
|
369
|
+
bridgeWakeAssertion.stop();
|
|
370
|
+
clearReconnectTimer();
|
|
371
|
+
clearRelayWatchdog();
|
|
372
|
+
bridgeStatusPublisher.stopHeartbeat();
|
|
373
|
+
stopContextUsageWatcher();
|
|
374
|
+
rolloutLiveMirror?.stopAll();
|
|
375
|
+
desktopIpcActionFollower?.stopAll();
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function stopBridge() {
|
|
379
|
+
if (isShuttingDown) {
|
|
380
|
+
return;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
prepareBridgeShutdown();
|
|
384
|
+
desktopRefresher.handleTransportReset();
|
|
385
|
+
failBridgeManagedCodexRequests(new Error("Bridge stopped before the request completed."));
|
|
386
|
+
forwardedRequestMethodsById.clear();
|
|
387
|
+
|
|
388
|
+
if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) {
|
|
389
|
+
socket.close();
|
|
390
|
+
}
|
|
391
|
+
codex.shutdown();
|
|
392
|
+
}
|
|
393
|
+
|
|
326
394
|
function startRelayWatchdog(trackedSocket) {
|
|
327
395
|
clearRelayWatchdog();
|
|
328
396
|
markRelayActivity();
|
|
@@ -361,6 +429,9 @@ function startBridge({
|
|
|
361
429
|
|
|
362
430
|
lastConnectionStatus = status;
|
|
363
431
|
lastConnectionError = lastError;
|
|
432
|
+
if (status !== "connected") {
|
|
433
|
+
activePhoneSummary = null;
|
|
434
|
+
}
|
|
364
435
|
publishBridgeStatus({
|
|
365
436
|
state: "running",
|
|
366
437
|
connectionStatus: status,
|
|
@@ -379,19 +450,9 @@ function startBridge({
|
|
|
379
450
|
return;
|
|
380
451
|
}
|
|
381
452
|
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
shutdown(codex, () => socket, () => {
|
|
386
|
-
isShuttingDown = true;
|
|
387
|
-
bridgeWakeAssertion.stop();
|
|
388
|
-
clearReconnectTimer();
|
|
389
|
-
clearRelayWatchdog();
|
|
390
|
-
bridgeStatusPublisher.stopHeartbeat();
|
|
391
|
-
closeExtraRelayChannels();
|
|
392
|
-
}, {
|
|
393
|
-
exitCode: closeCode === CLOSE_CODE_MAC_UNAUTHORIZED ? 1 : 0,
|
|
394
|
-
});
|
|
453
|
+
if (closeCode === 4000 || closeCode === 4001) {
|
|
454
|
+
logConnectionStatus("disconnected");
|
|
455
|
+
shutdown(codex, () => socket, prepareBridgeShutdown);
|
|
395
456
|
return;
|
|
396
457
|
}
|
|
397
458
|
|
|
@@ -400,7 +461,9 @@ function startBridge({
|
|
|
400
461
|
}
|
|
401
462
|
|
|
402
463
|
reconnectAttempt += 1;
|
|
403
|
-
const
|
|
464
|
+
const baseDelayMs = Math.min(1_000 * reconnectAttempt, 5_000);
|
|
465
|
+
const jitterMs = Math.floor(Math.random() * Math.min(baseDelayMs, 2_000));
|
|
466
|
+
const delayMs = baseDelayMs + jitterMs;
|
|
404
467
|
logConnectionStatus("connecting");
|
|
405
468
|
reconnectTimer = setTimeout(() => {
|
|
406
469
|
reconnectTimer = null;
|
|
@@ -415,6 +478,11 @@ function startBridge({
|
|
|
415
478
|
|
|
416
479
|
logConnectionStatus("connecting");
|
|
417
480
|
const nextSocket = new WebSocket(relaySessionUrl, {
|
|
481
|
+
perMessageDeflate: {
|
|
482
|
+
zlibDeflateOptions: { level: 6 },
|
|
483
|
+
threshold: 256,
|
|
484
|
+
concurrencyLimit: 4,
|
|
485
|
+
},
|
|
418
486
|
// The relay uses this per-session secret to authenticate the first push registration.
|
|
419
487
|
headers: {
|
|
420
488
|
"User-Agent": buildRelayUserAgentHeader(),
|
|
@@ -471,8 +539,8 @@ function startBridge({
|
|
|
471
539
|
socket = null;
|
|
472
540
|
}
|
|
473
541
|
stopContextUsageWatcher();
|
|
474
|
-
|
|
475
|
-
|
|
542
|
+
// Relay reconnects are transport-only: keep local live observers running
|
|
543
|
+
// so their output can enter secure replay and catch up on the next resume.
|
|
476
544
|
desktopRefresher.handleTransportReset();
|
|
477
545
|
scheduleRelayReconnect(code, closeReason);
|
|
478
546
|
});
|
|
@@ -680,12 +748,7 @@ function startBridge({
|
|
|
680
748
|
console.error(`[remodex] ${lastError}`);
|
|
681
749
|
process.exitCode = 1;
|
|
682
750
|
}
|
|
683
|
-
|
|
684
|
-
bridgeWakeAssertion.stop();
|
|
685
|
-
clearReconnectTimer();
|
|
686
|
-
stopContextUsageWatcher();
|
|
687
|
-
rolloutLiveMirror?.stopAll();
|
|
688
|
-
desktopIpcActionFollower?.stopAll();
|
|
751
|
+
prepareBridgeShutdown();
|
|
689
752
|
desktopRefresher.handleTransportReset();
|
|
690
753
|
failBridgeManagedCodexRequests(new Error("Codex transport closed before the bridge request completed."));
|
|
691
754
|
forwardedRequestMethodsById.clear();
|
|
@@ -696,22 +759,8 @@ function startBridge({
|
|
|
696
759
|
closeExtraRelayChannels();
|
|
697
760
|
});
|
|
698
761
|
|
|
699
|
-
process.on("SIGINT", () => shutdown(codex, () => socket,
|
|
700
|
-
|
|
701
|
-
bridgeWakeAssertion.stop();
|
|
702
|
-
clearReconnectTimer();
|
|
703
|
-
clearRelayWatchdog();
|
|
704
|
-
bridgeStatusPublisher.stopHeartbeat();
|
|
705
|
-
closeExtraRelayChannels();
|
|
706
|
-
}));
|
|
707
|
-
process.on("SIGTERM", () => shutdown(codex, () => socket, () => {
|
|
708
|
-
isShuttingDown = true;
|
|
709
|
-
bridgeWakeAssertion.stop();
|
|
710
|
-
clearReconnectTimer();
|
|
711
|
-
clearRelayWatchdog();
|
|
712
|
-
bridgeStatusPublisher.stopHeartbeat();
|
|
713
|
-
closeExtraRelayChannels();
|
|
714
|
-
}));
|
|
762
|
+
process.on("SIGINT", () => shutdown(codex, () => socket, prepareBridgeShutdown));
|
|
763
|
+
process.on("SIGTERM", () => shutdown(codex, () => socket, prepareBridgeShutdown));
|
|
715
764
|
|
|
716
765
|
// Routes decrypted app payloads through the same bridge handlers as before.
|
|
717
766
|
function handleApplicationMessage(rawMessage, relayChannel = primaryRelayChannel) {
|
|
@@ -745,6 +794,7 @@ function startBridge({
|
|
|
745
794
|
appPath: config.codexAppPath,
|
|
746
795
|
readBridgePreferences,
|
|
747
796
|
updateBridgePreferences,
|
|
797
|
+
updateBridgePackageAndRestart,
|
|
748
798
|
})) {
|
|
749
799
|
return;
|
|
750
800
|
}
|
|
@@ -974,12 +1024,19 @@ function startBridge({
|
|
|
974
1024
|
const response = await fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
975
1025
|
fetchPage: (params) => sendCodexRequest("thread/turns/list", params),
|
|
976
1026
|
});
|
|
977
|
-
const
|
|
1027
|
+
const jsonlFallback = maybeBuildJsonlThreadTurnsListFallback(request, response);
|
|
1028
|
+
const responsePayload = jsonlFallback?.response ?? response;
|
|
1029
|
+
const finalSanitizeContext = buildThreadTurnsListRelaySanitizeContext(request);
|
|
978
1030
|
relaySanitizedResponseMethodsById.set(String(request.id), {
|
|
979
1031
|
method: "thread/turns/list",
|
|
1032
|
+
...finalSanitizeContext,
|
|
980
1033
|
createdAt: Date.now(),
|
|
981
1034
|
});
|
|
982
|
-
sendResponse(
|
|
1035
|
+
sendResponse(sanitizeThreadHistoryImagesForRelay(
|
|
1036
|
+
JSON.stringify(responsePayload),
|
|
1037
|
+
"thread/turns/list",
|
|
1038
|
+
finalSanitizeContext
|
|
1039
|
+
));
|
|
983
1040
|
} catch (error) {
|
|
984
1041
|
sendResponse(createJsonRpcErrorResponse(
|
|
985
1042
|
request.id,
|
|
@@ -993,10 +1050,6 @@ function startBridge({
|
|
|
993
1050
|
}
|
|
994
1051
|
|
|
995
1052
|
function maybeBuildJsonlThreadTurnsListFallback(request, response) {
|
|
996
|
-
if (!isEmptyTurnsListResponse(response)) {
|
|
997
|
-
return null;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
1053
|
const params = request?.params || {};
|
|
1001
1054
|
const threadId = normalizeNonEmptyString(params.threadId)
|
|
1002
1055
|
|| normalizeNonEmptyString(params.thread_id);
|
|
@@ -1005,10 +1058,17 @@ function startBridge({
|
|
|
1005
1058
|
}
|
|
1006
1059
|
|
|
1007
1060
|
try {
|
|
1008
|
-
const
|
|
1061
|
+
const responseIsEmpty = isEmptyTurnsListResponse(response);
|
|
1062
|
+
const rolloutPath = resolveJsonlTurnsListRolloutPathForFallback({
|
|
1063
|
+
threadId,
|
|
1064
|
+
responseIsEmpty,
|
|
1065
|
+
readCachedPath: readCachedJsonlTurnsListRolloutPath,
|
|
1066
|
+
findAndCachePath: findAndCacheJsonlTurnsListRolloutPath,
|
|
1067
|
+
});
|
|
1009
1068
|
if (!rolloutPath) {
|
|
1010
1069
|
return null;
|
|
1011
1070
|
}
|
|
1071
|
+
|
|
1012
1072
|
const result = readThreadTurnsListPageFromSessionJsonl(rolloutPath, {
|
|
1013
1073
|
threadId,
|
|
1014
1074
|
limit: params.limit,
|
|
@@ -1020,16 +1080,69 @@ function startBridge({
|
|
|
1020
1080
|
return null;
|
|
1021
1081
|
}
|
|
1022
1082
|
|
|
1083
|
+
if (!responseIsEmpty) {
|
|
1084
|
+
const mergedResponse = maybeMergeLatestJsonlTurnIntoTurnsListResponse(request, response, result, params);
|
|
1085
|
+
return mergedResponse ? { response: mergedResponse, usesJsonl: true } : null;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1023
1088
|
return {
|
|
1024
|
-
|
|
1025
|
-
|
|
1089
|
+
response: {
|
|
1090
|
+
id: request.id,
|
|
1091
|
+
result,
|
|
1092
|
+
},
|
|
1093
|
+
usesJsonl: true,
|
|
1026
1094
|
};
|
|
1027
1095
|
} catch (error) {
|
|
1096
|
+
jsonlTurnsListRolloutCacheByThread.delete(threadId);
|
|
1028
1097
|
console.warn(`[remodex] thread/turns/list jsonl fallback failed: ${error.message}`);
|
|
1029
1098
|
return null;
|
|
1030
1099
|
}
|
|
1031
1100
|
}
|
|
1032
1101
|
|
|
1102
|
+
function findAndCacheJsonlTurnsListRolloutPath(threadId) {
|
|
1103
|
+
if (hasFreshJsonlTurnsListRolloutMiss(threadId)) {
|
|
1104
|
+
return "";
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
const rolloutPath = findRecentRolloutFileForContextRead(resolveSessionsRoot(), { threadId });
|
|
1108
|
+
if (rolloutPath) {
|
|
1109
|
+
jsonlTurnsListRolloutMissCacheByThread.delete(threadId);
|
|
1110
|
+
jsonlTurnsListRolloutCacheByThread.set(threadId, {
|
|
1111
|
+
rolloutPath,
|
|
1112
|
+
cachedAt: Date.now(),
|
|
1113
|
+
});
|
|
1114
|
+
} else {
|
|
1115
|
+
jsonlTurnsListRolloutMissCacheByThread.set(threadId, Date.now());
|
|
1116
|
+
}
|
|
1117
|
+
return rolloutPath;
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function readCachedJsonlTurnsListRolloutPath(threadId) {
|
|
1121
|
+
const cached = jsonlTurnsListRolloutCacheByThread.get(threadId);
|
|
1122
|
+
if (!cached) {
|
|
1123
|
+
return "";
|
|
1124
|
+
}
|
|
1125
|
+
if (Date.now() - cached.cachedAt > RELAY_JSONL_TURNS_LIST_CACHE_TTL_MS) {
|
|
1126
|
+
jsonlTurnsListRolloutCacheByThread.delete(threadId);
|
|
1127
|
+
return "";
|
|
1128
|
+
}
|
|
1129
|
+
// Non-empty app-server pages only consult this positive cache to avoid
|
|
1130
|
+
// walking the sessions tree during ordinary pagination.
|
|
1131
|
+
return cached.rolloutPath;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function hasFreshJsonlTurnsListRolloutMiss(threadId) {
|
|
1135
|
+
const missedAt = jsonlTurnsListRolloutMissCacheByThread.get(threadId);
|
|
1136
|
+
if (!missedAt) {
|
|
1137
|
+
return false;
|
|
1138
|
+
}
|
|
1139
|
+
if (Date.now() - missedAt <= RELAY_JSONL_TURNS_LIST_CACHE_TTL_MS) {
|
|
1140
|
+
return true;
|
|
1141
|
+
}
|
|
1142
|
+
jsonlTurnsListRolloutMissCacheByThread.delete(threadId);
|
|
1143
|
+
return false;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1033
1146
|
// ─── Bridge-owned auth snapshot ─────────────────────────────
|
|
1034
1147
|
|
|
1035
1148
|
// Handles the bridge-owned auth status wrappers without exposing tokens to the phone.
|
|
@@ -1178,10 +1291,17 @@ function startBridge({
|
|
|
1178
1291
|
});
|
|
1179
1292
|
}
|
|
1180
1293
|
if (relaySanitizedRequestMethods.has(method)) {
|
|
1181
|
-
|
|
1294
|
+
const trackedRequest = {
|
|
1182
1295
|
method,
|
|
1296
|
+
threadId: method === "thread/turns/list" || method === "thread/read" || method === "thread/resume"
|
|
1297
|
+
? threadIdFromRequestParams(parsed.params)
|
|
1298
|
+
: "",
|
|
1183
1299
|
createdAt: Date.now(),
|
|
1184
|
-
}
|
|
1300
|
+
};
|
|
1301
|
+
if (method === "thread/turns/list") {
|
|
1302
|
+
trackedRequest.skipJsonlArtifactAugmentation = false;
|
|
1303
|
+
}
|
|
1304
|
+
relaySanitizedResponseMethodsById.set(String(requestId), trackedRequest);
|
|
1185
1305
|
}
|
|
1186
1306
|
}
|
|
1187
1307
|
|
|
@@ -1207,7 +1327,7 @@ function startBridge({
|
|
|
1207
1327
|
}
|
|
1208
1328
|
relaySanitizedResponseMethodsById.delete(String(responseId));
|
|
1209
1329
|
|
|
1210
|
-
return sanitizeThreadHistoryImagesForRelay(normalizedMessage, trackedRequest.method);
|
|
1330
|
+
return sanitizeThreadHistoryImagesForRelay(normalizedMessage, trackedRequest.method, trackedRequest);
|
|
1211
1331
|
}
|
|
1212
1332
|
|
|
1213
1333
|
function updatePendingAuthLoginFromCodexMessage(rawMessage) {
|
|
@@ -1260,16 +1380,31 @@ function startBridge({
|
|
|
1260
1380
|
}
|
|
1261
1381
|
|
|
1262
1382
|
function pruneExpiredForwardedRequestMethods(now = Date.now()) {
|
|
1383
|
+
const expiredForwarded = [];
|
|
1263
1384
|
for (const [requestId, trackedRequest] of forwardedRequestMethodsById.entries()) {
|
|
1264
1385
|
if (!trackedRequest || (now - trackedRequest.createdAt) >= forwardedRequestMethodTTLms) {
|
|
1265
|
-
|
|
1386
|
+
expiredForwarded.push(requestId);
|
|
1266
1387
|
}
|
|
1267
1388
|
}
|
|
1389
|
+
for (const id of expiredForwarded) {
|
|
1390
|
+
forwardedRequestMethodsById.delete(id);
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
const expiredSanitized = [];
|
|
1268
1394
|
for (const [requestId, trackedRequest] of relaySanitizedResponseMethodsById.entries()) {
|
|
1269
1395
|
if (!trackedRequest || (now - trackedRequest.createdAt) >= forwardedRequestMethodTTLms) {
|
|
1270
|
-
|
|
1396
|
+
expiredSanitized.push(requestId);
|
|
1271
1397
|
}
|
|
1272
1398
|
}
|
|
1399
|
+
for (const id of expiredSanitized) {
|
|
1400
|
+
relaySanitizedResponseMethodsById.delete(id);
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
evictOldestEntries(forwardedRequestMethodsById, FORWARDED_REQUEST_METHODS_MAX_SIZE);
|
|
1404
|
+
evictOldestEntries(relaySanitizedResponseMethodsById, FORWARDED_REQUEST_METHODS_MAX_SIZE);
|
|
1405
|
+
evictOldestEntries(jsonlArtifactItemsCacheByThread, RELAY_JSONL_ARTIFACT_CACHE_MAX_ENTRIES);
|
|
1406
|
+
evictOldestEntries(jsonlTurnsListRolloutCacheByThread, JSONL_ROLLOUT_PATH_CACHE_MAX_SIZE);
|
|
1407
|
+
evictOldestEntries(jsonlTurnsListRolloutMissCacheByThread, JSONL_ROLLOUT_PATH_CACHE_MAX_SIZE);
|
|
1273
1408
|
}
|
|
1274
1409
|
|
|
1275
1410
|
function safeParseJSON(value) {
|
|
@@ -1406,6 +1541,20 @@ function startBridge({
|
|
|
1406
1541
|
function bridgeManagedInitializeCompatibilityError(params) {
|
|
1407
1542
|
const clientInfo = params && typeof params === "object" ? params.clientInfo : null;
|
|
1408
1543
|
const clientName = normalizeNonEmptyString(clientInfo?.name);
|
|
1544
|
+
const clientDeviceKind = classifyClientDeviceKind(clientName);
|
|
1545
|
+
if (clientDeviceKind) {
|
|
1546
|
+
deviceState = rememberLastSeenClientDeviceKind(deviceState, clientDeviceKind);
|
|
1547
|
+
if (activePhoneSummary?.connected) {
|
|
1548
|
+
activePhoneSummary = {
|
|
1549
|
+
...activePhoneSummary,
|
|
1550
|
+
deviceKind: clientDeviceKind,
|
|
1551
|
+
};
|
|
1552
|
+
const lastPublishedBridgeStatus = bridgeStatusPublisher.latest();
|
|
1553
|
+
if (lastPublishedBridgeStatus) {
|
|
1554
|
+
publishBridgeStatus(lastPublishedBridgeStatus);
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1409
1558
|
if (clientName !== "codexmobile_ios") {
|
|
1410
1559
|
return null;
|
|
1411
1560
|
}
|
|
@@ -1574,7 +1723,11 @@ function startBridge({
|
|
|
1574
1723
|
}
|
|
1575
1724
|
|
|
1576
1725
|
function publishBridgeStatus(status) {
|
|
1577
|
-
bridgeStatusPublisher.publish(
|
|
1726
|
+
bridgeStatusPublisher.publish({
|
|
1727
|
+
...status,
|
|
1728
|
+
activeDevice: activePhoneSummary,
|
|
1729
|
+
activePhone: activePhoneSummary,
|
|
1730
|
+
});
|
|
1578
1731
|
}
|
|
1579
1732
|
|
|
1580
1733
|
// Refreshes the relay's trusted-mac index after the QR bootstrap locks in a phone identity.
|
|
@@ -1629,6 +1782,64 @@ function startBridge({
|
|
|
1629
1782
|
|
|
1630
1783
|
return readBridgePreferences();
|
|
1631
1784
|
}
|
|
1785
|
+
|
|
1786
|
+
async function updateBridgePackageAndRestart() {
|
|
1787
|
+
if (process.platform !== "darwin") {
|
|
1788
|
+
const error = new Error("Bridge self-update is available only for the macOS bridge service.");
|
|
1789
|
+
error.errorCode = "unsupported_platform";
|
|
1790
|
+
error.userMessage = error.message;
|
|
1791
|
+
throw error;
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1794
|
+
try {
|
|
1795
|
+
await execFileAsync("/bin/zsh", [
|
|
1796
|
+
"-lc",
|
|
1797
|
+
[
|
|
1798
|
+
"export TERM=dumb",
|
|
1799
|
+
"source ~/.zshrc >/dev/null 2>/dev/null || true",
|
|
1800
|
+
BRIDGE_PACKAGE_UPDATE_COMMAND,
|
|
1801
|
+
].join("; "),
|
|
1802
|
+
], {
|
|
1803
|
+
timeout: BRIDGE_PACKAGE_UPDATE_TIMEOUT_MS,
|
|
1804
|
+
maxBuffer: 2 * 1024 * 1024,
|
|
1805
|
+
});
|
|
1806
|
+
} catch (error) {
|
|
1807
|
+
const nextError = new Error(
|
|
1808
|
+
truncateCommandOutput(error?.stderr || error?.stdout || error?.message)
|
|
1809
|
+
|| "Could not update the Remodex bridge package on this Mac."
|
|
1810
|
+
);
|
|
1811
|
+
nextError.errorCode = "bridge_update_failed";
|
|
1812
|
+
nextError.userMessage = nextError.message;
|
|
1813
|
+
nextError.cause = error;
|
|
1814
|
+
throw nextError;
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
scheduleBridgeServiceRestartAfterUpdate();
|
|
1818
|
+
return {
|
|
1819
|
+
success: true,
|
|
1820
|
+
command: BRIDGE_PACKAGE_UPDATE_COMMAND,
|
|
1821
|
+
restartScheduled: true,
|
|
1822
|
+
restartDelayMs: BRIDGE_RESTART_AFTER_UPDATE_DELAY_MS,
|
|
1823
|
+
};
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
// Restarts after the RPC response has crossed the encrypted phone channel.
|
|
1827
|
+
function scheduleBridgeServiceRestartAfterUpdate() {
|
|
1828
|
+
const restartTimer = setTimeout(() => {
|
|
1829
|
+
const cliPath = path.join(__dirname, "..", "bin", "remodex.js");
|
|
1830
|
+
const child = spawn(process.execPath, [cliPath, "restart"], {
|
|
1831
|
+
detached: true,
|
|
1832
|
+
stdio: "ignore",
|
|
1833
|
+
env: process.env,
|
|
1834
|
+
});
|
|
1835
|
+
child.unref?.();
|
|
1836
|
+
}, BRIDGE_RESTART_AFTER_UPDATE_DELAY_MS);
|
|
1837
|
+
restartTimer.unref?.();
|
|
1838
|
+
}
|
|
1839
|
+
|
|
1840
|
+
return {
|
|
1841
|
+
stop: stopBridge,
|
|
1842
|
+
};
|
|
1632
1843
|
}
|
|
1633
1844
|
|
|
1634
1845
|
// Holds a single macOS idle-sleep assertion for as long as the bridge process stays alive.
|
|
@@ -1769,6 +1980,47 @@ function buildMacRegistration(deviceState, pairingSession) {
|
|
|
1769
1980
|
};
|
|
1770
1981
|
}
|
|
1771
1982
|
|
|
1983
|
+
function buildActivePhoneSummary(session, deviceState = null) {
|
|
1984
|
+
const phoneFingerprint = shortFingerprint(session?.phoneDeviceId);
|
|
1985
|
+
if (!phoneFingerprint) {
|
|
1986
|
+
return null;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
return {
|
|
1990
|
+
connected: true,
|
|
1991
|
+
phoneFingerprint,
|
|
1992
|
+
deviceKind: normalizeNonEmptyString(deviceState?.lastSeenDeviceKind) || null,
|
|
1993
|
+
handshakeMode: normalizeNonEmptyString(session?.handshakeMode) || null,
|
|
1994
|
+
keyEpoch: Number.isFinite(session?.keyEpoch) ? session.keyEpoch : null,
|
|
1995
|
+
updatedAt: new Date().toISOString(),
|
|
1996
|
+
};
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
function classifyClientDeviceKind(clientName) {
|
|
2000
|
+
const normalized = normalizeNonEmptyString(clientName).toLowerCase();
|
|
2001
|
+
if (!normalized) {
|
|
2002
|
+
return null;
|
|
2003
|
+
}
|
|
2004
|
+
if (normalized.includes("android")) {
|
|
2005
|
+
return "android";
|
|
2006
|
+
}
|
|
2007
|
+
if (normalized.includes("ios") || normalized.includes("iphone")) {
|
|
2008
|
+
return "iphone";
|
|
2009
|
+
}
|
|
2010
|
+
if (normalized.includes("macos") || normalized.includes("mac")) {
|
|
2011
|
+
return "mac";
|
|
2012
|
+
}
|
|
2013
|
+
return null;
|
|
2014
|
+
}
|
|
2015
|
+
|
|
2016
|
+
function shortFingerprint(value) {
|
|
2017
|
+
const normalized = normalizeNonEmptyString(value);
|
|
2018
|
+
if (!normalized) {
|
|
2019
|
+
return null;
|
|
2020
|
+
}
|
|
2021
|
+
return createHash("sha256").update(normalized).digest("hex").slice(0, 8);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1772
2024
|
function shutdown(codex, getSocket, beforeExit = () => {}, { exitCode = 0 } = {}) {
|
|
1773
2025
|
beforeExit();
|
|
1774
2026
|
|
|
@@ -1935,6 +2187,14 @@ function normalizeNonEmptyString(value) {
|
|
|
1935
2187
|
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
1936
2188
|
}
|
|
1937
2189
|
|
|
2190
|
+
function truncateCommandOutput(value, maxChars = 1_200) {
|
|
2191
|
+
const normalized = normalizeNonEmptyString(value);
|
|
2192
|
+
if (!normalized || normalized.length <= maxChars) {
|
|
2193
|
+
return normalized;
|
|
2194
|
+
}
|
|
2195
|
+
return `...${normalized.slice(-maxChars)}`;
|
|
2196
|
+
}
|
|
2197
|
+
|
|
1938
2198
|
function parseAdaptiveThreadTurnsListRequest(rawMessage) {
|
|
1939
2199
|
const parsed = parseBridgeJSON(rawMessage);
|
|
1940
2200
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
@@ -1961,6 +2221,22 @@ function parseAdaptiveThreadTurnsListRequest(rawMessage) {
|
|
|
1961
2221
|
return parsed;
|
|
1962
2222
|
}
|
|
1963
2223
|
|
|
2224
|
+
function threadIdFromRequestParams(params) {
|
|
2225
|
+
return normalizeNonEmptyString(params?.threadId)
|
|
2226
|
+
|| normalizeNonEmptyString(params?.thread_id)
|
|
2227
|
+
|| normalizeNonEmptyString(params?.id)
|
|
2228
|
+
|| "";
|
|
2229
|
+
}
|
|
2230
|
+
|
|
2231
|
+
function buildThreadTurnsListRelaySanitizeContext(request, {
|
|
2232
|
+
skipJsonlArtifactAugmentation = false,
|
|
2233
|
+
} = {}) {
|
|
2234
|
+
return {
|
|
2235
|
+
threadId: threadIdFromRequestParams(request?.params || {}),
|
|
2236
|
+
skipJsonlArtifactAugmentation,
|
|
2237
|
+
};
|
|
2238
|
+
}
|
|
2239
|
+
|
|
1964
2240
|
async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
1965
2241
|
fetchPage,
|
|
1966
2242
|
now = Date.now,
|
|
@@ -1978,6 +2254,9 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
1978
2254
|
const requestedLimit = Number.isInteger(params?.limit) && params.limit > 0
|
|
1979
2255
|
? Math.min(params.limit, RELAY_TURNS_LIST_MAX_INITIAL_LIMIT)
|
|
1980
2256
|
: 1;
|
|
2257
|
+
const sanitizeContext = buildThreadTurnsListRelaySanitizeContext(request, {
|
|
2258
|
+
skipJsonlArtifactAugmentation: true,
|
|
2259
|
+
});
|
|
1981
2260
|
const startedAt = now();
|
|
1982
2261
|
let nextCursor = params?.cursor;
|
|
1983
2262
|
let turnsKey = null;
|
|
@@ -2002,6 +2281,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
2002
2281
|
fetchPage,
|
|
2003
2282
|
now,
|
|
2004
2283
|
sanitizeForRelay,
|
|
2284
|
+
sanitizeContext,
|
|
2005
2285
|
payloadSoftLimitBytes,
|
|
2006
2286
|
});
|
|
2007
2287
|
}
|
|
@@ -2014,6 +2294,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
2014
2294
|
fetchPage,
|
|
2015
2295
|
now,
|
|
2016
2296
|
sanitizeForRelay,
|
|
2297
|
+
sanitizeContext,
|
|
2017
2298
|
payloadSoftLimitBytes,
|
|
2018
2299
|
});
|
|
2019
2300
|
}
|
|
@@ -2032,7 +2313,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
2032
2313
|
combinedTurns = combinedTurns.concat(pageTurns);
|
|
2033
2314
|
response = buildSafeTurnsListResponse(request.id, firstResult, lastResult, turnsKey, combinedTurns);
|
|
2034
2315
|
|
|
2035
|
-
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) >= payloadSoftLimitBytes) {
|
|
2316
|
+
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, sanitizeContext) >= payloadSoftLimitBytes) {
|
|
2036
2317
|
response = buildLargestSafeTurnsListResponse({
|
|
2037
2318
|
requestId: request.id,
|
|
2038
2319
|
firstResult,
|
|
@@ -2041,6 +2322,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
2041
2322
|
turns: combinedTurns,
|
|
2042
2323
|
maxTurns: RELAY_TURNS_LIST_SAFE_RETRY_LIMIT,
|
|
2043
2324
|
sanitizeForRelay,
|
|
2325
|
+
sanitizeContext,
|
|
2044
2326
|
payloadSoftLimitBytes,
|
|
2045
2327
|
}) ?? buildEmptyTurnsListResponse(request);
|
|
2046
2328
|
break;
|
|
@@ -2052,7 +2334,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
2052
2334
|
}
|
|
2053
2335
|
|
|
2054
2336
|
const rawPageBytes = jsonByteLength(pageResult);
|
|
2055
|
-
const sanitizedResponseBytes = measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay);
|
|
2337
|
+
const sanitizedResponseBytes = measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, sanitizeContext);
|
|
2056
2338
|
const elapsedMs = Math.max(0, now() - startedAt);
|
|
2057
2339
|
const remainingBudgetMs = Math.max(0, targetBudgetMs - elapsedMs);
|
|
2058
2340
|
if (
|
|
@@ -2088,10 +2370,87 @@ function isEmptyTurnsListResponse(response) {
|
|
|
2088
2370
|
return Boolean(turnsKey) && response.result[turnsKey].length === 0;
|
|
2089
2371
|
}
|
|
2090
2372
|
|
|
2373
|
+
// Non-empty app-server pages can be stale for Mac-started runs, so the first page
|
|
2374
|
+
// still gets one JSONL lookup when the positive rollout cache is cold.
|
|
2375
|
+
function resolveJsonlTurnsListRolloutPathForFallback({
|
|
2376
|
+
threadId,
|
|
2377
|
+
responseIsEmpty,
|
|
2378
|
+
readCachedPath,
|
|
2379
|
+
findAndCachePath,
|
|
2380
|
+
}) {
|
|
2381
|
+
if (!threadId || typeof findAndCachePath !== "function") {
|
|
2382
|
+
return "";
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
if (responseIsEmpty) {
|
|
2386
|
+
return findAndCachePath(threadId);
|
|
2387
|
+
}
|
|
2388
|
+
|
|
2389
|
+
return typeof readCachedPath === "function"
|
|
2390
|
+
? readCachedPath(threadId) || findAndCachePath(threadId)
|
|
2391
|
+
: findAndCachePath(threadId);
|
|
2392
|
+
}
|
|
2393
|
+
|
|
2394
|
+
function maybeMergeLatestJsonlTurnIntoTurnsListResponse(request, response, jsonlResult, params = {}) {
|
|
2395
|
+
const responseResult = response?.result;
|
|
2396
|
+
const responseTurnsKey = findTurnsListResultKey(responseResult);
|
|
2397
|
+
const jsonlTurnsKey = findTurnsListResultKey(jsonlResult);
|
|
2398
|
+
if (!responseTurnsKey || !jsonlTurnsKey) {
|
|
2399
|
+
return null;
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
const responseTurns = responseResult[responseTurnsKey];
|
|
2403
|
+
const jsonlTurn = jsonlResult[jsonlTurnsKey]?.[0];
|
|
2404
|
+
const jsonlTurnId = turnListTurnIdentifier(jsonlTurn);
|
|
2405
|
+
if (!jsonlTurnId || responseTurns.some((turn) => turnListTurnIdentifier(turn) === jsonlTurnId)) {
|
|
2406
|
+
return null;
|
|
2407
|
+
}
|
|
2408
|
+
|
|
2409
|
+
if (!shouldMergeLatestJsonlTurn(jsonlTurn)) {
|
|
2410
|
+
return null;
|
|
2411
|
+
}
|
|
2412
|
+
|
|
2413
|
+
const requestedLimit = Number.isInteger(params?.limit) && params.limit > 0
|
|
2414
|
+
? params.limit
|
|
2415
|
+
: responseTurns.length + 1;
|
|
2416
|
+
const mergedTurns = [jsonlTurn, ...responseTurns].slice(0, requestedLimit);
|
|
2417
|
+
return {
|
|
2418
|
+
id: request.id,
|
|
2419
|
+
result: {
|
|
2420
|
+
...responseResult,
|
|
2421
|
+
[responseTurnsKey]: mergedTurns,
|
|
2422
|
+
remodexJsonlMergedLatest: true,
|
|
2423
|
+
},
|
|
2424
|
+
};
|
|
2425
|
+
}
|
|
2426
|
+
|
|
2427
|
+
function shouldMergeLatestJsonlTurn(turn) {
|
|
2428
|
+
if (!turn || typeof turn !== "object") {
|
|
2429
|
+
return false;
|
|
2430
|
+
}
|
|
2431
|
+
|
|
2432
|
+
const status = normalizeHistoryItemToken(turn.status);
|
|
2433
|
+
if (status === "running" || status === "inprogress" || status === "active") {
|
|
2434
|
+
return true;
|
|
2435
|
+
}
|
|
2436
|
+
|
|
2437
|
+
return Array.isArray(turn.items) && turn.items.some((item) => {
|
|
2438
|
+
const type = normalizeHistoryItemToken(item?.type);
|
|
2439
|
+
return type === "plan" || type === "filechange";
|
|
2440
|
+
});
|
|
2441
|
+
}
|
|
2442
|
+
|
|
2443
|
+
function turnListTurnIdentifier(turn) {
|
|
2444
|
+
return normalizeNonEmptyString(turn?.id)
|
|
2445
|
+
|| normalizeNonEmptyString(turn?.turnId)
|
|
2446
|
+
|| normalizeNonEmptyString(turn?.turn_id);
|
|
2447
|
+
}
|
|
2448
|
+
|
|
2091
2449
|
async function fetchSafeThreadTurnsListFallback(request, {
|
|
2092
2450
|
fetchPage,
|
|
2093
2451
|
now,
|
|
2094
2452
|
sanitizeForRelay,
|
|
2453
|
+
sanitizeContext = {},
|
|
2095
2454
|
payloadSoftLimitBytes,
|
|
2096
2455
|
}) {
|
|
2097
2456
|
const params = request?.params;
|
|
@@ -2119,6 +2478,7 @@ async function fetchSafeThreadTurnsListFallback(request, {
|
|
|
2119
2478
|
turns: pageResult[turnsKey],
|
|
2120
2479
|
maxTurns: safeLimit,
|
|
2121
2480
|
sanitizeForRelay,
|
|
2481
|
+
sanitizeContext,
|
|
2122
2482
|
payloadSoftLimitBytes,
|
|
2123
2483
|
});
|
|
2124
2484
|
if (response) {
|
|
@@ -2187,6 +2547,7 @@ function buildLargestSafeTurnsListResponse({
|
|
|
2187
2547
|
turns,
|
|
2188
2548
|
maxTurns,
|
|
2189
2549
|
sanitizeForRelay,
|
|
2550
|
+
sanitizeContext = {},
|
|
2190
2551
|
payloadSoftLimitBytes,
|
|
2191
2552
|
}) {
|
|
2192
2553
|
const sliceLimit = Math.min(turns.length, maxTurns);
|
|
@@ -2198,7 +2559,7 @@ function buildLargestSafeTurnsListResponse({
|
|
|
2198
2559
|
turnsKey,
|
|
2199
2560
|
turns.slice(0, count)
|
|
2200
2561
|
);
|
|
2201
|
-
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) < payloadSoftLimitBytes) {
|
|
2562
|
+
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, sanitizeContext) < payloadSoftLimitBytes) {
|
|
2202
2563
|
return response;
|
|
2203
2564
|
}
|
|
2204
2565
|
}
|
|
@@ -2208,6 +2569,7 @@ function buildLargestSafeTurnsListResponse({
|
|
|
2208
2569
|
turnsKey,
|
|
2209
2570
|
turn: turns[0],
|
|
2210
2571
|
sanitizeForRelay,
|
|
2572
|
+
sanitizeContext,
|
|
2211
2573
|
payloadSoftLimitBytes,
|
|
2212
2574
|
});
|
|
2213
2575
|
}
|
|
@@ -2218,6 +2580,7 @@ function buildEmergencySingleTurnResponse({
|
|
|
2218
2580
|
turnsKey,
|
|
2219
2581
|
turn,
|
|
2220
2582
|
sanitizeForRelay,
|
|
2583
|
+
sanitizeContext = {},
|
|
2221
2584
|
payloadSoftLimitBytes,
|
|
2222
2585
|
}) {
|
|
2223
2586
|
if (!turn || typeof turn !== "object" || Array.isArray(turn)) {
|
|
@@ -2240,7 +2603,7 @@ function buildEmergencySingleTurnResponse({
|
|
|
2240
2603
|
remodexEmergencySingleTurnForRelay: true,
|
|
2241
2604
|
},
|
|
2242
2605
|
};
|
|
2243
|
-
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) < payloadSoftLimitBytes) {
|
|
2606
|
+
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, sanitizeContext) < payloadSoftLimitBytes) {
|
|
2244
2607
|
return response;
|
|
2245
2608
|
}
|
|
2246
2609
|
}
|
|
@@ -2261,6 +2624,10 @@ function compactEmergencySingleTurnForRelay(turn, maxChars, maxItems) {
|
|
|
2261
2624
|
"created_at",
|
|
2262
2625
|
"completedAt",
|
|
2263
2626
|
"completed_at",
|
|
2627
|
+
"timeZoneIdentifier",
|
|
2628
|
+
"timeZone",
|
|
2629
|
+
"timezone",
|
|
2630
|
+
"time_zone",
|
|
2264
2631
|
"status",
|
|
2265
2632
|
"role",
|
|
2266
2633
|
"kind",
|
|
@@ -2318,10 +2685,10 @@ function jsonByteLength(value) {
|
|
|
2318
2685
|
}
|
|
2319
2686
|
}
|
|
2320
2687
|
|
|
2321
|
-
function measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) {
|
|
2688
|
+
function measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, requestContext = {}) {
|
|
2322
2689
|
try {
|
|
2323
2690
|
const rawResponse = JSON.stringify(response);
|
|
2324
|
-
const sanitizedResponse = sanitizeForRelay(rawResponse, "thread/turns/list");
|
|
2691
|
+
const sanitizedResponse = sanitizeForRelay(rawResponse, "thread/turns/list", requestContext);
|
|
2325
2692
|
return Buffer.byteLength(sanitizedResponse, "utf8");
|
|
2326
2693
|
} catch {
|
|
2327
2694
|
return Number.POSITIVE_INFINITY;
|
|
@@ -2432,9 +2799,9 @@ function isRelayBoundServerRequestMethod(method) {
|
|
|
2432
2799
|
|
|
2433
2800
|
// Shrinks thread history snapshots/pages for mobile relay delivery.
|
|
2434
2801
|
// This elides bulky blobs and replaces oversized older history with a compact marker.
|
|
2435
|
-
function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
|
|
2802
|
+
function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod, requestContext = {}) {
|
|
2436
2803
|
if (requestMethod === "thread/turns/list") {
|
|
2437
|
-
return sanitizeThreadTurnsListForRelay(rawMessage);
|
|
2804
|
+
return sanitizeThreadTurnsListForRelay(rawMessage, requestContext);
|
|
2438
2805
|
}
|
|
2439
2806
|
|
|
2440
2807
|
if (requestMethod !== "thread/read" && requestMethod !== "thread/resume") {
|
|
@@ -2447,12 +2814,15 @@ function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
|
|
|
2447
2814
|
return rawMessage;
|
|
2448
2815
|
}
|
|
2449
2816
|
|
|
2450
|
-
const threadId = normalizeNonEmptyString(
|
|
2817
|
+
const threadId = normalizeNonEmptyString(requestContext?.threadId)
|
|
2818
|
+
|| normalizeNonEmptyString(thread.id)
|
|
2451
2819
|
|| normalizeNonEmptyString(thread.threadId)
|
|
2452
2820
|
|| normalizeNonEmptyString(thread.thread_id);
|
|
2453
2821
|
const { turns: sanitizedTurns, didSanitize } = sanitizeRelayHistoryTurns(thread.turns, threadId);
|
|
2822
|
+
const { thread: threadWithJsonlMetadata, didAugment: didAugmentThreadMetadata } = augmentRelayThreadWithJsonlMetadata(thread, threadId);
|
|
2823
|
+
const { turns: augmentedTurns, didAugment } = augmentRelayHistoryTurnsWithJsonlArtifacts(sanitizedTurns, threadId);
|
|
2454
2824
|
|
|
2455
|
-
if (!didSanitize) {
|
|
2825
|
+
if (!didSanitize && !didAugment && !didAugmentThreadMetadata) {
|
|
2456
2826
|
const trimmedPayload = trimThreadPayloadForRelay(parsed, thread);
|
|
2457
2827
|
return trimmedPayload == null ? rawMessage : trimmedPayload;
|
|
2458
2828
|
}
|
|
@@ -2462,8 +2832,8 @@ function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
|
|
|
2462
2832
|
result: {
|
|
2463
2833
|
...parsed.result,
|
|
2464
2834
|
thread: {
|
|
2465
|
-
...
|
|
2466
|
-
turns:
|
|
2835
|
+
...threadWithJsonlMetadata,
|
|
2836
|
+
turns: augmentedTurns,
|
|
2467
2837
|
},
|
|
2468
2838
|
},
|
|
2469
2839
|
});
|
|
@@ -2471,7 +2841,7 @@ function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
|
|
|
2471
2841
|
return trimThreadPayloadForRelay(parseBridgeJSON(sanitizedPayload), null) ?? sanitizedPayload;
|
|
2472
2842
|
}
|
|
2473
2843
|
|
|
2474
|
-
function sanitizeThreadTurnsListForRelay(rawMessage) {
|
|
2844
|
+
function sanitizeThreadTurnsListForRelay(rawMessage, requestContext = {}) {
|
|
2475
2845
|
const parsed = parseBridgeJSON(rawMessage);
|
|
2476
2846
|
const result = parsed?.result;
|
|
2477
2847
|
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
@@ -2483,23 +2853,372 @@ function sanitizeThreadTurnsListForRelay(rawMessage) {
|
|
|
2483
2853
|
return rawMessage;
|
|
2484
2854
|
}
|
|
2485
2855
|
|
|
2486
|
-
const threadId = normalizeNonEmptyString(
|
|
2856
|
+
const threadId = normalizeNonEmptyString(requestContext?.threadId)
|
|
2857
|
+
|| normalizeNonEmptyString(result.threadId)
|
|
2487
2858
|
|| normalizeNonEmptyString(result.thread_id)
|
|
2488
2859
|
|| normalizeNonEmptyString(result.thread?.id)
|
|
2489
2860
|
|| normalizeNonEmptyString(result.thread?.threadId)
|
|
2490
|
-
|| normalizeNonEmptyString(result.thread?.thread_id)
|
|
2861
|
+
|| normalizeNonEmptyString(result.thread?.thread_id)
|
|
2862
|
+
|| inferThreadIdFromTurns(result[turnsKey]);
|
|
2491
2863
|
const { turns: sanitizedTurns, didSanitize } = sanitizeRelayHistoryTurns(result[turnsKey], threadId);
|
|
2492
|
-
const
|
|
2864
|
+
const shouldAugmentJsonlArtifacts = requestContext?.skipJsonlArtifactAugmentation !== true;
|
|
2865
|
+
const { turns: augmentedTurns, didAugment } = shouldAugmentJsonlArtifacts
|
|
2866
|
+
? augmentRelayHistoryTurnsWithJsonlArtifacts(sanitizedTurns, threadId)
|
|
2867
|
+
: { turns: sanitizedTurns, didAugment: false };
|
|
2868
|
+
const didChange = didSanitize || didAugment;
|
|
2869
|
+
const sanitizedParsed = didChange
|
|
2493
2870
|
? {
|
|
2494
2871
|
...parsed,
|
|
2495
2872
|
result: {
|
|
2496
2873
|
...result,
|
|
2497
|
-
[turnsKey]:
|
|
2874
|
+
[turnsKey]: augmentedTurns,
|
|
2498
2875
|
},
|
|
2499
2876
|
}
|
|
2500
2877
|
: parsed;
|
|
2501
2878
|
|
|
2502
|
-
return trimTurnsListPayloadForRelay(sanitizedParsed, turnsKey,
|
|
2879
|
+
return trimTurnsListPayloadForRelay(sanitizedParsed, turnsKey, didChange ? null : rawMessage);
|
|
2880
|
+
}
|
|
2881
|
+
|
|
2882
|
+
function augmentRelayThreadWithJsonlMetadata(thread, threadId = "") {
|
|
2883
|
+
const cwd = readJsonlThreadCwd(threadId);
|
|
2884
|
+
if (!cwd || !thread || typeof thread !== "object") {
|
|
2885
|
+
return { thread, didAugment: false };
|
|
2886
|
+
}
|
|
2887
|
+
|
|
2888
|
+
if (normalizeNonEmptyString(thread.cwd) === cwd
|
|
2889
|
+
&& normalizeNonEmptyString(thread.current_working_directory) === cwd) {
|
|
2890
|
+
return { thread, didAugment: false };
|
|
2891
|
+
}
|
|
2892
|
+
|
|
2893
|
+
return {
|
|
2894
|
+
thread: {
|
|
2895
|
+
...thread,
|
|
2896
|
+
cwd,
|
|
2897
|
+
current_working_directory: cwd,
|
|
2898
|
+
},
|
|
2899
|
+
didAugment: true,
|
|
2900
|
+
};
|
|
2901
|
+
}
|
|
2902
|
+
|
|
2903
|
+
function readJsonlThreadCwd(threadId) {
|
|
2904
|
+
const normalizedThreadId = normalizeNonEmptyString(threadId);
|
|
2905
|
+
if (!normalizedThreadId) {
|
|
2906
|
+
return "";
|
|
2907
|
+
}
|
|
2908
|
+
|
|
2909
|
+
try {
|
|
2910
|
+
const rolloutPath = findRecentRolloutFileForContextRead(resolveSessionsRoot(), { threadId: normalizedThreadId });
|
|
2911
|
+
if (!rolloutPath) {
|
|
2912
|
+
return "";
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
const metadata = parseSessionJsonlMetadata(fs.readFileSync(rolloutPath, "utf8"));
|
|
2916
|
+
const cwd = normalizeNonEmptyString(metadata?.cwd);
|
|
2917
|
+
return cwd && path.isAbsolute(cwd) ? cwd : "";
|
|
2918
|
+
} catch {
|
|
2919
|
+
return "";
|
|
2920
|
+
}
|
|
2921
|
+
}
|
|
2922
|
+
|
|
2923
|
+
function augmentRelayHistoryTurnsWithJsonlArtifacts(turns, threadId = "") {
|
|
2924
|
+
const normalizedThreadId = normalizeNonEmptyString(threadId);
|
|
2925
|
+
if (!normalizedThreadId || !Array.isArray(turns) || turns.length === 0) {
|
|
2926
|
+
return { turns, didAugment: false };
|
|
2927
|
+
}
|
|
2928
|
+
|
|
2929
|
+
const jsonlArtifactsByTurnId = readJsonlArtifactItemsByTurnId(normalizedThreadId);
|
|
2930
|
+
if (jsonlArtifactsByTurnId.size === 0) {
|
|
2931
|
+
return { turns, didAugment: false };
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
let didAugment = false;
|
|
2935
|
+
const augmentedTurns = turns.map((turn) => {
|
|
2936
|
+
const turnId = normalizeNonEmptyString(turn?.id)
|
|
2937
|
+
|| normalizeNonEmptyString(turn?.turnId)
|
|
2938
|
+
|| normalizeNonEmptyString(turn?.turn_id);
|
|
2939
|
+
const artifacts = turnId ? jsonlArtifactsByTurnId.get(turnId) : null;
|
|
2940
|
+
if (!artifacts || !turn || typeof turn !== "object") {
|
|
2941
|
+
return turn;
|
|
2942
|
+
}
|
|
2943
|
+
|
|
2944
|
+
const items = Array.isArray(turn.items) ? turn.items : [];
|
|
2945
|
+
let nextItems = items;
|
|
2946
|
+
if (artifacts.fileChangeItem && !hasEquivalentFileChangeItem(nextItems, artifacts.fileChangeItem)) {
|
|
2947
|
+
nextItems = nextItems === items ? [...items] : nextItems;
|
|
2948
|
+
nextItems.push(artifacts.fileChangeItem);
|
|
2949
|
+
}
|
|
2950
|
+
for (const imageViewItem of artifacts.imageViewItems || []) {
|
|
2951
|
+
if (hasEquivalentImageViewItem(nextItems, imageViewItem)) {
|
|
2952
|
+
continue;
|
|
2953
|
+
}
|
|
2954
|
+
nextItems = nextItems === items ? [...items] : nextItems;
|
|
2955
|
+
nextItems.push(imageViewItem);
|
|
2956
|
+
}
|
|
2957
|
+
if (artifacts.progressPlanItem && !hasEquivalentProgressPlanItem(nextItems, artifacts.progressPlanItem)) {
|
|
2958
|
+
nextItems = nextItems === items ? [...items] : nextItems;
|
|
2959
|
+
nextItems.push(artifacts.progressPlanItem);
|
|
2960
|
+
}
|
|
2961
|
+
|
|
2962
|
+
if (nextItems === items) {
|
|
2963
|
+
return turn;
|
|
2964
|
+
}
|
|
2965
|
+
|
|
2966
|
+
didAugment = true;
|
|
2967
|
+
return {
|
|
2968
|
+
...turn,
|
|
2969
|
+
items: nextItems,
|
|
2970
|
+
};
|
|
2971
|
+
});
|
|
2972
|
+
|
|
2973
|
+
return { turns: didAugment ? augmentedTurns : turns, didAugment };
|
|
2974
|
+
}
|
|
2975
|
+
|
|
2976
|
+
function readJsonlArtifactItemsByTurnId(threadId) {
|
|
2977
|
+
const emptyArtifactsByTurnId = new Map();
|
|
2978
|
+
const normalizedThreadId = normalizeNonEmptyString(threadId);
|
|
2979
|
+
if (!normalizedThreadId) {
|
|
2980
|
+
return emptyArtifactsByTurnId;
|
|
2981
|
+
}
|
|
2982
|
+
|
|
2983
|
+
const sessionsRoot = resolveSessionsRoot();
|
|
2984
|
+
const cacheKey = buildJsonlArtifactItemsCacheKey(sessionsRoot, normalizedThreadId);
|
|
2985
|
+
const cachedArtifacts = readCachedJsonlArtifactItems(cacheKey, normalizedThreadId);
|
|
2986
|
+
if (cachedArtifacts) {
|
|
2987
|
+
return cachedArtifacts;
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2990
|
+
try {
|
|
2991
|
+
const rolloutPath = findRecentRolloutFileForContextRead(sessionsRoot, { threadId: normalizedThreadId });
|
|
2992
|
+
if (!rolloutPath) {
|
|
2993
|
+
jsonlArtifactItemsCacheByThread.delete(cacheKey);
|
|
2994
|
+
return emptyArtifactsByTurnId;
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
return readAndCacheJsonlArtifactItems(cacheKey, rolloutPath, normalizedThreadId);
|
|
2998
|
+
} catch (error) {
|
|
2999
|
+
jsonlArtifactItemsCacheByThread.delete(cacheKey);
|
|
3000
|
+
console.warn(`[remodex] history jsonl artifact augmentation failed for ${normalizedThreadId}: ${error.message}`);
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
return emptyArtifactsByTurnId;
|
|
3004
|
+
}
|
|
3005
|
+
|
|
3006
|
+
function buildJsonlArtifactItemsCacheKey(sessionsRoot, threadId) {
|
|
3007
|
+
return `${sessionsRoot}\0${threadId}`;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
function readCachedJsonlArtifactItems(cacheKey, threadId) {
|
|
3011
|
+
const cached = jsonlArtifactItemsCacheByThread.get(cacheKey);
|
|
3012
|
+
if (!cached) {
|
|
3013
|
+
return null;
|
|
3014
|
+
}
|
|
3015
|
+
|
|
3016
|
+
const stat = statJsonlArtifactRollout(cached.rolloutPath);
|
|
3017
|
+
if (!stat) {
|
|
3018
|
+
jsonlArtifactItemsCacheByThread.delete(cacheKey);
|
|
3019
|
+
return null;
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
if (stat.mtimeMs !== cached.mtimeMs || stat.size !== cached.size) {
|
|
3023
|
+
try {
|
|
3024
|
+
return readAndCacheJsonlArtifactItems(cacheKey, cached.rolloutPath, threadId, stat);
|
|
3025
|
+
} catch (error) {
|
|
3026
|
+
jsonlArtifactItemsCacheByThread.delete(cacheKey);
|
|
3027
|
+
console.warn(`[remodex] history jsonl artifact cache refresh failed for ${threadId}: ${error.message}`);
|
|
3028
|
+
return null;
|
|
3029
|
+
}
|
|
3030
|
+
}
|
|
3031
|
+
|
|
3032
|
+
const now = Date.now();
|
|
3033
|
+
if (now - cached.checkedAt <= RELAY_JSONL_ARTIFACT_CACHE_TTL_MS) {
|
|
3034
|
+
return cached.artifactsByTurnId;
|
|
3035
|
+
}
|
|
3036
|
+
|
|
3037
|
+
cached.checkedAt = now;
|
|
3038
|
+
return null;
|
|
3039
|
+
}
|
|
3040
|
+
|
|
3041
|
+
function readAndCacheJsonlArtifactItems(cacheKey, rolloutPath, threadId, stat = null) {
|
|
3042
|
+
const rolloutStat = stat || fs.statSync(rolloutPath);
|
|
3043
|
+
const artifactsByTurnId = new Map();
|
|
3044
|
+
try {
|
|
3045
|
+
const turns = parseSessionJsonlTurns(fs.readFileSync(rolloutPath, "utf8"), { threadId });
|
|
3046
|
+
for (const turn of turns) {
|
|
3047
|
+
const turnId = normalizeNonEmptyString(turn?.id);
|
|
3048
|
+
const turnItems = Array.isArray(turn?.items) ? turn.items : [];
|
|
3049
|
+
if (!turnId || turnItems.length === 0) {
|
|
3050
|
+
continue;
|
|
3051
|
+
}
|
|
3052
|
+
|
|
3053
|
+
const fileChanges = turnItems.filter((item) => normalizeHistoryItemToken(item?.type) === "filechange");
|
|
3054
|
+
const progressPlan = turnItems.find((item) => (
|
|
3055
|
+
normalizeHistoryItemToken(item?.type) === "plan"
|
|
3056
|
+
&& item?.remodexJsonlProgressPlan === true
|
|
3057
|
+
));
|
|
3058
|
+
const artifacts = {
|
|
3059
|
+
fileChangeItem: null,
|
|
3060
|
+
imageViewItems: [],
|
|
3061
|
+
progressPlanItem: null,
|
|
3062
|
+
};
|
|
3063
|
+
|
|
3064
|
+
const changes = [];
|
|
3065
|
+
for (const item of fileChanges) {
|
|
3066
|
+
if (Array.isArray(item.changes)) {
|
|
3067
|
+
changes.push(...item.changes);
|
|
3068
|
+
}
|
|
3069
|
+
}
|
|
3070
|
+
if (changes.length > 0) {
|
|
3071
|
+
artifacts.fileChangeItem = {
|
|
3072
|
+
id: `remodex-jsonl-file-change-${turnId}`,
|
|
3073
|
+
type: "fileChange",
|
|
3074
|
+
status: "completed",
|
|
3075
|
+
changes,
|
|
3076
|
+
remodexJsonlFileChangeAggregate: true,
|
|
3077
|
+
};
|
|
3078
|
+
}
|
|
3079
|
+
if (progressPlan) {
|
|
3080
|
+
artifacts.progressPlanItem = {
|
|
3081
|
+
...progressPlan,
|
|
3082
|
+
id: normalizeNonEmptyString(progressPlan.id) || `remodex-jsonl-progress-plan-${turnId}`,
|
|
3083
|
+
};
|
|
3084
|
+
}
|
|
3085
|
+
artifacts.imageViewItems = turnItems
|
|
3086
|
+
.filter((item) => normalizeHistoryItemToken(item?.type) === "imageview")
|
|
3087
|
+
.map((item, index) => ({
|
|
3088
|
+
...item,
|
|
3089
|
+
id: normalizeNonEmptyString(item.id) || `remodex-jsonl-image-view-${turnId}-${index + 1}`,
|
|
3090
|
+
}));
|
|
3091
|
+
|
|
3092
|
+
if (artifacts.fileChangeItem || artifacts.progressPlanItem || artifacts.imageViewItems.length > 0) {
|
|
3093
|
+
artifactsByTurnId.set(turnId, artifacts);
|
|
3094
|
+
}
|
|
3095
|
+
}
|
|
3096
|
+
} catch (error) {
|
|
3097
|
+
jsonlArtifactItemsCacheByThread.delete(cacheKey);
|
|
3098
|
+
throw error;
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
rememberJsonlArtifactItemsCache(cacheKey, {
|
|
3102
|
+
rolloutPath,
|
|
3103
|
+
mtimeMs: rolloutStat.mtimeMs,
|
|
3104
|
+
size: rolloutStat.size,
|
|
3105
|
+
checkedAt: Date.now(),
|
|
3106
|
+
artifactsByTurnId,
|
|
3107
|
+
});
|
|
3108
|
+
return artifactsByTurnId;
|
|
3109
|
+
}
|
|
3110
|
+
|
|
3111
|
+
function statJsonlArtifactRollout(rolloutPath) {
|
|
3112
|
+
try {
|
|
3113
|
+
return fs.statSync(rolloutPath);
|
|
3114
|
+
} catch {
|
|
3115
|
+
return null;
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
function rememberJsonlArtifactItemsCache(cacheKey, entry) {
|
|
3120
|
+
jsonlArtifactItemsCacheByThread.set(cacheKey, entry);
|
|
3121
|
+
while (jsonlArtifactItemsCacheByThread.size > RELAY_JSONL_ARTIFACT_CACHE_MAX_ENTRIES) {
|
|
3122
|
+
const oldestKey = jsonlArtifactItemsCacheByThread.keys().next().value;
|
|
3123
|
+
if (oldestKey == null) {
|
|
3124
|
+
break;
|
|
3125
|
+
}
|
|
3126
|
+
jsonlArtifactItemsCacheByThread.delete(oldestKey);
|
|
3127
|
+
}
|
|
3128
|
+
}
|
|
3129
|
+
|
|
3130
|
+
function hasEquivalentFileChangeItem(items, incomingItem) {
|
|
3131
|
+
const incomingId = normalizeNonEmptyString(incomingItem?.id);
|
|
3132
|
+
const incomingPaths = fileChangePathSet(incomingItem);
|
|
3133
|
+
return items.some((item) => {
|
|
3134
|
+
if (normalizeHistoryItemToken(item?.type) !== "filechange") {
|
|
3135
|
+
return false;
|
|
3136
|
+
}
|
|
3137
|
+
if (incomingId && normalizeNonEmptyString(item.id) === incomingId) {
|
|
3138
|
+
return true;
|
|
3139
|
+
}
|
|
3140
|
+
if (item.remodexJsonlFileChangeAggregate === true) {
|
|
3141
|
+
return true;
|
|
3142
|
+
}
|
|
3143
|
+
|
|
3144
|
+
const existingPaths = fileChangePathSet(item);
|
|
3145
|
+
if (incomingPaths.size === 0 || existingPaths.size === 0) {
|
|
3146
|
+
return false;
|
|
3147
|
+
}
|
|
3148
|
+
for (const pathKey of incomingPaths) {
|
|
3149
|
+
if (!existingPaths.has(pathKey)) {
|
|
3150
|
+
return false;
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
return true;
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
|
|
3157
|
+
function hasEquivalentProgressPlanItem(items, incomingItem) {
|
|
3158
|
+
const incomingId = normalizeNonEmptyString(incomingItem?.id);
|
|
3159
|
+
return items.some((item) => {
|
|
3160
|
+
if (normalizeHistoryItemToken(item?.type) !== "plan") {
|
|
3161
|
+
return false;
|
|
3162
|
+
}
|
|
3163
|
+
return item.remodexJsonlProgressPlan === true
|
|
3164
|
+
|| (incomingId && normalizeNonEmptyString(item.id) === incomingId);
|
|
3165
|
+
});
|
|
3166
|
+
}
|
|
3167
|
+
|
|
3168
|
+
function hasEquivalentImageViewItem(items, incomingItem) {
|
|
3169
|
+
const incomingId = normalizeNonEmptyString(incomingItem?.id);
|
|
3170
|
+
const incomingPath = normalizeImageViewPathKey(incomingItem);
|
|
3171
|
+
return items.some((item) => {
|
|
3172
|
+
if (normalizeHistoryItemToken(item?.type) !== "imageview") {
|
|
3173
|
+
return false;
|
|
3174
|
+
}
|
|
3175
|
+
const itemId = normalizeNonEmptyString(item.id);
|
|
3176
|
+
if (incomingId && itemId === incomingId) {
|
|
3177
|
+
return true;
|
|
3178
|
+
}
|
|
3179
|
+
return incomingPath && normalizeImageViewPathKey(item) === incomingPath;
|
|
3180
|
+
});
|
|
3181
|
+
}
|
|
3182
|
+
|
|
3183
|
+
function normalizeImageViewPathKey(item) {
|
|
3184
|
+
return normalizeNonEmptyString(item?.path)
|
|
3185
|
+
|| normalizeNonEmptyString(item?.saved_path)
|
|
3186
|
+
|| normalizeNonEmptyString(item?.savedPath)
|
|
3187
|
+
|| normalizeNonEmptyString(item?.file_path)
|
|
3188
|
+
|| normalizeNonEmptyString(item?.filePath);
|
|
3189
|
+
}
|
|
3190
|
+
|
|
3191
|
+
function fileChangePathSet(item) {
|
|
3192
|
+
const paths = new Set();
|
|
3193
|
+
const changes = Array.isArray(item?.changes) ? item.changes : [];
|
|
3194
|
+
for (const change of changes) {
|
|
3195
|
+
const pathKey = normalizeFileChangePathKey(change?.path || change?.file || change?.filePath || change?.file_path);
|
|
3196
|
+
if (pathKey) {
|
|
3197
|
+
paths.add(pathKey);
|
|
3198
|
+
}
|
|
3199
|
+
}
|
|
3200
|
+
return paths;
|
|
3201
|
+
}
|
|
3202
|
+
|
|
3203
|
+
function normalizeFileChangePathKey(value) {
|
|
3204
|
+
return normalizeNonEmptyString(value).replace(/\\/g, "/").replace(/^\/+/, "").toLowerCase();
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
function inferThreadIdFromTurns(turns) {
|
|
3208
|
+
if (!Array.isArray(turns)) {
|
|
3209
|
+
return "";
|
|
3210
|
+
}
|
|
3211
|
+
for (const turn of turns) {
|
|
3212
|
+
const threadId = normalizeNonEmptyString(turn?.threadId)
|
|
3213
|
+
|| normalizeNonEmptyString(turn?.thread_id)
|
|
3214
|
+
|| normalizeNonEmptyString(turn?.thread?.id)
|
|
3215
|
+
|| normalizeNonEmptyString(turn?.thread?.threadId)
|
|
3216
|
+
|| normalizeNonEmptyString(turn?.thread?.thread_id);
|
|
3217
|
+
if (threadId) {
|
|
3218
|
+
return threadId;
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
return "";
|
|
2503
3222
|
}
|
|
2504
3223
|
|
|
2505
3224
|
function sanitizeRelayHistoryTurns(turns, threadId = "") {
|
|
@@ -2530,7 +3249,12 @@ function sanitizeRelayHistoryTurn(turn, threadId = "") {
|
|
|
2530
3249
|
}
|
|
2531
3250
|
|
|
2532
3251
|
let itemDidChange = false;
|
|
2533
|
-
let sanitizedItem =
|
|
3252
|
+
let sanitizedItem = convertApplyPatchHistoryItem(item) || item;
|
|
3253
|
+
if (sanitizedItem !== item) {
|
|
3254
|
+
itemDidChange = true;
|
|
3255
|
+
}
|
|
3256
|
+
|
|
3257
|
+
sanitizedItem = annotateImageGenerationHistoryItem(sanitizedItem, turnThreadId);
|
|
2534
3258
|
if (sanitizedItem !== item) {
|
|
2535
3259
|
itemDidChange = true;
|
|
2536
3260
|
}
|
|
@@ -2573,6 +3297,26 @@ function sanitizeRelayHistoryTurn(turn, threadId = "") {
|
|
|
2573
3297
|
: turn;
|
|
2574
3298
|
}
|
|
2575
3299
|
|
|
3300
|
+
function convertApplyPatchHistoryItem(item) {
|
|
3301
|
+
const itemType = normalizeHistoryItemToken(item?.type);
|
|
3302
|
+
const toolName = normalizeNonEmptyString(item?.name);
|
|
3303
|
+
if (toolName !== "apply_patch" || itemType !== "customtoolcall") {
|
|
3304
|
+
return null;
|
|
3305
|
+
}
|
|
3306
|
+
|
|
3307
|
+
const fileChangeItem = buildApplyPatchFileChangeItem({
|
|
3308
|
+
callId: normalizeNonEmptyString(item.call_id) || normalizeNonEmptyString(item.callId) || normalizeNonEmptyString(item.id),
|
|
3309
|
+
patch: normalizeNonEmptyString(item.input),
|
|
3310
|
+
status: normalizeNonEmptyString(item.status) || "completed",
|
|
3311
|
+
idFallback: normalizeNonEmptyString(item.id) || "history-apply-patch-file-change",
|
|
3312
|
+
});
|
|
3313
|
+
return fileChangeItem ? { ...item, ...fileChangeItem } : null;
|
|
3314
|
+
}
|
|
3315
|
+
|
|
3316
|
+
function normalizeHistoryItemToken(value) {
|
|
3317
|
+
return normalizeNonEmptyString(value).toLowerCase().replace(/[\s_-]+/g, "");
|
|
3318
|
+
}
|
|
3319
|
+
|
|
2576
3320
|
// Annotates live image-generation notifications so the phone can render a local-file
|
|
2577
3321
|
// preview and does not receive the bulky inline base64 result over the relay.
|
|
2578
3322
|
function sanitizeLiveGeneratedImageMessageForRelay(rawMessage) {
|
|
@@ -3149,6 +3893,20 @@ function compactHistoryItemForRelay(item, maxChars) {
|
|
|
3149
3893
|
type: typeof item?.type === "string" ? item.type : "relay_truncated_item",
|
|
3150
3894
|
role: typeof item?.role === "string" ? item.role : undefined,
|
|
3151
3895
|
itemId: typeof item?.itemId === "string" ? item.itemId : undefined,
|
|
3896
|
+
turnId: typeof item?.turnId === "string" ? item.turnId : undefined,
|
|
3897
|
+
turn_id: typeof item?.turn_id === "string" ? item.turn_id : undefined,
|
|
3898
|
+
createdAt: relayScalarHistoryMetadata(item?.createdAt),
|
|
3899
|
+
created_at: relayScalarHistoryMetadata(item?.created_at),
|
|
3900
|
+
startedAt: relayScalarHistoryMetadata(item?.startedAt),
|
|
3901
|
+
started_at: relayScalarHistoryMetadata(item?.started_at),
|
|
3902
|
+
completedAt: relayScalarHistoryMetadata(item?.completedAt),
|
|
3903
|
+
completed_at: relayScalarHistoryMetadata(item?.completed_at),
|
|
3904
|
+
timestamp: relayScalarHistoryMetadata(item?.timestamp),
|
|
3905
|
+
time: relayScalarHistoryMetadata(item?.time),
|
|
3906
|
+
timeZoneIdentifier: relayScalarHistoryMetadata(item?.timeZoneIdentifier),
|
|
3907
|
+
timeZone: relayScalarHistoryMetadata(item?.timeZone),
|
|
3908
|
+
timezone: relayScalarHistoryMetadata(item?.timezone),
|
|
3909
|
+
time_zone: relayScalarHistoryMetadata(item?.time_zone),
|
|
3152
3910
|
relayPayloadTruncated: true,
|
|
3153
3911
|
};
|
|
3154
3912
|
const tailText = maxChars > 0 ? firstRelayTextTail(item, maxChars) : "";
|
|
@@ -3161,6 +3919,10 @@ function compactHistoryItemForRelay(item, maxChars) {
|
|
|
3161
3919
|
);
|
|
3162
3920
|
}
|
|
3163
3921
|
|
|
3922
|
+
function relayScalarHistoryMetadata(value) {
|
|
3923
|
+
return typeof value === "string" || typeof value === "number" ? value : undefined;
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3164
3926
|
function firstRelayTextTail(value, maxChars) {
|
|
3165
3927
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3166
3928
|
return "";
|
|
@@ -3209,6 +3971,7 @@ function persistBridgePreferences(
|
|
|
3209
3971
|
}
|
|
3210
3972
|
|
|
3211
3973
|
module.exports = {
|
|
3974
|
+
buildThreadTurnsListRelaySanitizeContext,
|
|
3212
3975
|
buildHeartbeatBridgeStatus,
|
|
3213
3976
|
buildRelayCloseStatusError,
|
|
3214
3977
|
buildRelayAccessTokenHeaders,
|
|
@@ -3220,6 +3983,7 @@ module.exports = {
|
|
|
3220
3983
|
isTerminalRelayCloseCode,
|
|
3221
3984
|
normalizeRelayBoundJsonRpcMessage,
|
|
3222
3985
|
persistBridgePreferences,
|
|
3986
|
+
resolveJsonlTurnsListRolloutPathForFallback,
|
|
3223
3987
|
sanitizeLiveGeneratedImageMessageForRelay,
|
|
3224
3988
|
sanitizeThreadHistoryImagesForRelay,
|
|
3225
3989
|
startBridge,
|