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