From 3040a4eb269d10814dbe4852afaf1015bad53ffd Mon Sep 17 00:00:00 2001 From: "prompt.ac/@jeffrey" Date: Mon, 7 Sep 2026 10:27:51 -0400 Subject: [PATCH] oskiewar relay: a claimed room lets go of the dead host's sequence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Joining and leaving both ends of a versus room verify clean end to end — join opens the fight in ~1.2s, a closed guest reparks the chair 2.6s later, the same link reseats a returning guest, and a dead host's room is claimed by its challenger in 2.7s. But after every claim the room sat wire-silent for ~14 seconds: the successor publishes from its own seq zero while publish()'s stale-frame guard still held the corpse's high-water mark, so one frame was dropped for every frame the old host had ever sent. The same leftover snapshot handed mid-handoff spectators a freeze-frame of the old fight and let the matchmaker offer a chair beside a corpse. addPublisher now drops the previous snapshot when a new publisher takes the chair; a test pins the takeover (fresh seq fans out, no stale frame served). Measured with a two-window CDP harness on blueberry: page enters the lobby 2s after the claim, publisher socket sends 30/s from 2.7s — only the relay was muting the room. Claude-Session: https://claude.ai/code/session_01Nj9XDa5sX89pPk8N1cwh4g --- session-server/oskiewar-live-manager.mjs | 12 +++++++++ session-server/oskiewar-live-manager.test.mjs | 26 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/session-server/oskiewar-live-manager.mjs b/session-server/oskiewar-live-manager.mjs index 514d9b7a10..634b1e2f77 100644 --- a/session-server/oskiewar-live-manager.mjs +++ b/session-server/oskiewar-live-manager.mjs @@ -203,6 +203,18 @@ export class OskiewarLiveManager { ws.close?.(4409, "Publisher already connected"); return; } + // A new publisher counts frames from its own zero, so the room lets go of + // the old one's snapshot — otherwise the stale-frame guard in publish() + // reads every frame of a claimed room as old until the successor + // out-counts the dead host's sequence (measured: ~14 silent seconds after + // each versus handoff, one for each second the old host had been live). + // Dropping the snapshot also stops a mid-handoff spectator being handed a + // freeze-frame of the previous fight, and stops the matchmaker offering a + // chair beside a corpse. + if (room.state) { + room.state = null; + room.publishedAt = 0; + } room.publisher = ws; room.publisherSurface = surface; room.updatedAt = this.now(); diff --git a/session-server/oskiewar-live-manager.test.mjs b/session-server/oskiewar-live-manager.test.mjs index d36890f57c..ec1f18a7f7 100644 --- a/session-server/oskiewar-live-manager.test.mjs +++ b/session-server/oskiewar-live-manager.test.mjs @@ -111,6 +111,32 @@ test("a second live publisher cannot take over a match", () => { assert.equal(second.closed?.code, 4409); }); +test("a successor publisher counts from zero — no corpse seq gate, no stale frame", () => { + let now = 100; + const manager = new OskiewarLiveManager({ now: () => now }); + const first = new FakeSocket(), viewer = new FakeSocket(); + const url = "/oskiewar-live?match=bafegu-dorimi-kunapo"; + manager.handleConnection(first, { url: `${url}&role=publisher` }); + manager.handleConnection(viewer, { url }); + now += 30; + first.emit("message", Buffer.from(JSON.stringify( + { type: "oskiewar:state", content: state(500) }))); + assert.equal(viewer.sent.at(-1).content.seq, 500); + first.close(); + const second = new FakeSocket(); + manager.handleConnection(second, { url: `${url}&role=publisher` }); + const late = new FakeSocket(); + manager.handleConnection(late, { url }); + assert.ok(!late.sent.some((m) => m.type === "oskiewar:state"), + "a mid-handoff spectator must not be handed the dead host's frame"); + now += 30; + second.emit("message", Buffer.from(JSON.stringify( + { type: "oskiewar:state", content: state(1) }))); + assert.equal(viewer.sent.at(-1).content.seq, 1, + "the claimed room must fan the new host's first frame, not mute it"); + assert.equal(late.sent.at(-1).content.seq, 1); +}); + test("a challenger is seated, watches state, and their presses reach the publisher", () => { let now = 100; const manager = new OskiewarLiveManager({ now: () => now }); -- 2.51.2