@minhspark/codex-mcp-bridge 1.25.1 → 1.25.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,22 +1,6 @@
1
- import fs from "node:fs";
2
1
  import net from "node:net";
3
- import path from "node:path";
4
2
 
5
- import { codexHome, resolveRelayThreadId } from "./native-relay.mjs";
6
- import { isCodexDesktopRunning, openThreadInCodexApp } from "./platform.mjs";
7
- import { captureProjectScope } from "./project-scope.mjs";
8
- import { readRolloutFirstRecord } from "./rollout-reader.mjs";
9
-
10
- const THREAD_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
11
- const ROLLOUT = /^rollout-.+-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl(?:\.zst)?$/i;
12
3
  export const RELAY_WAKE_WAIT_MS = 20000;
13
- export const RELAY_WAKE_COOLDOWN_MS = 60000;
14
- export const RELAY_KEEPER_INTERVAL_MS = 30000;
15
- export const RELAY_KEEPER_WAIT_MS = 60000;
16
- export const RELAY_KEEPER_MAX_BACKOFF_MS = 30 * 60000;
17
- export const DESKTOP_RECHECK_MS = 60000;
18
- const MAX_DAY_DIRECTORIES = 14;
19
- const MAX_CANDIDATES = 40;
20
4
 
21
5
  export function relayWakeEnabled(env = process.env, platform = process.platform) {
22
6
  const value = (env.CODEX_BRIDGE_RELAY_WAKE ?? "").toLowerCase();
@@ -24,71 +8,14 @@ export function relayWakeEnabled(env = process.env, platform = process.platform)
24
8
  return platform === "darwin" || platform === "win32";
25
9
  }
26
10
 
27
- function dayDirectories(sessions) {
28
- const directories = [];
29
- const children = (directory, pattern) => {
30
- try { return fs.readdirSync(directory, { withFileTypes: true }).filter((entry) => entry.isDirectory() && pattern.test(entry.name)).map((entry) => entry.name).sort().reverse(); }
31
- catch { return []; }
32
- };
33
- for (const year of children(sessions, /^\d{4}$/)) {
34
- for (const month of children(path.join(sessions, year), /^\d{2}$/)) {
35
- for (const day of children(path.join(sessions, year, month), /^\d{2}$/)) {
36
- directories.push(path.join(sessions, year, month, day));
37
- if (directories.length >= MAX_DAY_DIRECTORIES) return directories;
38
- }
39
- }
40
- }
41
- return directories;
42
- }
43
-
44
- function sameProject(senderCwd, taskCwd) {
45
- try { captureProjectScope(senderCwd, taskCwd); return true; }
46
- catch { return false; }
47
- }
48
-
49
- export function relayKeeperEnabled(env = process.env, platform = process.platform) {
50
- const value = (env.CODEX_BRIDGE_RELAY_KEEPER ?? "").toLowerCase();
51
- if (["0", "off", "false"].includes(value)) return false;
52
- if (!relayWakeEnabled(env, platform)) return false;
53
- if (platform === "win32") return ["1", "on", "true"].includes(value);
54
- return true;
55
- }
56
-
57
- export function relayWakeStateFile(env = process.env) {
58
- return path.join(codexHome(env), "bridge-relay-wake.json");
59
- }
60
-
61
- export async function recentDesktopTaskFor(cwd, { env = process.env, sessions = path.join(codexHome(env), "sessions"), exclude = [], anyProject = false } = {}) {
62
- if (!anyProject && (typeof cwd !== "string" || !cwd)) return null;
63
- const excluded = new Set(exclude.filter(Boolean));
64
- const candidates = [];
65
- for (const directory of dayDirectories(sessions)) {
66
- let entries;
67
- try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch { continue; }
68
- for (const entry of entries) {
69
- const match = entry.isFile() ? ROLLOUT.exec(entry.name) : null;
70
- if (!match || excluded.has(match[1])) continue;
71
- const file = path.join(directory, entry.name);
72
- try { candidates.push({ file, threadId: match[1], mtimeMs: fs.lstatSync(file).mtimeMs }); } catch {}
73
- }
74
- if (candidates.length >= MAX_CANDIDATES) break;
75
- }
76
- candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
77
- for (const candidate of candidates.slice(0, MAX_CANDIDATES)) {
78
- let record;
79
- try { record = await readRolloutFirstRecord(candidate.file, { maxBytes: 256 * 1024 }); } catch { continue; }
80
- const meta = record?.type === "session_meta" ? record.payload : null;
81
- if (meta?.id !== candidate.threadId || meta.originator !== "Codex Desktop" || typeof meta.cwd !== "string") continue;
82
- if (anyProject || sameProject(cwd, meta.cwd)) return candidate.threadId;
83
- }
84
- return null;
85
- }
86
-
87
11
  export function endpointAnswers(socketPath, { timeoutMs = 1000 } = {}) {
88
12
  if (typeof socketPath !== "string" || !socketPath) return Promise.resolve(false);
89
13
  return new Promise((resolve) => {
90
14
  const probe = net.connect({ path: socketPath });
15
+ let settled = false;
91
16
  const done = (answer) => {
17
+ if (settled) return;
18
+ settled = true;
92
19
  globalThis.clearTimeout(timer);
93
20
  probe.destroy();
94
21
  resolve(answer);
@@ -96,163 +23,33 @@ export function endpointAnswers(socketPath, { timeoutMs = 1000 } = {}) {
96
23
  const timer = globalThis.setTimeout(() => done(false), timeoutMs);
97
24
  probe.once("connect", () => done(true));
98
25
  probe.once("error", () => done(false));
26
+ probe.once("close", () => done(false));
99
27
  });
100
28
  }
101
29
 
102
- function readWakeState(file) {
103
- if (!file) return {};
104
- try {
105
- const state = JSON.parse(fs.readFileSync(file, "utf8"));
106
- return state && typeof state === "object" && !Array.isArray(state) ? state : {};
107
- } catch { return {}; }
108
- }
109
-
110
- function writeWakeState(file, state) {
111
- if (!file) return;
112
- const temporary = `${file}.${process.pid}.tmp`;
113
- try {
114
- fs.writeFileSync(temporary, JSON.stringify(state));
115
- fs.renameSync(temporary, file);
116
- } catch { try { fs.rmSync(temporary, { force: true }); } catch {} }
117
- }
118
-
119
- function acquireWakeLock(file, now, staleMs) {
120
- if (!file) return () => {};
121
- const lock = `${file}.lock`;
122
- const claim = () => {
123
- const descriptor = fs.openSync(lock, "wx");
124
- try { fs.writeSync(descriptor, JSON.stringify({ pid: process.pid, at: now() })); } finally { fs.closeSync(descriptor); }
125
- return () => { try { fs.rmSync(lock, { force: true }); } catch {} };
126
- };
127
- try { return claim(); }
128
- catch (error) { if (error.code !== "EEXIST") return null; }
129
- let heldSince;
130
- try { heldSince = JSON.parse(fs.readFileSync(lock, "utf8")).at; } catch { heldSince = 0; }
131
- if (Number.isFinite(heldSince) && now() - heldSince < staleMs) return null;
132
- try { fs.rmSync(lock, { force: true }); return claim(); } catch { return null; }
133
- }
134
-
135
- export function keeperBackoffMs(failures, { baseMs = RELAY_WAKE_COOLDOWN_MS, maxMs = RELAY_KEEPER_MAX_BACKOFF_MS } = {}) {
136
- const count = Number.isInteger(failures) && failures > 0 ? Math.min(failures, 16) : 0;
137
- return count === 0 ? 0 : Math.min(baseMs * 2 ** (count - 1), maxMs);
138
- }
139
-
140
30
  export function createRelayWake({
141
31
  env = process.env,
142
32
  enabled = relayWakeEnabled(env),
143
- desktopRunning = () => isCodexDesktopRunning(),
144
- senderCwd = () => null,
145
- findTask = (cwd, exclude) => recentDesktopTaskFor(cwd, { env, exclude }),
146
- findAnyTask = (exclude) => recentDesktopTaskFor(null, { env, exclude, anyProject: true }),
147
- executorThreadId = () => { try { return resolveRelayThreadId(env).threadId; } catch { return null; } },
148
- open = (threadId) => openThreadInCodexApp(threadId, { activate: false }),
149
33
  answers = endpointAnswers,
150
34
  waitMs = RELAY_WAKE_WAIT_MS,
151
- backgroundWaitMs = RELAY_KEEPER_WAIT_MS,
152
- cooldownMs = RELAY_WAKE_COOLDOWN_MS,
153
- desktopRecheckMs = DESKTOP_RECHECK_MS,
154
- stateFile = null,
155
35
  pollMs = 250,
156
36
  now = Date.now,
157
37
  sleep = (ms) => new Promise((resolve) => globalThis.setTimeout(resolve, ms)),
158
38
  log = () => {},
159
39
  } = {}) {
160
- let inFlight = null;
161
- let failedAt = -Infinity;
162
- const waitFor = async (socketPath, until) => {
40
+ return async ({ socketPath, deadline }) => {
41
+ if (!enabled || typeof socketPath !== "string" || !socketPath) return null;
42
+ const until = Math.min(now() + waitMs, deadline ?? Infinity);
163
43
  while (now() < until) {
164
- if (await answers(socketPath)) return true;
165
- await sleep(pollMs);
166
- }
167
- return false;
168
- };
169
- const attempt = async ({ socketPath, threadId, deadline, background = false }) => {
170
- if (!enabled) return null;
171
- const state = readWakeState(stateFile);
172
- if (background) {
173
- if (state.desktopRunning === false && now() - (state.desktopCheckedAt ?? -Infinity) < desktopRecheckMs) return null;
174
- if (now() - (state.attemptAt ?? -Infinity) < Math.max(keeperBackoffMs(state.failures), cooldownMs)) return null;
175
- } else if (now() - failedAt < cooldownMs) return null;
176
- const running = await desktopRunning();
177
- if (stateFile) writeWakeState(stateFile, { ...readWakeState(stateFile), desktopCheckedAt: now(), desktopRunning: Boolean(running) });
178
- if (!running) return null;
179
- const wait = background ? backgroundWaitMs : waitMs;
180
- const release = acquireWakeLock(stateFile, now, wait + 15000);
181
- if (!release) {
182
- if (background) return null;
183
- return await waitFor(socketPath, Math.min(now() + wait, deadline ?? Infinity)) ? { threadId: null } : null;
184
- }
185
- try {
186
- const executor = executorThreadId();
187
- const target = typeof threadId === "string" && THREAD_ID.test(threadId) && threadId !== executor
188
- ? threadId : (await findTask(senderCwd(), [executor])) ?? (await findAnyTask([executor]));
189
- const startedAt = now();
190
- const failures = Number.isInteger(state.failures) ? state.failures : 0;
191
- if (!target) {
192
- if (background && stateFile) writeWakeState(stateFile, { ...readWakeState(stateFile), attemptAt: startedAt, failures: failures + 1 });
193
- return null;
44
+ if (await answers(socketPath, { timeoutMs: Math.min(1000, until - now()) })) {
45
+ if (now() >= until) return null;
46
+ log("relay transport available; the original operation will verify native dispatch");
47
+ return { threadId: null };
194
48
  }
195
- const fail = (message) => {
196
- failedAt = now();
197
- if (stateFile) writeWakeState(stateFile, { ...readWakeState(stateFile), attemptAt: startedAt, failures: failures + 1 });
198
- log(message);
199
- return null;
200
- };
201
- try { await open(target); }
202
- catch (error) { return fail(`could not open Codex task ${target} to start the native relay: ${error.message}`); }
203
- if (await waitFor(socketPath, Math.min(now() + wait, deadline ?? Infinity))) {
204
- if (stateFile) writeWakeState(stateFile, { ...readWakeState(stateFile), attemptAt: startedAt, failures: 0 });
205
- log(`started the Codex Desktop native relay by opening task ${target} in the background`);
206
- return { threadId: target };
207
- }
208
- return fail(`opened Codex task ${target}, but the native relay did not start within ${wait} ms`);
209
- } finally {
210
- release();
49
+ const remaining = until - now();
50
+ if (remaining > 0) await sleep(Math.min(pollMs, remaining));
211
51
  }
212
- };
213
- return (request) => {
214
- if (!inFlight) return inFlight = attempt(request).finally(() => { inFlight = null; });
215
- if (request.background || !request.socketPath) return inFlight;
216
- const until = Math.min(now() + waitMs, request.deadline ?? Infinity);
217
- return Promise.race([inFlight, waitFor(request.socketPath, until).then((ready) => ready ? { threadId: null } : null)]);
218
- };
219
- }
220
-
221
- export function startRelayKeeper({
222
- wake,
223
- socketPath,
224
- answers = endpointAnswers,
225
- intervalMs = RELAY_KEEPER_INTERVAL_MS,
226
- initialDelayMs = 3000,
227
- timers = globalThis,
228
- log = () => {},
229
- }) {
230
- let busy = false;
231
- let stopped = false;
232
- const tick = async () => {
233
- if (busy || stopped) return null;
234
- busy = true;
235
- try {
236
- const target = socketPath();
237
- if (await answers(target)) return null;
238
- return await wake({ socketPath: target, background: true });
239
- } catch (error) {
240
- log(`relay keeper check failed: ${error.message}`);
241
- return null;
242
- } finally {
243
- busy = false;
244
- }
245
- };
246
- const first = timers.setTimeout(tick, initialDelayMs);
247
- const repeat = timers.setInterval(tick, intervalMs);
248
- first?.unref?.();
249
- repeat?.unref?.();
250
- return {
251
- tick,
252
- stop() {
253
- stopped = true;
254
- timers.clearTimeout(first);
255
- timers.clearInterval(repeat);
256
- },
52
+ log("native relay recovery timed out; no task or window was opened");
53
+ return null;
257
54
  };
258
55
  }
package/src/setup.mjs CHANGED
@@ -89,7 +89,7 @@ export function setupEnvironment(env, paths, codex) {
89
89
  }
90
90
 
91
91
  function configurationCheck(label, check, components) {
92
- try { check(); components.push({ name: label, status: "configured" }); }
92
+ try { const details = check(); components.push({ name: label, status: "configured", ...details }); }
93
93
  catch (error) { components.push({ name: label, status: "incomplete", detail: error.message }); }
94
94
  }
95
95
 
@@ -99,8 +99,9 @@ function assertEntry(entry, worker, root, { caller } = {}) {
99
99
  const expected = worker === "index.mjs" || worker === "claude-bridge.mjs" || worker === "native-relay-companion.mjs"
100
100
  ? [path.join(root, "src", "mcp-supervisor.mjs"), worker] : [path.join(root, "src", worker)];
101
101
  if (JSON.stringify(args) !== JSON.stringify(expected)) throw new Error("MCP entry points to a different installation or unsupported launcher");
102
- if (entry.enabled === false || entry.disabled === true) throw new Error("MCP entry is disabled; the saved choice was preserved");
103
102
  if (caller && entry.env?.ANTIGRAVITY_BRIDGE_CALLER !== caller) throw new Error("Antigravity caller registration is missing or conflicting");
103
+ if (entry.enabled === false || entry.disabled === true) return { deliveryEnabled: false,
104
+ notices: ["This MCP registration is intentionally disabled. Its saved choice was preserved; delivery through this connector is disabled."] };
104
105
  }
105
106
 
106
107
  export function checkSetup({ env = process.env, platform = process.platform, root = ROOT, run = execute, resolve = resolveCodexBin } = {}) {
@@ -119,8 +120,9 @@ export function checkSetup({ env = process.env, platform = process.platform, roo
119
120
  const item = inventory?.find((server) => server.name === name);
120
121
  if (!item) throw new Error("MCP registration is missing");
121
122
  const entry = JSON.parse(run(codex, ["mcp", "get", name, "--json"], setupEnvironment(env, paths, codex), 30000));
122
- assertEntry({ ...entry.transport, enabled: entry.enabled }, worker, root, name === "antigravity-bridge" ? { caller: "codex" } : {});
123
+ const details = assertEntry({ ...entry.transport, enabled: entry.enabled }, worker, root, name === "antigravity-bridge" ? { caller: "codex" } : {});
123
124
  if (name === "codex-native-relay" && !entry.transport.env_vars?.includes("CODEX_APP_TOOLS_PIPE_PATH")) throw new Error("Native pipe forwarding is missing");
125
+ return details;
124
126
  }, components);
125
127
  }
126
128
  for (const [label, file, name, worker, caller] of [
@@ -130,14 +132,26 @@ export function checkSetup({ env = process.env, platform = process.platform, roo
130
132
  ["Antigravity / agent-bridge", path.join(paths.gemini, "mcp_config.json"), "agent-bridge", "antigravity-outbound-bridge.mjs"],
131
133
  ]) configurationCheck(label, () => {
132
134
  const entry = readJson(file)?.mcpServers?.[name];
133
- assertEntry(entry, worker, root, { caller });
135
+ const details = assertEntry(entry, worker, root, { caller });
136
+ if (worker === "index.mjs" && entry.env?.CODEX_BRIDGE_OPEN_IN_APP === "1") {
137
+ throw new Error("Managed bridge settings still allow opening Codex Desktop. Run codex-bridge-setup to apply the current silent configuration.");
138
+ }
134
139
  if (name === "codex-bridge-desktop" && entry.env?.CODEX_BRIDGE_DESKTOP_TASKS === "1") {
135
- throw new Error("Shared Desktop chat cannot use native Code routing. Its saved choice was preserved; explicitly set CODEX_BRIDGE_DESKTOP_TASKS=0 for the local app-server route and CODEX_BRIDGE_AUTOSTART=1 if the bridge should start that server.");
140
+ return { ...details, nativeCodeOnly: true, sharedChatDelivery: "unsupported",
141
+ notices: [...(details?.notices ?? []), "The saved native Code-only policy was preserved. Ordinary Claude Desktop Chat cannot deliver through this connector; native delivery uses the registered Claude Desktop Code connector and remains unverified by setup."] };
136
142
  }
143
+ return details;
144
+ }, components);
145
+ configurationCheck("Claude Desktop / background preference", () => {
146
+ if (readJson(paths.desktop)?.preferences?.menuBarEnabled !== true) throw new Error("The managed Claude background preference is not enabled. Run codex-bridge-setup to apply it; live activation is verified separately.");
137
147
  }, components);
138
148
  configurationCheck("Antigravity / codex-bridge sidecar", () => {
139
- assertEntry(readJson(path.join(paths.gemini, "sidecars", "codex-bridge", "sidecar.json")), "antigravity-sidecar.mjs", root);
140
- if (readJson(path.join(paths.gemini, "config.json"))?.sidecars?.["codex-bridge"]?.enabled !== true) throw new Error("Antigravity sidecar is not enabled");
149
+ const details = assertEntry(readJson(path.join(paths.gemini, "sidecars", "codex-bridge", "sidecar.json")), "antigravity-sidecar.mjs", root);
150
+ const enabled = readJson(path.join(paths.gemini, "config.json"))?.sidecars?.["codex-bridge"]?.enabled;
151
+ if (typeof enabled !== "boolean") throw new Error("Antigravity sidecar enabled setting is missing or invalid");
152
+ if (!enabled) return { deliveryEnabled: false,
153
+ notices: [...(details?.notices ?? []), "The Antigravity bridge sidecar is intentionally disabled. Its saved choice was preserved; native Desktop delivery through this sidecar is disabled."] };
154
+ return details;
141
155
  }, components);
142
156
  if (native) configurationCheck("Native relay executor", () => {
143
157
  const relay = readJson(path.join(paths.codex, "native-relay.json"));
@@ -151,7 +165,7 @@ export function checkSetup({ env = process.env, platform = process.platform, roo
151
165
 
152
166
  export function runSetup({ args = [], env = process.env, platform = process.platform, root = ROOT, run = execute, resolve = resolveCodexBin } = {}) {
153
167
  const options = setupArguments(args);
154
- if (options.help) return { help: "Usage: codex-bridge-setup [--check|--dry-run] [--json] [--no-download]\nGlobal npm installation configures Claude Desktop, Claude Code, Codex and Antigravity automatically. CODEX_BRIDGE_SKIP_SETUP=1 opts out." };
168
+ if (options.help) return { help: "Usage: codex-bridge-setup [--check|--dry-run] [--json] [--no-download]\nUpdate the package and apply managed settings with: npm install -g @minhspark/codex-mcp-bridge@latest --allow-scripts=@minhspark/codex-mcp-bridge\nGlobal npm installation configures Claude Desktop, Claude Code, Codex and Antigravity automatically. CODEX_BRIDGE_SKIP_SETUP=1 opts out. Saved configuration and loaded app connections are verified separately." };
155
169
  if (options.postinstall && !automaticSetupEnabled(env)) return { skipped: true, reason: "Automatic configuration runs only for a global npm install; use codex-bridge-setup for manual setup." };
156
170
  if (options.check) return checkSetup({ env, platform, root, run, resolve });
157
171
  const native = platform === "win32" || platform === "darwin";
@@ -166,16 +180,18 @@ export function runSetup({ args = [], env = process.env, platform = process.plat
166
180
  const install = (name, script, arguments_, environment = base) => {
167
181
  try {
168
182
  const output = run(process.execPath, [path.join(root, "scripts", script), ...arguments_], environment, 180000);
169
- let notices = [];
170
- try { notices = JSON.parse(output).notices ?? []; } catch {}
171
- components.push({ name, status: "configured", ...(notices.length ? { notices } : {}) });
183
+ let details = {};
184
+ try { details = JSON.parse(output); } catch {}
185
+ const notices = details.notices ?? [];
186
+ components.push({ name, status: "configured", ...(notices.length ? { notices } : {}),
187
+ ...(details.backgroundPreference ? { backgroundPreference: details.backgroundPreference } : {}) });
172
188
  return true;
173
189
  } catch (error) { components.push({ name, status: "incomplete", detail: error.message }); return false; }
174
190
  };
175
- const code = install(planned[0], "install-claude-code.mjs", ["--install", "--desktop-config", paths.desktop], {
191
+ const code = install(planned[0], "install-claude-code.mjs", ["--install", "--setup", "--desktop-config", paths.desktop], {
176
192
  ...base, CODEX_BRIDGE_DESKTOP_TASKS: native ? "1" : "0", CODEX_BRIDGE_AUTOSTART: native ? "0" : "1", CODEX_BRIDGE_THREAD_POLICY: "roots",
177
193
  });
178
- if (code) install(planned[1], "install-claude-desktop.mjs", ["--entry-name", "codex-bridge-desktop", "--quiet"]);
194
+ if (code) install(planned[1], "install-claude-desktop.mjs", ["--entry-name", "codex-bridge-desktop", "--quiet", "--setup"]);
179
195
  if (native) {
180
196
  const relay = readJson(path.join(paths.codex, "native-relay.json"));
181
197
  install("Codex / codex-native-relay and executor", "install-native-relay.mjs", ["--setup", ...(relay?.desktopTasks === undefined ? ["--desktop-tasks"] : [])]);
@@ -185,20 +201,24 @@ export function runSetup({ args = [], env = process.env, platform = process.plat
185
201
  const inspection = checkSetup({ env: base, platform, root, run, resolve: () => runtime.binary });
186
202
  return { configured: components.every((item) => item.status === "configured") && inspection.configured, liveVerified: false,
187
203
  cliInstalled: runtime.installed, components, verification: inspection.components,
188
- nextActions: ["Open or reconnect Claude, Codex and Antigravity after active work finishes so they load the new entries.",
189
- "Use the same saved project in Claude Desktop Code and Codex Desktop. Keep the apps signed in.",
190
- "Verify codex_bridge_status in Claude Code, native_relay_status in Codex, and agent_bridge_status in Antigravity. Shared Claude Desktop chat uses the local app-server route."] };
204
+ nextActions: ["Running MCP processes may still hold earlier settings. Setup does not start apps or open conversations.",
205
+ "Existing compatible supervisors reload installed source when idle; configuration changes are not proof of loaded-process readiness.",
206
+ "Native delivery uses the same saved project and signed-in accounts in Claude Desktop Code and Codex Desktop.",
207
+ "codex_bridge_status, native_relay_status and agent_bridge_status report live readiness separately from this configuration check."] };
191
208
  }
192
209
 
193
210
  export function printSetup(result, { json = false, output = console.log } = {}) {
194
211
  if (json) { output(JSON.stringify(result, null, 2)); return; }
195
212
  if (result.help) { output(result.help); return; }
196
213
  if (result.skipped) { output(result.reason); return; }
197
- output(result.dryRun ? "Bridge setup preview (no changes)" : result.configured ? "Bridge configuration verified. Live connections still require the apps to load it." : "Bridge setup is incomplete. Fix the items below, then run codex-bridge-setup again.");
214
+ output(result.dryRun ? "Bridge setup preview (no changes)" : result.configured ? "Bridge configuration verified. Running app connections have not been verified." : "Bridge setup is incomplete. Fix the items below, then run codex-bridge-setup again.");
198
215
  for (const item of result.components ?? []) {
199
216
  output(`[${item.status}] ${item.name}${item.detail ? `: ${item.detail}` : ""}`);
200
217
  for (const notice of item.notices ?? []) output(` ${notice}`);
201
218
  }
202
- for (const item of result.verification ?? []) if (item.status !== "configured") output(`[${item.status}] ${item.name}: ${item.detail}`);
219
+ for (const item of result.verification ?? []) {
220
+ if (item.status !== "configured") output(`[${item.status}] ${item.name}: ${item.detail}`);
221
+ for (const notice of item.notices ?? []) output(`${item.name}: ${notice}`);
222
+ }
203
223
  for (const action of result.nextActions ?? []) output(action);
204
224
  }
@@ -9,6 +9,7 @@ import { captureCodexRolloutWatermark, inspectCodexNativeTurn, readCodexNativeTu
9
9
  import { captureProjectScope, recheckProjectScope } from "./project-scope.mjs";
10
10
  import { isRolloutChangedDuringRead } from "./rollout-segments.mjs";
11
11
  import { requireClaudeSenderContext } from "./claude-sender-context.mjs";
12
+ import { bridgeDiagnostic } from "./recipient-preflight.mjs";
12
13
 
13
14
  /**
14
15
  * Which backend puts a message into a Codex thread.
@@ -74,7 +75,7 @@ export function matchDesktopProject(projects, cwd, { canonicalize = realpathSync
74
75
  return matches[0];
75
76
  }
76
77
 
77
- export const RELAY_DOWN_GUIDANCE = "The Codex Desktop native relay is not listening: 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.";
78
+ export const RELAY_DOWN_GUIDANCE = "The Codex Desktop native relay is not listening after waiting for silent recovery. Its Desktop-owned supervisor restarts crashed relay workers without replaying requests. An unloaded native host cannot be cold-started by this bridge. Inspect codex_bridge_status and include its diagnostic in a report. Never open an app or an old, new, empty or failed chat to load a relay.";
78
79
 
79
80
  function relayUnreachable(error) {
80
81
  return error?.code === "RELAY_UNREACHABLE" && error.sent === false;
@@ -126,23 +127,25 @@ export class DesktopTaskDelivery {
126
127
  if (scopeBinding) this.security.hardenedRoots.recheck(scopeBinding);
127
128
  const projectScope = expectedScope ?? await this.mutationProjectScope(operation, args, { deadline });
128
129
  if (expectedScope) recheckProjectScope(expectedScope, this.senderProjectScope(expectedScope.recipient.input));
129
- const remaining = deadline === undefined ? undefined : deadline - this.now();
130
- if (remaining !== undefined && remaining <= 0) throw Object.assign(new Error("The bridge's response deadline has elapsed; this operation was not sent"), { code: "DESKTOP_RESPONSE_DEADLINE" });
131
130
  const accountContext = this.accountContext?.();
132
- const send = () => this.relay.requestDesktop(operation, args, {
133
- ...(remaining === undefined ? {} : { timeoutMs: remaining }),
134
- ...(this.beforeRequest || projectScope || scopeBinding ? { beforeSend: async () => {
135
- await this.beforeRequest?.({ operation, args, phase: "write" });
136
- if (projectScope) {
137
- recheckProjectScope(projectScope);
138
- recheckProjectScope(projectScope, await this.mutationProjectScope(operation, args, { deadline }));
139
- }
140
- await this.beforeRequest?.({ operation, args, phase: "dispatch" });
141
- if (scopeBinding) this.security.hardenedRoots.recheck(scopeBinding);
142
- } } : {}),
143
- ...(accountContext ? { accountContext } : {}),
144
- ...(creationReceiptId ? { creationReceiptId } : {}),
145
- });
131
+ const send = () => {
132
+ const remaining = deadline === undefined ? undefined : deadline - this.now();
133
+ if (remaining !== undefined && remaining <= 0) throw Object.assign(new Error("The bridge's response deadline has elapsed; this operation was not sent"), { code: "DESKTOP_RESPONSE_DEADLINE" });
134
+ return this.relay.requestDesktop(operation, args, {
135
+ ...(remaining === undefined ? {} : { timeoutMs: remaining }),
136
+ ...(this.beforeRequest || projectScope || scopeBinding ? { beforeSend: async () => {
137
+ await this.beforeRequest?.({ operation, args, phase: "write" });
138
+ if (projectScope) {
139
+ recheckProjectScope(projectScope);
140
+ recheckProjectScope(projectScope, await this.mutationProjectScope(operation, args, { deadline }));
141
+ }
142
+ await this.beforeRequest?.({ operation, args, phase: "dispatch" });
143
+ if (scopeBinding) this.security.hardenedRoots.recheck(scopeBinding);
144
+ } } : {}),
145
+ ...(accountContext ? { accountContext } : {}),
146
+ ...(creationReceiptId ? { creationReceiptId } : {}),
147
+ });
148
+ };
146
149
  let response;
147
150
  try {
148
151
  response = await send();
@@ -194,13 +197,17 @@ export class DesktopTaskDelivery {
194
197
  async status() {
195
198
  const accountContext = this.accountContext?.();
196
199
  const socketPath = this.relay.status?.({ accountContext })?.socketPath ?? this.relay.socketPath;
197
- if (this.accountContext && !accountContext) return { available: false, socketPath, reason: "The current Claude and Codex accounts could not both be verified. Finish signing in and inspect the account diagnostics before sending work." };
200
+ if (this.accountContext && !accountContext) {
201
+ const reason = "The current Claude and Codex accounts could not both be verified. Finish signing in and inspect the account diagnostics before sending work.";
202
+ return { available: false, socketPath, code: "BRIDGE_ACCOUNT_UNVERIFIED", reason, diagnostic: bridgeDiagnostic(new Error(reason), { component: "codex-bridge", operation: "codex_bridge_status", code: "BRIDGE_ACCOUNT_UNVERIFIED", readOnly: true }) };
203
+ }
198
204
  try {
199
205
  const response = await this.request("list_projects", {});
200
206
  if (!Array.isArray(response?.projects)) throw new Error("Desktop returned no project list");
201
207
  return { available: true, socketPath, localProjects: response.projects.filter((project) => project.projectKind === "local" && project.hostId === "local").length };
202
208
  } catch (err) {
203
- return { available: false, socketPath, reason: err.message };
209
+ const diagnostic = bridgeDiagnostic(err, { component: "codex-bridge", operation: "codex_bridge_status", readOnly: true });
210
+ return { available: false, socketPath, code: diagnostic.code, reason: err.message, diagnostic };
204
211
  }
205
212
  }
206
213
 
@@ -334,11 +341,11 @@ export class DesktopTaskDelivery {
334
341
  const confirmedId = response?.threadId ?? response?.conversationId;
335
342
  threadId = confirmedId;
336
343
  if (!threadId && (response?.status === "outcome-unknown" || response?.firstTurn?.status === "outcome-unknown")) threadId = response?.clientThreadId;
337
- if (typeof confirmedId !== "string" || !confirmedId.trim() || response?.status === "outcome-unknown" || response.hostId !== "local") throw new Error(`Desktop creation is not confirmed: ${JSON.stringify(response)}`);
344
+ if (typeof confirmedId !== "string" || !confirmedId.trim() || response?.status === "outcome-unknown" || response.hostId !== "local") throw Object.assign(new Error("Desktop creation is not confirmed; inspect the original native receipt."), { code: "CODEX_CREATION_UNCONFIRMED", deliveryState: "unknown", phase: "creation" });
338
345
  await this.receipts.write(identity.key, { ...receipt, state: "known", threadId });
339
346
  creationConfirmed = true;
340
347
  this.security.registerThread(threadId);
341
- if (response.firstTurn && response.firstTurn.status !== "accepted") throw new Error(`Desktop created task ${threadId}; its first turn reports ${response.firstTurn.status ?? "an unconfirmed state"}: ${JSON.stringify(response)}`);
348
+ if (response.firstTurn && response.firstTurn.status !== "accepted") throw Object.assign(new Error(`Desktop created task ${threadId}; its first turn reports ${["failed", "waitingOnApproval", "waitingOnUserInput", "outcome-unknown"].includes(response.firstTurn.status) ? response.firstTurn.status : "an unconfirmed state"}. Inspect the original native receipt.`), { code: "CODEX_FIRST_TURN_NOT_ACCEPTED", deliveryState: "sent", phase: "first_turn" });
342
349
  const created = { threadId, name, cwd, projectId: project.projectId, projectName: project.label, backend: NATIVE_BACKEND };
343
350
  const delivered = { ...created, previousTurnId: null, deliveryStatus: "accepted", responseObservation: {
344
351
  operation: "create_thread", threadId, previousTurnId: null, expectedCwd: cwd,