fix(runtime): process.stdout/stderr.write must invoke its completion callback — pi print-mode exit code (#6672) - #6691
Conversation
|
Important Review skippedNo new commits to review since the last review. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughProcess stream write stubs now accept optional trailing callbacks, schedule callable callbacks asynchronously, and preserve callback arguments through dispatch. New tests validate stdout/stderr overloads, callback timing, async continuations, and exit-code parity. ChangesStream write callback support
Estimated code review effort: 3 (Moderate) | ~20 minutes Sequence Diagram(s)sequenceDiagram
participant Caller as process.stdout.write
participant Stub as process_stdout_write_stub
participant Queue as js_queue_next_tick
Caller->>Stub: pass chunk and optional callback
Stub->>Stub: write and flush stdout
Stub->>Queue: schedule callback
Queue-->>Caller: invoke callback asynchronously
Possibly related issues
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 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 |
…back (PerryTS#6672) Node's stream.write(chunk[, encoding][, callback]) calls the optional trailing callback once the chunk is handled. Perry's write stubs took a single arg and ignored the callback, so await new Promise(r => process.stdout.write(x, r)) never resolved: the promise hung, its awaiter never resumed, and the event loop drained and exited with the continuation (and any process.exitCode it would set) left unrun. That is the pi print-mode exit-code divergence: pi's flushRawStdout awaits this callback, so on a request error the natural-exit code stayed 0 where Node exits 1 (GATE 2a rc divergence). The write stub now takes (chunk, arg2, arg3), registers arity 3 so dispatch pads/truncates to those three (Direct dispatch would otherwise size the call to the call site and drop the trailing callback), and schedules the trailing function arg on the next tick — matching Node's async, no-error-on-success completion contract. Claude-Session: https://claude.ai/code/session_01JuiiePQfrXhAFD9fuCygB9
…after-flush parity Two byte-identical-to-node fixtures for the process.stdout/stderr.write completion-callback fix: - test_gap_6672_stream_write_callback: awaits the callback across stdout, stderr, the 3-arg write(chunk, encoding, cb) overload, a return-through- awaiting-finally, async ordering (never sync), and a plain no-callback write. Under the bug the awaited continuations never run and the transcript diverges from node. - test_gap_6672_exit_code_after_stream_flush: the pi print-mode shape reduced to essence — process.exitCode set by a continuation that runs only after an awaited stdout flush; node exits 1, and the stored expected-output + expected-exit=1 assert Perry now matches. Claude-Session: https://claude.ai/code/session_01JuiiePQfrXhAFD9fuCygB9
dfd357d to
b4beb84
Compare
|
@coderabbitai review |
✅ Action performedReview finished.
|
Which mechanism (a/b/c) was real
None of the three the issue hypothesized. The issue framed this as an async
catch's mutation of a sharedoutputobject not being visible at state-commit(a: aliasing, b: threading/continuation-context, c: commit ordering). A runtime
trace disproves all three: with probes on the pi bundle,
outputcarriesstopReason === "error"at every stage — the catch, the stream consumer,the state-store reducer (
message_end→state.messages.push), andrunPrintMode's read ofsession.state.messages[last]. The mutation is fullyvisible. The
assistantMsg.stopReason === "error"branch runs,exitCode = 1,and
runPrintModereachesreturn exitCode.The divergence is downstream of all of that: after
return exitCode, thecaller's continuation (
const exitCode = await runPrintMode(...); if (exitCode !== 0) process.exitCode = exitCode) never runs — so the process exits withprocess.exitCodestill 0.Root cause
process.stdout.write(chunk[, encoding][, callback])(andstderr) neverinvoked the optional completion callback. Perry's write stubs took a single
argument and dropped the callback entirely.
pi's
flushRawStdoutawaits exactly that callback:With the callback dropped, that promise never resolves. It sits inside
runPrintMode'sfinally, sorunPrintMode's returned promise never settles,the awaiting caller never resumes, the event loop drains empty, and natural exit
reads
process.exitCode === 0where Node exits 1. (#6666's natural-exitprocess.exitCodeconsultation is a necessary prerequisite — the value it wouldconsult was simply never set.)
Minimal reproduction (identical to Node except rc):
Fix
crates/perry-runtime/src/os_process_streams.rs: the stdout/stderr/stdin writestubs now take
(chunk, arg2, arg3)and register ABI arity 3 (Direct dispatchwould otherwise size the call to the call site and drop the trailing callback).
After writing, the trailing function argument —
write(chunk, cb)orwrite(chunk, encoding, cb)— is scheduled on the next tick (matchingNode's async, no-error-on-success completion contract) via
js_queue_next_tick.Tests
Two byte-identical-to-node fixtures under
test-files/:test_gap_6672_stream_write_callback.ts— awaits the callback across stdout,stderr, the 3-arg overload, a return-through-awaiting-finally, async ordering
(never synchronous), and a plain no-callback write.
test_gap_6672_exit_code_after_stream_flush.ts— the pi print-mode shapereduced to essence (
process.exitCodeset by a continuation that only runsafter an awaited stdout flush; Node exits 1), with a stored expected-output +
expected-exit=1.cargo test -p perry-runtime --lib -- --test-threads=1: 1426 passed, 0failed.
cargo fmtclean on the touched file. (An unrelated pre-existingcodegen test,
executable_exit_releases_collection_side_allocations_last, failsidentically on the base commit and current main; this change does not touch
perry-codegen.)GATE 2a — full pass
Fresh pi bundle recompiled with the fix and run against the intended bogus-key
401:
401 …authentication_error…The fix flips perry's exit code 0 → 1, completing GATE 2a rc parity.
Fixes #6672
https://claude.ai/code/session_01JuiiePQfrXhAFD9fuCygB9
Summary by CodeRabbit
Bug Fixes
process.stdout.writeandprocess.stderr.write, including overloads with encoding and ensuring callbacks are dispatched after the write.process.stdin.writenow also safely accepts an optional completion callback.Tests