@makerbi/remodex 3.2.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 +269 -92
- 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
|
@@ -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,6 +1972,14 @@ 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,
|
|
@@ -1877,6 +1996,7 @@ function createDesktopIpcActionFollower({
|
|
|
1877
1996
|
params: {
|
|
1878
1997
|
conversationId: threadId,
|
|
1879
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])])),
|
|
1880
2000
|
expectedTurnId: readString(params.expectedTurnId) || readString(params.expected_turn_id),
|
|
1881
2001
|
},
|
|
1882
2002
|
};
|
|
@@ -1906,27 +2026,36 @@ function createDesktopIpcActionFollower({
|
|
|
1906
2026
|
}
|
|
1907
2027
|
|
|
1908
2028
|
function submitDesktopFollowerRequest(route, originalMessage) {
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
}
|
|
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 });
|
|
1921
2041
|
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
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) {
|
|
1930
2059
|
commitPhoneRuntimeSettings(
|
|
1931
2060
|
route.threadId,
|
|
1932
2061
|
resolvedRequest.turnStartParams,
|
|
@@ -1962,7 +2091,7 @@ function createDesktopIpcActionFollower({
|
|
|
1962
2091
|
}
|
|
1963
2092
|
|
|
1964
2093
|
function appServerResultForFollowerRequest(method, result) {
|
|
1965
|
-
if (method === "thread-follower-start-turn"
|
|
2094
|
+
if ((method === "thread-follower-start-turn" || method === "thread-follower-steer-turn")
|
|
1966
2095
|
&& result
|
|
1967
2096
|
&& typeof result === "object"
|
|
1968
2097
|
&& !Array.isArray(result)
|
|
@@ -1987,28 +2116,17 @@ function createDesktopIpcActionFollower({
|
|
|
1987
2116
|
const params = turnStartParams && typeof turnStartParams === "object"
|
|
1988
2117
|
? turnStartParams
|
|
1989
2118
|
: {};
|
|
1990
|
-
const
|
|
1991
|
-
|
|
1992
|
-
:
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|| readString(collaborationSettings?.reasoningEffort);
|
|
1999
|
-
if (!model && !effort && !collaborationMode) {
|
|
2000
|
-
return;
|
|
2001
|
-
}
|
|
2002
|
-
|
|
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;
|
|
2003
2127
|
await ipc.sendRequest("thread-follower-update-thread-settings", {
|
|
2004
2128
|
conversationId: threadId,
|
|
2005
|
-
threadSettings
|
|
2006
|
-
...(model ? { model } : {}),
|
|
2007
|
-
effort: effort || null,
|
|
2008
|
-
// turn/start omission is the app-server representation of Normal speed.
|
|
2009
|
-
serviceTier: readString(params.serviceTier) || readString(params.service_tier) || null,
|
|
2010
|
-
...(collaborationMode ? { collaborationMode } : {}),
|
|
2011
|
-
},
|
|
2129
|
+
threadSettings,
|
|
2012
2130
|
});
|
|
2013
2131
|
}
|
|
2014
2132
|
|
|
@@ -2135,6 +2253,7 @@ function createDesktopIpcActionFollower({
|
|
|
2135
2253
|
rawStatesByThreadId.set(threadId, nextState);
|
|
2136
2254
|
rawStateUpdatedAtByThreadId.set(threadId, now());
|
|
2137
2255
|
rebuildNormalizedLiveIndex(threadId, nextState);
|
|
2256
|
+
notifyActivityState(threadId, nextState);
|
|
2138
2257
|
if (baselineState && typeof baselineState === "object"
|
|
2139
2258
|
&& !backgroundOnlyThreadIds.has(threadId)) {
|
|
2140
2259
|
const liveState = boundedDesktopLiveState(
|
|
@@ -2157,6 +2276,7 @@ function createDesktopIpcActionFollower({
|
|
|
2157
2276
|
|
|
2158
2277
|
return {
|
|
2159
2278
|
observeInbound,
|
|
2279
|
+
observeThreadListResponse,
|
|
2160
2280
|
stopAll,
|
|
2161
2281
|
// True while this thread has live Desktop-owned IPC state mirrored to the
|
|
2162
2282
|
// phone; used to keep fallback mirrors (rollout tail) silent.
|
|
@@ -2188,7 +2308,7 @@ function createDesktopIpcActionFollower({
|
|
|
2188
2308
|
// has actually moved recently. Keep hasLiveThreadState's broader meaning
|
|
2189
2309
|
// for callers that need cached/idle Desktop state, but expose this explicit
|
|
2190
2310
|
// lease check for source arbitration.
|
|
2191
|
-
hasFreshLiveThreadState(threadId, { fallbackActivityAt = 0 } = {}) {
|
|
2311
|
+
hasFreshLiveThreadState(threadId, { fallbackActivityAt = 0, probeFallbackActivity = false } = {}) {
|
|
2192
2312
|
const normalizedThreadId = readString(threadId);
|
|
2193
2313
|
if (pendingSnapshotsByThreadId.has(normalizedThreadId)) {
|
|
2194
2314
|
return Boolean(normalizedThreadId);
|
|
@@ -2209,8 +2329,11 @@ function createDesktopIpcActionFollower({
|
|
|
2209
2329
|
if (hasActiveProjectedTurn(thread)) {
|
|
2210
2330
|
const updatedAt = rawStateUpdatedAtByThreadId.get(normalizedThreadId) || 0;
|
|
2211
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.
|
|
2212
2334
|
return hasResponsiveDesktopIpc()
|
|
2213
|
-
&& (!isRawStateStaleForActiveRead(normalizedThreadId)
|
|
2335
|
+
&& (!isRawStateStaleForActiveRead(normalizedThreadId)
|
|
2336
|
+
|| (!probeFallbackActivity && !hasNewerFallbackActivity));
|
|
2214
2337
|
}
|
|
2215
2338
|
return !isRawStateStaleForActiveRead(normalizedThreadId);
|
|
2216
2339
|
},
|
|
@@ -2516,6 +2639,22 @@ function desktopFollowerPayloadForResponse(route, responseMessage) {
|
|
|
2516
2639
|
return null;
|
|
2517
2640
|
}
|
|
2518
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
|
+
|
|
2519
2658
|
if (route.method === "item/tool/requestUserInput") {
|
|
2520
2659
|
const answers = responseMessage?.result?.answers;
|
|
2521
2660
|
if (!answers || typeof answers !== "object" || Array.isArray(answers)) {
|
|
@@ -2534,7 +2673,7 @@ function desktopFollowerPayloadForResponse(route, responseMessage) {
|
|
|
2534
2673
|
};
|
|
2535
2674
|
}
|
|
2536
2675
|
|
|
2537
|
-
const decision =
|
|
2676
|
+
const decision = readString(responseMessage?.result?.decision);
|
|
2538
2677
|
if (!APPROVAL_DECISIONS.has(decision)) {
|
|
2539
2678
|
return null;
|
|
2540
2679
|
}
|
|
@@ -2543,53 +2682,12 @@ function desktopFollowerPayloadForResponse(route, responseMessage) {
|
|
|
2543
2682
|
method,
|
|
2544
2683
|
params: {
|
|
2545
2684
|
conversationId: route.threadId,
|
|
2546
|
-
requestId: route.requestId,
|
|
2685
|
+
requestId: route.desktopRequestId ?? route.requestId,
|
|
2547
2686
|
decision,
|
|
2548
2687
|
},
|
|
2549
2688
|
};
|
|
2550
2689
|
}
|
|
2551
2690
|
|
|
2552
|
-
function desktopApprovalDecisionForResponse(method, result) {
|
|
2553
|
-
const explicitDecision = readString(result?.decision);
|
|
2554
|
-
if (explicitDecision) {
|
|
2555
|
-
return explicitDecision;
|
|
2556
|
-
}
|
|
2557
|
-
|
|
2558
|
-
if (method !== "item/permissions/requestApproval") {
|
|
2559
|
-
return "";
|
|
2560
|
-
}
|
|
2561
|
-
|
|
2562
|
-
// Permission approvals use a grant payload on app-server, while Desktop IPC
|
|
2563
|
-
// currently exposes only decision-style follower replies.
|
|
2564
|
-
return hasGrantedPermission(result?.permissions) ? "accept" : "decline";
|
|
2565
|
-
}
|
|
2566
|
-
|
|
2567
|
-
function hasGrantedPermission(value) {
|
|
2568
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
2569
|
-
return false;
|
|
2570
|
-
}
|
|
2571
|
-
|
|
2572
|
-
if (Object.keys(value).length === 0) {
|
|
2573
|
-
return false;
|
|
2574
|
-
}
|
|
2575
|
-
|
|
2576
|
-
return Object.values(value).some((entry) => {
|
|
2577
|
-
if (entry == null) {
|
|
2578
|
-
return false;
|
|
2579
|
-
}
|
|
2580
|
-
if (typeof entry === "boolean") {
|
|
2581
|
-
return entry;
|
|
2582
|
-
}
|
|
2583
|
-
if (Array.isArray(entry)) {
|
|
2584
|
-
return entry.length > 0;
|
|
2585
|
-
}
|
|
2586
|
-
if (typeof entry === "object") {
|
|
2587
|
-
return Object.keys(entry).length > 0;
|
|
2588
|
-
}
|
|
2589
|
-
return true;
|
|
2590
|
-
});
|
|
2591
|
-
}
|
|
2592
|
-
|
|
2593
2691
|
function projectPendingDesktopActions(threadId, conversationState) {
|
|
2594
2692
|
const requests = Array.isArray(conversationState?.requests) ? conversationState.requests : [];
|
|
2595
2693
|
return requests
|
|
@@ -3014,6 +3112,84 @@ function boundedDesktopLiveState(
|
|
|
3014
3112
|
};
|
|
3015
3113
|
}
|
|
3016
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
|
+
|
|
3017
3193
|
function boundedDesktopLiveTurns(
|
|
3018
3194
|
state,
|
|
3019
3195
|
nowValue = Date.now(),
|
|
@@ -3063,7 +3239,7 @@ function boundedDesktopLiveTurns(
|
|
|
3063
3239
|
return normalizeBoundedTurnsForRuntime(selectedTurns, state, retainedTurnIds);
|
|
3064
3240
|
}
|
|
3065
3241
|
|
|
3066
|
-
function boundedIndexedDesktopLiveTurns(state, index, nowValue, retainedTurnIds) {
|
|
3242
|
+
function boundedIndexedDesktopLiveTurns(state, index, nowValue, retainedTurnIds, canonicalActivity = false) {
|
|
3067
3243
|
if (index.entries.length === 0) {
|
|
3068
3244
|
return [];
|
|
3069
3245
|
}
|
|
@@ -3104,10 +3280,11 @@ function boundedIndexedDesktopLiveTurns(state, index, nowValue, retainedTurnIds)
|
|
|
3104
3280
|
if (!turn) {
|
|
3105
3281
|
return null;
|
|
3106
3282
|
}
|
|
3107
|
-
return turnIdOf(turn)
|
|
3283
|
+
return turnIdOf(turn) || (canonicalActivity && entry.rawIndex != null)
|
|
3284
|
+
? turn : { ...turn, id: entry.id };
|
|
3108
3285
|
})
|
|
3109
3286
|
.filter(Boolean);
|
|
3110
|
-
return normalizeBoundedTurnsForRuntime(selectedTurns, state, retainedTurnIds);
|
|
3287
|
+
return canonicalActivity ? selectedTurns : normalizeBoundedTurnsForRuntime(selectedTurns, state, retainedTurnIds);
|
|
3111
3288
|
}
|
|
3112
3289
|
|
|
3113
3290
|
function resolveIndexedTurn(state, entry) {
|