Appearance
Player-Funds Lifecycle Audit
Decided 2026-06-11, shipped 2026-06-12. The full ADR is retained in the project's internal planning archive.
What was decided
One table-state machine: transitionTableState() is the only writer of tables.status / freeze_reason / closed_at — legal transitions enforced, atomic writes (two-flag drift unrepresentable), a span per transition, live table_state_changed broadcast, lobby drops frozen/closed. Participant rows get enforced end-of-life in the settle flow (phantom seats die structurally). Reconstruction mirrors the engine so restarts heal at every journey stage. Escape pays the exact per-player split and closes the table through the machine; the idle reaper gives abandoned tables a conclusion. The full journey is replayable from spans alone, and test/lifecycle/ + a stage-parameterized restart matrix gate every merge.
Key tradeoffs accepted
- Spans are the history — event-sourcing was rejected; OTel traces + the OHH/position-chain records carry the audit story.
- Per-route hardening rejected — the failing status quo; the machine is the single chokepoint instead.
- Reaper authority is scoped to what it can observe (from a later hardening pass): poker-only, any-chips-funded, tournament-state and in-flight-deposit exclusions. Never reap what you can't check.
- Escape memorialization deferred to custody-layer — rather than an interim direct-pay hash anchor, escape will hand the pool + per-player claim records to a CustodyFacet (free/disputed two-state claims).
What the hardening pass added
Seven confirmed-then-fixed bugs after the work "looked done" — including escape paying sitting-out players 0 and a convergence sweep that found a real stranded table on its first boot. See the changelog entry and the Table Lifecycle guide.