Skip to content

fix(shell): preserve output from fast-exiting commands - #46463

Merged
kitlangton merged 1 commit into
v2from
preserve-shell-output
Aug 31, 2026
Merged

kitlangton merged 1 commit into
v2from
preserve-shell-output

Conversation

@kitlangton

@kitlangton kitlangton commented Aug 31, 2026

Copy link
Copy Markdown
Contributor

Why

Short-lived shell commands can finish successfully but return (no output) instead of the bytes they printed. Bun resumes child stdout and stderr on exit; our Effect readers attach lazily, so output can be discarded before consumption starts.

The failure also reproduces using the spawner implementation from before #46085. This is an older capture bug, not a reason to remove the post-exit drain deadline.

What Changes

  • Pipe stdout and stderr into backpressured PassThrough buffers when setting up the handle, before the lazy Effect readers attach. This follows the capture pattern in Effect's Node spawner.
  • Let the stream adapter clean up the capture buffer while preserving the existing raw-stream cleanup policy.
  • Unpipe before deadline-driven discard, so late descendant output cannot refill an abandoned capture buffer.
flowchart LR
  Child[Child stdout / stderr] --> Buffer[Eager PassThrough buffer]
  Buffer --> Reader[Lazy Effect reader]
Loading

The regression tests keep the process scope open, wait for a real child to exit, then read its output. Before the fix, separate and combined capture both return empty output in under 60 ms. Afterward, both retain stdout and stderr. A third case checks simultaneous 256 KiB outputs across backpressure.

Scope

Only child-output capture and its tests change. The shared one-second post-exit deadline, process-group termination, SIGKILL escalation, and MCP shutdown behavior remain intact. No public API or SDK cancellation-contract changes.

Verification

Run from package directories on macOS with Bun 1.3.14:

cd packages/core
bun typecheck
bun run test test/effect/cross-spawn-spawner.test.ts test/tool-shell.test.ts test/session-shell.test.ts test/shell-cleanup.test.ts test/mcp.test.ts
bun run test test/effect/cross-spawn-spawner.test.ts --test-name-pattern 'delayed output consumption|captures stdout via' --rerun-each 100
bun run test test/tool-shell.test.ts --test-name-pattern 'ordinary shell syntax' --rerun-each 10
bun run test

cd ../util
bun typecheck
bun run test
bun run build
  • Targeted spawner, shell, Session-shell, cleanup, and MCP suites: 168 pass, 19 skip, 0 fail.
  • Repeated capture checks: 300 pass, 0 fail. Repeated shell-syntax checks: 320 pass, 190 skip, 0 fail.
  • A separate real-process probe ran 500 commands at concurrency 16 and retained both stdout and stderr in every case.
  • The built Util package also retained output in 100 delayed-consumption checks under Node 26.5.0 at concurrency 8.
  • Util tests: 27 pass, 0 fail. Util builds, Core and Util typechecks, and all 33 repository pre-push typecheck tasks pass.
  • Prettier and git diff --check pass. Targeted lint has no errors and three pre-existing warnings.
  • Full Core suite: 4,047 pass, 40 skip, 0 fail across 229 files, including the formerly intermittent shell-output failures.

@kitlangton
kitlangton merged commit a6b49b3 into v2 Aug 31, 2026
15 of 17 checks passed
@kitlangton
kitlangton deleted the preserve-shell-output branch August 31, 2026 23:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant