Skip to content

Commit b2d1104

Browse files
committed
clarify active Bun runtime diagnostics
1 parent e1fc0f2 commit b2d1104

8 files changed

Lines changed: 78 additions & 14 deletions

File tree

src/cli/doctor.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -523,6 +523,8 @@ export async function probeWham(fetchImpl: typeof fetch = fetch): Promise<WhamPr
523523
export type ServiceMemoryData = {
524524
pid: number;
525525
bunVersion: string;
526+
bunRevision?: string;
527+
bunRuntimeSource?: "override" | "bundled" | "process" | null;
526528
platform: string;
527529
rss: number;
528530
heapUsed: number;
@@ -579,6 +581,10 @@ export async function fetchServiceMemory(
579581
data: {
580582
pid: body.pid,
581583
bunVersion: body.bunVersion,
584+
bunRevision: typeof body.bunRevision === "string" ? body.bunRevision : undefined,
585+
bunRuntimeSource: body.bunRuntimeSource === "override" || body.bunRuntimeSource === "bundled" || body.bunRuntimeSource === "process"
586+
? body.bunRuntimeSource
587+
: null,
582588
platform: typeof body.platform === "string" ? body.platform : "unknown",
583589
rss: body.rss,
584590
heapUsed: typeof body.heapUsed === "number" ? body.heapUsed : 0,
@@ -625,7 +631,14 @@ export function formatServiceMemoryLines(report: ServiceMemoryReport): string[]
625631
return lines;
626632
}
627633
const d = report.data;
628-
lines.push(` ok service pid ${d.pid}: Bun ${d.bunVersion} on ${d.platform}`);
634+
const sourceLabel = d.bunRuntimeSource === "override"
635+
? "OPENCODEX_BUN_PATH override"
636+
: d.bunRuntimeSource;
637+
const identityDetails = [
638+
d.bunRevision ? `revision=${d.bunRevision}` : null,
639+
sourceLabel ? `source=${sourceLabel}` : null,
640+
].filter((value): value is string => Boolean(value)).join(", ");
641+
lines.push(` ok service pid ${d.pid}: Bun ${d.bunVersion} on ${d.platform}${identityDetails ? ` (${identityDetails})` : ""}`);
629642
const observed = observedMemory(d);
630643
const observedBytes = d.observedBytes ?? d.watchdog?.observedBytes ?? observed.bytes;
631644
const observedMetric = d.observedMetric ?? d.watchdog?.observedMetric ?? observed.metric;
@@ -652,12 +665,15 @@ export function formatServiceMemoryLines(report: ServiceMemoryReport): string[]
652665
} else {
653666
lines.push(" !! high RSS, indeterminate split — capture two doctor runs over time to see the trend");
654667
}
655-
// Version-claiming (never binary-claiming): the endpoint cannot distinguish
656-
// the bundled binary from an OPENCODEX_BUN_PATH override of the same version.
657668
if (d.platform === "win32" && d.eagerRelay?.reason === "auto-known-bad") {
658-
lines.push(` service is running Bun ${d.bunVersion} on Windows — a version affected by the upstream Bun memory issue.`);
659-
lines.push(" Options: wait for a bundled runtime update, or set OPENCODEX_BUN_PATH to a runtime you trust (unvalidated — own risk),");
660-
lines.push(" or opt into streamMode \"eager-relay\" via PUT /api/settings (crash risk on this runtime; see docs).");
669+
if (d.bunRuntimeSource === "override") {
670+
lines.push(` OPENCODEX_BUN_PATH is already active for Bun ${d.bunVersion}; automatic eager relay has not validated this runtime revision.`);
671+
lines.push(" Keep streamMode \"auto\" for the conservative path, or explicitly opt into \"eager-relay\" at your own risk; see docs.");
672+
} else {
673+
lines.push(` service is running Bun ${d.bunVersion} on Windows — a version affected by the upstream Bun memory issue.`);
674+
lines.push(" Options: wait for a bundled runtime update, or set OPENCODEX_BUN_PATH to a runtime you trust (unvalidated — own risk),");
675+
lines.push(" or opt into streamMode \"eager-relay\" via PUT /api/settings (crash risk on this runtime; see docs).");
676+
}
661677
}
662678
return lines;
663679
}

src/lib/bun-runtime.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,12 @@ export { isRealBunBinary };
1919
const require = createRequire(import.meta.url);
2020

2121
const BUN_OVERRIDE_ENV = "OPENCODEX_BUN_PATH";
22+
export const BUN_RUNTIME_SOURCE_ENV = "OCX_BUN_RUNTIME_SOURCE";
23+
export type BunRuntimeSource = "override" | "bundled" | "process";
2224

2325
export type DurableBunRuntime = {
2426
path: string;
25-
source: "override" | "bundled" | "process";
27+
source: BunRuntimeSource;
2628
overrideEnv: typeof BUN_OVERRIDE_ENV;
2729
};
2830

@@ -60,6 +62,18 @@ export function durableBunRuntime(): DurableBunRuntime {
6062
return { path: process.execPath, source: "process", overrideEnv: BUN_OVERRIDE_ENV };
6163
}
6264

65+
/**
66+
* Runtime origin reported by the running proxy. Durable launchers bake the
67+
* source selected at install time so a later shell environment cannot
68+
* misidentify an already-installed service. Direct starts fall back to the
69+
* same resolver that selected their Bun executable.
70+
*/
71+
export function effectiveBunRuntimeSource(): BunRuntimeSource {
72+
const baked = process.env[BUN_RUNTIME_SOURCE_ENV]?.trim();
73+
if (baked === "override" || baked === "bundled" || baked === "process") return baked;
74+
return durableBunRuntime().source;
75+
}
76+
6377
/**
6478
* Bun path to bake into durable artifacts (launchd/systemd/Task Scheduler and
6579
* the Codex auto-start shim). Prefer the bundled binary — it lives under the

src/server/management/system-routes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
* dashboard drain-and-restart confirm UX — never request bodies or IDs.
2121
*/
2222
import { selectEagerPath } from "../../lib/bun-stream-caps";
23+
import { effectiveBunRuntimeSource } from "../../lib/bun-runtime";
2324
import { getActiveTurnCount, isDraining } from "../lifecycle";
2425
import { getActiveMemoryWatchdog, observedMemoryCounter } from "../memory-watchdog";
2526
import { responseStateMetrics } from "../../responses/state";
@@ -75,6 +76,7 @@ export async function handleSystemRoutes(ctx: ManagementContext): Promise<Respon
7576
pid: process.pid,
7677
bunVersion: Bun.version,
7778
bunRevision: Bun.revision,
79+
bunRuntimeSource: effectiveBunRuntimeSource(),
7880
platform: process.platform,
7981
uptimeSeconds: process.uptime(),
8082
rss: usage.rss,

src/service.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { loadConfig } from "./config";
1515
import { restoreNativeCodex } from "./codex/inject";
1616
import { stripGrokConfig } from "./grok/inject";
1717
import { isWslRuntime } from "./codex/home";
18-
import { durableBunPath, durableBunRuntime } from "./lib/bun-runtime";
18+
import { BUN_RUNTIME_SOURCE_ENV, durableBunPath, durableBunRuntime } from "./lib/bun-runtime";
1919
import { isProcessAlive, stopProxy } from "./lib/process-control";
2020
import { serviceApiTokenFilePath } from "./lib/service-secrets";
2121
import { randomUUID } from "node:crypto";
@@ -269,12 +269,14 @@ function writeServiceApiTokenFile(): string | null {
269269

270270
export function buildPlist(): string {
271271
const { bun, cli } = cliEntry();
272+
const bunRuntime = durableBunRuntime();
272273
const log = logPath();
273274
const path = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
274275
const codexHome = process.env.CODEX_HOME?.trim();
275276
const opencodexHome = process.env.OPENCODEX_HOME?.trim();
276277
const envLines = [
277278
` <key>OCX_SERVICE</key><string>1</string>`,
279+
` <key>${BUN_RUNTIME_SOURCE_ENV}</key><string>${bunRuntime.source}</string>`,
278280
` <key>PATH</key><string>${plistString(path)}</string>`,
279281
codexHome ? ` <key>CODEX_HOME</key><string>${plistString(codexHome)}</string>` : null,
280282
opencodexHome ? ` <key>OPENCODEX_HOME</key><string>${plistString(opencodexHome)}</string>` : null,
@@ -1032,6 +1034,7 @@ export function buildWindowsServiceScript(entry = cliEntry(), port = resolveServ
10321034
// it to UTF-8 is safe (no leak into user shells) and lets cmd parse UTF-8 remnants.
10331035
"chcp 65001 >nul",
10341036
windowsBatchSet("OCX_SERVICE", "1"),
1037+
windowsBatchSet(BUN_RUNTIME_SOURCE_ENV, bunRuntime.source),
10351038
windowsBatchSet("PATH", path, "pathList"),
10361039
windowsBatchSet("CODEX_HOME", process.env.CODEX_HOME?.trim(), "path"),
10371040
windowsBatchSet("OPENCODEX_HOME", process.env.OPENCODEX_HOME?.trim(), "path"),
@@ -1571,12 +1574,14 @@ function unitPath(): string {
15711574

15721575
export function buildUnit(): string {
15731576
const { bun, cli } = cliEntry();
1577+
const bunRuntime = durableBunRuntime();
15741578
const log = logPath();
15751579
const path = process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin";
15761580
const codexHome = systemdEnvironmentAssignment("CODEX_HOME", process.env.CODEX_HOME?.trim());
15771581
const opencodexHome = systemdEnvironmentAssignment("OPENCODEX_HOME", process.env.OPENCODEX_HOME?.trim());
15781582
const envLines = [
15791583
systemdEnvironmentAssignment("OCX_SERVICE", "1"),
1584+
systemdEnvironmentAssignment(BUN_RUNTIME_SOURCE_ENV, bunRuntime.source),
15801585
systemdEnvironmentAssignment("PATH", path),
15811586
codexHome,
15821587
opencodexHome,
@@ -2163,4 +2168,3 @@ export async function serviceCommand(...args: (string | undefined)[]): Promise<v
21632168
process.exit(1);
21642169
}
21652170
}
2166-

structure/05_gui-and-management-api.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ this document owns is which module holds which area and what invariant that area
7878
| V2 / Multi-agent mode | `GET/PUT /api/v2` — reports/sets the codex `multi_agent_v2` feature flag, the 3-state `multiAgentMode` override (`v1`/`default`/`v2`), and the logical maximum thread count. Selecting `v2` enables the native flag and migrates `[agents] max_threads` to the v2 key; selecting `v1` disables it and migrates the same value back. `default` leaves the native flag unchanged. PUT accepts `enabled`, `multiAgentMode`, and/or the compatibility-named `maxConcurrentThreadsPerSession`; contradictory mode/flag pairs are rejected before writes. Every transition is rollback-safe and resyncs the catalog. |
7979
| Logs & Debug | One sidebar entry (`/#logs`) with two tabs. Logs tab: request/runtime logs for local diagnosis. Debug tab (`/#logs/debug`; legacy `/#debug` deep links redirect there): provider + usage toggles, refresh/follow log viewer. `GET/PUT /api/debug`; `GET /api/debug/logs` and `GET /api/debug/usage-logs` (monotonic `after` cursor, legacy `since` accepted). CLI: `ocx debug provider|usage …` (both streams via running proxy API). |
8080
| Usage | `GET /api/usage` aggregate read-only summary derived from `~/.opencodex/usage.jsonl`; measured / reported / unreported / unsupported / estimated counts, daily zero-filled grid, model and provider breakdowns. Never exposes prompts. |
81-
| System | `POST /api/system/restart` restarts the proxy in place. `GET /api/system/memory` — service-process runtime/memory identity (pid, Bun version/revision, platform, RSS/heap/external/ArrayBuffers scalars, observed memory = max(RSS, external, ArrayBuffers), `bun:jsc` heap context, streamMode + eager-relay gate decision, watchdog snapshot sliced to the last 60 samples). Scalar-only payload; rides the standard management auth gate and must never move to unauthenticated `/healthz`. Consumed by `ocx doctor`'s Memory/runtime section and the dashboard Memory observability card. |
81+
| System | `POST /api/system/restart` restarts the proxy in place. `GET /api/system/memory` — service-process runtime/memory identity (pid, Bun version/revision/runtime source, platform, RSS/heap/external/ArrayBuffers scalars, observed memory = max(RSS, external, ArrayBuffers), `bun:jsc` heap context, streamMode + eager-relay gate decision, watchdog snapshot sliced to the last 60 samples). Scalar-only payload; rides the standard management auth gate and must never move to unauthenticated `/healthz`. Consumed by `ocx doctor`'s Memory/runtime section and the dashboard Memory observability card. |
8282
| Stop | `POST /api/stop` — restore native Codex, stop any installed service, and exit the proxy. |
8383
| Diagnostics/sync | `src/server/management/config-routes.ts``GET /api/diagnostics/project-config` reports project-level Codex config that bypasses managed routing; `POST /api/sync` re-runs catalog/config sync. The diagnostic reports the bypass; it does not rewrite the project file. |
8484
| Sidecar/shadow-call settings | `src/server/management/config-routes.ts``GET/PUT /api/sidecar-settings` and `GET/PUT /api/shadow-call-settings`. PUT accepts model and backend plus optional `webSearch.reasoning` and `vision.maxDescriptionsPerTurn`; the read and PUT-response payload reports model, backend, and the vision per-turn limit. Credentials live in the provider and OAuth stores instead. Both shadow-call responses also report the resolved `sourceModels` — the prefixes the runtime actually intercepts (`src/lib/shadow-call.ts`, default `gpt-5.4-mini` + `gpt-5.6-luna`), so no client hard-codes a helper slug that a Codex release can invalidate. |

tests/doctor.test.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,8 @@ describe("service memory section (#314 WP4)", () => {
326326
const baseData: ServiceMemoryData = {
327327
pid: 4242,
328328
bunVersion: "1.3.14",
329+
bunRevision: "1.3.14+0d9b296a",
330+
bunRuntimeSource: "bundled",
329331
platform: "win32",
330332
rss: 5 * 1024 ** 3,
331333
heapUsed: 200 * 1024 ** 2,
@@ -401,11 +403,28 @@ describe("service memory section (#314 WP4)", () => {
401403
expect(lines.some(l => l.includes("looks normal"))).toBe(false);
402404
});
403405

404-
test("guidance gating: win32 + auto-known-bad prints version-claiming guidance", () => {
406+
test("guidance gating: bundled win32 + auto-known-bad offers an override", () => {
405407
const lines = formatServiceMemoryLines({ status: "ok", data: baseData });
408+
expect(lines.some(l => l.includes("source=bundled"))).toBe(true);
406409
expect(lines.some(l => l.includes("OPENCODEX_BUN_PATH"))).toBe(true);
407-
// Version-claiming, never binary-claiming.
408-
expect(lines.join("\n")).not.toContain("bundled binary");
410+
});
411+
412+
test("guidance gating: active override reports revision without repeating setup advice", () => {
413+
const lines = formatServiceMemoryLines({
414+
status: "ok",
415+
data: {
416+
...baseData,
417+
bunVersion: "1.4.0",
418+
bunRevision: "1.4.0-canary.1+5f65d3785",
419+
bunRuntimeSource: "override",
420+
},
421+
});
422+
const text = lines.join("\n");
423+
expect(text).toContain("revision=1.4.0-canary.1+5f65d3785");
424+
expect(text).toContain("source=OPENCODEX_BUN_PATH override");
425+
expect(text).toContain("OPENCODEX_BUN_PATH is already active");
426+
expect(text).not.toContain("set OPENCODEX_BUN_PATH");
427+
expect(text).not.toContain("a version affected by the upstream Bun memory issue");
409428
});
410429

411430
test("guidance gating: darwin auto-off or fixed Windows runtime prints no override guidance", () => {

tests/memory-watchdog.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -175,7 +175,7 @@ describe("GET /api/system/memory", () => {
175175
expect(res).not.toBeNull();
176176
expect(res!.status).toBe(200);
177177
const body = await res!.json() as {
178-
pid: number; bunVersion: string; platform: string; rss: number;
178+
pid: number; bunVersion: string; bunRevision: string; bunRuntimeSource: string; platform: string; rss: number;
179179
heapUsed: number; external: number; arrayBuffers: number; observedBytes: number; observedMetric: string;
180180
jscHeap: { heapSize: number } | null;
181181
responseState: { count: number; totalBytes: number; largestBytes: number; oldestAgeMs: number };
@@ -189,6 +189,8 @@ describe("GET /api/system/memory", () => {
189189
};
190190
expect(body.pid).toBe(process.pid);
191191
expect(body.bunVersion).toBe(Bun.version);
192+
expect(body.bunRevision).toBe(Bun.revision);
193+
expect(["override", "bundled", "process"]).toContain(body.bunRuntimeSource);
192194
expect(body.rss).toBeGreaterThan(0);
193195
expect(body.heapUsed).toBeGreaterThan(0);
194196
expect(body.external).toBeGreaterThanOrEqual(0);

tests/service.test.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,13 @@ describe("service listen-port bake", () => {
7979
expect(buildPlist()).toContain("start --port 13337");
8080
expect(buildUnit()).toContain("start --port 13337");
8181
});
82+
83+
test("durable service artifacts bake the selected Bun runtime source", () => {
84+
const script = buildWindowsServiceScript({ bun: "C:\\OpenCodex\\bun.exe", cli: "C:\\OpenCodex\\cli.ts" });
85+
expect(script).toMatch(/set "OCX_BUN_RUNTIME_SOURCE=(override|bundled|process)"/);
86+
expect(buildPlist()).toMatch(/<key>OCX_BUN_RUNTIME_SOURCE<\/key><string>(override|bundled|process)<\/string>/);
87+
expect(buildUnit()).toMatch(/Environment="OCX_BUN_RUNTIME_SOURCE=(override|bundled|process)"/);
88+
});
8289
});
8390

8491
describe("systemd service unit", () => {

0 commit comments

Comments
 (0)