# Poker logging service + message-type prompts - **Date:** 2026-06-28 - **Status:** Sub-project 1 spec ready for review; sub-project 2 parked. - **Branch:** `feat/poker-mode-prompts` ## Origin This started as "make Lyra's poker replies less generic" (message-type-specific prompts). During design we decided to **build the logging tool first** as a standalone system of record with a clean API and a human-usable UI, then wire Lyra in as a *client* of it. Rationale: - Brian can log and **correct** data himself, independent of whether Lyra parsed it right (she mislabeled "Dave the rock" vs "Dave the mechanic" mid-session). - The data stops being hostage to the agent. Lyra becomes one client among potentially several. - It's reusable: RTO (the solver) and a **fine-tuned poker model on the MI50** could consume the same hand/session data through the same contract. ## Decomposition Two sub-projects, built and shipped in order. ### Sub-project 1 — Poker logging service *(this spec)* Harden `lyra/poker.py` into a well-bounded store, expose a **complete REST API** over it, define a **stable, documented tool/API contract**, and build the human UI to log/edit/correct everything. Fully usable by Brian alone, zero LLM dependency. ### Sub-project 2 — Lyra wiring *(parked; summarized at the end)* Message-type classifier + type-specific prompt fragments; Lyra's tools call the sub-project 1 service. Separately, enabling tool-calling on the MI50 backend so a fine-tuned poker model can drive the same contract. **Why the contract is first-class:** in every design (in-process, REST, MCP) the *model* never calls the API directly — it emits a tool-call and the host app executes it. So what lets the cloud model, the MI50 fine-tune, RTO, and a human UI all interoperate is a single **stable tool/API schema** (operation names + JSON arg schemas). That contract is the training target for the fine-tune and the seam for every backend. MCP is deferred: it's a thin wrap over the same service, worth adding only when a *second host application* appears. --- # Sub-project 1 — Poker logging service ## Goals 1. A complete API surface over the poker data model — create/read/update/delete for every entity, not just the few edit/delete endpoints exposed today. 2. A single **documented, versioned tool/API contract** that the REST API, Lyra's LLM tools, the human UI, a future MCP wrapper, and the MI50 fine-tune all share. 3. A human UI to **log** (fast capture) and **edit/correct** (fix Lyra's mistakes) every entity. 4. The 2nd input box (stack quick-capture) and the iOS-PWA bottom safe-area fix. 5. Pure data capture never touches the LLM. ## Non-goals - Lyra's classifier / prompt fragments (sub-project 2). - Enabling tools on the MI50 backend (sub-project 2). - MCP wrapper (deferred until a second host app exists). - Itemized buy-in history — buy-ins stay a single `buy_in_total` scalar. - Rewriting the SQLite schema; we build on the existing tables. ## Current state (what exists) - **Store & logic:** `lyra/poker.py` — schema at `poker.py:21` (tables `poker_sessions`, `poker_hands`, `poker_stack_log`, `poker_rituals`, `poker_players`, `player_reads`, `player_observations`). Functions: `start_session` (157), `add_buyin` (389), `log_stack` (407), `stack_state` (446), `update_session` (362), `end_session` (515), `log_hand` (541, flat/no-LLM), `record_hand` (770, LLM-parses shorthand), `add_read` (1010), `hud` (1245). - **Exposed endpoints (`lyra/web/server.py`):** `GET /session/data` (hud), `PATCH /session/{id}`, `DELETE /session/entry/{kind}/{id}`, `GET/DELETE /history`, `GET /hand/{id}/data`, `POST /hand/{id}/reconstruct`, `GET /hands/data`, `GET /recap/...`. **No** direct create endpoint for stack/buyin/hand/read/session — those are reachable only through chat → tool-calling. - **UI:** `index.html` (chat), `session.html` (live HUD: stack card + sparkline + a PATCH-based edit form via `saveEdit()` at `session.html:192`, and `del(kind,id)` at `211`), `history.html`, `hand.html`. - **Tool specs:** `lyra/tools.py` already defines arg schemas for each operation (`_f(...)` specs, `tools.py:469-658`) — the embryo of the contract. ## Design ### 1. Store layer — harden `poker.py` Keep the existing functions and tables; tighten the module into a clean service boundary so both the REST layer and Lyra's tools call the *same* functions. Each operation: validates input, resolves the target session (`_resolve`), writes, returns a consistent dict. No behavior change to existing callers; this is consolidation, not a rewrite. ### 2. The tool/API contract *(first-class deliverable)* A single source-of-truth document + schema defining every operation: name, purpose, JSON arg schema, return shape, and which REST route + which LLM tool map to it. Versioned (e.g. `contract_version: 1`). Lives at `docs/POKER_API.md` (or a machine-readable `poker_contract.py` that both the REST routes and `tools.py` specs derive from — preferred, so they can't drift). Operations (the canonical set): | Operation | Args | Entity | |---|---|---| | `start_session` | venue, stakes, game, format, buy_in, mantra | session | | `update_session` | venue, stakes, game, format, buy_in_total, cash_out, mantra, mood | session | | `end_session` | cash_out, mood | session | | `delete_session` | id | session | | `log_stack` | amount, note | stack entry | | `delete_stack` | id | stack entry | | `add_buyin` | amount | session (increments buy_in_total) | | `log_hand` | position, hole_cards, board, streets…, pot, result, tag, lesson | hand | | `record_hand` | shorthand (LLM-parsed) | hand | | `update_hand` | id, any hand field | hand | | `delete_hand` | id | hand | | `add_read` | note, name, seat, tendencies, adjustment, category, venue | player/read | | `update_read` / `update_player` | id, fields | player/read | | `delete_read` | id | player/read | | rituals: `scar_note`, `confidence_bank`, `alligator_blood`, `reset_ritual` | … | ritual | ### 3. REST API — complete the surface (`lyra/web/server.py`) Add the missing **create/update** routes so the human UI (and any non-LLM client) can do everything: - `POST /session/stack` → `log_stack(amount, note?)`; server-stamped time; returns `stack_state()`. - `POST /session/buyin` → `add_buyin(amount)`; returns `buy_in_total`. - `POST /session` → `start_session(...)`. - `POST /session/hand` → `log_hand(...)` (flat) and/or `record_hand(shorthand)`. - `PATCH /hand/{id}` → `update_hand(...)`; `DELETE /hand/{id}`. - `POST /session/read` → `add_read(...)`; `PATCH /read/{id}`; `DELETE` via existing entry-delete. - Keep existing `PATCH /session/{id}`, `DELETE /session/entry/{kind}/{id}`, `GET /session/data`. All return `{ok, ...}` and a clear error on "no live session." Routes are thin wrappers over the store, mirroring the contract one-to-one. ### 4. Human UI — log + edit/correct **Fast capture:** - **2nd input box** (`index.html`): slim row **below the message input, above the bottom nav icons**. Type a number → `POST /session/stack` → time-stamped, sparkline updates, a one-line confirmation drops into the **Live Log**. **No chat message, no LLM call.** Stack-only in v1. Tolerates `$685`/`685`. - **HUD widget** (`session.html`, in the Stack card at `:280`, mirroring `saveEdit()` at `:192`): stack field (`POST /session/stack`), buy-in field (`POST /session/buyin`), cash-out field (existing PATCH). **Edit / correct (fix Lyra's mistakes):** - Edit any session field (exists via the PATCH edit form — verify coverage). - Hands list with edit + delete (`hand.html` + new PATCH/DELETE) — fix mislabeled villains, wrong board, wrong result. - Reads/players list with edit + delete — rename "Dave the rock" ≠ "Dave the mechanic", fix tendencies. - Stack entries deletable (exists via `del('stack', id)`) — verify. ### 5. iOS-PWA bottom safe-area fix The empty band below the nav icons is a safe-area issue (likely `100vh` not accounting for `env(safe-area-inset-bottom)` / the home indicator). Fix the layout container + bottom nav CSS so the app fills the viewport with the icons seated above the home indicator. Use the `building-ios-pwas` skill at implementation time. ## Testing / verification - **Contract conformance:** a test asserting each REST route and each `tools.py` spec matches the canonical contract (names, required args) — catches drift between the human API and the LLM API. - **Endpoint round-trips:** create → read → update → delete for stack, buyin, hand, read against a test session; assert rows written, time stamped, `stack_state()`/`hud()` reflect changes; assert clean error with no live session. - **UI manual pass:** log a stack via the 2nd box and confirm it lands in Live Log + sparkline without a chat reply; edit a hand's villain and confirm persistence; delete a bad read. - **PWA:** on the iOS PWA, confirm the bottom gap is gone and the 2nd input box sits above the nav with the keyboard open. --- # Sub-project 2 — Lyra wiring *(parked)* Detail preserved here; gets its own spec → plan after sub-project 1 is MVP'd. ## Why it exists (diagnosis from real sessions) Evidence from `sess-dff2s91c` (2026-06-27 Meadows, 2026-06-28 Wheeling): - **Coaching essay on every turn, including pure data** — `Stack=$685` drew 4–6 sentences of "keep that momentum rolling." (Sub-project 1's dumb capture removes these from the LLM entirely.) - **False tilt/fatigue reads** — "table broke, it's 11:50pm" → repeated "late-night fatigue… mental reset"; Brian: *"you seem to be reading me as tilted."* Cause: the `_route` mood nudge (`mind.py:328`) firing on non-mood messages. - **No bet-intent reasoning** — a value bet ($40, full house) that folded out 88 was praised as "the power of representing something stronger." It was value *lost*, not a successful rep. - **Eyeballs instead of `analyze_spot`** — 77 multiway got "a disciplined fold might have been better," no math, violating the persona's "never eyeball poker math" rule. - **Even her sharp reads leak bad logic** — the Connie read included "limp-checking in position" (contradictory). Root cause: one broad per-turn card (`_CASH_CARD`, `modes.py:66`) describes traits; the model satisfies trait language with safe abstraction. ## Planned approach - **Classifier** (`lyra/poker_classify.py`): `classify(message) -> HAND | STATUS | MENTAL | LOG | CHAT`. Heuristic v1 (card-token regex, position/street keywords, feeling phrases, time/venue), swappable for an LLM/MI50 classifier behind the same signature. Ambiguous → CHAT. - **Pipeline:** a `_classify` step in `mind.PIPELINE` sets `ctx.msg_type` (poker mode only); `build_messages` injects the **type fragment** in the seat now held by `mode.card` (`mind.py:152`) instead of the whole `_CASH_CARD`. - **Fragments** (`lyra/poker_prompts.py`, `POKER_FRAGMENTS`): concrete rules + response shape per type. - **HAND:** log it → `analyze_spot` if close → reason about **bet intent** (value/bluff/protection — did it work? a fold to a value bet = value lost, flag it) → name leaks (value-owning, missed value, sizing) → one opinion. No reflexive praise. Pull `player_profile` before referencing a villain. Seeded with 2 real-hand exemplars (the $40 value bet; the 77 spot). - **STATUS:** 1–2 sentences, no coaching, no tilt-reading. - **MENTAL:** full presence (already works — preserve). - **LOG:** one line. - **CHAT:** full voice, real opinion. - **Pipeline fixes:** kill the misfiring `_route` mood nudge (`mind.py:328`); trim the always-on mode-menu note (`mind.py:77`) in poker mode. - **MI50 tool-calling:** enable tools on the MI50 backend (`chat.py` `TOOL_BACKENDS = {"cloud"}` at `:21`) so a fine-tuned, tool-calling poker model can drive the contract. Requires the fine-tune to emit the contract's tool-call format. ## HAND exemplars (preserved for sub-project 2) > *Brian:* "Flopped bottom set with 22, turned a full house, bet $40 on the river, he folded 88." > *Lyra:* "That's a value bet that got no value — 88 was never folding to a smaller stab. $40 into that pot was too much; ~$20–25 gets called by the over-pair and worse two-pairs. The hand's a monster, the leak is the sizing." > *Brian:* "77 multiway, flop 4h5c3c, HJ bets 75, BTN shoves 74, I call. Turn/river check through, HJ wins with 64." > *Lyra:* "Against a bet and a shove on 4-5-3 you're drawing thin — sets, two pair, and the made wheel are all ahead, and you block almost none of it. The stack-depth read (he only had ~150 behind) is real, but that's a reason to fold and wait, not to call off light. This is the value-owning spot you flagged yourself."