@makerbi/remodex 3.1.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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,15 +1972,21 @@ function createDesktopIpcActionFollower({
1861
1972
  return null;
1862
1973
  }
1863
1974
 
1975
+ if (method === "thread/settings/update") {
1976
+ const threadSettings = normalizeThreadSettingsUpdate(params);
1977
+ return {
1978
+ threadId,
1979
+ method: "thread-follower-update-thread-settings",
1980
+ params: { conversationId: threadId, threadSettings },
1981
+ };
1982
+ }
1864
1983
  if (method === "turn/start") {
1865
1984
  return {
1866
1985
  threadId,
1867
1986
  method: "thread-follower-start-turn",
1868
- params: {
1869
- conversationId: threadId,
1870
- senderRequestId: requestId,
1871
- turnStartParams: params,
1872
- },
1987
+ params: { conversationId: threadId },
1988
+ senderRequestId: requestId,
1989
+ turnStartParams: params,
1873
1990
  };
1874
1991
  }
1875
1992
  if (method === "turn/steer") {
@@ -1879,6 +1996,7 @@ function createDesktopIpcActionFollower({
1879
1996
  params: {
1880
1997
  conversationId: threadId,
1881
1998
  input: Array.isArray(params.input) ? params.input : [],
1999
+ ...Object.fromEntries(["clientUserMessageId", "additionalContext", "toolOutput"].filter((key) => hasOwn(params, key)).map((key) => [key, cloneJSON(params[key])])),
1882
2000
  expectedTurnId: readString(params.expectedTurnId) || readString(params.expected_turn_id),
1883
2001
  },
1884
2002
  };
@@ -1889,7 +2007,8 @@ function createDesktopIpcActionFollower({
1889
2007
  method: "thread-follower-interrupt-turn",
1890
2008
  params: {
1891
2009
  conversationId: threadId,
1892
- turnId: readString(params.turnId) || readString(params.turn_id),
2010
+ mode: "user-stop",
2011
+ expectedTurnId: readString(params.turnId) || readString(params.turn_id),
1893
2012
  },
1894
2013
  };
1895
2014
  }
@@ -1907,30 +2026,39 @@ function createDesktopIpcActionFollower({
1907
2026
  }
1908
2027
 
1909
2028
  function submitDesktopFollowerRequest(route, originalMessage) {
1910
- Promise.resolve()
1911
- .then(() => resolveFollowerRequestParams(route))
1912
- .then(async (resolvedParams) => {
1913
- if (route.method === "thread-follower-start-turn") {
1914
- try {
1915
- await syncDesktopOwnerRuntimeSettings(route.threadId, resolvedParams.turnStartParams);
1916
- } catch (error) {
1917
- // The actual turn has not reached Desktop yet. Even if the settings
1918
- // request timed out after being applied, continuing through the local
1919
- // app-server is safe because there is no Desktop turn to duplicate.
1920
- throw markDeliveryFailureError(error);
1921
- }
2029
+ enqueueMutation(route.threadId, async () => {
2030
+ const revisionBefore = runtimeSettingsStore?.get?.(route.threadId)?.revision;
2031
+ const resolvedRequest = await resolveFollowerRequest(route);
2032
+ if (route.method === "thread-follower-start-turn") {
2033
+ // A rejected settings update does not relinquish the Desktop writer.
2034
+ // Propagate it without turning a timeout into local delivery failure.
2035
+ try {
2036
+ await syncDesktopOwnerRuntimeSettings(route.threadId, resolvedRequest.turnStartParams);
2037
+ } catch (error) {
2038
+ // Failure to deliver settings does not prove that a turn sent locally
2039
+ // would be safe. Only the start-turn route can authorize that fallback.
2040
+ throw new Error(error.message, { cause: error });
1922
2041
  }
1923
- return {
1924
- resolvedParams,
1925
- result: await ipc.sendRequest(route.method, resolvedParams),
1926
- };
1927
- })
1928
- .then(({ resolvedParams, result }) => {
1929
- const appServerResult = appServerResultForFollowerRequest(route.method, result);
1930
- 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) {
1931
2059
  commitPhoneRuntimeSettings(
1932
2060
  route.threadId,
1933
- resolvedParams.turnStartParams,
2061
+ resolvedRequest.turnStartParams,
1934
2062
  readTurnIdFromAppServerResult(appServerResult)
1935
2063
  );
1936
2064
  }
@@ -1963,7 +2091,7 @@ function createDesktopIpcActionFollower({
1963
2091
  }
1964
2092
 
1965
2093
  function appServerResultForFollowerRequest(method, result) {
1966
- if (method === "thread-follower-start-turn"
2094
+ if ((method === "thread-follower-start-turn" || method === "thread-follower-steer-turn")
1967
2095
  && result
1968
2096
  && typeof result === "object"
1969
2097
  && !Array.isArray(result)
@@ -1988,28 +2116,17 @@ function createDesktopIpcActionFollower({
1988
2116
  const params = turnStartParams && typeof turnStartParams === "object"
1989
2117
  ? turnStartParams
1990
2118
  : {};
1991
- const collaborationMode = params.collaborationMode && typeof params.collaborationMode === "object"
1992
- ? cloneJSON(params.collaborationMode)
1993
- : null;
1994
- const collaborationSettings = collaborationMode?.settings;
1995
- const model = readString(params.model) || readString(collaborationSettings?.model);
1996
- const effort = readString(params.effort)
1997
- || readString(params.reasoningEffort)
1998
- || readString(collaborationSettings?.reasoning_effort)
1999
- || readString(collaborationSettings?.reasoningEffort);
2000
- if (!model && !effort && !collaborationMode) {
2001
- return;
2002
- }
2003
-
2119
+ const patch = runtimeSettingsPatch(params);
2120
+ const threadSettings = {
2121
+ ...(patch.model ? { model: patch.model } : {}),
2122
+ ...(hasOwn(patch, "reasoningEffort") ? { effort: patch.reasoningEffort } : {}),
2123
+ ...(hasOwn(patch, "serviceTier") ? { serviceTier: patch.serviceTier } : {}),
2124
+ ...(params.collaborationMode ? { collaborationMode: cloneJSON(params.collaborationMode) } : {}),
2125
+ };
2126
+ if (Object.keys(threadSettings).length === 0) return;
2004
2127
  await ipc.sendRequest("thread-follower-update-thread-settings", {
2005
2128
  conversationId: threadId,
2006
- threadSettings: {
2007
- ...(model ? { model } : {}),
2008
- effort: effort || null,
2009
- // turn/start omission is the app-server representation of Normal speed.
2010
- serviceTier: readString(params.serviceTier) || readString(params.service_tier) || null,
2011
- ...(collaborationMode ? { collaborationMode } : {}),
2012
- },
2129
+ threadSettings,
2013
2130
  });
2014
2131
  }
2015
2132
 
@@ -2026,19 +2143,34 @@ function createDesktopIpcActionFollower({
2026
2143
 
2027
2144
  // Desktop-followed turn starts must apply the same param normalization as
2028
2145
  // requests forwarded straight to the local app-server.
2029
- async function resolveFollowerRequestParams(route) {
2146
+ async function resolveFollowerRequest(route) {
2030
2147
  if (route.method !== "thread-follower-start-turn") {
2031
- return route.params;
2148
+ return {
2149
+ params: route.params,
2150
+ turnStartParams: null,
2151
+ };
2032
2152
  }
2033
2153
 
2034
2154
  const normalized = await Promise.resolve(
2035
- normalizeTurnStartParams(cloneJSON(route.params.turnStartParams))
2155
+ normalizeTurnStartParams(cloneJSON(route.turnStartParams))
2036
2156
  );
2037
2157
  const turnStartParams = normalized && typeof normalized === "object" && !Array.isArray(normalized)
2038
2158
  ? normalized
2039
- : route.params.turnStartParams;
2159
+ : route.turnStartParams;
2160
+ const request = cloneJSON(turnStartParams);
2161
+ if (!readString(request.clientUserMessageId)) {
2162
+ request.clientUserMessageId = route.senderRequestId;
2163
+ }
2040
2164
  return {
2041
- ...route.params,
2165
+ params: {
2166
+ ...route.params,
2167
+ turnStart: {
2168
+ request,
2169
+ context: {
2170
+ inheritThreadSettings: true,
2171
+ },
2172
+ },
2173
+ },
2042
2174
  turnStartParams,
2043
2175
  };
2044
2176
  }
@@ -2121,6 +2253,7 @@ function createDesktopIpcActionFollower({
2121
2253
  rawStatesByThreadId.set(threadId, nextState);
2122
2254
  rawStateUpdatedAtByThreadId.set(threadId, now());
2123
2255
  rebuildNormalizedLiveIndex(threadId, nextState);
2256
+ notifyActivityState(threadId, nextState);
2124
2257
  if (baselineState && typeof baselineState === "object"
2125
2258
  && !backgroundOnlyThreadIds.has(threadId)) {
2126
2259
  const liveState = boundedDesktopLiveState(
@@ -2143,6 +2276,7 @@ function createDesktopIpcActionFollower({
2143
2276
 
2144
2277
  return {
2145
2278
  observeInbound,
2279
+ observeThreadListResponse,
2146
2280
  stopAll,
2147
2281
  // True while this thread has live Desktop-owned IPC state mirrored to the
2148
2282
  // phone; used to keep fallback mirrors (rollout tail) silent.
@@ -2174,7 +2308,7 @@ function createDesktopIpcActionFollower({
2174
2308
  // has actually moved recently. Keep hasLiveThreadState's broader meaning
2175
2309
  // for callers that need cached/idle Desktop state, but expose this explicit
2176
2310
  // lease check for source arbitration.
2177
- hasFreshLiveThreadState(threadId, { fallbackActivityAt = 0 } = {}) {
2311
+ hasFreshLiveThreadState(threadId, { fallbackActivityAt = 0, probeFallbackActivity = false } = {}) {
2178
2312
  const normalizedThreadId = readString(threadId);
2179
2313
  if (pendingSnapshotsByThreadId.has(normalizedThreadId)) {
2180
2314
  return Boolean(normalizedThreadId);
@@ -2195,8 +2329,11 @@ function createDesktopIpcActionFollower({
2195
2329
  if (hasActiveProjectedTurn(thread)) {
2196
2330
  const updatedAt = rawStateUpdatedAtByThreadId.get(normalizedThreadId) || 0;
2197
2331
  const hasNewerFallbackActivity = Number(fallbackActivityAt) > updatedAt;
2332
+ // Let a stale stream check file metadata before deciding who emits.
2333
+ // The subsequent check with the real mtime still protects quiet work.
2198
2334
  return hasResponsiveDesktopIpc()
2199
- && (!isRawStateStaleForActiveRead(normalizedThreadId) || !hasNewerFallbackActivity);
2335
+ && (!isRawStateStaleForActiveRead(normalizedThreadId)
2336
+ || (!probeFallbackActivity && !hasNewerFallbackActivity));
2200
2337
  }
2201
2338
  return !isRawStateStaleForActiveRead(normalizedThreadId);
2202
2339
  },
@@ -2502,6 +2639,22 @@ function desktopFollowerPayloadForResponse(route, responseMessage) {
2502
2639
  return null;
2503
2640
  }
2504
2641
 
2642
+ if (route.method === "item/permissions/requestApproval") {
2643
+ const result = responseMessage?.result;
2644
+ if (!result?.permissions || typeof result.permissions !== "object"
2645
+ || Array.isArray(result.permissions) || !["turn", "session"].includes(result.scope)) {
2646
+ return null;
2647
+ }
2648
+ return {
2649
+ method,
2650
+ params: {
2651
+ conversationId: route.threadId,
2652
+ requestId: route.desktopRequestId ?? route.requestId,
2653
+ response: cloneJSON(result),
2654
+ },
2655
+ };
2656
+ }
2657
+
2505
2658
  if (route.method === "item/tool/requestUserInput") {
2506
2659
  const answers = responseMessage?.result?.answers;
2507
2660
  if (!answers || typeof answers !== "object" || Array.isArray(answers)) {
@@ -2520,7 +2673,7 @@ function desktopFollowerPayloadForResponse(route, responseMessage) {
2520
2673
  };
2521
2674
  }
2522
2675
 
2523
- const decision = desktopApprovalDecisionForResponse(route.method, responseMessage?.result);
2676
+ const decision = readString(responseMessage?.result?.decision);
2524
2677
  if (!APPROVAL_DECISIONS.has(decision)) {
2525
2678
  return null;
2526
2679
  }
@@ -2529,53 +2682,12 @@ function desktopFollowerPayloadForResponse(route, responseMessage) {
2529
2682
  method,
2530
2683
  params: {
2531
2684
  conversationId: route.threadId,
2532
- requestId: route.requestId,
2685
+ requestId: route.desktopRequestId ?? route.requestId,
2533
2686
  decision,
2534
2687
  },
2535
2688
  };
2536
2689
  }
2537
2690
 
2538
- function desktopApprovalDecisionForResponse(method, result) {
2539
- const explicitDecision = readString(result?.decision);
2540
- if (explicitDecision) {
2541
- return explicitDecision;
2542
- }
2543
-
2544
- if (method !== "item/permissions/requestApproval") {
2545
- return "";
2546
- }
2547
-
2548
- // Permission approvals use a grant payload on app-server, while Desktop IPC
2549
- // currently exposes only decision-style follower replies.
2550
- return hasGrantedPermission(result?.permissions) ? "accept" : "decline";
2551
- }
2552
-
2553
- function hasGrantedPermission(value) {
2554
- if (!value || typeof value !== "object" || Array.isArray(value)) {
2555
- return false;
2556
- }
2557
-
2558
- if (Object.keys(value).length === 0) {
2559
- return false;
2560
- }
2561
-
2562
- return Object.values(value).some((entry) => {
2563
- if (entry == null) {
2564
- return false;
2565
- }
2566
- if (typeof entry === "boolean") {
2567
- return entry;
2568
- }
2569
- if (Array.isArray(entry)) {
2570
- return entry.length > 0;
2571
- }
2572
- if (typeof entry === "object") {
2573
- return Object.keys(entry).length > 0;
2574
- }
2575
- return true;
2576
- });
2577
- }
2578
-
2579
2691
  function projectPendingDesktopActions(threadId, conversationState) {
2580
2692
  const requests = Array.isArray(conversationState?.requests) ? conversationState.requests : [];
2581
2693
  return requests
@@ -3000,6 +3112,84 @@ function boundedDesktopLiveState(
3000
3112
  };
3001
3113
  }
3002
3114
 
3115
+ function boundedDesktopActivityState(state) {
3116
+ const rawState = state && typeof state === "object" ? state : {};
3117
+ return {
3118
+ title: readString(rawState.title) || readString(rawState.name),
3119
+ cwd: readString(rawState.cwd) || readString(rawState.current_working_directory),
3120
+ threadRuntimeStatus: compactDesktopRuntimeStatus(rawState.threadRuntimeStatus),
3121
+ status: compactDesktopRuntimeStatus(rawState.status),
3122
+ hasUnreadTurn: rawState.hasUnreadTurn ?? rawState.has_unread_turn,
3123
+ unreadMessageCount: rawState.unreadMessageCount ?? rawState.unread_message_count,
3124
+ requests: (Array.isArray(rawState.requests) ? rawState.requests : []).map((request) => ({
3125
+ id: request?.id,
3126
+ method: readString(request?.method),
3127
+ completed: request?.completed === true,
3128
+ })),
3129
+ turns: (Array.isArray(rawState.turns) ? rawState.turns : []).map(compactDesktopActivityTurn),
3130
+ };
3131
+ }
3132
+
3133
+ function compactDesktopRuntimeStatus(status) {
3134
+ if (status && typeof status === "object" && !Array.isArray(status)) {
3135
+ return {
3136
+ type: readString(status.type),
3137
+ activeFlags: Array.isArray(status.activeFlags) ? status.activeFlags.filter((flag) => (
3138
+ flag === "waitingOnApproval" || flag === "waitingOnUserInput"
3139
+ )) : [],
3140
+ };
3141
+ }
3142
+ return readString(status);
3143
+ }
3144
+
3145
+ function compactDesktopActivityTurn(turn) {
3146
+ return {
3147
+ id: readString(turn?.id),
3148
+ turnId: readString(turn?.turnId),
3149
+ turn_id: readString(turn?.turn_id),
3150
+ status: readString(turn?.status),
3151
+ error: turn?.error ? true : null,
3152
+ startedAt: finiteTimestamp(turn?.startedAt),
3153
+ started_at: finiteTimestamp(turn?.started_at),
3154
+ completedAt: finiteTimestamp(turn?.completedAt),
3155
+ completed_at: finiteTimestamp(turn?.completed_at),
3156
+ turnStartedAtMs: finiteTimestamp(turn?.turnStartedAtMs),
3157
+ turn_started_at_ms: finiteTimestamp(turn?.turn_started_at_ms),
3158
+ turnCompletedAtMs: finiteTimestamp(turn?.turnCompletedAtMs),
3159
+ turn_completed_at_ms: finiteTimestamp(turn?.turn_completed_at_ms),
3160
+ startedAtMs: finiteTimestamp(turn?.startedAtMs ?? turn?.started_at_ms),
3161
+ completedAtMs: finiteTimestamp(turn?.completedAtMs ?? turn?.completed_at_ms),
3162
+ items: compactLatestActivityItems(turn),
3163
+ };
3164
+ }
3165
+
3166
+ function compactLatestActivityItems(turn) {
3167
+ const items = Array.isArray(turn?.items) ? turn.items : [];
3168
+ // Token patches must not copy the growing item history. A small tail is
3169
+ // enough for a current semantic label; absence remains unknown.
3170
+ for (let index = items.length - 1; index >= Math.max(0, items.length - 32); index -= 1) {
3171
+ const item = items[index];
3172
+ if (!projectSemanticItem(item)) {
3173
+ continue;
3174
+ }
3175
+ return [{
3176
+ id: readString(item?.id),
3177
+ itemId: readString(item?.itemId),
3178
+ item_id: readString(item?.item_id),
3179
+ type: readString(item?.type),
3180
+ }];
3181
+ }
3182
+ return [];
3183
+ }
3184
+
3185
+ function finiteTimestamp(value) {
3186
+ if (value == null || value === "") {
3187
+ return null;
3188
+ }
3189
+ const number = Number(value);
3190
+ return Number.isFinite(number) ? number : null;
3191
+ }
3192
+
3003
3193
  function boundedDesktopLiveTurns(
3004
3194
  state,
3005
3195
  nowValue = Date.now(),
@@ -3049,7 +3239,7 @@ function boundedDesktopLiveTurns(
3049
3239
  return normalizeBoundedTurnsForRuntime(selectedTurns, state, retainedTurnIds);
3050
3240
  }
3051
3241
 
3052
- function boundedIndexedDesktopLiveTurns(state, index, nowValue, retainedTurnIds) {
3242
+ function boundedIndexedDesktopLiveTurns(state, index, nowValue, retainedTurnIds, canonicalActivity = false) {
3053
3243
  if (index.entries.length === 0) {
3054
3244
  return [];
3055
3245
  }
@@ -3090,10 +3280,11 @@ function boundedIndexedDesktopLiveTurns(state, index, nowValue, retainedTurnIds)
3090
3280
  if (!turn) {
3091
3281
  return null;
3092
3282
  }
3093
- return turnIdOf(turn) ? turn : { ...turn, id: entry.id };
3283
+ return turnIdOf(turn) || (canonicalActivity && entry.rawIndex != null)
3284
+ ? turn : { ...turn, id: entry.id };
3094
3285
  })
3095
3286
  .filter(Boolean);
3096
- return normalizeBoundedTurnsForRuntime(selectedTurns, state, retainedTurnIds);
3287
+ return canonicalActivity ? selectedTurns : normalizeBoundedTurnsForRuntime(selectedTurns, state, retainedTurnIds);
3097
3288
  }
3098
3289
 
3099
3290
  function resolveIndexedTurn(state, entry) {