zebra-spaces: pick up epoch on peer-booted + role-change → release mod queue gate

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.
This commit is contained in:
Russell Ballestrini 2026-06-04 14:22:27 -04:00
parent 6972424052
commit f45b22054e
No known key found for this signature in database

View file

@ -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');
<footer style="margin:2.2rem auto 0;font-size:0.65rem;color:#999;line-height:1.7;word-break:break-all;font-family:monospace">
<span id="pi-seal" style="color:#777;cursor:default;user-select:none" title="">page integrity</span> &nbsp;·&nbsp; built <span class="stamp-date">2026-06-04</span><br>
md5 <span class="stamp-md5">e1e3c08e99aff562a8434fe4fd943462</span><br>
sha256 <span class="stamp-sha">1a64b52c3792eed4d8aa4001d9d3347938039f398c3201166ec3bfd99d6c0d12</span><br>
md5 <span class="stamp-md5">67528afd1d51390c2a2ab8da5fa00105</span><br>
sha256 <span class="stamp-sha">cf6c2988e44172286d60aeadde0f448f5b3b91fdf1270129cfa2ebae8213fe21</span><br>
<span style="color:#bbb">hashes are of this page with these two fields zeroed — to verify, blank them and re-hash</span><br>
<span style="color:#bbb">one self-contained file — <strong>save a copy</strong> and verify against these hashes; point at your own servers with ?signal= and ?turncred=, or <a href="host-your-own.html" style="color:#999">host your own community</a></span>
</footer>