diff --git a/src/codex/prompt-text-probe.ts b/src/codex/prompt-text-probe.ts index 6db8d1e49e..7782b34298 100644 --- a/src/codex/prompt-text-probe.ts +++ b/src/codex/prompt-text-probe.ts @@ -87,6 +87,13 @@ export interface PromptTextProbe { detail?: string; } +// The prompt page is URL-addressable, so multiple mounts can ask for the same +// process-backed snapshot at once. Keep that work single-flight: callers share +// the current read instead of each consuming another child process and output +// buffer. The slot is cleared after either success or failure so later operator +// visits still observe current prompt state. +let activeProbe: Promise | null = null; + function resolveCodexBinary(): string | null { const candidates = [ join(homedir(), ".codex/packages/standalone/current/bin/codex"), @@ -182,7 +189,7 @@ export const extractSectionsForTests = extractSections; * `cwd` matters: AGENTS.md and environment context are directory-dependent, so a * probe from the wrong place would describe a prompt the user never sees. */ -export async function probePromptText(timeoutMs = 15_000): Promise { +async function performPromptTextProbe(timeoutMs: number): Promise { // The probe runs in CODEX_HOME, never in a caller-supplied directory. A `cwd` // parameter let an authenticated request read any readable folder's AGENTS.md, // and it also described a prompt that depends on where Codex happened to run. @@ -236,3 +243,15 @@ export async function probePromptText(timeoutMs = 15_000): Promise { + if (activeProbe) return activeProbe; + + const probe = performPromptTextProbe(timeoutMs); + activeProbe = probe; + try { + return await probe; + } finally { + if (activeProbe === probe) activeProbe = null; + } +} diff --git a/tests/codex-prompt-route.test.ts b/tests/codex-prompt-route.test.ts index 31f0dfd421..4a3635024c 100644 --- a/tests/codex-prompt-route.test.ts +++ b/tests/codex-prompt-route.test.ts @@ -799,6 +799,14 @@ describe("020 coverage completions", () => { // Decoding per chunk corrupts UTF-8 that straddles a chunk boundary. expect(probe).toContain("Buffer.concat(chunks).toString(\"utf8\")"); }); + test("27. concurrent prompt reads share one process-backed probe", async () => { + // The prompt panel is URL-addressable. Repeated mounts must not turn + // concurrent authenticated reads into concurrent Codex child processes. + const probe = await Bun.file(new URL("../src/codex/prompt-text-probe.ts", import.meta.url)).text(); + expect(probe).toContain("let activeProbe: Promise | null = null;"); + expect(probe).toContain("if (activeProbe) return activeProbe;"); + expect(probe).toContain("if (activeProbe === probe) activeProbe = null;"); + }); test("24. every ownership state is named, not collapsed into a boolean", async () => { // developerInstructionsOwned:false covers an ABSENT key and an EXTERNAL one, and // a GUI that cannot tell them apart hides its own create affordance from every