termiod architecture

August 24, 2026 · View on GitHub

A session lives in a host. Viewers only attach.
Composable · Direct.

ComposableDirect
Host · protocol · clients · pipes plug independentlyOne hop: client → protocol → host → PTY
Any client, any pipe, any hostNo second session owner, no nested WM, no invented crypto

Local is remote to localhost. Design: docs/design/20260730-termiod-session-mux.md.

Three parts

   CLIENTS                         PROTOCOL                      HOST
   (stateless)                     (versioned)                   (authority)
┌──────────────┐              ┌─────────────────┐          ┌──────────────┐
│ termio.app   │──┐           │ attach / detach │          │   termiod    │
│ TermioMobile │  │  transport│ create / list   │  always  │ session table│
│ CLI (ref)    │──┼──────────►│ I/O · resize    │─────────►│ owns PTYs    │
│ (web later)  │  │  (pipe)   │ status · roster │          │ (later: vt)  │
└──────────────┘──┘           └─────────────────┘          └──────┬───────┘


                                                         shell / agent
PartWhat it isWhat it is not
HostLong-lived process on a machine; owns every session PTYA public TCP service; a window manager
ProtocolFramed control + terminal bytes (v0); later snapshots/diffsTied to SSH or Unix sockets
ClientsViewers that attach/detachOwners of process lifetime

Transport is not a fourth product. It is how protocol bytes move:

PipeUse
Unix socketHost on this machine
System SSHHost on a VPS / devbox (auth + crypto for free)
WSS / relayPhone / hostile networks (optional)

Hard rules

  1. Host ≠ viewer — closing every client leaves sessions alive.
  2. PTY only on the host — clients never allocate the agent’s TTY.
  3. SSH is a pipe — never ssh -tt host claude as the product path.
  4. No nested WM — tabs/splits belong to the OS / native app (zmx).
  5. No invented crypto — build host + protocol only.

Vocabulary

SayDon’t say
host / daemon / termiod“the CLI tool” (as the architecture)
attach client“the SSH session” (for our session object)
transport / pipe“remote mode that owns the agent”

The shipped binary is host and a reference CLI client (tmux/zmx packaging). That does not change the model: termiod serve is the product core; attach is a client.

Data path (v0.1 POC)

client ──► pipe ──► termiod ──► PTY ──► agent

                └── fan-out to other clients
                └── ring replay on reattach
  • Hot path: raw PTY bytes over the pipe.
  • Every negotiated channel starts with hello; a no-hello v0 control frame enters legacy mode with no capabilities.
  • One interactive attachment owns the write/resize token. Attaching takes it only when nobody holds it; after that the token follows the device being used — a claim_writer demotes (but does not detach) the prior writer. Observers never claim.
  • E frames fan out status, writer, resize, exit, and roster deltas. A control channel can subscribe without attaching to a PTY.
  • VT / libghostty snapshot: later (host-side sidecar for resync), not in the critical path for every keystroke.

Protocol v0.1 contract

[ kind:u8 ][ len:u32 big-endian ][ payload ]
KindPayloadPlane
CJSON object tagged by oplifecycle/control
Draw bytes, ≤64 KiB per emitted frameterminal
Rrows:u16 BE · cols:u16 BEterminal
EJSON object tagged by evevents

Reads reject frames above 16 MiB. Unknown JSON operations/events are additive and ignored; unknown kinds close the channel after proto_error.

Negotiated clients advertise a protocol range, channel role, and capabilities in hello. Protocol 1 currently offers events and send_wait. The reply supplies a stable random host_id (persisted as host.id beside the Unix socket) and a per-connection client_id. Incompatible ranges are refused; legacy v0 first operations remain accepted.

Optional request seq is echoed by response re. The control channel stays open for multiplexed requests, roster/status subscriptions, and asynchronous wait results. Session records carry optional workstream metadata and live status, title, attached_clients, and writer_client_id fields.

Devices

There is no "remote". There are devices, each running one termiod, and every UI is a client that attaches to the device owning the session. Local is not a special case — it is the device whose route is a Unix socket.

  web     ─WSS──┐
  iPhone  ─WSS──┼─► termiod (Mac)    ─► PTY ─► shell / agent
  Mac app ─unix─┘

  web     ─WSS──┐
  iPhone  ─WSS──┼─► termiod (Linux)  ─► PTY ─► shell / agent
  Mac app ─ssh──┘

A client never reaches a session through another client. The rule is what keeps one attach path instead of four, and it is why the phone is not a satellite of the Mac: iOS and the browser are clients of the same kind as termio.app.

Same protocol on every leg — only the pipe changes. A daemon on a VPS auto-starts or runs under systemd --user; SSH disconnect detaches the client and never kills the session.

Shipped: unix and ssh. Planned: WSS — the binding is designed in the web-client RFC and not yet built. Until it lands the phone still reaches sessions through the Mac's companion wire, which is the shape this diagram replaces.

Map to source

ModulePart
daemon.rs · session.rs · pty.rsHost
protocol.rsv0/v0.1 codecs, handshake/control/event types, frame limits
client.rs · CLI in main.rsReference client
remote.rsTransport helper (SSH deploy / stdio bridge)
paths.rsSocket location

Three-step product sequence

  1. Incredible host — durable multi-session runtime (direct).
  2. Composable agent surface — status, worktrees, tools as real protocol clients (composable).
  3. Multi-device operable — Mac/iOS, discovery, optional relay — still direct pipes, no required cloud.

Superlogical: mux → composable → production. We keep composable and insist on direct.