@makerbi/remodex 2.0.0 → 2.0.1

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/bin/remodex.js CHANGED
@@ -164,7 +164,7 @@ async function main({
164
164
  ok: true,
165
165
  currentVersion: version,
166
166
  plistPath: result?.plistPath,
167
- pairingSession: result?.pairingSession,
167
+ pairingSession: sanitizePairingSessionForOutput(result?.pairingSession),
168
168
  });
169
169
  return;
170
170
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makerbi/remodex",
3
- "version": "2.0.0",
3
+ "version": "2.0.1",
4
4
  "description": "Local bridge between Codex and the Remodex mobile app. Run `remodex up` to start.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -7,10 +7,12 @@
7
7
  const net = require("net");
8
8
  const os = require("os");
9
9
  const path = require("path");
10
+ const { buildApplyPatchFileChangeItem } = require("./apply-patch-changes");
10
11
 
11
12
  const FRAME_HEADER_BYTES = 4;
12
13
  const MAX_FRAME_BYTES = 256 * 1024 * 1024;
13
14
  const REQUEST_TIMEOUT_MS = 10_000;
15
+ const TURN_COMPLETION_IDLE_MS = 3_500;
14
16
  const DESKTOP_IPC_ACTION_SOURCE = "desktop-ipc-action-follower";
15
17
  const DESKTOP_RESUME_METHODS = new Set(["thread/read", "thread/resume"]);
16
18
  const ACTION_METHODS = new Set([
@@ -44,6 +46,9 @@ function createDesktopIpcActionFollower({
44
46
  netModule = net,
45
47
  now = () => Date.now(),
46
48
  requestTimeoutMs = REQUEST_TIMEOUT_MS,
49
+ turnCompletionIdleMs = TURN_COMPLETION_IDLE_MS,
50
+ setTimeoutFn = setTimeout,
51
+ clearTimeoutFn = clearTimeout,
47
52
  } = {}) {
48
53
  const ipc = createDesktopIpcClient({
49
54
  socketPath,
@@ -56,6 +61,9 @@ function createDesktopIpcActionFollower({
56
61
  });
57
62
  const rawStatesByThreadId = new Map();
58
63
  const assistantMessageTextsByThreadId = new Map();
64
+ const mirroredActivityKeysByThreadId = new Map();
65
+ const mirroredUserMessageKeysByThreadId = new Map();
66
+ const activeDesktopTurnsByThreadId = new Map();
59
67
  const pendingRoutesByRequestId = new Map();
60
68
  const activeThreadIds = new Set();
61
69
  const recoveringThreadIds = new Set();
@@ -87,6 +95,9 @@ function createDesktopIpcActionFollower({
87
95
  function stopAll() {
88
96
  rawStatesByThreadId.clear();
89
97
  assistantMessageTextsByThreadId.clear();
98
+ mirroredActivityKeysByThreadId.clear();
99
+ mirroredUserMessageKeysByThreadId.clear();
100
+ clearAllDesktopTurnCompletionTimers();
90
101
  pendingRoutesByRequestId.clear();
91
102
  activeThreadIds.clear();
92
103
  recoveringThreadIds.clear();
@@ -135,13 +146,16 @@ function createDesktopIpcActionFollower({
135
146
  }
136
147
 
137
148
  rawStatesByThreadId.set(threadId, nextState);
138
- syncProjectedAssistantDeltas(threadId, previousState, nextState);
149
+ syncProjectedLiveState(threadId, previousState, nextState);
139
150
  syncProjectedActions(threadId, projectPendingDesktopActions(threadId, nextState));
140
151
  }
141
152
 
142
153
  function onDisconnect() {
143
154
  rawStatesByThreadId.clear();
144
155
  assistantMessageTextsByThreadId.clear();
156
+ mirroredActivityKeysByThreadId.clear();
157
+ mirroredUserMessageKeysByThreadId.clear();
158
+ clearAllDesktopTurnCompletionTimers();
145
159
  pendingRoutesByRequestId.clear();
146
160
  recoveringThreadIds.clear();
147
161
  queuedChangesByThreadId.clear();
@@ -278,14 +292,21 @@ function createDesktopIpcActionFollower({
278
292
  }
279
293
 
280
294
  rawStatesByThreadId.set(threadId, nextState);
281
- syncProjectedAssistantDeltas(threadId, baselineState, nextState);
295
+ syncProjectedLiveState(threadId, baselineState, nextState);
282
296
  syncProjectedActions(threadId, projectPendingDesktopActions(threadId, nextState));
283
297
  }
284
298
 
299
+ function syncProjectedLiveState(threadId, previousState, nextState) {
300
+ syncProjectedAssistantDeltas(threadId, previousState, nextState);
301
+ syncProjectedDesktopActivities(threadId, nextState);
302
+ }
303
+
285
304
  function syncProjectedAssistantDeltas(threadId, previousState, nextState) {
305
+ refreshTrackedDesktopTurnState(threadId, nextState);
286
306
  const previousTexts = assistantMessageTextsByThreadId.get(threadId);
287
307
  if (!previousTexts && !previousState) {
288
308
  assistantMessageTextsByThreadId.set(threadId, snapshotAssistantMessageTexts(nextState));
309
+ syncProjectedDesktopTurnCompletions(threadId, nextState);
289
310
  return;
290
311
  }
291
312
 
@@ -297,13 +318,179 @@ function createDesktopIpcActionFollower({
297
318
  );
298
319
  if (notifications.length === 0) {
299
320
  assistantMessageTextsByThreadId.set(threadId, snapshotAssistantMessageTexts(nextState));
321
+ syncProjectedDesktopTurnCompletions(threadId, nextState);
300
322
  return;
301
323
  }
302
324
 
303
- for (const notification of notifications) {
325
+ const activeDeltaTurnIds = new Set(
326
+ notifications
327
+ .map((notification) => readString(notification?.params?.turnId) || readString(notification?.params?.turn_id))
328
+ .filter(Boolean)
329
+ );
330
+ const userNotifications = projectDesktopUserMessageNotifications(
331
+ threadId,
332
+ nextState,
333
+ mirroredUserMessageKeysForThread(threadId),
334
+ activeDeltaTurnIds
335
+ );
336
+
337
+ // Desktop IPC state can report assistant text growth before rollout replay catches
338
+ // up with the user prelude. Emit the opening prompt first to avoid mobile row jumps.
339
+ for (const notification of [...userNotifications, ...notifications]) {
304
340
  sendApplicationResponse(JSON.stringify(notification));
305
341
  }
342
+ for (const turnId of activeDeltaTurnIds) {
343
+ noteDesktopIpcTurnActivity(threadId, turnId, nextState);
344
+ }
306
345
  assistantMessageTextsByThreadId.set(threadId, snapshotAssistantMessageTexts(nextState));
346
+ syncProjectedDesktopTurnCompletions(threadId, nextState);
347
+ }
348
+
349
+ function syncProjectedDesktopActivities(threadId, nextState) {
350
+ refreshTrackedDesktopTurnState(threadId, nextState);
351
+ const activityKeys = mirroredActivityKeysForThread(threadId);
352
+ const notifications = projectDesktopActivityNotifications(threadId, nextState, activityKeys);
353
+ if (notifications.length === 0) {
354
+ syncProjectedDesktopTurnCompletions(threadId, nextState);
355
+ return;
356
+ }
357
+
358
+ const activeActivityTurnIds = new Set(
359
+ notifications
360
+ .map((notification) => readString(notification?.params?.turnId) || readString(notification?.params?.turn_id))
361
+ .filter(Boolean)
362
+ );
363
+ const userNotifications = projectDesktopUserMessageNotifications(
364
+ threadId,
365
+ nextState,
366
+ mirroredUserMessageKeysForThread(threadId),
367
+ activeActivityTurnIds
368
+ );
369
+
370
+ // Tool-call snapshots are independent from assistant text deltas. Emit them
371
+ // through the same app-server event names rollout mirroring uses so iOS keeps
372
+ // showing active tool rows while the Mac-owned run is still executing.
373
+ for (const notification of [...userNotifications, ...notifications]) {
374
+ sendApplicationResponse(JSON.stringify(notification));
375
+ }
376
+ for (const turnId of activeActivityTurnIds) {
377
+ noteDesktopIpcTurnActivity(threadId, turnId, nextState);
378
+ }
379
+ syncProjectedDesktopTurnCompletions(threadId, nextState);
380
+ }
381
+
382
+ function mirroredUserMessageKeysForThread(threadId) {
383
+ let keys = mirroredUserMessageKeysByThreadId.get(threadId);
384
+ if (!keys) {
385
+ keys = new Set();
386
+ mirroredUserMessageKeysByThreadId.set(threadId, keys);
387
+ }
388
+ return keys;
389
+ }
390
+
391
+ function mirroredActivityKeysForThread(threadId) {
392
+ let keys = mirroredActivityKeysByThreadId.get(threadId);
393
+ if (!keys) {
394
+ keys = new Set();
395
+ mirroredActivityKeysByThreadId.set(threadId, keys);
396
+ }
397
+ return keys;
398
+ }
399
+
400
+ function noteDesktopIpcTurnActivity(threadId, turnId, latestState) {
401
+ if (!turnId || turnCompletionIdleMs <= 0) {
402
+ return;
403
+ }
404
+
405
+ let turns = activeDesktopTurnsByThreadId.get(threadId);
406
+ if (!turns) {
407
+ turns = new Map();
408
+ activeDesktopTurnsByThreadId.set(threadId, turns);
409
+ }
410
+
411
+ const existing = turns.get(turnId);
412
+ if (existing?.timer) {
413
+ clearTimeoutFn(existing.timer);
414
+ }
415
+
416
+ const entry = { latestState, timer: null };
417
+ turns.set(turnId, entry);
418
+ scheduleDesktopIpcTurnIdleCompletion(threadId, turnId, entry);
419
+ }
420
+
421
+ function scheduleDesktopIpcTurnIdleCompletion(threadId, turnId, entry) {
422
+ entry.timer = setTimeoutFn(() => {
423
+ const currentTurns = activeDesktopTurnsByThreadId.get(threadId);
424
+ const currentEntry = currentTurns?.get(turnId);
425
+ if (!currentEntry) {
426
+ return;
427
+ }
428
+
429
+ if (hasOpenDesktopRequestForTurn(currentEntry.latestState, turnId)
430
+ || hasActiveDesktopActivityForTurn(currentEntry.latestState, turnId)) {
431
+ scheduleDesktopIpcTurnIdleCompletion(threadId, turnId, currentEntry);
432
+ return;
433
+ }
434
+
435
+ completeDesktopIpcTurn(threadId, turnId);
436
+ }, turnCompletionIdleMs);
437
+ entry.timer.unref?.();
438
+ }
439
+
440
+ function refreshTrackedDesktopTurnState(threadId, latestState) {
441
+ const turns = activeDesktopTurnsByThreadId.get(threadId);
442
+ if (!turns) {
443
+ return;
444
+ }
445
+
446
+ for (const entry of turns.values()) {
447
+ entry.latestState = latestState;
448
+ }
449
+ }
450
+
451
+ function syncProjectedDesktopTurnCompletions(threadId, nextState) {
452
+ const turns = activeDesktopTurnsByThreadId.get(threadId);
453
+ if (!turns || turns.size === 0) {
454
+ return;
455
+ }
456
+
457
+ const completions = projectDesktopTurnCompletedNotifications(
458
+ threadId,
459
+ nextState,
460
+ new Set(turns.keys())
461
+ );
462
+ for (const notification of completions) {
463
+ completeDesktopIpcTurn(threadId, notification.params.turnId, notification.params.status || "completed");
464
+ }
465
+ }
466
+
467
+ function completeDesktopIpcTurn(threadId, turnId, status = "completed") {
468
+ const turns = activeDesktopTurnsByThreadId.get(threadId);
469
+ const entry = turns?.get(turnId);
470
+ if (!entry) {
471
+ return;
472
+ }
473
+
474
+ if (entry.timer) {
475
+ clearTimeoutFn(entry.timer);
476
+ }
477
+ turns.delete(turnId);
478
+ if (turns.size === 0) {
479
+ activeDesktopTurnsByThreadId.delete(threadId);
480
+ }
481
+
482
+ sendApplicationResponse(JSON.stringify(createDesktopIpcTurnCompletedNotification(threadId, turnId, status)));
483
+ }
484
+
485
+ function clearAllDesktopTurnCompletionTimers() {
486
+ for (const turns of activeDesktopTurnsByThreadId.values()) {
487
+ for (const entry of turns.values()) {
488
+ if (entry.timer) {
489
+ clearTimeoutFn(entry.timer);
490
+ }
491
+ }
492
+ }
493
+ activeDesktopTurnsByThreadId.clear();
307
494
  }
308
495
 
309
496
  return {
@@ -605,6 +792,7 @@ function projectDesktopAssistantDeltaNotifications(
605
792
  turnId: message.turnId,
606
793
  itemId: message.itemId,
607
794
  delta,
795
+ ...(message.phase ? { phase: message.phase } : {}),
608
796
  },
609
797
  });
610
798
  }
@@ -612,10 +800,263 @@ function projectDesktopAssistantDeltaNotifications(
612
800
  return notifications;
613
801
  }
614
802
 
803
+ function projectDesktopUserMessageNotifications(
804
+ threadId,
805
+ conversationState,
806
+ mirroredKeys = new Set(),
807
+ turnIdFilter = null
808
+ ) {
809
+ const messages = collectUserMessages(conversationState);
810
+ const notifications = [];
811
+
812
+ for (const message of messages) {
813
+ if (turnIdFilter && turnIdFilter.size > 0 && !turnIdFilter.has(message.turnId)) {
814
+ continue;
815
+ }
816
+ if (mirroredKeys.has(message.key)) {
817
+ continue;
818
+ }
819
+
820
+ mirroredKeys.add(message.key);
821
+ notifications.push({
822
+ method: "codex/event/user_message",
823
+ params: {
824
+ threadId,
825
+ turnId: message.turnId,
826
+ message: message.text,
827
+ ...(message.itemId ? { id: message.itemId } : {}),
828
+ ...(message.timestamp ? { timestamp: message.timestamp } : {}),
829
+ remodexDesktopMirror: true,
830
+ remodexDesktopIpcMirror: true,
831
+ },
832
+ });
833
+ }
834
+
835
+ return notifications;
836
+ }
837
+
838
+ function projectDesktopActivityNotifications(threadId, conversationState, mirroredKeys = new Set()) {
839
+ const turns = Array.isArray(conversationState?.turns) ? conversationState.turns : [];
840
+ const notifications = [];
841
+
842
+ for (const turn of turns) {
843
+ const turnId = readString(turn?.id) || readString(turn?.turnId) || readString(turn?.turn_id);
844
+ if (!turnId || (
845
+ !hasActiveDesktopActivityForTurn(conversationState, turnId)
846
+ && !isDesktopTurnLive(turn, conversationState)
847
+ && !hasMirroredActivityOutputPending(turn, mirroredKeys)
848
+ )) {
849
+ continue;
850
+ }
851
+
852
+ const items = Array.isArray(turn?.items) ? turn.items : [];
853
+ const callsById = new Map();
854
+ for (const item of items) {
855
+ if (isDesktopActivityCallItem(item)) {
856
+ const callId = desktopActivityCallId(item);
857
+ if (callId) {
858
+ callsById.set(callId, item);
859
+ }
860
+ }
861
+ }
862
+
863
+ for (const item of items) {
864
+ if (isDesktopActivityCallItem(item)) {
865
+ notifications.push(...projectDesktopActivityBeginNotifications(threadId, turnId, item, mirroredKeys));
866
+ } else if (isDesktopActivityOutputItem(item)) {
867
+ const callId = desktopActivityCallId(item);
868
+ notifications.push(...projectDesktopActivityOutputNotifications(
869
+ threadId,
870
+ turnId,
871
+ item,
872
+ callsById.get(callId),
873
+ mirroredKeys
874
+ ));
875
+ }
876
+ }
877
+ }
878
+
879
+ return notifications;
880
+ }
881
+
882
+ function projectDesktopActivityBeginNotifications(threadId, turnId, item, mirroredKeys) {
883
+ const callId = desktopActivityCallId(item);
884
+ const toolName = readString(item?.name) || readString(item?.toolName) || readString(item?.tool_name);
885
+ if (!callId || !toolName || !markMirroredActivityKey(mirroredKeys, turnId, callId, "begin")) {
886
+ return [];
887
+ }
888
+
889
+ if (isCommandToolName(toolName)) {
890
+ const argumentsObject = parseToolArguments(item?.arguments);
891
+ return [createDesktopIpcNotification("codex/event/exec_command_begin", {
892
+ threadId,
893
+ turnId,
894
+ call_id: callId,
895
+ command: resolveToolCommand(toolName, argumentsObject),
896
+ cwd: resolveToolWorkingDirectory(argumentsObject, item),
897
+ status: "running",
898
+ })];
899
+ }
900
+
901
+ if (toolName === "apply_patch") {
902
+ const isCompletedPatch = Boolean(terminalStatusFromObject(item));
903
+ const fileChange = buildApplyPatchFileChangeItem({
904
+ callId,
905
+ patch: readString(item?.input) || readString(item?.arguments),
906
+ status: readString(item?.status) || (isCompletedPatch ? "completed" : "inProgress"),
907
+ idFallback: buildSyntheticActivityItemId("file-change", threadId, turnId, callId),
908
+ cwd: readString(item?.cwd) || readString(item?.workdir),
909
+ });
910
+ if (fileChange) {
911
+ return [createDesktopIpcNotification(
912
+ isCompletedPatch ? "codex/event/patch_apply_end" : "codex/event/patch_apply_begin",
913
+ {
914
+ threadId,
915
+ turnId,
916
+ id: turnId,
917
+ call_id: callId,
918
+ itemId: fileChange.id,
919
+ status: fileChange.status,
920
+ ...(isCompletedPatch ? { success: true } : {}),
921
+ changes: fileChange.changes,
922
+ }
923
+ )];
924
+ }
925
+ }
926
+
927
+ return [createDesktopIpcNotification("codex/event/background_event", {
928
+ threadId,
929
+ turnId,
930
+ call_id: callId,
931
+ message: genericToolActivityMessage(toolName),
932
+ })];
933
+ }
934
+
935
+ function projectDesktopActivityOutputNotifications(threadId, turnId, item, callItem, mirroredKeys) {
936
+ const callId = desktopActivityCallId(item);
937
+ const toolName = readString(callItem?.name) || readString(callItem?.toolName) || readString(callItem?.tool_name);
938
+ if (!callId || !toolName || !mirroredKeys.has(activityMirrorKey(turnId, callId, "begin"))) {
939
+ return [];
940
+ }
941
+ if (!markMirroredActivityKey(mirroredKeys, turnId, callId, "output")) {
942
+ return [];
943
+ }
944
+
945
+ if (!isCommandToolName(toolName)) {
946
+ return [];
947
+ }
948
+
949
+ const argumentsObject = parseToolArguments(callItem?.arguments);
950
+ const command = resolveToolCommand(toolName, argumentsObject);
951
+ const cwd = resolveToolWorkingDirectory(argumentsObject, callItem);
952
+ const output = readString(item?.output) || readString(item?.text) || readString(item?.content);
953
+ const notifications = [];
954
+ if (output) {
955
+ notifications.push(createDesktopIpcNotification("codex/event/exec_command_output_delta", {
956
+ threadId,
957
+ turnId,
958
+ call_id: callId,
959
+ command,
960
+ cwd,
961
+ chunk: output,
962
+ }));
963
+ }
964
+ notifications.push(createDesktopIpcNotification("codex/event/exec_command_end", {
965
+ threadId,
966
+ turnId,
967
+ call_id: callId,
968
+ command,
969
+ cwd,
970
+ status: "completed",
971
+ output: output || "",
972
+ }));
973
+ return notifications;
974
+ }
975
+
976
+ function projectDesktopTurnCompletedNotifications(
977
+ threadId,
978
+ conversationState,
979
+ trackedTurnIds = new Set()
980
+ ) {
981
+ if (!trackedTurnIds || trackedTurnIds.size === 0) {
982
+ return [];
983
+ }
984
+
985
+ const turns = Array.isArray(conversationState?.turns) ? conversationState.turns : [];
986
+ const notifications = [];
987
+ for (const turn of turns) {
988
+ const turnId = readString(turn?.id) || readString(turn?.turnId) || readString(turn?.turn_id);
989
+ if (!turnId || !trackedTurnIds.has(turnId)) {
990
+ continue;
991
+ }
992
+ // Terminal snapshots can race active tool rows. Keep tracking the turn until
993
+ // the activity itself closes, otherwise the phone may stay running forever.
994
+ if (hasActiveDesktopActivityForTurn(conversationState, turnId)) {
995
+ continue;
996
+ }
997
+ if (!isDesktopTurnTerminal(turn, conversationState)) {
998
+ continue;
999
+ }
1000
+
1001
+ notifications.push(createDesktopIpcTurnCompletedNotification(
1002
+ threadId,
1003
+ turnId,
1004
+ desktopTerminalStatus(turn, conversationState) || "completed"
1005
+ ));
1006
+ }
1007
+ return notifications;
1008
+ }
1009
+
1010
+ function createDesktopIpcTurnCompletedNotification(threadId, turnId, status = "completed") {
1011
+ return {
1012
+ method: "turn/completed",
1013
+ params: {
1014
+ threadId,
1015
+ turnId,
1016
+ id: turnId,
1017
+ status,
1018
+ remodexDesktopMirror: true,
1019
+ remodexDesktopIpcMirror: true,
1020
+ },
1021
+ };
1022
+ }
1023
+
615
1024
  function snapshotAssistantMessageTexts(conversationState) {
616
1025
  return new Map(collectAssistantMessages(conversationState).map((message) => [message.key, message.text]));
617
1026
  }
618
1027
 
1028
+ function collectUserMessages(conversationState) {
1029
+ const turns = Array.isArray(conversationState?.turns) ? conversationState.turns : [];
1030
+ const messages = [];
1031
+ for (const turn of turns) {
1032
+ const turnId = readString(turn?.id) || readString(turn?.turnId) || readString(turn?.turn_id);
1033
+ const items = Array.isArray(turn?.items) ? turn.items : [];
1034
+ for (const item of items) {
1035
+ if (!isUserMessageItem(item)) {
1036
+ continue;
1037
+ }
1038
+
1039
+ const text = userMessageText(item);
1040
+ if (!turnId || !text) {
1041
+ continue;
1042
+ }
1043
+
1044
+ const itemId = readString(item?.id) || readString(item?.itemId) || readString(item?.item_id);
1045
+ messages.push({
1046
+ key: userMessageKey(turnId, itemId, text),
1047
+ turnId,
1048
+ itemId,
1049
+ text,
1050
+ timestamp: readString(item?.createdAt)
1051
+ || readString(item?.created_at)
1052
+ || readString(item?.timestamp)
1053
+ || readString(item?.time),
1054
+ });
1055
+ }
1056
+ }
1057
+ return messages;
1058
+ }
1059
+
619
1060
  function collectAssistantMessages(conversationState) {
620
1061
  const turns = Array.isArray(conversationState?.turns) ? conversationState.turns : [];
621
1062
  const messages = [];
@@ -629,6 +1070,7 @@ function collectAssistantMessages(conversationState) {
629
1070
 
630
1071
  const itemId = readString(item?.id) || readString(item?.itemId) || readString(item?.item_id);
631
1072
  const text = assistantMessageText(item);
1073
+ const phase = assistantMessagePhase(item);
632
1074
  if (!turnId || !itemId) {
633
1075
  continue;
634
1076
  }
@@ -637,6 +1079,7 @@ function collectAssistantMessages(conversationState) {
637
1079
  key: `${turnId}:${itemId}`,
638
1080
  turnId,
639
1081
  itemId,
1082
+ phase,
640
1083
  text,
641
1084
  });
642
1085
  }
@@ -644,6 +1087,295 @@ function collectAssistantMessages(conversationState) {
644
1087
  return messages;
645
1088
  }
646
1089
 
1090
+ function userMessageKey(turnId, itemId, text) {
1091
+ if (itemId) {
1092
+ return `${turnId}:${itemId}`;
1093
+ }
1094
+ return `${turnId}:text:${crypto
1095
+ .createHash("sha256")
1096
+ .update(text)
1097
+ .digest("hex")
1098
+ .slice(0, 16)}`;
1099
+ }
1100
+
1101
+ function markMirroredActivityKey(mirroredKeys, turnId, callId, phase) {
1102
+ const key = activityMirrorKey(turnId, callId, phase);
1103
+ if (mirroredKeys.has(key)) {
1104
+ return false;
1105
+ }
1106
+ mirroredKeys.add(key);
1107
+ return true;
1108
+ }
1109
+
1110
+ function activityMirrorKey(turnId, callId, phase) {
1111
+ return `${turnId}:${callId}:${phase}`;
1112
+ }
1113
+
1114
+ function hasMirroredActivityOutputPending(turn, mirroredKeys) {
1115
+ const turnId = readString(turn?.id) || readString(turn?.turnId) || readString(turn?.turn_id);
1116
+ const items = Array.isArray(turn?.items) ? turn.items : [];
1117
+ return items.some((item) => {
1118
+ if (!isDesktopActivityOutputItem(item)) {
1119
+ return false;
1120
+ }
1121
+ const callId = desktopActivityCallId(item);
1122
+ return callId
1123
+ && mirroredKeys.has(activityMirrorKey(turnId, callId, "begin"))
1124
+ && !mirroredKeys.has(activityMirrorKey(turnId, callId, "output"));
1125
+ });
1126
+ }
1127
+
1128
+ function isDesktopTurnTerminal(turn, conversationState) {
1129
+ if (desktopTerminalStatus(turn, conversationState)) {
1130
+ return true;
1131
+ }
1132
+
1133
+ return hasExplicitFalseFlag(turn, ["running", "isRunning", "streaming", "isStreaming"])
1134
+ || (
1135
+ hasExplicitFalseFlag(conversationState, ["running", "isRunning", "streaming", "isStreaming"])
1136
+ && !hasOpenDesktopRequestForTurn(conversationState, readString(turn?.id) || readString(turn?.turnId) || readString(turn?.turn_id))
1137
+ );
1138
+ }
1139
+
1140
+ function isDesktopTurnLive(turn, conversationState) {
1141
+ return hasExplicitTrueFlag(turn, ["running", "isRunning", "streaming", "isStreaming"])
1142
+ || hasExplicitTrueFlag(conversationState, ["running", "isRunning", "streaming", "isStreaming"])
1143
+ || ACTIVE_STATUS_TOKENS.has(normalizeToken(readString(turn?.status)))
1144
+ || ACTIVE_STATUS_TOKENS.has(normalizeToken(readString(turn?.state)))
1145
+ || ACTIVE_STATUS_TOKENS.has(normalizeToken(readString(turn?.phase)))
1146
+ || ACTIVE_STATUS_TOKENS.has(normalizeToken(readString(conversationState?.status)))
1147
+ || ACTIVE_STATUS_TOKENS.has(normalizeToken(readString(conversationState?.state)))
1148
+ || ACTIVE_STATUS_TOKENS.has(normalizeToken(readString(conversationState?.phase)));
1149
+ }
1150
+
1151
+ function desktopTerminalStatus(turn, conversationState) {
1152
+ const terminal = terminalStatusFromObject(turn)
1153
+ || terminalStatusFromObject(turn?.turn)
1154
+ || terminalStatusFromObject(conversationState);
1155
+ return terminal || "";
1156
+ }
1157
+
1158
+ function terminalStatusFromObject(value) {
1159
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1160
+ return "";
1161
+ }
1162
+
1163
+ const booleanStatus = terminalStatusFromBooleans(value);
1164
+ if (booleanStatus) {
1165
+ return booleanStatus;
1166
+ }
1167
+
1168
+ const candidates = [
1169
+ value.status,
1170
+ value.state,
1171
+ value.phase,
1172
+ value.lifecycle,
1173
+ value.lifecycleStatus,
1174
+ value.lifecycle_status,
1175
+ value.runStatus,
1176
+ value.run_status,
1177
+ value.turnStatus,
1178
+ value.turn_status,
1179
+ ];
1180
+ for (const candidate of candidates) {
1181
+ const token = normalizeToken(readString(candidate));
1182
+ if (TERMINAL_STATUS_TOKENS.has(token)) {
1183
+ return canonicalTerminalStatus(token);
1184
+ }
1185
+ }
1186
+ return "";
1187
+ }
1188
+
1189
+ function terminalStatusFromBooleans(value) {
1190
+ if (value.completed === true || value.complete === true || value.done === true || value.finished === true) {
1191
+ return "completed";
1192
+ }
1193
+ if (value.failed === true || value.error === true) {
1194
+ return "failed";
1195
+ }
1196
+ if (value.cancelled === true || value.canceled === true || value.interrupted === true) {
1197
+ return "canceled";
1198
+ }
1199
+ return "";
1200
+ }
1201
+
1202
+ const TERMINAL_STATUS_TOKENS = new Set([
1203
+ "completed",
1204
+ "complete",
1205
+ "finished",
1206
+ "succeeded",
1207
+ "success",
1208
+ "failed",
1209
+ "failure",
1210
+ "error",
1211
+ "cancelled",
1212
+ "canceled",
1213
+ "interrupted",
1214
+ ]);
1215
+
1216
+ function canonicalTerminalStatus(token) {
1217
+ if (token === "failed" || token === "failure" || token === "error") {
1218
+ return "failed";
1219
+ }
1220
+ if (token === "cancelled" || token === "canceled" || token === "interrupted") {
1221
+ return "canceled";
1222
+ }
1223
+ return "completed";
1224
+ }
1225
+
1226
+ function hasExplicitFalseFlag(value, keys) {
1227
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1228
+ return false;
1229
+ }
1230
+
1231
+ return keys.some((key) => Object.prototype.hasOwnProperty.call(value, key) && value[key] === false);
1232
+ }
1233
+
1234
+ function hasExplicitTrueFlag(value, keys) {
1235
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1236
+ return false;
1237
+ }
1238
+
1239
+ return keys.some((key) => Object.prototype.hasOwnProperty.call(value, key) && value[key] === true);
1240
+ }
1241
+
1242
+ const ACTIVE_STATUS_TOKENS = new Set([
1243
+ "running",
1244
+ "streaming",
1245
+ "inprogress",
1246
+ "inflight",
1247
+ "started",
1248
+ "pending",
1249
+ "active",
1250
+ ]);
1251
+
1252
+ function hasOpenDesktopRequestForTurn(conversationState, turnId) {
1253
+ if (!turnId) {
1254
+ return false;
1255
+ }
1256
+
1257
+ const requests = Array.isArray(conversationState?.requests) ? conversationState.requests : [];
1258
+ return requests.some((request) => {
1259
+ if (!request || request.completed === true) {
1260
+ return false;
1261
+ }
1262
+
1263
+ const params = request.params && typeof request.params === "object" && !Array.isArray(request.params)
1264
+ ? request.params
1265
+ : {};
1266
+ const requestTurnId = readString(params.turnId)
1267
+ || readString(params.turn_id)
1268
+ || readString(request.turnId)
1269
+ || readString(request.turn_id);
1270
+ return requestTurnId === turnId;
1271
+ });
1272
+ }
1273
+
1274
+ function hasActiveDesktopActivityForTurn(conversationState, turnId) {
1275
+ const turn = desktopTurnById(conversationState, turnId);
1276
+ const items = Array.isArray(turn?.items) ? turn.items : [];
1277
+ const completedActivityIds = completedDesktopActivityIds(items);
1278
+ return items.some((item) => isActiveDesktopActivityItem(item, completedActivityIds));
1279
+ }
1280
+
1281
+ function desktopTurnById(conversationState, turnId) {
1282
+ if (!turnId) {
1283
+ return null;
1284
+ }
1285
+
1286
+ const turns = Array.isArray(conversationState?.turns) ? conversationState.turns : [];
1287
+ return turns.find((turn) => {
1288
+ const candidateTurnId = readString(turn?.id) || readString(turn?.turnId) || readString(turn?.turn_id);
1289
+ return candidateTurnId === turnId;
1290
+ }) || null;
1291
+ }
1292
+
1293
+ function isActiveDesktopActivityItem(item, completedActivityIds = new Set()) {
1294
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
1295
+ return false;
1296
+ }
1297
+ if (!isDesktopActivityItem(item) || isDesktopActivityOutputItem(item) || terminalStatusFromObject(item)) {
1298
+ return false;
1299
+ }
1300
+ if (hasExplicitTrueFlag(item, ["running", "isRunning", "streaming", "isStreaming"])
1301
+ || ACTIVE_STATUS_TOKENS.has(normalizeToken(readString(item.status)))
1302
+ || ACTIVE_STATUS_TOKENS.has(normalizeToken(readString(item.state)))
1303
+ || ACTIVE_STATUS_TOKENS.has(normalizeToken(readString(item.phase)))
1304
+ || ACTIVE_STATUS_TOKENS.has(normalizeToken(readString(item.lifecycle)))) {
1305
+ return true;
1306
+ }
1307
+
1308
+ // Codex Desktop often represents a live tool as a bare function_call/custom_tool_call
1309
+ // with only call_id, then appends a separate *_output item. Treat the call as active
1310
+ // until that output appears, even when no explicit "running" status is present.
1311
+ const activityId = desktopActivityCallId(item);
1312
+ return isDesktopActivityCallItem(item) && activityId && !completedActivityIds.has(activityId);
1313
+ }
1314
+
1315
+ function isDesktopActivityItem(item) {
1316
+ const type = normalizeToken(item?.type);
1317
+ return type.includes("tool")
1318
+ || type.includes("command")
1319
+ || type.includes("exec")
1320
+ || type.includes("mcp")
1321
+ || type.includes("function");
1322
+ }
1323
+
1324
+ function isDesktopActivityCallItem(item) {
1325
+ const type = normalizeToken(item?.type);
1326
+ if (isDesktopActivityOutputItem(item)) {
1327
+ return false;
1328
+ }
1329
+ return type.endsWith("call")
1330
+ || type.includes("toolcall")
1331
+ || type.includes("functioncall")
1332
+ || type.includes("commandexecution")
1333
+ || type.includes("localshellcall");
1334
+ }
1335
+
1336
+ function isDesktopActivityOutputItem(item) {
1337
+ const type = normalizeToken(item?.type);
1338
+ return type.includes("output")
1339
+ || type.includes("result")
1340
+ || type.endsWith("end")
1341
+ || type.includes("completed");
1342
+ }
1343
+
1344
+ function completedDesktopActivityIds(items) {
1345
+ const ids = new Set();
1346
+ for (const item of items) {
1347
+ if (!item || typeof item !== "object" || Array.isArray(item)) {
1348
+ continue;
1349
+ }
1350
+ if (!isDesktopActivityOutputItem(item) && !terminalStatusFromObject(item)) {
1351
+ continue;
1352
+ }
1353
+ const id = desktopActivityCallId(item);
1354
+ if (id) {
1355
+ ids.add(id);
1356
+ }
1357
+ }
1358
+ return ids;
1359
+ }
1360
+
1361
+ function desktopActivityCallId(item) {
1362
+ return readString(item?.call_id)
1363
+ || readString(item?.callId)
1364
+ || readString(item?.tool_call_id)
1365
+ || readString(item?.toolCallId)
1366
+ || readString(item?.requestId)
1367
+ || readString(item?.request_id)
1368
+ || readString(item?.id);
1369
+ }
1370
+
1371
+ function isUserMessageItem(item) {
1372
+ const type = normalizeToken(item?.type);
1373
+ if (type === "usermessage") {
1374
+ return true;
1375
+ }
1376
+ return type === "message" && normalizeToken(item?.role) === "user";
1377
+ }
1378
+
647
1379
  function isAssistantMessageItem(item) {
648
1380
  const type = normalizeToken(item?.type);
649
1381
  if (type === "agentmessage" || type === "assistantmessage") {
@@ -652,6 +1384,41 @@ function isAssistantMessageItem(item) {
652
1384
  return type === "message" && normalizeToken(item?.role) === "assistant";
653
1385
  }
654
1386
 
1387
+ function userMessageText(item) {
1388
+ const directText = readString(item?.text) || readString(item?.message);
1389
+ if (directText) {
1390
+ return directText;
1391
+ }
1392
+
1393
+ const content = Array.isArray(item?.content) ? item.content : [];
1394
+ return content
1395
+ .map((entry) => entry && typeof entry === "object" ? entry : null)
1396
+ .filter(Boolean)
1397
+ .map((entry) => readString(entry.text) || readString(entry?.data?.text))
1398
+ .filter(Boolean)
1399
+ .join("");
1400
+ }
1401
+
1402
+ function assistantMessagePhase(item) {
1403
+ return normalizeAssistantPhase(
1404
+ readString(item?.phase)
1405
+ || readString(item?.assistantPhase)
1406
+ || readString(item?.assistant_phase)
1407
+ || readString(item?.metadata?.phase)
1408
+ );
1409
+ }
1410
+
1411
+ function normalizeAssistantPhase(value) {
1412
+ const normalized = normalizeToken(value);
1413
+ if (!normalized) {
1414
+ return "";
1415
+ }
1416
+ if (normalized === "finalanswer") {
1417
+ return "final_answer";
1418
+ }
1419
+ return normalized;
1420
+ }
1421
+
655
1422
  function assistantMessageText(item) {
656
1423
  const directText = readString(item?.text) || readString(item?.message);
657
1424
  if (directText) {
@@ -817,6 +1584,68 @@ function requestIdKey(value) {
817
1584
  return "";
818
1585
  }
819
1586
 
1587
+ function parseToolArguments(rawArguments) {
1588
+ const parsed = safeParseJSON(rawArguments);
1589
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
1590
+ }
1591
+
1592
+ function resolveToolCommand(toolName, argumentsObject) {
1593
+ if (!isCommandToolName(toolName)) {
1594
+ return toolName;
1595
+ }
1596
+
1597
+ return readString(argumentsObject.cmd)
1598
+ || readString(argumentsObject.command)
1599
+ || readString(argumentsObject.raw_command)
1600
+ || readString(argumentsObject.rawCommand)
1601
+ || toolName;
1602
+ }
1603
+
1604
+ function resolveToolWorkingDirectory(argumentsObject, item = {}) {
1605
+ return readString(argumentsObject.workdir)
1606
+ || readString(argumentsObject.cwd)
1607
+ || readString(argumentsObject.working_directory)
1608
+ || readString(item?.cwd)
1609
+ || readString(item?.workdir)
1610
+ || "";
1611
+ }
1612
+
1613
+ function isCommandToolName(toolName) {
1614
+ const normalized = readString(toolName).toLowerCase();
1615
+ return normalized === "exec_command"
1616
+ || normalized === "shell_command"
1617
+ || normalized.endsWith(".exec_command")
1618
+ || normalized.endsWith(".shell_command");
1619
+ }
1620
+
1621
+ function genericToolActivityMessage(toolName) {
1622
+ switch (readString(toolName).toLowerCase()) {
1623
+ case "apply_patch":
1624
+ return "Applying patch";
1625
+ case "write_stdin":
1626
+ return "Writing to terminal";
1627
+ case "read_thread_terminal":
1628
+ return "Reading terminal output";
1629
+ default:
1630
+ return `Running ${toolName}`;
1631
+ }
1632
+ }
1633
+
1634
+ function buildSyntheticActivityItemId(kind, threadId, turnId, callId) {
1635
+ return `${kind}:${threadId}:${turnId}:${callId}`;
1636
+ }
1637
+
1638
+ function createDesktopIpcNotification(method, params = {}) {
1639
+ return {
1640
+ method,
1641
+ params: {
1642
+ remodexDesktopMirror: true,
1643
+ remodexDesktopIpcMirror: true,
1644
+ ...params,
1645
+ },
1646
+ };
1647
+ }
1648
+
820
1649
  function readString(value) {
821
1650
  return typeof value === "string" && value.trim() ? value.trim() : "";
822
1651
  }
@@ -843,7 +1672,11 @@ module.exports = {
843
1672
  applyConversationStateChange,
844
1673
  createDesktopIpcActionFollower,
845
1674
  desktopFollowerPayloadForResponse,
1675
+ hasActiveDesktopActivityForTurn,
846
1676
  projectDesktopAssistantDeltaNotifications,
1677
+ projectDesktopActivityNotifications,
1678
+ projectDesktopTurnCompletedNotifications,
1679
+ projectDesktopUserMessageNotifications,
847
1680
  projectPendingDesktopActions,
848
1681
  resolveDefaultIpcSocketPath,
849
1682
  seedConversationStateFromThreadRead,
@@ -18,6 +18,10 @@ const DEFAULT_POLL_INTERVAL_MS = 700;
18
18
  const DEFAULT_LOOKUP_TIMEOUT_MS = 5_000;
19
19
  const DEFAULT_IDLE_TIMEOUT_MS = 60_000;
20
20
  const DEFAULT_ACTIVITY_HEARTBEAT_MS = 5_000;
21
+ const DEFAULT_BOOTSTRAP_REPLAY_BATCH_INTERVAL_MS = 0;
22
+ const BOOTSTRAP_REPLAY_CHUNK_SIZE = 50;
23
+ const BOOTSTRAP_REPLAY_CHUNK_MAX_BYTES = 128 * 1024;
24
+ const BOOTSTRAP_REPLAY_CACHE_LIMIT = 32;
21
25
  const DESKTOP_RESUME_METHODS = new Set(["thread/read", "thread/resume"]);
22
26
 
23
27
  // Observes desktop-authored rollout files and replays the currently active run as
@@ -29,12 +33,16 @@ function createRolloutLiveMirrorController({
29
33
  now = () => Date.now(),
30
34
  setIntervalFn = setInterval,
31
35
  clearIntervalFn = clearInterval,
36
+ setTimeoutFn = setTimeout,
37
+ clearTimeoutFn = clearTimeout,
32
38
  pollIntervalMs = DEFAULT_POLL_INTERVAL_MS,
33
39
  lookupTimeoutMs = DEFAULT_LOOKUP_TIMEOUT_MS,
34
40
  idleTimeoutMs = DEFAULT_IDLE_TIMEOUT_MS,
35
41
  activityHeartbeatMs = DEFAULT_ACTIVITY_HEARTBEAT_MS,
42
+ bootstrapReplayBatchIntervalMs = DEFAULT_BOOTSTRAP_REPLAY_BATCH_INTERVAL_MS,
36
43
  } = {}) {
37
44
  const mirrorsByThreadId = new Map();
45
+ const bootstrapReplayCache = new Map();
38
46
 
39
47
  function observeInbound(rawMessage) {
40
48
  const request = safeParseJSON(rawMessage);
@@ -63,10 +71,14 @@ function createRolloutLiveMirrorController({
63
71
  now,
64
72
  setIntervalFn,
65
73
  clearIntervalFn,
74
+ setTimeoutFn,
75
+ clearTimeoutFn,
66
76
  pollIntervalMs,
67
77
  lookupTimeoutMs,
68
78
  idleTimeoutMs,
69
79
  activityHeartbeatMs,
80
+ bootstrapReplayBatchIntervalMs,
81
+ bootstrapReplayCache,
70
82
  onStop() {
71
83
  if (mirrorsByThreadId.get(threadId) === mirror) {
72
84
  mirrorsByThreadId.delete(threadId);
@@ -99,14 +111,19 @@ function createThreadRolloutLiveMirror({
99
111
  now,
100
112
  setIntervalFn,
101
113
  clearIntervalFn,
114
+ setTimeoutFn,
115
+ clearTimeoutFn,
102
116
  pollIntervalMs,
103
117
  lookupTimeoutMs,
104
118
  idleTimeoutMs,
105
119
  activityHeartbeatMs,
120
+ bootstrapReplayBatchIntervalMs,
121
+ bootstrapReplayCache,
106
122
  onStop = () => {},
107
123
  }) {
108
124
  const startedAt = now();
109
125
  const state = createMirrorState(threadId);
126
+ const bootstrapReplayTimeouts = new Set();
110
127
 
111
128
  let isStopped = false;
112
129
  let rolloutPath = null;
@@ -151,6 +168,10 @@ function createThreadRolloutLiveMirror({
151
168
  state,
152
169
  fsModule,
153
170
  sendApplicationResponse,
171
+ bootstrapReplayCache,
172
+ setTimeoutFn,
173
+ bootstrapReplayBatchIntervalMs,
174
+ pendingTimeouts: bootstrapReplayTimeouts,
154
175
  });
155
176
  lastSize = fileSize;
156
177
  lastActivityAt = currentTime;
@@ -216,6 +237,10 @@ function createThreadRolloutLiveMirror({
216
237
 
217
238
  isStopped = true;
218
239
  clearIntervalFn(intervalId);
240
+ for (const timeout of bootstrapReplayTimeouts) {
241
+ clearTimeoutFn(timeout);
242
+ }
243
+ bootstrapReplayTimeouts.clear();
219
244
  onStop();
220
245
  }
221
246
 
@@ -231,6 +256,10 @@ function bootstrapFromExistingRollout({
231
256
  state,
232
257
  fsModule,
233
258
  sendApplicationResponse,
259
+ bootstrapReplayCache,
260
+ setTimeoutFn = setTimeout,
261
+ bootstrapReplayBatchIntervalMs = DEFAULT_BOOTSTRAP_REPLAY_BATCH_INTERVAL_MS,
262
+ pendingTimeouts,
234
263
  }) {
235
264
  const initialContents = readFileSlice(rolloutPath, 0, fileSize, fsModule);
236
265
  if (!initialContents) {
@@ -261,7 +290,7 @@ function bootstrapFromExistingRollout({
261
290
  const taskEventType = parsed?.type === "event_msg"
262
291
  ? readString(parsed?.payload?.type)
263
292
  : "";
264
- if (taskEventType === "user_message") {
293
+ if (taskEventType === "user_message" && !insideActiveRun) {
265
294
  pendingUserPreludeLine = line;
266
295
  }
267
296
  if (taskEventType === "task_started") {
@@ -273,6 +302,7 @@ function bootstrapFromExistingRollout({
273
302
  if (pendingUserPreludeLine) {
274
303
  activeRunLines.push(pendingUserPreludeLine);
275
304
  }
305
+ pendingUserPreludeLine = null;
276
306
  activeRunLines.push(line);
277
307
  continue;
278
308
  }
@@ -282,7 +312,7 @@ function bootstrapFromExistingRollout({
282
312
  }
283
313
 
284
314
  activeRunLines.push(line);
285
- if (taskEventType === "task_complete") {
315
+ if (isRolloutTerminalTaskEvent(taskEventType)) {
286
316
  insideActiveRun = false;
287
317
  activeTurnId = "";
288
318
  activeRunLines.length = 0;
@@ -296,7 +326,33 @@ function bootstrapFromExistingRollout({
296
326
  }
297
327
 
298
328
  state.isDesktopOrigin = true;
299
- processRolloutLines(activeRunLines, state, sendApplicationResponse);
329
+ const replayId = bootstrapReplayId(state.threadId, rolloutPath, initialContents);
330
+ const cachedBatches = bootstrapReplayCache?.get(replayId);
331
+ const replayNotifications = cachedBatches ? null : [];
332
+ // Rebuild local mirror state from the already-written run while collecting
333
+ // every notification into bounded catch-up batches for the phone.
334
+ processRolloutLines(activeRunLines, state, (rawNotification) => {
335
+ if (cachedBatches) {
336
+ return;
337
+ }
338
+ const notification = safeParseJSON(rawNotification);
339
+ if (notification) {
340
+ replayNotifications.push(notification);
341
+ }
342
+ });
343
+ const batches = cachedBatches || chunkBootstrapReplayNotifications(replayNotifications);
344
+ rememberBootstrapReplayBatches(bootstrapReplayCache, replayId, batches);
345
+ emitBootstrapReplayBatches(state, replayId, batches, sendApplicationResponse, {
346
+ setTimeoutFn,
347
+ batchIntervalMs: bootstrapReplayBatchIntervalMs,
348
+ pendingTimeouts,
349
+ });
350
+ }
351
+
352
+ function isRolloutTerminalTaskEvent(eventType) {
353
+ return eventType === "task_complete"
354
+ || eventType === "turn_aborted"
355
+ || eventType === "task_aborted";
300
356
  }
301
357
 
302
358
  function processRolloutLines(lines, state, sendApplicationResponse) {
@@ -322,6 +378,106 @@ function processRolloutLines(lines, state, sendApplicationResponse) {
322
378
  }
323
379
  }
324
380
 
381
+ function emitBootstrapReplayBatches(
382
+ state,
383
+ replayId,
384
+ batches,
385
+ sendApplicationResponse,
386
+ {
387
+ setTimeoutFn = setTimeout,
388
+ batchIntervalMs = DEFAULT_BOOTSTRAP_REPLAY_BATCH_INTERVAL_MS,
389
+ pendingTimeouts,
390
+ } = {}
391
+ ) {
392
+ if (!Array.isArray(batches) || batches.length === 0) {
393
+ return;
394
+ }
395
+
396
+ const sendBatch = (batch, batchIndex) => {
397
+ sendApplicationResponse(JSON.stringify(createNotification("remodex/rollout/bootstrapReplay", {
398
+ threadId: state.threadId,
399
+ replayId,
400
+ batchIndex,
401
+ batchCount: batches.length,
402
+ notifications: batch,
403
+ })));
404
+ };
405
+
406
+ for (let batchIndex = 0; batchIndex < batches.length; batchIndex += 1) {
407
+ const batch = batches[batchIndex];
408
+ if (batchIndex === 0) {
409
+ sendBatch(batch, batchIndex);
410
+ continue;
411
+ }
412
+
413
+ if (batchIntervalMs <= 0) {
414
+ sendBatch(batch, batchIndex);
415
+ continue;
416
+ }
417
+
418
+ const timeout = setTimeoutFn(() => {
419
+ pendingTimeouts?.delete(timeout);
420
+ sendBatch(batch, batchIndex);
421
+ }, batchIndex * batchIntervalMs);
422
+ pendingTimeouts?.add(timeout);
423
+ }
424
+ }
425
+
426
+ function chunkBootstrapReplayNotifications(notifications) {
427
+ const batches = [];
428
+ let batch = [];
429
+ let batchBytes = 0;
430
+
431
+ for (const notification of notifications) {
432
+ const notificationBytes = Buffer.byteLength(JSON.stringify(notification), "utf8");
433
+ const shouldStartNextBatch = batch.length > 0
434
+ && (
435
+ batch.length >= BOOTSTRAP_REPLAY_CHUNK_SIZE
436
+ || batchBytes + notificationBytes > BOOTSTRAP_REPLAY_CHUNK_MAX_BYTES
437
+ );
438
+ if (shouldStartNextBatch) {
439
+ batches.push(batch);
440
+ batch = [];
441
+ batchBytes = 0;
442
+ }
443
+
444
+ batch.push(notification);
445
+ batchBytes += notificationBytes;
446
+ }
447
+
448
+ if (batch.length > 0) {
449
+ batches.push(batch);
450
+ }
451
+ return batches;
452
+ }
453
+
454
+ function bootstrapReplayId(threadId, rolloutPath, contents) {
455
+ return crypto
456
+ .createHash("sha256")
457
+ .update(readString(threadId))
458
+ .update("\0")
459
+ .update(readString(rolloutPath))
460
+ .update("\0")
461
+ .update(String(contents || ""))
462
+ .digest("hex")
463
+ .slice(0, 24);
464
+ }
465
+
466
+ function rememberBootstrapReplayBatches(cache, replayId, batches) {
467
+ if (!cache || cache.has(replayId)) {
468
+ return;
469
+ }
470
+
471
+ cache.set(replayId, batches);
472
+ while (cache.size > BOOTSTRAP_REPLAY_CACHE_LIMIT) {
473
+ const oldestKey = cache.keys().next().value;
474
+ if (!oldestKey) {
475
+ break;
476
+ }
477
+ cache.delete(oldestKey);
478
+ }
479
+ }
480
+
325
481
  function synthesizeNotificationsFromRolloutEntry(entry, state) {
326
482
  if (entry?.type === "session_meta") {
327
483
  populateSessionMetaState(state, entry.payload);