UniPty v0.2.2: new zigpty Backend
UniPty v0.2.2 adds the official Backend package @unipty/backend-zigpty, built on the zero-dependency Zig PTY engine zigpty. Windows runs (adapter-mediated buffering semantics), and output has an optional disk-backed buffer, outputSpool. This post compares the route against node-pty and lists the caveats to know before use.
Esta publicación está escrita en inglés. Léela en chino →
$ npm view @unipty/backend-zigpty version
0.2.1
UniPty v0.2.2 is out (GitHub Release cut at 2026-09-07 20:53 UTC). It adds one official Backend package, @unipty/backend-zigpty, on top of zigpty (npm zigpty@0.2.1, MIT, maintained by pi0), a PTY engine written in Zig. Node now has two engines to choose from; the other one is the existing @unipty/backend-node-pty.
zigpty vs node-pty
The two routes share one public contract; the differences are all at the engine level. The table below is the honest selection surface.
| Dimension | zigpty | node-pty (@lydell distribution) |
|---|---|---|
| Runtime dependencies | 0 | platform sub-packages (optionalDependencies) |
| Install scripts | none | none |
| Distribution | prebuilds for 8 platform tuples in the main tarball, ~420KB | one sub-package per platform, installed per host |
| Install robustness | --omit=optional and similar postures change nothing | optional deps skipped means not installed |
| Native text output | yes (encoding: "utf8") | yes (encoding: "utf8") |
| Byte writes | rejected by default, needs writeDecode | accepted by default |
| Windows | runs, buffered output (see below) | declared, evidence-gated |
| Transport read errors surfaced | indistinguishable (see below) | surfaced as unsupported |
zigpty's advantage is distribution: no runtime dependencies, no platform sub-packages, all prebuilds in the main package. What you install is what you get, regardless of how the installer treats optional dependencies. That is more robust in environments you do not control (restricted CI, offline mirrors).
The costs are in the caveats below and in the next two sections: byte writes need an extra option, and Windows output is buffered, with the memory bound behind an option.
One upstream behavior to watch: when a prebuild cannot load, the zigpty library silently falls back to a pipe-based pseudo-PTY, where isatty and kernel geometry both fail. The adapter hard-gates that fallback at readiness; hasNative false means unsupported, never a fake PTY.
Usage
npm install unipty @unipty/backend-zigpty
import { UniPty } from "unipty";
import { createZigptyBackend } from "@unipty/backend-zigpty";
const backend = await createZigptyBackend();
const unipty = new UniPty({ backend });
const pty = unipty.spawn(["/bin/sh", "-i"], {
terminal: { cols: 120, rows: 40 },
});
for await (const text of pty.stream({ encoding: "utf8" })) {
process.stdout.write(text);
}
spawn, stream, write, resize, close, terminate, and exited behave identically to the other three routes. The 25-scenario public contract suite ran in full against the installed package artifact: one native evidence record on local darwin-arm64, plus one each from CI ubuntu and macos (2026-09-07, v0.2.2 evidence), all in the release catalog.
Windows: the adapter owns output flow control
zigpty's Windows build ships a ConPTY prebuild, but its public pause() / resume() are empty methods there, so consumer-paced output backpressure cannot reach the kernel. UniPty cannot discipline every engine; third-party developers will bring their own backends, and bridging engine differences in adapter code is precisely the point of this architecture. So the route does not fail closed on Windows: the adapter keeps draining engine callbacks, declares "output backpressure does not reach the kernel on this platform" as a route-level limitation (the same class the Deno route applies to its internal reader thread), and no platform branch is needed anywhere:
const backend = await createZigptyBackend(); // same line on Windows and unix
Support claims stay evidence-gated. Windows tuples remain declared-unverified until public contract evidence exists; the compatibility catalog presents them as such. Buffered operation is not a claim of verified support.
outputSpool: a disk-backed output buffer
A stalled consumer is the main memory risk. A terminal attached to a browser tab that the user switches away from for ten minutes produces ten minutes of output with nowhere to go, piling up in the adapter's queue; on Windows, where backpressure cannot reach the kernel, the pile grows faster.
outputSpool replaces that queue with a disk-backed FIFO:
const backend = await createZigptyBackend({
outputSpool: { memoryBytes: 4 * 1024 * 1024 }, // or just `true` for defaults
});
The mechanism has three parts. The in-memory head is capped at 1 MiB by default; records beyond it spill to one adapter-owned temp file; replay follows the consumer's pull pace strictly, never enqueuing past the stream's water mark. The public stream is byte-identical with and without the spool: text records round-trip per complete record and chunk boundaries are preserved. When the child exits, the completion signal waits for the backlog to drain, so a fast-exit tail is never cut by EOF; an explicit close() still completes synchronously and deletes the temp file.
Three boundaries to know before enabling it. Spill IO is synchronous; disk usage while backlogged is unbounded by design, bounded only by the child's own output; an abruptly-killed process leaves the temp file to OS tmp reaping. On platforms where the engine can pause (unix), passing the memory bound also propagates pressure back into the kernel, blocking the child instead of piling higher.
Caveats
The full difference surface is the capability matrix in the README. Beyond the two sections above, three things to know up front.
Byte writes need an option. The engine's
writeaccepts strings only, so the Endpoint rejects byte input withunsupportedby default. For byte input streams (an xterm.js front end and the like), enablewriteDecode; the decoding is stateful and safe across chunk splits:
const backend = await createZigptyBackend({ writeDecode: true });
pty.write(new TextEncoder().encode("echo hi\r"));
Transport read errors are indistinguishable. The engine exposes no transport-layer events. The adapter repossesses the read stream inside the exit window, uses its real
end/closeas EOF, and keeps a 50ms quiescence window as fallback. A read error cannot be distinguished from clean EOF on this route; that is a declared limitation.Exit shapes. A signalled death reports
{ exitCode: 0, signal: "SIGTERM" }(the exit code keeps the engine's original value); a missing executable reports{ exitCode: 1, signal: null }as an exit observation, not an exception.
Signalled-death shapes across the four routes
node-pty { exitCode: null, signal: "SIGTERM" }
zigpty { exitCode: 0, signal: "SIGTERM" }
bun { exitCode: null, signal: "SIGTERM" }
deno-ffi { exitCode: 1, signal: null }
Other changes
Fixed a killed flooded child never settling its exit observation while paused reads held a backlog:
terminate()now resumes master reads after kill (9bac5d7)Fixed output stranded in the kernel when a backpressure pause landed right before child exit: the repossessed stream is resumed inside the exit window (0a9a172)
Fixed fast-exit children losing output on ubuntu: the adapter now repossesses the read stream inside the exit window (3d56f71)
Fixed writeDecode saturation rejection advancing decoder state (f083127)
Release catalog registry re-keyed by route identity with dual package-plus-backend-id validation (b878ce4, f083127)
Edge-case battery covering memoryBytes extremes, empty records, corrupted records, and mid-backlog cancellation; package tests grew to 70 (f8ad01d)
Documentation synced repo-wide for the new route (021a82d)
Full list in compare v0.2.0...v0.2.2.
Upgrading
No breaking changes; purely additive. The Backend package's npm version is 0.2.1:
npm i @unipty/backend-zigpty@0.2.1
If you are already on unipty@0.2.0, Core needs nothing.
Thanks
The route is built on zigpty, pinned exactly at 0.2.1. Thanks to pi0 and the UnJS community.
Links
Changelog: GitHub Release v0.2.2 · compare v0.2.0...v0.2.2
Docs: unipty.jixoai.com · capability matrix · outputSpool option
Upgrade: non-breaking, one
npm iDiscussion: GitHub Issues
npm: @unipty/backend-zigpty · unipty
Previous release: UniPty v0.2.0
Chinese version: /zh/blog/2026-09-07-unipty-v0-2-2/
