@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.
Files changed (50) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/CHANGELOG.md +58 -0
  3. package/README.md +81 -32
  4. package/docs/new-macos-setup.md +47 -21
  5. package/docs/new-windows-setup.md +34 -9
  6. package/docs/project-onboarding.md +21 -8
  7. package/package.json +2 -1
  8. package/plugin.json +1 -1
  9. package/scripts/background-relay-hidden.vbs +11 -0
  10. package/scripts/install-antigravity-bridge.mjs +65 -23
  11. package/scripts/install-background-relay.mjs +428 -0
  12. package/scripts/install-claude-code.mjs +2 -2
  13. package/scripts/install-claude-desktop.mjs +12 -7
  14. package/scripts/install-native-relay.mjs +2 -2
  15. package/scripts/setup-background-relay.mjs +58 -0
  16. package/scripts/setup.mjs +3 -3
  17. package/scripts/worker-relay.mjs +41 -0
  18. package/src/antigravity-bridge.mjs +96 -27
  19. package/src/antigravity-cli.mjs +13 -2
  20. package/src/antigravity-deliveries.mjs +32 -8
  21. package/src/antigravity-ipc.mjs +18 -9
  22. package/src/antigravity-outbound-bridge.mjs +71 -23
  23. package/src/antigravity-outbound-targets.mjs +100 -10
  24. package/src/background-antigravity-worker.mjs +192 -0
  25. package/src/background-claude-worker.mjs +240 -0
  26. package/src/background-codex-worker.mjs +344 -0
  27. package/src/background-relay-client.mjs +107 -0
  28. package/src/background-relay-config.mjs +98 -0
  29. package/src/background-relay-daemon.mjs +60 -0
  30. package/src/background-relay-mcp.mjs +45 -0
  31. package/src/background-relay-protocol.mjs +54 -0
  32. package/src/background-relay-registration.mjs +198 -0
  33. package/src/background-relay-service.mjs +311 -0
  34. package/src/background-relay-storage.mjs +203 -0
  35. package/src/background-relay-storage.ps1 +64 -0
  36. package/src/background-relay.mjs +373 -0
  37. package/src/bridge-readiness.mjs +2 -2
  38. package/src/claude-bridge.mjs +43 -25
  39. package/src/claude-code-registration.mjs +2 -1
  40. package/src/claude-session-router.mjs +8 -15
  41. package/src/index.mjs +39 -22
  42. package/src/mcp-supervisor.mjs +26 -16
  43. package/src/native-relay-companion.mjs +4 -1
  44. package/src/native-relay.mjs +5 -1
  45. package/src/peer-protocol.mjs +9 -6
  46. package/src/recipient-preflight.mjs +81 -0
  47. package/src/reload-control.mjs +2 -2
  48. package/src/setup.mjs +62 -25
  49. package/src/thread-delivery.mjs +10 -5
  50. 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, open, beforeOpen, sleep = delay, timeoutMs = 15000 }) {
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 beforeOpen();
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
- try { await open(task.taskId); }
45
- catch { return { recovery: { status: "launch_uncertain", sent: false, taskId: task.taskId, cwd: args.expectedCwd, reason: "The exact Desktop task could not be confirmed open. Inspect this task before retrying; no message was sent." } }; }
46
- const deadline = Date.now() + timeoutMs;
47
- for (;;) {
48
- await assertTask();
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. Open or reconnect the existing task in Claude Desktop.");
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.25.2";
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
- for (let cause = err; cause; cause = cause.cause) {
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
- return {
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 and starts visibly in " +
453
- "Codex Desktop using Desktop permissions. Otherwise it releases the bridge writer lock and opens the exact thread in " +
454
- "Codex Desktop. 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. " +
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
- return await reload.run(async () => {
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
- return failure(err);
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("Show the task in Codex Desktop; native tasks open immediately while running"),
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("Open the thread in Codex Desktop on Windows or macOS so a human can watch it live"),
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, immediate visibility",
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 ? `ready on ${PLATFORM_LABEL} (Codex Desktop only; external app-server disabled)` : `ready on ${PLATFORM_LABEL} (app-server endpoint: ${client.url}, codex: ${client.codexBin})`);
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})`);
@@ -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) => ({ jsonrpc: "2.0", id, error: { code: -32603, 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("Expected index.mjs, claude-bridge.mjs, or native-relay-companion.mjs");
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 routingConfiguration = () => entry === "native-relay-companion.mjs" ? null : desktopTasksConfigured(env);
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 = () => entry === "native-relay-companion.mjs" && ready && initializeResult && initializeCatalog;
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 (entry === "native-relay-companion.mjs") {
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 native relay tool catalog is incomplete");
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 (JSON.stringify(catalog) !== JSON.stringify(initializeCatalog)) throw new Error("The replacement tool catalog requires an explicit client reconnect");
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
- log(`recovered native relay worker after exit; recovery ${recoveries}, no requests replayed`);
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 = `Native relay recovery failed: ${error.message}; no requests were replayed`;
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 ? "; native relay recovery is pending, this request was not sent" : ""}`)); return; }
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.25.2";
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;
@@ -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
- reason: `no companion socket at ${socketPath}. Install the native relay if it is missing. Codex Desktop runs codex-native-relay only while a task has its MCP servers loaded: open any regular project task (not the internal Native Relay executor task) and send it a turn or reconnect its MCP servers, then retry.`,
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 };
@@ -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"}. Open or reconnect an existing Code session in Claude Desktop for the intended project. Do not launch a replacement CLI session. No message was sent.`);
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; reopen the existing Desktop task and inspect its session again.";
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 the existing Desktop task before retrying.");
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
  }
@@ -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; }