0007 — Mosh-class transport semantics and satellite forward-compat
0007 — Mosh-class transport semantics and satellite forward-compat
TL;DR. Mosh is decomposed: snapshot-on-attach is adopted via byte replay, predictive echo is adopted as a client feature, SSP is rejected in favor of QUIC for v0.2+. Transport is a trait so v0.1’s Unix socket and a future QUIC impl share one boundary. SessionId (and every other identity) carries a {LOCAL, SATELLITE} tag from day one so hub-and-spoke federation drops in without a wire break.
Post-ADR-0013 amendment (2026-05-25): ADR-0013 supersedes ADR-0002 — pane content now ships as VT bytes (
PANE_OUTPUT), not structured cell diffs. The Mosh-decomposition table below has been updated inline; the rest of this ADR (Transport trait, URI-shaped SessionId, hub-and-spoke satellites, forward-compat invariants) stands as-is. Satellite relaying is in fact simpler under ADR-0013 because the hub forwards opaque byte payloads instead of having to understand cell structure.Predictive local echo prose: pre-ADR-0013 the client maintained a “diff mirror” that the prediction overlay sat on top of. Under ADR-0013 the client maintains a libghostty
Terminaldirectly; predictive echo speculativelyvt_writes encoded keystrokes into a shadow terminal (or overlay), reconciles when authoritativePANE_OUTPUTbytes arrive. The UX guarantee is unchanged; the substrate is libghostty, not a phux-defined mirror.
Status: Accepted (forward-compat) Date: 2026-05-25
Update 2026-05-26: ADR-0015 §“Cross-cutting: Federation” generalizes the
{LOCAL, SATELLITE}tagged-union shape introduced here onSessionIdto every protocol identity uniformly —TerminalId(ADR-0016),CollectionId,SessionId. v0.1 servers constructLOCALonly; v0.1 decoders MUST acceptSATELLITEand respondERROR { code: UnsupportedSatelliteRoute }when not configured as a federation hub. The §“Sessions are URI-shaped” decision below is preserved and broadened: it is now an identity invariant, not aSessionId-specific one.Additionally, ADR-0013 renamed the per-pane content frame and snapshot frame to
TERMINAL_OUTPUTandTERMINAL_SNAPSHOT; the inline references toPANE_OUTPUT/PANE_SNAPSHOTbelow should be read with that substitution.
Context
Two requests have arrived for what look like distinct features but share a common architectural backbone:
- Satellite/federation. A user with several hosts (laptop, devbox, ephemeral sandboxes, agent VMs) wants one phux-server to act as a hub over the others, exposing remote sessions as if they were local.
- Mosh-class transport. A user wants the responsiveness of Mosh — roaming across networks, sub-second reconnect, instant local echo over high-latency links.
Both reduce to the same architectural question: what is the shape of the network plane below the wire protocol? If we answer that wrong now, both features become refactors later. If we answer it right and leave the door open, both become additive v0.2 work.
This ADR is not an instruction to implement satellites or QUIC in v0.1. It is a record of the design decision so v0.1 code does not preclude either.
Decision
1. Mosh is decomposed, not adopted
“Support Mosh” is rejected as an ambiguous goal. Mosh bundles three ideas; we treat them separately.
| Mosh innovation | Our treatment | Where it lives |
|---|---|---|
| Authoritative server state synthesized on attach | Adopted via byte-replay snapshots per ADR-0013. The server walks its libghostty Terminal grid to emit a VT byte sequence that catches a new client up — exactly Mosh’s snapshot trick, mapped onto our bytes-on-the-wire shape. | phux-protocol::wire::frame::PaneOutput (and PaneSnapshot) |
| Predictive local echo | Adopt as client feature. Transport-agnostic. | phux-client |
| UDP State Sync Protocol (SSP) | Reject. Use QUIC instead for v0.2+. | phux-server::transport |
Reasoning:
- Authoritative server + byte-replay snapshots already exist. SPEC §8 is the canonical statement; ADR-0013 records the bytes-on-the-wire shape that makes Mosh-style snapshot synthesis the natural attach path.
- Predictive echo is a client concern, not a transport concern.
The client speculatively
vt_writes keystrokes (encoded via libghostty’s encoders + the client’s best guess at mode) into a shadowTerminalor directly into the rendered one with a predicted-cells overlay, then reconciles when the authoritativePANE_OUTPUTarrives — diff-of-grids viagrid_ref(). It works over any transport — Unix socket, TCP, QUIC. - QUIC strictly dominates SSP for our use case. QUIC gives us
connection migration (roaming), 0-RTT resumption (sub-second
reconnect), TLS encryption, and congestion control — all the UX
properties of SSP. SSP’s unreliable+resync model is only a win when
the stream is large and lossy; our protocol ships small ordered
VT byte frames (ADR-0013) and structured input frames, for which
reliable+ordered is correct. Reimplementing SSP
(~1500 LoC of UDP framing, OCB-AES, ack windows, roaming) buys us
nothing that
quinndoesn’t already provide.
Mosh wire-compatibility (i.e. mosh-client attaching to phux) is
explicitly out of scope. Doing so would constrain our protocol
evolution to Mosh’s framing, with the only practical benefit being
attachment from environments that ship mosh-client but not phux-client
(notably iOS apps like Blink). Revisit only if that use case becomes
concrete.
2. Transport is a trait
The wire codec sits behind an async Transport trait on both server
and client. The trait surface is roughly:
trait Transport: AsyncRead + AsyncWrite + Send {
fn peer_identity(&self) -> PeerIdentity;
fn supports_migration(&self) -> bool; // QUIC: true; Unix sock: false
async fn on_path_change(&mut self) -> ...; // hook for roaming-aware clients
}
v0.1 ships one implementation: UnixSocketTransport. v0.2+ adds
QuicTransport (via quinn) and SshStdioTransport (for satellite
hops over existing SSH paths). Domain logic in phux-server and
phux-client MUST NOT depend on any concrete transport type.
3. Sessions are URI-shaped
SessionId is a tagged union, not an opaque u32. v0.1 only ever
constructs the Local variant, but the wire format reserves space for
satellite-routed sessions from day one. (Per the 2026-05-26 update at
the top of this ADR, every protocol identity — TerminalId,
CollectionId, SessionId — carries this tag uniformly under
ADR-0015.)
SessionUri = tagged_union {
LOCAL { id: u32 }, // v0.1 default
SATELLITE { host: str, id: u32 }, // v0.2+
}
This is the single hardest thing to retrofit. Without URI-shaped IDs, adding satellites later means rewriting every reference to a session in the codebase and on the wire. The v0.1 cost is one byte of tag per session reference plus a one-time match arm in code; the v0.2 dividend is “satellites just slot in.”
4. Satellite topology is hub-and-spoke
When satellites land in v0.2+, the architecture is hub-and-spoke, not mesh:
client ──wire──> hub-server ──wire──> satellite-server
│
└──wire──> satellite-server
- The client attaches to exactly one phux-server (the hub).
- The hub server can declare other phux-servers as satellites and re-export their sessions via the SATELLITE variant of SessionUri.
- Satellites do not know about each other or about the hub’s other satellites.
- The hub relays frames bidirectionally; it does NOT re-encode VT bytes. Predictive echo, FRAME_ACK, and snapshot fallback all function across the hub.
- Direct client → satellite paths (NAT-permitting shortcuts) are not precluded but are not v0.2 either.
Discovery and auth for v0.2: manual config (phux satellite add devbox ssh://devbox), SSH-key-derived identities, no central registry.
Must-not-preclude invariants for v0.1
Anything that violates these is a v0.1 bug, even though satellites aren’t shipping.
- Transport-agnostic domain. No
phux-serverorphux-clientmodule names a concrete transport. All I/O goes through theTransporttrait. Even the Unix socket impl lives behind it. - URI-shaped SessionId on the wire.
SessionIdencodes astagged_union { LOCAL(u32), SATELLITE { host: str, id: u32 } }from day one. v0.1 only ever constructs and acceptsLOCAL, but the decoder MUST acceptSATELLITEand return a cleanUnsupportedSatelliteRouteerror rather than rejecting at the frame layer. - Per-pane encoder isolation. Mouse, key, focus, paste encoders are per-pane (ADR-0006) and per-satellite when satellites land. No shared global encoder state.
- FRAME_ACK is on the protocol, not the transport. The predictive-echo confirmation signal is part of SPEC §6, not a QUIC feature. Predictive echo must work over Unix sockets in v0.1, even if the latency benefit only matters over QUIC.
Consequences
- The wire format has tag bytes on
SessionIdfor v0.1 work that satellites won’t use. Cost: one byte per session reference per message. Acceptable. - v0.1 ships with one transport (Unix socket). Adding QUIC and SSH-stdio in v0.2 is purely additive.
- v0.1 client gets predictive echo as a first-class feature, not an afterthought, because deferring it until QUIC ships would push the most visible Mosh-class UX out to v0.3.
- We are explicitly accepting that some users will want mosh wire-compat for iOS/embedded; we are not building it.
Related
- ADR-0013 — libghostty bytes on the wire (supersedes ADR-0002). The bytes-on-wire shape is what makes Mosh-style snapshot synthesis on attach the natural code path. Hub-and-spoke satellite relaying is also simpler under 0013 — the hub forwards opaque byte payloads instead of having to understand cell structure.
- ADR-0002 — diff-based protocol (superseded; retained as historical context for the bet that earlier framing of “Mosh-class semantics via cell diffs” rested on).
- ADR-0006 — input mirrors libghostty (encoder locality, which the hub inherits).
- SPEC §6 — FRAME_ACK and snapshot fallback (the protocol substrate predictive echo depends on).
- SPEC §10.1 — sessions and windows (SessionUri form).