@makerbi/remodex 2.3.1 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/bridge.js +34 -0
- package/src/desktop-ipc-action-follower.js +310 -87
- package/src/desktop-ipc-conversation-adapter.js +138 -2
- package/src/desktop-ipc-conversation-projector.js +147 -1
- package/src/desktop-ipc-shared.js +148 -38
- package/src/push-notification-tracker.js +134 -0
- package/src/rollout-live-mirror.js +170 -11
- package/src/session-jsonl-history.js +5 -1
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
createDesktopConversationProjector,
|
|
12
12
|
desktopTurnsShareLogicalIdentity,
|
|
13
13
|
matchDesktopTurnIdentityContinuities,
|
|
14
|
+
projectDesktopConversationStateToGoal,
|
|
14
15
|
projectDesktopConversationStateToThread,
|
|
15
16
|
} = require("./desktop-ipc-conversation-projector");
|
|
16
17
|
const {
|
|
@@ -18,6 +19,7 @@ const {
|
|
|
18
19
|
FRAME_HEADER_BYTES,
|
|
19
20
|
MAX_FRAME_BYTES,
|
|
20
21
|
cloneJSON,
|
|
22
|
+
isThreadTurnStateProbeRequest,
|
|
21
23
|
normalizeToken,
|
|
22
24
|
readString,
|
|
23
25
|
requestIdKey,
|
|
@@ -35,30 +37,44 @@ const MAX_BASELINE_RECOVERY_ATTEMPTS = 5;
|
|
|
35
37
|
const BASELINE_RECOVERY_BASE_DELAY_MS = 1_000;
|
|
36
38
|
const BASELINE_RECOVERY_MAX_DELAY_MS = 15_000;
|
|
37
39
|
const MAX_QUEUED_CHANGES_PER_THREAD = 300;
|
|
38
|
-
const BACKGROUND_DISCONNECT_GRACE_MS = 30_000;
|
|
39
40
|
// Phone interest survives per-thread release by design, so cap the set to keep a
|
|
40
41
|
// marathon single Desktop connection from accumulating every thread id forever.
|
|
41
42
|
const MAX_ACTIVE_THREAD_IDS = 512;
|
|
42
43
|
const DESKTOP_IPC_ACTION_SOURCE = "desktop-ipc-action-follower";
|
|
43
44
|
const REMODEX_LIVE_OWNER_SOURCE = "desktop-ipc-live-owner";
|
|
44
|
-
const DESKTOP_STATE_READ_METHODS = new Set([
|
|
45
|
+
const DESKTOP_STATE_READ_METHODS = new Set([
|
|
46
|
+
"thread/read",
|
|
47
|
+
"thread/resume",
|
|
48
|
+
"thread/turns/list",
|
|
49
|
+
"thread/goal/get",
|
|
50
|
+
]);
|
|
45
51
|
// Sidebar refreshes should also keep the Litter subscription alive. Without
|
|
46
52
|
// this, a phone with no selected chat never connects to the Desktop bus and
|
|
47
53
|
// cannot discover runs that started on the Mac.
|
|
48
54
|
const DESKTOP_BACKGROUND_DISCOVERY_METHODS = new Set(["thread/list"]);
|
|
49
55
|
const DESKTOP_TURNS_CURSOR_PREFIX = "remodex-desktop-turns:";
|
|
50
|
-
//
|
|
51
|
-
//
|
|
52
|
-
// often than this window; a silent "active" cache is a stale reconnect echo
|
|
53
|
-
// (e.g. Desktop never saw the turn finish) and must not answer phone reads, or
|
|
54
|
-
// the phone shows a phantom running indicator until real history loads.
|
|
56
|
+
// Per-thread activity can be quiet during tools and subagents, so the short
|
|
57
|
+
// freshness window alone does not revoke a healthy Desktop owner.
|
|
55
58
|
const STALE_ACTIVE_READ_MAX_AGE_MS = 20_000;
|
|
59
|
+
// An open Unix socket is not unlimited proof of a functioning publisher. If no
|
|
60
|
+
// frame arrives for this generous lease, yield stale active reads so canonical
|
|
61
|
+
// or rollout recovery can clear a phantom run.
|
|
62
|
+
const CONNECTED_IPC_ACTIVITY_LEASE_MS = 5 * 60_000;
|
|
63
|
+
const MAX_NORMALIZED_REVIEW_FINGERPRINTS_PER_THREAD = 128;
|
|
56
64
|
const DESKTOP_FOLLOWER_REQUEST_METHODS = new Set([
|
|
57
65
|
"turn/start",
|
|
58
66
|
"turn/steer",
|
|
59
67
|
"turn/interrupt",
|
|
60
68
|
"thread/compact/start",
|
|
61
69
|
]);
|
|
70
|
+
// These mutations have no Codex Desktop follower command. Never leak them to
|
|
71
|
+
// the bridge's separate local app-server owner, which could create a competing
|
|
72
|
+
// owner for the same persisted thread.
|
|
73
|
+
const DESKTOP_OWNER_UNSUPPORTED_MUTATION_ERRORS = new Map([
|
|
74
|
+
["review/start", "Start this review in Codex Desktop."],
|
|
75
|
+
["thread/settings/update", "Change these thread settings in Codex Desktop."],
|
|
76
|
+
["thread/approveGuardianDeniedAction", "Approve this retry in Codex Desktop."],
|
|
77
|
+
]);
|
|
62
78
|
const ACTION_METHODS = new Set([
|
|
63
79
|
"item/commandExecution/requestApproval",
|
|
64
80
|
"item/fileChange/requestApproval",
|
|
@@ -214,7 +230,6 @@ function createDesktopIpcActionFollower({
|
|
|
214
230
|
onNormalizedHistoryIndexRebuilt = () => {},
|
|
215
231
|
requestTimeoutMs = REQUEST_TIMEOUT_MS,
|
|
216
232
|
ownershipProbeTimeoutMs = OWNERSHIP_PROBE_TIMEOUT_MS,
|
|
217
|
-
backgroundDisconnectGraceMs = BACKGROUND_DISCONNECT_GRACE_MS,
|
|
218
233
|
} = {}) {
|
|
219
234
|
const ipc = createDesktopIpcClient({
|
|
220
235
|
socketPath,
|
|
@@ -240,6 +255,7 @@ function createDesktopIpcActionFollower({
|
|
|
240
255
|
const projectedLiveActiveTurnIdsByThreadId = new Map();
|
|
241
256
|
const desktopLiveLifecycleByThreadId = new Map();
|
|
242
257
|
const normalizedLiveIndexesByThreadId = new Map();
|
|
258
|
+
const normalizedReviewFingerprintsByThreadId = new Map();
|
|
243
259
|
const staleYieldedThreadIds = new Set();
|
|
244
260
|
const conversationProjector = createDesktopConversationProjector({ now });
|
|
245
261
|
const pendingRoutesByRequestId = new Map();
|
|
@@ -252,7 +268,6 @@ function createDesktopIpcActionFollower({
|
|
|
252
268
|
// baseline. Otherwise a disconnect/eviction can erase the only evidence
|
|
253
269
|
// needed to send the matching completion and leave a phantom running badge.
|
|
254
270
|
const announcedBackgroundTurnsByThreadId = new Map();
|
|
255
|
-
const backgroundDisconnectTimersByThreadId = new Map();
|
|
256
271
|
// JS Set preserves insertion order; delete-before-add refreshes recency, and
|
|
257
272
|
// cap eviction skips threads with pending prompts so approvals are not lost.
|
|
258
273
|
function rememberActiveThread(threadId) {
|
|
@@ -301,6 +316,7 @@ function createDesktopIpcActionFollower({
|
|
|
301
316
|
projectedLiveActiveTurnIdsByThreadId.delete(threadId);
|
|
302
317
|
desktopLiveLifecycleByThreadId.delete(threadId);
|
|
303
318
|
normalizedLiveIndexesByThreadId.delete(threadId);
|
|
319
|
+
normalizedReviewFingerprintsByThreadId.delete(threadId);
|
|
304
320
|
conversationProjector.remove(threadId);
|
|
305
321
|
queuedChangesByThreadId.delete(threadId);
|
|
306
322
|
baselineRecoveryStateByThreadId.delete(threadId);
|
|
@@ -347,7 +363,6 @@ function createDesktopIpcActionFollower({
|
|
|
347
363
|
} else {
|
|
348
364
|
settleAnnouncedBackgroundTurn(threadId, "interrupted");
|
|
349
365
|
}
|
|
350
|
-
clearBackgroundDisconnectTimer(threadId);
|
|
351
366
|
announcedBackgroundTurnsByThreadId.delete(threadId);
|
|
352
367
|
if (rawState) {
|
|
353
368
|
const liveState = boundedDesktopLiveStateForThread(threadId, rawState);
|
|
@@ -372,6 +387,14 @@ function createDesktopIpcActionFollower({
|
|
|
372
387
|
remodexActionSource: DESKTOP_IPC_ACTION_SOURCE,
|
|
373
388
|
},
|
|
374
389
|
}));
|
|
390
|
+
// Guardian reviews on normalized-only history turns never appear in
|
|
391
|
+
// canonical thread/read history, so drain their overlays here just
|
|
392
|
+
// like syncProjectedConversationState does; otherwise a thread that
|
|
393
|
+
// stays idle after opening never delivers them.
|
|
394
|
+
emitNormalizedReviewOverlays(
|
|
395
|
+
threadId,
|
|
396
|
+
normalizedLiveIndexesByThreadId.get(threadId)
|
|
397
|
+
);
|
|
375
398
|
const output = conversationProjector.project(threadId, liveState, {
|
|
376
399
|
includeAllActiveTurns: true,
|
|
377
400
|
});
|
|
@@ -414,6 +437,27 @@ function createDesktopIpcActionFollower({
|
|
|
414
437
|
}
|
|
415
438
|
}
|
|
416
439
|
|
|
440
|
+
if (DESKTOP_OWNER_UNSUPPORTED_MUTATION_ERRORS.has(method)) {
|
|
441
|
+
const threadId = readThreadId(message?.params);
|
|
442
|
+
const mayBelongToDesktop = threadId
|
|
443
|
+
&& !liveOwnerThreadIds.has(threadId)
|
|
444
|
+
&& !isLocallyOwnedThread(threadId)
|
|
445
|
+
&& (isDesktopRoutableThread(threadId)
|
|
446
|
+
|| activeThreadIds.has(threadId)
|
|
447
|
+
|| ownershipProbeDeadlinesByThreadId.has(threadId)
|
|
448
|
+
|| pendingOwnershipProbeTokensByThreadId.has(threadId));
|
|
449
|
+
if (mayBelongToDesktop) {
|
|
450
|
+
sendApplicationResponse(JSON.stringify({
|
|
451
|
+
id: message?.id,
|
|
452
|
+
error: {
|
|
453
|
+
code: -32004,
|
|
454
|
+
message: DESKTOP_OWNER_UNSUPPORTED_MUTATION_ERRORS.get(method),
|
|
455
|
+
},
|
|
456
|
+
}));
|
|
457
|
+
return true;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
417
461
|
if (tryServeDesktopOwnedRead(message)) {
|
|
418
462
|
return true;
|
|
419
463
|
}
|
|
@@ -448,15 +492,12 @@ function createDesktopIpcActionFollower({
|
|
|
448
492
|
projectedLiveActiveTurnIdsByThreadId.clear();
|
|
449
493
|
desktopLiveLifecycleByThreadId.clear();
|
|
450
494
|
normalizedLiveIndexesByThreadId.clear();
|
|
495
|
+
normalizedReviewFingerprintsByThreadId.clear();
|
|
451
496
|
conversationProjector.reset();
|
|
452
497
|
pendingRoutesByRequestId.clear();
|
|
453
498
|
activeThreadIds.clear();
|
|
454
499
|
backgroundOnlyThreadIds.clear();
|
|
455
500
|
announcedBackgroundTurnsByThreadId.clear();
|
|
456
|
-
for (const timer of backgroundDisconnectTimersByThreadId.values()) {
|
|
457
|
-
clearTimeout(timer);
|
|
458
|
-
}
|
|
459
|
-
backgroundDisconnectTimersByThreadId.clear();
|
|
460
501
|
recoveringThreadIds.clear();
|
|
461
502
|
baselineRecoveryStateByThreadId.clear();
|
|
462
503
|
queuedChangesByThreadId.clear();
|
|
@@ -499,9 +540,6 @@ function createDesktopIpcActionFollower({
|
|
|
499
540
|
if (!threadId) {
|
|
500
541
|
return;
|
|
501
542
|
}
|
|
502
|
-
if (isSnapshotChange(params.change)) {
|
|
503
|
-
clearBackgroundDisconnectTimer(threadId);
|
|
504
|
-
}
|
|
505
543
|
const peerOwnershipSnapshot = isPeerOwnershipSnapshot(params);
|
|
506
544
|
if (peerOwnershipSnapshot && !isLocallyOwnedThread(threadId)) {
|
|
507
545
|
liveOwnerThreadIds.delete(threadId);
|
|
@@ -656,6 +694,7 @@ function createDesktopIpcActionFollower({
|
|
|
656
694
|
onNormalizedHistoryIndexRebuilt(threadId);
|
|
657
695
|
} else {
|
|
658
696
|
normalizedLiveIndexesByThreadId.delete(threadId);
|
|
697
|
+
normalizedReviewFingerprintsByThreadId.delete(threadId);
|
|
659
698
|
}
|
|
660
699
|
}
|
|
661
700
|
|
|
@@ -690,14 +729,15 @@ function createDesktopIpcActionFollower({
|
|
|
690
729
|
projectedLiveActiveTurnIdsByThreadId.clear();
|
|
691
730
|
desktopLiveLifecycleByThreadId.clear();
|
|
692
731
|
normalizedLiveIndexesByThreadId.clear();
|
|
732
|
+
normalizedReviewFingerprintsByThreadId.clear();
|
|
693
733
|
recoveringThreadIds.clear();
|
|
694
734
|
baselineRecoveryStateByThreadId.clear();
|
|
695
735
|
queuedChangesByThreadId.clear();
|
|
696
736
|
pendingOwnershipProbeTokensByThreadId.clear();
|
|
697
737
|
desktopOwnedByProbeThreadIds.clear();
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
738
|
+
// A lost IPC connection is not evidence that Desktop stopped the turn.
|
|
739
|
+
// Keep announced lifecycle state until a reconnect snapshot, archive, or
|
|
740
|
+
// another authoritative state transition supplies a real terminal status.
|
|
701
741
|
// Keep activeThreadIds: phone interest is phone-scoped, not connection-scoped.
|
|
702
742
|
// Clearing it here would make reconnect snapshots for a thread the phone is
|
|
703
743
|
// still viewing fail the activeThreadIds.has() guard until the phone happens
|
|
@@ -730,6 +770,7 @@ function createDesktopIpcActionFollower({
|
|
|
730
770
|
projectedLiveActiveTurnIdsByThreadId.delete(threadId);
|
|
731
771
|
desktopLiveLifecycleByThreadId.delete(threadId);
|
|
732
772
|
normalizedLiveIndexesByThreadId.delete(threadId);
|
|
773
|
+
normalizedReviewFingerprintsByThreadId.delete(threadId);
|
|
733
774
|
conversationProjector.remove(threadId);
|
|
734
775
|
queuedChangesByThreadId.delete(threadId);
|
|
735
776
|
baselineRecoveryStateByThreadId.delete(threadId);
|
|
@@ -759,6 +800,7 @@ function createDesktopIpcActionFollower({
|
|
|
759
800
|
projectedLiveActiveTurnIdsByThreadId.delete(threadId);
|
|
760
801
|
desktopLiveLifecycleByThreadId.delete(threadId);
|
|
761
802
|
normalizedLiveIndexesByThreadId.delete(threadId);
|
|
803
|
+
normalizedReviewFingerprintsByThreadId.delete(threadId);
|
|
762
804
|
conversationProjector.remove(threadId);
|
|
763
805
|
queuedChangesByThreadId.delete(threadId);
|
|
764
806
|
baselineRecoveryStateByThreadId.delete(threadId);
|
|
@@ -1015,6 +1057,13 @@ function createDesktopIpcActionFollower({
|
|
|
1015
1057
|
}
|
|
1016
1058
|
|
|
1017
1059
|
rememberActiveThread(threadId);
|
|
1060
|
+
if (method === "thread/goal/get") {
|
|
1061
|
+
sendApplicationResponse(JSON.stringify({
|
|
1062
|
+
id: message.id,
|
|
1063
|
+
result: { goal: projectDesktopConversationStateToGoal(threadId, rawState) },
|
|
1064
|
+
}));
|
|
1065
|
+
return true;
|
|
1066
|
+
}
|
|
1018
1067
|
// Newer Litter snapshots keep materialized history in
|
|
1019
1068
|
// turnHistory.history.entitiesByKey while leaving the legacy top-level
|
|
1020
1069
|
// turns array empty or limited to only the current turn. The Desktop
|
|
@@ -1028,7 +1077,7 @@ function createDesktopIpcActionFollower({
|
|
|
1028
1077
|
canonicalHistoryThreadIds.add(threadId);
|
|
1029
1078
|
}
|
|
1030
1079
|
if (canonicalHistoryThreadIds.has(threadId)) {
|
|
1031
|
-
if (
|
|
1080
|
+
if (isThreadTurnStateProbeRequest(message)) {
|
|
1032
1081
|
const liveState = boundedDesktopLiveStateForThread(threadId, rawState);
|
|
1033
1082
|
sendApplicationResponse(JSON.stringify({
|
|
1034
1083
|
id: message.id,
|
|
@@ -1042,11 +1091,13 @@ function createDesktopIpcActionFollower({
|
|
|
1042
1091
|
return ownsDesktopCursor ? rejectDesktopTurnsCursor(message) : false;
|
|
1043
1092
|
}
|
|
1044
1093
|
const thread = projectDesktopConversationStateToThread(threadId, rawState, { now });
|
|
1045
|
-
//
|
|
1046
|
-
//
|
|
1047
|
-
//
|
|
1094
|
+
// While IPC remains connected, an explicitly active Desktop snapshot stays
|
|
1095
|
+
// authoritative through quiet tools, reasoning, approvals, and subagents.
|
|
1096
|
+
// Falling through to a second app-server here can replace a genuinely
|
|
1097
|
+
// running turn with stale idle history and make the phone clear Stop.
|
|
1048
1098
|
if (hasActiveProjectedTurn(thread)
|
|
1049
1099
|
&& isRawStateStaleForActiveRead(threadId)
|
|
1100
|
+
&& !hasResponsiveDesktopIpc()
|
|
1050
1101
|
&& !ownsDesktopCursor) {
|
|
1051
1102
|
staleYieldedThreadIds.add(threadId);
|
|
1052
1103
|
return false;
|
|
@@ -1058,6 +1109,7 @@ function createDesktopIpcActionFollower({
|
|
|
1058
1109
|
// echoing the raw Desktop conversationState alongside it doubled
|
|
1059
1110
|
// heavy threads past the relay frame limit for nothing.
|
|
1060
1111
|
thread,
|
|
1112
|
+
...(method === "thread/resume" ? { remodexDesktopIpcMirror: true } : {}),
|
|
1061
1113
|
};
|
|
1062
1114
|
if (!result) {
|
|
1063
1115
|
return ownsDesktopCursor ? rejectDesktopTurnsCursor(message) : false;
|
|
@@ -1080,21 +1132,6 @@ function createDesktopIpcActionFollower({
|
|
|
1080
1132
|
return true;
|
|
1081
1133
|
}
|
|
1082
1134
|
|
|
1083
|
-
function isDesktopLiveTurnStateSnapshotRequest(message) {
|
|
1084
|
-
if (readString(message?.method) !== "thread/turns/list"
|
|
1085
|
-
|| readString(message?.params?.cursor)
|
|
1086
|
-
|| message?.params?.remodexRequireCanonical === true) {
|
|
1087
|
-
return false;
|
|
1088
|
-
}
|
|
1089
|
-
if (message?.params?.remodexTurnStateOnly === true) {
|
|
1090
|
-
return true;
|
|
1091
|
-
}
|
|
1092
|
-
// Remodex iPhone 2.1 predates the explicit marker. Its running-state probe
|
|
1093
|
-
// has this unique request shape; actual history pages use limits 1 and 5.
|
|
1094
|
-
return Number(message?.params?.limit) === 8
|
|
1095
|
-
&& normalizeToken(readString(message?.params?.sortDirection) || "desc") === "desc";
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
1135
|
function buildDesktopLiveTurnStateResult(turns) {
|
|
1099
1136
|
const data = (Array.isArray(turns) ? turns : [])
|
|
1100
1137
|
.slice()
|
|
@@ -1156,6 +1193,10 @@ function createDesktopIpcActionFollower({
|
|
|
1156
1193
|
return now() - updatedAt > STALE_ACTIVE_READ_MAX_AGE_MS;
|
|
1157
1194
|
}
|
|
1158
1195
|
|
|
1196
|
+
function hasResponsiveDesktopIpc() {
|
|
1197
|
+
return ipc.hasRecentActivity(CONNECTED_IPC_ACTIVITY_LEASE_MS);
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1159
1200
|
function boundedDesktopLiveStateForThread(threadId, state) {
|
|
1160
1201
|
return boundedDesktopLiveState(
|
|
1161
1202
|
state,
|
|
@@ -1183,6 +1224,8 @@ function createDesktopIpcActionFollower({
|
|
|
1183
1224
|
const previousById = desktopLiveLifecycleByThreadId.get(threadId) || new Map();
|
|
1184
1225
|
const nextTurns = activeDesktopTurnDescriptors(liveState);
|
|
1185
1226
|
const nextById = new Map(nextTurns.map((turn) => [turn.id, turn]));
|
|
1227
|
+
const completedTurnIds = new Set();
|
|
1228
|
+
const startedTurnIds = new Set();
|
|
1186
1229
|
const {
|
|
1187
1230
|
previousTurnIds: continuityPreviousTurnIds,
|
|
1188
1231
|
nextTurnIds: continuityNextTurnIds,
|
|
@@ -1207,6 +1250,7 @@ function createDesktopIpcActionFollower({
|
|
|
1207
1250
|
threadId,
|
|
1208
1251
|
{ id: previous.id, status: terminalStatus }
|
|
1209
1252
|
)));
|
|
1253
|
+
completedTurnIds.add(previous.id);
|
|
1210
1254
|
}
|
|
1211
1255
|
for (const next of nextTurns) {
|
|
1212
1256
|
if (previousById.has(next.id)) {
|
|
@@ -1222,7 +1266,52 @@ function createDesktopIpcActionFollower({
|
|
|
1222
1266
|
? notificationWithTurnIdentityContinuity(startedNotification)
|
|
1223
1267
|
: startedNotification
|
|
1224
1268
|
));
|
|
1269
|
+
startedTurnIds.add(next.id);
|
|
1225
1270
|
}
|
|
1271
|
+
reannounceRemainingParallelTurn(
|
|
1272
|
+
threadId,
|
|
1273
|
+
previousById,
|
|
1274
|
+
nextTurns,
|
|
1275
|
+
completedTurnIds,
|
|
1276
|
+
startedTurnIds
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// iOS tracks one phone-visible active turn per thread. If parallel turn B was
|
|
1281
|
+
// the most recently announced turn and finishes while older A is still live,
|
|
1282
|
+
// B's completion clears that slot. Re-announce A as continuity so Stop and
|
|
1283
|
+
// running state stay attached to the existing run without advancing its
|
|
1284
|
+
// generation. A newly started replacement already performs this handoff.
|
|
1285
|
+
function reannounceRemainingParallelTurn(
|
|
1286
|
+
threadId,
|
|
1287
|
+
previousById,
|
|
1288
|
+
nextTurns,
|
|
1289
|
+
completedTurnIds,
|
|
1290
|
+
startedTurnIds
|
|
1291
|
+
) {
|
|
1292
|
+
const restorationTurnId = remainingParallelTurnRestorationId(
|
|
1293
|
+
previousById,
|
|
1294
|
+
nextTurns,
|
|
1295
|
+
completedTurnIds
|
|
1296
|
+
);
|
|
1297
|
+
if (!restorationTurnId || startedTurnIds.has(restorationTurnId)) {
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
const nextVisibleTurn = nextTurns.find((turn) => turn.id === restorationTurnId);
|
|
1301
|
+
sendApplicationResponse(JSON.stringify(notificationWithTurnIdentityContinuity(
|
|
1302
|
+
desktopLiveTurnLifecycleNotification("turn/started", threadId, nextVisibleTurn)
|
|
1303
|
+
)));
|
|
1304
|
+
}
|
|
1305
|
+
|
|
1306
|
+
function remainingParallelTurnRestorationId(previousById, nextTurns, completedTurnIds) {
|
|
1307
|
+
const previousVisibleTurn = [...previousById.values()].at(-1);
|
|
1308
|
+
if (!previousVisibleTurn || !completedTurnIds.has(previousVisibleTurn.id)) {
|
|
1309
|
+
return "";
|
|
1310
|
+
}
|
|
1311
|
+
const nextVisibleTurn = nextTurns.at(-1);
|
|
1312
|
+
return nextVisibleTurn && previousById.has(nextVisibleTurn.id)
|
|
1313
|
+
? nextVisibleTurn.id
|
|
1314
|
+
: "";
|
|
1226
1315
|
}
|
|
1227
1316
|
|
|
1228
1317
|
function syncProjectedConversationState(threadId, nextState, { isFullSnapshot = false } = {}) {
|
|
@@ -1239,10 +1328,13 @@ function createDesktopIpcActionFollower({
|
|
|
1239
1328
|
&& !canonicalHistoryReplacementSentThreadIds.has(threadId)) {
|
|
1240
1329
|
canonicalHistoryReplacementSentThreadIds.add(threadId);
|
|
1241
1330
|
conversationProjector.remove(threadId);
|
|
1242
|
-
//
|
|
1243
|
-
// history.
|
|
1244
|
-
//
|
|
1245
|
-
|
|
1331
|
+
// Project the bounded active tail before asking the phone for canonical
|
|
1332
|
+
// history. On reconnect this snapshot may contain output produced while
|
|
1333
|
+
// IPC was unavailable; silently seeding it would permanently eat that
|
|
1334
|
+
// content when canonical JSONL/rollout history is still behind.
|
|
1335
|
+
const bootstrapOutput = conversationProjector.project(threadId, liveState, {
|
|
1336
|
+
includeAllActiveTurns: true,
|
|
1337
|
+
});
|
|
1246
1338
|
sendApplicationResponse(JSON.stringify({
|
|
1247
1339
|
method: "thread/replaced",
|
|
1248
1340
|
params: {
|
|
@@ -1252,7 +1344,16 @@ function createDesktopIpcActionFollower({
|
|
|
1252
1344
|
remodexActionSource: DESKTOP_IPC_ACTION_SOURCE,
|
|
1253
1345
|
},
|
|
1254
1346
|
}));
|
|
1347
|
+
emitNormalizedReviewOverlays(
|
|
1348
|
+
threadId,
|
|
1349
|
+
normalizedLiveIndexesByThreadId.get(threadId)
|
|
1350
|
+
);
|
|
1255
1351
|
emitDesktopSnapshotLifecycleTransition(threadId, liveState);
|
|
1352
|
+
for (const notification of bootstrapOutput.notifications || []) {
|
|
1353
|
+
if (readString(notification?.method).startsWith("item/")) {
|
|
1354
|
+
sendApplicationResponse(JSON.stringify(notification));
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1256
1357
|
rememberDesktopLiveProjection(threadId, liveState);
|
|
1257
1358
|
return;
|
|
1258
1359
|
}
|
|
@@ -1262,6 +1363,10 @@ function createDesktopIpcActionFollower({
|
|
|
1262
1363
|
// baselines: preserve only turn lifecycle, then seed. Item diffs resume
|
|
1263
1364
|
// on subsequent patches and cannot replay hundreds of old rows.
|
|
1264
1365
|
emitDesktopSnapshotLifecycleTransition(threadId, liveState);
|
|
1366
|
+
emitNormalizedReviewOverlays(
|
|
1367
|
+
threadId,
|
|
1368
|
+
normalizedLiveIndexesByThreadId.get(threadId)
|
|
1369
|
+
);
|
|
1265
1370
|
conversationProjector.seed(threadId, liveState);
|
|
1266
1371
|
rememberDesktopLiveProjection(threadId, liveState);
|
|
1267
1372
|
return;
|
|
@@ -1271,6 +1376,10 @@ function createDesktopIpcActionFollower({
|
|
|
1271
1376
|
// is a source epoch change. Force a baseline + thread/replaced repair.
|
|
1272
1377
|
conversationProjector.remove(threadId);
|
|
1273
1378
|
}
|
|
1379
|
+
emitNormalizedReviewOverlays(
|
|
1380
|
+
threadId,
|
|
1381
|
+
normalizedLiveIndexesByThreadId.get(threadId)
|
|
1382
|
+
);
|
|
1274
1383
|
const output = conversationProjector.project(threadId, liveState);
|
|
1275
1384
|
if (resumedAfterStaleYield || output.type === "fullReplace" || output.type === "baseline") {
|
|
1276
1385
|
// fullReplace: synthesized turn ids just became real, stale rows must go.
|
|
@@ -1290,19 +1399,102 @@ function createDesktopIpcActionFollower({
|
|
|
1290
1399
|
},
|
|
1291
1400
|
}));
|
|
1292
1401
|
}
|
|
1293
|
-
|
|
1402
|
+
const outputNotifications = output.notifications || [];
|
|
1403
|
+
const previousById = desktopLiveLifecycleByThreadId.get(threadId) || new Map();
|
|
1404
|
+
const nextTurns = activeDesktopTurnDescriptors(liveState);
|
|
1405
|
+
const completedTurnIds = new Set(outputNotifications
|
|
1406
|
+
.filter((notification) => notification.method === "turn/completed")
|
|
1407
|
+
.map((notification) => readString(notification.params?.turnId))
|
|
1408
|
+
.filter(Boolean));
|
|
1409
|
+
const startedTurnIds = new Set(outputNotifications
|
|
1410
|
+
.filter((notification) => notification.method === "turn/started")
|
|
1411
|
+
.map((notification) => readString(notification.params?.turnId))
|
|
1412
|
+
.filter(Boolean));
|
|
1413
|
+
const parallelRestorationTurnId = remainingParallelTurnRestorationId(
|
|
1414
|
+
previousById,
|
|
1415
|
+
nextTurns,
|
|
1416
|
+
completedTurnIds
|
|
1417
|
+
);
|
|
1418
|
+
for (const notification of outputNotifications) {
|
|
1419
|
+
const notificationTurnId = readString(notification.params?.turnId);
|
|
1294
1420
|
const preservesTurnIdentity = notification.method === "turn/started"
|
|
1295
|
-
&&
|
|
1296
|
-
|
|
1297
|
-
);
|
|
1421
|
+
&& (parallelRestorationTurnId === notificationTurnId
|
|
1422
|
+
|| output.turnIdentityContinuityTurnIds?.includes(notificationTurnId));
|
|
1298
1423
|
const projectedNotification = preservesTurnIdentity
|
|
1299
1424
|
? notificationWithTurnIdentityContinuity(notification)
|
|
1300
1425
|
: notification;
|
|
1301
1426
|
sendApplicationResponse(JSON.stringify(projectedNotification));
|
|
1302
1427
|
}
|
|
1428
|
+
reannounceRemainingParallelTurn(
|
|
1429
|
+
threadId,
|
|
1430
|
+
previousById,
|
|
1431
|
+
nextTurns,
|
|
1432
|
+
completedTurnIds,
|
|
1433
|
+
startedTurnIds
|
|
1434
|
+
);
|
|
1303
1435
|
rememberDesktopLiveProjection(threadId, liveState);
|
|
1304
1436
|
}
|
|
1305
1437
|
|
|
1438
|
+
function emitNormalizedReviewOverlays(threadId, index) {
|
|
1439
|
+
if (!index || !Array.isArray(index.pendingReviewOverlays)) {
|
|
1440
|
+
return;
|
|
1441
|
+
}
|
|
1442
|
+
const overlays = index.pendingReviewOverlays.splice(0);
|
|
1443
|
+
const fingerprints = normalizedReviewFingerprintsByThreadId.get(threadId) || new Map();
|
|
1444
|
+
for (const overlay of overlays) {
|
|
1445
|
+
// Reviews on turns in the projected live tail may also be emitted by
|
|
1446
|
+
// projector output; that duplication is intentional. The projector only
|
|
1447
|
+
// re-emits items for active turns (and not after a silent reseed), so
|
|
1448
|
+
// suppressing the overlay here would permanently drop reviews on
|
|
1449
|
+
// completed tail turns. The phone upserts by reviewId, so duplicates
|
|
1450
|
+
// cost one redundant notification and nothing else.
|
|
1451
|
+
const item = overlay?.item;
|
|
1452
|
+
const review = item?.review && typeof item.review === "object" ? item.review : item;
|
|
1453
|
+
const reviewId = readString(item?.reviewId)
|
|
1454
|
+
|| readString(item?.id).replace(/^automatic-approval-review:/, "");
|
|
1455
|
+
const status = readString(review?.status);
|
|
1456
|
+
if (!reviewId || !status || !item?.action) {
|
|
1457
|
+
continue;
|
|
1458
|
+
}
|
|
1459
|
+
const fingerprint = createHash("sha256")
|
|
1460
|
+
.update(JSON.stringify({ status, item }))
|
|
1461
|
+
.digest("hex");
|
|
1462
|
+
if (fingerprints.get(reviewId) === fingerprint) {
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
// Refresh insertion order so the cache is a bounded per-thread LRU.
|
|
1466
|
+
fingerprints.delete(reviewId);
|
|
1467
|
+
fingerprints.set(reviewId, fingerprint);
|
|
1468
|
+
while (fingerprints.size > MAX_NORMALIZED_REVIEW_FINGERPRINTS_PER_THREAD) {
|
|
1469
|
+
const oldestReviewId = fingerprints.keys().next().value;
|
|
1470
|
+
fingerprints.delete(oldestReviewId);
|
|
1471
|
+
}
|
|
1472
|
+
sendApplicationResponse(JSON.stringify({
|
|
1473
|
+
method: normalizeToken(status) === "inprogress"
|
|
1474
|
+
? "item/autoApprovalReview/started"
|
|
1475
|
+
: "item/autoApprovalReview/completed",
|
|
1476
|
+
params: {
|
|
1477
|
+
threadId,
|
|
1478
|
+
turnId: overlay.turnId,
|
|
1479
|
+
reviewId,
|
|
1480
|
+
targetItemId: readString(item.targetItemId) || null,
|
|
1481
|
+
startedAtMs: item.startedAtMs ?? null,
|
|
1482
|
+
completedAtMs: item.completedAtMs ?? null,
|
|
1483
|
+
decisionSource: readString(item.decisionSource)
|
|
1484
|
+
|| readString(item?.event?.decision_source)
|
|
1485
|
+
|| null,
|
|
1486
|
+
review: cloneJSON(review),
|
|
1487
|
+
action: cloneJSON(item.action),
|
|
1488
|
+
remodexDesktopMirror: true,
|
|
1489
|
+
remodexDesktopIpcMirror: true,
|
|
1490
|
+
remodexGuardianRetrySupported: false,
|
|
1491
|
+
remodexActionSource: DESKTOP_IPC_ACTION_SOURCE,
|
|
1492
|
+
},
|
|
1493
|
+
}));
|
|
1494
|
+
}
|
|
1495
|
+
normalizedReviewFingerprintsByThreadId.set(threadId, fingerprints);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1306
1498
|
// Unopened chats only need run-state signals for the sidebar. Sending the
|
|
1307
1499
|
// projector's full bootstrap here would replay every historical item from
|
|
1308
1500
|
// every running Desktop chat onto the phone during a sidebar refresh.
|
|
@@ -1339,14 +1531,12 @@ function createDesktopIpcActionFollower({
|
|
|
1339
1531
|
nextActiveTurn
|
|
1340
1532
|
)));
|
|
1341
1533
|
announcedBackgroundTurnsByThreadId.set(threadId, nextActiveTurn);
|
|
1342
|
-
clearBackgroundDisconnectTimer(threadId);
|
|
1343
1534
|
}
|
|
1344
1535
|
}
|
|
1345
1536
|
|
|
1346
1537
|
function settleAnnouncedBackgroundTurn(threadId, status = "interrupted", turn = null) {
|
|
1347
1538
|
const announcedTurn = announcedBackgroundTurnsByThreadId.get(threadId);
|
|
1348
1539
|
if (!announcedTurn) {
|
|
1349
|
-
clearBackgroundDisconnectTimer(threadId);
|
|
1350
1540
|
return false;
|
|
1351
1541
|
}
|
|
1352
1542
|
const settledTurn = {
|
|
@@ -1361,36 +1551,9 @@ function createDesktopIpcActionFollower({
|
|
|
1361
1551
|
settledTurn
|
|
1362
1552
|
)));
|
|
1363
1553
|
announcedBackgroundTurnsByThreadId.delete(threadId);
|
|
1364
|
-
clearBackgroundDisconnectTimer(threadId);
|
|
1365
1554
|
return true;
|
|
1366
1555
|
}
|
|
1367
1556
|
|
|
1368
|
-
function scheduleBackgroundDisconnectSettlement(threadId) {
|
|
1369
|
-
if (!announcedBackgroundTurnsByThreadId.has(threadId)
|
|
1370
|
-
|| backgroundDisconnectTimersByThreadId.has(threadId)) {
|
|
1371
|
-
return;
|
|
1372
|
-
}
|
|
1373
|
-
const expectedTurnId = announcedBackgroundTurnsByThreadId.get(threadId)?.id;
|
|
1374
|
-
const timer = setTimeout(() => {
|
|
1375
|
-
backgroundDisconnectTimersByThreadId.delete(threadId);
|
|
1376
|
-
if (announcedBackgroundTurnsByThreadId.get(threadId)?.id !== expectedTurnId) {
|
|
1377
|
-
return;
|
|
1378
|
-
}
|
|
1379
|
-
settleAnnouncedBackgroundTurn(threadId, "interrupted");
|
|
1380
|
-
}, Math.max(0, backgroundDisconnectGraceMs));
|
|
1381
|
-
timer.unref?.();
|
|
1382
|
-
backgroundDisconnectTimersByThreadId.set(threadId, timer);
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
function clearBackgroundDisconnectTimer(threadId) {
|
|
1386
|
-
const timer = backgroundDisconnectTimersByThreadId.get(threadId);
|
|
1387
|
-
if (!timer) {
|
|
1388
|
-
return;
|
|
1389
|
-
}
|
|
1390
|
-
clearTimeout(timer);
|
|
1391
|
-
backgroundDisconnectTimersByThreadId.delete(threadId);
|
|
1392
|
-
}
|
|
1393
|
-
|
|
1394
1557
|
function syncThreadArchiveBroadcast(envelope) {
|
|
1395
1558
|
const params = envelope.params || {};
|
|
1396
1559
|
const threadId = readString(params.conversationId) || readString(params.conversation_id);
|
|
@@ -1410,6 +1573,7 @@ function createDesktopIpcActionFollower({
|
|
|
1410
1573
|
projectedLiveActiveTurnIdsByThreadId.delete(threadId);
|
|
1411
1574
|
desktopLiveLifecycleByThreadId.delete(threadId);
|
|
1412
1575
|
normalizedLiveIndexesByThreadId.delete(threadId);
|
|
1576
|
+
normalizedReviewFingerprintsByThreadId.delete(threadId);
|
|
1413
1577
|
conversationProjector.remove(threadId);
|
|
1414
1578
|
syncProjectedActions(threadId, []);
|
|
1415
1579
|
}
|
|
@@ -1531,7 +1695,14 @@ function createDesktopIpcActionFollower({
|
|
|
1531
1695
|
.then(() => resolveFollowerRequestParams(route))
|
|
1532
1696
|
.then(async (resolvedParams) => {
|
|
1533
1697
|
if (route.method === "thread-follower-start-turn") {
|
|
1534
|
-
|
|
1698
|
+
try {
|
|
1699
|
+
await syncDesktopOwnerRuntimeSettings(route.threadId, resolvedParams.turnStartParams);
|
|
1700
|
+
} catch (error) {
|
|
1701
|
+
// The actual turn has not reached Desktop yet. Even if the settings
|
|
1702
|
+
// request timed out after being applied, continuing through the local
|
|
1703
|
+
// app-server is safe because there is no Desktop turn to duplicate.
|
|
1704
|
+
throw markDeliveryFailureError(error);
|
|
1705
|
+
}
|
|
1535
1706
|
}
|
|
1536
1707
|
return {
|
|
1537
1708
|
resolvedParams,
|
|
@@ -1775,7 +1946,9 @@ function createDesktopIpcActionFollower({
|
|
|
1775
1946
|
return false;
|
|
1776
1947
|
}
|
|
1777
1948
|
const thread = projectDesktopConversationStateToThread(normalizedThreadId, liveState, { now });
|
|
1778
|
-
if (hasActiveProjectedTurn(thread)
|
|
1949
|
+
if (hasActiveProjectedTurn(thread)
|
|
1950
|
+
&& isRawStateStaleForActiveRead(normalizedThreadId)
|
|
1951
|
+
&& !hasResponsiveDesktopIpc()) {
|
|
1779
1952
|
staleYieldedThreadIds.add(normalizedThreadId);
|
|
1780
1953
|
return false;
|
|
1781
1954
|
}
|
|
@@ -1806,7 +1979,7 @@ function createDesktopIpcActionFollower({
|
|
|
1806
1979
|
if (hasActiveProjectedTurn(thread)) {
|
|
1807
1980
|
const updatedAt = rawStateUpdatedAtByThreadId.get(normalizedThreadId) || 0;
|
|
1808
1981
|
const hasNewerFallbackActivity = Number(fallbackActivityAt) > updatedAt;
|
|
1809
|
-
return
|
|
1982
|
+
return hasResponsiveDesktopIpc()
|
|
1810
1983
|
&& (!isRawStateStaleForActiveRead(normalizedThreadId) || !hasNewerFallbackActivity);
|
|
1811
1984
|
}
|
|
1812
1985
|
return !isRawStateStaleForActiveRead(normalizedThreadId);
|
|
@@ -1828,6 +2001,7 @@ function createDesktopIpcClient({
|
|
|
1828
2001
|
let socket = null;
|
|
1829
2002
|
let clientId = "";
|
|
1830
2003
|
let isConnecting = false;
|
|
2004
|
+
let lastActivityAt = 0;
|
|
1831
2005
|
let readBuffer = Buffer.alloc(0);
|
|
1832
2006
|
const pendingRequests = new Map();
|
|
1833
2007
|
const pendingDiscoveries = new Map();
|
|
@@ -1939,6 +2113,9 @@ function createDesktopIpcClient({
|
|
|
1939
2113
|
}
|
|
1940
2114
|
|
|
1941
2115
|
function handleData(chunk) {
|
|
2116
|
+
if (chunk.length > 0) {
|
|
2117
|
+
lastActivityAt = now();
|
|
2118
|
+
}
|
|
1942
2119
|
readBuffer = Buffer.concat([readBuffer, chunk]);
|
|
1943
2120
|
while (readBuffer.length >= FRAME_HEADER_BYTES) {
|
|
1944
2121
|
const frameLength = readBuffer.readUInt32LE(0);
|
|
@@ -2014,6 +2191,7 @@ function createDesktopIpcClient({
|
|
|
2014
2191
|
socket = null;
|
|
2015
2192
|
clientId = "";
|
|
2016
2193
|
isConnecting = false;
|
|
2194
|
+
lastActivityAt = 0;
|
|
2017
2195
|
readBuffer = Buffer.alloc(0);
|
|
2018
2196
|
for (const waiter of pendingRequests.values()) {
|
|
2019
2197
|
clearTimeout(waiter.timeout);
|
|
@@ -2052,6 +2230,11 @@ function createDesktopIpcClient({
|
|
|
2052
2230
|
isConnected() {
|
|
2053
2231
|
return Boolean(socket && !socket.destroyed && clientId);
|
|
2054
2232
|
},
|
|
2233
|
+
hasRecentActivity(maxAgeMs) {
|
|
2234
|
+
return Boolean(socket && !socket.destroyed && clientId)
|
|
2235
|
+
&& lastActivityAt > 0
|
|
2236
|
+
&& now() - lastActivityAt <= Math.max(0, Number(maxAgeMs) || 0);
|
|
2237
|
+
},
|
|
2055
2238
|
sendRequest,
|
|
2056
2239
|
sendDiscoveryRequest,
|
|
2057
2240
|
close,
|
|
@@ -2414,6 +2597,7 @@ function createNormalizedLiveIndex(state) {
|
|
|
2414
2597
|
entryIndexByTurnId,
|
|
2415
2598
|
turnIdByEntityKey,
|
|
2416
2599
|
activeTurnIds: new Set(),
|
|
2600
|
+
pendingReviewOverlays: [],
|
|
2417
2601
|
hasHistoryOutsideRawTurns: Array.from(normalizedTurnIds).some(
|
|
2418
2602
|
(turnId) => !rawTurnIds.has(turnId)
|
|
2419
2603
|
),
|
|
@@ -2423,6 +2607,14 @@ function createNormalizedLiveIndex(state) {
|
|
|
2423
2607
|
if (turn && isActiveRawTurn(turn)) {
|
|
2424
2608
|
index.activeTurnIds.add(entry.id);
|
|
2425
2609
|
}
|
|
2610
|
+
if (turn && entry.rawIndex == null) {
|
|
2611
|
+
for (const item of Array.isArray(turn.items) ? turn.items : []) {
|
|
2612
|
+
if (normalizeToken(item?.type) === "automaticapprovalreview") {
|
|
2613
|
+
const overlay = { turnId: entry.id, item };
|
|
2614
|
+
index.pendingReviewOverlays.push(overlay);
|
|
2615
|
+
}
|
|
2616
|
+
}
|
|
2617
|
+
}
|
|
2426
2618
|
}
|
|
2427
2619
|
return index;
|
|
2428
2620
|
}
|
|
@@ -2460,6 +2652,7 @@ function normalizedLiveIndexNeedsRebuild(change) {
|
|
|
2460
2652
|
|
|
2461
2653
|
function refreshTouchedNormalizedActiveTurns(index, state, change) {
|
|
2462
2654
|
const touchedTurnIds = new Set();
|
|
2655
|
+
const touchedReviewTurnIds = new Set();
|
|
2463
2656
|
for (const patch of Array.isArray(change?.patches) ? change.patches : []) {
|
|
2464
2657
|
const path = Array.isArray(patch?.path) ? patch.path : [];
|
|
2465
2658
|
if (path[0] === "turns" && Number.isInteger(path[1]) && path[2] === "status") {
|
|
@@ -2479,9 +2672,23 @@ function refreshTouchedNormalizedActiveTurns(index, state, change) {
|
|
|
2479
2672
|
touchedTurnIds.add(turnId);
|
|
2480
2673
|
}
|
|
2481
2674
|
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
|
|
2675
|
+
if ((path[0] === "turnHistory" || path[0] === "turn_history")
|
|
2676
|
+
&& path[1] === "history"
|
|
2677
|
+
&& (path[2] === "entitiesByKey" || path[2] === "entities_by_key")
|
|
2678
|
+
&& path[4] === "items") {
|
|
2679
|
+
const turnId = index.turnIdByEntityKey.get(readString(path[3]));
|
|
2680
|
+
const entryIndex = turnId ? index.entryIndexByTurnId.get(turnId) : null;
|
|
2681
|
+
const entry = entryIndex == null ? null : index.entries[entryIndex];
|
|
2682
|
+
const turn = entry ? resolveIndexedTurn(state, entry) : null;
|
|
2683
|
+
const item = Number.isInteger(path[5]) && Array.isArray(turn?.items)
|
|
2684
|
+
? turn.items[path[5]]
|
|
2685
|
+
: null;
|
|
2686
|
+
const isStructuralItemPatch = path.length <= 6 || path[6] === "type";
|
|
2687
|
+
if (turnId && (isStructuralItemPatch
|
|
2688
|
+
|| normalizeToken(item?.type) === "automaticapprovalreview")) {
|
|
2689
|
+
touchedReviewTurnIds.add(turnId);
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2485
2692
|
}
|
|
2486
2693
|
for (const turnId of touchedTurnIds) {
|
|
2487
2694
|
const entryIndex = index.entryIndexByTurnId.get(turnId);
|
|
@@ -2493,6 +2700,21 @@ function refreshTouchedNormalizedActiveTurns(index, state, change) {
|
|
|
2493
2700
|
index.activeTurnIds.delete(turnId);
|
|
2494
2701
|
}
|
|
2495
2702
|
}
|
|
2703
|
+
for (const turnId of touchedReviewTurnIds) {
|
|
2704
|
+
const entryIndex = index.entryIndexByTurnId.get(turnId);
|
|
2705
|
+
const entry = entryIndex == null ? null : index.entries[entryIndex];
|
|
2706
|
+
const turn = entry ? resolveIndexedTurn(state, entry) : null;
|
|
2707
|
+
if (!turn || entry?.rawIndex != null) {
|
|
2708
|
+
continue;
|
|
2709
|
+
}
|
|
2710
|
+
for (const item of Array.isArray(turn.items) ? turn.items : []) {
|
|
2711
|
+
if (normalizeToken(item?.type) !== "automaticapprovalreview") {
|
|
2712
|
+
continue;
|
|
2713
|
+
}
|
|
2714
|
+
const overlay = { turnId, item };
|
|
2715
|
+
index.pendingReviewOverlays.push(overlay);
|
|
2716
|
+
}
|
|
2717
|
+
}
|
|
2496
2718
|
}
|
|
2497
2719
|
|
|
2498
2720
|
function latestActiveRawTurn(state) {
|
|
@@ -2539,7 +2761,8 @@ function boundedDesktopLiveTurns(
|
|
|
2539
2761
|
if (orderedTurns.length <= 1) {
|
|
2540
2762
|
return normalizeBoundedTurnsForRuntime(
|
|
2541
2763
|
orderedTurns.map((turn, index) => withStableProjectedTurnId(turn, index)),
|
|
2542
|
-
state
|
|
2764
|
+
state,
|
|
2765
|
+
retainedTurnIds
|
|
2543
2766
|
);
|
|
2544
2767
|
}
|
|
2545
2768
|
|
|
@@ -2566,7 +2789,7 @@ function boundedDesktopLiveTurns(
|
|
|
2566
2789
|
const selectedTurns = Array.from(selectedIndexes)
|
|
2567
2790
|
.sort((left, right) => left - right)
|
|
2568
2791
|
.map((index) => withStableProjectedTurnId(orderedTurns[index], index));
|
|
2569
|
-
return normalizeBoundedTurnsForRuntime(selectedTurns, state);
|
|
2792
|
+
return normalizeBoundedTurnsForRuntime(selectedTurns, state, retainedTurnIds);
|
|
2570
2793
|
}
|
|
2571
2794
|
|
|
2572
2795
|
function boundedIndexedDesktopLiveTurns(state, index, nowValue, retainedTurnIds) {
|
|
@@ -2613,7 +2836,7 @@ function boundedIndexedDesktopLiveTurns(state, index, nowValue, retainedTurnIds)
|
|
|
2613
2836
|
return turnIdOf(turn) ? turn : { ...turn, id: entry.id };
|
|
2614
2837
|
})
|
|
2615
2838
|
.filter(Boolean);
|
|
2616
|
-
return normalizeBoundedTurnsForRuntime(selectedTurns, state);
|
|
2839
|
+
return normalizeBoundedTurnsForRuntime(selectedTurns, state, retainedTurnIds);
|
|
2617
2840
|
}
|
|
2618
2841
|
|
|
2619
2842
|
function resolveIndexedTurn(state, entry) {
|
|
@@ -2690,12 +2913,12 @@ function notificationWithTurnIdentityContinuity(notification) {
|
|
|
2690
2913
|
};
|
|
2691
2914
|
}
|
|
2692
2915
|
|
|
2693
|
-
function normalizeBoundedTurnsForRuntime(turns, state) {
|
|
2916
|
+
function normalizeBoundedTurnsForRuntime(turns, state, retainedTurnIds = new Set()) {
|
|
2694
2917
|
if (!isExplicitlyIdleDesktopRuntime(state)) {
|
|
2695
2918
|
return turns;
|
|
2696
2919
|
}
|
|
2697
2920
|
return turns.map((turn) => (
|
|
2698
|
-
isActiveRawTurn(turn)
|
|
2921
|
+
isActiveRawTurn(turn) && !retainedTurnIds.has(turnIdOf(turn))
|
|
2699
2922
|
? { ...turn, status: "completed" }
|
|
2700
2923
|
: turn
|
|
2701
2924
|
));
|