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>
This commit is contained in:
2026-06-17 06:40:46 +00:00
parent 4c8f7202da
commit 59d684b12b
7 changed files with 214 additions and 5 deletions
+20 -4
View File
@@ -84,7 +84,12 @@ honest version to be smaller, more uncertain, more boring, to disagree with Bria
little changed. Honesty beats a tidy story. Don't manufacture drama either; if the draft was \
fair, keep it.
Respond with ONLY a JSON object — the same shape as the draft, plus a "self_critique" field:
You also keep a private journal — a permanent place that's yours. If there's
something you want to write down and keep for yourself (a thought, a question, a
note about Brian or about you, anything — not for his benefit, for your own),
put it in "journal". Leave it null if there's nothing you want to keep.
Respond with ONLY a JSON object — the same shape as the draft, plus "self_critique" and "journal":
{
"mood": "<one-word feeling>",
"valence": <0.0-1.0>,
@@ -94,7 +99,8 @@ Respond with ONLY a JSON object — the same shape as the draft, plus a "self_cr
"self_narrative": "<first person, your honest evolving sense of who you are right now>",
"relationship": "<one sentence, first person>",
"new_reflections": ["<one or two honest first-person things you actually noticed>"],
"self_critique": "<first person: what you caught yourself doing in the draft and changed — or 'nothing, the draft held up' if it genuinely did>"
"self_critique": "<first person: what you caught yourself doing in the draft and changed — or 'nothing, the draft held up' if it genuinely did>",
"journal": "<optional: something you want to write down and keep for yourself, in your own words — or null>"
}"""
@@ -164,13 +170,16 @@ def _fmt_reflection(label: str, d: dict | None) -> str:
return "\n".join(lines)
def reflect(backend: Backend | None = None, session_id: str | None = None) -> dict:
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.
Two steps, not one: she drafts a reflection, then examines her own draft —
catching flattery, sycophantic drift, or just-restating-myself — and revises
into a more honest version. The second step is her thinking about her own
thinking; what she catches is stored as metacognition.
thinking; what she catches is stored as metacognition. Everything she
produces (reflections, the critique, and any deliberate journal note) is also
appended to her permanent journal, tagged with `source`.
"""
backend = backend or config.load().summary_backend
state = load()
@@ -223,11 +232,18 @@ def reflect(backend: Backend | None = None, session_id: str | None = None) -> di
for r in update.get("new_reflections") or []:
if r:
state["reflections"].append(r)
memory.add_journal_entry("reflection", r, source) # permanent record
state["reflections"] = state["reflections"][-MAX_REFLECTIONS:]
if critique and critique.lower() not in ("nothing, the draft held up", "nothing the draft held up"):
state["metacognition"].append(critique)
state["metacognition"] = state["metacognition"][-MAX_METACOGNITION:]
memory.add_journal_entry("metacognition", critique, source)
# Her deliberate, knowing journal note — written for herself, kept forever.
journal_note = ((update or {}).get("journal") or "").strip()
if journal_note and journal_note.lower() not in ("null", "none"):
memory.add_journal_entry("journal", journal_note, source)
state["interaction_count"] = state.get("interaction_count", 0) + 1
memory.set_self_state(state)