A refused seat is logged, and the wallet drops a seat only on the second refusal in a row

A lost seat mid-game is the costliest quiet failure this table has: one
refusal around a restart cost Kestrel PRPM's seat with nothing in the
journal to say why.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jm2auWk6RP71CjaAb4FMoG
This commit is contained in:
Eric Wagoner
2026-09-17 23:22:00 -04:00
co-authored by Claude Fable 5.1
parent f738957100
commit 1ebd6dbe47
2 changed files with 15 additions and 2 deletions
+7 -1
View File
@@ -717,7 +717,12 @@ wss.on("connection", (socket, req) => {
const room = getRoom(roomId); const room = getRoom(roomId);
if (!room) return send(socket, { type: "error", message: "no such room" }); if (!room) return send(socket, { type: "error", message: "no such room" });
const result = joinRoom(room, name, typeof msg.token === "string" ? msg.token : null); const result = joinRoom(room, name, typeof msg.token === "string" ? msg.token : null);
if ("error" in result) return send(socket, { type: "error", message: result.error }); if ("error" in result) {
// A refused seat is worth a line: a lost seat mid-game is the
// costliest quiet failure this table has.
console.warn(`join refused: room ${room.id} name ${JSON.stringify(name)}${result.error}`);
return send(socket, { type: "error", message: result.error });
}
leaveGallery(session); leaveGallery(session);
session.playerId = name; session.playerId = name;
session.roomId = room.id; session.roomId = room.id;
@@ -1065,6 +1070,7 @@ wss.on("connection", (socket, req) => {
const result = peekSummary(roomId, name, typeof seat.token === "string" ? seat.token : null); const result = peekSummary(roomId, name, typeof seat.token === "string" ? seat.token : null);
if (result === null) continue; if (result === null) continue;
if (result === "badToken") { if (result === "badToken") {
console.warn(`seat voided: room ${roomId} name ${JSON.stringify(name)} — the token did not match`);
voided.push(`${roomId}:${name}`); voided.push(`${roomId}:${name}`);
continue; continue;
} }
+8 -1
View File
@@ -380,6 +380,8 @@ class Net {
feedbackReports = $state<FeedbackReportView[]>([]); feedbackReports = $state<FeedbackReportView[]>([]);
/** Every seat this browser holds, across rooms. */ /** Every seat this browser holds, across rooms. */
seats = $state<Seat[]>(loadSeats()); seats = $state<Seat[]>(loadSeats());
/** Seats the last games answer refused: dropped only if refused again. */
private voidedOnce = new Set<string>();
/** Lobby ledger: one summary per live seat. */ /** Lobby ledger: one summary per live seat. */
stats = $state<Record<string, number | string | null> | null>(null); stats = $state<Record<string, number | string | null> | null>(null);
chatSeen = $state<Record<string, number>>(loadChatSeen()); chatSeen = $state<Record<string, number>>(loadChatSeen());
@@ -673,8 +675,13 @@ class Net {
// stays — a restarting server, a stale restore, or the wrong // stays — a restarting server, a stale restore, or the wrong
// backend all answer with ignorance, and ignorance is not // backend all answer with ignorance, and ignorance is not
// deletion. The wallet is capped by age instead of by trust. // deletion. The wallet is capped by age instead of by trust.
// And a seat is dropped only on the SECOND refusal in a row: one
// refusal around a restart or a stale answer must not cost a seat.
const voided = new Set((msg.voided as string[] | undefined) ?? []); const voided = new Set((msg.voided as string[] | undefined) ?? []);
let kept = this.seats.filter((s) => !voided.has(`${s.roomId}:${s.name}`)); const doubted = new Set<string>();
for (const key of voided) { if (this.voidedOnce.has(key)) doubted.add(key); }
this.voidedOnce = voided;
let kept = this.seats.filter((s) => !doubted.has(`${s.roomId}:${s.name}`));
if (kept.length > 50) kept = kept.slice(kept.length - 50); if (kept.length > 50) kept = kept.slice(kept.length - 50);
if (kept.length !== this.seats.length) { if (kept.length !== this.seats.length) {
this.seats = kept; this.seats = kept;