@makerbi/remodex 2.0.1 → 2.3.1
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/package.json +2 -2
- package/src/account-status.js +5 -4
- package/src/bridge.js +1813 -670
- package/src/codex-desktop-refresher.js +35 -7
- package/src/cursor-acp-client.js +242 -0
- package/src/cursor-models.js +134 -0
- package/src/cursor-provider.js +1197 -0
- package/src/desktop-ipc-action-follower.js +2331 -964
- package/src/desktop-ipc-conversation-adapter.js +1132 -0
- package/src/desktop-ipc-conversation-projector.js +1169 -0
- package/src/desktop-ipc-live-owner.js +1790 -0
- package/src/desktop-ipc-owner-transport.js +750 -0
- package/src/desktop-ipc-shared.js +473 -0
- package/src/desktop-ipc-state-patches.js +218 -0
- package/src/opencode-models.js +108 -0
- package/src/opencode-provider.js +1151 -0
- package/src/project-handler.js +50 -7
- package/src/project-registry.js +466 -0
- package/src/push-notification-tracker.js +4 -4
- package/src/rollout-live-mirror.js +930 -218
- package/src/rollout-turn-semantics.js +20 -0
- package/src/runtime-provider-models.js +164 -0
- package/src/runtime-provider-router.js +365 -0
- package/src/scripts/codex-refresh.applescript +26 -15
- package/src/secure-transport.js +204 -9
- package/src/session-jsonl-history.js +429 -39
- package/src/thread-context-handler.js +8 -6
- package/src/thread-runtime-settings-store.js +247 -0
- package/src/voice-audio.js +344 -0
- package/src/voice-handler.js +363 -173
package/src/bridge.js
CHANGED
|
@@ -5,7 +5,8 @@
|
|
|
5
5
|
// Depends on: ws, crypto, os, ./bridge-status, ./codex-desktop-refresher, ./codex-transport, ./rollout-watch, ./voice-handler
|
|
6
6
|
|
|
7
7
|
const WebSocket = require("ws");
|
|
8
|
-
const {
|
|
8
|
+
const { constants: bufferConstants } = require("buffer");
|
|
9
|
+
const { createHash, randomBytes } = require("crypto");
|
|
9
10
|
const { execFile, spawn } = require("child_process");
|
|
10
11
|
const fs = require("fs");
|
|
11
12
|
const path = require("path");
|
|
@@ -52,10 +53,19 @@ const {
|
|
|
52
53
|
} = require("./secure-device-state");
|
|
53
54
|
const { createBridgeSecureTransport } = require("./secure-transport");
|
|
54
55
|
const { createRolloutLiveMirrorController } = require("./rollout-live-mirror");
|
|
56
|
+
const {
|
|
57
|
+
isContextualUserText,
|
|
58
|
+
isUserRoleItem,
|
|
59
|
+
readUserItemText,
|
|
60
|
+
sanitizeUserRoleItem,
|
|
61
|
+
visibleUserPromptText,
|
|
62
|
+
} = require("./desktop-ipc-shared");
|
|
55
63
|
const {
|
|
56
64
|
createDesktopIpcActionFollower,
|
|
57
65
|
seedConversationStateFromThreadRead,
|
|
58
66
|
} = require("./desktop-ipc-action-follower");
|
|
67
|
+
const { createDesktopIpcLiveOwner } = require("./desktop-ipc-live-owner");
|
|
68
|
+
const { createThreadRuntimeSettingsStore } = require("./thread-runtime-settings-store");
|
|
59
69
|
const { version: bridgePackageVersion = "" } = require("../package.json");
|
|
60
70
|
const {
|
|
61
71
|
MINIMUM_SUPPORTED_IOS_APP_VERSION,
|
|
@@ -65,21 +75,29 @@ const {
|
|
|
65
75
|
} = require("./ios-app-compatibility");
|
|
66
76
|
const { createShortPairingCode, SHORT_PAIRING_CODE_LENGTH } = require("./qr");
|
|
67
77
|
const {
|
|
68
|
-
|
|
78
|
+
JSONL_OLDER_HANDOFF_CURSOR,
|
|
69
79
|
parseSessionJsonlTurns,
|
|
80
|
+
readRecentSessionJsonlTurns,
|
|
81
|
+
readSessionJsonlMetadataFromFile,
|
|
70
82
|
readThreadTurnsListPageFromSessionJsonl,
|
|
71
83
|
} = require("./session-jsonl-history");
|
|
72
84
|
const { buildApplyPatchFileChangeItem } = require("./apply-patch-changes");
|
|
85
|
+
const {
|
|
86
|
+
createRuntimeProviderRouter,
|
|
87
|
+
stripRuntimeProviderFieldsForCodex,
|
|
88
|
+
} = require("./runtime-provider-router");
|
|
89
|
+
const { createProjectRegistry } = require("./project-registry");
|
|
73
90
|
|
|
74
91
|
const execFileAsync = promisify(execFile);
|
|
75
92
|
const RELAY_WATCHDOG_PING_INTERVAL_MS = 10_000;
|
|
76
|
-
const CLOSE_CODE_INVALID_RELAY_REQUEST = 4000;
|
|
77
|
-
const CLOSE_CODE_REPLACED_BY_NEW_MAC = 4001;
|
|
78
|
-
const CLOSE_CODE_MAC_UNAUTHORIZED = 4005;
|
|
79
93
|
const RELAY_HISTORY_IMAGE_REFERENCE_URL = "remodex://history-image-elided";
|
|
80
94
|
const RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES = 4 * 1024 * 1024;
|
|
81
95
|
const RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS = 24_000;
|
|
82
|
-
|
|
96
|
+
// Recent-turn window used only when a thread/read payload already exceeds the
|
|
97
|
+
// relay soft budget: heavy threads first paint with this many newest turns and
|
|
98
|
+
// older history arrives via thread/turns/list pagination. Normal threads are
|
|
99
|
+
// never trimmed.
|
|
100
|
+
const RELAY_HISTORY_RECENT_TURN_TARGET = 16;
|
|
83
101
|
const RELAY_TURNS_LIST_TARGET_BUDGET_MS = 5_500;
|
|
84
102
|
const RELAY_TURNS_LIST_BUDGET_RESERVE_MS = 1_000;
|
|
85
103
|
const RELAY_TURNS_LIST_MAX_INITIAL_LIMIT = 5;
|
|
@@ -87,7 +105,22 @@ const RELAY_TURNS_LIST_SAFE_RETRY_LIMIT = 5;
|
|
|
87
105
|
const RELAY_JSONL_TURNS_LIST_CACHE_TTL_MS = 30_000;
|
|
88
106
|
const RELAY_JSONL_ARTIFACT_CACHE_TTL_MS = 2_000;
|
|
89
107
|
const RELAY_JSONL_ARTIFACT_CACHE_MAX_ENTRIES = 128;
|
|
90
|
-
|
|
108
|
+
// Session cwd is stable for a rollout file, but the same thread can later get a
|
|
109
|
+
// newer rollout with a different cwd; cache entries are validated against file identity.
|
|
110
|
+
const RELAY_JSONL_THREAD_CWD_CACHE_TTL_MS = 5 * 60_000;
|
|
111
|
+
const RELAY_JSONL_THREAD_EMPTY_CWD_CACHE_TTL_MS = 30_000;
|
|
112
|
+
const RELAY_JSONL_FAST_FIRST_PAGE_WAIT_MS = 1_500;
|
|
113
|
+
// The phone may be backgrounded between the provisional JSONL page and its
|
|
114
|
+
// canonical reconciliation. Keep the handoff long enough that a normal
|
|
115
|
+
// foreground/reconnect does not turn a coherent first page into a dead cursor.
|
|
116
|
+
const RELAY_JSONL_CANONICAL_HANDOFF_TTL_MS = 10 * 60_000;
|
|
117
|
+
const RELAY_JSONL_CANONICAL_HANDOFF_MAX_ENTRIES = 32;
|
|
118
|
+
const JSONL_CANONICAL_HANDOFF_CURSOR_PREFIX = "remodex-jsonl-handoff-v1:";
|
|
119
|
+
const RELAY_JSONL_FULL_ARTIFACT_FALLBACK_MAX_BYTES = Math.max(
|
|
120
|
+
0,
|
|
121
|
+
bufferConstants.MAX_STRING_LENGTH - (8 * 1024 * 1024)
|
|
122
|
+
);
|
|
123
|
+
const BRIDGE_PACKAGE_UPDATE_COMMAND = "npm install -g remodex@latest";
|
|
91
124
|
const BRIDGE_PACKAGE_UPDATE_TIMEOUT_MS = 180_000;
|
|
92
125
|
const BRIDGE_RESTART_AFTER_UPDATE_DELAY_MS = 750;
|
|
93
126
|
const MODELS_WITHOUT_REASONING_SUMMARY = new Set([
|
|
@@ -109,9 +142,12 @@ const RELAY_TURNS_LIST_PAGINATION_RESULT_KEYS = [
|
|
|
109
142
|
"previousCursor",
|
|
110
143
|
"previous_cursor",
|
|
111
144
|
];
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
145
|
+
const RELAY_TURNS_LIST_PREVIOUS_PAGINATION_RESULT_KEYS = new Set([
|
|
146
|
+
"prevCursor",
|
|
147
|
+
"prev_cursor",
|
|
148
|
+
"previousCursor",
|
|
149
|
+
"previous_cursor",
|
|
150
|
+
]);
|
|
115
151
|
|
|
116
152
|
function buildRelayUserAgentHeader({ version = bridgePackageVersion } = {}) {
|
|
117
153
|
const normalizedVersion = typeof version === "string" && version.trim()
|
|
@@ -131,6 +167,11 @@ function buildRelayAccessTokenHeaders(config = {}, env = process.env) {
|
|
|
131
167
|
: {};
|
|
132
168
|
}
|
|
133
169
|
|
|
170
|
+
const jsonlArtifactItemsCacheByThread = new Map();
|
|
171
|
+
const jsonlThreadCwdCacheByThread = new Map();
|
|
172
|
+
const FORWARDED_REQUEST_METHODS_MAX_SIZE = 500;
|
|
173
|
+
const JSONL_ROLLOUT_PATH_CACHE_MAX_SIZE = 200;
|
|
174
|
+
|
|
134
175
|
function evictOldestEntries(map, maxSize) {
|
|
135
176
|
if (map.size <= maxSize) {
|
|
136
177
|
return;
|
|
@@ -143,6 +184,540 @@ function evictOldestEntries(map, maxSize) {
|
|
|
143
184
|
}
|
|
144
185
|
}
|
|
145
186
|
|
|
187
|
+
function createThreadTurnsListFastPageCoordinator({
|
|
188
|
+
waitMs = RELAY_JSONL_FAST_FIRST_PAGE_WAIT_MS,
|
|
189
|
+
handoffTTLms = RELAY_JSONL_CANONICAL_HANDOFF_TTL_MS,
|
|
190
|
+
maxHandoffs = RELAY_JSONL_CANONICAL_HANDOFF_MAX_ENTRIES,
|
|
191
|
+
payloadSoftLimitBytes = RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES,
|
|
192
|
+
sanitizeForRelay = sanitizeThreadHistoryImagesForRelay,
|
|
193
|
+
now = Date.now,
|
|
194
|
+
setTimeoutImpl = setTimeout,
|
|
195
|
+
clearTimeoutImpl = clearTimeout,
|
|
196
|
+
createToken = () => randomBytes(12).toString("hex"),
|
|
197
|
+
} = {}) {
|
|
198
|
+
const handoffsByToken = new Map();
|
|
199
|
+
const latestHandoffTokenByThread = new Map();
|
|
200
|
+
const canonicalFirstPageByKey = new Map();
|
|
201
|
+
|
|
202
|
+
function pruneHandoffs() {
|
|
203
|
+
const cutoff = now() - handoffTTLms;
|
|
204
|
+
for (const [token, entry] of handoffsByToken) {
|
|
205
|
+
if (entry.createdAt >= cutoff) {
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
handoffsByToken.delete(token);
|
|
209
|
+
if (latestHandoffTokenByThread.get(entry.threadId) === token) {
|
|
210
|
+
latestHandoffTokenByThread.delete(entry.threadId);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
for (const [cacheKey, entry] of canonicalFirstPageByKey) {
|
|
214
|
+
if (entry.createdAt < cutoff) {
|
|
215
|
+
canonicalFirstPageByKey.delete(cacheKey);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
while (handoffsByToken.size > maxHandoffs) {
|
|
219
|
+
const oldestToken = handoffsByToken.keys().next().value;
|
|
220
|
+
const oldest = handoffsByToken.get(oldestToken);
|
|
221
|
+
handoffsByToken.delete(oldestToken);
|
|
222
|
+
if (oldest && latestHandoffTokenByThread.get(oldest.threadId) === oldestToken) {
|
|
223
|
+
latestHandoffTokenByThread.delete(oldest.threadId);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function rememberHandoff(threadId, canonicalOutcomePromise, jsonlFallback) {
|
|
229
|
+
pruneHandoffs();
|
|
230
|
+
const token = createToken();
|
|
231
|
+
const entry = {
|
|
232
|
+
token,
|
|
233
|
+
threadId,
|
|
234
|
+
canonicalOutcomePromise,
|
|
235
|
+
hadNonEmptyJsonl: Boolean(jsonlFallback?.response),
|
|
236
|
+
anchorTurnId: firstTurnsListTurnId(jsonlFallback?.response),
|
|
237
|
+
createdAt: now(),
|
|
238
|
+
};
|
|
239
|
+
handoffsByToken.set(token, entry);
|
|
240
|
+
latestHandoffTokenByThread.set(threadId, token);
|
|
241
|
+
pruneHandoffs();
|
|
242
|
+
return token;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function consumeHandoff(entry) {
|
|
246
|
+
if (!entry?.token) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
handoffsByToken.delete(entry.token);
|
|
250
|
+
if (latestHandoffTokenByThread.get(entry.threadId) === entry.token) {
|
|
251
|
+
latestHandoffTokenByThread.delete(entry.threadId);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function canonicalFirstPageOutcome(cacheKey, canonicalRequest, fetchCanonical) {
|
|
256
|
+
pruneHandoffs();
|
|
257
|
+
const existing = canonicalFirstPageByKey.get(cacheKey);
|
|
258
|
+
if (existing) {
|
|
259
|
+
return existing.canonicalOutcomePromise;
|
|
260
|
+
}
|
|
261
|
+
const canonicalOutcomePromise = settleThreadTurnsListCanonicalOutcome(
|
|
262
|
+
fetchCanonical(canonicalRequest)
|
|
263
|
+
);
|
|
264
|
+
canonicalFirstPageByKey.set(cacheKey, {
|
|
265
|
+
canonicalOutcomePromise,
|
|
266
|
+
createdAt: now(),
|
|
267
|
+
});
|
|
268
|
+
canonicalOutcomePromise.then(() => {
|
|
269
|
+
forgetCanonicalFirstPage(cacheKey, canonicalOutcomePromise);
|
|
270
|
+
});
|
|
271
|
+
return canonicalOutcomePromise;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function forgetCanonicalFirstPage(cacheKey, canonicalOutcomePromise) {
|
|
275
|
+
const existing = canonicalFirstPageByKey.get(cacheKey);
|
|
276
|
+
if (existing?.canonicalOutcomePromise === canonicalOutcomePromise) {
|
|
277
|
+
canonicalFirstPageByKey.delete(cacheKey);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function readHandoffEntry(request) {
|
|
282
|
+
pruneHandoffs();
|
|
283
|
+
const threadId = threadIdFromRequestParams(request?.params);
|
|
284
|
+
const cursor = request?.params?.cursor;
|
|
285
|
+
const token = threadTurnsListHandoffDescriptor(cursor)?.token
|
|
286
|
+
|| latestHandoffTokenByThread.get(threadId)
|
|
287
|
+
|| "";
|
|
288
|
+
const entry = token ? handoffsByToken.get(token) : null;
|
|
289
|
+
return entry?.threadId === threadId ? entry : null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function awaitCanonicalOutcome(canonicalOutcomePromise) {
|
|
293
|
+
const outcome = await canonicalOutcomePromise;
|
|
294
|
+
if (!outcome.ok) {
|
|
295
|
+
throw outcome.error;
|
|
296
|
+
}
|
|
297
|
+
return outcome.response;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
async function extendCanonicalResponseThroughAnchor(
|
|
301
|
+
response,
|
|
302
|
+
canonicalRequest,
|
|
303
|
+
anchorTurnId,
|
|
304
|
+
fetchCanonical,
|
|
305
|
+
maxPages = 12
|
|
306
|
+
) {
|
|
307
|
+
if (threadTurnsListResponseContainsAnchor(response, anchorTurnId)) {
|
|
308
|
+
return response;
|
|
309
|
+
}
|
|
310
|
+
const firstResult = response?.result;
|
|
311
|
+
const turnsKey = findTurnsListResultKey(firstResult);
|
|
312
|
+
if (!turnsKey) {
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
let lastResult = firstResult;
|
|
317
|
+
let combinedTurns = [...firstResult[turnsKey]];
|
|
318
|
+
let cursor = readTurnsListNextCursor(firstResult);
|
|
319
|
+
const seenCursors = new Set();
|
|
320
|
+
for (let pageIndex = 0; pageIndex < maxPages && hasRelayCursor(cursor); pageIndex += 1) {
|
|
321
|
+
const cursorKey = JSON.stringify(cursor);
|
|
322
|
+
if (seenCursors.has(cursorKey)) {
|
|
323
|
+
break;
|
|
324
|
+
}
|
|
325
|
+
seenCursors.add(cursorKey);
|
|
326
|
+
const nextRequest = {
|
|
327
|
+
...canonicalRequest,
|
|
328
|
+
params: buildAdaptiveTurnsListPageParams(
|
|
329
|
+
canonicalRequest.params,
|
|
330
|
+
RELAY_TURNS_LIST_SAFE_RETRY_LIMIT,
|
|
331
|
+
cursor
|
|
332
|
+
),
|
|
333
|
+
};
|
|
334
|
+
const nextResponse = await awaitCanonicalOutcome(
|
|
335
|
+
settleThreadTurnsListCanonicalOutcome(fetchCanonical(nextRequest))
|
|
336
|
+
);
|
|
337
|
+
const nextResult = nextResponse?.result;
|
|
338
|
+
const nextTurnsKey = findTurnsListResultKey(nextResult);
|
|
339
|
+
if (!nextTurnsKey) {
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
for (const turn of nextResult[nextTurnsKey]) {
|
|
343
|
+
const turnId = turnListTurnIdentifier(turn);
|
|
344
|
+
if (!turnId || !combinedTurns.some((existing) => turnListTurnIdentifier(existing) === turnId)) {
|
|
345
|
+
combinedTurns.push(turn);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
lastResult = nextResult;
|
|
349
|
+
const combinedResponse = buildSafeTurnsListResponse(
|
|
350
|
+
canonicalRequest.id,
|
|
351
|
+
firstResult,
|
|
352
|
+
lastResult,
|
|
353
|
+
turnsKey,
|
|
354
|
+
combinedTurns
|
|
355
|
+
);
|
|
356
|
+
if (threadTurnsListResponseContainsAnchor(combinedResponse, anchorTurnId)) {
|
|
357
|
+
// The cursor belongs after every turn through the anchor. Keep that
|
|
358
|
+
// complete boundary intact, compacting items if needed; never slice
|
|
359
|
+
// turns and accidentally make the omitted range unreachable.
|
|
360
|
+
return buildCompactedCompleteTurnsListResponse({
|
|
361
|
+
requestId: canonicalRequest.id,
|
|
362
|
+
firstResult,
|
|
363
|
+
lastResult,
|
|
364
|
+
turnsKey,
|
|
365
|
+
turns: combinedTurns,
|
|
366
|
+
sanitizeForRelay,
|
|
367
|
+
sanitizeContext: buildThreadTurnsListRelaySanitizeContext(canonicalRequest),
|
|
368
|
+
payloadSoftLimitBytes,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
const nextCursor = readTurnsListNextCursor(nextResult);
|
|
372
|
+
if (nextResult[nextTurnsKey].length === 0 || !hasRelayCursor(nextCursor)) {
|
|
373
|
+
break;
|
|
374
|
+
}
|
|
375
|
+
cursor = nextCursor;
|
|
376
|
+
}
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function resolveCanonicalRequest(request, fetchCanonical, existingEntry = null, {
|
|
381
|
+
alignToHandoffAnchor = false,
|
|
382
|
+
validateHandoffAnchor = false,
|
|
383
|
+
handoffAnchorTurnId = "",
|
|
384
|
+
} = {}) {
|
|
385
|
+
const canonicalRequest = canonicalThreadTurnsListRequest(request);
|
|
386
|
+
let entry = existingEntry;
|
|
387
|
+
let response = null;
|
|
388
|
+
const anchorTurnId = entry?.anchorTurnId || handoffAnchorTurnId;
|
|
389
|
+
const canMatchCanonicalAnchor = anchorTurnId
|
|
390
|
+
&& !isSyntheticJsonlHistoryTurnId(anchorTurnId);
|
|
391
|
+
if (entry) {
|
|
392
|
+
const observedOutcomePromise = entry.canonicalOutcomePromise;
|
|
393
|
+
const firstOutcome = await observedOutcomePromise;
|
|
394
|
+
const firstResponseIsUsable = firstOutcome.ok
|
|
395
|
+
&& !isEmptyTurnsListResponse(firstOutcome.response);
|
|
396
|
+
if (firstResponseIsUsable) {
|
|
397
|
+
response = firstOutcome.response;
|
|
398
|
+
} else {
|
|
399
|
+
if (entry.canonicalOutcomePromise === observedOutcomePromise) {
|
|
400
|
+
entry.canonicalOutcomePromise = settleThreadTurnsListCanonicalOutcome(
|
|
401
|
+
fetchCanonical(canonicalRequest)
|
|
402
|
+
);
|
|
403
|
+
entry.createdAt = now();
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
response = response || await awaitCanonicalOutcome(
|
|
409
|
+
entry?.canonicalOutcomePromise
|
|
410
|
+
|| settleThreadTurnsListCanonicalOutcome(fetchCanonical(canonicalRequest))
|
|
411
|
+
);
|
|
412
|
+
if (entry?.hadNonEmptyJsonl && isEmptyTurnsListResponse(response)) {
|
|
413
|
+
throw new Error("Canonical thread history was empty after a non-empty JSONL first page.");
|
|
414
|
+
}
|
|
415
|
+
if (validateHandoffAnchor
|
|
416
|
+
&& canMatchCanonicalAnchor
|
|
417
|
+
&& !threadTurnsListResponseContainsAnchor(response, anchorTurnId)) {
|
|
418
|
+
response = await extendCanonicalResponseThroughAnchor(
|
|
419
|
+
response,
|
|
420
|
+
canonicalRequest,
|
|
421
|
+
anchorTurnId,
|
|
422
|
+
fetchCanonical
|
|
423
|
+
);
|
|
424
|
+
if (!response) {
|
|
425
|
+
throw new Error("Canonical history does not contain the JSONL handoff anchor yet.");
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
const rebound = rebindThreadTurnsListResponseId(response, request.id);
|
|
429
|
+
if (!alignToHandoffAnchor || !canMatchCanonicalAnchor) {
|
|
430
|
+
return rebound;
|
|
431
|
+
}
|
|
432
|
+
const aligned = alignThreadTurnsListResponseToAnchor(rebound, anchorTurnId);
|
|
433
|
+
if (!aligned) {
|
|
434
|
+
throw new Error("Canonical history no longer contains the JSONL handoff anchor.");
|
|
435
|
+
}
|
|
436
|
+
return aligned;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
async function resolve(request, { fetchCanonical, readJsonl }) {
|
|
440
|
+
const params = request?.params || {};
|
|
441
|
+
const cursor = params.cursor;
|
|
442
|
+
const handoffDescriptor = threadTurnsListHandoffDescriptor(cursor);
|
|
443
|
+
const isHandoffRequest = cursor === JSONL_OLDER_HANDOFF_CURSOR
|
|
444
|
+
|| Boolean(handoffDescriptor);
|
|
445
|
+
const requiresCanonical = params.remodexRequireCanonical === true;
|
|
446
|
+
const hasOrdinaryCursor = hasRelayCursor(cursor) && !isHandoffRequest;
|
|
447
|
+
|
|
448
|
+
if (hasOrdinaryCursor) {
|
|
449
|
+
return {
|
|
450
|
+
source: "canonical",
|
|
451
|
+
response: await resolveCanonicalRequest(request, fetchCanonical),
|
|
452
|
+
usesJsonl: false,
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
if (isHandoffRequest || requiresCanonical) {
|
|
457
|
+
const handoffEntry = readHandoffEntry(request);
|
|
458
|
+
const response = await resolveCanonicalRequest(request, fetchCanonical, handoffEntry, {
|
|
459
|
+
alignToHandoffAnchor: isHandoffRequest,
|
|
460
|
+
validateHandoffAnchor: true,
|
|
461
|
+
handoffAnchorTurnId: handoffDescriptor?.anchorTurnId || "",
|
|
462
|
+
});
|
|
463
|
+
consumeHandoff(handoffEntry);
|
|
464
|
+
return {
|
|
465
|
+
source: "canonical",
|
|
466
|
+
response,
|
|
467
|
+
usesJsonl: false,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
const canonicalRequest = canonicalThreadTurnsListRequest(request);
|
|
472
|
+
const threadId = threadIdFromRequestParams(params);
|
|
473
|
+
const canonicalFirstPageCacheKey = canonicalThreadTurnsListRequestShapeKey(canonicalRequest);
|
|
474
|
+
const canonicalOutcomePromise = canonicalFirstPageOutcome(
|
|
475
|
+
canonicalFirstPageCacheKey,
|
|
476
|
+
canonicalRequest,
|
|
477
|
+
fetchCanonical
|
|
478
|
+
);
|
|
479
|
+
let jsonlFallback = null;
|
|
480
|
+
try {
|
|
481
|
+
jsonlFallback = await readJsonl(request);
|
|
482
|
+
} catch {
|
|
483
|
+
jsonlFallback = null;
|
|
484
|
+
}
|
|
485
|
+
// A rollout tail is a useful emergency baseline only when it contains a
|
|
486
|
+
// whole turn package. Never let a bare tail (for example a file-change or
|
|
487
|
+
// final assistant fragment) win the race with canonical history: iOS would
|
|
488
|
+
// render it as a complete conversation and then merge the real opener in
|
|
489
|
+
// later, which is exactly how orphan cards and duplicate rows appeared.
|
|
490
|
+
if (jsonlFallback?.response && !isCoherentJsonlFirstPageResponse(jsonlFallback.response)) {
|
|
491
|
+
jsonlFallback = null;
|
|
492
|
+
}
|
|
493
|
+
if (!jsonlFallback?.response) {
|
|
494
|
+
const response = await awaitCanonicalOutcome(canonicalOutcomePromise);
|
|
495
|
+
forgetCanonicalFirstPage(canonicalFirstPageCacheKey, canonicalOutcomePromise);
|
|
496
|
+
return {
|
|
497
|
+
source: "canonical",
|
|
498
|
+
response: rebindThreadTurnsListResponseId(response, request.id),
|
|
499
|
+
usesJsonl: false,
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
let timeoutId = null;
|
|
504
|
+
const deadline = new Promise((resolveDeadline) => {
|
|
505
|
+
timeoutId = setTimeoutImpl(() => resolveDeadline({ deadline: true }), waitMs);
|
|
506
|
+
});
|
|
507
|
+
const first = await Promise.race([canonicalOutcomePromise, deadline]);
|
|
508
|
+
if (timeoutId != null) {
|
|
509
|
+
clearTimeoutImpl(timeoutId);
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
if (first?.ok && !isEmptyTurnsListResponse(first.response)) {
|
|
513
|
+
if (shouldPreferJsonlFirstPage(first.response, jsonlFallback.response)) {
|
|
514
|
+
const token = rememberHandoff(threadId, canonicalOutcomePromise, jsonlFallback);
|
|
515
|
+
return {
|
|
516
|
+
source: "jsonl",
|
|
517
|
+
response: buildJsonlCanonicalHandoffResponse(
|
|
518
|
+
jsonlFallback.response,
|
|
519
|
+
request.id,
|
|
520
|
+
token,
|
|
521
|
+
firstTurnsListTurnId(jsonlFallback.response)
|
|
522
|
+
),
|
|
523
|
+
usesJsonl: true,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
forgetCanonicalFirstPage(canonicalFirstPageCacheKey, canonicalOutcomePromise);
|
|
527
|
+
return {
|
|
528
|
+
source: "canonical",
|
|
529
|
+
response: rebindThreadTurnsListResponseId(first.response, request.id),
|
|
530
|
+
usesJsonl: false,
|
|
531
|
+
jsonlFallback,
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const token = rememberHandoff(threadId, canonicalOutcomePromise, jsonlFallback);
|
|
536
|
+
return {
|
|
537
|
+
source: "jsonl",
|
|
538
|
+
response: buildJsonlCanonicalHandoffResponse(
|
|
539
|
+
jsonlFallback.response,
|
|
540
|
+
request.id,
|
|
541
|
+
token,
|
|
542
|
+
firstTurnsListTurnId(jsonlFallback.response)
|
|
543
|
+
),
|
|
544
|
+
usesJsonl: true,
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
return { resolve };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
function settleThreadTurnsListCanonicalOutcome(promise) {
|
|
552
|
+
return Promise.resolve(promise).then(
|
|
553
|
+
(response) => ({ ok: true, response }),
|
|
554
|
+
(error) => ({ ok: false, error })
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function threadTurnsListHandoffDescriptor(cursor) {
|
|
559
|
+
if (typeof cursor !== "string" || !cursor.startsWith(JSONL_CANONICAL_HANDOFF_CURSOR_PREFIX)) {
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
const raw = cursor.slice(JSONL_CANONICAL_HANDOFF_CURSOR_PREFIX.length);
|
|
563
|
+
const separatorIndex = raw.lastIndexOf(":");
|
|
564
|
+
if (separatorIndex < 0) {
|
|
565
|
+
return raw ? { anchorTurnId: "", token: raw } : null;
|
|
566
|
+
}
|
|
567
|
+
const token = raw.slice(separatorIndex + 1);
|
|
568
|
+
if (!token) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
let anchorTurnId = "";
|
|
572
|
+
try {
|
|
573
|
+
anchorTurnId = decodeURIComponent(raw.slice(0, separatorIndex));
|
|
574
|
+
} catch {
|
|
575
|
+
return null;
|
|
576
|
+
}
|
|
577
|
+
return { anchorTurnId, token };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function canonicalThreadTurnsListRequest(request) {
|
|
581
|
+
const params = { ...(request?.params || {}) };
|
|
582
|
+
delete params.remodexRequireCanonical;
|
|
583
|
+
delete params.remodexTurnStateOnly;
|
|
584
|
+
if (params.cursor === JSONL_OLDER_HANDOFF_CURSOR || threadTurnsListHandoffDescriptor(params.cursor)) {
|
|
585
|
+
delete params.cursor;
|
|
586
|
+
}
|
|
587
|
+
return { ...request, params };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
function canonicalThreadTurnsListRequestShapeKey(canonicalRequest) {
|
|
591
|
+
const params = canonicalRequest?.params || {};
|
|
592
|
+
return JSON.stringify(sortJsonValueForCacheKey({
|
|
593
|
+
threadId: threadIdFromRequestParams(params),
|
|
594
|
+
params,
|
|
595
|
+
}));
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
function sortJsonValueForCacheKey(value) {
|
|
599
|
+
if (Array.isArray(value)) {
|
|
600
|
+
return value.map(sortJsonValueForCacheKey);
|
|
601
|
+
}
|
|
602
|
+
if (!value || typeof value !== "object") {
|
|
603
|
+
return value;
|
|
604
|
+
}
|
|
605
|
+
return Object.fromEntries(
|
|
606
|
+
Object.keys(value)
|
|
607
|
+
.sort()
|
|
608
|
+
.map((key) => [key, sortJsonValueForCacheKey(value[key])])
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function rebindThreadTurnsListResponseId(response, requestId) {
|
|
613
|
+
return response && typeof response === "object"
|
|
614
|
+
? { ...response, id: requestId }
|
|
615
|
+
: response;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
function buildJsonlCanonicalHandoffResponse(response, requestId, token, anchorTurnId = "") {
|
|
619
|
+
const result = response?.result;
|
|
620
|
+
if (!result || typeof result !== "object" || Array.isArray(result)) {
|
|
621
|
+
return response;
|
|
622
|
+
}
|
|
623
|
+
return {
|
|
624
|
+
...response,
|
|
625
|
+
id: requestId,
|
|
626
|
+
result: {
|
|
627
|
+
...result,
|
|
628
|
+
nextCursor: `${JSONL_CANONICAL_HANDOFF_CURSOR_PREFIX}${encodeURIComponent(anchorTurnId)}:${token}`,
|
|
629
|
+
remodexJsonlFallback: true,
|
|
630
|
+
remodexCanonicalHandoff: true,
|
|
631
|
+
},
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function shouldPreferJsonlFirstPage(canonicalResponse, jsonlResponse) {
|
|
636
|
+
const canonicalResult = canonicalResponse?.result;
|
|
637
|
+
const jsonlResult = jsonlResponse?.result;
|
|
638
|
+
const canonicalTurnsKey = findTurnsListResultKey(canonicalResult);
|
|
639
|
+
const jsonlTurnsKey = findTurnsListResultKey(jsonlResult);
|
|
640
|
+
if (!canonicalTurnsKey || !jsonlTurnsKey) {
|
|
641
|
+
return false;
|
|
642
|
+
}
|
|
643
|
+
const jsonlTurn = jsonlResult[jsonlTurnsKey]?.[0];
|
|
644
|
+
const jsonlTurnId = turnListTurnIdentifier(jsonlTurn);
|
|
645
|
+
return Boolean(jsonlTurnId)
|
|
646
|
+
&& !canonicalResult[canonicalTurnsKey].some((turn) => turnListTurnIdentifier(turn) === jsonlTurnId)
|
|
647
|
+
&& shouldMergeLatestJsonlTurn(jsonlTurn);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
function firstTurnsListTurnId(response) {
|
|
651
|
+
const result = response?.result;
|
|
652
|
+
const turnsKey = findTurnsListResultKey(result);
|
|
653
|
+
return turnsKey ? turnListTurnIdentifier(result[turnsKey]?.[0]) : "";
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function isCoherentJsonlFirstPageResponse(response) {
|
|
657
|
+
const result = response?.result;
|
|
658
|
+
const turnsKey = findTurnsListResultKey(result);
|
|
659
|
+
const turns = turnsKey ? result[turnsKey] : null;
|
|
660
|
+
if (!Array.isArray(turns) || turns.length === 0) {
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
// A running turn must contain its materialized user opener. Otherwise an
|
|
664
|
+
// orphan file card or assistant tail can win the fast-page race and later
|
|
665
|
+
// be mistaken for a complete conversation. Explicit terminal turns are
|
|
666
|
+
// allowed without a user item because older compacted/system turns can be
|
|
667
|
+
// legitimately item-only.
|
|
668
|
+
const newestTurn = turns[0];
|
|
669
|
+
const items = Array.isArray(newestTurn?.items) ? newestTurn.items : null;
|
|
670
|
+
if (!items) {
|
|
671
|
+
return false;
|
|
672
|
+
}
|
|
673
|
+
if (items.length === 0) {
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
const status = String(newestTurn?.status || "").replace(/[_-]/g, "").toLowerCase();
|
|
677
|
+
const isExplicitTerminal = new Set(["completed", "failed", "aborted", "cancelled", "canceled", "interrupted"])
|
|
678
|
+
.has(status);
|
|
679
|
+
if (isExplicitTerminal) {
|
|
680
|
+
return true;
|
|
681
|
+
}
|
|
682
|
+
return items.some((item) => {
|
|
683
|
+
const role = String(item?.role || "").toLowerCase();
|
|
684
|
+
const type = String(item?.type || "").replace(/[_-]/g, "").toLowerCase();
|
|
685
|
+
return role === "user" || type === "usermessage";
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
function threadTurnsListResponseContainsAnchor(response, anchorTurnId) {
|
|
690
|
+
const result = response?.result;
|
|
691
|
+
const turnsKey = findTurnsListResultKey(result);
|
|
692
|
+
return Boolean(turnsKey) && result[turnsKey].some((turn) => (
|
|
693
|
+
turnListTurnIdentifier(turn) === anchorTurnId
|
|
694
|
+
));
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
function alignThreadTurnsListResponseToAnchor(response, anchorTurnId) {
|
|
698
|
+
const result = response?.result;
|
|
699
|
+
const turnsKey = findTurnsListResultKey(result);
|
|
700
|
+
if (!turnsKey) {
|
|
701
|
+
return null;
|
|
702
|
+
}
|
|
703
|
+
const anchorIndex = result[turnsKey].findIndex((turn) => (
|
|
704
|
+
turnListTurnIdentifier(turn) === anchorTurnId
|
|
705
|
+
));
|
|
706
|
+
if (anchorIndex < 0) {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
if (anchorIndex === 0) {
|
|
710
|
+
return response;
|
|
711
|
+
}
|
|
712
|
+
return {
|
|
713
|
+
...response,
|
|
714
|
+
result: {
|
|
715
|
+
...result,
|
|
716
|
+
[turnsKey]: result[turnsKey].slice(anchorIndex),
|
|
717
|
+
},
|
|
718
|
+
};
|
|
719
|
+
}
|
|
720
|
+
|
|
146
721
|
function startBridge({
|
|
147
722
|
config: explicitConfig = null,
|
|
148
723
|
printPairingQr = true,
|
|
@@ -181,6 +756,9 @@ function startBridge({
|
|
|
181
756
|
const notificationSecret = randomBytes(24).toString("hex");
|
|
182
757
|
const desktopRefresher = new CodexDesktopRefresher({
|
|
183
758
|
enabled: config.refreshEnabled,
|
|
759
|
+
// With IPC live sync streaming content, deep-link refreshes are only needed
|
|
760
|
+
// to navigate Desktop onto the phone-driven thread, not to reload content.
|
|
761
|
+
navigationOnly: config.desktopIpcLiveSyncEnabled,
|
|
184
762
|
debounceMs: config.refreshDebounceMs,
|
|
185
763
|
refreshCommand: config.refreshCommand,
|
|
186
764
|
bundleId: config.codexBundleId,
|
|
@@ -209,18 +787,17 @@ function startBridge({
|
|
|
209
787
|
let relayWatchdogTimer = null;
|
|
210
788
|
let lastRelayActivityAt = 0;
|
|
211
789
|
let lastConnectionStatus = null;
|
|
212
|
-
let lastConnectionError = "";
|
|
213
790
|
let codexLaunchState = config.codexEndpoint ? "connected" : "starting";
|
|
214
791
|
let codexHandshakeState = config.codexEndpoint ? "warm" : "cold";
|
|
215
792
|
const forwardedInitializeRequestIds = new Set();
|
|
216
793
|
const bridgeManagedCodexRequestWaiters = new Map();
|
|
217
794
|
const forwardedRequestMethodsById = new Map();
|
|
218
795
|
const relaySanitizedResponseMethodsById = new Map();
|
|
219
|
-
const
|
|
220
|
-
const codexResponseRoutesById = new Map();
|
|
221
|
-
const extraRelaySessionCount = readExtraRelaySessionCount(process.env);
|
|
796
|
+
const desktopIpcLiveOwnerObservedInboundKeys = new Set();
|
|
222
797
|
const jsonlTurnsListRolloutCacheByThread = new Map();
|
|
223
798
|
const jsonlTurnsListRolloutMissCacheByThread = new Map();
|
|
799
|
+
const threadTurnsListFastPageCoordinator = createThreadTurnsListFastPageCoordinator();
|
|
800
|
+
const threadRuntimeSettingsStore = createThreadRuntimeSettingsStore();
|
|
224
801
|
const trackedForwardedRequestMethods = new Set([
|
|
225
802
|
"account/login/start",
|
|
226
803
|
"account/login/cancel",
|
|
@@ -257,7 +834,6 @@ function startBridge({
|
|
|
257
834
|
}
|
|
258
835
|
},
|
|
259
836
|
});
|
|
260
|
-
let primaryRelayChannel = null;
|
|
261
837
|
// Keeps one stable sender identity across reconnects so buffered replay state
|
|
262
838
|
// reflects what actually made it onto the current relay socket.
|
|
263
839
|
function sendRelayWireMessage(wireMessage) {
|
|
@@ -273,6 +849,14 @@ function startBridge({
|
|
|
273
849
|
const rolloutLiveMirror = !config.codexEndpoint
|
|
274
850
|
? createRolloutLiveMirrorController({
|
|
275
851
|
sendApplicationResponse,
|
|
852
|
+
// One live source per thread. The follower keeps fresh/idle Desktop state
|
|
853
|
+
// authoritative, but yields an active cache that stopped broadcasting;
|
|
854
|
+
// a later Desktop snapshot is announced as a new source epoch so the
|
|
855
|
+
// phone performs canonical repair instead of mixing both mirrors.
|
|
856
|
+
shouldSuppressThread: (threadId) => shouldSuppressRolloutMirrorForThread(
|
|
857
|
+
threadId,
|
|
858
|
+
{ desktopIpcActionFollower, desktopIpcLiveOwner }
|
|
859
|
+
),
|
|
276
860
|
})
|
|
277
861
|
: null;
|
|
278
862
|
const desktopIpcActionFollower = !config.codexEndpoint
|
|
@@ -281,7 +865,29 @@ function startBridge({
|
|
|
281
865
|
readConversationState: async (threadId) => seedConversationStateFromThreadRead(
|
|
282
866
|
await sendCodexRequest("thread/read", { threadId })
|
|
283
867
|
),
|
|
868
|
+
forwardToLocalCodex: (rawMessage) => {
|
|
869
|
+
observeDesktopIpcLiveOwnerInbound(rawMessage);
|
|
870
|
+
forwardInboundRequestToCodex(rawMessage);
|
|
871
|
+
},
|
|
872
|
+
// Threads streamed by the bridge's own app-server must never be held,
|
|
873
|
+
// served from Desktop echoes, or routed over the IPC bus.
|
|
874
|
+
isLocallyOwnedThread: (threadId) => Boolean(desktopIpcLiveOwner?.isThreadOwned(threadId)),
|
|
875
|
+
normalizeTurnStartParams: normalizeTurnStartParamsForCodex,
|
|
876
|
+
runtimeSettingsStore: threadRuntimeSettingsStore,
|
|
284
877
|
socketPath: config.desktopIpcSocketPath || undefined,
|
|
878
|
+
snapshotDebounceMs: config.desktopIpcSnapshotDebounceMs,
|
|
879
|
+
})
|
|
880
|
+
: null;
|
|
881
|
+
const desktopIpcLiveOwner = !config.codexEndpoint
|
|
882
|
+
? createDesktopIpcLiveOwner({
|
|
883
|
+
enabled: config.desktopIpcLiveSyncEnabled !== false,
|
|
884
|
+
sendApplicationResponse,
|
|
885
|
+
sendCodexRequest,
|
|
886
|
+
sendRawCodexMessage: (rawMessage) => codex.send(rawMessage),
|
|
887
|
+
normalizeTurnStartParams: normalizeTurnStartParamsForCodex,
|
|
888
|
+
runtimeSettingsStore: threadRuntimeSettingsStore,
|
|
889
|
+
socketPath: config.desktopIpcSocketPath || undefined,
|
|
890
|
+
snapshotDebounceMs: config.desktopIpcSnapshotDebounceMs,
|
|
285
891
|
})
|
|
286
892
|
: null;
|
|
287
893
|
let contextUsageWatcher = null;
|
|
@@ -293,6 +899,14 @@ function startBridge({
|
|
|
293
899
|
appPath: config.codexAppPath,
|
|
294
900
|
logPrefix: "[remodex]",
|
|
295
901
|
});
|
|
902
|
+
const projectRegistry = createProjectRegistry();
|
|
903
|
+
const runtimeProviderRouter = createRuntimeProviderRouter({
|
|
904
|
+
sendApplicationResponse,
|
|
905
|
+
sendCodexRequest,
|
|
906
|
+
sendRuntimeMessage: sendRuntimeApplicationMessage,
|
|
907
|
+
projectRegistry,
|
|
908
|
+
logPrefix: "[remodex]",
|
|
909
|
+
});
|
|
296
910
|
const voiceHandler = createVoiceHandler({
|
|
297
911
|
sendCodexRequest,
|
|
298
912
|
logPrefix: "[remodex]",
|
|
@@ -370,9 +984,11 @@ function startBridge({
|
|
|
370
984
|
clearReconnectTimer();
|
|
371
985
|
clearRelayWatchdog();
|
|
372
986
|
bridgeStatusPublisher.stopHeartbeat();
|
|
987
|
+
runtimeProviderRouter.shutdown();
|
|
373
988
|
stopContextUsageWatcher();
|
|
374
989
|
rolloutLiveMirror?.stopAll();
|
|
375
990
|
desktopIpcActionFollower?.stopAll();
|
|
991
|
+
desktopIpcLiveOwner?.stopAll();
|
|
376
992
|
}
|
|
377
993
|
|
|
378
994
|
function stopBridge() {
|
|
@@ -422,13 +1038,12 @@ function startBridge({
|
|
|
422
1038
|
}
|
|
423
1039
|
|
|
424
1040
|
// Keeps npm start output compact by emitting only high-signal connection states.
|
|
425
|
-
function logConnectionStatus(status
|
|
426
|
-
if (lastConnectionStatus === status
|
|
1041
|
+
function logConnectionStatus(status) {
|
|
1042
|
+
if (lastConnectionStatus === status) {
|
|
427
1043
|
return;
|
|
428
1044
|
}
|
|
429
1045
|
|
|
430
1046
|
lastConnectionStatus = status;
|
|
431
|
-
lastConnectionError = lastError;
|
|
432
1047
|
if (status !== "connected") {
|
|
433
1048
|
activePhoneSummary = null;
|
|
434
1049
|
}
|
|
@@ -436,16 +1051,13 @@ function startBridge({
|
|
|
436
1051
|
state: "running",
|
|
437
1052
|
connectionStatus: status,
|
|
438
1053
|
pid: process.pid,
|
|
439
|
-
lastError,
|
|
1054
|
+
lastError: "",
|
|
440
1055
|
});
|
|
441
1056
|
console.log(`[remodex] ${status}`);
|
|
442
|
-
if (lastError) {
|
|
443
|
-
console.error(`[remodex] ${lastError}`);
|
|
444
|
-
}
|
|
445
1057
|
}
|
|
446
1058
|
|
|
447
1059
|
// Retries the relay socket while preserving the active Codex process and session id.
|
|
448
|
-
function scheduleRelayReconnect(closeCode
|
|
1060
|
+
function scheduleRelayReconnect(closeCode) {
|
|
449
1061
|
if (isShuttingDown) {
|
|
450
1062
|
return;
|
|
451
1063
|
}
|
|
@@ -514,7 +1126,7 @@ function startBridge({
|
|
|
514
1126
|
}
|
|
515
1127
|
},
|
|
516
1128
|
onApplicationMessage(plaintextMessage) {
|
|
517
|
-
handleApplicationMessage(plaintextMessage
|
|
1129
|
+
handleApplicationMessage(plaintextMessage);
|
|
518
1130
|
},
|
|
519
1131
|
})) {
|
|
520
1132
|
return;
|
|
@@ -529,12 +1141,11 @@ function startBridge({
|
|
|
529
1141
|
markRelayActivity();
|
|
530
1142
|
});
|
|
531
1143
|
|
|
532
|
-
nextSocket.on("close", (code
|
|
533
|
-
const closeReason = normalizeWebSocketCloseReason(reason);
|
|
1144
|
+
nextSocket.on("close", (code) => {
|
|
534
1145
|
if (socket === nextSocket) {
|
|
535
1146
|
clearRelayWatchdog();
|
|
536
1147
|
}
|
|
537
|
-
logConnectionStatus("disconnected"
|
|
1148
|
+
logConnectionStatus("disconnected");
|
|
538
1149
|
if (socket === nextSocket) {
|
|
539
1150
|
socket = null;
|
|
540
1151
|
}
|
|
@@ -542,7 +1153,7 @@ function startBridge({
|
|
|
542
1153
|
// Relay reconnects are transport-only: keep local live observers running
|
|
543
1154
|
// so their output can enter secure replay and catch up on the next resume.
|
|
544
1155
|
desktopRefresher.handleTransportReset();
|
|
545
|
-
scheduleRelayReconnect(code
|
|
1156
|
+
scheduleRelayReconnect(code);
|
|
546
1157
|
});
|
|
547
1158
|
|
|
548
1159
|
nextSocket.on("error", () => {
|
|
@@ -553,191 +1164,45 @@ function startBridge({
|
|
|
553
1164
|
});
|
|
554
1165
|
}
|
|
555
1166
|
|
|
556
|
-
// Optional draft path: one bridge process can expose extra one-mobile relay sessions without changing relay behavior.
|
|
557
|
-
function startExtraRelayChannels() {
|
|
558
|
-
for (let index = 1; index <= extraRelaySessionCount; index += 1) {
|
|
559
|
-
startExtraRelayChannel(index);
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
function startExtraRelayChannel(index) {
|
|
564
|
-
const extraSessionId = randomUUID();
|
|
565
|
-
const extraRelaySessionUrl = `${relayBaseUrl}/${extraSessionId}`;
|
|
566
|
-
let reconnectTimerForChannel = null;
|
|
567
|
-
let reconnectAttemptForChannel = 0;
|
|
568
|
-
const extraSecureTransport = createBridgeSecureTransport({
|
|
569
|
-
sessionId: extraSessionId,
|
|
570
|
-
relayUrl: relayBaseUrl,
|
|
571
|
-
deviceState,
|
|
572
|
-
onTrustedPhoneUpdate(nextDeviceState) {
|
|
573
|
-
deviceState = nextDeviceState;
|
|
574
|
-
sendRelayRegistrationUpdate(nextDeviceState);
|
|
575
|
-
sendExtraRelayRegistrationUpdate(extraRelayChannel, nextDeviceState);
|
|
576
|
-
},
|
|
577
|
-
});
|
|
578
|
-
const extraPairingSession = {
|
|
579
|
-
pairingPayload: extraSecureTransport.createPairingPayload(),
|
|
580
|
-
pairingCode: createShortPairingCode({ length: SHORT_PAIRING_CODE_LENGTH }),
|
|
581
|
-
};
|
|
582
|
-
const extraRelayChannel = {
|
|
583
|
-
label: `extra-${index}`,
|
|
584
|
-
pairingSession: extraPairingSession,
|
|
585
|
-
secureTransport: extraSecureTransport,
|
|
586
|
-
socket: null,
|
|
587
|
-
closing: false,
|
|
588
|
-
sendWireMessage(wireMessage) {
|
|
589
|
-
if (extraRelayChannel.socket?.readyState !== WebSocket.OPEN) {
|
|
590
|
-
return false;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
extraRelayChannel.socket.send(wireMessage);
|
|
594
|
-
return true;
|
|
595
|
-
},
|
|
596
|
-
close() {
|
|
597
|
-
extraRelayChannel.closing = true;
|
|
598
|
-
if (reconnectTimerForChannel) {
|
|
599
|
-
clearTimeout(reconnectTimerForChannel);
|
|
600
|
-
reconnectTimerForChannel = null;
|
|
601
|
-
}
|
|
602
|
-
if (
|
|
603
|
-
extraRelayChannel.socket?.readyState === WebSocket.OPEN
|
|
604
|
-
|| extraRelayChannel.socket?.readyState === WebSocket.CONNECTING
|
|
605
|
-
) {
|
|
606
|
-
extraRelayChannel.socket.close();
|
|
607
|
-
}
|
|
608
|
-
},
|
|
609
|
-
};
|
|
610
|
-
relayChannels.push(extraRelayChannel);
|
|
611
|
-
|
|
612
|
-
if (printPairingQr) {
|
|
613
|
-
console.error(`[remodex] Pair device ${index + 1}: scan this QR from your other device.`);
|
|
614
|
-
printQR(extraPairingSession);
|
|
615
|
-
}
|
|
616
|
-
|
|
617
|
-
connectExtraRelay();
|
|
618
|
-
|
|
619
|
-
function scheduleExtraReconnect() {
|
|
620
|
-
if (isShuttingDown || extraRelayChannel.closing || reconnectTimerForChannel) {
|
|
621
|
-
return;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
reconnectAttemptForChannel += 1;
|
|
625
|
-
const delayMs = Math.min(1_000 * reconnectAttemptForChannel, 5_000);
|
|
626
|
-
reconnectTimerForChannel = setTimeout(() => {
|
|
627
|
-
reconnectTimerForChannel = null;
|
|
628
|
-
connectExtraRelay();
|
|
629
|
-
}, delayMs);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
function connectExtraRelay() {
|
|
633
|
-
if (isShuttingDown || extraRelayChannel.closing) {
|
|
634
|
-
return;
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
const nextSocket = new WebSocket(extraRelaySessionUrl, {
|
|
638
|
-
headers: {
|
|
639
|
-
"User-Agent": buildRelayUserAgentHeader(),
|
|
640
|
-
"x-role": "mac",
|
|
641
|
-
"x-notification-secret": notificationSecret,
|
|
642
|
-
...buildRelayAccessTokenHeaders(config),
|
|
643
|
-
...buildMacRegistrationHeaders(deviceState, extraPairingSession),
|
|
644
|
-
},
|
|
645
|
-
});
|
|
646
|
-
extraRelayChannel.socket = nextSocket;
|
|
647
|
-
|
|
648
|
-
nextSocket.on("open", () => {
|
|
649
|
-
reconnectAttemptForChannel = 0;
|
|
650
|
-
extraSecureTransport.bindLiveSendWireMessage(extraRelayChannel.sendWireMessage);
|
|
651
|
-
sendExtraRelayRegistrationUpdate(extraRelayChannel, deviceState);
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
nextSocket.on("message", (data) => {
|
|
655
|
-
const message = typeof data === "string" ? data : data.toString("utf8");
|
|
656
|
-
extraSecureTransport.handleIncomingWireMessage(message, {
|
|
657
|
-
sendControlMessage(controlMessage) {
|
|
658
|
-
if (nextSocket.readyState === WebSocket.OPEN) {
|
|
659
|
-
nextSocket.send(JSON.stringify(controlMessage));
|
|
660
|
-
}
|
|
661
|
-
},
|
|
662
|
-
onApplicationMessage(plaintextMessage) {
|
|
663
|
-
handleApplicationMessage(plaintextMessage, extraRelayChannel);
|
|
664
|
-
},
|
|
665
|
-
});
|
|
666
|
-
});
|
|
667
|
-
|
|
668
|
-
nextSocket.on("close", () => {
|
|
669
|
-
if (extraRelayChannel.socket === nextSocket) {
|
|
670
|
-
extraRelayChannel.socket = null;
|
|
671
|
-
}
|
|
672
|
-
scheduleExtraReconnect();
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
nextSocket.on("error", () => {});
|
|
676
|
-
}
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
function closeExtraRelayChannels() {
|
|
680
|
-
for (const relayChannel of relayChannels) {
|
|
681
|
-
if (relayChannel !== primaryRelayChannel && typeof relayChannel.close === "function") {
|
|
682
|
-
relayChannel.close();
|
|
683
|
-
}
|
|
684
|
-
}
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
function sendExtraRelayRegistrationUpdate(relayChannel, nextDeviceState) {
|
|
688
|
-
if (!relayChannel?.socket || relayChannel.socket.readyState !== WebSocket.OPEN) {
|
|
689
|
-
return;
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
relayChannel.socket.send(JSON.stringify({
|
|
693
|
-
kind: "relayMacRegistration",
|
|
694
|
-
registration: buildMacRegistration(nextDeviceState, relayChannel.pairingSession),
|
|
695
|
-
}));
|
|
696
|
-
}
|
|
697
|
-
|
|
698
1167
|
const pairingPayload = secureTransport.createPairingPayload();
|
|
699
1168
|
const pairingSession = {
|
|
700
1169
|
pairingPayload,
|
|
701
1170
|
pairingCode: createShortPairingCode({ length: SHORT_PAIRING_CODE_LENGTH }),
|
|
702
1171
|
};
|
|
703
|
-
primaryRelayChannel = {
|
|
704
|
-
label: "primary",
|
|
705
|
-
pairingSession,
|
|
706
|
-
secureTransport,
|
|
707
|
-
sendWireMessage: sendRelayWireMessage,
|
|
708
|
-
};
|
|
709
|
-
relayChannels.push(primaryRelayChannel);
|
|
710
1172
|
onPairingSession?.(pairingSession);
|
|
711
1173
|
if (printPairingQr) {
|
|
712
|
-
if (extraRelaySessionCount > 0) {
|
|
713
|
-
console.error("[remodex] Pair device 1: scan this QR from your first device.");
|
|
714
|
-
}
|
|
715
1174
|
printQR(pairingSession);
|
|
716
1175
|
}
|
|
717
1176
|
pushServiceClient.logUnavailable();
|
|
718
1177
|
connectRelay();
|
|
719
|
-
startExtraRelayChannels();
|
|
720
1178
|
|
|
721
1179
|
codex.onMessage((message) => {
|
|
722
|
-
|
|
1180
|
+
// Streaming deltas make this the hottest path in the bridge: parse the
|
|
1181
|
+
// envelope once and share the read-only object with every observer.
|
|
1182
|
+
const parsedMessage = parseBridgeMessage(message);
|
|
1183
|
+
if (handleBridgeManagedCodexResponse(message, parsedMessage)) {
|
|
723
1184
|
return;
|
|
724
1185
|
}
|
|
725
|
-
updatePendingAuthLoginFromCodexMessage(message);
|
|
726
|
-
trackCodexHandshakeState(message);
|
|
727
|
-
desktopRefresher.handleOutbound(message);
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1186
|
+
updatePendingAuthLoginFromCodexMessage(message, parsedMessage);
|
|
1187
|
+
trackCodexHandshakeState(message, parsedMessage);
|
|
1188
|
+
desktopRefresher.handleOutbound(message, parsedMessage);
|
|
1189
|
+
desktopIpcLiveOwner?.observeOutbound(message, parsedMessage);
|
|
1190
|
+
pushNotificationTracker.handleOutbound(message, parsedMessage);
|
|
1191
|
+
rememberThreadFromMessage("codex", message, parsedMessage);
|
|
1192
|
+
secureTransport.queueOutboundApplicationMessage(
|
|
1193
|
+
sanitizeRelayBoundCodexMessage(message, parsedMessage),
|
|
1194
|
+
sendRelayWireMessage
|
|
1195
|
+
);
|
|
731
1196
|
});
|
|
732
1197
|
|
|
733
1198
|
codex.onClose(() => {
|
|
734
1199
|
const wasShuttingDown = isShuttingDown;
|
|
735
1200
|
clearRelayWatchdog();
|
|
736
1201
|
bridgeStatusPublisher.stopHeartbeat();
|
|
1202
|
+
logConnectionStatus("disconnected");
|
|
737
1203
|
const lastError = wasShuttingDown
|
|
738
1204
|
? ""
|
|
739
|
-
:
|
|
740
|
-
logConnectionStatus("disconnected", lastError);
|
|
1205
|
+
: "Codex transport closed unexpectedly.";
|
|
741
1206
|
publishBridgeStatus({
|
|
742
1207
|
state: wasShuttingDown ? "stopped" : "error",
|
|
743
1208
|
connectionStatus: "disconnected",
|
|
@@ -752,44 +1217,42 @@ function startBridge({
|
|
|
752
1217
|
desktopRefresher.handleTransportReset();
|
|
753
1218
|
failBridgeManagedCodexRequests(new Error("Codex transport closed before the bridge request completed."));
|
|
754
1219
|
forwardedRequestMethodsById.clear();
|
|
755
|
-
codexResponseRoutesById.clear();
|
|
756
1220
|
if (socket?.readyState === WebSocket.OPEN || socket?.readyState === WebSocket.CONNECTING) {
|
|
757
1221
|
socket.close();
|
|
758
1222
|
}
|
|
759
|
-
closeExtraRelayChannels();
|
|
760
1223
|
});
|
|
761
1224
|
|
|
762
1225
|
process.on("SIGINT", () => shutdown(codex, () => socket, prepareBridgeShutdown));
|
|
763
1226
|
process.on("SIGTERM", () => shutdown(codex, () => socket, prepareBridgeShutdown));
|
|
764
1227
|
|
|
765
1228
|
// Routes decrypted app payloads through the same bridge handlers as before.
|
|
766
|
-
function handleApplicationMessage(rawMessage
|
|
767
|
-
const
|
|
768
|
-
if (handleBridgeManagedHandshakeMessage(rawMessage,
|
|
1229
|
+
function handleApplicationMessage(rawMessage) {
|
|
1230
|
+
const parsedMessage = parseBridgeMessage(rawMessage);
|
|
1231
|
+
if (handleBridgeManagedHandshakeMessage(rawMessage, sendApplicationResponse, parsedMessage)) {
|
|
769
1232
|
return;
|
|
770
1233
|
}
|
|
771
|
-
if (handleBridgeManagedAccountRequest(rawMessage,
|
|
1234
|
+
if (handleBridgeManagedAccountRequest(rawMessage, sendApplicationResponse, parsedMessage)) {
|
|
772
1235
|
return;
|
|
773
1236
|
}
|
|
774
|
-
if (voiceHandler.handleVoiceRequest(rawMessage,
|
|
1237
|
+
if (voiceHandler.handleVoiceRequest(rawMessage, sendApplicationResponse, parsedMessage)) {
|
|
775
1238
|
return;
|
|
776
1239
|
}
|
|
777
|
-
if (handleThreadContextRequest(rawMessage,
|
|
1240
|
+
if (handleThreadContextRequest(rawMessage, sendApplicationResponse, parsedMessage)) {
|
|
778
1241
|
return;
|
|
779
1242
|
}
|
|
780
|
-
if (handleWorkspaceRequest(rawMessage,
|
|
1243
|
+
if (handleWorkspaceRequest(rawMessage, sendApplicationResponse)) {
|
|
781
1244
|
return;
|
|
782
1245
|
}
|
|
783
|
-
if (handleProjectRequest(rawMessage,
|
|
1246
|
+
if (handleProjectRequest(rawMessage, sendApplicationResponse, { projectRegistry })) {
|
|
784
1247
|
return;
|
|
785
1248
|
}
|
|
786
|
-
if (handlePetRequest(rawMessage,
|
|
1249
|
+
if (handlePetRequest(rawMessage, sendApplicationResponse)) {
|
|
787
1250
|
return;
|
|
788
1251
|
}
|
|
789
|
-
if (notificationsHandler.handleNotificationsRequest(rawMessage,
|
|
1252
|
+
if (notificationsHandler.handleNotificationsRequest(rawMessage, sendApplicationResponse)) {
|
|
790
1253
|
return;
|
|
791
1254
|
}
|
|
792
|
-
if (handleDesktopRequest(rawMessage,
|
|
1255
|
+
if (handleDesktopRequest(rawMessage, sendApplicationResponse, {
|
|
793
1256
|
bundleId: config.codexBundleId,
|
|
794
1257
|
appPath: config.codexAppPath,
|
|
795
1258
|
readBridgePreferences,
|
|
@@ -797,200 +1260,102 @@ function startBridge({
|
|
|
797
1260
|
updateBridgePackageAndRestart,
|
|
798
1261
|
})) {
|
|
799
1262
|
return;
|
|
800
|
-
}
|
|
801
|
-
if (handleGitRequest(rawMessage,
|
|
802
|
-
codexAppPath: config.codexAppPath,
|
|
803
|
-
onThreadNameSet: sendThreadNameUpdatedNotification,
|
|
804
|
-
})) {
|
|
805
|
-
return;
|
|
806
|
-
}
|
|
807
|
-
desktopRefresher.handleInbound(rawMessage);
|
|
808
|
-
rolloutLiveMirror?.observeInbound(rawMessage);
|
|
809
|
-
if (desktopIpcActionFollower?.observeInbound(rawMessage)) {
|
|
810
|
-
return;
|
|
811
|
-
}
|
|
812
|
-
if (handleBridgeManagedThreadTurnsListRequest(rawMessage, sendResponse)) {
|
|
813
|
-
return;
|
|
814
|
-
}
|
|
815
|
-
const codexRequest = disableUnsupportedReasoningSummaryForTurnStart(rawMessage);
|
|
816
|
-
const codexMessage = prepareCodexForwardMessage(codexRequest, relayChannel);
|
|
817
|
-
rememberForwardedRequestMethod(codexMessage);
|
|
818
|
-
rememberThreadFromMessage("phone", codexMessage);
|
|
819
|
-
mirrorUserMessageToPeerDevices(rawMessage, relayChannel);
|
|
820
|
-
codex.send(codexMessage);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
// Encrypts bridge-generated responses instead of letting the relay see plaintext.
|
|
824
|
-
function sendApplicationResponse(rawMessage) {
|
|
825
|
-
sendApplicationResponseToChannels(rawMessage, relayChannels);
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
function sendApplicationResponseToChannel(rawMessage, relayChannel = primaryRelayChannel) {
|
|
829
|
-
sendApplicationResponseToChannels(rawMessage, [relayChannel]);
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
function sendApplicationResponseToChannels(rawMessage, channels) {
|
|
833
|
-
const normalizedChannels = channels.filter(Boolean);
|
|
834
|
-
if (normalizedChannels.length === 0) {
|
|
1263
|
+
}
|
|
1264
|
+
if (handleGitRequest(rawMessage, sendApplicationResponse, {
|
|
1265
|
+
codexAppPath: config.codexAppPath,
|
|
1266
|
+
onThreadNameSet: sendThreadNameUpdatedNotification,
|
|
1267
|
+
})) {
|
|
835
1268
|
return;
|
|
836
1269
|
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1270
|
+
desktopRefresher.handleInbound(rawMessage, parsedMessage);
|
|
1271
|
+
rolloutLiveMirror?.observeInbound(rawMessage, parsedMessage);
|
|
1272
|
+
// Track the request method BEFORE follower interception: responses the
|
|
1273
|
+
// follower serves from projected Desktop state must hit the same relay
|
|
1274
|
+
// sanitize/trim budget as app-server responses, or heavy threads ship as
|
|
1275
|
+
// one oversized frame and kill the phone's websocket (EMSGSIZE).
|
|
1276
|
+
rememberForwardedRequestMethod(rawMessage);
|
|
1277
|
+
if (desktopIpcActionFollower?.observeInbound(rawMessage, parsedMessage)) {
|
|
1278
|
+
return;
|
|
841
1279
|
}
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
1280
|
+
if (runtimeProviderRouter.handleApplicationMessage(rawMessage, {
|
|
1281
|
+
sendResponse: sendApplicationResponse,
|
|
1282
|
+
})) {
|
|
1283
|
+
return;
|
|
1284
|
+
}
|
|
1285
|
+
observeDesktopIpcLiveOwnerInbound(rawMessage, parsedMessage);
|
|
1286
|
+
if (handleBridgeManagedThreadTurnsListRequest(rawMessage, sendApplicationResponse)) {
|
|
846
1287
|
return;
|
|
847
1288
|
}
|
|
1289
|
+
forwardInboundRequestToCodex(rawMessage);
|
|
1290
|
+
}
|
|
848
1291
|
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
1292
|
+
function forwardInboundRequestToCodex(rawMessage) {
|
|
1293
|
+
const codexRequest = stripRuntimeProviderFieldsForCodex(
|
|
1294
|
+
normalizeTurnStartForCodex(rawMessage)
|
|
852
1295
|
);
|
|
1296
|
+
rememberKnownProjectFromRequest("codex-request", codexRequest);
|
|
1297
|
+
rememberForwardedRequestMethod(rawMessage);
|
|
1298
|
+
rememberThreadFromMessage("phone", codexRequest);
|
|
1299
|
+
codex.send(codexRequest);
|
|
853
1300
|
}
|
|
854
1301
|
|
|
855
|
-
//
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
if (!
|
|
859
|
-
return
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
pruneExpiredCodexResponseRoutes();
|
|
863
|
-
const originalId = parsed.id;
|
|
864
|
-
const forwardedId = `mobile:${relayChannel.label}:${randomBytes(8).toString("hex")}`;
|
|
865
|
-
if (parsed.method === "initialize") {
|
|
866
|
-
forwardedInitializeRequestIds.delete(String(originalId));
|
|
867
|
-
forwardedInitializeRequestIds.add(String(forwardedId));
|
|
1302
|
+
// Held Desktop-ownership probes can later fall back locally, so observe each
|
|
1303
|
+
// phone request at most once in the live owner even if it passes both paths.
|
|
1304
|
+
function observeDesktopIpcLiveOwnerInbound(rawMessage, parsedMessage = null) {
|
|
1305
|
+
if (!desktopIpcLiveOwner) {
|
|
1306
|
+
return;
|
|
868
1307
|
}
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
originalId,
|
|
873
|
-
createdAt: Date.now(),
|
|
874
|
-
});
|
|
875
|
-
return JSON.stringify(parsed);
|
|
876
|
-
}
|
|
877
|
-
|
|
878
|
-
function sendCodexOutboundToMobile(rawMessage) {
|
|
879
|
-
pruneExpiredCodexResponseRoutes();
|
|
880
|
-
const parsed = safeParseJSON(rawMessage);
|
|
881
|
-
const responseId = parsed?.id;
|
|
882
|
-
if (responseId != null) {
|
|
883
|
-
const route = codexResponseRoutesById.get(String(responseId));
|
|
884
|
-
if (route) {
|
|
885
|
-
codexResponseRoutesById.delete(String(responseId));
|
|
886
|
-
const sanitizedMessage = sanitizeRelayBoundCodexMessage(rawMessage);
|
|
887
|
-
const sanitizedParsed = safeParseJSON(sanitizedMessage);
|
|
888
|
-
if (sanitizedParsed && typeof sanitizedParsed === "object") {
|
|
889
|
-
sanitizedParsed.id = route.originalId;
|
|
890
|
-
queueSanitizedApplicationMessageToChannel(JSON.stringify(sanitizedParsed), route.relayChannel);
|
|
891
|
-
return;
|
|
892
|
-
}
|
|
893
|
-
queueSanitizedApplicationMessageToChannel(sanitizedMessage, route.relayChannel);
|
|
1308
|
+
const inboundKey = desktopIpcLiveOwnerInboundKey(rawMessage, parsedMessage);
|
|
1309
|
+
if (inboundKey) {
|
|
1310
|
+
if (desktopIpcLiveOwnerObservedInboundKeys.has(inboundKey)) {
|
|
894
1311
|
return;
|
|
895
1312
|
}
|
|
1313
|
+
desktopIpcLiveOwnerObservedInboundKeys.add(inboundKey);
|
|
1314
|
+
evictOldestEntries(desktopIpcLiveOwnerObservedInboundKeys, FORWARDED_REQUEST_METHODS_MAX_SIZE);
|
|
896
1315
|
}
|
|
897
|
-
|
|
898
|
-
sendApplicationResponse(rawMessage);
|
|
899
|
-
}
|
|
900
|
-
|
|
901
|
-
function pruneExpiredCodexResponseRoutes() {
|
|
902
|
-
const cutoff = Date.now() - forwardedRequestMethodTTLms;
|
|
903
|
-
for (const [requestId, route] of codexResponseRoutesById.entries()) {
|
|
904
|
-
if (!route || route.createdAt < cutoff) {
|
|
905
|
-
codexResponseRoutesById.delete(requestId);
|
|
906
|
-
}
|
|
907
|
-
}
|
|
1316
|
+
desktopIpcLiveOwner.observeInbound(rawMessage, parsedMessage);
|
|
908
1317
|
}
|
|
909
1318
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
const
|
|
913
|
-
if (!
|
|
914
|
-
return;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
const peerChannels = relayChannels.filter((relayChannel) => relayChannel !== originRelayChannel);
|
|
918
|
-
if (peerChannels.length === 0) {
|
|
919
|
-
return;
|
|
1319
|
+
function desktopIpcLiveOwnerInboundKey(rawMessage, parsedMessage = null) {
|
|
1320
|
+
const parsed = parsedMessage ?? safeParseJSON(rawMessage);
|
|
1321
|
+
const method = typeof parsed?.method === "string" ? parsed.method : "";
|
|
1322
|
+
if (!method || parsed?.id == null) {
|
|
1323
|
+
return "";
|
|
920
1324
|
}
|
|
921
|
-
|
|
922
|
-
|
|
1325
|
+
// extractThreadId only understands turn/thread start and completion params;
|
|
1326
|
+
// archive, steer, interrupt, and compact requests need the generic fields so
|
|
1327
|
+
// same-id requests for different threads never share a dedupe key.
|
|
1328
|
+
const threadId = extractThreadId(method, parsed.params)
|
|
1329
|
+
|| readString(parsed?.params?.threadId)
|
|
1330
|
+
|| readString(parsed?.params?.thread_id)
|
|
1331
|
+
|| readString(parsed?.params?.conversationId)
|
|
1332
|
+
|| readString(parsed?.params?.conversation_id)
|
|
1333
|
+
|| "";
|
|
1334
|
+
return `${method}:${threadId}:${String(parsed.id)}`;
|
|
923
1335
|
}
|
|
924
1336
|
|
|
925
|
-
function
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
return null;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
|
-
const params = parsed?.params && typeof parsed.params === "object" ? parsed.params : null;
|
|
933
|
-
const threadId = readString(params?.threadId || params?.thread_id);
|
|
934
|
-
const text = extractTextFromTurnPayload(params);
|
|
935
|
-
if (!threadId || !text) {
|
|
1337
|
+
function parseBridgeMessage(rawMessage) {
|
|
1338
|
+
try {
|
|
1339
|
+
return JSON.parse(rawMessage);
|
|
1340
|
+
} catch {
|
|
936
1341
|
return null;
|
|
937
1342
|
}
|
|
938
|
-
|
|
939
|
-
const turnId = readString(params?.turnId || params?.turn_id || params?.expectedTurnId || params?.expected_turn_id);
|
|
940
|
-
return {
|
|
941
|
-
method: "codex/event/user_message",
|
|
942
|
-
params: {
|
|
943
|
-
threadId,
|
|
944
|
-
thread_id: threadId,
|
|
945
|
-
turnId: turnId || undefined,
|
|
946
|
-
turn_id: turnId || undefined,
|
|
947
|
-
message: text,
|
|
948
|
-
text,
|
|
949
|
-
source: "peer-mobile",
|
|
950
|
-
},
|
|
951
|
-
};
|
|
952
1343
|
}
|
|
953
1344
|
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
return extractTextFromTurnInput(params?.input);
|
|
1345
|
+
// Encrypts bridge-generated responses instead of letting the relay see plaintext.
|
|
1346
|
+
function sendApplicationResponse(rawMessage) {
|
|
1347
|
+
secureTransport.queueOutboundApplicationMessage(
|
|
1348
|
+
sanitizeRelayBoundCodexMessage(rawMessage),
|
|
1349
|
+
sendRelayWireMessage
|
|
1350
|
+
);
|
|
961
1351
|
}
|
|
962
1352
|
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
const directText = readString(input.text || input.message || input.content);
|
|
970
|
-
if (directText) {
|
|
971
|
-
return directText;
|
|
972
|
-
}
|
|
973
|
-
}
|
|
974
|
-
|
|
975
|
-
const inputItems = Array.isArray(input)
|
|
976
|
-
? input
|
|
977
|
-
: Array.isArray(input?.items)
|
|
978
|
-
? input.items
|
|
979
|
-
: [];
|
|
980
|
-
const textParts = [];
|
|
981
|
-
for (const item of inputItems) {
|
|
982
|
-
if (!item || typeof item !== "object") {
|
|
983
|
-
continue;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
const itemType = readString(item.type).toLowerCase();
|
|
987
|
-
const itemText = readString(item.text || item.message || item.content);
|
|
988
|
-
if ((itemType === "text" || itemType === "input_text" || itemType === "message") && itemText) {
|
|
989
|
-
textParts.push(itemText);
|
|
990
|
-
}
|
|
991
|
-
}
|
|
992
|
-
|
|
993
|
-
return readString(textParts.join("\n\n"));
|
|
1353
|
+
// Provider output keeps the same desktop refresh, push, and secure relay side effects as Codex output.
|
|
1354
|
+
function sendRuntimeApplicationMessage(provider, rawMessage) {
|
|
1355
|
+
desktopRefresher.handleOutbound(rawMessage);
|
|
1356
|
+
pushNotificationTracker.handleOutbound(rawMessage);
|
|
1357
|
+
rememberThreadFromMessage(provider, rawMessage);
|
|
1358
|
+
sendApplicationResponse(rawMessage);
|
|
994
1359
|
}
|
|
995
1360
|
|
|
996
1361
|
// Mirrors accepted local renames back to the phone using the existing push-event shape.
|
|
@@ -1020,25 +1385,41 @@ function startBridge({
|
|
|
1020
1385
|
|
|
1021
1386
|
rememberThreadFromMessage("phone", rawMessage);
|
|
1022
1387
|
(async () => {
|
|
1388
|
+
let didRespond = false;
|
|
1389
|
+
const respondOnce = (payload) => {
|
|
1390
|
+
if (didRespond) {
|
|
1391
|
+
return;
|
|
1392
|
+
}
|
|
1393
|
+
didRespond = true;
|
|
1394
|
+
sendResponse(payload);
|
|
1395
|
+
};
|
|
1023
1396
|
try {
|
|
1024
|
-
const
|
|
1025
|
-
|
|
1397
|
+
const selection = await threadTurnsListFastPageCoordinator.resolve(request, {
|
|
1398
|
+
fetchCanonical: (canonicalRequest) => fetchAdaptiveThreadTurnsListForRelay(canonicalRequest, {
|
|
1399
|
+
fetchPage: (params) => sendCodexRequest("thread/turns/list", params),
|
|
1400
|
+
}),
|
|
1401
|
+
readJsonl: (jsonlRequest) => maybeBuildJsonlThreadTurnsListFallback(jsonlRequest, null),
|
|
1026
1402
|
});
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1403
|
+
let responsePayload = selection.response;
|
|
1404
|
+
if (selection.source === "canonical" && selection.jsonlFallback?.response?.result) {
|
|
1405
|
+
responsePayload = maybeMergeLatestJsonlTurnIntoTurnsListResponse(
|
|
1406
|
+
request,
|
|
1407
|
+
selection.response,
|
|
1408
|
+
selection.jsonlFallback.response.result
|
|
1409
|
+
) || selection.response;
|
|
1410
|
+
}
|
|
1411
|
+
sendBridgeManagedThreadTurnsListResponse(request, responsePayload, respondOnce, {
|
|
1412
|
+
skipJsonlArtifactAugmentation: selection.usesJsonl,
|
|
1034
1413
|
});
|
|
1035
|
-
sendResponse(sanitizeThreadHistoryImagesForRelay(
|
|
1036
|
-
JSON.stringify(responsePayload),
|
|
1037
|
-
"thread/turns/list",
|
|
1038
|
-
finalSanitizeContext
|
|
1039
|
-
));
|
|
1040
1414
|
} catch (error) {
|
|
1041
|
-
|
|
1415
|
+
const jsonlFallback = maybeBuildJsonlThreadTurnsListFallback(request, null);
|
|
1416
|
+
if (jsonlFallback?.response && isCoherentJsonlFirstPageResponse(jsonlFallback.response)) {
|
|
1417
|
+
sendBridgeManagedThreadTurnsListResponse(request, jsonlFallback.response, respondOnce, {
|
|
1418
|
+
skipJsonlArtifactAugmentation: true,
|
|
1419
|
+
});
|
|
1420
|
+
return;
|
|
1421
|
+
}
|
|
1422
|
+
respondOnce(createJsonRpcErrorResponse(
|
|
1042
1423
|
request.id,
|
|
1043
1424
|
error,
|
|
1044
1425
|
"thread_turns_list_failed"
|
|
@@ -1049,16 +1430,34 @@ function startBridge({
|
|
|
1049
1430
|
return true;
|
|
1050
1431
|
}
|
|
1051
1432
|
|
|
1433
|
+
function sendBridgeManagedThreadTurnsListResponse(request, response, sendResponse, {
|
|
1434
|
+
skipJsonlArtifactAugmentation = false,
|
|
1435
|
+
} = {}) {
|
|
1436
|
+
const finalSanitizeContext = buildThreadTurnsListRelaySanitizeContext(request, {
|
|
1437
|
+
skipJsonlArtifactAugmentation,
|
|
1438
|
+
});
|
|
1439
|
+
relaySanitizedResponseMethodsById.set(String(request.id), {
|
|
1440
|
+
method: "thread/turns/list",
|
|
1441
|
+
...finalSanitizeContext,
|
|
1442
|
+
createdAt: Date.now(),
|
|
1443
|
+
});
|
|
1444
|
+
sendResponse(sanitizeThreadHistoryImagesForRelay(
|
|
1445
|
+
JSON.stringify(response),
|
|
1446
|
+
"thread/turns/list",
|
|
1447
|
+
finalSanitizeContext
|
|
1448
|
+
));
|
|
1449
|
+
}
|
|
1450
|
+
|
|
1052
1451
|
function maybeBuildJsonlThreadTurnsListFallback(request, response) {
|
|
1053
1452
|
const params = request?.params || {};
|
|
1054
1453
|
const threadId = normalizeNonEmptyString(params.threadId)
|
|
1055
1454
|
|| normalizeNonEmptyString(params.thread_id);
|
|
1056
|
-
if (!threadId || hasRelayCursor(params.cursor)) {
|
|
1455
|
+
if (!threadId || hasRelayCursor(params.cursor) || params.remodexRequireCanonical === true) {
|
|
1057
1456
|
return null;
|
|
1058
1457
|
}
|
|
1059
1458
|
|
|
1060
1459
|
try {
|
|
1061
|
-
const responseIsEmpty = isEmptyTurnsListResponse(response);
|
|
1460
|
+
const responseIsEmpty = response == null || isEmptyTurnsListResponse(response);
|
|
1062
1461
|
const rolloutPath = resolveJsonlTurnsListRolloutPathForFallback({
|
|
1063
1462
|
threadId,
|
|
1064
1463
|
responseIsEmpty,
|
|
@@ -1069,10 +1468,20 @@ function startBridge({
|
|
|
1069
1468
|
return null;
|
|
1070
1469
|
}
|
|
1071
1470
|
|
|
1471
|
+
// A first page is the local baseline for a newly opened thread. Honor a
|
|
1472
|
+
// caller's larger request, but never manufacture the old one-turn tail:
|
|
1473
|
+
// it has no room to preserve surrounding history while canonical data is
|
|
1474
|
+
// still catching up.
|
|
1475
|
+
const requestedLimit = Number.isInteger(params.limit) && params.limit > 0
|
|
1476
|
+
? params.limit
|
|
1477
|
+
: RELAY_TURNS_LIST_MAX_INITIAL_LIMIT;
|
|
1478
|
+
const firstPageLimit = params.cursor == null
|
|
1479
|
+
? Math.max(requestedLimit, RELAY_TURNS_LIST_MAX_INITIAL_LIMIT)
|
|
1480
|
+
: requestedLimit;
|
|
1072
1481
|
const result = readThreadTurnsListPageFromSessionJsonl(rolloutPath, {
|
|
1073
1482
|
threadId,
|
|
1074
|
-
limit:
|
|
1075
|
-
maxLimit:
|
|
1483
|
+
limit: firstPageLimit,
|
|
1484
|
+
maxLimit: RELAY_TURNS_LIST_MAX_INITIAL_LIMIT,
|
|
1076
1485
|
cursor: params.cursor,
|
|
1077
1486
|
});
|
|
1078
1487
|
const turnsKey = findTurnsListResultKey(result);
|
|
@@ -1081,7 +1490,7 @@ function startBridge({
|
|
|
1081
1490
|
}
|
|
1082
1491
|
|
|
1083
1492
|
if (!responseIsEmpty) {
|
|
1084
|
-
const mergedResponse = maybeMergeLatestJsonlTurnIntoTurnsListResponse(request, response, result
|
|
1493
|
+
const mergedResponse = maybeMergeLatestJsonlTurnIntoTurnsListResponse(request, response, result);
|
|
1085
1494
|
return mergedResponse ? { response: mergedResponse, usesJsonl: true } : null;
|
|
1086
1495
|
}
|
|
1087
1496
|
|
|
@@ -1147,11 +1556,9 @@ function startBridge({
|
|
|
1147
1556
|
|
|
1148
1557
|
// Handles the bridge-owned auth status wrappers without exposing tokens to the phone.
|
|
1149
1558
|
// This dispatcher stays synchronous so non-account messages can continue down the normal routing chain.
|
|
1150
|
-
function handleBridgeManagedAccountRequest(rawMessage, sendResponse) {
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
parsed = JSON.parse(rawMessage);
|
|
1154
|
-
} catch {
|
|
1559
|
+
function handleBridgeManagedAccountRequest(rawMessage, sendResponse, parsedMessage = null) {
|
|
1560
|
+
const parsed = parsedMessage || parseBridgeMessage(rawMessage);
|
|
1561
|
+
if (!parsed) {
|
|
1155
1562
|
return false;
|
|
1156
1563
|
}
|
|
1157
1564
|
|
|
@@ -1306,16 +1713,30 @@ function startBridge({
|
|
|
1306
1713
|
}
|
|
1307
1714
|
|
|
1308
1715
|
// Replaces huge inline desktop-history images with lightweight references before relay encryption.
|
|
1309
|
-
function sanitizeRelayBoundCodexMessage(rawMessage) {
|
|
1716
|
+
function sanitizeRelayBoundCodexMessage(rawMessage, parsedMessage = null) {
|
|
1310
1717
|
pruneExpiredForwardedRequestMethods();
|
|
1311
|
-
|
|
1718
|
+
let normalizedMessage = normalizeRelayBoundJsonRpcMessage(rawMessage, {
|
|
1312
1719
|
pendingRequestMethodsById: relaySanitizedResponseMethodsById,
|
|
1720
|
+
parsedMessage,
|
|
1313
1721
|
});
|
|
1314
1722
|
if (!normalizedMessage) {
|
|
1315
1723
|
return null;
|
|
1316
1724
|
}
|
|
1317
1725
|
|
|
1318
|
-
|
|
1726
|
+
// Streaming deltas hit this path dozens of times per second; when the
|
|
1727
|
+
// envelope passed through normalization untouched, reuse the parse the
|
|
1728
|
+
// caller already paid for instead of re-parsing the same bytes.
|
|
1729
|
+
let parsed = normalizedMessage === rawMessage && parsedMessage
|
|
1730
|
+
? parsedMessage
|
|
1731
|
+
: safeParseJSON(normalizedMessage);
|
|
1732
|
+
const sanitizedLiveMessage = sanitizeLiveUserNotification(parsed);
|
|
1733
|
+
if (!sanitizedLiveMessage) {
|
|
1734
|
+
return null;
|
|
1735
|
+
}
|
|
1736
|
+
if (sanitizedLiveMessage !== parsed) {
|
|
1737
|
+
parsed = sanitizedLiveMessage;
|
|
1738
|
+
normalizedMessage = JSON.stringify(parsed);
|
|
1739
|
+
}
|
|
1319
1740
|
const responseId = parsed?.id;
|
|
1320
1741
|
if (responseId == null) {
|
|
1321
1742
|
return sanitizeLiveGeneratedImageMessageForRelay(normalizedMessage);
|
|
@@ -1327,12 +1748,19 @@ function startBridge({
|
|
|
1327
1748
|
}
|
|
1328
1749
|
relaySanitizedResponseMethodsById.delete(String(responseId));
|
|
1329
1750
|
|
|
1751
|
+
if (trackedRequest.method === "thread/list"
|
|
1752
|
+
|| trackedRequest.method === "thread/read"
|
|
1753
|
+
|| trackedRequest.method === "thread/resume") {
|
|
1754
|
+
threadRuntimeSettingsStore.enrichResponse(trackedRequest.method, parsed);
|
|
1755
|
+
normalizedMessage = JSON.stringify(parsed);
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1330
1758
|
return sanitizeThreadHistoryImagesForRelay(normalizedMessage, trackedRequest.method, trackedRequest);
|
|
1331
1759
|
}
|
|
1332
1760
|
|
|
1333
|
-
function updatePendingAuthLoginFromCodexMessage(rawMessage) {
|
|
1761
|
+
function updatePendingAuthLoginFromCodexMessage(rawMessage, parsedMessage = null) {
|
|
1334
1762
|
pruneExpiredForwardedRequestMethods();
|
|
1335
|
-
const parsed = safeParseJSON(rawMessage);
|
|
1763
|
+
const parsed = parsedMessage ?? safeParseJSON(rawMessage);
|
|
1336
1764
|
const responseId = parsed?.id;
|
|
1337
1765
|
if (responseId != null) {
|
|
1338
1766
|
const trackedRequest = forwardedRequestMethodsById.get(String(responseId));
|
|
@@ -1405,6 +1833,7 @@ function startBridge({
|
|
|
1405
1833
|
evictOldestEntries(jsonlArtifactItemsCacheByThread, RELAY_JSONL_ARTIFACT_CACHE_MAX_ENTRIES);
|
|
1406
1834
|
evictOldestEntries(jsonlTurnsListRolloutCacheByThread, JSONL_ROLLOUT_PATH_CACHE_MAX_SIZE);
|
|
1407
1835
|
evictOldestEntries(jsonlTurnsListRolloutMissCacheByThread, JSONL_ROLLOUT_PATH_CACHE_MAX_SIZE);
|
|
1836
|
+
evictOldestEntries(jsonlThreadCwdCacheByThread, JSONL_ROLLOUT_PATH_CACHE_MAX_SIZE);
|
|
1408
1837
|
}
|
|
1409
1838
|
|
|
1410
1839
|
function safeParseJSON(value) {
|
|
@@ -1415,8 +1844,8 @@ function startBridge({
|
|
|
1415
1844
|
}
|
|
1416
1845
|
}
|
|
1417
1846
|
|
|
1418
|
-
function rememberThreadFromMessage(source, rawMessage) {
|
|
1419
|
-
const context = extractBridgeMessageContext(rawMessage);
|
|
1847
|
+
function rememberThreadFromMessage(source, rawMessage, parsedMessage = null) {
|
|
1848
|
+
const context = extractBridgeMessageContext(rawMessage, parsedMessage);
|
|
1420
1849
|
if (!context.threadId) {
|
|
1421
1850
|
return;
|
|
1422
1851
|
}
|
|
@@ -1427,6 +1856,33 @@ function startBridge({
|
|
|
1427
1856
|
}
|
|
1428
1857
|
}
|
|
1429
1858
|
|
|
1859
|
+
// Captures explicit cwd selections before Codex creates the first thread, so
|
|
1860
|
+
// provider-neutral pickers do not depend on a later provider-specific message.
|
|
1861
|
+
function rememberKnownProjectFromRequest(source, rawMessage) {
|
|
1862
|
+
const parsed = safeParseJSON(rawMessage);
|
|
1863
|
+
const method = typeof parsed?.method === "string" ? parsed.method.trim() : "";
|
|
1864
|
+
if (method !== "thread/start" && method !== "turn/start") {
|
|
1865
|
+
return;
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
const params = parsed?.params || {};
|
|
1869
|
+
const cwd = normalizeNonEmptyString(
|
|
1870
|
+
params.cwd || params.current_working_directory || params.working_directory
|
|
1871
|
+
);
|
|
1872
|
+
if (!cwd) {
|
|
1873
|
+
return;
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
try {
|
|
1877
|
+
projectRegistry.rememberProjectPath(cwd, {
|
|
1878
|
+
source,
|
|
1879
|
+
provider: "codex",
|
|
1880
|
+
});
|
|
1881
|
+
} catch {
|
|
1882
|
+
// Registry persistence is best-effort; thread creation must keep flowing.
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
|
|
1430
1886
|
// Mirrors CodexMonitor's persisted token_count fallback so the phone keeps
|
|
1431
1887
|
// receiving context-window usage even when the runtime omits live thread usage.
|
|
1432
1888
|
function ensureContextUsageWatcher({ threadId, turnId }) {
|
|
@@ -1493,11 +1949,9 @@ function startBridge({
|
|
|
1493
1949
|
// The spawned/shared Codex app-server stays warm across phone reconnects.
|
|
1494
1950
|
// When iPhone reconnects it sends initialize again, but forwarding that to the
|
|
1495
1951
|
// already-initialized Codex transport only produces "Already initialized".
|
|
1496
|
-
function handleBridgeManagedHandshakeMessage(rawMessage, sendResponse = sendApplicationResponse) {
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
parsed = JSON.parse(rawMessage);
|
|
1500
|
-
} catch {
|
|
1952
|
+
function handleBridgeManagedHandshakeMessage(rawMessage, sendResponse = sendApplicationResponse, parsedMessage = null) {
|
|
1953
|
+
const parsed = parsedMessage || parseBridgeMessage(rawMessage);
|
|
1954
|
+
if (!parsed) {
|
|
1501
1955
|
return false;
|
|
1502
1956
|
}
|
|
1503
1957
|
|
|
@@ -1602,11 +2056,9 @@ function startBridge({
|
|
|
1602
2056
|
}
|
|
1603
2057
|
|
|
1604
2058
|
// Learns whether the underlying Codex transport has already completed its own MCP handshake.
|
|
1605
|
-
function trackCodexHandshakeState(rawMessage) {
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
parsed = JSON.parse(rawMessage);
|
|
1609
|
-
} catch {
|
|
2059
|
+
function trackCodexHandshakeState(rawMessage, parsedMessage = null) {
|
|
2060
|
+
const parsed = parsedMessage ?? safeParseJSON(rawMessage);
|
|
2061
|
+
if (!parsed) {
|
|
1610
2062
|
return;
|
|
1611
2063
|
}
|
|
1612
2064
|
|
|
@@ -1670,11 +2122,9 @@ function startBridge({
|
|
|
1670
2122
|
|
|
1671
2123
|
// Intercepts responses for bridge-private requests so only user-visible app-server traffic
|
|
1672
2124
|
// is forwarded back through secure transport.
|
|
1673
|
-
function handleBridgeManagedCodexResponse(rawMessage) {
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
parsed = JSON.parse(rawMessage);
|
|
1677
|
-
} catch {
|
|
2125
|
+
function handleBridgeManagedCodexResponse(rawMessage, parsedMessage = null) {
|
|
2126
|
+
const parsed = parsedMessage ?? safeParseJSON(rawMessage);
|
|
2127
|
+
if (!parsed) {
|
|
1678
2128
|
return false;
|
|
1679
2129
|
}
|
|
1680
2130
|
|
|
@@ -1734,11 +2184,6 @@ function startBridge({
|
|
|
1734
2184
|
function sendRelayRegistrationUpdate(nextDeviceState) {
|
|
1735
2185
|
deviceState = nextDeviceState;
|
|
1736
2186
|
if (socket?.readyState !== WebSocket.OPEN) {
|
|
1737
|
-
for (const relayChannel of relayChannels) {
|
|
1738
|
-
if (relayChannel !== primaryRelayChannel) {
|
|
1739
|
-
sendExtraRelayRegistrationUpdate(relayChannel, nextDeviceState);
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
2187
|
return;
|
|
1743
2188
|
}
|
|
1744
2189
|
|
|
@@ -1746,11 +2191,6 @@ function startBridge({
|
|
|
1746
2191
|
kind: "relayMacRegistration",
|
|
1747
2192
|
registration: buildMacRegistration(nextDeviceState, pairingSession),
|
|
1748
2193
|
}));
|
|
1749
|
-
for (const relayChannel of relayChannels) {
|
|
1750
|
-
if (relayChannel !== primaryRelayChannel) {
|
|
1751
|
-
sendExtraRelayRegistrationUpdate(relayChannel, nextDeviceState);
|
|
1752
|
-
}
|
|
1753
|
-
}
|
|
1754
2194
|
}
|
|
1755
2195
|
|
|
1756
2196
|
function readBridgePreferences() {
|
|
@@ -1832,6 +2272,9 @@ function startBridge({
|
|
|
1832
2272
|
stdio: "ignore",
|
|
1833
2273
|
env: process.env,
|
|
1834
2274
|
});
|
|
2275
|
+
child.on?.("error", (error) => {
|
|
2276
|
+
console.warn(`[remodex] Failed to schedule the post-update bridge restart: ${error?.message || error}`);
|
|
2277
|
+
});
|
|
1835
2278
|
child.unref?.();
|
|
1836
2279
|
}, BRIDGE_RESTART_AFTER_UPDATE_DELAY_MS);
|
|
1837
2280
|
restartTimer.unref?.();
|
|
@@ -1932,7 +2375,7 @@ function createMacOSBridgeWakeAssertion({
|
|
|
1932
2375
|
};
|
|
1933
2376
|
}
|
|
1934
2377
|
|
|
1935
|
-
// Registers the canonical Mac identity
|
|
2378
|
+
// Registers the canonical Mac identity and the one trusted phone allowed for auto-resolve.
|
|
1936
2379
|
function buildMacRegistrationHeaders(deviceState, pairingSession) {
|
|
1937
2380
|
const registration = buildMacRegistration(deviceState, pairingSession);
|
|
1938
2381
|
const headers = {
|
|
@@ -1950,20 +2393,6 @@ function buildMacRegistrationHeaders(deviceState, pairingSession) {
|
|
|
1950
2393
|
return headers;
|
|
1951
2394
|
}
|
|
1952
2395
|
|
|
1953
|
-
function readExtraRelaySessionCount(env = process.env) {
|
|
1954
|
-
const rawValue = readString(env.REMODEX_EXTRA_RELAY_SESSIONS || env.PHODEX_EXTRA_RELAY_SESSIONS);
|
|
1955
|
-
if (!rawValue) {
|
|
1956
|
-
return 0;
|
|
1957
|
-
}
|
|
1958
|
-
|
|
1959
|
-
const parsed = Number.parseInt(rawValue, 10);
|
|
1960
|
-
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
1961
|
-
return 0;
|
|
1962
|
-
}
|
|
1963
|
-
|
|
1964
|
-
return Math.min(parsed, 3);
|
|
1965
|
-
}
|
|
1966
|
-
|
|
1967
2396
|
function buildMacRegistration(deviceState, pairingSession) {
|
|
1968
2397
|
const trustedPhoneEntry = Object.entries(deviceState?.trustedPhones || {})[0] || null;
|
|
1969
2398
|
return {
|
|
@@ -2021,7 +2450,7 @@ function shortFingerprint(value) {
|
|
|
2021
2450
|
return createHash("sha256").update(normalized).digest("hex").slice(0, 8);
|
|
2022
2451
|
}
|
|
2023
2452
|
|
|
2024
|
-
function shutdown(codex, getSocket, beforeExit = () => {}
|
|
2453
|
+
function shutdown(codex, getSocket, beforeExit = () => {}) {
|
|
2025
2454
|
beforeExit();
|
|
2026
2455
|
|
|
2027
2456
|
const socket = getSocket();
|
|
@@ -2031,41 +2460,7 @@ function shutdown(codex, getSocket, beforeExit = () => {}, { exitCode = 0 } = {}
|
|
|
2031
2460
|
|
|
2032
2461
|
codex.shutdown();
|
|
2033
2462
|
|
|
2034
|
-
setTimeout(() => process.exit(
|
|
2035
|
-
}
|
|
2036
|
-
|
|
2037
|
-
function isTerminalRelayCloseCode(closeCode) {
|
|
2038
|
-
return closeCode === CLOSE_CODE_INVALID_RELAY_REQUEST
|
|
2039
|
-
|| closeCode === CLOSE_CODE_REPLACED_BY_NEW_MAC
|
|
2040
|
-
|| closeCode === CLOSE_CODE_MAC_UNAUTHORIZED;
|
|
2041
|
-
}
|
|
2042
|
-
|
|
2043
|
-
function normalizeWebSocketCloseReason(reason) {
|
|
2044
|
-
if (typeof reason === "string") {
|
|
2045
|
-
return reason.trim();
|
|
2046
|
-
}
|
|
2047
|
-
|
|
2048
|
-
if (Buffer.isBuffer(reason)) {
|
|
2049
|
-
return reason.toString("utf8").trim();
|
|
2050
|
-
}
|
|
2051
|
-
|
|
2052
|
-
return "";
|
|
2053
|
-
}
|
|
2054
|
-
|
|
2055
|
-
function buildRelayCloseStatusError(closeCode, closeReason = "") {
|
|
2056
|
-
if (!Number.isInteger(closeCode) || closeCode === 1000 || closeCode === 1005) {
|
|
2057
|
-
return "";
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
const normalizedReason = normalizeNonEmptyString(closeReason);
|
|
2061
|
-
if (closeCode === CLOSE_CODE_MAC_UNAUTHORIZED) {
|
|
2062
|
-
return normalizedReason
|
|
2063
|
-
|| "Relay authorization failed. Set REMODEX_RELAY_ACCESS_TOKEN or use a relay that does not require a Mac access token.";
|
|
2064
|
-
}
|
|
2065
|
-
|
|
2066
|
-
return normalizedReason
|
|
2067
|
-
? `Relay closed the connection (${closeCode}): ${normalizedReason}`
|
|
2068
|
-
: `Relay closed the connection (${closeCode}).`;
|
|
2463
|
+
setTimeout(() => process.exit(0), 100);
|
|
2069
2464
|
}
|
|
2070
2465
|
|
|
2071
2466
|
// Forces app-server summary generation off for models whose Responses API calls
|
|
@@ -2097,17 +2492,86 @@ function disableUnsupportedReasoningSummaryForTurnStart(rawMessage) {
|
|
|
2097
2492
|
});
|
|
2098
2493
|
}
|
|
2099
2494
|
|
|
2495
|
+
function normalizeTurnStartParamsForCodex(params) {
|
|
2496
|
+
const normalizedRawMessage = normalizeTurnStartForCodex(JSON.stringify({
|
|
2497
|
+
method: "turn/start",
|
|
2498
|
+
params,
|
|
2499
|
+
}));
|
|
2500
|
+
const parsed = parseBridgeJSON(normalizedRawMessage);
|
|
2501
|
+
return parsed?.params && typeof parsed.params === "object" && !Array.isArray(parsed.params)
|
|
2502
|
+
? parsed.params
|
|
2503
|
+
: params;
|
|
2504
|
+
}
|
|
2505
|
+
|
|
2506
|
+
// A turn/start can carry the same runtime choice twice: in the legacy top-level
|
|
2507
|
+
// model/effort fields and in collaborationMode.settings. Codex treats the nested
|
|
2508
|
+
// collaboration settings as authoritative, so a stale Desktop value there can
|
|
2509
|
+
// silently override the model selected on the phone. Keep both representations
|
|
2510
|
+
// aligned before either direct app-server forwarding or Desktop-follower routing.
|
|
2511
|
+
function normalizeTurnStartForCodex(rawMessage) {
|
|
2512
|
+
const parsed = parseBridgeJSON(rawMessage);
|
|
2513
|
+
if (!parsed || parsed.method !== "turn/start") {
|
|
2514
|
+
return rawMessage;
|
|
2515
|
+
}
|
|
2516
|
+
|
|
2517
|
+
const params = parsed.params && typeof parsed.params === "object" && !Array.isArray(parsed.params)
|
|
2518
|
+
? parsed.params
|
|
2519
|
+
: null;
|
|
2520
|
+
if (!params) {
|
|
2521
|
+
return rawMessage;
|
|
2522
|
+
}
|
|
2523
|
+
|
|
2524
|
+
const model = normalizeNonEmptyString(params.model);
|
|
2525
|
+
const effort = normalizeNonEmptyString(params.effort);
|
|
2526
|
+
let changed = false;
|
|
2527
|
+
let nextParams = params;
|
|
2528
|
+
|
|
2529
|
+
for (const collaborationKey of ["collaborationMode", "collaboration_mode"]) {
|
|
2530
|
+
const collaborationMode = nextParams[collaborationKey];
|
|
2531
|
+
const settings = collaborationMode?.settings;
|
|
2532
|
+
if (!settings || typeof settings !== "object" || Array.isArray(settings)) {
|
|
2533
|
+
continue;
|
|
2534
|
+
}
|
|
2535
|
+
|
|
2536
|
+
const nextSettings = { ...settings };
|
|
2537
|
+
let settingsChanged = false;
|
|
2538
|
+
if (model && normalizeNonEmptyString(settings.model) !== model) {
|
|
2539
|
+
nextSettings.model = model;
|
|
2540
|
+
settingsChanged = true;
|
|
2541
|
+
}
|
|
2542
|
+
if (effort && normalizeNonEmptyString(settings.reasoning_effort) !== effort) {
|
|
2543
|
+
nextSettings.reasoning_effort = effort;
|
|
2544
|
+
settingsChanged = true;
|
|
2545
|
+
}
|
|
2546
|
+
if (!settingsChanged) {
|
|
2547
|
+
continue;
|
|
2548
|
+
}
|
|
2549
|
+
|
|
2550
|
+
nextParams = {
|
|
2551
|
+
...nextParams,
|
|
2552
|
+
[collaborationKey]: {
|
|
2553
|
+
...collaborationMode,
|
|
2554
|
+
settings: nextSettings,
|
|
2555
|
+
},
|
|
2556
|
+
};
|
|
2557
|
+
changed = true;
|
|
2558
|
+
}
|
|
2559
|
+
|
|
2560
|
+
const alignedRawMessage = changed
|
|
2561
|
+
? JSON.stringify({ ...parsed, params: nextParams })
|
|
2562
|
+
: rawMessage;
|
|
2563
|
+
return disableUnsupportedReasoningSummaryForTurnStart(alignedRawMessage);
|
|
2564
|
+
}
|
|
2565
|
+
|
|
2100
2566
|
function readTurnStartModel(params) {
|
|
2101
2567
|
return normalizeNonEmptyString(params?.model).toLowerCase()
|
|
2102
2568
|
|| normalizeNonEmptyString(params?.collaborationMode?.settings?.model).toLowerCase()
|
|
2103
2569
|
|| normalizeNonEmptyString(params?.collaboration_mode?.settings?.model).toLowerCase();
|
|
2104
2570
|
}
|
|
2105
2571
|
|
|
2106
|
-
function extractBridgeMessageContext(rawMessage) {
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
parsed = JSON.parse(rawMessage);
|
|
2110
|
-
} catch {
|
|
2572
|
+
function extractBridgeMessageContext(rawMessage, parsedMessage = null) {
|
|
2573
|
+
const parsed = parsedMessage ?? parseBridgeJSON(rawMessage);
|
|
2574
|
+
if (!parsed) {
|
|
2111
2575
|
return { method: "", threadId: null, turnId: null };
|
|
2112
2576
|
}
|
|
2113
2577
|
|
|
@@ -2269,6 +2733,7 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
2269
2733
|
const remaining = requestedLimit - combinedTurns.length;
|
|
2270
2734
|
const pageLimit = selectAdaptiveTurnsListBatchLimit(combinedTurns.length, remaining);
|
|
2271
2735
|
const pageParams = buildAdaptiveTurnsListPageParams(params, pageLimit, nextCursor);
|
|
2736
|
+
const responseBeforePage = response;
|
|
2272
2737
|
let page;
|
|
2273
2738
|
|
|
2274
2739
|
try {
|
|
@@ -2314,7 +2779,31 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
2314
2779
|
response = buildSafeTurnsListResponse(request.id, firstResult, lastResult, turnsKey, combinedTurns);
|
|
2315
2780
|
|
|
2316
2781
|
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, sanitizeContext) >= payloadSoftLimitBytes) {
|
|
2317
|
-
|
|
2782
|
+
if (responseBeforePage) {
|
|
2783
|
+
// The server cursor belongs after the entire oversized batch. Return
|
|
2784
|
+
// the previous complete cursor boundary instead of slicing turns out
|
|
2785
|
+
// of this batch and making them unreachable.
|
|
2786
|
+
response = responseBeforePage;
|
|
2787
|
+
break;
|
|
2788
|
+
}
|
|
2789
|
+
if (pageTurns.length > pageLimit) {
|
|
2790
|
+
const completeResponse = buildCompactedCompleteTurnsListResponse({
|
|
2791
|
+
requestId: request.id,
|
|
2792
|
+
firstResult,
|
|
2793
|
+
lastResult,
|
|
2794
|
+
turnsKey,
|
|
2795
|
+
turns: pageTurns,
|
|
2796
|
+
sanitizeForRelay,
|
|
2797
|
+
sanitizeContext,
|
|
2798
|
+
payloadSoftLimitBytes,
|
|
2799
|
+
});
|
|
2800
|
+
if (!completeResponse) {
|
|
2801
|
+
throw new Error("thread/turns/list returned an oversized batch without a safe cursor boundary.");
|
|
2802
|
+
}
|
|
2803
|
+
response = completeResponse;
|
|
2804
|
+
break;
|
|
2805
|
+
}
|
|
2806
|
+
const boundedResponse = buildLargestSafeTurnsListResponse({
|
|
2318
2807
|
requestId: request.id,
|
|
2319
2808
|
firstResult,
|
|
2320
2809
|
lastResult,
|
|
@@ -2324,7 +2813,11 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
2324
2813
|
sanitizeForRelay,
|
|
2325
2814
|
sanitizeContext,
|
|
2326
2815
|
payloadSoftLimitBytes,
|
|
2327
|
-
})
|
|
2816
|
+
});
|
|
2817
|
+
if (!boundedResponse) {
|
|
2818
|
+
throw new Error("The newest chat turn is too large to relay safely.");
|
|
2819
|
+
}
|
|
2820
|
+
response = boundedResponse;
|
|
2328
2821
|
break;
|
|
2329
2822
|
}
|
|
2330
2823
|
|
|
@@ -2347,22 +2840,10 @@ async function fetchAdaptiveThreadTurnsListForRelay(request, {
|
|
|
2347
2840
|
}
|
|
2348
2841
|
}
|
|
2349
2842
|
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
},
|
|
2355
|
-
};
|
|
2356
|
-
}
|
|
2357
|
-
|
|
2358
|
-
function buildEmptyTurnsListResponse(request) {
|
|
2359
|
-
return {
|
|
2360
|
-
id: request.id,
|
|
2361
|
-
result: {
|
|
2362
|
-
data: [],
|
|
2363
|
-
nextCursor: null,
|
|
2364
|
-
},
|
|
2365
|
-
};
|
|
2843
|
+
if (!response) {
|
|
2844
|
+
throw new Error("thread/turns/list completed without a relayable page.");
|
|
2845
|
+
}
|
|
2846
|
+
return response;
|
|
2366
2847
|
}
|
|
2367
2848
|
|
|
2368
2849
|
function isEmptyTurnsListResponse(response) {
|
|
@@ -2391,7 +2872,7 @@ function resolveJsonlTurnsListRolloutPathForFallback({
|
|
|
2391
2872
|
: findAndCachePath(threadId);
|
|
2392
2873
|
}
|
|
2393
2874
|
|
|
2394
|
-
function maybeMergeLatestJsonlTurnIntoTurnsListResponse(request, response, jsonlResult
|
|
2875
|
+
function maybeMergeLatestJsonlTurnIntoTurnsListResponse(request, response, jsonlResult) {
|
|
2395
2876
|
const responseResult = response?.result;
|
|
2396
2877
|
const responseTurnsKey = findTurnsListResultKey(responseResult);
|
|
2397
2878
|
const jsonlTurnsKey = findTurnsListResultKey(jsonlResult);
|
|
@@ -2410,16 +2891,17 @@ function maybeMergeLatestJsonlTurnIntoTurnsListResponse(request, response, jsonl
|
|
|
2410
2891
|
return null;
|
|
2411
2892
|
}
|
|
2412
2893
|
|
|
2413
|
-
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
const mergedTurns = [jsonlTurn, ...responseTurns]
|
|
2894
|
+
// Keep the canonical page intact. Slicing this back to the requested limit
|
|
2895
|
+
// can retain the newer JSONL turn while dropping the canonical cursor anchor,
|
|
2896
|
+
// making that canonical turn permanently unreachable.
|
|
2897
|
+
const mergedTurns = [jsonlTurn, ...responseTurns];
|
|
2417
2898
|
return {
|
|
2418
2899
|
id: request.id,
|
|
2419
2900
|
result: {
|
|
2420
2901
|
...responseResult,
|
|
2421
2902
|
[responseTurnsKey]: mergedTurns,
|
|
2422
2903
|
remodexJsonlMergedLatest: true,
|
|
2904
|
+
remodexJsonlFallback: true,
|
|
2423
2905
|
},
|
|
2424
2906
|
};
|
|
2425
2907
|
}
|
|
@@ -2446,6 +2928,10 @@ function turnListTurnIdentifier(turn) {
|
|
|
2446
2928
|
|| normalizeNonEmptyString(turn?.turn_id);
|
|
2447
2929
|
}
|
|
2448
2930
|
|
|
2931
|
+
function isSyntheticJsonlHistoryTurnId(turnId) {
|
|
2932
|
+
return normalizeNonEmptyString(turnId).startsWith("turn-line-");
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2449
2935
|
async function fetchSafeThreadTurnsListFallback(request, {
|
|
2450
2936
|
fetchPage,
|
|
2451
2937
|
now,
|
|
@@ -2460,35 +2946,29 @@ async function fetchSafeThreadTurnsListFallback(request, {
|
|
|
2460
2946
|
const safeLimit = Math.min(requestedLimit, RELAY_TURNS_LIST_SAFE_RETRY_LIMIT);
|
|
2461
2947
|
const safeParams = buildAdaptiveTurnsListPageParams(params, safeLimit, params?.cursor);
|
|
2462
2948
|
|
|
2463
|
-
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
|
|
2467
|
-
|
|
2468
|
-
return buildEmptyTurnsListResponse(request);
|
|
2469
|
-
}
|
|
2470
|
-
|
|
2471
|
-
// If the normal pagination path returns a bad first page, retry once with a small page.
|
|
2472
|
-
// The retry response is intentionally minimal so Swift does not decode stale server metadata.
|
|
2473
|
-
const response = buildLargestSafeTurnsListResponse({
|
|
2474
|
-
requestId: request.id,
|
|
2475
|
-
firstResult: pageResult,
|
|
2476
|
-
lastResult: pageResult,
|
|
2477
|
-
turnsKey,
|
|
2478
|
-
turns: pageResult[turnsKey],
|
|
2479
|
-
maxTurns: safeLimit,
|
|
2480
|
-
sanitizeForRelay,
|
|
2481
|
-
sanitizeContext,
|
|
2482
|
-
payloadSoftLimitBytes,
|
|
2483
|
-
});
|
|
2484
|
-
if (response) {
|
|
2485
|
-
return response;
|
|
2486
|
-
}
|
|
2487
|
-
} catch {
|
|
2488
|
-
// Fall through to a valid empty page: the phone can keep the thread open instead of crashing.
|
|
2949
|
+
const page = await fetchMeasuredAdaptiveTurnsListPage(fetchPage, safeParams, now);
|
|
2950
|
+
const pageResult = unwrapAppServerPayloadResult(page.result);
|
|
2951
|
+
const turnsKey = findTurnsListResultKey(pageResult);
|
|
2952
|
+
if (!turnsKey) {
|
|
2953
|
+
throw new Error("thread/turns/list returned no turns array.");
|
|
2489
2954
|
}
|
|
2490
2955
|
|
|
2491
|
-
|
|
2956
|
+
// If the normal pagination path returns a bad first page, retry once with a small page.
|
|
2957
|
+
// The retry response is intentionally minimal so Swift does not decode stale server metadata.
|
|
2958
|
+
const response = buildCompactedCompleteTurnsListResponse({
|
|
2959
|
+
requestId: request.id,
|
|
2960
|
+
firstResult: pageResult,
|
|
2961
|
+
lastResult: pageResult,
|
|
2962
|
+
turnsKey,
|
|
2963
|
+
turns: pageResult[turnsKey],
|
|
2964
|
+
sanitizeForRelay,
|
|
2965
|
+
sanitizeContext,
|
|
2966
|
+
payloadSoftLimitBytes,
|
|
2967
|
+
});
|
|
2968
|
+
if (response) {
|
|
2969
|
+
return response;
|
|
2970
|
+
}
|
|
2971
|
+
throw new Error("thread/turns/list returned a page that is too large to relay safely.");
|
|
2492
2972
|
}
|
|
2493
2973
|
|
|
2494
2974
|
async function fetchMeasuredAdaptiveTurnsListPage(fetchPage, params, now) {
|
|
@@ -2538,6 +3018,48 @@ function buildSafeTurnsListResponse(requestId, firstResult, lastResult, turnsKey
|
|
|
2538
3018
|
};
|
|
2539
3019
|
}
|
|
2540
3020
|
|
|
3021
|
+
function buildCompactedCompleteTurnsListResponse({
|
|
3022
|
+
requestId,
|
|
3023
|
+
firstResult,
|
|
3024
|
+
lastResult,
|
|
3025
|
+
turnsKey,
|
|
3026
|
+
turns,
|
|
3027
|
+
sanitizeForRelay,
|
|
3028
|
+
sanitizeContext = {},
|
|
3029
|
+
payloadSoftLimitBytes,
|
|
3030
|
+
}) {
|
|
3031
|
+
const response = buildSafeTurnsListResponse(
|
|
3032
|
+
requestId,
|
|
3033
|
+
firstResult,
|
|
3034
|
+
lastResult,
|
|
3035
|
+
turnsKey,
|
|
3036
|
+
turns
|
|
3037
|
+
);
|
|
3038
|
+
if (measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, sanitizeContext) < payloadSoftLimitBytes) {
|
|
3039
|
+
return response;
|
|
3040
|
+
}
|
|
3041
|
+
|
|
3042
|
+
for (const maxChars of [
|
|
3043
|
+
RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS,
|
|
3044
|
+
Math.floor(RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS / 4),
|
|
3045
|
+
1_000,
|
|
3046
|
+
0,
|
|
3047
|
+
]) {
|
|
3048
|
+
const compacted = buildSafeTurnsListResponse(
|
|
3049
|
+
requestId,
|
|
3050
|
+
firstResult,
|
|
3051
|
+
lastResult,
|
|
3052
|
+
turnsKey,
|
|
3053
|
+
turns.map((turn) => compactTurnsListTurnForRelay(turn, maxChars))
|
|
3054
|
+
);
|
|
3055
|
+
compacted.result.remodexPageCompactedForRelay = true;
|
|
3056
|
+
if (measureSanitizedTurnsListResponseBytes(compacted, sanitizeForRelay, sanitizeContext) < payloadSoftLimitBytes) {
|
|
3057
|
+
return compacted;
|
|
3058
|
+
}
|
|
3059
|
+
}
|
|
3060
|
+
return null;
|
|
3061
|
+
}
|
|
3062
|
+
|
|
2541
3063
|
// Trims oversized history pages progressively: normal page -> 5 turns -> ... -> 1 turn.
|
|
2542
3064
|
function buildLargestSafeTurnsListResponse({
|
|
2543
3065
|
requestId,
|
|
@@ -2639,19 +3161,47 @@ function compactEmergencySingleTurnForRelay(turn, maxChars, maxItems) {
|
|
|
2639
3161
|
}
|
|
2640
3162
|
|
|
2641
3163
|
const items = Array.isArray(turn.items) ? turn.items : [];
|
|
2642
|
-
safeTurn.items = items
|
|
3164
|
+
safeTurn.items = selectEmergencyHistoryItemsForRelay(items, maxItems)
|
|
3165
|
+
.map((item) => compactHistoryItemForRelay(item, maxChars));
|
|
2643
3166
|
safeTurn.remodexEmergencySingleTurnForRelay = true;
|
|
2644
3167
|
safeTurn.remodexPageCompactedForRelay = true;
|
|
2645
3168
|
return safeTurn;
|
|
2646
3169
|
}
|
|
2647
3170
|
|
|
3171
|
+
function selectEmergencyHistoryItemsForRelay(items, maxItems) {
|
|
3172
|
+
if (!Array.isArray(items) || items.length <= maxItems) {
|
|
3173
|
+
return Array.isArray(items) ? items : [];
|
|
3174
|
+
}
|
|
3175
|
+
|
|
3176
|
+
const selectedIndices = new Set();
|
|
3177
|
+
const firstUserIndex = items.findIndex((item) => isUserRoleItem(item));
|
|
3178
|
+
if (firstUserIndex >= 0) {
|
|
3179
|
+
selectedIndices.add(firstUserIndex);
|
|
3180
|
+
}
|
|
3181
|
+
for (let index = items.length - 1; index >= 0 && selectedIndices.size < maxItems; index -= 1) {
|
|
3182
|
+
const type = normalizeHistoryItemToken(items[index]?.type);
|
|
3183
|
+
if (type === "plan" || type === "filechange") {
|
|
3184
|
+
selectedIndices.add(index);
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
for (let index = items.length - 1; index >= 0 && selectedIndices.size < maxItems; index -= 1) {
|
|
3188
|
+
selectedIndices.add(index);
|
|
3189
|
+
}
|
|
3190
|
+
return [...selectedIndices]
|
|
3191
|
+
.sort((left, right) => left - right)
|
|
3192
|
+
.map((index) => items[index]);
|
|
3193
|
+
}
|
|
3194
|
+
|
|
2648
3195
|
function buildAdaptiveTurnsListResult(firstResult, lastResult, turnsKey, turns) {
|
|
2649
3196
|
const result = {};
|
|
2650
3197
|
result[turnsKey] = turns;
|
|
2651
3198
|
|
|
2652
3199
|
for (const key of RELAY_TURNS_LIST_PAGINATION_RESULT_KEYS) {
|
|
2653
|
-
|
|
2654
|
-
|
|
3200
|
+
const sourceResult = RELAY_TURNS_LIST_PREVIOUS_PAGINATION_RESULT_KEYS.has(key)
|
|
3201
|
+
? firstResult
|
|
3202
|
+
: lastResult;
|
|
3203
|
+
if (Object.prototype.hasOwnProperty.call(sourceResult, key)) {
|
|
3204
|
+
result[key] = sourceResult[key];
|
|
2655
3205
|
} else {
|
|
2656
3206
|
delete result[key];
|
|
2657
3207
|
}
|
|
@@ -2698,8 +3248,10 @@ function measureSanitizedTurnsListResponseBytes(response, sanitizeForRelay, requ
|
|
|
2698
3248
|
// Keeps app-server responses in the JSON-RPC shape that the App Store iOS client decodes.
|
|
2699
3249
|
function normalizeRelayBoundJsonRpcMessage(rawMessage, {
|
|
2700
3250
|
pendingRequestMethodsById = null,
|
|
3251
|
+
// Optional pre-parsed envelope shared by the caller; treated as read-only.
|
|
3252
|
+
parsedMessage = null,
|
|
2701
3253
|
} = {}) {
|
|
2702
|
-
const parsed = parseBridgeJSON(rawMessage);
|
|
3254
|
+
const parsed = parsedMessage ?? parseBridgeJSON(rawMessage);
|
|
2703
3255
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
2704
3256
|
return null;
|
|
2705
3257
|
}
|
|
@@ -2818,11 +3370,32 @@ function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod, requestC
|
|
|
2818
3370
|
|| normalizeNonEmptyString(thread.id)
|
|
2819
3371
|
|| normalizeNonEmptyString(thread.threadId)
|
|
2820
3372
|
|| normalizeNonEmptyString(thread.thread_id);
|
|
2821
|
-
const { turns: sanitizedTurns, didSanitize } = sanitizeRelayHistoryTurns(thread.turns, threadId);
|
|
2822
|
-
const { thread: threadWithJsonlMetadata, didAugment: didAugmentThreadMetadata } = augmentRelayThreadWithJsonlMetadata(thread, threadId);
|
|
2823
|
-
const { turns: augmentedTurns, didAugment } = augmentRelayHistoryTurnsWithJsonlArtifacts(sanitizedTurns, threadId);
|
|
2824
3373
|
|
|
2825
|
-
|
|
3374
|
+
// Oversized histories get their turn window trimmed before the per-turn sanitize
|
|
3375
|
+
// and augment passes so full-history work is not spent on turns the payload
|
|
3376
|
+
// budget discards anyway. The byte-budget trim below still enforces the cap.
|
|
3377
|
+
const didPreTrimTurnWindow = Buffer.byteLength(rawMessage, "utf8") > RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES
|
|
3378
|
+
&& thread.turns.length > RELAY_HISTORY_RECENT_TURN_TARGET;
|
|
3379
|
+
const workingTurns = didPreTrimTurnWindow
|
|
3380
|
+
? thread.turns.slice(-RELAY_HISTORY_RECENT_TURN_TARGET)
|
|
3381
|
+
: thread.turns;
|
|
3382
|
+
const workingThread = didPreTrimTurnWindow ? { ...thread, turns: workingTurns } : thread;
|
|
3383
|
+
const trimOptions = didPreTrimTurnWindow
|
|
3384
|
+
? {
|
|
3385
|
+
preOmittedTurnCount: thread.turns.length - workingTurns.length,
|
|
3386
|
+
compactionIdSource: thread.turns[0],
|
|
3387
|
+
}
|
|
3388
|
+
: {};
|
|
3389
|
+
|
|
3390
|
+
const { turns: sanitizedTurns, didSanitize } = sanitizeRelayHistoryTurns(workingTurns, threadId);
|
|
3391
|
+
const { thread: threadWithJsonlMetadata, didAugment: didAugmentThreadMetadata } = augmentRelayThreadWithJsonlMetadata(workingThread, threadId);
|
|
3392
|
+
const { turns: augmentedTurns, didAugment } = augmentRelayHistoryTurnsWithJsonlArtifacts(
|
|
3393
|
+
sanitizedTurns,
|
|
3394
|
+
threadId,
|
|
3395
|
+
{ includeHistoryItems: true }
|
|
3396
|
+
);
|
|
3397
|
+
|
|
3398
|
+
if (!didSanitize && !didAugment && !didAugmentThreadMetadata && !didPreTrimTurnWindow) {
|
|
2826
3399
|
const trimmedPayload = trimThreadPayloadForRelay(parsed, thread);
|
|
2827
3400
|
return trimmedPayload == null ? rawMessage : trimmedPayload;
|
|
2828
3401
|
}
|
|
@@ -2838,7 +3411,7 @@ function sanitizeThreadHistoryImagesForRelay(rawMessage, requestMethod, requestC
|
|
|
2838
3411
|
},
|
|
2839
3412
|
});
|
|
2840
3413
|
|
|
2841
|
-
return trimThreadPayloadForRelay(parseBridgeJSON(sanitizedPayload), null) ?? sanitizedPayload;
|
|
3414
|
+
return trimThreadPayloadForRelay(parseBridgeJSON(sanitizedPayload), null, trimOptions) ?? sanitizedPayload;
|
|
2842
3415
|
}
|
|
2843
3416
|
|
|
2844
3417
|
function sanitizeThreadTurnsListForRelay(rawMessage, requestContext = {}) {
|
|
@@ -2906,27 +3479,104 @@ function readJsonlThreadCwd(threadId) {
|
|
|
2906
3479
|
return "";
|
|
2907
3480
|
}
|
|
2908
3481
|
|
|
3482
|
+
const sessionsRoot = resolveSessionsRoot();
|
|
3483
|
+
const cacheKey = buildJsonlThreadCacheKey(sessionsRoot, normalizedThreadId);
|
|
3484
|
+
|
|
2909
3485
|
try {
|
|
2910
|
-
const rolloutPath = findRecentRolloutFileForContextRead(
|
|
3486
|
+
const rolloutPath = findRecentRolloutFileForContextRead(sessionsRoot, { threadId: normalizedThreadId });
|
|
2911
3487
|
if (!rolloutPath) {
|
|
2912
3488
|
return "";
|
|
2913
3489
|
}
|
|
2914
3490
|
|
|
2915
|
-
const
|
|
2916
|
-
|
|
2917
|
-
|
|
3491
|
+
const cached = readCachedJsonlThreadCwd(cacheKey, rolloutPath);
|
|
3492
|
+
if (cached) {
|
|
3493
|
+
return cached.cwd;
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
return readAndCacheJsonlThreadCwd(cacheKey, rolloutPath);
|
|
2918
3497
|
} catch {
|
|
2919
3498
|
return "";
|
|
2920
3499
|
}
|
|
2921
3500
|
}
|
|
2922
3501
|
|
|
2923
|
-
function
|
|
3502
|
+
function readCachedJsonlThreadCwd(cacheKey, rolloutPath) {
|
|
3503
|
+
const cached = jsonlThreadCwdCacheByThread.get(cacheKey);
|
|
3504
|
+
if (!cached || cached.rolloutPath !== rolloutPath) {
|
|
3505
|
+
return null;
|
|
3506
|
+
}
|
|
3507
|
+
|
|
3508
|
+
const stat = statJsonlRollout(rolloutPath);
|
|
3509
|
+
if (!stat) {
|
|
3510
|
+
jsonlThreadCwdCacheByThread.delete(cacheKey);
|
|
3511
|
+
return null;
|
|
3512
|
+
}
|
|
3513
|
+
|
|
3514
|
+
if (stat.mtimeMs !== cached.mtimeMs || stat.size !== cached.size) {
|
|
3515
|
+
return null;
|
|
3516
|
+
}
|
|
3517
|
+
|
|
3518
|
+
const ttl = cached.cwd ? RELAY_JSONL_THREAD_CWD_CACHE_TTL_MS : RELAY_JSONL_THREAD_EMPTY_CWD_CACHE_TTL_MS;
|
|
3519
|
+
if (Date.now() - cached.checkedAt > ttl) {
|
|
3520
|
+
return null;
|
|
3521
|
+
}
|
|
3522
|
+
|
|
3523
|
+
return { cwd: cached.cwd };
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3526
|
+
function readAndCacheJsonlThreadCwd(cacheKey, rolloutPath, stat = null) {
|
|
3527
|
+
const rolloutStat = stat || statJsonlRollout(rolloutPath);
|
|
3528
|
+
if (!rolloutStat) {
|
|
3529
|
+
jsonlThreadCwdCacheByThread.delete(cacheKey);
|
|
3530
|
+
return "";
|
|
3531
|
+
}
|
|
3532
|
+
|
|
3533
|
+
let cwd = "";
|
|
3534
|
+
try {
|
|
3535
|
+
const metadata = readSessionJsonlMetadataFromFile(rolloutPath);
|
|
3536
|
+
const parsedCwd = normalizeNonEmptyString(metadata?.cwd);
|
|
3537
|
+
cwd = parsedCwd && path.isAbsolute(parsedCwd) ? parsedCwd : "";
|
|
3538
|
+
} catch {
|
|
3539
|
+
cwd = "";
|
|
3540
|
+
}
|
|
3541
|
+
|
|
3542
|
+
rememberJsonlThreadCwdCache(cacheKey, {
|
|
3543
|
+
rolloutPath,
|
|
3544
|
+
cwd,
|
|
3545
|
+
mtimeMs: rolloutStat.mtimeMs,
|
|
3546
|
+
size: rolloutStat.size,
|
|
3547
|
+
checkedAt: Date.now(),
|
|
3548
|
+
});
|
|
3549
|
+
return cwd;
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
function rememberJsonlThreadCwdCache(cacheKey, entry) {
|
|
3553
|
+
jsonlThreadCwdCacheByThread.set(cacheKey, entry);
|
|
3554
|
+
while (jsonlThreadCwdCacheByThread.size > JSONL_ROLLOUT_PATH_CACHE_MAX_SIZE) {
|
|
3555
|
+
const oldestKey = jsonlThreadCwdCacheByThread.keys().next().value;
|
|
3556
|
+
if (oldestKey == null) {
|
|
3557
|
+
break;
|
|
3558
|
+
}
|
|
3559
|
+
jsonlThreadCwdCacheByThread.delete(oldestKey);
|
|
3560
|
+
}
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3563
|
+
function augmentRelayHistoryTurnsWithJsonlArtifacts(turns, threadId = "", {
|
|
3564
|
+
includeHistoryItems = false,
|
|
3565
|
+
} = {}) {
|
|
2924
3566
|
const normalizedThreadId = normalizeNonEmptyString(threadId);
|
|
2925
3567
|
if (!normalizedThreadId || !Array.isArray(turns) || turns.length === 0) {
|
|
2926
3568
|
return { turns, didAugment: false };
|
|
2927
3569
|
}
|
|
2928
3570
|
|
|
2929
|
-
const
|
|
3571
|
+
const requestedTurnIds = new Set(turns.map((turn) => (
|
|
3572
|
+
normalizeNonEmptyString(turn?.id)
|
|
3573
|
+
|| normalizeNonEmptyString(turn?.turnId)
|
|
3574
|
+
|| normalizeNonEmptyString(turn?.turn_id)
|
|
3575
|
+
)).filter(Boolean));
|
|
3576
|
+
const jsonlArtifactsByTurnId = readJsonlArtifactItemsByTurnId(
|
|
3577
|
+
normalizedThreadId,
|
|
3578
|
+
requestedTurnIds
|
|
3579
|
+
);
|
|
2930
3580
|
if (jsonlArtifactsByTurnId.size === 0) {
|
|
2931
3581
|
return { turns, didAugment: false };
|
|
2932
3582
|
}
|
|
@@ -2942,22 +3592,17 @@ function augmentRelayHistoryTurnsWithJsonlArtifacts(turns, threadId = "") {
|
|
|
2942
3592
|
}
|
|
2943
3593
|
|
|
2944
3594
|
const items = Array.isArray(turn.items) ? turn.items : [];
|
|
2945
|
-
|
|
2946
|
-
|
|
2947
|
-
|
|
2948
|
-
|
|
2949
|
-
|
|
2950
|
-
|
|
2951
|
-
|
|
2952
|
-
|
|
3595
|
+
const merged = mergeRelayHistoryItemsWithJsonlItems(
|
|
3596
|
+
items,
|
|
3597
|
+
artifacts.timelineItems,
|
|
3598
|
+
normalizedThreadId,
|
|
3599
|
+
{
|
|
3600
|
+
includeJsonlItem: includeHistoryItems
|
|
3601
|
+
? () => true
|
|
3602
|
+
: isJsonlHistoryArtifactItem,
|
|
2953
3603
|
}
|
|
2954
|
-
|
|
2955
|
-
|
|
2956
|
-
}
|
|
2957
|
-
if (artifacts.progressPlanItem && !hasEquivalentProgressPlanItem(nextItems, artifacts.progressPlanItem)) {
|
|
2958
|
-
nextItems = nextItems === items ? [...items] : nextItems;
|
|
2959
|
-
nextItems.push(artifacts.progressPlanItem);
|
|
2960
|
-
}
|
|
3604
|
+
);
|
|
3605
|
+
const nextItems = merged.items;
|
|
2961
3606
|
|
|
2962
3607
|
if (nextItems === items) {
|
|
2963
3608
|
return turn;
|
|
@@ -2973,7 +3618,7 @@ function augmentRelayHistoryTurnsWithJsonlArtifacts(turns, threadId = "") {
|
|
|
2973
3618
|
return { turns: didAugment ? augmentedTurns : turns, didAugment };
|
|
2974
3619
|
}
|
|
2975
3620
|
|
|
2976
|
-
function readJsonlArtifactItemsByTurnId(threadId) {
|
|
3621
|
+
function readJsonlArtifactItemsByTurnId(threadId, requestedTurnIds = new Set()) {
|
|
2977
3622
|
const emptyArtifactsByTurnId = new Map();
|
|
2978
3623
|
const normalizedThreadId = normalizeNonEmptyString(threadId);
|
|
2979
3624
|
if (!normalizedThreadId) {
|
|
@@ -2982,7 +3627,11 @@ function readJsonlArtifactItemsByTurnId(threadId) {
|
|
|
2982
3627
|
|
|
2983
3628
|
const sessionsRoot = resolveSessionsRoot();
|
|
2984
3629
|
const cacheKey = buildJsonlArtifactItemsCacheKey(sessionsRoot, normalizedThreadId);
|
|
2985
|
-
const cachedArtifacts = readCachedJsonlArtifactItems(
|
|
3630
|
+
const cachedArtifacts = readCachedJsonlArtifactItems(
|
|
3631
|
+
cacheKey,
|
|
3632
|
+
normalizedThreadId,
|
|
3633
|
+
requestedTurnIds
|
|
3634
|
+
);
|
|
2986
3635
|
if (cachedArtifacts) {
|
|
2987
3636
|
return cachedArtifacts;
|
|
2988
3637
|
}
|
|
@@ -2994,7 +3643,13 @@ function readJsonlArtifactItemsByTurnId(threadId) {
|
|
|
2994
3643
|
return emptyArtifactsByTurnId;
|
|
2995
3644
|
}
|
|
2996
3645
|
|
|
2997
|
-
return readAndCacheJsonlArtifactItems(
|
|
3646
|
+
return readAndCacheJsonlArtifactItems(
|
|
3647
|
+
cacheKey,
|
|
3648
|
+
rolloutPath,
|
|
3649
|
+
normalizedThreadId,
|
|
3650
|
+
null,
|
|
3651
|
+
requestedTurnIds
|
|
3652
|
+
);
|
|
2998
3653
|
} catch (error) {
|
|
2999
3654
|
jsonlArtifactItemsCacheByThread.delete(cacheKey);
|
|
3000
3655
|
console.warn(`[remodex] history jsonl artifact augmentation failed for ${normalizedThreadId}: ${error.message}`);
|
|
@@ -3004,16 +3659,20 @@ function readJsonlArtifactItemsByTurnId(threadId) {
|
|
|
3004
3659
|
}
|
|
3005
3660
|
|
|
3006
3661
|
function buildJsonlArtifactItemsCacheKey(sessionsRoot, threadId) {
|
|
3662
|
+
return buildJsonlThreadCacheKey(sessionsRoot, threadId);
|
|
3663
|
+
}
|
|
3664
|
+
|
|
3665
|
+
function buildJsonlThreadCacheKey(sessionsRoot, threadId) {
|
|
3007
3666
|
return `${sessionsRoot}\0${threadId}`;
|
|
3008
3667
|
}
|
|
3009
3668
|
|
|
3010
|
-
function readCachedJsonlArtifactItems(cacheKey, threadId) {
|
|
3669
|
+
function readCachedJsonlArtifactItems(cacheKey, threadId, requestedTurnIds = new Set()) {
|
|
3011
3670
|
const cached = jsonlArtifactItemsCacheByThread.get(cacheKey);
|
|
3012
3671
|
if (!cached) {
|
|
3013
3672
|
return null;
|
|
3014
3673
|
}
|
|
3015
3674
|
|
|
3016
|
-
const stat =
|
|
3675
|
+
const stat = statJsonlRollout(cached.rolloutPath);
|
|
3017
3676
|
if (!stat) {
|
|
3018
3677
|
jsonlArtifactItemsCacheByThread.delete(cacheKey);
|
|
3019
3678
|
return null;
|
|
@@ -3021,7 +3680,13 @@ function readCachedJsonlArtifactItems(cacheKey, threadId) {
|
|
|
3021
3680
|
|
|
3022
3681
|
if (stat.mtimeMs !== cached.mtimeMs || stat.size !== cached.size) {
|
|
3023
3682
|
try {
|
|
3024
|
-
return readAndCacheJsonlArtifactItems(
|
|
3683
|
+
return readAndCacheJsonlArtifactItems(
|
|
3684
|
+
cacheKey,
|
|
3685
|
+
cached.rolloutPath,
|
|
3686
|
+
threadId,
|
|
3687
|
+
stat,
|
|
3688
|
+
requestedTurnIds
|
|
3689
|
+
);
|
|
3025
3690
|
} catch (error) {
|
|
3026
3691
|
jsonlArtifactItemsCacheByThread.delete(cacheKey);
|
|
3027
3692
|
console.warn(`[remodex] history jsonl artifact cache refresh failed for ${threadId}: ${error.message}`);
|
|
@@ -3030,6 +3695,11 @@ function readCachedJsonlArtifactItems(cacheKey, threadId) {
|
|
|
3030
3695
|
}
|
|
3031
3696
|
|
|
3032
3697
|
const now = Date.now();
|
|
3698
|
+
const coversRequestedTurns = cached.coversEntireRollout
|
|
3699
|
+
|| [...requestedTurnIds].every((turnId) => cached.coveredTurnIds?.has(turnId));
|
|
3700
|
+
if (!coversRequestedTurns) {
|
|
3701
|
+
return null;
|
|
3702
|
+
}
|
|
3033
3703
|
if (now - cached.checkedAt <= RELAY_JSONL_ARTIFACT_CACHE_TTL_MS) {
|
|
3034
3704
|
return cached.artifactsByTurnId;
|
|
3035
3705
|
}
|
|
@@ -3038,11 +3708,37 @@ function readCachedJsonlArtifactItems(cacheKey, threadId) {
|
|
|
3038
3708
|
return null;
|
|
3039
3709
|
}
|
|
3040
3710
|
|
|
3041
|
-
function readAndCacheJsonlArtifactItems(
|
|
3711
|
+
function readAndCacheJsonlArtifactItems(
|
|
3712
|
+
cacheKey,
|
|
3713
|
+
rolloutPath,
|
|
3714
|
+
threadId,
|
|
3715
|
+
stat = null,
|
|
3716
|
+
requestedTurnIds = new Set()
|
|
3717
|
+
) {
|
|
3042
3718
|
const rolloutStat = stat || fs.statSync(rolloutPath);
|
|
3043
3719
|
const artifactsByTurnId = new Map();
|
|
3720
|
+
let coveredTurnIds = new Set();
|
|
3721
|
+
let coversEntireRollout = false;
|
|
3044
3722
|
try {
|
|
3045
|
-
const
|
|
3723
|
+
const recent = readRecentSessionJsonlTurns(rolloutPath, {
|
|
3724
|
+
threadId,
|
|
3725
|
+
limit: RELAY_TURNS_LIST_SAFE_RETRY_LIMIT,
|
|
3726
|
+
});
|
|
3727
|
+
let turns = recent?.turns || [];
|
|
3728
|
+
coversEntireRollout = recent ? !recent.hasOlderTurns : false;
|
|
3729
|
+
coveredTurnIds = new Set(turns.map((turn) => normalizeNonEmptyString(turn?.id)).filter(Boolean));
|
|
3730
|
+
const missesRequestedTurn = [...requestedTurnIds].some((turnId) => !coveredTurnIds.has(turnId));
|
|
3731
|
+
|
|
3732
|
+
// Preserve the old exact artifact behavior for files V8 can safely decode,
|
|
3733
|
+
// but only pay that cost when an older cursor page actually asks for a turn
|
|
3734
|
+
// outside the fast tail. Multi-gigabyte files never enter this path.
|
|
3735
|
+
if (!coversEntireRollout
|
|
3736
|
+
&& missesRequestedTurn
|
|
3737
|
+
&& rolloutStat.size <= RELAY_JSONL_FULL_ARTIFACT_FALLBACK_MAX_BYTES) {
|
|
3738
|
+
turns = parseSessionJsonlTurns(fs.readFileSync(rolloutPath, "utf8"), { threadId });
|
|
3739
|
+
coversEntireRollout = true;
|
|
3740
|
+
coveredTurnIds = new Set(turns.map((turn) => normalizeNonEmptyString(turn?.id)).filter(Boolean));
|
|
3741
|
+
}
|
|
3046
3742
|
for (const turn of turns) {
|
|
3047
3743
|
const turnId = normalizeNonEmptyString(turn?.id);
|
|
3048
3744
|
const turnItems = Array.isArray(turn?.items) ? turn.items : [];
|
|
@@ -3050,47 +3746,9 @@ function readAndCacheJsonlArtifactItems(cacheKey, rolloutPath, threadId, stat =
|
|
|
3050
3746
|
continue;
|
|
3051
3747
|
}
|
|
3052
3748
|
|
|
3053
|
-
const
|
|
3054
|
-
|
|
3055
|
-
|
|
3056
|
-
&& item?.remodexJsonlProgressPlan === true
|
|
3057
|
-
));
|
|
3058
|
-
const artifacts = {
|
|
3059
|
-
fileChangeItem: null,
|
|
3060
|
-
imageViewItems: [],
|
|
3061
|
-
progressPlanItem: null,
|
|
3062
|
-
};
|
|
3063
|
-
|
|
3064
|
-
const changes = [];
|
|
3065
|
-
for (const item of fileChanges) {
|
|
3066
|
-
if (Array.isArray(item.changes)) {
|
|
3067
|
-
changes.push(...item.changes);
|
|
3068
|
-
}
|
|
3069
|
-
}
|
|
3070
|
-
if (changes.length > 0) {
|
|
3071
|
-
artifacts.fileChangeItem = {
|
|
3072
|
-
id: `remodex-jsonl-file-change-${turnId}`,
|
|
3073
|
-
type: "fileChange",
|
|
3074
|
-
status: "completed",
|
|
3075
|
-
changes,
|
|
3076
|
-
remodexJsonlFileChangeAggregate: true,
|
|
3077
|
-
};
|
|
3078
|
-
}
|
|
3079
|
-
if (progressPlan) {
|
|
3080
|
-
artifacts.progressPlanItem = {
|
|
3081
|
-
...progressPlan,
|
|
3082
|
-
id: normalizeNonEmptyString(progressPlan.id) || `remodex-jsonl-progress-plan-${turnId}`,
|
|
3083
|
-
};
|
|
3084
|
-
}
|
|
3085
|
-
artifacts.imageViewItems = turnItems
|
|
3086
|
-
.filter((item) => normalizeHistoryItemToken(item?.type) === "imageview")
|
|
3087
|
-
.map((item, index) => ({
|
|
3088
|
-
...item,
|
|
3089
|
-
id: normalizeNonEmptyString(item.id) || `remodex-jsonl-image-view-${turnId}-${index + 1}`,
|
|
3090
|
-
}));
|
|
3091
|
-
|
|
3092
|
-
if (artifacts.fileChangeItem || artifacts.progressPlanItem || artifacts.imageViewItems.length > 0) {
|
|
3093
|
-
artifactsByTurnId.set(turnId, artifacts);
|
|
3749
|
+
const timelineItems = buildOrderedJsonlTimelineItems(turnItems, turnId);
|
|
3750
|
+
if (timelineItems.length > 0) {
|
|
3751
|
+
artifactsByTurnId.set(turnId, { timelineItems });
|
|
3094
3752
|
}
|
|
3095
3753
|
}
|
|
3096
3754
|
} catch (error) {
|
|
@@ -3104,11 +3762,13 @@ function readAndCacheJsonlArtifactItems(cacheKey, rolloutPath, threadId, stat =
|
|
|
3104
3762
|
size: rolloutStat.size,
|
|
3105
3763
|
checkedAt: Date.now(),
|
|
3106
3764
|
artifactsByTurnId,
|
|
3765
|
+
coveredTurnIds,
|
|
3766
|
+
coversEntireRollout,
|
|
3107
3767
|
});
|
|
3108
3768
|
return artifactsByTurnId;
|
|
3109
3769
|
}
|
|
3110
3770
|
|
|
3111
|
-
function
|
|
3771
|
+
function statJsonlRollout(rolloutPath) {
|
|
3112
3772
|
try {
|
|
3113
3773
|
return fs.statSync(rolloutPath);
|
|
3114
3774
|
} catch {
|
|
@@ -3127,57 +3787,406 @@ function rememberJsonlArtifactItemsCache(cacheKey, entry) {
|
|
|
3127
3787
|
}
|
|
3128
3788
|
}
|
|
3129
3789
|
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3790
|
+
// Keeps JSONL-only rows in rollout order while treating app-server rows as the
|
|
3791
|
+
// authoritative spine. Matching anchors let us place missing rows without ever
|
|
3792
|
+
// moving server-only findings, messages, or richer tool records to the tail.
|
|
3793
|
+
function mergeRelayHistoryItemsWithJsonlItems(existingItems, jsonlItems, threadId = "", {
|
|
3794
|
+
includeJsonlItem = () => true,
|
|
3795
|
+
} = {}) {
|
|
3796
|
+
if (!Array.isArray(existingItems) || !Array.isArray(jsonlItems) || jsonlItems.length === 0) {
|
|
3797
|
+
return { items: existingItems, didMerge: false };
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
const sanitizedJsonlItems = jsonlItems
|
|
3801
|
+
.map((item) => sanitizeJsonlHistoryItemForRelayMerge(item, threadId))
|
|
3802
|
+
.filter(Boolean);
|
|
3803
|
+
if (sanitizedJsonlItems.length === 0) {
|
|
3804
|
+
return { items: existingItems, didMerge: false };
|
|
3805
|
+
}
|
|
3806
|
+
|
|
3807
|
+
if (existingItems.length === 0) {
|
|
3808
|
+
const insertedItems = sanitizedJsonlItems.filter(includeJsonlItem);
|
|
3809
|
+
return insertedItems.length > 0
|
|
3810
|
+
? { items: insertedItems, didMerge: true }
|
|
3811
|
+
: { items: existingItems, didMerge: false };
|
|
3812
|
+
}
|
|
3813
|
+
|
|
3814
|
+
const usedExistingIndices = new Set();
|
|
3815
|
+
const resolvedExistingItems = existingItems.slice();
|
|
3816
|
+
const insertionsBefore = new Map();
|
|
3817
|
+
const insertionsAfter = new Map();
|
|
3818
|
+
let pendingItems = [];
|
|
3819
|
+
let previousMatchedIndex = null;
|
|
3820
|
+
let matchedAnchorCount = 0;
|
|
3821
|
+
let didReplaceMatchedItem = false;
|
|
3822
|
+
|
|
3823
|
+
// The rollout's turn+text alias is the only identity shared across the
|
|
3824
|
+
// live-owner (item_N) and rollout (msg_...) views of one assistant reply.
|
|
3825
|
+
// Carrying it onto the matched server row lets the phone join this item
|
|
3826
|
+
// with its other-source representations instead of duplicating it.
|
|
3827
|
+
const adoptJsonlSourceAlias = (index, jsonlItem) => {
|
|
3828
|
+
const sourceKey = normalizeNonEmptyString(jsonlItem?.remodexSourceItemKey);
|
|
3829
|
+
const existing = resolvedExistingItems[index];
|
|
3830
|
+
if (!sourceKey || !existing || typeof existing !== "object"
|
|
3831
|
+
|| normalizeNonEmptyString(existing.remodexSourceItemKey)) {
|
|
3832
|
+
return;
|
|
3136
3833
|
}
|
|
3137
|
-
|
|
3138
|
-
|
|
3834
|
+
resolvedExistingItems[index] = { ...existing, remodexSourceItemKey: sourceKey };
|
|
3835
|
+
didReplaceMatchedItem = true;
|
|
3836
|
+
};
|
|
3837
|
+
|
|
3838
|
+
const placePendingItems = () => {
|
|
3839
|
+
if (pendingItems.length === 0) {
|
|
3840
|
+
return;
|
|
3139
3841
|
}
|
|
3140
|
-
if (
|
|
3141
|
-
|
|
3842
|
+
if (previousMatchedIndex == null) {
|
|
3843
|
+
insertionsBefore.set(0, pendingItems);
|
|
3844
|
+
} else {
|
|
3845
|
+
const existing = insertionsAfter.get(previousMatchedIndex) || [];
|
|
3846
|
+
insertionsAfter.set(previousMatchedIndex, existing.concat(pendingItems));
|
|
3142
3847
|
}
|
|
3848
|
+
pendingItems = [];
|
|
3849
|
+
};
|
|
3143
3850
|
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3851
|
+
for (const jsonlItem of sanitizedJsonlItems) {
|
|
3852
|
+
const unusedMatch = (candidate, index) => !usedExistingIndices.has(index);
|
|
3853
|
+
const eligibleMatch = (candidate, index) => (
|
|
3854
|
+
(previousMatchedIndex == null || index > previousMatchedIndex)
|
|
3855
|
+
&& unusedMatch(candidate, index)
|
|
3856
|
+
);
|
|
3857
|
+
let existingIndex = findRelayHistoryExactMatchIndex(existingItems, jsonlItem, eligibleMatch);
|
|
3858
|
+
if (existingIndex === -1) {
|
|
3859
|
+
// Exact identity remains authoritative even when server and rollout order
|
|
3860
|
+
// disagree. Consume that occurrence before considering a later semantic
|
|
3861
|
+
// lookalike, otherwise repeated rows can bind to the wrong server item.
|
|
3862
|
+
const representedExactIndex = findRelayHistoryExactMatchIndex(
|
|
3863
|
+
existingItems,
|
|
3864
|
+
jsonlItem,
|
|
3865
|
+
unusedMatch
|
|
3866
|
+
);
|
|
3867
|
+
if (representedExactIndex !== -1) {
|
|
3868
|
+
usedExistingIndices.add(representedExactIndex);
|
|
3869
|
+
if (isProgressPlanItem(jsonlItem)) {
|
|
3870
|
+
resolvedExistingItems[representedExactIndex] = resolvedProgressPlanHistoryItem(
|
|
3871
|
+
existingItems[representedExactIndex],
|
|
3872
|
+
jsonlItem
|
|
3873
|
+
);
|
|
3874
|
+
didReplaceMatchedItem = true;
|
|
3875
|
+
}
|
|
3876
|
+
adoptJsonlSourceAlias(representedExactIndex, jsonlItem);
|
|
3877
|
+
continue;
|
|
3878
|
+
}
|
|
3879
|
+
existingIndex = findRelayHistorySemanticMatchIndex(existingItems, jsonlItem, eligibleMatch);
|
|
3880
|
+
}
|
|
3881
|
+
if (existingIndex === -1) {
|
|
3882
|
+
// The row can already exist before the monotonic placement frontier when
|
|
3883
|
+
// server and rollout order disagree. Consume each represented occurrence
|
|
3884
|
+
// once; an extra identical JSONL occurrence must remain visible instead
|
|
3885
|
+
// of repeatedly matching the same server row and disappearing.
|
|
3886
|
+
const representedIndex = findRelayHistorySemanticMatchIndex(
|
|
3887
|
+
existingItems,
|
|
3888
|
+
jsonlItem,
|
|
3889
|
+
unusedMatch
|
|
3890
|
+
);
|
|
3891
|
+
if (representedIndex !== -1) {
|
|
3892
|
+
usedExistingIndices.add(representedIndex);
|
|
3893
|
+
if (isProgressPlanItem(jsonlItem)) {
|
|
3894
|
+
resolvedExistingItems[representedIndex] = resolvedProgressPlanHistoryItem(
|
|
3895
|
+
existingItems[representedIndex],
|
|
3896
|
+
jsonlItem
|
|
3897
|
+
);
|
|
3898
|
+
didReplaceMatchedItem = true;
|
|
3899
|
+
}
|
|
3900
|
+
adoptJsonlSourceAlias(representedIndex, jsonlItem);
|
|
3901
|
+
continue;
|
|
3151
3902
|
}
|
|
3903
|
+
if (includeJsonlItem(jsonlItem)) {
|
|
3904
|
+
pendingItems.push(jsonlItem);
|
|
3905
|
+
}
|
|
3906
|
+
continue;
|
|
3152
3907
|
}
|
|
3153
|
-
|
|
3154
|
-
|
|
3908
|
+
|
|
3909
|
+
placePendingItems();
|
|
3910
|
+
usedExistingIndices.add(existingIndex);
|
|
3911
|
+
if (isProgressPlanItem(jsonlItem)) {
|
|
3912
|
+
resolvedExistingItems[existingIndex] = resolvedProgressPlanHistoryItem(
|
|
3913
|
+
existingItems[existingIndex],
|
|
3914
|
+
jsonlItem
|
|
3915
|
+
);
|
|
3916
|
+
didReplaceMatchedItem = true;
|
|
3917
|
+
}
|
|
3918
|
+
adoptJsonlSourceAlias(existingIndex, jsonlItem);
|
|
3919
|
+
previousMatchedIndex = existingIndex;
|
|
3920
|
+
matchedAnchorCount += 1;
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3923
|
+
if (matchedAnchorCount === 0) {
|
|
3924
|
+
const unanchoredArtifacts = sanitizedJsonlItems.filter((item) => (
|
|
3925
|
+
includeJsonlItem(item) && isJsonlHistoryArtifactItem(item)
|
|
3926
|
+
));
|
|
3927
|
+
if (unanchoredArtifacts.length === 0) {
|
|
3928
|
+
return { items: existingItems, didMerge: false };
|
|
3929
|
+
}
|
|
3930
|
+
const firstAssistantIndex = existingItems.findIndex(isRelayAssistantHistoryItem);
|
|
3931
|
+
const insertionIndex = firstAssistantIndex === -1 ? existingItems.length : firstAssistantIndex;
|
|
3932
|
+
return {
|
|
3933
|
+
items: existingItems.slice(0, insertionIndex)
|
|
3934
|
+
.concat(unanchoredArtifacts, existingItems.slice(insertionIndex)),
|
|
3935
|
+
didMerge: true,
|
|
3936
|
+
};
|
|
3937
|
+
}
|
|
3938
|
+
if (pendingItems.length > 0 && previousMatchedIndex != null) {
|
|
3939
|
+
const existing = insertionsAfter.get(previousMatchedIndex) || [];
|
|
3940
|
+
insertionsAfter.set(previousMatchedIndex, existing.concat(pendingItems));
|
|
3941
|
+
}
|
|
3942
|
+
|
|
3943
|
+
if (insertionsBefore.size === 0 && insertionsAfter.size === 0 && !didReplaceMatchedItem) {
|
|
3944
|
+
return { items: existingItems, didMerge: false };
|
|
3945
|
+
}
|
|
3946
|
+
|
|
3947
|
+
const mergedItems = [];
|
|
3948
|
+
for (const [index, item] of resolvedExistingItems.entries()) {
|
|
3949
|
+
mergedItems.push(...(insertionsBefore.get(index) || []));
|
|
3950
|
+
mergedItems.push(item);
|
|
3951
|
+
mergedItems.push(...(insertionsAfter.get(index) || []));
|
|
3952
|
+
}
|
|
3953
|
+
return { items: mergedItems, didMerge: true };
|
|
3155
3954
|
}
|
|
3156
3955
|
|
|
3157
|
-
function
|
|
3158
|
-
|
|
3159
|
-
|
|
3160
|
-
|
|
3161
|
-
|
|
3956
|
+
function buildOrderedJsonlTimelineItems(turnItems, turnId) {
|
|
3957
|
+
if (!Array.isArray(turnItems) || turnItems.length === 0) {
|
|
3958
|
+
return [];
|
|
3959
|
+
}
|
|
3960
|
+
|
|
3961
|
+
let latestProgressPlanIndex = -1;
|
|
3962
|
+
for (const [index, item] of turnItems.entries()) {
|
|
3963
|
+
if (isProgressPlanItem(item)) {
|
|
3964
|
+
latestProgressPlanIndex = index;
|
|
3965
|
+
}
|
|
3966
|
+
}
|
|
3967
|
+
|
|
3968
|
+
let imageViewIndex = 0;
|
|
3969
|
+
return turnItems.flatMap((item, index) => {
|
|
3970
|
+
if (!shouldIncludeJsonlTimelineItem(item)) {
|
|
3971
|
+
return [];
|
|
3972
|
+
}
|
|
3973
|
+
if (isProgressPlanItem(item) && index !== latestProgressPlanIndex) {
|
|
3974
|
+
return [];
|
|
3975
|
+
}
|
|
3976
|
+
|
|
3977
|
+
const itemType = normalizeHistoryItemToken(item?.type);
|
|
3978
|
+
if (isProgressPlanItem(item)) {
|
|
3979
|
+
return [{
|
|
3980
|
+
...item,
|
|
3981
|
+
id: normalizeNonEmptyString(item?.id) || `remodex-jsonl-progress-plan-${turnId}`,
|
|
3982
|
+
remodexProgressPlan: true,
|
|
3983
|
+
remodexJsonlProgressPlan: true,
|
|
3984
|
+
}];
|
|
3162
3985
|
}
|
|
3163
|
-
|
|
3164
|
-
|
|
3986
|
+
if (itemType === "imageview") {
|
|
3987
|
+
imageViewIndex += 1;
|
|
3988
|
+
return [{
|
|
3989
|
+
...item,
|
|
3990
|
+
id: normalizeNonEmptyString(item?.id)
|
|
3991
|
+
|| `remodex-jsonl-image-view-${turnId}-${imageViewIndex}`,
|
|
3992
|
+
}];
|
|
3993
|
+
}
|
|
3994
|
+
return [item];
|
|
3165
3995
|
});
|
|
3166
3996
|
}
|
|
3167
3997
|
|
|
3168
|
-
function
|
|
3169
|
-
const
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3998
|
+
function shouldIncludeJsonlTimelineItem(item) {
|
|
3999
|
+
const itemType = normalizeHistoryItemToken(item?.type);
|
|
4000
|
+
return Boolean(itemType)
|
|
4001
|
+
&& itemType !== "toolcalloutput"
|
|
4002
|
+
&& itemType !== "functioncalloutput"
|
|
4003
|
+
&& itemType !== "customtoolcalloutput";
|
|
4004
|
+
}
|
|
4005
|
+
|
|
4006
|
+
function isJsonlHistoryArtifactItem(item) {
|
|
4007
|
+
const itemType = normalizeHistoryItemToken(item?.type);
|
|
4008
|
+
return itemType === "filechange"
|
|
4009
|
+
|| itemType === "imageview"
|
|
4010
|
+
|| isProgressPlanItem(item);
|
|
4011
|
+
}
|
|
4012
|
+
|
|
4013
|
+
function isProgressPlanItem(item) {
|
|
4014
|
+
const itemType = normalizeHistoryItemToken(item?.type);
|
|
4015
|
+
return (itemType === "plan" || itemType === "todolist")
|
|
4016
|
+
&& (item?.remodexJsonlProgressPlan === true || item?.remodexProgressPlan === true);
|
|
4017
|
+
}
|
|
4018
|
+
|
|
4019
|
+
function resolvedProgressPlanHistoryItem(existingItem, jsonlItem) {
|
|
4020
|
+
return {
|
|
4021
|
+
...existingItem,
|
|
4022
|
+
text: jsonlItem.text,
|
|
4023
|
+
explanation: jsonlItem.explanation,
|
|
4024
|
+
plan: jsonlItem.plan,
|
|
4025
|
+
remodexProgressPlan: true,
|
|
4026
|
+
remodexJsonlProgressPlan: true,
|
|
4027
|
+
};
|
|
4028
|
+
}
|
|
4029
|
+
|
|
4030
|
+
function findRelayHistoryExactMatchIndex(items, incomingItem, predicate = () => true) {
|
|
4031
|
+
return items.findIndex((candidate, index) => (
|
|
4032
|
+
predicate(candidate, index) && relayHistoryItemsHaveExactIdentity(candidate, incomingItem)
|
|
4033
|
+
));
|
|
4034
|
+
}
|
|
4035
|
+
|
|
4036
|
+
function findRelayHistorySemanticMatchIndex(items, incomingItem, predicate = () => true) {
|
|
4037
|
+
return items.findIndex((candidate, index) => (
|
|
4038
|
+
predicate(candidate, index) && areEquivalentRelayHistoryItems(candidate, incomingItem)
|
|
4039
|
+
));
|
|
4040
|
+
}
|
|
4041
|
+
|
|
4042
|
+
function relayHistoryItemsHaveExactIdentity(first, second) {
|
|
4043
|
+
const firstIdentity = relayHistoryItemIdentity(first);
|
|
4044
|
+
const secondIdentity = relayHistoryItemIdentity(second);
|
|
4045
|
+
if (firstIdentity && secondIdentity && firstIdentity === secondIdentity) {
|
|
4046
|
+
return true;
|
|
4047
|
+
}
|
|
4048
|
+
const firstCallId = relayHistoryItemCallId(first);
|
|
4049
|
+
const secondCallId = relayHistoryItemCallId(second);
|
|
4050
|
+
return Boolean(firstCallId && secondCallId && firstCallId === secondCallId);
|
|
4051
|
+
}
|
|
4052
|
+
|
|
4053
|
+
function isRelayAssistantHistoryItem(item) {
|
|
4054
|
+
const role = normalizeNonEmptyString(item?.role).toLowerCase();
|
|
4055
|
+
const itemType = normalizeHistoryItemToken(item?.type);
|
|
4056
|
+
return role === "assistant"
|
|
4057
|
+
|| itemType === "assistantmessage"
|
|
4058
|
+
|| itemType === "agentmessage"
|
|
4059
|
+
|| (itemType === "message" && role !== "user");
|
|
4060
|
+
}
|
|
4061
|
+
|
|
4062
|
+
function sanitizeJsonlHistoryItemForRelayMerge(item, threadId) {
|
|
4063
|
+
const sanitizedTurn = sanitizeRelayHistoryTurn({ items: [item] }, threadId);
|
|
4064
|
+
return sanitizedTurn?.items?.[0] || item;
|
|
4065
|
+
}
|
|
4066
|
+
|
|
4067
|
+
function areEquivalentRelayHistoryItems(first, second) {
|
|
4068
|
+
const firstIdentity = relayHistoryItemIdentity(first);
|
|
4069
|
+
const secondIdentity = relayHistoryItemIdentity(second);
|
|
4070
|
+
if (firstIdentity && secondIdentity && firstIdentity === secondIdentity) {
|
|
4071
|
+
return true;
|
|
4072
|
+
}
|
|
4073
|
+
|
|
4074
|
+
const firstCallId = relayHistoryItemCallId(first);
|
|
4075
|
+
const secondCallId = relayHistoryItemCallId(second);
|
|
4076
|
+
if (firstCallId && secondCallId && firstCallId === secondCallId) {
|
|
4077
|
+
return true;
|
|
4078
|
+
}
|
|
4079
|
+
|
|
4080
|
+
if (isProgressPlanItem(first) && isProgressPlanItem(second)) {
|
|
4081
|
+
return true;
|
|
4082
|
+
}
|
|
4083
|
+
|
|
4084
|
+
// JSONL line ids are source-local fallbacks, not provider identities. They
|
|
4085
|
+
// may reconcile semantically with a real app-server id; occurrence tracking
|
|
4086
|
+
// in the merge keeps intentional repeated rows distinct. Assistant messages
|
|
4087
|
+
// are exempt from the two-stable-ids refusal: the live-owner state keys them
|
|
4088
|
+
// by app-server event id (item_N) while the rollout records the provider id
|
|
4089
|
+
// (msg_...), so the same reply legitimately carries two stable identities.
|
|
4090
|
+
if (relayHistoryIdentityIsStable(firstIdentity)
|
|
4091
|
+
&& relayHistoryIdentityIsStable(secondIdentity)
|
|
4092
|
+
&& !(isRelayAssistantHistoryItem(first) && isRelayAssistantHistoryItem(second))) {
|
|
4093
|
+
return false;
|
|
4094
|
+
}
|
|
4095
|
+
if (relayHistoryIdentityIsStable(firstCallId)
|
|
4096
|
+
&& relayHistoryIdentityIsStable(secondCallId)) {
|
|
4097
|
+
return false;
|
|
4098
|
+
}
|
|
4099
|
+
|
|
4100
|
+
const firstType = normalizeHistoryItemToken(first?.type);
|
|
4101
|
+
const secondType = normalizeHistoryItemToken(second?.type);
|
|
4102
|
+
if (firstType === "imageview" && secondType === "imageview") {
|
|
4103
|
+
const firstPath = normalizeImageViewPathKey(first);
|
|
4104
|
+
const secondPath = normalizeImageViewPathKey(second);
|
|
4105
|
+
if (firstPath && firstPath === secondPath) {
|
|
4106
|
+
return true;
|
|
3174
4107
|
}
|
|
3175
|
-
|
|
3176
|
-
|
|
4108
|
+
}
|
|
4109
|
+
if (firstType === "filechange" && secondType === "filechange") {
|
|
4110
|
+
const firstPaths = fileChangePathSet(first);
|
|
4111
|
+
const secondPaths = fileChangePathSet(second);
|
|
4112
|
+
if (firstPaths.size > 0
|
|
4113
|
+
&& firstPaths.size === secondPaths.size
|
|
4114
|
+
&& Array.from(firstPaths).every((pathKey) => secondPaths.has(pathKey))) {
|
|
3177
4115
|
return true;
|
|
3178
4116
|
}
|
|
3179
|
-
|
|
3180
|
-
|
|
4117
|
+
}
|
|
4118
|
+
|
|
4119
|
+
const firstText = relayHistoryItemText(first);
|
|
4120
|
+
const secondText = relayHistoryItemText(second);
|
|
4121
|
+
if (!firstText || !secondText || firstText !== secondText) {
|
|
4122
|
+
return false;
|
|
4123
|
+
}
|
|
4124
|
+
|
|
4125
|
+
return relayHistoryItemKindsCompatible(first, second);
|
|
4126
|
+
}
|
|
4127
|
+
|
|
4128
|
+
function relayHistoryItemKindsCompatible(first, second) {
|
|
4129
|
+
const firstType = normalizeHistoryItemToken(first?.type);
|
|
4130
|
+
const secondType = normalizeHistoryItemToken(second?.type);
|
|
4131
|
+
if (firstType && secondType && firstType === secondType) {
|
|
4132
|
+
return true;
|
|
4133
|
+
}
|
|
4134
|
+
|
|
4135
|
+
const firstRole = normalizeNonEmptyString(first?.role).toLowerCase();
|
|
4136
|
+
const secondRole = normalizeNonEmptyString(second?.role).toLowerCase();
|
|
4137
|
+
if (firstRole && secondRole && firstRole === secondRole) {
|
|
4138
|
+
return true;
|
|
4139
|
+
}
|
|
4140
|
+
|
|
4141
|
+
return isRelayMessageLikeHistoryType(firstType) && isRelayMessageLikeHistoryType(secondType);
|
|
4142
|
+
}
|
|
4143
|
+
|
|
4144
|
+
function isRelayMessageLikeHistoryType(itemType) {
|
|
4145
|
+
return itemType === "message"
|
|
4146
|
+
|| itemType === "assistantmessage"
|
|
4147
|
+
|| itemType === "agentmessage"
|
|
4148
|
+
|| itemType === "usermessage";
|
|
4149
|
+
}
|
|
4150
|
+
|
|
4151
|
+
function relayHistoryItemIdentity(item) {
|
|
4152
|
+
return normalizeNonEmptyString(item?.id)
|
|
4153
|
+
|| normalizeNonEmptyString(item?.itemId)
|
|
4154
|
+
|| normalizeNonEmptyString(item?.item_id);
|
|
4155
|
+
}
|
|
4156
|
+
|
|
4157
|
+
function relayHistoryIdentityIsStable(identity) {
|
|
4158
|
+
const normalizedIdentity = normalizeNonEmptyString(identity);
|
|
4159
|
+
if (!normalizedIdentity) {
|
|
4160
|
+
return false;
|
|
4161
|
+
}
|
|
4162
|
+
return !/^(?:user-message-line|response-item-line|apply-patch-line)-\d+$/.test(normalizedIdentity);
|
|
4163
|
+
}
|
|
4164
|
+
|
|
4165
|
+
function relayHistoryItemCallId(item) {
|
|
4166
|
+
return normalizeNonEmptyString(item?.call_id)
|
|
4167
|
+
|| normalizeNonEmptyString(item?.callId);
|
|
4168
|
+
}
|
|
4169
|
+
|
|
4170
|
+
function relayHistoryItemText(item) {
|
|
4171
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) {
|
|
4172
|
+
return "";
|
|
4173
|
+
}
|
|
4174
|
+
|
|
4175
|
+
for (const key of ["text", "message", "summary", "output", "outputText", "output_text", "command"]) {
|
|
4176
|
+
const value = normalizeNonEmptyString(item[key]);
|
|
4177
|
+
if (value) {
|
|
4178
|
+
return value;
|
|
4179
|
+
}
|
|
4180
|
+
}
|
|
4181
|
+
|
|
4182
|
+
if (Array.isArray(item.content)) {
|
|
4183
|
+
return item.content
|
|
4184
|
+
.map(relayHistoryItemText)
|
|
4185
|
+
.filter(Boolean)
|
|
4186
|
+
.join("\n");
|
|
4187
|
+
}
|
|
4188
|
+
|
|
4189
|
+
return "";
|
|
3181
4190
|
}
|
|
3182
4191
|
|
|
3183
4192
|
function normalizeImageViewPathKey(item) {
|
|
@@ -3249,7 +4258,16 @@ function sanitizeRelayHistoryTurn(turn, threadId = "") {
|
|
|
3249
4258
|
}
|
|
3250
4259
|
|
|
3251
4260
|
let itemDidChange = false;
|
|
3252
|
-
let sanitizedItem =
|
|
4261
|
+
let sanitizedItem = sanitizeUserRoleItem(item);
|
|
4262
|
+
if (!sanitizedItem) {
|
|
4263
|
+
turnDidChange = true;
|
|
4264
|
+
return null;
|
|
4265
|
+
}
|
|
4266
|
+
if (sanitizedItem !== item) {
|
|
4267
|
+
itemDidChange = true;
|
|
4268
|
+
}
|
|
4269
|
+
|
|
4270
|
+
sanitizedItem = convertApplyPatchHistoryItem(sanitizedItem) || sanitizedItem;
|
|
3253
4271
|
if (sanitizedItem !== item) {
|
|
3254
4272
|
itemDidChange = true;
|
|
3255
4273
|
}
|
|
@@ -3287,7 +4305,7 @@ function sanitizeRelayHistoryTurn(turn, threadId = "") {
|
|
|
3287
4305
|
}
|
|
3288
4306
|
|
|
3289
4307
|
return itemDidChange ? sanitizedItem : item;
|
|
3290
|
-
});
|
|
4308
|
+
}).filter(Boolean);
|
|
3291
4309
|
|
|
3292
4310
|
return turnDidChange
|
|
3293
4311
|
? {
|
|
@@ -3297,6 +4315,67 @@ function sanitizeRelayHistoryTurn(turn, threadId = "") {
|
|
|
3297
4315
|
: turn;
|
|
3298
4316
|
}
|
|
3299
4317
|
|
|
4318
|
+
// Compatibility predicate for callers that only need a drop/no-drop decision.
|
|
4319
|
+
// The full sanitizer below also rewrites mixed items without losing attachments.
|
|
4320
|
+
const LIVE_ITEM_LIFECYCLE_METHODS = new Set([
|
|
4321
|
+
"item/started",
|
|
4322
|
+
"item/updated",
|
|
4323
|
+
"item/completed",
|
|
4324
|
+
]);
|
|
4325
|
+
|
|
4326
|
+
function isContextualUserItemNotification(parsed) {
|
|
4327
|
+
const method = typeof parsed?.method === "string" ? parsed.method : "";
|
|
4328
|
+
if (!LIVE_ITEM_LIFECYCLE_METHODS.has(method)) {
|
|
4329
|
+
return false;
|
|
4330
|
+
}
|
|
4331
|
+
const item = parsed?.params?.item;
|
|
4332
|
+
if (!isUserRoleItem(item)) {
|
|
4333
|
+
return false;
|
|
4334
|
+
}
|
|
4335
|
+
return isContextualUserText(readUserItemText(item));
|
|
4336
|
+
}
|
|
4337
|
+
|
|
4338
|
+
// Sanitizes both raw app-server item events and fallback user_message events
|
|
4339
|
+
// before they can become mobile bubbles. Structured attachments stay intact.
|
|
4340
|
+
function sanitizeLiveUserNotification(parsed) {
|
|
4341
|
+
if (!parsed || typeof parsed !== "object") {
|
|
4342
|
+
return parsed;
|
|
4343
|
+
}
|
|
4344
|
+
const method = typeof parsed.method === "string" ? parsed.method : "";
|
|
4345
|
+
if (LIVE_ITEM_LIFECYCLE_METHODS.has(method)) {
|
|
4346
|
+
const item = parsed?.params?.item;
|
|
4347
|
+
if (!isUserRoleItem(item)) {
|
|
4348
|
+
return parsed;
|
|
4349
|
+
}
|
|
4350
|
+
const sanitizedItem = sanitizeUserRoleItem(item);
|
|
4351
|
+
if (!sanitizedItem) {
|
|
4352
|
+
return null;
|
|
4353
|
+
}
|
|
4354
|
+
return sanitizedItem === item ? parsed : {
|
|
4355
|
+
...parsed,
|
|
4356
|
+
params: { ...parsed.params, item: sanitizedItem },
|
|
4357
|
+
};
|
|
4358
|
+
}
|
|
4359
|
+
|
|
4360
|
+
if (method !== "codex/event/user_message") {
|
|
4361
|
+
return parsed;
|
|
4362
|
+
}
|
|
4363
|
+
const key = typeof parsed?.params?.message === "string"
|
|
4364
|
+
? "message"
|
|
4365
|
+
: (typeof parsed?.params?.text === "string" ? "text" : "");
|
|
4366
|
+
if (!key) {
|
|
4367
|
+
return parsed;
|
|
4368
|
+
}
|
|
4369
|
+
const visible = visibleUserPromptText(parsed.params[key]);
|
|
4370
|
+
if (!visible) {
|
|
4371
|
+
return null;
|
|
4372
|
+
}
|
|
4373
|
+
return visible === parsed.params[key] ? parsed : {
|
|
4374
|
+
...parsed,
|
|
4375
|
+
params: { ...parsed.params, [key]: visible },
|
|
4376
|
+
};
|
|
4377
|
+
}
|
|
4378
|
+
|
|
3300
4379
|
function convertApplyPatchHistoryItem(item) {
|
|
3301
4380
|
const itemType = normalizeHistoryItemToken(item?.type);
|
|
3302
4381
|
const toolName = normalizeNonEmptyString(item?.name);
|
|
@@ -3608,12 +4687,17 @@ function parseBridgeJSON(value) {
|
|
|
3608
4687
|
}
|
|
3609
4688
|
}
|
|
3610
4689
|
|
|
3611
|
-
function trimThreadPayloadForRelay(parsed, explicitThread = undefined) {
|
|
4690
|
+
function trimThreadPayloadForRelay(parsed, explicitThread = undefined, options = {}) {
|
|
3612
4691
|
const thread = explicitThread ?? parsed?.result?.thread;
|
|
3613
4692
|
if (!parsed || !thread || typeof thread !== "object" || !Array.isArray(thread.turns)) {
|
|
3614
4693
|
return null;
|
|
3615
4694
|
}
|
|
3616
4695
|
|
|
4696
|
+
// Callers that pre-trimmed the turn window pass the dropped count and the original
|
|
4697
|
+
// first turn here so compaction markers keep reporting whole-thread numbers.
|
|
4698
|
+
const preOmittedTurnCount = Math.max(0, options.preOmittedTurnCount ?? 0);
|
|
4699
|
+
const compactionIdSource = options.compactionIdSource ?? null;
|
|
4700
|
+
|
|
3617
4701
|
let workingThread = thread;
|
|
3618
4702
|
let encoded = encodeRelayThreadPayload(parsed, workingThread);
|
|
3619
4703
|
if (encoded == null) {
|
|
@@ -3621,7 +4705,16 @@ function trimThreadPayloadForRelay(parsed, explicitThread = undefined) {
|
|
|
3621
4705
|
}
|
|
3622
4706
|
|
|
3623
4707
|
if (Buffer.byteLength(encoded, "utf8") <= RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES) {
|
|
3624
|
-
|
|
4708
|
+
if (preOmittedTurnCount <= 0) {
|
|
4709
|
+
return explicitThread === undefined ? null : encoded;
|
|
4710
|
+
}
|
|
4711
|
+
const compactedThread = buildRelayHistoryCompactedThread(
|
|
4712
|
+
thread,
|
|
4713
|
+
buildRelayCompactedHistoryTurns(thread.turns, thread.turns, preOmittedTurnCount, compactionIdSource),
|
|
4714
|
+
preOmittedTurnCount,
|
|
4715
|
+
thread.turns.length
|
|
4716
|
+
);
|
|
4717
|
+
return encodeRelayThreadPayload(parsed, compactedThread) ?? encoded;
|
|
3625
4718
|
}
|
|
3626
4719
|
|
|
3627
4720
|
const turns = thread.turns;
|
|
@@ -3634,8 +4727,8 @@ function trimThreadPayloadForRelay(parsed, explicitThread = undefined) {
|
|
|
3634
4727
|
}
|
|
3635
4728
|
const candidateThread = buildRelayHistoryCompactedThread(
|
|
3636
4729
|
thread,
|
|
3637
|
-
buildRelayCompactedHistoryTurns(turns, trimmedTurns),
|
|
3638
|
-
Math.max(0, turns.length - trimmedTurns.length),
|
|
4730
|
+
buildRelayCompactedHistoryTurns(turns, trimmedTurns, preOmittedTurnCount, compactionIdSource),
|
|
4731
|
+
preOmittedTurnCount + Math.max(0, turns.length - trimmedTurns.length),
|
|
3639
4732
|
trimmedTurns.length
|
|
3640
4733
|
);
|
|
3641
4734
|
encoded = encodeRelayThreadPayload(parsed, candidateThread);
|
|
@@ -3655,9 +4748,9 @@ function trimThreadPayloadForRelay(parsed, explicitThread = undefined) {
|
|
|
3655
4748
|
while (trimmedItems.length > 1) {
|
|
3656
4749
|
trimmedItems = trimmedItems.slice(1);
|
|
3657
4750
|
const compactedTurnPrefix = buildRelayHistoryCompactionTurn(
|
|
3658
|
-
Math.max(0, turns.length - 1),
|
|
4751
|
+
preOmittedTurnCount + Math.max(0, turns.length - 1),
|
|
3659
4752
|
1,
|
|
3660
|
-
thread
|
|
4753
|
+
compactionIdSource ?? thread
|
|
3661
4754
|
);
|
|
3662
4755
|
const candidateThread = buildRelayHistoryCompactedThread(
|
|
3663
4756
|
thread,
|
|
@@ -3668,7 +4761,7 @@ function trimThreadPayloadForRelay(parsed, explicitThread = undefined) {
|
|
|
3668
4761
|
...newestTurn,
|
|
3669
4762
|
items: trimmedItems,
|
|
3670
4763
|
}],
|
|
3671
|
-
Math.max(0, turns.length - 1),
|
|
4764
|
+
preOmittedTurnCount + Math.max(0, turns.length - 1),
|
|
3672
4765
|
1
|
|
3673
4766
|
);
|
|
3674
4767
|
encoded = encodeRelayThreadPayload(parsed, candidateThread);
|
|
@@ -3690,13 +4783,13 @@ function trimThreadPayloadForRelay(parsed, explicitThread = undefined) {
|
|
|
3690
4783
|
let candidateThread = buildRelayHistoryCompactedThread(
|
|
3691
4784
|
thread,
|
|
3692
4785
|
[
|
|
3693
|
-
...buildRelayCompactedHistoryTurns(turns, [newestTurn]).slice(0, -1),
|
|
4786
|
+
...buildRelayCompactedHistoryTurns(turns, [newestTurn], preOmittedTurnCount, compactionIdSource).slice(0, -1),
|
|
3694
4787
|
{
|
|
3695
4788
|
...newestTurn,
|
|
3696
4789
|
items: [truncatedItem],
|
|
3697
4790
|
},
|
|
3698
4791
|
],
|
|
3699
|
-
Math.max(0, turns.length - 1),
|
|
4792
|
+
preOmittedTurnCount + Math.max(0, turns.length - 1),
|
|
3700
4793
|
1
|
|
3701
4794
|
);
|
|
3702
4795
|
encoded = encodeRelayThreadPayload(parsed, candidateThread);
|
|
@@ -3707,13 +4800,13 @@ function trimThreadPayloadForRelay(parsed, explicitThread = undefined) {
|
|
|
3707
4800
|
candidateThread = buildRelayHistoryCompactedThread(
|
|
3708
4801
|
thread,
|
|
3709
4802
|
[
|
|
3710
|
-
...buildRelayCompactedHistoryTurns(turns, [newestTurn]).slice(0, -1),
|
|
4803
|
+
...buildRelayCompactedHistoryTurns(turns, [newestTurn], preOmittedTurnCount, compactionIdSource).slice(0, -1),
|
|
3711
4804
|
{
|
|
3712
4805
|
...newestTurn,
|
|
3713
4806
|
items: [compactHistoryItemForRelay(mostRecentItem, RELAY_HISTORY_TEXT_TAIL_LIMIT_CHARS)],
|
|
3714
4807
|
},
|
|
3715
4808
|
],
|
|
3716
|
-
Math.max(0, turns.length - 1),
|
|
4809
|
+
preOmittedTurnCount + Math.max(0, turns.length - 1),
|
|
3717
4810
|
1
|
|
3718
4811
|
);
|
|
3719
4812
|
return encodeRelayThreadPayload(parsed, candidateThread);
|
|
@@ -3753,6 +4846,32 @@ function trimTurnsListPayloadForRelay(parsed, turnsKey, originalRawMessage = nul
|
|
|
3753
4846
|
}
|
|
3754
4847
|
}
|
|
3755
4848
|
|
|
4849
|
+
// A bounded JSONL first page can still describe one exceptionally large
|
|
4850
|
+
// turn with many small items. Keep that provisional response relay-safe while
|
|
4851
|
+
// preserving its handoff flags/cursor; the canonical background page will
|
|
4852
|
+
// replace it with the authoritative history.
|
|
4853
|
+
if (result.remodexJsonlFallback === true) {
|
|
4854
|
+
for (const maxItems of [64, 16, 4, 1]) {
|
|
4855
|
+
for (const maxChars of [1_000, 0]) {
|
|
4856
|
+
const emergencyTurns = turns.map((turn) => (
|
|
4857
|
+
compactEmergencySingleTurnForRelay(turn, maxChars, maxItems)
|
|
4858
|
+
));
|
|
4859
|
+
const emergencyPayload = JSON.stringify({
|
|
4860
|
+
...parsed,
|
|
4861
|
+
result: {
|
|
4862
|
+
...result,
|
|
4863
|
+
[turnsKey]: emergencyTurns,
|
|
4864
|
+
remodexPageCompactedForRelay: true,
|
|
4865
|
+
remodexEmergencyJsonlPageForRelay: true,
|
|
4866
|
+
},
|
|
4867
|
+
});
|
|
4868
|
+
if (Buffer.byteLength(emergencyPayload, "utf8") <= RELAY_THREAD_PAYLOAD_SOFT_LIMIT_BYTES) {
|
|
4869
|
+
return emergencyPayload;
|
|
4870
|
+
}
|
|
4871
|
+
}
|
|
4872
|
+
}
|
|
4873
|
+
}
|
|
4874
|
+
|
|
3756
4875
|
return fallbackCompactedPayload ?? (originalRawMessage ?? encoded);
|
|
3757
4876
|
}
|
|
3758
4877
|
|
|
@@ -3779,12 +4898,12 @@ function buildRelayHistoryCompactedThread(thread, turns, omittedTurnCount, keptT
|
|
|
3779
4898
|
};
|
|
3780
4899
|
}
|
|
3781
4900
|
|
|
3782
|
-
function buildRelayCompactedHistoryTurns(allTurns, keptTurns) {
|
|
3783
|
-
const omittedTurnCount = Math.max(0, allTurns.length - keptTurns.length);
|
|
4901
|
+
function buildRelayCompactedHistoryTurns(allTurns, keptTurns, preOmittedTurnCount = 0, idSourceOverride = null) {
|
|
4902
|
+
const omittedTurnCount = preOmittedTurnCount + Math.max(0, allTurns.length - keptTurns.length);
|
|
3784
4903
|
const compactionTurn = buildRelayHistoryCompactionTurn(
|
|
3785
4904
|
omittedTurnCount,
|
|
3786
4905
|
keptTurns.length,
|
|
3787
|
-
allTurns[0]
|
|
4906
|
+
idSourceOverride ?? allTurns[0]
|
|
3788
4907
|
);
|
|
3789
4908
|
return compactionTurn ? [compactionTurn, ...keptTurns] : keptTurns;
|
|
3790
4909
|
}
|
|
@@ -3808,6 +4927,9 @@ function buildRelayHistoryCompactionTurn(omittedTurnCount, keptTurnCount, idSour
|
|
|
3808
4927
|
|
|
3809
4928
|
return {
|
|
3810
4929
|
id: `remodex-history-compacted-${baseId}`,
|
|
4930
|
+
// A status-less turn reads as interruptible/running to the phone's
|
|
4931
|
+
// turn-state snapshot, flagging idle heavy threads as "thinking".
|
|
4932
|
+
status: "completed",
|
|
3811
4933
|
remodexSynthetic: true,
|
|
3812
4934
|
remodexHistoryCompacted: true,
|
|
3813
4935
|
remodexOmittedTurnCount: omittedTurnCount,
|
|
@@ -3970,21 +5092,42 @@ function persistBridgePreferences(
|
|
|
3970
5092
|
});
|
|
3971
5093
|
}
|
|
3972
5094
|
|
|
5095
|
+
function shouldSuppressRolloutMirrorForThread(
|
|
5096
|
+
threadId,
|
|
5097
|
+
{ desktopIpcActionFollower = null, desktopIpcLiveOwner = null } = {},
|
|
5098
|
+
{ fallbackActivityAt = 0 } = {}
|
|
5099
|
+
) {
|
|
5100
|
+
// Desktop ownership is an expiring live lease, not a permanent boolean. A
|
|
5101
|
+
// stale IPC snapshot used to mute an actively growing rollout forever.
|
|
5102
|
+
const followerIsFresh = typeof desktopIpcActionFollower?.hasFreshLiveThreadState === "function"
|
|
5103
|
+
? desktopIpcActionFollower.hasFreshLiveThreadState(threadId, { fallbackActivityAt })
|
|
5104
|
+
: desktopIpcActionFollower?.hasLiveThreadState(threadId);
|
|
5105
|
+
const ownerIsFresh = typeof desktopIpcLiveOwner?.isFreshThreadOwned === "function"
|
|
5106
|
+
? desktopIpcLiveOwner.isFreshThreadOwned(threadId)
|
|
5107
|
+
: false;
|
|
5108
|
+
return Boolean(followerIsFresh) || Boolean(ownerIsFresh);
|
|
5109
|
+
}
|
|
5110
|
+
|
|
3973
5111
|
module.exports = {
|
|
3974
5112
|
buildThreadTurnsListRelaySanitizeContext,
|
|
3975
5113
|
buildHeartbeatBridgeStatus,
|
|
3976
|
-
buildRelayCloseStatusError,
|
|
3977
5114
|
buildRelayAccessTokenHeaders,
|
|
3978
5115
|
buildRelayUserAgentHeader,
|
|
5116
|
+
canonicalThreadTurnsListRequest,
|
|
3979
5117
|
createMacOSBridgeWakeAssertion,
|
|
5118
|
+
createThreadTurnsListFastPageCoordinator,
|
|
3980
5119
|
disableUnsupportedReasoningSummaryForTurnStart,
|
|
3981
5120
|
fetchAdaptiveThreadTurnsListForRelay,
|
|
3982
5121
|
hasRelayConnectionGoneStale,
|
|
3983
|
-
|
|
5122
|
+
isContextualUserItemNotification,
|
|
5123
|
+
maybeMergeLatestJsonlTurnIntoTurnsListResponse,
|
|
5124
|
+
normalizeTurnStartForCodex,
|
|
3984
5125
|
normalizeRelayBoundJsonRpcMessage,
|
|
3985
5126
|
persistBridgePreferences,
|
|
3986
5127
|
resolveJsonlTurnsListRolloutPathForFallback,
|
|
3987
5128
|
sanitizeLiveGeneratedImageMessageForRelay,
|
|
5129
|
+
sanitizeLiveUserNotification,
|
|
3988
5130
|
sanitizeThreadHistoryImagesForRelay,
|
|
5131
|
+
shouldSuppressRolloutMirrorForThread,
|
|
3989
5132
|
startBridge,
|
|
3990
5133
|
};
|