Repository navigation
fix(interpreter): run pipeline stages as subshells - #2538
Merged
Merged
Conversation
Deploying with
|
| Status | Name | Latest Commit | Preview URL | Updated (UTC) |
|---|---|---|---|---|
| ✅ Deployment successful! View logs |
bashkit | 9a406b9 | Commit Preview URL Branch Preview URL |
Oct 07 2026, 05:52 AM |
chaliy
force-pushed
the
claude/fidelity-pipelines
branch
from
October 7, 2026 05:28
f589686 to
a5ef256
Compare
2 tasks done
Every stage of a multi-command pipeline now rolls back its state changes like bash; shopt -s lastpipe keeps the last stage in the shell. Streaming callers no longer see a non-last stage's output. Records L-PIPE-001 (stages still run sequentially). Claude-Session: https://claude.ai/code/session_019aFikmptPc91Fj4N2iDXQA
chaliy
force-pushed
the
claude/fidelity-pipelines
branch
from
October 7, 2026 05:51
a5ef256 to
9a406b9
Compare
chaliy
added a commit
that referenced
this pull request
Oct 7, 2026
…2541) <!-- ccr-projects-attribution: {"github_login":"chaliy"} --> _Requested by **Михайло** · [project thread](https://claude.ai/code/project/chan_019YEkBJuZMHJdXrjom86cGX?thread=cmsg_019YEkBJuZMHJdXrjom86cGX6hNmzgZPpfCgsgRMptbhdM)_ Stacked on #2538. ## What changed - Pipeline stages now stream into each other concurrently, starting at the first stage that runs shell code (a loop, group, function, `eval`). Each such stage runs on a forked shell and writes into a 4 KiB pipe with backpressure. The last stage stays in the caller's shell, so its output still streams to `exec_streaming` callers. - SIGPIPE: when a reader is done (`head` got its lines, a `while read` loop broke, or the stage never reads stdin), the writer's next write fails and it ends with 141. `while :; do echo x; done | head -n 2` prints two lines and sets `PIPESTATUS` to `141 0`, as in bash. Before, the loop ran until the command limit and the whole `exec` failed. - `read` pulls one line and `head -n/-c` pulls only what it needs from a streaming pipe. Commands that never read stdin (`echo`, `true`, `sleep`, ...) do not wait for the producer. - Leading stages that are one builtin call (`echo x | grep x`) still run in sequence without forking, so the common case costs nothing extra. - `concurrent_jobs(false)` also turns this off. - New TM-DOS-124 covers runaway producers and recursive pipelines (`f() { f | f; }`). A streaming group counts as a subshell level, and past 4 nested levels stages run in sequence, which keeps native stack near plain function recursion. - L-PIPE-001 is narrowed to the remaining gaps: - Leading single builtins (`yes | head`) still produce their whole output before the next stage reads. - `cat`/`grep` downstream of a streaming stage read all input first. - The pipe is 4 KiB, not 64 KiB. ## Why This is step 2 of the bash-fidelity roadmap. `producer | head` and `producer | while read ...; break` are everyday idioms. They either ran away into limits or produced the wrong `PIPESTATUS`. The pipe design (a shared buffer polled on the caller's task, with no threads) is also the building block for wiring a reader-backed stdin into `Terminal` (L-TERM-002). ## Before / After ``` $ bashkit -c 'while :; do echo x; done | head -n 2; echo "${PIPESTATUS[*]}"' ``` Before: `x` printed thousands of times, then `resource limit exceeded: maximum command count exceeded (10000)`. After: `x`, `x`, `141 0`. This is identical to GNU bash 5.2. The new spec cases in `pipes-redirects.test.sh` (endless producer into `head`, into two `read`s, through a middle `while read` stage (`141 141 0`), and into a loop that `break`s) all match real bash locally. ## Risk - Medium. Pipelines whose producer is a loop or function now run concurrently, so stage side effects interleave like in bash rather than strictly in order. Output order of the final stage is unchanged. - Stack: measured min stack for `f() { f | f; }` in debug builds: 1.99 MB sequential (main 1.90), 2.19 MB with streaming. The 4-level cap keeps it bounded. The existing nested `$(...)` canary is unchanged (1994531 bytes). - A finite loop writing between 4 and 64 KiB into a reader that quits early exits 141 where bash usually exits 0 (documented in L-PIPE-001). ## Checklist - [x] Tests added or updated (`pipe` unit tests, `pipeline_tests`, 4 bash-compared spec cases, TM-DOS-124 regressions) - [x] Backward compatibility considered (`concurrent_jobs(false)` keeps sequential stages) https://claude.ai/code/session_019aFikmptPc91Fj4N2iDXQA --- _Generated by [Claude Code](https://claude.ai/code/session_019aFikmptPc91Fj4N2iDXQA)_
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.
Requested by Михайло · project thread
What changed
exit/returnend only that stage.shopt -s lastpipekeeps the last stage in the current shell (bash does this whenever job control is off).exec_streaming, the CLI) no longer receive a non-last stage's output.for ...; done | tacstreamed the loop's lines in input order.Why
This is step 2 of the bash-fidelity roadmap.
echo x | read vsettingvandcd dir | catchanging the cwd are divergences that scripts written against bash trip over. The streaming leak printed wrong output in the CLI.Before / After
Before (main):
1 2 3,v=x,x=2After:
3 2 1,v=unset,x=1. This is identical to GNU bash 5.Existing spec cases that were marked
bash_diff: pipes don't create subshells in bashkitnow useshopt -s lastpipeand match bash.Risk
cmd | while read; do n=$((n+1)); done; echo $n) now get bash's answer (0).shopt -s lastpiperestores the old behavior.echo | catloop: 54 ms → 67 ms).Checklist
pipeline_tests,pipes-redirects.test.sh, L-PIPE-001 evidence)https://claude.ai/code/session_019aFikmptPc91Fj4N2iDXQA
Generated by Claude Code