The Terminal Runtime Boundary
Termless sits between two kinds of software that are easy to conflate: terminal engines (emulators that turn a byte stream into screen state) and terminal runtimes (applications that persist, resume, and multiplex live sessions). This page pins down which responsibilities live where, so a consumer never has to guess which layer owns a behavior — or worse, reimplement one layer inside another.
The three layers
runtime persist / resume / multiplex live sessions (e.g. a session host)
harness compare, replay, assert (termless)
engine bytes → screen state (vterm.js, xterm, ghostty-web, vt100.js)Dependencies point down only. Engines know nothing about the harness; the harness consumes engines through backends. Runtimes consume engines directly as their state authority; they may use Termless in tests and tooling, never as a production runtime dependency.
What the engine owns (vterm.js and peers)
An engine is the source of truth for terminal state, byte-for-byte:
- the grid, scrollback, alternate screen, and per-cell attributes,
- the cursor — including the deferred-autowrap position after a full-width write, which survives both a same-geometry resize and a snapshot/restore roundtrip,
- resize reflow: soft-wrapped logical lines repack at the new width and the cursor follows its logical line,
- modes, title, and OSC state,
- a snapshot/restore contract:
snapshot()captures everything above as plain data;restore()rebuilds an equivalent terminal, so a checkpointed session resumes exactly where it stopped, - identity-preserving serialization: palette-indexed color re-emits as palette SGR, truecolor as truecolor — state is never round-tripped through a lossy re-encoding.
vterm.js is the reference engine this project treats as its restore-fidelity oracle; the differential suites hold it to that bar against xterm and ghostty rather than trusting it.
What the harness owns (termless)
Termless never emulates. It hosts engines and makes claims about them checkable:
- the shared
Terminalcontract and per-engine backends, so every assertion works across engines, - state digests — one vocabulary for "are these two terminals in the same state?",
- differential suites: engine vs engine on the same input, live vs replay vs rehydrate on the same recording,
- corpora: the conformance corpus (upstream-derived cases plus recorded-session differentials) and journal / asciicast replay,
- matchers and renderers for test output.
When the harness finds an engine divergence, the divergence is data (a ledger entry or a catalog line), not a patch inside the harness: engine fixes belong upstream in the engine.
What a runtime owns (and must not reimplement)
A session runtime — anything that detaches, persists, and reattaches live terminals — should consume the engine as its state authority:
- fold PTY output into an engine instance in arrival order (output and resizes share one ordered stream),
- checkpoint by
snapshot(), resume byrestore()— never by replaying re-encoded screen text, - read projections (plain text, styled ANSI) from engine state; never treat a projection as the state.
The recurring failure mode this boundary prevents: a runtime keeps its own half-emulator (a text buffer plus ad-hoc ANSI handling), which drifts from real terminal semantics exactly at the hard cases — deferred wrap, reflow, palette identity — and every one of those drifts becomes a user-visible resume artifact.
Where conformance targets come from
terminfo.dev defines the feature matrix engines aim at; upstream emulator test suites (see the corpus page for licensing rules) supply executable expectations; recorded real sessions supply the shapes synthetic cases miss. The harness runs all three against every backend.