a73x

d057e034

refactor: an unzoomed browser tile claims no size, like every other watcher

a73x   2026-08-20 11:53

Commit message
refactor: an unzoomed browser tile claims no size, like every other watcher

Passivity had two spellings. muxa and the CLI wall's stripes attach 0x0 —
no size claim at all — while an unzoomed browser tile attached 1x1. Both
are refused by applySize's floor of 2, so nothing behaved differently; the
tree simply said the same thing two ways, and one of them was the way that
had already caused a bug.

1x1 is the worse spelling because it is a size a real client sends: a
terminal can be one column wide. It therefore cannot mean "I claim
nothing" without also meaning "I want 1x1", and attachOrCreate's comment
records what that ambiguity cost — gating creation on nonzero forked a
shell whose engine, pty and winsize were all 1x1, which applySize then
refused to move, so the client that caused the size could never fix it.
0x0 is unambiguous: no client wants a zero-column terminal.

web/mux.js sends 0,0 unzoomed; the zoomed tile still claims the real
terminal. mux_attach_payload encodes what it is given and does not clamp,
which is deliberate and now said out loud — a size the wasm core refused
would be a second opinion on a rule the daemon owns.

The prose followed the wire. attachOrCreate, wasm_core's payload doc,
wsclient's attach seam and four e2e legs all described tiles as 1x1
attachers; the browser stand-in now scripts `attach 0 0` throughout.

Kept: "a 1x1 attach joins but never creates". It is not a relic of the old
spelling — it is the degenerate-input pin, and its comment now says so.
Nothing spells passivity as 1x1 any more, which is exactly why the daemon
has to stay unclaimable by it on the size threshold ALONE, with no contract
to lean on. e2e drives that client for real on a genuine 1x1 pty, so the
input is reachable rather than theoretical; that leg lost its browser
framing and its misleading "web passivity" failure label with it.

web/verify.js pinned the wall spelling as 1x1 in as many words. Now 0x0 —
and because a 0x0 round-trip cannot tell a working encoder from one that
writes nothing, the 80x24 case grew the cols/rows assertions the zero case
can no longer make. Watched fail on a mutant.

zig build check rc=0, make test rc=0 (706 wasm ABI checks), make e2e rc=0,
pin unmoved at 55 scenarios / 35 convergence points.

src/server.zig
Old New
@@ -784,15 +784,18 @@ pub const Server = struct {
784 784
785 /// Attach-or-create. Creation demands a size the session can actually 785 /// Attach-or-create. Creation demands a size the session can actually
786 /// live at, and that is the SAME threshold applySize enforces — not a 786 /// live at, and that is the SAME threshold applySize enforces — not a
787 /// weaker one. A 0x0 attach makes no size claim (muxa send) and a 787 /// weaker one. A 0x0 attach makes no size claim at all — muxa, the CLI
788 /// client with no size must never be the reason a shell spawns; but 788 /// wall's stripes and an unzoomed browser tile all spell passivity that
789 /// 1x1 is worse than 0x0, because it is a size a client really sends: 789 /// way — and a client with no size must never be the reason a shell
790 /// every unzoomed BROWSER wall tile attaches 1x1 (web/mux.js); the CLI 790 /// spawns.
791 /// wall's stripes attach 0x0. Gating on 791 ///
792 /// "nonzero" let `muxweb host#newname` fork a shell whose engine, pty 792 /// The threshold has to be the real one and not merely "nonzero",
793 /// because 1x1 is a size a client genuinely sends: a terminal can be
794 /// one column wide. Gating on nonzero forked a shell whose engine, pty
793 /// and winsize were all 1x1 — and then applySize refused to move it, 795 /// and winsize were all 1x1 — and then applySize refused to move it,
794 /// recordSize was skipped, the slot stayed 0x0, and claimGrid could 796 /// recordSize was skipped, the slot stayed 0x0, and claimGrid could
795 /// never claim, so the tile that caused the size could never fix it. 797 /// never claim, so the client that caused the size could never fix it.
798 /// A session nobody can use is worse than a refusal nobody can miss.
796 /// One rule, spelled once, in both places that decide it. 799 /// One rule, spelled once, in both places that decide it.
797 /// 800 ///
798 /// Null is a refusal (bad name, table full, too small to create at, or 801 /// Null is a refusal (bad name, table full, too small to create at, or
@@ -7420,13 +7423,16 @@ test "Server: a 1x1 attach joins but never creates" {
7420 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" }); 7423 var srv = try Server.init(alloc, .{ .sock_path = sock_path, .shell = "/bin/cat" });
7421 defer srv.deinit(); 7424 defer srv.deinit();
7422 7425
7423 // 1x1 is the size every unzoomed BROWSER wall tile attaches at 7426 // Unlike 0x0, nothing spells passivity as 1x1 — which is exactly why
7424 // (web/mux.js) — the CLI wall's stripes attach 0x0 — so 7427 // this case needs its own pin. 0x0 is a contract and gets refused by
7425 // unlike 0x0 this is a size a real client really sends. It must be 7428 // one; 1x1 is a real terminal, one column wide, and the daemon has to
7426 // refused for creation exactly as 0x0 is: applySize will not move a 7429 // stay unclaimable by it on the size threshold alone. e2e drives that
7430 // client for real, on a genuine 1x1 pty.
7431 //
7432 // Refused for creation exactly as 0x0 is: applySize will not move a
7427 // grid to 1x1, so a session created at 1x1 could never be resized by 7433 // grid to 1x1, so a session created at 1x1 could never be resized by
7428 // the tile that caused it — a shell nobody can use, spawned by opening 7434 // the client that caused it — a shell nobody can use, spawned by
7429 // a wall page with a name nobody had created yet. 7435 // attaching with a name nobody had created yet.
7430 const c1 = try std.net.connectUnixSocket(sock_path); 7436 const c1 = try std.net.connectUnixSocket(sock_path);
7431 defer c1.close(); 7437 defer c1.close();
7432 try attachNamed(c1.handle, 1, 1, "tile"); 7438 try attachNamed(c1.handle, 1, 1, "tile");
@@ -7445,8 +7451,9 @@ test "Server: a 1x1 attach joins but never creates" {
7445 f2.deinit(alloc); 7451 f2.deinit(alloc);
7446 const si = srv.findSession("tile") orelse return error.NotCreated; 7452 const si = srv.findSession("tile") orelse return error.NotCreated;
7447 7453
7448 // ...and the same 1x1 attach now JOINS it, leaving the grid alone. That 7454 // ...and the same 1x1 attach now JOINS it, leaving the grid alone.
7449 // is the passivity contract: a tile may watch any session, and may 7455 // Refusing the creation must not cost the client its view: a terminal
7456 // too small to spawn a session is still allowed to watch one, and may
7450 // never be the reason one exists or the reason one resizes. 7457 // never be the reason one exists or the reason one resizes.
7451 const c3 = try std.net.connectUnixSocket(sock_path); 7458 const c3 = try std.net.connectUnixSocket(sock_path);
7452 defer c3.close(); 7459 defer c3.close();
src/wasm_core.zig
Old New
@@ -390,9 +390,11 @@ export fn mux_mark_all_dirty() void {
390 // --------------------------------------------------------------------- 390 // ---------------------------------------------------------------------
391 391
392 /// The whole 20-byte attach payload into the output buffer, so JS never 392 /// The whole 20-byte attach payload into the output buffer, so JS never
393 /// hand-assembles a u64. `cols`/`rows` are what this client claims — a 393 /// hand-assembles a u64. `cols`/`rows` are what this client claims — an
394 /// wall tile passes 1x1 (the passivity contract), the zoomed tile its 394 /// unzoomed wall tile passes 0x0 and claims nothing (the passivity
395 /// real size. Quotes the replica's resume coordinates; pass fresh=1 to 395 /// contract), the zoomed tile its real size. Encoded as given: a size
396 /// this refused would be a second opinion on a rule server.zig owns.
397 /// Quotes the replica's resume coordinates; pass fresh=1 to
396 /// quote (0,0) instead (the resync re-attach). 398 /// quote (0,0) instead (the resync re-attach).
397 export fn mux_attach_payload(cols: u32, rows: u32, fresh: u32) i32 { 399 export fn mux_attach_payload(cols: u32, rows: u32, fresh: u32) i32 {
398 const c = core orelse return -1; 400 const c = core orelse return -1;
test/e2e.sh
Old New
@@ -3998,7 +3998,7 @@ wait_sock "$SOCK21" "$OUT.m18.d" "M18 multi-session daemon never bound"
3998 # Two clients, two names, one socket. Each plants a marker its own shell 3998 # Two clients, two names, one socket. Each plants a marker its own shell
3999 # has to EXPAND — the typed line reads `printf "m18a-%s\n" pin` and only 3999 # has to EXPAND — the typed line reads `printf "m18a-%s\n" pin` and only
4000 # the output reads `m18a-pin`, so a grep that hits is the shell's work and 4000 # the output reads `m18a-pin`, so a grep that hits is the shell's work and
4001 # not an echo of our keystrokes (the M-web passivity block's trick). 4001 # not an echo of our keystrokes (the tiny-attach block's trick).
4002 # 4002 #
4003 # Both run in the foreground and DETACH (\034\034, the Ctrl-\ chord) rather than staying 4003 # Both run in the foreground and DETACH (\034\034, the Ctrl-\ chord) rather than staying
4004 # up: the session has to outlive its client for every assertion below to 4004 # up: the session has to outlive its client for every assertion below to
@@ -4131,7 +4131,7 @@ wait_for "$OUT.m18wh" "serving" 10 || {
4131 echo "e2e FAIL: M18 wall hub never reported serving"; cat "$OUT.m18wh"; exit 1; } 4131 echo "e2e FAIL: M18 wall hub never reported serving"; cat "$OUT.m18wh"; exit 1; }
4132 4132
4133 # Content for each session, planted by a CLI client and left behind: the 4133 # Content for each session, planted by a CLI client and left behind: the
4134 # tiles are passive 1x1 wall tiles and can never type anything themselves. 4134 # tiles are passive 0x0 wall tiles and can never type anything themselves.
4135 { printf 'printf "wall-a-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \ 4135 { printf 'printf "wall-a-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \
4136 timeout 40 "$MUX" --sock "$SOCK22" --session a > "$OUT.m18wa" 2> "$OUT.m18wa.err" 4136 timeout 40 "$MUX" --sock "$SOCK22" --session a > "$OUT.m18wa" 2> "$OUT.m18wa.err"
4137 wait_grid "$SOCK22" "wall-a-pin" "M18 wall: session a's marker" a 4137 wait_grid "$SOCK22" "wall-a-pin" "M18 wall: session a's marker" a
@@ -4141,8 +4141,8 @@ wait_grid "$SOCK22" "wall-b-pin" "M18 wall: session b's marker" b
4141 4141
4142 # Tile 0 is session a's; tile 1 is session b's. The stand-in spells the 4142 # Tile 0 is session a's; tile 1 is session b's. The stand-in spells the
4143 # name the way mux.js does — bytes appended after the fixed 20 — and 4143 # name the way mux.js does — bytes appended after the fixed 20 — and
4144 # attaches at 1x1, the passivity contract: both sessions already exist, so 4144 # attaches at 0x0, the passivity contract: both sessions already exist, so
4145 # these are joins, and applySize refuses to move an 80-wide grid to 1. 4145 # these are joins, and a slot that claimed no size can never move the grid.
4146 for _t in 0 1; do 4146 for _t in 0 1; do
4147 case "$_t" in 4147 case "$_t" in
4148 0) _sn=a; _mine=wall-a-pin; _theirs=wall-b-pin ;; 4148 0) _sn=a; _mine=wall-a-pin; _theirs=wall-b-pin ;;
@@ -4151,7 +4151,7 @@ for _t in 0 1; do
4151 set +e 4151 set +e
4152 timeout 40 "$WSCLIENT" --port "$WPORT3" --tile "$_t" \ 4152 timeout 40 "$WSCLIENT" --port "$WPORT3" --tile "$_t" \
4153 --out "$OUT.m18ws$_t" --err "$OUT.m18ws$_t.err" <<EOF 4153 --out "$OUT.m18ws$_t" --err "$OUT.m18ws$_t.err" <<EOF
4154 attach 1 1 $_sn 4154 attach 0 0 $_sn
4155 expectstate up 10000 4155 expectstate up 10000
4156 expectgrid $_mine 15000 4156 expectgrid $_mine 15000
4157 settle 500 10000 4157 settle 500 10000
@@ -4298,17 +4298,20 @@ assert_stopped "$SOCK23" "$D20PID" "M18 quic multi-session" "$OUT.m18qstop"
4298 D20PID="" 4298 D20PID=""
4299 ok "quic: two dials are two sessions, and one name is one session on either transport" 4299 ok "quic: two dials are two sessions, and one name is one session on either transport"
4300 4300
4301 # --- M-web (a): a 1x1 attach is refused the grid and can never claim it. 4301 # --- a 1x1 attach is refused the grid and can never claim it.
4302 # The wall tile's passivity contract, pinned DAEMON-SIDE and hub-free on 4302 # The degenerate size a client can genuinely arrive with — a terminal one
4303 # purpose: the mechanism is applySize's cols<2 refusal plus claimGrid's 4303 # column wide — driven here on a real 1x1 pty. Nothing spells passivity
4304 # 0x0-slot refusal (server.zig), and it must hold whether or not muxweb 4304 # this way (that is 0x0, and it is refused by contract); this leg is the
4305 # exists. The probe for "the grid did not move" is textual: a marker 4305 # daemon staying unclaimable on the size threshold ALONE. Pinned
4306 # DAEMON-SIDE and hub-free on purpose: the mechanism is applySize's cols<2
4307 # refusal plus claimGrid's 0x0-slot refusal (server.zig), and it must hold
4308 # for any client that sends a size nobody can live at. The probe for "the grid did not move" is textual: a marker
4306 # string typed at 80 wide can only appear CONTIGUOUS in `muxd dump` if 4309 # string typed at 80 wide can only appear CONTIGUOUS in `muxd dump` if
4307 # the grid is still 80 wide — a 1-column grid puts every glyph on its 4310 # the grid is still 80 wide — a 1-column grid puts every glyph on its
4308 # own row, so grep itself is the geometry assertion. 4311 # own row, so grep itself is the geometry assertion.
4309 "$MUXD" run --sock "$SOCK18" --shell /bin/sh > "$OUT.weba.d" 2>&1 & 4312 "$MUXD" run --sock "$SOCK18" --shell /bin/sh > "$OUT.weba.d" 2>&1 &
4310 D14PID=$! 4313 D14PID=$!
4311 wait_sock "$SOCK18" "$OUT.weba.d" "web passivity daemon never bound" 4314 wait_sock "$SOCK18" "$OUT.weba.d" "tiny attach daemon never bound"
4312 4315
4313 # The 80x24 client: types the first marker, holds the session open long 4316 # The 80x24 client: types the first marker, holds the session open long
4314 # enough for the 1x1 attacher to come and go, types the second marker 4317 # enough for the 1x1 attacher to come and go, types the second marker
@@ -4316,7 +4319,7 @@ wait_sock "$SOCK18" "$OUT.weba.d" "web passivity daemon never bound"
4316 { printf 'printf "wall-%%s\\n" pin\n'; sleep 4; printf 'printf "zz-%%s\\n" web\n'; sleep 2; printf '\034\034'; } | \ 4319 { printf 'printf "wall-%%s\\n" pin\n'; sleep 4; printf 'printf "zz-%%s\\n" web\n'; sleep 2; printf '\034\034'; } | \
4317 timeout 40 "$MUX" --sock "$SOCK18" > "$OUT.weba" 2> "$OUT.weba.err" & 4320 timeout 40 "$MUX" --sock "$SOCK18" > "$OUT.weba" 2> "$OUT.weba.err" &
4318 WCLIPID=$! 4321 WCLIPID=$!
4319 wait_grid "$SOCK18" "wall-pin" "web passivity: the first marker" 4322 wait_grid "$SOCK18" "wall-pin" "tiny attach: the first marker"
4320 4323
4321 # The 1x1 attacher, on a real 1x1 pty. Its attach is answered with a 4324 # The 1x1 attacher, on a real 1x1 pty. Its attach is answered with a
4322 # unicast snapshot (the alt-screen enter proves the first frame came), 4325 # unicast snapshot (the alt-screen enter proves the first frame came),
@@ -4336,16 +4339,16 @@ EOF
4336 RC=$? 4339 RC=$?
4337 set -e 4340 set -e
4338 [ "$RC" -eq 0 ] || { 4341 [ "$RC" -eq 0 ] || {
4339 echo "e2e FAIL: web passivity: 1x1 client exited $RC" 4342 echo "e2e FAIL: tiny attach: 1x1 client exited $RC"
4340 cat "$OUT.web1x1.log"; cat -v "$OUT.web1x1.err" 2>/dev/null; exit 1; } 4343 cat "$OUT.web1x1.log"; cat -v "$OUT.web1x1.err" 2>/dev/null; exit 1; }
4341 4344
4342 # The geometry assertion: both markers contiguous AFTER the 1x1 client 4345 # The geometry assertion: both markers contiguous AFTER the 1x1 client
4343 # attached, typed, and left. A grid moved to 1 wide cannot hold either. 4346 # attached, typed, and left. A grid moved to 1 wide cannot hold either.
4344 "$MUXD" dump --sock "$SOCK18" | grep -q "wall-pin" || { 4347 "$MUXD" dump --sock "$SOCK18" | grep -q "wall-pin" || {
4345 echo "e2e FAIL: web passivity: first marker lost after 1x1 attach:" 4348 echo "e2e FAIL: tiny attach: first marker lost after 1x1 attach:"
4346 "$MUXD" dump --sock "$SOCK18"; exit 1; } 4349 "$MUXD" dump --sock "$SOCK18"; exit 1; }
4347 "$MUXD" dump --sock "$SOCK18" | grep -q "zz-web" || { 4350 "$MUXD" dump --sock "$SOCK18" | grep -q "zz-web" || {
4348 echo "e2e FAIL: web passivity: the 1x1 attacher moved the grid (marker not contiguous):" 4351 echo "e2e FAIL: tiny attach: the 1x1 attacher moved the grid (marker not contiguous):"
4349 "$MUXD" dump --sock "$SOCK18"; exit 1; } 4352 "$MUXD" dump --sock "$SOCK18"; exit 1; }
4350 4353
4351 set +e 4354 set +e
@@ -4353,10 +4356,10 @@ wait "$WCLIPID"
4353 RC=$? 4356 RC=$?
4354 set -e 4357 set -e
4355 WCLIPID="" 4358 WCLIPID=""
4356 [ "$RC" -eq 0 ] || { echo "e2e FAIL: web passivity: 80x24 client exited $RC"; cat "$OUT.weba.err"; exit 1; } 4359 [ "$RC" -eq 0 ] || { echo "e2e FAIL: tiny attach: 80x24 client exited $RC"; cat "$OUT.weba.err"; exit 1; }
4357 assert_converged "$OUT.weba" "$SOCK18" "web passivity: the 80x24 client never glitched" 4360 assert_converged "$OUT.weba" "$SOCK18" "tiny attach: the 80x24 client never glitched"
4358 4361
4359 assert_stopped "$SOCK18" "$D14PID" "web passivity" "$OUT.webstop" 4362 assert_stopped "$SOCK18" "$D14PID" "tiny attach" "$OUT.webstop"
4360 D14PID="" 4363 D14PID=""
4361 ok "a 1x1 attach is refused the grid and can never claim it" 4364 ok "a 1x1 attach is refused the grid and can never claim it"
4362 4365
@@ -4374,12 +4377,12 @@ wait_for "$OUT.webh" "serving" 10 || {
4374 WCLIPID=$! 4377 WCLIPID=$!
4375 wait_grid "$SOCK19" "web-b1" "web hub: the typing client's marker" 4378 wait_grid "$SOCK19" "web-b1" "web hub: the typing client's marker"
4376 4379
4377 # The browser stand-in: a passive 1x1 wall tile through the hub. Its 4380 # The browser stand-in: a passive 0x0 wall tile through the hub. Its
4378 # final grid is the daemon's replica as REPLAYED THROUGH the WebSocket 4381 # final grid is the daemon's replica as REPLAYED THROUGH the WebSocket
4379 # leg, dumped in muxd dump's own format. 4382 # leg, dumped in muxd dump's own format.
4380 set +e 4383 set +e
4381 timeout 40 "$WSCLIENT" --port "$WPORT" --tile 0 --out "$OUT.webws" --err "$OUT.webws.err" <<'EOF' 4384 timeout 40 "$WSCLIENT" --port "$WPORT" --tile 0 --out "$OUT.webws" --err "$OUT.webws.err" <<'EOF'
4382 attach 1 1 4385 attach 0 0
4383 expectstate up 10000 4386 expectstate up 10000
4384 expectgrid web-b1 15000 4387 expectgrid web-b1 15000
4385 settle 500 10000 4388 settle 500 10000
@@ -4445,12 +4448,12 @@ wait_for "$OUT.webh2" "serving" 10 || {
4445 wait_grid "$SOCK20" "web-c1" "web tear: the first session's marker" 4448 wait_grid "$SOCK20" "web-c1" "web tear: the first session's marker"
4446 4449
4447 timeout 90 "$WSCLIENT" --port "$WPORT2" --tile 0 --out "$OUT.webws2" --err "$OUT.webws2.err" <<'EOF' & 4450 timeout 90 "$WSCLIENT" --port "$WPORT2" --tile 0 --out "$OUT.webws2" --err "$OUT.webws2.err" <<'EOF' &
4448 attach 1 1 4451 attach 0 0
4449 expectstate up 10000 4452 expectstate up 10000
4450 expectgrid web-c1 15000 4453 expectgrid web-c1 15000
4451 expectstate reconnecting 25000 4454 expectstate reconnecting 25000
4452 expectstate up 25000 4455 expectstate up 25000
4453 attach 1 1 4456 attach 0 0
4454 expectgrid web-c2 30000 4457 expectgrid web-c2 30000
4455 settle 500 10000 4458 settle 500 10000
4456 dumpexit 4459 dumpexit
@@ -4549,9 +4552,10 @@ RC=$(curl -s -o /dev/null -w '%{http_code}' -H "Origin: $DWORIG" -X POST \
4549 4552
4550 # A POSTed tile is a REAL tile: a marker typed through the CLI door is read 4553 # A POSTed tile is a REAL tile: a marker typed through the CLI door is read
4551 # back through the WebSocket door of the tile the page just created. The 4554 # back through the WebSocket door of the tile the page just created. The
4552 # stand-in attaches at 1x1 and the CLI client has already detached, which is 4555 # stand-in attaches at 0x0 and the CLI client has already detached, so the
4553 # the M-web (a) contract in passing — a 1x1 attach never claims the grid, so 4556 # passivity contract is in play in passing — a tile that claimed no size
4554 # the 80-wide marker is still one contiguous string to expect. 4557 # never moves the grid, and the 80-wide marker is still one contiguous
4558 # string to expect.
4555 set +e 4559 set +e
4556 { printf 'printf "dyn-%%s\\n" w1\n'; sleep 4; printf '\034\034'; } | \ 4560 { printf 'printf "dyn-%%s\\n" w1\n'; sleep 4; printf '\034\034'; } | \
4557 timeout 40 "$MUX" --sock "$SOCK25" > "$OUT.dwcli" 2> "$OUT.dwcli.err" 4561 timeout 40 "$MUX" --sock "$SOCK25" > "$OUT.dwcli" 2> "$OUT.dwcli.err"
@@ -4562,7 +4566,7 @@ set -e
4562 wait_grid "$SOCK25" "dyn-w1" "dyn wall: CLI marker" 4566 wait_grid "$SOCK25" "dyn-w1" "dyn wall: CLI marker"
4563 set +e 4567 set +e
4564 timeout 40 "$WSCLIENT" --port "$WPORT4" --tile 0 --out "$OUT.dwws" --err "$OUT.dwws.err" <<'EOF' 4568 timeout 40 "$WSCLIENT" --port "$WPORT4" --tile 0 --out "$OUT.dwws" --err "$OUT.dwws.err" <<'EOF'
4565 attach 1 1 4569 attach 0 0
4566 expectstate up 10000 4570 expectstate up 10000
4567 expectgrid dyn-w1 15000 4571 expectgrid dyn-w1 15000
4568 dumpexit 4572 dumpexit
test/wsclient.zig
Old New
@@ -158,15 +158,16 @@ const Client = struct {
158 } 158 }
159 159
160 /// The ONE place an attach frame is built, which is what keeps the 160 /// The ONE place an attach frame is built, which is what keeps the
161 /// passivity contract honest. A wall tile attaches at its own size — 161 /// passivity contract honest. An unzoomed wall tile attaches at 0x0 —
162 /// 1x1, refused the grid by applySize and refused claimGrid forever 162 /// no size claim at all, refused the grid by applySize and refused
163 /// after — and every LATER attach has to quote that same size. The 163 /// claimGrid forever after — and every LATER attach has to quote the
164 /// resync path is the trap: re-attaching at the grid the snapshot 164 /// same size the script chose. The resync path is the trap: re-attaching
165 /// taught us (80x24) is an attach at a differing size, which moves the 165 /// at the grid the snapshot taught us (80x24) is an attach at a
166 /// shared grid and repaints every other client, so a stand-in that did 166 /// differing size, which moves the shared grid and repaints every other
167 /// that would be pinning the opposite of the contract. The browser has 167 /// client, so a stand-in that did that would be pinning the opposite of
168 /// the same rule structurally: mux.js routes every attach through 168 /// the contract. The browser has the same rule structurally: mux.js
169 /// sendAttach, which quotes the tile's scripted size. 169 /// routes every attach through sendAttach, which quotes the tile's
170 /// scripted size.
170 /// 171 ///
171 /// `fresh` quotes (0,0) instead of the replica's resume coordinates — 172 /// `fresh` quotes (0,0) instead of the replica's resume coordinates —
172 /// what a resync needs, since there the replica is the suspect part. 173 /// what a resync needs, since there the replica is the suspect part.
@@ -574,12 +575,12 @@ test "ws reader: split delivery reassembles; server frames arrive unmasked" {
574 575
575 test "the resync re-attach quotes the TILE's size, never the grid it learned" { 576 test "the resync re-attach quotes the TILE's size, never the grid it learned" {
576 // The passivity contract, pinned at the WS WRITE SEAM: a wall tile 577 // The passivity contract, pinned at the WS WRITE SEAM: a wall tile
577 // attaches at 1x1, learns the authoritative 80x24 grid from the 578 // attaches at 0x0, learns the authoritative 80x24 grid from the
578 // snapshot, and then hits a garbled delta. The re-attach that follows 579 // snapshot, and then hits a garbled delta. The re-attach that follows
579 // must still say 1x1 — quoting the learned grid would be an attach at 580 // must still claim nothing — quoting the learned grid would be an
580 // a differing size, which claims the shared session and repaints every 581 // attach at a differing size, which claims the shared session and
581 // other client, i.e. the exact opposite of the contract scenario (a) 582 // repaints every other client, i.e. the exact opposite of what a
582 // exists to hold. 583 // passive tile is for.
583 const alloc = std.testing.allocator; 584 const alloc = std.testing.allocator;
584 const fds = try std.posix.pipe(); 585 const fds = try std.posix.pipe();
585 defer std.posix.close(fds[0]); 586 defer std.posix.close(fds[0]);
web/mux.js
Old New
@@ -427,11 +427,15 @@ class Tile {
427 } 427 }
428 } 428 }
429 sendAttach(fresh) { 429 sendAttach(fresh) {
430 // THE PASSIVITY CONTRACT (spec amendment 1): a wall tile attaches at 430 // THE PASSIVITY CONTRACT: an unzoomed wall tile attaches at 0x0, which
431 // 1x1. The daemon refuses the degenerate size, answers a unicast 431 // is how the CLI wall's stripes and muxa spell the same thing — no size
432 // snapshot carrying the true grid, and the slot stays 0x0 forever — 432 // claim at all. The daemon answers a unicast snapshot carrying the true
433 // this tile can never move the shared session. Only the zoomed tile 433 // grid and the slot stays 0x0 forever, so this tile can never move the
434 // claims its real size. 434 // shared session. Only the zoomed tile claims its real size.
435 //
436 // 0x0 rather than a small size on purpose: 1x1 is a size a client on a
437 // tiny terminal genuinely sends, so it cannot mean "no claim" without
438 // also meaning "this client wants 1x1" (server.zig attachOrCreate).
435 // ONE gate for every attach, wherever it comes from — the `up` 439 // ONE gate for every attach, wherever it comes from — the `up`
436 // control message included. A tile that gave up on replaying must 440 // control message included. A tile that gave up on replaying must
437 // not be talked back into asking for the same frame again. 441 // not be talked back into asking for the same frame again.
@@ -441,8 +445,8 @@ class Tile {
441 this.cancelScrollRequest(); 445 this.cancelScrollRequest();
442 this.clearSelection(); 446 this.clearSelection();
443 if (this.replayDead) return; 447 if (this.replayDead) return;
444 const cols = this.zoomed ? this.zoomCols() : 1; 448 const cols = this.zoomed ? this.zoomCols() : 0;
445 const rows = this.zoomed ? this.zoomRows() : 1; 449 const rows = this.zoomed ? this.zoomRows() : 0;
446 const n = this.core.mux_attach_payload(cols, rows, fresh ? 1 : 0); 450 const n = this.core.mux_attach_payload(cols, rows, fresh ? 1 : 0);
447 if (n > 0) { 451 if (n > 0) {
448 // The session name is transport dressing, appended AFTER the payload 452 // The session name is transport dressing, appended AFTER the payload
@@ -1637,11 +1641,12 @@ function buildAddTile() {
1637 const body = await res.json().catch(() => null); 1641 const body = await res.json().catch(() => null);
1638 input.value = ''; 1642 input.value = '';
1639 await refetchWall(); 1643 await refetchWall();
1640 // A wall tile attaches 1x1 passive and the daemon refuses to CREATE a 1644 // A wall tile attaches passive at 0x0 and the daemon refuses to
1641 // session at that size, so a tile added for a session that does not 1645 // CREATE a session for a client claiming no size, so a tile added for
1642 // exist yet would sit on a badge that never resolves. Zoom attaches at 1646 // a session that does not exist yet would sit on a badge that never
1643 // the real size, and that is what creates it — "new session here" has 1647 // resolves. Zoom attaches at the real size, and that is what creates
1644 // to land the user in the shell they asked for. 1648 // it — "new session here" has to land the user in the shell they
1649 // asked for.
1645 const fresh = body === null ? null : tilesById.get(body.id); 1650 const fresh = body === null ? null : tilesById.get(body.id);
1646 if (fresh) { 1651 if (fresh) {
1647 await fresh.startup; // refetchWall fires start(), it does not await it 1652 await fresh.startup; // refetchWall fires start(), it does not await it
web/verify.js
Old New
@@ -2548,7 +2548,7 @@ async function main() {
2548 // shell never wanted them (it cannot hand-assemble a u64 anyway), so 2548 // shell never wanted them (it cannot hand-assemble a u64 anyway), so
2549 // the payload IS the interface, and what asserts on it is this. 2549 // the payload IS the interface, and what asserts on it is this.
2550 const resumeArgs = () => { 2550 const resumeArgs = () => {
2551 e.mux_attach_payload(1, 1, 0); 2551 e.mux_attach_payload(0, 0, 0);
2552 const b = outBytes(); 2552 const b = outBytes();
2553 return { seq: b.readBigUInt64LE(4), epoch: b.readBigUInt64LE(12) }; 2553 return { seq: b.readBigUInt64LE(4), epoch: b.readBigUInt64LE(12) };
2554 }; 2554 };
@@ -2755,11 +2755,11 @@ async function main() {
2755 clientAction.selection, 2755 clientAction.selection,
2756 ); 2756 );
2757 2757
2758 // --- attach payload before any state: quotes (0,0); wall spelling 1x1 --- 2758 // --- attach payload before any state: quotes (0,0); wall spelling 0x0 ---
2759 check('attach len', e.mux_attach_payload(1, 1, 0), 20); 2759 check('attach len', e.mux_attach_payload(0, 0, 0), 20);
2760 let att = outBytes(); 2760 let att = outBytes();
2761 check('attach cols', att.readUInt16LE(0), 1); 2761 check('attach cols', att.readUInt16LE(0), 0);
2762 check('attach rows', att.readUInt16LE(2), 1); 2762 check('attach rows', att.readUInt16LE(2), 0);
2763 check('attach seq0', att.readBigUInt64LE(4), 0n); 2763 check('attach seq0', att.readBigUInt64LE(4), 0n);
2764 check('attach epoch0', att.readBigUInt64LE(12), 0n); 2764 check('attach epoch0', att.readBigUInt64LE(12), 0n);
2765 2765
@@ -2802,6 +2802,10 @@ async function main() {
2802 // --- attach after state: quotes what we hold; fresh=1 re-quotes zero --- 2802 // --- attach after state: quotes what we hold; fresh=1 re-quotes zero ---
2803 e.mux_attach_payload(80, 24, 0); 2803 e.mux_attach_payload(80, 24, 0);
2804 att = outBytes(); 2804 att = outBytes();
2805 // The size really is encoded and not merely left zero — which the wall
2806 // spelling above, being 0x0, can no longer tell you on its own.
2807 check('attach cols held', att.readUInt16LE(0), 80);
2808 check('attach rows held', att.readUInt16LE(2), 24);
2805 check('attach seq held', att.readBigUInt64LE(4), 8n); 2809 check('attach seq held', att.readBigUInt64LE(4), 8n);
2806 check('attach epoch held', att.readBigUInt64LE(12), 0xabcdn); 2810 check('attach epoch held', att.readBigUInt64LE(12), 0xabcdn);
2807 e.mux_attach_payload(80, 24, 1); 2811 e.mux_attach_payload(80, 24, 1);