Appearance
Felt Transition Sequencer
Status: shipped (2026-07-21) · Plan: .indusk/planning/archive/felt-transition-sequencer/ (no ADR — falsification-driven UI-fix plan)
Decision
The server paces the two raced felt transitions so the snapshot-diffing brat felt cannot skip their required intermediate states. Each prerequisite state is emitted as its own committed, held frame, and the next state is gated behind the prior — mirroring projectOhh's cadence.
- Street close — matched chips → sweep → next card. The new-street broadcast is deferred behind the matched-bets frame by
matchedBeatHoldMs(default 650 = the felt'sbetSweepMs) viapaceStreetTransition(apps/game-server/src/games/poker/transition-sequencer.ts). Only a real matched-bets street-OPEN close is paced; non-closing actions and round-OVER (hand-end) closes stay synchronous. - Hand end — winner ceremony → next hand. The next-hand deal is
resolveCeremonyHoldMs(autoDealDelayMs, ceremonyBudgetMs) = max(ceremonyBudgetMs, CEREMONY_MIN_MS) + max(0, autoDealDelayMs)— the ceremony plays for its full budget (default 4000), THEN the short post-ceremony gap (default 1000) runs before the deal. Sequential, not overlapping.CEREMONY_MIN_MS = 2700(= the client pot-drain HOLD 1600 + DRAIN 1100) floors the budget so no misconfiguration can shorten the ceremony below a full visible drain.
Why
The felt render was correct throughout — it just never received the frame it needed. modeled-felt-behaviors shipped a synchronous matched-bets beat and hoped the two server broadcasts landed as separate client commits; the live ui.felt.bet_sweep seats=6-when-seat-7-called telemetry proved they coalesced (WS/SSE can deliver two broadcasts in one client message event). A snapshot-diffing client cannot reconstruct an intermediate state the server collapsed — the server must EMIT it as its own frame and pace the next state behind it.
The same logic applies to the ceremony: the live regression was a misset game_settings.autoDealDelayMs = 5 (its comment literally called it "the SHOWDOWN CEREMONY HOLD"), flashing the winner ceremony for 5ms. Gating the deal on the ceremony's full display — and flooring every knob that feeds the gate — makes the "the ceremony always displays" invariant hold against any misconfiguration.
Falsification hardening (Phase 3)
Re-reading the shipped code against its own claims found three real bugs:
- A9 — per-street verbs were suppressed for a repeated verb (call the flop, call the turn; check-check down). The client re-stamped only on a verb-string change. Fix (coordinated server + client): the server clears
lastActionBySeatat the street boundary so a new-street action is a change-from-empty; the clientlastVerbsRefcaptures the matched frame's verbs for the sweep beat. A client-only heuristic over the latest snapshot is structurally insufficient. - A10 —
resolveCeremonyHoldMsclampedautoDealDelayMsbut not the newly-addedceremonyBudgetMs, re-opening the very invariant Phase 2 closed. Fixed with theCEREMONY_MIN_MSfloor. Lesson: clamp EVERY knob that feeds a guarded value, not just the one you were thinking about. - A11 — the pot bridge held the last pot for any
!handInProgress, re-showing the previous pot during the next hand's blind collection. Scoped to the hand-end→ceremony window viaceremonyShownRef.
Tradeoffs accepted
- The sequencer is best-effort for DISPLAY; engine progress stays load-bearing. A paced frame that throws must never block
autoAdvance/ hand-end — the pacing timers are hand-id-guarded and exception-swallowing (themodeled-felt-behaviorsT7 lesson, extended to every paced step). - No new server span. The delay floors are pure control-flow; the existing
ui.felt.bet_sweep/ui.felt.ceremony_firedwitnesses are the acceptance signal (a per-frame server span would be exactly the amplification the felt witnesses exist to avoid). - Verified-done is deferred to deploy. The two live-witness predicates (sweep-covers-caller and ceremony-gap ≥ budget, both on the merge sha) can only close once the PR merges and deploys.
Knowledge home
The canonical felt-behavior reference is packages/game-ui/FELT-REGISTRY.md (rows 8/17/58/59) + the CLAUDE.md matched-bets-beat gotcha — per the UI DEBUG ROUTING convention, that is where the next felt-debugging session looks.