@makerbi/remodex 3.2.0 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makerbi/remodex",
3
- "version": "3.2.0",
3
+ "version": "3.3.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",
package/src/bridge.js CHANGED
@@ -68,6 +68,13 @@ const {
68
68
  } = require("./desktop-ipc-action-follower");
69
69
  const { createDesktopIpcLiveOwner } = require("./desktop-ipc-live-owner");
70
70
  const { createThreadRuntimeSettingsStore } = require("./thread-runtime-settings-store");
71
+ const { normalizeServiceTier, hasOwn } = require("./codex-runtime-settings");
72
+ const {
73
+ APP_SERVER_SOURCE,
74
+ DESKTOP_IPC_SOURCE,
75
+ createThreadActivityProjector,
76
+ } = require("./thread-activity-projector");
77
+ const { createThreadActivityStore } = require("./thread-activity-store");
71
78
  const { createThreadListProvenanceEnricher } = require("./thread-list-provenance");
72
79
  const { createWorktreeOriginEnricher } = require("./worktree-origin");
73
80
  const { forEachThreadRowInResponse } = require("./thread-row-enrichment");
@@ -600,6 +607,7 @@ function annotateTurnStateProbeWithMirrorActiveTurn(request, response, getMirror
600
607
 
601
608
  function canonicalThreadTurnsListRequest(request) {
602
609
  const params = { ...(request?.params || {}) };
610
+ params.itemsView ??= params.remodexTurnStateOnly === true ? "summary" : "full";
603
611
  delete params.remodexRequireCanonical;
604
612
  delete params.remodexTurnStateOnly;
605
613
  if (params.cursor === JSONL_OLDER_HANDOFF_CURSOR || threadTurnsListHandoffDescriptor(params.cursor)) {
@@ -761,9 +769,8 @@ function startBridge({
761
769
  const relaySessionUrl = `${relayBaseUrl}/${sessionId}`;
762
770
  const notificationSecret = randomBytes(24).toString("hex");
763
771
  const desktopRefresher = new CodexDesktopRefresher({
764
- // IPC snapshots are accepted only after Codex mounts the route and
765
- // announces itself as a follower. Auto-follow performs that one-time
766
- // activation; refreshEnabled still controls the legacy reload workaround.
772
+ // Opening a thread manually establishes its IPC subscription. Automatic
773
+ // navigation and the legacy refresh workaround both require an opt-in.
767
774
  enabled: config.refreshEnabled || config.desktopAutoFollowEnabled === true,
768
775
  // With IPC live sync streaming content, deep-link refreshes are only needed
769
776
  // to navigate Desktop onto the phone-driven thread, not to reload content.
@@ -806,9 +813,23 @@ function startBridge({
806
813
  const jsonlTurnsListRolloutCacheByThread = new Map();
807
814
  const jsonlTurnsListRolloutMissCacheByThread = new Map();
808
815
  const threadTurnsListFastPageCoordinator = createThreadTurnsListFastPageCoordinator();
809
- const threadRuntimeSettingsStore = createThreadRuntimeSettingsStore();
816
+ const threadRuntimeSettingsStore = createThreadRuntimeSettingsStore({
817
+ onChange(threadId, runtimeSettings) {
818
+ sendApplicationResponse(JSON.stringify({
819
+ method: "remodex/runtimeSettings/updated",
820
+ params: { threadId, runtimeSettings },
821
+ }));
822
+ },
823
+ });
810
824
  const threadListProvenanceEnricher = createThreadListProvenanceEnricher();
811
825
  const worktreeOriginEnricher = createWorktreeOriginEnricher();
826
+ const activityProjector = createThreadActivityProjector();
827
+ const activityStore = createThreadActivityStore({
828
+ sendApplicationResponse,
829
+ onEvict(threadId, source) {
830
+ activityProjector.forget(threadId, source);
831
+ },
832
+ });
812
833
  const trackedForwardedRequestMethods = new Set([
813
834
  "account/login/start",
814
835
  "account/login/cancel",
@@ -838,6 +859,7 @@ function startBridge({
838
859
  sendRelayRegistrationUpdate(nextDeviceState);
839
860
  },
840
861
  onSecureSessionReady(session) {
862
+ activityStore.resetSubscriber();
841
863
  activePhoneSummary = buildActivePhoneSummary(session, deviceState);
842
864
  const lastPublishedBridgeStatus = bridgeStatusPublisher.latest();
843
865
  if (lastPublishedBridgeStatus) {
@@ -864,9 +886,10 @@ function startBridge({
864
886
  // authoritative, but yields an active cache that stopped broadcasting;
865
887
  // a later Desktop snapshot is announced as a new source epoch so the
866
888
  // phone performs canonical repair instead of mixing both mirrors.
867
- shouldSuppressThread: (threadId) => shouldSuppressRolloutMirrorForThread(
889
+ shouldSuppressThread: (threadId, context) => shouldSuppressRolloutMirrorForThread(
868
890
  threadId,
869
- { desktopIpcActionFollower, desktopIpcLiveOwner }
891
+ { desktopIpcActionFollower, desktopIpcLiveOwner },
892
+ context
870
893
  ),
871
894
  })
872
895
  : null;
@@ -877,6 +900,7 @@ function startBridge({
877
900
  await sendCodexRequest("thread/read", buildCompleteThreadReadParams(threadId))
878
901
  ),
879
902
  forwardToLocalCodex: (rawMessage) => {
903
+ if (handleLocalRuntimeSettingsRequest(parseBridgeMessage(rawMessage))) return;
880
904
  observeDesktopIpcLiveOwnerInbound(rawMessage);
881
905
  forwardInboundRequestToCodex(rawMessage);
882
906
  },
@@ -890,6 +914,7 @@ function startBridge({
890
914
  onFollowerStateChanged(threadId, following) {
891
915
  desktopRefresher.handleFollowerStateChanged(threadId, following);
892
916
  },
917
+ onActivityObservation: handleDesktopActivityObservation,
893
918
  })
894
919
  : null;
895
920
  const desktopIpcLiveOwner = !config.codexEndpoint
@@ -1003,6 +1028,7 @@ function startBridge({
1003
1028
  bridgeStatusPublisher.stopHeartbeat();
1004
1029
  runtimeProviderRouter.shutdown();
1005
1030
  stopContextUsageWatcher();
1031
+ activityStore.dispose();
1006
1032
  rolloutLiveMirror?.stopAll();
1007
1033
  desktopIpcActionFollower?.stopAll();
1008
1034
  desktopIpcLiveOwner?.stopAll();
@@ -1196,6 +1222,14 @@ function startBridge({
1196
1222
  // Streaming deltas make this the hottest path in the bridge: parse the
1197
1223
  // envelope once and share the read-only object with every observer.
1198
1224
  const parsedMessage = parseBridgeMessage(message);
1225
+ if (parsedMessage?.result && forwardedInitializeRequestIds.has(String(parsedMessage.id))) {
1226
+ parsedMessage.result.remodexRuntimeSettingsVersion = 2;
1227
+ message = JSON.stringify(parsedMessage);
1228
+ }
1229
+ if (parsedMessage?.method === "thread/settings/updated"
1230
+ && parsedMessage.params?.threadSettings) {
1231
+ threadRuntimeSettingsStore.observe(parsedMessage.params.threadId, parsedMessage.params.threadSettings);
1232
+ }
1199
1233
  if (handleBridgeManagedCodexResponse(message, parsedMessage)) {
1200
1234
  return;
1201
1235
  }
@@ -1203,6 +1237,7 @@ function startBridge({
1203
1237
  trackCodexHandshakeState(message, parsedMessage);
1204
1238
  desktopRefresher.handleOutbound(message, parsedMessage);
1205
1239
  desktopIpcLiveOwner?.observeOutbound(message, parsedMessage);
1240
+ observeAppServerActivity(parsedMessage);
1206
1241
  pushNotificationTracker.handleOutbound(message, parsedMessage);
1207
1242
  rememberThreadFromMessage("codex", message, parsedMessage);
1208
1243
  secureTransport.queueOutboundApplicationMessage(
@@ -1212,6 +1247,7 @@ function startBridge({
1212
1247
  });
1213
1248
 
1214
1249
  codex.onClose(() => {
1250
+ activityStore.markSourceStale(APP_SERVER_SOURCE);
1215
1251
  const wasShuttingDown = isShuttingDown;
1216
1252
  clearRelayWatchdog();
1217
1253
  bridgeStatusPublisher.stopHeartbeat();
@@ -1243,7 +1279,11 @@ function startBridge({
1243
1279
 
1244
1280
  // Routes decrypted app payloads through the same bridge handlers as before.
1245
1281
  function handleApplicationMessage(rawMessage) {
1282
+ rawMessage = normalizePhoneRuntimeRequest(rawMessage);
1246
1283
  const parsedMessage = parseBridgeMessage(rawMessage);
1284
+ if (activityStore.handleRequest(parsedMessage)) {
1285
+ return;
1286
+ }
1247
1287
  if (handleBridgeManagedHandshakeMessage(rawMessage, sendApplicationResponse, parsedMessage)) {
1248
1288
  return;
1249
1289
  }
@@ -1277,9 +1317,17 @@ function startBridge({
1277
1317
  })) {
1278
1318
  return;
1279
1319
  }
1320
+ // Provider-owned renames must reach their runtime instead of the Codex-only
1321
+ // catalog persistence path in the local git/thread handler.
1322
+ if (parsedMessage?.method === "thread/name/set"
1323
+ && runtimeProviderRouter.handleApplicationMessage(rawMessage, {
1324
+ sendResponse: sendApplicationResponse,
1325
+ })) {
1326
+ return;
1327
+ }
1280
1328
  if (handleGitRequest(rawMessage, sendApplicationResponse, {
1281
1329
  codexAppPath: config.codexAppPath,
1282
- onThreadNameSet: sendThreadNameUpdatedNotification,
1330
+ sendCodexRequest,
1283
1331
  })) {
1284
1332
  return;
1285
1333
  }
@@ -1298,6 +1346,7 @@ function startBridge({
1298
1346
  })) {
1299
1347
  return;
1300
1348
  }
1349
+ if (handleLocalRuntimeSettingsRequest(parsedMessage)) return;
1301
1350
  observeDesktopIpcLiveOwnerInbound(rawMessage, parsedMessage);
1302
1351
  if (handleBridgeManagedThreadTurnsListRequest(rawMessage, sendApplicationResponse)) {
1303
1352
  return;
@@ -1305,6 +1354,24 @@ function startBridge({
1305
1354
  forwardInboundRequestToCodex(rawMessage);
1306
1355
  }
1307
1356
 
1357
+ function handleLocalRuntimeSettingsRequest(parsedMessage) {
1358
+ if (parsedMessage?.method === "thread/settings/update" && parsedMessage.id != null) {
1359
+ const { threadId, ...settings } = parsedMessage.params || {};
1360
+ const update = desktopIpcLiveOwner?.updateThreadSettings
1361
+ ? desktopIpcLiveOwner.updateThreadSettings(threadId, settings)
1362
+ : sendCodexRequest("thread/settings/update", { threadId, ...settings }).then(() => ({
1363
+ runtimeSettings: threadRuntimeSettingsStore.commit(threadId, settings, { source: "phone" }),
1364
+ }));
1365
+ Promise.resolve(update).then((result) => {
1366
+ sendApplicationResponse(JSON.stringify({ id: parsedMessage.id, result }));
1367
+ }).catch((error) => {
1368
+ sendApplicationResponse(createJsonRpcErrorResponse(parsedMessage.id, error, "runtime_settings_update_failed"));
1369
+ });
1370
+ return true;
1371
+ }
1372
+ return false;
1373
+ }
1374
+
1308
1375
  function forwardInboundRequestToCodex(rawMessage) {
1309
1376
  const codexRequest = stripRuntimeProviderFieldsForCodex(
1310
1377
  normalizeTurnStartForCodex(rawMessage)
@@ -1358,6 +1425,58 @@ function startBridge({
1358
1425
  }
1359
1426
  }
1360
1427
 
1428
+ function observeAppServerActivity(message) {
1429
+ if (isShuttingDown) {
1430
+ return;
1431
+ }
1432
+ const projection = activityProjector.observeAppServer(message);
1433
+ if (projection?.entry) {
1434
+ const entry = projection.entry;
1435
+ const previous = activityStore.get(entry.threadId);
1436
+ if (previous?.source === DESKTOP_IPC_SOURCE
1437
+ && !desktopIpcLiveOwner?.isThreadOwned(entry.threadId)) {
1438
+ // Catalog changes do not transfer runtime ownership to this app-server.
1439
+ if (message.method === "thread/name/updated" || message.method === "thread/started") {
1440
+ activityStore.upsert({
1441
+ ...previous,
1442
+ ...(entry.title ? { title: entry.title } : {}),
1443
+ ...(entry.cwd ? { cwd: entry.cwd } : {}),
1444
+ });
1445
+ }
1446
+ return;
1447
+ }
1448
+ activityStore.upsert({
1449
+ ...(previous?.title ? { title: previous.title } : {}),
1450
+ ...(previous?.cwd ? { cwd: previous.cwd } : {}),
1451
+ ...entry,
1452
+ });
1453
+ } else if (projection?.removedThreadId) {
1454
+ activityStore.remove(projection.removedThreadId);
1455
+ }
1456
+ }
1457
+
1458
+ function handleDesktopActivityObservation(observation) {
1459
+ if (isShuttingDown) {
1460
+ return;
1461
+ }
1462
+ if (observation?.type === "state") {
1463
+ const entry = activityProjector.projectDesktopState(
1464
+ observation.threadId,
1465
+ observation.state,
1466
+ observation.sourceGeneration
1467
+ );
1468
+ activityStore.upsert(entry);
1469
+ return;
1470
+ }
1471
+ if (observation?.type === "disconnected") {
1472
+ activityStore.markSourceStale(DESKTOP_IPC_SOURCE, observation.sourceGeneration);
1473
+ return;
1474
+ }
1475
+ if (observation?.type === "removed") {
1476
+ activityStore.remove(observation.threadId, { source: DESKTOP_IPC_SOURCE });
1477
+ }
1478
+ }
1479
+
1361
1480
  // Encrypts bridge-generated responses instead of letting the relay see plaintext.
1362
1481
  function sendApplicationResponse(rawMessage) {
1363
1482
  secureTransport.queueOutboundApplicationMessage(
@@ -1374,25 +1493,6 @@ function startBridge({
1374
1493
  sendApplicationResponse(rawMessage);
1375
1494
  }
1376
1495
 
1377
- // Mirrors accepted local renames back to the phone using the existing push-event shape.
1378
- function sendThreadNameUpdatedNotification(result) {
1379
- const threadId = readString(result?.threadId || result?.thread_id);
1380
- const name = readString(result?.name || result?.title);
1381
- if (!threadId || !name) {
1382
- return;
1383
- }
1384
-
1385
- sendApplicationResponse(JSON.stringify({
1386
- method: "thread/name/updated",
1387
- params: {
1388
- threadId,
1389
- thread_id: threadId,
1390
- name,
1391
- title: name,
1392
- },
1393
- }));
1394
- }
1395
-
1396
1496
  function handleBridgeManagedThreadTurnsListRequest(rawMessage, sendResponse = sendApplicationResponse) {
1397
1497
  const request = parseAdaptiveThreadTurnsListRequest(rawMessage);
1398
1498
  if (!request) {
@@ -1713,6 +1813,10 @@ function startBridge({
1713
1813
  if (relaySanitizedRequestMethods.has(method)) {
1714
1814
  const trackedRequest = {
1715
1815
  method,
1816
+ isActiveThreadCatalog: method === "thread/list"
1817
+ && parsed.params?.archived !== true
1818
+ && !parsed.params?.cursor,
1819
+ threadListLimit: method === "thread/list" ? parsed.params?.limit : undefined,
1716
1820
  threadId: method === "thread/turns/list" || method === "thread/read" || method === "thread/resume"
1717
1821
  ? threadIdFromRequestParams(parsed.params)
1718
1822
  : "",
@@ -1771,6 +1875,9 @@ function startBridge({
1771
1875
  worktreeOriginEnricher.attachToThread(thread);
1772
1876
  });
1773
1877
  normalizedMessage = JSON.stringify(parsed);
1878
+ if (trackedRequest.isActiveThreadCatalog) {
1879
+ desktopIpcActionFollower?.observeThreadListResponse(parsed.result, { limit: trackedRequest.threadListLimit });
1880
+ }
1774
1881
  }
1775
1882
 
1776
1883
  return sanitizeThreadHistoryImagesForRelay(normalizedMessage, trackedRequest.method, trackedRequest);
@@ -1997,6 +2104,7 @@ function startBridge({
1997
2104
  id: parsed.id,
1998
2105
  result: {
1999
2106
  bridgeManaged: true,
2107
+ remodexRuntimeSettingsVersion: 2,
2000
2108
  },
2001
2109
  }));
2002
2110
  return true;
@@ -2107,8 +2215,10 @@ function startBridge({
2107
2215
 
2108
2216
  // Runs bridge-private JSON-RPC calls against the local app-server so token-bearing responses
2109
2217
  // can power bridge features like transcription without ever reaching the phone.
2110
- function sendCodexRequest(method, params) {
2111
- const requestId = `bridge-managed-${randomBytes(12).toString("hex")}`;
2218
+ function sendCodexRequest(method, params, callerRequestId = null) {
2219
+ const requestId = callerRequestId == null
2220
+ ? `bridge-managed-${randomBytes(12).toString("hex")}`
2221
+ : String(callerRequestId);
2112
2222
  const payload = JSON.stringify({
2113
2223
  id: requestId,
2114
2224
  method,
@@ -2510,6 +2620,25 @@ function disableUnsupportedReasoningSummaryForTurnStart(rawMessage) {
2510
2620
  });
2511
2621
  }
2512
2622
 
2623
+ // Upgrade the old phone wire convention once at ingress. Internally an omitted
2624
+ // speed always inherits; modern clients distinguish that from explicit Standard.
2625
+ function normalizePhoneRuntimeRequest(rawMessage) {
2626
+ const parsed = parseBridgeJSON(rawMessage);
2627
+ if (!parsed || !["turn/start", "thread/start", "thread/settings/update"].includes(parsed.method)
2628
+ || !parsed.params || typeof parsed.params !== "object") return rawMessage;
2629
+ const params = { ...parsed.params };
2630
+ if (parsed.method === "turn/start" && params.remodexRuntimeSettingsVersion !== 2
2631
+ && !hasOwn(params, "serviceTier") && !hasOwn(params, "service_tier")) {
2632
+ params.serviceTier = "default";
2633
+ }
2634
+ delete params.remodexRuntimeSettingsVersion;
2635
+ if (hasOwn(params, "serviceTier") && params.serviceTier != null) {
2636
+ params.serviceTier = normalizeServiceTier(params.serviceTier)
2637
+ ?? (parsed.method === "thread/settings/update" ? null : "default");
2638
+ }
2639
+ return JSON.stringify({ ...parsed, params });
2640
+ }
2641
+
2513
2642
  function normalizeTurnStartParamsForCodex(params) {
2514
2643
  const normalizedRawMessage = normalizeTurnStartForCodex(JSON.stringify({
2515
2644
  method: "turn/start",
@@ -5113,12 +5242,12 @@ function persistBridgePreferences(
5113
5242
  function shouldSuppressRolloutMirrorForThread(
5114
5243
  threadId,
5115
5244
  { desktopIpcActionFollower = null, desktopIpcLiveOwner = null } = {},
5116
- { fallbackActivityAt = 0 } = {}
5245
+ context = {}
5117
5246
  ) {
5118
5247
  // Desktop ownership is an expiring live lease, not a permanent boolean. A
5119
5248
  // stale IPC snapshot used to mute an actively growing rollout forever.
5120
5249
  const followerIsFresh = typeof desktopIpcActionFollower?.hasFreshLiveThreadState === "function"
5121
- ? desktopIpcActionFollower.hasFreshLiveThreadState(threadId, { fallbackActivityAt })
5250
+ ? desktopIpcActionFollower.hasFreshLiveThreadState(threadId, context)
5122
5251
  : desktopIpcActionFollower?.hasLiveThreadState(threadId);
5123
5252
  const ownerIsFresh = typeof desktopIpcLiveOwner?.isFreshThreadOwned === "function"
5124
5253
  ? desktopIpcLiveOwner.isFreshThreadOwned(threadId)
@@ -5141,6 +5270,7 @@ module.exports = {
5141
5270
  isContextualUserItemNotification,
5142
5271
  maybeMergeLatestJsonlTurnIntoTurnsListResponse,
5143
5272
  normalizeTurnStartForCodex,
5273
+ normalizePhoneRuntimeRequest,
5144
5274
  normalizeRelayBoundJsonRpcMessage,
5145
5275
  persistBridgePreferences,
5146
5276
  resolveJsonlTurnsListRolloutPathForFallback,
@@ -101,6 +101,9 @@ class CodexDesktopRefresher {
101
101
  }
102
102
 
103
103
  handleInbound(rawMessage, parsedMessage = null) {
104
+ if (!this.canRefresh()) {
105
+ return;
106
+ }
104
107
  const parsed = parsedMessage ?? safeParseJSON(rawMessage);
105
108
  if (!parsed) {
106
109
  return;
@@ -147,6 +150,9 @@ class CodexDesktopRefresher {
147
150
  }
148
151
 
149
152
  handleOutbound(rawMessage, parsedMessage = null) {
153
+ if (!this.canRefresh()) {
154
+ return;
155
+ }
150
156
  const parsed = parsedMessage ?? safeParseJSON(rawMessage);
151
157
  if (!parsed) {
152
158
  return;
@@ -744,11 +750,6 @@ function readBridgeConfig({
744
750
  const desktopIpcLiveSyncEnabled = explicitDesktopIpcLiveSyncEnabled == null
745
751
  ? true
746
752
  : explicitDesktopIpcLiveSyncEnabled;
747
- const defaultDesktopAutoFollowEnabled = (
748
- platform === "darwin"
749
- && !codexEndpoint
750
- && desktopIpcLiveSyncEnabled
751
- );
752
753
  return {
753
754
  relayUrl,
754
755
  relayAccessToken,
@@ -774,9 +775,8 @@ function readBridgeConfig({
774
775
  codexEndpoint,
775
776
  desktopIpcSocketPath: readFirstDefinedEnv(["REMODEX_DESKTOP_IPC_SOCKET"], "", env),
776
777
  desktopIpcLiveSyncEnabled,
777
- desktopAutoFollowEnabled: explicitDesktopAutoFollowEnabled == null
778
- ? defaultDesktopAutoFollowEnabled
779
- : explicitDesktopAutoFollowEnabled,
778
+ // Live sync must not navigate or activate Desktop without an explicit opt-in.
779
+ desktopAutoFollowEnabled: explicitDesktopAutoFollowEnabled === true,
780
780
  desktopIpcSnapshotDebounceMs: parseIntegerEnv(
781
781
  readFirstDefinedEnv(["REMODEX_DESKTOP_IPC_SNAPSHOT_DEBOUNCE_MS"], "75", env),
782
782
  75
@@ -0,0 +1,113 @@
1
+ // Canonical runtime settings shared by the app-server and Desktop IPC adapters.
2
+ // Missing fields inherit; null clears speed, while null effort inherits unless
3
+ // supplied in collaboration settings or an authoritative snapshot. Turn-only overrides
4
+ // are deliberately excluded from the persistent next-turn settings.
5
+ const hasOwn = (value, key) => Object.prototype.hasOwnProperty.call(value || {}, key);
6
+ const string = (value) => typeof value === "string" ? value.trim() : "";
7
+ const THREAD_SETTINGS_UPDATE_KEYS = [
8
+ "approvalPolicy", "approvalsReviewer", "collaborationMode", "cwd", "effort", "model",
9
+ "multiAgentMode", "permissions", "personality", "sandboxPolicy", "serviceTier", "summary",
10
+ ];
11
+
12
+ function normalizeThreadSettingsUpdate(params, options = {}) {
13
+ const settings = Object.fromEntries(THREAD_SETTINGS_UPDATE_KEYS.filter((key) => hasOwn(params, key))
14
+ .map((key) => [key, structuredClone(params[key])]));
15
+ const patch = runtimeSettingsPatch(params, options);
16
+ if (settings.effort == null && !options.authoritative) delete settings.effort;
17
+ if (hasOwn(patch, "serviceTier")) settings.serviceTier = patch.serviceTier;
18
+ if (settings.collaborationMode?.settings) {
19
+ if (patch.model) settings.collaborationMode.settings.model = patch.model;
20
+ if (hasOwn(patch, "reasoningEffort")) settings.collaborationMode.settings.reasoning_effort = patch.reasoningEffort;
21
+ }
22
+ return settings;
23
+ }
24
+
25
+ // Confirmations may be partial (for example the first edit only selects Fast).
26
+ // Preserve omission instead of manufacturing resets for unknown fields.
27
+ function threadSettingsFromRuntimeSettings(settings) {
28
+ const patch = runtimeSettingsPatch(settings);
29
+ const result = { ...patch };
30
+ if (hasOwn(patch, "reasoningEffort")) {
31
+ result.effort = patch.reasoningEffort;
32
+ delete result.reasoningEffort;
33
+ }
34
+ return result;
35
+ }
36
+
37
+ function normalizeServiceTier(value) {
38
+ const tier = string(value);
39
+ if (!tier || tier === "default") return null;
40
+ return tier === "fast" ? "priority" : tier;
41
+ }
42
+
43
+ function runtimeSettingsPatch(params = {}, { authoritative = false } = {}) {
44
+ const nested = params.collaborationMode?.settings || params.collaboration_mode?.settings || {};
45
+ const patch = {};
46
+ if (string(params.model) || string(nested.model)) patch.model = string(params.model) || string(nested.model);
47
+ for (const [object, key] of [[params, "effort"], [params, "reasoningEffort"], [nested, "reasoning_effort"], [nested, "reasoningEffort"]]) {
48
+ if (hasOwn(object, key) && (key !== "effort" || object[key] != null || authoritative)) {
49
+ patch.reasoningEffort = string(object[key]) || null;
50
+ break;
51
+ }
52
+ }
53
+ if (hasOwn(params, "serviceTier") || hasOwn(params, "service_tier")) {
54
+ patch.serviceTier = normalizeServiceTier(hasOwn(params, "serviceTier") ? params.serviceTier : params.service_tier);
55
+ }
56
+ return patch;
57
+ }
58
+
59
+ function runtimeSettingsFromConversation(conversation = {}) {
60
+ const settings = conversation.latestThreadSettings || {};
61
+ const fallback = {};
62
+ if (string(conversation.latestModel)) fallback.model = conversation.latestModel;
63
+ if (hasOwn(conversation, "latestReasoningEffort")) fallback.reasoningEffort = conversation.latestReasoningEffort;
64
+ if (hasOwn(conversation, "latestServiceTier")) fallback.serviceTier = conversation.latestServiceTier;
65
+ return { ...runtimeSettingsPatch(fallback), ...runtimeSettingsPatch(settings, { authoritative: true }) };
66
+ }
67
+
68
+ function applyRuntimeSettingsToConversation(conversation, params, options = {}) {
69
+ if (!conversation) return;
70
+ const patch = runtimeSettingsPatch(params, options);
71
+ const settings = { ...(conversation.latestThreadSettings || {}), ...normalizeThreadSettingsUpdate(params, options) };
72
+ if (hasOwn(patch, "model")) conversation.latestModel = settings.model = patch.model;
73
+ if (hasOwn(patch, "reasoningEffort")) conversation.latestReasoningEffort = settings.effort = patch.reasoningEffort;
74
+ if (hasOwn(patch, "serviceTier")) conversation.latestServiceTier = settings.serviceTier = patch.serviceTier;
75
+ const mode = params.collaborationMode || conversation.latestCollaborationMode;
76
+ if (mode || patch.model) {
77
+ conversation.latestCollaborationMode = {
78
+ ...(mode || { mode: "default" }),
79
+ settings: {
80
+ developer_instructions: null,
81
+ ...(mode?.settings || {}),
82
+ ...(patch.model ? { model: patch.model } : {}),
83
+ ...(hasOwn(patch, "reasoningEffort") ? { reasoning_effort: patch.reasoningEffort } : {}),
84
+ },
85
+ };
86
+ settings.collaborationMode = conversation.latestCollaborationMode;
87
+ }
88
+ conversation.latestThreadSettings = settings;
89
+ }
90
+
91
+ // Ordering is scoped to one runtime owner and task. Failures don't poison the
92
+ // queue, and the next mutation always waits for the preceding acknowledgement.
93
+ function createThreadMutationQueue() {
94
+ const pending = new Map();
95
+ return function enqueue(threadId, operation) {
96
+ const result = (pending.get(threadId) || Promise.resolve()).catch(() => {}).then(operation);
97
+ pending.set(threadId, result);
98
+ result.finally(() => { if (pending.get(threadId) === result) pending.delete(threadId); }).catch(() => {});
99
+ return result;
100
+ };
101
+ }
102
+
103
+ module.exports = {
104
+ applyRuntimeSettingsToConversation,
105
+ createThreadMutationQueue,
106
+ hasOwn,
107
+ normalizeServiceTier,
108
+ normalizeThreadSettingsUpdate,
109
+ THREAD_SETTINGS_UPDATE_KEYS,
110
+ threadSettingsFromRuntimeSettings,
111
+ runtimeSettingsFromConversation,
112
+ runtimeSettingsPatch,
113
+ };