Make terminal and stdin input native async without blocking threads
Class
Terminal input used a blocking-pool handoff for every batch and could leave a thread waiting after its future was dropped. Tty::events now exposes the shared native async decoder stream and deletes the 24-hour read deadline.
Readiness did not protect an inherited blocking descriptor: a competing reader could drain it before read. Dropping the detached worker then left it consuming input after ownership had returned. Recycling short batches also repeated initialization of already initialized storage.
Why it cannot recur
- Linux privately reopened nonblocking input stays on AsyncFd. Shared descriptions and files use one native worker without changing shared flags.
- The worker enables deferred POSIX cancellation only in its C poll/read frames, and disables it during every Rust callback. Pending cancellation covers admission; cancellation releases a read already in flight. Drop closes the bounded handoff, cancels and joins before freeing the file or storage.
- Unix async reads enter through a narrow read-only RestoreGuard method, so they cannot increment the screen invalidation counter. The merged redraw regression and its tracked-read negative control exercise real PTY frames.
- InputBatch owns initialized storage and a separate valid prefix. Both Harness and TUI handoffs return the complete storage for the next read; neither truncates or resizes it.
- Terminal input retains epoll/kqueue on Unix and the persistent console waiter on Windows. The shared decoder, lifecycle fences and mode restoration remain in use.
Verification
Shared #869 changes at 60ecbc8c7 are validated through #910 descendant bb4ac5464: native worker, InputBatch and TUI handoff sources are byte-identical. Linux/macOS source is unchanged from the validated 35b742dc8; final Windows checks cover the owner-field migration. The descendant also validates its additional Windows stdin path.
Linux at #910 descendant 35b742dc8: stdin 34/34 in 0.31 s; InputBatch properties 2/2 in <0.01 s; terminal units 17/17 in 0.09 s; PTY input 6/6 in 0.89 s; signals 7/7 in 0.04 s; CLI 6/6 in 0.31 s. Native build 14m00s. Dedicated production-reader models/properties passed 13/13 in 0.23 s (direct compile 0.617 s); terminal models and negative controls passed 45/45 in 2.41 s (build 20.75 s). Exactly zero idle wakeups; input p50 19.580 us, p99 44.317 us, max 61.693 us. Windows GNU libraries/tests for Harness, TUI, native-runtime and GraphCoder: passed at bb4ac5464 in 333.23 s on bb2 (cold target; queue excluded).
Syscall-controlled production-reader tests steal readiness before read, require acknowledged shutdown, then write a byte after Drop and verify it remains unread. The detached-reader negative control consumes that byte. The earlier sleep-based drop check covered an idle poll, but missed a competing reader stealing readiness before the blocking read. This belongs in native reader regression coverage: the new controlled interleaving calls the production reader at that admission boundary and requires shutdown acknowledgment before post-drop input. Real-channel Loom models cover shutdown, queue-space notification and atomic waker registration, with deliberately broken controls.
Both handoffs require exactly zero warm allocations and zero tail writes. Negative controls restore exactly 65,535 and 1,023 writes and fail the same zero-work assertions. Independently initialized Vec baselines compare complete bytes, exact errors and read/publication/receive order through acyclic_test_support::differential. InputBatch proptests check generated valid prefixes and unchanged initialized storage.
macOS: 12 production-reader/property/native checks passed in 0.381 s (existing target compiled directly in 0.212 s), including the strengthened admitted-read probe and its negative control. Terminal units and both handoff checks passed 17/17 in 0.22 s (build 2.43 s), including exactly zero redundant PTY redraw bytes and the tracked-read negative control. Real macOS PTY input passed 8/8 in 0.90 s and signals 7/7 in 0.19 s (build 3.01 s). Input p50 3.5 us, p99 12.125 us, max 48.375 us, with exactly zero idle wakeups. After all macOS jobs ended, the lane target ~/mac-target/tui-async and source ~/tui-async-src were deleted under the updated disk rule; free space was 19 GiB. Future runs use RUSTC_WRAPPER=/opt/homebrew/bin/sccache and remove the target and source after validation.
Real Windows at final bb4ac5464: waiter checks 5/5 in 0.22 s (build 8.45 s); frame checks 5/5 in 0.07 s and handoff differential/work checks 3/3 in <0.01 s, all using the same binary. ConPTY break/close/blocked-output restoration passed in 1.363 s. Warm allocations and idle wakeups were exactly zero; handoff p50 2.6 us, p99 510.4 us, max 518.7 us. The Windows-only owner-field migration preserves the single shared console owner. Those are historical bb4ac5464 results; current-head results are recorded below.
Windows stdin's unchanged production path passed 6/6 in 0.04 s; ConPTY Unicode/surrogate-pair, Ctrl+Z and cancellation under stdin lock passed in 1.493 s. The full MSVC Harness unit compiler crashed with STATUS_ACCESS_VIOLATION; the focused existing production-reader target compiled in 1.83 s against fresh native dependencies and passed the same Windows tests and host script.
Commands: cargo test --locked -p acyclic-harness -p graphcoder -p acyclic-native-runtime -p acyclic-harness-tui --features acyclic-harness/filesystem-local,acyclic-harness/native-execution,acyclic-harness/test-support,acyclic-harness/terminal,acyclic-harness/tui --lib --test stdin-reader-model --test cli --test terminal_input --test terminal_signals --no-run; run the emitted focused binaries directly. Reader models: rustc --edition=2024 --test --crate-name stdin_reader_model rust/crates/harness/tests/stdin_reader_model.rs --cfg acyclic_harness_loom, with --extern and -L dependency paths from that native build JSON, then run the emitted binary with reader::. Terminal models: RUSTFLAGS='--cfg acyclic_harness_tui_loom' cargo test --locked -p acyclic-harness-tui --features tui --lib term::. Paired work/behavior checks: reader::tests::recycled_ and term::handoff::reuse_tests:: in the ordinary binaries.
Review fix relative to 26cffc5a5: 493 lines deleted across native reader and handoff paths, including the detached Unix reader and cancel-pipe path.
Decisions
- Native cancellation stays within the existing native-runtime OS boundary; no new unsafe lint exceptions, application signal handlers or shared descriptor flag changes.
- Fold #910 into #869 as one coherent native terminal and JSONL input change; preserve the published #869 history with a signed squash commit.
- #874 and #957 remain drafts under the lane's two-PR review limit. #871 was closed because main #885 superseded it.
Live steps
The full macOS GraphCoder CLI qualification was attempted: build 13m50s; 1/6 passed and 5/6 failed in 1.32 s before input handling with "source does not support original native work custody". Filesystem and CLI test sources match main 66ddfd542. The custody invariant is preserved. Every focused changed macOS native check passed. No lane target, source copy or tarball remains on ivar. Future builds use RUSTC_WRAPPER=/opt/homebrew/bin/sccache and delete their target and source after validation.
Guarantee
- Class: B/G: check/use races and unfinished native effects during shutdown; H: redundant readiness work; J/I: missing exported consumers and incomplete verification closure. A private
Consoleowns its read descriptor and exposes one fenced read entry. Negotiation and streaming register a cancellable thread before admission. Close refuses further reads, retriesCancelSynchronousIoacross kernel admission, waits for unregister acknowledgement, then restores modes. The Unix C worker acquires storage before reading and polls only afterWouldBlock; cancellation is enabled only on the C stack, and join precedes release of Rust-owned buffers. - Proof: the production
InputFenceuses the crate-local loom shim to explore the kernel-admission race and restoration ordering, with both the unfenced restoration control and a source-mutated one-shot cancellation loop failing the same invariant. A real ConPTY test consumes the peeked key through a competing reader, exercises the actualConsole::readandReadFile, and requires cancelled-read acknowledgement before restored-mode access. Exact C syscall tests compile the production translation unit with counting wrappers at N=32 and 64, both directly readable and initially WouldBlock. An independently initialized control restores the redundant polls and fails the same exact-work assertions while preserving bytes, EOF/errors and callback counts. Existing production-reader and handoff models cover publication, wakeup, shutdown and retained buffer ownership, with their negative controls. The exact-head Harness and GraphCoder compile checks cover the restored exported multiplexer and animation helpers; the reviewed pre-fold head is the known compile-failing control. - Entry points: Windows TUI
Tty::receive/ProbeIo::receiveduring detection and negotiation;Tty::eventsthrough the persistent waiter; normalTty::restore/Drop, event-source Drop and console control-handler close. All enterConsole::read/Console::close. JSONLstdin_jsonlenters Unixstdin::from_file(private nonblocking reactor or joined nativeInputReader) or Windowsstdin::from_file(overlapped IOCP, console or joined synchronous reader). Unix terminal events retain their reactor path and read-only owner access. The C worker's acquire/read/publish/finish loop and stop/join cover every Unix blocking-worker entry. Decoder/buffer properties and the checked-in UTF-16 fuzz corpus cover Windows console byte conversion. - Gaps: open; current-head Linux proofs and native Windows checks passed. The current-head Windows GNU check passed in241.349s. macOS current-head validation is pending Tailscale access; this PR remains draft until that platform proof is complete. Shared DST integration remains owned independently by co-dst per the coordinator's ordering clarification.
Current Windows head 052082037: test build 9.57 s; waiter checks 5/5 in 0.21 s; real stolen-key cancellation/acknowledgement ConPTY check passed in 0.475 s; break/close/blocked-output restoration passed in 0.691 s. Deleting the production CancelSynchronousIo call caused the same ConPTY acknowledgement oracle to fail (mutant build 3.01 s; harness run 3.80 s); restoring it passed (rebuild 2.27 s; run 0.407 s). Linux/model validation passed in one bb2 job: native build860.306s; stdin34/0.313s; terminal17/0.117s; exact production-C syscall tests2/0.003s; InputBatch properties2/0.003s; focused reader target12/0.006s; PTY input6/0.887s; signals7/0.040s; CLI6/0.417s. Production-reader models13/0.265s (direct compile0.429s). Terminal loom models45/0.766s (build46.388s), including both admission-race tests. Mutating the actual fence loop to cancel once fails the identical restoration invariant in0.003s (control compile0.276s). Windows GNU compilation for Harness, TUI, native-runtime and GraphCoder libraries/tests passed in241.349s. The combined job was admitted after404s queued and finished successfully; no additional build job ran concurrently.