Skip to content

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.

  1. Street close — matched chips → sweep → next card. The new-street broadcast is deferred behind the matched-bets frame by matchedBeatHoldMs (default 650 = the felt's betSweepMs) via paceStreetTransition (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.
  2. 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 lastActionBySeat at the street boundary so a new-street action is a change-from-empty; the client lastVerbsRef captures the matched frame's verbs for the sweep beat. A client-only heuristic over the latest snapshot is structurally insufficient.
  • A10resolveCeremonyHoldMs clamped autoDealDelayMs but not the newly-added ceremonyBudgetMs, re-opening the very invariant Phase 2 closed. Fixed with the CEREMONY_MIN_MS floor. 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 via ceremonyShownRef.

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 (the modeled-felt-behaviors T7 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_fired witnesses 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.