From 6339d18a527183784a5e52254a3c4d195cc8ec75 Mon Sep 17 00:00:00 2001 From: cozy Date: Sat, 11 Apr 2026 02:03:34 -0400 Subject: [PATCH] fix(engines): swallow EPIPE on subprocess stdin to prevent host crash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a CLI subprocess exits before the parent finishes writing the prompt (e.g. argument validation fails on launch), Node emits an EPIPE error event on child.stdin. Without a listener, that error bubbles as an uncaught exception and crashes whatever process owns the engine — Vitest in CI, the gateway in production, or the agent loop interactively. The exit code path that already runs after the subprocess closes is the source of truth for failure reporting, so silently dropping EPIPE on the stdin stream is safe. Other error codes are surfaced as a text_delta so they don't disappear silently. Surfaced as an uncaught error in the subprocess.test.ts run on PR #12 CI: all 2711 tests passed but the runner exited 1. --- packages/engines/src/subprocess.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/engines/src/subprocess.ts b/packages/engines/src/subprocess.ts index d202818..4d3404e 100644 --- a/packages/engines/src/subprocess.ts +++ b/packages/engines/src/subprocess.ts @@ -306,6 +306,15 @@ export class SubprocessEngine implements IEngine { // Write prompt to stdin if using stdin mode. if (child.stdin) { + // The subprocess can exit before we finish writing — for example if + // the CLI rejects the args and dies during launch. Without a listener, + // the resulting EPIPE on stdin bubbles as an uncaught exception and + // crashes the host process (Vitest, gateway, agent loop, etc.). The + // exit code path below is the source of truth for failure reporting, + // so swallowing EPIPE here is safe. + child.stdin.on('error', (err: NodeJS.ErrnoException) => { + if (err.code !== 'EPIPE') emit({ type: 'text_delta', delta: `\n[stdin error: ${err.message}]\n` }); + }); if (this.promptMode === 'stdin') { child.stdin.write(prompt); // Keep stdin open for steer() permission-prompt responses.