Hex1b Muxer Protocol (HMP) v1
September 15, 2026 · View on GitHub
The Hex1b Muxer Protocol is a binary framing protocol for multiplexing terminal sessions over any bidirectional byte stream (Unix domain sockets, TCP, named pipes, etc.).
In-place update — no protocol version bump. Hex1b is pre-v1 and currently coordinates its first-party consumers. HMP1 has been extended in-place with multi-head primary/secondary semantics and four new frame types (
RequestPrimary,RoleChange,PeerJoin,PeerLeave) plus an extendedHellopayload and a new client-emittedClientHello. Old binaries cannot speak the updated HMP1 — all builds upgrade together. Animation replay also adds the one-timeKgpAnimationStatecheckpoint described below. Activity replay adds a mandatoryActivityStatecheckpoint after everyStateSync; producers and consumers must upgrade together.
Frame Format
Every message is a frame with the following structure:
+------+-----------+---------+
| Type | Length | Payload |
| 1 B | 4 B (LE) | N bytes |
+------+-----------+---------+
- Type (1 byte): Frame type identifier (see below)
- Length (4 bytes, little-endian): Payload length in bytes (0–16,777,216)
- Payload (variable): Type-specific data
Maximum payload size: 16 MB.
Frame Types
| Name | Value | Direction | Description |
|---|---|---|---|
| Hello | 0x01 | Server → Client | Initial handshake: protocol version, dimensions, assigned peer id, current primary, roster |
| StateSync | 0x02 | Server → Client | Full screen snapshot (raw ANSI) |
| Output | 0x03 | Server → Client | Incremental terminal output (raw ANSI) |
| Input | 0x04 | Client → Server | Keyboard input (raw bytes) |
| Resize | 0x05 | Bidirectional | Terminal dimensions changed (silently dropped server-side from a non-primary peer) |
| Exit | 0x06 | Server → Client | Terminal session has ended |
| RequestPrimary | 0x07 | Client → Server | Asks the server to make this peer the primary at the given dimensions |
| RoleChange | 0x08 | Server → Client (broadcast) | Primary peer changed (or transitioned to "no primary") |
| PeerJoin | 0x09 | Server → Client (broadcast) | A new peer joined the session |
| PeerLeave | 0x0A | Server → Client (broadcast) | An existing peer disconnected |
| ClientHello | 0x0B | Client → Server | Client identifies itself before the server's Hello (display name, default role) |
| KgpAnimationState | 0x0C | Server → Client | One-time playback-progress checkpoint following KGP animation replay |
| ActivityState | 0x0D | Server → Client | Mandatory activity baseline immediately following each StateSync |
Peer IDs
A peer ID is an opaque string assigned by the producer (server) to each
attached client. Peer IDs appear in the Hello, RoleChange, PeerJoin,
and PeerLeave frames as well as in the peers[] roster. They are the
sole means by which the producer and clients refer to specific connected
peers.
Lifetime and scope
- Assigned by the producer when it accepts a connection and emits its
Hello. - Stable for the lifetime of a single connection.
- Per-connection, not per-client identity. A client that disconnects and reconnects is a new peer with a new ID. The protocol does not carry a notion of identity that survives reconnects.
- Locally unique within the set of currently-connected peers of a single producer. They are not globally unique and are not intended to be cross-referenced across producers, sessions, or time.
Opacity contract
Peer IDs must be treated as opaque values by clients. Specifically, clients must not:
- assume any particular length, character set, prefix, or encoding;
- parse, decode, or extract structure from a peer ID;
- generate or invent peer IDs (the only field a client supplies in
ClientHellois an optional human-readabledisplayName); - assume two equal peer IDs from different connections refer to the same client.
The only operations clients should perform on peer IDs are:
- byte-wise / string equality comparison (e.g.
peerId == primaryPeerIdto determine whether this client is currently the primary); - storing them in a roster keyed by peer ID;
- copying them verbatim into outbound frames (none of the client-emitted frames currently carry a peer ID, but future versions may);
- displaying them in diagnostic UI or logs.
Producer flexibility
The producer is free to change the format of peer IDs in any release — length, character set, prefix, encoding — without a protocol version bump, provided the new format is still a UTF-8 string that satisfies the equality contract above.
ClientHello (0x0B)
Sent once by the client immediately on connect, before the server emits
its Hello. Lets the client declare a friendly display name and a default
role hint. Both fields are optional.
Payload: UTF-8 JSON:
{
"displayName": "aspire-cli",
"defaultRole": "secondary"
}
displayName— Free-form label that other peers see in roster snapshots (Hello.peers[*].displayName,PeerJoin.displayName). May benullor omitted; the server uses an empty string in that case.defaultRole—"primary"or"secondary". A hint to the consumer's UX (e.g. CLI viewers default to"secondary"so they don't promote themselves to primary on attach). The server does not auto-promote on the basis of this field in the current iteration; explicitRequestPrimaryis always required. The split is about screen-size following:"primary"peers drive the producer's PTY dims,"secondary"peers follow them. Both are fully interactive.
A server that does not receive a ClientHello frame within a short window of
the connection being established may time out and disconnect the client.
Hello (0x01)
Sent once by the server after it has received the client's ClientHello.
Payload: UTF-8 JSON:
{
"version": 1,
"width": 80,
"height": 24,
"peerId": "p3a1b2c4",
"primaryPeerId": null,
"peers": [
{ "peerId": "p9f8e7d6", "displayName": "dashboard" }
]
}
version— Protocol version (currently1). Clients must reject unknown versions.width/height— Current PTY dimensions (set by the most recentRequestPrimaryfrom the current primary, or the producer's configured defaults if there is no primary).peerId— Server-assigned opaque identifier for this client, stable for the lifetime of the connection. See the Peer IDs section for the full opacity contract.primaryPeerId—peerIdof the current primary, ornullif no peer currently holds the primary role.peers— Roster of other peers currently attached (excluding self), each withpeerIdanddisplayName.
StateSync (0x02)
Sent by the server immediately after the Hello frame. Contains a full snapshot of the current terminal screen as raw ANSI escape sequences, allowing the client to render the current display without waiting for incremental output.
Payload: Raw ANSI bytes (UTF-8). Includes clear-screen and cursor positioning sequences.
The payload may be empty if no screen content is available yet.
Viewport cell hyperlinks are replayed with their OSC 8 destinations and parameters, including links spanning rows or wide characters. The active hyperlink is restored after painting so subsequent output retains its original link state. Sixel damage repaint also preserves cell links without changing that active state.
Every StateSync is immediately followed by ActivityState, including an empty
screen replay. The receiver buffers both as one state transaction before exposing
the connected baseline. This also applies to later StateSync frames. A relay
preserves this pair as control frames, not ordinary live output.
The raw ANSI includes a canonical OSC 9;4 sequence for the current progress indicator, including explicit clear, so an ANSI-only/native presentation can restore progress. An authoritative replica suppresses intermediate replay notifications and uses the checkpoint for its final state. Shell phase is never reconstructed by emitting OSC 133 marker chains.
If the snapshot contains graphics, the server queues KGP and Sixel replay after
ActivityState and before subsequent live output. The clear-screen sequence in
StateSync would erase graphics sent before it. Graphics use separate Output
frames because their encoded data may exceed the maximum size of one HMP frame.
An unfinished ANSI parser prefix follows graphics replay and precedes live output,
so a late-attaching replica can finish a fragmented OSC sequence without losing it.
KGP animation replay sends the root and all fully composed frames, timing gaps,
current-frame selection, placements, and playback controls. A subsequent
KgpAnimationState frame restores the captured loop progress and frame age.
There is no per-animation-tick retransmission of these pixels.
ActivityState (0x0D)
This mandatory server-to-client UTF-8 JSON checkpoint is limited to 1,024 bytes, with required fields (including explicit nulls):
{
"progress": { "state": 1, "percentage": 42 },
"shellIntegration": { "phase": 3, "lastExitCode": -1 }
}
Progress states are 0 (none), 1 (normal), 2 (error), 3 (indeterminate), and 4
(warning). States 0 and 3 require a null percentage; the others require an
integer from 0 through 100. Shell phases are 0 (unknown), 1 (prompt),
2 (command line), 3 (executing), and 4 (finished). lastExitCode is null or a
signed 32-bit integer; unknown phase requires null. Prompt, command-line, and
executing phases may preserve the latest reported completion result.
Defaults are none/null and unknown/null. Missing, malformed, oversized, incomplete, or unpaired checkpoints fail the connection, rather than making a partial screen baseline ready. Unknown or duplicate fields are rejected.
The checkpoint restores current state, not command history or execution events. Identical resyncs do not publish changed-state notifications; later real OSC 9;4 and OSC 133 output is parsed normally. Browser presentations may coalesce transitions. Disconnect and workload exit retain the last reported state; they do not synthesize a command completion or progress clear.
KgpAnimationState (0x0C)
This server-to-client JSON checkpoint follows the KGP replay Output frames,
in the same ordered stream. It supplements standard KGP commands with progress
that those commands cannot express. It is not an HWT1 frame or a new election
mechanism.
{
"images": [{
"imageId": 1,
"imageNumber": 0,
"currentFrameNumber": 2,
"playbackState": 3,
"maximumLoops": 1,
"completedLoops": 0,
"elapsedTicks": 800000
}]
}
Each entry addresses an already-replayed image by imageId, or by
imageNumber with imageId: 0 for numbered images. currentFrameNumber is
one-based. playbackState is numeric: 1 stopped, 2 loading, 3 running.
maximumLoops and completedLoops restore the image store's loop counters;
maximumLoops: 1 denotes infinite playback. elapsedTicks is the captured
frame age in 100-nanosecond ticks, or null when its presentation time is not
initialized.
Hex1b applies the checkpoint after the preceding pixel/control bytes have been interpreted, never from the network reader ahead of queued output. The checkpoint must reference valid replayed frames and playback counters; invalid checkpoints fail instead of silently resetting the animation. Byte-only consumers still receive standard KGP playback commands but do not restore this additional progress. Captured frame age is restored relative to the consumer's clock; this is not a transport-latency compensation or cross-peer wall-clock synchronization guarantee.
Output (0x03)
Incremental terminal output from the server's workload (e.g., PTY process). Sent continuously as the workload produces output.
Payload: Raw ANSI bytes (UTF-8).
For a terminal-backed producer, this is a rendering stream, not a byte-for-byte
capture of workload output. The producer answers supported terminal queries using
its own capabilities and state, even when no clients are connected. It consumes
DA1 (CSI c / CSI 0 c), XTWINOPS reports (CSI 14/16/18 t), supported status
and cursor-position reports (CSI 5/6 n), and recognized Kitty query commands
before broadcasting output. Clients must not answer these queries again.
State-changing Kitty commands still reach viewers, with their quiet control set
to q=2 to suppress downstream success and error acknowledgements. The producer
processes the original quiet control and returns the appropriate response to the
workload. Other output retains its original bytes; unsupported or malformed
commands are not covered by this response-ownership guarantee.
Projection is streaming across Output boundaries. A partially received query is not seeded into a newly attached client's parser; ordinary incomplete sequences retain their projected continuation. CSI and Kitty control headers are bounded to 64 KiB; exceeding the limit fails output processing with an error rather than silently truncating the header. Graphics payloads are streamed, not buffered by this header limit.
Hex1b HMP1 workload adapters also declare upstream ownership, suppressing locally
generated replies in replicas. This protects Hex1b replicas receiving queries
from older producers, but an external raw terminal can still answer queries
forwarded by an older producer. Upgrade the producer to prevent those replies.
A transport-only Hmp1PresentationAdapter with no attached Hex1bTerminal
continues to forward bytes unchanged: it has no terminal responsible for replies.
Important: Output frames are stateful — ANSI escape sequences build on previous state (colors, cursor position, modes). Dropping or reordering Output frames will cause visual corruption. If a client falls behind, it should be disconnected and reconnected (which triggers a fresh Hello + StateSync).
Input (0x04)
Keyboard input from the client to the server.
Payload: Raw input bytes (UTF-8). May include ANSI escape sequences for special keys (arrows, function keys, etc.).
Resize (0x05)
Terminal resize notification. Bidirectional but with asymmetric semantics:
- Client → Server: A peer requests that the underlying workload be resized
to its local dimensions. The server applies the resize only if the sending
peer is the current primary (see
RequestPrimaryand the role state machine below). If the peer is not primary the frame is silently dropped (logged at debug level) and the workload's dimensions are unchanged. - Server → Client (broadcast): The producer's PTY was resized (either by
the primary's
Resizeor by aRequestPrimaryfrom any peer). The producer echoes the new dimensions to all peers including the sender so every client treats the producer as the single source of truth for current dimensions.
Payload: 8 bytes:
+-------+--------+
| Width | Height |
| 4B LE | 4B LE |
+-------+--------+
Width— Terminal width in columns (4 bytes, little-endian)Height— Terminal height in rows (4 bytes, little-endian)
Exit (0x06)
Sent by the server when the terminal session has ended (workload exited).
Payload: 4 bytes:
+----------+
| ExitCode |
| 4B LE |
+----------+
ExitCode— Process exit code (4 bytes, little-endian, signed)
RequestPrimary (0x07)
Sent by a peer to ask the server to make this peer the primary at the
given dimensions. The server always grants the request in this iteration —
the previous primary (if any) is demoted, the PTY is resized to the requested
dimensions, and a RoleChange frame is broadcast to all peers (including the
new primary).
Payload: UTF-8 JSON:
{
"cols": 120,
"rows": 40
}
cols/rows— Dimensions the requester wants the producer to drive at. These become the new PTY size on success.
RoleChange (0x08)
Broadcast by the server to all attached peers when the primary changes — both
on a successful RequestPrimary and on the involuntary "primary disconnected"
transition.
Payload: UTF-8 JSON:
{
"primaryPeerId": "p3a1b2c4",
"width": 120,
"height": 40,
"reason": "RequestPrimary"
}
primaryPeerId—peerIdof the new primary, ornullif the previous primary disconnected and no new primary has taken over.width/height— Current PTY dimensions after the role change.reason— Free-form short string indicating why the role changed ("RequestPrimary"or"PrimaryDisconnected"in this iteration).
PeerJoin (0x09)
Broadcast by the server to all other attached peers when a new peer
joins the session. The newly joined peer learns about the existing roster
via its own Hello.peers[], not via PeerJoin.
Payload: UTF-8 JSON:
{
"peerId": "pe5fefc1",
"displayName": "aspire-cli"
}
PeerLeave (0x0A)
Broadcast by the server to all remaining peers when a peer disconnects.
If the leaving peer was the primary, a RoleChange (with primaryPeerId: null, reason: "PrimaryDisconnected") is broadcast before PeerLeave,
so observers see the role transition in causal order.
Payload: UTF-8 JSON:
{
"peerId": "pe5fefc1"
}
Connection Sequence
Client Server
| |
| ─ ─ ─ ClientHello ─ ─ ─ ─ ─ ─ →| Display name + default role hint
| ← ─ ─ Hello ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ | version + dims + peerId + roster
| ← ─ ─ StateSync ─ ─ ─ ─ ─ ─ ─ ─ | Full screen snapshot
| |
| ← ─ ─ Output ─ ─ ─ ─ ─ ─ ─ ─ ─ | Incremental output
| ─ ─ ─ Input ─ ─ ─ ─ ─ ─ ─ ─ ─ →| Keyboard input
| ← ─ ─ Output ─ ─ ─ ─ ─ ─ ─ ─ ─ |
| |
| ─ ─ ─ RequestPrimary ─ ─ ─ ─ ─→| Take control at local dims
| ← ─ ─ RoleChange (broadcast) ─ ─| Reflects new primary + dims
| ← ─ ─ Resize (broadcast) ─ ─ ─ ─| Echo of accepted dims
| ─ ─ ─ Resize ─ ─ ─ ─ ─ ─ ─ ─ →| As primary, push dim updates
| ← ─ ─ Resize (broadcast) ─ ─ ─ ─| Server echoes accepted dims
| |
| ← ─ ─ PeerJoin (broadcast) ─ ─ ─| A new peer attached
| ← ─ ─ PeerLeave (broadcast) ─ ─ ─| A peer disconnected
| |
| ← ─ ─ Exit ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ | Workload has exited
| |
- Client establishes a bidirectional stream (e.g., connects to a Unix domain socket).
- Client sends ClientHello with its display name and (optional) role hint.
- Server replies with Hello carrying the protocol version, current PTY
dimensions, the assigned
peerId, the currentprimaryPeerId, and the roster of other attached peers. - Server sends StateSync with the full current text screen content, followed by ordered graphics Output frames and, for KGP animation, KgpAnimationState checkpoints.
- Normal operation: Output flows server → client; Input flows client → server.
- To take control of the PTY size, a peer sends RequestPrimary. The server applies the resize and broadcasts RoleChange, including the accepted dimensions, to all peers.
- While primary, a peer may send Resize frames; the server applies them and broadcasts the accepted dimensions back to all peers.
- Roster changes are broadcast via PeerJoin / PeerLeave.
- When the workload exits, the server sends an Exit frame and closes the stream.
Multi-Client Behavior — Primary / Secondary
A single server can serve multiple clients simultaneously. Each client receives
its own Hello + StateSync on connection. Output is multicast to all
connected clients. Input from any client is forwarded to the workload
without arbitration (so multiple peers may type into the same PTY at once —
that's a UX concern, not a protocol concern).
Protocol replies. The original producer is the answerer, not the primary client. Attachment count, role changes, and disconnects do not transfer that responsibility. The primary can change the producer's dimensions, so subsequent size reports reflect those dimensions, but viewer capabilities do not replace the producer's capability model. Ordinary input from secondaries remains enabled.
Resize policy. Exactly one peer at a time may hold the primary role.
Only the primary can drive the PTY's dimensions. There is no implicit primary;
the producer starts with primaryPeerId = null and a configurable default
PTY size, and the workload runs at those defaults until some peer explicitly
sends RequestPrimary.
┌─────────┐ RequestPrimary{cols, rows} ┌────────────┐
│ null │ ─────────────────────────────► │ primary = │
│ (no │ │ sender │
│ primary)│ ◄───────────────────────────── │ │
└─────────┘ primary disconnects └────────────┘
│ ▲
│ │ RequestPrimary
│ │ from another peer
│ │ (always granted —
│ │ producer demotes
│ │ old primary)
▼ │
┌────────────┐
│ primary = │
│ new sender│
└────────────┘
State machine:
- No primary → Primary on
RequestPrimaryfrom any peer. Producer applies the requested dimensions to the PTY and broadcastsRoleChange+ echoedResize. - Primary → Different primary on
RequestPrimaryfrom another peer. Producer always grants. Old primary is demoted to secondary. BroadcastsRoleChange+ echoedResize. - Primary → No primary when the current primary disconnects. PTY size
is not reset; whatever dimensions were last applied stay in effect.
Producer broadcasts
RoleChange { primaryPeerId: null, reason: "PrimaryDisconnected" }followed byPeerLeave.
A Resize frame from a non-primary peer is silently dropped server-side
(no NACK frame in this iteration; consider adding Status in a future
revision).
A peer with defaultRole: "secondary" is not auto-promoted on first attach;
the role hint is purely a UX signal. Explicit RequestPrimary is always
required.
This iteration's policy is intentionally simple: every RequestPrimary is
granted. Future iterations may add (a) capability negotiation or take-over
denial driven by the current primary, (b) an activity guard that denies
take-overs within N seconds of input from the current primary, and (c) a
Status / NACK frame for explicit rejection.
Transport
The protocol is transport-agnostic and works over any reliable, ordered, bidirectional byte stream:
- Unix domain sockets (recommended for local use)
- TCP sockets
- Named pipes
- WebSocket (via a stream adapter)
Security (encryption, authentication) is the transport's responsibility. Use TLS, SSH tunnels, or other transport-level security as needed.
Versioning
The protocol version is communicated in the Hello frame. Clients must reject connections with an unsupported version. The current version is 1.
Pre-v1 in-place updates. Hex1b is pre-v1 and HMP1 has been extended in place rather than versioned. The
versionfield still reads1after the multi-head primary/secondary additions; the protocol identity has not changed, but the wire format is incompatible with prior implementations. A future iteration may either bump toversion: 2or introduce a capability-negotiation field — to be decided when there are two consumer codebases that need coordination.
Future versions may add:
- Capability negotiation in the Hello frame
- Terminal mode replay (mouse tracking, bracketed paste, etc.) in StateSync
- An activity guard or NACK /
Statusframe for primary handoff - Compression for Output frames
Changelog
- (in-place, pre-v1) Multi-head primary / secondary roles. Adds
RequestPrimary (0x07),RoleChange (0x08),PeerJoin (0x09),PeerLeave (0x0A), andClientHello (0x0B). ExtendsHellopayload withpeerId,primaryPeerId, andpeers. TightensResizesemantics: client → serverResizeis silently dropped from non-primary peers, and the server echoes accepted dimensions to all peers including the sender so the producer is the single source of truth. - (in-place, pre-v1) Terminal mode replay in
StateSync: mouse tracking, focus events, bracketed paste,DECTCEM,DECCKM,DECKPAM,DECSCUSR, mouse encoding plus alt-screenDECSETordered before cell repaint. (SeeHmp1ProtocolBuildStateSync*helpers.) - (in-place, pre-v1) KGP animation replay restores composed frames, gaps,
current frame, and playback controls, followed by
KgpAnimationState (0x0C)for completed-loop counters and captured frame age. Late viewers can advance a silent producer's animation without a per-tick output stream.