Skip to content

Commit 00df53d

Browse files
committed
docs(changelog): record the omni exec failed-command passthrough fix under Unreleased (#122)
1 parent a020c59 commit 00df53d

1 file changed

Lines changed: 2 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99
## [Unreleased]
1010

1111
### Fixed
12-
- **A failed command could be distilled into output that reads as success (#120)**: OMNI's `normalize` layer parsed each agent's failure signal and then threw it away — Codex `exit_code` was never read into `CodexInput`, Pi `toolResponse.isError` sat behind `#[allow(dead_code)]`, and MCP `result.isError` was named in a comment but never deserialized — so a command that exited non-zero still ran the full distiller. A failed `docker build` (`exit_code 1`) on the heavy-noise fixture came out **9,207 → 6,090 bytes**, silently trimmed by the same `DEBUG`/`INFO` stripping a *successful* build gets; the filed case was a `vault` call that failed `exit=2` on a network timeout yet surfaced a clean, fictional `["n8n"]`. This is the worst failure mode — a fabricated success terminates investigation, while a fabricated error only costs a retry. `NormalizedInput` now carries `failed`, set from each agent's own signal, and `post_tool::process_payload` passes a failed command through verbatim at zero marker cost before any distiller runs. Successful commands are untouched — the same output with `exit_code 0` still distils to 6,090 bytes. Claude Code needed no code change: it already sends a failed command as a bare `tool_response` string (`"Error: Exit code N…"`) that never parses into a success summary, and a regression test locks that in so a future, more-lenient parser cannot silently reintroduce the fabrication. The `omni exec` / `pipe.rs` path reads piped stdout only and never sees the child exit code; hardening it is tracked separately.
12+
- **A failed command could be distilled into output that reads as success (#120)**: OMNI's `normalize` layer parsed each agent's failure signal and then threw it away — Codex `exit_code` was never read into `CodexInput`, Pi `toolResponse.isError` sat behind `#[allow(dead_code)]`, and MCP `result.isError` was named in a comment but never deserialized — so a command that exited non-zero still ran the full distiller. A failed `docker build` (`exit_code 1`) on the heavy-noise fixture came out **9,207 → 6,090 bytes**, silently trimmed by the same `DEBUG`/`INFO` stripping a *successful* build gets; the filed case was a `vault` call that failed `exit=2` on a network timeout yet surfaced a clean, fictional `["n8n"]`. This is the worst failure mode — a fabricated success terminates investigation, while a fabricated error only costs a retry. `NormalizedInput` now carries `failed`, set from each agent's own signal, and `post_tool::process_payload` passes a failed command through verbatim at zero marker cost before any distiller runs. Successful commands are untouched — the same output with `exit_code 0` still distils to 6,090 bytes. Claude Code needed no code change: it already sends a failed command as a bare `tool_response` string (`"Error: Exit code N…"`) that never parses into a success summary, and a regression test locks that in so a future, more-lenient parser cannot silently reintroduce the fabrication. The `omni exec` / `pipe.rs` path reads piped stdout only and never sees the child exit code; that gap is closed by the next entry.
13+
- **`omni exec` distilled failed commands too (#122)**: the same invariant as #120, one layer down and from a different cause. `cli::exec` streamed the child's stdout straight through the distiller and only called `child.wait()` *afterwards*, so by the time the non-zero exit was known the distilled output had already been written — a failed command run through OMNI's own reproduction harness came out distilled. The exit code is only knowable once stdout is fully drained (and draining before `wait()` is also what avoids a full-pipe deadlock), so exec now buffers stdout, waits, then gates: non-zero exit emits stdout **verbatim** and skips distillation; zero exit distils exactly as before. Measured on 60 identical noise lines: `exit 1` now yields 60 verbatim lines where it previously collapsed to a single `[60 similar lines collapsed]` marker; `exit 0` still collapses. Stream-mode commands (`docker`/`npm`/`bun`) emit line-by-line before the exit code exists and cannot be gated, so they keep streaming; the stream-filter lookup is now shared via `pipe::stream_filter_for` so exec and the pipeline agree on which commands stream.
1314

1415
## [0.6.2] - 2026-07-17
1516

0 commit comments

Comments
 (0)