@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.
@@ -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-file-approval-decision"],
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
- followDesktopThread(threadId);
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
- Promise.resolve()
1910
- .then(() => resolveFollowerRequest(route))
1911
- .then(async (resolvedRequest) => {
1912
- if (route.method === "thread-follower-start-turn") {
1913
- try {
1914
- await syncDesktopOwnerRuntimeSettings(route.threadId, resolvedRequest.turnStartParams);
1915
- } catch (error) {
1916
- // The actual turn has not reached Desktop yet. Even if the settings
1917
- // request timed out after being applied, continuing through the local
1918
- // app-server is safe because there is no Desktop turn to duplicate.
1919
- throw markDeliveryFailureError(error);
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
- return {
1923
- resolvedRequest,
1924
- result: await ipc.sendRequest(route.method, resolvedRequest.params),
1925
- };
1926
- })
1927
- .then(({ resolvedRequest, result }) => {
1928
- const appServerResult = appServerResultForFollowerRequest(route.method, result);
1929
- if (route.method === "thread-follower-start-turn") {
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 collaborationMode = params.collaborationMode && typeof params.collaborationMode === "object"
1991
- ? cloneJSON(params.collaborationMode)
1992
- : null;
1993
- const collaborationSettings = collaborationMode?.settings;
1994
- const model = readString(params.model) || readString(collaborationSettings?.model);
1995
- const effort = readString(params.effort)
1996
- || readString(params.reasoningEffort)
1997
- || readString(collaborationSettings?.reasoning_effort)
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) || !hasNewerFallbackActivity);
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 = desktopApprovalDecisionForResponse(route.method, responseMessage?.result);
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) ? turn : { ...turn, id: entry.id };
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) {