From f45b22054e4f90b513bd5f0b0732f67cf6ee0613 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Thu, 4 Jun 2026 14:22:27 -0400 Subject: [PATCH] =?UTF-8?q?zebra-spaces:=20pick=20up=20epoch=20on=20peer-b?= =?UTF-8?q?ooted=20+=20role-change=20=E2=86=92=20release=20mod=20queue=20g?= =?UTF-8?q?ate?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mod-action serializer (runModSerial) was waiting for case 'state' to release the next sign — but no 'state' broadcast follows a successful kick/ban/promote. The server bumps rm.epoch and emits the action-specific event (peer-booted / role-change) carrying the new epoch. The client was already updating roomEpoch in role-change but never released the queue gate, and the peer-booted handler did neither — so kick/ban always burned the 1500ms fallback timeout AND signed the next action with the stale epoch (signal bounces it with "stale epoch"). Fox 2026-06-04: "couldn't kick until leaving as host" — leaving + welcome was the only thing that refreshed roomEpoch. Pair with signal-side fix that moves rm.epoch++ before the peer-booted broadcast and includes "epoch" in the payload. --- web/zebra-spaces.html | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/web/zebra-spaces.html b/web/zebra-spaces.html index 3c36d0b..9c2eaac 100644 --- a/web/zebra-spaces.html +++ b/web/zebra-spaces.html @@ -4236,6 +4236,11 @@ async function handleSignal(raw){ const prev = mm.role; mm.role = m.role; roomEpoch = m.epoch; + /* same gate-release as case 'state' / case 'peer-booted' — + * role-change carries a fresh epoch, so the next mod action + * (e.g. promote-then-kick) shouldn't have to wait the 1500ms + * timeout fallback before signing. */ + resolvePendingStateUpdate(); if (m.role === 'host') hostUUID = m.uuid; const subjPub = pubHexFromMsg(m, 'pubkey', 'pubkey_hex') || myKeys && (m.uuid===myUUID ? myKeys.pubHex : ''); const byPub = pubHexFromMsg(m, 'by_pubkey'); @@ -4314,6 +4319,15 @@ async function handleSignal(raw){ break; case 'peer-booted': { + /* Server bumps room epoch on every kick/ban and includes it in + * this broadcast. Pick it up + release the mod-action queue + * gate so the next kick signs against the fresh epoch — without + * this the second kick in a session hits "stale epoch". Fox + * 2026-06-04: "couldn't kick until leaving as host." */ + if (typeof m.epoch === 'number') { + roomEpoch = m.epoch; + resolvePendingStateUpdate(); + } const mm = members.get(m.uuid); const by = members.get(m.by); const victPub = pubHexFromMsg(m, 'pubkey') @@ -5870,8 +5884,8 @@ logLine('', 'ready — pick a handle, type a rendezvous code, enter the space');