Class: lost signal → potential false claim. Split out of #120, which fixed the
hook path only. The omni exec / pipe.rs path has the same invariant hole but a
different cause, so #120's fix does not cover it.
Summary
#120 established the invariant: a command that exited non-zero passes through
verbatim, never distilled. On the hook path the exit signal arrives in the agent
JSON (NormalizedInput.failed, guarded in post_tool::process_payload). The
omni exec path never gets that signal at all: pipe::run_inner reads the wrapped
command's stdout over a pipe and distills it, with no knowledge of the child's
exit code. So a failed command run through omni exec — the repo's own repro
harness — is still distilled.
Reproduction
$ omni exec sh -c 'cat tests/fixtures/heavy_noise.txt; exit 1'
sh exits 1, but the noise is distilled anyway (only an empty output currently
passes through, via the InputCheck::Empty arm at src/hooks/pipe.rs:104).
Root cause
src/hooks/pipe.rs — run_inner reads input (piped stdout) and flows straight
into Phase 4 distill. There is no exit-code input to gate on. The wrapper that
launches the child (src/cli/exec.rs) captures the child's ExitStatus but does
not forward it into the pipe engine.
Suggested fix
Have cli::exec capture the child exit code and pass it to run_inner (a new
param, or an env/side-channel), then apply the same guard #120 added: non-zero →
emit stdout verbatim, skip distillation. Keep the token savings on exit == 0.
Observation vs diagnosis
Reproduction and the stdout-only data flow are observed. The exact wiring in
cli::exec (whether it already has the ExitStatus in hand at the right point) is
diagnosis — confirm before implementing.
Environment
Class: lost signal → potential false claim. Split out of #120, which fixed the
hook path only. The
omni exec/pipe.rspath has the same invariant hole but adifferent cause, so #120's fix does not cover it.
Summary
#120 established the invariant: a command that exited non-zero passes through
verbatim, never distilled. On the hook path the exit signal arrives in the agent
JSON (
NormalizedInput.failed, guarded inpost_tool::process_payload). Theomni execpath never gets that signal at all:pipe::run_innerreads the wrappedcommand's stdout over a pipe and distills it, with no knowledge of the child's
exit code. So a failed command run through
omni exec— the repo's own reproharness — is still distilled.
Reproduction
shexits 1, but the noise is distilled anyway (only an empty output currentlypasses through, via the
InputCheck::Emptyarm atsrc/hooks/pipe.rs:104).Root cause
src/hooks/pipe.rs—run_innerreadsinput(piped stdout) and flows straightinto Phase 4
distill. There is no exit-code input to gate on. The wrapper thatlaunches the child (
src/cli/exec.rs) captures the child'sExitStatusbut doesnot forward it into the pipe engine.
Suggested fix
Have
cli::execcapture the child exit code and pass it torun_inner(a newparam, or an env/side-channel), then apply the same guard #120 added: non-zero →
emit stdout verbatim, skip distillation. Keep the token savings on
exit == 0.Observation vs diagnosis
Reproduction and the stdout-only data flow are observed. The exact wiring in
cli::exec(whether it already has theExitStatusin hand at the right point) isdiagnosis — confirm before implementing.
Environment
fajarhide/omni, branchmain, follow-up to Distillation can fabricate a successful result for a command that failed with a network error #120 (merged in fix(hooks): pass failed commands through verbatim, never distill a non-zero exit into success #121).