From 8acb9c6201e588df17f8c2a2c079c6b96d24330f Mon Sep 17 00:00:00 2001 From: Adam Kauffman <26984068+A9G-Data-Droid@users.noreply.github.com> Date: Tue, 21 Apr 2026 16:18:57 -0700 Subject: [PATCH] Do not pipe stdio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `src/run/ralph-process.ts:216` - changed ["ignore", "pipe", "pipe"] to ["ignore", "ignore", "ignore"]. - When the dashboard is active, stdout/stderr were piped but never drained; after ~64KiB accumulated the kernel blocked bash's next write indefinitely. - Switching to "ignore" eliminates the pipes entirely — the dashboard reads everything from files already, so nothing is lost. - The detach() method's if (child.stdout) guards were already correct for this case. `tests/run/ralph-process.test.ts` - updated the existing stdio assertion - added "detach does not throw when stdout/stderr are null (ignore stdio mode)" to make the guard's intent explicit and prevent regression. --- src/run/ralph-process.ts | 2 +- tests/run/ralph-process.test.ts | 17 +++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/run/ralph-process.ts b/src/run/ralph-process.ts index e3ac703..d508c0e 100644 --- a/src/run/ralph-process.ts +++ b/src/run/ralph-process.ts @@ -213,7 +213,7 @@ export function spawnRalphLoop( const child = spawn(cachedBashCommand ?? "bash", [BASH_RALPH_LOOP_PATH], { cwd: projectDir, env, - stdio: options.inheritStdio ? "inherit" : ["ignore", "pipe", "pipe"], + stdio: options.inheritStdio ? "inherit" : ["ignore", "ignore", "ignore"], detached: process.platform !== "win32", windowsHide: true, }); diff --git a/tests/run/ralph-process.test.ts b/tests/run/ralph-process.test.ts index 5c0317c..505ff77 100644 --- a/tests/run/ralph-process.test.ts +++ b/tests/run/ralph-process.test.ts @@ -393,7 +393,7 @@ describe("spawnRalphLoop", () => { ); }); - it("uses piped stdio when inheritStdio is false", async () => { + it("uses ignored stdio when inheritStdio is false", async () => { const mockChild = createMockChild(); mockSpawn.mockReturnValue(mockChild); @@ -404,11 +404,24 @@ describe("spawnRalphLoop", () => { "bash", expect.any(Array), expect.objectContaining({ - stdio: ["ignore", "pipe", "pipe"], + stdio: ["ignore", "ignore", "ignore"], }) ); }); + it("detach does not throw when stdout/stderr are null (ignore stdio mode)", async () => { + const mockChild = createMockChild(); + mockSpawn.mockReturnValue(mockChild); + + const { spawnRalphLoop } = await import("../../src/run/ralph-process.js"); + const rp = spawnRalphLoop("/project", "claude-code", { inheritStdio: false }); + + // child.stdout and child.stderr are null when stdio is "ignore" — detach() must not throw + expect(() => rp.detach()).not.toThrow(); + expect(mockChild.unref).toHaveBeenCalled(); + expect(rp.state).toBe("detached"); + }); + it("tracks exit code and updates state on child exit", async () => { const mockChild = createMockChild(); mockSpawn.mockReturnValue(mockChild);