Merge train: #9686 #9688 #9689 #9691 - #9694
Merged
Merged
Conversation
…or good (#9676) "TUI input dies after real use", root-caused to two asymmetries on the `process.stdin` OBJECT path — an alias, a parameter, or a destructured field, which is what ink and every TUI built on it use. Both leave input permanently dead while the process stays alive, the loop keeps ticking and the terminal stays in raw mode. 1. `unref()` was wired to `process_stdin_detach_stub`, the same stub as `pause`/`destroy`: it sets the process-global `STDIN_DETACHED` latch, and the runtime's fd-0 reader thread breaks its loop on that latch and EXITS. `ref()` was wired to a no-op stub, so nothing ever cleared the latch or restarted the reader. One `unref()`/`ref()` pair left the process with no reader on fd 0 for the rest of its life. Ink runs exactly that pair whenever its raw-mode refcount drops to zero and comes back — i.e. whenever the last `useInput` component unmounts and a new one mounts, which is what a tool call does (confirmed verbatim in cc's own bundle). Split the latch: `STDIN_DETACHED` (destroy/pause) stops the reader and drops the loop hold; `STDIN_UNREFED` (unref/ref) drops only the loop hold, matching Node, where an unref'd stdin still emits 'data'. `stdin_is_detached()` is now the liveness view (either flag); the reader consults `stdin_reader_should_stop()` (detach only). `ref()` is a pure inverse and deliberately starts no reader — `resume()` remains the one call that does. 2. `rl.close()` and a literal `process.stdin.pause()` set perry-stdlib readline's `STDIN_PAUSED`, whose pump branch returns without draining `PENDING_DATA` — while readline's fd-0 reader keeps reading and keeps waking the main thread. Only the LITERAL `process.stdin.resume()` spelling could clear it; an aliased `s.resume()` landed on the runtime's object stub, which cleared only the runtime's own flags. A TUI holding stdin in a variable that opens one readline prompt therefore went permanently deaf with bytes still consumed off the terminal and CPU still burnt per keystroke — the exact signature the issue recorded. Bridge `pause`/`resume` through a registered op pair, as `on`/`off` already were. Regression test drives the child over a real PTY. That is load-bearing: on a pipe perry-stdlib's readline reader owns fd 0 and never consults these flags, so a pipe fixture passes before AND after the fix. Measured on 17d00b2 over a PTY: 2 of 157 keystrokes delivered for the unref cycle, 1 of 157 for rl.close + aliased resume, 157 of 157 for the no-lifecycle control.
This was referenced Sep 4, 2026
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 four fixes, all cherry-picked clean:
process.stdin.unref()no longer kills the reader thread,ref()is no longer a no-op, and object-modepause()/resume()reach readline's flow state (TUI input dies after real use: bytes reach the process and are consumed, but never reach JS — live forensics point at the GC (swept listener) #9676).Two gate fixes carried:
perry-hir/src/lower/tests.rscrossed the 2000-line cap again (#9689 added 44 test lines) so the unresolved-newtest moved to its own module, and that new module'suse super::*was unused under-D warnings.Worth noting for later:
lower/tests.rshas now hit the cap three times in one day. Shaving the tail each time works but the real fix is reorganising it into topic modules — a separate piece of work from any of these PRs.Validation:
run_lint_gates.shall 64 gates pass; release build green; RUST_TEST_THREADS=1 perry-runtime 3083/0; perry-stdlib green; perry-hir + perry-transform green.Rebase-merge preserving authorship.