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
+8 -1
View File
@@ -380,6 +380,8 @@ class Net {
feedbackReports = $state<FeedbackReportView[]>([]);
/** Every seat this browser holds, across rooms. */
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. */
stats = $state<Record<string, number | string | null> | null>(null);
chatSeen = $state<Record<string, number>>(loadChatSeen());
@@ -673,8 +675,13 @@ class Net {
// stays — a restarting server, a stale restore, or the wrong
// backend all answer with ignorance, and ignorance is not
// 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) ?? []);
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 !== this.seats.length) {
this.seats = kept;