@minhspark/codex-mcp-bridge 1.25.1 → 1.25.2
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/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +13 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install-native-relay.mjs +1 -1
- package/src/claude-bridge.mjs +1 -1
- package/src/index.mjs +3 -4
- package/src/mcp-supervisor.mjs +82 -5
- package/src/native-relay-companion.mjs +20 -3
- package/src/native-relay.mjs +3 -3
- package/src/platform.mjs +1 -0
- package/src/relay-wake.mjs +15 -218
- package/src/thread-delivery.mjs +19 -17
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ Follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and [SemVer](ht
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [1.25.2] - 2026-10-31
|
|
8
|
+
|
|
9
|
+
### Fixed
|
|
10
|
+
|
|
11
|
+
- Relay recovery no longer opens a task or activates a Desktop window, including request-triggered recovery. The old keeper flag cannot restore navigation. Windows background task navigation now fails explicitly instead of silently stealing focus.
|
|
12
|
+
- The Desktop-owned supervisor restarts crashed native relay workers with bounded exponential backoff and validates their protocol and tool catalog before activation. In-flight operations fail without replay; client disconnect still shuts down the supervisor and its workers.
|
|
13
|
+
- Stale inherited native pipe hints can recover through verified Desktop ancestry. Explicitly configured endpoints stay pinned, and uncertain native sends are never replayed.
|
|
14
|
+
- Recovery waits and subsequent retries share the original request deadline. Native relay status checks a read-only operation through the serving shared relay instead of treating socket availability as proof of working dispatch.
|
|
15
|
+
|
|
16
|
+
### Limitations
|
|
17
|
+
|
|
18
|
+
- Reconnect the relay MCP once after upgrading to load the new supervisor. Worker hot reload alone cannot replace its running parent. Automatic recovery requires that Desktop-owned supervisor to remain alive; it cannot cold-start MCP in an idle application with no loaded session.
|
|
19
|
+
|
|
7
20
|
## [1.25.1] - 2026-10-31
|
|
8
21
|
|
|
9
22
|
### Fixed
|
package/README.md
CHANGED
|
@@ -669,9 +669,9 @@ For a repeatable failure, capture the app/OS/Node versions, the version reported
|
|
|
669
669
|
|
|
670
670
|
**Symptoms:** `RELAY_UNAVAILABLE`, `connect ENOENT`, a missing native relay socket, or `thread ... already has an active writer`.
|
|
671
671
|
|
|
672
|
-
Native Desktop handoffs require Codex Desktop to be installed, signed in and open on Windows/macOS. The app runs `codex-native-relay` while a regular task has its MCP servers loaded.
|
|
672
|
+
Native Desktop handoffs require Codex Desktop to be installed, signed in and open on Windows/macOS. The app runs `codex-native-relay` while a regular task has its MCP servers loaded. Recovery never navigates to a task, launches a window, or activates Codex. The Desktop-owned supervisor restarts crashed native relay workers with bounded exponential backoff; pending requests fail without replay, and later requests use the recovered worker. Inherited native pipe hints are rediscovered through the original Desktop process ancestry when they become stale. Requests that never reached the relay wait up to 20 seconds, within their original deadline, for its transport to recover. `CODEX_BRIDGE_RELAY_WAKE=0` disables that wait; the retired `CODEX_BRIDGE_RELAY_KEEPER` flag cannot enable UI navigation.
|
|
673
673
|
|
|
674
|
-
|
|
674
|
+
Check `native_relay_status` in a regular project task and `codex_bridge_status` in the sending Code session. Native status performs a read-only `list_projects` probe and reports native dispatch separately from socket availability. Recovery requires the original Desktop-owned supervisor to remain alive: it cannot cold-start MCP when Desktop has loaded no session, and it stops when the MCP client closes. After five unsuccessful worker replacements, reconnect MCP in a regular task. Do not open the internal **Native Relay** executor; it is not an interactive chat and may show a missing-rollout error. Rerun setup if registration/executor configuration is missing, and verify [native relay pipe forwarding](#native-relay-pipe-forwarding). Never persist a pipe address copied from a different app process. After upgrading from a supervisor without crash recovery, reconnect the relay MCP once to load the updated supervisor; worker hot reload alone cannot replace its parent.
|
|
675
675
|
|
|
676
676
|
An active-writer error usually identifies the legacy app-server route attempting to attach to a thread already owned by Desktop. Use the registered native Code-to-Desktop route for that conversation. Do not start another app-server or terminate shared work to force access. The ordinary `codex-bridge-desktop` Chat connector remains a legacy app-server connection; it is not a substitute for the verified native sender. An unused `CODEX_APP_SERVER_URL` alongside `external app-server disabled` is not by itself a native connection failure.
|
|
677
677
|
|
|
@@ -720,7 +720,7 @@ The catalog below records user-visible failures fixed in earlier releases. It is
|
|
|
720
720
|
|
|
721
721
|
| Historical symptom | Fix version and cause | Current recovery |
|
|
722
722
|
|---|---|---|
|
|
723
|
-
| Relay vanished after app restart or was absent when a companion first started | [1.
|
|
723
|
+
| Relay vanished after app restart or was absent when a companion first started | [1.25.2](CHANGELOG.md#1252---2026-10-31): silent worker recovery and stale-pipe rediscovery replace the historical task wake and keeper. | Follow [relay recovery](#relay-unavailable-stale-pipe-or-an-active-writer). Reconnect MCP once after upgrading the supervisor; an idle app with no loaded MCP session cannot be cold-started by silent recovery. |
|
|
724
724
|
| `No handler registered for method: tools/call` or `RELAY_UNAVAILABLE` after a Codex Desktop update | [1.18.2](CHANGELOG.md#1182---2026-09-26): native caller-source compatibility. [1.22.13](CHANGELOG.md#12213---2026-10-20): discover Windows `codex-app-tools-*` pipes instead of probing only browser pipes. | Update/reload the native companion as well as the sending bridge. Check `native_relay_status`; never copy a foreign process's pipe to bypass discovery. |
|
|
725
725
|
| A Windows task opened successfully, but `open_codex_thread` or relay wake reported failure | [1.22.14](CHANGELOG.md#12214---2026-10-21): replace reliance on `explorer.exe`'s misleading exit code with the native URI launcher. | Upgrade/reconnect and inspect the opened task. A navigation error is not evidence that an accepted prompt should be sent again. |
|
|
726
726
|
| A macOS companion appeared installed but every native delivery was unconfirmed | [1.13.4](CHANGELOG.md#1134---2026-09-06): use the Codex-bundled runtime trusted by Desktop. | Run current setup with Codex Desktop installed and reconnect. Keep the runtime selected by the installer; an arbitrary Homebrew/nvm Node may not satisfy native host checks. |
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "codex-mcp-bridge",
|
|
4
|
-
"version": "1.25.
|
|
4
|
+
"version": "1.25.2",
|
|
5
5
|
"description": "Connect ChatGPT to your own Codex Desktop projects and conversations.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Bui Dang Minh",
|
|
@@ -25,7 +25,7 @@ import { removeRelayFile } from "../src/native-relay-files.mjs";
|
|
|
25
25
|
|
|
26
26
|
exitForVersionRequest(import.meta.url);
|
|
27
27
|
|
|
28
|
-
const VERSION = "1.25.
|
|
28
|
+
const VERSION = "1.25.2";
|
|
29
29
|
const root = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
|
|
30
30
|
const entry = path.join(root, "src", "mcp-supervisor.mjs");
|
|
31
31
|
const entryArgs = ["native-relay-companion.mjs"];
|
package/src/claude-bridge.mjs
CHANGED
|
@@ -30,7 +30,7 @@ import { discoverClaudeProject } from "./claude-project-discovery.mjs";
|
|
|
30
30
|
|
|
31
31
|
exitForVersionRequest(import.meta.url);
|
|
32
32
|
|
|
33
|
-
const VERSION = "1.25.
|
|
33
|
+
const VERSION = "1.25.2";
|
|
34
34
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
35
35
|
const FORWARD_MIN_INTERVAL_MS = 5000;
|
|
36
36
|
const FORWARD_MAX_PER_SESSION = 50;
|
package/src/index.mjs
CHANGED
|
@@ -37,11 +37,11 @@ import { captureProjectScope } from "./project-scope.mjs";
|
|
|
37
37
|
import { DesktopReplyReceipts } from "./desktop-reply-receipts.mjs";
|
|
38
38
|
import { registerDesktopReadTools } from "./desktop-read-tools.mjs";
|
|
39
39
|
import { PROJECT_SETUP_TOOLS, registerProjectOnboardingTools } from "./project-onboarding-tools.mjs";
|
|
40
|
-
import { createRelayWake
|
|
40
|
+
import { createRelayWake } from "./relay-wake.mjs";
|
|
41
41
|
|
|
42
42
|
exitForVersionRequest(import.meta.url);
|
|
43
43
|
|
|
44
|
-
const VERSION = "1.25.
|
|
44
|
+
const VERSION = "1.25.2";
|
|
45
45
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
46
46
|
const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
|
|
47
47
|
|
|
@@ -76,7 +76,7 @@ const desktopTasksEnabled = desktopTasksConfigured();
|
|
|
76
76
|
const runtime = createRuntimeState({ configuration: desktopTasksConfigured });
|
|
77
77
|
const desktopOperation = new AsyncLocalStorage();
|
|
78
78
|
const replyReceipts = new DesktopReplyReceipts();
|
|
79
|
-
const relayWake = createRelayWake({
|
|
79
|
+
const relayWake = createRelayWake({ log });
|
|
80
80
|
const desktopTasks = new DesktopTaskDelivery({ security, beforeRequest: beforeDesktopRequest, accountContext: () => desktopOperation.getStore()?.accounts, senderContext: () => desktopOperation.getStore()?.caller, wakeRelay: relayWake });
|
|
81
81
|
|
|
82
82
|
async function assertDesktopOperation(context, { verifyProcess = false } = {}) {
|
|
@@ -1097,7 +1097,6 @@ registerTool(
|
|
|
1097
1097
|
|
|
1098
1098
|
const transport = new StdioServerTransport();
|
|
1099
1099
|
await server.connect(transport);
|
|
1100
|
-
if (desktopTasksEnabled && relayKeeperEnabled()) startRelayKeeper({ wake: relayWake, socketPath: () => desktopTasks.relay.accountSocketPath, log });
|
|
1101
1100
|
let shutdownPromise;
|
|
1102
1101
|
const shutdown = () => shutdownPromise ??= (async () => {
|
|
1103
1102
|
client?.close();
|
package/src/mcp-supervisor.mjs
CHANGED
|
@@ -170,6 +170,7 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
170
170
|
let active;
|
|
171
171
|
let initializeParams;
|
|
172
172
|
let initializeResult;
|
|
173
|
+
let initializeCatalog;
|
|
173
174
|
let ready = false;
|
|
174
175
|
let switching = false;
|
|
175
176
|
let stopping = false;
|
|
@@ -181,6 +182,17 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
181
182
|
let retryAt = 0;
|
|
182
183
|
let reason = null;
|
|
183
184
|
let reloads = 0;
|
|
185
|
+
let recoveries = 0;
|
|
186
|
+
let recoveryAttempts = 0;
|
|
187
|
+
let recoveryAt = 0;
|
|
188
|
+
let activeSince = Date.now();
|
|
189
|
+
const recoveryDelayMs = Math.min(30000, supervisorInterval(env.CODEX_BRIDGE_RECOVERY_DELAY_MS, 1000));
|
|
190
|
+
const recoveryLimit = 5;
|
|
191
|
+
const canRecover = () => entry === "native-relay-companion.mjs" && ready && initializeResult && initializeCatalog;
|
|
192
|
+
const scheduleRecovery = () => {
|
|
193
|
+
if (recoveryAttempts >= recoveryLimit) return;
|
|
194
|
+
recoveryAt = Date.now() + Math.min(30000, recoveryDelayMs * 2 ** recoveryAttempts);
|
|
195
|
+
};
|
|
184
196
|
const queue = [];
|
|
185
197
|
const requests = new Map();
|
|
186
198
|
const reverse = new Map();
|
|
@@ -189,7 +201,7 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
189
201
|
revision: active?.release.revision ?? null, availableRevision: observed,
|
|
190
202
|
routingConfiguration: active?.routingConfiguration ?? null, availableRoutingConfiguration: observedRouting,
|
|
191
203
|
pending: Boolean(observed && (observed !== active?.release.revision || observedRouting !== active?.routingConfiguration)),
|
|
192
|
-
state: failure ? "failed" : switching ? "reloading" : reason ? "deferred" : "current", reason: failure ?? reason, reloads });
|
|
204
|
+
state: failure ? canRecover() && recoveryAttempts < recoveryLimit ? "recovering" : "failed" : switching ? "reloading" : reason ? "deferred" : "current", reason: failure ?? reason, reloads, recoveries, recoveryAttempts });
|
|
193
205
|
|
|
194
206
|
const handlers = {
|
|
195
207
|
message(worker, message) {
|
|
@@ -210,12 +222,16 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
210
222
|
const respond = async () => {
|
|
211
223
|
if (pending.method === "initialize" && !message.error) {
|
|
212
224
|
initializeResult = message.result;
|
|
225
|
+
if (entry === "native-relay-companion.mjs") {
|
|
226
|
+
initializeCatalog = await worker.rpc("tools/list", {});
|
|
227
|
+
if (!Array.isArray(initializeCatalog.tools) || !initializeCatalog.tools.length || initializeCatalog.nextCursor) throw new Error("The native relay tool catalog is incomplete");
|
|
228
|
+
}
|
|
213
229
|
await worker.control("activate");
|
|
214
230
|
ready = true;
|
|
215
231
|
}
|
|
216
232
|
if (message.result && ["codex_bridge_status", "claude_bridge_status", "native_relay_status"].includes(pending.tool)) {
|
|
217
233
|
message.result.structuredContent = { ...message.result.structuredContent, autoReload: diagnostics() };
|
|
218
|
-
message.result.content = [...(message.result.content ?? []), { type: "text", text: `automatic reload: ${diagnostics().state}\nsupervisor pid: ${process.pid}\nworker pid: ${worker.process.pid}\nreload count: ${reloads}${reason ? `\nreload detail: ${reason}` : ""}` }];
|
|
234
|
+
message.result.content = [...(message.result.content ?? []), { type: "text", text: `automatic reload: ${diagnostics().state}\nsupervisor pid: ${process.pid}\nworker pid: ${worker.process.pid}\nreload count: ${reloads}\nrecovery count: ${recoveries}${reason ? `\nreload detail: ${reason}` : ""}` }];
|
|
219
235
|
}
|
|
220
236
|
output({ ...message, id: pending.id });
|
|
221
237
|
};
|
|
@@ -228,7 +244,15 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
228
244
|
worker.clientPending.delete(id);
|
|
229
245
|
}
|
|
230
246
|
for (const [id, pending] of reverse) if (pending.worker === worker) reverse.delete(id);
|
|
231
|
-
if (worker === active && !worker.retiring && !stopping) {
|
|
247
|
+
if (worker === active && !worker.retiring && !stopping) {
|
|
248
|
+
failure = `${error.message}; automatic replay is disabled because delivery state may be unknown`;
|
|
249
|
+
if (canRecover()) {
|
|
250
|
+
if (Date.now() - activeSince >= 60000) recoveryAttempts = 0;
|
|
251
|
+
scheduleRecovery();
|
|
252
|
+
if (recoveryAttempts >= recoveryLimit) failure += "; recovery limit reached, reconnect MCP";
|
|
253
|
+
}
|
|
254
|
+
log(failure);
|
|
255
|
+
}
|
|
232
256
|
},
|
|
233
257
|
};
|
|
234
258
|
const spawn = (release, routing) => {
|
|
@@ -245,7 +269,56 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
245
269
|
return worker;
|
|
246
270
|
};
|
|
247
271
|
const check = async () => {
|
|
248
|
-
if (switching || !ready || stopping
|
|
272
|
+
if (switching || !ready || stopping) return;
|
|
273
|
+
if (failure) {
|
|
274
|
+
if (!canRecover() || recoveryAttempts >= recoveryLimit || Date.now() < recoveryAt) return;
|
|
275
|
+
switching = true;
|
|
276
|
+
recoveryAttempts++;
|
|
277
|
+
const previous = active;
|
|
278
|
+
let candidate;
|
|
279
|
+
try {
|
|
280
|
+
await previous.stop();
|
|
281
|
+
workers.delete(previous);
|
|
282
|
+
if (stopping) return;
|
|
283
|
+
const source = currentRoot();
|
|
284
|
+
const release = createReleaseSnapshot(source, { expectedRevision: sourceRevision(source), lease: true });
|
|
285
|
+
candidate = spawn(release, routingConfiguration());
|
|
286
|
+
const initialized = await candidate.rpc("initialize", initializeParams);
|
|
287
|
+
if (initialized.protocolVersion !== initializeResult.protocolVersion || JSON.stringify(initialized.capabilities) !== JSON.stringify(initializeResult.capabilities)) {
|
|
288
|
+
throw new Error("The replacement MCP protocol or capabilities require an explicit client reconnect");
|
|
289
|
+
}
|
|
290
|
+
candidate.write({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
291
|
+
const catalog = await candidate.rpc("tools/list", {});
|
|
292
|
+
if (JSON.stringify(catalog) !== JSON.stringify(initializeCatalog)) throw new Error("The replacement tool catalog requires an explicit client reconnect");
|
|
293
|
+
await candidate.control("activate");
|
|
294
|
+
if (stopping || candidate.closed) throw new Error("The relay replacement stopped before activation completed");
|
|
295
|
+
active = candidate;
|
|
296
|
+
candidate = null;
|
|
297
|
+
activeSince = Date.now();
|
|
298
|
+
failure = null;
|
|
299
|
+
reason = null;
|
|
300
|
+
recoveries++;
|
|
301
|
+
log(`recovered native relay worker after exit; recovery ${recoveries}, no requests replayed`);
|
|
302
|
+
schedulePrune();
|
|
303
|
+
} catch (error) {
|
|
304
|
+
failure = `Native relay recovery failed: ${error.message}; no requests were replayed`;
|
|
305
|
+
if (candidate) {
|
|
306
|
+
try {
|
|
307
|
+
await candidate.stop();
|
|
308
|
+
workers.delete(candidate);
|
|
309
|
+
} catch (stopError) {
|
|
310
|
+
recoveryAttempts = recoveryLimit;
|
|
311
|
+
failure += `; replacement shutdown could not be confirmed: ${stopError.message}`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
scheduleRecovery();
|
|
315
|
+
if (recoveryAttempts >= recoveryLimit) failure += "; recovery limit reached, reconnect MCP";
|
|
316
|
+
log(failure);
|
|
317
|
+
} finally {
|
|
318
|
+
switching = false;
|
|
319
|
+
}
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
249
322
|
let source;
|
|
250
323
|
let revision;
|
|
251
324
|
let routing;
|
|
@@ -279,8 +352,12 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
279
352
|
await candidate.control("restore", exported.state);
|
|
280
353
|
await candidate.control("activate");
|
|
281
354
|
if (stopping) throw new Error("The MCP client disconnected during activation");
|
|
355
|
+
if (candidate.closed) throw new Error("The replacement worker exited during activation");
|
|
282
356
|
active = candidate;
|
|
283
357
|
candidate = null;
|
|
358
|
+
initializeCatalog = catalog;
|
|
359
|
+
activeSince = Date.now();
|
|
360
|
+
failure = null;
|
|
284
361
|
reloads++;
|
|
285
362
|
reason = null;
|
|
286
363
|
if (JSON.stringify(catalog) !== JSON.stringify(oldCatalog)) output({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
|
|
@@ -321,7 +398,7 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
321
398
|
}
|
|
322
399
|
return;
|
|
323
400
|
}
|
|
324
|
-
if (failure) { if (hasId(message)) output(errorMessage(message.id, failure)); return; }
|
|
401
|
+
if (failure) { if (hasId(message)) output(errorMessage(message.id, `${failure}${canRecover() && recoveryAttempts < recoveryLimit ? "; native relay recovery is pending, this request was not sent" : ""}`)); return; }
|
|
325
402
|
if (switching || !active) {
|
|
326
403
|
const bytes = queue.reduce((size, queued) => size + Buffer.byteLength(JSON.stringify(queued)), 0);
|
|
327
404
|
if (queue.length >= MAX_QUEUE || bytes + Buffer.byteLength(JSON.stringify(message)) > MAX_FRAME_BYTES) { if (hasId(message)) output(errorMessage(message.id, "Reload request queue is full; no operation was dispatched")); return; }
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
responseTooLargeError,
|
|
15
15
|
NATIVE_DISPATCH_METHOD,
|
|
16
16
|
NativeToolsClient,
|
|
17
|
+
NativeDesktopRelay,
|
|
17
18
|
NativeRelayError,
|
|
18
19
|
RELAY_PROTOCOL_VERSION,
|
|
19
20
|
ACCOUNT_RELAY_PROTOCOL_VERSION,
|
|
@@ -28,7 +29,7 @@ import {
|
|
|
28
29
|
} from "./native-relay.mjs";
|
|
29
30
|
import { IS_WINDOWS, PLATFORM_LABEL } from "./platform.mjs";
|
|
30
31
|
import { exitForVersionRequest } from "./cli-version.mjs";
|
|
31
|
-
import { assertAccountIdentity } from "./bridge-account-context.mjs";
|
|
32
|
+
import { accountIdentity, assertAccountIdentity, readBridgeAccounts } from "./bridge-account-context.mjs";
|
|
32
33
|
import { createReloadControl, drainPendingAccepts, exitWhenClientCloses } from "./reload-control.mjs";
|
|
33
34
|
import { createRuntimeState } from "./runtime-state.mjs";
|
|
34
35
|
import { hardenedBridgeEnabled } from "./hardened-root-policy.mjs";
|
|
@@ -42,7 +43,7 @@ import { remoteCodexSocketPath, remoteNativeAdapter, remoteRequestResponse } fro
|
|
|
42
43
|
|
|
43
44
|
exitForVersionRequest(import.meta.url);
|
|
44
45
|
|
|
45
|
-
const VERSION = "1.25.
|
|
46
|
+
const VERSION = "1.25.2";
|
|
46
47
|
const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
|
|
47
48
|
|
|
48
49
|
function errorResponse(code, message, sent, details) {
|
|
@@ -865,6 +866,20 @@ export function createNativeRelayLifecycle({ nativeTools, relays, log: logFn = (
|
|
|
865
866
|
};
|
|
866
867
|
}
|
|
867
868
|
|
|
869
|
+
export async function probeRelayDispatch({ env = process.env, timeoutMs = 3000, accounts = readBridgeAccounts, relay = new NativeDesktopRelay({ env, socketPath: desktopTaskSocketPath(env), accountSocketPath: accountRelaySocketPath(env) }) } = {}) {
|
|
870
|
+
try {
|
|
871
|
+
const accountContext = accountIdentity(accounts());
|
|
872
|
+
const response = await relay.requestDesktop("list_projects", {}, {
|
|
873
|
+
timeoutMs,
|
|
874
|
+
...(accountContext ? { accountContext, beforeSend: () => assertAccountIdentity(accountContext) } : {}),
|
|
875
|
+
});
|
|
876
|
+
if (!Array.isArray(response?.result?.projects)) throw new Error("Native Desktop did not return a project list");
|
|
877
|
+
return { ready: true, checkedAt: new Date().toISOString(), scope: "shared-relay-native-dispatch" };
|
|
878
|
+
} catch (error) {
|
|
879
|
+
return { ready: false, checkedAt: new Date().toISOString(), scope: "shared-relay-native-dispatch", code: error.code ?? "NATIVE_HEALTH_CHECK_FAILED", reason: error.message };
|
|
880
|
+
}
|
|
881
|
+
}
|
|
882
|
+
|
|
868
883
|
export function createRelayServers({ nativeTools, env = process.env, signalExit = true, log: logFn = () => {} }) {
|
|
869
884
|
const hardened = hardenedBridgeEnabled(env);
|
|
870
885
|
if (hardened && !desktopTasksConfigured(env)) throw new Error("CODEX_BRIDGE_HARDENED=1 requires CODEX_BRIDGE_DESKTOP_TASKS=1; legacy relay listeners are disabled");
|
|
@@ -938,8 +953,9 @@ if (invokedDirectly) {
|
|
|
938
953
|
} catch (err) {
|
|
939
954
|
executor = err.message;
|
|
940
955
|
}
|
|
956
|
+
const nativeDispatch = listening ? await probeRelayDispatch() : { ready: false, reason: "Relay transport is not listening" };
|
|
941
957
|
return {
|
|
942
|
-
structuredContent: { runtime: runtime.status() },
|
|
958
|
+
structuredContent: { runtime: runtime.status(), nativeDispatch },
|
|
943
959
|
content: [
|
|
944
960
|
{
|
|
945
961
|
type: "text",
|
|
@@ -953,6 +969,7 @@ if (invokedDirectly) {
|
|
|
953
969
|
`remote Codex: ${remoteRelay.socketPath} (${await remoteRelay.isListening() ? "listening" : "not listening"})`,
|
|
954
970
|
`executor: ${executor}`,
|
|
955
971
|
`dispatch: ${process.env.CODEX_NATIVE_RELAY_METHOD ?? NATIVE_DISPATCH_METHOD}`,
|
|
972
|
+
`native dispatch: ${nativeDispatch.ready ? "verified (read-only list_projects)" : `unavailable (${nativeDispatch.reason})`}`,
|
|
956
973
|
`native pipe: ${nativeTools.socketPath ?? "unavailable (requires Codex Desktop)"}`,
|
|
957
974
|
].join("\n"),
|
|
958
975
|
},
|
package/src/native-relay.mjs
CHANGED
|
@@ -560,9 +560,9 @@ export async function resolveNativeToolsPipePath({
|
|
|
560
560
|
}
|
|
561
561
|
|
|
562
562
|
export class NativeToolsClient {
|
|
563
|
-
constructor({ env = process.env, socketPath
|
|
563
|
+
constructor({ env = process.env, socketPath, timeoutMs = DEFAULT_TIMEOUT_MS, resolveSocketPath = () => resolveNativeToolsPipePath({ env: { ...env, CODEX_APP_TOOLS_PIPE_PATH: undefined } }), assertAccount = assertAccountIdentity } = {}) {
|
|
564
564
|
this.env = env;
|
|
565
|
-
this.socketPath = socketPath;
|
|
565
|
+
this.socketPath = socketPath ?? env.CODEX_APP_TOOLS_PIPE_PATH;
|
|
566
566
|
this.explicitSocketPath = socketPath || null;
|
|
567
567
|
this.hasDiscoveredSocket = false;
|
|
568
568
|
this.timeoutMs = timeoutMs;
|
|
@@ -581,7 +581,7 @@ export class NativeToolsClient {
|
|
|
581
581
|
if (this.socket && !this.socket.destroyed) return;
|
|
582
582
|
const generation = this.connectionGeneration;
|
|
583
583
|
this.connecting = (async () => {
|
|
584
|
-
const socketPath = this.socketPath || await this.resolveSocketPath();
|
|
584
|
+
const socketPath = this.socketPath || await this.resolveSocketPath() || this.env.CODEX_APP_TOOLS_PIPE_PATH;
|
|
585
585
|
if (generation !== this.connectionGeneration) {
|
|
586
586
|
throw new NativeRelayError("Native tools client closed while discovering the Desktop pipe", "NATIVE_PIPE_UNAVAILABLE");
|
|
587
587
|
}
|
package/src/platform.mjs
CHANGED
|
@@ -545,6 +545,7 @@ export async function openThreadInCodexApp(threadId, { activate = true, platform
|
|
|
545
545
|
if (typeof threadId !== "string" || !threadId) throw new Error("A Codex thread id is required");
|
|
546
546
|
const url = codexThreadUrl(encodeURIComponent(threadId));
|
|
547
547
|
if (platform === "win32") {
|
|
548
|
+
if (!activate) throw Object.assign(new Error("Windows cannot open a Codex task without activating its window; background navigation is disabled"), { code: "BACKGROUND_NAVIGATION_UNAVAILABLE" });
|
|
548
549
|
const [shell, args] = windowsOpenUrlCommand(url, env);
|
|
549
550
|
await run(shell, args, { windowsHide: true, timeout: 10000 });
|
|
550
551
|
return url;
|
package/src/relay-wake.mjs
CHANGED
|
@@ -1,22 +1,6 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
1
|
import net from "node:net";
|
|
3
|
-
import path from "node:path";
|
|
4
2
|
|
|
5
|
-
import { codexHome, resolveRelayThreadId } from "./native-relay.mjs";
|
|
6
|
-
import { isCodexDesktopRunning, openThreadInCodexApp } from "./platform.mjs";
|
|
7
|
-
import { captureProjectScope } from "./project-scope.mjs";
|
|
8
|
-
import { readRolloutFirstRecord } from "./rollout-reader.mjs";
|
|
9
|
-
|
|
10
|
-
const THREAD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
11
|
-
const ROLLOUT = /^rollout-.+-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl(?:\.zst)?$/i;
|
|
12
3
|
export const RELAY_WAKE_WAIT_MS = 20000;
|
|
13
|
-
export const RELAY_WAKE_COOLDOWN_MS = 60000;
|
|
14
|
-
export const RELAY_KEEPER_INTERVAL_MS = 30000;
|
|
15
|
-
export const RELAY_KEEPER_WAIT_MS = 60000;
|
|
16
|
-
export const RELAY_KEEPER_MAX_BACKOFF_MS = 30 * 60000;
|
|
17
|
-
export const DESKTOP_RECHECK_MS = 60000;
|
|
18
|
-
const MAX_DAY_DIRECTORIES = 14;
|
|
19
|
-
const MAX_CANDIDATES = 40;
|
|
20
4
|
|
|
21
5
|
export function relayWakeEnabled(env = process.env, platform = process.platform) {
|
|
22
6
|
const value = (env.CODEX_BRIDGE_RELAY_WAKE ?? "").toLowerCase();
|
|
@@ -24,71 +8,14 @@ export function relayWakeEnabled(env = process.env, platform = process.platform)
|
|
|
24
8
|
return platform === "darwin" || platform === "win32";
|
|
25
9
|
}
|
|
26
10
|
|
|
27
|
-
function dayDirectories(sessions) {
|
|
28
|
-
const directories = [];
|
|
29
|
-
const children = (directory, pattern) => {
|
|
30
|
-
try { return fs.readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory() && pattern.test(entry.name)).map((entry) => entry.name).sort().reverse(); }
|
|
31
|
-
catch { return []; }
|
|
32
|
-
};
|
|
33
|
-
for (const year of children(sessions, /^\d{4}$/)) {
|
|
34
|
-
for (const month of children(path.join(sessions, year), /^\d{2}$/)) {
|
|
35
|
-
for (const day of children(path.join(sessions, year, month), /^\d{2}$/)) {
|
|
36
|
-
directories.push(path.join(sessions, year, month, day));
|
|
37
|
-
if (directories.length >= MAX_DAY_DIRECTORIES) return directories;
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
return directories;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function sameProject(senderCwd, taskCwd) {
|
|
45
|
-
try { captureProjectScope(senderCwd, taskCwd); return true; }
|
|
46
|
-
catch { return false; }
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export function relayKeeperEnabled(env = process.env, platform = process.platform) {
|
|
50
|
-
const value = (env.CODEX_BRIDGE_RELAY_KEEPER ?? "").toLowerCase();
|
|
51
|
-
if (["0", "off", "false"].includes(value)) return false;
|
|
52
|
-
if (!relayWakeEnabled(env, platform)) return false;
|
|
53
|
-
if (platform === "win32") return ["1", "on", "true"].includes(value);
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export function relayWakeStateFile(env = process.env) {
|
|
58
|
-
return path.join(codexHome(env), "bridge-relay-wake.json");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
export async function recentDesktopTaskFor(cwd, { env = process.env, sessions = path.join(codexHome(env), "sessions"), exclude = [], anyProject = false } = {}) {
|
|
62
|
-
if (!anyProject && (typeof cwd !== "string" || !cwd)) return null;
|
|
63
|
-
const excluded = new Set(exclude.filter(Boolean));
|
|
64
|
-
const candidates = [];
|
|
65
|
-
for (const directory of dayDirectories(sessions)) {
|
|
66
|
-
let entries;
|
|
67
|
-
try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch { continue; }
|
|
68
|
-
for (const entry of entries) {
|
|
69
|
-
const match = entry.isFile() ? ROLLOUT.exec(entry.name) : null;
|
|
70
|
-
if (!match || excluded.has(match[1])) continue;
|
|
71
|
-
const file = path.join(directory, entry.name);
|
|
72
|
-
try { candidates.push({ file, threadId: match[1], mtimeMs: fs.lstatSync(file).mtimeMs }); } catch {}
|
|
73
|
-
}
|
|
74
|
-
if (candidates.length >= MAX_CANDIDATES) break;
|
|
75
|
-
}
|
|
76
|
-
candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
77
|
-
for (const candidate of candidates.slice(0, MAX_CANDIDATES)) {
|
|
78
|
-
let record;
|
|
79
|
-
try { record = await readRolloutFirstRecord(candidate.file, { maxBytes: 256 * 1024 }); } catch { continue; }
|
|
80
|
-
const meta = record?.type === "session_meta" ? record.payload : null;
|
|
81
|
-
if (meta?.id !== candidate.threadId || meta.originator !== "Codex Desktop" || typeof meta.cwd !== "string") continue;
|
|
82
|
-
if (anyProject || sameProject(cwd, meta.cwd)) return candidate.threadId;
|
|
83
|
-
}
|
|
84
|
-
return null;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
11
|
export function endpointAnswers(socketPath, { timeoutMs = 1000 } = {}) {
|
|
88
12
|
if (typeof socketPath !== "string" || !socketPath) return Promise.resolve(false);
|
|
89
13
|
return new Promise((resolve) => {
|
|
90
14
|
const probe = net.connect({ path: socketPath });
|
|
15
|
+
let settled = false;
|
|
91
16
|
const done = (answer) => {
|
|
17
|
+
if (settled) return;
|
|
18
|
+
settled = true;
|
|
92
19
|
globalThis.clearTimeout(timer);
|
|
93
20
|
probe.destroy();
|
|
94
21
|
resolve(answer);
|
|
@@ -96,163 +23,33 @@ export function endpointAnswers(socketPath, { timeoutMs = 1000 } = {}) {
|
|
|
96
23
|
const timer = globalThis.setTimeout(() => done(false), timeoutMs);
|
|
97
24
|
probe.once("connect", () => done(true));
|
|
98
25
|
probe.once("error", () => done(false));
|
|
26
|
+
probe.once("close", () => done(false));
|
|
99
27
|
});
|
|
100
28
|
}
|
|
101
29
|
|
|
102
|
-
function readWakeState(file) {
|
|
103
|
-
if (!file) return {};
|
|
104
|
-
try {
|
|
105
|
-
const state = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
106
|
-
return state && typeof state === "object" && !Array.isArray(state) ? state : {};
|
|
107
|
-
} catch { return {}; }
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function writeWakeState(file, state) {
|
|
111
|
-
if (!file) return;
|
|
112
|
-
const temporary = `${file}.${process.pid}.tmp`;
|
|
113
|
-
try {
|
|
114
|
-
fs.writeFileSync(temporary, JSON.stringify(state));
|
|
115
|
-
fs.renameSync(temporary, file);
|
|
116
|
-
} catch { try { fs.rmSync(temporary, { force: true }); } catch {} }
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
function acquireWakeLock(file, now, staleMs) {
|
|
120
|
-
if (!file) return () => {};
|
|
121
|
-
const lock = `${file}.lock`;
|
|
122
|
-
const claim = () => {
|
|
123
|
-
const descriptor = fs.openSync(lock, "wx");
|
|
124
|
-
try { fs.writeSync(descriptor, JSON.stringify({ pid: process.pid, at: now() })); } finally { fs.closeSync(descriptor); }
|
|
125
|
-
return () => { try { fs.rmSync(lock, { force: true }); } catch {} };
|
|
126
|
-
};
|
|
127
|
-
try { return claim(); }
|
|
128
|
-
catch (error) { if (error.code !== "EEXIST") return null; }
|
|
129
|
-
let heldSince;
|
|
130
|
-
try { heldSince = JSON.parse(fs.readFileSync(lock, "utf8")).at; } catch { heldSince = 0; }
|
|
131
|
-
if (Number.isFinite(heldSince) && now() - heldSince < staleMs) return null;
|
|
132
|
-
try { fs.rmSync(lock, { force: true }); return claim(); } catch { return null; }
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
export function keeperBackoffMs(failures, { baseMs = RELAY_WAKE_COOLDOWN_MS, maxMs = RELAY_KEEPER_MAX_BACKOFF_MS } = {}) {
|
|
136
|
-
const count = Number.isInteger(failures) && failures > 0 ? Math.min(failures, 16) : 0;
|
|
137
|
-
return count === 0 ? 0 : Math.min(baseMs * 2 ** (count - 1), maxMs);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
30
|
export function createRelayWake({
|
|
141
31
|
env = process.env,
|
|
142
32
|
enabled = relayWakeEnabled(env),
|
|
143
|
-
desktopRunning = () => isCodexDesktopRunning(),
|
|
144
|
-
senderCwd = () => null,
|
|
145
|
-
findTask = (cwd, exclude) => recentDesktopTaskFor(cwd, { env, exclude }),
|
|
146
|
-
findAnyTask = (exclude) => recentDesktopTaskFor(null, { env, exclude, anyProject: true }),
|
|
147
|
-
executorThreadId = () => { try { return resolveRelayThreadId(env).threadId; } catch { return null; } },
|
|
148
|
-
open = (threadId) => openThreadInCodexApp(threadId, { activate: false }),
|
|
149
33
|
answers = endpointAnswers,
|
|
150
34
|
waitMs = RELAY_WAKE_WAIT_MS,
|
|
151
|
-
backgroundWaitMs = RELAY_KEEPER_WAIT_MS,
|
|
152
|
-
cooldownMs = RELAY_WAKE_COOLDOWN_MS,
|
|
153
|
-
desktopRecheckMs = DESKTOP_RECHECK_MS,
|
|
154
|
-
stateFile = null,
|
|
155
35
|
pollMs = 250,
|
|
156
36
|
now = Date.now,
|
|
157
37
|
sleep = (ms) => new Promise((resolve) => globalThis.setTimeout(resolve, ms)),
|
|
158
38
|
log = () => {},
|
|
159
39
|
} = {}) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
40
|
+
return async ({ socketPath, deadline }) => {
|
|
41
|
+
if (!enabled || typeof socketPath !== "string" || !socketPath) return null;
|
|
42
|
+
const until = Math.min(now() + waitMs, deadline ?? Infinity);
|
|
163
43
|
while (now() < until) {
|
|
164
|
-
if (await answers(socketPath))
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
};
|
|
169
|
-
const attempt = async ({ socketPath, threadId, deadline, background = false }) => {
|
|
170
|
-
if (!enabled) return null;
|
|
171
|
-
const state = readWakeState(stateFile);
|
|
172
|
-
if (background) {
|
|
173
|
-
if (state.desktopRunning === false && now() - (state.desktopCheckedAt ?? -Infinity) < desktopRecheckMs) return null;
|
|
174
|
-
if (now() - (state.attemptAt ?? -Infinity) < Math.max(keeperBackoffMs(state.failures), cooldownMs)) return null;
|
|
175
|
-
} else if (now() - failedAt < cooldownMs) return null;
|
|
176
|
-
const running = await desktopRunning();
|
|
177
|
-
if (stateFile) writeWakeState(stateFile, { ...readWakeState(stateFile), desktopCheckedAt: now(), desktopRunning: Boolean(running) });
|
|
178
|
-
if (!running) return null;
|
|
179
|
-
const wait = background ? backgroundWaitMs : waitMs;
|
|
180
|
-
const release = acquireWakeLock(stateFile, now, wait + 15000);
|
|
181
|
-
if (!release) {
|
|
182
|
-
if (background) return null;
|
|
183
|
-
return await waitFor(socketPath, Math.min(now() + wait, deadline ?? Infinity)) ? { threadId: null } : null;
|
|
184
|
-
}
|
|
185
|
-
try {
|
|
186
|
-
const executor = executorThreadId();
|
|
187
|
-
const target = typeof threadId === "string" && THREAD_ID.test(threadId) && threadId !== executor
|
|
188
|
-
? threadId : (await findTask(senderCwd(), [executor])) ?? (await findAnyTask([executor]));
|
|
189
|
-
const startedAt = now();
|
|
190
|
-
const failures = Number.isInteger(state.failures) ? state.failures : 0;
|
|
191
|
-
if (!target) {
|
|
192
|
-
if (background && stateFile) writeWakeState(stateFile, { ...readWakeState(stateFile), attemptAt: startedAt, failures: failures + 1 });
|
|
193
|
-
return null;
|
|
44
|
+
if (await answers(socketPath, { timeoutMs: Math.min(1000, until - now()) })) {
|
|
45
|
+
if (now() >= until) return null;
|
|
46
|
+
log("relay transport available; the original operation will verify native dispatch");
|
|
47
|
+
return { threadId: null };
|
|
194
48
|
}
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
if (stateFile) writeWakeState(stateFile, { ...readWakeState(stateFile), attemptAt: startedAt, failures: failures + 1 });
|
|
198
|
-
log(message);
|
|
199
|
-
return null;
|
|
200
|
-
};
|
|
201
|
-
try { await open(target); }
|
|
202
|
-
catch (error) { return fail(`could not open Codex task ${target} to start the native relay: ${error.message}`); }
|
|
203
|
-
if (await waitFor(socketPath, Math.min(now() + wait, deadline ?? Infinity))) {
|
|
204
|
-
if (stateFile) writeWakeState(stateFile, { ...readWakeState(stateFile), attemptAt: startedAt, failures: 0 });
|
|
205
|
-
log(`started the Codex Desktop native relay by opening task ${target} in the background`);
|
|
206
|
-
return { threadId: target };
|
|
207
|
-
}
|
|
208
|
-
return fail(`opened Codex task ${target}, but the native relay did not start within ${wait} ms`);
|
|
209
|
-
} finally {
|
|
210
|
-
release();
|
|
49
|
+
const remaining = until - now();
|
|
50
|
+
if (remaining > 0) await sleep(Math.min(pollMs, remaining));
|
|
211
51
|
}
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
if (!inFlight) return inFlight = attempt(request).finally(() => { inFlight = null; });
|
|
215
|
-
if (request.background || !request.socketPath) return inFlight;
|
|
216
|
-
const until = Math.min(now() + waitMs, request.deadline ?? Infinity);
|
|
217
|
-
return Promise.race([inFlight, waitFor(request.socketPath, until).then((ready) => ready ? { threadId: null } : null)]);
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
export function startRelayKeeper({
|
|
222
|
-
wake,
|
|
223
|
-
socketPath,
|
|
224
|
-
answers = endpointAnswers,
|
|
225
|
-
intervalMs = RELAY_KEEPER_INTERVAL_MS,
|
|
226
|
-
initialDelayMs = 3000,
|
|
227
|
-
timers = globalThis,
|
|
228
|
-
log = () => {},
|
|
229
|
-
}) {
|
|
230
|
-
let busy = false;
|
|
231
|
-
let stopped = false;
|
|
232
|
-
const tick = async () => {
|
|
233
|
-
if (busy || stopped) return null;
|
|
234
|
-
busy = true;
|
|
235
|
-
try {
|
|
236
|
-
const target = socketPath();
|
|
237
|
-
if (await answers(target)) return null;
|
|
238
|
-
return await wake({ socketPath: target, background: true });
|
|
239
|
-
} catch (error) {
|
|
240
|
-
log(`relay keeper check failed: ${error.message}`);
|
|
241
|
-
return null;
|
|
242
|
-
} finally {
|
|
243
|
-
busy = false;
|
|
244
|
-
}
|
|
245
|
-
};
|
|
246
|
-
const first = timers.setTimeout(tick, initialDelayMs);
|
|
247
|
-
const repeat = timers.setInterval(tick, intervalMs);
|
|
248
|
-
first?.unref?.();
|
|
249
|
-
repeat?.unref?.();
|
|
250
|
-
return {
|
|
251
|
-
tick,
|
|
252
|
-
stop() {
|
|
253
|
-
stopped = true;
|
|
254
|
-
timers.clearTimeout(first);
|
|
255
|
-
timers.clearInterval(repeat);
|
|
256
|
-
},
|
|
52
|
+
log("native relay recovery timed out; no task or window was opened");
|
|
53
|
+
return null;
|
|
257
54
|
};
|
|
258
55
|
}
|
package/src/thread-delivery.mjs
CHANGED
|
@@ -74,7 +74,7 @@ export function matchDesktopProject(projects, cwd, { canonicalize = realpathSync
|
|
|
74
74
|
return matches[0];
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
export const RELAY_DOWN_GUIDANCE = "The Codex Desktop native relay is not listening
|
|
77
|
+
export const RELAY_DOWN_GUIDANCE = "The Codex Desktop native relay is not listening after waiting for silent recovery. Its Desktop-owned supervisor restarts crashed relay workers without replaying requests. If Desktop has not loaded the relay MCP, reconnect it in a regular project task; automatic recovery never opens a task or window. Do not open the internal Native Relay executor task.";
|
|
78
78
|
|
|
79
79
|
function relayUnreachable(error) {
|
|
80
80
|
return error?.code === "RELAY_UNREACHABLE" && error.sent === false;
|
|
@@ -126,23 +126,25 @@ export class DesktopTaskDelivery {
|
|
|
126
126
|
if (scopeBinding) this.security.hardenedRoots.recheck(scopeBinding);
|
|
127
127
|
const projectScope = expectedScope ?? await this.mutationProjectScope(operation, args, { deadline });
|
|
128
128
|
if (expectedScope) recheckProjectScope(expectedScope, this.senderProjectScope(expectedScope.recipient.input));
|
|
129
|
-
const remaining = deadline === undefined ? undefined : deadline - this.now();
|
|
130
|
-
if (remaining !== undefined && remaining <= 0) throw Object.assign(new Error("The bridge's response deadline has elapsed; this operation was not sent"), { code: "DESKTOP_RESPONSE_DEADLINE" });
|
|
131
129
|
const accountContext = this.accountContext?.();
|
|
132
|
-
const send = () =>
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
130
|
+
const send = () => {
|
|
131
|
+
const remaining = deadline === undefined ? undefined : deadline - this.now();
|
|
132
|
+
if (remaining !== undefined && remaining <= 0) throw Object.assign(new Error("The bridge's response deadline has elapsed; this operation was not sent"), { code: "DESKTOP_RESPONSE_DEADLINE" });
|
|
133
|
+
return this.relay.requestDesktop(operation, args, {
|
|
134
|
+
...(remaining === undefined ? {} : { timeoutMs: remaining }),
|
|
135
|
+
...(this.beforeRequest || projectScope || scopeBinding ? { beforeSend: async () => {
|
|
136
|
+
await this.beforeRequest?.({ operation, args, phase: "write" });
|
|
137
|
+
if (projectScope) {
|
|
138
|
+
recheckProjectScope(projectScope);
|
|
139
|
+
recheckProjectScope(projectScope, await this.mutationProjectScope(operation, args, { deadline }));
|
|
140
|
+
}
|
|
141
|
+
await this.beforeRequest?.({ operation, args, phase: "dispatch" });
|
|
142
|
+
if (scopeBinding) this.security.hardenedRoots.recheck(scopeBinding);
|
|
143
|
+
} } : {}),
|
|
144
|
+
...(accountContext ? { accountContext } : {}),
|
|
145
|
+
...(creationReceiptId ? { creationReceiptId } : {}),
|
|
146
|
+
});
|
|
147
|
+
};
|
|
146
148
|
let response;
|
|
147
149
|
try {
|
|
148
150
|
response = await send();
|