Files
project-lyra/docs/SCOUTING_DESK.md
T
serversdown 22526d7938 docs: spec the scouting desk — proactive recall + villain identity resolution
Design for the "she remembers" north star: a poker stats-desk that slides
relevant structured context into her prompt before she replies (extends the
recall already running in mind.build_messages), plus the hard part —
nameless-villain identity resolution.

Captures: the two retrieval channels (deterministic entity vs semantic pattern);
descriptor-as-fuzzy-key identity (name becomes optional; descriptor embedding,
venue scoping, distinctiveness gating so generic descriptions don't cause
wrong-guy citations); seat-as-within-session-alias; the live confirm-to-merge
loop; and the async review interface (/players browser + a "possible merges /
needs clarification" queue that silence-at-the-table routes into, with rejected
merges recorded as known-distinct and the merge scan run in the dream cycle).
6-phase sequencing, to build after the trial-by-fire logging session.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 22:37:23 +00:00

168 lines
8.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# The Scouting Desk — proactive poker recall + villain identity resolution
*Design spec. Not built yet. Companion to the "she remembers" north star in the
`poker-copilot` memory. Written 2026-07-03, before the trial-by-fire session.*
## Purpose
Turn the copilot from a logbook into a copilot that **remembers across sessions,
unprompted** — the way a broadcast stats desk slides a note to the color
commentator: *"he mentioned the guy's hot streak → here are his last 10 games."*
Target moments:
- *"you had this exact leak last week too, remember?"*
- *"neck-tattoo guy just 3-bet you — last time he did that at the Meadows he had it."*
- *"Sleepy John was here two weeks ago; you stacked off AK into his set."*
The failure mode to avoid at all costs: **confident-but-wrong.** A stats desk that
guesses gets the commentator burned on air. **Silence is the default; the desk
speaks only when there's real signal.**
## What already exists (don't rebuild it)
`mind.build_messages()` already runs a recall pass on **every** message:
`memory.recall(user_msg)` over past exchanges + `memory.recall_summaries(user_msg)`
over session gists, injected as system notes before she replies. The
"slide-a-note-in-before-she-speaks" machinery is already the architecture. This
spec **adds a poker desk** to that pass — it does not build a new RAG system.
Episodic links also already exist: `link_hand_players` writes a
`player_observations` row per named villain in a recorded hand, carrying
`hand_id` AND `session_id`; `player_reads` carry `session_id`. So villain →
observation → hand → session/date is reconstructable today.
## Two retrieval channels (don't conflate them)
1. **Entity desk — deterministic.** A known **name** in the message → exact/fuzzy
SQL match on `poker_players` → pull dossier + your history vs him. ~1ms, no
hallucination. This is the "hears the name, pulls last 10 games" case.
2. **Pattern desk — semantic.** No entity to key on ("I keep punting these river
bluffs") → embed the message, retrieve similar **scar notes / hands / recap
passages** by meaning. This is where embeddings earn their keep. Also the
backbone of nameless-villain matching (below).
Both feed one injected **STATS DESK** system note, relevance-gated.
## The hard part: nameless villains
Most live villains have no name. Brian identifies them by **physical descriptor**
("guy with the lips/neck tattoo"), by **seat** ("seat 4", "two to my left"), or —
uselessly — **generically** ("mid-aged white dude with glasses").
### Current gap
`poker_players.name` is `NOT NULL` and identity is an **exact name match**
(`upsert_player``WHERE name = ?`). The `description` column exists but is dead
weight: not a key, not embedded, never matched. **Nameless villains can't exist
today.** This is the core schema fix.
### Identity model — descriptor as a fuzzy primary key
Store a villain as:
- `name` — now **optional**.
- `descriptors` — accumulated distinctive physical tags heard over time
("neck tattoo", "lips ink", "heavyset", "bald+beard").
- `descriptor_embedding` — embedding of the accumulated distinctive tags, for
semantic match against drifting phrasings.
- `venue` — a strong disambiguator (the neck-tattoo reg at the Meadows ≠ the one
at Wheeling, unless Brian travels).
- `distinctiveness` — a weight; distinctive features (tattoos, scars, a name)
score high, generic ones (age/race/glasses) near zero.
### Resolver — matching an incoming reference
1. **Name present** → exact/fuzzy SQL match (entity desk). Done.
2. **Descriptor present** → embed it, compare to `descriptor_embedding` of known
villains **scoped to the current venue**, weighted by distinctiveness.
3. **Confidence bands:**
- **High** (distinctive + strong match) → surface the file; if live, a light
confirm ("the neck-tattoo LAG from 3 weeks ago?").
- **Medium/ambiguous** (several candidates, or a middling score) → **do NOT
interrupt.** File a `needs_clarification` task to the review queue and stay
quiet, OR ask only if it's decision-relevant right now.
- **Generic-only** (no distinctive signal) → **refuse to guess.** Stay silent
or ask for one distinctive detail ("anything that stands out — ink, chips,
how he plays?"). Wrong-guy citation is worse than nothing.
- **No match** → new villain; open a descriptor-keyed dossier.
### Seat = within-session alias only
The live session keeps a `seat → villain` map so reads accumulate whether Brian
says "seat 4" or "the tattoo guy." Seats evaporate when the session ends — they
mean nothing next week.
## Confirmation loop (live, in chat)
Auto-merging on a fuzzy match is dangerous, so she **proposes and Brian confirms**
in natural language:
> Brian: "neck tattoo guy just 3-bet me again"
> Lyra: "The neck-tattoo LAG from the Meadows three weeks ago — the one who
> stacked you with the flush? Or new guy?"
> Brian: "yeah him" → reinforce identity · "nah different" → split, and learn
> what distinguishes them.
Handles name-arrives-later for free: catch his name off Bravo → "merge neck-tattoo
guy into 'Danny'" → history follows.
## The review interface (async, out-of-band)
Silence at the table ≠ forget it → it routes to a queue Brian clears at his pace.
### `/players` — villain file browser
List: name-or-lead-descriptor, venue, category (feeder/risky/reg), hands
observed, VPIP/PFR (when sample is real), last seen, distinctive tags. Detail
view: reads, showdowns, notable hands (link to `/hand/{id}`), sessions seen,
stats. Edit / rename / retag / delete / manual-merge.
### Resolution queue — two lanes
- **Possible merges** — two profiles likely one person (high descriptor
similarity + same venue, below auto-merge). Side-by-side → **Same guy** (merge)
/ **Different** (split).
- **Needs clarification** — a descriptor that matched several candidates, or a
nameless villain the resolver couldn't place → pick match / **New guy**.
### Two rules that keep the queue from rotting
1. **A rejected merge stays rejected** — record the pair as *known-distinct* so it
never re-surfaces; start tracking the distinguishing tell.
2. **Merge-candidate scan runs in the dream cycle**, not the hot path — nightly,
compare descriptor embeddings within each venue, file new maybes. Zero live
latency.
## Injection format & gating
A single system note, clearly marked as structured fact so she cites it (not
confabulates), e.g.:
```
STATS DESK — Neck-tattoo guy (Meadows, LAG/reg): seen 3×, last 2wk ago.
vs you: hand #38 (AK, stacked off into his set). Reads: overfolds turn,
3-bets light from the CO. Sample: 22 hands — VPIP 41 / PFR 28.
```
Gate hard: inject only on a confident entity hit or a strong semantic score.
Default to nothing. Never inject a generic-only guess.
## Honest limits
Never perfect. Some players are genuinely indistinguishable — fine. The system's
only job: **right when there's signal, quiet when there isn't.**
## New data model (sketch)
- `poker_players`: `name` → nullable; add `descriptors TEXT`,
`descriptor_embedding BLOB`, `distinctiveness REAL`.
- `player_distinct_pairs(a_id, b_id, note, created_at)` — rejected merges.
- `identity_queue(id, kind, player_ids, descriptor, context, session_id,
confidence, status, resolution, created_at)` — kind ∈ {merge_candidate,
needs_clarification}.
- Live-session `seat → player_id` alias map (in-session only).
## Sequencing (after the trial-by-fire session — recall feeds on real data)
1. **Nameless identity + resolver** — schema, descriptor embedding, venue-scoped
semantic match, distinctiveness gate. (Unblocks everything.)
2. **Scouting-desk injection** — wire entity + pattern recall into
`build_messages` as the gated STATS DESK note.
3. **Confirmation loop** — the live propose/confirm/merge/split UX in the persona.
4. **`/players` browser + resolution queue UI** — the async review interface.
5. **Dream-cycle merge scan** — nightly candidate generation.
6. **Pattern desk** — semantic recall over scars/notes/recaps for "this leak
again."