feat: session timeline (running log) + reliable live logging

She was logging stacks but skipping hands — the CASH card framed logging as one of
two registers, so she'd 'talk about' a hand instead of recording it (streaming
returns content OR tool calls). Fixes:

- CASH card: logging is mandatory and log-FIRST — trackable facts get the tool call
  before the reply, both not either/or, hands never skipped for conversation.
- log_stack gains a note ('card dead', 'doubled up vs the LAG') -> timeline context;
  tool spec + handler updated. Migration adds poker_stack_log.note.
- poker.timeline(): interleaves session start, stack updates (+context), hands
  (linkable), reads, and rituals chronologically in local time (clock.short()).
  Added to the hud() bundle.
- Session HUD: a 📜 Timeline card renders the running log with hand links — the
  '10:19pm start … 1:34a doubled up, $750 (hand)' view Brian wanted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-27 07:26:14 +00:00
parent ba00530caf
commit cb4ed10c1a
5 changed files with 101 additions and 16 deletions
+56 -8
View File
@@ -16,7 +16,7 @@ import json
import re
from datetime import datetime, timezone
from lyra import llm, memory
from lyra import clock, llm, memory
_SCHEMA = """
CREATE TABLE IF NOT EXISTS poker_sessions (
@@ -138,7 +138,8 @@ def _c():
conn.executescript(_SCHEMA)
# Add columns introduced after a DB already had the tables (no-op if present).
for ddl in ("ALTER TABLE poker_hands ADD COLUMN structured TEXT",
"ALTER TABLE poker_sessions ADD COLUMN chat_session_id TEXT"):
"ALTER TABLE poker_sessions ADD COLUMN chat_session_id TEXT",
"ALTER TABLE poker_stack_log ADD COLUMN note TEXT"):
try:
conn.execute(ddl)
except Exception:
@@ -403,17 +404,18 @@ def add_buyin(amount: float, session_id: int | None = None) -> float:
# --- stack tracking ---
def log_stack(amount: float, session_id: int | None = None) -> dict:
"""Record Brian's current chip stack. Returns {current, buy_in, net} where net
is his live net while sitting (current stack total bought in)."""
def log_stack(amount: float, note: str | None = None, session_id: int | None = None) -> dict:
"""Record Brian's current chip stack, optionally with the why ("card dead",
"doubled up vs Sal") — that context becomes the session-timeline line. Returns
{current, buy_in, net} where net is his live net while sitting."""
sid = _resolve(session_id)
if sid is None:
raise ValueError("no live session")
conn = _c()
with conn:
conn.execute(
"INSERT INTO poker_stack_log (session_id, amount, created_at) VALUES (?, ?, ?)",
(sid, float(amount), _now()),
"INSERT INTO poker_stack_log (session_id, amount, note, created_at) VALUES (?, ?, ?, ?)",
(sid, float(amount), (note or "").strip() or None, _now()),
)
return stack_state(sid)
@@ -436,7 +438,7 @@ def stack_log(session_id: int | None = None) -> list[dict]:
if sid is None:
return []
return [dict(r) for r in _c().execute(
"SELECT id, amount, created_at FROM poker_stack_log WHERE session_id = ? ORDER BY id",
"SELECT id, amount, note, created_at FROM poker_stack_log WHERE session_id = ? ORDER BY id",
(sid,),
).fetchall()]
@@ -1179,6 +1181,51 @@ def running_stats(stakes: str | None = None, venue: str | None = None,
# --- live session HUD (everything tracked in the current session, for the UI) ---
def timeline(session_id: int | None = None) -> list[dict]:
"""The session's running log: start, stack updates (+context), hands (linkable),
reads, and rituals, interleaved chronologically with local time-of-day stamps.
This is what Brian sees as the night's story — '10:45 start … 12:00a doubled up,
$750 (hand)'. Each entry: {time, at, kind, text, hand_id?, amount?, result?}."""
sid = _resolve(session_id)
if sid is None:
return []
s = get_session(sid) or {}
events: list[dict] = []
if s.get("started_at"):
bits = [s.get("stakes"), s.get("game"), f"at {s['venue']}" if s.get("venue") else None]
label = " ".join(b for b in bits if b)
events.append({"at": s["started_at"], "kind": "start",
"text": ("Session start — " + label) if label else "Session start"})
for r in stack_log(sid):
events.append({"at": r["created_at"], "kind": "stack",
"amount": r.get("amount"), "text": r.get("note") or "stack update"})
for h in list_hands(sid):
desc = " ".join(b for b in (h.get("position"), h.get("hole_cards")) if b)
events.append({"at": h["at"], "kind": "hand", "hand_id": h["id"],
"result": h.get("result"), "text": desc or "hand"})
for r in _c().execute(
"SELECT pr.created_at AS at, pr.seat AS seat, pr.note AS note, p.name AS name "
"FROM player_reads pr LEFT JOIN poker_players p ON p.id = pr.player_id "
"WHERE pr.session_id = ?", (sid,),
).fetchall():
who = r["name"] or (f"seat {r['seat']}" if r["seat"] else "villain")
events.append({"at": r["at"], "kind": "read", "text": f"Read — {who}: {r['note']}"})
for r in list_rituals(sid):
tag = f"[{r['classification']}] " if r.get("classification") else ""
events.append({"at": r["created_at"], "kind": r["kind"],
"text": tag + (r.get("content") or r["kind"]), "hand_id": r.get("hand_id")})
events.sort(key=lambda e: e["at"] or "")
for e in events:
e["time"] = clock.short(e["at"])
return events
def _session_villains(sid: int) -> list[dict]:
"""Players read this session, with their standing dossier fields."""
rows = _c().execute(
@@ -1251,6 +1298,7 @@ def hud(session_id: int | None = None) -> dict | None:
},
"hands": hands,
"villains": _session_villains(sid),
"timeline": timeline(sid),
"notes": notes,
"rituals": {
"alligator": alligator_active(sid),