land #8861: deliver piped stdin to -p - #8863
Merged
Merged
Conversation
added 2 commits
August 26, 2026 16:11
…nd` listener, buffer pull-mode bytes
`echo hi | claude -p "…"` produced NOTHING (exit 0, zero bytes on both
streams) where node prints the result. Three independent defects in the
`process.stdin` path stacked up; each is fixed here.
1. `process.stdin.once(…)` was never lowered.
perry-hir matched only `("stdin","on") | ("stdin","addListener")`, so
`once` fell through to the generic member-call path and never reached
`js_readline_stdin_on` — the listener was never registered with the
fd-0 reader and simply never fired. Claude Code's print-mode reader is
`stdin.on("data", acc)` + `await race(stdin.once("end"), timeout(3000))`,
so with `once` dropped the `end` half could never win; the race fell to
the timer, and because that timer is unref'd nothing kept the event loop
alive and the process exited silently.
2. Only ONE `stdin.on("end")` listener survived.
They shared readline's single-slot `CLOSE_CALLBACK` ("only one terminal
close listener is supported"), so each registration clobbered the
previous. The bundle registers three; the one that resolves its
read-stdin promise was dropped. Replaced with `STDIN_END_CALLBACKS`, a
list fired in registration order, honoured by the keep-alive predicate
and by `removeListener`.
3. Pull-mode (`on("readable")` + `read()`) bytes were discarded.
The fd-0 reader routed bytes by mode: raw and `data`-flowing went to
`PENDING_DATA`, everything else to `PENDING_LINES` — readline's *line*
queue, which `process.stdin.read()` never drains. Paused/pull mode set
neither flag, so its bytes were consumed off fd 0 and thrown away and
`read()` returned null forever. New `STDIN_PULL_MODE` flag, set while a
`readable` listener exists, routes those bytes (and the EOF trailing
chunk) to the buffer `read()` actually drains. This is the same hazard
the `PENDING_LINES` comment already records for the `data` case (#5227),
left unfixed for `readable`.
Verified against node with the real bundle and with focused replicas:
* `on("readable")+read()` — was "", now "hello pipe" (node: "hello pipe")
* three `on("end")` listeners — now all fire, in order, with the data
* a faithful replica of the `-p` reader stops taking the 3s timeout path
Tests: `process_stdin_once_lowering.rs` (sabotage-checked — both cases fail
without the lowering arm), plus `every_stdin_end_listener_fires` and
`readable_listener_enables_pull_mode` in the readline suite. Full
perry-stdlib readline suite green (19/19).
Claude-Session: https://claude.ai/code/session_01Ay8VyLkKbm8Hkc1xmvTEsP
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthrough
ChangesStdin listener support
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant ProcessStdinOn
participant ReadlineListeners
participant ReadlinePump
ProcessStdinOn->>ReadlineListeners: register stdin listener
ReadlineListeners->>ReadlinePump: enable pull mode or EOF observation
ReadlinePump->>ReadlineListeners: deliver input and end/close callbacks
✨ Finishing Touches 💡 1🛠️ Fix failing CI checks 💡
📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
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.
Lands #8861 (
fix(stdin): deliver piped stdin to -p) with a missing GC root fixed.The PR itself fixes three stacked defects that together made
echo hi | claude -p "…"print nothing while exiting 0. Its diagnosis holds up on review:process.stdin.once(…)was never lowered —perry-hirmatched only("stdin","on") | ("stdin","addListener"), sooncefell through to the generic member-call path and never registered with the fd-0 reader. Decisive, because the print-mode reader awaitsrace(stdin.once("end"), timeout(3000)); withoncedropped theendhalf could never win, and the timer isunref'd so nothing kept the loop alive.stdin.on("end")listener survived — they shared readline's single-slotCLOSE_CALLBACK; the bundle registers three. Now a list fired in registration order. Verified the pump usesstd::mem::take, so each fires exactly once and all fire in order.on("readable")+read()set neither the raw nor the flowing flag, so bytes were routed to readline's line queue thatread()never drains.Fix applied while landing: a missing GC root
gc_runtime_root_holdersflagged the newSTDIN_END_CALLBACKS: Mutex<Vec<i64>>— it stores closure pointers and no registered scanner reached it.This is a real missing root, not a classification gap.
scan_readline_roots_mutalready visitsDATA_CALLBACKS,KEYPRESS_CALLBACKS,READABLE_CALLBACKSand the single-slotCLOSE_CALLBACKthis list replaces — the new list was simply not added. Its closures are reachable only from there between registration and EOF, so a collection in that window would leave stale pointers that the pump then calls. Added&STDIN_END_CALLBACKSto the scanner.That is exactly the failure mode CLAUDE.md describes for an unrooted runtime-side cache: it goes bad at collection #0 and stays bad.
Also applied
check_file_size:readline/mod.rsreached 2049 lines. Extracted its#[cfg(test)] mod tests(262 lines) toreadline/mod_tests.rs; now 1796.cargo fmt --all.Note on
oncesemanticsThe lowering routes
onceto the same registry ason.once("end")gets true one-shot behaviour because the pump takes the list, butonce("data")/once("readable")behave likeon— a deviation from Node that the PR documents inline rather than hides. That is a net improvement (previouslyoncedid nothing at all), but it is a residual worth its own issue.Validation
process_stdin_once_loweringtest: 2 passedperry-stdlib122,perry-hir339,perry-runtime2702 — all 0 failedSummary by CodeRabbit
process.stdin.once(...)so EOF and other stream events are delivered correctly.end,close,readable, anddataevents.