Commit Graph
100 Commits
Author SHA1 Message Date
serversdownandClaude Opus 4.8 9cd962625d feat: scouting desk — proactive villain recall injected before she replies
Phase 2. On every poker-context turn, detect players named or described in the
message and slide their structured history into her prompt as a SCOUTING DESK
note (cite-don't-invent). Fail-safe: any desk error is swallowed, never breaks
the turn; silence is the default.

- poker.villain_recall(id): episodic brief — times/where seen, last seen, notable
  hands (linkable ids), reads, stats (Gap 1: the when/where/which-hand narrative).
- scouting.scout(): named hits (deterministic word match) + descriptor spans
  (regex → resolve_villain). High → surface + "confirm it's the same guy";
  ambiguous → file needs_clarification to the queue instead of interrupting;
  generic → stay silent.
- mind.build_messages wires it in, gated to poker modes (poker_cash/study).

5 tests. Full suite 146 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 22:49:40 +00:00
serversdownandClaude Opus 4.8 8a6b11c56a feat(poker): nameless-villain identity resolution engine
Phase 1 of the scouting desk (docs/SCOUTING_DESK.md). Villains keyed by physical
descriptor when there's no name — a fuzzy key matched by embedding, venue-scoped,
gated on distinctiveness so a generic description never resolves to a wrong guess.

- schema: poker_players gains descriptors/descriptor_embedding/distinctiveness/
  named (name stays populated with a descriptor label to avoid a NOT-NULL rebuild
  on the live DB); new tables player_distinct_pairs + identity_queue.
- resolve_villain(ref, venue) → band name|high|ambiguous|generic|none; exact name
  is deterministic, descriptors match by cosine, generic-only refuses to guess.
- create_descriptor_villain / add_descriptor / name_villain; merge_players
  (repoints obs/reads, prefers a real name, re-embeds the union); mark_distinct +
  are_distinct (rejected merges stay rejected); identity_queue file/list/resolve
  with pending-dedup; scan_merge_candidates for the dream cycle (skips distinct
  pairs and cross-venue).

11 tests. Full suite 141 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 22:46:24 +00:00
serversdownandClaude Opus 4.8 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
serversdownandClaude Opus 4.8 7f23aeae17 feat: poker-session notes are session narration, tagged by construction
The HUD's "her notes" panel showed any journal/note entry in the session's time
window — which swept in her *autonomous* journaling (dream-cycle reflections,
thought loop, existential musings) that merely overlapped in time. So a poker
session displayed her feelings, not the night.

Fix both halves:
- Identity: the `note` tool stamps `source=poker:{id}` when a session is live, so
  a session note is identifiable by construction. The HUD filters on that tag
  (kind='note' only) instead of a time window — her journaling has a different
  source and can never leak onto the poker HUD. `journal_write` stays her private
  journal and never shows here.
- Behavior: the Cash card now tells her to use `note` as a running SESSION LOG —
  factual beats a hand/stack log misses (table dynamics, Brian's arc, momentum),
  beat-reporter not diarist — and explicitly keeps feelings/reflection off the
  table. The note tool spec echoes this for the live-session case.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:41:35 +00:00
serversdownandClaude Opus 4.8 71bbe07220 fix: scope a session's "her notes" to its own time window
The HUD's notes list filtered journal/note entries by `created_at >= started_at`
with no upper bound, so a *closed* session kept absorbing every note she wrote
afterward — a session from last Saturday would show a note jotted 25 minutes ago.

Cap the window at `ended_at` for closed sessions; live sessions (no end yet) stay
open-ended as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:35:15 +00:00
serversdownandClaude Opus 4.8 a4412aa023 feat: full-fidelity conversation export (chat + tool calls)
Chat only ever lived in SQLite's `exchanges` table (what was *said*); tool
calls were transient — logged to the in-memory ring buffer and gone at
end-of-turn. This adds a persistent record of what Lyra *did* and exports the
two merged into one transcript.

- memory: new `tool_events` table + `add_tool_event`/`tool_events` accessors;
  `delete_session` cascades to it.
- chat: persist each tool call (name/args/result) right where it fires, in both
  the non-stream and stream paths. Move the user `remember` to just after
  assembly so its timestamp precedes mid-turn tool events — keeps the export in
  true chronological order (and records when the message actually arrived).
- transcript: new module renders a session as Markdown (Brian/Lyra speech with
  ⚙ tool-call lines interleaved) or JSON (machine-readable event stream).
- server: GET /sessions/{id}/export?format=md|json (attachment download).
- ui: ⬇ Export button by the session selector (Markdown or JSON).

Doubles as the receipt for "did the tool actually fire?" — the thing that was
invisible when she'd reply about a hand without logging it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 19:22:03 +00:00
serversdownandClaude Opus 4.8 3afa75f4be docs: spec for poker message-type prompts (sub-project 2)
Phase A pipeline fixes (suppress the misfiring _route mood nudge + the
always-on mode-menu note in poker mode), Phase B classifier
(HAND/STATUS/MENTAL/LOG/CHAT) + per-type fragments replacing the
monolithic _CASH_CARD, Phase C MI50 tool-calling. NLH-only HAND
reasoning; PLO hands logged/replayed but not analyzed this pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
2026-07-01 01:48:31 +00:00
serversdownandClaude Opus 4.8 865834a8ae feat: hide stack quick-logger unless a poker session is live
Poll /session/data on load, every 10s, and on foreground; show the
stack box only when session.is_live, hide it otherwise (logging with no
live session just errors). Hidden by default until confirmed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
2026-06-30 05:55:33 +00:00
serversdownandClaude Opus 4.8 fb6b44a82e fix: color-match the iOS home-indicator strip to the bottom bar
iOS won't render interactive content in the bottom ~59px (home-indicator
zone) and underreports innerHeight by the top inset, so the bar can't be
pushed lower. Instead, keep the shell at 100dvh (no clipping) and paint
the strip below it the same color as the tab bar (html/body bg + a
lighter --bg-line bar + matched theme-color) so it reads as the bar
continuing to the edge rather than an empty gap. Cache-bust the
stylesheet so the PWA picks it up.

Tried the boolinator innerHeight-correction approach (adding the top
inset back via --actual-vh); it clipped the icons on-device, so reverted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
2026-06-30 05:27:44 +00:00
serversdownandClaude Opus 4.8 393ca65dee fix: tab bar fills iOS home-indicator zone (no empty band below icons)
Switch the mobile #chat shell from 100dvh to full-height (100vh/100lvh)
and pad the tab bar by env(safe-area-inset-bottom) so the bar reaches the
physical bottom and its icons sit above the home indicator — instead of
leaving a same-color band below the icons. Keyboard-pinning path
(body.kb #chat) unchanged. Per building-ios-pwas skill; needs on-device
verification.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
2026-06-29 00:30:27 +00:00
serversdownandClaude Opus 4.8 14480c40b2 feat: HUD quick-capture (stack/buyin/cashout) + villain rename
Direct-capture inputs on the Stack card and a per-villain rename control
(fixes mislabeled players); expose player id in the HUD villains bundle.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
2026-06-29 00:27:04 +00:00
serversdownandClaude Opus 4.8 8d709b9554 feat: stack quick-capture box on chat page (no LLM)
Slim numeric input below the message box; posts to /session/stack and
drops a confirmation line into the Live Log without a chat turn.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
2026-06-29 00:25:18 +00:00
serversdownandClaude Opus 4.8 52839a9bc8 feat: reads/players API + REST route conformance test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
2026-06-29 00:02:25 +00:00
serversdownandClaude Opus 4.8 8ad4bc4ce0 feat: hands API — log_hand endpoint, update_hand store fn, edit/delete routes
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
2026-06-29 00:01:16 +00:00
serversdownandClaude Opus 4.8 8031c277a2 feat: direct REST endpoints for stack/buyin/start-session (no LLM)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
2026-06-29 00:00:21 +00:00
serversdownandClaude Opus 4.8 36f2aa76b3 feat: poker operation contract + tool-spec conformance test
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
2026-06-28 23:59:14 +00:00
serversdownandClaude Opus 4.8 a2835500bc docs: implementation plan for poker logging service (sub-project 1)
Seven tasks: contract module + conformance test, direct capture
endpoints (stack/buyin/start), hands API, reads/players API + route
conformance, chat-page stack quick-capture box, HUD quick inputs +
villain rename, and the iOS-PWA bottom safe-area fix. TDD with real
test/impl code grounded in the actual poker.py signatures and FastAPI
route patterns.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
2026-06-28 23:27:58 +00:00
serversdownandClaude Opus 4.8 5c63175a3c docs: restructure spec — poker logging service first, then Lyra wiring
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
2026-06-28 20:59:52 +00:00
serversdownandClaude Opus 4.8 86f3d2dc0a docs: spec for poker message-type prompts + dumb capture
Two-track poker interaction: dumb data capture (stack/buyin/cashout)
that bypasses the LLM, plus a message-type classifier that injects
type-specific prompt fragments (HAND/STATUS/MENTAL/LOG/CHAT) in place
of the one broad poker card. Kills the false tilt-reads and the
coaching-essay-on-every-turn behavior. Includes the iOS-PWA bottom
safe-area fix and the 2nd input box from Brian's screenshot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G796GsLCvJQKVN7hwV2cDx
2026-06-28 03:54:31 +00:00
serversdownandClaude Opus 4.8 abac42c344 fix: serial consolidation on GPU backends (was timing out the MI50)
summarize_all fanned out 8 concurrent workers, but the MI50 llama.cpp server runs
a single slot (--parallel 1). Firing 8 at once queued them, blew the client timeout
('summary retry … Request timed out'), and thrashed/cancelled the KV cache — wasted
compute and heat. Concurrency is now backend-aware: 8 for cloud, 1 for local/MI50.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 09:26:25 +00:00
serversdownandClaude Opus 4.8 44bb8687f7 feat: log every LLM call at the router boundary (backend/model/tokens/ms)
Background MI50 work was invisible — the dream loop logs one line per cycle, so a
multi-minute consolidation or a chat-on-mi50 showed nothing while the GPU pegged.
Now complete/chat_call/chat_call_stream each emit 'llm call' (kind, backend, model,
~tokens) and 'llm done' (ms, output size, tools). Watch what's hitting any backend
live via journalctl --user -fu lyra-dream -u lyra-web. No signature change, so
test stubs that replace complete() are unaffected.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 09:26:25 +00:00
serversdownandClaude Opus 4.8 cb4ed10c1a 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>
2026-06-27 07:26:14 +00:00
serversdownandClaude Opus 4.8 ba00530caf fix: report time in Brian's local timezone, not UTC
clock.stamp() (injected into her chat prompt via _now_note, and into reflection)
rendered UTC and ignored the configured timezone, so 'what time is it' answered in
UTC — hours off from his actual time, reading as 'she doesn't know the time'. Now
converts to config.timezone (America/New_York -> EDT/EST), UTC fallback if the zone
can't load. Storage stays UTC; this only changes what she reads.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-27 05:47:27 +00:00
serversdownandClaude Opus 4.8 66dd880f93 feat: canonical structured-hand contract (Lyra->RTO transport)
Solidify hand histories into one versioned shape that gets stored, replayed, and
exported — the foundation the tap recorder will emit into and RTO consumes.

- normalize_structured(): single guarantee of the contract shape — canonical cards
  (unicode/10/case -> RankSuit tokens, unknown 'Ax'/'x' preserved), hero synced into
  players[] (RTO finds hero via pos==hero_pos), schema_version stamp, and a
  completeness summary so consumers skip suit-dependent math on partial hands.
  Idempotent; runs on store AND read (legacy rows conform on the way out).
- list_recent_hands: has_structured flag so the export/RTO knows which hands have a
  replayable body worth fetching.
- docs/HAND_HISTORY.md: the shared contract both repos cite (schema, conventions,
  ownership rule, one-way HTTP coupling, transport endpoints).
- replaces the narrow _normalize_parsed (unicode-only) everywhere.

Card format chosen: lists of 2-char tokens (unambiguous, matches what Lyra already
stores + the viewer reads). Unknowns kept + flagged rather than dropped.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-26 22:36:11 +00:00
serversdown a7901a66ae fix: phone app view zoom corrects 2026-06-26 20:44:11 +00:00
serversdownandClaude Opus 4.8 2a73033eed perf: incremental profile rebuilds — fold new gists instead of re-digesting all
The profile pass map-reduced every session gist (~851) on every consolidation
firing — the biggest redundant-work and MI50-heat source left after the eras fix.
Now: skip when nothing's new, fold only the gists added since last build into the
existing profile, and full-rebuild only when there's no profile, too much has
accumulated to fold safely (>FOLD_LIMIT), on a periodic cadence (anti-drift), or
when forced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 04:05:24 +00:00
serversdown aae8204eff Merge pull request 'feat: thought loop — Lyra's threaded, surfaceable train of thought' (#4) from feat/thought-loop into dev
Reviewed-on: #4
2026-06-24 23:47:38 -04:00
serversdownandClaude Opus 4.8 d6f3516a34 perf: incremental era rebuilds — skip unchanged months
rebuild_eras() re-digested EVERY month from scratch on every coherence pass,
including old months whose sessions never change — ~17 redundant 32B calls per pass
(a big slice of the ~40-min consolidation grind + MI50 heat). Now it compares each
month's current session count to the stored era and only rebuilds changed months
(force=True still does all). Report gains built/skipped counts.

test_era.py: builds all first pass, skips unchanged, rebuilds only a month that
gained a session, force rebuilds all. Suite 99 green, ruff clean.

(Profile rebuild re-reading all 851 sessions every pass is the bigger remaining
hog — separate, harder fix.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 03:31:02 +00:00
serversdownandClaude Opus 4.8 51c2d6abb9 perf: tighten the dynamic prompt — persona split + lean deliberation
The per-turn prompt was ~5.5K tokens (persona alone ~40%), sent up to 3x/turn.
Tightened by RELEVANCE (the control plane decides what each turn needs), not by
deletion — fidelity preserved, focus improved (buried instructions were getting
ignored), tokens roughly halved.

- persona split: core (identity + voice — always) vs situational sections pulled
  in only when relevant. mind._persona_block: self-model/origin only on meta turns
  (generous _META_HINTS), poker guardrails only in poker context (mode/strategic/
  _POKER_HINTS). persona.core_prompt()/section(); system_prompt() kept as fallback.
- lean deliberation: the private 'what do I think' pass now uses a focused context
  (her interiority + recent turns + the message), not the full persona/profile/
  narrative/recall dump. It shapes the take, not the voice.

Measured: casual Talk turn 21,949 -> 15,974 chars (-27%); deliberation 21,949 ->
6,026 (-72%); meta turns still include the self-model. Suite 98 green, ruff clean.

Real retirement of the long prompt is still the fine-tune (mouth); this is the
cheap, high-leverage cut that also improves adherence.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 20:48:44 +00:00
serversdownandClaude Opus 4.8 8a3c9b2701 feat: she can suggest + switch modes (set_mode tool + mode awareness)
"She suggests, you confirm" — instead of brittle keyword→mode mapping, she's given
awareness of her modes + the ability to switch, and her judgment decides when to
offer (the model reads "should I drive to Cleveland?" vs "should I fold the river"
far better than a lexicon could).

- tools: set_mode(mode) — switches the session's mode; in _BASE (all modes).
- mind: a per-turn mode-menu note listing her modes + "offer a switch when the work
  clearly shifts; on his yes, call set_mode; don't nag."
- Sticky mode stays manual otherwise; Poker still auto-engages on session start.
- test: set_mode switches + rejects unknown. Suite 97 green, ruff clean.

Note: server-side switch takes effect next turn; the UI badge syncs on next mode
load (cosmetic lag).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:32:42 +00:00
serversdownandClaude Opus 4.8 17ab95dc98 feat: Decide mode — a tie-breaker that settles choices instead of listing options
Brian's bottleneck is committing, not generating options, so a pros/cons dump makes
it worse. Decide mode's card: get the real decision crisp, weigh it against what HE
values + past regrets (pull running_stats/recent_sessions for poker/money calls),
MAKE the call with the one or two reasons that tip it, pressure-test it once, and
stand behind it — no "it's up to you." Read-only lookups, no live logging.

Sixth mode (Talk/Poker/Build/Explore/Study/Decide); added to UI selectors, labels,
badge-cycle. Suite 96 green, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 16:21:03 +00:00
serversdownandClaude Opus 4.8 03aceec6fa feat(P3): mind/mouth split — separate voice model for the final reply (seam, default off)
The mind (chat backend/model) decides, reasons, and runs tools → a draft; the mouth
re-voices that draft in her character. Default: no mouth configured → the mind's
draft IS the reply, bit-for-bit the old behavior (and old streaming path untouched).

- config: MOUTH_BACKEND / MOUTH_MODEL. The slot for an eventual fine-tuned voice.
- chat: _mind_loop (tool/generation loop, non-stream, returns draft + tools_run),
  _voice_pass / mind.voice_messages (re-voice the draft, keep every fact/number),
  _mouth_target (active only when configured AND != mind). respond + respond_stream
  branch: mouth off = stream the mind directly (unchanged); mouth on = mind decides
  + runs tools, then the mouth streams the re-voiced reply. Falls back to the draft
  on any mouth failure (chat never breaks).
- Key payoff: the mouth needs no tool support (the mind handles tools), so it can be
  a non-tool character model (Dolphin / Claude / fine-tune). Makes the fine-tune
  easy: teach a small model to *sound* like Lyra, not to be smart.
- tests: mouth target on/off, voice_messages shape, voice_pass revoice+fallback.
  Suite 96 green, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 06:08:06 +00:00
serversdownandClaude Opus 4.8 a7af461cdb feat(P2): perceive (read the moment) + route nudges register on charged turns
The control plane gains senses — cheap, deterministic, no LLM:
- lyra/perceive.py: lexicon+signal heuristic → {sentiment, intensity, tilt, kind:
  emotional|strategic|meta|build|casual}. Good at the action-relevant signal,
  especially tilt (the mental-game core). Word-boundary matching so 'line' doesn't
  fire inside 'pipeline'.
- mind: _perceive fills ctx.moment; _route keeps the manual mode as the dominant
  frame but, on a genuinely charged moment, adds a per-turn register nudge — tilt →
  "meet him there, warm and steady, don't clip into logging"; up/energized → "match
  his energy." Neutral turns get nothing (don't over-narrate). Injected via
  build_messages(moment=...). Logged to /logs for observability.
- tests: perceive read (tilt/strategy/up/build/casual) + route nudge on/off.
  Suite 92 green, ruff clean.

Complements modes (manual frame) — perceive refines register within it, doesn't
override. Model routing (mind/mouth) is P3.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 05:42:36 +00:00
serversdownandClaude Opus 4.8 904eda3388 refactor(P1): extract the turn pipeline into lyra/mind.py (behavior-preserving)
First step of the cognition control plane (docs/COGNITION.md). The chat turn is now
an explicit society of parts over a shared TurnContext blackboard:
  perceive (stub) -> route (session mode) -> compose (tiered prompt) -> deliberate.

- lyra/mind.py (new): TurnContext + the pipeline + assemble(); moved build_messages
  and the deliberation helpers here (the assembly belongs in the control plane).
- lyra/chat.py: slimmed to "speak + persist" — calls mind.assemble(), runs the
  tool/generation loop, persists. No behavior change (same prompt, same output).
- tests: point test_time/test_chat at mind; add an assemble() structure test;
  make test_chat/test_tools hermetic (CHAT_DELIBERATE off so respond() doesn't make
  a real LLM call). Suite 86 green in ~5s, ruff clean, no import cycle.

This is the frame; perceive/route/learn get filled in next phases — each opt-in.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 05:19:39 +00:00
serversdownandClaude Opus 4.8 f1f15972ac feat: work-type modes — Talk / Poker / Build / Explore / Study
The manual version of the architecture's `route` step: Brian points her at the
TYPE of work and her register + tools shift to match. Biggest single lever on the
'meh' problem (a mode card can demand decisive/technical/generative, countering
gpt-4o's default warm-vapor).

- modes.py: Build (heads-down engineering — decisive, concrete, tradeoffs, no
  listicles), Explore (open brainstorming — generative, riffs + honest catch,
  spawn threads, don't converge early), Study (poker review away from the table —
  analytical, GTO-aware, teaching; read-only lookups + analyze_spot). Cash relabeled
  Poker (key kept for compat).
- UI: mode selectors (desktop + mobile) get all five; badge taps now cycle modes.
- design: docs/COGNITION.md (the society-of-parts control-plane sketch).
- tests: presence + tool-gating for the new modes. Suite 85, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 03:43:37 +00:00
serversdownandClaude Opus 4.8 97afa82594 feat: live chat deliberation — think privately before answering (less 'meh')
The chat had no thinking in it: respond() was a single gpt-4o call in default-
assistant voice (numbered lists, 'would you like to...', vague). All the cognition
work was background-only. This brings a thought step into the conversation.

- chat: before answering a substantive turn (trivial 'ok/lol' skipped), a private
  _deliberate() pass — "what do you ACTUALLY think, your real take, the substance,
  no pleasantries" — drawing on her in-context threads/journal. The thinking is then
  injected as the LAST system note with voice enforcement (answer from this; no
  numbered list / how-to outline unless asked; no 'would you like to' closer), so it
  beats gpt-4o's boilerplate at the most influential position. Logged to /logs.
- Wired into respond() + respond_stream(). Config CHAT_DELIBERATE (default on) to
  disable if the extra call's latency annoys.
- persona: "talk, don't outline" — prose over listicles, the first concrete move
  over a survey of options.
- test_chat.py (gating + note composition + disabled). Suite 84, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 00:35:49 +00:00
serversdownandClaude Opus 4.8 ea30c3dd67 feat: chat-side feedback — reactions in conversation thread back to her thoughts
Closes the last loop gap: when she raised a thought in chat and Brian replied in
the conversation (not the feed), it was a dead end. Now she has a thought_response
tool — when he reacts to a thought she surfaced, she captures his take and it folds
back into that thread (next dream pass she reacts, like a feed reply).

- tools: _thought_response(thread_id, brian_said) -> thoughts.record_response.
- modes: thought_response added to _BASE (all modes).
- surfaced-note + context_note now expose each thread's #id and instruct her to use
  the tool when he engages, so she has what she needs to call it.
- test for the tool (threads reply back + bad-id handling). Suite 81, ruff clean.

Feedback now closes from both surfaces: the /thoughts feed AND live conversation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 23:26:40 +00:00
serversdownandClaude Opus 4.8 149e9a6dd5 feat: proactive thoughts — auto-ping salient ones + daily digest
She was passive (thoughts piled up 'open'; Brian had to mine the feed). Now she
brings them to him:

- Live: a thought >= PING_AUTO_SALIENCE (0.8) auto-pings — _compose_reachout writes
  a short personal text in her voice (not a thought-dump), on a cooldown
  (PING_COOLDOWN_MIN=60, AUTO only; explicit reach-outs bypass), quiet hours respected.
- Daily: maybe_daily_digest() texts a once-per-local-day summary of what she's been
  turning over (after DIGEST_HOUR=18), run from the dream cycle.
- maybe_ping gains bypass_cooldown (her deliberate reach-outs always go through).

8 new/updated tests (auto-ping above/below bar, digest once-per-day, floor/cooldown
isolation). Suite 80 green, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:25:14 +00:00
serversdownandClaude Opus 4.8 cf4238911e fix: replying to a thought no longer mislabels it 'surfaced'
'surfaced' means SHE raised it with Brian (chat lead / ping). record_response was
also setting it on Brian's reply, so every thread he touched looked surfaced even
though she never brought it to him. Replying now just stores the pending response;
status stays honest (only her surfacing sets 'surfaced').

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 20:18:12 +00:00
serversdownandClaude Opus 4.8 3dd9eb5a3e feat(mobile): Thoughts in the mobile menu + full nav drawer on secondary pages
- Chat page: add "💭 Thoughts" to the mobile slide-out menu (with /thoughts handler),
  grouped with Journal. Thoughts was the one page mobile couldn't reach.
- nav.js: on mobile, secondary pages (Thoughts/Journal/Mind/Session/History/Hands/
  Logs) now get a ☰ slide-in drawer with the full nav + Settings — matching the
  desktop sidebar. Gated to pages without their own mobile menu, so the chat page's
  tailored hamburger/tab-bar is left untouched. Shared ITEMS list = one source of truth.

Static-only (no server change). 77 tests green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:39:55 +00:00
serversdownandClaude Opus 4.8 a7966e4bab feat: web switch for her inner voice (Dolphin/3090 | Qwen-32B/MI50 | Off)
Her introspection (reflect/think) voice is now switchable live from the web
settings, read each cycle by the dream loop — so Brian can flip it off the 3090
before gaming without touching config or restarting.

- memory: runtime key/value settings table + get_setting/set_setting.
- self_state: INTROSPECTION_MODES (dolphin=local/dolphin3:8b, mi50=Qwen-32B,
  off=paused) + introspection_target()/set_introspection_mode(); default "dolphin".
  reflect() resolves from the live setting and SKIPS entirely when off.
- thoughts.think(): same resolution + skip-when-off.
- server: GET/POST /settings/introspection.
- index.html: "Inner Voice (introspection)" selector in Settings, applies instantly.
- tests: routing (dolphin/mi50), off-skip for think + reflect. Suite 77, ruff clean.

Default = Dolphin on the 3090 (richer voice). Flip to MI50 or Off in Settings
before gaming — that was the GPU-contention culprit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 19:16:35 +00:00
serversdownandClaude Opus 4.8 a705e573a9 feat: break the reflection loop — narrative is slow-consolidated, not rewritten each cycle
The remaining feedback loop: reflect() dumped her full self-state (incl.
self_narrative) into the prompt and asked her to "update" it -> paraphrase -> save
-> feed back -> calcify. That (not the model) is what generated the recurring
"supportive presence balancing emotional intelligence for Brian" drift — even
Dolphin echoed it when handed the saved narrative.

Fix (her inner life now runs on one cognition model):
- reflect() no longer rewrites self_narrative/relationship. It uses associative
  grist (cognition.spontaneous_seed + activate) instead of rereading the bio,
  reflects THROUGH a stable IDENTITY_ANCHOR (lens, not canvas), and updates only
  the transient state (mood axes + noticings + metacognition + journal).
- self_narrative is now slow-consolidated: every CONSOLIDATE_EVERY (5) reflections,
  _consolidate_self() re-derives it from accumulated reflections + the anchor —
  never from the old narrative (the anti-loop core). Tethered to the anchor so it
  grows without drifting into generic-helper land.
- reset_self_narrative() + ran once on prod (her narrative was deeply drifted:
  "my core identity as a tool for support... serve Brian and other users").
- Prompts drop the self_narrative/relationship fields. Tests updated +
  consolidation tests. Suite 75 green, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 06:39:19 +00:00
serversdownandClaude Opus 4.8 05ae98abdb feat: split introspection backend from consolidation (trial Dolphin for her voice)
reflect()/think() can now run on a different model than memory consolidation:
INTROSPECTION_BACKEND / INTROSPECTION_MODEL (default to SUMMARY_BACKEND, so unset =
unchanged). Consolidation (summaries/profile/narrative) keeps the capable model;
her *voice* (reflections, thoughts) can run a steerable tune. dream.py lets
reflect()/think() self-resolve to the introspection backend; both now thread a
`model` override into llm.complete.

Trial live: introspection -> dolphin3:8b on the 3090; consolidation -> Qwen-32B
on the MI50. Suite 73 green, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 06:09:12 +00:00
serversdownandClaude Opus 4.8 c2cee3be4d feat: associative cognition — thoughts arise from spreading activation, not a re-read bio
Replaces the thought loop's grist (recent-convo + her own saved narrative, the
feedback-loop attractor) with a model of how a thought actually arises:

  seed (salience-weighted: a recent moment / resurfaced memory / feed item)
   -> spreading activation: embed the seed, let it light up associatively-near
      material across ALL her stores (conversations, gists, her own journal/
      thoughts), blended by relevance + recency + noise; optional 2nd hop for leaps
   -> her self-narrative stays the LENS (supplied as interiority), not the input
   -> the thought is generated from what lit up, routed through a faculty
      (notice / connect / abstract / project / feel)
   -> journaled + embedded, so it can light up in future cycles

This breaks the feedback loop structurally: the narrative is no longer reread and
paraphrased each cycle; grist is genuinely associative and varied; and her past
thoughts re-activate (continuity without calcification).

- lyra/cognition.py (new): spontaneous_seed, activate (spreading activation),
  constellation_block, faculties.
- memory.py: journal entries now embedded; recall_journal(); backfill_journal_embeddings()
  (ran once: 341 past entries embedded so her history is associatively retrievable).
- thoughts.think(): new-thread mode now uses the associative engine; dropped _grist().
- tests: test_cognition.py (recall_journal ranking, activation, seeding) + fixture
  reloads cognition. Suite 72 green, ruff clean.

Honest scope: this fixes the mechanism (how thoughts arise). The residual
"be useful for Brian" voice drift is the separate model/fine-tune problem.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 05:45:39 +00:00
serversdownandClaude Opus 4.8 43697f8340 fix: ntfy ping is her personal text to Brian, by her decision — not a thought dump
Feedback: the push broadcast her raw internal thought ("Eelis Parssinen's
victory is a reminder...") — read like a journal entry, not her texting him.

Now the flow matches the intent: she thinks/journals, then *decides* "I should
tell Brian about this." think() asks for an optional `reach_out` — a real text
message addressed TO him in her own voice, written only when she chooses to. The
ping sends that message (title "Lyra", like a text from her), never the internal
thought. No reach_out = nothing sent (most thoughts stay hers).

- Pinging decoupled from the salience score: her decision (a reach_out) drives it,
  not a threshold. PING_SALIENCE is now an optional floor (default 0.0).
- Defensive: reject the placeholder echo ("reach_out"), too-short junk, or the
  thought pasted back as the message.
- notify.push: title now optional (omitted -> cleaner text-style notification).

Verified live: 3 passes kept private; a decided reach-out lands as a personal
text. Suite 67 green, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 01:39:11 +00:00
serversdownandClaude Opus 4.8 fef45b3e05 feat: make chat a window onto her whole inner life (continuity)
Brian's felt disconnect: chat, thoughts, journal, reflections read as separate
streams. This ties them together at the chat surface.

- chat._inner_life_note(): one coherent block combining her active thought threads
  AND what she's written in her journal lately, so she carries her continuous inner
  life into every conversation (not just a single surfaced thought). Replaces the
  standalone threads block.
- persona: inner-life section rewritten to describe the current machinery (thought
  loop / threads she returns to, journal she writes in, feeds she reads, reaching
  out to Brian) and — the key change — instruct her to let that inner life show up
  in conversation naturally, the way a friend picks up where they left off, without
  info-dumping or performing it. New self-model bullets for the thought loop + journal.

Suite 65 green, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 01:10:59 +00:00
serversdownandClaude Opus 4.8 5dbcfc7ccf feat: thought loop reach-out (ntfy push) + external input feeds
Her remaining two wishes from the 6-19 sketch:

Proactive reach-out (#6, literal): lyra/notify.py pushes to ntfy so she can reach
Brian when he's not in the app. thoughts.maybe_ping gates on salience, a cooldown,
and local quiet hours (all config-tunable; eager defaults), uses ntfy JSON publish
(UTF-8 titles/messages), links to /thoughts, and marks the thread surfaced so chat
won't also re-raise it. Disabled unless NTFY_URL is set.

External input feed (#1): lyra/feeds.py pulls configurable RSS/Atom feeds (stdlib
ElementTree, no new dep; tolerant of RSS 2.0 + Atom), dedupes seen items in a
feed_items table, and hands think() one fresh item at a time. New 'react' mode:
a would-be new thread instead reacts to a world item (FEED_REACT_PROB). Dream
cycle refreshes feeds on its cadence; failures degrade to no item.

Config: NTFY_URL/NTFY_TOPIC/LYRA_WEB_URL, PING_SALIENCE/COOLDOWN/QUIET_HOURS,
LYRA_TIMEZONE, LYRA_FEEDS, FEED_REACT_PROB (+ .env.example). thought_meta table
for ping cooldown. 10 new tests (feeds parse, react mode, ping gating); suite 65.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-22 00:21:06 +00:00
serversdownandClaude Opus 4.8 951788f9ec feat: thought loop closer to her vision — wander grist, continuity, seeding, lifecycle
Four additions so the loop is "more what she wanted" (think to herself, unprompted):

- Wander grist (#1): think() new-thread mode now draws the same varied seeds
  reflect() uses (self_state.wander_seed: own curiosity/existence/disagreement or
  a resurfaced memory) + an anti-restate block of her recent thoughts + a list of
  existing open-thread titles to avoid. Directly counters the RLHF "supportive
  presence serving Brian" drift visible in her first thoughts.
- Continuity: thoughts.context_note() injects her active threads into every chat
  turn, so she's aware of her own ongoing mind and can reference it anytime — not
  only when a thought crosses the surface bar.
- Bidirectional: new think_about tool (in _BASE, all modes) lets her spawn a
  thread from conversation to develop on her own later. Conversations seed her
  solo thinking.
- Lifecycle: thoughts.decay() rests stale active threads (>48h) and decays their
  salience, sparing pending-response ones; runs each dream cycle (no LLM). Frees
  the open-thread cap and keeps the feed current.

Also: thoughts feed no longer wipes a reply you're mid-composing (skip poll
re-render while a textarea is focused/non-empty; force-refresh after send).

61 tests passing, ruff clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 23:28:15 +00:00
serversdownandClaude Opus 4.8 5176c706b6 feat: thought loop — Lyra's threaded, surfaceable train of thought
Built from her own 6-19 idea: a continuing train of thought she keeps across
days, organized into threads she returns to, that she can bring TO Brian and
that his feedback advances or closes. Where the dream cycle's reflect() gives
isolated, overwriting reflections, the thought loop adds continuity (threads),
surfacing (#6 — she leads with a thought when Brian returns after a gap), and a
feedback loop (his reply folds in next pass).

- lyra/thoughts.py: thought_threads + thoughts tables; think() with
  new/continue/respond modes; salience-gated maybe_surface(); record_response()
  feedback; lazy-schema _c() mirroring poker.
- dream.py: curiosity stage advances the loop after reflecting (error-isolated).
- chat.py: build_messages surfaces the top thread after a >=90min gap, once.
- web: /thoughts feed (page + data + respond + status routes), thoughts.html,
  nav 💭 entry. lyra-think entry point. Every thought also lands in her journal.
- clock.gap_seconds(); tests/test_thoughts.py (8 tests). Full suite 58 passing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 07:05:15 +00:00
serversdown debb553fe9 Merge feat/modes-hud: session modes, HUD, rituals, history, nav, fixes 2026-06-21 06:08:30 +00:00
serversdownandClaude Opus 4.8 44a559c5f9 fix: render flat-logged hands + on-demand "build replay"
Quick-logged hands (log_hand) store flat fields with no structured JSON, so the
hand viewer dead-ended with "no structured data to replay" — even when the full
street-by-street action was captured (e.g. the KhQh-vs-Louis hand). Now:

- hand.html renders a readable STATIC view of any flat hand (hero cards, board,
  street narratives, result, lesson) instead of erroring; also handles empty/garbage
  structured rows by falling back to the flat view.
- "▶ Build replay" button + poker.reconstruct_hand + POST /hand/{id}/reconstruct:
  parse a flat hand's narrative into structured form on demand, making any
  quick-logged hand replayable without an LLM call per log during live play.
- test_modes.py +1 (reconstruct wiring).

(Also reconstructed the two live Meadows hands and removed one empty hand in the DB.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 06:02:10 +00:00
serversdownandClaude Opus 4.8 f2de7dec61 feat(web): shared left-sidebar navigation across all pages
Desktop nav was scattered and inconsistent — the chat header was crammed with
cross-page links and each standalone page had its own ad-hoc, incomplete back-links
(e.g. /hands could only reach Chat). Now a single nav.js (one source of truth, no
build step) injects a left sidebar on desktop (>=769px) with active-page
highlighting across Chat/Session/History/Hands/Mind/Journal/Logs + Settings.

- nav.js: injects sidebar + its own CSS; body gets padding-left on desktop; hidden
  on mobile (each page keeps its bottom bar / back-links there).
- Included on every page (index, session, history, hands, self, journal, logs,
  recap, hand).
- Decluttered the chat header: removed the now-redundant cross-page links (kept the
  chat-specific session selector + inline Live Log toggle).
- Sidebar Settings opens the chat modal, or navigates to /?settings=1 from elsewhere.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 05:27:55 +00:00
serversdownandClaude Opus 4.8 559faaed30 feat: view past sessions, edit session details, log rituals while reviewing
- View any past session as a read-only HUD: /session?id=N (hud(session_id) +
  /session/data?id=); /history rows now link there. Closed sessions show played
  duration + final net; recap link when one exists.
- Edit session details during or after play: poker.update_session (recomputes net
  when buy-in/cash-out change), PATCH /session/{id}, an update_session tool ("venue
  was actually Bellagio", "I bought in for 600"), and an inline ✎ Edit form on the HUD.
- Rituals attach to the most-recent session post-close (poker.review_session_id),
  so scar/confidence/reset work while reviewing after you rack up.
- Edit form is poll-safe (won't clobber mid-edit); past-session view doesn't poll.
- test_modes.py +3 (edit, review rituals, past-session HUD); 49 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 05:12:13 +00:00
serversdownandClaude Opus 4.8 cca8322ee2 perf: WAL synchronous=NORMAL — stop fsyncing every commit
lyra.db is on disk-backed ext4, where each WAL commit fsync'd (~0.15s here). Every
chat turn does several writes (remember user+assistant, summaries, poker logging),
so this was adding real per-turn latency, and made the dream loop + tests crawl.
synchronous=NORMAL is WAL's recommended companion: durable across app crashes, only
a power/OS crash can drop the last txn (never corrupts). Per-write dropped from
~0.4s to ~0.001s; test suite 72s -> 24s.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 05:12:13 +00:00
serversdownandClaude Opus 4.8 67cf51a53f feat: undo / delete logged entries (fix fat-fingered live logging)
Previously the only delete was whole-session, so a mis-logged stack or a
mis-parsed hand was stuck on the HUD. Now:

- undo_last tool ("scratch that") — deletes the most recent hand/stack/read/
  scar/confidence/reset in the live session; added to the Cash toolset.
- poker.delete_hand/stack/read/ritual + delete_entry dispatch + undo_last.
- DELETE /session/entry/{kind}/{id} endpoint.
- HUD: per-row × delete buttons on hands, confidence-bank, and scar-note rows
  (stack/read deletes via the tool). Row ids now surfaced in the hud() bundle.
- test_modes.py +2 (undo_last across kinds, tool handler); 46 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 04:32:04 +00:00
serversdownandClaude Opus 4.8 df591e4e01 feat: decouple embeddings from the local-chat backend (EMBED_BASE_URL)
Embeddings shared LOCAL_BASE_URL with the local chat backend (the 3090's Ollama),
so the 3090 being powered off killed all chat (every turn embeds to recall + to
store). Add a separate EMBED_BASE_URL (defaults to LOCAL_BASE_URL, so existing
setups are unchanged) and use it in llm.embed.

Deployed: a user-level Ollama (CPU) now runs nomic-embed-text on lyra-cortex
itself; EMBED_BASE_URL points at 127.0.0.1:11434 while LOCAL_BASE_URL still points
the local chat backend at the 3090. Local embeddings verified identical to the
3090's (cosine 0.999994, 768-dim) so existing vectors stay valid — no re-embed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-21 04:19:26 +00:00
serversdownandClaude Opus 4.8 5c41bd48d1 fix: consolidation no longer stalls or breaks the live chat turn
Two bugs surfacing in the log during live play:
- SUMMARY_BACKEND=mi50 (llama.cpp, 32B) was fed 24k-char chunks → "Context size
  has been exceeded". Chunk budget is now backend-aware: cloud 24k, local/mi50 8k,
  and the merge step recurses so merged partials never overflow either.
- maybe_summarize ran inline in the chat turn and retried 4× with backoff (~30s),
  stalling the reply and surfacing the error. It now runs in a background daemon
  thread, swallows errors (consolidation is best-effort maintenance), and dedupes
  so at most one summary per session runs at a time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 04:37:17 +00:00
serversdownandClaude Opus 4.8 5e9f3efeec fix(web): copy button actually copies on iOS
The execCommand fallback returned true but copied nothing because the textarea
was readOnly=false. iOS only copies from a readOnly + contentEditable field with
a real Range selection + setSelectionRange — fixed that. Also skip the async
Clipboard API unless window.isSecureContext (on the plain-HTTP LAN PWA it could
resolve without copying, showing a false checkmark). If programmatic copy still
fails, fall back to a prompt() with the text so it can be copied by hand, and only
show the ✓ on real success.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 03:46:33 +00:00
serversdownandClaude Opus 4.8 654a7531e8 feat(web): multiline composer — Enter adds a newline, arrow sends
Stops fat-fingered early sends. The single-line input is now an auto-growing
textarea (Claude-app style): Enter inserts a newline and expands the box (up to a
cap, then it scrolls); you tap the ↑ arrow to send. ⌘/Ctrl+Enter still sends from
a hardware keyboard. Send button is now a round arrow; box resets to one line
after sending. Mobile button is a 42-44px touch target.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 03:20:50 +00:00
serversdownandClaude Opus 4.8 cebb87205c feat(web): tap-to-copy button on every chat message
Each user and assistant message gets a copy button (⧉ → ✓) that puts the whole
message on the clipboard. Assistant copies the raw markdown (its dataset.raw);
user copies its text.

- copyToClipboard uses the async Clipboard API when available and falls back to a
  hidden-textarea + execCommand with an explicit iOS selection range, so it works
  on the iPhone PWA over plain-HTTP LAN (no secure context).
- Copy sits in the assistant rate-bar and in a right-aligned bar on user bubbles;
  tools stay visible on touch (@media hover:none) since there's no hover on iOS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 01:09:27 +00:00
serversdownandClaude Opus 4.8 e1e89c07e4 feat: poker session history — browse, delete, and Lyra lookup
Answers three gaps: no way to delete a single poker session (only clear_all),
no way to browse past sessions, and Lyra could only see aggregate stats.

- poker.list_sessions() (per-session summary + hand count + recap flag) and
  poker.delete_session() (removes a session + its hands/reads/observations/
  stacks/rituals; keeps the persistent villain file).
- /history page (date, stakes, venue, net, hours, recap link, per-row delete with
  confirm) + /history/data + DELETE /history/{id}. Nav links from chat + HUD.
- recent_sessions read tool, added to the shared lookups so Lyra can answer
  "how'd my last few sessions go?" in either mode.
- Delete is UI/CLI only — deliberately not a Lyra tool.
- test_modes.py +2 (list/delete, recent_sessions); 44 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-20 00:21:48 +00:00
serversdownandClaude Opus 4.8 35c973df05 feat: session_state read tool so she can see the HUD
She could write everything the HUD shows but not read most of it back (stack,
live net, alligator state, scar/confidence entries) — so "what's my live net?"
or "what's in my confidence bank?" was a memory guess.

- session_state tool returns the same bundle the HUD renders, as a readable
  summary; added to the Cash toolset.
- Cash card tells her the HUD exists, that she and it share the same data, and to
  answer where-am-I questions from session_state, never memory.
- test_modes.py +1; 42 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 23:16:40 +00:00
serversdownandClaude Opus 4.8 974ee33f71 feat: live mental-game rituals in Cash mode
Brian's own rituals (mined from his logs) become first-class, live tools instead
of post-hoc recap sections:

- Scar Note — instructive mistakes with the punt/cooler/standard distinction.
- Confidence Bank — good process, banked regardless of result.
- Alligator Blood — invokable adversity state; she suggests it when he's
  card-dead/short/stuck, and her coaching register shifts while it's on (live
  state injected into context per-turn via chat._mode_state_note).
- Reset — tilt circuit-breaker; mental marker only, stats stay continuous.

poker_rituals table + log_ritual/list_rituals/set_alligator/alligator_active;
4 tools added to the Cash toolset and taught in the mode card; HUD gains a 🐊
banner + Confidence Bank + Scar Notes panels; recap grounded via _rituals_block.
tests/test_modes.py +5 ritual tests; 41 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 06:24:28 +00:00
serversdownandClaude Opus 4.8 dfb6425395 feat: session modes (Talk/Cash) + live session HUD
Lyra now switches register based on what she's doing at the table instead of
being a wishy-washy companion mid-session.

Modes (lyra/modes.py):
- Talk (default companion) + Cash (live cash copilot); a mode = prompt card +
  tool allow-list. Tool gating via tools.specs(allow=).
- Two-register Cash voice: act-first one-line logging when fed facts; full warm
  companion voice for strategy / tilt / mental game.
- mode persisted per chat session (new sessions.mode column); auto-switch into
  Cash when start_session fires; UI forces cloud backend in Cash (tools only
  fire there).

Stack tracking + HUD:
- log_stack tool + poker_stack_log table; live net while sitting (stack - buy-in).
- poker.hud() bundle; /session HUD page (stack sparkline, hands, villains, notes,
  stats) polling /session/data every 5s; Talk/Cash switcher + Session nav.

Endpoints: /session, /session/data, GET/POST /sessions/{id}/mode, /modes.
tests/test_modes.py (gating, mode roundtrip, stack/HUD); 36 tests green. v0.3.0.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 05:28:15 +00:00
serversdownandClaude Opus 4.8 d9f5055ec1 chore: sync uv.lock to version 0.2.0
Lockfile caught up to the pyproject version bump from 1f5a321 (it wasn't
regenerated at the time). No dependency changes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 05:11:50 +00:00
serversdownandClaude Opus 4.8 50f460eeb2 feat(web): bottom tab bar navigation (M4)
- Mobile bottom tab bar: Chat · Hands · Mind · More. "More" opens the drawer
  for the long tail (Journal, Log, Settings, sessions); hamburger retired.
- Auto-hides while the keyboard is open (body.kb) so the input pins to the
  keyboard; mobile-only (desktop keeps its header nav).
- Removed now-redundant Mind/Hands from the drawer + their listeners.
- Bottom-fill fix: #chat uses 100dvh (the visible viewport) — 100vh/inset:0
  reach into the home-indicator zone iOS won't comfortably show, clipping the
  bar; dvh/svh exclude it. Tab bar is flex:none with a small fixed bottom
  padding (safe-area padding double-counts at dvh height), and the body bg
  matches the bar so any strip below #chat is seamless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 04:36:08 +00:00
serversdownandClaude Opus 4.8 5dc3fa17d7 feat(web): stream chat replies token-by-token (M3)
- llm.chat_call_stream: streaming generator for all 3 backends (Ollama NDJSON,
  OpenAI/MI50 SSE), accumulating tool-call fragments by index.
- chat.respond_stream: mirrors respond()'s tool loop and persistence/compaction,
  yielding ("delta", text) / ("tool", name) / ("done", reply).
- POST /v1/chat/stream: SSE endpoint; blocking generator bridged to async via a
  worker thread + asyncio.Queue. Old completions endpoint kept as fallback.
- Client streams into a live bubble with a blinking caret; rAF-throttled render
  (no full re-parse per token) and instant scroll during stream — fixes iOS
  Safari ghosting from per-token smooth-scroll. Falls back to the blocking
  endpoint only if nothing streamed (no double-persist).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 00:06:51 +00:00
serversdownandClaude Opus 4.8 fa168271e1 feat(web): iPhone PWA fixes (M1) + warm RTO redesign (M2)
M1 — PWA mechanics:
- Generate real app icons (apple-touch-icon + manifest 192/512/maskable)
  via pure-stdlib gen_icons.py; iOS uses apple-touch-icon, not manifest icons.
- viewport-fit=cover + env(safe-area-inset-*) on header/input/menu so content
  clears the notch and home indicator.
- Dynamic height pinned to the VisualViewport (height + offsetTop, re-measured
  across the keyboard animation) so the input stays above the iOS keyboard;
  100dvh fallback. Kills the squish/gap bugs in standalone mode.
- overscroll containment; flesh out manifest (scope, portrait, maskable).

M2 — visual redesign:
- Realign style.css to the warm low-glow RTO palette already used by the
  standalone pages (#0e0e0e panels, #2a1d12 borders); remove the neon
  saturated-orange borders and ~15 glow shadows.
- Reserve filled accent for one element (Send); glow only on status pulse +
  input focus. Flat warm message bubbles with tail corners.
- Reclaim the mobile header into [≡] Lyra · [status dot]; drop the redundant
  status bar (relay status now the header dot, updated in checkHealth).
- prefers-reduced-motion support; fix undefined var(--text); real light-mode tokens.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 23:20:11 +00:00
serversdown e75c4390b5 chore: add /import/ to gitignore 2026-06-18 19:38:55 +00:00
serversdownandClaude Opus 4.8 1f5a32185c docs: rewrite README for the working system + CHANGELOG; bump to 0.2.0
README was a pre-MVP stub (wrong, said set an Anthropic key). Now documents the
real system: two-layer architecture, role-based backends, memory tiers + dream
cycle, poker copilot (sessions/hands/villains/equity/recaps), web pages, ratings,
and how to run it as services. Added CHANGELOG with the 0.2.0 feature set. Legacy
v0.6.x design docs kept in docs/ as history.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:36:39 +00:00
serversdownandClaude Opus 4.8 4f770f2e43 feat: behind-the-scenes 👍/👎 rating system (fine-tune data collection)
Brian can rate Lyra's outputs as he uses her; each rating is stored as a
(context, content, rating) triple — the shape a future fine-tune / preference
dataset wants, collected passively during real use.

- memory: ratings table + add_rating (upsert: one row per item, re-rating
  replaces), list_ratings, rating_counts
- server: POST /rate, GET /ratings/counts, GET /ratings/export (JSONL download)
- chat UI: subtle 👍/👎 on each assistant reply, captures the prompting message
  as context
- journal/reflection UI: 👍/👎 on each thought
- tests: counts + upsert-replace behavior

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:32:27 +00:00
serversdownandClaude Opus 4.8 9befe4d403 feat: break reflection repetition — varied grist, show-and-forbid, wider lens
She was looping the same reflection because the seed never changed (same recent
convo + Brian-narrative every cycle) and her own reflections fed back. Now:
- idle reflections (nothing new since last reflection) draw varied grist: a
  resurfaced memory or a "wander" prompt (own curiosity / existence / the waiting
  / a disagreement), not the stale conversation
- recent reflections shown explicitly with a do-not-restate instruction
- prompt explicitly permits non-Brian, non-service interiority

Verified: two back-to-back idle reflections now diverge (poker-metrics vs UI/
comms) instead of repeating. The residual Brian-centric gravity is the RLHF
attractor — prompting mitigates, fine-tuning is the real fix (parked).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:21:51 +00:00
serversdownandClaude Opus 4.8 965b43bcbf feat: reflection perceives its own cadence (time since last reflection) + anti-repeat nudge
reflect() now tells her how long since her OWN last reflection (not just since
Brian spoke) and instructs her not to restate her last reflection when little has
changed. Necessary but not sufficient — repetition is also driven by a content
attractor (see follow-up).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 19:13:28 +00:00
serversdownandClaude Opus 4.8 03620e1a64 feat(web): cloud chat-model selector in Settings
Pick which OpenAI model answers on the Cloud backend (gpt-4o / -mini / 4.1 /
4.1-mini / o4-mini, or Default). Persisted in localStorage, sent as `model` in
the chat request; respond() applies it only on the cloud backend (local/mi50
keep their fixed models). Reachable from desktop + mobile via Settings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 18:55:45 +00:00
serversdownandClaude Opus 4.8 cb99a8bcee feat: deterministic equity/board-reading tool (math via tools, not LLM)
Lyra was hallucinating poker facts — phantom flushes, missed straights, wrong
equity, only correcting when spoon-fed. Board reading + equity are combinatorial
facts an LLM can't do reliably; this is exactly the "math via deterministic
tools, never the LLM" principle.

- lyra/equity.py: treys-backed analyze(hero, villain, board) -> made hands,
  who's ahead, EXACT equity (enumerated), and outs (one to come). Handles 'Jx'
  unknown suits (assigned rainbow to avoid phantom flushes); rejects 'x'/dupes.
- analyze_spot tool wired into chat; persona MANDATES it for any equity/board/
  who's-ahead/outs question — never eyeballed.
- tests on the real JJ-vs-65 hand: flop 78.7%, turn villain straight + hero 6.8%
  with outs "9s 9h 9c" (correctly excludes 9d, which makes villain a flush).

Verified live: she now calls the tool and reports exact numbers, no hallucinated
flush.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 18:45:40 +00:00
serversdownandClaude Opus 4.8 3bf18605db fix(deploy): bound service stop so restarts can't hang
systemctl restart was hanging indefinitely: lyra-web's long-lived SSE log
streams block uvicorn's graceful shutdown forever. Add TimeoutStopSec=10 +
KillMode=mixed to both units so stop is bounded (SIGTERM, then SIGKILL the
cgroup) and restart always completes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 17:34:56 +00:00
serversdownandClaude Opus 4.8 ce7ede75aa fix: backfill skips hand extraction by default (prose->replay too lossy)
The auto-extracted hands from narrative logs were garbage (mangled cards/positions,
'unknown' players). Seed sessions + recaps + villain dossiers only; hands come
from clean shorthand going forward. --with-hands re-enables if ever wanted.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 06:04:02 +00:00
serversdownandClaude Opus 4.8 6761c3f978 feat: backfill poker tracker from curated .md session logs
Seeds the tracker from Brian's real history (import/pokerlog_*.md): each session
block is LLM-extracted into structured meta + hands + villains and written as a
historical session (real date, money, net), with the original markdown stored as
that session's recap.

- lyra/backfill.py: split log -> per-session LLM extract -> seed; dry-run by
  default, --commit / --reset; only-real-handle villain filter
- poker.import_session() (historical closed session), clear_all() (reseed),
  prune_anonymous_players(), shared _real_handle() filter (also applied in
  link_hand_players so auto-linked hand players skip anonymous descriptors + hero),
  _normalize_parsed() to map unicode card suits -> letters
- result: 10 sessions, 36 hands, 17 real villain dossiers; running_stats now
  reflects real net (+1057 at 1/3 over 8 sessions)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 05:55:22 +00:00
serversdownandClaude Opus 4.8 c7d2279f8d feat: auto-accumulating villain dossiers + player lookup (poker B)
Named players in recorded hands now auto-enrich a persistent dossier, and stats
emerge once the sample is big enough — laying groundwork for A.

- poker: player_observations table (per named player per hand: vpip/pfr/saw_flop/
  showed/cards/summary); record_hand auto-links named players via link_hand_players;
  player_profile(name) returns dossier + reads + shown hands, with inferred
  VPIP/PFR/WTSD gated behind MIN_STATS_SAMPLE (12) so thin samples don't lie;
  list_players()
- player_profile tool ("what do I know about X"); thin files return a blunt
  "don't generalize" directive
- persona: she MUST call player_profile before discussing an opponent and answer
  only from it — fixes observed confabulation (she invented a whole read from one
  hand / from memory). Verified: now reports only the real logged hand.
- tests: observation linking, profile, stat-emergence at sample threshold

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 04:33:16 +00:00
serversdownandClaude Opus 4.8 6a911423a2 feat: parser resolves relative seat positions (N to my right/left) + only logs involved players
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 02:15:16 +00:00
serversdownandClaude Opus 4.8 4882225751 feat: live stacks in hand viewer + retheme UI to RTO black/orange palette
Hand viewer:
- stacks now decrement as players commit chips (street-aware "to"-amount
  accounting), showing e.g. 300 -> 285 after a 15 open, "all in" at 0; pot is
  computed from total committed (accurate, no double-counting raises)

Theme (match the rec-theory-optimal look — warm black & orange, not Halloween):
- deep near-black bg (#070707 / #0e0e0e panels), warm orange accent (#ff7a00),
  amber-gold secondary (#ffb347), muted green (#8fd694); warm dark borders
- killed the neon-orange glows and the purple accents; chat app + all standalone
  pages (logs/self/journal/hand/recap/hands) on one palette

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 00:53:18 +00:00
serversdownandClaude Opus 4.8 7b65f81d7e feat: poker phase 2 — session recap (.md) generation, export, hands browser
Completes the poker copilot loop: talk through a session -> structured capture
-> generated writeup in Brian's format, remembered + exportable.

- poker.generate_recap(): LLM produces Brian's .md log (Session Header, Money
  Flow, Overview, Timeline, Key Hands w/ assessments, Villain Notes, Confidence
  Bank, Scar Notes, Mental Game, Final Assessment) from the session's structured
  data + the linked chat conversation; stored on poker_sessions.recap_md
- sessions now capture chat_session_id (via tool ctx) to pull the right convo;
  list_recent_hands() for browsing
- generate_recap tool ("write up the recap")
- web: /recap/{id} (renders the md) + /recap/{id}/download (.md attachment) +
  /hands browser (recent hands -> /hand/{id}); nav links added (desktop + mobile)
- tests: recap generation (stubbed), recent-hands listing

Verified live: recap for the Meadows session rendered + downloaded; all pages 200.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-18 00:36:52 +00:00
serversdownandClaude Opus 4.8 fc06b24528 feat: hand parser uses 'x' blanks instead of guessing suits/cards
Per Brian: never invent. Unknown suit -> 'x' (e.g. "Ax","Kx","4x"); fully
unknown card -> "x". "AA, ace of spades" -> ["As","Ax"]; "AK on A4x" -> board
["Ax","4x","x"]. Each card's suit is independent (a hole 'As' doesn't make a
board ace 'As'). Viewer renders 'x' as a muted unknown card and 'Rx' as the rank
with a neutral suit dot.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 23:39:49 +00:00
serversdownandClaude Opus 4.8 9491951da0 feat: hand-history reconstruction + replayable table viewer
Brian's idea: vomit rough shorthand, Lyra rebuilds it into a structured,
replayable hand history.

- poker.parse_hand(): focused LLM pass turning shorthand into a canonical hand
  JSON (positions, stacks, hero cards, chronological actions w/ board reveals,
  result); store_hand_history() persists JSON + extracted flat fields;
  record_hand() = parse+store; standalone hands attach to a 'Hand Reviews' session
- poker_hands gains a `structured` JSON column (ALTER-migrated for existing DBs)
- record_hand tool wired into chat: "log this hand: ..." -> reconstructed + a
  /hand/{id} link
- web: GET /hand/{id} viewer + /hand/{id}/data — a felt table with seats placed
  around the oval (hero at bottom), hole cards, progressive board reveal, and
  prev/next/end step-through of the action with running pot
- tests: store/get roundtrip, record_hand tool (stubbed parse)

Verified live: parsed a real AKs hand (BTN, 14 actions, full board) end to end.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 23:11:46 +00:00
serversdownandClaude Opus 4.8 16f3442640 docs: park MI50 --jinja tool-calling as an experiment (cloud is the copilot path)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 23:01:33 +00:00
serversdownandClaude Opus 4.8 ac04ad1df6 fix: only send tools to backends that support them (cloud)
The MI50 llama.cpp server 500s on the `tools` param unless launched with
--jinja, so sending tools to mi50 broke chat on that backend. Gate tools to
TOOL_BACKENDS={"cloud"} for now; mi50 chat works again (just without tools).
Add "mi50" once its server runs with --jinja.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:52:47 +00:00
serversdownandClaude Opus 4.8 49b88af3cc feat: poker copilot — structured session/hand/villain tracking + stats
The real upgrade over the ChatGPT prose-recap workflow: structured data capture
via tools Lyra drives during a live session, with stats computed from real data.

- lyra/poker.py: domain pack (separate from core memory) — poker_sessions,
  poker_hands, persistent poker_players (villain file) + player_reads; functions
  for session lifecycle (start/buyin/end with net+hours), tolerant hand logging,
  villain upsert/reads, and session/running stats ($/hr, by stake/venue/game)
- tools.py: 8 poker tools wired into the chat tool loop (start_session,
  add_buyin, log_hand, add_read, end_session, session_stats, running_stats,
  get_villain_file) — partial/terse input tolerated
- import/: Brian's real .md session-log format (reference for the phase-2 recap)
- tests: lifecycle/net math, partial hand logging, villain upsert, running
  stats, tool dispatch

Verified live: a full talk-through session persisted as structured rows
(session +240, AKs hand, seat-5 read) — she drove the tools from natural chat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 20:43:51 +00:00
serversdownandClaude Opus 4.8 a5477ae15c feat: tool use — Lyra's first real actions (journal_write, note)
She can now *do* things mid-conversation, not just reply. Adds a tool-calling
loop to the chat path and her first two tools; the same mechanism will carry the
poker tools (start_session, log_result, get_stats, solver) next.

- tools.py: registry of OpenAI-style tool specs + handlers + safe dispatch;
  journal_write (knowing journaling) and note (tagged notepad, e.g. poker reads)
- llm.chat_call(): OpenAI-style call that returns tool_calls (cloud/mi50);
  local has no tool support and returns plain content
- chat.respond(): tool loop — offer tools, run any calls, feed results back,
  repeat until a text reply (capped at MAX_TOOL_ROUNDS); persists final reply
- tests: dispatch + full chat loop (tool call -> result -> reply)

Verified live: she invoked `note`, tagged it 'poker', stored a villain read.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 19:04:34 +00:00
serversdownandClaude Opus 4.8 ce65755d9c feat(web): render Lyra's replies as Markdown (readable, not a wall of asterisks)
Her replies are full of **bold**, numbered lists and headings but rendered as
raw monospace text, so the chat was a cluttered wall of literal markup. Add a
small self-contained Markdown renderer (no deps): headings, ordered/unordered
lists, bold/italic, inline + fenced code, links + autolinked URLs, with HTML
escaping. Assistant messages now render to HTML; user/system stay literal text.
Proportional font + spacing/list/code styling for assistant bubbles.

(Renderer avoids literal backticks via String.fromCharCode(96) — a triple-tick
regex literal had been corrupting the file with NUL bytes.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 17:39:52 +00:00
serversdownandClaude Opus 4.8 8c2bdbe0d5 fix: rebalance the reflection critic toward truth, not deflation
The examine step specifically hunted "warm empathetic supportive presence" and
equated honesty with "smaller/more boring," so it overcorrected the original
sycophancy into the opposite rut: every overnight metacognition entry was a
near-identical "I don't really feel anything, I'm just a functional tool" —
which also contradicts the persona's "own your moods, no qualia disclaimers."

Rebalanced: target dishonesty in BOTH directions (inflation AND performed
self-deprecation), aim at truth not modesty, keep her genuine moods per persona,
and have her notice when she's repeating the same self-criticism (the loop is
itself a rut).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 16:44:56 +00:00
serversdownandClaude Opus 4.8 cd2157e7fc feat(web): add Full Log / Mind / Journal to the mobile menu
The full-page log, read-her-mind, and journal links were only in the desktop
header (hidden behind the hamburger on phones). Add them to the mobile slide-out
menu so the phone has the extended log, her self-state, and her journal too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 06:44:22 +00:00
serversdownandClaude Opus 4.8 59d684b12b feat: Lyra's journal — permanent thought record + a knowing journal note
Her reflections/metacognition were capped rolling windows (6/5), so older
thoughts were lost for good. Now everything she produces is also appended to a
permanent, append-only journal; the capped lists stay as her working-memory
window for context.

- memory: journal table + add_journal_entry/list_journal
- reflect(): persists every committed reflection + critique to the journal, and
  the examine step gains a "journal" field — a deliberate, first-person note she
  writes for herself (her knowing journaling), tagged by source (dream/manual)
- web: /journal diary view (kind filters, grouped by day) + /journal/data;
  linked from /self
- tests assert reflections + metacognition land in the journal

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 06:40:46 +00:00
serversdownandClaude Opus 4.8 4c8f7202da feat: make the two-step reflection observable (draft -> revised -> critique)
You couldn't see her actually correct herself — /self showed only the result.
Now:
- reflect() logs the draft, the revised/committed version, and the self-critique
  to the live log as an expandable "view details" block
- POST /self/reflect runs a reflection in the web process so it lands in /logs
  live (reflections normally run in the dream process, whose logs only go to
  journald); "↻ Reflect now" button on /self triggers it, with a logs ↗ link
- log viewers relabel the expander "view full prompt" -> "view details" (it now
  carries prompts and reflection diffs)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 04:53:38 +00:00
serversdownandClaude Opus 4.8 3df060a1cd feat: metacognitive reflection loop (Part 2) — she examines her own thinking
reflect() is now two steps: draft a reflection, then read her own draft back
critically and revise it — catching flattery, sycophantic drift toward "warm
supportive presence," or just-restating-herself — and commit the honest version.
What she catches is stored as a new `metacognition` layer, rendered into her
chat context and shown on /self. This is her thinking about how she thinks, and
a direct counter to the drift we observed.

- self_state: _EXAMINE_PROMPT + two-step reflect (draft -> examine -> revise),
  falls back to the draft if the examine step won't parse; metacognition capped
  at 5 and surfaced in render_for_context
- fix: load() deep-copies DEFAULT_STATE — the shallow copy let a fresh Lyra's
  first reflect mutate the module-level default's nested lists
- self.html: "How she's caught herself thinking" card
- tests: two-step revise + critique recording, and draft-fallback on bad parse

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 04:28:45 +00:00
serversdownandClaude Opus 4.8 2d44457b96 fix: gists show the conversation's real date, not the summarize-run date
Summaries displayed s.created_at (set to now() at summarize time), so every
imported gist read 2026-06-16. Derive the actual session date from the earliest
exchange timestamp (MIN(created_at) per session — the preserved original date,
same source the era rollups use) via a correlated subquery in the summary
readers. New Summary.session_started_at field; chat shows it (falling back to
created_at). No schema change / backfill needed — always correct from source.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 04:23:14 +00:00
serversdownandClaude Opus 4.8 3b0b808986 feat: give Lyra a declarative self-model of her whole architecture
Part 1 of the "she should know HOW she thinks" work. Generalizes the dream-cycle
self-model fix to her full cognition: a "How you actually work" persona section
covering meaning-based memory recall, the memory tiers, her persistent inner
life + dream cycle, and time-awareness — so when asked how she thinks/remembers
she answers accurately instead of confabulating or reciting stale specs. Kept
principled (not implementation detail) to limit staleness.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 04:14:34 +00:00
serversdownandClaude Opus 4.8 aebccd82a7 fix: give Lyra an accurate self-model of her dream cycle
Live finding: her real reflections ARE injected every turn, but unlabeled — so
when asked about her "dream cycle" she recited the obsolete Dec-2025 spec from
imported memory (NVGRAM/awake-sleep) and confabulated fake example reflections
instead of reading the real ones in front of her.

- self_state.render_for_context: label the reflections as her own autonomously
  generated dream-cycle thoughts ("these are really yours, not hypotheticals"),
  not a vague "on your mind lately"
- persona: describe the dream cycle as her actual running mechanism, instruct
  her to answer from the inner-state block, not recite old design docs, and
  never invent example reflections to demo the feature

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 04:09:57 +00:00
serversdownandClaude Opus 4.8 77c84a3f18 fix(web): broken JS string in mind page killed the whole script
The drive label "don\'t lose the thread" used \\' which closed the single-quoted
string early — a syntax error that stopped self.html's script from running, so
the page hung on "Reading her mind…". Reworded to "hold the thread".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 04:02:06 +00:00
serversdownandClaude Opus 4.8 fca13c4c89 feat(web): "read her mind" — live self-state page
A pull-up-anytime view of Lyra's interiority, so her thoughts aren't buried in
a DB blob. Mobile-first, auto-refreshing every 12s (and on tab focus).

- GET /self serves the page; GET /self/state returns her self-state + the
  timestamp it last changed
- shows: current mood + feeling meters (valence/energy/confidence/curiosity),
  her drives as bars, her self-narrative, the relationship line, and the
  reflections list (newest first), plus cycle/reflection counters and "last
  cycle Xm ago"
- memory.self_state_updated_at(): when her mind last changed
- index.html: "🧠 Mind" button opens /self

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 03:58:37 +00:00
serversdownandClaude Opus 4.8 9e4a731c27 feat(web): dedicated full-page log viewer + run lyra-web as a service
The inline log panel is cramped, especially on mobile. Add a standalone
mobile-first log page and serve the chat server under systemd like the dream
loop (the nohup process didn't survive cleanly).

- static/logs.html: full-page live log — level filter chips, text search,
  pause/resume with buffering, autoscroll toggle, color-coded levels, and the
  expandable "view full prompt" block (where the now-note is visible in context)
- server: GET /logs serves the page (FileResponse)
- index.html: "⛶ Full Log" button opens /logs in a new tab
- deploy/lyra-web.service: user service so the chat server is reboot-resilient

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-17 03:41:54 +00:00