Environment
Issue
CustomWriteStream.write() queues data and relies on the async fs.write completion callback to advance the queue:
write() only starts processing when the queue transitions 0 → 1
_processWriteQueue() issues one fs.write and continues from its callback
fs.write completes on the libuv threadpool. If the pool stops delivering completions — which we observed live in production: all 4 workers idle in pthread_cond_wait, submitted work never picked up, event loop perfectly healthy — the callback never fires, the queue head never advances, and every subsequent write() appends to the queue forever. There is no error, no timeout, and no log: from the embedder's perspective the terminal simply stops accepting input while output (event-loop reads) keeps flowing. Terminal.write() returns void, so the embedder cannot observe or mitigate this from outside.
We confirmed this mechanism while investigating Orca's long-running "terminal intermittently stops accepting input" issue (stablyai/orca#8104). In the live wedged process, _writeQueue held every lost keystroke with the first task still at offset: 0, and hot-patching write to fs.writeSync restored input instantly — the fd was fine, only the threadpool path was dead.
We're investigating the Electron/libuv threadpool wedge separately. This issue is about how node-pty behaves when it happens: input stays queued indefinitely with no error and no recovery path.
Deterministic repro
The wedge can be simulated exactly by pinning every threadpool worker with a FIFO reader-open (blocks in open() until a writer appears):
const fs = require('fs');
const pty = require('node-pty');
const { execFileSync } = require('child_process');
execFileSync('mkfifo', ['/tmp/stall.fifo']);
const p = pty.spawn('/bin/sh', ['-c', 'read line; echo "GOT:$line"'], {});
p.onData((d) => process.stdout.write(d));
setTimeout(() => {
for (let i = 0; i < 6; i++) fs.open('/tmp/stall.fifo', 'r', () => {});
setTimeout(() => {
p.write('hello\n'); // never reaches the shell
fs.stat('/tmp', () => console.log('pool alive')); // never prints
}, 300);
}, 300);
Environment
CustomWriteStreamwrite path from Investigate replacing the setTimeouts in the backpressure handling #833)Issue
CustomWriteStream.write()queues data and relies on the asyncfs.writecompletion callback to advance the queue:write()only starts processing when the queue transitions 0 → 1_processWriteQueue()issues onefs.writeand continues from its callbackfs.writecompletes on the libuv threadpool. If the pool stops delivering completions — which we observed live in production: all 4 workers idle inpthread_cond_wait, submitted work never picked up, event loop perfectly healthy — the callback never fires, the queue head never advances, and every subsequentwrite()appends to the queue forever. There is no error, no timeout, and no log: from the embedder's perspective the terminal simply stops accepting input while output (event-loop reads) keeps flowing.Terminal.write()returns void, so the embedder cannot observe or mitigate this from outside.We confirmed this mechanism while investigating Orca's long-running "terminal intermittently stops accepting input" issue (stablyai/orca#8104). In the live wedged process,
_writeQueueheld every lost keystroke with the first task still atoffset: 0, and hot-patchingwritetofs.writeSyncrestored input instantly — the fd was fine, only the threadpool path was dead.We're investigating the Electron/libuv threadpool wedge separately. This issue is about how node-pty behaves when it happens: input stays queued indefinitely with no error and no recovery path.
Deterministic repro
The wedge can be simulated exactly by pinning every threadpool worker with a FIFO reader-open (blocks in
open()until a writer appears):