fix(exec): pass a failed command's stdout through verbatim on omni exec, never distill it#123
Merged
Merged
Conversation
…xec`, never distill it (#122) #120 closed this invariant hole on the hook path, where the exit signal arrives in the agent JSON. `omni exec` has the same hole from a different cause: it streamed the child's stdout straight through the distiller and only learned the exit code afterwards (`child.wait()` ran after `run_inner` had already consumed stdout and written the distilled form), so a failed command's output was distilled — the vault case from #120, one layer down. The exit code is only knowable after stdout is fully drained (draining before wait() also avoids the classic full-pipe deadlock), so exec now buffers stdout, waits, then gates: non-zero exit writes stdout verbatim and skips distillation; zero exit distills exactly as before. Stream-mode commands (docker/npm/bun, via `stream_mode` TOML filters) emit line-by-line before the exit code exists, so the gate cannot apply to them — they keep streaming. The stream-filter lookup that Phase 0.5 did inline is factored into `pipe::stream_filter_for` so exec and the pipeline agree on which commands stream (SSOT). Verified through the real binary: 60 identical noise lines with `exit 1` come out as 60 verbatim lines; the same output with `exit 0` still collapses to one `[60 similar lines collapsed]` marker. Teeth proven by forcing the success branch and watching the failed-command test go red.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #122
Problem
#120 enforced "a non-zero exit passes through verbatim, never distilled" on the hook path, where the exit signal is in the agent JSON.
omni exechad the same hole from a different cause:cli::execstreamed the child's stdout straight intorun_inner(the distiller) and only calledchild.wait()afterwards — so by the time the exit code was known, the distilled output had already been written. A command that failed underomni execstill got distilled. That's the #120 vault case, one layer down, andomni execis the repo's own reproduction harness.Why it couldn't be a one-liner
The child's exit code is only knowable after its stdout is fully drained — and draining before
wait()is also what avoids the classic full-pipe deadlock. So exec now:wait()s for the real exit code,run_inner, so the pipeline is unchanged).Stream-mode commands (
docker/npm/bun, viastream_modeTOML filters) emit distilled output line-by-line before the exit code exists, so the gate cannot apply — they keep streaming. The stream-filter lookup Phase 0.5 did inline is factored intopipe::stream_filter_forso exec and the pipeline agree on which commands stream (SSOT).Verified end-to-end (real binary)
60 identical noise lines:
[60 similar lines collapsed]Teeth: forcing the success branch turns
failed_command_passes_through_verbatimred; restoring it green. Newtests/exec_fail_passthrough.rs(unix-gated — the repro drives POSIXsh; the gate itself is OS-agnostic).Gates:
cargo fmt/clippy -D warnings/cargo test(1029 passed, 0 failed) on local Homebrew 1.94.0, not CI's pinned 1.97.0 — CI is the source of truth for clippy.Noted, not fixed here
omni exec sh -c '…'double-wraps (exec re-prependssh -c, mangling quotes) — a pre-existing exec quoting quirk, also glimpsed in #120's "not reported" notes. Separate issue if it's worth filing.PR Auto Describe
Summary
Fixes #122:
omni execnow gates distillation on the wrapped command's exit code. Failed commands pass stdout verbatim (no distillation); successful commands are distilled as before. Stream-mode filters remain un-gated for live output. Also extractsstream_filter_forinto a reusable helper inhooks/pipe.rs.Key Changes
stream_filter_for()— buffered path drains stdout first, gatesrun_inner()onstatus.success(), else writes raw bytesstream_filter_for(cmd) -> Option<TomlFilter>as public reusable function; simplifyrun_innerto use itexec_fail_passthrough.rsverifies omni exec / pipe.rs distills failed commands: it reads piped stdout and never sees the child exit code #122 invariant (failed=verbatim, success=distilled)Detailed Breakdown
omni execoutput was piped through distillation. Now non-zero exit skipsrun_innerentirely; stdout is written raw to stdoutchild_stdoutbeforewait()avoids classic full-pipe deadlock;Cursor::new(&buf)feeds buffered bytes to distillation when applicablestream_filter_forreplaces inline filter lookup inrun_inner; matches existing Phase 0.5 semanticsNotes
Breaking Changes
None.
Last updated: 2026-07-21 01:48:49