@makerbi/remodex 3.1.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makerbi/remodex",
3
- "version": "3.1.0",
3
+ "version": "3.2.0",
4
4
  "description": "Local bridge between Codex and the Remodex mobile app. Run `remodex up` to start.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1865,11 +1865,9 @@ function createDesktopIpcActionFollower({
1865
1865
  return {
1866
1866
  threadId,
1867
1867
  method: "thread-follower-start-turn",
1868
- params: {
1869
- conversationId: threadId,
1870
- senderRequestId: requestId,
1871
- turnStartParams: params,
1872
- },
1868
+ params: { conversationId: threadId },
1869
+ senderRequestId: requestId,
1870
+ turnStartParams: params,
1873
1871
  };
1874
1872
  }
1875
1873
  if (method === "turn/steer") {
@@ -1889,7 +1887,8 @@ function createDesktopIpcActionFollower({
1889
1887
  method: "thread-follower-interrupt-turn",
1890
1888
  params: {
1891
1889
  conversationId: threadId,
1892
- turnId: readString(params.turnId) || readString(params.turn_id),
1890
+ mode: "user-stop",
1891
+ expectedTurnId: readString(params.turnId) || readString(params.turn_id),
1893
1892
  },
1894
1893
  };
1895
1894
  }
@@ -1908,11 +1907,11 @@ function createDesktopIpcActionFollower({
1908
1907
 
1909
1908
  function submitDesktopFollowerRequest(route, originalMessage) {
1910
1909
  Promise.resolve()
1911
- .then(() => resolveFollowerRequestParams(route))
1912
- .then(async (resolvedParams) => {
1910
+ .then(() => resolveFollowerRequest(route))
1911
+ .then(async (resolvedRequest) => {
1913
1912
  if (route.method === "thread-follower-start-turn") {
1914
1913
  try {
1915
- await syncDesktopOwnerRuntimeSettings(route.threadId, resolvedParams.turnStartParams);
1914
+ await syncDesktopOwnerRuntimeSettings(route.threadId, resolvedRequest.turnStartParams);
1916
1915
  } catch (error) {
1917
1916
  // The actual turn has not reached Desktop yet. Even if the settings
1918
1917
  // request timed out after being applied, continuing through the local
@@ -1921,16 +1920,16 @@ function createDesktopIpcActionFollower({
1921
1920
  }
1922
1921
  }
1923
1922
  return {
1924
- resolvedParams,
1925
- result: await ipc.sendRequest(route.method, resolvedParams),
1923
+ resolvedRequest,
1924
+ result: await ipc.sendRequest(route.method, resolvedRequest.params),
1926
1925
  };
1927
1926
  })
1928
- .then(({ resolvedParams, result }) => {
1927
+ .then(({ resolvedRequest, result }) => {
1929
1928
  const appServerResult = appServerResultForFollowerRequest(route.method, result);
1930
1929
  if (route.method === "thread-follower-start-turn") {
1931
1930
  commitPhoneRuntimeSettings(
1932
1931
  route.threadId,
1933
- resolvedParams.turnStartParams,
1932
+ resolvedRequest.turnStartParams,
1934
1933
  readTurnIdFromAppServerResult(appServerResult)
1935
1934
  );
1936
1935
  }
@@ -2026,19 +2025,34 @@ function createDesktopIpcActionFollower({
2026
2025
 
2027
2026
  // Desktop-followed turn starts must apply the same param normalization as
2028
2027
  // requests forwarded straight to the local app-server.
2029
- async function resolveFollowerRequestParams(route) {
2028
+ async function resolveFollowerRequest(route) {
2030
2029
  if (route.method !== "thread-follower-start-turn") {
2031
- return route.params;
2030
+ return {
2031
+ params: route.params,
2032
+ turnStartParams: null,
2033
+ };
2032
2034
  }
2033
2035
 
2034
2036
  const normalized = await Promise.resolve(
2035
- normalizeTurnStartParams(cloneJSON(route.params.turnStartParams))
2037
+ normalizeTurnStartParams(cloneJSON(route.turnStartParams))
2036
2038
  );
2037
2039
  const turnStartParams = normalized && typeof normalized === "object" && !Array.isArray(normalized)
2038
2040
  ? normalized
2039
- : route.params.turnStartParams;
2041
+ : route.turnStartParams;
2042
+ const request = cloneJSON(turnStartParams);
2043
+ if (!readString(request.clientUserMessageId)) {
2044
+ request.clientUserMessageId = route.senderRequestId;
2045
+ }
2040
2046
  return {
2041
- ...route.params,
2047
+ params: {
2048
+ ...route.params,
2049
+ turnStart: {
2050
+ request,
2051
+ context: {
2052
+ inheritThreadSettings: true,
2053
+ },
2054
+ },
2055
+ },
2042
2056
  turnStartParams,
2043
2057
  };
2044
2058
  }
@@ -454,14 +454,24 @@ function buildConversationStateFromThread(thread, {
454
454
  now,
455
455
  });
456
456
 
457
- return {
457
+ const state = {
458
458
  id: threadId,
459
+ forkedFromId: previous?.forkedFromId || null,
459
460
  hostId,
460
461
  turns,
461
462
  requests: cloneJSON(previous?.requests || []),
462
463
  createdAt: createdAtMs,
463
464
  updatedAt: updatedAtMs,
465
+ recencyAt: updatedAtMs,
464
466
  title: readString(thread?.name) || previous?.title || null,
467
+ source: readString(thread?.source) || previous?.source || "vscode",
468
+ agentNickname: previous?.agentNickname || null,
469
+ threadSource: readString(thread?.threadSource) || previous?.threadSource || "user",
470
+ historyMode: previous?.historyMode || "legacy",
471
+ parentThreadId: previous?.parentThreadId || null,
472
+ mode: previous?.mode || "default",
473
+ threadStartKind: previous?.threadStartKind || "default",
474
+ modelProvider: readString(thread?.modelProvider) || previous?.modelProvider || "openai",
465
475
  latestModel,
466
476
  latestReasoningEffort: previous?.latestReasoningEffort || null,
467
477
  latestServiceTier: previous?.latestServiceTier || null,
@@ -488,7 +498,10 @@ function buildConversationStateFromThread(thread, {
488
498
  workspaceBrowserRoot: previous?.workspaceBrowserRoot || null,
489
499
  projectlessOutputDirectory: previous?.projectlessOutputDirectory || null,
490
500
  currentPermissions: cloneJSON(previous?.currentPermissions || null),
501
+ sessionId: readString(thread?.sessionId) || previous?.sessionId || null,
491
502
  };
503
+ synchronizeDesktopConversationCompatibility(state);
504
+ return state;
492
505
  }
493
506
 
494
507
  function mergeConversationTurnsFromThread(threadTurns, {
@@ -552,7 +565,7 @@ function createEmptyConversationState(threadId, {
552
565
  cwd = "",
553
566
  } = {}) {
554
567
  const timestamp = now();
555
- return {
568
+ const state = {
556
569
  id: threadId,
557
570
  hostId,
558
571
  turns: [],
@@ -587,6 +600,189 @@ function createEmptyConversationState(threadId, {
587
600
  projectlessOutputDirectory: null,
588
601
  currentPermissions: null,
589
602
  };
603
+ synchronizeDesktopConversationCompatibility(state);
604
+ return state;
605
+ }
606
+
607
+ // Desktop 26.825 moved rendered history to a canonical entity graph. It still
608
+ // accepts legacy turns during normalization, but unified-timeline selectors now
609
+ // assume the graph and several nested collections exist. Keep the bridge's
610
+ // mutable legacy turns for app-server event handling, and mirror them into the
611
+ // current Desktop shape before every stream broadcast.
612
+ function synchronizeDesktopConversationCompatibility(state) {
613
+ if (!state || typeof state !== "object" || Array.isArray(state)) {
614
+ return state;
615
+ }
616
+
617
+ const turns = Array.isArray(state.turns) ? state.turns : [];
618
+ const entries = [];
619
+ const entitiesByKey = {};
620
+ for (const turn of turns) {
621
+ const turnId = readString(turn?.turnId) || readString(turn?.id);
622
+ if (!turnId) {
623
+ continue;
624
+ }
625
+ turn.params = normalizeTurnParamsCompatibility(turn.params, {
626
+ cwd: readString(turn.params?.cwd) || readString(state.cwd),
627
+ });
628
+ turn.items = Array.isArray(turn.items) ? turn.items : [];
629
+ turn.hookRuns = Array.isArray(turn.hookRuns) ? turn.hookRuns : [];
630
+ const key = `turn:${turnId}`;
631
+ entries.push({ key, value: key });
632
+ const { id: _legacyId, ...canonicalTurn } = turn;
633
+ entitiesByKey[key] = {
634
+ ...canonicalTurn,
635
+ turnId,
636
+ };
637
+ }
638
+
639
+ const islandId = "tail:0";
640
+ state.turnHistory = {
641
+ kind: "canonical",
642
+ history: {
643
+ entitiesByKey,
644
+ generation: 0,
645
+ isComplete: true,
646
+ islands: [{
647
+ id: islandId,
648
+ entries,
649
+ olderBoundary: {
650
+ status: "exhausted",
651
+ boundaryId: `${islandId}:older`,
652
+ },
653
+ newerBoundary: {
654
+ status: "exhausted",
655
+ boundaryId: `${islandId}:newer`,
656
+ },
657
+ }],
658
+ },
659
+ };
660
+ state.turnsPagination = {
661
+ olderCursor: null,
662
+ oldestLoadedTurnId: readString(turns[0]?.turnId) || readString(turns[0]?.id) || null,
663
+ isLoadingOlder: false,
664
+ hasLoadedOldest: true,
665
+ };
666
+
667
+ const latestParams = [...turns]
668
+ .reverse()
669
+ .map((turn) => turn?.params)
670
+ .find((params) => params && typeof params === "object") || null;
671
+ const sandboxPolicy = normalizeSandboxPolicyCompatibility(
672
+ latestParams?.sandboxPolicy || state.currentPermissions?.sandboxPolicy
673
+ );
674
+ if (sandboxPolicy) {
675
+ const runtimeWorkspaceRoots = Array.isArray(latestParams?.runtimeWorkspaceRoots)
676
+ ? cloneJSON(latestParams.runtimeWorkspaceRoots)
677
+ : Array.isArray(state.currentPermissions?.runtimeWorkspaceRoots)
678
+ ? cloneJSON(state.currentPermissions.runtimeWorkspaceRoots)
679
+ : [];
680
+ state.currentPermissions = {
681
+ activePermissionProfile: cloneJSON(
682
+ latestParams?.activePermissionProfile
683
+ ?? state.currentPermissions?.activePermissionProfile
684
+ ?? permissionProfileFromId(latestParams?.permissions)
685
+ ),
686
+ approvalPolicy: latestParams?.approvalPolicy
687
+ ?? state.currentPermissions?.approvalPolicy
688
+ ?? "on-request",
689
+ approvalsReviewer: latestParams?.approvalsReviewer
690
+ ?? state.currentPermissions?.approvalsReviewer
691
+ ?? "user",
692
+ runtimeWorkspaceRoots,
693
+ sandboxPolicy,
694
+ };
695
+ }
696
+
697
+ state.recencyAt = Number.isFinite(state.recencyAt) ? state.recencyAt : state.updatedAt;
698
+ state.source ||= "vscode";
699
+ state.threadSource ||= "user";
700
+ state.historyMode ||= "legacy";
701
+ state.mode ||= "default";
702
+ state.threadStartKind ||= "default";
703
+ state.modelProvider ||= "openai";
704
+ state.latestThreadSettings = {
705
+ cwd: readString(latestParams?.cwd) || readString(state.cwd) || null,
706
+ approvalPolicy: latestParams?.approvalPolicy ?? state.currentPermissions?.approvalPolicy ?? null,
707
+ approvalsReviewer: latestParams?.approvalsReviewer
708
+ ?? state.currentPermissions?.approvalsReviewer
709
+ ?? null,
710
+ ...(sandboxPolicy ? { sandboxPolicy } : {}),
711
+ activePermissionProfile: cloneJSON(state.currentPermissions?.activePermissionProfile ?? null),
712
+ model: readString(state.latestThreadSettings?.model)
713
+ || readString(latestParams?.model)
714
+ || readString(state.latestModel)
715
+ || null,
716
+ modelProvider: readString(state.latestThreadSettings?.modelProvider)
717
+ || readString(state.modelProvider)
718
+ || "openai",
719
+ serviceTier: readString(state.latestThreadSettings?.serviceTier)
720
+ || readString(latestParams?.serviceTier)
721
+ || readString(state.latestServiceTier)
722
+ || null,
723
+ effort: state.latestThreadSettings?.effort
724
+ ?? latestParams?.effort
725
+ ?? state.latestReasoningEffort
726
+ ?? null,
727
+ summary: latestParams?.summary ?? state.latestThreadSettings?.summary ?? "none",
728
+ collaborationMode: cloneJSON(
729
+ state.latestThreadSettings?.collaborationMode
730
+ || latestParams?.collaborationMode
731
+ || state.latestCollaborationMode
732
+ || null
733
+ ),
734
+ multiAgentMode: latestParams?.multiAgentMode
735
+ ?? state.latestThreadSettings?.multiAgentMode
736
+ ?? null,
737
+ personality: latestParams?.personality
738
+ ?? state.latestThreadSettings?.personality
739
+ ?? null,
740
+ };
741
+ return state;
742
+ }
743
+
744
+ function normalizeTurnParamsCompatibility(params, { cwd = "" } = {}) {
745
+ const normalized = params && typeof params === "object" && !Array.isArray(params)
746
+ ? params
747
+ : {};
748
+ normalized.input = Array.isArray(normalized.input) ? normalized.input : [];
749
+ normalized.attachments = Array.isArray(normalized.attachments) ? normalized.attachments : [];
750
+ normalized.cwd = readString(normalized.cwd) || readString(cwd) || null;
751
+ normalized.summary ??= "none";
752
+ normalized.personality ??= null;
753
+ normalized.outputSchema ??= null;
754
+ normalized.collaborationMode ??= null;
755
+ const sandboxPolicy = normalizeSandboxPolicyCompatibility(normalized.sandboxPolicy);
756
+ if (sandboxPolicy) {
757
+ normalized.sandboxPolicy = sandboxPolicy;
758
+ }
759
+ return normalized;
760
+ }
761
+
762
+ function normalizeSandboxPolicyCompatibility(policy) {
763
+ if (!policy || typeof policy !== "object" || Array.isArray(policy)) {
764
+ return null;
765
+ }
766
+ const normalized = cloneJSON(policy);
767
+ if (normalizeToken(normalized.type) === "workspacewrite") {
768
+ normalized.type = "workspaceWrite";
769
+ normalized.writableRoots = Array.isArray(normalized.writableRoots)
770
+ ? normalized.writableRoots
771
+ : [];
772
+ normalized.excludeSlashTmp = Boolean(normalized.excludeSlashTmp);
773
+ normalized.excludeTmpdirEnvVar = Boolean(normalized.excludeTmpdirEnvVar);
774
+ normalized.networkAccess = Boolean(normalized.networkAccess);
775
+ }
776
+ if (normalizeToken(normalized.type) === "readonly") {
777
+ normalized.type = "readOnly";
778
+ normalized.networkAccess = Boolean(normalized.networkAccess);
779
+ }
780
+ return normalized;
781
+ }
782
+
783
+ function permissionProfileFromId(value) {
784
+ const id = readString(value);
785
+ return id ? { id, extends: null } : null;
590
786
  }
591
787
 
592
788
  function buildConversationTurn(turn, {
@@ -596,7 +792,7 @@ function buildConversationTurn(turn, {
596
792
  now = () => Date.now(),
597
793
  } = {}) {
598
794
  const turnId = readString(turn?.id) || readString(turn?.turnId) || readString(turn?.turn_id);
599
- const params = cloneJSON(previousTurn?.params || {
795
+ const params = normalizeTurnParamsCompatibility(cloneJSON(previousTurn?.params || {
600
796
  threadId,
601
797
  input: [],
602
798
  cwd: cwd || null,
@@ -611,7 +807,7 @@ function buildConversationTurn(turn, {
611
807
  outputSchema: null,
612
808
  collaborationMode: null,
613
809
  attachments: [],
614
- });
810
+ }), { cwd });
615
811
  const builtTurn = {
616
812
  id: turnId,
617
813
  turnId,
@@ -678,6 +874,9 @@ function applyPendingTurnStartParams(
678
874
  ...turn.params,
679
875
  ...cloneJSON(pendingParams),
680
876
  };
877
+ turn.params = normalizeTurnParamsCompatibility(turn.params, {
878
+ cwd: readString(turn.params?.cwd) || readString(conversation?.cwd),
879
+ });
681
880
  normalizeTurnInitialPrompt(turn);
682
881
  }
683
882
 
@@ -1262,6 +1461,7 @@ module.exports = {
1262
1461
  readThreadIdFromParams,
1263
1462
  readTurnIdFromParams,
1264
1463
  readTurnIdFromTurn,
1464
+ synchronizeDesktopConversationCompatibility,
1265
1465
  timestampSecondsToMs,
1266
1466
  upsertItem,
1267
1467
  upsertTurn,
@@ -27,6 +27,7 @@ const {
27
27
  buildConversationStateFromThread,
28
28
  createEmptyConversationState,
29
29
  readThreadIdFromParams,
30
+ synchronizeDesktopConversationCompatibility,
30
31
  } = require("./desktop-ipc-conversation-adapter");
31
32
  const {
32
33
  DEFAULT_MAX_PATCH_BYTES,
@@ -94,20 +95,33 @@ const OWNER_INBOUND_METHODS = new Set([
94
95
 
95
96
  const THREAD_READ_METHODS = new Set(["thread/read", "thread/resume"]);
96
97
 
97
- const ALLOWED_TURN_START_PARAM_KEYS = new Set([
98
+ // Current app-server v2 TurnStartParams fields. Desktop's adjacent turnStart.context
99
+ // is presentation state, not part of this RPC shape, so it stays outside the request.
100
+ const APP_SERVER_TURN_START_PARAM_KEYS = new Set([
98
101
  "threadId",
99
102
  "input",
103
+ "additionalContext",
100
104
  "cwd",
101
105
  "approvalPolicy",
102
106
  "approvalsReviewer",
103
107
  "sandboxPolicy",
104
108
  "model",
105
109
  "serviceTier",
110
+ "serviceTierForTurn",
106
111
  "effort",
107
112
  "summary",
108
113
  "personality",
109
114
  "outputSchema",
110
115
  "collaborationMode",
116
+ "clientUserMessageId",
117
+ "cyberAccessProgram",
118
+ "environments",
119
+ "multiAgentMode",
120
+ "permissions",
121
+ "responsesapiClientMetadata",
122
+ "runtimeWorkspaceRoots",
123
+ "toolOutput",
124
+ "turnTrigger",
111
125
  ]);
112
126
 
113
127
  function createDesktopIpcLiveOwner({
@@ -1104,6 +1118,7 @@ function createDesktopIpcLiveOwner({
1104
1118
  return true;
1105
1119
  }
1106
1120
  runtimeSettingsStore?.attachToConversation?.(threadId, conversationState);
1121
+ synchronizeDesktopConversationCompatibility(conversationState);
1107
1122
  if (shouldDelayInitialSnapshotForHistory(threadId)) {
1108
1123
  return false;
1109
1124
  }
@@ -1367,10 +1382,7 @@ function createDesktopIpcLiveOwner({
1367
1382
  }
1368
1383
 
1369
1384
  async function handleFollowerStartTurn(conversationId, params) {
1370
- const rawTurnStartParams = params.turnStartParams
1371
- || params.turn_start_params
1372
- || params.turnStart
1373
- || params;
1385
+ const rawTurnStartParams = readFollowerTurnStartParams(params);
1374
1386
  const codexParams = mergeFollowerRuntimeOverrides(conversationId, sanitizeTurnStartParams({
1375
1387
  ...rawTurnStartParams,
1376
1388
  threadId: conversationId,
@@ -1380,7 +1392,9 @@ function createDesktopIpcLiveOwner({
1380
1392
  ? normalizedParams
1381
1393
  : codexParams;
1382
1394
  markOwnedThread(conversationId);
1383
- const senderRequestId = params.senderRequestId || params.sender_request_id;
1395
+ const senderRequestId = params.senderRequestId
1396
+ || params.sender_request_id
1397
+ || rawTurnStartParams.clientUserMessageId;
1384
1398
  const isKnownHeldPhoneStart = Boolean(
1385
1399
  requestIdKey(senderRequestId)
1386
1400
  && pendingTurnStartEntriesByRequestId.has(requestIdKey(senderRequestId))
@@ -1417,6 +1431,17 @@ function createDesktopIpcLiveOwner({
1417
1431
  }
1418
1432
  }
1419
1433
 
1434
+ function readFollowerTurnStartParams(params) {
1435
+ const turnStart = isPlainJSONObject(params.turnStart) ? params.turnStart : null;
1436
+ if (isPlainJSONObject(turnStart?.request)) {
1437
+ return turnStart.request;
1438
+ }
1439
+ return params.turnStartParams
1440
+ || params.turn_start_params
1441
+ || params.turnStart
1442
+ || params;
1443
+ }
1444
+
1420
1445
  function mirrorFollowerUserPromptToPhone(threadId, turnStartParams, turnStartResult = null) {
1421
1446
  const text = visibleUserPromptFromInputEntries(turnStartParams?.input);
1422
1447
  if (!text) {
@@ -1461,16 +1486,50 @@ function createDesktopIpcLiveOwner({
1461
1486
  }
1462
1487
 
1463
1488
  async function handleFollowerInterruptTurn(conversationId, params) {
1464
- const turnId = readString(params.turnId)
1465
- || readString(params.turn_id)
1466
- || activeTurnIdForConversation(conversationId);
1489
+ const requestedTurnId = readString(params.expectedTurnId)
1490
+ || readString(params.expected_turn_id)
1491
+ || readString(params.turnId)
1492
+ || readString(params.turn_id);
1493
+ const activeTurnId = activeTurnIdForConversation(conversationId);
1494
+ if (requestedTurnId && requestedTurnId !== activeTurnId) {
1495
+ return {
1496
+ interruptedTurnId: null,
1497
+ ok: true,
1498
+ };
1499
+ }
1500
+
1501
+ const goalPauseError = await pauseActiveGoalForUserStop(
1502
+ conversationId,
1503
+ params.mode,
1504
+ Boolean(requestedTurnId)
1505
+ );
1506
+ const turnId = requestedTurnId || activeTurnId;
1467
1507
  if (!turnId) {
1468
- throw new Error("Missing turnId for follower interrupt request.");
1508
+ return followerInterruptResult(null, goalPauseError);
1469
1509
  }
1470
- return await sendCodexRequest("turn/interrupt", {
1510
+ await sendCodexRequest("turn/interrupt", {
1471
1511
  threadId: conversationId,
1472
1512
  turnId,
1473
1513
  });
1514
+ return followerInterruptResult(turnId, goalPauseError);
1515
+ }
1516
+
1517
+ async function pauseActiveGoalForUserStop(conversationId, mode, hasRequestedTurnId) {
1518
+ if (mode !== "user-stop" || hasRequestedTurnId) {
1519
+ return "";
1520
+ }
1521
+ if (conversations.get(conversationId)?.threadGoal?.status !== "active") {
1522
+ return "";
1523
+ }
1524
+ try {
1525
+ await sendCodexRequest("thread/goal/set", {
1526
+ threadId: conversationId,
1527
+ status: "paused",
1528
+ });
1529
+ return "";
1530
+ } catch (error) {
1531
+ return error?.message || "Failed to pause thread goal.";
1532
+ }
1474
1533
  }
1475
1534
 
1476
1535
  // Desktop follower approvals only carry decision-style payloads, but app-server
@@ -1786,8 +1845,7 @@ function createDesktopIpcLiveOwner({
1786
1845
  return turnId;
1787
1846
  }
1788
1847
  }
1789
- const latestTurn = turns[turns.length - 1];
1790
- return readString(latestTurn?.turnId) || readString(latestTurn?.id);
1848
+ return "";
1791
1849
  }
1792
1850
 
1793
1851
  return {
@@ -1881,7 +1939,7 @@ function normalizeInputEntriesForDesktop(input) {
1881
1939
  function sanitizeTurnStartParams(params) {
1882
1940
  const sanitized = {};
1883
1941
  for (const [key, value] of Object.entries(params || {})) {
1884
- if (ALLOWED_TURN_START_PARAM_KEYS.has(key)) {
1942
+ if (APP_SERVER_TURN_START_PARAM_KEYS.has(key)) {
1885
1943
  sanitized[key] = value;
1886
1944
  }
1887
1945
  }
@@ -1891,6 +1949,14 @@ function sanitizeTurnStartParams(params) {
1891
1949
  return sanitized;
1892
1950
  }
1893
1951
 
1952
+ function followerInterruptResult(interruptedTurnId, goalPauseError) {
1953
+ return {
1954
+ interruptedTurnId,
1955
+ ...(goalPauseError ? { goalPauseError } : {}),
1956
+ ok: true,
1957
+ };
1958
+ }
1959
+
1894
1960
  function readThreadFromResponse(message) {
1895
1961
  const result = message?.result || message?.payload || {};
1896
1962
  return readThreadFromPayload(result);
@@ -28,12 +28,12 @@ const DESKTOP_IPC_METHOD_VERSIONS = new Map([
28
28
  ["thread-unarchived", 1],
29
29
  ["thread-read-state-changed", 2],
30
30
  ["thread-queued-followups-changed", 1],
31
- ["thread-follower-start-turn", 1],
31
+ ["thread-follower-start-turn", 2],
32
32
  ["thread-follower-load-complete-history", 1],
33
33
  ["thread-follower-update-thread-settings", 1],
34
34
  ["thread-follower-compact-thread", 1],
35
35
  ["thread-follower-steer-turn", 1],
36
- ["thread-follower-interrupt-turn", 3],
36
+ ["thread-follower-interrupt-turn", 4],
37
37
  ["thread-follower-set-model-and-reasoning", 1],
38
38
  ["thread-follower-set-collaboration-mode", 1],
39
39
  ["thread-follower-edit-last-user-turn", 2],