From 328e22c0791299886aacf0d308b72df0def1ec38 Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 30 Jun 2026 01:09:26 +0000 Subject: [PATCH 1/2] fix(prompting): bounded stdout drain on successful probes Even on exit 0 the GPU probe can leave a descendant holding stdout open. Race the EOF wait against a 250ms grace window so the success path cancels the reader instead of blocking until the descendant exits, and unref the prep deadline timer so a one-shot CLI is not held alive by it once all prep work returns. --- .../coding-agent/src/system-prompt.test.ts | 18 +++++++++++++++++- packages/coding-agent/src/system-prompt.ts | 17 ++++++++++++++--- 2 files changed, 31 insertions(+), 4 deletions(-) diff --git a/packages/coding-agent/src/system-prompt.test.ts b/packages/coding-agent/src/system-prompt.test.ts index a55f77208..89274c65e 100644 --- a/packages/coding-agent/src/system-prompt.test.ts +++ b/packages/coding-agent/src/system-prompt.test.ts @@ -14,6 +14,7 @@ async function runProbeScenario(options: { runs: number; sleepSeconds?: number; holdStdoutOpen?: boolean; + descendantHoldsStdout?: boolean; }): Promise { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "omp-gpu-probe-")); try { @@ -25,7 +26,7 @@ async function runProbeScenario(options: { const lspciPath = path.join(binDir, "lspci"); await Bun.write( lspciPath, - '#!/usr/bin/env sh\nprintf x >> "$OMP_GPU_PROBE_COUNT"\nif [ "$OMP_GPU_PROBE_HOLD_STDOUT_OPEN" = "true" ]; then sleep "$OMP_GPU_PROBE_SLEEP" & wait "$!"; fi\nif [ -n "$OMP_GPU_PROBE_SLEEP" ]; then exec sleep "$OMP_GPU_PROBE_SLEEP"; fi\nexit 0\n', + '#!/usr/bin/env sh\nprintf x >> "$OMP_GPU_PROBE_COUNT"\nif [ "$OMP_GPU_PROBE_DESCENDANT_HOLDS_STDOUT" = "true" ]; then sleep "$OMP_GPU_PROBE_SLEEP" & exit 0; fi\nif [ "$OMP_GPU_PROBE_HOLD_STDOUT_OPEN" = "true" ]; then sleep "$OMP_GPU_PROBE_SLEEP" & wait "$!"; fi\nif [ -n "$OMP_GPU_PROBE_SLEEP" ]; then exec sleep "$OMP_GPU_PROBE_SLEEP"; fi\nexit 0\n', ); await fs.chmod(lspciPath, 0o755); @@ -83,6 +84,11 @@ console.log(JSON.stringify({ elapsedMs: Math.round(performance.now() - startedAt } else { delete env.OMP_GPU_PROBE_HOLD_STDOUT_OPEN; } + if (options.descendantHoldsStdout) { + env.OMP_GPU_PROBE_DESCENDANT_HOLDS_STDOUT = "true"; + } else { + delete env.OMP_GPU_PROBE_DESCENDANT_HOLDS_STDOUT; + } const childStartedAt = performance.now(); const child = Bun.spawn([process.execPath, scenarioPath], { stdout: "pipe", stderr: "pipe", env }); @@ -118,4 +124,14 @@ describe.skipIf(process.platform !== "linux")("system prompt GPU probe", () => { // not linger until a descendant holding stdout (sleep 7) exits on its own. expect(result.childElapsedMs).toBeLessThan(6500); }, 15_000); + + it("does not wait on stdout held by a descendant after a successful probe", async () => { + const result = await runProbeScenario({ runs: 1, sleepSeconds: 3, descendantHoldsStdout: true }); + + expect(result.cached).toEqual({ gpu: null }); + // Probe exits 0 immediately but leaves a backgrounded sleep holding the stdout + // pipe. The success path MUST bound the drain wait, not block until sleep exits. + expect(result.elapsedMs).toBeLessThan(2000); + expect(result.childElapsedMs).toBeLessThan(2000); + }, 15_000); }); diff --git a/packages/coding-agent/src/system-prompt.ts b/packages/coding-agent/src/system-prompt.ts index d409d0e3a..8aab13c2e 100644 --- a/packages/coding-agent/src/system-prompt.ts +++ b/packages/coding-agent/src/system-prompt.ts @@ -113,6 +113,8 @@ function parseWmicTable(output: string, header: string): string | null { const SYSTEM_PROMPT_PREP_TIMEOUT_MS = 5000; /** Kept below prep timeout so timed-out probes can still write the null cache before fallback. */ const GPU_PROBE_TIMEOUT_MS = SYSTEM_PROMPT_PREP_TIMEOUT_MS - 500; +/** Drop stdout from a probe descendant that inherited the pipe after the probe exited. */ +const GPU_PROBE_STDOUT_DRAIN_MS = 250; async function runGpuProbe(cmd: string[]): Promise { try { @@ -138,12 +140,17 @@ async function runGpuProbe(cmd: string[]): Promise { stdout += decoder.decode(); })(); const exitCode = await proc.exited; - if (exitCode !== 0) { + // Even on exit 0, a probe wrapper can leave a descendant holding stdout open. + // Bound the EOF wait so getCachedGpu cannot outlive the probe in either path. + const drained = await Promise.race([ + stdoutDone.then(() => "ok" as const).catch(() => "err" as const), + Bun.sleep(GPU_PROBE_STDOUT_DRAIN_MS).then(() => "timeout" as const), + ]); + if (exitCode !== 0 || drained !== "ok") { await stdoutReader.cancel().catch(() => undefined); await stdoutDone.catch(() => undefined); return null; } - await stdoutDone; return stdout; } catch { return null; @@ -531,7 +538,10 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): gpu: undefined as string | undefined, }; - const deadline = Bun.sleep(SYSTEM_PROMPT_PREP_TIMEOUT_MS).then(() => "__timeout__" as const); + const { promise: deadline, resolve: fireDeadline } = Promise.withResolvers<"__timeout__">(); + const deadlineTimer = setTimeout(() => fireDeadline("__timeout__"), SYSTEM_PROMPT_PREP_TIMEOUT_MS); + // Unref so a fast prep does not hold a one-shot CLI alive waiting for this timer. + deadlineTimer.unref(); const timedOut: string[] = []; const failed: Array<{ name: string; error: unknown }> = []; @@ -630,6 +640,7 @@ export async function buildSystemPrompt(options: BuildSystemPromptOptions = {}): withDeadline("resolveActiveRepoContext", activeRepoContextPromise, prepDefaults.activeRepoContext), withDeadline("getCachedGpu", gpuPromise, prepDefaults.gpu), ]); + clearTimeout(deadlineTimer); const agentsMdFiles = Array.from(new Set(workspaceTree.agentsMdFiles)).sort().slice(0, AGENTS_MD_LIMIT); if (timedOut.length > 0) { From 0ae97347082b93a1bffdccafc98a6b6025675786 Mon Sep 17 00:00:00 2001 From: roboomp Date: Tue, 30 Jun 2026 01:34:11 +0000 Subject: [PATCH 2/2] fix(prompting): kept gpu probe output when descendants delay eof On a probe that exits 0 with valid output but leaves a descendant holding stdout open, the bounded drain previously discarded the captured bytes and cached { gpu: null }. Use the already-captured stdout when the probe itself succeeded; only treat a non-zero/timeout exit as a failure. --- .../coding-agent/src/system-prompt.test.ts | 23 ++++++++++++++++++- packages/coding-agent/src/system-prompt.ts | 8 +++---- 2 files changed, 26 insertions(+), 5 deletions(-) diff --git a/packages/coding-agent/src/system-prompt.test.ts b/packages/coding-agent/src/system-prompt.test.ts index 89274c65e..456d1964f 100644 --- a/packages/coding-agent/src/system-prompt.test.ts +++ b/packages/coding-agent/src/system-prompt.test.ts @@ -15,6 +15,7 @@ async function runProbeScenario(options: { sleepSeconds?: number; holdStdoutOpen?: boolean; descendantHoldsStdout?: boolean; + validOutput?: string; }): Promise { const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "omp-gpu-probe-")); try { @@ -26,7 +27,7 @@ async function runProbeScenario(options: { const lspciPath = path.join(binDir, "lspci"); await Bun.write( lspciPath, - '#!/usr/bin/env sh\nprintf x >> "$OMP_GPU_PROBE_COUNT"\nif [ "$OMP_GPU_PROBE_DESCENDANT_HOLDS_STDOUT" = "true" ]; then sleep "$OMP_GPU_PROBE_SLEEP" & exit 0; fi\nif [ "$OMP_GPU_PROBE_HOLD_STDOUT_OPEN" = "true" ]; then sleep "$OMP_GPU_PROBE_SLEEP" & wait "$!"; fi\nif [ -n "$OMP_GPU_PROBE_SLEEP" ]; then exec sleep "$OMP_GPU_PROBE_SLEEP"; fi\nexit 0\n', + '#!/usr/bin/env sh\nprintf x >> "$OMP_GPU_PROBE_COUNT"\nif [ -n "$OMP_GPU_PROBE_VALID_OUTPUT" ]; then printf "%s\\n" "$OMP_GPU_PROBE_VALID_OUTPUT"; fi\nif [ "$OMP_GPU_PROBE_DESCENDANT_HOLDS_STDOUT" = "true" ]; then sleep "$OMP_GPU_PROBE_SLEEP" & exit 0; fi\nif [ "$OMP_GPU_PROBE_HOLD_STDOUT_OPEN" = "true" ]; then sleep "$OMP_GPU_PROBE_SLEEP" & wait "$!"; fi\nif [ -n "$OMP_GPU_PROBE_SLEEP" ]; then exec sleep "$OMP_GPU_PROBE_SLEEP"; fi\nexit 0\n', ); await fs.chmod(lspciPath, 0o755); @@ -89,6 +90,11 @@ console.log(JSON.stringify({ elapsedMs: Math.round(performance.now() - startedAt } else { delete env.OMP_GPU_PROBE_DESCENDANT_HOLDS_STDOUT; } + if (options.validOutput !== undefined) { + env.OMP_GPU_PROBE_VALID_OUTPUT = options.validOutput; + } else { + delete env.OMP_GPU_PROBE_VALID_OUTPUT; + } const childStartedAt = performance.now(); const child = Bun.spawn([process.execPath, scenarioPath], { stdout: "pipe", stderr: "pipe", env }); @@ -134,4 +140,19 @@ describe.skipIf(process.platform !== "linux")("system prompt GPU probe", () => { expect(result.elapsedMs).toBeLessThan(2000); expect(result.childElapsedMs).toBeLessThan(2000); }, 15_000); + + it("keeps probe output captured before a descendant delays EOF", async () => { + const result = await runProbeScenario({ + runs: 1, + sleepSeconds: 3, + descendantHoldsStdout: true, + validOutput: "00:02.0 VGA compatible controller: NVIDIA TestGPU", + }); + + // Probe exited 0 with valid output before bg sleep held stdout open. + // Captured stdout MUST be cached, not discarded as if the probe failed. + expect(result.cached).toEqual({ gpu: "02.0 VGA compatible controller: NVIDIA TestGPU" }); + expect(result.elapsedMs).toBeLessThan(2000); + expect(result.childElapsedMs).toBeLessThan(2000); + }, 15_000); }); diff --git a/packages/coding-agent/src/system-prompt.ts b/packages/coding-agent/src/system-prompt.ts index 8aab13c2e..3c410b396 100644 --- a/packages/coding-agent/src/system-prompt.ts +++ b/packages/coding-agent/src/system-prompt.ts @@ -141,17 +141,17 @@ async function runGpuProbe(cmd: string[]): Promise { })(); const exitCode = await proc.exited; // Even on exit 0, a probe wrapper can leave a descendant holding stdout open. - // Bound the EOF wait so getCachedGpu cannot outlive the probe in either path. + // Bound the EOF wait so getCachedGpu cannot outlive the probe in either path; + // keep whatever bytes the reader already captured before cancelling. const drained = await Promise.race([ stdoutDone.then(() => "ok" as const).catch(() => "err" as const), Bun.sleep(GPU_PROBE_STDOUT_DRAIN_MS).then(() => "timeout" as const), ]); - if (exitCode !== 0 || drained !== "ok") { + if (drained !== "ok") { await stdoutReader.cancel().catch(() => undefined); await stdoutDone.catch(() => undefined); - return null; } - return stdout; + return exitCode === 0 ? stdout : null; } catch { return null; }