@bitkyc08/opencodex 2.54.0 → 2.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/{index-CkvITofZ.js → index-VuoiWj9J.js} +10 -10
- package/gui/dist/index.html +1 -1
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +57 -0
- package/src/adapters/anthropic-image-normalize.ts +28 -1
- package/src/adapters/anthropic.ts +68 -6
- package/src/adapters/base.ts +8 -0
- package/src/adapters/coding-agent/protocol.ts +41 -16
- package/src/adapters/cursor/cursor-errors.ts +1 -1
- package/src/adapters/cursor/live-transport.ts +5 -1
- package/src/adapters/cursor/native-exec-fs.ts +10 -10
- package/src/adapters/cursor/native-exec-network.ts +2 -2
- package/src/adapters/cursor/native-exec-shell.ts +13 -12
- package/src/adapters/cursor/native-exec.ts +51 -10
- package/src/adapters/cursor/policy-error.ts +75 -0
- package/src/adapters/cursor/protobuf-request.ts +105 -1
- package/src/adapters/devin/cloud-direct/catalog.ts +34 -2
- package/src/adapters/devin/live-models.ts +33 -2
- package/src/adapters/google-wire-compiler.ts +8 -0
- package/src/adapters/google.ts +46 -0
- package/src/adapters/input-media-guard.ts +45 -0
- package/src/adapters/kiro/adapter.ts +8 -0
- package/src/adapters/kiro/payload.ts +28 -6
- package/src/adapters/kiro-events.ts +25 -6
- package/src/adapters/kiro-images.ts +30 -0
- package/src/adapters/kiro-retry.ts +8 -0
- package/src/adapters/openai-chat.ts +33 -4
- package/src/adapters/openai-responses.ts +26 -0
- package/src/adapters/registry.ts +4 -0
- package/src/bridge.ts +163 -116
- package/src/chat/image-parts.ts +151 -0
- package/src/chat/inbound.ts +70 -33
- package/src/cli/connect.ts +30 -9
- package/src/cli/dispatch.ts +7 -3
- package/src/cli/index.ts +3 -0
- package/src/cli/runtime-api.ts +25 -0
- package/src/cli/status.ts +21 -19
- package/src/cli/system-restart-client.ts +25 -0
- package/src/clients/config-export.ts +14 -4
- package/src/codex/app-server-processes.ts +25 -0
- package/src/codex/auth-context.ts +8 -0
- package/src/codex/autostart-health.ts +36 -2
- package/src/codex/catalog/provider-fetch.ts +41 -0
- package/src/codex/catalog-auto-refresh.ts +182 -0
- package/src/codex/catalog-refresh-status.ts +93 -0
- package/src/codex/history-provider.ts +55 -0
- package/src/codex/model-entitlements.ts +78 -0
- package/src/codex/native-profile-processes.ts +114 -15
- package/src/codex/prompt-text-probe.ts +274 -41
- package/src/codex/routing-adoption.ts +189 -0
- package/src/codex/routing.ts +520 -48
- package/src/codex/runtime.ts +249 -7
- package/src/combos/failover.ts +45 -0
- package/src/config.ts +124 -4
- package/src/generated/compatibility-version.json +110 -74
- package/src/generated/model-metadata.ts +1 -0
- package/src/lib/request-execution-budget.ts +202 -0
- package/src/lib/upstream-retry.ts +95 -8
- package/src/lib/workflow-budget.ts +172 -0
- package/src/oauth/devin.ts +57 -12
- package/src/providers/quota.ts +37 -6
- package/src/providers/registry.ts +53 -6
- package/src/responses/input-media.ts +65 -0
- package/src/responses/parser-content.ts +42 -0
- package/src/responses/schema.ts +12 -2
- package/src/server/audio-live.ts +1 -2
- package/src/server/audio-transcriptions.ts +1 -2
- package/src/server/auth-cors.ts +1 -1
- package/src/server/background-lifecycle.ts +18 -0
- package/src/server/chat-completions.ts +23 -8
- package/src/server/chat-native.ts +17 -17
- package/src/server/index.ts +24 -0
- package/src/server/management/request-history-routes.ts +5 -0
- package/src/server/request-log.ts +8 -2
- package/src/server/responses/compact.ts +51 -3
- package/src/server/responses/core.ts +238 -28
- package/src/server/search.ts +7 -9
- package/src/types/config.ts +51 -6
- package/src/usage/log.ts +37 -0
- package/src/vision/eligibility.ts +37 -4
- package/src/vision/index.ts +1 -0
- package/src/vision/plan.ts +45 -10
- package/src/web-search/alpha-search.ts +324 -0
- package/src/web-search/index.ts +13 -22
- package/src/web-search/passthrough-bridge.ts +195 -22
- package/src/web-search/sidecar-providers.ts +22 -0
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
2
|
import { basename } from "node:path";
|
|
3
3
|
import { resolveTrustedWindowsPowerShellExe } from "../lib/windows-elevation";
|
|
4
|
+
import {
|
|
5
|
+
listProcessSnapshots,
|
|
6
|
+
tokenizeCommandLine,
|
|
7
|
+
type ProcessSnapshot,
|
|
8
|
+
} from "./app-server-processes";
|
|
4
9
|
|
|
5
10
|
const PROCESS_LIST_MAX_BUFFER = 16 * 1024 * 1024;
|
|
6
11
|
const DIRECT_CODEX_BASENAMES = new Set(["codex", "codex.exe"]);
|
|
@@ -39,6 +44,114 @@ export type NativeCodexProcessProbe =
|
|
|
39
44
|
| { status: "busy"; count: number }
|
|
40
45
|
| { status: "unknown"; count: 0 };
|
|
41
46
|
|
|
47
|
+
export interface CodexClientProcess {
|
|
48
|
+
pid: number;
|
|
49
|
+
commandLine: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Synchronous Codex-CLI process list for routing-adoption (#4550).
|
|
54
|
+
*
|
|
55
|
+
* "enumerated" is a successful read, including the empty list, which means
|
|
56
|
+
* no matching client is running. "unavailable" means we could not name PIDs:
|
|
57
|
+
* a failed snapshot walk. The two must not collapse. An empty array is
|
|
58
|
+
* "none running" and would otherwise produce a false adopted. Windows is
|
|
59
|
+
* enumerable here: listProcessSnapshots already returns CLI command lines.
|
|
60
|
+
* Do not route through listCodexAppServerProcesses; that maps a failed
|
|
61
|
+
* walk to [] for the #476 kill contract.
|
|
62
|
+
*/
|
|
63
|
+
export type CodexClientProcessList =
|
|
64
|
+
| { status: "enumerated"; processes: CodexClientProcess[] }
|
|
65
|
+
| { status: "unavailable" };
|
|
66
|
+
|
|
67
|
+
export interface ListCodexClientProcessesOptions {
|
|
68
|
+
platform?: NodeJS.Platform;
|
|
69
|
+
pid?: number;
|
|
70
|
+
getuid?: () => number | undefined;
|
|
71
|
+
/** Test seam: raw snapshots. Enumeration failure is thrown, not []. */
|
|
72
|
+
listSnapshots?: () => ProcessSnapshot[];
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* True when a ps comm field plus args string is a Codex CLI client.
|
|
77
|
+
*
|
|
78
|
+
* Direct "codex" / "codex.exe" basenames, or a known interpreter whose
|
|
79
|
+
* immediate entrypoint is a Codex CLI script. The busy-count probe and the
|
|
80
|
+
* routing-adoption lister both call this so the rules cannot drift (#2457).
|
|
81
|
+
*/
|
|
82
|
+
export function isCodexClientProcess(command: string, args: string): boolean {
|
|
83
|
+
const comm = basename(command).toLowerCase();
|
|
84
|
+
const [rawArgv0 = "", rawEntrypoint = ""] = args.trim().split(/\s+/, 2);
|
|
85
|
+
const argv0 = basename(rawArgv0).toLowerCase();
|
|
86
|
+
const entrypoint = basename(rawEntrypoint).toLowerCase();
|
|
87
|
+
const isDirectCodex = DIRECT_CODEX_BASENAMES.has(comm)
|
|
88
|
+
|| DIRECT_CODEX_BASENAMES.has(argv0);
|
|
89
|
+
const isInterpreterWrappedCodex = CODEX_INTERPRETER_BASENAMES.has(argv0)
|
|
90
|
+
&& CODEX_ENTRYPOINT_BASENAMES.has(entrypoint);
|
|
91
|
+
return isDirectCodex || isInterpreterWrappedCodex;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function parseUnixPsLine(line: string): { pid: number; command: string; args: string } | null {
|
|
95
|
+
const match = line.trim().match(/^(\d+)\s+(\S+)\s*(.*)$/);
|
|
96
|
+
if (!match) return null;
|
|
97
|
+
const pid = Number(match[1]);
|
|
98
|
+
if (!Number.isSafeInteger(pid)) return null;
|
|
99
|
+
return { pid, command: match[2]!, args: match[3]! };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function unixCodexClientsFromPs(output: string, selfPid: number): CodexClientProcess[] {
|
|
103
|
+
const processes: CodexClientProcess[] = [];
|
|
104
|
+
for (const line of output.split("\n")) {
|
|
105
|
+
const parsed = parseUnixPsLine(line);
|
|
106
|
+
if (!parsed || parsed.pid === selfPid) continue;
|
|
107
|
+
if (!isCodexClientProcess(parsed.command, parsed.args)) continue;
|
|
108
|
+
processes.push({
|
|
109
|
+
pid: parsed.pid,
|
|
110
|
+
commandLine: parsed.args.trim() || parsed.command,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return processes;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Name running Codex CLI processes so status can tell a pre-injection client
|
|
118
|
+
* from one that had a chance to read the injected route (#4550).
|
|
119
|
+
*
|
|
120
|
+
* probeNativeCodexProcesses is async and count-only; collectStartupHealth is
|
|
121
|
+
* synchronous, so this path cannot reuse it. Snapshots come from
|
|
122
|
+
* listProcessSnapshots so Windows CLI PIDs are named too; a thrown walk
|
|
123
|
+
* stays unavailable rather than an empty adopted list.
|
|
124
|
+
*/
|
|
125
|
+
export function listCodexClientProcesses({
|
|
126
|
+
platform = process.platform,
|
|
127
|
+
pid = process.pid,
|
|
128
|
+
getuid,
|
|
129
|
+
listSnapshots,
|
|
130
|
+
}: ListCodexClientProcessesOptions = {}): CodexClientProcessList {
|
|
131
|
+
try {
|
|
132
|
+
const snapshots = listSnapshots
|
|
133
|
+
? listSnapshots()
|
|
134
|
+
: listProcessSnapshots({ platform, getuid });
|
|
135
|
+
const seen = new Set<number>();
|
|
136
|
+
const processes: CodexClientProcess[] = [];
|
|
137
|
+
for (const snapshot of snapshots) {
|
|
138
|
+
if (snapshot.pid === pid || seen.has(snapshot.pid)) continue;
|
|
139
|
+
if (!snapshotIsCodexClient(snapshot)) continue;
|
|
140
|
+
seen.add(snapshot.pid);
|
|
141
|
+
processes.push({ pid: snapshot.pid, commandLine: snapshot.commandLine });
|
|
142
|
+
}
|
|
143
|
+
return { status: "enumerated", processes };
|
|
144
|
+
} catch {
|
|
145
|
+
return { status: "unavailable" };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function snapshotIsCodexClient(snapshot: ProcessSnapshot): boolean {
|
|
150
|
+
if (isCodexClientProcess(snapshot.executable ?? "", snapshot.commandLine)) return true;
|
|
151
|
+
const argv0 = tokenizeCommandLine(snapshot.commandLine)[0] ?? "";
|
|
152
|
+
return argv0 !== "" && isCodexClientProcess(argv0, snapshot.commandLine);
|
|
153
|
+
}
|
|
154
|
+
|
|
42
155
|
/** Async, shell-free child execution with runtime-enforced timeout and output bounds. */
|
|
43
156
|
export const executeNativeProcess: NativeProcessExecutor = (file, args, options) => new Promise((resolve, reject) => {
|
|
44
157
|
execFile(file, args, {
|
|
@@ -87,21 +200,7 @@ async function unixProcessCount(run: NativeProcessExecutor, pid: number): Promis
|
|
|
87
200
|
shell: false,
|
|
88
201
|
killSignal: "SIGKILL",
|
|
89
202
|
});
|
|
90
|
-
|
|
91
|
-
for (const line of output.split("\n")) {
|
|
92
|
-
const match = line.trim().match(/^(\d+)\s+(\S+)\s*(.*)$/);
|
|
93
|
-
if (!match || Number(match[1]) === pid) continue;
|
|
94
|
-
const command = basename(match[2]!).toLowerCase();
|
|
95
|
-
const [rawArgv0 = "", rawEntrypoint = ""] = match[3]!.trim().split(/\s+/, 2);
|
|
96
|
-
const argv0 = basename(rawArgv0).toLowerCase();
|
|
97
|
-
const entrypoint = basename(rawEntrypoint).toLowerCase();
|
|
98
|
-
const isDirectCodex = DIRECT_CODEX_BASENAMES.has(command)
|
|
99
|
-
|| DIRECT_CODEX_BASENAMES.has(argv0);
|
|
100
|
-
const isInterpreterWrappedCodex = CODEX_INTERPRETER_BASENAMES.has(argv0)
|
|
101
|
-
&& CODEX_ENTRYPOINT_BASENAMES.has(entrypoint);
|
|
102
|
-
if (isDirectCodex || isInterpreterWrappedCodex) count += 1;
|
|
103
|
-
}
|
|
104
|
-
return count;
|
|
203
|
+
return unixCodexClientsFromPs(output, pid).length;
|
|
105
204
|
}
|
|
106
205
|
|
|
107
206
|
/** Best-effort, read-only process probe. It never terminates a user process. */
|
|
@@ -19,8 +19,15 @@
|
|
|
19
19
|
import { spawn } from "node:child_process";
|
|
20
20
|
import { existsSync, statSync } from "node:fs";
|
|
21
21
|
import { join } from "node:path";
|
|
22
|
-
import {
|
|
22
|
+
import { codexExecInvocation } from "./exec-invocation";
|
|
23
23
|
import { resolveCodexHomeDir } from "./home";
|
|
24
|
+
import {
|
|
25
|
+
CODEX_PROGRAM_NOT_FOUND_REASON,
|
|
26
|
+
displayCodexRuntimePath,
|
|
27
|
+
resolveCodexRuntime,
|
|
28
|
+
type CodexRuntimeSource,
|
|
29
|
+
type ResolveCodexRuntimeResult,
|
|
30
|
+
} from "./runtime";
|
|
24
31
|
|
|
25
32
|
/**
|
|
26
33
|
* Layer id -> the tag Codex actually renders it under.
|
|
@@ -79,27 +86,49 @@ export interface LayerText {
|
|
|
79
86
|
sourcePath?: string;
|
|
80
87
|
}
|
|
81
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Why a probe failed, as a stable token the caller can branch on. The prose
|
|
91
|
+
* `detail` string is kept for display, but matching on it was never a contract:
|
|
92
|
+
* a caller that needs "is Codex installed at all" versus "Codex rejected the
|
|
93
|
+
* command" cannot get that from a sentence.
|
|
94
|
+
*/
|
|
95
|
+
export type PromptProbeFailureKind =
|
|
96
|
+
| "program-not-found"
|
|
97
|
+
| "command-unsupported"
|
|
98
|
+
| "execution-failed"
|
|
99
|
+
| "output-invalid";
|
|
100
|
+
|
|
101
|
+
export interface PromptProbeFailure {
|
|
102
|
+
kind: PromptProbeFailureKind;
|
|
103
|
+
/** The command line that was attempted or resolved, for display. */
|
|
104
|
+
command: string;
|
|
105
|
+
/**
|
|
106
|
+
* A short fixed phrase plus the command - never captured process output.
|
|
107
|
+
* Codex stderr can carry the user's config path, model name, or environment
|
|
108
|
+
* details, and this response is served over the management API, so raw
|
|
109
|
+
* process output does not belong in it.
|
|
110
|
+
*/
|
|
111
|
+
detail: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
82
114
|
export interface PromptTextProbe {
|
|
83
115
|
ok: boolean;
|
|
84
116
|
/** The Codex home the probe reported on. */
|
|
85
117
|
codexHome: string;
|
|
86
118
|
layers: Record<string, LayerText>;
|
|
119
|
+
/** The runtime the probe resolved and tried, when resolution produced one. */
|
|
120
|
+
runtime?: { command: string; source: CodexRuntimeSource };
|
|
121
|
+
/** Stable failure classification; `detail` remains the display string. */
|
|
122
|
+
failure?: PromptProbeFailure;
|
|
87
123
|
detail?: string;
|
|
88
124
|
}
|
|
89
125
|
|
|
90
|
-
function resolveCodexBinary(): string | null {
|
|
91
|
-
const candidates = [
|
|
92
|
-
join(homedir(), ".codex/packages/standalone/current/bin/codex"),
|
|
93
|
-
join(homedir(), ".local/bin/codex"),
|
|
94
|
-
"/usr/local/bin/codex",
|
|
95
|
-
"/opt/homebrew/bin/codex",
|
|
96
|
-
];
|
|
97
|
-
return candidates.find(path => existsSync(path)) ?? null;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
126
|
/** 8 MiB is far above any real prompt and far below anything that hurts the server. */
|
|
101
127
|
const MAX_PROBE_OUTPUT_BYTES = 8 * 1024 * 1024;
|
|
102
128
|
|
|
129
|
+
/** stderr is captured only to classify the failure, never to echo back. */
|
|
130
|
+
const MAX_PROBE_STDERR_BYTES = 64 * 1024;
|
|
131
|
+
|
|
103
132
|
interface ProbeCommand {
|
|
104
133
|
binary: string;
|
|
105
134
|
args: string[];
|
|
@@ -111,7 +140,7 @@ interface ProbeCommand {
|
|
|
111
140
|
interface PromptProbeFlight {
|
|
112
141
|
key: string;
|
|
113
142
|
controller: AbortController;
|
|
114
|
-
result: Promise<
|
|
143
|
+
result: Promise<PromptProbeExecutionResult | null>;
|
|
115
144
|
closed: Promise<void>;
|
|
116
145
|
waiters: number;
|
|
117
146
|
joinable: boolean;
|
|
@@ -120,17 +149,27 @@ interface PromptProbeFlight {
|
|
|
120
149
|
}
|
|
121
150
|
|
|
122
151
|
interface PromptProbeExecution {
|
|
123
|
-
result: Promise<
|
|
152
|
+
result: Promise<PromptProbeExecutionResult | null>;
|
|
124
153
|
closed: Promise<void>;
|
|
125
154
|
}
|
|
126
155
|
|
|
156
|
+
/**
|
|
157
|
+
* The process outcome travels with its classification so a shared flight hands
|
|
158
|
+
* every joined caller the same `failure`, not just the same null.
|
|
159
|
+
*/
|
|
160
|
+
interface PromptProbeExecutionResult {
|
|
161
|
+
raw: string | null;
|
|
162
|
+
failure: PromptProbeFailure | null;
|
|
163
|
+
}
|
|
164
|
+
|
|
127
165
|
type SharedPromptProbeOutcome =
|
|
128
166
|
| { kind: "output"; raw: string }
|
|
129
|
-
| { kind: "failed" }
|
|
167
|
+
| { kind: "failed"; failure?: PromptProbeFailure }
|
|
130
168
|
| { kind: "busy" };
|
|
131
169
|
|
|
132
170
|
let activePromptProbe: PromptProbeFlight | null = null;
|
|
133
171
|
let probeCommandForTests: { binary: string; args: string[] } | null = null;
|
|
172
|
+
let probeRuntimeForTests: { command: string; source: CodexRuntimeSource } | null | undefined;
|
|
134
173
|
let probeSpawnAttemptsForTests = 0;
|
|
135
174
|
let probeCloseBarrierForTests: Promise<void> | null = null;
|
|
136
175
|
|
|
@@ -144,24 +183,85 @@ function commandKey(command: ProbeCommand): string {
|
|
|
144
183
|
]);
|
|
145
184
|
}
|
|
146
185
|
|
|
147
|
-
function completedExecution(value:
|
|
186
|
+
function completedExecution(value: PromptProbeExecutionResult | null): PromptProbeExecution {
|
|
148
187
|
return { result: Promise.resolve(value), closed: Promise.resolve() };
|
|
149
188
|
}
|
|
150
189
|
|
|
190
|
+
/**
|
|
191
|
+
* The command line as it may be shown to a caller.
|
|
192
|
+
*
|
|
193
|
+
* Redacted, because this response is served over the management API and a
|
|
194
|
+
* resolved Codex path is a user path: the Windows Codex App lives under the
|
|
195
|
+
* profile directory, so echoing the raw command would put the account name in
|
|
196
|
+
* a diagnostic. `displayCodexRuntimePath` is the same helper the runtime log
|
|
197
|
+
* line and doctor output already use, so the probe reports a path in the form
|
|
198
|
+
* the rest of the product reports it.
|
|
199
|
+
*/
|
|
200
|
+
function commandDescription(command: ProbeCommand): string {
|
|
201
|
+
return [displayCodexRuntimePath(command.binary), ...command.args].join(" ");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function probeFailure(
|
|
205
|
+
command: ProbeCommand,
|
|
206
|
+
kind: PromptProbeFailureKind,
|
|
207
|
+
detail: string,
|
|
208
|
+
): PromptProbeFailure {
|
|
209
|
+
return { kind, command: commandDescription(command), detail };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* A non-zero exit whose stderr reports an unknown subcommand means the resolved
|
|
214
|
+
* binary is a Codex too old (or too new) for `debug prompt-input` - a different
|
|
215
|
+
* remedy than "the process died". The stderr text itself is used only for this
|
|
216
|
+
* check; it never enters the response.
|
|
217
|
+
*/
|
|
218
|
+
function classifyProcessFailure(command: ProbeCommand, code: number | null, stderr: string): PromptProbeFailure {
|
|
219
|
+
const lower = stderr.toLowerCase();
|
|
220
|
+
const unsupported =
|
|
221
|
+
(/unrecognized|unknown|unexpected|invalid/.test(lower) && /subcommand|command|argument|option/.test(lower))
|
|
222
|
+
|| /usage:/.test(lower);
|
|
223
|
+
const kind: PromptProbeFailureKind = unsupported ? "command-unsupported" : "execution-failed";
|
|
224
|
+
const phrase = unsupported
|
|
225
|
+
? "codex does not support this probe command"
|
|
226
|
+
: `codex probe exited with code ${code ?? "unknown"}`;
|
|
227
|
+
return probeFailure(command, kind, `${phrase}: ${commandDescription(command)}`);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The resolver reports why each candidate lost. A candidate that is simply not
|
|
232
|
+
* there (issue 4458's repeated "path does not exist" on Windows) is a
|
|
233
|
+
* program-not-found; a candidate that exists but could not be probed is an
|
|
234
|
+
* execution problem on an installed program.
|
|
235
|
+
*/
|
|
236
|
+
function classifyRuntimeFailure(result: ResolveCodexRuntimeResult): PromptProbeFailure {
|
|
237
|
+
const isNotFound = (reason: string) =>
|
|
238
|
+
reason === CODEX_PROGRAM_NOT_FOUND_REASON || /does not exist|not found|ENOENT/i.test(reason);
|
|
239
|
+
const representative = result.failures.find(item => !isNotFound(item.reason))
|
|
240
|
+
?? result.failures[0];
|
|
241
|
+
const kind: PromptProbeFailureKind = !representative || isNotFound(representative.reason)
|
|
242
|
+
? "program-not-found"
|
|
243
|
+
: "execution-failed";
|
|
244
|
+
// Same redaction obligation as commandDescription: a rejected candidate is a
|
|
245
|
+
// real filesystem path, and every one of them is reported to the caller.
|
|
246
|
+
const command = displayCodexRuntimePath(representative?.command ?? result.runtime.command);
|
|
247
|
+
const phrase = kind === "program-not-found" ? "codex program not found" : "codex runtime could not be probed";
|
|
248
|
+
return { kind, command, detail: `${phrase}: ${command}` };
|
|
249
|
+
}
|
|
250
|
+
|
|
151
251
|
function runProbe(
|
|
152
252
|
command: ProbeCommand,
|
|
153
253
|
signal: AbortSignal,
|
|
154
254
|
onStopping: () => void,
|
|
155
255
|
): PromptProbeExecution {
|
|
156
|
-
if (signal.aborted) return completedExecution(null);
|
|
157
|
-
let resolveResult!: (value:
|
|
256
|
+
if (signal.aborted) return completedExecution({ raw: null, failure: null });
|
|
257
|
+
let resolveResult!: (value: PromptProbeExecutionResult | null) => void;
|
|
158
258
|
let resolveClosed!: () => void;
|
|
159
|
-
const result = new Promise<
|
|
259
|
+
const result = new Promise<PromptProbeExecutionResult | null>(resolve => { resolveResult = resolve; });
|
|
160
260
|
const closed = new Promise<void>(resolve => { resolveClosed = resolve; });
|
|
161
261
|
let resultSettled = false;
|
|
162
262
|
let closeSettled = false;
|
|
163
263
|
|
|
164
|
-
const finishResult = (value:
|
|
264
|
+
const finishResult = (value: PromptProbeExecutionResult | null) => {
|
|
165
265
|
if (resultSettled) return;
|
|
166
266
|
resultSettled = true;
|
|
167
267
|
resolveResult(value);
|
|
@@ -179,22 +279,39 @@ function runProbe(
|
|
|
179
279
|
let child: ReturnType<typeof spawn>;
|
|
180
280
|
try {
|
|
181
281
|
if (probeCommandForTests) probeSpawnAttemptsForTests += 1;
|
|
182
|
-
|
|
282
|
+
// Route through the shared invocation helper: on Windows a resolved
|
|
283
|
+
// `codex.cmd` cannot be spawned directly and must go through cmd.exe,
|
|
284
|
+
// which `commandInvocation` does with correct metacharacter escaping.
|
|
285
|
+
const invocation = codexExecInvocation(command.binary, command.args, process.platform);
|
|
286
|
+
child = spawn(invocation.file, invocation.args, {
|
|
183
287
|
cwd: command.cwd,
|
|
184
|
-
stdio: ["ignore", "pipe", "
|
|
288
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
289
|
+
windowsHide: true,
|
|
290
|
+
...invocation.options,
|
|
291
|
+
});
|
|
292
|
+
} catch (error) {
|
|
293
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
294
|
+
finishResult({
|
|
295
|
+
raw: null,
|
|
296
|
+
failure: probeFailure(
|
|
297
|
+
command,
|
|
298
|
+
/ENOENT|not found/i.test(message) ? "program-not-found" : "execution-failed",
|
|
299
|
+
`codex probe could not start: ${commandDescription(command)}`,
|
|
300
|
+
),
|
|
185
301
|
});
|
|
186
|
-
} catch {
|
|
187
|
-
finishResult(null);
|
|
188
302
|
finishClosed();
|
|
189
303
|
return { result, closed };
|
|
190
304
|
}
|
|
191
305
|
const chunks: Buffer[] = [];
|
|
306
|
+
const errorChunks: Buffer[] = [];
|
|
192
307
|
let size = 0;
|
|
308
|
+
let errorSize = 0;
|
|
193
309
|
let settled = false;
|
|
194
310
|
let stopping = false;
|
|
311
|
+
let stoppingFailure: PromptProbeFailure | null = null;
|
|
195
312
|
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
196
313
|
|
|
197
|
-
const finish = (value:
|
|
314
|
+
const finish = (value: PromptProbeExecutionResult) => {
|
|
198
315
|
if (settled) return;
|
|
199
316
|
settled = true;
|
|
200
317
|
if (timer) clearTimeout(timer);
|
|
@@ -205,9 +322,16 @@ function runProbe(
|
|
|
205
322
|
|
|
206
323
|
// Keep the flight admitted until `close`: kill() only requests termination
|
|
207
324
|
// and does not prove the exact child has released its process and stdio.
|
|
208
|
-
const terminate = (
|
|
325
|
+
const terminate = (
|
|
326
|
+
failure = probeFailure(
|
|
327
|
+
command,
|
|
328
|
+
"execution-failed",
|
|
329
|
+
`codex probe was terminated: ${commandDescription(command)}`,
|
|
330
|
+
),
|
|
331
|
+
) => {
|
|
209
332
|
if (settled || stopping) return;
|
|
210
333
|
stopping = true;
|
|
334
|
+
stoppingFailure = failure;
|
|
211
335
|
onStopping();
|
|
212
336
|
if (timer) clearTimeout(timer);
|
|
213
337
|
signal.removeEventListener("abort", onAbort);
|
|
@@ -215,7 +339,7 @@ function runProbe(
|
|
|
215
339
|
// The caller is bounded even if OS termination later fails. Admission is
|
|
216
340
|
// retained separately by `closed`, and later probes fail soft while this
|
|
217
341
|
// exact child remains unproven terminal.
|
|
218
|
-
finishResult(null);
|
|
342
|
+
finishResult({ raw: null, failure });
|
|
219
343
|
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
220
344
|
try {
|
|
221
345
|
child.kill("SIGKILL");
|
|
@@ -233,18 +357,50 @@ function runProbe(
|
|
|
233
357
|
if (size > MAX_PROBE_OUTPUT_BYTES) { terminate(); return; }
|
|
234
358
|
chunks.push(chunk);
|
|
235
359
|
});
|
|
236
|
-
child.on("
|
|
360
|
+
child.stderr?.on("data", (chunk: Buffer) => {
|
|
361
|
+
errorSize += chunk.length;
|
|
362
|
+
if (errorSize <= MAX_PROBE_STDERR_BYTES) errorChunks.push(chunk);
|
|
363
|
+
});
|
|
364
|
+
child.on("error", error => {
|
|
237
365
|
// No PID means spawn itself failed, so there is no live child to drain.
|
|
238
366
|
if (child.pid === undefined) {
|
|
239
|
-
|
|
367
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
368
|
+
finish({
|
|
369
|
+
raw: null,
|
|
370
|
+
failure: probeFailure(
|
|
371
|
+
command,
|
|
372
|
+
/ENOENT|not found/i.test(message) ? "program-not-found" : "execution-failed",
|
|
373
|
+
`codex probe could not start: ${commandDescription(command)}`,
|
|
374
|
+
),
|
|
375
|
+
});
|
|
240
376
|
}
|
|
241
|
-
else terminate(
|
|
377
|
+
else terminate(probeFailure(
|
|
378
|
+
command,
|
|
379
|
+
"execution-failed",
|
|
380
|
+
`codex probe process error: ${commandDescription(command)}`,
|
|
381
|
+
));
|
|
242
382
|
});
|
|
243
383
|
child.on("close", code => {
|
|
244
384
|
// Decode once, at the end: `String(chunk)` per chunk corrupts any UTF-8
|
|
245
385
|
// character that straddles a chunk boundary.
|
|
246
386
|
const recordClose = () => {
|
|
247
|
-
|
|
387
|
+
if (stopping) {
|
|
388
|
+
finish({
|
|
389
|
+
raw: null,
|
|
390
|
+
failure: stoppingFailure ?? probeFailure(
|
|
391
|
+
command,
|
|
392
|
+
"execution-failed",
|
|
393
|
+
`codex probe was terminated: ${commandDescription(command)}`,
|
|
394
|
+
),
|
|
395
|
+
});
|
|
396
|
+
} else if (code === 0) {
|
|
397
|
+
finish({ raw: Buffer.concat(chunks).toString("utf8"), failure: null });
|
|
398
|
+
} else {
|
|
399
|
+
finish({
|
|
400
|
+
raw: null,
|
|
401
|
+
failure: classifyProcessFailure(command, code, Buffer.concat(errorChunks).toString("utf8")),
|
|
402
|
+
});
|
|
403
|
+
}
|
|
248
404
|
};
|
|
249
405
|
const barrier = probeCloseBarrierForTests;
|
|
250
406
|
if (barrier) void barrier.then(recordClose, recordClose);
|
|
@@ -253,7 +409,14 @@ function runProbe(
|
|
|
253
409
|
// Close the race between the pre-spawn check and listener registration.
|
|
254
410
|
if (signal.aborted) terminate();
|
|
255
411
|
} catch {
|
|
256
|
-
finishResult(
|
|
412
|
+
finishResult({
|
|
413
|
+
raw: null,
|
|
414
|
+
failure: probeFailure(
|
|
415
|
+
command,
|
|
416
|
+
"execution-failed",
|
|
417
|
+
`codex probe failed: ${commandDescription(command)}`,
|
|
418
|
+
),
|
|
419
|
+
});
|
|
257
420
|
finishClosed();
|
|
258
421
|
}
|
|
259
422
|
return { result, closed };
|
|
@@ -296,19 +459,26 @@ async function runSharedPromptProbe(
|
|
|
296
459
|
if (signal?.aborted) return { kind: "failed" };
|
|
297
460
|
const active = activePromptProbe;
|
|
298
461
|
if (!active) {
|
|
299
|
-
const
|
|
300
|
-
|
|
462
|
+
const result = await waitForPromptProbeFlight(startPromptProbeFlight(command), signal);
|
|
463
|
+
if (!result) return { kind: "failed" };
|
|
464
|
+
if (result.failure) return { kind: "failed", failure: result.failure };
|
|
465
|
+
return result.raw === null ? { kind: "failed" } : { kind: "output", raw: result.raw };
|
|
301
466
|
}
|
|
302
467
|
if (active.key === key && active.joinable && !active.controller.signal.aborted) {
|
|
303
|
-
const
|
|
304
|
-
|
|
468
|
+
const result = await waitForPromptProbeFlight(active, signal);
|
|
469
|
+
if (!result) return { kind: "failed" };
|
|
470
|
+
if (result.failure) return { kind: "failed", failure: result.failure };
|
|
471
|
+
return result.raw === null ? { kind: "failed" } : { kind: "output", raw: result.raw };
|
|
305
472
|
}
|
|
306
473
|
// A different or terminating flight still owns the sole process slot. Never
|
|
307
474
|
// wait unboundedly for an unproven close and never launch beside it.
|
|
308
475
|
return { kind: "busy" };
|
|
309
476
|
}
|
|
310
477
|
|
|
311
|
-
async function waitForPromptProbeFlight(
|
|
478
|
+
async function waitForPromptProbeFlight(
|
|
479
|
+
flight: PromptProbeFlight,
|
|
480
|
+
signal?: AbortSignal,
|
|
481
|
+
): Promise<PromptProbeExecutionResult | null> {
|
|
312
482
|
if (signal?.aborted) {
|
|
313
483
|
if (flight.waiters === 0 && !flight.settled) flight.controller.abort();
|
|
314
484
|
return null;
|
|
@@ -317,7 +487,7 @@ async function waitForPromptProbeFlight(flight: PromptProbeFlight, signal?: Abor
|
|
|
317
487
|
let onAbort: (() => void) | undefined;
|
|
318
488
|
try {
|
|
319
489
|
if (!signal) return await flight.result;
|
|
320
|
-
const aborted = new Promise<null>(resolve => {
|
|
490
|
+
const aborted = new Promise<PromptProbeExecutionResult | null>(resolve => {
|
|
321
491
|
onAbort = () => resolve(null);
|
|
322
492
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
323
493
|
if (signal.aborted) onAbort();
|
|
@@ -390,9 +560,51 @@ export async function probePromptText(
|
|
|
390
560
|
if (signal?.aborted) {
|
|
391
561
|
return { ok: false, codexHome, layers: {}, detail: "prompt probe cancelled" };
|
|
392
562
|
}
|
|
393
|
-
|
|
563
|
+
// Resolve through the shared runtime resolver, not a private path list: the
|
|
564
|
+
// old four-path POSIX check could never match the Codex App's Windows install
|
|
565
|
+
// under %LOCALAPPDATA%\OpenAI\Codex\bin\<version>, so the probe reported
|
|
566
|
+
// "not found" on machines where Codex was plainly installed (issue 4458).
|
|
567
|
+
//
|
|
568
|
+
// Both flags are deliberate. This runs on a request path: discoverAlternatives
|
|
569
|
+
// would walk the whole PATH just to fill a newerAvailable diagnostic the probe
|
|
570
|
+
// never shows, and probeVersion would pay a blocking `--version` exec per
|
|
571
|
+
// candidate. The probe needs a command it can spawn; the version is irrelevant.
|
|
572
|
+
const resolved = probeCommandForTests || probeRuntimeForTests !== undefined
|
|
573
|
+
? null
|
|
574
|
+
: resolveCodexRuntime({ discoverAlternatives: false, probeVersion: false });
|
|
575
|
+
const runtime: { command: string; source: CodexRuntimeSource } | undefined =
|
|
576
|
+
probeRuntimeForTests !== undefined
|
|
577
|
+
? probeRuntimeForTests ?? undefined
|
|
578
|
+
: resolved && resolved.runtime.source !== "fallback"
|
|
579
|
+
? { command: resolved.runtime.command, source: resolved.runtime.source }
|
|
580
|
+
: undefined;
|
|
581
|
+
// A `fallback` result is the resolver saying "nothing concrete was found" -
|
|
582
|
+
// its command is the bare word "codex", not a located binary. Reporting it as
|
|
583
|
+
// resolved would just relabel the same not-found as a spawn failure.
|
|
584
|
+
const binary = probeCommandForTests?.binary ?? runtime?.command ?? null;
|
|
585
|
+
// The response travels over the management API, so the reported runtime is
|
|
586
|
+
// redacted while `binary` keeps the real path the spawn needs. On Windows the
|
|
587
|
+
// Codex App install sits under the user's profile directory, so the raw
|
|
588
|
+
// command carries the account name.
|
|
589
|
+
const reportedRuntime = runtime
|
|
590
|
+
? { command: displayCodexRuntimePath(runtime.command), source: runtime.source }
|
|
591
|
+
: undefined;
|
|
394
592
|
if (!binary) {
|
|
395
|
-
|
|
593
|
+
const failure = resolved
|
|
594
|
+
? classifyRuntimeFailure(resolved)
|
|
595
|
+
: probeFailure(
|
|
596
|
+
{ binary: "codex", args: ["debug", "prompt-input"], cwd: codexHome, timeoutMs, promptStateFingerprint },
|
|
597
|
+
"program-not-found",
|
|
598
|
+
"codex program not found: codex debug prompt-input",
|
|
599
|
+
);
|
|
600
|
+
return {
|
|
601
|
+
ok: false,
|
|
602
|
+
codexHome,
|
|
603
|
+
layers: {},
|
|
604
|
+
...(reportedRuntime ? { runtime: reportedRuntime } : {}),
|
|
605
|
+
failure,
|
|
606
|
+
detail: "codex binary not found",
|
|
607
|
+
};
|
|
396
608
|
}
|
|
397
609
|
const command: ProbeCommand = {
|
|
398
610
|
binary,
|
|
@@ -407,6 +619,8 @@ export async function probePromptText(
|
|
|
407
619
|
ok: false,
|
|
408
620
|
codexHome,
|
|
409
621
|
layers: {},
|
|
622
|
+
...(reportedRuntime ? { runtime: reportedRuntime } : {}),
|
|
623
|
+
...(outcome.kind === "failed" && outcome.failure ? { failure: outcome.failure } : {}),
|
|
410
624
|
detail: signal?.aborted
|
|
411
625
|
? "prompt probe cancelled"
|
|
412
626
|
: outcome.kind === "busy"
|
|
@@ -419,7 +633,18 @@ export async function probePromptText(
|
|
|
419
633
|
if (sections.size === 0) {
|
|
420
634
|
// Zero sections from a zero-exit probe means the output did not parse, which
|
|
421
635
|
// is a failed read - not fifteen layers that each chose to send nothing.
|
|
422
|
-
return {
|
|
636
|
+
return {
|
|
637
|
+
ok: false,
|
|
638
|
+
codexHome,
|
|
639
|
+
layers: {},
|
|
640
|
+
...(reportedRuntime ? { runtime: reportedRuntime } : {}),
|
|
641
|
+
failure: probeFailure(
|
|
642
|
+
command,
|
|
643
|
+
"output-invalid",
|
|
644
|
+
`codex prompt output could not be parsed: ${commandDescription(command)}`,
|
|
645
|
+
),
|
|
646
|
+
detail: "prompt output could not be parsed",
|
|
647
|
+
};
|
|
423
648
|
}
|
|
424
649
|
const layers: Record<string, LayerText> = {};
|
|
425
650
|
for (const [layerId, tag] of Object.entries(LAYER_SECTION_TAGS)) {
|
|
@@ -453,7 +678,7 @@ export async function probePromptText(
|
|
|
453
678
|
for (const id of UNMAPPED_LAYER_IDS) {
|
|
454
679
|
layers[id] ??= { text: null, reason: "not-exposed", bytes: 0 };
|
|
455
680
|
}
|
|
456
|
-
return { ok: true, codexHome, layers };
|
|
681
|
+
return { ok: true, codexHome, layers, ...(reportedRuntime ? { runtime: reportedRuntime } : {}) };
|
|
457
682
|
}
|
|
458
683
|
|
|
459
684
|
/** Test-only command seam; production always resolves the installed Codex binary. */
|
|
@@ -461,6 +686,13 @@ export function setPromptTextProbeCommandForTests(command: { binary: string; arg
|
|
|
461
686
|
probeCommandForTests = command ? { binary: command.binary, args: [...command.args] } : null;
|
|
462
687
|
}
|
|
463
688
|
|
|
689
|
+
/** Test-only runtime seam: stands in for the shared resolver's answer. */
|
|
690
|
+
export function setPromptTextProbeRuntimeForTests(
|
|
691
|
+
runtime: { command: string; source: CodexRuntimeSource } | null | undefined,
|
|
692
|
+
): void {
|
|
693
|
+
probeRuntimeForTests = runtime;
|
|
694
|
+
}
|
|
695
|
+
|
|
464
696
|
/** Test-only process-start counter for proving admission without timing guesses. */
|
|
465
697
|
export function promptTextProbeSpawnAttemptsForTests(): number {
|
|
466
698
|
return probeSpawnAttemptsForTests;
|
|
@@ -484,6 +716,7 @@ export async function resetPromptTextProbeForTests(): Promise<void> {
|
|
|
484
716
|
}
|
|
485
717
|
if (activePromptProbe === active) activePromptProbe = null;
|
|
486
718
|
probeCommandForTests = null;
|
|
719
|
+
probeRuntimeForTests = undefined;
|
|
487
720
|
probeSpawnAttemptsForTests = 0;
|
|
488
721
|
probeCloseBarrierForTests = null;
|
|
489
722
|
}
|