Files
orca/config/patches
nireak b87a6c0f23 fix(pty): pace the EAGAIN write retry so a stalled reader can't saturate the daemon thread (#15319)
node-pty's CustomWriteStream retries an EAGAIN write with setImmediate, which
re-attempts within microseconds. A pty whose child has stopped draining stdin
keeps that branch EAGAIN-ing, so the retry becomes a busy-loop on the thread
that owns every pty on the runtime. Measured against this commit's parent on
macOS arm64: 121,316 EAGAIN/s at 101.6% CPU, versus 805/s at 4.1% with the
retry paced to 1ms.

The delay is 1ms rather than longer because the cost lands on readers that
drain in bursts -- what an agent does between event-loop ticks. Delivering 2MB
to a reader that drains 20ms out of every 100ms: 689ms unpaced, 907ms at 1ms,
1414ms at 5ms. 1ms keeps essentially all of the CPU saving without the
delivery regression.

clearImmediate -> clearTimeout in dispose() is required, not cosmetic: once the
handle is a Timeout, clearImmediate does not cancel it and a pending retry can
fire after dispose. The disposal guards that make that harmless (_fd = -1, queue
drop) are already on main; this mirrors them into src/unixTerminal.ts so the
TypeScript twin no longer drifts from the compiled lib.

Scope: this fixes the CPU saturation. It does not stop other terminals from
being serviced -- a second live pty kept answering echo round-trips throughout
the storm in every configuration tested (1 and 8 stalled writers, macOS and
Linux, 8 CPUs and 1), with throughput down ~20-50% rather than hung. The
"every terminal froze" symptom in #11178 has another cause and that issue
stays open.

Upstream chose setImmediate deliberately (microsoft/node-pty#831, #833) to fix
large-paste latency, and rejected polling POLLOUT because it reports writable
rather than flushed. That reasoning targets a per-write delay in an interactive
terminal; this delays only the EAGAIN branch in a long-lived daemon. Pastes to
a draining reader are unaffected (0-3 EAGAINs per MB in every arm).

Verified: patch applies to a pristine node-pty@1.1.0 tarball, the patched
src/unixTerminal.ts compiles byte-identical to the patched lib/unixTerminal.js,
patch_hash matches the file, and on Windows the changed code never executes
(WindowsTerminal, 0 EAGAINs on a 300KB conpty write).
2026-09-14 02:32:08 -07:00
..