termiod architecture
August 24, 2026 · View on GitHub
A session lives in a host. Viewers only attach.
Composable · Direct.
| Composable | Direct | |
|---|---|---|
| Host · protocol · clients · pipes plug independently | One hop: client → protocol → host → PTY | |
| Any client, any pipe, any host | No 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
| Part | What it is | What it is not |
|---|---|---|
| Host | Long-lived process on a machine; owns every session PTY | A public TCP service; a window manager |
| Protocol | Framed control + terminal bytes (v0); later snapshots/diffs | Tied to SSH or Unix sockets |
| Clients | Viewers that attach/detach | Owners of process lifetime |
Transport is not a fourth product. It is how protocol bytes move:
| Pipe | Use |
|---|---|
| Unix socket | Host on this machine |
| System SSH | Host on a VPS / devbox (auth + crypto for free) |
| WSS / relay | Phone / hostile networks (optional) |
Hard rules
- Host ≠ viewer — closing every client leaves sessions alive.
- PTY only on the host — clients never allocate the agent’s TTY.
- SSH is a pipe — never
ssh -tt host claudeas the product path. - No nested WM — tabs/splits belong to the OS / native app (zmx).
- No invented crypto — build host + protocol only.
Vocabulary
| Say | Don’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-hellov0 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_writerdemotes (but does not detach) the prior writer. Observers never claim. Eframes 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 ]
| Kind | Payload | Plane |
|---|---|---|
C | JSON object tagged by op | lifecycle/control |
D | raw bytes, ≤64 KiB per emitted frame | terminal |
R | rows:u16 BE · cols:u16 BE | terminal |
E | JSON object tagged by ev | events |
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
| Module | Part |
|---|---|
daemon.rs · session.rs · pty.rs | Host |
protocol.rs | v0/v0.1 codecs, handshake/control/event types, frame limits |
client.rs · CLI in main.rs | Reference client |
remote.rs | Transport helper (SSH deploy / stdio bridge) |
paths.rs | Socket location |
Three-step product sequence
- Incredible host — durable multi-session runtime (direct).
- Composable agent surface — status, worktrees, tools as real protocol clients (composable).
- 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.