@makerbi/remodex 3.2.0 → 3.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/bridge.js +160 -30
- package/src/codex-desktop-refresher.js +8 -8
- package/src/codex-runtime-settings.js +113 -0
- package/src/desktop-ipc-action-follower.js +289 -95
- package/src/desktop-ipc-conversation-adapter.js +56 -51
- package/src/desktop-ipc-conversation-projector.js +1 -0
- package/src/desktop-ipc-live-owner.js +189 -164
- package/src/desktop-ipc-shared.js +35 -1
- package/src/git-handler.js +20 -17
- package/src/rollout-live-mirror.js +3 -4
- package/src/runtime-provider-router.js +10 -2
- package/src/thread-activity-projector.js +694 -0
- package/src/thread-activity-store.js +325 -0
- package/src/thread-runtime-settings-store.js +60 -74
|
@@ -5,11 +5,19 @@
|
|
|
5
5
|
// Depends on: net, ./desktop-ipc-conversation-adapter, ./desktop-ipc-owner-transport, ./desktop-ipc-state-patches, ./desktop-ipc-shared
|
|
6
6
|
|
|
7
7
|
const net = require("net");
|
|
8
|
+
const {
|
|
9
|
+
applyRuntimeSettingsToConversation,
|
|
10
|
+
createThreadMutationQueue,
|
|
11
|
+
hasOwn,
|
|
12
|
+
normalizeThreadSettingsUpdate,
|
|
13
|
+
threadSettingsFromRuntimeSettings,
|
|
14
|
+
} = require("./codex-runtime-settings");
|
|
8
15
|
|
|
9
16
|
const {
|
|
10
17
|
CLIENT_STATUS_CHANGED,
|
|
11
18
|
DESKTOP_IPC_METHOD_VERSIONS: METHOD_VERSION_BY_NAME,
|
|
12
19
|
buildCompleteThreadReadParams,
|
|
20
|
+
buildThreadReadStateContext,
|
|
13
21
|
cloneJSON,
|
|
14
22
|
conversationSnapshotShowsActiveTurn,
|
|
15
23
|
isPlainJSONObject,
|
|
@@ -66,6 +74,7 @@ const THREAD_QUEUED_FOLLOWUPS_CHANGED = "thread-queued-followups-changed";
|
|
|
66
74
|
const REMODEX_LIVE_OWNER_SOURCE = "desktop-ipc-live-owner";
|
|
67
75
|
|
|
68
76
|
const SUPPORTED_FOLLOWER_REQUEST_METHODS = new Set([
|
|
77
|
+
"thread-owner-discovery",
|
|
69
78
|
"thread-follower-start-turn",
|
|
70
79
|
"thread-follower-load-complete-history",
|
|
71
80
|
"thread-follower-update-thread-settings",
|
|
@@ -141,6 +150,7 @@ function createDesktopIpcLiveOwner({
|
|
|
141
150
|
maxPatchBytes = DEFAULT_MAX_PATCH_BYTES,
|
|
142
151
|
requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
143
152
|
reconnectMs = DEFAULT_RECONNECT_MS,
|
|
153
|
+
startRouterWhenMissing = false,
|
|
144
154
|
initialHistoryRetryMs = DEFAULT_INITIAL_HISTORY_RETRY_MS,
|
|
145
155
|
initialHistoryMaxAttempts = DEFAULT_INITIAL_HISTORY_MAX_ATTEMPTS,
|
|
146
156
|
liveOwnershipFreshnessMs = DEFAULT_LIVE_OWNERSHIP_FRESHNESS_MS,
|
|
@@ -184,6 +194,7 @@ function createDesktopIpcLiveOwner({
|
|
|
184
194
|
// until the first user item and turn completion prove the rollout was written.
|
|
185
195
|
const pendingSidebarMaterializationThreadIds = new Set();
|
|
186
196
|
const replayedSidebarMaterializationThreadIds = new Set();
|
|
197
|
+
const enqueueMutation = createThreadMutationQueue();
|
|
187
198
|
const pendingTurnStartParamsByThreadId = new Map();
|
|
188
199
|
const pendingTurnStartEntriesByRequestId = new Map();
|
|
189
200
|
const followerRuntimeOverridesByThreadId = new Map();
|
|
@@ -192,6 +203,7 @@ function createDesktopIpcLiveOwner({
|
|
|
192
203
|
const queuedFollowUpsByThreadId = new Map();
|
|
193
204
|
const runningQueuedFollowUpThreadIds = new Set();
|
|
194
205
|
const announcedReadStateThreadIds = new Set();
|
|
206
|
+
const pendingReadStateByThreadId = new Map();
|
|
195
207
|
const pendingThreadArchiveMetadataByThreadId = new Map();
|
|
196
208
|
const dirtyThreadIds = new Set();
|
|
197
209
|
let snapshotTimer = null;
|
|
@@ -204,6 +216,10 @@ function createDesktopIpcLiveOwner({
|
|
|
204
216
|
requestTimeoutMs,
|
|
205
217
|
reconnectMs,
|
|
206
218
|
logPrefix,
|
|
219
|
+
// Let Desktop/VSCode own its bus. Restarting a bridge-owned router forces
|
|
220
|
+
// concurrent Desktop clients to re-elect routers and can split their sockets.
|
|
221
|
+
// Phone history remains local while Desktop is closed; reconnect replays it.
|
|
222
|
+
startRouterWhenMissing,
|
|
207
223
|
onConnected() {
|
|
208
224
|
flushPendingThreadArchiveMetadataBroadcasts();
|
|
209
225
|
requestFollowerStatusForAllOwnedThreads();
|
|
@@ -302,7 +318,24 @@ function createDesktopIpcLiveOwner({
|
|
|
302
318
|
if (!message || typeof message !== "object") {
|
|
303
319
|
return;
|
|
304
320
|
}
|
|
305
|
-
|
|
321
|
+
if (message.method === "account/updated") {
|
|
322
|
+
announcedReadStateThreadIds.clear();
|
|
323
|
+
pendingReadStateByThreadId.clear();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (message.method === "thread/settings/updated") {
|
|
327
|
+
const threadId = readThreadIdFromParams(message.params);
|
|
328
|
+
const settings = message.params?.threadSettings;
|
|
329
|
+
if (threadId && settings && ownedThreadIds.has(threadId)) {
|
|
330
|
+
followerRuntimeOverridesByThreadId.set(threadId, {
|
|
331
|
+
...(followerRuntimeOverridesByThreadId.get(threadId) || {}),
|
|
332
|
+
...normalizeThreadSettingsUpdate(settings, { authoritative: true }),
|
|
333
|
+
});
|
|
334
|
+
applyRuntimeSettingsToConversation(conversations.get(threadId), settings, { authoritative: true });
|
|
335
|
+
runtimeSettingsStore?.observe?.(threadId, settings);
|
|
336
|
+
scheduleSnapshot(threadId);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
306
339
|
const responseId = message.id == null ? "" : String(message.id);
|
|
307
340
|
if (responseId && !message.method) {
|
|
308
341
|
resolvePendingTurnStartResponse(responseId, message);
|
|
@@ -351,6 +384,11 @@ function createDesktopIpcLiveOwner({
|
|
|
351
384
|
refreshOptimisticFallbackForThread(update.threadId);
|
|
352
385
|
scheduleSnapshot(update.threadId);
|
|
353
386
|
replaySidebarAnnouncementAfterMaterialization(message, update.threadId);
|
|
387
|
+
if (message.method === "thread/name/updated") {
|
|
388
|
+
// Unopened Desktop threads ignore stream snapshots. Refresh their catalog
|
|
389
|
+
// metadata after the name is persisted, even if the first turn has ended.
|
|
390
|
+
broadcastThreadUnarchived(update.threadId);
|
|
391
|
+
}
|
|
354
392
|
}
|
|
355
393
|
|
|
356
394
|
if (readString(message.method) === "turn/completed") {
|
|
@@ -404,6 +442,7 @@ function createDesktopIpcLiveOwner({
|
|
|
404
442
|
queuedFollowUpsByThreadId.clear();
|
|
405
443
|
runningQueuedFollowUpThreadIds.clear();
|
|
406
444
|
announcedReadStateThreadIds.clear();
|
|
445
|
+
pendingReadStateByThreadId.clear();
|
|
407
446
|
ownedThreadIds.clear();
|
|
408
447
|
conversations.clear();
|
|
409
448
|
ipc.close();
|
|
@@ -423,16 +462,44 @@ function createDesktopIpcLiveOwner({
|
|
|
423
462
|
if (hadUnread) {
|
|
424
463
|
conversation.hasUnreadTurn = false;
|
|
425
464
|
conversation.unreadMessageCount = 0;
|
|
465
|
+
announcedReadStateThreadIds.delete(normalizedThreadId);
|
|
426
466
|
scheduleSnapshot(normalizedThreadId);
|
|
427
467
|
} else if (announcedReadStateThreadIds.has(normalizedThreadId)) {
|
|
428
468
|
return;
|
|
429
469
|
}
|
|
430
|
-
if (
|
|
431
|
-
|
|
432
|
-
hasUnreadTurn: false,
|
|
433
|
-
})) {
|
|
434
|
-
announcedReadStateThreadIds.add(normalizedThreadId);
|
|
470
|
+
if (pendingReadStateByThreadId.has(normalizedThreadId)) {
|
|
471
|
+
return;
|
|
435
472
|
}
|
|
473
|
+
const request = Symbol();
|
|
474
|
+
pendingReadStateByThreadId.set(normalizedThreadId, request);
|
|
475
|
+
Promise.resolve()
|
|
476
|
+
.then(() => sendCodexRequest("getAuthStatus", { includeToken: true, refreshToken: false }))
|
|
477
|
+
.then((authStatus) => {
|
|
478
|
+
if (pendingReadStateByThreadId.get(normalizedThreadId) !== request
|
|
479
|
+
|| !ownedThreadIds.has(normalizedThreadId)) {
|
|
480
|
+
return;
|
|
481
|
+
}
|
|
482
|
+
const current = conversations.get(normalizedThreadId);
|
|
483
|
+
if (current?.hasUnreadTurn || current?.unreadMessageCount > 0) {
|
|
484
|
+
return;
|
|
485
|
+
}
|
|
486
|
+
const context = buildThreadReadStateContext(authStatus, hostId);
|
|
487
|
+
if (context && ipc.sendBroadcast(THREAD_READ_STATE_CHANGED, {
|
|
488
|
+
conversationId: normalizedThreadId,
|
|
489
|
+
hostId,
|
|
490
|
+
hasUnreadTurn: false,
|
|
491
|
+
context,
|
|
492
|
+
})) {
|
|
493
|
+
announcedReadStateThreadIds.add(normalizedThreadId);
|
|
494
|
+
}
|
|
495
|
+
})
|
|
496
|
+
// A failed identity lookup leaves the next phone read free to retry.
|
|
497
|
+
.catch(() => {})
|
|
498
|
+
.finally(() => {
|
|
499
|
+
if (pendingReadStateByThreadId.get(normalizedThreadId) === request) {
|
|
500
|
+
pendingReadStateByThreadId.delete(normalizedThreadId);
|
|
501
|
+
}
|
|
502
|
+
});
|
|
436
503
|
}
|
|
437
504
|
|
|
438
505
|
// Snappier Stop UX on Desktop: flip the active turn to interrupted right away;
|
|
@@ -479,7 +546,7 @@ function createDesktopIpcLiveOwner({
|
|
|
479
546
|
}
|
|
480
547
|
const sanitizedParams = sanitizeTurnStartParams(cloneJSON(params));
|
|
481
548
|
sanitizedParams.input = normalizeInputEntriesForDesktop(sanitizedParams.input);
|
|
482
|
-
const entry = { params: sanitizedParams, requestId: normalizedRequestId || null };
|
|
549
|
+
const entry = { params: sanitizedParams, requestId: normalizedRequestId || null, runtimeSettingsRevision: runtimeSettingsStore?.get?.(normalizedThreadId)?.revision ?? 0 };
|
|
483
550
|
const queue = pendingTurnStartParamsByThreadId.get(normalizedThreadId) || [];
|
|
484
551
|
queue.push(entry);
|
|
485
552
|
pendingTurnStartParamsByThreadId.set(normalizedThreadId, queue);
|
|
@@ -539,7 +606,8 @@ function createDesktopIpcLiveOwner({
|
|
|
539
606
|
pending.threadId,
|
|
540
607
|
pending.entry?.params,
|
|
541
608
|
"phone",
|
|
542
|
-
readTurnIdFromResult(message.result)
|
|
609
|
+
readTurnIdFromResult(message.result),
|
|
610
|
+
pending.entry?.runtimeSettingsRevision
|
|
543
611
|
);
|
|
544
612
|
scheduleSnapshot(pending.threadId);
|
|
545
613
|
}
|
|
@@ -711,6 +779,7 @@ function createDesktopIpcLiveOwner({
|
|
|
711
779
|
}
|
|
712
780
|
runningQueuedFollowUpThreadIds.delete(normalizedThreadId);
|
|
713
781
|
announcedReadStateThreadIds.delete(normalizedThreadId);
|
|
782
|
+
pendingReadStateByThreadId.delete(normalizedThreadId);
|
|
714
783
|
cancelSidebarAnnouncement(normalizedThreadId);
|
|
715
784
|
announcedSidebarThreadIds.delete(normalizedThreadId);
|
|
716
785
|
pendingSidebarMaterializationThreadIds.delete(normalizedThreadId);
|
|
@@ -1299,6 +1368,13 @@ function createDesktopIpcLiveOwner({
|
|
|
1299
1368
|
if (!SUPPORTED_FOLLOWER_REQUEST_METHODS.has(method)) {
|
|
1300
1369
|
return false;
|
|
1301
1370
|
}
|
|
1371
|
+
const requestedHostId = readString(params.hostId || envelope.request?.hostId || envelope.hostId);
|
|
1372
|
+
if (method === "thread-owner-discovery" && !requestedHostId) {
|
|
1373
|
+
return false;
|
|
1374
|
+
}
|
|
1375
|
+
if (requestedHostId && requestedHostId !== hostId) {
|
|
1376
|
+
return false;
|
|
1377
|
+
}
|
|
1302
1378
|
const threadId = readConversationIdFromFollowerParams(params);
|
|
1303
1379
|
return Boolean(threadId && ownedThreadIds.has(threadId));
|
|
1304
1380
|
}
|
|
@@ -1307,11 +1383,14 @@ function createDesktopIpcLiveOwner({
|
|
|
1307
1383
|
const method = readString(envelope?.method);
|
|
1308
1384
|
const params = envelope?.params && typeof envelope.params === "object" ? envelope.params : {};
|
|
1309
1385
|
const conversationId = readConversationIdFromFollowerParams(params);
|
|
1310
|
-
if (!conversationId || !
|
|
1386
|
+
if (!conversationId || !canHandleFollowerRequest(envelope)) {
|
|
1311
1387
|
throw new Error("conversation-not-owned");
|
|
1312
1388
|
}
|
|
1313
1389
|
|
|
1314
1390
|
switch (method) {
|
|
1391
|
+
case "thread-owner-discovery":
|
|
1392
|
+
// Remodex cannot yet inject Desktop's untrusted app response items.
|
|
1393
|
+
return { supportsUntrustedAppInput: false };
|
|
1315
1394
|
case "thread-follower-start-turn":
|
|
1316
1395
|
return await handleFollowerStartTurn(conversationId, params);
|
|
1317
1396
|
case "thread-follower-load-complete-history":
|
|
@@ -1381,7 +1460,15 @@ function createDesktopIpcLiveOwner({
|
|
|
1381
1460
|
return { revision };
|
|
1382
1461
|
}
|
|
1383
1462
|
|
|
1384
|
-
|
|
1463
|
+
function handleFollowerStartTurn(conversationId, params) {
|
|
1464
|
+
return enqueueMutation(conversationId, () => startFollowerTurn(conversationId, params));
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
async function startFollowerTurn(conversationId, params) {
|
|
1468
|
+
if (params.turnStart?.context?.responseItems?.length > 0) {
|
|
1469
|
+
throw new Error("Remodex does not support untrusted app input yet.");
|
|
1470
|
+
}
|
|
1471
|
+
const revisionBefore = runtimeSettingsStore?.get?.(conversationId)?.revision ?? 0;
|
|
1385
1472
|
const rawTurnStartParams = readFollowerTurnStartParams(params);
|
|
1386
1473
|
const codexParams = mergeFollowerRuntimeOverrides(conversationId, sanitizeTurnStartParams({
|
|
1387
1474
|
...rawTurnStartParams,
|
|
@@ -1414,7 +1501,8 @@ function createDesktopIpcLiveOwner({
|
|
|
1414
1501
|
conversationId,
|
|
1415
1502
|
nextCodexParams,
|
|
1416
1503
|
isKnownHeldPhoneStart ? "phone" : "desktop",
|
|
1417
|
-
readTurnIdFromResult(turnStartResult)
|
|
1504
|
+
readTurnIdFromResult(turnStartResult),
|
|
1505
|
+
revisionBefore
|
|
1418
1506
|
);
|
|
1419
1507
|
scheduleSnapshot(conversationId);
|
|
1420
1508
|
if (!isKnownHeldPhoneStart) {
|
|
@@ -1478,11 +1566,13 @@ function createDesktopIpcLiveOwner({
|
|
|
1478
1566
|
if (!expectedTurnId) {
|
|
1479
1567
|
throw new Error("Missing expectedTurnId for follower steer request.");
|
|
1480
1568
|
}
|
|
1481
|
-
|
|
1569
|
+
const result = await sendCodexRequest("turn/steer", {
|
|
1482
1570
|
threadId: conversationId,
|
|
1483
1571
|
input: Array.isArray(rawSteerParams.input) ? rawSteerParams.input : [],
|
|
1484
1572
|
expectedTurnId,
|
|
1573
|
+
...Object.fromEntries(["clientUserMessageId", "additionalContext", "toolOutput"].filter((key) => hasOwn(rawSteerParams, key)).map((key) => [key, cloneJSON(rawSteerParams[key])])),
|
|
1485
1574
|
});
|
|
1575
|
+
return { result };
|
|
1486
1576
|
}
|
|
1487
1577
|
|
|
1488
1578
|
async function handleFollowerInterruptTurn(conversationId, params) {
|
|
@@ -1569,100 +1659,49 @@ function createDesktopIpcLiveOwner({
|
|
|
1569
1659
|
return { ok: true };
|
|
1570
1660
|
}
|
|
1571
1661
|
|
|
1572
|
-
//
|
|
1573
|
-
// merged into later Desktop-origin turn starts, so acknowledging them is honest
|
|
1574
|
-
// instead of a cosmetic broadcast-only update.
|
|
1662
|
+
// Legacy IPC verbs enter the same acknowledged settings path.
|
|
1575
1663
|
function applyFollowerModelAndReasoning(conversationId, params) {
|
|
1576
|
-
const
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
overrides.model = readString(params.model);
|
|
1580
|
-
if (conversation) {
|
|
1581
|
-
conversation.latestModel = overrides.model;
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
if (Object.prototype.hasOwnProperty.call(params, "reasoningEffort")) {
|
|
1585
|
-
overrides.effort = params.reasoningEffort || null;
|
|
1586
|
-
if (conversation) {
|
|
1587
|
-
conversation.latestReasoningEffort = overrides.effort;
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
if (Object.prototype.hasOwnProperty.call(params, "serviceTier")) {
|
|
1591
|
-
overrides.serviceTier = readString(params.serviceTier) || null;
|
|
1592
|
-
if (conversation) {
|
|
1593
|
-
conversation.latestServiceTier = overrides.serviceTier;
|
|
1594
|
-
}
|
|
1595
|
-
}
|
|
1596
|
-
followerRuntimeOverridesByThreadId.set(conversationId, overrides);
|
|
1597
|
-
if (conversation) {
|
|
1598
|
-
scheduleSnapshot(conversationId);
|
|
1599
|
-
}
|
|
1600
|
-
return { ok: true };
|
|
1664
|
+
const settings = normalizeThreadSettingsUpdate(params);
|
|
1665
|
+
if (hasOwn(params, "reasoningEffort")) settings.effort = params.reasoningEffort;
|
|
1666
|
+
return applyFollowerThreadSettings(conversationId, settings);
|
|
1601
1667
|
}
|
|
1602
1668
|
|
|
1603
1669
|
function applyFollowerCollaborationMode(conversationId, params) {
|
|
1604
|
-
|
|
1605
|
-
return { ok: true };
|
|
1606
|
-
}
|
|
1607
|
-
const overrides = followerRuntimeOverridesByThreadId.get(conversationId) || {};
|
|
1608
|
-
overrides.collaborationMode = cloneJSON(params.collaborationMode);
|
|
1609
|
-
followerRuntimeOverridesByThreadId.set(conversationId, overrides);
|
|
1610
|
-
const conversation = conversations.get(conversationId);
|
|
1611
|
-
if (conversation) {
|
|
1612
|
-
conversation.latestCollaborationMode = cloneJSON(params.collaborationMode);
|
|
1613
|
-
scheduleSnapshot(conversationId);
|
|
1614
|
-
}
|
|
1615
|
-
return { ok: true };
|
|
1670
|
+
return applyFollowerThreadSettings(conversationId, { collaborationMode: params.collaborationMode });
|
|
1616
1671
|
}
|
|
1617
1672
|
|
|
1618
|
-
//
|
|
1619
|
-
//
|
|
1620
|
-
function applyFollowerThreadSettings(conversationId, threadSettings) {
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
const overrides = followerRuntimeOverridesByThreadId.get(conversationId) || {};
|
|
1625
|
-
const model = readString(threadSettings.model)
|
|
1626
|
-
|| readString(threadSettings.collaborationMode?.settings?.model);
|
|
1627
|
-
const effort = threadSettings.effort;
|
|
1628
|
-
const hasServiceTier = Object.prototype.hasOwnProperty.call(threadSettings, "serviceTier");
|
|
1629
|
-
if (model) {
|
|
1630
|
-
overrides.model = model;
|
|
1631
|
-
}
|
|
1632
|
-
if (effort !== undefined) {
|
|
1633
|
-
overrides.effort = effort ?? null;
|
|
1634
|
-
}
|
|
1635
|
-
if (hasServiceTier) {
|
|
1636
|
-
overrides.serviceTier = readString(threadSettings.serviceTier) || null;
|
|
1637
|
-
}
|
|
1638
|
-
if (threadSettings.collaborationMode && typeof threadSettings.collaborationMode === "object") {
|
|
1639
|
-
overrides.collaborationMode = cloneJSON(threadSettings.collaborationMode);
|
|
1640
|
-
}
|
|
1641
|
-
followerRuntimeOverridesByThreadId.set(conversationId, overrides);
|
|
1642
|
-
|
|
1643
|
-
const conversation = conversations.get(conversationId);
|
|
1644
|
-
if (conversation) {
|
|
1645
|
-
conversation.latestThreadSettings = {
|
|
1646
|
-
...(conversation.latestThreadSettings && typeof conversation.latestThreadSettings === "object"
|
|
1647
|
-
? conversation.latestThreadSettings
|
|
1648
|
-
: {}),
|
|
1649
|
-
...cloneJSON(threadSettings),
|
|
1650
|
-
};
|
|
1651
|
-
if (model) {
|
|
1652
|
-
conversation.latestModel = model;
|
|
1673
|
+
// Settings are acknowledged by the app-server before they become composer
|
|
1674
|
+
// state. Updating a mirror alone never changes the executing runtime.
|
|
1675
|
+
function applyFollowerThreadSettings(conversationId, threadSettings, source = "desktop") {
|
|
1676
|
+
return enqueueMutation(conversationId, async () => {
|
|
1677
|
+
if (!threadSettings || typeof threadSettings !== "object" || Array.isArray(threadSettings)) {
|
|
1678
|
+
throw new Error("Missing thread settings.");
|
|
1653
1679
|
}
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
}
|
|
1657
|
-
if (
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1680
|
+
const revisionBefore = runtimeSettingsStore?.get?.(conversationId)?.revision;
|
|
1681
|
+
const settings = normalizeThreadSettingsUpdate(threadSettings);
|
|
1682
|
+
await sendCodexRequest("thread/settings/update", { threadId: conversationId, ...settings });
|
|
1683
|
+
if (!ownedThreadIds.has(conversationId)) {
|
|
1684
|
+
// A successful settings mutation proves the local runtime has this task
|
|
1685
|
+
// loaded. Announce ownership only after that acknowledgement and hydrate
|
|
1686
|
+
// complete history before publishing the first Desktop snapshot.
|
|
1687
|
+
markOwnedThread(conversationId);
|
|
1688
|
+
threadsAwaitingInitialHistoryByThreadId.add(conversationId);
|
|
1689
|
+
seedOwnedConversation(conversationId);
|
|
1690
|
+
requestInitialHistoryBaselineIfDue(conversationId);
|
|
1662
1691
|
}
|
|
1692
|
+
const current = runtimeSettingsStore?.get?.(conversationId);
|
|
1693
|
+
const confirmed = current && current.revision !== revisionBefore
|
|
1694
|
+
? current
|
|
1695
|
+
: runtimeSettingsStore?.commit?.(conversationId, settings, { source });
|
|
1696
|
+
const applied = confirmed ? {
|
|
1697
|
+
...settings, ...threadSettingsFromRuntimeSettings(confirmed),
|
|
1698
|
+
} : settings;
|
|
1699
|
+
const overrides = followerRuntimeOverridesByThreadId.get(conversationId) || {};
|
|
1700
|
+
followerRuntimeOverridesByThreadId.set(conversationId, { ...overrides, ...applied });
|
|
1701
|
+
applyRuntimeSettingsToConversation(conversations.get(conversationId), applied, { authoritative: !!confirmed });
|
|
1663
1702
|
scheduleSnapshot(conversationId);
|
|
1664
|
-
|
|
1665
|
-
|
|
1703
|
+
return { ok: true, runtimeSettings: confirmed || null };
|
|
1704
|
+
});
|
|
1666
1705
|
}
|
|
1667
1706
|
|
|
1668
1707
|
// Desktop followers hand the owner the full queue map and expect it to run
|
|
@@ -1700,72 +1739,58 @@ function createDesktopIpcLiveOwner({
|
|
|
1700
1739
|
if (!Array.isArray(queue) || queue.length === 0 || runningQueuedFollowUpThreadIds.has(threadId)) {
|
|
1701
1740
|
return;
|
|
1702
1741
|
}
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
if (queue.length === 0) {
|
|
1717
|
-
queuedFollowUpsByThreadId.delete(threadId);
|
|
1718
|
-
}
|
|
1742
|
+
runningQueuedFollowUpThreadIds.add(threadId);
|
|
1743
|
+
let retryChangedQueue = false;
|
|
1744
|
+
enqueueMutation(threadId, async () => {
|
|
1745
|
+
// Desktop replaces queue arrays on refresh. Resolve the current entry only
|
|
1746
|
+
// after settings settle, and retry if another refresh changes it while
|
|
1747
|
+
// request normalization is awaiting context.
|
|
1748
|
+
const entry = queuedFollowUpsByThreadId.get(threadId)?.[0];
|
|
1749
|
+
const canRun = () => ownedThreadIds.has(threadId) && !activeTurnIdForConversation(threadId);
|
|
1750
|
+
if (!entry || entry.pausedReason || !canRun()) return;
|
|
1751
|
+
const text = readString(entry.context?.text) || readString(entry.text) || readString(entry.prompt);
|
|
1752
|
+
if (!text) {
|
|
1753
|
+
if (typeof entry === "object") entry.pausedReason = "remodex-unsupported-entry";
|
|
1754
|
+
else queuedFollowUpsByThreadId.get(threadId)?.shift();
|
|
1719
1755
|
broadcastQueuedFollowUps(threadId);
|
|
1720
1756
|
return;
|
|
1721
1757
|
}
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
? normalized
|
|
1741
|
-
: startParams;
|
|
1742
|
-
queuedTurnParams = params;
|
|
1743
|
-
const pendingEntry = rememberPendingTurnStart(threadId, params);
|
|
1744
|
-
if (pendingEntry) {
|
|
1745
|
-
insertOptimisticPendingTurn(threadId, pendingEntry);
|
|
1746
|
-
scheduleSnapshot(threadId);
|
|
1747
|
-
}
|
|
1748
|
-
return sendCodexRequest("turn/start", params);
|
|
1749
|
-
})
|
|
1750
|
-
.then((turnStartResult) => {
|
|
1751
|
-
commitAcceptedRuntimeSettings(
|
|
1752
|
-
threadId,
|
|
1753
|
-
queuedTurnParams,
|
|
1754
|
-
"desktop",
|
|
1755
|
-
readTurnIdFromResult(turnStartResult)
|
|
1756
|
-
);
|
|
1758
|
+
const revisionBefore = runtimeSettingsStore?.get?.(threadId)?.revision ?? 0;
|
|
1759
|
+
const conversation = conversations.get(threadId);
|
|
1760
|
+
const startParams = mergeFollowerRuntimeOverrides(threadId, sanitizeTurnStartParams({
|
|
1761
|
+
threadId,
|
|
1762
|
+
input: [{ type: "text", text }],
|
|
1763
|
+
cwd: readString(entry?.cwd) || readString(conversation?.cwd) || undefined,
|
|
1764
|
+
}));
|
|
1765
|
+
const normalized = await normalizeTurnStartParams(cloneJSON(startParams));
|
|
1766
|
+
if (queuedFollowUpsByThreadId.get(threadId)?.[0] !== entry) {
|
|
1767
|
+
retryChangedQueue = true;
|
|
1768
|
+
return;
|
|
1769
|
+
}
|
|
1770
|
+
if (!canRun() || entry.pausedReason) return;
|
|
1771
|
+
const params = normalized && typeof normalized === "object" && !Array.isArray(normalized)
|
|
1772
|
+
? normalized : startParams;
|
|
1773
|
+
const pendingEntry = rememberPendingTurnStart(threadId, params);
|
|
1774
|
+
if (pendingEntry) {
|
|
1775
|
+
insertOptimisticPendingTurn(threadId, pendingEntry);
|
|
1757
1776
|
scheduleSnapshot(threadId);
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1777
|
+
}
|
|
1778
|
+
const turnStartResult = await sendCodexRequest("turn/start", params);
|
|
1779
|
+
commitAcceptedRuntimeSettings(threadId, params, "desktop", readTurnIdFromResult(turnStartResult), revisionBefore);
|
|
1780
|
+
scheduleSnapshot(threadId);
|
|
1781
|
+
const currentQueue = queuedFollowUpsByThreadId.get(threadId);
|
|
1782
|
+
const sentIndex = currentQueue?.findIndex((candidate) => candidate === entry
|
|
1783
|
+
|| (readString(entry.id) && candidate.id === entry.id)) ?? -1;
|
|
1784
|
+
if (sentIndex >= 0) currentQueue.splice(sentIndex, 1);
|
|
1785
|
+
if (currentQueue?.length === 0) queuedFollowUpsByThreadId.delete(threadId);
|
|
1786
|
+
broadcastQueuedFollowUps(threadId);
|
|
1787
|
+
})
|
|
1764
1788
|
.catch((error) => {
|
|
1765
1789
|
console.warn(`${logPrefix} desktop queued follow-up failed for ${threadId}: ${error?.message || "unknown error"}`);
|
|
1766
1790
|
})
|
|
1767
1791
|
.finally(() => {
|
|
1768
1792
|
runningQueuedFollowUpThreadIds.delete(threadId);
|
|
1793
|
+
if (retryChangedQueue) runNextQueuedFollowUp(threadId);
|
|
1769
1794
|
});
|
|
1770
1795
|
}
|
|
1771
1796
|
|
|
@@ -1773,11 +1798,7 @@ function createDesktopIpcLiveOwner({
|
|
|
1773
1798
|
// the request itself does not specify them. Phone-origin turns are untouched.
|
|
1774
1799
|
function mergeFollowerRuntimeOverrides(conversationId, params) {
|
|
1775
1800
|
const persisted = runtimeSettingsStore?.get?.(conversationId) || null;
|
|
1776
|
-
const persistedOverrides = persisted ?
|
|
1777
|
-
model: persisted.model,
|
|
1778
|
-
effort: persisted.reasoningEffort,
|
|
1779
|
-
serviceTier: persisted.serviceTier,
|
|
1780
|
-
} : null;
|
|
1801
|
+
const persistedOverrides = persisted ? threadSettingsFromRuntimeSettings(persisted) : null;
|
|
1781
1802
|
const liveOverrides = followerRuntimeOverridesByThreadId.get(conversationId) || null;
|
|
1782
1803
|
const overrides = persistedOverrides || liveOverrides
|
|
1783
1804
|
? { ...(persistedOverrides || {}), ...(liveOverrides || {}) }
|
|
@@ -1789,11 +1810,11 @@ function createDesktopIpcLiveOwner({
|
|
|
1789
1810
|
if (overrides.model && !readString(merged.model)) {
|
|
1790
1811
|
merged.model = overrides.model;
|
|
1791
1812
|
}
|
|
1792
|
-
if (overrides
|
|
1813
|
+
if (hasOwn(overrides, "effort") && !hasOwn(merged, "effort")) {
|
|
1793
1814
|
merged.effort = overrides.effort;
|
|
1794
1815
|
}
|
|
1795
|
-
if (overrides
|
|
1796
|
-
merged.serviceTier = overrides.serviceTier;
|
|
1816
|
+
if (hasOwn(overrides, "serviceTier") && !hasOwn(merged, "serviceTier")) {
|
|
1817
|
+
merged.serviceTier = overrides.serviceTier || "default";
|
|
1797
1818
|
}
|
|
1798
1819
|
if (overrides.collaborationMode && merged.collaborationMode == null) {
|
|
1799
1820
|
merged.collaborationMode = cloneJSON(overrides.collaborationMode);
|
|
@@ -1801,11 +1822,14 @@ function createDesktopIpcLiveOwner({
|
|
|
1801
1822
|
return merged;
|
|
1802
1823
|
}
|
|
1803
1824
|
|
|
1804
|
-
function commitAcceptedRuntimeSettings(threadId, params, source, turnId) {
|
|
1825
|
+
function commitAcceptedRuntimeSettings(threadId, params, source, turnId, revisionBefore) {
|
|
1805
1826
|
try {
|
|
1806
|
-
const
|
|
1827
|
+
const current = runtimeSettingsStore?.get?.(threadId);
|
|
1828
|
+
const settings = current && revisionBefore != null && current.revision !== revisionBefore
|
|
1829
|
+
? current : runtimeSettingsStore?.commit?.(threadId, params, { source, turnId });
|
|
1807
1830
|
const conversation = conversations.get(threadId);
|
|
1808
1831
|
if (settings && conversation) {
|
|
1832
|
+
applyRuntimeSettingsToConversation(conversation, threadSettingsFromRuntimeSettings(settings), { authoritative: true });
|
|
1809
1833
|
runtimeSettingsStore.attachToConversation(threadId, conversation);
|
|
1810
1834
|
}
|
|
1811
1835
|
return settings;
|
|
@@ -1849,6 +1873,7 @@ function createDesktopIpcLiveOwner({
|
|
|
1849
1873
|
}
|
|
1850
1874
|
|
|
1851
1875
|
return {
|
|
1876
|
+
updateThreadSettings: (threadId, params) => applyFollowerThreadSettings(threadId, params, "phone"),
|
|
1852
1877
|
observeInbound,
|
|
1853
1878
|
observeOutbound,
|
|
1854
1879
|
stopAll,
|
|
@@ -26,7 +26,8 @@ const DESKTOP_IPC_METHOD_VERSIONS = new Map([
|
|
|
26
26
|
["thread-stream-following-status-requested", 1],
|
|
27
27
|
["thread-archived", 2],
|
|
28
28
|
["thread-unarchived", 1],
|
|
29
|
-
["thread-
|
|
29
|
+
["thread-owner-discovery", 1],
|
|
30
|
+
["thread-read-state-changed", 3],
|
|
30
31
|
["thread-queued-followups-changed", 1],
|
|
31
32
|
["thread-follower-start-turn", 2],
|
|
32
33
|
["thread-follower-load-complete-history", 1],
|
|
@@ -582,6 +583,38 @@ function buildCompleteThreadReadParams(threadId) {
|
|
|
582
583
|
};
|
|
583
584
|
}
|
|
584
585
|
|
|
586
|
+
// Desktop 26.903 scopes read receipts to the authenticated identity and runtime.
|
|
587
|
+
// Only the local stdio runtime is owned by this bridge; never broadcast tokens.
|
|
588
|
+
function buildThreadReadStateContext(authStatus, hostId = "local") {
|
|
589
|
+
if (!authStatus || hostId !== "local") {
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
592
|
+
let identity;
|
|
593
|
+
if (authStatus.authMethod === "chatgpt" || authStatus.authMethod === "chatgptAuthTokens") {
|
|
594
|
+
try {
|
|
595
|
+
const payload = JSON.parse(Buffer.from(
|
|
596
|
+
readString(authStatus.authToken).split(".")[1] || "", "base64url"
|
|
597
|
+
).toString("utf8"));
|
|
598
|
+
const auth = payload["https://api.openai.com/auth"];
|
|
599
|
+
const accountId = readString(auth?.chatgpt_account_id ?? auth?.account_id);
|
|
600
|
+
const userId = readString(auth?.user_id ?? auth?.chatgpt_user_id);
|
|
601
|
+
if (!accountId || !userId) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
identity = { kind: "chatgpt", accountId, userId };
|
|
605
|
+
} catch {
|
|
606
|
+
return null;
|
|
607
|
+
}
|
|
608
|
+
} else {
|
|
609
|
+
if (authStatus.authMethod == null && authStatus.requiresOpenaiAuth !== false) {
|
|
610
|
+
return null;
|
|
611
|
+
}
|
|
612
|
+
identity = { kind: "execution-storage", authMode: authStatus.authMethod ?? "none" };
|
|
613
|
+
}
|
|
614
|
+
const hostHash = createHash("sha256").update(JSON.stringify(["local", hostId, null])).digest("hex");
|
|
615
|
+
return { identity, executionHostKey: `${hostId}:${hostHash}` };
|
|
616
|
+
}
|
|
617
|
+
|
|
585
618
|
function resolveIpcSocketPathCandidates() {
|
|
586
619
|
if (process.platform === "win32") {
|
|
587
620
|
return ["\\\\.\\pipe\\codex-ipc"];
|
|
@@ -666,6 +699,7 @@ function responseItemMessageText(payload) {
|
|
|
666
699
|
}
|
|
667
700
|
|
|
668
701
|
module.exports = {
|
|
702
|
+
buildThreadReadStateContext,
|
|
669
703
|
CLIENT_STATUS_CHANGED,
|
|
670
704
|
buildCompleteThreadReadParams,
|
|
671
705
|
buildIpcRequestEnvelope,
|