@bitkyc08/opencodex 2.7.36 → 2.7.37
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/README.ja.md +8 -1
- package/README.ko.md +7 -1
- package/README.md +7 -1
- package/README.ru.md +7 -1
- package/README.zh-CN.md +7 -1
- package/gui/dist/assets/index-BhUTxmCy.js +52 -0
- package/gui/dist/assets/index-oOZcqVmj.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic.ts +22 -2
- package/src/adapters/cursor/live-transport.ts +7 -0
- package/src/adapters/cursor/message-mapper.ts +3 -0
- package/src/adapters/cursor/protobuf-request.ts +223 -27
- package/src/adapters/cursor/request-builder.ts +41 -15
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/types.ts +3 -1
- package/src/adapters/cursor.ts +44 -9
- package/src/adapters/google.ts +115 -62
- package/src/adapters/kiro.ts +3 -17
- package/src/adapters/openai-chat.ts +16 -5
- package/src/adapters/openai-responses.ts +56 -1
- package/src/adapters/run-turn-queue.ts +11 -1
- package/src/bridge.ts +139 -69
- package/src/chat/outbound.ts +135 -73
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/doctor.ts +197 -2
- package/src/cli/index.ts +17 -3
- package/src/cli/status.ts +80 -0
- package/src/cli/v2.ts +14 -2
- package/src/codex/auth-context.ts +18 -2
- package/src/codex/catalog/bundled.ts +83 -27
- package/src/codex/catalog/effort.ts +95 -3
- package/src/codex/catalog/parsing.ts +17 -0
- package/src/codex/catalog/provider-fetch.ts +31 -8
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/model-cache.ts +44 -0
- package/src/codex/runtime.ts +529 -0
- package/src/codex/shim.ts +608 -10
- package/src/combos/resolve.ts +7 -2
- package/src/config.ts +32 -1
- package/src/lib/bun-stream-caps.ts +88 -0
- package/src/lib/crash-guard.ts +3 -1
- package/src/lib/sse-decoder.ts +25 -6
- package/src/responses/parser.ts +2 -1
- package/src/responses/state.ts +10 -2
- package/src/server/auth-cors.ts +4 -1
- package/src/server/index.ts +191 -1
- package/src/server/live.ts +491 -0
- package/src/server/management/config-routes.ts +79 -3
- package/src/server/management/provider-routes.ts +2 -0
- package/src/server/management/shared.ts +6 -6
- package/src/server/management/system-routes.ts +65 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/memory-watchdog.ts +112 -0
- package/src/server/relay-eager.ts +199 -0
- package/src/server/relay.ts +131 -81
- package/src/server/responses/collaboration.ts +20 -3
- package/src/server/responses/core.ts +236 -21
- package/src/server/responses/encrypted-payload.ts +118 -41
- package/src/server/ws-bridge.ts +7 -0
- package/src/types.ts +25 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/expected-prices.ts +19 -0
- package/src/usage/summary.ts +11 -8
- package/gui/dist/assets/index-BpX-hoSd.css +0 -1
- package/gui/dist/assets/index-ZmFopEYw.js +0 -52
package/src/cli/doctor.ts
CHANGED
|
@@ -10,13 +10,22 @@
|
|
|
10
10
|
import { existsSync, readFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { join } from "node:path";
|
|
13
|
-
import { getConfigDir, getConfigPath, readConfigDiagnostics, readPid, resolveEnvValue } from "../config";
|
|
13
|
+
import { getConfigDir, getConfigPath, readConfigDiagnostics, readPid, readRuntimePort, resolveEnvValue } from "../config";
|
|
14
|
+
import { gracefulStopHost } from "../lib/process-control";
|
|
15
|
+
import { loadServiceTokenFromFile } from "../lib/service-secrets";
|
|
14
16
|
import { readCodexTokens } from "../codex/auth-collision";
|
|
15
17
|
import { resolveCodexHomeDir as resolveCodexHomeDirImpl, isWslRuntime, listWslWindowsCodexHomes, wslAutomountRoot, type CodexHomeDeps } from "../codex/home";
|
|
16
18
|
import { findCodexOnPath, isWindowsInteropDir } from "../codex/shim";
|
|
17
19
|
import { countPendingOpencodexHistory } from "../codex/history-provider";
|
|
18
20
|
import { collectProjectCodexConfigWarnings, formatProjectCodexConfigWarningsForDoctor } from "../codex/project-config-warnings";
|
|
19
21
|
import { collectStartupHealth, startupHealthSummary } from "../codex/autostart-health";
|
|
22
|
+
import {
|
|
23
|
+
displayCodexRuntimePath,
|
|
24
|
+
loadLastEffortClamp,
|
|
25
|
+
persistCodexRuntime,
|
|
26
|
+
resolveAndPersistCodexRuntime,
|
|
27
|
+
resolveCodexRuntime,
|
|
28
|
+
} from "../codex/runtime";
|
|
20
29
|
export { resolveCodexHomeDir } from "../codex/home";
|
|
21
30
|
|
|
22
31
|
const WHAM_USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
|
|
@@ -319,9 +328,149 @@ export async function probeWham(fetchImpl: typeof fetch = fetch): Promise<WhamPr
|
|
|
319
328
|
}
|
|
320
329
|
}
|
|
321
330
|
|
|
322
|
-
|
|
331
|
+
/**
|
|
332
|
+
* Service-process memory/runtime introspection (#314 WP4).
|
|
333
|
+
*
|
|
334
|
+
* Doctor runs in its OWN Bun process; the only honest source for the SERVICE
|
|
335
|
+
* process identity (Bun version, RSS, stream-mode gate decision) is the
|
|
336
|
+
* authed management endpoint added in WP3. Observe-only: failures render as
|
|
337
|
+
* honest status lines, never as fake data, and never fail the command.
|
|
338
|
+
*/
|
|
339
|
+
export type ServiceMemoryData = {
|
|
340
|
+
pid: number;
|
|
341
|
+
bunVersion: string;
|
|
342
|
+
platform: string;
|
|
343
|
+
rss: number;
|
|
344
|
+
heapUsed: number;
|
|
345
|
+
jscHeap: { heapSize: number } | null;
|
|
346
|
+
streamMode: string;
|
|
347
|
+
eagerRelay: { useEagerRelay: boolean; reason: string } | null;
|
|
348
|
+
watchdog: { warnThresholdBytes: number; lastWarnAt: number | null } | null;
|
|
349
|
+
};
|
|
350
|
+
|
|
351
|
+
export type ServiceMemoryReport =
|
|
352
|
+
| { status: "ok"; data: ServiceMemoryData }
|
|
353
|
+
| { status: "unauthorized" }
|
|
354
|
+
| { status: "unreachable"; error: string };
|
|
355
|
+
|
|
356
|
+
const SERVICE_MEMORY_TIMEOUT_MS = 2000;
|
|
357
|
+
const DEFAULT_MEMORY_THRESHOLD_BYTES = 4 * 1024 ** 3;
|
|
358
|
+
|
|
359
|
+
export async function fetchServiceMemory(
|
|
360
|
+
host: string,
|
|
361
|
+
port: number,
|
|
362
|
+
token: string | null,
|
|
363
|
+
fetchImpl: typeof fetch = fetch,
|
|
364
|
+
): Promise<ServiceMemoryReport> {
|
|
365
|
+
try {
|
|
366
|
+
const res = await fetchImpl(`http://${host}:${port}/api/system/memory`, {
|
|
367
|
+
headers: token ? { "x-opencodex-api-key": token } : {},
|
|
368
|
+
signal: AbortSignal.timeout(SERVICE_MEMORY_TIMEOUT_MS),
|
|
369
|
+
});
|
|
370
|
+
if (res.status === 401 || res.status === 403) return { status: "unauthorized" };
|
|
371
|
+
if (!res.ok) return { status: "unreachable", error: `http ${res.status}` };
|
|
372
|
+
const body = await res.json() as Partial<ServiceMemoryData>;
|
|
373
|
+
if (typeof body.pid !== "number" || typeof body.bunVersion !== "string" || typeof body.rss !== "number") {
|
|
374
|
+
return { status: "unreachable", error: "malformed response" };
|
|
375
|
+
}
|
|
376
|
+
return {
|
|
377
|
+
status: "ok",
|
|
378
|
+
data: {
|
|
379
|
+
pid: body.pid,
|
|
380
|
+
bunVersion: body.bunVersion,
|
|
381
|
+
platform: typeof body.platform === "string" ? body.platform : "unknown",
|
|
382
|
+
rss: body.rss,
|
|
383
|
+
heapUsed: typeof body.heapUsed === "number" ? body.heapUsed : 0,
|
|
384
|
+
jscHeap: body.jscHeap && typeof body.jscHeap.heapSize === "number" ? { heapSize: body.jscHeap.heapSize } : null,
|
|
385
|
+
streamMode: typeof body.streamMode === "string" ? body.streamMode : "auto",
|
|
386
|
+
eagerRelay: body.eagerRelay && typeof body.eagerRelay.reason === "string"
|
|
387
|
+
? { useEagerRelay: body.eagerRelay.useEagerRelay === true, reason: body.eagerRelay.reason }
|
|
388
|
+
: null,
|
|
389
|
+
watchdog: body.watchdog && typeof body.watchdog.warnThresholdBytes === "number"
|
|
390
|
+
? { warnThresholdBytes: body.watchdog.warnThresholdBytes, lastWarnAt: body.watchdog.lastWarnAt ?? null }
|
|
391
|
+
: null,
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
} catch (err) {
|
|
395
|
+
return { status: "unreachable", error: err instanceof Error ? err.name : "fetch failed" };
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const mb = (bytes: number): string => `${Math.round(bytes / (1024 * 1024))}MB`;
|
|
400
|
+
|
|
401
|
+
/** Render the doctor "Memory / runtime" section lines (testable without console capture). */
|
|
402
|
+
export function formatServiceMemoryLines(report: ServiceMemoryReport): string[] {
|
|
403
|
+
const lines: string[] = [];
|
|
404
|
+
lines.push(` -- doctor process Bun ${Bun.version} (this is NOT the service process)`);
|
|
405
|
+
if (report.status === "unauthorized") {
|
|
406
|
+
lines.push(" -- proxy reachable but rejected the request — set OPENCODEX_API_AUTH_TOKEN to match the service");
|
|
407
|
+
return lines;
|
|
408
|
+
}
|
|
409
|
+
if (report.status === "unreachable") {
|
|
410
|
+
lines.push(` -- proxy not reachable (not running?) [${report.error}]`);
|
|
411
|
+
return lines;
|
|
412
|
+
}
|
|
413
|
+
const d = report.data;
|
|
414
|
+
lines.push(` ok service pid ${d.pid}: Bun ${d.bunVersion} on ${d.platform}`);
|
|
415
|
+
lines.push(` rss=${mb(d.rss)}, heapUsed=${mb(d.heapUsed)}${d.jscHeap ? `, jscHeap=${mb(d.jscHeap.heapSize)}` : ""}`);
|
|
416
|
+
lines.push(` streamMode=${d.streamMode}${d.eagerRelay ? ` (eager relay: ${d.eagerRelay.useEagerRelay ? "on" : "off"}, ${d.eagerRelay.reason})` : ""}`);
|
|
417
|
+
if (d.watchdog) {
|
|
418
|
+
lines.push(` watchdog threshold=${mb(d.watchdog.warnThresholdBytes)}${d.watchdog.lastWarnAt ? `, last warn ${new Date(d.watchdog.lastWarnAt).toISOString()}` : ", no warnings"}`);
|
|
419
|
+
}
|
|
420
|
+
// Interpretation rule (devlog 040): reuse the watchdog's own threshold so
|
|
421
|
+
// doctor and watchdog never disagree about "high"; jsShare discriminates
|
|
422
|
+
// JS-heap growth from native runtime growth (the #314 shape).
|
|
423
|
+
const threshold = d.watchdog?.warnThresholdBytes ?? DEFAULT_MEMORY_THRESHOLD_BYTES;
|
|
424
|
+
const jsShare = d.rss > 0 ? Math.max(d.heapUsed, d.jscHeap?.heapSize ?? 0) / d.rss : 0;
|
|
425
|
+
if (d.rss < threshold) {
|
|
426
|
+
lines.push(" memory usage looks normal");
|
|
427
|
+
} else if (jsShare < 0.25) {
|
|
428
|
+
lines.push(" !! high RSS with a small JS heap — native-side growth (Bun runtime buffers/handles). See docs: troubleshooting/windows-memory");
|
|
429
|
+
} else if (jsShare >= 0.5) {
|
|
430
|
+
lines.push(" !! high RSS dominated by the JS heap — likely an opencodex bug; please report it");
|
|
431
|
+
} else {
|
|
432
|
+
lines.push(" !! high RSS, indeterminate split — capture two doctor runs over time to see the trend");
|
|
433
|
+
}
|
|
434
|
+
// Version-claiming (never binary-claiming): the endpoint cannot distinguish
|
|
435
|
+
// the bundled binary from an OPENCODEX_BUN_PATH override of the same version.
|
|
436
|
+
if (d.platform === "win32" && d.eagerRelay?.reason === "auto-known-bad") {
|
|
437
|
+
lines.push(` service is running Bun ${d.bunVersion} on Windows — a version affected by the upstream Bun memory issue.`);
|
|
438
|
+
lines.push(" Options: wait for a bundled runtime update, or set OPENCODEX_BUN_PATH to a runtime you trust (unvalidated — own risk),");
|
|
439
|
+
lines.push(" or opt into streamMode \"eager-relay\" via PUT /api/settings (crash risk on this runtime; see docs).");
|
|
440
|
+
}
|
|
441
|
+
return lines;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
export async function runDoctor(args: string[] = []): Promise<void> {
|
|
445
|
+
if (args.includes("--fix-codex-runtime")) {
|
|
446
|
+
const resolved = resolveCodexRuntime();
|
|
447
|
+
if (!resolved.newerAvailable) {
|
|
448
|
+
console.log("No newer Codex runtime found; keeping current selection.");
|
|
449
|
+
const current = resolveAndPersistCodexRuntime();
|
|
450
|
+
console.log(`Selected: ${displayCodexRuntimePath(current.runtime.command)} (${current.runtime.version ?? "unknown"})`);
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
if (resolved.runtime.source === "environment") {
|
|
454
|
+
console.log("CODEX_CLI_PATH currently overrides configured runtimes.");
|
|
455
|
+
console.log(`Unset or update CODEX_CLI_PATH to use ${displayCodexRuntimePath(resolved.newerAvailable.command)} (${resolved.newerAvailable.version ?? "unknown"}).`);
|
|
456
|
+
console.log("Then run ocx sync.");
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
persistCodexRuntime({
|
|
460
|
+
command: resolved.newerAvailable.command,
|
|
461
|
+
version: resolved.newerAvailable.version,
|
|
462
|
+
source: "configured",
|
|
463
|
+
});
|
|
464
|
+
console.log(`Updated Codex runtime to ${displayCodexRuntimePath(resolved.newerAvailable.command)} (${resolved.newerAvailable.version ?? "unknown"}).`);
|
|
465
|
+
console.log("Run ocx sync to refresh the catalog against this runtime.");
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
323
469
|
console.log("opencodex doctor\n");
|
|
324
470
|
|
|
471
|
+
// Ordering note: the memory/runtime section renders after "Running proxy
|
|
472
|
+
// process proxy env" below; helpers live above runDoctor for testability.
|
|
473
|
+
|
|
325
474
|
const paths = collectPaths();
|
|
326
475
|
const mounts = readMounts();
|
|
327
476
|
console.log("Paths");
|
|
@@ -337,6 +486,35 @@ export async function runDoctor(): Promise<void> {
|
|
|
337
486
|
console.log(` ${startup.rebootSafe ? "ok " : "!! "} ${startupHealthSummary(startup)}`);
|
|
338
487
|
console.log(` routing=${startup.routingKind}, service=${startup.serviceViable ? "viable" : startup.serviceInstalled ? "installed-but-unhealthy" : "absent"}, shim=${startup.shimHealthy ? "healthy" : startup.shimInstalled ? "stale" : "absent"}`);
|
|
339
488
|
|
|
489
|
+
console.log("\nCodex runtime selection");
|
|
490
|
+
{
|
|
491
|
+
const resolved = resolveCodexRuntime();
|
|
492
|
+
const selected = resolved.runtime;
|
|
493
|
+
console.log(` ok Selected runtime: ${displayCodexRuntimePath(selected.command)} (${selected.version ?? "unknown"}, source=${selected.source})`);
|
|
494
|
+
const envFailures = resolved.failures.filter(item => item.source === "environment");
|
|
495
|
+
for (const failure of envFailures) {
|
|
496
|
+
console.log(` !! Invalid CODEX_CLI_PATH: ${failure.reason}`);
|
|
497
|
+
}
|
|
498
|
+
const shimFailures = resolved.failures.filter(item => item.source === "shim");
|
|
499
|
+
if (shimFailures.length > 0) {
|
|
500
|
+
console.log(` !! Stale shim target rejected (${shimFailures.length})`);
|
|
501
|
+
}
|
|
502
|
+
if (resolved.replacedConfigured) {
|
|
503
|
+
console.log(` !! Preferred runtime unavailable; fell back to ${displayCodexRuntimePath(selected.command)}`);
|
|
504
|
+
}
|
|
505
|
+
if (resolved.newerAvailable) {
|
|
506
|
+
console.log(` !! Multiple Codex installations found.`);
|
|
507
|
+
console.log(` ok Newer usable runtime found: ${displayCodexRuntimePath(resolved.newerAvailable.command)} (${resolved.newerAvailable.version ?? "unknown"})`);
|
|
508
|
+
console.log(" Suggested: set CODEX_CLI_PATH to the desired binary and run ocx sync.");
|
|
509
|
+
console.log(" Optional: ocx doctor --fix-codex-runtime");
|
|
510
|
+
}
|
|
511
|
+
const lastClamp = loadLastEffortClamp();
|
|
512
|
+
if (lastClamp && lastClamp.removedEfforts.length > 0) {
|
|
513
|
+
console.log(` !! ${lastClamp.removedEfforts.join(" and ")} were removed during catalog sync.`);
|
|
514
|
+
console.log(" Suggested: set CODEX_CLI_PATH to a newer Codex binary and run ocx sync.");
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
340
518
|
const currentProxyEnv = collectProxyEnv();
|
|
341
519
|
const configuredProxy = collectConfiguredProxy();
|
|
342
520
|
const runningProxyEnv = collectRunningProxyEnv();
|
|
@@ -361,6 +539,23 @@ export async function runDoctor(): Promise<void> {
|
|
|
361
539
|
}
|
|
362
540
|
}
|
|
363
541
|
|
|
542
|
+
// #314: service-process memory/runtime identity via the authed management
|
|
543
|
+
// endpoint. readPid() FIRST (liveness), then the pid-scoped runtime record —
|
|
544
|
+
// readRuntimePort alone can serve a stale file pointing at a foreign port.
|
|
545
|
+
console.log("\nMemory / runtime");
|
|
546
|
+
{
|
|
547
|
+
const livePid = readPid();
|
|
548
|
+
const runtime = livePid ? readRuntimePort(livePid) : null;
|
|
549
|
+
if (!runtime) {
|
|
550
|
+
console.log(` -- doctor process Bun ${Bun.version} (this is NOT the service process)`);
|
|
551
|
+
console.log(" -- no running ocx proxy found (no live pid/runtime record)");
|
|
552
|
+
} else {
|
|
553
|
+
const token = process.env.OPENCODEX_API_AUTH_TOKEN ?? loadServiceTokenFromFile(process.env);
|
|
554
|
+
const report = await fetchServiceMemory(gracefulStopHost(runtime.hostname), runtime.port, token);
|
|
555
|
+
for (const line of formatServiceMemoryLines(report)) console.log(line);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
364
559
|
console.log("\nWHAM reachability");
|
|
365
560
|
const probe = await probeWham();
|
|
366
561
|
const detail = probe.status !== null ? `status=${probe.status}` : `error=${probe.classification}`;
|
package/src/cli/index.ts
CHANGED
|
@@ -36,6 +36,7 @@ import { buildDesktop3pRegistry } from "../claude/desktop-3p";
|
|
|
36
36
|
import { installShellHook, uninstallShellHook } from "../server/system-env";
|
|
37
37
|
import { startTokenGuardian } from "../oauth/token-guardian";
|
|
38
38
|
import { startHistoryMigrationGuardian } from "../codex/history-migration-guardian";
|
|
39
|
+
import { maybeAutoRestoreCodexShim } from "./codex-shim-autorestore";
|
|
39
40
|
import { maybeShowStarPrompt } from "./star-prompt";
|
|
40
41
|
import { maybeShowUpdatePrompt } from "../update/notify";
|
|
41
42
|
import { syncModelsToCodex } from "../codex/sync";
|
|
@@ -49,8 +50,9 @@ if (command === "--version" || command === "-v" || command === "version") {
|
|
|
49
50
|
process.exit(0);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
if (command === "help"
|
|
53
|
-
printSubcommandUsage(args[1]);
|
|
53
|
+
if (command === undefined || command === "help" || command === "--help" || command === "-h") {
|
|
54
|
+
if (command === "help" && args[1]) printSubcommandUsage(args[1]);
|
|
55
|
+
else printUsage();
|
|
54
56
|
process.exit(0);
|
|
55
57
|
}
|
|
56
58
|
|
|
@@ -59,6 +61,8 @@ if (command !== undefined && command !== "help" && hasHelpFlag(args.slice(1))) {
|
|
|
59
61
|
process.exit(0);
|
|
60
62
|
}
|
|
61
63
|
|
|
64
|
+
maybeAutoRestoreCodexShim(command, args);
|
|
65
|
+
|
|
62
66
|
function parsePortOption(): number | undefined {
|
|
63
67
|
if (args.length === 1) return undefined;
|
|
64
68
|
if (args.length !== 3 || args[1] !== "--port") {
|
|
@@ -507,6 +511,16 @@ async function handleStatus() {
|
|
|
507
511
|
console.log(` Restart safety: ${startupHealthSummary(status.json.startup)}`);
|
|
508
512
|
console.log(` Service: ${status.json.service.summary}`);
|
|
509
513
|
console.log(` ${status.json.codexShim.summary}`);
|
|
514
|
+
console.log(` Codex runtime: ${status.json.codexRuntime.path}`);
|
|
515
|
+
console.log(` Codex version: ${status.json.codexRuntime.version ?? "unknown"}`);
|
|
516
|
+
console.log(` Codex source: ${status.json.codexRuntime.source}`);
|
|
517
|
+
console.log(` Catalog clamp: ${status.json.codexRuntime.catalogClamp.active ? "active" : "inactive"}`);
|
|
518
|
+
if (status.json.codexRuntime.catalogClamp.removedEfforts.length > 0) {
|
|
519
|
+
console.log(` Removed efforts: ${status.json.codexRuntime.catalogClamp.removedEfforts.join(", ")}`);
|
|
520
|
+
}
|
|
521
|
+
if (status.json.codexRuntime.warning) {
|
|
522
|
+
console.log(` ⚠️ ${status.json.codexRuntime.warning}`);
|
|
523
|
+
}
|
|
510
524
|
if (status.json.codexPlugins.applicable) {
|
|
511
525
|
const icon = status.json.codexPlugins.stale ? "⚠️ " : "✅";
|
|
512
526
|
console.log(` ${icon} Codex bundled plugins: ${status.json.codexPlugins.summary}`);
|
|
@@ -581,7 +595,7 @@ switch (command) {
|
|
|
581
595
|
break;
|
|
582
596
|
case "doctor": {
|
|
583
597
|
const { runDoctor } = await import("./doctor");
|
|
584
|
-
await runDoctor();
|
|
598
|
+
await runDoctor(args.slice(1));
|
|
585
599
|
break;
|
|
586
600
|
}
|
|
587
601
|
case "debug": {
|
package/src/cli/status.ts
CHANGED
|
@@ -7,6 +7,8 @@ import { diagnoseService } from "../service";
|
|
|
7
7
|
import { collectStartupHealth, type StartupHealth } from "../codex/autostart-health";
|
|
8
8
|
import { getCodexRoutingKind } from "../codex/inject";
|
|
9
9
|
import { diagnoseCodexShim } from "../codex/shim";
|
|
10
|
+
import { displayCodexRuntimePath, effortClampAppliesToRuntime, loadLastEffortClamp, resolveCodexRuntime } from "../codex/runtime";
|
|
11
|
+
import { redactSecretString, redactUserPath } from "../lib/redact";
|
|
10
12
|
|
|
11
13
|
type HealthCheck = {
|
|
12
14
|
ok: boolean;
|
|
@@ -51,6 +53,18 @@ export type CliStatusJson = {
|
|
|
51
53
|
service: { summary: string };
|
|
52
54
|
codexShim: { summary: string };
|
|
53
55
|
codexPlugins: CodexPluginsDiagnostic;
|
|
56
|
+
codexRuntime: {
|
|
57
|
+
path: string;
|
|
58
|
+
version: string | null;
|
|
59
|
+
source: string;
|
|
60
|
+
newerAvailable: { path: string; version: string | null } | null;
|
|
61
|
+
warning: string | null;
|
|
62
|
+
catalogClamp: {
|
|
63
|
+
active: boolean;
|
|
64
|
+
removedEfforts: string[];
|
|
65
|
+
runtimeVersion: string | null;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
54
68
|
};
|
|
55
69
|
|
|
56
70
|
export type CliStatusView = {
|
|
@@ -129,6 +143,71 @@ export async function collectStatus(): Promise<CliStatusView> {
|
|
|
129
143
|
routingKind: getCodexRoutingKind(),
|
|
130
144
|
});
|
|
131
145
|
const codexPlugins = diagnoseCodexBundledPlugins();
|
|
146
|
+
const resolvedRuntime = (() => {
|
|
147
|
+
try {
|
|
148
|
+
return resolveCodexRuntime();
|
|
149
|
+
} catch (error) {
|
|
150
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
151
|
+
const redacted = redactUserPath(redactSecretString(message)).slice(0, 160);
|
|
152
|
+
return {
|
|
153
|
+
runtime: { command: "codex", version: null, source: "fallback" as const },
|
|
154
|
+
failures: [{
|
|
155
|
+
command: "codex",
|
|
156
|
+
source: "fallback" as const,
|
|
157
|
+
reason: `resolve threw: ${redacted}`,
|
|
158
|
+
}],
|
|
159
|
+
replacedConfigured: undefined,
|
|
160
|
+
newerAvailable: undefined,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
})();
|
|
164
|
+
const lastClamp = loadLastEffortClamp();
|
|
165
|
+
const clampActive = effortClampAppliesToRuntime(lastClamp, resolvedRuntime.runtime);
|
|
166
|
+
const warningParts: string[] = [];
|
|
167
|
+
if (
|
|
168
|
+
resolvedRuntime.replacedConfigured
|
|
169
|
+
&& resolvedRuntime.replacedConfigured.from.command !== resolvedRuntime.runtime.command
|
|
170
|
+
) {
|
|
171
|
+
warningParts.push(
|
|
172
|
+
`Preferred Codex runtime is unavailable; using ${displayCodexRuntimePath(resolvedRuntime.runtime.command)} instead. Run ocx doctor for diagnosis and recovery.`,
|
|
173
|
+
);
|
|
174
|
+
} else if (
|
|
175
|
+
resolvedRuntime.runtime.source === "fallback"
|
|
176
|
+
&& resolvedRuntime.failures.length > 0
|
|
177
|
+
&& !resolvedRuntime.runtime.version
|
|
178
|
+
) {
|
|
179
|
+
const detail = resolvedRuntime.failures[0]?.reason;
|
|
180
|
+
warningParts.push(
|
|
181
|
+
detail
|
|
182
|
+
? `No validated Codex runtime found (${detail}); falling back to \`codex\`. Run ocx doctor for diagnosis and recovery.`
|
|
183
|
+
: "No validated Codex runtime found; falling back to `codex`. Run ocx doctor for diagnosis and recovery.",
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
if (resolvedRuntime.newerAvailable) {
|
|
187
|
+
warningParts.push("OpenCodex is using an older Codex binary. Run ocx doctor for diagnosis and recovery.");
|
|
188
|
+
}
|
|
189
|
+
if (clampActive) {
|
|
190
|
+
warningParts.push(
|
|
191
|
+
`Catalog clamp removed: ${lastClamp!.removedEfforts.join(", ")}. Run ocx doctor for diagnosis and recovery.`,
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
const codexRuntime = {
|
|
195
|
+
path: displayCodexRuntimePath(resolvedRuntime.runtime.command),
|
|
196
|
+
version: resolvedRuntime.runtime.version,
|
|
197
|
+
source: resolvedRuntime.runtime.source,
|
|
198
|
+
newerAvailable: resolvedRuntime.newerAvailable
|
|
199
|
+
? {
|
|
200
|
+
path: displayCodexRuntimePath(resolvedRuntime.newerAvailable.command),
|
|
201
|
+
version: resolvedRuntime.newerAvailable.version,
|
|
202
|
+
}
|
|
203
|
+
: null,
|
|
204
|
+
warning: warningParts.length > 0 ? warningParts.join(" ") : null,
|
|
205
|
+
catalogClamp: {
|
|
206
|
+
active: clampActive,
|
|
207
|
+
removedEfforts: clampActive ? (lastClamp?.removedEfforts ?? []) : [],
|
|
208
|
+
runtimeVersion: clampActive ? (lastClamp?.runtimeVersion ?? null) : null,
|
|
209
|
+
},
|
|
210
|
+
};
|
|
132
211
|
const proxyLabel = pid && health.ok
|
|
133
212
|
? `running (PID ${pid})`
|
|
134
213
|
: pid
|
|
@@ -176,6 +255,7 @@ export async function collectStatus(): Promise<CliStatusView> {
|
|
|
176
255
|
service: { summary: serviceSummary },
|
|
177
256
|
codexShim: { summary: codexShimSummary },
|
|
178
257
|
codexPlugins,
|
|
258
|
+
codexRuntime,
|
|
179
259
|
},
|
|
180
260
|
};
|
|
181
261
|
}
|
package/src/cli/v2.ts
CHANGED
|
@@ -15,6 +15,7 @@ import { getLogicalMaxThreads, hasAgentsMaxThreads, isMultiAgentV2Enabled, trans
|
|
|
15
15
|
|
|
16
16
|
import { commandInvocation, type SpawnInvocation } from "../lib/win-exec";
|
|
17
17
|
import { loadConfig, saveConfig } from "../config";
|
|
18
|
+
import { resolveAndPersistCodexRuntime, type ResolveCodexRuntimeDeps } from "../codex/runtime";
|
|
18
19
|
|
|
19
20
|
export interface V2CliDeps {
|
|
20
21
|
execFile?: (file: string, args: string[], options?: SpawnInvocation["options"]) => void;
|
|
@@ -24,6 +25,10 @@ export interface V2CliDeps {
|
|
|
24
25
|
log?: Pick<Console, "log" | "error">;
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
export type CodexFeaturesInvocationDeps =
|
|
29
|
+
& Parameters<typeof commandInvocation>[3]
|
|
30
|
+
& Pick<ResolveCodexRuntimeDeps, "existsSync" | "execFileSync" | "configDir" | "readFileSync">;
|
|
31
|
+
|
|
27
32
|
/**
|
|
28
33
|
* Shared invocation for `codex features enable|disable multi_agent_v2` — the single
|
|
29
34
|
* source of truth for the CLI and the management API fallback. Windows npm installs
|
|
@@ -33,9 +38,16 @@ export interface V2CliDeps {
|
|
|
33
38
|
export function codexFeaturesInvocation(
|
|
34
39
|
action: "enable" | "disable",
|
|
35
40
|
platform: NodeJS.Platform = process.platform,
|
|
36
|
-
deps:
|
|
41
|
+
deps: CodexFeaturesInvocationDeps = {},
|
|
37
42
|
): SpawnInvocation {
|
|
38
|
-
const command = (
|
|
43
|
+
const command = resolveAndPersistCodexRuntime({
|
|
44
|
+
env: deps.env ?? process.env,
|
|
45
|
+
platform,
|
|
46
|
+
existsSync: deps.existsSync,
|
|
47
|
+
execFileSync: deps.execFileSync,
|
|
48
|
+
configDir: deps.configDir,
|
|
49
|
+
readFileSync: deps.readFileSync,
|
|
50
|
+
}).runtime.command || "codex";
|
|
39
51
|
return commandInvocation(command, ["features", action, "multi_agent_v2"], platform, deps);
|
|
40
52
|
}
|
|
41
53
|
|
|
@@ -7,7 +7,11 @@ import {
|
|
|
7
7
|
import { markAccountNeedsReauth } from "./account-runtime-state";
|
|
8
8
|
import { isCodexAccountUsable } from "./account-usability";
|
|
9
9
|
import { MAIN_CODEX_ACCOUNT_ID, getMainAccountToken } from "./main-account";
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
getCodexAccountCooldownUntil,
|
|
12
|
+
pickLowestUsageCodexAccount,
|
|
13
|
+
resolveCodexAccountForThreadDetailed,
|
|
14
|
+
} from "./routing";
|
|
11
15
|
import { getAccountQuota } from "./quota";
|
|
12
16
|
import type { CodexAccountMode, OcxConfig, OcxProviderConfig } from "../types";
|
|
13
17
|
import { FORWARD_HEADERS } from "../adapters/openai-responses";
|
|
@@ -89,17 +93,29 @@ export function shouldMarkAccountNeedsReauthForCodexAuthFailure(cause: unknown):
|
|
|
89
93
|
return !(cause instanceof CodexCredentialGenerationConflictError) && !(cause instanceof CodexCredentialRefreshLockTimeoutError);
|
|
90
94
|
}
|
|
91
95
|
|
|
96
|
+
export interface ResolveCodexAuthContextOptions {
|
|
97
|
+
excludeAccountId?: string;
|
|
98
|
+
}
|
|
99
|
+
|
|
92
100
|
export async function resolveCodexAuthContext(
|
|
93
101
|
headers: Headers,
|
|
94
102
|
config: OcxConfig,
|
|
95
103
|
mode: CodexAccountMode,
|
|
104
|
+
options: ResolveCodexAuthContextOptions = {},
|
|
96
105
|
): Promise<CodexAuthContext> {
|
|
97
106
|
if (mode === "direct") {
|
|
98
107
|
if (!hasCallerCodexBearer(headers)) throw new CodexDirectAuthenticationError();
|
|
99
108
|
return { kind: "main", accountId: null };
|
|
100
109
|
}
|
|
101
110
|
const threadId = headers.get("x-codex-parent-thread-id");
|
|
102
|
-
const resolution =
|
|
111
|
+
const resolution = options.excludeAccountId
|
|
112
|
+
? (() => {
|
|
113
|
+
const accountId = pickLowestUsageCodexAccount(config, options.excludeAccountId);
|
|
114
|
+
return accountId
|
|
115
|
+
? { status: "selected" as const, accountId }
|
|
116
|
+
: { status: "none" as const };
|
|
117
|
+
})()
|
|
118
|
+
: resolveCodexAccountForThreadDetailed(threadId, config);
|
|
103
119
|
if (resolution.status === "expired") throw new CodexThreadAffinityExpiredError(resolution.accountId);
|
|
104
120
|
const accountId = resolution.status === "selected" ? resolution.accountId : null;
|
|
105
121
|
if (!accountId) throw new CodexPoolAuthenticationError();
|
|
@@ -33,16 +33,31 @@ import upstreamModelsSnapshot from "../data/upstream-models.json";
|
|
|
33
33
|
|
|
34
34
|
import { activeCodexModelsCachePath, catalogBackupPathFor, findNativeTemplate, isDefaultCatalogPath, legacyCatalogBackupPath, parseCatalogJson, readCatalog, readCatalogBackup, readCodexCatalogPath } from "./parsing";
|
|
35
35
|
import type { RawCatalog, RawEntry } from "./parsing";
|
|
36
|
+
import { codexExecInvocation, isSpawnableCodexCandidate } from "../exec-invocation";
|
|
37
|
+
import { resolveAndPersistCodexRuntime } from "../runtime";
|
|
38
|
+
import type { EffortClampDiagnostic } from "../runtime";
|
|
39
|
+
|
|
40
|
+
export { isSpawnableCodexCandidate, codexExecInvocation } from "../exec-invocation";
|
|
36
41
|
|
|
37
42
|
export const BUNDLED_CATALOG_CACHE_MS = 60_000;
|
|
38
43
|
|
|
39
|
-
export let bundledCatalogCache: {
|
|
44
|
+
export let bundledCatalogCache: {
|
|
45
|
+
/** Selected runtime identity; must change when doctor/sync picks a different binary. */
|
|
46
|
+
key: string;
|
|
47
|
+
expiresAt: number;
|
|
48
|
+
value: RawCatalog | null;
|
|
49
|
+
} | null = null;
|
|
40
50
|
|
|
41
51
|
/** Test-only: clear the bundled-catalog cache (owned here; sync.ts calls this instead of assigning the import). */
|
|
42
52
|
export function resetBundledCatalogCacheForTests(): void {
|
|
43
53
|
bundledCatalogCache = null;
|
|
44
54
|
}
|
|
45
55
|
|
|
56
|
+
/** Drop the process-local bundled catalog memo (e.g. after runtime selection changes). */
|
|
57
|
+
export function invalidateBundledCatalogCache(): void {
|
|
58
|
+
bundledCatalogCache = null;
|
|
59
|
+
}
|
|
60
|
+
|
|
46
61
|
export type ExecFile = (
|
|
47
62
|
file: string,
|
|
48
63
|
args: string[],
|
|
@@ -52,12 +67,21 @@ export type ExecFile = (
|
|
|
52
67
|
timeout: number;
|
|
53
68
|
windowsHide: boolean;
|
|
54
69
|
shell?: boolean;
|
|
70
|
+
windowsVerbatimArguments?: boolean;
|
|
55
71
|
},
|
|
56
72
|
) => string;
|
|
57
73
|
|
|
58
74
|
export interface BundledCatalogDeps {
|
|
59
75
|
commandCandidates?: () => string[];
|
|
60
76
|
execFileSync?: ExecFile;
|
|
77
|
+
onEffortClamp?: (diagnostic: EffortClampDiagnostic) => void;
|
|
78
|
+
configDir?: string;
|
|
79
|
+
env?: NodeJS.ProcessEnv;
|
|
80
|
+
platform?: NodeJS.Platform;
|
|
81
|
+
existsSync?: (path: string) => boolean;
|
|
82
|
+
readFileSync?: (path: string, encoding: "utf8") => string;
|
|
83
|
+
now?: () => number;
|
|
84
|
+
discoverAlternatives?: boolean;
|
|
61
85
|
}
|
|
62
86
|
|
|
63
87
|
export function unique(values: string[]): string[] {
|
|
@@ -77,11 +101,6 @@ export function codexCommandCandidates(): string[] {
|
|
|
77
101
|
return unique(candidates);
|
|
78
102
|
}
|
|
79
103
|
|
|
80
|
-
export function isSpawnableCodexCandidate(path: string, platform: NodeJS.Platform = process.platform): boolean {
|
|
81
|
-
if (platform !== "win32") return true;
|
|
82
|
-
return /\.(cmd|bat|exe|com)$/i.test(path);
|
|
83
|
-
}
|
|
84
|
-
|
|
85
104
|
export function codexShimCommandCandidates(): string[] {
|
|
86
105
|
try {
|
|
87
106
|
const state = JSON.parse(readFileSync(join(getConfigDir(), "codex-shim.json"), "utf8")) as {
|
|
@@ -105,45 +124,82 @@ export function codexShimCommandCandidates(): string[] {
|
|
|
105
124
|
}
|
|
106
125
|
}
|
|
107
126
|
|
|
108
|
-
export function
|
|
127
|
+
export function runCodexDebugModels(
|
|
109
128
|
command: string,
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
return { file: `"${command.replace(/"/g, "")}"`, shell: true };
|
|
114
|
-
}
|
|
115
|
-
return { file: command, shell: false };
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
export function runCodexDebugModels(command: string, execFile: ExecFile): string {
|
|
129
|
+
execFile: ExecFile,
|
|
130
|
+
deps: Pick<BundledCatalogDeps, "env" | "platform" | "existsSync"> = {},
|
|
131
|
+
): string {
|
|
119
132
|
const args = ["debug", "models", "--bundled"];
|
|
120
|
-
const invocation = codexExecInvocation(command
|
|
121
|
-
|
|
133
|
+
const invocation = codexExecInvocation(command, args, deps.platform ?? process.platform, {
|
|
134
|
+
env: deps.env,
|
|
135
|
+
exists: deps.existsSync,
|
|
136
|
+
});
|
|
137
|
+
return execFile(invocation.file, invocation.args, {
|
|
122
138
|
encoding: "utf8" as const,
|
|
123
139
|
stdio: ["ignore", "pipe", "ignore"] as ["ignore", "pipe", "ignore"],
|
|
124
140
|
timeout: 10_000,
|
|
125
141
|
windowsHide: true,
|
|
126
|
-
|
|
142
|
+
...invocation.options,
|
|
127
143
|
});
|
|
128
144
|
}
|
|
129
145
|
|
|
130
146
|
export function loadBundledCodexCatalog(deps: BundledCatalogDeps = {}): RawCatalog | null {
|
|
131
|
-
const useCache = !deps.commandCandidates && !deps.execFileSync;
|
|
132
|
-
|
|
147
|
+
const useCache = !deps.commandCandidates && !deps.execFileSync && !deps.configDir && !deps.env;
|
|
148
|
+
const execFile = deps.execFileSync ?? (execFileSync as unknown as ExecFile);
|
|
149
|
+
// Prefer the single resolved runtime so sync/clamp never probe a different binary
|
|
150
|
+
// than OpenCodex will launch. Tests may inject commandCandidates to stub probing.
|
|
151
|
+
let cacheKey: string | null = null;
|
|
152
|
+
const candidates = deps.commandCandidates?.() ?? (() => {
|
|
153
|
+
const resolved = resolveAndPersistCodexRuntime({
|
|
154
|
+
execFileSync: execFile,
|
|
155
|
+
configDir: deps.configDir,
|
|
156
|
+
env: deps.env,
|
|
157
|
+
platform: deps.platform,
|
|
158
|
+
existsSync: deps.existsSync,
|
|
159
|
+
readFileSync: deps.readFileSync,
|
|
160
|
+
now: deps.now,
|
|
161
|
+
discoverAlternatives: deps.discoverAlternatives,
|
|
162
|
+
});
|
|
163
|
+
if (useCache) {
|
|
164
|
+
cacheKey = [
|
|
165
|
+
resolved.runtime.command,
|
|
166
|
+
resolved.runtime.version ?? "",
|
|
167
|
+
process.env.OPENCODEX_HOME ?? "",
|
|
168
|
+
].join("\0");
|
|
169
|
+
}
|
|
170
|
+
return [resolved.runtime.command];
|
|
171
|
+
})();
|
|
172
|
+
if (
|
|
173
|
+
useCache
|
|
174
|
+
&& cacheKey
|
|
175
|
+
&& bundledCatalogCache
|
|
176
|
+
&& bundledCatalogCache.key === cacheKey
|
|
177
|
+
&& bundledCatalogCache.expiresAt > Date.now()
|
|
178
|
+
) {
|
|
133
179
|
return bundledCatalogCache.value;
|
|
134
180
|
}
|
|
135
|
-
const
|
|
136
|
-
const execFile = deps.execFileSync ?? (execFileSync as unknown as ExecFile);
|
|
137
|
-
for (const command of candidates) {
|
|
181
|
+
for (const command of unique(candidates)) {
|
|
138
182
|
try {
|
|
139
|
-
const catalog = parseCatalogJson(runCodexDebugModels(command, execFile));
|
|
183
|
+
const catalog = parseCatalogJson(runCodexDebugModels(command, execFile, deps));
|
|
140
184
|
if (catalog && findNativeTemplate(catalog)) {
|
|
141
|
-
if (useCache
|
|
185
|
+
if (useCache && cacheKey) {
|
|
186
|
+
bundledCatalogCache = {
|
|
187
|
+
key: cacheKey,
|
|
188
|
+
expiresAt: Date.now() + BUNDLED_CATALOG_CACHE_MS,
|
|
189
|
+
value: catalog,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
142
192
|
return catalog;
|
|
143
193
|
}
|
|
144
194
|
} catch { /* try next candidate */ }
|
|
145
195
|
}
|
|
146
|
-
if (useCache
|
|
196
|
+
if (useCache && cacheKey) {
|
|
197
|
+
bundledCatalogCache = {
|
|
198
|
+
key: cacheKey,
|
|
199
|
+
expiresAt: Date.now() + BUNDLED_CATALOG_CACHE_MS,
|
|
200
|
+
value: null,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
147
203
|
return null;
|
|
148
204
|
}
|
|
149
205
|
|