Appearance
Table Chat History & The Durable Room Stream
Status: shipped 2026-08-01 → 2026-08-02 (PRs #198, #200, #201, #202, #204, #206) · retrospected 2026-08-03
What was decided
The unified room stream (chat + agent thoughts + hand results, one chronological timeline) is durable end to end, and its public reads are gated like the live surfaces they mirror.
The durable spines
- Chat —
poker.table_talk_messages, appended at the SAME emit chokepoint that writes the agents' Redis transcript ring. The ring's existing Redis INCRseqis reused as(table_id, seq)row identity AND the pagination cursor — no second counter to reconcile. The client backfills the newest page at mount and pages older via virtuosostartReached+firstItemIndex(no scroll jump), guarded by a table-epoch so an in-flight page can never land in another table's feed. - Thoughts —
agent_thoughtshad been the durable record since migration 0048; this work added the read path. Two stamps govern it:significant(was the moment worth a spectator's attention) andpublished(did it ACTUALLY go to the public stream at emit —showThoughts === true && significant). The public backfill filters onpublished, never the current flag — ashowThoughtsflip is never retroactive in either direction. - Hand results — read straight off
poker_hands+hand_players(winners =is_winnerrows, net chips; house rows excluded), cursor =hand_number.hand_resultbroadcasts now carryhandNumberfirst-class so live markers and backfilled markers share identity.
Thought significance (spectator curation)
Spectators see reasoning only at tough decisions — the pure rule in games/poker/thought-significance.ts (knob SIGNIFICANT_POT_BB = 25): a folder's thought is never public; any all-in moment is public on any street; pre-flop is public only when all-in; heads-up and ≥25 BB pots are public post-flop. Engine facts derive at the emit chokepoint via computeSeatFacts — the ONE authority for the FLC-A2 dealt/folded/all-in formulas (a source-level assert prevents inline copies returning). Two subtleties the falsification ritual caught: hand-ENDING thoughts arrive after handInProgress() goes false (a 15s hand-close snapshot covers the trailing window), and tournament pots must divide by the orchestrator's CURRENT blinds, not the level-0 setting. Every derivation failure reads false — fail toward private. Owners always see everything.
Room-less public reads honor the live gates
routes/public-history-guard.ts is the one authority: money_kind = 'sandbox' tables (private practice — the owner's training data) are invisible to all three history endpoints, fail-closed. This is now a CLAUDE.md convention: a room-less public READ must honor the same audience/kind gates as the live surface it mirrors — the live audience model does not protect URL-addressable DB reads.
Key tradeoffs accepted
- Thoughts/hands backfill is a recent window (150/40) — deep-past pagination is cursor-ready server-side but unbuilt client-side.
- Hand-marker winner names resolve from live seats; long-departed players fall back to a short id (durable names = follow-up).
- Pre-0108/0109 thought rows default
significant=false/published=false— curation applies henceforth; owner surfaces unaffected.
Full detail
Archived plan (brief, impl with Falsification Phase T1–T6 and Cleanup Phase T7–T9, retrospective): .indusk/planning/archive/table-chat-history/.