Files
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

8.0 KiB
Raw Permalink Blame History

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_playerWHERE 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."