Skip to content

A runtime fault keeps the output that ran before it - #448

Merged
tobert merged 2 commits into
mainfrom
fix/error-keeps-prior-output
Sep 13, 2026
Merged

tobert merged 2 commits into
mainfrom
fix/error-keeps-prior-output

Conversation

@tobert

@tobert tobert commented Sep 12, 2026

Copy link
Copy Markdown
Owner

break, continue, return, and exit already carry the output a block produced before they leave it. A runtime fault propagated as a bare anyhow::Error and dropped that output at every block on the way up. A function that faulted became exit 1 with neither its output nor the real cause, because the pipeline stage folded the error with to_string().

echo left && x=$((1/0))                     # printed only the error; now prints left first
if true; then echo in-if; x=$((1/0)); fi    # lost in-if
f() { echo in-f; x=$((1/0)); }; f           # exit 1, "failed to evaluate assignment"
                                            # now: in-f, then the full cause, exit 1

A block that faults wraps the error in a private carrier holding its accumulated output, merging with any output an inner block already attached. The sites are if and while conditions and bodies, for and case bodies, both sides of && and ||, function bodies, source, and .kai scripts. A command substitution keeps only stderr, because its stdout was captured and never printed. The carrier renders exactly as the error it wraps, so no message text changes.

At the top level, a streaming caller receives the faulting statement's partial output through on_output before the Err. A pipeline stage that faults becomes a failed result holding the carried output and the {:#} cause chain.

BREAKING: KernelError::Execution becomes Execution { error, output } so a non-streaming caller can read the same output. A KernelError::Execution(e) pattern no longer compiles; kaijutsu and kaibo do not match on the variant.

This stacks on #445.

Co-Authored-By: DeepSeek V4 Flash noreply@deepseek.com
Co-Authored-By: Claude Opus 5 noreply@anthropic.com

🤖 Generated with Claude Code

`break`, `continue`, `return`, and `exit` already carry the output a
block produced before they leave it. A runtime fault propagated as a
bare `anyhow::Error` and dropped that output at every block on the way
up: `echo left && x=$((1/0))` printed only the error, an `if` body lost
what it printed, and a function that faulted became exit 1 with neither
its output nor the real cause, since the stage fold used `to_string()`.

A block that faults now wraps the error in a private carrier holding
its accumulated output, merging with any output an inner block already
attached: if and while conditions and bodies, for and case bodies, both
sides of && and ||, function bodies, `source`, and `.kai` scripts. A
command substitution keeps only stderr, because its stdout was
captured, never printed. The carrier renders exactly as the error it
wraps, so no message text changes.

At the top level, a streaming caller receives the faulting statement's
partial output through `on_output` before the `Err`.
`KernelError::Execution` becomes `{ error, output }` so a non-streaming
caller can read the same output; this breaks `Execution(e)` patterns.
A pipeline stage that faults becomes a failed result holding the
carried output and the `{:#}` cause chain.

Co-Authored-By: DeepSeek V4 Flash <noreply@deepseek.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tobert
tobert changed the base branch from kaijutsu-async-programs to main September 13, 2026 11:10
Brings in #445, #447, #450, and #446. CHANGELOG.md conflicted where #446
and this branch both added Fixed bullets; both are kept, in merge order.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tobert
tobert merged commit 6d7a560 into main Sep 13, 2026
3 checks passed
tobert added a commit that referenced this pull request Sep 13, 2026
Brings in #445, #447, #450, #446, and #448. Two conflicts, both from
additions at the same spot: CHANGELOG.md Fixed bullets (all kept, in merge
order) and scheduler/pipeline.rs, where #448's `fault_result` and this
branch's `redirects_stdout` were each added after
`finalize_scatter_gather_error`. Both helpers are kept; the dispatch site
merged cleanly with `fault_result(e)` and the stream-flag restore.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tobert
tobert deleted the fix/error-keeps-prior-output branch September 13, 2026 11:59
tobert added a commit that referenced this pull request Sep 21, 2026
A spilled background job and one that genuinely exited 3 were the same
report. `Job::to_info` copied `result.code` and nothing else, so
`did_spill` and `original_code` never reached `JobInfo`, and `failed:3`
was the whole answer either way. An embedder reading `jobs --json` had
no way to ask whether the 3 was the command's own.

`failed:3` stays as the status string — it is the loud signal that GH
#212 installed, and replacing it would trade one ambiguity for a
compatibility break. The two facts ride alongside it instead. `JobInfo`
is `#[non_exhaustive]` with a builder, so the fields are additive, and
`jobs --json` serializes `JobInfo` directly, so they appear with no
rendering change:

```
{"id":1,"command":"seq 1 5000","status":"failed","exit_code":3,"did_spill":true,"original_code":0}
```

The test that proves it goes through `Job::to_info` rather than building
a `JobInfo` by hand — which is how the gap stayed invisible, since every
existing `JobInfo` test constructs the struct itself and so cannot miss
a field the converter forgets. It runs two jobs under one output limit
and separates them on the new fields alone.

Second: the test-only `BackendDispatcher` drained an external's stdout
into its own ring with no tee into the background job's stream, so no
test driven through that dispatcher could observe stream routing at all
— a hole directly under the streaming work in #446/#448/#449, in the
twin that exists so tests can reach exactly that. It now tees the way
the production spawn site does, under the same only-or-last-stage rule,
with a test that fails when the tee is removed.

Gates: `cargo test --all` clean, `cargo clippy --all --all-targets` zero
warnings.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant