Decompose into two sub-projects: (1) harden poker.py into a standalone logging service with a complete REST API, a first-class documented tool/API contract, and a human UI to log/edit/correct everything — usable by Brian alone, zero LLM dependency; (2) wire Lyra in as a client (classifier + message-type prompts), parked. Contract is first-class because it's the shared seam for the cloud model, a fine-tuned MI50 poker model, RTO, the human UI, and a future MCP wrap. MCP deferred until a second host app exists. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
12 KiB
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
- 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.
- 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.
- A human UI to log (fast capture) and edit/correct (fix Lyra's mistakes) every entity.
- The 2nd input box (stack quick-capture) and the iOS-PWA bottom safe-area fix.
- 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_totalscalar. - Rewriting the SQLite schema; we build on the existing tables.
Current state (what exists)
- Store & logic:
lyra/poker.py— schema atpoker.py:21(tablespoker_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 viasaveEdit()atsession.html:192, anddel(kind,id)at211),history.html,hand.html. - Tool specs:
lyra/tools.pyalready 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; returnsstack_state().POST /session/buyin→add_buyin(amount); returnsbuy_in_total.POST /session→start_session(...).POST /session/hand→log_hand(...)(flat) and/orrecord_hand(shorthand).PATCH /hand/{id}→update_hand(...);DELETE /hand/{id}.POST /session/read→add_read(...);PATCH /read/{id};DELETEvia 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, mirroringsaveEdit()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.pyspec 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=$685drew 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
_routemood 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
_classifystep inmind.PIPELINEsetsctx.msg_type(poker mode only);build_messagesinjects the type fragment in the seat now held bymode.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_spotif 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. Pullplayer_profilebefore 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.
- HAND: log it →
- Pipeline fixes: kill the misfiring
_routemood 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.pyTOOL_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."