@minhspark/codex-mcp-bridge 1.25.1 → 1.25.3
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 +40 -0
- package/README.md +40 -16
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/scripts/install-antigravity-bridge.mjs +40 -10
- package/scripts/install-claude-code.mjs +2 -2
- package/scripts/install-claude-desktop.mjs +12 -7
- package/scripts/install-native-relay.mjs +2 -2
- package/scripts/setup.mjs +3 -3
- package/src/antigravity-bridge.mjs +79 -25
- package/src/antigravity-deliveries.mjs +32 -8
- package/src/antigravity-ipc.mjs +18 -9
- package/src/antigravity-outbound-bridge.mjs +46 -19
- package/src/antigravity-outbound-targets.mjs +41 -6
- package/src/bridge-readiness.mjs +2 -2
- package/src/claude-bridge.mjs +38 -23
- package/src/claude-code-registration.mjs +2 -1
- package/src/claude-session-router.mjs +8 -15
- package/src/index.mjs +35 -24
- package/src/mcp-supervisor.mjs +91 -10
- package/src/native-relay-companion.mjs +23 -3
- package/src/native-relay.mjs +8 -4
- package/src/peer-protocol.mjs +9 -6
- package/src/platform.mjs +1 -0
- package/src/recipient-preflight.mjs +81 -0
- package/src/relay-wake.mjs +15 -218
- package/src/setup.mjs +38 -18
- package/src/thread-delivery.mjs +28 -21
package/src/mcp-supervisor.mjs
CHANGED
|
@@ -7,6 +7,7 @@ import { createReleaseSnapshot, pruneReleaseSnapshots, snapshotRoot, sourceRevis
|
|
|
7
7
|
import { desktopTasksConfigured } from "./native-relay.mjs";
|
|
8
8
|
import { isMainModule, stableInstallPath } from "./install-paths.mjs";
|
|
9
9
|
import { CLIENT_CLOSE_DRAIN_MS, CLIENT_CLOSE_STOP_MS } from "./reload-control.mjs";
|
|
10
|
+
import { withBridgeDiagnostic } from "./recipient-preflight.mjs";
|
|
10
11
|
|
|
11
12
|
const WORKER_NODE = stableInstallPath(process.execPath);
|
|
12
13
|
const ENTRIES = new Set(["index.mjs", "claude-bridge.mjs", "native-relay-companion.mjs"]);
|
|
@@ -16,7 +17,10 @@ const RETIRE_GRACE_MS = 6000;
|
|
|
16
17
|
const CLIENT_CLOSE_GRACE_MS = CLIENT_CLOSE_DRAIN_MS + CLIENT_CLOSE_STOP_MS + 1000;
|
|
17
18
|
const hasId = (message) => Object.hasOwn(message, "id");
|
|
18
19
|
const idKey = (id) => JSON.stringify(id);
|
|
19
|
-
const errorMessage = (id, message
|
|
20
|
+
const errorMessage = (id, message, options = {}) => {
|
|
21
|
+
const result = withBridgeDiagnostic({ content: [] }, new Error(message), { component: "mcp-supervisor", code: "MCP_WORKER_UNAVAILABLE", ...options });
|
|
22
|
+
return { jsonrpc: "2.0", id, error: { code: -32603, message: `${message}\n${result.content[0].text}`, data: { diagnostic: result.structuredContent.diagnostic } } };
|
|
23
|
+
};
|
|
20
24
|
|
|
21
25
|
export function supervisorInterval(value, fallback) {
|
|
22
26
|
const parsed = Number(value);
|
|
@@ -170,6 +174,7 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
170
174
|
let active;
|
|
171
175
|
let initializeParams;
|
|
172
176
|
let initializeResult;
|
|
177
|
+
let initializeCatalog;
|
|
173
178
|
let ready = false;
|
|
174
179
|
let switching = false;
|
|
175
180
|
let stopping = false;
|
|
@@ -181,6 +186,17 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
181
186
|
let retryAt = 0;
|
|
182
187
|
let reason = null;
|
|
183
188
|
let reloads = 0;
|
|
189
|
+
let recoveries = 0;
|
|
190
|
+
let recoveryAttempts = 0;
|
|
191
|
+
let recoveryAt = 0;
|
|
192
|
+
let activeSince = Date.now();
|
|
193
|
+
const recoveryDelayMs = Math.min(30000, supervisorInterval(env.CODEX_BRIDGE_RECOVERY_DELAY_MS, 1000));
|
|
194
|
+
const recoveryLimit = 5;
|
|
195
|
+
const canRecover = () => entry === "native-relay-companion.mjs" && ready && initializeResult && initializeCatalog;
|
|
196
|
+
const scheduleRecovery = () => {
|
|
197
|
+
if (recoveryAttempts >= recoveryLimit) return;
|
|
198
|
+
recoveryAt = Date.now() + Math.min(30000, recoveryDelayMs * 2 ** recoveryAttempts);
|
|
199
|
+
};
|
|
184
200
|
const queue = [];
|
|
185
201
|
const requests = new Map();
|
|
186
202
|
const reverse = new Map();
|
|
@@ -189,7 +205,7 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
189
205
|
revision: active?.release.revision ?? null, availableRevision: observed,
|
|
190
206
|
routingConfiguration: active?.routingConfiguration ?? null, availableRoutingConfiguration: observedRouting,
|
|
191
207
|
pending: Boolean(observed && (observed !== active?.release.revision || observedRouting !== active?.routingConfiguration)),
|
|
192
|
-
state: failure ? "failed" : switching ? "reloading" : reason ? "deferred" : "current", reason: failure ?? reason, reloads });
|
|
208
|
+
state: failure ? canRecover() && recoveryAttempts < recoveryLimit ? "recovering" : "failed" : switching ? "reloading" : reason ? "deferred" : "current", reason: failure ?? reason, reloads, recoveries, recoveryAttempts });
|
|
193
209
|
|
|
194
210
|
const handlers = {
|
|
195
211
|
message(worker, message) {
|
|
@@ -210,25 +226,37 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
210
226
|
const respond = async () => {
|
|
211
227
|
if (pending.method === "initialize" && !message.error) {
|
|
212
228
|
initializeResult = message.result;
|
|
229
|
+
if (entry === "native-relay-companion.mjs") {
|
|
230
|
+
initializeCatalog = await worker.rpc("tools/list", {});
|
|
231
|
+
if (!Array.isArray(initializeCatalog.tools) || !initializeCatalog.tools.length || initializeCatalog.nextCursor) throw new Error("The native relay tool catalog is incomplete");
|
|
232
|
+
}
|
|
213
233
|
await worker.control("activate");
|
|
214
234
|
ready = true;
|
|
215
235
|
}
|
|
216
236
|
if (message.result && ["codex_bridge_status", "claude_bridge_status", "native_relay_status"].includes(pending.tool)) {
|
|
217
237
|
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}` : ""}` }];
|
|
238
|
+
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
239
|
}
|
|
220
240
|
output({ ...message, id: pending.id });
|
|
221
241
|
};
|
|
222
|
-
void respond().catch((error) => { failure = error.message; output(errorMessage(pending.id, error.message)); });
|
|
242
|
+
void respond().catch((error) => { failure = error.message; output(errorMessage(pending.id, error.message, { code: "MCP_RESPONSE_FAILED", operation: pending.tool ?? pending.method, deliveryState: "unknown" })); });
|
|
223
243
|
},
|
|
224
244
|
exit(worker, error) {
|
|
225
245
|
for (const [id, pending] of worker.clientPending) {
|
|
226
246
|
requests.delete(idKey(pending.id));
|
|
227
|
-
output(errorMessage(pending.id, `${error.message}. The operation may already have been dispatched; it was not retried. Retain the original task and receipts
|
|
247
|
+
output(errorMessage(pending.id, `${error.message}. The operation may already have been dispatched; it was not retried. Retain the original task and receipts.`, { code: "MCP_WORKER_EXITED", operation: pending.tool ?? pending.method, deliveryState: "unknown" }));
|
|
228
248
|
worker.clientPending.delete(id);
|
|
229
249
|
}
|
|
230
250
|
for (const [id, pending] of reverse) if (pending.worker === worker) reverse.delete(id);
|
|
231
|
-
if (worker === active && !worker.retiring && !stopping) {
|
|
251
|
+
if (worker === active && !worker.retiring && !stopping) {
|
|
252
|
+
failure = `${error.message}; automatic replay is disabled because delivery state may be unknown`;
|
|
253
|
+
if (canRecover()) {
|
|
254
|
+
if (Date.now() - activeSince >= 60000) recoveryAttempts = 0;
|
|
255
|
+
scheduleRecovery();
|
|
256
|
+
if (recoveryAttempts >= recoveryLimit) failure += "; recovery limit reached, reconnect MCP";
|
|
257
|
+
}
|
|
258
|
+
log(failure);
|
|
259
|
+
}
|
|
232
260
|
},
|
|
233
261
|
};
|
|
234
262
|
const spawn = (release, routing) => {
|
|
@@ -245,7 +273,56 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
245
273
|
return worker;
|
|
246
274
|
};
|
|
247
275
|
const check = async () => {
|
|
248
|
-
if (switching || !ready || stopping
|
|
276
|
+
if (switching || !ready || stopping) return;
|
|
277
|
+
if (failure) {
|
|
278
|
+
if (!canRecover() || recoveryAttempts >= recoveryLimit || Date.now() < recoveryAt) return;
|
|
279
|
+
switching = true;
|
|
280
|
+
recoveryAttempts++;
|
|
281
|
+
const previous = active;
|
|
282
|
+
let candidate;
|
|
283
|
+
try {
|
|
284
|
+
await previous.stop();
|
|
285
|
+
workers.delete(previous);
|
|
286
|
+
if (stopping) return;
|
|
287
|
+
const source = currentRoot();
|
|
288
|
+
const release = createReleaseSnapshot(source, { expectedRevision: sourceRevision(source), lease: true });
|
|
289
|
+
candidate = spawn(release, routingConfiguration());
|
|
290
|
+
const initialized = await candidate.rpc("initialize", initializeParams);
|
|
291
|
+
if (initialized.protocolVersion !== initializeResult.protocolVersion || JSON.stringify(initialized.capabilities) !== JSON.stringify(initializeResult.capabilities)) {
|
|
292
|
+
throw new Error("The replacement MCP protocol or capabilities require an explicit client reconnect");
|
|
293
|
+
}
|
|
294
|
+
candidate.write({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
295
|
+
const catalog = await candidate.rpc("tools/list", {});
|
|
296
|
+
if (JSON.stringify(catalog) !== JSON.stringify(initializeCatalog)) throw new Error("The replacement tool catalog requires an explicit client reconnect");
|
|
297
|
+
await candidate.control("activate");
|
|
298
|
+
if (stopping || candidate.closed) throw new Error("The relay replacement stopped before activation completed");
|
|
299
|
+
active = candidate;
|
|
300
|
+
candidate = null;
|
|
301
|
+
activeSince = Date.now();
|
|
302
|
+
failure = null;
|
|
303
|
+
reason = null;
|
|
304
|
+
recoveries++;
|
|
305
|
+
log(`recovered native relay worker after exit; recovery ${recoveries}, no requests replayed`);
|
|
306
|
+
schedulePrune();
|
|
307
|
+
} catch (error) {
|
|
308
|
+
failure = `Native relay recovery failed: ${error.message}; no requests were replayed`;
|
|
309
|
+
if (candidate) {
|
|
310
|
+
try {
|
|
311
|
+
await candidate.stop();
|
|
312
|
+
workers.delete(candidate);
|
|
313
|
+
} catch (stopError) {
|
|
314
|
+
recoveryAttempts = recoveryLimit;
|
|
315
|
+
failure += `; replacement shutdown could not be confirmed: ${stopError.message}`;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
scheduleRecovery();
|
|
319
|
+
if (recoveryAttempts >= recoveryLimit) failure += "; recovery limit reached, reconnect MCP";
|
|
320
|
+
log(failure);
|
|
321
|
+
} finally {
|
|
322
|
+
switching = false;
|
|
323
|
+
}
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
249
326
|
let source;
|
|
250
327
|
let revision;
|
|
251
328
|
let routing;
|
|
@@ -279,8 +356,12 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
279
356
|
await candidate.control("restore", exported.state);
|
|
280
357
|
await candidate.control("activate");
|
|
281
358
|
if (stopping) throw new Error("The MCP client disconnected during activation");
|
|
359
|
+
if (candidate.closed) throw new Error("The replacement worker exited during activation");
|
|
282
360
|
active = candidate;
|
|
283
361
|
candidate = null;
|
|
362
|
+
initializeCatalog = catalog;
|
|
363
|
+
activeSince = Date.now();
|
|
364
|
+
failure = null;
|
|
284
365
|
reloads++;
|
|
285
366
|
reason = null;
|
|
286
367
|
if (JSON.stringify(catalog) !== JSON.stringify(oldCatalog)) output({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
|
|
@@ -321,16 +402,16 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
321
402
|
}
|
|
322
403
|
return;
|
|
323
404
|
}
|
|
324
|
-
if (failure) { if (hasId(message)) output(errorMessage(message.id, failure)); return; }
|
|
405
|
+
if (failure) { if (hasId(message)) output(errorMessage(message.id, `${failure}${canRecover() && recoveryAttempts < recoveryLimit ? "; native relay recovery is pending, this request was not sent" : ""}`, { operation: message.params?.name ?? message.method, deliveryState: "not_sent" })); return; }
|
|
325
406
|
if (switching || !active) {
|
|
326
407
|
const bytes = queue.reduce((size, queued) => size + Buffer.byteLength(JSON.stringify(queued)), 0);
|
|
327
|
-
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; }
|
|
408
|
+
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", { code: "MCP_QUEUE_FULL", operation: message.params?.name ?? message.method, deliveryState: "not_sent" })); return; }
|
|
328
409
|
queue.push(message);
|
|
329
410
|
return;
|
|
330
411
|
}
|
|
331
412
|
if (message.method === "initialize") initializeParams = message.params;
|
|
332
413
|
if (hasId(message)) {
|
|
333
|
-
if (requests.has(idKey(message.id))) { output(errorMessage(message.id, "Duplicate active JSON-RPC request ID")); return; }
|
|
414
|
+
if (requests.has(idKey(message.id))) { output(errorMessage(message.id, "Duplicate active JSON-RPC request ID", { code: "MCP_DUPLICATE_REQUEST", operation: message.params?.name ?? message.method, deliveryState: "unknown" })); return; }
|
|
334
415
|
const id = `client:${++sequence}`;
|
|
335
416
|
active.clientPending.set(id, { id: message.id, method: message.method, tool: message.params?.name });
|
|
336
417
|
requests.set(idKey(message.id), { worker: active, workerId: id });
|
|
@@ -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.3";
|
|
46
47
|
const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
|
|
47
48
|
|
|
48
49
|
function errorResponse(code, message, sent, details) {
|
|
@@ -344,6 +345,9 @@ async function handleDesktopRequest(payload, { dispatchDesktop, resolveExecutor,
|
|
|
344
345
|
if (payload.operation === "send_message_to_thread" && payload.arguments.threadId === executorThreadId) {
|
|
345
346
|
return errorResponse("RELAY_BAD_REQUEST", "The relay executor cannot receive its own relayed message");
|
|
346
347
|
}
|
|
348
|
+
if (payload.operation === "navigate_to_codex_page" && payload.arguments.threadId === executorThreadId) {
|
|
349
|
+
return errorResponse("RELAY_BAD_REQUEST", "The internal relay executor has no user conversation to open");
|
|
350
|
+
}
|
|
347
351
|
// Reserve before dispatch. A disconnected caller must not discard a late
|
|
348
352
|
// result, and even identical duplicate writes must not dispatch twice.
|
|
349
353
|
const creation = payload.creationReceiptId ? creationReceipts.reserve(payload.creationReceiptId, payload.arguments, accountContext, executorThreadId) : null;
|
|
@@ -865,6 +869,20 @@ export function createNativeRelayLifecycle({ nativeTools, relays, log: logFn = (
|
|
|
865
869
|
};
|
|
866
870
|
}
|
|
867
871
|
|
|
872
|
+
export async function probeRelayDispatch({ env = process.env, timeoutMs = 3000, accounts = readBridgeAccounts, relay = new NativeDesktopRelay({ env, socketPath: desktopTaskSocketPath(env), accountSocketPath: accountRelaySocketPath(env) }) } = {}) {
|
|
873
|
+
try {
|
|
874
|
+
const accountContext = accountIdentity(accounts());
|
|
875
|
+
const response = await relay.requestDesktop("list_projects", {}, {
|
|
876
|
+
timeoutMs,
|
|
877
|
+
...(accountContext ? { accountContext, beforeSend: () => assertAccountIdentity(accountContext) } : {}),
|
|
878
|
+
});
|
|
879
|
+
if (!Array.isArray(response?.result?.projects)) throw new Error("Native Desktop did not return a project list");
|
|
880
|
+
return { ready: true, checkedAt: new Date().toISOString(), scope: "shared-relay-native-dispatch" };
|
|
881
|
+
} catch (error) {
|
|
882
|
+
return { ready: false, checkedAt: new Date().toISOString(), scope: "shared-relay-native-dispatch", code: error.code ?? "NATIVE_HEALTH_CHECK_FAILED", reason: error.message };
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
|
|
868
886
|
export function createRelayServers({ nativeTools, env = process.env, signalExit = true, log: logFn = () => {} }) {
|
|
869
887
|
const hardened = hardenedBridgeEnabled(env);
|
|
870
888
|
if (hardened && !desktopTasksConfigured(env)) throw new Error("CODEX_BRIDGE_HARDENED=1 requires CODEX_BRIDGE_DESKTOP_TASKS=1; legacy relay listeners are disabled");
|
|
@@ -938,8 +956,9 @@ if (invokedDirectly) {
|
|
|
938
956
|
} catch (err) {
|
|
939
957
|
executor = err.message;
|
|
940
958
|
}
|
|
959
|
+
const nativeDispatch = listening ? await probeRelayDispatch() : { ready: false, reason: "Relay transport is not listening" };
|
|
941
960
|
return {
|
|
942
|
-
structuredContent: { runtime: runtime.status() },
|
|
961
|
+
structuredContent: { runtime: runtime.status(), nativeDispatch },
|
|
943
962
|
content: [
|
|
944
963
|
{
|
|
945
964
|
type: "text",
|
|
@@ -953,6 +972,7 @@ if (invokedDirectly) {
|
|
|
953
972
|
`remote Codex: ${remoteRelay.socketPath} (${await remoteRelay.isListening() ? "listening" : "not listening"})`,
|
|
954
973
|
`executor: ${executor}`,
|
|
955
974
|
`dispatch: ${process.env.CODEX_NATIVE_RELAY_METHOD ?? NATIVE_DISPATCH_METHOD}`,
|
|
975
|
+
`native dispatch: ${nativeDispatch.ready ? "verified (read-only list_projects)" : `unavailable (${nativeDispatch.reason})`}`,
|
|
956
976
|
`native pipe: ${nativeTools.socketPath ?? "unavailable (requires Codex Desktop)"}`,
|
|
957
977
|
].join("\n"),
|
|
958
978
|
},
|
package/src/native-relay.mjs
CHANGED
|
@@ -254,6 +254,9 @@ export function validateDesktopOperation(operation, args) {
|
|
|
254
254
|
|
|
255
255
|
export function nativeDesktopOperationParams({ executorThreadId, operation, arguments: args }) {
|
|
256
256
|
validateDesktopOperation(operation, args);
|
|
257
|
+
if (operation === "navigate_to_codex_page" && args.threadId === executorThreadId) {
|
|
258
|
+
throw new NativeRelayError("The internal relay executor has no user conversation to open", "RELAY_BAD_REQUEST");
|
|
259
|
+
}
|
|
257
260
|
return {
|
|
258
261
|
arguments: args,
|
|
259
262
|
callerSource: "codex",
|
|
@@ -560,9 +563,9 @@ export async function resolveNativeToolsPipePath({
|
|
|
560
563
|
}
|
|
561
564
|
|
|
562
565
|
export class NativeToolsClient {
|
|
563
|
-
constructor({ env = process.env, socketPath
|
|
566
|
+
constructor({ env = process.env, socketPath, timeoutMs = DEFAULT_TIMEOUT_MS, resolveSocketPath = () => resolveNativeToolsPipePath({ env: { ...env, CODEX_APP_TOOLS_PIPE_PATH: undefined } }), assertAccount = assertAccountIdentity } = {}) {
|
|
564
567
|
this.env = env;
|
|
565
|
-
this.socketPath = socketPath;
|
|
568
|
+
this.socketPath = socketPath ?? env.CODEX_APP_TOOLS_PIPE_PATH;
|
|
566
569
|
this.explicitSocketPath = socketPath || null;
|
|
567
570
|
this.hasDiscoveredSocket = false;
|
|
568
571
|
this.timeoutMs = timeoutMs;
|
|
@@ -581,7 +584,7 @@ export class NativeToolsClient {
|
|
|
581
584
|
if (this.socket && !this.socket.destroyed) return;
|
|
582
585
|
const generation = this.connectionGeneration;
|
|
583
586
|
this.connecting = (async () => {
|
|
584
|
-
const socketPath = this.socketPath || await this.resolveSocketPath();
|
|
587
|
+
const socketPath = this.socketPath || await this.resolveSocketPath() || this.env.CODEX_APP_TOOLS_PIPE_PATH;
|
|
585
588
|
if (generation !== this.connectionGeneration) {
|
|
586
589
|
throw new NativeRelayError("Native tools client closed while discovering the Desktop pipe", "NATIVE_PIPE_UNAVAILABLE");
|
|
587
590
|
}
|
|
@@ -774,7 +777,8 @@ export function nativeRelayStatus(env = process.env) {
|
|
|
774
777
|
enabled: false,
|
|
775
778
|
mode,
|
|
776
779
|
socketPath,
|
|
777
|
-
|
|
780
|
+
code: "RELAY_UNREACHABLE",
|
|
781
|
+
reason: `The native relay endpoint is unavailable at ${socketPath}. Its Desktop-owned host may be unloaded or disconnected; socket absence alone cannot distinguish these causes. Silent worker recovery requires a running native supervisor. Do not open or create chats to load the relay; include the bridge diagnostic in a report.`,
|
|
778
782
|
};
|
|
779
783
|
}
|
|
780
784
|
return { enabled: true, mode, socketPath, reason: null };
|
package/src/peer-protocol.mjs
CHANGED
|
@@ -235,7 +235,7 @@ export function findClaudeSession(target, { desktopOnly = false, expectedCwd } =
|
|
|
235
235
|
if (matches.length !== 1) throw new Error(`Desktop-only mode requires an unambiguous sessionId or pid; "${needle}" matches multiple sessions. No message was sent.`);
|
|
236
236
|
const session = matches[0];
|
|
237
237
|
if (session.entrypoint !== "claude-desktop") {
|
|
238
|
-
throw new Error(`Desktop-only mode refuses Claude session ${session.sessionId ?? session.pid} with entrypoint ${session.entrypoint ?? "unknown"}.
|
|
238
|
+
throw Object.assign(new Error(`Desktop-only mode refuses Claude session ${session.sessionId ?? session.pid} with entrypoint ${session.entrypoint ?? "unknown"}. Inspect native session discovery; do not open chats to load a relay or launch a replacement CLI session. No message was sent.`), { code: "CLAUDE_DESKTOP_TASK_UNVERIFIED", sent: false });
|
|
239
239
|
}
|
|
240
240
|
if (expectedCwd !== undefined) assertClaudeSessionCwd(session, expectedCwd);
|
|
241
241
|
return session;
|
|
@@ -268,13 +268,13 @@ export function assertClaudeSessionCwd(session, expectedCwd) {
|
|
|
268
268
|
}
|
|
269
269
|
|
|
270
270
|
export function assertClaudeSessionProcess(session) {
|
|
271
|
-
const changed = "The live Claude process identity is missing or changed. No message was sent;
|
|
272
|
-
if (!session?.alive || typeof session.processStart !== "string" || !session.processStart) throw new Error(changed);
|
|
271
|
+
const changed = "The live Claude process identity is missing or changed. No message was sent; inspect list_claude_sessions without opening an app or chat.";
|
|
272
|
+
if (!session?.alive || typeof session.processStart !== "string" || !session.processStart) throw Object.assign(new Error(changed), { code: "CLAUDE_PROCESS_IDENTITY_CHANGED", sent: false });
|
|
273
273
|
const current = readProcessStart(session.pid);
|
|
274
274
|
if (current === null) {
|
|
275
|
-
throw new Error("The live Claude process identity could not be read, so this send was refused rather than sent unverified. That failure is not itself evidence the session changed; inspect
|
|
275
|
+
throw Object.assign(new Error("The live Claude process identity could not be read, so this send was refused rather than sent unverified. That failure is not itself evidence the session changed; inspect native session discovery before retrying."), { code: "CLAUDE_PROCESS_IDENTITY_UNAVAILABLE", sent: false });
|
|
276
276
|
}
|
|
277
|
-
if (current !== session.processStart) throw new Error(changed);
|
|
277
|
+
if (current !== session.processStart) throw Object.assign(new Error(changed), { code: "CLAUDE_PROCESS_IDENTITY_CHANGED", sent: false });
|
|
278
278
|
}
|
|
279
279
|
|
|
280
280
|
/**
|
|
@@ -1050,6 +1050,7 @@ export class PeerEndpoint {
|
|
|
1050
1050
|
+ "Inspect read_claude_delivery and wait for Claude's outstanding replies. Changing waitSec or starting another bridge must not be used to bypass a pending message.",
|
|
1051
1051
|
);
|
|
1052
1052
|
error.code = "PEER_REPLY_PENDING";
|
|
1053
|
+
error.sent = false;
|
|
1053
1054
|
throw error;
|
|
1054
1055
|
}
|
|
1055
1056
|
const since = Date.now();
|
|
@@ -1076,8 +1077,9 @@ export class PeerEndpoint {
|
|
|
1076
1077
|
}
|
|
1077
1078
|
} catch (err) {
|
|
1078
1079
|
const sent = this.sentMessages.get(msgId);
|
|
1079
|
-
if (sent) sent.error = err.message;
|
|
1080
|
+
if (sent) { sent.error = err.message; sent.errorCode = err.code ?? "CLAUDE_PEER_SEND_FAILED"; }
|
|
1080
1081
|
if (!err.deliveryUncertain) {
|
|
1082
|
+
err.sent = false;
|
|
1081
1083
|
this.#removePendingReply(targetSocket, msgId);
|
|
1082
1084
|
if (sent) sent.failed = true;
|
|
1083
1085
|
}
|
|
@@ -1160,6 +1162,7 @@ export class PeerEndpoint {
|
|
|
1160
1162
|
msgId,
|
|
1161
1163
|
status: sent.reply ? "reply_received" : sent.failed ? "send_failed" : delivery?.status ?? "sent_unconfirmed",
|
|
1162
1164
|
reason: delivery?.reason ?? sent.error ?? null,
|
|
1165
|
+
...(sent.errorCode || delivery?.reasonCode ? { reasonCode: sent.errorCode ?? delivery.reasonCode } : {}),
|
|
1163
1166
|
sentAt: sent.sentAt,
|
|
1164
1167
|
sessionId: session?.sessionId ?? null,
|
|
1165
1168
|
cwd: session?.cwd ?? null,
|
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;
|
|
@@ -1,5 +1,86 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
|
|
4
|
+
const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
|
|
5
|
+
const diagnosticCode = (value) => typeof value === "string" && /^[A-Z][A-Z0-9_]{0,79}$/.test(value) ? value : null;
|
|
6
|
+
const diagnosticId = (value) => typeof value === "string" && /^(?:[a-f0-9]{64}|(?:local_)?[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12})$/i.test(value) ? value : null;
|
|
7
|
+
|
|
8
|
+
export function diagnosticText(value) {
|
|
9
|
+
return String(value ?? "Unknown bridge failure")
|
|
10
|
+
.replace(/\{[\s\S]*\}|\[\s*(?:\{|"|true|false|null|\d)[\s\S]*\]/g, "[structured details omitted]")
|
|
11
|
+
.replace(/\b(?:Bearer|Basic)\s+[A-Za-z0-9._~+/=-]+/gi, "[credential]")
|
|
12
|
+
.replace(/((?:access[_-]?token|refresh[_-]?token|api[_-]?key|token|secret|password|authorization)\s*["']?\s*[:=]\s*)["']?[^\s,"';}]+["']?/gi, "$1[redacted]")
|
|
13
|
+
.replace(/https?:\/\/[^\s<>"']+/gi, "[url]")
|
|
14
|
+
.replace(/(?:[a-z]:[\\/]|\\\\)[^\r\n"'<>|,;]*/gi, "[path]")
|
|
15
|
+
.replace(/(^|[\s"'(])\/(?:Users|home|tmp|var|private|Volumes|mnt)\/[^\r\n"'<>|,;]*/g, "$1[path]")
|
|
16
|
+
.replace(/[\w.+-]+@[\w.-]+\.[a-z]{2,}/gi, "[email]")
|
|
17
|
+
.replace(/\b(?:sk-[a-z0-9_-]+|gh[pousr]_[a-z0-9_]+)\b/gi, "[credential]")
|
|
18
|
+
.replace(/[\u0000-\u001f\u007f]/g, " ").slice(0, 1400);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function bridgeDiagnostic(error, options = {}) {
|
|
22
|
+
const previous = error?.diagnostic?.schemaVersion === 1 ? error.diagnostic : null;
|
|
23
|
+
const chain = [];
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
for (let cause = error; cause && !seen.has(cause) && chain.length < 8; cause = cause.cause) {
|
|
26
|
+
seen.add(cause);
|
|
27
|
+
chain.push(cause);
|
|
28
|
+
}
|
|
29
|
+
const codes = [...new Set(chain.flatMap((cause) => [cause?.preflight?.code, cause?.code, cause?.reasonCode, cause?.diagnostic?.code]).map(diagnosticCode).filter(Boolean))];
|
|
30
|
+
const code = diagnosticCode(previous?.code) ?? diagnosticCode(options.code) ?? codes[0] ?? "BRIDGE_OPERATION_FAILED";
|
|
31
|
+
const state = options.deliveryState ?? previous?.deliveryState ?? error?.deliveryState;
|
|
32
|
+
const deliveryState = state === "not_applicable" || options.readOnly && !previous ? "not_applicable"
|
|
33
|
+
: ["not_sent", "not-sent"].includes(state) ? "not_sent"
|
|
34
|
+
: state === "sent" ? "sent"
|
|
35
|
+
: state === "unknown" || error?.deliveryUncertain ? "unknown"
|
|
36
|
+
: chain.some((cause) => cause?.sent === false || cause?.preflight?.sent === false) ? "not_sent" : "unknown";
|
|
37
|
+
const name = (value, fallback) => typeof value === "string" && /^[a-zA-Z][a-zA-Z0-9_./-]{0,99}$/.test(value) ? value : fallback;
|
|
38
|
+
const ids = {};
|
|
39
|
+
for (const key of ["requestId", "msgId", "deliveryId", "threadId", "taskId", "conversationId"]) {
|
|
40
|
+
const id = diagnosticId(previous?.ids?.[key] ?? options[key] ?? error?.[key]);
|
|
41
|
+
if (id) ids[key] = id;
|
|
42
|
+
}
|
|
43
|
+
const timestamp = previous?.timestamp;
|
|
44
|
+
return {
|
|
45
|
+
schemaVersion: 1,
|
|
46
|
+
reportId: diagnosticId(previous?.reportId) ?? randomUUID(),
|
|
47
|
+
timestamp: typeof timestamp === "string" && /^\d{4}-\d{2}-\d{2}T[\d:.]+Z$/.test(timestamp) ? timestamp : new Date().toISOString(),
|
|
48
|
+
component: name(previous?.component ?? options.component, "bridge"),
|
|
49
|
+
operation: name(previous?.operation === "unknown" ? options.operation : previous?.operation ?? options.operation, "unknown"),
|
|
50
|
+
version: typeof previous?.version === "string" && /^\d+\.\d+\.\d+(?:-[a-z0-9.-]+)?$/i.test(previous.version) ? previous.version : version,
|
|
51
|
+
platform: process.platform,
|
|
52
|
+
nodeVersion: process.versions.node,
|
|
53
|
+
code,
|
|
54
|
+
causeCodes: [...new Set([...(Array.isArray(previous?.causeCodes) ? previous.causeCodes : []).map(diagnosticCode).filter(Boolean), ...codes])].filter((value) => value !== code).slice(0, 8),
|
|
55
|
+
phase: name(previous?.phase ?? options.phase ?? error?.phase ?? error?.diagnostic?.stage, deliveryState === "not_sent" ? "preflight" : options.readOnly ? "inspection" : "delivery"),
|
|
56
|
+
deliveryState,
|
|
57
|
+
sent: deliveryState === "not_sent" ? false : deliveryState === "sent" ? true : null,
|
|
58
|
+
retrySafe: deliveryState === "not_sent",
|
|
59
|
+
nextAction: deliveryState === "not_sent" ? "Inspect the reported cause and bridge status before retrying. Do not open or create chats to load a relay."
|
|
60
|
+
: deliveryState === "not_applicable" ? "Inspect bridge status and include this diagnostic in the report."
|
|
61
|
+
: "Inspect the original delivery receipt. Do not resend automatically or create a replacement chat.",
|
|
62
|
+
reason: diagnosticText(previous?.reason ?? error?.preflight?.reason ?? error?.message ?? error),
|
|
63
|
+
ids,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function withBridgeDiagnostic(result, error, options = {}) {
|
|
68
|
+
const diagnostic = bridgeDiagnostic({ ...error, cause: error?.cause, message: error?.message ?? String(error ?? "Bridge operation failed"), diagnostic: result?.structuredContent?.diagnostic ?? result?.diagnostic ?? error?.diagnostic }, options);
|
|
69
|
+
const report = ["Bridge diagnostic (include this block in a bug report)", `${diagnostic.code} | ${diagnostic.component} / ${diagnostic.operation}`,
|
|
70
|
+
`Bridge ${diagnostic.version} | ${diagnostic.platform} | Node ${diagnostic.nodeVersion}`,
|
|
71
|
+
`Phase: ${diagnostic.phase} | Delivery: ${diagnostic.deliveryState} | Sent: ${diagnostic.sent ?? "unconfirmed"}`,
|
|
72
|
+
`Reason: ${diagnostic.reason}`, ...(diagnostic.causeCodes.length ? [`Cause codes: ${diagnostic.causeCodes.join(", ")}`] : []),
|
|
73
|
+
...Object.entries(diagnostic.ids).map(([key, value]) => `${key}: ${value}`),
|
|
74
|
+
`Report: ${diagnostic.reportId} | ${diagnostic.timestamp}`, diagnostic.nextAction].join("\n");
|
|
75
|
+
return { ...result, content: [...(result?.content ?? []).filter((item) => item.type !== "text" || !item.text.startsWith("Bridge diagnostic (")), { type: "text", text: report }],
|
|
76
|
+
structuredContent: { ...result?.structuredContent, diagnostic } };
|
|
77
|
+
}
|
|
78
|
+
|
|
1
79
|
export function preflightFailure(code, reason) {
|
|
2
80
|
const error = new Error(`${reason} No message was sent.`);
|
|
81
|
+
error.code = code;
|
|
82
|
+
error.deliveryState = "not_sent";
|
|
83
|
+
error.sent = false;
|
|
3
84
|
error.preflight = { status: "blocked", code, reason, sent: false };
|
|
4
85
|
return error;
|
|
5
86
|
}
|