@makerbi/remodex 1.5.4 → 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/bin/remodex.js +22 -9
- 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 +752 -71
- 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/index.js +4 -2
- package/src/macos-launch-agent.js +87 -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/rollout-live-mirror.js +331 -20
- package/src/rollout-watch.js +5 -1
- package/src/secure-device-state.js +26 -1
- package/src/secure-transport.js +40 -12
- package/src/session-jsonl-history.js +796 -15
- package/src/voice-handler.js +129 -62
- package/src/workspace-handler.js +327 -14
package/src/bridge.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
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");
|
|
@@ -52,6 +53,7 @@ const { createBridgeSecureTransport } = require("./secure-transport");
|
|
|
52
53
|
const { createRolloutLiveMirrorController } = require("./rollout-live-mirror");
|
|
53
54
|
const {
|
|
54
55
|
createDesktopIpcActionFollower,
|
|
56
|
+
seedConversationStateFromThreadRead,
|
|
55
57
|
} = require("./desktop-ipc-action-follower");
|
|
56
58
|
const { version: bridgePackageVersion = "" } = require("../package.json");
|
|
57
59
|
const {
|
|
@@ -62,8 +64,11 @@ const {
|
|
|
62
64
|
} = require("./ios-app-compatibility");
|
|
63
65
|
const { createShortPairingCode, SHORT_PAIRING_CODE_LENGTH } = require("./qr");
|
|
64
66
|
const {
|
|
67
|
+
parseSessionJsonlMetadata,
|
|
68
|
+
parseSessionJsonlTurns,
|
|
65
69
|
readThreadTurnsListPageFromSessionJsonl,
|
|
66
70
|
} = require("./session-jsonl-history");
|
|
71
|
+
const { buildApplyPatchFileChangeItem } = require("./apply-patch-changes");
|
|
67
72
|
|
|
68
73
|
const execFileAsync = promisify(execFile);
|
|
69
74
|
const RELAY_WATCHDOG_PING_INTERVAL_MS = 10_000;
|
|
@@ -78,6 +83,12 @@ const RELAY_TURNS_LIST_TARGET_BUDGET_MS = 5_500;
|
|
|
78
83
|
const RELAY_TURNS_LIST_BUDGET_RESERVE_MS = 1_000;
|
|
79
84
|
const RELAY_TURNS_LIST_MAX_INITIAL_LIMIT = 5;
|
|
80
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;
|
|
81
92
|
const MODELS_WITHOUT_REASONING_SUMMARY = new Set([
|
|
82
93
|
"gpt-5.3-codex-spark",
|
|
83
94
|
]);
|
|
@@ -97,6 +108,9 @@ const RELAY_TURNS_LIST_PAGINATION_RESULT_KEYS = [
|
|
|
97
108
|
"previousCursor",
|
|
98
109
|
"previous_cursor",
|
|
99
110
|
];
|
|
111
|
+
const jsonlArtifactItemsCacheByThread = new Map();
|
|
112
|
+
const FORWARDED_REQUEST_METHODS_MAX_SIZE = 500;
|
|
113
|
+
const JSONL_ROLLOUT_PATH_CACHE_MAX_SIZE = 200;
|
|
100
114
|
|
|
101
115
|
function buildRelayUserAgentHeader({ version = bridgePackageVersion } = {}) {
|
|
102
116
|
const normalizedVersion = typeof version === "string" && version.trim()
|
|
@@ -116,6 +130,18 @@ function buildRelayAccessTokenHeaders(config = {}, env = process.env) {
|
|
|
116
130
|
: {};
|
|
117
131
|
}
|
|
118
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
|
+
|
|
119
145
|
function startBridge({
|
|
120
146
|
config: explicitConfig = null,
|
|
121
147
|
printPairingQr = true,
|
|
@@ -192,6 +218,8 @@ function startBridge({
|
|
|
192
218
|
const relayChannels = [];
|
|
193
219
|
const codexResponseRoutesById = new Map();
|
|
194
220
|
const extraRelaySessionCount = readExtraRelaySessionCount(process.env);
|
|
221
|
+
const jsonlTurnsListRolloutCacheByThread = new Map();
|
|
222
|
+
const jsonlTurnsListRolloutMissCacheByThread = new Map();
|
|
195
223
|
const trackedForwardedRequestMethods = new Set([
|
|
196
224
|
"account/login/start",
|
|
197
225
|
"account/login/cancel",
|
|
@@ -214,6 +242,7 @@ function startBridge({
|
|
|
214
242
|
sessionId,
|
|
215
243
|
relayUrl: relayBaseUrl,
|
|
216
244
|
deviceState,
|
|
245
|
+
displayName: os.hostname(),
|
|
217
246
|
onTrustedPhoneUpdate(nextDeviceState) {
|
|
218
247
|
deviceState = nextDeviceState;
|
|
219
248
|
sendRelayRegistrationUpdate(nextDeviceState);
|
|
@@ -240,6 +269,9 @@ function startBridge({
|
|
|
240
269
|
const desktopIpcActionFollower = !config.codexEndpoint
|
|
241
270
|
? createDesktopIpcActionFollower({
|
|
242
271
|
sendApplicationResponse,
|
|
272
|
+
readConversationState: async (threadId) => seedConversationStateFromThreadRead(
|
|
273
|
+
await sendCodexRequest("thread/read", { threadId })
|
|
274
|
+
),
|
|
243
275
|
socketPath: config.desktopIpcSocketPath || undefined,
|
|
244
276
|
})
|
|
245
277
|
: null;
|
|
@@ -323,6 +355,33 @@ function startBridge({
|
|
|
323
355
|
relayWatchdogTimer = null;
|
|
324
356
|
}
|
|
325
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
|
+
|
|
326
385
|
function startRelayWatchdog(trackedSocket) {
|
|
327
386
|
clearRelayWatchdog();
|
|
328
387
|
markRelayActivity();
|
|
@@ -379,19 +438,9 @@ function startBridge({
|
|
|
379
438
|
return;
|
|
380
439
|
}
|
|
381
440
|
|
|
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
|
-
});
|
|
441
|
+
if (closeCode === 4000 || closeCode === 4001) {
|
|
442
|
+
logConnectionStatus("disconnected");
|
|
443
|
+
shutdown(codex, () => socket, prepareBridgeShutdown);
|
|
395
444
|
return;
|
|
396
445
|
}
|
|
397
446
|
|
|
@@ -400,7 +449,9 @@ function startBridge({
|
|
|
400
449
|
}
|
|
401
450
|
|
|
402
451
|
reconnectAttempt += 1;
|
|
403
|
-
const
|
|
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;
|
|
404
455
|
logConnectionStatus("connecting");
|
|
405
456
|
reconnectTimer = setTimeout(() => {
|
|
406
457
|
reconnectTimer = null;
|
|
@@ -415,6 +466,11 @@ function startBridge({
|
|
|
415
466
|
|
|
416
467
|
logConnectionStatus("connecting");
|
|
417
468
|
const nextSocket = new WebSocket(relaySessionUrl, {
|
|
469
|
+
perMessageDeflate: {
|
|
470
|
+
zlibDeflateOptions: { level: 6 },
|
|
471
|
+
threshold: 256,
|
|
472
|
+
concurrencyLimit: 4,
|
|
473
|
+
},
|
|
418
474
|
// The relay uses this per-session secret to authenticate the first push registration.
|
|
419
475
|
headers: {
|
|
420
476
|
"User-Agent": buildRelayUserAgentHeader(),
|
|
@@ -471,8 +527,8 @@ function startBridge({
|
|
|
471
527
|
socket = null;
|
|
472
528
|
}
|
|
473
529
|
stopContextUsageWatcher();
|
|
474
|
-
|
|
475
|
-
|
|
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.
|
|
476
532
|
desktopRefresher.handleTransportReset();
|
|
477
533
|
scheduleRelayReconnect(code, closeReason);
|
|
478
534
|
});
|
|
@@ -680,12 +736,7 @@ function startBridge({
|
|
|
680
736
|
console.error(`[remodex] ${lastError}`);
|
|
681
737
|
process.exitCode = 1;
|
|
682
738
|
}
|
|
683
|
-
|
|
684
|
-
bridgeWakeAssertion.stop();
|
|
685
|
-
clearReconnectTimer();
|
|
686
|
-
stopContextUsageWatcher();
|
|
687
|
-
rolloutLiveMirror?.stopAll();
|
|
688
|
-
desktopIpcActionFollower?.stopAll();
|
|
739
|
+
prepareBridgeShutdown();
|
|
689
740
|
desktopRefresher.handleTransportReset();
|
|
690
741
|
failBridgeManagedCodexRequests(new Error("Codex transport closed before the bridge request completed."));
|
|
691
742
|
forwardedRequestMethodsById.clear();
|
|
@@ -696,22 +747,8 @@ function startBridge({
|
|
|
696
747
|
closeExtraRelayChannels();
|
|
697
748
|
});
|
|
698
749
|
|
|
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
|
-
}));
|
|
750
|
+
process.on("SIGINT", () => shutdown(codex, () => socket, prepareBridgeShutdown));
|
|
751
|
+
process.on("SIGTERM", () => shutdown(codex, () => socket, prepareBridgeShutdown));
|
|
715
752
|
|
|
716
753
|
// Routes decrypted app payloads through the same bridge handlers as before.
|
|
717
754
|
function handleApplicationMessage(rawMessage, relayChannel = primaryRelayChannel) {
|
|
@@ -745,6 +782,7 @@ function startBridge({
|
|
|
745
782
|
appPath: config.codexAppPath,
|
|
746
783
|
readBridgePreferences,
|
|
747
784
|
updateBridgePreferences,
|
|
785
|
+
updateBridgePackageAndRestart,
|
|
748
786
|
})) {
|
|
749
787
|
return;
|
|
750
788
|
}
|
|
@@ -974,12 +1012,19 @@ function startBridge({
|
|
|
974
1012
|
const response = await fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
975
1013
|
fetchPage: (params) => sendCodexRequest("thread/turns/list", params),
|
|
976
1014
|
});
|
|
977
|
-
const
|
|
1015
|
+
const jsonlFallback = maybeBuildJsonlThreadTurnsListFallback(request, response);
|
|
1016
|
+
const responsePayload = jsonlFallback?.response ?? response;
|
|
1017
|
+
const finalSanitizeContext = buildThreadTurnsListRelaySanitizeContext(request);
|
|
978
1018
|
relaySanitizedResponseMethodsById.set(String(request.id), {
|
|
979
1019
|
method: "thread/turns/list",
|
|
1020
|
+
...finalSanitizeContext,
|
|
980
1021
|
createdAt: Date.now(),
|
|
981
1022
|
});
|
|
982
|
-
sendResponse(
|
|
1023
|
+
sendResponse(sanitizeThreadHistoryImagesForRelay(
|
|
1024
|
+
JSON.stringify(responsePayload),
|
|
1025
|
+
"thread/turns/list",
|
|
1026
|
+
finalSanitizeContext
|
|
1027
|
+
));
|
|
983
1028
|
} catch (error) {
|
|
984
1029
|
sendResponse(createJsonRpcErrorResponse(
|
|
985
1030
|
request.id,
|
|
@@ -993,10 +1038,6 @@ function startBridge({
|
|
|
993
1038
|
}
|
|
994
1039
|
|
|
995
1040
|
function maybeBuildJsonlThreadTurnsListFallback(request, response) {
|
|
996
|
-
if (!isEmptyTurnsListResponse(response)) {
|
|
997
|
-
return null;
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
1041
|
const params = request?.params || {};
|
|
1001
1042
|
const threadId = normalizeNonEmptyString(params.threadId)
|
|
1002
1043
|
|| normalizeNonEmptyString(params.thread_id);
|
|
@@ -1005,10 +1046,17 @@ function startBridge({
|
|
|
1005
1046
|
}
|
|
1006
1047
|
|
|
1007
1048
|
try {
|
|
1008
|
-
const
|
|
1049
|
+
const responseIsEmpty = isEmptyTurnsListResponse(response);
|
|
1050
|
+
const rolloutPath = resolveJsonlTurnsListRolloutPathForFallback({
|
|
1051
|
+
threadId,
|
|
1052
|
+
responseIsEmpty,
|
|
1053
|
+
readCachedPath: readCachedJsonlTurnsListRolloutPath,
|
|
1054
|
+
findAndCachePath: findAndCacheJsonlTurnsListRolloutPath,
|
|
1055
|
+
});
|
|
1009
1056
|
if (!rolloutPath) {
|
|
1010
1057
|
return null;
|
|
1011
1058
|
}
|
|
1059
|
+
|
|
1012
1060
|
const result = readThreadTurnsListPageFromSessionJsonl(rolloutPath, {
|
|
1013
1061
|
threadId,
|
|
1014
1062
|
limit: params.limit,
|
|
@@ -1020,16 +1068,69 @@ function startBridge({
|
|
|
1020
1068
|
return null;
|
|
1021
1069
|
}
|
|
1022
1070
|
|
|
1071
|
+
if (!responseIsEmpty) {
|
|
1072
|
+
const mergedResponse = maybeMergeLatestJsonlTurnIntoTurnsListResponse(request, response, result, params);
|
|
1073
|
+
return mergedResponse ? { response: mergedResponse, usesJsonl: true } : null;
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1023
1076
|
return {
|
|
1024
|
-
|
|
1025
|
-
|
|
1077
|
+
response: {
|
|
1078
|
+
id: request.id,
|
|
1079
|
+
result,
|
|
1080
|
+
},
|
|
1081
|
+
usesJsonl: true,
|
|
1026
1082
|
};
|
|
1027
1083
|
} catch (error) {
|
|
1084
|
+
jsonlTurnsListRolloutCacheByThread.delete(threadId);
|
|
1028
1085
|
console.warn(`[remodex] thread/turns/list jsonl fallback failed: ${error.message}`);
|
|
1029
1086
|
return null;
|
|
1030
1087
|
}
|
|
1031
1088
|
}
|
|
1032
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
|
+
|
|
1033
1134
|
// ─── Bridge-owned auth snapshot ─────────────────────────────
|
|
1034
1135
|
|
|
1035
1136
|
// Handles the bridge-owned auth status wrappers without exposing tokens to the phone.
|
|
@@ -1178,10 +1279,17 @@ function startBridge({
|
|
|
1178
1279
|
});
|
|
1179
1280
|
}
|
|
1180
1281
|
if (relaySanitizedRequestMethods.has(method)) {
|
|
1181
|
-
|
|
1282
|
+
const trackedRequest = {
|
|
1182
1283
|
method,
|
|
1284
|
+
threadId: method === "thread/turns/list" || method === "thread/read" || method === "thread/resume"
|
|
1285
|
+
? threadIdFromRequestParams(parsed.params)
|
|
1286
|
+
: "",
|
|
1183
1287
|
createdAt: Date.now(),
|
|
1184
|
-
}
|
|
1288
|
+
};
|
|
1289
|
+
if (method === "thread/turns/list") {
|
|
1290
|
+
trackedRequest.skipJsonlArtifactAugmentation = false;
|
|
1291
|
+
}
|
|
1292
|
+
relaySanitizedResponseMethodsById.set(String(requestId), trackedRequest);
|
|
1185
1293
|
}
|
|
1186
1294
|
}
|
|
1187
1295
|
|
|
@@ -1207,7 +1315,7 @@ function startBridge({
|
|
|
1207
1315
|
}
|
|
1208
1316
|
relaySanitizedResponseMethodsById.delete(String(responseId));
|
|
1209
1317
|
|
|
1210
|
-
return sanitizeThreadHistoryImagesForRelay(normalizedMessage, trackedRequest.method);
|
|
1318
|
+
return sanitizeThreadHistoryImagesForRelay(normalizedMessage, trackedRequest.method, trackedRequest);
|
|
1211
1319
|
}
|
|
1212
1320
|
|
|
1213
1321
|
function updatePendingAuthLoginFromCodexMessage(rawMessage) {
|
|
@@ -1260,16 +1368,31 @@ function startBridge({
|
|
|
1260
1368
|
}
|
|
1261
1369
|
|
|
1262
1370
|
function pruneExpiredForwardedRequestMethods(now = Date.now()) {
|
|
1371
|
+
const expiredForwarded = [];
|
|
1263
1372
|
for (const [requestId, trackedRequest] of forwardedRequestMethodsById.entries()) {
|
|
1264
1373
|
if (!trackedRequest || (now - trackedRequest.createdAt) >= forwardedRequestMethodTTLms) {
|
|
1265
|
-
|
|
1374
|
+
expiredForwarded.push(requestId);
|
|
1266
1375
|
}
|
|
1267
1376
|
}
|
|
1377
|
+
for (const id of expiredForwarded) {
|
|
1378
|
+
forwardedRequestMethodsById.delete(id);
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
const expiredSanitized = [];
|
|
1268
1382
|
for (const [requestId, trackedRequest] of relaySanitizedResponseMethodsById.entries()) {
|
|
1269
1383
|
if (!trackedRequest || (now - trackedRequest.createdAt) >= forwardedRequestMethodTTLms) {
|
|
1270
|
-
|
|
1384
|
+
expiredSanitized.push(requestId);
|
|
1271
1385
|
}
|
|
1272
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);
|
|
1273
1396
|
}
|
|
1274
1397
|
|
|
1275
1398
|
function safeParseJSON(value) {
|
|
@@ -1629,6 +1752,64 @@ function startBridge({
|
|
|
1629
1752
|
|
|
1630
1753
|
return readBridgePreferences();
|
|
1631
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
|
+
};
|
|
1632
1813
|
}
|
|
1633
1814
|
|
|
1634
1815
|
// Holds a single macOS idle-sleep assertion for as long as the bridge process stays alive.
|
|
@@ -1935,6 +2116,14 @@ function normalizeNonEmptyString(value) {
|
|
|
1935
2116
|
return typeof value === "string" && value.trim() ? value.trim() : "";
|
|
1936
2117
|
}
|
|
1937
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
|
+
|
|
1938
2127
|
function parseAdaptiveThreadTurnsListRequest(rawMessage) {
|
|
1939
2128
|
const parsed = parseBridgeJSON(rawMessage);
|
|
1940
2129
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
@@ -1961,6 +2150,22 @@ function parseAdaptiveThreadTurnsListRequest(rawMessage) {
|
|
|
1961
2150
|
return parsed;
|
|
1962
2151
|
}
|
|
1963
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
|
+
|
|
1964
2169
|
async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
1965
2170
|
fetchPage,
|
|
1966
2171
|
now = Date.now,
|
|
@@ -1978,6 +2183,9 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
1978
2183
|
const requestedLimit = Number.isInteger(params?.limit) && params.limit > 0
|
|
1979
2184
|
? Math.min(params.limit, RELAY_TURNS_LIST_MAX_INITIAL_LIMIT)
|
|
1980
2185
|
: 1;
|
|
2186
|
+
const sanitizeContext = buildThreadTurnsListRelaySanitizeContext(request, {
|
|
2187
|
+
skipJsonlArtifactAugmentation: true,
|
|
2188
|
+
});
|
|
1981
2189
|
const startedAt = now();
|
|
1982
2190
|
let nextCursor = params?.cursor;
|
|
1983
2191
|
let turnsKey = null;
|
|
@@ -2002,6 +2210,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
2002
2210
|
fetchPage,
|
|
2003
2211
|
now,
|
|
2004
2212
|
sanitizeForRelay,
|
|
2213
|
+
sanitizeContext,
|
|
2005
2214
|
payloadSoftLimitBytes,
|
|
2006
2215
|
});
|
|
2007
2216
|
}
|
|
@@ -2014,6 +2223,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
2014
2223
|
fetchPage,
|
|
2015
2224
|
now,
|
|
2016
2225
|
sanitizeForRelay,
|
|
2226
|
+
sanitizeContext,
|
|
2017
2227
|
payloadSoftLimitBytes,
|
|
2018
2228
|
});
|
|
2019
2229
|
}
|
|
@@ -2032,7 +2242,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
2032
2242
|
combinedTurns = combinedTurns.concat(pageTurns);
|
|
2033
2243
|
response = buildSafeTurnsListResponse(request.id, firstResult, lastResult, turnsKey, combinedTurns);
|
|
2034
2244
|
|
|
2035
|
-
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) >= payloadSoftLimitBytes) {
|
|
2245
|
+
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, sanitizeContext) >= payloadSoftLimitBytes) {
|
|
2036
2246
|
response = buildLargestSafeTurnsListResponse({
|
|
2037
2247
|
requestId: request.id,
|
|
2038
2248
|
firstResult,
|
|
@@ -2041,6 +2251,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
2041
2251
|
turns: combinedTurns,
|
|
2042
2252
|
maxTurns: RELAY_TURNS_LIST_SAFE_RETRY_LIMIT,
|
|
2043
2253
|
sanitizeForRelay,
|
|
2254
|
+
sanitizeContext,
|
|
2044
2255
|
payloadSoftLimitBytes,
|
|
2045
2256
|
}) ?? buildEmptyTurnsListResponse(request);
|
|
2046
2257
|
break;
|
|
@@ -2052,7 +2263,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
2052
2263
|
}
|
|
2053
2264
|
|
|
2054
2265
|
const rawPageBytes = jsonByteLength(pageResult);
|
|
2055
|
-
const sanitizedResponseBytes = measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay);
|
|
2266
|
+
const sanitizedResponseBytes = measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, sanitizeContext);
|
|
2056
2267
|
const elapsedMs = Math.max(0, now() - startedAt);
|
|
2057
2268
|
const remainingBudgetMs = Math.max(0, targetBudgetMs - elapsedMs);
|
|
2058
2269
|
if (
|
|
@@ -2088,10 +2299,87 @@ function isEmptyTurnsListResponse(response) {
|
|
|
2088
2299
|
return Boolean(turnsKey) && response.result[turnsKey].length === 0;
|
|
2089
2300
|
}
|
|
2090
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
|
+
|
|
2091
2378
|
async function fetchSafeThreadTurnsListFallback(request, {
|
|
2092
2379
|
fetchPage,
|
|
2093
2380
|
now,
|
|
2094
2381
|
sanitizeForRelay,
|
|
2382
|
+
sanitizeContext = {},
|
|
2095
2383
|
payloadSoftLimitBytes,
|
|
2096
2384
|
}) {
|
|
2097
2385
|
const params = request?.params;
|
|
@@ -2119,6 +2407,7 @@ async function fetchSafeThreadTurnsListFallback(request, {
|
|
|
2119
2407
|
turns: pageResult[turnsKey],
|
|
2120
2408
|
maxTurns: safeLimit,
|
|
2121
2409
|
sanitizeForRelay,
|
|
2410
|
+
sanitizeContext,
|
|
2122
2411
|
payloadSoftLimitBytes,
|
|
2123
2412
|
});
|
|
2124
2413
|
if (response) {
|
|
@@ -2187,6 +2476,7 @@ function buildLargestSafeTurnsListResponse({
|
|
|
2187
2476
|
turns,
|
|
2188
2477
|
maxTurns,
|
|
2189
2478
|
sanitizeForRelay,
|
|
2479
|
+
sanitizeContext = {},
|
|
2190
2480
|
payloadSoftLimitBytes,
|
|
2191
2481
|
}) {
|
|
2192
2482
|
const sliceLimit = Math.min(turns.length, maxTurns);
|
|
@@ -2198,7 +2488,7 @@ function buildLargestSafeTurnsListResponse({
|
|
|
2198
2488
|
turnsKey,
|
|
2199
2489
|
turns.slice(0, count)
|
|
2200
2490
|
);
|
|
2201
|
-
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) < payloadSoftLimitBytes) {
|
|
2491
|
+
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, sanitizeContext) < payloadSoftLimitBytes) {
|
|
2202
2492
|
return response;
|
|
2203
2493
|
}
|
|
2204
2494
|
}
|
|
@@ -2208,6 +2498,7 @@ function buildLargestSafeTurnsListResponse({
|
|
|
2208
2498
|
turnsKey,
|
|
2209
2499
|
turn: turns[0],
|
|
2210
2500
|
sanitizeForRelay,
|
|
2501
|
+
sanitizeContext,
|
|
2211
2502
|
payloadSoftLimitBytes,
|
|
2212
2503
|
});
|
|
2213
2504
|
}
|
|
@@ -2218,6 +2509,7 @@ function buildEmergencySingleTurnResponse({
|
|
|
2218
2509
|
turnsKey,
|
|
2219
2510
|
turn,
|
|
2220
2511
|
sanitizeForRelay,
|
|
2512
|
+
sanitizeContext = {},
|
|
2221
2513
|
payloadSoftLimitBytes,
|
|
2222
2514
|
}) {
|
|
2223
2515
|
if (!turn || typeof turn !== "object" || Array.isArray(turn)) {
|
|
@@ -2240,7 +2532,7 @@ function buildEmergencySingleTurnResponse({
|
|
|
2240
2532
|
remodexEmergencySingleTurnForRelay: true,
|
|
2241
2533
|
},
|
|
2242
2534
|
};
|
|
2243
|
-
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) < payloadSoftLimitBytes) {
|
|
2535
|
+
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, sanitizeContext) < payloadSoftLimitBytes) {
|
|
2244
2536
|
return response;
|
|
2245
2537
|
}
|
|
2246
2538
|
}
|
|
@@ -2318,10 +2610,10 @@ function jsonByteLength(value) {
|
|
|
2318
2610
|
}
|
|
2319
2611
|
}
|
|
2320
2612
|
|
|
2321
|
-
function measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) {
|
|
2613
|
+
function measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, requestContext = {}) {
|
|
2322
2614
|
try {
|
|
2323
2615
|
const rawResponse = JSON.stringify(response);
|
|
2324
|
-
const sanitizedResponse = sanitizeForRelay(rawResponse, "thread/turns/list");
|
|
2616
|
+
const sanitizedResponse = sanitizeForRelay(rawResponse, "thread/turns/list", requestContext);
|
|
2325
2617
|
return Buffer.byteLength(sanitizedResponse, "utf8");
|
|
2326
2618
|
} catch {
|
|
2327
2619
|
return Number.POSITIVE_INFINITY;
|
|
@@ -2432,9 +2724,9 @@ function isRelayBoundServerRequestMethod(method) {
|
|
|
2432
2724
|
|
|
2433
2725
|
// Shrinks thread history snapshots/pages for mobile relay delivery.
|
|
2434
2726
|
// This elides bulky blobs and replaces oversized older history with a compact marker.
|
|
2435
|
-
function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
|
|
2727
|
+
function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod, requestContext = {}) {
|
|
2436
2728
|
if (requestMethod === "thread/turns/list") {
|
|
2437
|
-
return sanitizeThreadTurnsListForRelay(rawMessage);
|
|
2729
|
+
return sanitizeThreadTurnsListForRelay(rawMessage, requestContext);
|
|
2438
2730
|
}
|
|
2439
2731
|
|
|
2440
2732
|
if (requestMethod !== "thread/read" && requestMethod !== "thread/resume") {
|
|
@@ -2447,12 +2739,15 @@ function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
|
|
|
2447
2739
|
return rawMessage;
|
|
2448
2740
|
}
|
|
2449
2741
|
|
|
2450
|
-
const threadId = normalizeNonEmptyString(
|
|
2742
|
+
const threadId = normalizeNonEmptyString(requestContext?.threadId)
|
|
2743
|
+
|| normalizeNonEmptyString(thread.id)
|
|
2451
2744
|
|| normalizeNonEmptyString(thread.threadId)
|
|
2452
2745
|
|| normalizeNonEmptyString(thread.thread_id);
|
|
2453
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);
|
|
2454
2749
|
|
|
2455
|
-
if (!didSanitize) {
|
|
2750
|
+
if (!didSanitize && !didAugment && !didAugmentThreadMetadata) {
|
|
2456
2751
|
const trimmedPayload = trimThreadPayloadForRelay(parsed, thread);
|
|
2457
2752
|
return trimmedPayload == null ? rawMessage : trimmedPayload;
|
|
2458
2753
|
}
|
|
@@ -2462,8 +2757,8 @@ function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
|
|
|
2462
2757
|
result: {
|
|
2463
2758
|
...parsed.result,
|
|
2464
2759
|
thread: {
|
|
2465
|
-
...
|
|
2466
|
-
turns:
|
|
2760
|
+
...threadWithJsonlMetadata,
|
|
2761
|
+
turns: augmentedTurns,
|
|
2467
2762
|
},
|
|
2468
2763
|
},
|
|
2469
2764
|
});
|
|
@@ -2471,7 +2766,7 @@ function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
|
|
|
2471
2766
|
return trimThreadPayloadForRelay(parseBridgeJSON(sanitizedPayload), null) ?? sanitizedPayload;
|
|
2472
2767
|
}
|
|
2473
2768
|
|
|
2474
|
-
function sanitizeThreadTurnsListForRelay(rawMessage) {
|
|
2769
|
+
function sanitizeThreadTurnsListForRelay(rawMessage, requestContext = {}) {
|
|
2475
2770
|
const parsed = parseBridgeJSON(rawMessage);
|
|
2476
2771
|
const result = parsed?.result;
|
|
2477
2772
|
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
@@ -2483,23 +2778,372 @@ function sanitizeThreadTurnsListForRelay(rawMessage) {
|
|
|
2483
2778
|
return rawMessage;
|
|
2484
2779
|
}
|
|
2485
2780
|
|
|
2486
|
-
const threadId = normalizeNonEmptyString(
|
|
2781
|
+
const threadId = normalizeNonEmptyString(requestContext?.threadId)
|
|
2782
|
+
|| normalizeNonEmptyString(result.threadId)
|
|
2487
2783
|
|| normalizeNonEmptyString(result.thread_id)
|
|
2488
2784
|
|| normalizeNonEmptyString(result.thread?.id)
|
|
2489
2785
|
|| normalizeNonEmptyString(result.thread?.threadId)
|
|
2490
|
-
|| normalizeNonEmptyString(result.thread?.thread_id)
|
|
2786
|
+
|| normalizeNonEmptyString(result.thread?.thread_id)
|
|
2787
|
+
|| inferThreadIdFromTurns(result[turnsKey]);
|
|
2491
2788
|
const { turns: sanitizedTurns, didSanitize } = sanitizeRelayHistoryTurns(result[turnsKey], threadId);
|
|
2492
|
-
const
|
|
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
|
|
2493
2795
|
? {
|
|
2494
2796
|
...parsed,
|
|
2495
2797
|
result: {
|
|
2496
2798
|
...result,
|
|
2497
|
-
[turnsKey]:
|
|
2799
|
+
[turnsKey]: augmentedTurns,
|
|
2498
2800
|
},
|
|
2499
2801
|
}
|
|
2500
2802
|
: parsed;
|
|
2501
2803
|
|
|
2502
|
-
return trimTurnsListPayloadForRelay(sanitizedParsed, turnsKey,
|
|
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 "";
|
|
2503
3147
|
}
|
|
2504
3148
|
|
|
2505
3149
|
function sanitizeRelayHistoryTurns(turns, threadId = "") {
|
|
@@ -2530,7 +3174,12 @@ function sanitizeRelayHistoryTurn(turn, threadId = "") {
|
|
|
2530
3174
|
}
|
|
2531
3175
|
|
|
2532
3176
|
let itemDidChange = false;
|
|
2533
|
-
let sanitizedItem =
|
|
3177
|
+
let sanitizedItem = convertApplyPatchHistoryItem(item) || item;
|
|
3178
|
+
if (sanitizedItem !== item) {
|
|
3179
|
+
itemDidChange = true;
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
sanitizedItem = annotateImageGenerationHistoryItem(sanitizedItem, turnThreadId);
|
|
2534
3183
|
if (sanitizedItem !== item) {
|
|
2535
3184
|
itemDidChange = true;
|
|
2536
3185
|
}
|
|
@@ -2573,6 +3222,26 @@ function sanitizeRelayHistoryTurn(turn, threadId = "") {
|
|
|
2573
3222
|
: turn;
|
|
2574
3223
|
}
|
|
2575
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
|
+
|
|
2576
3245
|
// Annotates live image-generation notifications so the phone can render a local-file
|
|
2577
3246
|
// preview and does not receive the bulky inline base64 result over the relay.
|
|
2578
3247
|
function sanitizeLiveGeneratedImageMessageForRelay(rawMessage) {
|
|
@@ -3149,6 +3818,12 @@ function compactHistoryItemForRelay(item, maxChars) {
|
|
|
3149
3818
|
type: typeof item?.type === "string" ? item.type : "relay_truncated_item",
|
|
3150
3819
|
role: typeof item?.role === "string" ? item.role : undefined,
|
|
3151
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),
|
|
3152
3827
|
relayPayloadTruncated: true,
|
|
3153
3828
|
};
|
|
3154
3829
|
const tailText = maxChars > 0 ? firstRelayTextTail(item, maxChars) : "";
|
|
@@ -3161,6 +3836,10 @@ function compactHistoryItemForRelay(item, maxChars) {
|
|
|
3161
3836
|
);
|
|
3162
3837
|
}
|
|
3163
3838
|
|
|
3839
|
+
function relayScalarHistoryMetadata(value) {
|
|
3840
|
+
return typeof value === "string" || typeof value === "number" ? value : undefined;
|
|
3841
|
+
}
|
|
3842
|
+
|
|
3164
3843
|
function firstRelayTextTail(value, maxChars) {
|
|
3165
3844
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
3166
3845
|
return "";
|
|
@@ -3209,6 +3888,7 @@ function persistBridgePreferences(
|
|
|
3209
3888
|
}
|
|
3210
3889
|
|
|
3211
3890
|
module.exports = {
|
|
3891
|
+
buildThreadTurnsListRelaySanitizeContext,
|
|
3212
3892
|
buildHeartbeatBridgeStatus,
|
|
3213
3893
|
buildRelayCloseStatusError,
|
|
3214
3894
|
buildRelayAccessTokenHeaders,
|
|
@@ -3220,6 +3900,7 @@ module.exports = {
|
|
|
3220
3900
|
isTerminalRelayCloseCode,
|
|
3221
3901
|
normalizeRelayBoundJsonRpcMessage,
|
|
3222
3902
|
persistBridgePreferences,
|
|
3903
|
+
resolveJsonlTurnsListRolloutPathForFallback,
|
|
3223
3904
|
sanitizeLiveGeneratedImageMessageForRelay,
|
|
3224
3905
|
sanitizeThreadHistoryImagesForRelay,
|
|
3225
3906
|
startBridge,
|