a73x

28cd9a55

docs: the model ships — mux is the wall

a73x   2026-08-20 07:55

Commit message
docs: the model ships — mux is the wall

CLAUDE.md gains the invariant the spec promised at ship time: an unzoomed
tile claims nothing; zoom promotes/demotes the tile's existing connection.
"muxa attaches at 0×0" survives verbatim beside it.

decisions.md gets the phase-3c entry — the dial seam, `Transport.adopt`,
the exit rule, the d/w break, the ring reuse, deferred hydration, and the
frames-before-keys ordering fault the suite found — and the superseded-by
note on "the wall's zoom is a real attach", whose argument was not wrong
so much as built on a premise (a typing tile types at 0×0) that the
promote's resize removed.

README leads with the model instead of describing two programs: `mux` IS
the wall, one navigation axis, one key table for both doors into it. The
zoomed-in table now says what `d`, `w`, `n`/`p`/`c` and `l` actually do,
and the exit rule is stated where a reader will look for it.

CLAUDE.md
Old New
@@ -55,6 +55,11 @@ real pty), `wsclient` (browser stand-in), `rawmode`, `delaypipe`, `render`.
55 - **One replay core.** CLI, wasm, and test fixtures all go through `replica.zig`. 55 - **One replay core.** CLI, wasm, and test fixtures all go through `replica.zig`.
56 Do not hand-roll a second applier. 56 Do not hand-roll a second applier.
57 - **Latest wins.** The grid follows the most recently active client. 57 - **Latest wins.** The grid follows the most recently active client.
58 - **An unzoomed tile claims nothing; zoom promotes/demotes the tile's existing
59 connection.** Promote resizes the attach from 0×0 to the terminal and starts
60 forwarding; demote is client-local and sends nothing. `mux [TARGET]` is a
61 wall of one tile entered zoomed — there is ONE interaction loop, and it is a
62 tile pump.
58 - **`muxa` attaches at 0×0** so an agent never resizes a human's session. 63 - **`muxa` attaches at 0×0** so an agent never resizes a human's session.
59 - **OSC 133 marks are opt-in** (`MUX_SHELL_INTEGRATION=1`); without them `muxa` 64 - **OSC 133 marks are opt-in** (`MUX_SHELL_INTEGRATION=1`); without them `muxa`
60 falls back to `pgid`/`settle` and there is no real exit code. Every `muxa` 65 falls back to `pgid`/`settle` and there is no real exit code. Every `muxa`
README.md
Old New
@@ -31,23 +31,47 @@ muxd run & # the daemon: hosts your shell and the authoritative screen
31 mux # attach 31 mux # attach
32 ``` 32 ```
33 33
34 Inside a session: 34 **`mux` IS the wall.** There is one navigation axis and one program behind
35 it: zoomed IN is a session, full screen; zoomed OUT is the wall. `mux` and
36 `mux HOST` open zoomed into the session they just attached to; `mux wall`
37 opens on the wall. `Ctrl-\` `w` moves between them, in place — no child
38 process, no second screen, and every tile's replica stays hot, so coming
39 back is a repaint and not a re-attach. Bare `mux` still feels like a plain
40 client, because a wall of one tile is what a plain client always was.
41
42 Zoomed in — the session has the terminal, and only the prefix is held back:
35 43
36 | Key | Effect | 44 | Key | Effect |
37 |---|---| 45 |---|---|
38 | `Ctrl-\` | command prefix — press it, then a command key | 46 | `Ctrl-\` | command prefix — press it, then a command key |
39 | `Ctrl-\` `d` (or `Ctrl-\` `Ctrl-\`) | detach (session keeps running) | 47 | `Ctrl-\` `d` (or `Ctrl-\` `Ctrl-\`) | detach and leave mux (session keeps running) |
40 | `Ctrl-\` `c` | create a session and switch to it (the old one keeps running) | 48 | `Ctrl-\` `w` | zoom out to the wall |
41 | `Ctrl-\` `n` / `Ctrl-\` `p` | switch to the next / previous session, wrapping | 49 | `Ctrl-\` `c` | create a session, put it on the wall, zoom it |
42 | `Ctrl-\` `w` | show every session as a wall (`j`/`k` select, `Enter` zooms, `q` comes back) | 50 | `Ctrl-\` `n` / `Ctrl-\` `p` | move the zoom to the next / previous session of this daemon, wrapping |
43 51 | `Ctrl-\` `l` | skip back to the last tile you zoomed (out, if that was this one) |
44 The wall `Ctrl-\` `w` opens is a real wall, so `x` in it edits the saved
45 wall file like any other — the tiles it lists are spelled exactly as the
46 recorded lines, so forgetting one there forgets it everywhere. The session
47 keeps running either way.
48
49 | `Shift+PageUp` / `Shift+PageDown` | scrollback, a screen at a time (any other key returns to live) | 52 | `Shift+PageUp` / `Shift+PageDown` | scrollback, a screen at a time (any other key returns to live) |
50 | mouse wheel | scrollback, three rows a notch (arrow keys to a full-screen app) | 53 | mouse wheel | scrollback, three rows a notch (arrow keys to a full-screen app) |
54 | any key not behind `Ctrl-\` | goes to the session |
55
56 Zoomed out — the wall reads the keys and nothing reaches a session:
57
58 | Key | Effect |
59 |---|---|
60 | `j` / `k`, `1`-`9` | select a tile |
61 | `Enter` | zoom the selected tile |
62 | `x` | forget the selected tile (off the wall file; the session keeps running) |
63 | `q` (or `Ctrl-\`) | leave mux |
64
65 `n`, `p` and `c` are about SESSIONS, not tiles: they walk the daemon's own
66 session ring. A session that already has a tile is an instant zoom move; one
67 that does not gets a tile, because visiting it is attaching to it and every
68 attach is recorded (see "the wall is your attach history" below). The wall's
69 `j`/`k` and the digits are what walk tiles.
70
71 A session whose shell exits ends `mux` with that shell's exit code when it
72 was the only tile on the wall — so `mux` stays something you can put in a
73 script — and drops you to the wall when it was not, with the tile that ended
74 saying so.
51 75
52 The wheel goes to whatever is in front of it. At a shell prompt it moves 76 The wheel goes to whatever is in front of it. At a shell prompt it moves
53 mux's scrollback, three rows a notch. In a full-screen application that did 77 mux's scrollback, three rows a notch. In a full-screen application that did
@@ -65,11 +89,11 @@ because a terminal that is reporting the mouse is not selecting with it.
65 `kill -9` if you like; the session doesn't care. 89 `kill -9` if you like; the session doesn't care.
66 90
67 One daemon hosts up to four named sessions; the first is called `0`. 91 One daemon hosts up to four named sessions; the first is called `0`.
68 `Ctrl-\` `c` creates one named after the lowest free number and switches 92 `Ctrl-\` `c` creates one named after the lowest free number and zooms it,
69 to it, `Ctrl-\` `n` and `Ctrl-\` `p` step through them in the daemon's 93 `Ctrl-\` `n` and `Ctrl-\` `p` step through them in the daemon's order and
70 order and wrap at the ends, and `mux --session NAME` attaches to any of 94 wrap at the ends, and `mux --session NAME` attaches to any of them by name
71 them by name (creating it if there is room). With only one session open 95 (creating it if there is room). With only one session open `n` and `p` do
72 `n` and `p` do nothing — there is nowhere to step to. 96 nothing — there is nowhere to step to.
73 97
74 Every session shell is told where it lives: `$MUX_SOCK` is the daemon's 98 Every session shell is told where it lives: `$MUX_SOCK` is the daemon's
75 socket path and `$MUX_SESSION` is the session's name. Scripts and prompts 99 socket path and `$MUX_SESSION` is the session's name. Scripts and prompts
@@ -184,6 +208,10 @@ as `HOST#build` and as `quic://HOST#build` is two tiles, deliberately.
184 do the wall's own stripes. Writing the file is best effort — if it cannot be 208 do the wall's own stripes. Writing the file is best effort — if it cannot be
185 written you get one warning line and the attach happens anyway. 209 written you get one warning line and the attach happens anyway.
186 210
211 That file is what `Ctrl-\` `w` shows you: zooming out from any session puts
212 up the saved wall with the tile you are standing on among them. There is one
213 wall, and no way to end up looking at a second one by accident.
214
187 `mux wall add`/`rm` are the same edit without an attach: file operations 215 `mux wall add`/`rm` are the same edit without an attach: file operations
188 only, so neither dials, and `rm` never touches the session. A wall or hub 216 only, so neither dials, and `rm` never touches the session. A wall or hub
189 that is already running does not notice the change until you reopen it. 217 that is already running does not notice the change until you reopen it.
@@ -203,27 +231,14 @@ watching never resizes anyone. The stripes are cut once, from the terminal's
203 size when the wall opened, so resizing that terminal while the wall is up 231 size when the wall opened, so resizing that terminal while the wall is up
204 misplaces them until you leave and reopen it. 232 misplaces them until you leave and reopen it.
205 233
206 Zoomed out (the wall): 234 The keys are the ones listed under "Quick start, local" — `mux wall` and
207 235 `mux` are the same program, entered through different doors, so there is
208 | Key | Effect | 236 one key table and not two. Zoomed out, `j`/`k` (or `n`/`p`) and `1`-`9`
209 |---|---| 237 select, `Enter` zooms, `x` forgets, `q` leaves. Zoomed in, the session has
210 | `j` / `k` (or `n` / `p`) | select the next / previous stripe, wrapping | 238 the terminal and `Ctrl-\` is the only thing held back: `w` zooms out, `d`
211 | `1`-`9` | select that stripe, counting from the top | 239 detaches and leaves, `n`/`p`/`c` walk the daemon's session ring, `l` skips
212 | `Enter` | zoom the selected stripe — type into that session | 240 back to the last tile you zoomed. Any other command key after the prefix is
213 | `x` | forget the selected tile (off the wall file; the session keeps running) | 241 swallowed along with it, as in a plain client.
214 | `q` (or `Ctrl-\`) | leave the wall |
215
216 Zoomed in (one session, full screen):
217
218 | Key | Effect |
219 |---|---|
220 | `Ctrl-\` `d` / `Ctrl-\` `w` | zoom back out to the wall |
221 | `Ctrl-\` `n` / `Ctrl-\` `p` | move the zoom to the next / previous tile |
222 | `Ctrl-\` `l` | skip to the last tile you zoomed (out, if that was this one) |
223 | any key not behind `Ctrl-\` | goes to the session |
224
225 `Ctrl-\` is the prefix, so it never reaches the session; any other command
226 key after it is swallowed along with it, as in a plain client.
227 242
228 The selected stripe's label bar carries a `>` marker. `Enter` zooms it in 243 The selected stripe's label bar carries a `>` marker. `Enter` zooms it in
229 place: the tile's own connection is resized from 0×0 to this terminal's 244 place: the tile's own connection is resized from 0×0 to this terminal's
docs/decisions.md
Old New
@@ -4087,7 +4087,15 @@ pass for a client that printed the line and attached anyway. The daemon runs
4087 dump and a grid wraps — at 80 the wall refusal, whose length follows `$TMPDIR`, 4087 dump and a grid wraps — at 80 the wall refusal, whose length follows `$TMPDIR`,
4088 split mid-sentence and a correct refusal read as a missing one. 4088 split mid-sentence and a correct refusal read as a missing one.
4089 4089
4090 **The wall's zoom is a real attach, and that is the whole design.** `Enter` on 4090 **The wall's zoom is a real attach, and that is the whole design.**
4091 SUPERSEDED by the 2026-08-19 phase-1 entry below (zoom is a lens: the child
4092 spawn is gone, and `Enter` promotes the connection the tile already has).
4093 The argument recorded here is not wrong; its premise stopped holding. It
4094 assumed a typing tile types AT 0×0, and a promote resizes the attach to the
4095 terminal before the first keystroke, so the client that types claims the
4096 grid the legitimate way and the rule keeps no exception after all. Kept
4097 verbatim because the reasoning is the one a reader needs in order to
4098 understand why the promote must resize FIRST. `Enter` on
4091 the selected stripe spawns an ordinary `mux` on this terminal, waits for it, and 4099 the selected stripe spawns an ordinary `mux` on this terminal, waits for it, and
4092 takes the terminal back when it detaches. The obvious alternative — forward 4100 takes the terminal back when it detaches. The obvious alternative — forward
4093 keystrokes down the selected tile's transport — forks the latest-wins rule at 4101 keystrokes down the selected tile's transport — forks the latest-wins rule at
@@ -4557,3 +4565,149 @@ targeted answer (here, `muxa send` against a hashed wall file, standalone). No
4557 mutation was needed for `x`: 4565 mutation was needed for `x`:
4558 the leg asserts a line count, a surviving spelling and a session that still 4566 the leg asserts a line count, a surviving spelling and a session that still
4559 answers, and no two of those can be satisfied by the same accident. 4567 answers, and no two of those can be satisfied by the same accident.
4568
4569 ## 2026-08-20 (the wall is the home screen, phase 3c: convergence)
4570
4571 Spec: `docs/superpowers/specs/2026-08-19-wall-home-screen-design.md`. The last
4572 of three. `mux [TARGET]` is now a wall of one tile, entered zoomed; `mux wall`
4573 is the same program entered on the wall. There is one interaction loop in the
4574 tree, and it is a tile pump.
4575
4576 **The seam is the DIAL, not the loop.** `wallview.runAttach` opens the
4577 transport on the main thread — with the tty, before any of the wall exists —
4578 and hands it to the tile's pump, which `adopt`s it. Everything that made the
4579 old `client.attach` worth keeping lives on that side of the seam: a first
4580 contact reaches ssh through a shell and can want the terminal for a hostkey or
4581 a password (the `b1abaa3` fix); a bare `Ctrl-\` has to abort the wait; and a
4582 dial that never came up owes the user `openFailure`'s sentence and its exit
4583 code, not a wall with a tile stuck on `[connecting]`. Nothing above the dial
4584 needed a second copy, so nothing above it has one.
4585
4586 `Transport.adopt` exists for one fact: a `Transport`'s QUIC out-queue holds an
4587 allocator, the dialling thread's allocator is not the pump's, and two threads
4588 allocating from one non-thread-safe arena for a connection's whole life is a
4589 bug nobody would find twice. Legal only while the queue is empty, which is
4590 exactly the moment between `open` and the first frame — asserted, because
4591 there is no other such moment on any path.
4592
4593 **A session that ends under the zoom ends MUX when it was the wall's only
4594 tile, and drops to the wall when it was not.** This is the rule the
4595 convergence had to invent, and both halves are load-bearing. `mux` is what
4596 people put in scripts, so the single-tile case must propagate the shell's exit
4597 code exactly as the plain client did — a wall that always dropped to itself
4598 would hang a pipeline forever. And a wall with other tiles on it has somewhere
4599 to go and something to say when it gets there: phase 1 already built the
4600 dead-tile narration, so the drop lands on `[exited]` beside the survivors.
4601 A session ending while UNZOOMED is not this rule at all: its stripe narrates
4602 and the wall goes on, which is what a wall is for.
4603
4604 Review found the rule was stated with one half missing, and reproduced the
4605 cost: **a wall nobody can reach the keyboard of is not somewhere to drop a
4606 user.** A piped `mux` with two tiles whose zoomed session ends AFTER stdin
4607 has closed dropped to a wall it could never leave and sat in `poll(-1)`
4608 forever with the exit code in hand — RC=124, measured twice (and RC=7 with
4609 the fix, in 8s). The first cut enforced "nobody left to steer" only at the
4610 read that discovers EOF, so it caught the case where stdin closes on an
4611 unzoomed wall and missed the case where the wall arrives afterwards. The fix
4612 is where the fix belongs: `stdin_open` is an ARGUMENT to `endAction`, so the
4613 whole rule lives in the one pure function that states it and can be tabled in
4614 a unit test. That test now fails on the old behaviour, which is the point —
4615 `endAction` had none, and it is the function deciding whether `mux` returns a
4616 shell's exit code or does not return at all.
4617
4618 The same shape answers a refused attach. A tile a chord created and the daemon
4619 would not have (`Ctrl-\ c` on a full table) leaves the wall without a trace —
4620 it recorded nothing, because a refusal is precisely "no state since attach" —
4621 and the zoom goes back to the tile the chord was typed at. That is the plain
4622 client's `.refused` recovery, kept because the M2 leg is the only thing in the
4623 suite that reaches it.
4624
4625 **`d` and `w` stopped being the same key, and that is a deliberate break.**
4626 While the zoom was the child-spawn's replacement, `d` meant "come back",
4627 because the child was a real client and `Ctrl-\ d` detached it. Now that `mux`
4628 IS the wall, `d` has to keep the meaning a user's fingers already have —
4629 detach the session, leave mux — or the convergence breaks the muscle memory it
4630 is judged by. `w` is the model's key and the only way out of a zoom that stays
4631 inside mux. The in-place-zoom e2e leg moved from `d` to `w`; nothing else did.
4632
4633 **`n`/`p`/`c` are the DAEMON's ring, not the wall's tiles.** The spec's rule,
4634 and the reason the client's `sessions_req`/`ringNeighbour`/`PendingSwitch`
4635 machinery was reused rather than deleted: which sessions exist is the daemon's
4636 to say, so all three chords become one question asked over the zoomed tile's
4637 own link. A sibling with a tile is an instant zoom move at zero round trips; a
4638 sibling without one gets a tile, because visiting it is an attach and attach
4639 adds. The question is asked by the PUMP (a Transport has one owning thread)
4640 and answered to the KEYBOARD (only it may move a zoom or grow the wall), which
4641 is what the `ask`/`ans` pair on a tile is for. The `[no session list: upgrade
4642 muxd]` banner survives intact — same `PendingSwitch`, same deadline, painted
4643 by the pump that owns the terminal.
4644
4645 **Hydration is deferred to the first unzoom, and that is a decision.** `Ctrl-\
4646 w` shows THE wall — the saved file's tiles plus the one you are standing on —
4647 but a bare `mux` that dialled a long attach history before showing the session
4648 would have converged the code and broken the feel. Nothing is dialled until
4649 somebody asks to see the wall, deduped by spelling (the file's own rule), and
4650 a wall with no room for another stripe simply stops rather than growing a tile
4651 pointing at rows that belong to somebody else. Known sharp edge, recorded
4652 rather than fixed: a saved wall full of dead sockets makes the first `Ctrl-\ w`
4653 a screenful of `[connecting]`. `x` is the answer, and the model is telling the
4654 truth about what it was asked to remember.
4655
4656 **Frames before keys, and drain the whole burst that has arrived.** The
4657 convergence's own bug, found by the suite two runs in three. `Core.forward`
4658 decides what a wheel notch MEANS from the session's terminal modes, and those
4659 arrive at the END of a resync burst — snapshot, pty mode, title, modes. A
4660 single-threaded client hid the hazard by being busy painting the snapshot when
4661 the notch arrived; a keyboard on its own thread notices immediately, so the
4662 pump judged a notch against a session it had not finished listening to and an
4663 application holding the mouse lost it to this client's scrollback. Reading one
4664 frame per pass was never "one event, one frame" — it was one frame per event —
4665 so a pass now polls with a zero timeout and keeps reading while bytes are
4666 already there.
4667
4668 **Five smaller promises the old loop kept by where its code sat.** Each was
4669 invisible until the suite asked. A pump's end must ring the keyboard AFTER
4670 `alive` clears, or the wake finds nothing and a piped `mux` whose shell exited
4671 hangs. A tile promoted before its first snapshot must not repaint, or every
4672 `mux` opens by painting a blank grid the plain client never drew.
4673 `[reconnecting]` is a banner as well as a label, because a zoomed tile has no
4674 bar on screen. SIGWINCH has to be armed by the wall (no tile calls
4675 `takeTerminal`) and answered by the promoted pump, because a zoomed tile is a
4676 full-screen client and `mux` resizing with its terminal is part of "unchanged
4677 in feel"; a tile promoted after a resize adopts the size it missed.
4678 And `MUX_PREDICT_STATS` moved out of `Core.deinit`: a wall tile's Core is on a
4679 detached thread the process exit kills where it stands, so the counters are
4680 published to the driver that owns the exit and printed once, after the
4681 terminal is back — same line, same screen, same order.
4682
4683 **A terminal that cannot be MEASURED is still a terminal.** `Core.init` always
4684 answered those two questions apart — `is_tty` off stdin, the size off an ioctl
4685 that gives up below two columns — and the wall's first cut conflated them. A
4686 1×1 pty is a real terminal reporting a size nothing can paint at; the plain
4687 client took it over and painted 80×24 into it, and the conflation put that
4688 client on no alternate screen at all. The wall's terminal writes are gated on
4689 `is_tty` alone, which is also what lets a scripted `mux` on a pipe be a wall of
4690 one zoomed tile that writes exactly the bytes the old client wrote: no
4691 alternate screen, no raw mode, no clears, nothing to undo.
4692
4693 **The abort key inherited a rule from a client that had nowhere to go.** "While
4694 dialling or reconnecting there is no session to command, so a bare Ctrl-\
4695 aborts" was true of `drainStdinForQuit` because a plain client's only answer to
4696 a dead link was to leave. On a wall it is not: `Ctrl-\ w` while a tile is
4697 reconnecting means "show me the other tile", and the first cut scanned the raw
4698 chunk for `0x1c` and exited instead. The two readings of that byte are told
4699 apart by the filter that already exists — a `\x1c` still HOLDING OUT for its
4700 command key (`PrefixFilter.pending`, with no action produced) is the abort,
4701 while `\x1c w` is a chord — so the abort is judged after `feed`, not before it.
4702 Secondary, from the same review: the sentence was wrong for half the states it
4703 covered. Nothing has been "detached while reconnecting" from a first attach
4704 that never came up, so `.connecting` says `aborted before attaching` instead —
4705 which is also what `openFailure` says for an abort one layer down.
4706
4707 Two mutations, to check the new legs have teeth. Making a ring step re-dial a
4708 sibling that already has a tile fails `ring grow` at `the daemon accepted 3
4709 attaches (want 2)` — the flat-count assertion, and nothing else in the suite
4710 notices. Making a single-tile session end drop to the wall instead of exiting
4711 fails `exit semantics` at `the only tile's shell exited 7 and mux exited 124`,
4712 which is the timeout: a wall nobody can leave is exactly what that half of the
4713 rule exists to refuse, and the 124 says so in the failure's own words.