From 46ec179b2bda2de065096c29d92a2439fac88577 Mon Sep 17 00:00:00 2001 From: Yvette Carlisle Date: Tue, 16 Jun 2026 22:28:32 +0800 Subject: [PATCH] {"schema":"decodex/commit/1","summary":"Handle status broken pipe output","authority":"manual"} --- apps/decodex/src/orchestrator/entrypoints.rs | 25 ++++++++--- .../tests/operator/status/text.rs | 41 +++++++++++++++++++ docs/reference/operator-control-plane.md | 4 +- docs/spec/runtime.md | 1 + 4 files changed, 65 insertions(+), 6 deletions(-) diff --git a/apps/decodex/src/orchestrator/entrypoints.rs b/apps/decodex/src/orchestrator/entrypoints.rs index 261419c1f..74514602f 100644 --- a/apps/decodex/src/orchestrator/entrypoints.rs +++ b/apps/decodex/src/orchestrator/entrypoints.rs @@ -1,3 +1,5 @@ +use std::io; + use state::{ConnectorBackoff, ConnectorBackoffInput}; use crate::runtime; @@ -804,13 +806,26 @@ fn print_operator_status_snapshot( snapshot: &OperatorStatusSnapshot, json: bool, ) -> Result<()> { - if json { - println!("{}", serde_json::to_string_pretty(snapshot)?); + let output = if json { + format!("{}\n", serde_json::to_string_pretty(snapshot)?) } else { - print!("{}", render_operator_status(snapshot)); - } + render_operator_status(snapshot) + }; + let stdout = io::stdout(); + let mut stdout = stdout.lock(); - Ok(()) + write_cli_output(&mut stdout, &output) +} + +fn write_cli_output(writer: &mut W, output: &str) -> Result<()> +where + W: Write, +{ + match writer.write_all(output.as_bytes()).and_then(|()| writer.flush()) { + Ok(()) => Ok(()), + Err(error) if error.kind() == ErrorKind::BrokenPipe => Ok(()), + Err(error) => Err(error.into()), + } } fn operator_connector_backoff_status( diff --git a/apps/decodex/src/orchestrator/tests/operator/status/text.rs b/apps/decodex/src/orchestrator/tests/operator/status/text.rs index 0a8197144..18d77af31 100644 --- a/apps/decodex/src/orchestrator/tests/operator/status/text.rs +++ b/apps/decodex/src/orchestrator/tests/operator/status/text.rs @@ -1,3 +1,44 @@ +mod operator_status_output_tests { + use std::io::{Error, ErrorKind, Result, Write}; + + struct BrokenPipeWriter; + + struct FlushBrokenPipeWriter; + + impl Write for BrokenPipeWriter { + fn write(&mut self, _buffer: &[u8]) -> Result { + Err(Error::from(ErrorKind::BrokenPipe)) + } + + fn flush(&mut self) -> Result<()> { + Ok(()) + } + } + + impl Write for FlushBrokenPipeWriter { + fn write(&mut self, buffer: &[u8]) -> Result { + Ok(buffer.len()) + } + + fn flush(&mut self) -> Result<()> { + Err(Error::from(ErrorKind::BrokenPipe)) + } + } + + #[test] + fn operator_status_output_accepts_closed_downstream_pipe() { + let mut writer = BrokenPipeWriter; + + crate::orchestrator::write_cli_output(&mut writer, "partial status output\n") + .expect("broken stdout pipe should be accepted"); + + let mut writer = FlushBrokenPipeWriter; + + crate::orchestrator::write_cli_output(&mut writer, "buffered status output\n") + .expect("broken stdout flush should be accepted"); + } +} + use crate::execution_program::{ ExecutionLinearIssueMapping, ExecutionProgram, ExecutionProgramDependency, ExecutionProgramNode, ExecutionProgramNodeStage, ExecutionQueueIntent, diff --git a/docs/reference/operator-control-plane.md b/docs/reference/operator-control-plane.md index 5ebf9a76a..8b131a426 100644 --- a/docs/reference/operator-control-plane.md +++ b/docs/reference/operator-control-plane.md @@ -265,7 +265,9 @@ JSON output identifies cache hits with `"status_source": "operator_snapshot_cach the command falls back to a direct local runtime read and emits `status_cached_snapshot_unavailable` in `warning_details`. `decodex status --live` always bypasses the cached snapshot and rebuilds status with fresh Linear/GitHub -observers. +observers. If a downstream consumer such as `head` closes stdout before status output +finishes, the CLI treats that broken pipe as normal truncated output rather than an +operator status failure. Operator JSON snapshots include `execution_programs[]` for Program Intake and Execution Program readback. Each program row carries public intake kind/summary, diff --git a/docs/spec/runtime.md b/docs/spec/runtime.md index ab66271a5..6a7fd5761 100644 --- a/docs/spec/runtime.md +++ b/docs/spec/runtime.md @@ -712,6 +712,7 @@ The minimum supported surface is: - structured runtime logs with stable identifiers such as `project_id`, `issue_id`, `issue`, `run_id`, `attempt`, `branch`, and repository-relative `worktree_path` - a local status command that renders the current service snapshot in both human-readable and JSON forms, including non-secret GitHub CLI authority diagnostics for the resolved command path, discovery tier, configured path when present, availability, and operator next action +- status command output must treat a downstream-closed stdout pipe as normal operator-side output truncation, not as a runtime, database, tracker, or GitHub failure - an agent evidence command, `decodex diagnose`, that writes a compact derived handoff index, blocker snapshots, run capsules, and an append-only evidence event stream under `~/.codex/decodex/agent-evidence//`; the handoff index includes the same non-secret GitHub CLI authority readback so repair agents can diagnose missing or fallback-only `gh` authority Structured logs remain diagnostic. They may help explain a live failure, but they are