a73x

77e31dd9

docs: the listing cost is a sum, and the upgrade answer is not chosen yet

a73x   2026-08-28 19:53

Commit message
docs: the listing cost is a sum, and the upgrade answer is not chosen yet

`hostsList` dials in a serial loop, so the README's "as long as the slowest
one answers" was the wrong shape entirely: the cost is the SUM. Measured
rather than reasoned — 2002 ms for one silent daemon, 4002 ms for two, ~1 ms
for any number of ABSENT sockets, which is the distinction the sentence was
missing. Only a host that accepts the dial and then says nothing pays the
wait.

Ruling 21 is parked for the user, and both files were presenting it as
settled. README now says upgrading every walled daemon is what happens
today and names the alternative — `[upgrade muxd]` for "dialled fine, list
timed out" — as the open choice; decisions.md carries the two options with
what the second would cost, and why it was not built unasked.

`[no sessions]` gets the one README sentence it was missing: unreachable
from your own machine, reachable for a remote daemon something else holds
open.

Three counts corrected: decisions.md is 7.3k lines, not 7.1k in one place
and 4.9k in the other; and "every named attach writes a host" undercounted
— a bare `mux` on an empty file records the local daemon too.

The bisect in the headless paragraph now names 2782c6b, which is the whole
point of writing it down.

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

CLAUDE.md
Old New
@@ -27,7 +27,7 @@ costs ~800k tokens; every token stays in context and is re-billed each turn.
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 `src/wallview.zig` (6.6k), `src/interact.zig` (4.5k), 29 `src/wallview.zig` (6.6k), `src/interact.zig` (4.5k),
30 `docs/decisions.md` (7.1k). `test/e2e.sh` is a 189-line runner now; the 30 `docs/decisions.md` (7.3k). `test/e2e.sh` is a 189-line runner now; the
31 bodies are `test/e2e_NN_*.sh` (`E2E_ONLY=<group>` runs one). 31 bodies are `test/e2e_NN_*.sh` (`E2E_ONLY=<group>` runs one).
32 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.
33 `src/client.zig` is 1.9k now — cheap enough to read. 33 `src/client.zig` is 1.9k now — cheap enough to read.
@@ -169,8 +169,10 @@ real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`.
169 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.
170 - `make check` before commit, `make ci` before delivery; capture `$?` before 170 - `make check` before commit, `make ci` before delivery; capture `$?` before
171 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).
172 - **Any hand-run rig exports an isolated `XDG_STATE_HOME`.** Every named 172 - **Any hand-run rig exports an isolated `XDG_STATE_HOME`.** Every `mux`
173 attach writes a host into `$XDG_STATE_HOME/mux/hosts`; the `make` scripts 173 that attaches writes its daemon into `$XDG_STATE_HOME/mux/hosts` — a bare
174 `mux` on an empty file included, since that records the local one. The
175 `make` scripts
174 all isolate it, but an ad-hoc repro or perf rig with the real `HOME` leaves 176 all isolate it, but an ad-hoc repro or perf rig with the real `HOME` leaves
175 a dead `--sock /tmp/...` line on the user's wall per throwaway socket (49 177 a dead `--sock /tmp/...` line on the user's wall per throwaway socket (49
176 of them found 2026-08-22, under the old per-attach file). `export 178 of them found 2026-08-22, under the old per-attach file). `export
@@ -186,7 +188,7 @@ real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`.
186 ## Where the answers live 188 ## Where the answers live
187 189
188 `docs/roadmap.md` (~650 ln) the ranked queue, but stale past 2026-08-16 — 190 `docs/roadmap.md` (~650 ln) the ranked queue, but stale past 2026-08-16 —
189 `git-collab issue list` is the live order · `docs/decisions.md` (4.9k ln, grep 191 `git-collab issue list` is the live order · `docs/decisions.md` (7.3k ln, grep
190 only) every decision + measurement · `docs/handoff.md` (~200 ln) the original 192 only) every decision + measurement · `docs/handoff.md` (~200 ln) the original
191 design, historical · `README.md` user-facing usage. 193 design, historical · `README.md` user-facing usage.
192 194
README.md
Old New
@@ -365,8 +365,12 @@ box [unreachable]
365 ``` 365 ```
366 366
367 — the line, a tab, then its live session count or what went wrong. That 367 — the line, a tab, then its live session count or what went wrong. That
368 listing dials every host, so it takes as long as the slowest one answers (up 368 listing dials the hosts one at a time and waits up to two seconds for each,
369 to two seconds each). `mux hosts rm` reads the file verbatim too, so a line 369 so the cost is the SUM and not the slowest: two silent hosts is four
370 seconds. Only a host that accepts the dial and then says nothing costs the
371 full wait — a socket with nothing behind it is refused instantly (measured:
372 2002 ms for one silent daemon, 4002 ms for two, ~1 ms for any number of
373 absent sockets). `mux hosts rm` reads the file verbatim too, so a line
370 you hand-edited into something no longer legal can be typed back byte for 374 you hand-edited into something no longer legal can be typed back byte for
371 byte and removed instead of wedging every command that touches the wall. 375 byte and removed instead of wedging every command that touches the wall.
372 376
@@ -380,12 +384,22 @@ leaves. Your own daemon is the exception that needs no stripe: a listed
380 local `--sock <default>` that nothing is serving is started for you, because 384 local `--sock <default>` that nothing is serving is started for you, because
381 it dies on every reboot while its line lives on. 385 it dies on every reboot while its line lives on.
382 386
387 A host that answers with an EMPTY list is a stripe reading `[no sessions]`
388 instead. You will not see it from your own machine — `muxd` exits when its
389 last session ends, so a local daemon that answers at all has at least
390 session `0` — but a remote daemon something else is holding open can sit
391 there with nothing to show.
392
383 **A daemon too old to be asked reads as `[unreachable]` too.** Listing a 393 **A daemon too old to be asked reads as `[unreachable]` too.** Listing a
384 daemon's live sessions is a new question on the wire, and a daemon from 394 daemon's live sessions is a new question on the wire, and a daemon from
385 before this change answers it with the silence it answers every unknown 395 before this change answers it with the silence it answers every unknown
386 frame with. It is up, `mux --sock PATH` still attaches to it, and the wall 396 frame with. It is up, `mux --sock PATH` still attaches to it, and the wall
387 still shows it as a stripe that never heals — so `muxd upgrade` (or a 397 still shows it as a stripe that never heals, so today `muxd upgrade` (or a
388 restart) on every box you put on the wall is part of taking this version. 398 restart) on every box you put on the wall is what clears it. Whether that
399 stays the answer is undecided: the alternative is to tell the two apart —
400 `[upgrade muxd]` when the dial succeeded and only the list timed out,
401 `[unreachable]` when nothing answered at all — which costs a word and a
402 branch and is not built until someone asks for it.
389 403
390 Restarting a daemon re-creates **nothing**. The old wall would have brought 404 Restarting a daemon re-creates **nothing**. The old wall would have brought
391 your sessions back from the file; this one cannot, because the file has no 405 your sessions back from the file; this one cannot, because the file has no
docs/decisions.md
Old New
@@ -7215,8 +7215,10 @@ 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 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 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 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 7218 wants one. Bisected to `2782c6b`, not guessed — the commit that made a
7219 terminal and says `mux: wall needs a terminal`. 7219 host's live sessions its tiles, and removed `Entry.hydrated` on the way
7220 past. A wall of MANY does need a terminal and says `mux: wall needs a
7221 terminal`.
7220 7222
7221 **The sidecar is saved by every wall left on a terminal, including `mux 7223 **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 7224 TARGET`.** Under the old model `mux TARGET` was an argv wall and argv walls
@@ -7263,10 +7265,21 @@ 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 7265 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 7266 their own sockets, both answering `muxd stats` with `sessions=1` — `mux
7265 hosts` printed `1` for the first and `[unreachable]` for the second, after 7267 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 7268 the full two-second wait. The xversion gate pins both halves of the story
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 (the stripe, and `Ctrl-\ x`'s `[daemon too old to end a session]`).
7269 7270
7271 **What to DO about that is parked, not decided.** Two options, and the
7272 choice is the user's. Accept it: every daemon on the wall must be upgraded,
7273 which this release arguably demands anyway since it adds wire verbs. Or
7274 distinguish the two silences — `[upgrade muxd]` when the dial succeeded and
7275 only the list timed out, `[unreachable]` when nothing answered — which costs
7276 one word, one branch in `hostsList` and the poller, and turns "your box is
7277 gone" into "your box is old". Nothing is built for the second: it was not
7278 asked for, and building an unasked-for mitigation into a release note is how
7279 a stopgap becomes the design. Recorded here so the choice is visible rather
7280 than made by default. Until it is made, the README says the first is what
7281 happens and names the second as open.
7282
7270 **What went, and the legs that went with it.** `wall.zig`'s `#SESSION` 7283 **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 7284 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 7285 `client.hydratedCreates` and the saved-local-line attach-or-create rule with