Commit Graph

7 Commits

Author SHA1 Message Date
bvandeusen 9f8b451d15 fix(journal-prep): bucket tasks + drop non-proximate events (#159)
Two filtering issues that made the daily prep noisy and trained the
user to ignore it.

## Tasks: bucket into due-today / upcoming / overdue

The prep was calling `list_notes(due_before=day_date)` and labeling the
result as "tasks due today". That filter is strictly less-than, so it
returned only OVERDUE tasks (a single 68-day-stale task in this user's
case), while the prompt still framed them as fresh today's work. Each
day of the prep treated the same overdue task as new — the user
learned to ignore the line entirely.

`gather_daily_sections` now runs three queries:
- `tasks_due_today` — `due_after=day_date AND due_before=day_date+1`
- `tasks_upcoming` — next 7 days, exclusive of today
- `tasks_overdue` — strictly before today
Overdue entries carry a `days_overdue` count. `_render_sections_for_prompt`
emits three labeled headers ("TASKS DUE TODAY", "UPCOMING TASKS",
"OVERDUE TASKS (still on the list, not currently due)"). The system
prompt has a new TASK BUCKETS rule telling the model: don't call
overdue items "due today"; surface them with their staleness duration
("still on the list 68 days") and frame as a backlog reminder rather
than today's work.

Backwards-compat: `sections["tasks"]` still exists, now as the union
of all three buckets — strictly more useful than the prior overdue-
only behavior any frontend consumer was getting before.

## Events: tz-aware window + proximity filter

The user's "Birthday — 2026-09-29 (FREQ=YEARLY)" event was surfacing
in every daily prep, 5 months out. Root cause: `gather_daily_sections`
built `day_start`/`day_end` as NAIVE datetimes; `list_events` then
called `rrulestr(...).between(naive_from, naive_to)` against an
aware `dtstart`, which throws TypeError, hits the `except Exception`
fallback, and appends the canonical event row — regardless of whether
today is anywhere near a recurrence.

Fix:
1. Construct the day window as TZ-aware in the user's local timezone
   and convert to UTC before the query. RRULE expansion now runs
   correctly.
2. Defense-in-depth `_filter_proximate_events` drops events whose
   start_dt is more than 7 days from `day_date` (in the user's local
   TZ — not UTC, so a Friday 23:00 NY event isn't misclassified as
   Saturday). If list_events ever leaks a far-future row again, the
   prep doesn't surface it.

10 new tests in `tests/test_journal_prep_filtering.py` cover task
bucketing (overdue marker, due-today no-marker, no-due-date), the
proximity filter (the 4/29 reproducer, in-window keeps, local-vs-UTC
boundary, unparseable dates kept rather than suppressed), and the
rendering (overdue staleness shown, due-today doesn't repeat the date,
correct section ordering).

53 tests pass across journal_prep + journal_search + record_moment +
calendar_tool + events. Ruff clean.

Closes Fable task #159.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-29 09:31:12 -04:00
bvandeusen 0ed9cbf666 fix(journal): restore prep prose; soften persona toward chat-like
Per user clarification: previous over-rotation dropped the LLM-generated
prep prose entirely (just a phase greeting) and made the chat persona
extremely sparse ("you are a place where words go down"). User actually
wanted only the chat replies pulled back, NOT the prep dropped, and the
chat to behave largely like normal /chat — asking follow-ups and
verifying earlier details.

services/journal_prep.py — restored:
- _render_sections_for_prompt
- _PREP_SYSTEM_PROMPT (the direct, briefing-style prompt from 590a07b)
- _generate_prep_prose
- _fallback_prep_text
- ensure_daily_prep_message now calls _generate_prep_prose again
- removed _phase_for_now / _phase_prompt helpers (no longer needed)

services/journal_pipeline.py — persona rewritten:
- Old: "You are the user's journal. Be quiet. Listen. You are not helpful."
- New: "You are the user's assistant. Behave like the rest of the app's
  chat: respond conversationally, ask follow-up questions, verify details
  from earlier turns, use tools naturally."
- Calibration block reorganized: PEOPLE/PLACES (ask first), MOMENTS
  (silent + use *_names), STATE-CHANGING TOOLS (confirmation flow),
  OTHER, RESPONSE STYLE.
- RESPONSE STYLE keeps the no-apologizing / no-option-menus /
  no-verbatim-repetition / match-user-length rules but drops the "be
  quiet, one short sentence" framing.

Net behavior:
- Open journal → LLM-generated prep prose with today's tasks/events/weather
- Reply → assistant responds conversationally like /chat, asks follow-ups,
  verifies details, uses tools
- Background: silently records moments via *_names, asks before creating
  new people/places

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 18:04:34 -04:00
bvandeusen 4668c0950b fix(journal): minimal prep + quieter persona
The prep was generating a multi-sentence recap of tasks/events/weather/
projects/recent moments via an LLM call. Per user direction, that's
redundant — the right-side widgets already show today's data — and the
verbosity made the journal feel chatty when the user wanted quiet.

Replaces the prep prose generator with a single phase-aware check-in
question (drawn from a static map: morning="How are you starting the
day?", midday="How's it going so far?", evening="How did the day shake
out?"). No LLM call. The structured `sections` are still gathered and
persisted on msg_metadata for provenance and possible future tooling
(e.g., search), they just don't render in the prep message.

Also pulls the journal persona way back. The prior framing pushed the
model toward stock therapy-template patterns ("I'm sorry you're feeling…"
+ numbered option lists). The new persona is "you are the user's journal —
listen, be quiet, stay out of the way." RESPONSE STYLE rules now lead the
calibration block and explicitly forbid:

- apologizing for the user's feelings
- offering to help / pitching tools
- multi-option menus
- verbatim repetition of prior replies
- padding short replies into paragraphs

Most replies should be one short sentence. Sometimes the right reply is
"Got it" + a record_moment tool call.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 17:48:41 -04:00
bvandeusen 590a07bc13 fix(journal): tighten prep prompt — direct briefing, not flowery letter
Previous system prompt asked for "warm, conversational, like a friend
writing a letter" which produced flowery preludes that buried the actual
data. Rewritten to:

- Lead with practical data (tasks, events, weather) — concrete and specific
- 4-7 sentences total, tight prose, no padding
- Recent moments / open threads mentioned briefly at the END as context,
  not as the lead
- Voice: "competent assistant briefing the user" not "friend writing a letter"
- Close with a short journal invitation under 8 words

Also dropped max_tokens 600 -> 400 to bias toward concision.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 16:10:57 -04:00
bvandeusen c9f2134ad4 feat(journal): conversational LLM-generated daily prep (replaces card)
The structured prep card was data-rich but voiceless. Replaced with an
LLM-generated conversational opener — same shape the briefing's compilation
slot had — that renders as a normal assistant chat bubble at the top of
the day's conversation.

Backend (services/journal_prep.py):
- Renamed generate_daily_prep -> gather_daily_sections (still pure data
  fetching, no LLM); kept the old name as a backwards-compat alias.
- New _generate_prep_prose: hands the gathered sections to generate_completion
  with a warm-conversational system prompt; returns prose. Falls back to a
  plain greeting if the LLM call fails or no model is configured.
- ensure_daily_prep_message now persists the prep as role='assistant' with
  the prose as content. Structured sections stay on msg_metadata for
  provenance. Auto-upgrades legacy system-role preps in place on next call.

Frontend:
- Drop the <article class="daily-prep"> structured block from JournalView.
  The prep is now just the first chat bubble — picks up the existing
  Illuminated Transcript styling automatically.
- Drop dayMessages / prepMessage / prepSections / asArray helpers — no
  longer needed.
- ChatMessage hideMessage filter: comment refined to clarify it only
  catches LEGACY system-role prep rows. Current preps are assistant-role
  and render normally.

Net effect: open /journal -> first thing you see is a warm assistant bubble
that talks about your day -> input bar below to reply.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 15:42:30 -04:00
bvandeusen cacfcac86a fix(journal): restore weather + events panels; hide daily-prep system msg in chat
The journal UI was over-stripped earlier — weather panel, current-conditions
poll, and the upcoming-events sidebar were all dropped. Restored those (calls
the new /api/journal/weather, /api/journal/weather/current,
/api/journal/weather/refresh, /api/journal/weather/geocode endpoints).

Also: the daily-prep system message was rendering as flat text inside
ChatPanel because there's no .role-system bubble styling. Added a hideMessage
guard in ChatMessage so daily-prep system messages don't render in the chat
stream — they're already shown above as a structured prep card.

News / RSS reactions / article-discuss are intentionally NOT in the journal
(scoped out per user direction). The broader RSS infrastructure cleanup is
a separate, larger task.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-26 12:22:51 -04:00
bvandeusen d9ab538ef4 feat(journal): backend services (moments, search, prep, scheduler, pipeline)
- services/moments.py — CRUD with embedding sync + entity-link helpers
- services/journal_search.py — three-mode search (temporal / entity / semantic)
  with notes-RAG isolation; cosine-similarity helper unit-tested
- services/journal_prep.py — gathers tasks/events/weather/news/projects/
  recent-moments/open-threads into a structured prep block
- services/journal_scheduler.py — per-user APScheduler cron for daily prep,
  follows the BackgroundScheduler + threadsafe-async pattern from event_scheduler
- services/journal_pipeline.py — system prompt (persona + calibration rules)
  with last-48h ambient moments injection
- app.py: wire journal scheduler start/stop hooks
- routes/settings.py: re-add live-reschedule on timezone change (now via
  journal_scheduler.update_user_schedule)

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-25 22:37:39 -04:00