a73x

2843fee1

docs: one binary, the exec that starts it, and the break it costs

a73x   2026-08-28 22:34

Commit message
docs: one binary, the exec that starts it, and the break it costs

decisions.md is appended to, never edited: the entry carries the fork attempt
and why it was abandoned, because the rule it leaves behind is bigger than
this repo — fork-without-exec inherits process-global caches keyed on the
pid, and a single-thread precondition does not cover them.

The break is stated where someone hits it: a daemon of v0.0.1-15 or older
cannot be upgraded in place into this binary. Its version probe demands
`muxd <version>` and the probe is a bare `--version`, so no argv reaches the
candidate to say otherwise. `mux d upgrade` translates that refusal into the
one-time stop/start.

CLAUDE.md
Old New
@@ -1,6 +1,6 @@
1 # mux 1 # mux
2 2
3 Terminal multiplexer: ghostty-vt engine runs authoritatively in `muxd`, replicated 3 Terminal multiplexer: ghostty-vt engine runs authoritatively in `mux d`, replicated
4 in the client. Attach = one snapshot, then row deltas. Zig, Linux only, prototype. 4 in the client. Attach = one snapshot, then row deltas. Zig, Linux only, prototype.
5 5
6 ## Toolchain (pinned — system zig will NOT build this) 6 ## Toolchain (pinned — system zig will NOT build this)
@@ -11,7 +11,7 @@ make build test e2e # Makefile already points at it
11 make check # fmt + unit tests + shell syntax + comment-claim refs — pre-commit gate 11 make check # fmt + unit tests + shell syntax + comment-claim refs — pre-commit gate
12 make ci # check + e2e + agent + throughput — the delivery gate 12 make ci # check + e2e + agent + throughput — the delivery gate
13 make agent soak bench throughput 13 make agent soak bench throughput
14 make install # the four user binaries -> ~/.local/bin, ReleaseSafe 14 make install # the one user binary -> ~/.local/bin, ReleaseSafe
15 make xversion-build xversion # cross-version gate; XVER_OLD_WORKTREE points at the old checkout (default ..) 15 make xversion-build xversion # cross-version gate; XVER_OLD_WORKTREE points at the old checkout (default ..)
16 ``` 16 ```
17 17
@@ -53,7 +53,7 @@ engine and a client can link those folders and paint its own way:
53 | `src/server/` | `server`(+`_agent` `_sessions` `_test_*`) `pty` `quic_server` `cmd` `shellint` `upgrade` | 53 | `src/server/` | `server`(+`_agent` `_sessions` `_test_*`) `pty` `quic_server` `cmd` `shellint` `upgrade` |
54 | `src/client/` | `client` `client_core` `hosts` `handoff` `spawn` `quic_client` `layout` `wall` `webhub` `keymap` `wasm_core` | 54 | `src/client/` | `client` `client_core` `hosts` `handoff` `spawn` `quic_client` `layout` `wall` `webhub` `keymap` `wasm_core` |
55 | `src/tui/` | `wallview`(+`wall_host` `wall_picker` `wall_pump` `wall_layout` `wall_test_*`) `interact` `paint` `select` | 55 | `src/tui/` | `wallview`(+`wall_host` `wall_picker` `wall_pump` `wall_layout` `wall_test_*`) `interact` `paint` `select` |
56 | `src/cli/` | `main`(muxd) `mux_main` `muxa` `webhub_main` `flags` | 56 | `src/cli/` | `mux`(dispatch) `main`(daemon) `mux_main`(client) `muxa`(agent) `webhub_main`(hub) `flags` |
57 | `src/` | `xdg` `sockpath` `proxy` `quic` `testtmp` — what both sides link | 57 | `src/` | `xdg` `sockpath` `proxy` `quic` `testtmp` — what both sides link |
58 58
59 `build.zig`'s `checkFolderRules` enforces it: engine and client name no tui, 59 `build.zig`'s `checkFolderRules` enforces it: engine and client name no tui,
@@ -70,14 +70,20 @@ Layers are enforced in the same module table (grep `.layer =` for the graph).
70 |---|---| 70 |---|---|
71 | 0 | `protocol` `engine` `pty` `quic` `keymap` `xdg` `sockpath` `proxy` `cliflags` `testtmp` | 71 | 0 | `protocol` `engine` `pty` `quic` `keymap` `xdg` `sockpath` `proxy` `cliflags` `testtmp` |
72 | 1 | `client_core` `quic_server` `quic_client` `predict` `spawn` `handoff` `delta` `cmd` `wall` `shellint` `replica` `paint` `layout` `select` `upgrade` | 72 | 1 | `client_core` `quic_server` `quic_client` `predict` `spawn` `handoff` `delta` `cmd` `wall` `shellint` `replica` `paint` `layout` `select` `upgrade` |
73 | 2 | `server` `muxa` `interact` `hosts` | 73 | 2 | `server` `agent_main` `interact` `hosts` |
74 | 3 | `client` `main`(muxd) | 74 | 3 | `client` `daemon_main` |
75 | 4 | `webhub` `wallview` | 75 | 4 | `webhub` `wallview` |
76 | 5 | `mux_main` `webhub_main`(muxweb) | 76 | 5 | `client_main` `hub_main` |
77 | 6 | `mux` — the dispatcher, and the only executable |
77 78
78 Binaries: `muxd` (daemon), `mux` (client), `muxa` (agent client, JSON verbs), 79 ONE binary, `mux`, and the first word picks a mode: `mux d` the daemon,
79 `muxweb` (browser hub). Test fixtures in `test/`: `ptyclient` (real client on a 80 `mux a` the agent surface (JSON verbs), `mux web` the browser hub, and no
80 real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`. 81 letter at all the client. `mux run …` is the one alias — a v0.0.1-15 daemon's
82 upgrade execs `<candidate> run --resume-fd N`. Module names in the table are
83 `daemon_main` `agent_main` `hub_main` `client_main`; the files keep their old
84 paths (`src/cli/main.zig` is the daemon). Test fixtures in `test/`:
85 `ptyclient` (real client on a real pty), `wsclient` (browser stand-in),
86 `rawmode`, `delaypipe`, `render` — those stay separate binaries.
81 87
82 ## Invariants — do not break, they are load-bearing 88 ## Invariants — do not break, they are load-bearing
83 89
@@ -102,15 +108,15 @@ real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`.
102 - **Rails are painted from `relayout` and tiles cannot reach them.** Every 108 - **Rails are painted from `relayout` and tiles cannot reach them.** Every
103 tile clear is span-bounded ECH, never line-wide, so a tile cannot erase 109 tile clear is span-bounded ECH, never line-wide, so a tile cannot erase
104 a neighbor's cells or a rail. 110 a neighbor's cells or a rail.
105 - **`muxa` attaches at 0×0** so an agent never resizes a human's session. 111 - **`mux a` attaches at 0×0** so an agent never resizes a human's session.
106 - **OSC 133 marks are opt-in** (`MUX_SHELL_INTEGRATION=1`); without them `muxa` 112 - **OSC 133 marks are opt-in** (`MUX_SHELL_INTEGRATION=1`); without them `mux a`
107 falls back to `pgid`/`settle` and there is no real exit code. Every `muxa` 113 falls back to `pgid`/`settle` and there is no real exit code. Every `mux a`
108 reply names the `mechanism` that answered it. 114 reply names the `mechanism` that answered it.
109 - **OSC 52 clipboard READ is refused deliberately.** Not a gap. 115 - **OSC 52 clipboard READ is refused deliberately.** Not a gap.
110 - **Agent forwarding is opt-in (`-A`) and the daemon pumps blind.** The 116 - **Agent forwarding is opt-in (`-A`) and the daemon pumps blind.** The
111 per-session agent socket always exists and overwrites the daemon's own 117 per-session agent socket always exists and overwrites the daemon's own
112 `SSH_AUTH_SOCK` — unset when there is no socket, never inherited. The 118 `SSH_AUTH_SOCK` — unset when there is no socket, never inherited. The
113 latest-active `-A` client answers. `muxa` and browsers never OFFER an 119 latest-active `-A` client answers. `mux a` and browsers never OFFER an
114 agent, though anything they run inside a session uses that session's 120 agent, though anything they run inside a session uses that session's
115 socket like any other process. Frames, never transport; both ends cap 121 socket like any other process. Frames, never transport; both ends cap
116 a frame at `agent_data_max`. 122 a frame at `agent_data_max`.
@@ -123,9 +129,9 @@ real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`.
123 anyone appears and an exit disappears, and `n`/`p`/digits walk the one 129 anyone appears and an exit disappears, and `n`/`p`/digits walk the one
124 list. A host with no live session contributes NOTHING — the wall shows 130 list. A host with no live session contributes NOTHING — the wall shows
125 sessions and nothing else, and a wall with none says so; tiles 131 sessions and nothing else, and a wall with none says so; tiles
126 ride out a blip rather than vanishing. `--via` and `muxa` record no host. 132 ride out a blip rather than vanishing. `--via` and `mux a` record no host.
127 Nothing re-creates a session: a daemon restart heals to what that daemon 133 Nothing re-creates a session: a daemon restart heals to what that daemon
128 now has, which for a fresh `muxd` is session `0`. 134 now has, which for a fresh daemon is session `0`.
129 `hosts.load` is strict — a bad line refuses `mux` with rc 2 and prints 135 `hosts.load` is strict — a bad line refuses `mux` with rc 2 and prints
130 it — while `mux hosts` lists and `mux hosts rm` edits VERBATIM, so the 136 it — while `mux hosts` lists and `mux hosts rm` edits VERBATIM, so the
131 repair tool can read what the loader will not. The hub is phase 2: it 137 repair tool can read what the loader will not. The hub is phase 2: it
@@ -153,32 +159,50 @@ real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`.
153 `x` on a session other clients hold and answers the count; the client 159 `x` on a session other clients hold and answers the count; the client
154 arms 3 s for the forcing second press. An accepted end is BOUNDED — 160 arms 3 s for the forcing second press. An accepted end is BOUNDED —
155 SIGKILL past `Pty.term_grace_ms` — so no shell can refuse to die, and 161 SIGKILL past `Pty.term_grace_ms` — so no shell can refuse to die, and
156 `muxd upgrade` is refused while any session is ending rather than carry 162 `mux d upgrade` is refused while any session is ending rather than carry
157 a masterless pty across the exec. A daemon with no arm answers nothing 163 a masterless pty across the exec. A daemon with no arm answers nothing
158 and the client banners `[daemon too old to end a session]`. The tile 164 and the client banners `[daemon too old to end a session]`. The tile
159 leaves on the next list, not on the keypress — EXCEPT a tile that has 165 leaves on the next list, not on the keypress — EXCEPT a tile that has
160 never come up, which names no session to end: `x` there closes the tile 166 never come up, which names no session to end: `x` there closes the tile
161 locally, because a pump parked in `dial` reads no ask and birthing onto 167 locally, because a pump parked in `dial` reads no ask and birthing onto
162 an `unreachable` row is a designed path. 168 an `unreachable` row is a designed path.
163 - **A daemon lives until `muxd stop`; emptiness is not an exit.** `x` ends a 169 - **A daemon lives until `mux d stop`; emptiness is not an exit.** `x` ends a
164 session, never a box: `reap` and `pumpOnce` answer nothing, a shell's code 170 session, never a box: `reap` and `pumpOnce` answer nothing, a shell's code
165 reaches that shell's own clients as `exit_status`, and an emptied daemon 171 reaches that shell's own clients as `exit_status`, and an emptied daemon
166 keeps serving so a birth can take the default name back. One idle process 172 keeps serving so a birth can take the default name back. One idle process
167 per machine is the accepted cost. `muxd run` exits 0 on every shutdown — 173 per machine is the accepted cost. `mux d run` exits 0 on every shutdown —
168 a supervisor reads nonzero on a clean stop as a crash — and nonzero only 174 a supervisor reads nonzero on a clean stop as a crash — and nonzero only
169 for the boot failures `main.run` picks before anything binds. 175 for the boot failures `main.run` picks before anything binds.
170 - **A daemon starts only when someone asks; a read never starts one.** 176 - **A daemon starts only when someone asks; a read never starts one.**
171 `muxd start`, or the local client's own entry spawn, are the only 177 `mux d start`, or the local client's own entry spawn, are the only
172 starters. `muxd endpoint` and `muxd proxy` answer what is on the box and 178 starters. `mux d endpoint` and `mux d proxy` answer what is on the box and
173 exit 1 when nothing is — the wall polls `endpoint` over ssh once a second 179 exit 1 when nothing is — the wall polls `endpoint` over ssh once a second
174 per host, and a poll that started daemons undid every remote `muxd stop` 180 per host, and a poll that started daemons undid every remote `mux d stop`
175 within a second (found on a live box). The dial a user ASKED for — the 181 within a second (found on a live box). The dial a user ASKED for — the
176 `mux HOST` entry dial, picker Enter — runs `ssh HOST 'muxd start'` once 182 `mux HOST` entry dial, picker Enter — runs `ssh HOST 'mux d start'` once
177 and redials once (`HandoffTarget.asked`, spent after the first attempt); 183 and redials once (`HandoffTarget.asked`, spent after the first attempt);
178 the poll, `mux hosts` and every redial read `unreachable` instead. 184 the poll, `mux hosts` and every redial read `unreachable` instead.
179 `asked` DEFAULTS to false: a new dial path that forgets the field starts 185 `asked` DEFAULTS to false: a new dial path that forgets the field starts
180 nothing, because a missing start says so on screen while a spurious one is 186 nothing, because a missing start says so on screen while a spurious one is
181 a daemon on someone else's box that nothing reports. 187 a daemon on someone else's box that nothing reports.
188 - **A local start execs THIS image; nothing looks a daemon up by name.**
189 `spawn.ensureDaemon` execs `spawn.self_exe` — `/proc/self/exe`, the running
190 image by its own kernel link — with argv `mux d run …`. No `execvp`, no
191 PATH walk, so an auto-start can only run the binary that is already
192 running. (It used to exec a `muxd` off PATH, and an e2e leg whose daemon
193 had died graded an installed v0.0.1-10 with no agent code in it.) The pin
194 is `readlink /proc/PID/exe` on the started daemon, in `e2e_03_side`.
195 Running the daemon IN the fork instead was tried and crashes every Debug
196 build: `std.debug.MemoryAccessor` caches the pid it reads memory through,
197 so the child's first DebugAllocator stack trace calls `process_vm_readv`
198 on the parent and hits `unreachable // own pid is always valid`
199 (decisions.md, 2026-08-28).
200 - **Remote is still ssh, and still by name.** `handoff` spells
201 `ssh HOST 'mux d endpoint'` / `'mux d start'`, so a remote running an
202 installation of v0.0.1-15 or older reads `[unreachable]` until it is
203 upgraded. There is no in-place upgrade across the rename either: a ≤15
204 daemon probes the candidate for `muxd <version>` and this binary answers
205 `mux <version>`, so that one migration is `mux d stop` then `mux d start`.
182 - **The layout sidecar is derived convenience, not authored intent.** 206 - **The layout sidecar is derived convenience, not authored intent.**
183 `$XDG_STATE_HOME/mux/layout` stores the pane tree on last detach, from 207 `$XDG_STATE_HOME/mux/layout` stores the pane tree on last detach, from
184 every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall 208 every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall
@@ -191,7 +215,7 @@ real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`.
191 - **Detach is a goodbye, not a tear.** A pump that has sent its detach frame 215 - **Detach is a goodbye, not a tear.** A pump that has sent its detach frame
192 never redials — redial checks `detach_ack` — so the daemon's close after 216 never redials — redial checks `detach_ack` — so the daemon's close after
193 a detach is final, not a network event to recover from. 217 a detach is final, not a network event to recover from.
194 - **An upgrade is an exec, not a restart.** `muxd upgrade` execs the candidate 218 - **An upgrade is an exec, not a restart.** `mux d upgrade` execs the candidate
195 over the RUNNING daemon: same pid, same children, same fds — so `waitpid`, 219 over the RUNNING daemon: same pid, same children, same fds — so `waitpid`,
196 the pid-named shim and agent dirs, and the socket path all survive untouched, 220 the pid-named shim and agent dirs, and the socket path all survive untouched,
197 and `Server.deinit` never runs on that path. The manifest memfd carries only 221 and `Server.deinit` never runs on that path. The manifest memfd carries only
@@ -201,7 +225,7 @@ real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`.
201 dropped on purpose. A return watermark is RE-STAMPED, never carried — it 225 dropped on purpose. A return watermark is RE-STAMPED, never carried — it
202 belongs to the seq space that minted it. Rollback is another exec, of the 226 belongs to the seq space that minted it. Rollback is another exec, of the
203 binary the manifest names, with `MUX_UPGRADE_ROLLBACK` as the loop guard. 227 binary the manifest names, with `MUX_UPGRADE_ROLLBACK` as the loop guard.
204 - No socket stealing: `muxd run` refuses a path another daemon owns. 228 - No socket stealing: `mux d run` refuses a path another daemon owns.
205 229
206 ## Working rules 230 ## Working rules
207 231
@@ -219,7 +243,7 @@ real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`.
219 - **Ask the OS about the OS, not the daemon.** A claim about a pid, an fd, a 243 - **Ask the OS about the OS, not the daemon.** A claim about a pid, an fd, a
220 socket or a process tree is asserted against `/proc`, `ps`, or the shell's 244 socket or a process tree is asserted against `/proc`, `ps`, or the shell's
221 own `$$`. A daemon reporting on itself cannot catch itself being wrong — 245 own `$$`. A daemon reporting on itself cannot catch itself being wrong —
222 which is why `muxd upgrade`'s gate is a shell pid read off the grid. 246 which is why `mux d upgrade`'s gate is a shell pid read off the grid.
223 - Comments say *why*, not *how*. Existing ones are load-bearing — trim noise, 247 - Comments say *why*, not *how*. Existing ones are load-bearing — trim noise,
224 keep rationale. `zig build check` gates the claims (symbol refs must 248 keep rationale. `zig build check` gates the claims (symbol refs must
225 resolve, no history codenames in src comments, and every file's flagged 249 resolve, no history codenames in src comments, and every file's flagged
README.md
Old New
@@ -1,7 +1,7 @@
1 # mux 1 # mux
2 2
3 Terminal multiplexer where detach/reattach is **state sync, not replay**: 3 Terminal multiplexer where detach/reattach is **state sync, not replay**:
4 the terminal engine (ghostty-vt) runs authoritatively in a daemon (`muxd`) 4 the terminal engine (ghostty-vt) runs authoritatively in a daemon (`mux d`)
5 and replicated in the client (`mux`). Attach costs one snapshot; everything 5 and replicated in the client (`mux`). Attach costs one snapshot; everything
6 after is row deltas; a dropped connection is a non-event. Linux only, 6 after is row deltas; a dropped connection is a non-event. Linux only,
7 prototype (see [Status](#status)). 7 prototype (see [Status](#status)).
@@ -15,7 +15,7 @@ dependency). The Makefile points at a pinned toolchain path; override with
15 ```sh 15 ```sh
16 make build # first build fetches + compiles the QUIC deps: ~30MB, a few minutes, once 16 make build # first build fetches + compiles the QUIC deps: ~30MB, a few minutes, once
17 make test && make e2e # verify 17 make test && make e2e # verify
18 make install # the four binaries to ~/.local/bin (override BINDIR) 18 make install # the one binary to ~/.local/bin (override BINDIR)
19 ``` 19 ```
20 20
21 Binaries land in `zig-out/bin/`. For remote machines, build a static binary 21 Binaries land in `zig-out/bin/`. For remote machines, build a static binary
@@ -38,7 +38,7 @@ ssh HOST 'mkdir -p ~/.local/bin && tar xzf - -C ~/.local/bin' < dist/mux-vVERSIO
38 ## Quick start, local 38 ## Quick start, local
39 39
40 ```sh 40 ```sh
41 muxd run & # the daemon: hosts your shell and the authoritative screen 41 mux d run & # the daemon: hosts your shell and the authoritative screen
42 mux # attach 42 mux # attach
43 ``` 43 ```
44 44
@@ -52,7 +52,7 @@ not, so moving focus is a repaint and not a re-attach — no child process,
52 no second screen. 52 no second screen.
53 53
54 The list is the daemons' answer, not your history: `mux` asks every listed 54 The list is the daemons' answer, not your history: `mux` asks every listed
55 daemon once a second what it is hosting, so a session a split, a `muxa` or 55 daemon once a second what it is hosting, so a session a split, a `mux a` or
56 somebody else's client births turns up as a tile by itself, and a session 56 somebody else's client births turns up as a tile by itself, and a session
57 that ends takes its tile with it. Nothing on the wall can outlive the shell 57 that ends takes its tile with it. Nothing on the wall can outlive the shell
58 behind it, and nothing in the file can bring one back. 58 behind it, and nothing in the file can bring one back.
@@ -118,7 +118,7 @@ from `n`. From inside the wall it is the picker's `x` (`Ctrl-\` `s`, then
118 `x` ends a session, never a box. A daemon whose last session ends stays up 118 `x` ends a session, never a box. A daemon whose last session ends stays up
119 and empty, and the next `mux HOST` is born into it — so emptying a machine 119 and empty, and the next `mux HOST` is born into it — so emptying a machine
120 costs you nothing but the shell you meant to end. Ending the daemon itself 120 costs you nothing but the shell you meant to end. Ending the daemon itself
121 is `muxd stop`. 121 is `mux d stop`.
122 122
123 A session whose shell exits takes its tile off the wall. On a terminal the 123 A session whose shell exits takes its tile off the wall. On a terminal the
124 LAST tile's exit leaves you on the empty wall with the picker over it, not 124 LAST tile's exit leaves you on the empty wall with the picker over it, not
@@ -160,7 +160,7 @@ not reach them. Not worth designing past until someone has thirty-three
160 shells. 160 shells.
161 161
162 To see which sessions are alive without attaching to one: `mux hosts` prints 162 To see which sessions are alive without attaching to one: `mux hosts` prints
163 every daemon on the wall with its live session count, and `muxd stats` names 163 every daemon on the wall with its live session count, and `mux d stats` names
164 every session of one daemon with the number of clients watching it. `stats` 164 every session of one daemon with the number of clients watching it. `stats`
165 is the question to ask when `mux` will not let you in — thirty-two sessions 165 is the question to ask when `mux` will not let you in — thirty-two sessions
166 is the table, and the thirty-third name has nowhere to go. 166 is the table, and the thirty-third name has nowhere to go.
@@ -178,24 +178,24 @@ if you really want the loop.
178 178
179 ## Quick start, remote over ssh 179 ## Quick start, remote over ssh
180 180
181 Works anywhere ssh works. On the remote host: put `muxd` on PATH (`make 181 Works anywhere ssh works. On the remote host: put `mux` on PATH (`make
182 install` puts it in `~/.local/bin`), then: 182 install` puts it in `~/.local/bin`), then:
183 183
184 ```sh 184 ```sh
185 ssh HOST 'muxd start' 185 ssh HOST 'mux d start'
186 mux HOST # attach; Ctrl-\ d detaches, running it again reattaches 186 mux HOST # attach; Ctrl-\ d detaches, running it again reattaches
187 ``` 187 ```
188 188
189 `muxd start` spawns the daemon detached and waits until it answers, so it 189 `mux d start` spawns the daemon detached and waits until it answers, so it
190 is safe to run every time — if one is already up it says so and exits 0. 190 is safe to run every time — if one is already up it says so and exits 0.
191 191
192 `mux HOST` runs `muxd endpoint` on HOST over ssh (finding a muxd in 192 `mux HOST` runs `mux d endpoint` on HOST over ssh (finding a `mux` in
193 `~/.local/bin` even though ssh's non-login shell leaves it off PATH). On a 193 `~/.local/bin` even though ssh's non-login shell leaves it off PATH). On a
194 box with no daemon that verb answers nothing, and `mux HOST` runs `muxd 194 box with no daemon that verb answers nothing, and `mux HOST` runs `mux d
195 start` there once, over the same ssh, before asking again — so the 195 start` there once, over the same ssh, before asking again — so the
196 `ssh HOST 'muxd start'` line above is optional after the first time. 196 `ssh HOST 'mux d start'` line above is optional after the first time.
197 Nothing else starts a daemon: the wall's per-host poll, `mux hosts`, every 197 Nothing else starts a daemon: the wall's per-host poll, `mux hosts`, every
198 reconnect and `mux --via 'ssh HOST muxd proxy'` all report the box 198 reconnect and `mux --via 'ssh HOST mux d proxy'` all report the box
199 unreachable and leave it alone. Your 199 unreachable and leave it alone. Your
200 ssh config (aliases, ports, 200 ssh config (aliases, ports,
201 ProxyJump) all keeps working, since `mux` never parses HOST. That ssh 201 ProxyJump) all keeps working, since `mux` never parses HOST. That ssh
@@ -232,8 +232,8 @@ carries — it pumps bytes between the session and your machine's agent — so
232 policy stays in the agent, `ssh-add -c` confirmations included. 232 policy stays in the agent, `ssh-add -c` confirmations included.
233 233
234 **What this changes for local sessions:** the daemon's socket *replaces* 234 **What this changes for local sessions:** the daemon's socket *replaces*
235 whatever `SSH_AUTH_SOCK` muxd itself was started with, in every session it 235 whatever `SSH_AUTH_SOCK` the daemon itself was started with, in every session it
236 spawns. Before agent forwarding, a locally auto-started muxd passed its own 236 spawns. Before agent forwarding, a locally auto-started daemon passed its own
237 inherited agent straight through, and `git push` inside a local session 237 inherited agent straight through, and `git push` inside a local session
238 worked with no flag at all; now it finds an agent that answers only while an 238 worked with no flag at all; now it finds an agent that answers only while an
239 `-A` client is attached, and refuses otherwise. This is deliberate — a 239 `-A` client is attached, and refuses otherwise. This is deliberate — a
@@ -267,7 +267,7 @@ own daemon) forwards on that tile, and a sibling grown from it by chord
267 exposed. Every OTHER tile on the wall arrives from a daemon's session list 267 exposed. Every OTHER tile on the wall arrives from a daemon's session list
268 rather than from your command line, and those never offer an agent: the 268 rather than from your command line, and those never offer an agent: the
269 wall is a list of machines, and a machine you have never typed the flag for 269 wall is a list of machines, and a machine you have never typed the flag for
270 must not get your keys because it happens to be on it. `muxa` never 270 must not get your keys because it happens to be on it. `mux a` never
271 forwards, under any flag. 271 forwards, under any flag.
272 272
273 ## Quick start, remote over QUIC 273 ## Quick start, remote over QUIC
@@ -276,30 +276,30 @@ No ssh in the path; needs inbound UDP to the host. Authentication is a
276 shared 32-byte key (TLS 1.3 external PSK — no certificates), so the 276 shared 32-byte key (TLS 1.3 external PSK — no certificates), so the
277 handshake is one round trip. 277 handshake is one round trip.
278 278
279 Put `muxd` on the host (`scp` a static binary), then: 279 Put `mux` on the host (`scp` a static binary), then:
280 280
281 ```sh 281 ```sh
282 muxd keygen # once 282 mux d keygen # once
283 ssh HOST 'mkdir -p -m 700 ~/.config/mux && cat > ~/.config/mux/key \ 283 ssh HOST 'mkdir -p -m 700 ~/.config/mux && cat > ~/.config/mux/key \
284 && chmod 600 ~/.config/mux/key' < ~/.config/mux/key # once per host 284 && chmod 600 ~/.config/mux/key' < ~/.config/mux/key # once per host
285 ssh HOST 'muxd start --quic 0.0.0.0' # once per host boot 285 ssh HOST 'mux d start --quic 0.0.0.0' # once per host boot
286 mux quic://HOST # every attach 286 mux quic://HOST # every attach
287 ``` 287 ```
288 288
289 The port defaults to 4433 on both ends, and both binaries look for the key 289 The port defaults to 4433 on both ends, and both sides look for the key
290 at `~/.config/mux/key` — `--key FILE` or `MUX_KEY_FILE` override it, in 290 at `~/.config/mux/key` — `--key FILE` or `MUX_KEY_FILE` override it, in
291 that order. 291 that order.
292 292
293 **Trust model, in one sentence:** anyone holding that key file can attach 293 **Trust model, in one sentence:** anyone holding that key file can attach
294 to that session, so it is exactly as sensitive as an ssh private key — 294 to that session, so it is exactly as sensitive as an ssh private key —
295 `muxd` and `mux` both refuse one that is readable by group or other, and 295 `mux` refuses one that is readable by group or other, and
296 there is no unauthenticated mode to fall back to. 296 there is no unauthenticated mode to fall back to.
297 297
298 ## Quick start, a wall of sessions in a browser 298 ## Quick start, a wall of sessions in a browser
299 299
300 ```sh 300 ```sh
301 muxweb # restores the wall you left; opens on 127.0.0.1:7681 301 mux web # restores the wall you left; opens on 127.0.0.1:7681
302 muxweb HOST quic://OTHER --sock /tmp/s.sock#b # ...or state it: shows these, ADDS them to the saved wall 302 mux web HOST quic://OTHER --sock /tmp/s.sock#b # ...or state it: shows these, ADDS them to the saved wall
303 ``` 303 ```
304 304
305 Each tile is a live session, dialed the way `mux` dials it — the same 305 Each tile is a live session, dialed the way `mux` dials it — the same
@@ -310,7 +310,7 @@ box adds a tile, `×` removes one (which detaches — the session and
310 everything in it keep running), `+` starts a new session on that tile's host, 310 everything in it keep running), `+` starts a new session on that tile's host,
311 and tiles drag into whatever order you want. The result is saved to 311 and tiles drag into whatever order you want. The result is saved to
312 `$XDG_STATE_HOME/mux/wall` (`~/.local/state/mux/wall`), one spelling per 312 `$XDG_STATE_HOME/mux/wall` (`~/.local/state/mux/wall`), one spelling per
313 line, so the next bare `muxweb` comes back to the same wall. That saved 313 line, so the next bare `mux web` comes back to the same wall. That saved
314 spelling — `--sock PATH` in one piece — is also accepted on the command 314 spelling — `--sock PATH` in one piece — is also accepted on the command
315 line, so a line from the file can be pasted back verbatim. 315 line, so a line from the file can be pasted back verbatim.
316 316
@@ -323,7 +323,7 @@ all, because its file names no session to re-create.
323 Localhost only, deliberately: to see it from another machine, forward it — 323 Localhost only, deliberately: to see it from another machine, forward it —
324 `ssh -L 7681:127.0.0.1:7681 HOST`. 324 `ssh -L 7681:127.0.0.1:7681 HOST`.
325 325
326 **The hub and the CLI wall have parted company for now.** `muxweb` still 326 **The hub and the CLI wall have parted company for now.** `mux web` still
327 keeps its own list of *sessions* in `~/.local/state/mux/wall`, and nothing 327 keeps its own list of *sessions* in `~/.local/state/mux/wall`, and nothing
328 on the command line writes that file any more — so on a fresh machine the 328 on the command line writes that file any more — so on a fresh machine the
329 hub opens empty until you name tiles on its command line or in the page, and 329 hub opens empty until you name tiles on its command line or in the page, and
@@ -361,7 +361,7 @@ Every `mux` that names a target records that target — `mux HOST`,
361 `mux quic://HOST`, `mux --sock PATH`, and a bare `mux`, which records your 361 `mux quic://HOST`, `mux --sock PATH`, and a bare `mux`, which records your
362 own daemon. `--via CMD` never records one, because an arbitrary command is 362 own daemon. `--via CMD` never records one, because an arbitrary command is
363 not an address and the grammar has no line to write for it; the session is 363 not an address and the grammar has no line to write for it; the session is
364 real, it is just not a machine the wall can name. `muxa` records nothing 364 real, it is just not a machine the wall can name. `mux a` records nothing
365 either (it attaches at 0×0 and claims nothing). Writing the file is best 365 either (it attaches at 0×0 and claims nothing). Writing the file is best
366 effort — if it cannot be written you get one warning line and the attach 366 effort — if it cannot be written you get one warning line and the attach
367 happens anyway. 367 happens anyway.
@@ -401,7 +401,7 @@ it dies on every reboot while its line lives on.
401 401
402 A host that answers with an EMPTY list contributes nothing either, and that 402 A host that answers with an EMPTY list contributes nothing either, and that
403 is an ordinary state rather than a remote curiosity: a daemon lives until 403 is an ordinary state rather than a remote curiosity: a daemon lives until
404 `muxd stop`, so one whose last session you ended sits there with nothing to 404 `mux d stop`, so one whose last session you ended sits there with nothing to
405 show until someone is born into it again. A wall on which no host has a live 405 show until someone is born into it again. A wall on which no host has a live
406 session is empty, and says so on one line rather than showing a blank screen 406 session is empty, and says so on one line rather than showing a blank screen
407 that reads as hung. 407 that reads as hung.
@@ -425,7 +425,7 @@ still exits with the shell's exit code.
425 sessions is a new question on the wire, and a daemon from before this change 425 sessions is a new question on the wire, and a daemon from before this change
426 answers it with the silence it answers every unknown frame with. It is up, 426 answers it with the silence it answers every unknown frame with. It is up,
427 `mux --sock PATH` still attaches to it, and none of its sessions reach the 427 `mux --sock PATH` still attaches to it, and none of its sessions reach the
428 wall, so today `muxd upgrade` (or a restart) on every box you put on the 428 wall, so today `mux d upgrade` (or a restart) on every box you put on the
429 wall is what fills it in. 429 wall is what fills it in.
430 430
431 Restarting a daemon re-creates **nothing**. The old wall would have brought 431 Restarting a daemon re-creates **nothing**. The old wall would have brought
@@ -444,7 +444,7 @@ There is no `mux wall` any more — the wall has no separate door to open, so
444 `mux wall` is read as a HOST called `wall` like any other bare word, and ssh 444 `mux wall` is read as a HOST called `wall` like any other bare word, and ssh
445 says what it thinks of that. `hosts` is the one name a subcommand claims: 445 says what it thinks of that. `hosts` is the one name a subcommand claims:
446 `mux hosts` lists, so a machine called `hosts` is attached to as 446 `mux hosts` lists, so a machine called `hosts` is attached to as
447 `quic://hosts` or `--via ssh hosts muxd proxy`, though `mux hosts add hosts` 447 `quic://hosts` or `--via ssh hosts mux d proxy`, though `mux hosts add hosts`
448 puts it on the wall by that name quite happily. 448 puts it on the wall by that name quite happily.
449 449
450 Every tile on the wall is a real attach at its own rectangle — label bar plus 450 Every tile on the wall is a real attach at its own rectangle — label bar plus
@@ -503,28 +503,28 @@ has none.
503 ## Everything else 503 ## Everything else
504 504
505 ```sh 505 ```sh
506 muxd dump [--vt] # debug: print the authoritative grid (what the screen *should* be) 506 mux d dump [--vt] # debug: print the authoritative grid (what the screen *should* be)
507 muxd stats # live sessions by name + clients each; wire stats (deltas vs snapshot bytes) 507 mux d stats # live sessions by name + clients each; wire stats (deltas vs snapshot bytes)
508 muxd upgrade # exec a newly installed binary in place; every session keeps running 508 mux d upgrade # exec a newly installed binary in place; every session keeps running
509 make bench # typing-workload byte-ratio measurement 509 make bench # typing-workload byte-ratio measurement
510 ``` 510 ```
511 511
512 `muxa` is the agent-facing client — verbs `status`, `capture`, `send`, 512 `mux a` is the agent-facing mode — verbs `status`, `capture`, `send`,
513 `run`, `await`, one JSON object each, against a local socket or 513 `run`, `await`, one JSON object each, against a local socket or
514 `--quic HOST`. `muxa run "make test"` sends the command line, waits for the 514 `--quic HOST`. `mux a run "make test"` sends the command line, waits for the
515 shell to return it, and prints the exit code with the output rows; no 515 shell to return it, and prints the exit code with the output rows; no
516 polling and no sleeps, because the daemon holds the wait. It attaches at 516 polling and no sleeps, because the daemon holds the wait. It attaches at
517 0×0 so it never resizes the session a human is using — which also means it 517 0×0 so it never resizes the session a human is using — which also means it
518 can only join a session that already exists, and `--session NAME` for a name 518 can only join a session that already exists, and `--session NAME` for a name
519 no daemon holds prints `{"error":"attach refused"}` and exits 1 rather than 519 no daemon holds prints `{"error":"attach refused"}` and exits 1 rather than
520 creating one. (`capture` is the exception: it reads by name without 520 creating one. (`capture` is the exception: it reads by name without
521 attaching, so it answers `{"grid":"muxd: no such session: NAME"}` and exits 521 attaching, so it answers `{"grid":"mux d: no such session: NAME"}` and exits
522 0.) Every reply names the `mechanism` that answered — an exit code is real 522 0.) Every reply names the `mechanism` that answered — an exit code is real
523 only under `marks` (OSC 133 shell integration, injected at spawn), absent 523 only under `marks` (OSC 133 shell integration, injected at spawn), absent
524 under the `pgid` and `settle` fallbacks. 524 under the `pgid` and `settle` fallbacks.
525 525
526 Marks are **opt-in**: start the daemon with `MUX_SHELL_INTEGRATION=1` in its 526 Marks are **opt-in**: start the daemon with `MUX_SHELL_INTEGRATION=1` in its
527 environment, or `muxa` gets the fallbacks and no exit code. The injection is 527 environment, or `mux a` gets the fallbacks and no exit code. The injection is
528 off by default because it is not free on a shell you actually live in — under 528 off by default because it is not free on a shell you actually live in — under
529 zsh it points `ZDOTDIR` at a shim directory that has no `.zshenv`, and under 529 zsh it points `ZDOTDIR` at a shim directory that has no `.zshenv`, and under
530 bash its `DEBUG` trap displaces yours (atuin, bash-preexec, iTerm2). A shell 530 bash its `DEBUG` trap displaces yours (atuin, bash-preexec, iTerm2). A shell
@@ -535,13 +535,13 @@ recently active client — typing, attaching, or resizing claims it (latest
535 wins). A session survives logout (this assumes systemd-logind's default 535 wins). A session survives logout (this assumes systemd-logind's default
536 `KillUserProcesses=no`; a box configured to kill user processes at logout 536 `KillUserProcesses=no`; a box configured to kill user processes at logout
537 kills the daemon with them), though not a reboot. 537 kills the daemon with them), though not a reboot.
538 `muxd run` refuses a socket another daemon already owns; there is no 538 `mux d run` refuses a socket another daemon already owns; there is no
539 socket-stealing. It runs until something stops it — no session's exit ends 539 socket-stealing. It runs until something stops it — no session's exit ends
540 it — and exits 0 when `muxd stop` or a supervisor's SIGTERM asks; a nonzero 540 it — and exits 0 when `mux d stop` or a supervisor's SIGTERM asks; a nonzero
541 exit means it never got as far as serving. 541 exit means it never got as far as serving.
542 542
543 Rolling a new daemon out does **not** mean killing the sessions. Install the 543 Rolling a new daemon out does **not** mean killing the sessions. Install the
544 new binary and run `muxd upgrade` — the new binary asks the running daemon 544 new binary and run `mux d upgrade` — the new binary asks the running daemon
545 to become it, and the daemon `execve`s it in place: same pid, same shells, 545 to become it, and the daemon `execve`s it in place: same pid, same shells,
546 same socket, same ssh-agent sockets. Attached clients see one reconnect and 546 same socket, same ssh-agent sockets. Attached clients see one reconnect and
547 one repaint. It refuses anything that is not strictly newer, naming both 547 one repaint. It refuses anything that is not strictly newer, naming both
@@ -552,7 +552,7 @@ changes nothing at all. If the new binary cannot adopt what it read, it
552 execs the old one back and the old one carries on serving. Scrollback does 552 execs the old one back and the old one carries on serving. Scrollback does
553 not survive the handover; the visible grid, the titles, the command marks 553 not survive the handover; the visible grid, the titles, the command marks
554 and the exit codes do. A daemon too old to know the request answers nothing, 554 and the exit codes do. A daemon too old to know the request answers nothing,
555 and `muxd upgrade` says so: `no reply: this daemon predates upgrade — stop 555 and `mux d upgrade` says so: `no reply: this daemon predates upgrade — stop
556 and run`. A session in the middle of being ended (`Ctrl-\` `x`, up to half a 556 and run`. A session in the middle of being ended (`Ctrl-\` `x`, up to half a
557 second while its shell is being hung up) refuses the upgrade with `session 557 second while its shell is being hung up) refuses the upgrade with `session
558 ending, retry` rather than carrying a shell with no master across the exec. 558 ending, retry` rather than carrying a shell with no master across the exec.
@@ -573,7 +573,7 @@ deltas. A client that attaches — or *re*attaches after a kill, a network
573 tear, or a laptop sleep — tells the daemon what sequence number it already 573 tear, or a laptop sleep — tells the daemon what sequence number it already
574 holds, and the daemon answers with a delta instead of a repaint whenever it 574 holds, and the daemon answers with a delta instead of a repaint whenever it
575 can. Transport is deliberately dumb: the same frames ride a unix socket, an 575 can. Transport is deliberately dumb: the same frames ride a unix socket, an
576 ssh pipe (`muxd proxy` is a byte pump with zero protocol knowledge), or a 576 ssh pipe (`mux d proxy` is a byte pump with zero protocol knowledge), or a
577 QUIC stream, and the wire protocol has survived all three without changing. 577 QUIC stream, and the wire protocol has survived all three without changing.
578 578
579 Every decision and measurement: `docs/decisions.md`. What's next: `git-collab 579 Every decision and measurement: `docs/decisions.md`. What's next: `git-collab
docs/decisions.md
Old New
@@ -7401,3 +7401,64 @@ bound is refused by `connect(2)`: 1 ms for one and 1 ms for four, so absent
7401 sockets are free at any count. What this does NOT bound is a blackholed 7401 sockets are free at any count. What this does NOT bound is a blackholed
7402 HOST, where ssh sits in the kernel's TCP retry schedule; `handoff.sshLine`'s 7402 HOST, where ssh sits in the kernel's TCP retry schedule; `handoff.sshLine`'s
7403 batch recipe carries `ConnectTimeout=5` for that. 7403 batch recipe carries `ConnectTimeout=5` for that.
7404
7405 ## 2026-08-28 — four binaries became one `mux`, and the start execs itself
7406
7407 `muxd`, `mux`, `muxa` and `muxweb` are one executable, `mux`, whose first
7408 word picks a mode: `d` the daemon, `a` the agent surface, `web` the hub, no
7409 letter the client. The four mains stay four files under `src/cli/` and became
7410 modules of one root (`src/cli/mux.zig`); each is handed the argv slice it
7411 already read (`args[1..]`, whose `[0]` is the mode word), so no parser
7412 changed. One alias exists, `mux run …` → `mux d run …`, because a daemon of
7413 v0.0.1-15 or older execs its upgrade candidate as `<binary> run --resume-fd N`.
7414
7415 The local auto-start no longer walks PATH. `spawn.ensureDaemon` execs
7416 `spawn.self_exe` — `/proc/self/exe`, the running image by its own kernel
7417 link — with argv `mux d run …`. `findInPath` is gone. That closes the
7418 ambient-PATH trap by construction rather than by a `PATH=` prefix in the e2e
7419 runner: there is no name left in the tree to resolve, so none to resolve
7420 wrong.
7421
7422 Running the daemon IN the fork instead of exec'ing was tried first and
7423 abandoned, deterministically and for a reason worth writing down.
7424 `std.debug.MemoryAccessor` caches the pid it reads memory through (a private
7425 `cached_pid`), so a forked child's first DebugAllocator stack trace calls
7426 `process_vm_readv` on the PARENT and gets ESRCH — whose arm is
7427 `unreachable, // own pid is always valid`. The child died the first time a
7428 client asked it for a snapshot, minutes after `up (0.1s)` had been printed;
7429 the core trace reads `collectStackTrace <- DebugAllocator.alloc <-
7430 DeltaTracker.rebuild <- Server.pumpOnce`. Nothing about it is reachable from
7431 here: the cache is private and its SRCH arm is not an error to catch. The
7432 general rule it leaves behind is bigger than this repo — fork-without-exec
7433 inherits process-global caches keyed on the pid, and a single-thread
7434 precondition does not cover them.
7435
7436 The pin is `e2e_03_side`: after a client auto-start, `readlink /proc/PID/exe`
7437 must be the binary the suite was handed. The kernel answering about the
7438 kernel, and the LINK rather than the argv, because argv is what a wrong
7439 spawn still gets right. Shown to discriminate by starting the same binary
7440 from a copy elsewhere, where the link names the copy. `spawn.zig`'s own test
7441 asks a stub for `$0` and `$*` — the file the kernel exec'd, and the argv it
7442 was handed — and nothing on this box resolves BY NAME to a file in a fresh
7443 tmp dir, so a spawn that searched PATH cannot pass it.
7444
7445 `test/e2e_lib.sh`'s `proxy_pid` had to change anyway: one binary means the
7446 CLIENT is a `mux` too, and its argv carries the whole `--via '… d proxy …'`
7447 string, so the match is positional (`$4 == d && $5 == proxy`) rather than
7448 anywhere in the line.
7449
7450 One cost, and it is a break:
7451
7452 A daemon of v0.0.1-15 or older cannot be upgraded in place into this binary.
7453 Its `checkVersionOutput` runs `<candidate> --version` and demands exactly
7454 `muxd <version>`; this binary answers `mux <version>`, and there is no argv
7455 that reaches the candidate to say otherwise — the probe is a bare
7456 `--version`. `mux d upgrade` translates that daemon's
7457 `version: output mismatch` into the one-time instruction (`mux d stop`,
7458 `mux d start`). The `run` alias still earns its place: it is what the OLD
7459 daemon's exec spells, and what the rollback exec spells back, since the
7460 binary rolled back TO is by definition the older one.
7461
7462 Remote is unchanged in mechanism and changed in spelling: `handoff` builds
7463 `ssh HOST 'mux d endpoint'` and `'mux d start'`, so a box still running a
7464 ≤v0.0.1-15 install reads `[unreachable]` on the wall until it is upgraded.
docs/roadmap.md
Old New
@@ -24,7 +24,7 @@ being predicted, and shipped as M13.
24 **Target, stated 2026-08-15:** replace tmux as a daily driver. An agent 24 **Target, stated 2026-08-15:** replace tmux as a daily driver. An agent
25 driving a session is a nice-to-have behind that, not a peer. The first 25 driving a session is a nice-to-have behind that, not a peer. The first
26 consequence was immediate — shell integration became opt-in, because its 26 consequence was immediate — shell integration became opt-in, because its
27 two known costs land on the human's shell and only `muxa` reads what they 27 two known costs land on the human's shell and only `mux a` reads what they
28 buy (decisions.md, 2026-08-15). The queue below is ranked against the old 28 buy (decisions.md, 2026-08-15). The queue below is ranked against the old
29 framing and is being re-ranked against this one; the tracker 29 framing and is being re-ranked against this one; the tracker
30 (`git-collab issue list`) carries the live order. 30 (`git-collab issue list`) carries the live order.
@@ -54,14 +54,14 @@ proven so by a mutation that fired immediately.
54 ## M10 — QUIC ergonomics — complete 54 ## M10 — QUIC ergonomics — complete
55 55
56 **Verdict: cleared on the real box.** The whole QUIC story is four 56 **Verdict: cleared on the real box.** The whole QUIC story is four
57 commands — `muxd keygen`, one ssh line placing the key, `ssh HOST 'muxd 57 commands — `mux d keygen`, one ssh line placing the key, `ssh HOST 'mux d
58 start --quic 0.0.0.0'`, `mux quic://HOST` — no port or key path typed 58 start --quic 0.0.0.0'`, `mux quic://HOST` — no port or key path typed
59 anywhere, trust posture unchanged. Kill criterion held on the LAN box: 59 anywhere, trust posture unchanged. Kill criterion held on the LAN box:
60 first-attach success from a clean slate, a marker surviving full ssh 60 first-attach success from a clean slate, a marker surviving full ssh
61 logout, ten attach/detach cycles with exactly one daemon counted by 61 logout, ten attach/detach cycles with exactly one daemon counted by
62 tracked pid (decisions.md, M10). systemd is deleted, not deferred: the 62 tracked pid (decisions.md, M10). systemd is deleted, not deferred: the
63 contrib/ units and `LISTEN_FDS` activation are gone, with the removal 63 contrib/ units and `LISTEN_FDS` activation are gone, with the removal
64 commit as the resurrection reference. En route: a panic on `muxd 64 commit as the resurrection reference. En route: a panic on `mux d
65 start`'s own first-run-mistake path, two plan mutation gates that could 65 start`'s own first-run-mistake path, two plan mutation gates that could
66 not fail, and a third cross-binary constant drift — all fixed, all with 66 not fail, and a third cross-binary constant drift — all fixed, all with
67 the rule recorded ("assert the literal, never the constant the code 67 the rule recorded ("assert the literal, never the constant the code
@@ -108,13 +108,13 @@ failing with the mutation's own signature — and the rule extracted is
108 **an assertion about order must be an assertion the bytes can actually 108 **an assertion about order must be an assertion the bytes can actually
109 carry**. 109 carry**.
110 110
111 ## M13 — trial friction: auto-start, `muxd stop`, error audit — complete 111 ## M13 — trial friction: auto-start, `mux d stop`, error audit — complete
112 112
113 **Verdict: cleared, both legs.** The three field findings that headed 113 **Verdict: cleared, both legs.** The three field findings that headed
114 this list are closed. `mux user@host` and `muxd proxy` against a box 114 this list are closed. `mux user@host` and `mux d proxy` against a box
115 with no daemon now start one — one helper, `spawn.ensureForAttach`, at 115 with no daemon now start one — one helper, `spawn.ensureForAttach`, at
116 both call sites, spawning a **bare** `run --sock` (never `--quic`), no 116 both call sites, spawning a **bare** `run --sock` (never `--quic`), no
117 opt-out, and the warm path stays silent by contract. `muxd stop` exists 117 opt-out, and the warm path stays silent by contract. `mux d stop` exists
118 as a protocol verb (`stop_req = 0x07`), armed on both dispatches because 118 as a protocol verb (`stop_req = 0x07`), armed on both dispatches because
119 a connection is an *observer* until it attaches; it is idempotent when 119 a connection is an *observer* until it attaches; it is idempotent when
120 nothing is listening (exit 0) and honest when a daemon is wedged — 120 nothing is listening (exit 0) and honest when a daemon is wedged —
@@ -130,14 +130,14 @@ predicted them, printing the predicted text (decisions.md, M13, which
130 carries the table and the failure lines). En route: auto-start turns "a 130 carries the table and the failure lines). En route: auto-start turns "a
131 second daemon on another socket" from an explicit act into an ssh side 131 second daemon on another socket" from an explicit act into an ssh side
132 effect, which is why the daemon log is now opened **append-only** and 132 effect, which is why the daemon log is now opened **append-only** and
133 truncation is reserved for `muxd start`; and three review prescriptions 133 truncation is reserved for `mux d start`; and three review prescriptions
134 were rejected with evidence, which is the adversarial flow running in 134 were rejected with evidence, which is the adversarial flow running in
135 the direction it is easier to forget. 135 the direction it is easier to forget.
136 136
137 ## M14 — ssh→QUIC handoff — complete 137 ## M14 — ssh→QUIC handoff — complete
138 138
139 **Verdict: cleared, both legs.** `mux HOST` no longer chooses a 139 **Verdict: cleared, both legs.** `mux HOST` no longer chooses a
140 transport; it earns one. An ssh to `muxd endpoint` fetches the daemon's 140 transport; it earns one. An ssh to `mux d endpoint` fetches the daemon's
141 QUIC port and key as a mandatory one-line announce, the session moves 141 QUIC port and key as a mandatory one-line announce, the session moves
142 onto QUIC and the coordination ssh is reaped, and the coordinates are 142 onto QUIC and the coordination ssh is reaped, and the coordinates are
143 cached so every later attach dials direct with no ssh process at all. 143 cached so every later attach dials direct with no ssh process at all.
@@ -209,15 +209,15 @@ and **CONVERGED byte-clean** against the daemon's grid on detach; UDP
209 stayed `up` → rule removed → `up` with the grid content intact across 209 stayed `up` → rule removed → `up` with the grid content intact across
210 the epoch; page + /tiles served through an ssh tunnel (−R; the box has 210 the epoch; page + /tiles served through an ssh tunnel (−R; the box has
211 no ssh route back, so −L was exercised as its mirror image). Two field 211 no ssh route back, so −L was exercised as its mirror image). Two field
212 findings, filed below in the candidates: muxweb has ONE `--key` for 212 findings, filed below in the candidates: the hub has ONE `--key` for
213 all quic tiles (per-box keys cannot mix), and the deployment habit of 213 all quic tiles (per-box keys cannot mix), and the deployment habit of
214 per-box `muxd keygen` had left desktop/LAN/WAN with three different 214 per-box `mux d keygen` had left desktop/LAN/WAN with three different
215 keys — silent mutual-auth refusal presenting as "did not answer" on 215 keys — silent mutual-auth refusal presenting as "did not answer" on
216 every dial. Trial unified the boxes on a fresh key; the CLI observer 216 every dial. Trial unified the boxes on a fresh key; the CLI observer
217 clip also confirmed renderClipped's top-left window is what an 217 clip also confirmed renderClipped's top-left window is what an
218 undersized client sees of a larger grid. 218 undersized client sees of a larger grid.
219 219
220 What shipped: `muxweb` (localhost-only hub; tiles are argv in mux's own 220 What shipped: `mux web` (localhost-only hub; tiles are argv in mux's own
221 TARGET spellings; one WebSocket per tile; std.http's upgrade with the 221 TARGET spellings; one WebSocket per tile; std.http's upgrade with the
222 Origin gate ours), `replica.zig` (the replay core extracted from 222 Origin gate ours), `replica.zig` (the replay core extracted from
223 `session()` and unit-tested for the first time), `keymap.zig`, 223 `session()` and unit-tested for the first time), `keymap.zig`,
@@ -248,9 +248,9 @@ spawn (`shellint.zig` — zsh `ZDOTDIR` shim, bash `--init-file`, fish
248 `vendor_conf.d`), a mux-owned ghostty-vt stream handler that surfaces them 248 `vendor_conf.d`), a mux-owned ghostty-vt stream handler that surfaces them
249 as row-stamped events, a pure command state machine (`cmd.zig`), three 249 as row-stamped events, a pure command state machine (`cmd.zig`), three
250 frame pairs (`cmd_state`, `await_req`/`await_reply`, 250 frame pairs (`cmd_state`, `await_req`/`await_reply`,
251 `status_req`/`status_reply`) that ride `muxd proxy` untouched, server-held 251 `status_req`/`status_reply`) that ride `mux d proxy` untouched, server-held
252 awaits with a pgid edge and a settle floor under the marks path, and 252 awaits with a pgid edge and a settle floor under the marks path, and
253 `muxa` — a fourth binary, five verbs, one JSON object per verb, attaching 253 `mux a` — a fourth binary, five verbs, one JSON object per verb, attaching
254 at 0×0 so an agent never claims the human's grid. e2e is `test/agent.sh`, 254 at 0×0 so an agent never claims the human's grid. e2e is `test/agent.sh`,
255 **9 scenarios**, including a QUIC tear healed mid-await with the command 255 **9 scenarios**, including a QUIC tear healed mid-await with the command
256 proven to have run exactly once. The decisions worth reading before 256 proven to have run exactly once. The decisions worth reading before
@@ -281,20 +281,20 @@ they are no longer on the default path. See decisions.md, that date.)*
281 - **`--timeout N` can overshoot N** by the client's grace window (2s over a 281 - **`--timeout N` can overshoot N** by the client's grace window (2s over a
282 socket, up to 30s over a slow QUIC handshake): an agent budgeting wall 282 socket, up to 30s over a slow QUIC handshake): an agent budgeting wall
283 clock must add it. 283 clock must add it.
284 - **`muxa` has no idle-timeout flag**, deliberately — `--timeout` is the 284 - **`mux a` has no idle-timeout flag**, deliberately — `--timeout` is the
285 only bound on a wait. 285 only bound on a wait.
286 286
287 **Deferred, and named** (the spec's non-goals plus what review added): an 287 **Deferred, and named** (the spec's non-goals plus what review added): an
288 MCP server wrapping `muxa`; read-only or capability-scoped auth (one key = 288 MCP server wrapping `mux a`; read-only or capability-scoped auth (one key =
289 full control still); input attribution; semantic event subscriptions 289 full control still); input attribution; semantic event subscriptions
290 beyond the `cmd_state` push; `muxa drive` as single-shot sugar; the 290 beyond the `cmd_state` push; `mux a drive` as single-shot sugar; the
291 `.zshenv` shim; bash-preexec coexistence; the shim-directory startup 291 `.zshenv` shim; bash-preexec coexistence; the shim-directory startup
292 sweep. The `parseQuicAddr`/`resolveHost` dedup that review endorsed is 292 sweep. The `parseQuicAddr`/`resolveHost` dedup that review endorsed is
293 **paid**, not deferred — one owner in `quic.zig`, landed after the e2e had 293 **paid**, not deferred — one owner in `quic.zig`, landed after the e2e had
294 pinned the spellings. 294 pinned the spellings.
295 295
296 **Dropped in execution, not decided — the one item here that nobody chose:** 296 **Dropped in execution, not decided — the one item here that nobody chose:**
297 `muxa capture --diff-since SEQ`. The spec promises it three times and no 297 `mux a capture --diff-since SEQ`. The spec promises it three times and no
298 code was written; the whole-branch review found it. A future one needs a 298 code was written; the whole-branch review found it. A future one needs a
299 protocol field of its own for the live stream seq, because `status_reply`'s 299 protocol field of its own for the live stream seq, because `status_reply`'s
300 seq now carries the return watermark — right for awaits, wrong for a diff 300 seq now carries the return watermark — right for awaits, wrong for a diff
@@ -302,7 +302,7 @@ to quote (decisions.md). Two coverage narrowings ride with it: the **fish**
302 injection arm is unit-tested only (no fish on this box; zsh and bash are 302 injection arm is unit-tested only (no fish on this box; zsh and bash are
303 driven live), and the spec's **version-skew fixture** was never built (the 303 driven live), and the spec's **version-skew fixture** was never built (the
304 behaviour is pinned through the dead-daemon paths, an old binary is not). 304 behaviour is pinned through the dead-daemon paths, an old binary is not).
305 And `muxa capture` is **the one verb with no automated coverage at all** — 305 And `mux a capture` is **the one verb with no automated coverage at all** —
306 `test/agent.sh` drives run, await, send and status; capture is where 306 `test/agent.sh` drives run, await, send and status; capture is where
307 `--diff-since` would land, so the test and the flag are owed together. 307 `--diff-since` would land, so the test and the flag are owed together.
308 *(Closed in M18: the multi-session e2e block drives `capture --session` 308 *(Closed in M18: the multi-session e2e block drives `capture --session`
@@ -323,16 +323,16 @@ frame tagging, no QUIC stream surgery, the transport still one bidi
323 stream per connection. Same-host tiles are separate connections to the 323 stream per connection. Same-host tiles are separate connections to the
324 same port. Attach-or-create is tmux-style; a session ends when its shell 324 same port. Attach-or-create is tmux-style; a session ends when its shell
325 exits and the daemon exits with the last one. `status_req`, `await_req` 325 exits and the daemon exits with the last one. `status_req`, `await_req`
326 and `debug_dump` grew the same tail, which is what lets `muxa` and `muxd 326 and `debug_dump` grew the same tail, which is what lets `mux a` and `mux d
327 dump` address one session of several without attaching to it. 327 dump` address one session of several without attaching to it.
328 328
329 **Field-verified 2026-08-15 on both real boxes.** They had been on 329 **Field-verified 2026-08-15 on both real boxes.** They had been on
330 **0.0.1-3** — behind the agent surface as well as M18 — and a site power 330 **0.0.1-3** — behind the agent surface as well as M18 — and a site power
331 cut had taken them down, which made the upgrade window a clean one: no 331 cut had taken them down, which made the upgrade window a clean one: no
332 daemon running on either, so nothing to tear down and no session to lose. 332 daemon running on either, so nothing to tear down and no session to lose.
333 Static musl `muxd` (stripped, 23MB → 8.6MB) installed to 333 Static musl `mux` (stripped, 23MB → 8.6MB) installed to
334 `/usr/local/bin`, identical sha256 on both boxes and on the build host, 334 `/usr/local/bin`, identical sha256 on both boxes and on the build host,
335 then `muxd start --quic` on each. On the LAN box over real QUIC, and on 335 then `mux d start --quic` on each. On the LAN box over real QUIC, and on
336 the WAN box over QUIC through the LAN-side UDP forward (the gate carries 336 the WAN box over QUIC through the LAN-side UDP forward (the gate carries
337 TCP only): two dials, `--session a` and `--session b`, produced 337 TCP only): two dials, `--session a` and `--session b`, produced
338 `sessions=3 session 0 / session a / session b` on **both** boxes, with 338 `sessions=3 session 0 / session a / session b` on **both** boxes, with
@@ -340,9 +340,9 @@ each session holding its own marker and neither holding its neighbour's,
340 and the default session holding neither. The milestone's central claim, 340 and the default session holding neither. The milestone's central claim,
341 on real hardware over a real link. 341 on real hardware over a real link.
342 342
343 Two deployment notes worth keeping. `muxd --version` still prints 343 Two deployment notes worth keeping. `mux d --version` still prints
344 **0.0.1-5** after the upgrade — M18 is untagged, so the version string 344 **0.0.1-5** after the upgrade — M18 is untagged, so the version string
345 cannot tell you whether a box has it; `muxd stats | grep sessions=` is 345 cannot tell you whether a box has it; `mux d stats | grep sessions=` is
346 the check that can, which is the same discriminator the banked 346 the check that can, which is the same discriminator the banked
347 capability probe uses. And the trial PSK survived the power cycle 347 capability probe uses. And the trial PSK survived the power cycle
348 identical on both boxes (`ba046148…`), so the M17 key-drift failure did 348 identical on both boxes (`ba046148…`), so the M17 key-drift failure did
@@ -354,17 +354,17 @@ shell over QUIC as over the unix socket, and two QUIC dials to one port
354 are two shells. Pinned end to end by the QUIC block: two dials into two 354 are two shells. Pinned end to end by the QUIC block: two dials into two
355 sessions, then a socket client joins one of them and both clients' 355 sessions, then a socket client joins one of them and both clients'
356 markers land on one grid with the session count unmoved. That also makes 356 markers land on one grid with the session count unmoved. That also makes
357 a mixed-transport wall of a single host work — `muxweb HOST#a 357 a mixed-transport wall of a single host work — `mux web HOST#a
358 quic://HOST:8787#b` is tile 0 over ssh and tile 1 over QUIC against one 358 quic://HOST:8787#b` is tile 0 over ssh and tile 1 over QUIC against one
359 daemon. (`muxd run --sock PATH --quic ADDR` binds both; ssh needs no 359 daemon. (`mux d run --sock PATH --quic ADDR` binds both; ssh needs no
360 listener of its own, since `muxd proxy` pumps into the same socket.) 360 listener of its own, since `mux d proxy` pumps into the same socket.)
361 361
362 **The compat story held**, measured against main @ 5d41e10 (the branch 362 **The compat story held**, measured against main @ 5d41e10 (the branch
363 point) in both directions. An empty name is the default session and 363 point) in both directions. An empty name is the default session and
364 encodes to byte-identical pre-M18 payloads: an M18 client drove a 364 encodes to byte-identical pre-M18 payloads: an M18 client drove a
365 pre-M18 daemon's default session; a pre-M18 client drove an M18 365 pre-M18 daemon's default session; a pre-M18 client drove an M18
366 daemon's and created nothing (`sessions=1`, `session 0`); and a pre-M18 366 daemon's and created nothing (`sessions=1`, `session 0`); and a pre-M18
367 `muxa`'s empty `status_req` still answered. The known limitation is 367 `mux a`'s empty `status_req` still answered. The known limitation is
368 documented rather than fixed, and turns out to be **two** failures 368 documented rather than fixed, and turns out to be **two** failures
369 split by transport — socket/ssh get a clean exit 1 (the old 369 split by transport — socket/ssh get a clean exit 1 (the old
370 `serviceObserver` drops the connection), **QUIC hangs** (the old 370 `serviceObserver` drops the connection), **QUIC hangs** (the old
@@ -380,7 +380,7 @@ per predecessor, riding through exec into a shell that had no idea it
380 held it. A master with a second holder never sees its last close, so 380 held it. A master with a second holder never sees its last close, so
381 `Pty.deinit`'s close stopped hanging up, the interactive shell ignored 381 `Pty.deinit`'s close stopped hanging up, the interactive shell ignored
382 the SIGTERM that followed, and the blocking `waitpid` never returned: 382 the SIGTERM that followed, and the blocking `waitpid` never returned:
383 `muxd stop` unlinked the socket, printed `muxd: stopped`, and left the 383 `mux d stop` unlinked the socket, printed `mux d: stopped`, and left the
384 daemon in `do_wait` forever with every session's shell alive. Strictly 384 daemon in `do_wait` forever with every session's shell alive. Strictly
385 unreachable at one session per daemon. The pin closes a master while a 385 unreachable at one session per daemon. The pin closes a master while a
386 later session is live and polls for the hangup with a deadline — the bug 386 later session is live and polls for the hangup with a deadline — the bug
@@ -391,7 +391,7 @@ is a hang, and a hung `zig build test` step prints nothing at all.
391 and the reverse, each daemon in a container of its own, both sides built 391 and the reverse, each daemon in a container of its own, both sides built
392 static musl so one artifact runs in the container and on the host. Eight 392 static musl so one artifact runs in the container and on the host. Eight
393 legs: both directions on the default session, both halves of the 393 legs: both directions on the default session, both halves of the
394 `--session` limitation, an old `muxa`'s empty `status_req`, the 394 `--session` limitation, an old `mux a`'s empty `status_req`, the
395 `stats_req` version discriminator the banked mitigation would need, and a 395 `stats_req` version discriminator the banked mitigation would need, and a
396 teardown observed rather than claimed. It exists because M18's by-hand 396 teardown observed rather than claimed. It exists because M18's by-hand
397 run found a *documentation* error days after the sentence was written; a 397 run found a *documentation* error days after the sentence was written; a
@@ -399,7 +399,7 @@ gate finds that on the next run.
399 399
400 Two things about it stated rather than left implied. **Containers share 400 Two things about it stated rather than left implied. **Containers share
401 the host kernel**, so nothing there validates pty, poll or fd semantics 401 the host kernel**, so nothing there validates pty, poll or fd semantics
402 against a different one — most of what muxd is. That needs VMs and is not 402 against a different one — most of what the daemon is. That needs VMs and is not
403 claimed. And the rig is **falsifiable on purpose**: point `XVER_OLD_BIN` 403 claimed. And the rig is **falsifiable on purpose**: point `XVER_OLD_BIN`
404 at the new binaries and legs must fail. Doing exactly that caught a false 404 at the new binaries and legs must fail. Doing exactly that caught a false
405 pass in the gate's own first draft — the QUIC arm keyed on the client's 405 pass in the gate's own first draft — the QUIC arm keyed on the client's
@@ -408,7 +408,7 @@ attached until `timeout` kills it, so 124 meant both things. It now asks
408 the daemon whether a session called `zz` appeared, which only a daemon 408 the daemon whether a session called `zz` appeared, which only a daemon
409 that understood the name can produce. 409 that understood the name can produce.
410 410
411 `muxa capture` is **no longer the one verb with no automated coverage**: 411 `mux a capture` is **no longer the one verb with no automated coverage**:
412 the M18 block drives it by name and asserts it returns one session's 412 the M18 block drives it by name and asserts it returns one session's
413 grid and not its neighbour's. `--diff-since` is still owed. 413 grid and not its neighbour's. `--diff-since` is still owed.
414 414
@@ -437,9 +437,9 @@ a seam up there would have recorded the session you started at and none of
437 the ones you visited. It fires on the first STATE, not on a successful dial: 437 the ones you visited. It fires on the first STATE, not on a successful dial:
438 a dial that comes up can still be refused, and a refusal that had already 438 a dial that comes up can still be refused, and a refusal that had already
439 written its line would strand a tile naming a session that never existed. 439 written its line would strand a tile naming a session that never existed.
440 That is why nothing ever needs to un-record. `muxa` and `--via` record 440 That is why nothing ever needs to un-record. `mux a` and `--via` record
441 nothing; `mux wall`'s argv is a view, not an attach; `muxweb`'s argv ADDS 441 nothing; `mux wall`'s argv is a view, not an attach; `mux web`'s argv ADDS
442 rather than replaces, because the file stopped being "the last wall muxweb 442 rather than replaces, because the file stopped being "the last wall the hub
443 was told to show" the moment attaches started writing to it. Forgetting 443 was told to show" the moment attaches started writing to it. Forgetting
444 stays explicit: the page's `×`, the wall's `x`, `mux wall rm`. 444 stays explicit: the page's `×`, the wall's `x`, `mux wall rm`.
445 445
@@ -468,7 +468,7 @@ Shipped 2026-08-16. The three discovery issues are now accounted for:
468 `7c777ec6`'s OSC 52 **set** reaches the host clipboard (the query direction 468 `7c777ec6`'s OSC 52 **set** reaches the host clipboard (the query direction
469 is deliberately refused, with a 64 KiB cap); `ee062dd9`'s bracketed-paste 469 is deliberately refused, with a 64 KiB cap); `ee062dd9`'s bracketed-paste
470 half is mirrored; and `063cec67` now has implementation evidence for its 470 half is mirrored; and `063cec67` now has implementation evidence for its
471 **muxweb** half: mouse-drag selection reaches daemon scrollback and explicit 471 **hub** half: mouse-drag selection reaches daemon scrollback and explicit
472 copy. Native CLI keyboard copy mode remains a separate problem. Window-title 472 copy. Native CLI keyboard copy mode remains a separate problem. Window-title
473 state and bells rode the same delivery mechanisms. 473 state and bells rode the same delivery mechanisms.
474 474
@@ -537,7 +537,7 @@ next one that arrives outranks all of it.
537 537
538 The evidence that this costs something is that **the tests cannot read 538 The evidence that this costs something is that **the tests cannot read
539 it either**. `test/e2e.sh` has to disambiguate through a side channel 539 it either**. `test/e2e.sh` has to disambiguate through a side channel
540 (`muxd stats | grep sessions=`), and `test/xversion.sh` had to invent 540 (`mux d stats | grep sessions=`), and `test/xversion.sh` had to invent
541 `assert_name_not_honoured` to ask "did a session appear?" because the 541 `assert_name_not_honoured` to ask "did a session appear?" because the
542 reply itself could not say. When a protocol's own suite has to go 542 reply itself could not say. When a protocol's own suite has to go
543 around the answer to find out what happened, an operator staring at a 543 around the answer to find out what happened, an operator staring at a
@@ -586,7 +586,7 @@ next one that arrives outranks all of it.
586 layer *and* end to end, and the unit pair failed first and cheapest. 586 layer *and* end to end, and the unit pair failed first and cheapest.
587 - **Log rotation, via `logPathFor`** — created by M13 and its named, 587 - **Log rotation, via `logPathFor`** — created by M13 and its named,
588 accepted cost. Auto-start opens the daemon log append-only, so a user 588 accepted cost. Auto-start opens the daemon log append-only, so a user
589 who never types `muxd start` never truncates it and the file grows 589 who never types `mux d start` never truncates it and the file grows
590 without bound. The seam is already in place; only the policy is owed 590 without bound. The seam is already in place; only the policy is owed
591 (decisions.md, M13). 591 (decisions.md, M13).
592 - **Per-socket daemon logs** — also from M13. Auto-start makes a second 592 - **Per-socket daemon logs** — also from M13. Auto-start makes a second
@@ -628,7 +628,7 @@ next one that arrives outranks all of it.
628 performance win's clothes (decisions.md, "Banked by M8"). 628 performance win's clothes (decisions.md, "Banked by M8").
629 2. **Half-open reaping policy** — handshake-without-attach holds a slot 629 2. **Half-open reaping policy** — handshake-without-attach holds a slot
630 until idle timeout; bounded, measured at 0 held under a ten-tear loop; 630 until idle timeout; bounded, measured at 0 held under a ten-tear loop;
631 `muxd stats clients=N` is the instrument for deciding the policy. 631 `mux d stats clients=N` is the instrument for deciding the policy.
632 3. **`idle_ms` split three ways** — death detection, handshake bound, 632 3. **`idle_ms` split three ways** — death detection, handshake bound,
633 half-open reaping are one knob doing three jobs. 633 half-open reaping are one knob doing three jobs.
634 4. **`timeoutMs` buffered-frame short-circuit** — a whole frame already 634 4. **`timeoutMs` buffered-frame short-circuit** — a whole frame already