2843fee1
docs: one binary, the exec that starts it, and the break it costs
a73x 2026-08-28 22:34
Commit message
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 |