@minhspark/codex-mcp-bridge 1.25.2 → 1.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +58 -0
- package/README.md +81 -32
- package/docs/new-macos-setup.md +47 -21
- package/docs/new-windows-setup.md +34 -9
- package/docs/project-onboarding.md +21 -8
- package/package.json +2 -1
- package/plugin.json +1 -1
- package/scripts/background-relay-hidden.vbs +11 -0
- package/scripts/install-antigravity-bridge.mjs +65 -23
- package/scripts/install-background-relay.mjs +428 -0
- 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-background-relay.mjs +58 -0
- package/scripts/setup.mjs +3 -3
- package/scripts/worker-relay.mjs +41 -0
- package/src/antigravity-bridge.mjs +96 -27
- package/src/antigravity-cli.mjs +13 -2
- package/src/antigravity-deliveries.mjs +32 -8
- package/src/antigravity-ipc.mjs +18 -9
- package/src/antigravity-outbound-bridge.mjs +71 -23
- package/src/antigravity-outbound-targets.mjs +100 -10
- package/src/background-antigravity-worker.mjs +192 -0
- package/src/background-claude-worker.mjs +240 -0
- package/src/background-codex-worker.mjs +344 -0
- package/src/background-relay-client.mjs +107 -0
- package/src/background-relay-config.mjs +98 -0
- package/src/background-relay-daemon.mjs +60 -0
- package/src/background-relay-mcp.mjs +45 -0
- package/src/background-relay-protocol.mjs +54 -0
- package/src/background-relay-registration.mjs +198 -0
- package/src/background-relay-service.mjs +311 -0
- package/src/background-relay-storage.mjs +203 -0
- package/src/background-relay-storage.ps1 +64 -0
- package/src/background-relay.mjs +373 -0
- package/src/bridge-readiness.mjs +2 -2
- package/src/claude-bridge.mjs +43 -25
- package/src/claude-code-registration.mjs +2 -1
- package/src/claude-session-router.mjs +8 -15
- package/src/index.mjs +39 -22
- package/src/mcp-supervisor.mjs +26 -16
- package/src/native-relay-companion.mjs +4 -1
- package/src/native-relay.mjs +5 -1
- package/src/peer-protocol.mjs +9 -6
- package/src/recipient-preflight.mjs +81 -0
- package/src/reload-control.mjs +2 -2
- package/src/setup.mjs +62 -25
- package/src/thread-delivery.mjs +10 -5
- package/src/windows-pipe-acl.ps1 +1 -1
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { assertClaudeSessionCwd } from "./peer-protocol.mjs";
|
|
2
2
|
import { preflightFailure } from "./recipient-preflight.mjs";
|
|
3
|
-
import { setTimeout as delay } from "node:timers/promises";
|
|
4
3
|
|
|
5
4
|
function assertCwd(session, expectedCwd) {
|
|
6
5
|
try {
|
|
@@ -24,13 +23,13 @@ function uniqueLiveSessions(sessions) {
|
|
|
24
23
|
return [...unique.values()];
|
|
25
24
|
}
|
|
26
25
|
|
|
27
|
-
export async function recoverClaudeDesktopSession(args, { listTasks, listSessions,
|
|
26
|
+
export async function recoverClaudeDesktopSession(args, { listTasks, listSessions, beforeInspect }) {
|
|
28
27
|
try { return { session: resolveClaudeDesktopSession(args) }; }
|
|
29
28
|
catch (error) {
|
|
30
29
|
if (error.preflight?.code !== "CLAUDE_SESSION_NOT_FOUND" || args.target !== "auto" || !args.expectedTaskId) throw error;
|
|
31
30
|
}
|
|
32
31
|
const assertTask = async () => {
|
|
33
|
-
await
|
|
32
|
+
await beforeInspect();
|
|
34
33
|
const tasks = await listTasks();
|
|
35
34
|
if (!Array.isArray(tasks) || tasks.length > 8192) throw preflightFailure("CLAUDE_DESKTOP_TASK_UNVERIFIED", "Saved Desktop tasks cannot be verified.");
|
|
36
35
|
const matches = tasks.filter((task) => task.taskId === args.expectedTaskId);
|
|
@@ -41,17 +40,11 @@ export async function recoverClaudeDesktopSession(args, { listTasks, listSession
|
|
|
41
40
|
return matches[0];
|
|
42
41
|
};
|
|
43
42
|
const task = await assertTask();
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
try { return { session: resolveClaudeDesktopSession({ ...args, sessions: await listSessions() }) }; }
|
|
50
|
-
catch (error) { if (error.preflight?.code !== "CLAUDE_SESSION_NOT_FOUND") throw error; }
|
|
51
|
-
if (Date.now() >= deadline) break;
|
|
52
|
-
await sleep(Math.min(250, Math.max(0, deadline - Date.now())));
|
|
53
|
-
}
|
|
54
|
-
return { recovery: { status: "awaiting_session", sent: false, taskId: task.taskId, cwd: args.expectedCwd, reason: "The exact saved task was opened in Claude Desktop and is reconnecting. Retry target auto with this expectedTaskId; no message has been sent." } };
|
|
43
|
+
const sessions = await listSessions();
|
|
44
|
+
await assertTask();
|
|
45
|
+
try { return { session: resolveClaudeDesktopSession({ ...args, sessions }) }; }
|
|
46
|
+
catch (error) { if (error.preflight?.code !== "CLAUDE_SESSION_NOT_FOUND") throw error; }
|
|
47
|
+
throw Object.assign(preflightFailure("CLAUDE_SESSION_NOT_READY", "The exact saved Claude Desktop task has no live native Code endpoint. Background delivery requires that endpoint; this bridge has no supported silent cold-start API. No app or chat was opened. Inspect claude_bridge_status and report this diagnostic; do not create or open a chat to load the relay."), { taskId: task.taskId, phase: "recipient_discovery" });
|
|
55
48
|
}
|
|
56
49
|
|
|
57
50
|
export function sameClaudeDesktopRecipient(selected, current) {
|
|
@@ -133,7 +126,7 @@ export function resolveClaudeDesktopSession({ target, expectedCwd, expectedTaskI
|
|
|
133
126
|
throw preflightFailure("CLAUDE_SESSION_AMBIGUOUS", "Multiple live Claude Desktop sessions match the current account and intended project/task; inspect the intended task and provide its exact identity.");
|
|
134
127
|
}
|
|
135
128
|
if (!candidates.length) {
|
|
136
|
-
throw preflightFailure("CLAUDE_SESSION_NOT_FOUND", "No live Claude Desktop session matches the current account and intended project/task.
|
|
129
|
+
throw preflightFailure("CLAUDE_SESSION_NOT_FOUND", "No live Claude Desktop session matches the current account and intended project/task. Inspect list_claude_sessions and claude_bridge_status; do not open or create a chat to load the relay.");
|
|
137
130
|
}
|
|
138
131
|
return candidates[0];
|
|
139
132
|
}
|
package/src/index.mjs
CHANGED
|
@@ -38,10 +38,13 @@ 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
40
|
import { createRelayWake } from "./relay-wake.mjs";
|
|
41
|
+
import { withBridgeDiagnostic } from "./recipient-preflight.mjs";
|
|
42
|
+
import { BACKGROUND_TOOLS } from "./background-relay-config.mjs";
|
|
43
|
+
import { BACKGROUND_WORKER_GUIDANCE, registerBackgroundTools } from "./background-relay-mcp.mjs";
|
|
41
44
|
|
|
42
45
|
exitForVersionRequest(import.meta.url);
|
|
43
46
|
|
|
44
|
-
const VERSION = "1.
|
|
47
|
+
const VERSION = "1.26.0";
|
|
45
48
|
void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
|
|
46
49
|
const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
|
|
47
50
|
|
|
@@ -52,9 +55,7 @@ const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
|
|
|
52
55
|
*/
|
|
53
56
|
const DEFAULT_MODEL = process.env.CODEX_BRIDGE_MODEL || null;
|
|
54
57
|
const DEFAULT_EFFORT = process.env.CODEX_BRIDGE_EFFORT || null;
|
|
55
|
-
const DEFAULT_OPEN_IN_APP = process.env.CODEX_BRIDGE_OPEN_IN_APP
|
|
56
|
-
? process.env.CODEX_BRIDGE_OPEN_IN_APP === "1"
|
|
57
|
-
: IS_WINDOWS;
|
|
58
|
+
const DEFAULT_OPEN_IN_APP = process.env.CODEX_BRIDGE_OPEN_IN_APP === "1";
|
|
58
59
|
const DEFAULT_RELEASE_AFTER_TURN = process.env.CODEX_BRIDGE_RELEASE_AFTER_TURN
|
|
59
60
|
? process.env.CODEX_BRIDGE_RELEASE_AFTER_TURN === "1"
|
|
60
61
|
: IS_WINDOWS;
|
|
@@ -155,15 +156,18 @@ const textResult = (text, isError = false) => ({
|
|
|
155
156
|
...(isError ? { isError: true } : {}),
|
|
156
157
|
});
|
|
157
158
|
|
|
158
|
-
const failure = (err) => {
|
|
159
|
+
const failure = (err, options = {}) => {
|
|
159
160
|
const result = textResult(`Codex bridge error: ${err?.message ?? String(err)}`, true);
|
|
160
|
-
|
|
161
|
+
const seen = new Set();
|
|
162
|
+
for (let cause = err; cause && !seen.has(cause) && seen.size < 8; cause = cause.cause) {
|
|
163
|
+
seen.add(cause);
|
|
161
164
|
if (["RESPONSE_TOO_LARGE", "INVALID_RESPONSE_PREFERENCES", "INVALID_CURSOR", "CURSOR_EXPIRED"].includes(cause.code)) {
|
|
162
165
|
result.structuredContent = { error: Object.fromEntries(["code", "boundary", "bytes", "limit"].filter((key) => cause[key] !== undefined).map((key) => [key, cause[key]])) };
|
|
163
166
|
break;
|
|
164
167
|
}
|
|
165
168
|
}
|
|
166
|
-
return result
|
|
169
|
+
return withBridgeDiagnostic(result, err, { component: "codex-bridge", operation: desktopOperation.getStore()?.name,
|
|
170
|
+
...(desktopOperation.getStore() ? { deliveryState: desktopOperation.getStore().dispatched ? "unknown" : "not_sent", readOnly: desktopOperation.getStore().readOnly ?? desktopOperation.getStore().diagnostic } : {}), ...options });
|
|
167
171
|
};
|
|
168
172
|
|
|
169
173
|
function replyResult(delivered, deliveryId, result, notes = []) {
|
|
@@ -173,10 +177,14 @@ function replyResult(delivered, deliveryId, result, notes = []) {
|
|
|
173
177
|
const observationUnavailable = result.status === "observation_unavailable";
|
|
174
178
|
if (observationUnavailable) notes = [...notes, OBSERVATION_UNAVAILABLE_GUIDANCE];
|
|
175
179
|
const continuation = deliveryId ? `deliveryId: ${deliveryId}\n${pending ? "Continue automatically with wait_codex_reply and this deliveryId; it only observes the original send. Do not resend the prompt." : "Keep this deliveryId to inspect the same send after reconnecting."}` : "";
|
|
176
|
-
|
|
180
|
+
const output = {
|
|
177
181
|
...textResult([...notes, `deliveryStatus: ${deliveryStatus}`, formatTurn(result, { desktop: true }), continuation].filter(Boolean).join("\n"), result.status === "failed" || result.status === "systemError"),
|
|
178
182
|
structuredContent: { ...result, threadId: delivered.threadId, deliveryStatus, ...(deliveryId ? { deliveryId } : {}), nextAction: observationUnavailable ? "inspect_existing_task" : pending && deliveryId ? "wait_codex_reply" : "none" },
|
|
179
183
|
};
|
|
184
|
+
return output.isError ? withBridgeDiagnostic(output, new Error("The native Codex turn failed. Inspect the original delivery receipt and local tool output for details."), {
|
|
185
|
+
component: "codex-bridge", operation: desktopOperation.getStore()?.name, code: result.status === "failed" ? "CODEX_TURN_FAILED" : "CODEX_TURN_SYSTEM_ERROR",
|
|
186
|
+
deliveryState: deliveryStatus === "accepted" ? "sent" : "unknown", deliveryId, threadId: delivered.threadId,
|
|
187
|
+
}) : output;
|
|
180
188
|
}
|
|
181
189
|
|
|
182
190
|
/**
|
|
@@ -245,7 +253,7 @@ function creationVerificationFailure({ created, deliveryId, requestId, err }) {
|
|
|
245
253
|
threadId ? "Task was accepted; do not resend. Resolve the verification error, then continue observing with wait_codex_reply and this deliveryId." : "Do not resend or create again. Continue observing with wait_codex_reply and this deliveryId.");
|
|
246
254
|
return {
|
|
247
255
|
...textResult(lines.join("\n"), true),
|
|
248
|
-
structuredContent: { ...failure(err).structuredContent, threadId, deliveryStatus: threadId ? "accepted" : "unconfirmed", ...(deliveryId ? { deliveryId } : {}), nextAction: deliveryId ? "wait_codex_reply" : "none" },
|
|
256
|
+
structuredContent: { ...failure(err, { deliveryState: threadId ? "sent" : "unknown", readOnly: false, requestId, deliveryId, threadId }).structuredContent, threadId, deliveryStatus: threadId ? "accepted" : "unconfirmed", ...(deliveryId ? { deliveryId } : {}), nextAction: deliveryId ? "wait_codex_reply" : "none" },
|
|
249
257
|
};
|
|
250
258
|
}
|
|
251
259
|
|
|
@@ -449,21 +457,23 @@ const server = new McpServer(
|
|
|
449
457
|
"In legacy app-server mode, use delegate_to_codex with the initial prompt and omit requestId; durable creation deduplication is unavailable there. " +
|
|
450
458
|
"Reuse that requestId only for retries of that creation. Continue unfinished work, fixes, clarifications, and results in its verified original threadId with send_to_codex_thread. " +
|
|
451
459
|
"A completed turn alone does not mean the task is finished. Do not choose an old task merely because its project matches or it was recently active. " +
|
|
452
|
-
"Without authorization to create a new conversation, ask before creating it. With Desktop tasks enabled it assigns the exact saved project
|
|
453
|
-
"
|
|
454
|
-
"
|
|
460
|
+
"Without authorization to create a new conversation, ask before creating it. With Desktop tasks enabled it assigns the exact saved project using Desktop permissions. Normal delivery does not navigate or focus a window. " +
|
|
461
|
+
"Never use Computer Use or open an old, new, empty or failed chat to load a relay. Report the Bridge diagnostic block when the native endpoint is unavailable. " +
|
|
462
|
+
"Desktop creation and sends always require the verified calling Claude task and the Codex destination to belong to the same project, even when cwd is omitted. " +
|
|
455
463
|
"Use send_to_codex_thread only when an existing threadId is intentional; use " +
|
|
456
464
|
"list_codex_threads or read_codex_thread to inspect sessions and codex_bridge_status to inspect wiring. " +
|
|
457
465
|
"When a Desktop send returns nextAction=wait_codex_reply, continue observing with that tool and the returned deliveryId until completion, user input is required, or the user stops the task. Never resend to obtain a reply. Each observation is bounded, and the receipt survives reconnection. " +
|
|
458
|
-
AGENT_PROMPT_GUIDANCE,
|
|
466
|
+
BACKGROUND_WORKER_GUIDANCE + AGENT_PROMPT_GUIDANCE,
|
|
459
467
|
},
|
|
460
468
|
);
|
|
461
469
|
|
|
462
470
|
function registerTool(name, definition, handler) {
|
|
463
471
|
server.registerTool(name, definition, async (...args) => {
|
|
472
|
+
let result;
|
|
464
473
|
try {
|
|
465
|
-
|
|
474
|
+
result = await reload.run(async () => {
|
|
466
475
|
if (!definition.annotations?.readOnlyHint) runtime.assertCurrent();
|
|
476
|
+
if (BACKGROUND_TOOLS.includes(name)) return handler(...args);
|
|
467
477
|
if (name === "codex_bridge_status") {
|
|
468
478
|
const state = runtime.status();
|
|
469
479
|
if (!state.current) return { ...failure(new Error(`${state.reason}; reconnect this MCP server in the existing task.`)), structuredContent: { runtime: state } };
|
|
@@ -482,8 +492,8 @@ function registerTool(name, definition, handler) {
|
|
|
482
492
|
const accounts = readBridgeAccounts();
|
|
483
493
|
const identity = requireBridgeAccounts(accounts);
|
|
484
494
|
const caller = requireClaudeSenderContext(await readClaudeSenderContext({ account: accounts.claude }));
|
|
485
|
-
const context = { accounts: identity, caller, dispatched: false, deadline, projectSetup: PROJECT_SETUP_TOOLS.has(name) };
|
|
486
495
|
const readOnly = definition.annotations?.readOnlyHint === true;
|
|
496
|
+
const context = { name, accounts: identity, caller, dispatched: false, deadline, readOnly, projectSetup: PROJECT_SETUP_TOOLS.has(name) };
|
|
487
497
|
const guidance = deliveryReadGuidance(name, args[0]);
|
|
488
498
|
return await desktopOperation.run(context, async () => {
|
|
489
499
|
let completed = false;
|
|
@@ -505,11 +515,18 @@ function registerTool(name, definition, handler) {
|
|
|
505
515
|
return await handler(...args);
|
|
506
516
|
});
|
|
507
517
|
} catch (err) {
|
|
508
|
-
|
|
518
|
+
result = failure(err);
|
|
509
519
|
}
|
|
520
|
+
return result.isError ? withBridgeDiagnostic(result, result.structuredContent?.error ?? new Error("The Codex bridge operation failed. Inspect the local tool result for details."), {
|
|
521
|
+
component: "codex-bridge", operation: name, readOnly: definition.annotations?.readOnlyHint === true,
|
|
522
|
+
requestId: args[0]?.requestId, deliveryId: result.structuredContent?.deliveryId ?? args[0]?.deliveryId,
|
|
523
|
+
threadId: result.structuredContent?.threadId ?? args[0]?.threadId,
|
|
524
|
+
}) : result;
|
|
510
525
|
});
|
|
511
526
|
}
|
|
512
527
|
|
|
528
|
+
registerBackgroundTools(registerTool);
|
|
529
|
+
|
|
513
530
|
registerDesktopReadTools(registerTool, {
|
|
514
531
|
desktopTasksEnabled, desktopTasks, replyReceipts, security, client,
|
|
515
532
|
context: () => desktopOperation.getStore(), assertOperation: assertDesktopOperation,
|
|
@@ -550,7 +567,7 @@ registerTool(
|
|
|
550
567
|
openInApp: z
|
|
551
568
|
.boolean()
|
|
552
569
|
.optional()
|
|
553
|
-
.describe("
|
|
570
|
+
.describe("Explicitly show the task in Codex Desktop while it runs. Default is false unless CODEX_BRIDGE_OPEN_IN_APP=1; normal delivery does not navigate or focus a window."),
|
|
554
571
|
releaseAfterTurn: z
|
|
555
572
|
.boolean()
|
|
556
573
|
.optional()
|
|
@@ -648,7 +665,7 @@ registerTool(
|
|
|
648
665
|
openInApp: z
|
|
649
666
|
.boolean()
|
|
650
667
|
.optional()
|
|
651
|
-
.describe("
|
|
668
|
+
.describe("Explicitly open the thread in Codex Desktop so a human can watch it live. Default is false unless CODEX_BRIDGE_OPEN_IN_APP=1; normal delivery does not navigate or focus a window."),
|
|
652
669
|
releaseAfterTurn: z
|
|
653
670
|
.boolean()
|
|
654
671
|
.optional()
|
|
@@ -1025,12 +1042,12 @@ registerTool(
|
|
|
1025
1042
|
if (desktopTasksEnabled) {
|
|
1026
1043
|
const native = await desktopTasks.status();
|
|
1027
1044
|
const sender = await readClaudeSenderContext({ account: readBridgeAccounts().claude });
|
|
1028
|
-
const readiness = bridgeReadiness({ sender, scope: security.hardenedRoots.mode === "project-policy" ? security.hardenedRoots : { assert: (candidate) => { security.assertCwd(candidate); return candidate; } }, targetCwd: cwd, nativeAvailable: native.available, verifyTargetProject: captureProjectScope });
|
|
1045
|
+
const readiness = bridgeReadiness({ sender, scope: security.hardenedRoots.mode === "project-policy" ? security.hardenedRoots : { assert: (candidate) => { security.assertCwd(candidate); return candidate; } }, targetCwd: cwd, nativeAvailable: native.available, nativeDiagnostic: native.diagnostic, verifyTargetProject: captureProjectScope });
|
|
1029
1046
|
return { ...textResult([
|
|
1030
1047
|
`platform: ${PLATFORM_LABEL} (${process.platform}/${process.arch})`,
|
|
1031
1048
|
`bridge version: ${VERSION}`,
|
|
1032
1049
|
`node: ${process.version} at ${process.execPath}`,
|
|
1033
|
-
"desktop tasks: enabled; Desktop permissions, exact saved project,
|
|
1050
|
+
"desktop tasks: enabled; Desktop permissions, exact saved project, silent delivery",
|
|
1034
1051
|
"project scope: verified Claude sender; same directory or Git repository, checked before each mutation",
|
|
1035
1052
|
`native relay: ${native.available ? "available; verified through Codex Desktop" : "unavailable"}`,
|
|
1036
1053
|
`native endpoint: ${native.socketPath}`,
|
|
@@ -1043,7 +1060,7 @@ registerTool(
|
|
|
1043
1060
|
...(readiness.projectPolicy ? [`project policy: ${readiness.projectPolicy.file}; revision ${readiness.projectPolicy.revision ?? "unavailable"}; changes apply on the next operation`] : []),
|
|
1044
1061
|
...(readiness.target ? [`target: ${readiness.target.cwd}; authorized=${readiness.target.authorized}${readiness.target.authorizedBy ? `; via ${readiness.target.authorizedBy.kind} ${readiness.target.authorizedBy.path}` : ""}`] : []),
|
|
1045
1062
|
...readiness.issues.map((issue) => `${issue.code}: ${issue.detail ?? ""} ${issue.action}`),
|
|
1046
|
-
].join("\n"), !readiness.ready), structuredContent: { readiness, projectScope: { enabled: true, policy: "verified-sender-same-project", match: "canonical-directory-or-git-repository", checkedBeforeMutation: true } } };
|
|
1063
|
+
].join("\n"), !readiness.ready), structuredContent: { readiness, native, ...(native.diagnostic ? { diagnostic: native.diagnostic } : !readiness.ready ? { error: { code: readiness.issues[0]?.code, message: readiness.issues[0]?.detail } } : {}), projectScope: { enabled: true, policy: "verified-sender-same-project", match: "canonical-directory-or-git-repository", checkedBeforeMutation: true } } };
|
|
1047
1064
|
}
|
|
1048
1065
|
const up = await client.isServerUp();
|
|
1049
1066
|
let liveThreads = null;
|
|
@@ -1106,4 +1123,4 @@ const shutdown = () => shutdownPromise ??= (async () => {
|
|
|
1106
1123
|
process.stdin.once("end", () => { void shutdown().catch((error) => log(`shutdown failed: ${error.message}`)); });
|
|
1107
1124
|
process.once("disconnect", () => { void shutdown().catch((error) => log(`shutdown failed: ${error.message}`)); });
|
|
1108
1125
|
reload.listen();
|
|
1109
|
-
log(desktopTasksEnabled ? `
|
|
1126
|
+
log(desktopTasksEnabled ? `MCP initialized on ${PLATFORM_LABEL}; native readiness is checked per operation (external app-server disabled)` : `MCP initialized on ${PLATFORM_LABEL} (app-server endpoint: ${client.url}, codex: ${client.codexBin})`);
|
package/src/mcp-supervisor.mjs
CHANGED
|
@@ -7,16 +7,21 @@ 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
|
-
const ENTRIES = new Set(["index.mjs", "claude-bridge.mjs", "native-relay-companion.mjs"]);
|
|
13
|
+
const ENTRIES = new Set(["index.mjs", "claude-bridge.mjs", "native-relay-companion.mjs", "antigravity-bridge.mjs", "antigravity-outbound-bridge.mjs"]);
|
|
14
|
+
const ANTIGRAVITY_ENTRIES = new Set(["antigravity-bridge.mjs", "antigravity-outbound-bridge.mjs"]);
|
|
13
15
|
const MAX_FRAME_BYTES = 16 * 1024 * 1024;
|
|
14
16
|
const MAX_QUEUE = 256;
|
|
15
17
|
const RETIRE_GRACE_MS = 6000;
|
|
16
18
|
const CLIENT_CLOSE_GRACE_MS = CLIENT_CLOSE_DRAIN_MS + CLIENT_CLOSE_STOP_MS + 1000;
|
|
17
19
|
const hasId = (message) => Object.hasOwn(message, "id");
|
|
18
20
|
const idKey = (id) => JSON.stringify(id);
|
|
19
|
-
const errorMessage = (id, message
|
|
21
|
+
const errorMessage = (id, message, options = {}) => {
|
|
22
|
+
const result = withBridgeDiagnostic({ content: [] }, new Error(message), { component: "mcp-supervisor", code: "MCP_WORKER_UNAVAILABLE", ...options });
|
|
23
|
+
return { jsonrpc: "2.0", id, error: { code: -32603, message: `${message}\n${result.content[0].text}`, data: { diagnostic: result.structuredContent.diagnostic } } };
|
|
24
|
+
};
|
|
20
25
|
|
|
21
26
|
export function supervisorInterval(value, fallback) {
|
|
22
27
|
const parsed = Number(value);
|
|
@@ -147,7 +152,7 @@ class Worker {
|
|
|
147
152
|
}
|
|
148
153
|
|
|
149
154
|
export async function runSupervisor(entry, options = {}) {
|
|
150
|
-
if (!ENTRIES.has(entry)) throw new Error(
|
|
155
|
+
if (!ENTRIES.has(entry)) throw new Error(`Expected one of: ${[...ENTRIES].join(", ")}`);
|
|
151
156
|
const installRoot = stableInstallPath(fs.realpathSync.native(options.root ?? path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..")));
|
|
152
157
|
const currentRoot = () => fs.realpathSync.native(installRoot);
|
|
153
158
|
const root = currentRoot();
|
|
@@ -158,7 +163,8 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
158
163
|
const log = (message) => process.stderr.write(`[bridge-supervisor] ${message}\n`);
|
|
159
164
|
const output = (message) => process.stdout.write(`${JSON.stringify(message)}\n`);
|
|
160
165
|
const env = { ...process.env, CODEX_BRIDGE_SOURCE_ROOT: installRoot };
|
|
161
|
-
const
|
|
166
|
+
const recoverable = entry === "native-relay-companion.mjs" || ANTIGRAVITY_ENTRIES.has(entry);
|
|
167
|
+
const routingConfiguration = () => recoverable ? null : desktopTasksConfigured(env);
|
|
162
168
|
if (entry === "native-relay-companion.mjs" && !env.CODEX_APP_TOOLS_PIPE_PATH) {
|
|
163
169
|
const { resolveNativeToolsPipePath } = await import("./native-relay.mjs");
|
|
164
170
|
const pipe = await resolveNativeToolsPipePath();
|
|
@@ -188,7 +194,7 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
188
194
|
let activeSince = Date.now();
|
|
189
195
|
const recoveryDelayMs = Math.min(30000, supervisorInterval(env.CODEX_BRIDGE_RECOVERY_DELAY_MS, 1000));
|
|
190
196
|
const recoveryLimit = 5;
|
|
191
|
-
const canRecover = () =>
|
|
197
|
+
const canRecover = () => recoverable && ready && initializeResult && initializeCatalog;
|
|
192
198
|
const scheduleRecovery = () => {
|
|
193
199
|
if (recoveryAttempts >= recoveryLimit) return;
|
|
194
200
|
recoveryAt = Date.now() + Math.min(30000, recoveryDelayMs * 2 ** recoveryAttempts);
|
|
@@ -222,25 +228,25 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
222
228
|
const respond = async () => {
|
|
223
229
|
if (pending.method === "initialize" && !message.error) {
|
|
224
230
|
initializeResult = message.result;
|
|
225
|
-
if (
|
|
231
|
+
if (recoverable) {
|
|
226
232
|
initializeCatalog = await worker.rpc("tools/list", {});
|
|
227
|
-
if (!Array.isArray(initializeCatalog.tools) || !initializeCatalog.tools.length || initializeCatalog.nextCursor) throw new Error("The
|
|
233
|
+
if (!Array.isArray(initializeCatalog.tools) || !initializeCatalog.tools.length || initializeCatalog.nextCursor) throw new Error("The bridge tool catalog is incomplete");
|
|
228
234
|
}
|
|
229
235
|
await worker.control("activate");
|
|
230
236
|
ready = true;
|
|
231
237
|
}
|
|
232
|
-
if (message.result && ["codex_bridge_status", "claude_bridge_status", "native_relay_status"].includes(pending.tool)) {
|
|
238
|
+
if (message.result && ["codex_bridge_status", "claude_bridge_status", "native_relay_status", "antigravity_bridge_status", "agent_bridge_status"].includes(pending.tool)) {
|
|
233
239
|
message.result.structuredContent = { ...message.result.structuredContent, autoReload: diagnostics() };
|
|
234
240
|
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}` : ""}` }];
|
|
235
241
|
}
|
|
236
242
|
output({ ...message, id: pending.id });
|
|
237
243
|
};
|
|
238
|
-
void respond().catch((error) => { failure = error.message; output(errorMessage(pending.id, error.message)); });
|
|
244
|
+
void respond().catch((error) => { failure = error.message; output(errorMessage(pending.id, error.message, { code: "MCP_RESPONSE_FAILED", operation: pending.tool ?? pending.method, deliveryState: "unknown" })); });
|
|
239
245
|
},
|
|
240
246
|
exit(worker, error) {
|
|
241
247
|
for (const [id, pending] of worker.clientPending) {
|
|
242
248
|
requests.delete(idKey(pending.id));
|
|
243
|
-
output(errorMessage(pending.id, `${error.message}. The operation may already have been dispatched; it was not retried. Retain the original task and receipts
|
|
249
|
+
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" }));
|
|
244
250
|
worker.clientPending.delete(id);
|
|
245
251
|
}
|
|
246
252
|
for (const [id, pending] of reverse) if (pending.worker === worker) reverse.delete(id);
|
|
@@ -289,19 +295,23 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
289
295
|
}
|
|
290
296
|
candidate.write({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
291
297
|
const catalog = await candidate.rpc("tools/list", {});
|
|
292
|
-
if (
|
|
298
|
+
if (!Array.isArray(catalog.tools) || !catalog.tools.length || catalog.nextCursor) throw new Error("The replacement tool catalog is incomplete");
|
|
299
|
+
const catalogChanged = JSON.stringify(catalog) !== JSON.stringify(initializeCatalog);
|
|
300
|
+
if (catalogChanged && (!ANTIGRAVITY_ENTRIES.has(entry) || initializeResult.capabilities?.tools?.listChanged !== true)) throw new Error("The replacement tool catalog requires an explicit client reconnect");
|
|
293
301
|
await candidate.control("activate");
|
|
294
302
|
if (stopping || candidate.closed) throw new Error("The relay replacement stopped before activation completed");
|
|
295
303
|
active = candidate;
|
|
296
304
|
candidate = null;
|
|
305
|
+
initializeCatalog = catalog;
|
|
297
306
|
activeSince = Date.now();
|
|
298
307
|
failure = null;
|
|
299
308
|
reason = null;
|
|
300
309
|
recoveries++;
|
|
301
|
-
|
|
310
|
+
if (catalogChanged) output({ jsonrpc: "2.0", method: "notifications/tools/list_changed" });
|
|
311
|
+
log(`recovered ${entry} worker after exit; recovery ${recoveries}, no requests replayed`);
|
|
302
312
|
schedulePrune();
|
|
303
313
|
} catch (error) {
|
|
304
|
-
failure = `
|
|
314
|
+
failure = `Bridge worker recovery failed: ${error.message}; no requests were replayed`;
|
|
305
315
|
if (candidate) {
|
|
306
316
|
try {
|
|
307
317
|
await candidate.stop();
|
|
@@ -398,16 +408,16 @@ export async function runSupervisor(entry, options = {}) {
|
|
|
398
408
|
}
|
|
399
409
|
return;
|
|
400
410
|
}
|
|
401
|
-
if (failure) { if (hasId(message)) output(errorMessage(message.id, `${failure}${canRecover() && recoveryAttempts < recoveryLimit ? ";
|
|
411
|
+
if (failure) { if (hasId(message)) output(errorMessage(message.id, `${failure}${canRecover() && recoveryAttempts < recoveryLimit ? "; bridge worker recovery is pending, this request was not sent" : ""}`, { operation: message.params?.name ?? message.method, deliveryState: "not_sent" })); return; }
|
|
402
412
|
if (switching || !active) {
|
|
403
413
|
const bytes = queue.reduce((size, queued) => size + Buffer.byteLength(JSON.stringify(queued)), 0);
|
|
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; }
|
|
414
|
+
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; }
|
|
405
415
|
queue.push(message);
|
|
406
416
|
return;
|
|
407
417
|
}
|
|
408
418
|
if (message.method === "initialize") initializeParams = message.params;
|
|
409
419
|
if (hasId(message)) {
|
|
410
|
-
if (requests.has(idKey(message.id))) { output(errorMessage(message.id, "Duplicate active JSON-RPC request ID")); return; }
|
|
420
|
+
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; }
|
|
411
421
|
const id = `client:${++sequence}`;
|
|
412
422
|
active.clientPending.set(id, { id: message.id, method: message.method, tool: message.params?.name });
|
|
413
423
|
requests.set(idKey(message.id), { worker: active, workerId: id });
|
|
@@ -43,7 +43,7 @@ import { remoteCodexSocketPath, remoteNativeAdapter, remoteRequestResponse } fro
|
|
|
43
43
|
|
|
44
44
|
exitForVersionRequest(import.meta.url);
|
|
45
45
|
|
|
46
|
-
const VERSION = "1.
|
|
46
|
+
const VERSION = "1.26.0";
|
|
47
47
|
const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
|
|
48
48
|
|
|
49
49
|
function errorResponse(code, message, sent, details) {
|
|
@@ -345,6 +345,9 @@ async function handleDesktopRequest(payload, { dispatchDesktop, resolveExecutor,
|
|
|
345
345
|
if (payload.operation === "send_message_to_thread" && payload.arguments.threadId === executorThreadId) {
|
|
346
346
|
return errorResponse("RELAY_BAD_REQUEST", "The relay executor cannot receive its own relayed message");
|
|
347
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
|
+
}
|
|
348
351
|
// Reserve before dispatch. A disconnected caller must not discard a late
|
|
349
352
|
// result, and even identical duplicate writes must not dispatch twice.
|
|
350
353
|
const creation = payload.creationReceiptId ? creationReceipts.reserve(payload.creationReceiptId, payload.arguments, accountContext, executorThreadId) : null;
|
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",
|
|
@@ -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,
|
|
@@ -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
|
}
|
package/src/reload-control.mjs
CHANGED
|
@@ -26,7 +26,7 @@ export async function drainPendingAccepts(turns = 3) {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export function createReloadControl({ entry, inspect = () => null, quiesce = () => {}, exportState = () => ({}), restore = () => {}, activate = () => {}, resume = activate, env = process.env, channel = process } = {}) {
|
|
29
|
-
if (!["index.mjs", "claude-bridge.mjs", "native-relay-companion.mjs"].includes(entry)) throw new Error("Invalid reload entry point");
|
|
29
|
+
if (!["index.mjs", "claude-bridge.mjs", "native-relay-companion.mjs", "antigravity-bridge.mjs", "antigravity-outbound-bridge.mjs"].includes(entry)) throw new Error("Invalid reload entry point");
|
|
30
30
|
const enabled = env.CODEX_BRIDGE_WORKER === "1" && typeof channel.send === "function";
|
|
31
31
|
let phase = enabled && env.CODEX_BRIDGE_STAGED === "1" ? "staged" : "active";
|
|
32
32
|
let active = 0;
|
|
@@ -83,7 +83,7 @@ export function createReloadControl({ entry, inspect = () => null, quiesce = ()
|
|
|
83
83
|
inspect: status,
|
|
84
84
|
defer(reason) { deferred ??= String(reason); },
|
|
85
85
|
async run(handler) {
|
|
86
|
-
if (phase !== "active") throw new Error(`Bridge worker is ${phase}; no tool operation was started`);
|
|
86
|
+
if (phase !== "active") throw Object.assign(new Error(`Bridge worker is ${phase}; no tool operation was started`), { code: "MCP_WORKER_QUIESCED", phase: "lifecycle", deliveryState: "not_sent", sent: false });
|
|
87
87
|
active += 1;
|
|
88
88
|
try { return await handler(); }
|
|
89
89
|
finally { active -= 1; }
|