28cd9a55
docs: the model ships — mux is the wall
a73x 2026-08-20 07:55
Commit message
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. | ||