b7d2b15a
docs: the restore rule reaches the browser wall
a73x 2026-08-26 06:51
Commit message
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. | ||