a73x

e5302436

docs: README restructured as a quick start

a73x   2026-08-08 14:17

Commit message
docs: README restructured as a quick start

Local, ssh, and QUIC attach paths lead; milestone chronicle compressed
into a status paragraph. All commands verified against the Makefile and
current flags.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

README.md
Old New
@@ -1,102 +1,121 @@
1 # mux 1 # mux
2 2
3 Prototype terminal multiplexer: the terminal engine (ghostty-vt) runs 3 Terminal multiplexer where detach/reattach is **state sync, not replay**:
4 authoritatively in a daemon and replicated in the client — state sync 4 the terminal engine (ghostty-vt) runs authoritatively in a daemon (`muxd`)
5 instead of escape-sequence replay. See `docs/handoff.md` for the design 5 and replicated in the client (`mux`). Attach costs one snapshot; everything
6 and `docs/decisions.md` for decisions made. 6 after is row deltas; a dropped connection is a non-event. Linux only,
7 7 prototype (see [Status](#status)).
8 Status: **M8 — QUIC.** A third transport arm: `mux quic://HOST:PORT --key 8
9 FILE` attaches straight to a `muxd run --quic`, with no ssh in the path. 9 ## Build
10 Authentication is TLS 1.3 external PSK — both ends hold the same 32-byte 10
11 key and nobody holds a certificate. Measured on a LAN box against ssh-via 11 Requires Zig **0.15.x** (ghostty pin — a newer Zig will not build the
12 in the same run: cold attach 6.9ms vs 9.8ms, and 234.1ms vs 235.7ms with 12 dependency). The Makefile points at a pinned toolchain path; override with
13 75ms of emulated delay on the real path. Ten consecutive network tears (a 13 `make ZIG=/path/to/zig` if yours lives elsewhere.
14 real UDP blackhole) all resumed hands-off and all delta-served. The wire 14
15 protocol did not change: `git diff` on `src/protocol.zig` is empty across 15 ```sh
16 the whole milestone, which is the third time transport has turned out to 16 make build # first build fetches + compiles the QUIC deps: ~30MB, a few minutes, once
17 be a swap rather than a redesign. 17 make test && make e2e # verify
18 18 ```
19 ### QUIC quickstart 19
20 Binaries land in `zig-out/bin/`. For remote machines, build a static binary
21 that runs on any x86_64 Linux:
22
23 ```sh
24 ~/Downloads/zig-x86_64-linux-0.15.2/zig build -Dtarget=x86_64-linux-musl
25 ```
26
27 ## Quick start, local
28
29 ```sh
30 muxd run & # the daemon: hosts your shell and the authoritative screen
31 mux # attach
32 ```
33
34 Inside a session:
35
36 | Key | Effect |
37 |---|---|
38 | `Ctrl-\` | detach (session keeps running) |
39 | `Shift+PageUp` / `Shift+PageDown` | scrollback (any other key returns to live) |
40
41 `mux` again reattaches — full TUI screens included. Kill the client with
42 `kill -9` if you like; the session doesn't care.
43
44 ## Quick start, remote over ssh
45
46 Works anywhere ssh works. On the remote host: put `muxd` on PATH and start
47 the daemon once (`loginctl enable-linger $USER` keeps it alive after you
48 log out; systemd user units are in `contrib/`).
49
50 ```sh
51 ssh HOST 'setsid nohup muxd run >/dev/null 2>&1 &'
52 mux HOST # attach; Ctrl-\ detaches, running it again reattaches
53 ```
54
55 `mux HOST` is sugar for `mux --via "ssh HOST muxd proxy"` — ssh config
56 (aliases, ports, ProxyJump) all keeps working, since `mux` never parses
57 HOST. It **is** interpolated into a shell command, so HOST must be trusted:
58 feeding it unreviewed from an inventory file or a cloud API is handing that
59 source a shell. `--via` accepts any command that exposes the daemon's
60 socket on stdio, for custom socket paths or transports.
61
62 If the link drops, the client reconnects and resumes by itself; `Ctrl-\`
63 gives up waiting.
64
65 ## Quick start, remote over QUIC
66
67 No ssh in the path; needs inbound UDP to the host. Authentication is a
68 shared 32-byte key (TLS 1.3 external PSK — no certificates), so the
69 handshake is one round trip.
20 70
21 ```sh 71 ```sh
22 head -c 32 /dev/urandom > ~/.mux.key && chmod 600 ~/.mux.key # 32 raw bytes or 64 hex chars 72 head -c 32 /dev/urandom > ~/.mux.key && chmod 600 ~/.mux.key # 32 raw bytes or 64 hex chars
23 scp ~/.mux.key muxd HOST: # the key and a static binary 73 scp ~/.mux.key muxd HOST: # the key and a static binary
24 ssh HOST 'chmod 600 ~/.mux.key; muxd run --quic 0.0.0.0:4433 --key ~/.mux.key' 74 ssh HOST 'chmod 600 ~/.mux.key; setsid nohup muxd run --quic 0.0.0.0:4433 --key ~/.mux.key >/dev/null 2>&1 &'
25 mux quic://HOST:4433 --key ~/.mux.key # or set MUX_KEY_FILE 75 mux quic://HOST:4433 --key ~/.mux.key # or set MUX_KEY_FILE
76 ```
77
78 **Trust model, in one sentence:** anyone holding that key file can attach
79 to that session, so it is exactly as sensitive as an ssh private key —
80 `muxd` and `mux` both refuse one that is readable by group or other, and
81 there is no unauthenticated mode to fall back to.
82
83 ## Everything else
84
85 ```sh
86 muxd dump [--vt] # debug: print the authoritative grid (what the screen *should* be)
87 muxd stats # wire stats: deltas vs snapshot bytes, attached clients
88 make bench # typing-workload byte-ratio measurement
26 ``` 89 ```
27 90
28 **Trust model, in one sentence:** anyone holding that key file can attach to 91 Multiple clients may attach to one session; the grid follows the most
29 that session, so it is exactly as sensitive as an ssh private key — `muxd`
30 and `mux` both refuse one that is readable by group or other, and there is
31 no unauthenticated mode to fall back to.
32
33 Previously, **M7 — reconnect.** A dropped link is a non-event: the client
34 keeps its replica, rebuilds the transport, and re-attaches quoting what it
35 already holds, so the daemon can answer with a delta instead of a repaint.
36 Measured over a real WAN link: 10 consecutive transport kills against a
37 live session, all 10 resumed hands-off, all 10 delta-served (the daemon's
38 snapshot counter never moved), first frame back in ~254 ms — most of which
39 is the client's own 200 ms first retry. `mux HOST` attaches over ssh in
40 one word.
41
42 Before that, **M6 — transport.** All handoff milestones complete; all kill
43 criteria cleared. The prototype's two founding questions are both
44 answered yes: ghostty-vt serves as an authoritative headless grid in a
45 daemon without forking it, and detach/reattach as state sync is correct
46 and fast — a killed client reattaches into a live full-screen `nvim`
47 session in ~5 ms from one snapshot, and steady-state delta traffic is 1%
48 of the snapshot-equivalent cost. M6 answered the third: transport is a
49 swap, not a redesign. Measured over a real WAN link, the protocol adds
50 ~4 ms to a raw ssh byte round-trip (constant, not a multiple of it), and
51 reattach-after-kill costs ~1.3× the round trip in protocol time on top
52 of ssh's own channel setup.
53
54 Requires Zig 0.15.x (ghostty pin); the Makefile points at the pinned
55 toolchain, override with `make ZIG=...`.
56
57 The first build also builds the vendored QUIC stack (ngtcp2 + wolfSSL, in
58 `deps/quic`), which **downloads ~30MB and takes a few minutes once**; every
59 build after that skips it. `make deps` does it on its own if you would
60 rather pay that cost deliberately, and `make clean` deliberately does *not*
61 throw it away — `make clean-deps` is the one that does. The sources are
62 pinned by version and sha256 and built by the same pinned Zig as everything
63 else; there is no prebuilt binary in the repo on purpose.
64
65 make test && make e2e # verify
66 make build
67 ./zig-out/bin/muxd run & # daemon
68 ./zig-out/bin/mux # attach (Ctrl-\ detach, Shift+PgUp scroll)
69 ./zig-out/bin/muxd dump [--vt] # debug: print the authoritative grid
70 ./zig-out/bin/muxd stats # wire stats: deltas vs snapshot bytes
71 make bench # typing-workload byte-ratio measurement
72
73 Attach over a network:
74
75 ./zig-out/bin/mux HOST # sugar for the --via line below
76 ./zig-out/bin/mux --via "ssh host /path/muxd proxy --sock /path/muxd.sock"
77
78 `mux HOST` runs `ssh HOST muxd proxy`, so muxd must be on HOST's PATH;
79 anything more specific (a custom socket, a non-default path) is what
80 `--via` is for. `mux` itself does not parse HOST, so ssh's own config —
81 host aliases, ports, ProxyJump — keeps working. It is interpolated into a
82 shell command, though, exactly as the `--via` string is: **HOST must be
83 trusted**. Feeding it unreviewed from an inventory file or a cloud API is
84 handing that source a shell. If the link drops, the client reconnects on
85 its own and resumes the session; Ctrl-\ gives up waiting.
86
87 `--via` runs the protocol over any command that exposes the daemon's
88 socket on its stdio — `muxd proxy` is one such command, and it is a
89 frame-agnostic byte pump that contains no protocol knowledge at all.
90 `test/wan.sh` is the env-driven harness that measured this over a real
91 WAN link (see the script header for the variables it needs).
92
93 Multiple mux clients may attach to one session; the grid follows the most
94 recently active client — typing, attaching, or resizing claims it (latest 92 recently active client — typing, attaching, or resizing claims it (latest
95 wins). 93 wins). With linger enabled a session survives logout, though not a reboot.
96 94 `muxd run` refuses a socket another daemon already owns; there is no
97 Detach, or kill the client outright — the session survives and `mux` 95 socket-stealing.
98 resumes it from a state snapshot, including TUI screens and the primary 96
99 screen behind them. Scrollback is fetched lazily (Shift+PageUp/PageDown 97 ## How it works
100 pages; any other key returns to live). systemd user units (socket 98
101 activation) live in `contrib/`; with `loginctl enable-linger` a session 99 Both ends run a real terminal engine. The daemon's is authoritative: shell
102 also survives logout, though not a reboot. 100 output mutates it, and clients receive a sequence-numbered stream of row
101 deltas. A client that attaches — or *re*attaches after a kill, a network
102 tear, or a laptop sleep — tells the daemon what sequence number it already
103 holds, and the daemon answers with a delta instead of a repaint whenever it
104 can. Transport is deliberately dumb: the same frames ride a unix socket, an
105 ssh pipe (`muxd proxy` is a byte pump with zero protocol knowledge), or a
106 QUIC stream, and the wire protocol has survived all three without changing.
107
108 Design: `docs/handoff.md`. Every decision and measurement: `docs/decisions.md`.
109
110 ## Status
111
112 Prototype, eight milestones in, first trial cut tagged `v0.0.1-1`. All
113 founding kill criteria cleared, measured on real networks rather than
114 loopback: reattach into a live full-screen `nvim` in ~5ms from one
115 snapshot; steady-state delta traffic ~1% of snapshot-equivalent cost; the
116 protocol adds ~4ms to a raw ssh round trip (constant, not a multiple of
117 RTT); ten consecutive real network tears resumed hands-off, all
118 delta-served, on both ssh and QUIC transports; QUIC cold attach beats
119 ssh-via (6.9ms vs 9.8ms clean, 234.1ms vs 235.7ms with 75ms emulated
120 delay on a real path). Not yet here: daemon auto-start on attach,
121 predictive local echo for high-RTT links, reconnect backoff tuning.