Bug
pty.exited never resolves — and pty.onExit never fires — when the PTY is the only async operation keeping the event loop alive.
Minimal repro:
import { spawn } from 'zigpty';
const pty = spawn('uv', ['--help'], { cols: 80, rows: 40 });
const exitCode = await pty.exited; // never resolves — Node exits first
console.log('exit code:', exitCode);
$ node zigpty-repro.mjs
Warning: Detected unsettled top-level await at zigpty-repro.mjs:3
const exitCode = await pty.exited;
^
$ echo $?
13
Same failure with pty.onExit(cb) or terminal: { data() {} }. Any command: uv --help, tsc --help, pnpm add --help.
Root cause
zig/pty_unix.zig, line 125:
_ = napi.napi_unref_threadsafe_function(env, tsfn);
The exit-callback tsfn is unref'd immediately after creation. Once the child exits and the tty.ReadStream closes, no ref'd handles remain → Node fires beforeExit and exits before the Zig exit-monitor thread can dispatch napi_call_threadsafe_function.
Proof — adding any ref'd timer makes it work:
const keepAlive = setInterval(() => {}, 100);
const pty = spawn('uv', ['--help'], { cols: 80, rows: 40 });
pty.exited.then((code) => {
clearInterval(keepAlive);
console.log('exit code:', code); // → 0
});
Suggested fix
Remove the early napi_unref_threadsafe_function call. Let the tsfn keep the event loop alive and release it only after the exit callback is delivered. The exit-monitor thread already calls napi_release_threadsafe_function in its defer block — removing line 125 should be sufficient.
Workaround
const keepAlive = setInterval(() => {}, 100);
const pty = spawn('uv', ['--help'], { cols: 80, rows: 40 });
const exitCode = await pty.exited;
clearInterval(keepAlive);
Environment
- Node: v24.11.0
- zigpty: 0.1.6
- OS: macOS 25.2.0 (darwin-arm64)
Happy to test a fix or send a PR.
Bug
pty.exitednever resolves — andpty.onExitnever fires — when the PTY is the only async operation keeping the event loop alive.Minimal repro:
Same failure with
pty.onExit(cb)orterminal: { data() {} }. Any command:uv --help,tsc --help,pnpm add --help.Root cause
zig/pty_unix.zig, line 125:The exit-callback tsfn is unref'd immediately after creation. Once the child exits and the
tty.ReadStreamcloses, no ref'd handles remain → Node firesbeforeExitand exits before the Zig exit-monitor thread can dispatchnapi_call_threadsafe_function.Proof — adding any ref'd timer makes it work:
Suggested fix
Remove the early
napi_unref_threadsafe_functioncall. Let the tsfn keep the event loop alive and release it only after the exit callback is delivered. The exit-monitor thread already callsnapi_release_threadsafe_functionin itsdeferblock — removing line 125 should be sufficient.Workaround
Environment
Happy to test a fix or send a PR.