- 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>
- 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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
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>
The MI50 box (CT202) runs an OpenAI-compatible llama.cpp server on
10.0.0.44:8080. Wire it in as a third backend:
- llm.complete gains backend="mi50" (OpenAI client pointed at MI50_BASE_URL)
- config: MI50_BASE_URL (default http://10.0.0.44:8080/v1) + MI50_MODEL
- chat.respond labels the model per backend; web _backend_for maps "mi50"
- UI backend selector adds "MI50 — local GPU"
Verified end-to-end: llm.complete(backend="mi50") returns from the live server.
See homelab-inference memory for the box topology.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "context built" event now carries the fully-rendered prompt (persona, gists,
recalled details, recent turns, the new message) plus a total char count. The
log panel renders it as a collapsed "view full prompt" block — clean by default,
one click to see exactly what hit the model.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Turn the inert "Show Work" thinking panel into a real live activity log:
- lyra/logbus.py: thread-safe in-memory ring buffer other modules publish to
- chat.respond logs backend/model/embed per turn, recall counts, reply size;
web layer logs chat errors
- server: replace the keep-alive /stream/thinking stub with /stream/logs, an
SSE endpoint that replays the recent buffer then streams new events
- UI: repurpose the panel as a global "Live Log" — connects on load, renders
level/time/msg/fields, drops the old per-session localStorage + dead popup
Every turn now shows its backend + model in-app, so local-vs-cloud (free vs
paid) is visible at a glance.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Phase 1 — persona + persistent memory chat loop:
- lyra/persona.py + personas/lyra.md: editable identity/voice (friend-first,
honest, never invents poker math)
- lyra/chat.py: turn loop assembling persona + cross-session recall + recent
context, persisting both sides to SQLite
- lyra/session.py, lyra/__main__.py: session lifecycle + `lyra` REPL
Phase 1.25 — reuse the old web UI:
- vendored the prior single-page UI into lyra/web/static, repointed to
same-origin
- lyra/web/server.py (FastAPI): serves the UI and backs its endpoint contract
(/v1/chat/completions, session CRUD, health, inert thinking-stream) with the
new chat loop + memory; SQLite stays the single source of truth
- `lyra-web` console script
Local backends — test for free, no OpenAI key:
- llm.embed routes via EMBED_BACKEND (cloud=OpenAI, local=Ollama /api/embed)
- simplified UI backend selector to Local (Ollama) / Cloud (OpenAI), default local
- memory connection opened check_same_thread=False for the threaded server
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>