diff --git a/drivers/stream-sandbox.ts b/drivers/stream-sandbox.ts index e7ec70c..31ce654 100644 --- a/drivers/stream-sandbox.ts +++ b/drivers/stream-sandbox.ts @@ -285,6 +285,17 @@ export function createStreamSandboxDriver(options: StreamSandboxDriverOptions): // The agent's own output is not an action channel: actions arrive through // the file. Its stderr is still kept, bounded, because it is the only thing // that can explain an agent which never wrote an action. + // A killed child's pipes can emit `error` after the fact, and a stream with + // no error listener throws to the top of the process. MEASURED: a 42-cell + // study died at cell 3 on an unhandled `EPIPE` from one of these, after two + // cells had already cost twenty minutes each. The child's fate is already + // reported through `health()`, so these listeners record and do not rethrow. + started.stdout?.on('error', (error: Error) => { + if (agentStderr.length < 4096) agentStderr += `\n[stdout] ${error.message}` + }) + started.stderr?.on('error', (error: Error) => { + if (agentStderr.length < 4096) agentStderr += `\n[stderr] ${error.message}` + }) started.stdout?.resume() started.stderr?.setEncoding('utf8') started.stderr?.on('data', (chunk: string) => { diff --git a/harnesses/author-policy b/harnesses/author-policy index 2e9a94d..9bc533c 100755 --- a/harnesses/author-policy +++ b/harnesses/author-policy @@ -214,6 +214,10 @@ function account(event) { publish() const child = spawn(command, args, { cwd: sandbox, stdio: ['ignore', 'pipe', 'inherit'] }) +// A pipe to a killed child emits `error`, and a stream with no listener throws +// to the top of the process. This launcher's whole job is to outlive its agent +// long enough to publish the meter, so it cannot die on the agent's pipe. +child.stdout.on('error', (error) => console.error(`playproof: agent stdout: ${error.message}`)) let carry = '' child.stdout.setEncoding('utf8') child.stdout.on('data', (chunk) => { diff --git a/harnesses/claude-code b/harnesses/claude-code index 9d88b2f..358d569 100755 --- a/harnesses/claude-code +++ b/harnesses/claude-code @@ -223,6 +223,10 @@ function account(event) { publish() const child = spawn(command, args, { cwd: sandbox, stdio: ['ignore', 'pipe', 'inherit'] }) +// A pipe to a killed child emits `error`, and a stream with no listener throws +// to the top of the process. This launcher's whole job is to outlive its agent +// long enough to publish the meter, so it cannot die on the agent's pipe. +child.stdout.on('error', (error) => console.error(`playproof: agent stdout: ${error.message}`)) let carry = '' child.stdout.setEncoding('utf8') child.stdout.on('data', (chunk) => {