@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
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+ import fs from "node:fs";
3
+ import { callBackgroundRelay } from "../src/background-relay-client.mjs";
4
+ import { withBridgeDiagnostic } from "../src/recipient-preflight.mjs";
5
+ import { relayFailure, RELAY_UUID } from "../src/background-relay-config.mjs";
6
+ import { exitForVersionRequest } from "../src/cli-version.mjs";
7
+
8
+ exitForVersionRequest(import.meta.url);
9
+ const args = process.argv.slice(2);
10
+ const action = args.shift() ?? "status";
11
+ const option = name => { const index = args.indexOf(name); return index < 0 ? undefined : args[index + 1]; };
12
+ try {
13
+ let result;
14
+ if (action === "status" || action === "list") result = await callBackgroundRelay(action);
15
+ else if (action === "read") result = await callBackgroundRelay("read", { requestId: option("--request-id"), waitSec: Number(option("--wait") ?? 0) }, { timeoutMs: (Number(option("--wait") ?? 0) + 10) * 1000 });
16
+ else if (action === "send") {
17
+ if (!args.includes("--stdin") || !RELAY_UUID.test(option("--request-id") ?? "")) throw relayFailure("WORKER_REQUEST_INVALID", "Use --stdin and a persistent --request-id UUID so uncertain deliveries can be inspected safely");
18
+ const chunks = [];
19
+ let bytes = 0;
20
+ for (;;) {
21
+ const buffer = Buffer.alloc(65536);
22
+ const count = fs.readSync(0, buffer, 0, buffer.length, null);
23
+ if (!count) break;
24
+ bytes += count;
25
+ if (bytes > 256 * 1024) throw relayFailure("WORKER_REQUEST_TOO_LARGE", "The authorized prompt exceeds the worker input limit");
26
+ chunks.push(buffer.subarray(0, count));
27
+ }
28
+ const prompt = Buffer.concat(chunks).toString("utf8");
29
+ result = await callBackgroundRelay("send", { agent: option("--agent"), cwd: option("--cwd"), requestId: option("--request-id"), prompt });
30
+ } else if (action === "--help" || action === "help") {
31
+ process.stdout.write("worker-relay status | list | send --agent <codex|claude|antigravity> --cwd <project> --request-id <uuid> --stdin | read --request-id <uuid> [--wait <0-60>]\n");
32
+ } else throw relayFailure("WORKER_COMMAND_INVALID", "Unknown worker relay command");
33
+ if (result) {
34
+ process.stdout.write(JSON.stringify(result, null, 2) + "\n");
35
+ if (result.status === "failed" || result.status === "unknown") process.exitCode = 1;
36
+ }
37
+ } catch (error) {
38
+ const result = withBridgeDiagnostic({ isError: true, content: [] }, error, { component: "worker-relay-cli", operation: action, requestId: option("--request-id") });
39
+ process.stderr.write(result.content.map(item => item.text ?? "").join("\n") + "\n");
40
+ process.exitCode = 1;
41
+ }
@@ -9,11 +9,13 @@ import { readClaudeSenderContext, requireClaudeSenderContext, stopProcessInspect
9
9
  import { readClaudeAccountContext } from "./desktop-account-context.mjs";
10
10
  import { createHardenedRootPolicy } from "./hardened-root-policy.mjs";
11
11
  import { antigravityBridgeDirectory, callAntigravityDesktop } from "./antigravity-ipc.mjs";
12
- import { AntigravityDeliveries } from "./antigravity-deliveries.mjs";
12
+ import { AntigravityDeliveries, antigravityDeliveryError } from "./antigravity-deliveries.mjs";
13
13
  import { AntigravityCliSession } from "./antigravity-cli.mjs";
14
14
  import { exitForVersionRequest } from "./cli-version.mjs";
15
15
  import { NativeDesktopRelay, desktopTaskSocketPath, accountRelaySocketPath } from "./native-relay.mjs";
16
16
  import { requireBridgeAccounts, readBridgeAccounts, assertAccountIdentity } from "./bridge-account-context.mjs";
17
+ import { bridgeDiagnostic, withBridgeDiagnostic, preflightFailure } from "./recipient-preflight.mjs";
18
+ import { createReloadControl, exitWhenClientCloses } from "./reload-control.mjs";
17
19
 
18
20
  exitForVersionRequest(import.meta.url);
19
21
 
@@ -27,8 +29,21 @@ const caller = process.env.ANTIGRAVITY_BRIDGE_CALLER;
27
29
  const senderRelay = new NativeDesktopRelay({ socketPath: desktopTaskSocketPath(), accountSocketPath: accountRelaySocketPath() });
28
30
  const result = (value) => ({ content: [{ type: "text", text: JSON.stringify(value, null, 2) }] });
29
31
  const schema = { requestId: z.string().uuid(), cwd: z.string().min(1), prompt: z.string().min(1).max(65536) };
32
+ const reloadReason = () => startingSessions ? "Antigravity CLI sessions are still starting"
33
+ : deliveries.active.size ? "Antigravity deliveries are still active"
34
+ : sessions.size ? "Open Antigravity CLI sessions retain process pipes and cannot migrate" : null;
35
+ const reload = createReloadControl({
36
+ entry: "antigravity-bridge.mjs",
37
+ inspect: reloadReason,
38
+ exportState: () => ({ caller: caller ?? null, directory }),
39
+ restore: (state) => {
40
+ if (state.caller !== (caller ?? null) || typeof state.directory !== "string" || path.relative(state.directory, directory)) {
41
+ throw new Error("Antigravity caller or durable receipt directory changed during reload");
42
+ }
43
+ },
44
+ });
30
45
 
31
- async function sender(extra) {
46
+ async function identifySender(extra) {
32
47
  if (caller === "codex") {
33
48
  const context = await resolveCodexSenderContext(extra?._meta, { inspectDesktopTask: async (threadId) => {
34
49
  const accounts = requireBridgeAccounts(readBridgeAccounts());
@@ -46,17 +61,34 @@ async function sender(extra) {
46
61
  throw new Error("Install this bridge for Codex or Claude; unknown callers cannot read conversations or send work");
47
62
  }
48
63
 
64
+ async function sender(extra) {
65
+ try { return await identifySender(extra); }
66
+ catch (error) {
67
+ error.code ??= "ANTIGRAVITY_SENDER_UNVERIFIED";
68
+ error.phase ??= "sender";
69
+ error.deliveryState = "not_sent";
70
+ throw error;
71
+ }
72
+ }
73
+
49
74
  function sameCwd(expected, actual) {
50
- if (!path.isAbsolute(expected) || !path.isAbsolute(actual)) throw new Error("Expected an absolute project directory");
51
- const left = fs.realpathSync.native(expected);
52
- const right = fs.realpathSync.native(actual);
53
- if (path.relative(left, right)) throw new Error("Antigravity destination must match the verified sender's project directory");
54
- rootPolicy.assert(left, "Antigravity project");
55
- return left;
75
+ try {
76
+ if (!path.isAbsolute(expected) || !path.isAbsolute(actual)) throw new Error("Expected an absolute project directory");
77
+ const left = fs.realpathSync.native(expected);
78
+ const right = fs.realpathSync.native(actual);
79
+ if (path.relative(left, right)) throw new Error("Antigravity destination must match the verified sender's project directory");
80
+ rootPolicy.assert(left, "Antigravity project");
81
+ return left;
82
+ } catch (error) {
83
+ error.code ??= "ANTIGRAVITY_PROJECT_SCOPE_INVALID";
84
+ error.phase ??= "scope";
85
+ error.deliveryState = "not_sent";
86
+ throw error;
87
+ }
56
88
  }
57
89
 
58
90
  function owns(record, identity) {
59
- if (!record || record.sender?.provider !== identity.provider || record.sender?.id !== identity.id) throw new Error("Delivery does not belong to this sending conversation");
91
+ if (!record || record.sender?.provider !== identity.provider || record.sender?.id !== identity.id) throw preflightFailure("ANTIGRAVITY_RECEIPT_OWNER_MISMATCH", "Delivery does not belong to this sending conversation");
60
92
  sameCwd(identity.cwd, record.cwd);
61
93
  return record;
62
94
  }
@@ -65,20 +97,49 @@ function handoff(prompt, identity) {
65
97
  return `[Agent handoff from ${identity.provider} conversation ${identity.id}. Native Antigravity permissions remain in effect.]\n\n${prompt}`;
66
98
  }
67
99
 
68
- const server = new McpServer({ name: "antigravity-bridge", version }, { instructions: "Send authorized work to Antigravity Desktop through its native sidecar or to an explicitly created CLI streaming session. Always select the requested surface; never substitute CLI for Desktop. The exact destination directory must match the verified sending conversation. Desktop delivery acceptance is not an agent reply. CLI sessions are bridge-owned headless sessions, not attached interactive terminals. Preserve requestId across retries and read_antigravity_delivery after pending or unknown outcomes; never resend automatically. Native tool approvals remain in force. Do not claim support for Antigravity-origin handoffs into Codex or Claude; this server receives from those hosts." });
100
+ const server = new McpServer({ name: "antigravity-bridge", version }, { instructions: "Send authorized work to Antigravity Desktop through its native sidecar or to an explicitly created CLI streaming session. Always select the requested surface; never substitute CLI for Desktop. The exact destination directory must match the verified sending conversation. Desktop delivery acceptance is not an agent reply. CLI sessions are bridge-owned headless sessions, not attached interactive terminals. Preserve requestId across retries and read_antigravity_delivery after pending or unknown outcomes; never resend automatically. Native tool approvals remain in force. Antigravity Desktop can send authorized handoffs back to existing Codex Desktop tasks and live Claude Desktop Code sessions in the same project through its separate agent-bridge MCP server. From Antigravity, verify agent_bridge_status, discover list_agent_tasks, then use send_to_agent_task and read_agent_delivery. This inbound server does not initiate those outbound calls, and its status does not verify the outbound server's readiness. Antigravity CLI outbound and automatic App reply forwarding are not supported. An unreachable sidecar does not prove that the Antigravity App is closed; report its diagnostic and inspect sidecar configuration. Never use Computer Use, open an old chat, or create an empty, new, or error chat to load or recover a relay." });
69
101
 
70
102
  function register(name, description, inputSchema, readOnly, handler) {
71
103
  server.registerTool(name, { description, inputSchema, annotations: { readOnlyHint: readOnly, destructiveHint: !readOnly, idempotentHint: true, openWorldHint: !readOnly } }, async (args, extra) => {
72
- try { return result(await handler(args, extra)); }
73
- catch (error) { return { ...result({ error: error.message, deliveryState: error.deliveryState ?? "not_sent" }), isError: true }; }
104
+ const options = { component: "antigravity-bridge", operation: name, requestId: args.requestId, conversationId: args.conversationId, readOnly };
105
+ try {
106
+ const value = await reload.run(() => handler(args, extra));
107
+ const failure = antigravityDeliveryError(value);
108
+ if (failure) return withBridgeDiagnostic({ ...result(value), isError: true }, failure, { ...options, readOnly: false, msgId: value.result?.msgId });
109
+ if (value?.desktop?.available === false) return withBridgeDiagnostic({ ...result(value), isError: true }, { diagnostic: value.desktop.diagnostic }, options);
110
+ return result(value);
111
+ } catch (error) {
112
+ const diagnostic = bridgeDiagnostic(error, options);
113
+ return withBridgeDiagnostic({ ...result({ error: diagnostic.reason, code: diagnostic.code, deliveryState: error.deliveryState ?? diagnostic.deliveryState }), isError: true }, { diagnostic }, options);
114
+ }
74
115
  });
75
116
  }
76
117
 
77
- register("antigravity_bridge_status", "Inspect connection capabilities without sending prompts.", {}, true, async () => {
118
+ register("antigravity_bridge_status", "Inspect native sidecar connectivity and supported inbound/outbound routes without sending prompts. Outbound readiness must be checked with agent_bridge_status inside Antigravity Desktop.", {}, true, async () => {
78
119
  let desktop;
79
120
  try { desktop = await callAntigravityDesktop({ operation: "status" }, { timeoutMs: 5000 }); }
80
- catch { desktop = { available: false, reason: "Open Antigravity with the installed codex-bridge sidecar enabled" }; }
81
- return { version, caller: caller ?? "unconfigured", desktop, cli: { transport: "stream-json", activeSessions: sessions.size, permissionPolicy: "native-unchanged", interactiveTerminalAttachment: false } };
121
+ catch (cause) {
122
+ const error = Object.assign(new Error("The native Antigravity sidecar could not be reached. This does not prove that the App is closed. Inspect its codex-bridge sidecar configuration and include the diagnostic in the report; do not open a chat to load the relay."), { code: "ANTIGRAVITY_SIDECAR_UNAVAILABLE", phase: cause.phase ?? "connect", cause });
123
+ const diagnostic = bridgeDiagnostic(error, { component: "antigravity-bridge", operation: "antigravity_bridge_status", readOnly: true });
124
+ desktop = { available: false, code: diagnostic.code, reason: diagnostic.reason, diagnostic };
125
+ }
126
+ return {
127
+ version,
128
+ caller: caller ?? "unconfigured",
129
+ desktop,
130
+ cli: { transport: "stream-json", activeSessions: sessions.size, permissionPolicy: "native-unchanged", interactiveTerminalAttachment: false },
131
+ outbound: {
132
+ supported: true,
133
+ server: "agent-bridge",
134
+ surface: "desktop",
135
+ targets: ["codex-desktop", "claude-desktop-code"],
136
+ scope: "existing-conversations-in-the-same-project",
137
+ readiness: "not_checked",
138
+ statusTool: "agent_bridge_status",
139
+ cliSupported: false,
140
+ automaticReplyForwarding: false,
141
+ },
142
+ };
82
143
  });
83
144
 
84
145
  register("list_antigravity_conversations", "List native Desktop conversations and bridge-owned CLI sessions in the sending project.", { surface: z.enum(["desktop", "cli"]) }, true, async ({ surface }, extra) => {
@@ -94,28 +155,29 @@ register("send_to_antigravity_conversation", "Send one authorized prompt to an e
94
155
  const identity = await sender(extra);
95
156
  const cwd = sameCwd(identity.cwd, args.cwd);
96
157
  const prompt = handoff(args.prompt, identity);
97
- if (args.surface === "desktop" && (prompt.length > 30000 || prompt.includes("\0"))) throw new Error("Desktop handoff must fit within 30000 characters and contain no NUL");
158
+ if (args.surface === "desktop" && (prompt.length > 30000 || prompt.includes("\0"))) throw preflightFailure("ANTIGRAVITY_PROMPT_INVALID", "Desktop handoff must fit within 30000 characters and contain no NUL");
98
159
  if (args.surface === "desktop") return callAntigravityDesktop({ operation: "send", ...args, cwd, prompt, sender: identity });
99
160
  const entry = sessions.get(args.conversationId);
100
- if (!entry || entry.sender.id !== identity.id || entry.sender.provider !== identity.provider) throw new Error("CLI session is not owned by this sender and bridge process");
161
+ if (!entry || entry.sender.id !== identity.id || entry.sender.provider !== identity.provider) throw preflightFailure("ANTIGRAVITY_SESSION_OWNER_MISMATCH", "CLI session is not owned by this sender and bridge process");
101
162
  sameCwd(cwd, entry.session.read().cwd);
102
163
  return deliveries.dispatch({ ...args, cwd, sender: identity, prompt }, async () => {
103
164
  const current = await sender(extra);
104
165
  sameCwd(cwd, current.cwd);
105
- if (current.id !== identity.id) throw Object.assign(new Error("Sender changed before delivery"), { deliveryState: "not_sent" });
166
+ if (current.id !== identity.id) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Sender changed before delivery");
106
167
  return entry.session.send(prompt);
107
- });
168
+ }, { component: "antigravity-bridge", operation: "send_to_antigravity_conversation" });
108
169
  });
109
170
 
110
171
  register("start_antigravity_cli_session", "Create a new bridge-owned Antigravity CLI streaming session for explicitly authorized new work; sends the initial prompt and returns a durable delivery receipt.", schema, false, async (args, extra) => {
111
172
  const identity = await sender(extra);
112
173
  const cwd = sameCwd(identity.cwd, args.cwd);
113
- if (sessions.size + startingSessions >= 4) throw new Error("At most four CLI sessions may be open in one bridge");
174
+ if (sessions.size + startingSessions >= 4) throw preflightFailure("ANTIGRAVITY_SESSION_CAPACITY", "At most four CLI sessions may be open in one bridge");
114
175
  return deliveries.dispatch({ ...args, cwd, sender: identity, surface: "cli" }, async () => {
115
- if (sessions.size + startingSessions >= 4) throw Object.assign(new Error("CLI session capacity reached"), { deliveryState: "not_sent" });
176
+ if (sessions.size + startingSessions >= 4) throw preflightFailure("ANTIGRAVITY_SESSION_CAPACITY", "CLI session capacity reached");
116
177
  startingSessions++;
117
178
  let reserved = true;
118
179
  let session;
180
+ let sending = false;
119
181
  try {
120
182
  const env = Object.fromEntries(Object.entries(process.env).filter(([name]) => !/^(?:CODEX_|CLAUDE|ANTHROPIC_|MCP_)/i.test(name)));
121
183
  session = new AntigravityCliSession({ cwd, env });
@@ -126,9 +188,15 @@ register("start_antigravity_cli_session", "Create a new bridge-owned Antigravity
126
188
  reserved = false;
127
189
  const current = await sender(extra);
128
190
  sameCwd(cwd, current.cwd);
129
- if (current.id !== identity.id) throw Object.assign(new Error("Sender changed before delivery"), { deliveryState: "not_sent" });
191
+ if (current.id !== identity.id) throw preflightFailure("ANTIGRAVITY_SENDER_CHANGED", "Sender changed before delivery");
192
+ sending = true;
130
193
  return await session.send(handoff(args.prompt, identity));
131
194
  } catch (error) {
195
+ if (!sending) {
196
+ error.code ??= "ANTIGRAVITY_CLI_START_FAILED";
197
+ error.phase ??= "startup";
198
+ error.deliveryState = "not_sent";
199
+ }
132
200
  if (error.deliveryState !== "unknown") {
133
201
  for (const [id, entry] of sessions) if (entry.session === session) sessions.delete(id);
134
202
  await session?.close();
@@ -137,7 +205,7 @@ register("start_antigravity_cli_session", "Create a new bridge-owned Antigravity
137
205
  } finally {
138
206
  if (reserved) startingSessions--;
139
207
  }
140
- });
208
+ }, { component: "antigravity-bridge", operation: "start_antigravity_cli_session" });
141
209
  });
142
210
 
143
211
  register("read_antigravity_delivery", "Read the original receipt without resending. Desktop accepted means the native API accepted it, not that the agent completed a turn.", { surface: z.enum(["desktop", "cli"]), requestId: z.string().uuid() }, true, async (args, extra) => {
@@ -149,9 +217,9 @@ register("read_antigravity_delivery", "Read the original receipt without resendi
149
217
  register("close_antigravity_cli_session", "Close an idle bridge-owned CLI session belonging to the caller.", { conversationId: z.string().uuid() }, false, async ({ conversationId }, extra) => {
150
218
  const identity = await sender(extra);
151
219
  const entry = sessions.get(conversationId);
152
- if (!entry || entry.sender.id !== identity.id || entry.sender.provider !== identity.provider) throw new Error("CLI session is not owned by this sender");
220
+ if (!entry || entry.sender.id !== identity.id || entry.sender.provider !== identity.provider) throw preflightFailure("ANTIGRAVITY_SESSION_OWNER_MISMATCH", "CLI session is not owned by this sender");
153
221
  sameCwd(identity.cwd, entry.session.read().cwd);
154
- if (["running", "starting"].includes(entry.session.read().status)) throw new Error("Wait for the active CLI delivery before closing");
222
+ if (["running", "starting"].includes(entry.session.read().status)) throw preflightFailure("ANTIGRAVITY_SESSION_BUSY", "Wait for the active CLI delivery before closing");
155
223
  const closed = await entry.session.close();
156
224
  if (closed.status !== "closing") sessions.delete(conversationId);
157
225
  return { closed: closed.status !== "closing", conversationId, status: closed.status };
@@ -165,6 +233,7 @@ async function stop() {
165
233
  await stopProcessInspectors();
166
234
  await server.close();
167
235
  }
168
- for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void stop(); });
169
- process.stdin.once("end", () => { void stop(); });
170
236
  await server.connect(new StdioServerTransport());
237
+ const shutdown = exitWhenClientCloses({ busy: () => startingSessions || deliveries.active.size ? "Antigravity deliveries are still active" : null, stop });
238
+ for (const signal of ["SIGINT", "SIGTERM"]) process.once(signal, () => { void shutdown.close(signal); });
239
+ reload.listen();
@@ -50,6 +50,8 @@ export class AntigravityCliSession {
50
50
  this.env = { ...(options.env ?? process.env) };
51
51
  this.binary = resolveAntigravityCliBinary({ binary: options.binary, env: this.env });
52
52
  this.spawn = options.spawn ?? spawnProcess;
53
+ this.onEvent = options.onEvent;
54
+ this.onStderr = options.onStderr;
53
55
  this.requestedConversationId = options.conversationId ?? null;
54
56
  if (this.requestedConversationId !== null && !UUID.test(this.requestedConversationId)) throw new Error("Invalid Antigravity conversation ID");
55
57
  this.startupTimeoutMs = positive(options.startupTimeoutMs, 30000, "startupTimeoutMs");
@@ -102,7 +104,11 @@ export class AntigravityCliSession {
102
104
  this.child.stdin.on("error", () => this.fail(failure("Antigravity CLI input pipe failed", "STDIN_ERROR", this.pending ? "unknown" : this.deliveryState)));
103
105
  this.child.stdout.on("error", () => this.fail(failure("Antigravity CLI output pipe failed", "STDOUT_ERROR", this.pending ? "unknown" : this.deliveryState)));
104
106
  this.child.stderr.on("error", () => this.fail(failure("Antigravity CLI diagnostic pipe failed", "STDERR_ERROR", this.pending ? "unknown" : this.deliveryState)));
105
- this.child.stderr.resume();
107
+ if (this.onStderr) this.child.stderr.on("data", (chunk) => {
108
+ try { this.onStderr(chunk); }
109
+ catch { this.fail(failure("Antigravity CLI diagnostic observer failed", "OBSERVER_ERROR", this.pending ? "unknown" : this.deliveryState)); }
110
+ });
111
+ else this.child.stderr.resume();
106
112
  this.child.stdout.on("data", (chunk) => this.consume(chunk));
107
113
  this.child.on("close", (code, signal) => {
108
114
  this.exited = true;
@@ -147,6 +153,7 @@ export class AntigravityCliSession {
147
153
  this.conversationId = event.conversation_id;
148
154
  this.permissionMode = event.init.permission_mode;
149
155
  this.status = "idle";
156
+ this.onEvent?.(structuredClone(event));
150
157
  clearTimeout(this.startTimer);
151
158
  this.startResolve(this.read());
152
159
  this.startResolve = null;
@@ -154,11 +161,15 @@ export class AntigravityCliSession {
154
161
  return;
155
162
  }
156
163
  if (!this.conversationId) throw new Error("Invalid Antigravity event");
157
- if (event.event !== "result") return;
164
+ if (event.event !== "result") {
165
+ this.onEvent?.(structuredClone(event));
166
+ return;
167
+ }
158
168
  const result = event.result;
159
169
  if (!this.awaitingResult || !result || result.conversation_id !== this.conversationId
160
170
  || typeof result.status !== "string" || !/^[A-Z][A-Z_]{0,127}$/.test(result.status)
161
171
  || typeof result.response !== "string" || result.error !== undefined && typeof result.error !== "string") throw new Error("Invalid Antigravity event");
172
+ this.onEvent?.(structuredClone(event));
162
173
  this.lastResult = { conversation_id: result.conversation_id, status: result.status, response: result.response, ...(result.error === undefined ? {} : { error: result.error }) };
163
174
  this.awaitingResult = false;
164
175
  this.deliveryState = "completed";
@@ -2,9 +2,19 @@ import crypto from "node:crypto";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { ensureBridgeDirectory } from "./antigravity-ipc.mjs";
5
+ import { bridgeDiagnostic } from "./recipient-preflight.mjs";
5
6
 
6
7
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
7
8
 
9
+ export function antigravityDeliveryError(record) {
10
+ if (!record || !UUID.test(record.requestId) || !["failed", "unknown"].includes(record.status)) return null;
11
+ return Object.assign(new Error(record.reason ?? "The original Antigravity delivery did not complete successfully"), {
12
+ code: record.diagnostic?.code ?? (record.status === "unknown" ? "ANTIGRAVITY_DELIVERY_UNKNOWN" : "ANTIGRAVITY_DELIVERY_FAILED"),
13
+ deliveryState: record.diagnostic?.deliveryState ?? "unknown",
14
+ diagnostic: record.diagnostic,
15
+ });
16
+ }
17
+
8
18
  export class AntigravityDeliveries {
9
19
  constructor(directory) {
10
20
  this.directory = ensureBridgeDirectory(directory);
@@ -39,7 +49,11 @@ export class AntigravityDeliveries {
39
49
  record = JSON.parse(buffer.subarray(0, bytes).toString("utf8"));
40
50
  } finally { fs.closeSync(descriptor); }
41
51
  if (record.requestId !== requestId) throw new Error("Delivery identity mismatch");
42
- if (record.status === "pending" && !this.active.has(requestId)) return { ...record, status: "unknown", reason: "This process cannot observe the original worker. Inspect the exact destination; do not resend." };
52
+ if (record.status === "pending" && !this.active.has(requestId)) {
53
+ const error = Object.assign(new Error("This process cannot observe the original worker. Inspect the original delivery receipt; do not resend or open a chat to load the relay."), { code: "ANTIGRAVITY_WORKER_UNAVAILABLE", phase: "receipt", deliveryState: "unknown" });
54
+ const diagnostic = bridgeDiagnostic(error, { component: "antigravity-deliveries", operation: record.operation ?? "send", requestId, conversationId: record.conversationId });
55
+ return { ...record, status: "unknown", reason: diagnostic.reason, diagnostic };
56
+ }
43
57
  return record;
44
58
  }
45
59
 
@@ -56,28 +70,38 @@ export class AntigravityDeliveries {
56
70
  fs.renameSync(temporary, this.file(record.requestId));
57
71
  }
58
72
 
59
- async dispatch(request, operation) {
73
+ async dispatch(request, operation, diagnosticOptions = {}) {
60
74
  const { requestId, ...identity } = request;
61
75
  const digest = crypto.createHash("sha256").update(JSON.stringify(identity)).digest("hex");
62
76
  const existing = this.read(requestId);
63
77
  if (existing) {
64
- if (existing.digest !== digest) throw new Error("requestId was already used for a different delivery");
78
+ if (existing.digest !== digest) throw Object.assign(new Error("requestId was already used for a different delivery; inspect the original receipt before further action"), { code: "ANTIGRAVITY_REQUEST_ID_CONFLICT", phase: "validation", deliveryState: "not_sent" });
65
79
  return existing;
66
80
  }
67
- const record = { requestId, digest, status: "pending", cwd: request.cwd, sender: request.sender, conversationId: request.conversationId ?? null, surface: request.surface, createdAt: new Date().toISOString() };
81
+ const record = { requestId, digest, status: "pending", operation: diagnosticOptions.operation ?? "send", cwd: request.cwd, sender: request.sender, conversationId: request.conversationId ?? null, surface: request.surface, createdAt: new Date().toISOString() };
82
+ const diagnose = (error) => bridgeDiagnostic(error, { component: "antigravity-deliveries", operation: record.operation, ...diagnosticOptions, requestId, conversationId: record.conversationId, msgId: error.msgId });
68
83
  try { fs.writeFileSync(this.file(requestId), JSON.stringify(record), { flag: "wx", mode: 0o600 }); }
69
84
  catch (error) {
70
- if (error.code !== "EEXIST") throw error;
71
- throw new Error("Delivery is owned by another process. Inspect requestId; do not resend.");
85
+ if (error.code !== "EEXIST") {
86
+ error.deliveryState = "not_sent";
87
+ error.phase = "receipt";
88
+ throw error;
89
+ }
90
+ throw Object.assign(new Error("Delivery is owned by another process. Inspect requestId; do not resend."), { code: "ANTIGRAVITY_DELIVERY_IN_PROGRESS", phase: "receipt", deliveryState: "unknown" });
72
91
  }
73
92
  this.active.add(requestId);
74
93
  void Promise.resolve().then(operation).then((result) => {
75
94
  record.status = ["accepted", "sent_unconfirmed", "held", "refused"].includes(result?.status) ? result.status : result?.status && result.status !== "SUCCESS" ? "failed" : "completed";
76
95
  record.result = result;
77
96
  record.conversationId = result?.conversationId ?? result?.conversation_id ?? record.conversationId;
97
+ if (record.status === "failed") {
98
+ record.diagnostic = diagnose(Object.assign(new Error(typeof result.error === "string" ? result.error : "The native Antigravity delivery failed"), { code: result.status, deliveryState: result.deliveryState, sent: result.sent }));
99
+ record.reason = record.diagnostic.reason;
100
+ }
78
101
  }, (error) => {
79
- record.status = ["not_sent", "not-sent"].includes(error.deliveryState) ? "failed" : "unknown";
80
- record.reason = error.message;
102
+ record.diagnostic = diagnose(error);
103
+ record.status = record.diagnostic.deliveryState === "not_sent" ? "failed" : "unknown";
104
+ record.reason = record.diagnostic.reason;
81
105
  if (typeof error.msgId === "string" && UUID.test(error.msgId)) record.result = { msgId: error.msgId, completed: false };
82
106
  if (error.accountContext) record.result = { ...record.result, accountContext: error.accountContext };
83
107
  }).finally(() => {
@@ -5,6 +5,7 @@ import os from "node:os";
5
5
  import path from "node:path";
6
6
  import { protectCurrentUserPipe } from "./windows-pipe-acl.mjs";
7
7
  import { relayFileIdentity, removeRelayFile } from "./native-relay-files.mjs";
8
+ import { bridgeDiagnostic } from "./recipient-preflight.mjs";
8
9
 
9
10
  const LIMIT = 2 * 1024 * 1024;
10
11
 
@@ -55,28 +56,30 @@ export function callAntigravityDesktop(request, { directory = antigravityBridgeD
55
56
  socket.destroy();
56
57
  if (error) {
57
58
  error.deliveryState ??= dispatched ? "unknown" : "not_sent";
59
+ error.phase ??= dispatched ? "dispatch" : "connect";
60
+ error.diagnostic = bridgeDiagnostic(error, { component: "antigravity-sidecar", operation: request?.operation, requestId: request?.requestId, conversationId: request?.conversationId, readOnly: ["status", "list", "read", "receipt"].includes(request?.operation) });
58
61
  reject(error);
59
62
  } else resolve(result);
60
63
  };
61
- const timer = setTimeout(() => done(Object.assign(new Error("Antigravity sidecar did not reply; inspect the original requestId before retrying"), { deliveryState: "unknown" })), timeoutMs);
64
+ const timer = setTimeout(() => done(Object.assign(new Error("Antigravity sidecar did not reply; inspect the original requestId before retrying"), { code: "ANTIGRAVITY_IPC_TIMEOUT" })), timeoutMs);
62
65
  socket.on("error", (error) => done(error));
63
- socket.on("end", () => done(new Error("Antigravity sidecar disconnected before replying")));
66
+ socket.on("end", () => done(Object.assign(new Error("Antigravity sidecar disconnected before replying"), { code: "ANTIGRAVITY_IPC_DISCONNECTED" })));
64
67
  socket.on("connect", () => {
65
68
  const frame = JSON.stringify(request) + "\n";
66
- if (Buffer.byteLength(frame) > LIMIT) return done(new Error("Request exceeds size limit"));
69
+ if (Buffer.byteLength(frame) > LIMIT) return done(Object.assign(new Error("Request exceeds size limit"), { code: "ANTIGRAVITY_REQUEST_TOO_LARGE", phase: "validation" }));
67
70
  dispatched = true;
68
71
  socket.write(frame);
69
72
  });
70
73
  socket.on("data", (chunk) => {
71
74
  buffer = Buffer.concat([buffer, chunk]);
72
- if (buffer.length > LIMIT) return done(new Error("Sidecar response exceeds size limit"));
75
+ if (buffer.length > LIMIT) return done(Object.assign(new Error("Sidecar response exceeds size limit"), { code: "ANTIGRAVITY_RESPONSE_TOO_LARGE", phase: "response" }));
73
76
  const end = buffer.indexOf(10);
74
77
  if (end < 0) return;
75
78
  try {
76
79
  const response = JSON.parse(buffer.subarray(0, end).toString("utf8"));
77
- if (response.error) return done(Object.assign(new Error(response.error), { deliveryState: response.deliveryState }));
80
+ if (response.error) return done(Object.assign(new Error(response.error), { code: response.code, phase: response.phase, deliveryState: response.deliveryState, diagnostic: response.diagnostic }));
78
81
  done(null, response.result);
79
- } catch (error) { done(error); }
82
+ } catch (error) { done(Object.assign(new Error("Antigravity sidecar returned an invalid response"), { code: "ANTIGRAVITY_RESPONSE_INVALID", phase: "response", cause: error })); }
80
83
  });
81
84
  });
82
85
  }
@@ -101,12 +104,18 @@ export async function serveAntigravityDesktop(handler, { directory = antigravity
101
104
  const end = buffer.indexOf(10);
102
105
  if (end < 0) return;
103
106
  received = true;
107
+ let request;
104
108
  try {
105
- const result = await handler(JSON.parse(buffer.subarray(0, end).toString("utf8")));
109
+ try { request = JSON.parse(buffer.subarray(0, end).toString("utf8")); }
110
+ catch { throw Object.assign(new Error("Invalid Antigravity sidecar request"), { code: "ANTIGRAVITY_REQUEST_INVALID", phase: "validation", deliveryState: "not_sent" }); }
111
+ const result = await handler(request);
106
112
  const frame = JSON.stringify({ result }) + "\n";
107
- if (Buffer.byteLength(frame) > LIMIT) throw new Error("Sidecar response exceeds size limit");
113
+ if (Buffer.byteLength(frame) > LIMIT) throw Object.assign(new Error("Sidecar response exceeds size limit"), { code: "ANTIGRAVITY_RESPONSE_TOO_LARGE", phase: "response" });
108
114
  socket.end(frame);
109
- } catch (error) { socket.end(JSON.stringify({ error: error.message, deliveryState: error.deliveryState }) + "\n"); }
115
+ } catch (error) {
116
+ const diagnostic = bridgeDiagnostic(error, { component: "antigravity-sidecar", operation: request?.operation, requestId: request?.requestId, conversationId: request?.conversationId, readOnly: ["status", "list", "read", "receipt"].includes(request?.operation), deliveryState: error.deliveryState ?? "unknown" });
117
+ socket.end(JSON.stringify({ error: diagnostic.reason, code: diagnostic.code, phase: diagnostic.phase, deliveryState: diagnostic.deliveryState, diagnostic }) + "\n");
118
+ }
110
119
  });
111
120
  });
112
121
  const privatePath = process.platform === "win32" ? socketPath : path.join(directory, `.${crypto.randomBytes(6).toString("hex")}.sock`);