@minhspark/codex-mcp-bridge 1.24.1 → 1.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.mjs CHANGED
@@ -41,7 +41,7 @@ import { createRelayWake, relayKeeperEnabled, relayWakeStateFile, startRelayKeep
41
41
 
42
42
  exitForVersionRequest(import.meta.url);
43
43
 
44
- const VERSION = "1.24.1";
44
+ const VERSION = "1.25.1";
45
45
  void import("./telemetry.mjs").then(({ startUsageReporting }) => startUsageReporting({ version: VERSION })).catch(() => {});
46
46
  const log = (msg) => process.stderr.write(`[codex-mcp-bridge] ${msg}\n`);
47
47
 
@@ -42,7 +42,7 @@ import { remoteCodexSocketPath, remoteNativeAdapter, remoteRequestResponse } fro
42
42
 
43
43
  exitForVersionRequest(import.meta.url);
44
44
 
45
- const VERSION = "1.24.1";
45
+ const VERSION = "1.25.1";
46
46
  const log = (msg) => process.stderr.write(`[native-relay] ${msg}\n`);
47
47
 
48
48
  function errorResponse(code, message, sent, details) {
package/src/platform.mjs CHANGED
@@ -1,5 +1,6 @@
1
1
  import { execFile, execFileSync } from "node:child_process";
2
2
  import { accessSync, constants, existsSync, readdirSync, realpathSync, statSync } from "node:fs";
3
+ import { createRequire } from "node:module";
3
4
  import os from "node:os";
4
5
  import path from "node:path";
5
6
  import { fileURLToPath } from "node:url";
@@ -168,16 +169,67 @@ function pathCandidates() {
168
169
  * ~/.local/bin, ~/.npm-global/bin or Homebrew. Probe every known install
169
170
  * location for the running platform before giving up on PATH resolution.
170
171
  */
171
- export function resolveCodexBin(explicit) {
172
+ export function resolvePackagedCodexBin({
173
+ platform = process.platform,
174
+ arch = process.arch,
175
+ resolvePackage = (specifier, from) => createRequire(from).resolve(specifier),
176
+ exists = isRunnable,
177
+ } = {}) {
178
+ const packagePlatform = platform === "android" ? "linux" : platform;
179
+ const target = {
180
+ "linux-x64": "x86_64-unknown-linux-musl",
181
+ "linux-arm64": "aarch64-unknown-linux-musl",
182
+ "darwin-x64": "x86_64-apple-darwin",
183
+ "darwin-arm64": "aarch64-apple-darwin",
184
+ "win32-x64": "x86_64-pc-windows-msvc",
185
+ "win32-arm64": "aarch64-pc-windows-msvc",
186
+ }[`${packagePlatform}-${arch}`];
187
+ if (!target) return null;
188
+
189
+ let codexManifest;
190
+ try { codexManifest = resolvePackage("@openai/codex/package.json", import.meta.url); } catch {}
191
+ if (typeof codexManifest !== "string" || !path.isAbsolute(codexManifest)) codexManifest = null;
192
+ const packageName = `@openai/codex-${packagePlatform}-${arch}/package.json`;
193
+ const roots = [];
194
+ for (const anchor of [codexManifest, import.meta.url].filter(Boolean)) {
195
+ try {
196
+ const manifest = resolvePackage(packageName, anchor);
197
+ if (typeof manifest === "string" && path.isAbsolute(manifest)) roots.push(path.dirname(manifest));
198
+ } catch {}
199
+ }
200
+ if (codexManifest) roots.push(path.dirname(codexManifest));
201
+ for (const root of new Set(roots)) {
202
+ const candidate = path.join(root, "vendor", target, "bin", platform === "win32" ? "codex.exe" : "codex");
203
+ if (exists(candidate)) return candidate;
204
+ }
205
+ return null;
206
+ }
207
+
208
+ export function resolveCodexBin(explicit, {
209
+ env = process.env,
210
+ platform = process.platform,
211
+ arch = process.arch,
212
+ candidates: discovered,
213
+ runnable = isRunnable,
214
+ packaged = resolvePackagedCodexBin,
215
+ } = {}) {
172
216
  const candidates = [
173
217
  explicit,
174
- process.env.CODEX_BIN,
175
- ...(IS_WINDOWS ? windowsCodexCandidates() : IS_MACOS ? macosCodexCandidates() : unixCodexCandidates()),
176
- ...pathCandidates(),
218
+ env.CODEX_BIN,
219
+ ...(discovered ?? [
220
+ ...(IS_WINDOWS ? windowsCodexCandidates() : IS_MACOS ? macosCodexCandidates() : unixCodexCandidates()),
221
+ ...pathCandidates(),
222
+ ]),
177
223
  ].filter(Boolean);
178
224
 
225
+ const deferredShim = (candidate) => platform === "win32" && /\.(?:cmd|bat)$/i.test(candidate);
226
+ for (const candidate of candidates) {
227
+ if (!deferredShim(candidate) && runnable(candidate)) return candidate;
228
+ }
229
+ const bundled = packaged({ platform, arch });
230
+ if (bundled) return bundled;
179
231
  for (const candidate of candidates) {
180
- if (isRunnable(candidate)) return candidate;
232
+ if (deferredShim(candidate) && runnable(candidate)) return candidate;
181
233
  }
182
234
  return "codex";
183
235
  }
@@ -49,7 +49,9 @@ function sameProject(senderCwd, taskCwd) {
49
49
  export function relayKeeperEnabled(env = process.env, platform = process.platform) {
50
50
  const value = (env.CODEX_BRIDGE_RELAY_KEEPER ?? "").toLowerCase();
51
51
  if (["0", "off", "false"].includes(value)) return false;
52
- return relayWakeEnabled(env, platform);
52
+ if (!relayWakeEnabled(env, platform)) return false;
53
+ if (platform === "win32") return ["1", "on", "true"].includes(value);
54
+ return true;
53
55
  }
54
56
 
55
57
  export function relayWakeStateFile(env = process.env) {
package/src/setup.mjs ADDED
@@ -0,0 +1,204 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import { createRequire } from "node:module";
5
+ import { spawnSync } from "node:child_process";
6
+ import { fileURLToPath } from "node:url";
7
+ import { resolveCodexBin, resolvePackagedCodexBin } from "./platform.mjs";
8
+ import { readRegularConfigFile } from "./project-policy.mjs";
9
+ import { stableInstallPath } from "./install-paths.mjs";
10
+ import { resolveClaudeDesktopConfigRoot } from "./desktop-account-context.mjs";
11
+
12
+ const ROOT = stableInstallPath(path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."));
13
+ const CODEX_PACKAGE = "@openai/codex@0.166.0";
14
+ const MAX_CONFIG = 16 * 1024 * 1024;
15
+
16
+ export function setupArguments(args) {
17
+ const allowed = new Set(["--postinstall", "--check", "--dry-run", "--json", "--no-download", "--help"]);
18
+ if (args.some((arg) => !allowed.has(arg))) throw new Error("Unknown setup option; use codex-bridge-setup --help");
19
+ if (args.includes("--check") && args.includes("--dry-run")) throw new Error("Use --check or --dry-run, not both");
20
+ return Object.fromEntries([...allowed].map((arg) => [arg.slice(2).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase()), args.includes(arg)]));
21
+ }
22
+
23
+ export function automaticSetupEnabled(env = process.env) {
24
+ return env.npm_config_global === "true" && env.CODEX_BRIDGE_SKIP_SETUP !== "1";
25
+ }
26
+
27
+ export function setupPaths(env = process.env, platform = process.platform) {
28
+ const home = env.HOME || env.USERPROFILE || os.homedir();
29
+ const codeDirectory = env.CLAUDE_CONFIG_DIR || home;
30
+ const configRoot = env.XDG_CONFIG_HOME || path.join(home, ".config");
31
+ const desktop = env.CLAUDE_DESKTOP_CONFIG || path.join(resolveClaudeDesktopConfigRoot({ env, platform }), "claude_desktop_config.json");
32
+ return { home, code: path.join(codeDirectory, ".claude.json"), desktop,
33
+ codex: env.CODEX_HOME || path.join(home, ".codex"),
34
+ gemini: path.join(home, ".gemini", "config"), tools: path.join(configRoot, "GptClaudeBridge", "tools") };
35
+ }
36
+
37
+ function readJson(file) {
38
+ const text = readRegularConfigFile(file, { missing: true, maxBytes: MAX_CONFIG });
39
+ if (text === null) return null;
40
+ const value = JSON.parse(text.replace(/^\uFEFF/, ""));
41
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`Expected a JSON object: ${file}`);
42
+ return value;
43
+ }
44
+
45
+ function execute(command, args, env, timeout = 120000) {
46
+ const result = spawnSync(command, args, { env, encoding: "utf8", windowsHide: true, shell: false, timeout,
47
+ maxBuffer: 4 * 1024 * 1024, stdio: ["ignore", "pipe", "pipe"] });
48
+ if (result.error || result.status !== 0) {
49
+ const lines = String(result.stderr || "").split(/\r?\n/).filter((line) => /^Error:|^(?:Expected|Refusing|Invalid|Unknown|Shared|Claude|Codex|Configuration|The existing|Missing|Unsafe)/.test(line)
50
+ && !/Command failed|--env|password|secret|token|authorization/i.test(line));
51
+ const detail = lines.slice(0, 3).join(" ") || result.error?.code || `exit ${result.status}`;
52
+ throw new Error(`${path.basename(command)} ${path.basename(args[0] ?? "")}: ${detail}`);
53
+ }
54
+ return result.stdout || "";
55
+ }
56
+
57
+ function usableBinary(binary, platform) {
58
+ return typeof binary === "string" && path.isAbsolute(binary) && fs.existsSync(binary)
59
+ && fs.statSync(binary).isFile() && (platform !== "win32" || /\.(exe|com)$/i.test(binary));
60
+ }
61
+
62
+ function managedBinary(paths, platform) {
63
+ return resolvePackagedCodexBin({ platform, resolvePackage: (specifier, from) => createRequire(
64
+ specifier === "@openai/codex/package.json" ? path.join(paths.tools, "package.json") : from,
65
+ ).resolve(specifier) });
66
+ }
67
+
68
+ export function ensureSetupCodex({ env, paths, platform, download = true, run = execute, resolve = resolveCodexBin }) {
69
+ const existing = resolve(env.CODEX_EXE, { env, platform });
70
+ if (usableBinary(existing, platform)) return { binary: existing, installed: false };
71
+ const managed = managedBinary(paths, platform);
72
+ if (usableBinary(managed, platform)) return { binary: managed, installed: false };
73
+ if (!download) throw new Error("Codex CLI was not found. Rerun setup without --no-download to install it automatically.");
74
+ const candidates = [env.npm_execpath, path.join(path.dirname(process.execPath), "node_modules", "npm", "bin", "npm-cli.js")];
75
+ const npm = candidates.find((file) => file && path.isAbsolute(file) && fs.existsSync(file) && /(?:npm-cli|npm)\.js$/i.test(file));
76
+ if (!npm) throw new Error("Codex CLI is missing and npm could not be located. Rerun the global npm install command to finish setup.");
77
+ fs.mkdirSync(paths.tools, { recursive: true, mode: 0o700 });
78
+ run(process.execPath, [npm, "install", "--global=false", "--prefix", paths.tools, "--ignore-scripts", "--no-audit", "--no-fund", "--no-package-lock", "--no-save", CODEX_PACKAGE],
79
+ { ...env, npm_config_global: "false", CODEX_BRIDGE_SKIP_SETUP: "1" }, 180000);
80
+ const binary = managedBinary(paths, platform);
81
+ if (!usableBinary(binary, platform)) throw new Error("The Codex CLI download did not provide a supported native executable. Check npm optional dependencies and rerun setup.");
82
+ return { binary, installed: true };
83
+ }
84
+
85
+ export function setupEnvironment(env, paths, codex) {
86
+ const result = Object.fromEntries(Object.entries(env).filter(([name]) => !/^(?:CODEX_BRIDGE_|CLAUDE_BRIDGE_|CODEX_NATIVE_RELAY_|CODEX_RELAY_ID$|CODEX_THREAD_ID$|CODEX_APP_TOOLS_PIPE_PATH$)/.test(name)));
87
+ return { ...result, HOME: paths.home, USERPROFILE: paths.home, CODEX_HOME: paths.codex,
88
+ CLAUDE_DESKTOP_CONFIG: paths.desktop, CODEX_EXE: codex, CODEX_BIN: codex };
89
+ }
90
+
91
+ function configurationCheck(label, check, components) {
92
+ try { check(); components.push({ name: label, status: "configured" }); }
93
+ catch (error) { components.push({ name: label, status: "incomplete", detail: error.message }); }
94
+ }
95
+
96
+ function assertEntry(entry, worker, root, { caller } = {}) {
97
+ if (!entry || typeof entry.command !== "string" || !fs.existsSync(entry.command)) throw new Error("MCP command is missing or unavailable");
98
+ const args = entry.args ?? [];
99
+ const expected = worker === "index.mjs" || worker === "claude-bridge.mjs" || worker === "native-relay-companion.mjs"
100
+ ? [path.join(root, "src", "mcp-supervisor.mjs"), worker] : [path.join(root, "src", worker)];
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
+ if (caller && entry.env?.ANTIGRAVITY_BRIDGE_CALLER !== caller) throw new Error("Antigravity caller registration is missing or conflicting");
104
+ }
105
+
106
+ export function checkSetup({ env = process.env, platform = process.platform, root = ROOT, run = execute, resolve = resolveCodexBin } = {}) {
107
+ const paths = setupPaths(env, platform);
108
+ const components = [];
109
+ let codex;
110
+ let inventory;
111
+ configurationCheck("Codex CLI", () => {
112
+ codex = ensureSetupCodex({ env, paths, platform, download: false, run, resolve }).binary;
113
+ inventory = JSON.parse(run(codex, ["mcp", "list", "--json"], setupEnvironment(env, paths, codex), 30000));
114
+ if (!Array.isArray(inventory)) throw new Error("Codex returned an invalid MCP inventory");
115
+ }, components);
116
+ const native = platform === "win32" || platform === "darwin";
117
+ for (const [name, worker] of [["claude-bridge", "claude-bridge.mjs"], ...(native ? [["codex-native-relay", "native-relay-companion.mjs"]] : []), ["antigravity-bridge", "antigravity-bridge.mjs"]]) {
118
+ configurationCheck(`Codex / ${name}`, () => {
119
+ const item = inventory?.find((server) => server.name === name);
120
+ if (!item) throw new Error("MCP registration is missing");
121
+ 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
+ if (name === "codex-native-relay" && !entry.transport.env_vars?.includes("CODEX_APP_TOOLS_PIPE_PATH")) throw new Error("Native pipe forwarding is missing");
124
+ }, components);
125
+ }
126
+ for (const [label, file, name, worker, caller] of [
127
+ ["Claude Code / codex-bridge", paths.code, "codex-bridge", "index.mjs"],
128
+ ["Claude Desktop / codex-bridge-desktop", paths.desktop, "codex-bridge-desktop", "index.mjs"],
129
+ ["Claude Code / antigravity-bridge", paths.code, "antigravity-bridge", "antigravity-bridge.mjs", "claude"],
130
+ ["Antigravity / agent-bridge", path.join(paths.gemini, "mcp_config.json"), "agent-bridge", "antigravity-outbound-bridge.mjs"],
131
+ ]) configurationCheck(label, () => {
132
+ const entry = readJson(file)?.mcpServers?.[name];
133
+ assertEntry(entry, worker, root, { caller });
134
+ 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.");
136
+ }
137
+ }, components);
138
+ 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");
141
+ }, components);
142
+ if (native) configurationCheck("Native relay executor", () => {
143
+ const relay = readJson(path.join(paths.codex, "native-relay.json"));
144
+ if (!relay?.relayThreadId) throw new Error("Native relay executor has not been initialized");
145
+ }, components);
146
+ configurationCheck("Claude registration names", () => {
147
+ if (readJson(paths.desktop)?.mcpServers?.["codex-bridge"]) throw new Error("Shared codex-bridge shadows the Claude Code registration");
148
+ }, components);
149
+ return { configured: components.every((item) => item.status === "configured"), liveVerified: false, components };
150
+ }
151
+
152
+ export function runSetup({ args = [], env = process.env, platform = process.platform, root = ROOT, run = execute, resolve = resolveCodexBin } = {}) {
153
+ 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." };
155
+ 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
+ if (options.check) return checkSetup({ env, platform, root, run, resolve });
157
+ const native = platform === "win32" || platform === "darwin";
158
+ const paths = setupPaths(env, platform);
159
+ const planned = ["Claude Code / codex-bridge", "Claude Desktop / codex-bridge-desktop", ...(native ? ["Codex / codex-native-relay and executor"] : []), "Codex / claude-bridge", "Antigravity / all components"];
160
+ if (options.dryRun) return { dryRun: true, configured: false, liveVerified: false, components: planned.map((name) => ({ name, status: "planned" })), files: [paths.code, paths.desktop, path.join(paths.codex, "config.toml"), paths.gemini] };
161
+ if (env.SUDO_USER && env.SUDO_USER !== "root" && typeof process.getuid === "function" && process.getuid() === 0) throw new Error("Run the global install without sudo in the account that runs the apps; setup will not write another account's configuration.");
162
+ const runtime = ensureSetupCodex({ env, paths, platform, download: !options.noDownload, run, resolve });
163
+ fs.mkdirSync(paths.codex, { recursive: true, mode: 0o700 });
164
+ const base = setupEnvironment(env, paths, runtime.binary);
165
+ const components = [];
166
+ const install = (name, script, arguments_, environment = base) => {
167
+ try {
168
+ 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 } : {}) });
172
+ return true;
173
+ } catch (error) { components.push({ name, status: "incomplete", detail: error.message }); return false; }
174
+ };
175
+ const code = install(planned[0], "install-claude-code.mjs", ["--install", "--desktop-config", paths.desktop], {
176
+ ...base, CODEX_BRIDGE_DESKTOP_TASKS: native ? "1" : "0", CODEX_BRIDGE_AUTOSTART: native ? "0" : "1", CODEX_BRIDGE_THREAD_POLICY: "roots",
177
+ });
178
+ if (code) install(planned[1], "install-claude-desktop.mjs", ["--entry-name", "codex-bridge-desktop", "--quiet"]);
179
+ if (native) {
180
+ const relay = readJson(path.join(paths.codex, "native-relay.json"));
181
+ install("Codex / codex-native-relay and executor", "install-native-relay.mjs", ["--setup", ...(relay?.desktopTasks === undefined ? ["--desktop-tasks"] : [])]);
182
+ }
183
+ install("Codex / claude-bridge", "install-codex-mcp.mjs", ["--setup"]);
184
+ if (code) install("Antigravity / all components", "install-antigravity-bridge.mjs", ["--all", "--setup"]);
185
+ const inspection = checkSetup({ env: base, platform, root, run, resolve: () => runtime.binary });
186
+ return { configured: components.every((item) => item.status === "configured") && inspection.configured, liveVerified: false,
187
+ 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."] };
191
+ }
192
+
193
+ export function printSetup(result, { json = false, output = console.log } = {}) {
194
+ if (json) { output(JSON.stringify(result, null, 2)); return; }
195
+ if (result.help) { output(result.help); return; }
196
+ 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.");
198
+ for (const item of result.components ?? []) {
199
+ output(`[${item.status}] ${item.name}${item.detail ? `: ${item.detail}` : ""}`);
200
+ for (const notice of item.notices ?? []) output(` ${notice}`);
201
+ }
202
+ for (const item of result.verification ?? []) if (item.status !== "configured") output(`[${item.status}] ${item.name}: ${item.detail}`);
203
+ for (const action of result.nextActions ?? []) output(action);
204
+ }
@@ -31,6 +31,7 @@ export const OBSERVATION_UNAVAILABLE_GUIDANCE = "The response could not be verif
31
31
  export const WORKSPACE_UNASSIGNED = "DESKTOP_WORKSPACE_UNASSIGNED";
32
32
  export const UNASSIGNED_WORKSPACE_GRACE_MS = 120000;
33
33
  const RELEASE_STATUSES = new Set(["completed", "interrupted", "failed"]);
34
+ const UNMATCHED_REPLY_GRACE_MS = 3000;
34
35
  const METADATA_READ_OPTIONS = { includeOutputs: false, maxOutputCharsPerItem: 1 };
35
36
 
36
37
  function unavailableWaitRenderer(error) {
@@ -573,6 +574,7 @@ export class DesktopTaskDelivery {
573
574
  let readSnapshots = false;
574
575
  let deferredReason = null;
575
576
  let deferredTurn = null;
577
+ let unmatchedTerminal = null;
576
578
  const readSnapshot = async () => {
577
579
  const result = await this.request("read_thread", { threadId, hostId: "local", turnLimit: 1, ...METADATA_READ_OPTIONS }, { deadline: startedAt + timeoutMs });
578
580
  const thread = result?.thread;
@@ -615,6 +617,7 @@ export class DesktopTaskDelivery {
615
617
  } catch (err) {
616
618
  if (deadlineError(err)) return expired();
617
619
  if (err.code === WORKSPACE_UNASSIGNED) {
620
+ unmatchedTerminal = null;
618
621
  await pause();
619
622
  continue;
620
623
  }
@@ -640,6 +643,7 @@ export class DesktopTaskDelivery {
640
643
  const observed = await observe(null, true);
641
644
  if (!observed) return expired();
642
645
  if (observed === observeLater) {
646
+ unmatchedTerminal = null;
643
647
  await pause();
644
648
  continue;
645
649
  }
@@ -658,6 +662,34 @@ export class DesktopTaskDelivery {
658
662
  };
659
663
  }
660
664
  if (needsAttention) return attention();
665
+ if (turnId === null && observed.dispatchObserved !== true && threadStatus === "idle" &&
666
+ typeof turn?.id === "string" && turn.id && turn.id !== previousTurnId && RELEASE_STATUSES.has(turn.status)) {
667
+ if (unmatchedTerminal?.id !== turn.id || unmatchedTerminal.status !== turn.status) {
668
+ unmatchedTerminal = { id: turn.id, status: turn.status, since: this.now(), polls: 1 };
669
+ } else unmatchedTerminal.polls++;
670
+ if (unmatchedTerminal.polls >= 3 && this.now() - unmatchedTerminal.since >= UNMATCHED_REPLY_GRACE_MS) {
671
+ let snapshot;
672
+ try { snapshot = (await readSnapshot()).polls[0]; }
673
+ catch (error) {
674
+ if (deadlineError(error)) return expired();
675
+ if (error.code === WORKSPACE_UNASSIGNED) {
676
+ unmatchedTerminal = null;
677
+ await pause();
678
+ continue;
679
+ }
680
+ throw error;
681
+ }
682
+ const status = typeof snapshot.thread.status === "string" ? snapshot.thread.status : snapshot.thread.status?.type;
683
+ if (status === "idle" && snapshot.latestTurn?.id === unmatchedTerminal.id && snapshot.latestTurn.status === unmatchedTerminal.status) {
684
+ return {
685
+ threadId, turnId: null, status: "observation_unavailable", responseStatus: "unavailable", observationStatus: "unavailable",
686
+ observationReason: "Codex Desktop reports an idle task, but its latest finished turn could not be correlated to this delivery after the rollout grace period. Inspect the existing task and receipt; do not resend the prompt.",
687
+ text: "", assistantItems: [], replySha256: null, responseSource: null, activity: [], errors: [], durationMs: this.now() - startedAt,
688
+ };
689
+ }
690
+ unmatchedTerminal = null;
691
+ }
692
+ } else unmatchedTerminal = null;
661
693
  if (this.now() - startedAt >= timeoutMs) return expired();
662
694
  await this.sleep(Math.min(1500, timeoutMs - (this.now() - startedAt)));
663
695
  continue;