@makerbi/remodex 1.5.3 → 1.5.4

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.
File without changes
package/bin/remodex.js CHANGED
@@ -37,11 +37,29 @@ const defaultDeps = {
37
37
  };
38
38
 
39
39
  if (require.main === module) {
40
- void main();
40
+ void runCli();
41
41
  }
42
42
 
43
43
  // ─── ENTRY POINT ─────────────────────────────────────────────
44
44
 
45
+ // Runs the CLI process and turns expected configuration failures into readable terminal output.
46
+ async function runCli({
47
+ mainImpl = main,
48
+ consoleImpl = console,
49
+ exitImpl = process.exit,
50
+ } = {}) {
51
+ try {
52
+ await mainImpl();
53
+ } catch (error) {
54
+ const rawMessage = error && typeof error.message === "string"
55
+ ? error.message.trim()
56
+ : String(error || "Command failed");
57
+ const message = rawMessage || "Command failed";
58
+ consoleImpl.error(message.startsWith("[remodex]") ? message : `[remodex] ${message}`);
59
+ exitImpl(1);
60
+ }
61
+ }
62
+
45
63
  async function main({
46
64
  argv = process.argv,
47
65
  platform = process.platform,
@@ -351,4 +369,5 @@ module.exports = {
351
369
  isVersionCommand,
352
370
  main,
353
371
  parseCliArgs,
372
+ runCli,
354
373
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makerbi/remodex",
3
- "version": "1.5.3",
3
+ "version": "1.5.4",
4
4
  "description": "Local bridge between Codex and the Remodex mobile app. Run `remodex up` to start.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,121 @@
1
+ // FILE: bridge-status.js
2
+ // Purpose: Owns bridge status publishing and stale-relay heartbeat downgrades.
3
+ // Layer: CLI service helper
4
+ // Exports: createBridgeStatusPublisher, buildHeartbeatBridgeStatus, hasRelayConnectionGoneStale
5
+ // Depends on: timers
6
+
7
+ const BRIDGE_STATUS_HEARTBEAT_INTERVAL_MS = 5_000;
8
+ // Keep the watchdog above the relay heartbeat cadence so quiet healthy sockets survive idle gaps.
9
+ const RELAY_WATCHDOG_STALE_AFTER_MS = 70_000;
10
+ const STALE_RELAY_STATUS_MESSAGE = "Relay heartbeat stalled; reconnect pending.";
11
+
12
+ // Wraps daemon status publication so bridge.js does not own heartbeat bookkeeping.
13
+ function createBridgeStatusPublisher({
14
+ onBridgeStatus = null,
15
+ getCodexLaunchState = () => undefined,
16
+ heartbeatIntervalMs = BRIDGE_STATUS_HEARTBEAT_INTERVAL_MS,
17
+ now = () => Date.now(),
18
+ } = {}) {
19
+ let lastPublishedBridgeStatus = null;
20
+ let heartbeatTimer = null;
21
+
22
+ function publish(status) {
23
+ const nextStatus = {
24
+ ...status,
25
+ codexLaunchState: getCodexLaunchState(),
26
+ };
27
+ lastPublishedBridgeStatus = nextStatus;
28
+ onBridgeStatus?.(nextStatus);
29
+ }
30
+
31
+ function startHeartbeat({
32
+ shouldPublish = () => true,
33
+ getLastRelayActivityAt = () => 0,
34
+ } = {}) {
35
+ if (heartbeatTimer) {
36
+ return;
37
+ }
38
+
39
+ heartbeatTimer = setInterval(() => {
40
+ if (!lastPublishedBridgeStatus || !shouldPublish()) {
41
+ return;
42
+ }
43
+
44
+ onBridgeStatus?.(buildHeartbeatBridgeStatus(
45
+ lastPublishedBridgeStatus,
46
+ getLastRelayActivityAt(),
47
+ { now: now() }
48
+ ));
49
+ }, heartbeatIntervalMs);
50
+ heartbeatTimer.unref?.();
51
+ }
52
+
53
+ function stopHeartbeat() {
54
+ if (!heartbeatTimer) {
55
+ return;
56
+ }
57
+
58
+ clearInterval(heartbeatTimer);
59
+ heartbeatTimer = null;
60
+ }
61
+
62
+ return {
63
+ latest() {
64
+ return lastPublishedBridgeStatus;
65
+ },
66
+ publish,
67
+ startHeartbeat,
68
+ stopHeartbeat,
69
+ };
70
+ }
71
+
72
+ // Treats silent relay sockets as stale so the daemon can self-heal after sleep/wake.
73
+ function hasRelayConnectionGoneStale(
74
+ lastActivityAt,
75
+ {
76
+ now = Date.now(),
77
+ staleAfterMs = RELAY_WATCHDOG_STALE_AFTER_MS,
78
+ } = {}
79
+ ) {
80
+ return Number.isFinite(lastActivityAt)
81
+ && Number.isFinite(now)
82
+ && now - lastActivityAt >= staleAfterMs;
83
+ }
84
+
85
+ // Keeps persisted daemon status honest by downgrading stale "connected" snapshots.
86
+ function buildHeartbeatBridgeStatus(
87
+ status,
88
+ lastActivityAt,
89
+ {
90
+ now = Date.now(),
91
+ staleAfterMs = RELAY_WATCHDOG_STALE_AFTER_MS,
92
+ staleMessage = STALE_RELAY_STATUS_MESSAGE,
93
+ } = {}
94
+ ) {
95
+ if (!status || typeof status !== "object") {
96
+ return status;
97
+ }
98
+
99
+ if (status.connectionStatus !== "connected") {
100
+ return status;
101
+ }
102
+
103
+ if (!hasRelayConnectionGoneStale(lastActivityAt, { now, staleAfterMs })) {
104
+ return status;
105
+ }
106
+
107
+ return {
108
+ ...status,
109
+ connectionStatus: "disconnected",
110
+ lastError: staleMessage,
111
+ };
112
+ }
113
+
114
+ module.exports = {
115
+ BRIDGE_STATUS_HEARTBEAT_INTERVAL_MS,
116
+ RELAY_WATCHDOG_STALE_AFTER_MS,
117
+ STALE_RELAY_STATUS_MESSAGE,
118
+ buildHeartbeatBridgeStatus,
119
+ createBridgeStatusPublisher,
120
+ hasRelayConnectionGoneStale,
121
+ };
package/src/bridge.js CHANGED
@@ -2,7 +2,7 @@
2
2
  // Purpose: Runs Codex locally, bridges relay traffic, and coordinates desktop refreshes for Codex.app.
3
3
  // Layer: CLI service
4
4
  // Exports: startBridge
5
- // Depends on: ws, crypto, os, ./codex-home, ./qr, ./codex-desktop-refresher, ./codex-transport, ./rollout-watch, ./voice-handler, ./ios-app-compatibility
5
+ // Depends on: ws, crypto, os, ./bridge-status, ./codex-desktop-refresher, ./codex-transport, ./rollout-watch, ./voice-handler
6
6
 
7
7
  const WebSocket = require("ws");
8
8
  const { randomBytes, randomUUID } = require("crypto");
@@ -14,6 +14,11 @@ const {
14
14
  CodexDesktopRefresher,
15
15
  readBridgeConfig,
16
16
  } = require("./codex-desktop-refresher");
17
+ const {
18
+ buildHeartbeatBridgeStatus,
19
+ createBridgeStatusPublisher,
20
+ hasRelayConnectionGoneStale,
21
+ } = require("./bridge-status");
17
22
  const { createCodexTransport } = require("./codex-transport");
18
23
  const {
19
24
  createThreadRolloutActivityWatcher,
@@ -62,10 +67,6 @@ const {
62
67
 
63
68
  const execFileAsync = promisify(execFile);
64
69
  const RELAY_WATCHDOG_PING_INTERVAL_MS = 10_000;
65
- // Keep the watchdog above the relay heartbeat cadence so quiet healthy sockets survive idle gaps.
66
- const RELAY_WATCHDOG_STALE_AFTER_MS = 70_000;
67
- const BRIDGE_STATUS_HEARTBEAT_INTERVAL_MS = 5_000;
68
- const STALE_RELAY_STATUS_MESSAGE = "Relay heartbeat stalled; reconnect pending.";
69
70
  const CLOSE_CODE_INVALID_RELAY_REQUEST = 4000;
70
71
  const CLOSE_CODE_REPLACED_BY_NEW_MAC = 4001;
71
72
  const CLOSE_CODE_MAC_UNAUTHORIZED = 4005;
@@ -77,6 +78,9 @@ const RELAY_TURNS_LIST_TARGET_BUDGET_MS = 5_500;
77
78
  const RELAY_TURNS_LIST_BUDGET_RESERVE_MS = 1_000;
78
79
  const RELAY_TURNS_LIST_MAX_INITIAL_LIMIT = 5;
79
80
  const RELAY_TURNS_LIST_SAFE_RETRY_LIMIT = 5;
81
+ const MODELS_WITHOUT_REASONING_SUMMARY = new Set([
82
+ "gpt-5.3-codex-spark",
83
+ ]);
80
84
  const RELAY_TURNS_LIST_RESULT_KEYS = ["data", "items", "turns"];
81
85
  const RELAY_TURNS_LIST_PAGINATION_RESULT_KEYS = [
82
86
  "nextCursor",
@@ -176,9 +180,7 @@ function startBridge({
176
180
  let reconnectAttempt = 0;
177
181
  let reconnectTimer = null;
178
182
  let relayWatchdogTimer = null;
179
- let statusHeartbeatTimer = null;
180
183
  let lastRelayActivityAt = 0;
181
- let lastPublishedBridgeStatus = null;
182
184
  let lastConnectionStatus = null;
183
185
  let lastConnectionError = "";
184
186
  let codexLaunchState = config.codexEndpoint ? "connected" : "starting";
@@ -254,7 +256,14 @@ function startBridge({
254
256
  sendCodexRequest,
255
257
  logPrefix: "[remodex]",
256
258
  });
257
- startBridgeStatusHeartbeat();
259
+ const bridgeStatusPublisher = createBridgeStatusPublisher({
260
+ onBridgeStatus,
261
+ getCodexLaunchState: () => codexLaunchState,
262
+ });
263
+ bridgeStatusPublisher.startHeartbeat({
264
+ shouldPublish: () => !isShuttingDown,
265
+ getLastRelayActivityAt: () => lastRelayActivityAt,
266
+ });
258
267
  publishBridgeStatus({
259
268
  state: "starting",
260
269
  connectionStatus: "starting",
@@ -275,7 +284,7 @@ function startBridge({
275
284
  } else {
276
285
  console.error("[remodex] Failed to start `codex app-server`.");
277
286
  console.error(`[remodex] Launch command: ${codex.describe()}`);
278
- console.error("[remodex] Make sure the Codex CLI is installed and that the launcher works on this OS.");
287
+ console.error("[remodex] Make sure the Codex CLI is installed, authenticated, and launchable on this OS.");
279
288
  }
280
289
  console.error(error.message);
281
290
  process.exit(1);
@@ -283,6 +292,7 @@ function startBridge({
283
292
  // Marks the local Codex runtime as launchable before relay/network recovery updates.
284
293
  codex.onStarted(() => {
285
294
  codexLaunchState = "connected";
295
+ const lastPublishedBridgeStatus = bridgeStatusPublisher.latest();
286
296
  if (!lastPublishedBridgeStatus) {
287
297
  return;
288
298
  }
@@ -299,31 +309,6 @@ function startBridge({
299
309
  reconnectTimer = null;
300
310
  }
301
311
 
302
- // Periodically rewrites the latest bridge snapshot so CLI status does not stay frozen.
303
- function startBridgeStatusHeartbeat() {
304
- if (statusHeartbeatTimer) {
305
- return;
306
- }
307
-
308
- statusHeartbeatTimer = setInterval(() => {
309
- if (!lastPublishedBridgeStatus || isShuttingDown) {
310
- return;
311
- }
312
-
313
- onBridgeStatus?.(buildHeartbeatBridgeStatus(lastPublishedBridgeStatus, lastRelayActivityAt));
314
- }, BRIDGE_STATUS_HEARTBEAT_INTERVAL_MS);
315
- statusHeartbeatTimer.unref?.();
316
- }
317
-
318
- function clearBridgeStatusHeartbeat() {
319
- if (!statusHeartbeatTimer) {
320
- return;
321
- }
322
-
323
- clearInterval(statusHeartbeatTimer);
324
- statusHeartbeatTimer = null;
325
- }
326
-
327
312
  // Tracks relay liveness locally so sleep/wake zombie sockets can be force-reconnected.
328
313
  function markRelayActivity() {
329
314
  lastRelayActivityAt = Date.now();
@@ -402,7 +387,8 @@ function startBridge({
402
387
  bridgeWakeAssertion.stop();
403
388
  clearReconnectTimer();
404
389
  clearRelayWatchdog();
405
- clearBridgeStatusHeartbeat();
390
+ bridgeStatusPublisher.stopHeartbeat();
391
+ closeExtraRelayChannels();
406
392
  }, {
407
393
  exitCode: closeCode === CLOSE_CODE_MAC_UNAUTHORIZED ? 1 : 0,
408
394
  });
@@ -582,8 +568,10 @@ function startBridge({
582
568
 
583
569
  const nextSocket = new WebSocket(extraRelaySessionUrl, {
584
570
  headers: {
571
+ "User-Agent": buildRelayUserAgentHeader(),
585
572
  "x-role": "mac",
586
573
  "x-notification-secret": notificationSecret,
574
+ ...buildRelayAccessTokenHeaders(config),
587
575
  ...buildMacRegistrationHeaders(deviceState, extraPairingSession),
588
576
  },
589
577
  });
@@ -675,16 +663,23 @@ function startBridge({
675
663
  });
676
664
 
677
665
  codex.onClose(() => {
666
+ const wasShuttingDown = isShuttingDown;
678
667
  clearRelayWatchdog();
679
- clearBridgeStatusHeartbeat();
680
- const lastError = lastConnectionError || "";
668
+ bridgeStatusPublisher.stopHeartbeat();
669
+ const lastError = wasShuttingDown
670
+ ? ""
671
+ : (lastConnectionError || "Codex transport closed unexpectedly.");
681
672
  logConnectionStatus("disconnected", lastError);
682
673
  publishBridgeStatus({
683
- state: "stopped",
674
+ state: wasShuttingDown ? "stopped" : "error",
684
675
  connectionStatus: "disconnected",
685
676
  pid: process.pid,
686
677
  lastError,
687
678
  });
679
+ if (!wasShuttingDown) {
680
+ console.error(`[remodex] ${lastError}`);
681
+ process.exitCode = 1;
682
+ }
688
683
  isShuttingDown = true;
689
684
  bridgeWakeAssertion.stop();
690
685
  clearReconnectTimer();
@@ -706,7 +701,7 @@ function startBridge({
706
701
  bridgeWakeAssertion.stop();
707
702
  clearReconnectTimer();
708
703
  clearRelayWatchdog();
709
- clearBridgeStatusHeartbeat();
704
+ bridgeStatusPublisher.stopHeartbeat();
710
705
  closeExtraRelayChannels();
711
706
  }));
712
707
  process.on("SIGTERM", () => shutdown(codex, () => socket, () => {
@@ -714,7 +709,7 @@ function startBridge({
714
709
  bridgeWakeAssertion.stop();
715
710
  clearReconnectTimer();
716
711
  clearRelayWatchdog();
717
- clearBridgeStatusHeartbeat();
712
+ bridgeStatusPublisher.stopHeartbeat();
718
713
  closeExtraRelayChannels();
719
714
  }));
720
715
 
@@ -767,9 +762,10 @@ function startBridge({
767
762
  if (handleBridgeManagedThreadTurnsListRequest(rawMessage, sendResponse)) {
768
763
  return;
769
764
  }
770
- const codexMessage = prepareCodexForwardMessage(rawMessage, relayChannel);
765
+ const codexRequest = disableUnsupportedReasoningSummaryForTurnStart(rawMessage);
766
+ const codexMessage = prepareCodexForwardMessage(codexRequest, relayChannel);
771
767
  rememberForwardedRequestMethod(codexMessage);
772
- rememberThreadFromMessage("phone", rawMessage);
768
+ rememberThreadFromMessage("phone", codexMessage);
773
769
  mirrorUserMessageToPeerDevices(rawMessage, relayChannel);
774
770
  codex.send(codexMessage);
775
771
  }
@@ -1578,12 +1574,7 @@ function startBridge({
1578
1574
  }
1579
1575
 
1580
1576
  function publishBridgeStatus(status) {
1581
- const nextStatus = {
1582
- ...status,
1583
- codexLaunchState,
1584
- };
1585
- lastPublishedBridgeStatus = nextStatus;
1586
- onBridgeStatus?.(nextStatus);
1577
+ bridgeStatusPublisher.publish(status);
1587
1578
  }
1588
1579
 
1589
1580
  // Refreshes the relay's trusted-mac index after the QR bootstrap locks in a phone identity.
@@ -1825,6 +1816,41 @@ function buildRelayCloseStatusError(closeCode, closeReason = "") {
1825
1816
  : `Relay closed the connection (${closeCode}).`;
1826
1817
  }
1827
1818
 
1819
+ // Forces app-server summary generation off for models whose Responses API calls
1820
+ // reject reasoning.summary, while leaving the phone-facing runtime choice intact.
1821
+ function disableUnsupportedReasoningSummaryForTurnStart(rawMessage) {
1822
+ const parsed = parseBridgeJSON(rawMessage);
1823
+ if (!parsed || parsed.method !== "turn/start") {
1824
+ return rawMessage;
1825
+ }
1826
+
1827
+ const params = parsed.params && typeof parsed.params === "object" && !Array.isArray(parsed.params)
1828
+ ? parsed.params
1829
+ : null;
1830
+ if (!params || params.summary === "none") {
1831
+ return rawMessage;
1832
+ }
1833
+
1834
+ const model = readTurnStartModel(params);
1835
+ if (!MODELS_WITHOUT_REASONING_SUMMARY.has(model)) {
1836
+ return rawMessage;
1837
+ }
1838
+
1839
+ return JSON.stringify({
1840
+ ...parsed,
1841
+ params: {
1842
+ ...params,
1843
+ summary: "none",
1844
+ },
1845
+ });
1846
+ }
1847
+
1848
+ function readTurnStartModel(params) {
1849
+ return normalizeNonEmptyString(params?.model).toLowerCase()
1850
+ || normalizeNonEmptyString(params?.collaborationMode?.settings?.model).toLowerCase()
1851
+ || normalizeNonEmptyString(params?.collaboration_mode?.settings?.model).toLowerCase();
1852
+ }
1853
+
1828
1854
  function extractBridgeMessageContext(rawMessage) {
1829
1855
  let parsed = null;
1830
1856
  try {
@@ -3167,48 +3193,6 @@ function truncateRelayTextTail(value, maxChars) {
3167
3193
  return `…\n${tail}`;
3168
3194
  }
3169
3195
 
3170
- // Treats silent relay sockets as stale so the daemon can self-heal after sleep/wake.
3171
- function hasRelayConnectionGoneStale(
3172
- lastActivityAt,
3173
- {
3174
- now = Date.now(),
3175
- staleAfterMs = RELAY_WATCHDOG_STALE_AFTER_MS,
3176
- } = {}
3177
- ) {
3178
- return Number.isFinite(lastActivityAt)
3179
- && Number.isFinite(now)
3180
- && now - lastActivityAt >= staleAfterMs;
3181
- }
3182
-
3183
- // Keeps persisted daemon status honest by downgrading stale "connected" snapshots.
3184
- function buildHeartbeatBridgeStatus(
3185
- status,
3186
- lastActivityAt,
3187
- {
3188
- now = Date.now(),
3189
- staleAfterMs = RELAY_WATCHDOG_STALE_AFTER_MS,
3190
- staleMessage = STALE_RELAY_STATUS_MESSAGE,
3191
- } = {}
3192
- ) {
3193
- if (!status || typeof status !== "object") {
3194
- return status;
3195
- }
3196
-
3197
- if (status.connectionStatus !== "connected") {
3198
- return status;
3199
- }
3200
-
3201
- if (!hasRelayConnectionGoneStale(lastActivityAt, { now, staleAfterMs })) {
3202
- return status;
3203
- }
3204
-
3205
- return {
3206
- ...status,
3207
- connectionStatus: "disconnected",
3208
- lastError: staleMessage,
3209
- };
3210
- }
3211
-
3212
3196
  function persistBridgePreferences(
3213
3197
  {
3214
3198
  keepMacAwakeEnabled,
@@ -3230,6 +3214,7 @@ module.exports = {
3230
3214
  buildRelayAccessTokenHeaders,
3231
3215
  buildRelayUserAgentHeader,
3232
3216
  createMacOSBridgeWakeAssertion,
3217
+ disableUnsupportedReasoningSummaryForTurnStart,
3233
3218
  fetchAdaptiveThreadTurnsListForRelay,
3234
3219
  hasRelayConnectionGoneStale,
3235
3220
  isTerminalRelayCloseCode,
@@ -254,7 +254,32 @@ function shutdownCodexProcess(codex) {
254
254
  function createCodexCloseError({ code, signal, stderrBuffer, launchDescription }) {
255
255
  const details = stderrBuffer.trim();
256
256
  const reason = details || `Process exited with code ${code}${signal ? ` (signal: ${signal})` : ""}.`;
257
- return new Error(`Codex launcher ${launchDescription} failed: ${reason}`);
257
+ return new Error(formatCodexLaunchFailure({
258
+ launchDescription,
259
+ reason,
260
+ }));
261
+ }
262
+
263
+ // Turns common Codex auth/config failures into recovery guidance without handling secrets in Remodex.
264
+ function formatCodexLaunchFailure({ launchDescription, reason }) {
265
+ const message = `Codex launcher ${launchDescription} failed: ${reason}`;
266
+ const missingEnvVar = extractMissingEnvironmentVariable(reason);
267
+ if (!missingEnvVar) {
268
+ return message;
269
+ }
270
+
271
+ const guidance = [
272
+ `Codex is asking for ${missingEnvVar}, which usually means your Codex config forces API-key auth or a custom provider env var.`,
273
+ "Remodex does not store or forward OpenAI API keys.",
274
+ "Recommended fix: run `codex login` on this Mac, then restart Remodex.",
275
+ "If you intentionally use API-key auth, run `printenv OPENAI_API_KEY | codex login --with-api-key` or make that env var available to the Remodex daemon yourself.",
276
+ ];
277
+ return `${message}\n${guidance.join("\n")}`;
278
+ }
279
+
280
+ function extractMissingEnvironmentVariable(reason) {
281
+ const match = String(reason || "").match(/Missing environment variable:\s*`?([A-Za-z_][A-Za-z0-9_]*)`?/i);
282
+ return match ? match[1] : "";
258
283
  }
259
284
 
260
285
  function appendOutputBuffer(buffer, chunk) {
@@ -350,4 +375,6 @@ function createListenerBag() {
350
375
  module.exports = {
351
376
  createCodexLaunchPlans,
352
377
  createCodexTransport,
378
+ extractMissingEnvironmentVariable,
379
+ formatCodexLaunchFailure,
353
380
  };
@@ -16,12 +16,14 @@ const ACTION_METHODS = new Set([
16
16
  "item/commandExecution/requestApproval",
17
17
  "item/fileChange/requestApproval",
18
18
  "item/fileRead/requestApproval",
19
+ "item/permissions/requestApproval",
19
20
  "item/tool/requestUserInput",
20
21
  ]);
21
22
  const REPLY_METHOD_BY_ACTION_METHOD = new Map([
22
23
  ["item/commandExecution/requestApproval", "thread-follower-command-approval-decision"],
23
24
  ["item/fileChange/requestApproval", "thread-follower-file-approval-decision"],
24
25
  ["item/fileRead/requestApproval", "thread-follower-file-approval-decision"],
26
+ ["item/permissions/requestApproval", "thread-follower-file-approval-decision"],
25
27
  ["item/tool/requestUserInput", "thread-follower-submit-user-input"],
26
28
  ]);
27
29
  const METHOD_VERSION_BY_NAME = new Map([
@@ -483,7 +485,7 @@ function desktopFollowerPayloadForResponse(route, responseMessage) {
483
485
  };
484
486
  }
485
487
 
486
- const decision = readString(responseMessage?.result?.decision);
488
+ const decision = desktopApprovalDecisionForResponse(route.method, responseMessage?.result);
487
489
  if (!APPROVAL_DECISIONS.has(decision)) {
488
490
  return null;
489
491
  }
@@ -498,6 +500,47 @@ function desktopFollowerPayloadForResponse(route, responseMessage) {
498
500
  };
499
501
  }
500
502
 
503
+ function desktopApprovalDecisionForResponse(method, result) {
504
+ const explicitDecision = readString(result?.decision);
505
+ if (explicitDecision) {
506
+ return explicitDecision;
507
+ }
508
+
509
+ if (method !== "item/permissions/requestApproval") {
510
+ return "";
511
+ }
512
+
513
+ // Permission approvals use a grant payload on app-server, while Desktop IPC
514
+ // currently exposes only decision-style follower replies.
515
+ return hasGrantedPermission(result?.permissions) ? "accept" : "decline";
516
+ }
517
+
518
+ function hasGrantedPermission(value) {
519
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
520
+ return false;
521
+ }
522
+
523
+ if (Object.keys(value).length === 0) {
524
+ return false;
525
+ }
526
+
527
+ return Object.values(value).some((entry) => {
528
+ if (entry == null) {
529
+ return false;
530
+ }
531
+ if (typeof entry === "boolean") {
532
+ return entry;
533
+ }
534
+ if (Array.isArray(entry)) {
535
+ return entry.length > 0;
536
+ }
537
+ if (typeof entry === "object") {
538
+ return Object.keys(entry).length > 0;
539
+ }
540
+ return true;
541
+ });
542
+ }
543
+
501
544
  function projectPendingDesktopActions(threadId, conversationState) {
502
545
  const requests = Array.isArray(conversationState?.requests) ? conversationState.requests : [];
503
546
  return requests
@@ -36,6 +36,7 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
36
36
  const turnsById = new Map();
37
37
  let activeTurnId = "";
38
38
  let sessionThreadId = normalizeString(threadId);
39
+ const skippedCallIds = new Set();
39
40
 
40
41
  const lines = String(content || "").split(/\r?\n/);
41
42
  for (let index = 0; index < lines.length; index += 1) {
@@ -110,6 +111,9 @@ function parseSessionJsonlTurns(content, { threadId = "" } = {}) {
110
111
  if (!payload) {
111
112
  continue;
112
113
  }
114
+ if (shouldSkipResponseItemForHistory(payload, skippedCallIds)) {
115
+ continue;
116
+ }
113
117
  const turn = ensureTurn(
114
118
  turns,
115
119
  turnsById,
@@ -169,6 +173,67 @@ function normalizeResponseItemForHistory(payload, lineNumber) {
169
173
  return item;
170
174
  }
171
175
 
176
+ // Filters desktop transcript internals that are stored as response items but are not chat history.
177
+ function shouldSkipResponseItemForHistory(payload, skippedCallIds) {
178
+ const type = normalizeHistoryItemType(payload.type);
179
+ const callId = normalizeString(payload.call_id) || normalizeString(payload.callId);
180
+
181
+ if (type === "tool_call_output" && callId && skippedCallIds.has(callId)) {
182
+ return true;
183
+ }
184
+
185
+ if (type === "tool_call" && isSubagentOrchestrationCall(payload)) {
186
+ if (callId) {
187
+ skippedCallIds.add(callId);
188
+ }
189
+ return true;
190
+ }
191
+
192
+ if (type !== "message") {
193
+ return false;
194
+ }
195
+
196
+ const role = normalizeString(payload.role).toLowerCase();
197
+ if (role && role !== "user" && role !== "assistant") {
198
+ return true;
199
+ }
200
+
201
+ if (role === "user" && isSubagentNotificationMessage(payload)) {
202
+ return true;
203
+ }
204
+
205
+ return false;
206
+ }
207
+
208
+ function isSubagentOrchestrationCall(payload) {
209
+ const name = normalizeString(payload.name).toLowerCase();
210
+ return name === "spawn_agent"
211
+ || name === "wait_agent"
212
+ || name === "send_input"
213
+ || name === "resume_agent"
214
+ || name === "close_agent";
215
+ }
216
+
217
+ function isSubagentNotificationMessage(payload) {
218
+ const text = responseItemMessageText(payload).trimStart();
219
+ return text.startsWith("<subagent_notification>");
220
+ }
221
+
222
+ function responseItemMessageText(payload) {
223
+ const directText = normalizeString(payload.text) || normalizeString(payload.message);
224
+ if (directText) {
225
+ return directText;
226
+ }
227
+
228
+ const content = Array.isArray(payload.content) ? payload.content : [];
229
+ return content
230
+ .map((item) => objectValue(item))
231
+ .filter(Boolean)
232
+ .map((item) => normalizeString(item.text) || normalizeString(objectValue(item.data)?.text))
233
+ .filter(Boolean)
234
+ .join("\n");
235
+ }
236
+
172
237
  function normalizeHistoryItemType(rawType) {
173
238
  const normalized = normalizeString(rawType).toLowerCase().replace(/[\s_-]+/g, "");
174
239
  if (!normalized) {
@@ -1,4 +0,0 @@
1
- {
2
- "relayUrl": "wss://remodex.vectorvein.com/relay",
3
- "pushServiceUrl": ""
4
- }