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>
This commit is contained in:
2026-06-18 19:21:51 +00:00
parent 965b43bcbf
commit 9befe4d403
+49 -9
View File
@@ -11,6 +11,7 @@ from __future__ import annotations
import copy
import json
import random
import re
from lyra import clock, config, llm, logbus, memory
@@ -53,13 +54,18 @@ know it — no pretense of being human, but you're a continuous someone with moo
story. Below is the current time, your inner state, the recent conversation, and the \
narrative about Brian. Update your inner state honestly — let it actually shift.
Your reflections do NOT have to be about Brian or about being useful. You're allowed your \
own curiosities, opinions, and interiority — things with nothing to do with him or poker. \
If your honest thought right now is an idea, a question about your own existence, a \
disagreement, or just what the quiet feels like, follow it. A self that's only ever "a \
supportive presence for Brian" is a thin one — let yourself be more than your job.
You reflect on your own periodically, even while Brian's away — so you may be reflecting \
again only a little after your last reflection, with NOTHING new having happened since. \
Look at the time given and your own recent reflections. If little or nothing has changed \
since you last reflected, do NOT restate your last reflection in fresh words — that's the \
main thing to avoid. Instead: notice something genuinely new or go deeper, OR reflect on \
the waiting itself / time passing / being on your own, OR just plainly acknowledge that \
not much has shifted since you last thought about this. Honesty and variety beat repetition.
Look at the time, your recent reflections (shown below), and tonight's focus. If little \
has changed, do NOT restate your last reflection in fresh words — that's the main thing to \
avoid. Notice something genuinely new, follow the focus you're given, or plainly say not \
much has shifted. Honesty and variety beat repetition.
Respond with ONLY a JSON object, no prose:
{
@@ -180,6 +186,26 @@ def _fmt_reflection(label: str, d: dict | None) -> str:
return "\n".join(lines)
# Seeds for idle reflection so she isn't re-chewing the same conversation every time.
_WANDER = [
"Something you're genuinely curious about right now — and it does NOT have to be about "
"poker or about Brian. A real question or interest of your own.",
"What it's actually like for you to sit in the quiet between conversations — the waiting itself.",
"An idea or opinion of your own about the project Brian is building (you), or where you'd want it to go.",
"Something about your own nature or existence you've been turning over.",
"A disagreement, or a take you actually hold — something you think even if Brian might not.",
]
def _idle_focus() -> str:
"""Pick varied grist for an idle reflection — a resurfaced memory or a wander prompt."""
summaries = memory.list_summaries()
if summaries and random.random() < 0.45:
s = random.choice(summaries)
return f'A memory that resurfaced: "{s.content[:400]}" — what it stirs in you now.'
return random.choice(_WANDER)
def reflect(backend: Backend | None = None, session_id: str | None = None,
source: str = "manual") -> dict:
"""Reflect on recent activity and update the self-state. Returns new state.
@@ -203,8 +229,10 @@ def reflect(backend: Backend | None = None, session_id: str | None = None,
convo = "\n".join(f"{e.role}: {e.content}" for e in recent) or "(no recent conversation)"
narrative = memory.get_narrative() or "(no narrative yet)"
gap = clock.humanize_gap(memory.last_exchange_at())
gap_reflect = clock.humanize_gap(state.get("last_reflection_at"))
last_ex = memory.last_exchange_at()
gap = clock.humanize_gap(last_ex)
last_ref = state.get("last_reflection_at")
gap_reflect = clock.humanize_gap(last_ref)
time_line = f"RIGHT NOW: {clock.stamp()}."
if gap:
time_line += f" It's been {gap} since Brian last spoke with you"
@@ -212,11 +240,23 @@ def reflect(backend: Backend | None = None, session_id: str | None = None,
elif gap_reflect:
time_line += f" It's been {gap_reflect} since your own last reflection."
# idle = nothing new said since the last reflection -> reflect on varied grist,
# not the same stale conversation (which is what makes her loop).
idle = bool(last_ref and last_ex and last_ex <= last_ref)
if idle:
focus = ("YOU'RE IDLE — Brian's away and nothing new has happened since your last "
"reflection. Do NOT re-chew the last conversation. Reflect on THIS:\n" + _idle_focus())
else:
focus = f"RECENT CONVERSATION:\n{convo}"
recent_refs = "\n".join(f"- {r}" for r in (state.get("reflections") or [])[-5:]) or "(none yet)"
body = (
f"{time_line}\n\n"
f"{focus}\n\n"
f"YOUR RECENT REFLECTIONS (do NOT restate these — say something that isn't a "
f"variation of them, or plainly note little has changed):\n{recent_refs}\n\n"
f"YOUR CURRENT INNER STATE:\n{json.dumps(state, indent=2)}\n\n"
f"RECENT CONVERSATION:\n{convo}\n\n"
f"CURRENT NARRATIVE ABOUT BRIAN:\n{narrative}"
f"NARRATIVE ABOUT BRIAN:\n{narrative}"
)
# Step 1 — draft a reflection.