Handle BrokenPipe in codex exec --json stdout streaming

Fixes #10248.

Replace println! in JSONL streaming with a locked stdout writer.
Treat ErrorKind::BrokenPipe as a graceful shutdown signal instead of panicking.
Add a regression test that closes stdout mid-stream and asserts a clean exit.
Why this looked like a 0.92.0 regression:

The panic-on-EPIPE behavior is older, but 0.92.0 appears to emit additional early --json stdout lines in some configurations.
In particular, 0.92.0 includes a new “under-development features enabled” warning event (c900de271, #9954) that prints to stdout in JSON mode.
More early stdout writes increase the chance of hitting EPIPE when downstream consumers (e.g., head, tee, detached runners) close the pipe mid-run.
This change makes --json streaming robust to stdout closure regardless of when it happens.
This commit is contained in:
Eric Traut
2026-01-30 12:38:12 -08:00
parent e6d913af2d
commit e4aa2b2c3b
5 changed files with 56 additions and 1 deletions

View File

@@ -1,4 +1,5 @@
use std::collections::HashMap;
use std::io::Write as _;
use std::path::PathBuf;
use std::sync::atomic::AtomicU64;
@@ -846,10 +847,17 @@ impl EventProcessor for EventProcessorWithJsonOutput {
#[allow(clippy::print_stdout)]
fn process_event(&mut self, event: protocol::Event) -> CodexStatus {
let aggregated = self.collect_thread_events(&event);
let mut stdout = std::io::stdout().lock();
for conv_event in aggregated {
match serde_json::to_string(&conv_event) {
Ok(line) => {
println!("{line}");
if let Err(err) = writeln!(stdout, "{line}") {
if err.kind() == std::io::ErrorKind::BrokenPipe {
return CodexStatus::InitiateShutdown;
}
error!("Failed to write event to stdout: {err:?}");
break;
}
}
Err(e) => {
error!("Failed to serialize event: {e:?}");