@makerbi/remodex 1.5.1 → 1.5.2
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 +501 -0
- package/bin/remodex.js +41 -2
- package/package.json +4 -2
- package/src/bridge.js +748 -52
- package/src/macos-launch-agent.js +44 -0
- package/src/secure-device-state.js +2 -2
- package/src/session-jsonl-history.js +197 -0
package/src/bridge.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// Depends on: ws, crypto, os, ./codex-home, ./qr, ./codex-desktop-refresher, ./codex-transport, ./rollout-watch, ./voice-handler, ./ios-app-compatibility
|
|
6
6
|
|
|
7
7
|
const WebSocket = require("ws");
|
|
8
|
-
const { randomBytes } = require("crypto");
|
|
8
|
+
const { randomBytes, randomUUID } = require("crypto");
|
|
9
9
|
const { execFile, spawn } = require("child_process");
|
|
10
10
|
const path = require("path");
|
|
11
11
|
const os = require("os");
|
|
@@ -15,7 +15,11 @@ const {
|
|
|
15
15
|
readBridgeConfig,
|
|
16
16
|
} = require("./codex-desktop-refresher");
|
|
17
17
|
const { createCodexTransport } = require("./codex-transport");
|
|
18
|
-
const {
|
|
18
|
+
const {
|
|
19
|
+
createThreadRolloutActivityWatcher,
|
|
20
|
+
findRecentRolloutFileForContextRead,
|
|
21
|
+
resolveSessionsRoot,
|
|
22
|
+
} = require("./rollout-watch");
|
|
19
23
|
const { printQR } = require("./qr");
|
|
20
24
|
const { rememberActiveThread } = require("./session-state");
|
|
21
25
|
const { handleDesktopRequest } = require("./desktop-handler");
|
|
@@ -52,6 +56,9 @@ const {
|
|
|
52
56
|
normalizeVersionString,
|
|
53
57
|
} = require("./ios-app-compatibility");
|
|
54
58
|
const { createShortPairingCode, SHORT_PAIRING_CODE_LENGTH } = require("./qr");
|
|
59
|
+
const {
|
|
60
|
+
readThreadTurnsListPageFromSessionJsonl,
|
|
61
|
+
} = require("./session-jsonl-history");
|
|
55
62
|
|
|
56
63
|
const execFileAsync = promisify(execFile);
|
|
57
64
|
const RELAY_WATCHDOG_PING_INTERVAL_MS = 10_000;
|
|
@@ -68,6 +75,8 @@ const RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS = 24_000;
|
|
|
68
75
|
const RELAY_HISTORY_RECENT_TURN_TARGET = 40;
|
|
69
76
|
const RELAY_TURNS_LIST_TARGET_BUDGET_MS = 5_500;
|
|
70
77
|
const RELAY_TURNS_LIST_BUDGET_RESERVE_MS = 1_000;
|
|
78
|
+
const RELAY_TURNS_LIST_MAX_INITIAL_LIMIT = 5;
|
|
79
|
+
const RELAY_TURNS_LIST_SAFE_RETRY_LIMIT = 5;
|
|
71
80
|
const RELAY_TURNS_LIST_RESULT_KEYS = ["data", "items", "turns"];
|
|
72
81
|
const RELAY_TURNS_LIST_PAGINATION_RESULT_KEYS = [
|
|
73
82
|
"nextCursor",
|
|
@@ -178,12 +187,16 @@ function startBridge({
|
|
|
178
187
|
const bridgeManagedCodexRequestWaiters = new Map();
|
|
179
188
|
const forwardedRequestMethodsById = new Map();
|
|
180
189
|
const relaySanitizedResponseMethodsById = new Map();
|
|
190
|
+
const relayChannels = [];
|
|
191
|
+
const codexResponseRoutesById = new Map();
|
|
192
|
+
const extraRelaySessionCount = readExtraRelaySessionCount(process.env);
|
|
181
193
|
const trackedForwardedRequestMethods = new Set([
|
|
182
194
|
"account/login/start",
|
|
183
195
|
"account/login/cancel",
|
|
184
196
|
"account/logout",
|
|
185
197
|
]);
|
|
186
198
|
const relaySanitizedRequestMethods = new Set([
|
|
199
|
+
"thread/list",
|
|
187
200
|
"thread/read",
|
|
188
201
|
"thread/resume",
|
|
189
202
|
"thread/turns/list",
|
|
@@ -204,6 +217,7 @@ function startBridge({
|
|
|
204
217
|
sendRelayRegistrationUpdate(nextDeviceState);
|
|
205
218
|
},
|
|
206
219
|
});
|
|
220
|
+
let primaryRelayChannel = null;
|
|
207
221
|
// Keeps one stable sender identity across reconnects so buffered replay state
|
|
208
222
|
// reflects what actually made it onto the current relay socket.
|
|
209
223
|
function sendRelayWireMessage(wireMessage) {
|
|
@@ -446,7 +460,7 @@ function startBridge({
|
|
|
446
460
|
}
|
|
447
461
|
},
|
|
448
462
|
onApplicationMessage(plaintextMessage) {
|
|
449
|
-
handleApplicationMessage(plaintextMessage);
|
|
463
|
+
handleApplicationMessage(plaintextMessage, primaryRelayChannel);
|
|
450
464
|
},
|
|
451
465
|
})) {
|
|
452
466
|
return;
|
|
@@ -485,17 +499,168 @@ function startBridge({
|
|
|
485
499
|
});
|
|
486
500
|
}
|
|
487
501
|
|
|
502
|
+
// Optional draft path: one bridge process can expose extra one-mobile relay sessions without changing relay behavior.
|
|
503
|
+
function startExtraRelayChannels() {
|
|
504
|
+
for (let index = 1; index <= extraRelaySessionCount; index += 1) {
|
|
505
|
+
startExtraRelayChannel(index);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function startExtraRelayChannel(index) {
|
|
510
|
+
const extraSessionId = randomUUID();
|
|
511
|
+
const extraRelaySessionUrl = `${relayBaseUrl}/${extraSessionId}`;
|
|
512
|
+
let reconnectTimerForChannel = null;
|
|
513
|
+
let reconnectAttemptForChannel = 0;
|
|
514
|
+
const extraSecureTransport = createBridgeSecureTransport({
|
|
515
|
+
sessionId: extraSessionId,
|
|
516
|
+
relayUrl: relayBaseUrl,
|
|
517
|
+
deviceState,
|
|
518
|
+
onTrustedPhoneUpdate(nextDeviceState) {
|
|
519
|
+
deviceState = nextDeviceState;
|
|
520
|
+
sendRelayRegistrationUpdate(nextDeviceState);
|
|
521
|
+
sendExtraRelayRegistrationUpdate(extraRelayChannel, nextDeviceState);
|
|
522
|
+
},
|
|
523
|
+
});
|
|
524
|
+
const extraPairingSession = {
|
|
525
|
+
pairingPayload: extraSecureTransport.createPairingPayload(),
|
|
526
|
+
pairingCode: createShortPairingCode({ length: SHORT_PAIRING_CODE_LENGTH }),
|
|
527
|
+
};
|
|
528
|
+
const extraRelayChannel = {
|
|
529
|
+
label: `extra-${index}`,
|
|
530
|
+
pairingSession: extraPairingSession,
|
|
531
|
+
secureTransport: extraSecureTransport,
|
|
532
|
+
socket: null,
|
|
533
|
+
closing: false,
|
|
534
|
+
sendWireMessage(wireMessage) {
|
|
535
|
+
if (extraRelayChannel.socket?.readyState !== WebSocket.OPEN) {
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
extraRelayChannel.socket.send(wireMessage);
|
|
540
|
+
return true;
|
|
541
|
+
},
|
|
542
|
+
close() {
|
|
543
|
+
extraRelayChannel.closing = true;
|
|
544
|
+
if (reconnectTimerForChannel) {
|
|
545
|
+
clearTimeout(reconnectTimerForChannel);
|
|
546
|
+
reconnectTimerForChannel = null;
|
|
547
|
+
}
|
|
548
|
+
if (
|
|
549
|
+
extraRelayChannel.socket?.readyState === WebSocket.OPEN
|
|
550
|
+
|| extraRelayChannel.socket?.readyState === WebSocket.CONNECTING
|
|
551
|
+
) {
|
|
552
|
+
extraRelayChannel.socket.close();
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
};
|
|
556
|
+
relayChannels.push(extraRelayChannel);
|
|
557
|
+
|
|
558
|
+
if (printPairingQr) {
|
|
559
|
+
console.error(`[remodex] Pair device ${index + 1}: scan this QR from your other device.`);
|
|
560
|
+
printQR(extraPairingSession);
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
connectExtraRelay();
|
|
564
|
+
|
|
565
|
+
function scheduleExtraReconnect() {
|
|
566
|
+
if (isShuttingDown || extraRelayChannel.closing || reconnectTimerForChannel) {
|
|
567
|
+
return;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
reconnectAttemptForChannel += 1;
|
|
571
|
+
const delayMs = Math.min(1_000 * reconnectAttemptForChannel, 5_000);
|
|
572
|
+
reconnectTimerForChannel = setTimeout(() => {
|
|
573
|
+
reconnectTimerForChannel = null;
|
|
574
|
+
connectExtraRelay();
|
|
575
|
+
}, delayMs);
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function connectExtraRelay() {
|
|
579
|
+
if (isShuttingDown || extraRelayChannel.closing) {
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const nextSocket = new WebSocket(extraRelaySessionUrl, {
|
|
584
|
+
headers: {
|
|
585
|
+
"x-role": "mac",
|
|
586
|
+
"x-notification-secret": notificationSecret,
|
|
587
|
+
...buildMacRegistrationHeaders(deviceState, extraPairingSession),
|
|
588
|
+
},
|
|
589
|
+
});
|
|
590
|
+
extraRelayChannel.socket = nextSocket;
|
|
591
|
+
|
|
592
|
+
nextSocket.on("open", () => {
|
|
593
|
+
reconnectAttemptForChannel = 0;
|
|
594
|
+
extraSecureTransport.bindLiveSendWireMessage(extraRelayChannel.sendWireMessage);
|
|
595
|
+
sendExtraRelayRegistrationUpdate(extraRelayChannel, deviceState);
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
nextSocket.on("message", (data) => {
|
|
599
|
+
const message = typeof data === "string" ? data : data.toString("utf8");
|
|
600
|
+
extraSecureTransport.handleIncomingWireMessage(message, {
|
|
601
|
+
sendControlMessage(controlMessage) {
|
|
602
|
+
if (nextSocket.readyState === WebSocket.OPEN) {
|
|
603
|
+
nextSocket.send(JSON.stringify(controlMessage));
|
|
604
|
+
}
|
|
605
|
+
},
|
|
606
|
+
onApplicationMessage(plaintextMessage) {
|
|
607
|
+
handleApplicationMessage(plaintextMessage, extraRelayChannel);
|
|
608
|
+
},
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
nextSocket.on("close", () => {
|
|
613
|
+
if (extraRelayChannel.socket === nextSocket) {
|
|
614
|
+
extraRelayChannel.socket = null;
|
|
615
|
+
}
|
|
616
|
+
scheduleExtraReconnect();
|
|
617
|
+
});
|
|
618
|
+
|
|
619
|
+
nextSocket.on("error", () => {});
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
function closeExtraRelayChannels() {
|
|
624
|
+
for (const relayChannel of relayChannels) {
|
|
625
|
+
if (relayChannel !== primaryRelayChannel && typeof relayChannel.close === "function") {
|
|
626
|
+
relayChannel.close();
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
function sendExtraRelayRegistrationUpdate(relayChannel, nextDeviceState) {
|
|
632
|
+
if (!relayChannel?.socket || relayChannel.socket.readyState !== WebSocket.OPEN) {
|
|
633
|
+
return;
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
relayChannel.socket.send(JSON.stringify({
|
|
637
|
+
kind: "relayMacRegistration",
|
|
638
|
+
registration: buildMacRegistration(nextDeviceState, relayChannel.pairingSession),
|
|
639
|
+
}));
|
|
640
|
+
}
|
|
641
|
+
|
|
488
642
|
const pairingPayload = secureTransport.createPairingPayload();
|
|
489
643
|
const pairingSession = {
|
|
490
644
|
pairingPayload,
|
|
491
645
|
pairingCode: createShortPairingCode({ length: SHORT_PAIRING_CODE_LENGTH }),
|
|
492
646
|
};
|
|
647
|
+
primaryRelayChannel = {
|
|
648
|
+
label: "primary",
|
|
649
|
+
pairingSession,
|
|
650
|
+
secureTransport,
|
|
651
|
+
sendWireMessage: sendRelayWireMessage,
|
|
652
|
+
};
|
|
653
|
+
relayChannels.push(primaryRelayChannel);
|
|
493
654
|
onPairingSession?.(pairingSession);
|
|
494
655
|
if (printPairingQr) {
|
|
656
|
+
if (extraRelaySessionCount > 0) {
|
|
657
|
+
console.error("[remodex] Pair device 1: scan this QR from your first device.");
|
|
658
|
+
}
|
|
495
659
|
printQR(pairingSession);
|
|
496
660
|
}
|
|
497
661
|
pushServiceClient.logUnavailable();
|
|
498
662
|
connectRelay();
|
|
663
|
+
startExtraRelayChannels();
|
|
499
664
|
|
|
500
665
|
codex.onMessage((message) => {
|
|
501
666
|
if (handleBridgeManagedCodexResponse(message)) {
|
|
@@ -506,10 +671,7 @@ function startBridge({
|
|
|
506
671
|
desktopRefresher.handleOutbound(message);
|
|
507
672
|
pushNotificationTracker.handleOutbound(message);
|
|
508
673
|
rememberThreadFromMessage("codex", message);
|
|
509
|
-
|
|
510
|
-
sanitizeRelayBoundCodexMessage(message),
|
|
511
|
-
sendRelayWireMessage
|
|
512
|
-
);
|
|
674
|
+
sendCodexOutboundToMobile(message);
|
|
513
675
|
});
|
|
514
676
|
|
|
515
677
|
codex.onClose(() => {
|
|
@@ -532,9 +694,11 @@ function startBridge({
|
|
|
532
694
|
desktopRefresher.handleTransportReset();
|
|
533
695
|
failBridgeManagedCodexRequests(new Error("Codex transport closed before the bridge request completed."));
|
|
534
696
|
forwardedRequestMethodsById.clear();
|
|
697
|
+
codexResponseRoutesById.clear();
|
|
535
698
|
if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) {
|
|
536
699
|
socket.close();
|
|
537
700
|
}
|
|
701
|
+
closeExtraRelayChannels();
|
|
538
702
|
});
|
|
539
703
|
|
|
540
704
|
process.on("SIGINT", () => shutdown(codex, () => socket, () => {
|
|
@@ -543,6 +707,7 @@ function startBridge({
|
|
|
543
707
|
clearReconnectTimer();
|
|
544
708
|
clearRelayWatchdog();
|
|
545
709
|
clearBridgeStatusHeartbeat();
|
|
710
|
+
closeExtraRelayChannels();
|
|
546
711
|
}));
|
|
547
712
|
process.on("SIGTERM", () => shutdown(codex, () => socket, () => {
|
|
548
713
|
isShuttingDown = true;
|
|
@@ -550,35 +715,37 @@ function startBridge({
|
|
|
550
715
|
clearReconnectTimer();
|
|
551
716
|
clearRelayWatchdog();
|
|
552
717
|
clearBridgeStatusHeartbeat();
|
|
718
|
+
closeExtraRelayChannels();
|
|
553
719
|
}));
|
|
554
720
|
|
|
555
721
|
// Routes decrypted app payloads through the same bridge handlers as before.
|
|
556
|
-
function handleApplicationMessage(rawMessage) {
|
|
557
|
-
|
|
722
|
+
function handleApplicationMessage(rawMessage, relayChannel = primaryRelayChannel) {
|
|
723
|
+
const sendResponse = (responseMessage) => sendApplicationResponseToChannel(responseMessage, relayChannel);
|
|
724
|
+
if (handleBridgeManagedHandshakeMessage(rawMessage, sendResponse)) {
|
|
558
725
|
return;
|
|
559
726
|
}
|
|
560
|
-
if (handleBridgeManagedAccountRequest(rawMessage,
|
|
727
|
+
if (handleBridgeManagedAccountRequest(rawMessage, sendResponse)) {
|
|
561
728
|
return;
|
|
562
729
|
}
|
|
563
|
-
if (voiceHandler.handleVoiceRequest(rawMessage,
|
|
730
|
+
if (voiceHandler.handleVoiceRequest(rawMessage, sendResponse)) {
|
|
564
731
|
return;
|
|
565
732
|
}
|
|
566
|
-
if (handleThreadContextRequest(rawMessage,
|
|
733
|
+
if (handleThreadContextRequest(rawMessage, sendResponse)) {
|
|
567
734
|
return;
|
|
568
735
|
}
|
|
569
|
-
if (handleWorkspaceRequest(rawMessage,
|
|
736
|
+
if (handleWorkspaceRequest(rawMessage, sendResponse)) {
|
|
570
737
|
return;
|
|
571
738
|
}
|
|
572
|
-
if (handleProjectRequest(rawMessage,
|
|
739
|
+
if (handleProjectRequest(rawMessage, sendResponse)) {
|
|
573
740
|
return;
|
|
574
741
|
}
|
|
575
|
-
if (handlePetRequest(rawMessage,
|
|
742
|
+
if (handlePetRequest(rawMessage, sendResponse)) {
|
|
576
743
|
return;
|
|
577
744
|
}
|
|
578
|
-
if (notificationsHandler.handleNotificationsRequest(rawMessage,
|
|
745
|
+
if (notificationsHandler.handleNotificationsRequest(rawMessage, sendResponse)) {
|
|
579
746
|
return;
|
|
580
747
|
}
|
|
581
|
-
if (handleDesktopRequest(rawMessage,
|
|
748
|
+
if (handleDesktopRequest(rawMessage, sendResponse, {
|
|
582
749
|
bundleId: config.codexBundleId,
|
|
583
750
|
appPath: config.codexAppPath,
|
|
584
751
|
readBridgePreferences,
|
|
@@ -586,7 +753,7 @@ function startBridge({
|
|
|
586
753
|
})) {
|
|
587
754
|
return;
|
|
588
755
|
}
|
|
589
|
-
if (handleGitRequest(rawMessage,
|
|
756
|
+
if (handleGitRequest(rawMessage, sendResponse, {
|
|
590
757
|
codexAppPath: config.codexAppPath,
|
|
591
758
|
onThreadNameSet: sendThreadNameUpdatedNotification,
|
|
592
759
|
})) {
|
|
@@ -597,22 +764,189 @@ function startBridge({
|
|
|
597
764
|
if (desktopIpcActionFollower?.observeInbound(rawMessage)) {
|
|
598
765
|
return;
|
|
599
766
|
}
|
|
600
|
-
if (handleBridgeManagedThreadTurnsListRequest(rawMessage)) {
|
|
767
|
+
if (handleBridgeManagedThreadTurnsListRequest(rawMessage, sendResponse)) {
|
|
601
768
|
return;
|
|
602
769
|
}
|
|
603
|
-
|
|
770
|
+
const codexMessage = prepareCodexForwardMessage(rawMessage, relayChannel);
|
|
771
|
+
rememberForwardedRequestMethod(codexMessage);
|
|
604
772
|
rememberThreadFromMessage("phone", rawMessage);
|
|
605
|
-
|
|
773
|
+
mirrorUserMessageToPeerDevices(rawMessage, relayChannel);
|
|
774
|
+
codex.send(codexMessage);
|
|
606
775
|
}
|
|
607
776
|
|
|
608
777
|
// Encrypts bridge-generated responses instead of letting the relay see plaintext.
|
|
609
778
|
function sendApplicationResponse(rawMessage) {
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
779
|
+
sendApplicationResponseToChannels(rawMessage, relayChannels);
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
function sendApplicationResponseToChannel(rawMessage, relayChannel = primaryRelayChannel) {
|
|
783
|
+
sendApplicationResponseToChannels(rawMessage, [relayChannel]);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
function sendApplicationResponseToChannels(rawMessage, channels) {
|
|
787
|
+
const normalizedChannels = channels.filter(Boolean);
|
|
788
|
+
if (normalizedChannels.length === 0) {
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
const sanitizedMessage = sanitizeRelayBoundCodexMessage(rawMessage);
|
|
793
|
+
for (const relayChannel of normalizedChannels) {
|
|
794
|
+
queueSanitizedApplicationMessageToChannel(sanitizedMessage, relayChannel);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function queueSanitizedApplicationMessageToChannel(sanitizedMessage, relayChannel) {
|
|
799
|
+
if (!relayChannel) {
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
relayChannel.secureTransport.queueOutboundApplicationMessage(
|
|
804
|
+
sanitizedMessage,
|
|
805
|
+
relayChannel.sendWireMessage
|
|
613
806
|
);
|
|
614
807
|
}
|
|
615
808
|
|
|
809
|
+
// Rewrites mobile request ids per relay channel so iPhone/iPad can use overlapping JSON-RPC ids safely.
|
|
810
|
+
function prepareCodexForwardMessage(rawMessage, relayChannel = primaryRelayChannel) {
|
|
811
|
+
const parsed = safeParseJSON(rawMessage);
|
|
812
|
+
if (!parsed || parsed.id == null || !relayChannel?.label) {
|
|
813
|
+
return rawMessage;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
pruneExpiredCodexResponseRoutes();
|
|
817
|
+
const originalId = parsed.id;
|
|
818
|
+
const forwardedId = `mobile:${relayChannel.label}:${randomBytes(8).toString("hex")}`;
|
|
819
|
+
if (parsed.method === "initialize") {
|
|
820
|
+
forwardedInitializeRequestIds.delete(String(originalId));
|
|
821
|
+
forwardedInitializeRequestIds.add(String(forwardedId));
|
|
822
|
+
}
|
|
823
|
+
parsed.id = forwardedId;
|
|
824
|
+
codexResponseRoutesById.set(String(forwardedId), {
|
|
825
|
+
relayChannel,
|
|
826
|
+
originalId,
|
|
827
|
+
createdAt: Date.now(),
|
|
828
|
+
});
|
|
829
|
+
return JSON.stringify(parsed);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function sendCodexOutboundToMobile(rawMessage) {
|
|
833
|
+
pruneExpiredCodexResponseRoutes();
|
|
834
|
+
const parsed = safeParseJSON(rawMessage);
|
|
835
|
+
const responseId = parsed?.id;
|
|
836
|
+
if (responseId != null) {
|
|
837
|
+
const route = codexResponseRoutesById.get(String(responseId));
|
|
838
|
+
if (route) {
|
|
839
|
+
codexResponseRoutesById.delete(String(responseId));
|
|
840
|
+
const sanitizedMessage = sanitizeRelayBoundCodexMessage(rawMessage);
|
|
841
|
+
const sanitizedParsed = safeParseJSON(sanitizedMessage);
|
|
842
|
+
if (sanitizedParsed && typeof sanitizedParsed === "object") {
|
|
843
|
+
sanitizedParsed.id = route.originalId;
|
|
844
|
+
queueSanitizedApplicationMessageToChannel(JSON.stringify(sanitizedParsed), route.relayChannel);
|
|
845
|
+
return;
|
|
846
|
+
}
|
|
847
|
+
queueSanitizedApplicationMessageToChannel(sanitizedMessage, route.relayChannel);
|
|
848
|
+
return;
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
sendApplicationResponse(rawMessage);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function pruneExpiredCodexResponseRoutes() {
|
|
856
|
+
const cutoff = Date.now() - forwardedRequestMethodTTLms;
|
|
857
|
+
for (const [requestId, route] of codexResponseRoutesById.entries()) {
|
|
858
|
+
if (!route || route.createdAt < cutoff) {
|
|
859
|
+
codexResponseRoutesById.delete(requestId);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
// Keeps secondary devices' timelines ordered by echoing the user's prompt before assistant deltas arrive.
|
|
865
|
+
function mirrorUserMessageToPeerDevices(rawMessage, originRelayChannel) {
|
|
866
|
+
const mirrorNotification = buildPeerUserMessageNotification(rawMessage);
|
|
867
|
+
if (!mirrorNotification) {
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
const peerChannels = relayChannels.filter((relayChannel) => relayChannel !== originRelayChannel);
|
|
872
|
+
if (peerChannels.length === 0) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
sendApplicationResponseToChannels(JSON.stringify(mirrorNotification), peerChannels);
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
function buildPeerUserMessageNotification(rawMessage) {
|
|
880
|
+
const parsed = safeParseJSON(rawMessage);
|
|
881
|
+
const method = typeof parsed?.method === "string" ? parsed.method.trim() : "";
|
|
882
|
+
if (method !== "turn/start" && method !== "turn/steer") {
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const params = parsed?.params && typeof parsed.params === "object" ? parsed.params : null;
|
|
887
|
+
const threadId = readString(params?.threadId || params?.thread_id);
|
|
888
|
+
const text = extractTextFromTurnPayload(params);
|
|
889
|
+
if (!threadId || !text) {
|
|
890
|
+
return null;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const turnId = readString(params?.turnId || params?.turn_id || params?.expectedTurnId || params?.expected_turn_id);
|
|
894
|
+
return {
|
|
895
|
+
method: "codex/event/user_message",
|
|
896
|
+
params: {
|
|
897
|
+
threadId,
|
|
898
|
+
thread_id: threadId,
|
|
899
|
+
turnId: turnId || undefined,
|
|
900
|
+
turn_id: turnId || undefined,
|
|
901
|
+
message: text,
|
|
902
|
+
text,
|
|
903
|
+
source: "peer-mobile",
|
|
904
|
+
},
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function extractTextFromTurnPayload(params) {
|
|
909
|
+
const directText = readString(params?.message || params?.text || params?.prompt);
|
|
910
|
+
if (directText) {
|
|
911
|
+
return directText;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
return extractTextFromTurnInput(params?.input);
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function extractTextFromTurnInput(input) {
|
|
918
|
+
if (typeof input === "string") {
|
|
919
|
+
return readString(input);
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
if (input && typeof input === "object" && !Array.isArray(input)) {
|
|
923
|
+
const directText = readString(input.text || input.message || input.content);
|
|
924
|
+
if (directText) {
|
|
925
|
+
return directText;
|
|
926
|
+
}
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const inputItems = Array.isArray(input)
|
|
930
|
+
? input
|
|
931
|
+
: Array.isArray(input?.items)
|
|
932
|
+
? input.items
|
|
933
|
+
: [];
|
|
934
|
+
const textParts = [];
|
|
935
|
+
for (const item of inputItems) {
|
|
936
|
+
if (!item || typeof item !== "object") {
|
|
937
|
+
continue;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const itemType = readString(item.type).toLowerCase();
|
|
941
|
+
const itemText = readString(item.text || item.message || item.content);
|
|
942
|
+
if ((itemType === "text" || itemType === "input_text" || itemType === "message") && itemText) {
|
|
943
|
+
textParts.push(itemText);
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
return readString(textParts.join("\n\n"));
|
|
948
|
+
}
|
|
949
|
+
|
|
616
950
|
// Mirrors accepted local renames back to the phone using the existing push-event shape.
|
|
617
951
|
function sendThreadNameUpdatedNotification(result) {
|
|
618
952
|
const threadId = readString(result?.threadId || result?.thread_id);
|
|
@@ -632,7 +966,7 @@ function startBridge({
|
|
|
632
966
|
}));
|
|
633
967
|
}
|
|
634
968
|
|
|
635
|
-
function handleBridgeManagedThreadTurnsListRequest(rawMessage) {
|
|
969
|
+
function handleBridgeManagedThreadTurnsListRequest(rawMessage, sendResponse = sendApplicationResponse) {
|
|
636
970
|
const request = parseAdaptiveThreadTurnsListRequest(rawMessage);
|
|
637
971
|
if (!request) {
|
|
638
972
|
return false;
|
|
@@ -644,13 +978,14 @@ function startBridge({
|
|
|
644
978
|
const response = await fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
645
979
|
fetchPage: (params) => sendCodexRequest("thread/turns/list", params),
|
|
646
980
|
});
|
|
981
|
+
const fallbackResponse = maybeBuildJsonlThreadTurnsListFallback(request, response);
|
|
647
982
|
relaySanitizedResponseMethodsById.set(String(request.id), {
|
|
648
983
|
method: "thread/turns/list",
|
|
649
984
|
createdAt: Date.now(),
|
|
650
985
|
});
|
|
651
|
-
|
|
986
|
+
sendResponse(JSON.stringify(fallbackResponse ?? response));
|
|
652
987
|
} catch (error) {
|
|
653
|
-
|
|
988
|
+
sendResponse(createJsonRpcErrorResponse(
|
|
654
989
|
request.id,
|
|
655
990
|
error,
|
|
656
991
|
"thread_turns_list_failed"
|
|
@@ -661,6 +996,44 @@ function startBridge({
|
|
|
661
996
|
return true;
|
|
662
997
|
}
|
|
663
998
|
|
|
999
|
+
function maybeBuildJsonlThreadTurnsListFallback(request, response) {
|
|
1000
|
+
if (!isEmptyTurnsListResponse(response)) {
|
|
1001
|
+
return null;
|
|
1002
|
+
}
|
|
1003
|
+
|
|
1004
|
+
const params = request?.params || {};
|
|
1005
|
+
const threadId = normalizeNonEmptyString(params.threadId)
|
|
1006
|
+
|| normalizeNonEmptyString(params.thread_id);
|
|
1007
|
+
if (!threadId || hasRelayCursor(params.cursor)) {
|
|
1008
|
+
return null;
|
|
1009
|
+
}
|
|
1010
|
+
|
|
1011
|
+
try {
|
|
1012
|
+
const rolloutPath = findRecentRolloutFileForContextRead(resolveSessionsRoot(), { threadId });
|
|
1013
|
+
if (!rolloutPath) {
|
|
1014
|
+
return null;
|
|
1015
|
+
}
|
|
1016
|
+
const result = readThreadTurnsListPageFromSessionJsonl(rolloutPath, {
|
|
1017
|
+
threadId,
|
|
1018
|
+
limit: params.limit,
|
|
1019
|
+
maxLimit: 1,
|
|
1020
|
+
cursor: params.cursor,
|
|
1021
|
+
});
|
|
1022
|
+
const turnsKey = findTurnsListResultKey(result);
|
|
1023
|
+
if (!turnsKey || result[turnsKey].length === 0) {
|
|
1024
|
+
return null;
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
return {
|
|
1028
|
+
id: request.id,
|
|
1029
|
+
result,
|
|
1030
|
+
};
|
|
1031
|
+
} catch (error) {
|
|
1032
|
+
console.warn(`[remodex] thread/turns/list jsonl fallback failed: ${error.message}`);
|
|
1033
|
+
return null;
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
664
1037
|
// ─── Bridge-owned auth snapshot ─────────────────────────────
|
|
665
1038
|
|
|
666
1039
|
// Handles the bridge-owned auth status wrappers without exposing tokens to the phone.
|
|
@@ -819,19 +1192,26 @@ function startBridge({
|
|
|
819
1192
|
// Replaces huge inline desktop-history images with lightweight references before relay encryption.
|
|
820
1193
|
function sanitizeRelayBoundCodexMessage(rawMessage) {
|
|
821
1194
|
pruneExpiredForwardedRequestMethods();
|
|
822
|
-
const
|
|
1195
|
+
const normalizedMessage = normalizeRelayBoundJsonRpcMessage(rawMessage, {
|
|
1196
|
+
pendingRequestMethodsById: relaySanitizedResponseMethodsById,
|
|
1197
|
+
});
|
|
1198
|
+
if (!normalizedMessage) {
|
|
1199
|
+
return null;
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
const parsed = safeParseJSON(normalizedMessage);
|
|
823
1203
|
const responseId = parsed?.id;
|
|
824
1204
|
if (responseId == null) {
|
|
825
|
-
return sanitizeLiveGeneratedImageMessageForRelay(
|
|
1205
|
+
return sanitizeLiveGeneratedImageMessageForRelay(normalizedMessage);
|
|
826
1206
|
}
|
|
827
1207
|
|
|
828
1208
|
const trackedRequest = relaySanitizedResponseMethodsById.get(String(responseId));
|
|
829
1209
|
if (!trackedRequest) {
|
|
830
|
-
return
|
|
1210
|
+
return normalizedMessage;
|
|
831
1211
|
}
|
|
832
1212
|
relaySanitizedResponseMethodsById.delete(String(responseId));
|
|
833
1213
|
|
|
834
|
-
return sanitizeThreadHistoryImagesForRelay(
|
|
1214
|
+
return sanitizeThreadHistoryImagesForRelay(normalizedMessage, trackedRequest.method);
|
|
835
1215
|
}
|
|
836
1216
|
|
|
837
1217
|
function updatePendingAuthLoginFromCodexMessage(rawMessage) {
|
|
@@ -982,7 +1362,7 @@ function startBridge({
|
|
|
982
1362
|
// The spawned/shared Codex app-server stays warm across phone reconnects.
|
|
983
1363
|
// When iPhone reconnects it sends initialize again, but forwarding that to the
|
|
984
1364
|
// already-initialized Codex transport only produces "Already initialized".
|
|
985
|
-
function handleBridgeManagedHandshakeMessage(rawMessage) {
|
|
1365
|
+
function handleBridgeManagedHandshakeMessage(rawMessage, sendResponse = sendApplicationResponse) {
|
|
986
1366
|
let parsed = null;
|
|
987
1367
|
try {
|
|
988
1368
|
parsed = JSON.parse(rawMessage);
|
|
@@ -998,7 +1378,7 @@ function startBridge({
|
|
|
998
1378
|
if (method === "initialize" && parsed.id != null) {
|
|
999
1379
|
const compatibilityError = bridgeManagedInitializeCompatibilityError(parsed.params || {});
|
|
1000
1380
|
if (compatibilityError) {
|
|
1001
|
-
|
|
1381
|
+
sendResponse(JSON.stringify({
|
|
1002
1382
|
id: parsed.id,
|
|
1003
1383
|
error: compatibilityError,
|
|
1004
1384
|
}));
|
|
@@ -1010,7 +1390,7 @@ function startBridge({
|
|
|
1010
1390
|
return false;
|
|
1011
1391
|
}
|
|
1012
1392
|
|
|
1013
|
-
|
|
1393
|
+
sendResponse(JSON.stringify({
|
|
1014
1394
|
id: parsed.id,
|
|
1015
1395
|
result: {
|
|
1016
1396
|
bridgeManaged: true,
|
|
@@ -1174,10 +1554,21 @@ function startBridge({
|
|
|
1174
1554
|
return true;
|
|
1175
1555
|
}
|
|
1176
1556
|
|
|
1177
|
-
waiter.resolve(parsed
|
|
1557
|
+
waiter.resolve(readBridgeManagedSuccessPayload(parsed));
|
|
1178
1558
|
return true;
|
|
1179
1559
|
}
|
|
1180
1560
|
|
|
1561
|
+
// Normalizes private app-server responses before the bridge re-wraps them for iOS.
|
|
1562
|
+
function readBridgeManagedSuccessPayload(parsed) {
|
|
1563
|
+
if (Object.prototype.hasOwnProperty.call(parsed, "result")) {
|
|
1564
|
+
return parsed.result ?? null;
|
|
1565
|
+
}
|
|
1566
|
+
if (Object.prototype.hasOwnProperty.call(parsed, "payload")) {
|
|
1567
|
+
return parsed.payload ?? null;
|
|
1568
|
+
}
|
|
1569
|
+
return null;
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1181
1572
|
function failBridgeManagedCodexRequests(error) {
|
|
1182
1573
|
for (const waiter of bridgeManagedCodexRequestWaiters.values()) {
|
|
1183
1574
|
clearTimeout(waiter.timeout);
|
|
@@ -1199,6 +1590,11 @@ function startBridge({
|
|
|
1199
1590
|
function sendRelayRegistrationUpdate(nextDeviceState) {
|
|
1200
1591
|
deviceState = nextDeviceState;
|
|
1201
1592
|
if (socket?.readyState !== WebSocket.OPEN) {
|
|
1593
|
+
for (const relayChannel of relayChannels) {
|
|
1594
|
+
if (relayChannel !== primaryRelayChannel) {
|
|
1595
|
+
sendExtraRelayRegistrationUpdate(relayChannel, nextDeviceState);
|
|
1596
|
+
}
|
|
1597
|
+
}
|
|
1202
1598
|
return;
|
|
1203
1599
|
}
|
|
1204
1600
|
|
|
@@ -1206,6 +1602,11 @@ function startBridge({
|
|
|
1206
1602
|
kind: "relayMacRegistration",
|
|
1207
1603
|
registration: buildMacRegistration(nextDeviceState, pairingSession),
|
|
1208
1604
|
}));
|
|
1605
|
+
for (const relayChannel of relayChannels) {
|
|
1606
|
+
if (relayChannel !== primaryRelayChannel) {
|
|
1607
|
+
sendExtraRelayRegistrationUpdate(relayChannel, nextDeviceState);
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1209
1610
|
}
|
|
1210
1611
|
|
|
1211
1612
|
function readBridgePreferences() {
|
|
@@ -1329,7 +1730,7 @@ function createMacOSBridgeWakeAssertion({
|
|
|
1329
1730
|
};
|
|
1330
1731
|
}
|
|
1331
1732
|
|
|
1332
|
-
// Registers the canonical Mac identity
|
|
1733
|
+
// Registers the canonical Mac identity; legacy relay headers can expose one trusted device for auto-resolve.
|
|
1333
1734
|
function buildMacRegistrationHeaders(deviceState, pairingSession) {
|
|
1334
1735
|
const registration = buildMacRegistration(deviceState, pairingSession);
|
|
1335
1736
|
const headers = {
|
|
@@ -1347,6 +1748,20 @@ function buildMacRegistrationHeaders(deviceState, pairingSession) {
|
|
|
1347
1748
|
return headers;
|
|
1348
1749
|
}
|
|
1349
1750
|
|
|
1751
|
+
function readExtraRelaySessionCount(env = process.env) {
|
|
1752
|
+
const rawValue = readString(env.REMODEX_EXTRA_RELAY_SESSIONS || env.PHODEX_EXTRA_RELAY_SESSIONS);
|
|
1753
|
+
if (!rawValue) {
|
|
1754
|
+
return 0;
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
const parsed = Number.parseInt(rawValue, 10);
|
|
1758
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1759
|
+
return 0;
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
return Math.min(parsed, 3);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1350
1765
|
function buildMacRegistration(deviceState, pairingSession) {
|
|
1351
1766
|
const trustedPhoneEntry = Object.entries(deviceState?.trustedPhones || {})[0] || null;
|
|
1352
1767
|
return {
|
|
@@ -1535,7 +1950,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
1535
1950
|
|
|
1536
1951
|
const params = request?.params;
|
|
1537
1952
|
const requestedLimit = Number.isInteger(params?.limit) && params.limit > 0
|
|
1538
|
-
? params.limit
|
|
1953
|
+
? Math.min(params.limit, RELAY_TURNS_LIST_MAX_INITIAL_LIMIT)
|
|
1539
1954
|
: 1;
|
|
1540
1955
|
const startedAt = now();
|
|
1541
1956
|
let nextCursor = params?.cursor;
|
|
@@ -1557,17 +1972,24 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
1557
1972
|
if (response) {
|
|
1558
1973
|
return response;
|
|
1559
1974
|
}
|
|
1560
|
-
|
|
1975
|
+
return await fetchSafeThreadTurnsListFallback(request, {
|
|
1976
|
+
fetchPage,
|
|
1977
|
+
now,
|
|
1978
|
+
sanitizeForRelay,
|
|
1979
|
+
payloadSoftLimitBytes,
|
|
1980
|
+
});
|
|
1561
1981
|
}
|
|
1562
1982
|
|
|
1563
|
-
const pageResult = page.result;
|
|
1983
|
+
const pageResult = unwrapAppServerPayloadResult(page.result);
|
|
1564
1984
|
const pageTurnsKey = findTurnsListResultKey(pageResult);
|
|
1565
1985
|
if (!pageTurnsKey) {
|
|
1566
1986
|
if (!response) {
|
|
1567
|
-
return {
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1987
|
+
return await fetchSafeThreadTurnsListFallback(request, {
|
|
1988
|
+
fetchPage,
|
|
1989
|
+
now,
|
|
1990
|
+
sanitizeForRelay,
|
|
1991
|
+
payloadSoftLimitBytes,
|
|
1992
|
+
});
|
|
1571
1993
|
}
|
|
1572
1994
|
return response;
|
|
1573
1995
|
}
|
|
@@ -1582,10 +2004,21 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
1582
2004
|
|
|
1583
2005
|
const pageTurns = pageResult[pageTurnsKey];
|
|
1584
2006
|
combinedTurns = combinedTurns.concat(pageTurns);
|
|
1585
|
-
response =
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
2007
|
+
response = buildSafeTurnsListResponse(request.id, firstResult, lastResult, turnsKey, combinedTurns);
|
|
2008
|
+
|
|
2009
|
+
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) >= payloadSoftLimitBytes) {
|
|
2010
|
+
response = buildLargestSafeTurnsListResponse({
|
|
2011
|
+
requestId: request.id,
|
|
2012
|
+
firstResult,
|
|
2013
|
+
lastResult,
|
|
2014
|
+
turnsKey,
|
|
2015
|
+
turns: combinedTurns,
|
|
2016
|
+
maxTurns: RELAY_TURNS_LIST_SAFE_RETRY_LIMIT,
|
|
2017
|
+
sanitizeForRelay,
|
|
2018
|
+
payloadSoftLimitBytes,
|
|
2019
|
+
}) ?? buildEmptyTurnsListResponse(request);
|
|
2020
|
+
break;
|
|
2021
|
+
}
|
|
1589
2022
|
|
|
1590
2023
|
nextCursor = readTurnsListNextCursor(pageResult);
|
|
1591
2024
|
if (combinedTurns.length >= requestedLimit || !hasRelayCursor(nextCursor) || pageTurns.length === 0) {
|
|
@@ -1614,6 +2047,64 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
1614
2047
|
};
|
|
1615
2048
|
}
|
|
1616
2049
|
|
|
2050
|
+
function buildEmptyTurnsListResponse(request) {
|
|
2051
|
+
return {
|
|
2052
|
+
id: request.id,
|
|
2053
|
+
result: {
|
|
2054
|
+
data: [],
|
|
2055
|
+
nextCursor: null,
|
|
2056
|
+
},
|
|
2057
|
+
};
|
|
2058
|
+
}
|
|
2059
|
+
|
|
2060
|
+
function isEmptyTurnsListResponse(response) {
|
|
2061
|
+
const turnsKey = findTurnsListResultKey(response?.result);
|
|
2062
|
+
return Boolean(turnsKey) && response.result[turnsKey].length === 0;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
async function fetchSafeThreadTurnsListFallback(request, {
|
|
2066
|
+
fetchPage,
|
|
2067
|
+
now,
|
|
2068
|
+
sanitizeForRelay,
|
|
2069
|
+
payloadSoftLimitBytes,
|
|
2070
|
+
}) {
|
|
2071
|
+
const params = request?.params;
|
|
2072
|
+
const requestedLimit = Number.isInteger(params?.limit) && params.limit > 0
|
|
2073
|
+
? params.limit
|
|
2074
|
+
: RELAY_TURNS_LIST_SAFE_RETRY_LIMIT;
|
|
2075
|
+
const safeLimit = Math.min(requestedLimit, RELAY_TURNS_LIST_SAFE_RETRY_LIMIT);
|
|
2076
|
+
const safeParams = buildAdaptiveTurnsListPageParams(params, safeLimit, params?.cursor);
|
|
2077
|
+
|
|
2078
|
+
try {
|
|
2079
|
+
const page = await fetchMeasuredAdaptiveTurnsListPage(fetchPage, safeParams, now);
|
|
2080
|
+
const pageResult = unwrapAppServerPayloadResult(page.result);
|
|
2081
|
+
const turnsKey = findTurnsListResultKey(pageResult);
|
|
2082
|
+
if (!turnsKey) {
|
|
2083
|
+
return buildEmptyTurnsListResponse(request);
|
|
2084
|
+
}
|
|
2085
|
+
|
|
2086
|
+
// If the normal pagination path returns a bad first page, retry once with a small page.
|
|
2087
|
+
// The retry response is intentionally minimal so Swift does not decode stale server metadata.
|
|
2088
|
+
const response = buildLargestSafeTurnsListResponse({
|
|
2089
|
+
requestId: request.id,
|
|
2090
|
+
firstResult: pageResult,
|
|
2091
|
+
lastResult: pageResult,
|
|
2092
|
+
turnsKey,
|
|
2093
|
+
turns: pageResult[turnsKey],
|
|
2094
|
+
maxTurns: safeLimit,
|
|
2095
|
+
sanitizeForRelay,
|
|
2096
|
+
payloadSoftLimitBytes,
|
|
2097
|
+
});
|
|
2098
|
+
if (response) {
|
|
2099
|
+
return response;
|
|
2100
|
+
}
|
|
2101
|
+
} catch {
|
|
2102
|
+
// Fall through to a valid empty page: the phone can keep the thread open instead of crashing.
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
return buildEmptyTurnsListResponse(request);
|
|
2106
|
+
}
|
|
2107
|
+
|
|
1617
2108
|
async function fetchMeasuredAdaptiveTurnsListPage(fetchPage, params, now) {
|
|
1618
2109
|
const startedAt = now();
|
|
1619
2110
|
const result = await fetchPage(params);
|
|
@@ -1654,13 +2145,115 @@ function findTurnsListResultKey(result) {
|
|
|
1654
2145
|
return RELAY_TURNS_LIST_RESULT_KEYS.find((key) => Array.isArray(result[key])) || null;
|
|
1655
2146
|
}
|
|
1656
2147
|
|
|
1657
|
-
function
|
|
1658
|
-
|
|
1659
|
-
|
|
2148
|
+
function buildSafeTurnsListResponse(requestId, firstResult, lastResult, turnsKey, turns) {
|
|
2149
|
+
return {
|
|
2150
|
+
id: requestId,
|
|
2151
|
+
result: buildAdaptiveTurnsListResult(firstResult, lastResult, turnsKey, turns),
|
|
1660
2152
|
};
|
|
1661
|
-
|
|
1662
|
-
|
|
2153
|
+
}
|
|
2154
|
+
|
|
2155
|
+
// Trims oversized history pages progressively: normal page -> 5 turns -> ... -> 1 turn.
|
|
2156
|
+
function buildLargestSafeTurnsListResponse({
|
|
2157
|
+
requestId,
|
|
2158
|
+
firstResult,
|
|
2159
|
+
lastResult,
|
|
2160
|
+
turnsKey,
|
|
2161
|
+
turns,
|
|
2162
|
+
maxTurns,
|
|
2163
|
+
sanitizeForRelay,
|
|
2164
|
+
payloadSoftLimitBytes,
|
|
2165
|
+
}) {
|
|
2166
|
+
const sliceLimit = Math.min(turns.length, maxTurns);
|
|
2167
|
+
for (let count = sliceLimit; count > 0; count -= 1) {
|
|
2168
|
+
const response = buildSafeTurnsListResponse(
|
|
2169
|
+
requestId,
|
|
2170
|
+
firstResult,
|
|
2171
|
+
lastResult,
|
|
2172
|
+
turnsKey,
|
|
2173
|
+
turns.slice(0, count)
|
|
2174
|
+
);
|
|
2175
|
+
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) < payloadSoftLimitBytes) {
|
|
2176
|
+
return response;
|
|
2177
|
+
}
|
|
1663
2178
|
}
|
|
2179
|
+
return buildEmergencySingleTurnResponse({
|
|
2180
|
+
requestId,
|
|
2181
|
+
lastResult,
|
|
2182
|
+
turnsKey,
|
|
2183
|
+
turn: turns[0],
|
|
2184
|
+
sanitizeForRelay,
|
|
2185
|
+
payloadSoftLimitBytes,
|
|
2186
|
+
});
|
|
2187
|
+
}
|
|
2188
|
+
|
|
2189
|
+
function buildEmergencySingleTurnResponse({
|
|
2190
|
+
requestId,
|
|
2191
|
+
lastResult,
|
|
2192
|
+
turnsKey,
|
|
2193
|
+
turn,
|
|
2194
|
+
sanitizeForRelay,
|
|
2195
|
+
payloadSoftLimitBytes,
|
|
2196
|
+
}) {
|
|
2197
|
+
if (!turn || typeof turn !== "object" || Array.isArray(turn)) {
|
|
2198
|
+
return null;
|
|
2199
|
+
}
|
|
2200
|
+
|
|
2201
|
+
for (const maxItems of [16, 4, 1]) {
|
|
2202
|
+
for (const maxChars of [
|
|
2203
|
+
RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS,
|
|
2204
|
+
Math.floor(RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS / 4),
|
|
2205
|
+
1_000,
|
|
2206
|
+
0,
|
|
2207
|
+
]) {
|
|
2208
|
+
const response = {
|
|
2209
|
+
id: requestId,
|
|
2210
|
+
result: {
|
|
2211
|
+
...buildAdaptiveTurnsListResult({}, lastResult, turnsKey, [
|
|
2212
|
+
compactEmergencySingleTurnForRelay(turn, maxChars, maxItems),
|
|
2213
|
+
]),
|
|
2214
|
+
remodexEmergencySingleTurnForRelay: true,
|
|
2215
|
+
},
|
|
2216
|
+
};
|
|
2217
|
+
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) < payloadSoftLimitBytes) {
|
|
2218
|
+
return response;
|
|
2219
|
+
}
|
|
2220
|
+
}
|
|
2221
|
+
}
|
|
2222
|
+
|
|
2223
|
+
return null;
|
|
2224
|
+
}
|
|
2225
|
+
|
|
2226
|
+
function compactEmergencySingleTurnForRelay(turn, maxChars, maxItems) {
|
|
2227
|
+
const safeTurn = {};
|
|
2228
|
+
for (const key of [
|
|
2229
|
+
"id",
|
|
2230
|
+
"turnId",
|
|
2231
|
+
"turn_id",
|
|
2232
|
+
"threadId",
|
|
2233
|
+
"thread_id",
|
|
2234
|
+
"createdAt",
|
|
2235
|
+
"created_at",
|
|
2236
|
+
"completedAt",
|
|
2237
|
+
"completed_at",
|
|
2238
|
+
"status",
|
|
2239
|
+
"role",
|
|
2240
|
+
"kind",
|
|
2241
|
+
]) {
|
|
2242
|
+
const value = turn[key];
|
|
2243
|
+
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
2244
|
+
safeTurn[key] = value;
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
|
|
2248
|
+
const items = Array.isArray(turn.items) ? turn.items : [];
|
|
2249
|
+
safeTurn.items = items.slice(-maxItems).map((item) => compactHistoryItemForRelay(item, maxChars));
|
|
2250
|
+
safeTurn.remodexEmergencySingleTurnForRelay = true;
|
|
2251
|
+
safeTurn.remodexPageCompactedForRelay = true;
|
|
2252
|
+
return safeTurn;
|
|
2253
|
+
}
|
|
2254
|
+
|
|
2255
|
+
function buildAdaptiveTurnsListResult(firstResult, lastResult, turnsKey, turns) {
|
|
2256
|
+
const result = {};
|
|
1664
2257
|
result[turnsKey] = turns;
|
|
1665
2258
|
|
|
1666
2259
|
for (const key of RELAY_TURNS_LIST_PAGINATION_RESULT_KEYS) {
|
|
@@ -1709,6 +2302,108 @@ function measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay) {
|
|
|
1709
2302
|
}
|
|
1710
2303
|
}
|
|
1711
2304
|
|
|
2305
|
+
// Keeps app-server responses in the JSON-RPC shape that the App Store iOS client decodes.
|
|
2306
|
+
function normalizeRelayBoundJsonRpcMessage(rawMessage, {
|
|
2307
|
+
pendingRequestMethodsById = null,
|
|
2308
|
+
} = {}) {
|
|
2309
|
+
const parsed = parseBridgeJSON(rawMessage);
|
|
2310
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2311
|
+
return null;
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
const hasMethod = typeof parsed.method === "string" && parsed.method.length > 0;
|
|
2315
|
+
const hasResponseId = parsed.id !== undefined && parsed.id !== null;
|
|
2316
|
+
const hasResult = Object.prototype.hasOwnProperty.call(parsed, "result");
|
|
2317
|
+
const hasError = Object.prototype.hasOwnProperty.call(parsed, "error");
|
|
2318
|
+
const hasPayload = Object.prototype.hasOwnProperty.call(parsed, "payload");
|
|
2319
|
+
if (hasResponseId && !hasMethod && !hasResult && !hasError && hasPayload) {
|
|
2320
|
+
const { payload, ...rest } = parsed;
|
|
2321
|
+
return JSON.stringify({
|
|
2322
|
+
...rest,
|
|
2323
|
+
result: payload ?? null,
|
|
2324
|
+
});
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
if (hasResponseId && !hasMethod && hasResult && !hasError) {
|
|
2328
|
+
const unwrappedResult = unwrapAppServerPayloadResult(parsed.result);
|
|
2329
|
+
if (unwrappedResult !== parsed.result) {
|
|
2330
|
+
return JSON.stringify({
|
|
2331
|
+
...parsed,
|
|
2332
|
+
result: unwrappedResult,
|
|
2333
|
+
});
|
|
2334
|
+
}
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
if (hasMethod && hasResponseId && !isRelayBoundServerRequestMethod(parsed.method)) {
|
|
2338
|
+
const trackedRequest = pendingRequestMethodsById?.get(String(parsed.id));
|
|
2339
|
+
const isTrackedResponse = trackedRequest?.method === parsed.method
|
|
2340
|
+
&& (hasResult || hasError || hasPayload);
|
|
2341
|
+
if (isTrackedResponse) {
|
|
2342
|
+
const { method, payload, ...rest } = parsed;
|
|
2343
|
+
if (!hasResult && !hasError && hasPayload) {
|
|
2344
|
+
return JSON.stringify({
|
|
2345
|
+
...rest,
|
|
2346
|
+
result: payload ?? null,
|
|
2347
|
+
});
|
|
2348
|
+
}
|
|
2349
|
+
if (hasResult && !hasError) {
|
|
2350
|
+
return JSON.stringify({
|
|
2351
|
+
...rest,
|
|
2352
|
+
result: unwrapAppServerPayloadResult(rest.result),
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
return JSON.stringify(rest);
|
|
2356
|
+
}
|
|
2357
|
+
|
|
2358
|
+
return null;
|
|
2359
|
+
}
|
|
2360
|
+
|
|
2361
|
+
if (!hasMethod && !hasResponseId) {
|
|
2362
|
+
return null;
|
|
2363
|
+
}
|
|
2364
|
+
|
|
2365
|
+
return rawMessage;
|
|
2366
|
+
}
|
|
2367
|
+
|
|
2368
|
+
function unwrapAppServerPayloadResult(value) {
|
|
2369
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2370
|
+
return value;
|
|
2371
|
+
}
|
|
2372
|
+
if (!Object.prototype.hasOwnProperty.call(value, "payload")) {
|
|
2373
|
+
return value;
|
|
2374
|
+
}
|
|
2375
|
+
|
|
2376
|
+
const payload = value.payload;
|
|
2377
|
+
if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
|
|
2378
|
+
return value;
|
|
2379
|
+
}
|
|
2380
|
+
|
|
2381
|
+
const directPayloadKeys = [
|
|
2382
|
+
"data",
|
|
2383
|
+
"items",
|
|
2384
|
+
"threads",
|
|
2385
|
+
"turns",
|
|
2386
|
+
"thread",
|
|
2387
|
+
];
|
|
2388
|
+
const hasDirectResultPayload = directPayloadKeys.some((key) => (
|
|
2389
|
+
Object.prototype.hasOwnProperty.call(payload, key)
|
|
2390
|
+
));
|
|
2391
|
+
if (!hasDirectResultPayload) {
|
|
2392
|
+
return value;
|
|
2393
|
+
}
|
|
2394
|
+
|
|
2395
|
+
return {
|
|
2396
|
+
...value,
|
|
2397
|
+
...payload,
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
function isRelayBoundServerRequestMethod(method) {
|
|
2402
|
+
return method === "item/tool/requestUserInput"
|
|
2403
|
+
|| method === "tool/requestUserInput"
|
|
2404
|
+
|| method.endsWith("requestApproval");
|
|
2405
|
+
}
|
|
2406
|
+
|
|
1712
2407
|
// Shrinks thread history snapshots/pages for mobile relay delivery.
|
|
1713
2408
|
// This elides bulky blobs and replaces oversized older history with a compact marker.
|
|
1714
2409
|
function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod) {
|
|
@@ -2538,6 +3233,7 @@ module.exports = {
|
|
|
2538
3233
|
fetchAdaptiveThreadTurnsListForRelay,
|
|
2539
3234
|
hasRelayConnectionGoneStale,
|
|
2540
3235
|
isTerminalRelayCloseCode,
|
|
3236
|
+
normalizeRelayBoundJsonRpcMessage,
|
|
2541
3237
|
persistBridgePreferences,
|
|
2542
3238
|
sanitizeLiveGeneratedImageMessageForRelay,
|
|
2543
3239
|
sanitizeThreadHistoryImagesForRelay,
|