Skip to content

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

  • Chatpoker.table_talk_messages, appended at the SAME emit chokepoint that writes the agents' Redis transcript ring. The ring's existing Redis INCR seq is 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 virtuoso startReached + firstItemIndex (no scroll jump), guarded by a table-epoch so an in-flight page can never land in another table's feed.
  • Thoughtsagent_thoughts had 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) and published (did it ACTUALLY go to the public stream at emit — showThoughts === true && significant). The public backfill filters on published, never the current flag — a showThoughts flip is never retroactive in either direction.
  • Hand results — read straight off poker_hands + hand_players (winners = is_winner rows, net chips; house rows excluded), cursor = hand_number. hand_result broadcasts now carry handNumber first-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/.