a73x

0a0fefea

docs: the wall lists daemons, and the tiles are their live sessions

a73x   2026-08-28 19:53

Commit message
docs: the wall lists daemons, and the tiles are their live sessions

README's "the wall is your attach history" becomes "the wall is your
hosts": what a line means, that nothing is resurrected, the `[unreachable]`
stripe, `mux hosts add/rm`, and the two chords that changed meaning — `x`
ends a session now, `:` adds a machine. Every claim run against the
binaries first, on an isolated XDG_STATE_HOME and XDG_RUNTIME_DIR.

Two claims found false while checking and fixed with the rest: `mux
--help` still called `Ctrl-\ :` a tile, and the wall section still said
unfocused tiles watch at no size — they have claimed their own rect since
multipane.

CLAUDE.md's two wall invariants are rewritten to the new model, and the
new `end_req`/`end_reply` two-step gets one of its own. decisions.md
carries the rulings that shaped it, the snapshot measurement with the two
candidate sites named, and the cross-version cost: a daemon that predates
this answers no observer `sessions_req`, so the wall stripes it
`[unreachable]` while `mux --sock` still attaches.

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

CLAUDE.md
Old New
@@ -26,8 +26,9 @@ costs ~800k tokens; every token stays in context and is re-billed each turn.
26 - **Never `cat` these:** `src/server.zig` (3.5k lines, ~35k tok; its tests are 26 - **Never `cat` these:** `src/server.zig` (3.5k lines, ~35k tok; its tests are
27 siblings, `src/server_test_*.zig`, none over 1.6k; the agent relay and the 27 siblings, `src/server_test_*.zig`, none over 1.6k; the agent relay and the
28 session table are `src/server_agent.zig` and `src/server_sessions.zig`), 28 session table are `src/server_agent.zig` and `src/server_sessions.zig`),
29 `test/e2e.sh` (9.7k), `docs/decisions.md` (7.0k), `src/wallview.zig` (5.5k), 29 `src/wallview.zig` (6.6k), `src/interact.zig` (4.5k),
30 `src/interact.zig` (4.5k). 30 `docs/decisions.md` (7.1k). `test/e2e.sh` is a 189-line runner now; the
31 bodies are `test/e2e_NN_*.sh` (`E2E_ONLY=<group>` runs one).
31 Use `grep -n` for the symbol, then `sed -n 'A,Bp'` for a window. 32 Use `grep -n` for the symbol, then `sed -n 'A,Bp'` for a window.
32 `src/client.zig` is 1.9k now — cheap enough to read. 33 `src/client.zig` is 1.9k now — cheap enough to read.
33 - Every module has a `//!` header stating its contract. `head -12 src/X.zig` 34 - Every module has a `//!` header stating its contract. `head -12 src/X.zig`
@@ -42,9 +43,9 @@ Layers are enforced in `build.zig`'s module table (grep `.layer =` for the graph
42 43
43 | Layer | Modules | 44 | Layer | Modules |
44 |---|---| 45 |---|---|
45 | 0 | `protocol` `engine` `pty` `quic` `keymap` `xdg` `sockpath` `proxy` `testtmp` | 46 | 0 | `protocol` `engine` `pty` `quic` `keymap` `xdg` `sockpath` `proxy` `cliflags` `testtmp` |
46 | 1 | `client_core` `quic_server` `quic_client` `predict` `spawn` `handoff` `delta` `cmd` `wall` `shellint` `replica` `paint` `layout` | 47 | 1 | `client_core` `quic_server` `quic_client` `predict` `spawn` `handoff` `delta` `cmd` `wall` `shellint` `replica` `paint` `layout` `select` `upgrade` |
47 | 2 | `server` `muxa` `interact` | 48 | 2 | `server` `muxa` `interact` `hosts` |
48 | 3 | `client` `main`(muxd) | 49 | 3 | `client` `main`(muxd) |
49 | 4 | `webhub` `wallview` | 50 | 4 | `webhub` `wallview` |
50 | 5 | `mux_main` `webhub_main`(muxweb) | 51 | 5 | `mux_main` `webhub_main`(muxweb) |
@@ -67,11 +68,12 @@ real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`.
67 the rect, relayout resends it. Focus is client-local and sends nothing 68 the rect, relayout resends it. Focus is client-local and sends nothing
68 on the wire: the keyboard writes the outgoing tile's `session_release` 69 on the wire: the keyboard writes the outgoing tile's `session_release`
69 under `paint_mu`, then doorbells the old pump (release) and the new 70 under `paint_mu`, then doorbells the old pump (release) and the new
70 (claim). `mux [TARGET]` is a wall of one tile whose rect is the whole 71 (claim). ONE interaction loop, a tile pump: `mux TARGET` is the wall
71 terminal — ONE interaction loop, a tile pump. A chord-born tile 72 zoomed on the tile it just attached to, and a wall of one tile whose rect
72 inherits the focused tile's target; `Ctrl-\ :` is the one chord that 73 is the whole terminal only when there is no terminal at all. A chord-born
73 takes a spelling, born with argv's row of the birth table (creates, 74 tile inherits the focused tile's target and its `-A`; `Ctrl-\ :` takes a
74 records, no `-A`) through the same `birthTile`. 75 spelling but adds a HOST, not a tile — its sessions arrive on that
76 host's first poll like any other host's.
75 - **Resize is gain-only.** `layout.Tree.resize` never shrinks a pane; the 77 - **Resize is gain-only.** `layout.Tree.resize` never shrinks a pane; the
76 shrink keys grow a neighbor at the focus's expense in `wallview.doResize`. 78 shrink keys grow a neighbor at the focus's expense in `wallview.doResize`.
77 - **Rails are painted from `relayout` and tiles cannot reach them.** Every 79 - **Rails are painted from `relayout` and tiles cannot reach them.** Every
@@ -89,30 +91,40 @@ real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`.
89 agent, though anything they run inside a session uses that session's 91 agent, though anything they run inside a session uses that session's
90 socket like any other process. Frames, never transport; both ends cap 92 socket like any other process. Frames, never transport; both ends cap
91 a frame at `agent_data_max`. 93 a frame at `agent_data_max`.
92 - **The wall file is attach history, written on first state.** 94 - **The wall file lists DAEMONS; tiles are their live sessions.**
93 `client.recordOnState` fires when the daemon's first state arrives and never 95 `$XDG_STATE_HOME/mux/hosts`, one spelling per line (`--sock PATH` |
94 on a successful dial, so a refused attach cannot strand a line — which is why 96 `HOST` | `quic://HOST[:PORT]`); `#SESSION` is refused, because a host
95 there is no `unrecordTile`. `--via` and `muxa` record nothing; `mux wall`'s 97 line must name nothing that could be resurrected. `hosts.zig` owns it.
96 argv is a view, not an attach. A RESTORED line attaches-or-creates when it 98 Tiles come from each host's own `sessions_req` on a fresh side
97 names the local socket and joins only when it is remote 99 connection, polled once a second — never from the file — so a birth by
98 (`client.hydratedCreates`, one owner for the CLI and the hub): a daemon 100 anyone appears and an exit disappears, and `n`/`p`/digits walk the one
99 restart must not leave the user's own wall a grid of `[refused]`, and a 101 list. A host contributes ONE stripe only when it has no tiles; tiles ride
100 saved file must not spawn a shell on another host. The hub reaches the 102 out a blip rather than vanishing. `--via` and `muxa` record no host.
101 same rule from the other side — it cannot claim a size, so it births the 103 Nothing re-creates a session: a daemon restart heals to what that daemon
102 session on a side connection when an attach is refused before any grid. 104 now has, which for a fresh `muxd` is session `0`.
103 "A session the user ENDED stays ended" needs BOTH halves: the hub's 105 `hosts.load` is strict — a bad line refuses `mux` with rc 2 and prints
104 `ended` latch, and `sendAttach` declining to attach an `exited` tile at 106 it — while `mux hosts` lists and `mux hosts rm` edits VERBATIM, so the
105 all. A zoomed tile's attach carries a size, and a sized attach creates — 107 repair tool can read what the loader will not. The hub is phase 2: it
106 so the page can resurrect a shell without the hub involved. 108 still reads the old `wall` file (`client.hydratedCreates`, the `ended`
109 latch), which nothing on the CLI writes any more.
110 - **`Ctrl-\ x` ends a session; the daemon owns the two-step.** `end_req`
111 `0x11` / `end_reply` `0x94`, observer verbs. The daemon refuses a first
112 `x` on a session other clients hold and answers the count; the client
113 arms 3 s for the forcing second press. An accepted end is BOUNDED —
114 SIGKILL past `Pty.term_grace_ms` — so no shell can refuse to die, and
115 `muxd upgrade` is refused while any session is ending rather than carry
116 a masterless pty across the exec. A daemon with no arm answers nothing
117 and the client banners `[daemon too old to end a session]`. The tile
118 leaves on the next list, not on the keypress.
107 - **The layout sidecar is derived convenience, not authored intent.** 119 - **The layout sidecar is derived convenience, not authored intent.**
108 `$XDG_STATE_HOME/mux/layout` stores the pane tree on last detach, hydrated 120 `$XDG_STATE_HOME/mux/layout` stores the pane tree on last detach, from
109 walls only (no-argv `mux wall` or post-fold; argv walls and `mux TARGET` 121 every wall left on a TERMINAL (`shared.is_tty`) — a piped `mux` is a wall
110 never write). Restore is verbatim; the aspect rule applies only when no 122 of one and neither saves nor restores. Restore is verbatim; the aspect
111 saved tree exists. Healing is per-leaf against wall drift. `wall.load` 123 rule applies only when no saved tree exists. Healing is per-leaf against
112 stays strict (a wall line is authored intent) while the sidecar is 124 the hosts' live lists. `hosts.load` stays strict (a host line is authored
113 lenient — every failure degrades silently to the default cut. The 125 intent) while the sidecar is lenient — every failure degrades silently to
114 asymmetry is deliberate: a bad wall line is a user's mistake to fix; a 126 the default cut. The asymmetry is deliberate: a bad host line is a user's
115 stale layout is a convenience to forget. 127 mistake to fix; a stale layout is a convenience to forget.
116 - **Detach is a goodbye, not a tear.** A pump that has sent its detach frame 128 - **Detach is a goodbye, not a tear.** A pump that has sent its detach frame
117 never redials — redial checks `detach_ack` — so the daemon's close after 129 never redials — redial checks `detach_ack` — so the daemon's close after
118 a detach is final, not a network event to recover from. 130 a detach is final, not a network event to recover from.
@@ -157,11 +169,14 @@ real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`.
157 stamps `Patch:` trailers. Each collab write costs a ~15s origin sync — batch. 169 stamps `Patch:` trailers. Each collab write costs a ~15s origin sync — batch.
158 - `make check` before commit, `make ci` before delivery; capture `$?` before 170 - `make check` before commit, `make ci` before delivery; capture `$?` before
159 piping (`make test | tail` reports tail's exit code, not the build's). 171 piping (`make test | tail` reports tail's exit code, not the build's).
160 - **Any hand-run rig exports an isolated `XDG_STATE_HOME`.** Every attach 172 - **Any hand-run rig exports an isolated `XDG_STATE_HOME`.** Every named
161 writes a tile into `$XDG_STATE_HOME/mux/wall`; the `make` scripts all 173 attach writes a host into `$XDG_STATE_HOME/mux/hosts`; the `make` scripts
162 isolate it, but an ad-hoc repro or perf rig with the real `HOME` leaves a 174 all isolate it, but an ad-hoc repro or perf rig with the real `HOME` leaves
163 dead `--sock /tmp/...` stripe on the user's wall per attach (49 of them 175 a dead `--sock /tmp/...` line on the user's wall per throwaway socket (49
164 found 2026-08-22). `export XDG_STATE_HOME=$SCRATCH/state` first, always. 176 of them found 2026-08-22, under the old per-attach file). `export
177 XDG_STATE_HOME=$SCRATCH/state` first, always — and a rig that dials a
178 default socket needs an isolated `XDG_RUNTIME_DIR` too, or a bare `mux`
179 auto-starts against the user's live daemon.
165 - **Never quote a speed number measured in the dev tree.** `zig build` is 180 - **Never quote a speed number measured in the dev tree.** `zig build` is
166 Debug, and Debug builds ghostty with its page-integrity check on — a hash 181 Debug, and Debug builds ghostty with its page-integrity check on — a hash
167 map over every cell per mutation, ~600x on output-heavy work. `make test`, 182 map over every cell per mutation, ~600x on output-heavy work. `make test`,
README.md
Old New
@@ -42,49 +42,77 @@ muxd run & # the daemon: hosts your shell and the authoritative screen
42 mux # attach 42 mux # attach
43 ``` 43 ```
44 44
45 **`mux` IS the wall.** There is one navigation axis and one program behind 45 **`mux` IS the wall, and the wall is a list of daemons.** The file
46 it: every tile is a live session on its own rectangle, and one tile is 46 `~/.local/state/mux/hosts` names machines — your own and every box you have
47 focused — it receives your keystrokes. `mux` and `mux HOST` open on a wall 47 attached to — one spelling per line, and the tiles are the sessions those
48 of one tile whose rect is the whole terminal; `mux wall` opens on the saved 48 daemons have live *right now*. There is one list and one navigation axis:
49 wall. `Ctrl-\` `1`-`9` focuses a tile, `Ctrl-\` `d` leaves — no child 49 `Ctrl-\` `1`-`9` focuses a tile, `Ctrl-\` `n`/`p` walk them all, `Ctrl-\`
50 process, no second screen, and every tile's replica stays hot, so moving 50 `d` leaves. Every tile's replica stays hot whether you are looking at it or
51 focus is a repaint and not a re-attach. Bare `mux` still feels like a plain 51 not, so moving focus is a repaint and not a re-attach — no child process,
52 client, because a wall of one tile is what a plain client always was. 52 no second screen.
53
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
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
58 behind it, and nothing in the file can bring one back.
59
60 `mux` on a machine that has never run it records your own daemon (the line
61 `--sock <default>`, starting it if nothing is listening) and opens zoomed on
62 its session `0` — a first run is still just a shell. After that, bare `mux`
63 opens on the wall itself, and `mux HOST` opens zoomed on HOST's session `0`,
64 adding HOST to the file if it is new, with the rest of the list on the wall
65 behind it. Every host dials at once, in the background, so a slow ssh
66 somewhere else never holds up the one you asked for; `Ctrl-\` `w` zooms back
67 out to see them. A wall of one tile still feels like a plain client, because
68 a wall of one tile is what a plain client always was.
53 69
54 A split births a session: not a window onto an existing one, but its own 70 A split births a session: not a window onto an existing one, but its own
55 shell, laid out beside or below the focus. The wall places tiles side by 71 shell on the focused tile's daemon, laid out beside or below the focus. The
56 side when the terminal is at least twice as wide as it is tall, and stacks 72 wall places tiles side by side when the terminal is at least twice as wide
57 them otherwise. Fullscreen is a layout where the focused pane takes the 73 as it is tall, and stacks them otherwise. Fullscreen is a layout where the
58 whole terminal; resize mode trades cells between the focus and its 74 focused pane takes the whole terminal; resize mode trades cells between the
59 neighbors, one per keystroke. The layout survives detach and comes back 75 focus and its neighbors, one per keystroke. The layout survives detach and
60 the next time the wall does — `mux wall`, or `Ctrl-\ w` from a plain `mux`. 76 comes back the next time you open a wall on a terminal.
61 77
62 The focused tile has the terminal, and only the prefix is held back: 78 The focused tile has the terminal, and only the prefix is held back:
63 79
64 | Key | Effect | 80 | Key | Effect |
65 |---|---| 81 |---|---|
66 | `Ctrl-\` | command prefix — press it, then a command key | 82 | `Ctrl-\` | command prefix — press it, then a command key |
67 | `Ctrl-\` `d` (or `Ctrl-\` `Ctrl-\`) | detach and leave mux (session keeps running) | 83 | `Ctrl-\` `d` (or `Ctrl-\` `Ctrl-\`) | disconnect: leave every tile and leave mux, interrupt nobody (every session keeps running) |
68 | `Ctrl-\` `c` | create a session, add a tile, focus it | 84 | `Ctrl-\` `c` | create a session on the focused tile's daemon, add a tile, focus it |
69 | `Ctrl-\` `n` / `Ctrl-\` `p` | focus the next / previous session of this daemon, wrapping | 85 | `Ctrl-\` `n` / `Ctrl-\` `p` | focus the next / previous tile on the wall, wrapping |
70 | `Ctrl-\` `h` `j` `k` `l` | focus the pane left / down / up / right | 86 | `Ctrl-\` `h` `j` `k` `l` | focus the pane left / down / up / right |
71 | `Ctrl-\` `\|` (or `\`) | split right: new session in a pane to the right | 87 | `Ctrl-\` `\|` (or `\`) | split right: new session in a pane to the right |
72 | `Ctrl-\` `-` | split below: new session in a pane below | 88 | `Ctrl-\` `-` | split below: new session in a pane below |
73 | `Ctrl-\` `f` | fullscreen the focused pane (toggle) | 89 | `Ctrl-\` `f` | fullscreen the focused pane (toggle) |
74 | `Ctrl-\` `r` | resize mode: `h`/`l` shrink/grow the focus's width, `k`/`j` its height, one cell per press; Esc leaves silently, any other key leaves and types | 90 | `Ctrl-\` `r` | resize mode: `h`/`l` shrink/grow the focus's width, `k`/`j` its height, one cell per press; Esc leaves silently, any other key leaves and types |
75 | `Ctrl-\` `1`-`9` | focus tile N — the digit on that tile's label bar | 91 | `Ctrl-\` `1`-`9` | focus tile N — the digit on that tile's label bar |
76 | `Ctrl-\` `x` | forget the focused tile (off the wall file; the session keeps running) | 92 | `Ctrl-\` `x` | end the focused session — the daemon hangs up its shell and every attached client sees the exit. With other clients on it the daemon refuses the first press and says how many; a second `x` within 3 seconds forces it |
77 | `Ctrl-\` `:` | add a tile by spelling: type `HOST#SESSION`, `quic://HOST#SESSION` or `--sock PATH#SESSION`, Enter adds it beside the focus (creating the session if needed) and records it; Esc or Ctrl-C cancels. The line editor takes printable ASCII only, so a path or hostname with any other byte in it goes in the wall file or on argv instead | 93 | `Ctrl-\` `w` | zoom out to the wall |
94 | `Ctrl-\` `:` | add a daemon by spelling: type `HOST`, `quic://HOST[:PORT]` or `--sock PATH`, Enter puts it on the wall and its sessions arrive as tiles; Esc or Ctrl-C cancels. The line editor takes printable ASCII only, so a path or hostname with any other byte in it goes in the hosts file or on argv instead |
78 | `Shift+PageUp` / `Shift+PageDown` | scrollback, a screen at a time (any other key returns to live) | 95 | `Shift+PageUp` / `Shift+PageDown` | scrollback, a screen at a time (any other key returns to live) |
79 | mouse wheel | scrollback, three rows a notch (arrow keys to a full-screen app) | 96 | mouse wheel | scrollback, three rows a notch (arrow keys to a full-screen app) |
80 | drag with the left button | select what it crosses, copy on release | 97 | drag with the left button | select what it crosses, copy on release |
81 | any key not behind `Ctrl-\` | goes to the focused tile | 98 | any key not behind `Ctrl-\` | goes to the focused tile |
82 99
83 Behind the `Ctrl-\` prefix, `n`, `p` and `c` are about SESSIONS, not tiles: 100 `n`, `p` and the digits walk the same list, because there is only one:
84 they walk the daemon's own session ring. A session that already has a tile 101 the wall's tiles, across every daemon on it, in host order and then in each
85 is an instant focus move; one that does not gets a tile, because visiting it 102 daemon's own slot order. `c`, `|` and `-` ask the FOCUSED tile's daemon for
86 is attaching to it and every attach is recorded (see "the wall is your 103 a new session, so a split on a remote tile births a shell on that machine
87 attach history" below). The digits are what walk tiles. 104 and not on yours.
105
106 `x` is the other half of `c`, and it is not a wall edit: it ends the
107 session. The tile leaves when the daemon's list no longer has it, not when
108 the key is pressed, so what you see is always what the daemons have.
109 Because ending interrupts anyone else watching, the daemon refuses a first
110 `x` on a session with other clients attached and says how many — the rail
111 reads `[1 other attached - x again to end]` — and a second `x` within three
112 seconds forces it. A shell that ignores the hangup is killed half a second
113 later; there is no session that cannot be ended. Dropping a whole MACHINE
114 is `mux hosts rm`, not a chord: it is rarer than ending a shell and must not
115 sit one key from `n`.
88 116
89 A session whose shell exits ends `mux` with that shell's exit code when it 117 A session whose shell exits ends `mux` with that shell's exit code when it
90 was the only tile on the wall — so `mux` stays something you can put in a 118 was the only tile on the wall — so `mux` stays something you can put in a
@@ -118,15 +146,17 @@ does worse.
118 146
119 One daemon hosts up to thirty-two named sessions; the first is called `0`. 147 One daemon hosts up to thirty-two named sessions; the first is called `0`.
120 `Ctrl-\` `c` creates one named after the lowest free number and focuses it, 148 `Ctrl-\` `c` creates one named after the lowest free number and focuses it,
121 `Ctrl-\` `n` and `Ctrl-\` `p` step through them in the daemon's order and 149 and `mux --session NAME` attaches to any of them by name (creating it if
122 wrap at the ends, and `mux --session NAME` attaches to any of them by name 150 there is room). One wall holds up to thirty-two tiles however many daemons
123 (creating it if there is room). With only one session open `n` and `p` do 151 they came from; past that the rail says `[+N not shown]` and the digits do
124 nothing — there is nowhere to step to. 152 not reach them. Not worth designing past until someone has thirty-three
125 153 shells.
126 To see which sessions are alive without attaching to one, ask the daemon: 154
127 `muxd stats` names every live session with the number of clients watching it. 155 To see which sessions are alive without attaching to one: `mux hosts` prints
128 That is the question to ask when `mux` will not let you in — thirty-two 156 every daemon on the wall with its live session count, and `muxd stats` names
129 sessions is the table, and the thirty-third name has nowhere to go. 157 every session of one daemon with the number of clients watching it. `stats`
158 is the question to ask when `mux` will not let you in — thirty-two sessions
159 is the table, and the thirty-third name has nowhere to go.
130 160
131 Every session shell is told where it lives: `$MUX_SOCK` is the daemon's 161 Every session shell is told where it lives: `$MUX_SOCK` is the daemon's
132 socket path and `$MUX_SESSION` is the session's name. Scripts and prompts 162 socket path and `$MUX_SESSION` is the session's name. Scripts and prompts
@@ -134,9 +164,10 @@ can read them, and `mux` reads them to refuse attaching to the very session
134 it is running in — that attach would feed the session's own paint back into 164 it is running in — that attach would feed the session's own paint back into
135 itself, take the alternate screen and swallow every keystroke, with no 165 itself, take the alternate screen and swallow every keystroke, with no
136 outer keyboard able to reach it. Attaching from one session to *another* on 166 outer keyboard able to reach it. Attaching from one session to *another* on
137 the same daemon is fine and unaffected; so is `mux wall`, unless one of its 167 the same daemon is fine and unaffected, and a wall opened from inside a
138 tiles is the session it was launched from. Unset `MUX_SESSION` if you 168 session simply never births that one tile — the rest of the daemon's
139 really want the loop. 169 sessions are there, the one you are standing in is not. Unset `MUX_SESSION`
170 if you really want the loop.
140 171
141 ## Quick start, remote over ssh 172 ## Quick start, remote over ssh
142 173
@@ -216,12 +247,15 @@ with nobody offering, the daemon hangs up and the nested `-A` is refused.
216 An agent that is merely slow still passes — the refusal is immediate, so 247 An agent that is merely slow still passes — the refusal is immediate, so
217 being slow is not what separates them. 248 being slow is not what separates them.
218 249
219 `-A` goes on the attach form (`mux -A HOST`, `mux -A quic://HOST`, 250 `-A` is offered by the tile you named and by nothing else. `mux -A HOST`
220 `mux -A --sock PATH`), not on `mux wall`, which refuses a flag where a 251 (or `mux -A quic://HOST`, `mux -A --sock PATH`, or a bare `mux -A` on your
221 target belongs. Sibling tiles grown from an `-A` attach by chord (`Ctrl-\ c`, 252 own daemon) forwards on that tile, and a sibling grown from it by chord
222 `n`, `p`) inherit it — same daemon, same host, nothing newly exposed — while tiles 253 (`Ctrl-\ c`, `|`, `-`) inherits it — same daemon, same host, nothing newly
223 restored from the saved wall never do. `muxa` never forwards, under any 254 exposed. Every OTHER tile on the wall arrives from a daemon's session list
224 flag. 255 rather than from your command line, and those never offer an agent: the
256 wall is a list of machines, and a machine you have never typed the flag for
257 must not get your keys because it happens to be on it. `muxa` never
258 forwards, under any flag.
225 259
226 ## Quick start, remote over QUIC 260 ## Quick start, remote over QUIC
227 261
@@ -267,89 +301,126 @@ line, so the next bare `muxweb` comes back to the same wall. That saved
267 spelling — `--sock PATH` in one piece — is also accepted on the command 301 spelling — `--sock PATH` in one piece — is also accepted on the command
268 line, so a line from the file can be pasted back verbatim. 302 line, so a line from the file can be pasted back verbatim.
269 303
270 A daemon restart is survived here the same way it is on the CLI wall: a 304 A daemon restart is survived here, and this is where the hub still differs
271 saved line on a local socket (`--sock PATH#NAME`) comes back as a fresh 305 from the terminal: a saved line on a local socket (`--sock PATH#NAME`) comes
272 session, a remote one (`HOST#NAME`, `quic://HOST#NAME`) is joined only and 306 back as a fresh session, a remote one (`HOST#NAME`, `quic://HOST#NAME`) is
273 its tile reads `refused`. 307 joined only and its tile reads `refused`. The CLI wall re-creates nothing at
308 all, because its file names no session to re-create.
274 309
275 Localhost only, deliberately: to see it from another machine, forward it — 310 Localhost only, deliberately: to see it from another machine, forward it —
276 `ssh -L 7681:127.0.0.1:7681 HOST`. 311 `ssh -L 7681:127.0.0.1:7681 HOST`.
277 312
278 The same wall, without a browser: 313 **The hub and the CLI wall have parted company for now.** `muxweb` still
314 keeps its own list of *sessions* in `~/.local/state/mux/wall`, and nothing
315 on the command line writes that file any more — so on a fresh machine the
316 hub opens empty until you name tiles on its command line or in the page, and
317 a host you add with `mux hosts add` does not appear there. Moving the hub
318 onto the `hosts` file below is the next piece of work, not a gap you can
319 configure around.
320
321 ## The wall is your hosts
279 322
280 ```sh 323 ```sh
281 mux wall # the saved wall, as stripes in this terminal 324 mux hosts # the daemons on the wall, one per line, with live session counts
282 mux wall HOST '--sock /tmp/s.sock#b' # ...or state it (one spelling per argument) 325 mux hosts add HOST # put a daemon on the wall without opening it
283 mux wall HOST --sock /tmp/s.sock#b # ...`--sock PATH` unquoted works too, as in muxweb 326 mux hosts add --sock /tmp/s.sock
284 mux wall add HOST'#build' # put a tile on the saved wall without attaching 327 mux hosts add quic://box:4433
285 mux wall rm HOST'#build' # take one off (the session keeps running) 328 mux hosts rm HOST # take one off (its sessions keep running)
329 ```
330
331 The file is `~/.local/state/mux/hosts` (`$XDG_STATE_HOME/mux/hosts`), one
332 daemon per line, in wall order:
333
334 ```
335 --sock /run/user/1000/muxd.sock
336 box
337 quic://gate.example:4433
286 ``` 338 ```
287 339
288 `add` and `rm` are subcommands, so they claim the first argument: a tile 340 A line names a **machine**, never a session: `#NAME` on a spelling is
289 spelled literally `add` or `rm` has to reach the wall some other way (the 341 refused, because the wall shows every session a daemon has and there is
290 `Ctrl-\` chords, `muxweb`, or an editor on the file). Naming neither a 342 nothing left for a `#` to pick. That is also what makes the file safe to
291 target nor a spelling is a usage error, not an empty edit. 343 keep — there is no line in it that anything could resurrect a shell from.
292 344 Lines are deduped byte for byte, so the same box written `box` and
293 Unlike `muxweb`, `mux wall TILE...` does not add what you name to the file: 345 `quic://box` is two hosts, deliberately: they are two different ways in.
294 spellings on this command line are a view of some tiles for this run, not an 346
295 attach of yours, so they are gone when you leave. `mux wall add` is how you 347 Every `mux` that names a target records that target — `mux HOST`,
296 make one stick. 348 `mux quic://HOST`, `mux --sock PATH`, and a bare `mux`, which records your
297 349 own daemon. `--via CMD` never records one, because an arbitrary command is
298 **The wall is your attach history**, kept in `~/.local/state/mux/wall`. 350 not an address and the grammar has no line to write for it; the session is
299 Every `mux` attach writes its own tile 351 real, it is just not a machine the wall can name. `muxa` records nothing
300 to that file, so a bare `mux` leaves `--sock <default>#0` behind and 352 either (it attaches at 0×0 and claims nothing). Writing the file is best
301 `mux HOST --session build` leaves `HOST#build` — as does every session you 353 effort — if it cannot be written you get one warning line and the attach
302 reach with `Ctrl-\` `c`/`n`/`p`, because visiting a session is attaching to 354 happens anyway.
303 it. Tiles are deduped by spelling, byte for byte: the same session reached 355
304 as `HOST#build` and as `quic://HOST#build` is two tiles, deliberately. 356 Reading it is not best effort. A line the grammar refuses stops `mux` with
305 `muxa` never adds one (it attaches at 0×0 and claims nothing); `--via` never 357 exit 2 and prints the line, because a wall silently missing one of the
306 adds one either, because an arbitrary command is not an address and the wall 358 machines you wrote down is worse than no wall. `mux hosts` is the exception
307 grammar has no line to write for it — the session is real, it is just not 359 and lists everything verbatim, marking the bad one:
308 addressable as a tile, and `Ctrl-\` `w` labels it `--via CMD#NAME` on a bar 360
309 that `Ctrl-\` `x` will find nothing in the file to forget; and neither do the wall's 361 ```
310 own stripes. Writing the file is best effort — if it cannot be 362 --sock /run/user/1000/muxd.sock 3
311 written you get one warning line and the attach happens anyway. 363 box [unreachable]
312 364 #build [bad host: names a session after '#': the wall lists daemons and shows every session they have]
313 That file is what `mux wall` opens on, and what `Ctrl-\` `w` folds in 365 ```
314 one-shot — the saved wall with the tile you are standing on among them. 366
315 There is one wall, and no way to end up looking at a second one by accident. 367 — the line, a tab, then its live session count or what went wrong. That
316 368 listing dials every host, so it takes as long as the slowest one answers (up
317 A daemon restart takes every session the file names with it. A line on a 369 to two seconds each). `mux hosts rm` reads the file verbatim too, so a line
318 local socket (`--sock PATH#NAME`) comes back as a fresh session rather than 370 you hand-edited into something no longer legal can be typed back byte for
319 `[refused]`: recreating a shell on your own box is cheap, and a wall you have 371 byte and removed instead of wedging every command that touches the wall.
320 to forget line by line after every reboot is not what attach history is for. 372
321 A remote line (`HOST#NAME`, `quic://HOST#NAME`) is joined only, and a refused 373 A daemon that is not answering is **one** stripe — `--sock PATH
322 tile stays refused for as long as the wall is up: it says `[refused]` until you 374 [unreachable]` — and not one dead tile per session it used to have. It keeps
323 reopen the wall with that session already there — a saved file must not spawn 375 redialling, forever and on purpose: a wall is a thing you leave up, and a
324 shells on a machine you are not looking at. Spellings on `mux wall`'s own command line 376 laptop that wakes an hour later should find its sessions. The cost is that a
325 are a view, so they join and never create. 377 daemon that is never coming back looks exactly like one whose network is
326 378 merely down. Nothing times it out; `mux hosts rm SPELLING` is how a line
327 `mux wall add`/`rm` are the same edit without an attach: file operations 379 leaves. Your own daemon is the exception that needs no stripe: a listed
328 only, so neither dials, and `rm` never touches the session. A wall or hub 380 local `--sock <default>` that nothing is serving is started for you, because
329 that is already running does not notice the change until you reopen it. 381 it dies on every reboot while its line lives on.
330 `rm` is also the repair tool: it is the one command that reads the file 382
331 leniently, so a line you hand-edited into something no longer legal can be 383 **A daemon too old to be asked reads as `[unreachable]` too.** Listing a
332 removed (and every other line survives it byte for byte) instead of 384 daemon's live sessions is a new question on the wire, and a daemon from
333 wedging every command that touches the wall. 385 before this change answers it with the silence it answers every unknown
334 386 frame with. It is up, `mux --sock PATH` still attaches to it, and the wall
335 `muxweb TILE...` ADDS its tiles to that file rather than replacing it — 387 still shows it as a stripe that never heals — so `muxd upgrade` (or a
336 naming a wall on the command line changes what this run shows, not what 388 restart) on every box you put on the wall is part of taking this version.
337 you have recorded. Forgetting stays explicit: the page's `×`, `Ctrl-\` `x`, 389
338 `mux wall rm`. 390 Restarting a daemon re-creates **nothing**. The old wall would have brought
339 391 your sessions back from the file; this one cannot, because the file has no
340 Watching is read-only: each stripe is a live session (label bar + the rows 392 session in it to bring back. The stripe goes `[unreachable]`, then heals to
341 around its cursor), attached the way `muxa` attaches — no size claim, so 393 whatever the daemon has when it returns — which, for a daemon someone just
342 watching never resizes anyone. 394 started, is one session called `0`.
343 395
344 The keys are the ones listed under "Quick start, local" — `mux wall` and 396 `mux hosts add`/`rm` are file operations only: neither dials, and `rm` never
345 `mux` are the same program, entered through different doors, so there is 397 touches the session — the shells on that machine go on running and `mux
398 HOST` reaches them again. A wall that is already up does not notice the
399 edit; the running wall's own way to add a machine is `Ctrl-\` `:`.
400
401 There is no `mux wall` any more — the wall has no separate door to open, so
402 `mux wall` is read as a HOST called `wall` like any other bare word, and ssh
403 says what it thinks of that. `hosts` is the one name a subcommand claims:
404 `mux hosts` lists, so a machine called `hosts` is attached to as
405 `quic://hosts` or `--via ssh hosts muxd proxy`, though `mux hosts add hosts`
406 puts it on the wall by that name quite happily.
407
408 Every tile on the wall is a real attach at its own rectangle — label bar plus
409 the rows its rect gives it — not a thumbnail and not a read-only view. What
410 the unfocused ones do NOT do is claim the session: only the focused tile's
411 title, mouse modes and clipboard reach your terminal, so a shell painting
412 away in the corner cannot take the screen off you.
413
414 The keys are the ones listed under "Quick start, local" — the zoomed tile
415 and the wall are one program entered through different doors, so there is
346 one key table and not two. Every typed key goes to the focused tile; only 416 one key table and not two. Every typed key goes to the focused tile; only
347 the `Ctrl-\` prefix is held back. `Ctrl-\` `1`-`9` focuses a tile (as does 417 the `Ctrl-\` prefix is held back. `Ctrl-\` `1`-`9` focuses a tile, `Ctrl-\`
348 clicking one), `Ctrl-\` `n`/`p`/`c` walk the daemon's session ring, `Ctrl-\` 418 `n`/`p` walk them, `Ctrl-\` `c` creates a session on the focused tile's
349 `h`/`j`/`k`/`l` move focus between panes, `Ctrl-\` `|`/`-` split right or 419 daemon, `Ctrl-\` `h`/`j`/`k`/`l` move focus between panes, `Ctrl-\` `|`/`-`
350 below, `Ctrl-\` `f` fullscreen, `Ctrl-\` `r` resize mode, `Ctrl-\` `x` 420 split right or below, `Ctrl-\` `f` fullscreen, `Ctrl-\` `r` resize mode,
351 forgets the focused tile, `Ctrl-\` `d` leaves. Any other command key after 421 `Ctrl-\` `:` adds a daemon, `Ctrl-\` `x` ends the focused session, `Ctrl-\`
352 the prefix is swallowed along with it, as in a plain client. 422 `w` zooms out, `Ctrl-\` `d` leaves. Any other command key after the prefix
423 is swallowed along with it, as in a plain client.
353 424
354 Every tile's label bar opens with its chord digit, and the focused tile's 425 Every tile's label bar opens with its chord digit, and the focused tile's
355 carries a `>` beside it — `1> ` has the focus, `2 ` does not. The digit is 426 carries a `>` beside it — `1> ` has the focus, `2 ` does not. The digit is
@@ -357,9 +428,9 @@ the order the tiles joined the wall — which is what `Ctrl-\` `1`-`9` indexes
357 — not their left-to-right position and not the session name. A pane splits 428 — not their left-to-right position and not the session name. A pane splits
358 beside the focus while its digit is appended, so on a wall you have split or 429 beside the focus while its digit is appended, so on a wall you have split or
359 restored the digits need not run in screen order. Each stays with its tile 430 restored the digits need not run in screen order. Each stays with its tile
360 for the whole run: forgetting one leaves a hole in the numbering rather than 431 for the whole run: a session that ends leaves a hole in the numbering rather
361 renumbering its neighbours. Past 9 the bar still prints the number, though 432 than renumbering its neighbours. Past 9 the bar still prints the number,
362 no chord reaches it. 433 though no chord reaches it.
363 434
364 Focus is client-local: moving it sends nothing on the wire, so `Ctrl-\` `n` 435 Focus is client-local: moving it sends nothing on the wire, so `Ctrl-\` `n`
365 and `Ctrl-\` `h` move between sessions instantly — no reconnect, no 436 and `Ctrl-\` `h` move between sessions instantly — no reconnect, no
@@ -372,20 +443,19 @@ a plain client's loop. The focused tile's Core owns the mouse modes; a tile
372 that is not focused holds none, so nothing it armed can report while you 443 that is not focused holds none, so nothing it armed can report while you
373 are looking elsewhere. 444 are looking elsewhere.
374 445
375 A tile whose host is not answering says `reconnecting` and keeps retrying, 446 The pane layout is remembered — where the cuts were, which tile had the
376 forever and on purpose — a wall is a thing you leave up, and a laptop that 447 focus — under `$XDG_STATE_HOME/mux/layout`, saved by any wall you leave from
377 wakes an hour later should find its sessions. The cost is that a tile 448 a terminal and restored over whatever sessions the daemons turn out to have
378 pointing at a daemon that is never coming back looks identical to one whose 449 when you come back. It is derived convenience and not something you authored:
379 network is merely down. Nothing times it out; `Ctrl-\` `x`, or `mux wall rm 450 every way it can fail (a missing file, a stale tree, a session that is no
380 SPELLING`, is how a line leaves. 451 longer there) degrades quietly to the default cut, where the wall places
381 452 tiles side by side on a wide terminal and stacks them on a tall one.
382 `Ctrl-\` `x` forgets the focused tile: its line leaves the wall file, its stripe
383 leaves the screen, and the remaining stripes are re-cut over the space.
384 Forgetting is never killing — the session goes on running and `mux` reaches
385 it again by name. Forget them all and the wall says so.
386 453
387 `Ctrl-\` `w` folds in the saved wall's tiles one-shot — the wall you are 454 A scripted `mux TARGET` — stdout on a pipe — is still a wall of one tile
388 already on, plus every session recorded in the file. `Ctrl-\` `d` leaves. 455 whose rect is the whole terminal, writing exactly what the plain client
456 wrote, with no other host dialled and no layout saved. A wall of MANY needs
457 a terminal to cut stripes from and says `mux: wall needs a terminal` if it
458 has none.
389 459
390 ## Everything else 460 ## Everything else
391 461
@@ -438,7 +508,9 @@ execs the old one back and the old one carries on serving. Scrollback does
438 not survive the handover; the visible grid, the titles, the command marks 508 not survive the handover; the visible grid, the titles, the command marks
439 and the exit codes do. A daemon too old to know the request answers nothing, 509 and the exit codes do. A daemon too old to know the request answers nothing,
440 and `muxd upgrade` says so: `no reply: this daemon predates upgrade — stop 510 and `muxd upgrade` says so: `no reply: this daemon predates upgrade — stop
441 and run`. 511 and run`. A session in the middle of being ended (`Ctrl-\` `x`, up to half a
512 second while its shell is being hung up) refuses the upgrade with `session
513 ending, retry` rather than carrying a shell with no master across the exec.
442 514
443 Copy and paste work through the session: a mux drag and an application's own 515 Copy and paste work through the session: a mux drag and an application's own
444 OSC 52 write both reach your terminal's clipboard (including from a remote box over QUIC, 516 OSC 52 write both reach your terminal's clipboard (including from a remote box over QUIC,
@@ -464,7 +536,7 @@ Design: `docs/handoff.md`. Every decision and measurement:
464 536
465 ## Status 537 ## Status
466 538
467 Prototype, eighteen milestones in, current cut tagged `v0.0.1-10`. All 539 Prototype, eighteen milestones in, current cut tagged `v0.0.1-15`. All
468 founding kill criteria cleared, measured on real networks rather than 540 founding kill criteria cleared, measured on real networks rather than
469 loopback: reattach into a live full-screen `nvim` in ~5ms from one 541 loopback: reattach into a live full-screen `nvim` in ~5ms from one
470 snapshot; steady-state delta traffic ~1% of snapshot-equivalent cost; the 542 snapshot; steady-state delta traffic ~1% of snapshot-equivalent cost; the
@@ -476,5 +548,8 @@ delay on a real path); and a predicted keystroke paints in **0.1ms** on a
476 150ms path where the same keystroke's authoritative echo takes **154ms** — 548 150ms path where the same keystroke's authoritative echo takes **154ms** —
477 typing stops waiting for the round trip, and the two numbers come from one 549 typing stops waiting for the round trip, and the two numbers come from one
478 keystroke on one connection so the gap is not an artefact of comparing 550 keystroke on one connection so the gap is not an artefact of comparing
479 runs. Not yet here: daemon auto-start on attach, reconnect backoff tuning, 551 runs. Auto-start is here for local unix-socket attaches — `mux` and `mux
480 prediction for backspace and for multi-byte input. 552 --sock PATH` start a daemon when nothing is serving that path — and the wall
553 starts the one on the default socket for the same reason. Not yet here:
554 auto-start on a remote host, the browser hub on the `hosts` file, reconnect
555 backoff tuning, prediction for backspace and for multi-byte input.
docs/decisions.md
Old New
@@ -7120,3 +7120,172 @@ leg's `timeout` scales with `E2E_TIME_SCALE`, against the lib's own
7120 "budgets only" wording: a wedged daemon never answers, so waiting longer 7120 "budgets only" wording: a wedged daemon never answers, so waiting longer
7121 changes nothing about the fact demanded, and a kcov-slowed answer must not 7121 changes nothing about the fact demanded, and a kcov-slowed answer must not
7122 read as the wedge. 7122 read as the wedge.
7123
7124 ## 2026-08-28 — the wall lists daemons; tiles are their live sessions
7125
7126 **There were two lists of "my sessions" and they disagreed.** The daemon's
7127 list is live and is what `Ctrl-\ n`/`p` walked (`sessions_req`); the wall
7128 file's list was attach history and is what `mux wall`, the digits and
7129 `Ctrl-\ w` showed. A session born by a split, by `muxa` or by another
7130 client was on one and not the other, so `n` landed the user in shells they
7131 did not know existed. The other direction was worse: a wall line for a
7132 session the daemon no longer had was re-created on restore
7133 (`client.hydratedCreates`), so the file resurrected shells — and bare `mux`
7134 had a third path of its own, attach-or-create `#0` re-recorded on every
7135 run, which meant `Ctrl-\ x` on it was undone by the next `mux`. One list
7136 was the fix, and the only list that cannot lie is the daemons'.
7137
7138 **So the file records HOSTS and nothing else.** `$XDG_STATE_HOME/mux/hosts`,
7139 one spelling per line — `--sock PATH` | `HOST` | `quic://HOST[:PORT]` — and
7140 `#SESSION` is a refusal with the rule in the message. That is the whole
7141 guarantee: a line that names no session is a line nothing can resurrect
7142 from, so "a session the user ENDED stays ended" stopped needing a latch to
7143 enforce it and became a property of the grammar. Tiles are each host's live
7144 sessions, in host order then the daemon's own slot order; births appear,
7145 exits disappear, and `n`/`p`/digits walk the one list. The old `wall` file
7146 is not read by the CLI and not migrated: its lines are attach history of
7147 sessions, and the model no longer has that.
7148
7149 **Poll, not push, and a fresh connection per poll.** Each host gets one
7150 `sessions_req` a second on a side connection that is opened, asked, read and
7151 closed. A subscription would be a new daemon concept — state per watcher,
7152 an invalidation path, a reconnect story — for a frame that is forty bytes
7153 over a link already carrying deltas. The measurement that would justify
7154 changing it is the one to take first: `muxd stats` delta_bytes against
7155 poll bytes on a wall of N hosts over an hour. If polling is ever the cost,
7156 `sessions_changed` is the push and this is the line that said so. A fresh
7157 connection each time is the same argument one level down — a held observer
7158 is a slot the daemon must reap, an idle deadline to tune, and a thing to
7159 re-establish across an upgrade, and the daemon already accepts a connection
7160 per `muxd stats`.
7161
7162 **A host is one stripe only when it has no tiles.** The naive rule — down
7163 host, dead tiles — turns a thirty-second network blip into a wall of
7164 `[refused]` and then a re-attach storm. Tiles ride out a blip on their own
7165 pumps (`reconnecting`, unchanged); the stripe exists for the case where
7166 there is nothing else to draw, and it keeps redialling. The one exception
7167 is the user's own machine: a listed local `--sock <default>` that nothing is
7168 serving is auto-started, because it dies on every reboot while its line
7169 lives on, and a wall that paints your own box `[unreachable]` until you find
7170 some other shell to start a daemon in is the empty-file case with one line
7171 in front of it.
7172
7173 **`[no sessions]` is a word the local path cannot reach.** `muxd` exits when
7174 its last session ends and `muxd run` creates `0` at start, so every daemon
7175 that answers at all contributes at least one tile. The word is kept because
7176 a remote daemon held open by something else could reach it, but phase 1
7177 pins `[unreachable]` only. Whether an empty daemon should STAY alive is a
7178 spec question for the user — it changes daemon lifetime, and it is the
7179 difference between "the host is gone" and "the host is idle" being
7180 distinguishable on the wall. Filed, not decided.
7181
7182 **`Ctrl-\ x` ends the session, and the DAEMON owns the two-step.** New
7183 observer verbs `end_req 0x11` / `end_reply 0x94`; nothing existing changed
7184 shape. The daemon refuses a first `x` on a session other clients hold and
7185 answers with the count, the client arms three seconds for the forcing second
7186 press, and the rail says `[1 other attached - x again to end]`. The count
7187 had to come from the daemon rather than from the client's own view: a client
7188 knows how many clients it can see, which is one, and a client-guessed
7189 "nobody else is here" is exactly the wrong answer to give about interrupting
7190 someone. An accepted end is BOUNDED — SIGHUP, SIGTERM, then SIGKILL past
7191 `Pty.term_grace_ms` (500 ms) — because a shell that traps both would
7192 otherwise be a session with no master that nothing can end, which is the bug
7193 the user asked to fix. The tile leaves when the daemon's list no longer has
7194 the session, not when the key is pressed, so the screen never claims
7195 something the daemon has not done.
7196
7197 **An upgrade is refused while any session is ending.** The 500 ms hangup
7198 window has a session whose pty master is already closed, and `execUpgrade`
7199 writes that master's fd into the manifest. Rather than teach the manifest a
7200 "masterless" case, `muxd upgrade` answers `session ending, retry` and the
7201 operator retries half a second later. Bounded refusal beats an unbounded
7202 number of adopt-time special cases.
7203
7204 **A vanished tile's wake pipe is never closed.** Pollers now open a socket a
7205 second, so fd recycling is real: closing a vanished tile's wake fd and
7206 having the number handed straight back to a live socket makes a stray
7207 doorbell byte land in someone's session. Up to `max_tiles` fds held for a
7208 wall's life is the cheaper side of that trade.
7209
7210 **A `mux` with no terminal is a wall of ONE.** The standing invariant is
7211 that a scripted `mux TARGET` writes byte-identically to what the plain
7212 client wrote, and there is nowhere to paint a second tile on a pipe. So a
7213 headless run stops at the entry host: no other hosts resolved, no pollers,
7214 no sidecar. This was found the hard way — removing the old `hydrated` flag
7215 made every wall save and restore the layout sidecar, piped ones included, a
7216 restored focus CLAIMED, the claim forced a redundant resize, and
7217 `01_boot`'s "quic delta resume" leg started wanting three snapshots where it
7218 wants one. Bisected to the commit, not guessed. A wall of MANY does need a
7219 terminal and says `mux: wall needs a terminal`.
7220
7221 **The sidecar is saved by every wall left on a terminal, including `mux
7222 TARGET`.** Under the old model `mux TARGET` was an argv wall and argv walls
7223 were views, so they never wrote. Under this one `mux TARGET` IS the wall,
7224 zoomed on one tile, and its layout is as much the user's as any other's.
7225 The sidecar stays derived convenience: restore is verbatim, healing is per
7226 leaf against the hosts' live lists, and every failure degrades silently to
7227 the default cut. Restore moved to "the first list arrives" rather than
7228 "the wall opens", because at open time the wall has one tile and the tree
7229 it is being asked to restore describes several.
7230
7231 **Births from one list keep the list's order.** Each birth anchored beside
7232 the FOCUS, so a wall entered on `a` that learns `{b, c}` in one answer laid
7233 them out `a, c, b`. Each birth now anchors beside the previously born tile
7234 of that same apply, the first beside the focus. A wall's reading order is
7235 the daemon's order, which is the only order the user has been given a reason
7236 to expect.
7237
7238 **Measured, and filed: a TTY wall pays one snapshot per tile per DISTINCT
7239 relayout.** A three-tile wall costs six snapshots to open, with or without a
7240 sidecar. The cause is not the sidecar: `relayout` flags `resize_pending` on
7241 every present tile, the pump turns that into a `.resize`, and
7242 `Server.onResize` calls `resyncSnapshot` even when `applySize` returned
7243 false — a tile whose rect did not change still buys a repaint. Two candidate
7244 fixes, both one-line and both outside this spec's contract: flag
7245 `resize_pending` only on a rect change, or skip the resync when `applySize`
7246 says nothing moved. Follow-up, with the sites named here so the next person
7247 does not re-derive them.
7248
7249 **Two defects named rather than papered over, in `e2e_12`'s own comment.**
7250 A heal that collapses a container loses that container's weight and
7251 orientation, so a restored wall can come back correctly cut but wrongly
7252 proportioned; and the focus after a healed restore lands on the newcomer
7253 rather than on the tile the sidecar named. The healing leg pins what it can
7254 actually hold — no vertical rail at 80x24 after restore — instead of a
7255 geometry assertion that would pass with the heal reverted. An assertion
7256 weakened without saying why is worse than an absent one.
7257
7258 **A daemon that predates this change reads as `[unreachable]`, and that is
7259 the cross-version cost.** `sessions_req` existed, but only on the attached
7260 client's path; answering it on a bare observer connection is new here. So a
7261 v0.0.1-15 daemon is up, `mux --sock PATH` attaches to it, and the wall still
7262 paints it a stripe that never heals. Measured side by side: one hosts file
7263 naming a tree-built daemon and an installed v0.0.1-15 daemon, both alive on
7264 their own sockets, both answering `muxd stats` with `sessions=1` — `mux
7265 hosts` printed `1` for the first and `[unreachable]` for the second, after
7266 the full two-second wait. `muxd upgrade` on every box on the wall is part
7267 of taking this version, and the xversion gate pins both halves of the story
7268 (the stripe, and `Ctrl-\ x`'s `[daemon too old to end a session]`).
7269
7270 **What went, and the legs that went with it.** `wall.zig`'s `#SESSION`
7271 split and attach-history semantics; `client.recordOnState`; the CLI's use of
7272 `client.hydratedCreates` and the saved-local-line attach-or-create rule with
7273 its dead-tile marker; `mux wall`, `mux wall add`, `mux wall rm`;
7274 `Entry.hydrate`/`hydrated`/`record0`; the `Ctrl-\ w` fold, now a plain
7275 unzoom because there is nothing left to fold in; the daemon session ring
7276 behind `n`/`p` and `client.ringNeighbour` with it. Their e2e legs were
7277 deleted with the rules they pinned and the suite's count pin moved 83 → 81,
7278 with a new group `09_hosts` carrying the model's own legs. No compatibility
7279 spelling for `mux wall` — the project is too early for one (2026-08-20), and
7280 `mux wall` now parses as a HOST called `wall`, which ssh says out loud. A
7281 machine literally named `wall` or `hosts` needs a `--via` or `quic://`
7282 spelling.
7283
7284 **The hub is phase 2 and was left strictly alone.** `muxweb` still reads and
7285 writes the old `wall` file, still lists sessions rather than hosts, and its
7286 e2e legs pass untouched — which is the evidence that this change is
7287 contained. The cost is a real divergence for a real user: nothing on the CLI
7288 writes that file any more, so a fresh machine's hub wall is empty until
7289 someone names tiles on its command line or in the page. Stated in the README
7290 rather than smoothed over, because a hub that quietly shows a different wall
7291 from the terminal is the two-lists bug again, one layer out.
docs/roadmap.md
Old New
@@ -1,5 +1,9 @@
1 # Roadmap 1 # Roadmap
2 2
3 > The wall became a list of DAEMONS on 2026-08-28 (decisions.md, same date):
4 > every wall-file item below describes the old attach-history file and is
5 > closed. The hub's move onto `hosts` is phase 2 and is the live item.
6
3 > This file is the ranked queue as of 2026-08-16 and has not moved since; 7 > This file is the ranked queue as of 2026-08-16 and has not moved since;
4 > the tracker is the live order and `decisions.md` is current. Sections 8 > the tracker is the live order and `decisions.md` is current. Sections
5 > below marked complete are accurate; the ranking is not. 9 > below marked complete are accurate; the ranking is not.
docs/superpowers/specs/2026-08-19-wall-home-screen-design.md
Old New
@@ -1,5 +1,10 @@
1 # The wall is the home screen; zoom is a lens 1 # The wall is the home screen; zoom is a lens
2 2
3 > **Superseded by `2026-08-27-wall-of-hosts-design.md`** for the SOURCE of
4 > tiles: the file lists daemons and tiles are their live sessions. The zoom
5 > lens, the layout tree, the sidecar and the "mux IS the wall" entry below
6 > all still hold.
7
3 Date: 2026-08-19. Status: model agreed in discussion; not yet commissioned. 8 Date: 2026-08-19. Status: model agreed in discussion; not yet commissioned.
4 Companion spec: `2026-08-19-muxd-upgrade-design.md` (independent work). 9 Companion spec: `2026-08-19-muxd-upgrade-design.md` (independent work).
5 10
src/cli/mux_main.zig
Old New
@@ -52,9 +52,12 @@ const usage =
52 \\ is refused, because the wall shows every session a daemon has. 52 \\ is refused, because the wall shows every session a daemon has.
53 \\ 53 \\
54 \\ On the wall, `Ctrl-\ 1-9` focuses a tile and types into it, 54 \\ On the wall, `Ctrl-\ 1-9` focuses a tile and types into it,
55 \\ `Ctrl-\ h/j/k/l` moves between panes, `Ctrl-\ |/-` split right/below, 55 \\ `Ctrl-\ n/p` walk the tiles, `Ctrl-\ h/j/k/l` moves between panes,
56 \\ `Ctrl-\ f` fullscreen, `Ctrl-\ r` resize mode, `Ctrl-\ :` adds a tile 56 \\ `Ctrl-\ c` and `Ctrl-\ |/-` create a session on the focused tile's
57 \\ by spelling (Enter adds, Esc cancels), `Ctrl-\ d` leaves. 57 \\ daemon, `Ctrl-\ x` ends the focused session (twice when others are
58 \\ attached), `Ctrl-\ f` fullscreen, `Ctrl-\ r` resize mode,
59 \\ `Ctrl-\ w` zooms out, `Ctrl-\ :` adds a DAEMON by spelling
60 \\ (Enter adds, Esc cancels), `Ctrl-\ d` leaves.
58 \\ 61 \\
59 ; 62 ;
60 63