@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
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
|
|
7
7
|
const { createHash } = require("crypto");
|
|
8
8
|
const net = require("net");
|
|
9
|
+
const { createThreadMutationQueue, runtimeSettingsPatch, hasOwn, normalizeThreadSettingsUpdate } = require("./codex-runtime-settings");
|
|
10
|
+
const { projectSemanticItem } = require("./thread-activity-projector");
|
|
9
11
|
|
|
10
12
|
const {
|
|
11
13
|
createDesktopConversationProjector,
|
|
@@ -73,6 +75,7 @@ const STALE_ACTIVE_READ_MAX_AGE_MS = 20_000;
|
|
|
73
75
|
const CONNECTED_IPC_ACTIVITY_LEASE_MS = 5 * 60_000;
|
|
74
76
|
const MAX_NORMALIZED_REVIEW_FINGERPRINTS_PER_THREAD = 128;
|
|
75
77
|
const DESKTOP_FOLLOWER_REQUEST_METHODS = new Set([
|
|
78
|
+
"thread/settings/update",
|
|
76
79
|
"turn/start",
|
|
77
80
|
"turn/steer",
|
|
78
81
|
"turn/interrupt",
|
|
@@ -83,7 +86,6 @@ const DESKTOP_FOLLOWER_REQUEST_METHODS = new Set([
|
|
|
83
86
|
// owner for the same persisted thread.
|
|
84
87
|
const DESKTOP_OWNER_UNSUPPORTED_MUTATION_ERRORS = new Map([
|
|
85
88
|
["review/start", "Start this review in Codex Desktop."],
|
|
86
|
-
["thread/settings/update", "Change these thread settings in Codex Desktop."],
|
|
87
89
|
["thread/approveGuardianDeniedAction", "Approve this retry in Codex Desktop."],
|
|
88
90
|
]);
|
|
89
91
|
const ACTION_METHODS = new Set([
|
|
@@ -97,7 +99,7 @@ const REPLY_METHOD_BY_ACTION_METHOD = new Map([
|
|
|
97
99
|
["item/commandExecution/requestApproval", "thread-follower-command-approval-decision"],
|
|
98
100
|
["item/fileChange/requestApproval", "thread-follower-file-approval-decision"],
|
|
99
101
|
["item/fileRead/requestApproval", "thread-follower-file-approval-decision"],
|
|
100
|
-
["item/permissions/requestApproval", "thread-follower-
|
|
102
|
+
["item/permissions/requestApproval", "thread-follower-permissions-request-approval-response"],
|
|
101
103
|
["item/tool/requestUserInput", "thread-follower-submit-user-input"],
|
|
102
104
|
]);
|
|
103
105
|
const APPROVAL_DECISIONS = new Set(["accept", "acceptForSession", "decline", "cancel"]);
|
|
@@ -242,9 +244,12 @@ function createDesktopIpcActionFollower({
|
|
|
242
244
|
clearTimeoutFn = clearTimeout,
|
|
243
245
|
onNormalizedHistoryIndexRebuilt = () => {},
|
|
244
246
|
onFollowerStateChanged = null,
|
|
247
|
+
onActivityObservation = null,
|
|
245
248
|
requestTimeoutMs = REQUEST_TIMEOUT_MS,
|
|
246
249
|
ownershipProbeTimeoutMs = OWNERSHIP_PROBE_TIMEOUT_MS,
|
|
247
250
|
} = {}) {
|
|
251
|
+
let desktopSourceGeneration = 0;
|
|
252
|
+
const enqueueMutation = createThreadMutationQueue();
|
|
248
253
|
const ipc = createDesktopIpcClient({
|
|
249
254
|
socketPath,
|
|
250
255
|
netModule,
|
|
@@ -253,6 +258,11 @@ function createDesktopIpcActionFollower({
|
|
|
253
258
|
logPrefix,
|
|
254
259
|
onEnvelope,
|
|
255
260
|
onConnected() {
|
|
261
|
+
desktopSourceGeneration += 1;
|
|
262
|
+
onActivityObservation?.({
|
|
263
|
+
type: "connected",
|
|
264
|
+
sourceGeneration: desktopSourceGeneration,
|
|
265
|
+
});
|
|
256
266
|
announceDesktopFollowForActiveThreads();
|
|
257
267
|
probeHeldFollowerRequests();
|
|
258
268
|
},
|
|
@@ -276,6 +286,8 @@ function createDesktopIpcActionFollower({
|
|
|
276
286
|
const pendingRoutesByRequestId = new Map();
|
|
277
287
|
const activeThreadIds = new Set();
|
|
278
288
|
const desktopFollowThreadIds = new Set();
|
|
289
|
+
const backgroundCatalogThreadIds = new Set();
|
|
290
|
+
let backgroundCatalogPageSize = 0;
|
|
279
291
|
const followerClientIdsByThreadId = new Map();
|
|
280
292
|
// Threads discovered from Litter snapshots before the phone reads them.
|
|
281
293
|
// Their raw state is retained for lifecycle detection, but their transcript
|
|
@@ -314,6 +326,7 @@ function createDesktopIpcActionFollower({
|
|
|
314
326
|
}
|
|
315
327
|
|
|
316
328
|
function unfollowDesktopThread(threadId) {
|
|
329
|
+
backgroundCatalogThreadIds.delete(threadId);
|
|
317
330
|
if (!desktopFollowThreadIds.delete(threadId)) {
|
|
318
331
|
return false;
|
|
319
332
|
}
|
|
@@ -330,6 +343,57 @@ function createDesktopIpcActionFollower({
|
|
|
330
343
|
}
|
|
331
344
|
}
|
|
332
345
|
|
|
346
|
+
// Current Desktop sends initial snapshots only to explicit followers. Warm a
|
|
347
|
+
// bounded recent catalog window without opening chats or claiming a writer.
|
|
348
|
+
// The existing background path forwards lifecycle only, never transcript items.
|
|
349
|
+
function observeThreadListResponse(result, { limit = null } = {}) {
|
|
350
|
+
const rows = result?.data || result?.items || result?.threads;
|
|
351
|
+
if (!Array.isArray(rows)) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
const candidates = new Set(rows
|
|
355
|
+
.map((thread) => readString(thread?.id))
|
|
356
|
+
.filter((threadId) => threadId && !isLocallyOwnedThread(threadId) && !liveOwnerThreadIds.has(threadId))
|
|
357
|
+
.slice(0, MAX_ACTIVE_THREAD_IDS));
|
|
358
|
+
// Foreground health probes request fewer rows than the sidebar. Keep those
|
|
359
|
+
// additive; only a request covering the established window may prune it.
|
|
360
|
+
const requestedPageSize = Number.isSafeInteger(limit) && limit > 0
|
|
361
|
+
? Math.min(limit, MAX_ACTIVE_THREAD_IDS)
|
|
362
|
+
: null;
|
|
363
|
+
// limit: 1 is the phone's health probe, including before its first sidebar
|
|
364
|
+
// request. It must never establish or replace the catalog window.
|
|
365
|
+
const isForegroundProbe = requestedPageSize === 1;
|
|
366
|
+
const replacesCatalog = !isForegroundProbe && (requestedPageSize === null
|
|
367
|
+
? !result.nextCursor && !result.hasMore
|
|
368
|
+
: requestedPageSize >= backgroundCatalogPageSize);
|
|
369
|
+
if (!isForegroundProbe) {
|
|
370
|
+
backgroundCatalogPageSize = Math.max(backgroundCatalogPageSize, requestedPageSize || 0);
|
|
371
|
+
}
|
|
372
|
+
if (replacesCatalog) {
|
|
373
|
+
for (const threadId of backgroundCatalogThreadIds) {
|
|
374
|
+
if (!candidates.has(threadId) && !announcedBackgroundTurnsByThreadId.has(threadId)) {
|
|
375
|
+
backgroundCatalogThreadIds.delete(threadId);
|
|
376
|
+
if (backgroundOnlyThreadIds.has(threadId) || !activeThreadIds.has(threadId)) {
|
|
377
|
+
unfollowDesktopThread(threadId);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
for (const threadId of candidates) {
|
|
383
|
+
if (backgroundCatalogThreadIds.has(threadId) || desktopFollowThreadIds.has(threadId)) {
|
|
384
|
+
continue;
|
|
385
|
+
}
|
|
386
|
+
if (backgroundCatalogThreadIds.size >= MAX_ACTIVE_THREAD_IDS) {
|
|
387
|
+
break;
|
|
388
|
+
}
|
|
389
|
+
backgroundCatalogThreadIds.add(threadId);
|
|
390
|
+
if (!activeThreadIds.has(threadId)) {
|
|
391
|
+
backgroundOnlyThreadIds.add(threadId);
|
|
392
|
+
}
|
|
393
|
+
followDesktopThread(threadId);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
333
397
|
function oldestEvictableActiveThreadId() {
|
|
334
398
|
for (const threadId of activeThreadIds) {
|
|
335
399
|
if (!hasPendingProjectedActions(threadId)
|
|
@@ -372,6 +436,7 @@ function createDesktopIpcActionFollower({
|
|
|
372
436
|
ownershipProbeDeadlinesByThreadId.delete(threadId);
|
|
373
437
|
pendingOwnershipProbeTokensByThreadId.delete(threadId);
|
|
374
438
|
desktopOwnedByProbeThreadIds.delete(threadId);
|
|
439
|
+
notifyActivityRemoval(threadId, "evicted");
|
|
375
440
|
}
|
|
376
441
|
const recoveringThreadIds = new Set();
|
|
377
442
|
const queuedChangesByThreadId = new Map();
|
|
@@ -551,6 +616,8 @@ function createDesktopIpcActionFollower({
|
|
|
551
616
|
unfollowDesktopThread(threadId);
|
|
552
617
|
}
|
|
553
618
|
desktopFollowThreadIds.clear();
|
|
619
|
+
backgroundCatalogThreadIds.clear();
|
|
620
|
+
backgroundCatalogPageSize = 0;
|
|
554
621
|
activeThreadIds.clear();
|
|
555
622
|
followerClientIdsByThreadId.clear();
|
|
556
623
|
backgroundOnlyThreadIds.clear();
|
|
@@ -709,6 +776,7 @@ function createDesktopIpcActionFollower({
|
|
|
709
776
|
}
|
|
710
777
|
rawStatesByThreadId.set(threadId, speculativeState);
|
|
711
778
|
rawStateUpdatedAtByThreadId.set(threadId, now());
|
|
779
|
+
notifyActivityState(threadId, speculativeState);
|
|
712
780
|
if (!backgroundOnlyThreadIds.has(threadId)) {
|
|
713
781
|
conversationProjector.seed(threadId, speculativeState);
|
|
714
782
|
}
|
|
@@ -760,6 +828,7 @@ function createDesktopIpcActionFollower({
|
|
|
760
828
|
releaseDesktopThreadState(threadId);
|
|
761
829
|
return false;
|
|
762
830
|
}
|
|
831
|
+
runtimeSettingsStore?.observeConversation?.(threadId, nextState);
|
|
763
832
|
runtimeSettingsStore?.attachToConversation?.(threadId, nextState);
|
|
764
833
|
if (isFullSnapshot) {
|
|
765
834
|
rebuildNormalizedLiveIndex(threadId, nextState);
|
|
@@ -769,6 +838,7 @@ function createDesktopIpcActionFollower({
|
|
|
769
838
|
const previousState = rawStatesByThreadId.get(threadId) || null;
|
|
770
839
|
rawStatesByThreadId.set(threadId, nextState);
|
|
771
840
|
rawStateUpdatedAtByThreadId.set(threadId, now());
|
|
841
|
+
notifyActivityState(threadId, nextState);
|
|
772
842
|
// A usable state arrived: recovery bookkeeping and pre-baseline queued
|
|
773
843
|
// patches are obsolete (snapshots replace state wholesale).
|
|
774
844
|
baselineRecoveryStateByThreadId.delete(threadId);
|
|
@@ -814,6 +884,10 @@ function createDesktopIpcActionFollower({
|
|
|
814
884
|
}
|
|
815
885
|
|
|
816
886
|
function onDisconnect() {
|
|
887
|
+
onActivityObservation?.({
|
|
888
|
+
type: "disconnected",
|
|
889
|
+
sourceGeneration: Math.max(1, desktopSourceGeneration),
|
|
890
|
+
});
|
|
817
891
|
// Patch baselines are connection-scoped (Desktop re-sends a snapshot after
|
|
818
892
|
// reconnect), but the projector cache is not: keeping it lets the reconnect
|
|
819
893
|
// snapshot diff against already-mirrored content instead of replaying it.
|
|
@@ -869,8 +943,15 @@ function createDesktopIpcActionFollower({
|
|
|
869
943
|
|
|
870
944
|
const followers = followerClientIdsByThreadId.get(threadId) || new Set();
|
|
871
945
|
if (params.following === true) {
|
|
946
|
+
// A Desktop renderer opening a chat is not phone transcript interest.
|
|
947
|
+
// Only an inbound phone read/resume may promote it out of background.
|
|
948
|
+
if (!activeThreadIds.has(threadId)) {
|
|
949
|
+
backgroundOnlyThreadIds.add(threadId);
|
|
950
|
+
}
|
|
872
951
|
rememberActiveThread(threadId);
|
|
873
|
-
|
|
952
|
+
if (!desktopFollowThreadIds.has(threadId)) {
|
|
953
|
+
followDesktopThread(threadId);
|
|
954
|
+
}
|
|
874
955
|
const wasUnfollowed = followers.size === 0;
|
|
875
956
|
followers.add(clientId);
|
|
876
957
|
followerClientIdsByThreadId.set(threadId, followers);
|
|
@@ -937,6 +1018,7 @@ function createDesktopIpcActionFollower({
|
|
|
937
1018
|
conversationProjector.remove(threadId);
|
|
938
1019
|
queuedChangesByThreadId.delete(threadId);
|
|
939
1020
|
baselineRecoveryStateByThreadId.delete(threadId);
|
|
1021
|
+
notifyActivityRemoval(threadId, "ownership-handoff");
|
|
940
1022
|
releaseHeldFollowerRequests(threadId, { toDesktop: false });
|
|
941
1023
|
}
|
|
942
1024
|
|
|
@@ -968,6 +1050,7 @@ function createDesktopIpcActionFollower({
|
|
|
968
1050
|
conversationProjector.remove(threadId);
|
|
969
1051
|
queuedChangesByThreadId.delete(threadId);
|
|
970
1052
|
baselineRecoveryStateByThreadId.delete(threadId);
|
|
1053
|
+
notifyActivityRemoval(threadId, "removed");
|
|
971
1054
|
rejectHeldFollowerRequests(threadId, "This thread is no longer available for Desktop routing.");
|
|
972
1055
|
}
|
|
973
1056
|
|
|
@@ -1377,6 +1460,33 @@ function createDesktopIpcActionFollower({
|
|
|
1377
1460
|
);
|
|
1378
1461
|
}
|
|
1379
1462
|
|
|
1463
|
+
function notifyActivityState(threadId, state) {
|
|
1464
|
+
if (typeof onActivityObservation !== "function") {
|
|
1465
|
+
return;
|
|
1466
|
+
}
|
|
1467
|
+
const index = normalizedLiveIndexesByThreadId.get(threadId);
|
|
1468
|
+
const turns = index
|
|
1469
|
+
? boundedIndexedDesktopLiveTurns(
|
|
1470
|
+
state, index, now(), projectedLiveActiveTurnIdsByThreadId.get(threadId) || new Set(), true
|
|
1471
|
+
)
|
|
1472
|
+
: (Array.isArray(state?.turns) ? state.turns.slice(-3) : []);
|
|
1473
|
+
onActivityObservation({
|
|
1474
|
+
type: "state",
|
|
1475
|
+
threadId,
|
|
1476
|
+
sourceGeneration: Math.max(1, desktopSourceGeneration),
|
|
1477
|
+
state: boundedDesktopActivityState({ ...state, turns }),
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
function notifyActivityRemoval(threadId, reason) {
|
|
1482
|
+
onActivityObservation?.({
|
|
1483
|
+
type: "removed",
|
|
1484
|
+
threadId,
|
|
1485
|
+
reason,
|
|
1486
|
+
sourceGeneration: Math.max(1, desktopSourceGeneration),
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1380
1490
|
function rememberDesktopLiveProjection(threadId, liveState) {
|
|
1381
1491
|
const activeTurns = activeDesktopTurnDescriptors(liveState);
|
|
1382
1492
|
const activeTurnIds = new Set(activeTurns.map((turn) => turn.id));
|
|
@@ -1792,6 +1902,7 @@ function createDesktopIpcActionFollower({
|
|
|
1792
1902
|
normalizedReviewFingerprintsByThreadId.delete(threadId);
|
|
1793
1903
|
conversationProjector.remove(threadId);
|
|
1794
1904
|
syncProjectedActions(threadId, []);
|
|
1905
|
+
notifyActivityRemoval(threadId, "archived");
|
|
1795
1906
|
}
|
|
1796
1907
|
sendApplicationResponse(JSON.stringify({
|
|
1797
1908
|
method: envelope.method === "thread-archived" ? "thread/archived" : "thread/unarchived",
|
|
@@ -1861,15 +1972,21 @@ function createDesktopIpcActionFollower({
|
|
|
1861
1972
|
return null;
|
|
1862
1973
|
}
|
|
1863
1974
|
|
|
1975
|
+
if (method === "thread/settings/update") {
|
|
1976
|
+
const threadSettings = normalizeThreadSettingsUpdate(params);
|
|
1977
|
+
return {
|
|
1978
|
+
threadId,
|
|
1979
|
+
method: "thread-follower-update-thread-settings",
|
|
1980
|
+
params: { conversationId: threadId, threadSettings },
|
|
1981
|
+
};
|
|
1982
|
+
}
|
|
1864
1983
|
if (method === "turn/start") {
|
|
1865
1984
|
return {
|
|
1866
1985
|
threadId,
|
|
1867
1986
|
method: "thread-follower-start-turn",
|
|
1868
|
-
params: {
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
turnStartParams: params,
|
|
1872
|
-
},
|
|
1987
|
+
params: { conversationId: threadId },
|
|
1988
|
+
senderRequestId: requestId,
|
|
1989
|
+
turnStartParams: params,
|
|
1873
1990
|
};
|
|
1874
1991
|
}
|
|
1875
1992
|
if (method === "turn/steer") {
|
|
@@ -1879,6 +1996,7 @@ function createDesktopIpcActionFollower({
|
|
|
1879
1996
|
params: {
|
|
1880
1997
|
conversationId: threadId,
|
|
1881
1998
|
input: Array.isArray(params.input) ? params.input : [],
|
|
1999
|
+
...Object.fromEntries(["clientUserMessageId", "additionalContext", "toolOutput"].filter((key) => hasOwn(params, key)).map((key) => [key, cloneJSON(params[key])])),
|
|
1882
2000
|
expectedTurnId: readString(params.expectedTurnId) || readString(params.expected_turn_id),
|
|
1883
2001
|
},
|
|
1884
2002
|
};
|
|
@@ -1889,7 +2007,8 @@ function createDesktopIpcActionFollower({
|
|
|
1889
2007
|
method: "thread-follower-interrupt-turn",
|
|
1890
2008
|
params: {
|
|
1891
2009
|
conversationId: threadId,
|
|
1892
|
-
|
|
2010
|
+
mode: "user-stop",
|
|
2011
|
+
expectedTurnId: readString(params.turnId) || readString(params.turn_id),
|
|
1893
2012
|
},
|
|
1894
2013
|
};
|
|
1895
2014
|
}
|
|
@@ -1907,30 +2026,39 @@ function createDesktopIpcActionFollower({
|
|
|
1907
2026
|
}
|
|
1908
2027
|
|
|
1909
2028
|
function submitDesktopFollowerRequest(route, originalMessage) {
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
}
|
|
2029
|
+
enqueueMutation(route.threadId, async () => {
|
|
2030
|
+
const revisionBefore = runtimeSettingsStore?.get?.(route.threadId)?.revision;
|
|
2031
|
+
const resolvedRequest = await resolveFollowerRequest(route);
|
|
2032
|
+
if (route.method === "thread-follower-start-turn") {
|
|
2033
|
+
// A rejected settings update does not relinquish the Desktop writer.
|
|
2034
|
+
// Propagate it without turning a timeout into local delivery failure.
|
|
2035
|
+
try {
|
|
2036
|
+
await syncDesktopOwnerRuntimeSettings(route.threadId, resolvedRequest.turnStartParams);
|
|
2037
|
+
} catch (error) {
|
|
2038
|
+
// Failure to deliver settings does not prove that a turn sent locally
|
|
2039
|
+
// would be safe. Only the start-turn route can authorize that fallback.
|
|
2040
|
+
throw new Error(error.message, { cause: error });
|
|
1922
2041
|
}
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
2042
|
+
}
|
|
2043
|
+
return {
|
|
2044
|
+
resolvedRequest,
|
|
2045
|
+
revisionBefore,
|
|
2046
|
+
result: await ipc.sendRequest(route.method, resolvedRequest.params),
|
|
2047
|
+
};
|
|
2048
|
+
})
|
|
2049
|
+
.then(({ resolvedRequest, revisionBefore, result }) => {
|
|
2050
|
+
const currentSettings = runtimeSettingsStore?.get?.(route.threadId);
|
|
2051
|
+
const receivedOwnerSettings = currentSettings && currentSettings.revision !== revisionBefore;
|
|
2052
|
+
let appServerResult = appServerResultForFollowerRequest(route.method, result);
|
|
2053
|
+
if (route.method === "thread-follower-update-thread-settings") {
|
|
2054
|
+
const settings = receivedOwnerSettings ? currentSettings
|
|
2055
|
+
: runtimeSettingsStore?.commit?.(route.threadId, route.params.threadSettings, { source: "phone" });
|
|
2056
|
+
appServerResult = { runtimeSettings: settings || null };
|
|
2057
|
+
}
|
|
2058
|
+
if (route.method === "thread-follower-start-turn" && !receivedOwnerSettings) {
|
|
1931
2059
|
commitPhoneRuntimeSettings(
|
|
1932
2060
|
route.threadId,
|
|
1933
|
-
|
|
2061
|
+
resolvedRequest.turnStartParams,
|
|
1934
2062
|
readTurnIdFromAppServerResult(appServerResult)
|
|
1935
2063
|
);
|
|
1936
2064
|
}
|
|
@@ -1963,7 +2091,7 @@ function createDesktopIpcActionFollower({
|
|
|
1963
2091
|
}
|
|
1964
2092
|
|
|
1965
2093
|
function appServerResultForFollowerRequest(method, result) {
|
|
1966
|
-
if (method === "thread-follower-start-turn"
|
|
2094
|
+
if ((method === "thread-follower-start-turn" || method === "thread-follower-steer-turn")
|
|
1967
2095
|
&& result
|
|
1968
2096
|
&& typeof result === "object"
|
|
1969
2097
|
&& !Array.isArray(result)
|
|
@@ -1988,28 +2116,17 @@ function createDesktopIpcActionFollower({
|
|
|
1988
2116
|
const params = turnStartParams && typeof turnStartParams === "object"
|
|
1989
2117
|
? turnStartParams
|
|
1990
2118
|
: {};
|
|
1991
|
-
const
|
|
1992
|
-
|
|
1993
|
-
:
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|| readString(collaborationSettings?.reasoningEffort);
|
|
2000
|
-
if (!model && !effort && !collaborationMode) {
|
|
2001
|
-
return;
|
|
2002
|
-
}
|
|
2003
|
-
|
|
2119
|
+
const patch = runtimeSettingsPatch(params);
|
|
2120
|
+
const threadSettings = {
|
|
2121
|
+
...(patch.model ? { model: patch.model } : {}),
|
|
2122
|
+
...(hasOwn(patch, "reasoningEffort") ? { effort: patch.reasoningEffort } : {}),
|
|
2123
|
+
...(hasOwn(patch, "serviceTier") ? { serviceTier: patch.serviceTier } : {}),
|
|
2124
|
+
...(params.collaborationMode ? { collaborationMode: cloneJSON(params.collaborationMode) } : {}),
|
|
2125
|
+
};
|
|
2126
|
+
if (Object.keys(threadSettings).length === 0) return;
|
|
2004
2127
|
await ipc.sendRequest("thread-follower-update-thread-settings", {
|
|
2005
2128
|
conversationId: threadId,
|
|
2006
|
-
threadSettings
|
|
2007
|
-
...(model ? { model } : {}),
|
|
2008
|
-
effort: effort || null,
|
|
2009
|
-
// turn/start omission is the app-server representation of Normal speed.
|
|
2010
|
-
serviceTier: readString(params.serviceTier) || readString(params.service_tier) || null,
|
|
2011
|
-
...(collaborationMode ? { collaborationMode } : {}),
|
|
2012
|
-
},
|
|
2129
|
+
threadSettings,
|
|
2013
2130
|
});
|
|
2014
2131
|
}
|
|
2015
2132
|
|
|
@@ -2026,19 +2143,34 @@ function createDesktopIpcActionFollower({
|
|
|
2026
2143
|
|
|
2027
2144
|
// Desktop-followed turn starts must apply the same param normalization as
|
|
2028
2145
|
// requests forwarded straight to the local app-server.
|
|
2029
|
-
async function
|
|
2146
|
+
async function resolveFollowerRequest(route) {
|
|
2030
2147
|
if (route.method !== "thread-follower-start-turn") {
|
|
2031
|
-
return
|
|
2148
|
+
return {
|
|
2149
|
+
params: route.params,
|
|
2150
|
+
turnStartParams: null,
|
|
2151
|
+
};
|
|
2032
2152
|
}
|
|
2033
2153
|
|
|
2034
2154
|
const normalized = await Promise.resolve(
|
|
2035
|
-
normalizeTurnStartParams(cloneJSON(route.
|
|
2155
|
+
normalizeTurnStartParams(cloneJSON(route.turnStartParams))
|
|
2036
2156
|
);
|
|
2037
2157
|
const turnStartParams = normalized && typeof normalized === "object" && !Array.isArray(normalized)
|
|
2038
2158
|
? normalized
|
|
2039
|
-
: route.
|
|
2159
|
+
: route.turnStartParams;
|
|
2160
|
+
const request = cloneJSON(turnStartParams);
|
|
2161
|
+
if (!readString(request.clientUserMessageId)) {
|
|
2162
|
+
request.clientUserMessageId = route.senderRequestId;
|
|
2163
|
+
}
|
|
2040
2164
|
return {
|
|
2041
|
-
|
|
2165
|
+
params: {
|
|
2166
|
+
...route.params,
|
|
2167
|
+
turnStart: {
|
|
2168
|
+
request,
|
|
2169
|
+
context: {
|
|
2170
|
+
inheritThreadSettings: true,
|
|
2171
|
+
},
|
|
2172
|
+
},
|
|
2173
|
+
},
|
|
2042
2174
|
turnStartParams,
|
|
2043
2175
|
};
|
|
2044
2176
|
}
|
|
@@ -2121,6 +2253,7 @@ function createDesktopIpcActionFollower({
|
|
|
2121
2253
|
rawStatesByThreadId.set(threadId, nextState);
|
|
2122
2254
|
rawStateUpdatedAtByThreadId.set(threadId, now());
|
|
2123
2255
|
rebuildNormalizedLiveIndex(threadId, nextState);
|
|
2256
|
+
notifyActivityState(threadId, nextState);
|
|
2124
2257
|
if (baselineState && typeof baselineState === "object"
|
|
2125
2258
|
&& !backgroundOnlyThreadIds.has(threadId)) {
|
|
2126
2259
|
const liveState = boundedDesktopLiveState(
|
|
@@ -2143,6 +2276,7 @@ function createDesktopIpcActionFollower({
|
|
|
2143
2276
|
|
|
2144
2277
|
return {
|
|
2145
2278
|
observeInbound,
|
|
2279
|
+
observeThreadListResponse,
|
|
2146
2280
|
stopAll,
|
|
2147
2281
|
// True while this thread has live Desktop-owned IPC state mirrored to the
|
|
2148
2282
|
// phone; used to keep fallback mirrors (rollout tail) silent.
|
|
@@ -2174,7 +2308,7 @@ function createDesktopIpcActionFollower({
|
|
|
2174
2308
|
// has actually moved recently. Keep hasLiveThreadState's broader meaning
|
|
2175
2309
|
// for callers that need cached/idle Desktop state, but expose this explicit
|
|
2176
2310
|
// lease check for source arbitration.
|
|
2177
|
-
hasFreshLiveThreadState(threadId, { fallbackActivityAt = 0 } = {}) {
|
|
2311
|
+
hasFreshLiveThreadState(threadId, { fallbackActivityAt = 0, probeFallbackActivity = false } = {}) {
|
|
2178
2312
|
const normalizedThreadId = readString(threadId);
|
|
2179
2313
|
if (pendingSnapshotsByThreadId.has(normalizedThreadId)) {
|
|
2180
2314
|
return Boolean(normalizedThreadId);
|
|
@@ -2195,8 +2329,11 @@ function createDesktopIpcActionFollower({
|
|
|
2195
2329
|
if (hasActiveProjectedTurn(thread)) {
|
|
2196
2330
|
const updatedAt = rawStateUpdatedAtByThreadId.get(normalizedThreadId) || 0;
|
|
2197
2331
|
const hasNewerFallbackActivity = Number(fallbackActivityAt) > updatedAt;
|
|
2332
|
+
// Let a stale stream check file metadata before deciding who emits.
|
|
2333
|
+
// The subsequent check with the real mtime still protects quiet work.
|
|
2198
2334
|
return hasResponsiveDesktopIpc()
|
|
2199
|
-
&& (!isRawStateStaleForActiveRead(normalizedThreadId)
|
|
2335
|
+
&& (!isRawStateStaleForActiveRead(normalizedThreadId)
|
|
2336
|
+
|| (!probeFallbackActivity && !hasNewerFallbackActivity));
|
|
2200
2337
|
}
|
|
2201
2338
|
return !isRawStateStaleForActiveRead(normalizedThreadId);
|
|
2202
2339
|
},
|
|
@@ -2502,6 +2639,22 @@ function desktopFollowerPayloadForResponse(route, responseMessage) {
|
|
|
2502
2639
|
return null;
|
|
2503
2640
|
}
|
|
2504
2641
|
|
|
2642
|
+
if (route.method === "item/permissions/requestApproval") {
|
|
2643
|
+
const result = responseMessage?.result;
|
|
2644
|
+
if (!result?.permissions || typeof result.permissions !== "object"
|
|
2645
|
+
|| Array.isArray(result.permissions) || !["turn", "session"].includes(result.scope)) {
|
|
2646
|
+
return null;
|
|
2647
|
+
}
|
|
2648
|
+
return {
|
|
2649
|
+
method,
|
|
2650
|
+
params: {
|
|
2651
|
+
conversationId: route.threadId,
|
|
2652
|
+
requestId: route.desktopRequestId ?? route.requestId,
|
|
2653
|
+
response: cloneJSON(result),
|
|
2654
|
+
},
|
|
2655
|
+
};
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2505
2658
|
if (route.method === "item/tool/requestUserInput") {
|
|
2506
2659
|
const answers = responseMessage?.result?.answers;
|
|
2507
2660
|
if (!answers || typeof answers !== "object" || Array.isArray(answers)) {
|
|
@@ -2520,7 +2673,7 @@ function desktopFollowerPayloadForResponse(route, responseMessage) {
|
|
|
2520
2673
|
};
|
|
2521
2674
|
}
|
|
2522
2675
|
|
|
2523
|
-
const decision =
|
|
2676
|
+
const decision = readString(responseMessage?.result?.decision);
|
|
2524
2677
|
if (!APPROVAL_DECISIONS.has(decision)) {
|
|
2525
2678
|
return null;
|
|
2526
2679
|
}
|
|
@@ -2529,53 +2682,12 @@ function desktopFollowerPayloadForResponse(route, responseMessage) {
|
|
|
2529
2682
|
method,
|
|
2530
2683
|
params: {
|
|
2531
2684
|
conversationId: route.threadId,
|
|
2532
|
-
requestId: route.requestId,
|
|
2685
|
+
requestId: route.desktopRequestId ?? route.requestId,
|
|
2533
2686
|
decision,
|
|
2534
2687
|
},
|
|
2535
2688
|
};
|
|
2536
2689
|
}
|
|
2537
2690
|
|
|
2538
|
-
function desktopApprovalDecisionForResponse(method, result) {
|
|
2539
|
-
const explicitDecision = readString(result?.decision);
|
|
2540
|
-
if (explicitDecision) {
|
|
2541
|
-
return explicitDecision;
|
|
2542
|
-
}
|
|
2543
|
-
|
|
2544
|
-
if (method !== "item/permissions/requestApproval") {
|
|
2545
|
-
return "";
|
|
2546
|
-
}
|
|
2547
|
-
|
|
2548
|
-
// Permission approvals use a grant payload on app-server, while Desktop IPC
|
|
2549
|
-
// currently exposes only decision-style follower replies.
|
|
2550
|
-
return hasGrantedPermission(result?.permissions) ? "accept" : "decline";
|
|
2551
|
-
}
|
|
2552
|
-
|
|
2553
|
-
function hasGrantedPermission(value) {
|
|
2554
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2555
|
-
return false;
|
|
2556
|
-
}
|
|
2557
|
-
|
|
2558
|
-
if (Object.keys(value).length === 0) {
|
|
2559
|
-
return false;
|
|
2560
|
-
}
|
|
2561
|
-
|
|
2562
|
-
return Object.values(value).some((entry) => {
|
|
2563
|
-
if (entry == null) {
|
|
2564
|
-
return false;
|
|
2565
|
-
}
|
|
2566
|
-
if (typeof entry === "boolean") {
|
|
2567
|
-
return entry;
|
|
2568
|
-
}
|
|
2569
|
-
if (Array.isArray(entry)) {
|
|
2570
|
-
return entry.length > 0;
|
|
2571
|
-
}
|
|
2572
|
-
if (typeof entry === "object") {
|
|
2573
|
-
return Object.keys(entry).length > 0;
|
|
2574
|
-
}
|
|
2575
|
-
return true;
|
|
2576
|
-
});
|
|
2577
|
-
}
|
|
2578
|
-
|
|
2579
2691
|
function projectPendingDesktopActions(threadId, conversationState) {
|
|
2580
2692
|
const requests = Array.isArray(conversationState?.requests) ? conversationState.requests : [];
|
|
2581
2693
|
return requests
|
|
@@ -3000,6 +3112,84 @@ function boundedDesktopLiveState(
|
|
|
3000
3112
|
};
|
|
3001
3113
|
}
|
|
3002
3114
|
|
|
3115
|
+
function boundedDesktopActivityState(state) {
|
|
3116
|
+
const rawState = state && typeof state === "object" ? state : {};
|
|
3117
|
+
return {
|
|
3118
|
+
title: readString(rawState.title) || readString(rawState.name),
|
|
3119
|
+
cwd: readString(rawState.cwd) || readString(rawState.current_working_directory),
|
|
3120
|
+
threadRuntimeStatus: compactDesktopRuntimeStatus(rawState.threadRuntimeStatus),
|
|
3121
|
+
status: compactDesktopRuntimeStatus(rawState.status),
|
|
3122
|
+
hasUnreadTurn: rawState.hasUnreadTurn ?? rawState.has_unread_turn,
|
|
3123
|
+
unreadMessageCount: rawState.unreadMessageCount ?? rawState.unread_message_count,
|
|
3124
|
+
requests: (Array.isArray(rawState.requests) ? rawState.requests : []).map((request) => ({
|
|
3125
|
+
id: request?.id,
|
|
3126
|
+
method: readString(request?.method),
|
|
3127
|
+
completed: request?.completed === true,
|
|
3128
|
+
})),
|
|
3129
|
+
turns: (Array.isArray(rawState.turns) ? rawState.turns : []).map(compactDesktopActivityTurn),
|
|
3130
|
+
};
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
function compactDesktopRuntimeStatus(status) {
|
|
3134
|
+
if (status && typeof status === "object" && !Array.isArray(status)) {
|
|
3135
|
+
return {
|
|
3136
|
+
type: readString(status.type),
|
|
3137
|
+
activeFlags: Array.isArray(status.activeFlags) ? status.activeFlags.filter((flag) => (
|
|
3138
|
+
flag === "waitingOnApproval" || flag === "waitingOnUserInput"
|
|
3139
|
+
)) : [],
|
|
3140
|
+
};
|
|
3141
|
+
}
|
|
3142
|
+
return readString(status);
|
|
3143
|
+
}
|
|
3144
|
+
|
|
3145
|
+
function compactDesktopActivityTurn(turn) {
|
|
3146
|
+
return {
|
|
3147
|
+
id: readString(turn?.id),
|
|
3148
|
+
turnId: readString(turn?.turnId),
|
|
3149
|
+
turn_id: readString(turn?.turn_id),
|
|
3150
|
+
status: readString(turn?.status),
|
|
3151
|
+
error: turn?.error ? true : null,
|
|
3152
|
+
startedAt: finiteTimestamp(turn?.startedAt),
|
|
3153
|
+
started_at: finiteTimestamp(turn?.started_at),
|
|
3154
|
+
completedAt: finiteTimestamp(turn?.completedAt),
|
|
3155
|
+
completed_at: finiteTimestamp(turn?.completed_at),
|
|
3156
|
+
turnStartedAtMs: finiteTimestamp(turn?.turnStartedAtMs),
|
|
3157
|
+
turn_started_at_ms: finiteTimestamp(turn?.turn_started_at_ms),
|
|
3158
|
+
turnCompletedAtMs: finiteTimestamp(turn?.turnCompletedAtMs),
|
|
3159
|
+
turn_completed_at_ms: finiteTimestamp(turn?.turn_completed_at_ms),
|
|
3160
|
+
startedAtMs: finiteTimestamp(turn?.startedAtMs ?? turn?.started_at_ms),
|
|
3161
|
+
completedAtMs: finiteTimestamp(turn?.completedAtMs ?? turn?.completed_at_ms),
|
|
3162
|
+
items: compactLatestActivityItems(turn),
|
|
3163
|
+
};
|
|
3164
|
+
}
|
|
3165
|
+
|
|
3166
|
+
function compactLatestActivityItems(turn) {
|
|
3167
|
+
const items = Array.isArray(turn?.items) ? turn.items : [];
|
|
3168
|
+
// Token patches must not copy the growing item history. A small tail is
|
|
3169
|
+
// enough for a current semantic label; absence remains unknown.
|
|
3170
|
+
for (let index = items.length - 1; index >= Math.max(0, items.length - 32); index -= 1) {
|
|
3171
|
+
const item = items[index];
|
|
3172
|
+
if (!projectSemanticItem(item)) {
|
|
3173
|
+
continue;
|
|
3174
|
+
}
|
|
3175
|
+
return [{
|
|
3176
|
+
id: readString(item?.id),
|
|
3177
|
+
itemId: readString(item?.itemId),
|
|
3178
|
+
item_id: readString(item?.item_id),
|
|
3179
|
+
type: readString(item?.type),
|
|
3180
|
+
}];
|
|
3181
|
+
}
|
|
3182
|
+
return [];
|
|
3183
|
+
}
|
|
3184
|
+
|
|
3185
|
+
function finiteTimestamp(value) {
|
|
3186
|
+
if (value == null || value === "") {
|
|
3187
|
+
return null;
|
|
3188
|
+
}
|
|
3189
|
+
const number = Number(value);
|
|
3190
|
+
return Number.isFinite(number) ? number : null;
|
|
3191
|
+
}
|
|
3192
|
+
|
|
3003
3193
|
function boundedDesktopLiveTurns(
|
|
3004
3194
|
state,
|
|
3005
3195
|
nowValue = Date.now(),
|
|
@@ -3049,7 +3239,7 @@ function boundedDesktopLiveTurns(
|
|
|
3049
3239
|
return normalizeBoundedTurnsForRuntime(selectedTurns, state, retainedTurnIds);
|
|
3050
3240
|
}
|
|
3051
3241
|
|
|
3052
|
-
function boundedIndexedDesktopLiveTurns(state, index, nowValue, retainedTurnIds) {
|
|
3242
|
+
function boundedIndexedDesktopLiveTurns(state, index, nowValue, retainedTurnIds, canonicalActivity = false) {
|
|
3053
3243
|
if (index.entries.length === 0) {
|
|
3054
3244
|
return [];
|
|
3055
3245
|
}
|
|
@@ -3090,10 +3280,11 @@ function boundedIndexedDesktopLiveTurns(state, index, nowValue, retainedTurnIds)
|
|
|
3090
3280
|
if (!turn) {
|
|
3091
3281
|
return null;
|
|
3092
3282
|
}
|
|
3093
|
-
return turnIdOf(turn)
|
|
3283
|
+
return turnIdOf(turn) || (canonicalActivity && entry.rawIndex != null)
|
|
3284
|
+
? turn : { ...turn, id: entry.id };
|
|
3094
3285
|
})
|
|
3095
3286
|
.filter(Boolean);
|
|
3096
|
-
return normalizeBoundedTurnsForRuntime(selectedTurns, state, retainedTurnIds);
|
|
3287
|
+
return canonicalActivity ? selectedTurns : normalizeBoundedTurnsForRuntime(selectedTurns, state, retainedTurnIds);
|
|
3097
3288
|
}
|
|
3098
3289
|
|
|
3099
3290
|
function resolveIndexedTurn(state, entry) {
|