@makerbi/remodex 3.1.0 → 3.3.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 +295 -104
- package/src/desktop-ipc-conversation-adapter.js +250 -45
- package/src/desktop-ipc-conversation-projector.js +1 -0
- package/src/desktop-ipc-live-owner.js +269 -178
- package/src/desktop-ipc-shared.js +37 -3
- 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,
|
|
@@ -27,6 +35,7 @@ const {
|
|
|
27
35
|
buildConversationStateFromThread,
|
|
28
36
|
createEmptyConversationState,
|
|
29
37
|
readThreadIdFromParams,
|
|
38
|
+
synchronizeDesktopConversationCompatibility,
|
|
30
39
|
} = require("./desktop-ipc-conversation-adapter");
|
|
31
40
|
const {
|
|
32
41
|
DEFAULT_MAX_PATCH_BYTES,
|
|
@@ -65,6 +74,7 @@ const THREAD_QUEUED_FOLLOWUPS_CHANGED = "thread-queued-followups-changed";
|
|
|
65
74
|
const REMODEX_LIVE_OWNER_SOURCE = "desktop-ipc-live-owner";
|
|
66
75
|
|
|
67
76
|
const SUPPORTED_FOLLOWER_REQUEST_METHODS = new Set([
|
|
77
|
+
"thread-owner-discovery",
|
|
68
78
|
"thread-follower-start-turn",
|
|
69
79
|
"thread-follower-load-complete-history",
|
|
70
80
|
"thread-follower-update-thread-settings",
|
|
@@ -94,20 +104,33 @@ const OWNER_INBOUND_METHODS = new Set([
|
|
|
94
104
|
|
|
95
105
|
const THREAD_READ_METHODS = new Set(["thread/read", "thread/resume"]);
|
|
96
106
|
|
|
97
|
-
|
|
107
|
+
// Current app-server v2 TurnStartParams fields. Desktop's adjacent turnStart.context
|
|
108
|
+
// is presentation state, not part of this RPC shape, so it stays outside the request.
|
|
109
|
+
const APP_SERVER_TURN_START_PARAM_KEYS = new Set([
|
|
98
110
|
"threadId",
|
|
99
111
|
"input",
|
|
112
|
+
"additionalContext",
|
|
100
113
|
"cwd",
|
|
101
114
|
"approvalPolicy",
|
|
102
115
|
"approvalsReviewer",
|
|
103
116
|
"sandboxPolicy",
|
|
104
117
|
"model",
|
|
105
118
|
"serviceTier",
|
|
119
|
+
"serviceTierForTurn",
|
|
106
120
|
"effort",
|
|
107
121
|
"summary",
|
|
108
122
|
"personality",
|
|
109
123
|
"outputSchema",
|
|
110
124
|
"collaborationMode",
|
|
125
|
+
"clientUserMessageId",
|
|
126
|
+
"cyberAccessProgram",
|
|
127
|
+
"environments",
|
|
128
|
+
"multiAgentMode",
|
|
129
|
+
"permissions",
|
|
130
|
+
"responsesapiClientMetadata",
|
|
131
|
+
"runtimeWorkspaceRoots",
|
|
132
|
+
"toolOutput",
|
|
133
|
+
"turnTrigger",
|
|
111
134
|
]);
|
|
112
135
|
|
|
113
136
|
function createDesktopIpcLiveOwner({
|
|
@@ -127,6 +150,7 @@ function createDesktopIpcLiveOwner({
|
|
|
127
150
|
maxPatchBytes = DEFAULT_MAX_PATCH_BYTES,
|
|
128
151
|
requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
129
152
|
reconnectMs = DEFAULT_RECONNECT_MS,
|
|
153
|
+
startRouterWhenMissing = false,
|
|
130
154
|
initialHistoryRetryMs = DEFAULT_INITIAL_HISTORY_RETRY_MS,
|
|
131
155
|
initialHistoryMaxAttempts = DEFAULT_INITIAL_HISTORY_MAX_ATTEMPTS,
|
|
132
156
|
liveOwnershipFreshnessMs = DEFAULT_LIVE_OWNERSHIP_FRESHNESS_MS,
|
|
@@ -170,6 +194,7 @@ function createDesktopIpcLiveOwner({
|
|
|
170
194
|
// until the first user item and turn completion prove the rollout was written.
|
|
171
195
|
const pendingSidebarMaterializationThreadIds = new Set();
|
|
172
196
|
const replayedSidebarMaterializationThreadIds = new Set();
|
|
197
|
+
const enqueueMutation = createThreadMutationQueue();
|
|
173
198
|
const pendingTurnStartParamsByThreadId = new Map();
|
|
174
199
|
const pendingTurnStartEntriesByRequestId = new Map();
|
|
175
200
|
const followerRuntimeOverridesByThreadId = new Map();
|
|
@@ -178,6 +203,7 @@ function createDesktopIpcLiveOwner({
|
|
|
178
203
|
const queuedFollowUpsByThreadId = new Map();
|
|
179
204
|
const runningQueuedFollowUpThreadIds = new Set();
|
|
180
205
|
const announcedReadStateThreadIds = new Set();
|
|
206
|
+
const pendingReadStateByThreadId = new Map();
|
|
181
207
|
const pendingThreadArchiveMetadataByThreadId = new Map();
|
|
182
208
|
const dirtyThreadIds = new Set();
|
|
183
209
|
let snapshotTimer = null;
|
|
@@ -190,6 +216,10 @@ function createDesktopIpcLiveOwner({
|
|
|
190
216
|
requestTimeoutMs,
|
|
191
217
|
reconnectMs,
|
|
192
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,
|
|
193
223
|
onConnected() {
|
|
194
224
|
flushPendingThreadArchiveMetadataBroadcasts();
|
|
195
225
|
requestFollowerStatusForAllOwnedThreads();
|
|
@@ -288,7 +318,24 @@ function createDesktopIpcLiveOwner({
|
|
|
288
318
|
if (!message || typeof message !== "object") {
|
|
289
319
|
return;
|
|
290
320
|
}
|
|
291
|
-
|
|
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
|
+
}
|
|
292
339
|
const responseId = message.id == null ? "" : String(message.id);
|
|
293
340
|
if (responseId && !message.method) {
|
|
294
341
|
resolvePendingTurnStartResponse(responseId, message);
|
|
@@ -337,6 +384,11 @@ function createDesktopIpcLiveOwner({
|
|
|
337
384
|
refreshOptimisticFallbackForThread(update.threadId);
|
|
338
385
|
scheduleSnapshot(update.threadId);
|
|
339
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
|
+
}
|
|
340
392
|
}
|
|
341
393
|
|
|
342
394
|
if (readString(message.method) === "turn/completed") {
|
|
@@ -390,6 +442,7 @@ function createDesktopIpcLiveOwner({
|
|
|
390
442
|
queuedFollowUpsByThreadId.clear();
|
|
391
443
|
runningQueuedFollowUpThreadIds.clear();
|
|
392
444
|
announcedReadStateThreadIds.clear();
|
|
445
|
+
pendingReadStateByThreadId.clear();
|
|
393
446
|
ownedThreadIds.clear();
|
|
394
447
|
conversations.clear();
|
|
395
448
|
ipc.close();
|
|
@@ -409,16 +462,44 @@ function createDesktopIpcLiveOwner({
|
|
|
409
462
|
if (hadUnread) {
|
|
410
463
|
conversation.hasUnreadTurn = false;
|
|
411
464
|
conversation.unreadMessageCount = 0;
|
|
465
|
+
announcedReadStateThreadIds.delete(normalizedThreadId);
|
|
412
466
|
scheduleSnapshot(normalizedThreadId);
|
|
413
467
|
} else if (announcedReadStateThreadIds.has(normalizedThreadId)) {
|
|
414
468
|
return;
|
|
415
469
|
}
|
|
416
|
-
if (
|
|
417
|
-
|
|
418
|
-
hasUnreadTurn: false,
|
|
419
|
-
})) {
|
|
420
|
-
announcedReadStateThreadIds.add(normalizedThreadId);
|
|
470
|
+
if (pendingReadStateByThreadId.has(normalizedThreadId)) {
|
|
471
|
+
return;
|
|
421
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
|
+
});
|
|
422
503
|
}
|
|
423
504
|
|
|
424
505
|
// Snappier Stop UX on Desktop: flip the active turn to interrupted right away;
|
|
@@ -465,7 +546,7 @@ function createDesktopIpcLiveOwner({
|
|
|
465
546
|
}
|
|
466
547
|
const sanitizedParams = sanitizeTurnStartParams(cloneJSON(params));
|
|
467
548
|
sanitizedParams.input = normalizeInputEntriesForDesktop(sanitizedParams.input);
|
|
468
|
-
const entry = { params: sanitizedParams, requestId: normalizedRequestId || null };
|
|
549
|
+
const entry = { params: sanitizedParams, requestId: normalizedRequestId || null, runtimeSettingsRevision: runtimeSettingsStore?.get?.(normalizedThreadId)?.revision ?? 0 };
|
|
469
550
|
const queue = pendingTurnStartParamsByThreadId.get(normalizedThreadId) || [];
|
|
470
551
|
queue.push(entry);
|
|
471
552
|
pendingTurnStartParamsByThreadId.set(normalizedThreadId, queue);
|
|
@@ -525,7 +606,8 @@ function createDesktopIpcLiveOwner({
|
|
|
525
606
|
pending.threadId,
|
|
526
607
|
pending.entry?.params,
|
|
527
608
|
"phone",
|
|
528
|
-
readTurnIdFromResult(message.result)
|
|
609
|
+
readTurnIdFromResult(message.result),
|
|
610
|
+
pending.entry?.runtimeSettingsRevision
|
|
529
611
|
);
|
|
530
612
|
scheduleSnapshot(pending.threadId);
|
|
531
613
|
}
|
|
@@ -697,6 +779,7 @@ function createDesktopIpcLiveOwner({
|
|
|
697
779
|
}
|
|
698
780
|
runningQueuedFollowUpThreadIds.delete(normalizedThreadId);
|
|
699
781
|
announcedReadStateThreadIds.delete(normalizedThreadId);
|
|
782
|
+
pendingReadStateByThreadId.delete(normalizedThreadId);
|
|
700
783
|
cancelSidebarAnnouncement(normalizedThreadId);
|
|
701
784
|
announcedSidebarThreadIds.delete(normalizedThreadId);
|
|
702
785
|
pendingSidebarMaterializationThreadIds.delete(normalizedThreadId);
|
|
@@ -1104,6 +1187,7 @@ function createDesktopIpcLiveOwner({
|
|
|
1104
1187
|
return true;
|
|
1105
1188
|
}
|
|
1106
1189
|
runtimeSettingsStore?.attachToConversation?.(threadId, conversationState);
|
|
1190
|
+
synchronizeDesktopConversationCompatibility(conversationState);
|
|
1107
1191
|
if (shouldDelayInitialSnapshotForHistory(threadId)) {
|
|
1108
1192
|
return false;
|
|
1109
1193
|
}
|
|
@@ -1284,6 +1368,13 @@ function createDesktopIpcLiveOwner({
|
|
|
1284
1368
|
if (!SUPPORTED_FOLLOWER_REQUEST_METHODS.has(method)) {
|
|
1285
1369
|
return false;
|
|
1286
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
|
+
}
|
|
1287
1378
|
const threadId = readConversationIdFromFollowerParams(params);
|
|
1288
1379
|
return Boolean(threadId && ownedThreadIds.has(threadId));
|
|
1289
1380
|
}
|
|
@@ -1292,11 +1383,14 @@ function createDesktopIpcLiveOwner({
|
|
|
1292
1383
|
const method = readString(envelope?.method);
|
|
1293
1384
|
const params = envelope?.params && typeof envelope.params === "object" ? envelope.params : {};
|
|
1294
1385
|
const conversationId = readConversationIdFromFollowerParams(params);
|
|
1295
|
-
if (!conversationId || !
|
|
1386
|
+
if (!conversationId || !canHandleFollowerRequest(envelope)) {
|
|
1296
1387
|
throw new Error("conversation-not-owned");
|
|
1297
1388
|
}
|
|
1298
1389
|
|
|
1299
1390
|
switch (method) {
|
|
1391
|
+
case "thread-owner-discovery":
|
|
1392
|
+
// Remodex cannot yet inject Desktop's untrusted app response items.
|
|
1393
|
+
return { supportsUntrustedAppInput: false };
|
|
1300
1394
|
case "thread-follower-start-turn":
|
|
1301
1395
|
return await handleFollowerStartTurn(conversationId, params);
|
|
1302
1396
|
case "thread-follower-load-complete-history":
|
|
@@ -1366,11 +1460,16 @@ function createDesktopIpcLiveOwner({
|
|
|
1366
1460
|
return { revision };
|
|
1367
1461
|
}
|
|
1368
1462
|
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
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;
|
|
1472
|
+
const rawTurnStartParams = readFollowerTurnStartParams(params);
|
|
1374
1473
|
const codexParams = mergeFollowerRuntimeOverrides(conversationId, sanitizeTurnStartParams({
|
|
1375
1474
|
...rawTurnStartParams,
|
|
1376
1475
|
threadId: conversationId,
|
|
@@ -1380,7 +1479,9 @@ function createDesktopIpcLiveOwner({
|
|
|
1380
1479
|
? normalizedParams
|
|
1381
1480
|
: codexParams;
|
|
1382
1481
|
markOwnedThread(conversationId);
|
|
1383
|
-
const senderRequestId = params.senderRequestId
|
|
1482
|
+
const senderRequestId = params.senderRequestId
|
|
1483
|
+
|| params.sender_request_id
|
|
1484
|
+
|| rawTurnStartParams.clientUserMessageId;
|
|
1384
1485
|
const isKnownHeldPhoneStart = Boolean(
|
|
1385
1486
|
requestIdKey(senderRequestId)
|
|
1386
1487
|
&& pendingTurnStartEntriesByRequestId.has(requestIdKey(senderRequestId))
|
|
@@ -1400,7 +1501,8 @@ function createDesktopIpcLiveOwner({
|
|
|
1400
1501
|
conversationId,
|
|
1401
1502
|
nextCodexParams,
|
|
1402
1503
|
isKnownHeldPhoneStart ? "phone" : "desktop",
|
|
1403
|
-
readTurnIdFromResult(turnStartResult)
|
|
1504
|
+
readTurnIdFromResult(turnStartResult),
|
|
1505
|
+
revisionBefore
|
|
1404
1506
|
);
|
|
1405
1507
|
scheduleSnapshot(conversationId);
|
|
1406
1508
|
if (!isKnownHeldPhoneStart) {
|
|
@@ -1417,6 +1519,17 @@ function createDesktopIpcLiveOwner({
|
|
|
1417
1519
|
}
|
|
1418
1520
|
}
|
|
1419
1521
|
|
|
1522
|
+
function readFollowerTurnStartParams(params) {
|
|
1523
|
+
const turnStart = isPlainJSONObject(params.turnStart) ? params.turnStart : null;
|
|
1524
|
+
if (isPlainJSONObject(turnStart?.request)) {
|
|
1525
|
+
return turnStart.request;
|
|
1526
|
+
}
|
|
1527
|
+
return params.turnStartParams
|
|
1528
|
+
|| params.turn_start_params
|
|
1529
|
+
|| params.turnStart
|
|
1530
|
+
|| params;
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1420
1533
|
function mirrorFollowerUserPromptToPhone(threadId, turnStartParams, turnStartResult = null) {
|
|
1421
1534
|
const text = visibleUserPromptFromInputEntries(turnStartParams?.input);
|
|
1422
1535
|
if (!text) {
|
|
@@ -1453,24 +1566,60 @@ function createDesktopIpcLiveOwner({
|
|
|
1453
1566
|
if (!expectedTurnId) {
|
|
1454
1567
|
throw new Error("Missing expectedTurnId for follower steer request.");
|
|
1455
1568
|
}
|
|
1456
|
-
|
|
1569
|
+
const result = await sendCodexRequest("turn/steer", {
|
|
1457
1570
|
threadId: conversationId,
|
|
1458
1571
|
input: Array.isArray(rawSteerParams.input) ? rawSteerParams.input : [],
|
|
1459
1572
|
expectedTurnId,
|
|
1573
|
+
...Object.fromEntries(["clientUserMessageId", "additionalContext", "toolOutput"].filter((key) => hasOwn(rawSteerParams, key)).map((key) => [key, cloneJSON(rawSteerParams[key])])),
|
|
1460
1574
|
});
|
|
1575
|
+
return { result };
|
|
1461
1576
|
}
|
|
1462
1577
|
|
|
1463
1578
|
async function handleFollowerInterruptTurn(conversationId, params) {
|
|
1464
|
-
const
|
|
1465
|
-
|| readString(params.
|
|
1466
|
-
||
|
|
1579
|
+
const requestedTurnId = readString(params.expectedTurnId)
|
|
1580
|
+
|| readString(params.expected_turn_id)
|
|
1581
|
+
|| readString(params.turnId)
|
|
1582
|
+
|| readString(params.turn_id);
|
|
1583
|
+
const activeTurnId = activeTurnIdForConversation(conversationId);
|
|
1584
|
+
if (requestedTurnId && requestedTurnId !== activeTurnId) {
|
|
1585
|
+
return {
|
|
1586
|
+
interruptedTurnId: null,
|
|
1587
|
+
ok: true,
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
const goalPauseError = await pauseActiveGoalForUserStop(
|
|
1592
|
+
conversationId,
|
|
1593
|
+
params.mode,
|
|
1594
|
+
Boolean(requestedTurnId)
|
|
1595
|
+
);
|
|
1596
|
+
const turnId = requestedTurnId || activeTurnId;
|
|
1467
1597
|
if (!turnId) {
|
|
1468
|
-
|
|
1598
|
+
return followerInterruptResult(null, goalPauseError);
|
|
1469
1599
|
}
|
|
1470
|
-
|
|
1600
|
+
await sendCodexRequest("turn/interrupt", {
|
|
1471
1601
|
threadId: conversationId,
|
|
1472
1602
|
turnId,
|
|
1473
1603
|
});
|
|
1604
|
+
return followerInterruptResult(turnId, goalPauseError);
|
|
1605
|
+
}
|
|
1606
|
+
|
|
1607
|
+
async function pauseActiveGoalForUserStop(conversationId, mode, hasRequestedTurnId) {
|
|
1608
|
+
if (mode !== "user-stop" || hasRequestedTurnId) {
|
|
1609
|
+
return "";
|
|
1610
|
+
}
|
|
1611
|
+
if (conversations.get(conversationId)?.threadGoal?.status !== "active") {
|
|
1612
|
+
return "";
|
|
1613
|
+
}
|
|
1614
|
+
try {
|
|
1615
|
+
await sendCodexRequest("thread/goal/set", {
|
|
1616
|
+
threadId: conversationId,
|
|
1617
|
+
status: "paused",
|
|
1618
|
+
});
|
|
1619
|
+
return "";
|
|
1620
|
+
} catch (error) {
|
|
1621
|
+
return error?.message || "Failed to pause thread goal.";
|
|
1622
|
+
}
|
|
1474
1623
|
}
|
|
1475
1624
|
|
|
1476
1625
|
// Desktop follower approvals only carry decision-style payloads, but app-server
|
|
@@ -1510,100 +1659,49 @@ function createDesktopIpcLiveOwner({
|
|
|
1510
1659
|
return { ok: true };
|
|
1511
1660
|
}
|
|
1512
1661
|
|
|
1513
|
-
//
|
|
1514
|
-
// merged into later Desktop-origin turn starts, so acknowledging them is honest
|
|
1515
|
-
// instead of a cosmetic broadcast-only update.
|
|
1662
|
+
// Legacy IPC verbs enter the same acknowledged settings path.
|
|
1516
1663
|
function applyFollowerModelAndReasoning(conversationId, params) {
|
|
1517
|
-
const
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
overrides.model = readString(params.model);
|
|
1521
|
-
if (conversation) {
|
|
1522
|
-
conversation.latestModel = overrides.model;
|
|
1523
|
-
}
|
|
1524
|
-
}
|
|
1525
|
-
if (Object.prototype.hasOwnProperty.call(params, "reasoningEffort")) {
|
|
1526
|
-
overrides.effort = params.reasoningEffort || null;
|
|
1527
|
-
if (conversation) {
|
|
1528
|
-
conversation.latestReasoningEffort = overrides.effort;
|
|
1529
|
-
}
|
|
1530
|
-
}
|
|
1531
|
-
if (Object.prototype.hasOwnProperty.call(params, "serviceTier")) {
|
|
1532
|
-
overrides.serviceTier = readString(params.serviceTier) || null;
|
|
1533
|
-
if (conversation) {
|
|
1534
|
-
conversation.latestServiceTier = overrides.serviceTier;
|
|
1535
|
-
}
|
|
1536
|
-
}
|
|
1537
|
-
followerRuntimeOverridesByThreadId.set(conversationId, overrides);
|
|
1538
|
-
if (conversation) {
|
|
1539
|
-
scheduleSnapshot(conversationId);
|
|
1540
|
-
}
|
|
1541
|
-
return { ok: true };
|
|
1664
|
+
const settings = normalizeThreadSettingsUpdate(params);
|
|
1665
|
+
if (hasOwn(params, "reasoningEffort")) settings.effort = params.reasoningEffort;
|
|
1666
|
+
return applyFollowerThreadSettings(conversationId, settings);
|
|
1542
1667
|
}
|
|
1543
1668
|
|
|
1544
1669
|
function applyFollowerCollaborationMode(conversationId, params) {
|
|
1545
|
-
|
|
1546
|
-
return { ok: true };
|
|
1547
|
-
}
|
|
1548
|
-
const overrides = followerRuntimeOverridesByThreadId.get(conversationId) || {};
|
|
1549
|
-
overrides.collaborationMode = cloneJSON(params.collaborationMode);
|
|
1550
|
-
followerRuntimeOverridesByThreadId.set(conversationId, overrides);
|
|
1551
|
-
const conversation = conversations.get(conversationId);
|
|
1552
|
-
if (conversation) {
|
|
1553
|
-
conversation.latestCollaborationMode = cloneJSON(params.collaborationMode);
|
|
1554
|
-
scheduleSnapshot(conversationId);
|
|
1555
|
-
}
|
|
1556
|
-
return { ok: true };
|
|
1670
|
+
return applyFollowerThreadSettings(conversationId, { collaborationMode: params.collaborationMode });
|
|
1557
1671
|
}
|
|
1558
1672
|
|
|
1559
|
-
//
|
|
1560
|
-
//
|
|
1561
|
-
function applyFollowerThreadSettings(conversationId, threadSettings) {
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
const overrides = followerRuntimeOverridesByThreadId.get(conversationId) || {};
|
|
1566
|
-
const model = readString(threadSettings.model)
|
|
1567
|
-
|| readString(threadSettings.collaborationMode?.settings?.model);
|
|
1568
|
-
const effort = threadSettings.effort;
|
|
1569
|
-
const hasServiceTier = Object.prototype.hasOwnProperty.call(threadSettings, "serviceTier");
|
|
1570
|
-
if (model) {
|
|
1571
|
-
overrides.model = model;
|
|
1572
|
-
}
|
|
1573
|
-
if (effort !== undefined) {
|
|
1574
|
-
overrides.effort = effort ?? null;
|
|
1575
|
-
}
|
|
1576
|
-
if (hasServiceTier) {
|
|
1577
|
-
overrides.serviceTier = readString(threadSettings.serviceTier) || null;
|
|
1578
|
-
}
|
|
1579
|
-
if (threadSettings.collaborationMode && typeof threadSettings.collaborationMode === "object") {
|
|
1580
|
-
overrides.collaborationMode = cloneJSON(threadSettings.collaborationMode);
|
|
1581
|
-
}
|
|
1582
|
-
followerRuntimeOverridesByThreadId.set(conversationId, overrides);
|
|
1583
|
-
|
|
1584
|
-
const conversation = conversations.get(conversationId);
|
|
1585
|
-
if (conversation) {
|
|
1586
|
-
conversation.latestThreadSettings = {
|
|
1587
|
-
...(conversation.latestThreadSettings && typeof conversation.latestThreadSettings === "object"
|
|
1588
|
-
? conversation.latestThreadSettings
|
|
1589
|
-
: {}),
|
|
1590
|
-
...cloneJSON(threadSettings),
|
|
1591
|
-
};
|
|
1592
|
-
if (model) {
|
|
1593
|
-
conversation.latestModel = model;
|
|
1594
|
-
}
|
|
1595
|
-
if (effort !== undefined) {
|
|
1596
|
-
conversation.latestReasoningEffort = effort ?? null;
|
|
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.");
|
|
1597
1679
|
}
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
}
|
|
1601
|
-
if (
|
|
1602
|
-
|
|
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);
|
|
1603
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 });
|
|
1604
1702
|
scheduleSnapshot(conversationId);
|
|
1605
|
-
|
|
1606
|
-
|
|
1703
|
+
return { ok: true, runtimeSettings: confirmed || null };
|
|
1704
|
+
});
|
|
1607
1705
|
}
|
|
1608
1706
|
|
|
1609
1707
|
// Desktop followers hand the owner the full queue map and expect it to run
|
|
@@ -1641,72 +1739,58 @@ function createDesktopIpcLiveOwner({
|
|
|
1641
1739
|
if (!Array.isArray(queue) || queue.length === 0 || runningQueuedFollowUpThreadIds.has(threadId)) {
|
|
1642
1740
|
return;
|
|
1643
1741
|
}
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
if (queue.length === 0) {
|
|
1658
|
-
queuedFollowUpsByThreadId.delete(threadId);
|
|
1659
|
-
}
|
|
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();
|
|
1660
1755
|
broadcastQueuedFollowUps(threadId);
|
|
1661
1756
|
return;
|
|
1662
1757
|
}
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
? normalized
|
|
1682
|
-
: startParams;
|
|
1683
|
-
queuedTurnParams = params;
|
|
1684
|
-
const pendingEntry = rememberPendingTurnStart(threadId, params);
|
|
1685
|
-
if (pendingEntry) {
|
|
1686
|
-
insertOptimisticPendingTurn(threadId, pendingEntry);
|
|
1687
|
-
scheduleSnapshot(threadId);
|
|
1688
|
-
}
|
|
1689
|
-
return sendCodexRequest("turn/start", params);
|
|
1690
|
-
})
|
|
1691
|
-
.then((turnStartResult) => {
|
|
1692
|
-
commitAcceptedRuntimeSettings(
|
|
1693
|
-
threadId,
|
|
1694
|
-
queuedTurnParams,
|
|
1695
|
-
"desktop",
|
|
1696
|
-
readTurnIdFromResult(turnStartResult)
|
|
1697
|
-
);
|
|
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);
|
|
1698
1776
|
scheduleSnapshot(threadId);
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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
|
+
})
|
|
1705
1788
|
.catch((error) => {
|
|
1706
1789
|
console.warn(`${logPrefix} desktop queued follow-up failed for ${threadId}: ${error?.message || "unknown error"}`);
|
|
1707
1790
|
})
|
|
1708
1791
|
.finally(() => {
|
|
1709
1792
|
runningQueuedFollowUpThreadIds.delete(threadId);
|
|
1793
|
+
if (retryChangedQueue) runNextQueuedFollowUp(threadId);
|
|
1710
1794
|
});
|
|
1711
1795
|
}
|
|
1712
1796
|
|
|
@@ -1714,11 +1798,7 @@ function createDesktopIpcLiveOwner({
|
|
|
1714
1798
|
// the request itself does not specify them. Phone-origin turns are untouched.
|
|
1715
1799
|
function mergeFollowerRuntimeOverrides(conversationId, params) {
|
|
1716
1800
|
const persisted = runtimeSettingsStore?.get?.(conversationId) || null;
|
|
1717
|
-
const persistedOverrides = persisted ?
|
|
1718
|
-
model: persisted.model,
|
|
1719
|
-
effort: persisted.reasoningEffort,
|
|
1720
|
-
serviceTier: persisted.serviceTier,
|
|
1721
|
-
} : null;
|
|
1801
|
+
const persistedOverrides = persisted ? threadSettingsFromRuntimeSettings(persisted) : null;
|
|
1722
1802
|
const liveOverrides = followerRuntimeOverridesByThreadId.get(conversationId) || null;
|
|
1723
1803
|
const overrides = persistedOverrides || liveOverrides
|
|
1724
1804
|
? { ...(persistedOverrides || {}), ...(liveOverrides || {}) }
|
|
@@ -1730,11 +1810,11 @@ function createDesktopIpcLiveOwner({
|
|
|
1730
1810
|
if (overrides.model && !readString(merged.model)) {
|
|
1731
1811
|
merged.model = overrides.model;
|
|
1732
1812
|
}
|
|
1733
|
-
if (overrides
|
|
1813
|
+
if (hasOwn(overrides, "effort") && !hasOwn(merged, "effort")) {
|
|
1734
1814
|
merged.effort = overrides.effort;
|
|
1735
1815
|
}
|
|
1736
|
-
if (overrides
|
|
1737
|
-
merged.serviceTier = overrides.serviceTier;
|
|
1816
|
+
if (hasOwn(overrides, "serviceTier") && !hasOwn(merged, "serviceTier")) {
|
|
1817
|
+
merged.serviceTier = overrides.serviceTier || "default";
|
|
1738
1818
|
}
|
|
1739
1819
|
if (overrides.collaborationMode && merged.collaborationMode == null) {
|
|
1740
1820
|
merged.collaborationMode = cloneJSON(overrides.collaborationMode);
|
|
@@ -1742,11 +1822,14 @@ function createDesktopIpcLiveOwner({
|
|
|
1742
1822
|
return merged;
|
|
1743
1823
|
}
|
|
1744
1824
|
|
|
1745
|
-
function commitAcceptedRuntimeSettings(threadId, params, source, turnId) {
|
|
1825
|
+
function commitAcceptedRuntimeSettings(threadId, params, source, turnId, revisionBefore) {
|
|
1746
1826
|
try {
|
|
1747
|
-
const
|
|
1827
|
+
const current = runtimeSettingsStore?.get?.(threadId);
|
|
1828
|
+
const settings = current && revisionBefore != null && current.revision !== revisionBefore
|
|
1829
|
+
? current : runtimeSettingsStore?.commit?.(threadId, params, { source, turnId });
|
|
1748
1830
|
const conversation = conversations.get(threadId);
|
|
1749
1831
|
if (settings && conversation) {
|
|
1832
|
+
applyRuntimeSettingsToConversation(conversation, threadSettingsFromRuntimeSettings(settings), { authoritative: true });
|
|
1750
1833
|
runtimeSettingsStore.attachToConversation(threadId, conversation);
|
|
1751
1834
|
}
|
|
1752
1835
|
return settings;
|
|
@@ -1786,11 +1869,11 @@ function createDesktopIpcLiveOwner({
|
|
|
1786
1869
|
return turnId;
|
|
1787
1870
|
}
|
|
1788
1871
|
}
|
|
1789
|
-
|
|
1790
|
-
return readString(latestTurn?.turnId) || readString(latestTurn?.id);
|
|
1872
|
+
return "";
|
|
1791
1873
|
}
|
|
1792
1874
|
|
|
1793
1875
|
return {
|
|
1876
|
+
updateThreadSettings: (threadId, params) => applyFollowerThreadSettings(threadId, params, "phone"),
|
|
1794
1877
|
observeInbound,
|
|
1795
1878
|
observeOutbound,
|
|
1796
1879
|
stopAll,
|
|
@@ -1881,7 +1964,7 @@ function normalizeInputEntriesForDesktop(input) {
|
|
|
1881
1964
|
function sanitizeTurnStartParams(params) {
|
|
1882
1965
|
const sanitized = {};
|
|
1883
1966
|
for (const [key, value] of Object.entries(params || {})) {
|
|
1884
|
-
if (
|
|
1967
|
+
if (APP_SERVER_TURN_START_PARAM_KEYS.has(key)) {
|
|
1885
1968
|
sanitized[key] = value;
|
|
1886
1969
|
}
|
|
1887
1970
|
}
|
|
@@ -1891,6 +1974,14 @@ function sanitizeTurnStartParams(params) {
|
|
|
1891
1974
|
return sanitized;
|
|
1892
1975
|
}
|
|
1893
1976
|
|
|
1977
|
+
function followerInterruptResult(interruptedTurnId, goalPauseError) {
|
|
1978
|
+
return {
|
|
1979
|
+
interruptedTurnId,
|
|
1980
|
+
...(goalPauseError ? { goalPauseError } : {}),
|
|
1981
|
+
ok: true,
|
|
1982
|
+
};
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1894
1985
|
function readThreadFromResponse(message) {
|
|
1895
1986
|
const result = message?.result || message?.payload || {};
|
|
1896
1987
|
return readThreadFromPayload(result);
|