a73x

b7d2b15a

docs: the restore rule reaches the browser wall

a73x   2026-08-26 06:51

Commit message
docs: the restore rule reaches the browser wall

README.md
Old New
@@ -267,6 +267,11 @@ 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 267 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. 268 line, so a line from the file can be pasted back verbatim.
269 269
270 A daemon restart is survived here the same way it is on the CLI wall: a
271 saved line on a local socket (`--sock PATH#NAME`) comes back as a fresh
272 session, a remote one (`HOST#NAME`, `quic://HOST#NAME`) is joined only and
273 its tile reads `refused`.
274
270 Localhost only, deliberately: to see it from another machine, forward it — 275 Localhost only, deliberately: to see it from another machine, forward it —
271 `ssh -L 7681:127.0.0.1:7681 HOST`. 276 `ssh -L 7681:127.0.0.1:7681 HOST`.
272 277
docs/decisions.md
Old New
@@ -6795,3 +6795,74 @@ the state word still survives truncation at every width, which is the property
6795 that test pins. On a bar too narrow for `marker + " [state]"` the status is what 6795 that test pins. On a bar too narrow for `marker + " [state]"` the status is what
6796 the final `text[0..cols]` cut loses, exactly as before the digit; a two-digit 6796 the final `text[0..cols]` cut loses, exactly as before the digit; a two-digit
6797 tile reaches that width one column sooner. 6797 tile reaches that width one column sooner.
6798
6799 ## 2026-08-26 — the browser wall restores a saved local line too
6800
6801 The 2026-08-25 ruling above ("a saved local line attaches-or-creates") was a CLI
6802 ruling only. `muxweb` reads the same file and put up the same tiles, and every
6803 one of them attached at 0×0 — the passivity contract, so that a browser can
6804 never move a grid a human is looking at. The daemon reads 0×0 as JOIN, so after
6805 a restart the browser wall was exactly the thing the CLI had just stopped being:
6806 a grid of dead local tiles.
6807
6808 Ruled: the same predicate, both doors. It moved out of `wallview` and down into
6809 `client.hydratedCreates` — what the entry above calls `wallview.hydratedCreates`
6810 lives there now. Not into `wall.zig` with the spelling grammar, though the
6811 spelling is where the distinction is visible: `wall.Spec` is resolved away in
6812 both doors (`wallview.resolveSpelling`, `webhub.resolveTile`) long before either
6813 decides, and a predicate neither caller could reach without carrying a derived
6814 flag alongside the target is two owners wearing one name. `client.Target` is
6815 what both hold at the moment of the decision, so that is where the rule lives.
6816
6817 The hub does NOT get there the way the CLI does. A wall stripe can simply put
6818 its rect on the attach, because a stripe owns a rect; a browser tile owns
6819 nothing and must not start. So the hub reads the refusal instead — `exit_status`
6820 as the FIRST daemon frame after the browser's attach, which is mux.js's own
6821 discriminator mirrored one layer down — and calls `client.birthSession` on a
6822 connection of its own: one sized attach at the daemon's own default 80×24, wait
6823 for the snapshot, detach. Then it tears the tile's connection and re-dials
6824 exactly as a network tear does. The browser sees `reconnecting` → `up`, which it
6825 has always handled, re-attaches at 0×0 on its own, and is never told a refusal
6826 happened. Nothing in the page decides anything about creation, which is the
6827 point: the page is a stand-in for native UIs to come, and a rule living in it
6828 would have to be written again in each of them.
6829
6830 The side connection is not incidental. A tile that claimed a size to get its
6831 session made would go on claiming it, and the shared grid would then follow
6832 whichever browser reloaded last — the passivity contract broken to fix a
6833 symptom of it. Paying one attach and leaving hands the session back at a size
6834 nobody is bound to.
6835
6836 The bound is NOT per connection, and the first cut of this got that wrong. Every
6837 refusal closes the hub's connection — `serviceObserver` answers an unseated
6838 attach with `exit_status` and then `dropObserver`, which closes the fd — so a
6839 flag cleared on the tear bounds nothing: refuse, redial, re-attach, refuse
6840 again, and the hub would birth once per turn of a spin it cannot end. The flags
6841 that matter therefore outlive the dial. `birth_tried` is cleared by a GRID
6842 arriving, so one birth per healthy period: a daemon that refuses the birth too
6843 (a full table, a name it will not make) is answered once and then believed,
6844 while a second daemon restart still heals because the tile was healthy in
6845 between.
6846
6847 The other half of that is `ended`, and it is the same bug wearing the user's
6848 clothes. A shell the user ends with `exit` reaches the hub as exactly the
6849 restore case: the daemon reaps the session and drops its clients, the hub
6850 redials, mux.js re-attaches on `up`, and the attach is refused because the
6851 session is gone. Nothing in those frames distinguishes "this session never came
6852 back from a reboot" from "I just closed this". So the pump remembers that it
6853 watched this session die and stops creating — `wallview`'s ruling, which ENDS
6854 its pump on an exit_status and leaves the tile dead, reached by a pump that
6855 cannot end because it still serves a browser. Without it, `exit` hands the user
6856 a new shell, and a short-lived one forks a process per turn.
6857
6858 The name the hub creates is the TILE's, off the wall spelling, not one read out
6859 of the browser's attach frame — a page cannot talk the hub into making a session
6860 it was not already standing in front of.
6861
6862 A remote tile is untouched throughout: no birth, refusal forwarded, and it reads
6863 `refused` for the same reason the CLI stripe reads `[refused]`.
6864
6865 That word is new. The page said `session full`, which was a guess at WHY, and
6866 the daemon never says why: a missing session and a full table refuse with the
6867 same `exit_status 1`. Most of the time the guess was wrong. It says `refused`
6868 now — the CLI's word, for the CLI's reason.