fix(journal): anti-hallucination hardening + message_count fix

Prep prose (services/journal_prep.py):
- Emit explicit "WEATHER: none available — do NOT mention weather"
  absent-marker so a small model can't invent partly-cloudy/temperature
  prose when both configured locations have empty addresses.
- Replace negative-only system rule with positive-anchored guidance
  forbidding weather/temp/precip mentions unless a numeric WEATHER
  section is present; also bans echoing parenthetical labels verbatim.
- Reword overdue header to "(past their due date, still open — backlog,
  not today's work)" and render lines as "was due <date>, N day(s)
  overdue" with correct singular/plural. Supersedes the wording noted
  in Fable task #159.
- Deterministic fabricated-weather reconciler: low-false-positive regex
  detects fabricated weather phrasing; on trip with an empty section,
  regenerate once with a corrective. Persistent fabrication logs ERROR
  rather than mangling prose.

Journal route (routes/journal.py):
- Override message_count with len(messages) in _day_payload. The chat
  path already does this; the journal path was hitting the
  Conversation.to_dict() fallback to 0 because messages aren't
  eager-loaded on that instance.

Tests:
- tests/test_journal_message_count.py — pins the model-level trap and
  the override contract (3 cases).
- tests/test_journal_prep_hardening.py — 11 cases covering the
  fabricated-weather reconciler and absent-marker rendering.
- tests/test_journal_prep_filtering.py — updated one stale assertion.

Tracks Fable task #171.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-20 18:54:35 -04:00
parent e6f2ee2b94
commit 5d2d27c499
5 changed files with 306 additions and 28 deletions
+7 -1
View File
@@ -407,8 +407,14 @@ async def _day_payload(*, user_id: int, day_date: datetime.date):
.order_by(Message.created_at)
)
messages = (await session.execute(msgs_stmt)).scalars().all()
# conv.to_dict() recomputes message_count from the `messages`
# relationship, which isn't eager-loaded here, so it would report 0.
# We already have the real list — override with the known count, same
# convention the chat-list path uses (services/chat.py).
conv_dict = conv.to_dict()
conv_dict["message_count"] = len(messages)
return jsonify({
"day_date": day_date.isoformat(),
"conversation": conv.to_dict(),
"conversation": conv_dict,
"messages": [m.to_dict() for m in messages],
})