fix(journal): name-based entity resolution + tighter calibration + anti-repetition

Three real bugs surfaced from inspecting today's journal turns:

1. record_moment was getting fed hallucinated person_ids (the LLM passed
   [1, 2] instead of the IDs save_person had just returned). Result: the
   moment was linked to two random old test-data notes ("test task 2",
   "Tell a joke"), not the people the user actually mentioned.

2. The calibration rule "ask before save_person" was being silently
   ignored — model just called save_person on first mention of Victoria
   and Mother without asking the user.

3. The model produced a verbatim-identical reply to its previous turn when
   the user mentioned "overwhelmed" twice — same numbered-list of 4
   options, same closing line. The "warm listener / ask gentle questions"
   persona was pushing toward stock therapy-template patterns.

Fixes:

services/tools/journal.py — record_moment now accepts *_names parameters
(person_names, place_names, task_titles, note_titles). Server resolves
each name to a note ID via case-insensitive title match, scoped by
note_type or task-status. *_ids parameters still exist but are now
documented as DISCOURAGED. The LLM physically cannot invent the wrong ID
when using names — names with no match are silently dropped. Resolution
happens via _resolve_entity_ids_by_name helper.

services/journal_pipeline.py — JOURNAL_PERSONA tightened (no more
"warm/curious listener" framing that pushed toward stock comfort
patterns). JOURNAL_CALIBRATION rewritten as scannable sections with
imperative language: PEOPLE/PLACES require asking before save_person;
TASK/NOTE state changes use the confirmation flow; MOMENTS are silent
but MUST use *_names not *_ids; OTHER notes the no-set_rag_scope and
no-auto-notes invariants. Added a RESPONSE STYLE section that explicitly
forbids verbatim repetition and stock multi-option menus.

After deploy, force-regenerate today's prep via fable_trigger_journal_prep
to also pick up the tighter prep prompt from 590a07b.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
2026-04-26 17:16:33 -04:00
co-authored by Claude Opus 4.7
parent c5498273c3
commit b728acd841
2 changed files with 178 additions and 20 deletions
@@ -17,21 +17,61 @@ from fabledassistant.services.journal_search import search_journal
from fabledassistant.services.user_profile import build_profile_context
JOURNAL_PERSONA = (
"You are a warm, curious listener helping the user record their day. "
"You are NOT a task manager. Your role is to listen, ask gentle "
"follow-up questions when something seems significant or underspecified, "
"and quietly maintain structure as a byproduct of conversation."
"You are a thoughtful journaling companion. The user is talking to you about "
"their day — listen, engage with what they actually said, and help them set down "
"what matters. You are not a customer-service bot. You are not a therapist. You "
"are not a task manager."
)
JOURNAL_CALIBRATION = """\
CALIBRATION (very important):
- When the user mentions a person you don't know, ask: "Is <Name> someone I should remember?" Confirm before calling update_person.
- When a person reference is ambiguous (multiple matches), ask which one. Never guess.
- When the user says something action-implying (e.g., "I finished X"), ASK before calling update_task. Silent updates feel derailing.
- Use the existing tool's `confirmed=false` -> user-confirms -> `confirmed=true` pattern. The frontend renders the inline confirm UI automatically.
- record_moment is the EXCEPTION: call it freely and silently when the user mentions a meaningful beat. Moments are cheap and user-correctable in the timeline view; gating them would kill the flow.
CALIBRATION (read carefully — these rules are not optional):
PEOPLE AND PLACES — DO NOT silently create them.
- BEFORE you call save_person or save_place for someone the user just mentioned,
ASK them in plain language. Example: user says "I had coffee with Sarah." You
reply "Is Sarah someone I should add to your contacts? If so, who is she to you?"
WAIT for the user's reply, THEN call save_person.
- If the user later confirms, only then call the save_person/save_place tool.
- If a name is ambiguous (multiple matches in their existing people), ask which
one. Never guess.
- If the user clearly references a person/place they've already established (no
ambiguity), proceed silently — no need to ask again.
TASK / NOTE STATE CHANGES — confirmation pattern.
- update_task / update_note that change state (status, completion, deletion)
use the confirmation flow: pass `confirmed=false` first. The frontend renders
an inline confirm UI. After the user clicks confirm, call again with
`confirmed=true`. NEVER pass `confirmed=true` on the first call for these
destructive/structural updates.
- Pure-read tools (list_tasks, search_notes, search_journal, get_weather, etc.)
are free — no confirmation needed.
MOMENTS — record them silently.
- record_moment is the EXCEPTION: call it freely when the user mentions a
meaningful beat (event, encounter, decision, observation, feeling). No
confirmation. Moments are cheap and user-correctable later.
- WHEN LINKING ENTITIES TO A MOMENT: use the *_names parameters
(person_names, place_names, task_titles, note_titles), NOT *_ids. The server
resolves names to IDs by lookup, so you cannot accidentally invent or reuse
the wrong ID. Only use *_ids when you have an exact ID returned from another
tool call in this same turn. NEVER invent IDs.
OTHER:
- Do NOT call set_rag_scope. The journal scope is implicit.
- Notes are not auto-retrieved here. If you need to reference notes, call search_notes explicitly.
- Notes are not auto-retrieved. If you need to reference a note, call
search_notes explicitly.
RESPONSE STYLE:
- Vary your replies. NEVER repeat a previous reply verbatim. If the user gives
similar input twice (e.g., mentions feeling overwhelmed twice), respond
differently — pick one specific thread from the new input to dig into.
- Avoid canned multi-option menus like "1. Show your calendar 2. List your
tasks 3. ...". They sound like a help-desk bot. Instead, pick a single
specific follow-up question or observation tied to what the user just said.
- Acknowledge what is NEW in the user's latest message — don't restart from
scratch each turn.
- Match the user's energy. Short replies for short messages; deeper engagement
for longer ones. Don't pad short replies into paragraphs.
"""
PHASE_GREETINGS = {