@makerbi/remodex 1.5.2 → 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.
- package/bin/remodex-jsonl-diagnose.js +0 -0
- package/bin/remodex.js +20 -1
- package/package.json +3 -3
- package/src/bridge-status.js +121 -0
- package/src/bridge.js +75 -90
- package/src/codex-transport.js +28 -1
- package/src/desktop-ipc-action-follower.js +44 -1
- package/src/macos-launch-agent.js +6 -3
- package/src/secure-transport.js +4 -1
- package/src/session-jsonl-history.js +65 -0
|
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
|
|
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
|
+
"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",
|
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
},
|
|
10
10
|
"main": "src/index.js",
|
|
11
11
|
"bin": {
|
|
12
|
-
"remodex": "
|
|
13
|
-
"remodex-jsonl-diagnose": "
|
|
12
|
+
"remodex": "bin/remodex.js",
|
|
13
|
+
"remodex-jsonl-diagnose": "bin/remodex-jsonl-diagnose.js"
|
|
14
14
|
},
|
|
15
15
|
"files": [
|
|
16
16
|
"bin/",
|
|
@@ -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, ./
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
680
|
-
const lastError =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
765
|
+
const codexRequest = disableUnsupportedReasoningSummaryForTurnStart(rawMessage);
|
|
766
|
+
const codexMessage = prepareCodexForwardMessage(codexRequest, relayChannel);
|
|
771
767
|
rememberForwardedRequestMethod(codexMessage);
|
|
772
|
-
rememberThreadFromMessage("phone",
|
|
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
|
-
|
|
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,
|
package/src/codex-transport.js
CHANGED
|
@@ -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(
|
|
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 =
|
|
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
|
|
@@ -255,8 +255,7 @@ function printMacOSBridgeServiceStatus(options = {}) {
|
|
|
255
255
|
|
|
256
256
|
function printMacOSBridgePairingQr({ pairingSession = null, env = process.env, fsImpl = fs } = {}) {
|
|
257
257
|
const nextPairingSession = pairingSession || readPairingSession({ env, fsImpl });
|
|
258
|
-
|
|
259
|
-
if (!pairingPayload) {
|
|
258
|
+
if (!nextPairingSession?.pairingPayload) {
|
|
260
259
|
throw new Error("The macOS bridge service did not publish a pairing payload yet.");
|
|
261
260
|
}
|
|
262
261
|
|
|
@@ -351,7 +350,11 @@ async function waitForFreshPairingSession({
|
|
|
351
350
|
while (Date.now() <= deadline) {
|
|
352
351
|
const pairingSession = readPairingSession({ env, fsImpl });
|
|
353
352
|
const createdAt = Date.parse(pairingSession?.createdAt || "");
|
|
354
|
-
if (
|
|
353
|
+
if (
|
|
354
|
+
pairingSession?.pairingPayload
|
|
355
|
+
&& Number.isFinite(createdAt)
|
|
356
|
+
&& createdAt >= startedAt
|
|
357
|
+
) {
|
|
355
358
|
return pairingSession;
|
|
356
359
|
}
|
|
357
360
|
await sleep(intervalMs);
|
package/src/secure-transport.js
CHANGED
|
@@ -364,7 +364,10 @@ function createBridgeSecureTransport({
|
|
|
364
364
|
pendingHandshake.phoneIdentityPublicKey
|
|
365
365
|
);
|
|
366
366
|
if (previousTrustedPhonePublicKey !== pendingHandshake.phoneIdentityPublicKey) {
|
|
367
|
-
onTrustedPhoneUpdate?.(currentDeviceState
|
|
367
|
+
onTrustedPhoneUpdate?.(currentDeviceState, {
|
|
368
|
+
phoneDeviceId: pendingHandshake.phoneDeviceId,
|
|
369
|
+
phoneIdentityPublicKey: pendingHandshake.phoneIdentityPublicKey,
|
|
370
|
+
});
|
|
368
371
|
}
|
|
369
372
|
}
|
|
370
373
|
if (pendingHandshake.handshakeMode === HANDSHAKE_MODE_QR_BOOTSTRAP) {
|
|
@@ -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) {
|