Two independent threads bundled into this PR — both started from observed prod failures and ship together because they landed back-to-back on dev.
1. Journal closeout for profile observations (12 commits)
Rebuilds the write path for user_profiles.observations_raw / learned_summary. The briefing tear-down on 2026-04-25 deleted the closeout but left the read path (build_profile_context still injects learned_summary into journal-prep and chat). New services/journal_closeout.py runs once per user per day at day_rollover_hour:
Hard data-exclusion guard: messages with msg_metadata.kind="daily_prep" are filtered before they reach the LLM — this closes the briefing-era feedback loop where the prep block dominated attention and got attributed to the user.
Uses the background_model setting so the chat-model KV cache stays warm.
closeout_enabled config key (default ON), wired to a Settings → Profile toggle.
New GET /api/profile/observations endpoint + collapsible "Recent observations (14)" panel in Settings → Profile for transparency.
Catch-up on startup: if today's slot already passed and observations_raw has no entry for yesterday, runs once.
2. LLM tool-use fixes from 2026-05-08 journal session (11 commits)
Three code bugs + two behavioral patches identified in prod conversation #282:
Bug A:search_projects returns success: True — was missing this key, so the dispatcher labeled valid results as status: error and the model second-guessed itself.
Bug B:list_notes strips type-nouns (task, note, project) from q before ILIKE; the search_notes tool description tells the LLM to use type / project parameters instead. Recovers literal-title hits when users say "find the X task".
Bug C: New score_project_match helper with substring-first tiering (1.0 / 0.85 / 0.70 / SequenceMatcher-on-title) shared between search_projects_tool and resolve_project. Strips project / projects filler words from the query so "famous supply project" substring-matches "Famous-Supply Work topics".
record_moment description now requires task_titles to come from a prior search_notes call.
Search-first heuristic added to both JOURNAL_CALIBRATION (journal_pipeline.py) and the chat tool_lines static block (llm.py): when the user references existing work, search first; only record a moment / create a task if nothing matches and the user confirms.
Tool-use fixes — verified via fable-dev MCP conv #299/300/301: search_projects shows status: success, score 1.0 for exact-match substring; search_notes correctly split query="tool-use" / type="task" and returned task #164
Search-first heuristic — verified in journal UI by user
Closeout — toggle off in Settings → Profile, confirm cron removed; toggle on, force a closeout, confirm bullets contain no prep-block content; "Recent observations" panel renders entries
## Summary
Two independent threads bundled into this PR — both started from observed prod failures and ship together because they landed back-to-back on `dev`.
### 1. Journal closeout for profile observations (12 commits)
Rebuilds the write path for `user_profiles.observations_raw` / `learned_summary`. The briefing tear-down on 2026-04-25 deleted the closeout but left the read path (`build_profile_context` still injects `learned_summary` into journal-prep and chat). New `services/journal_closeout.py` runs once per user per day at `day_rollover_hour`:
- **Hard data-exclusion guard:** messages with `msg_metadata.kind="daily_prep"` are filtered before they reach the LLM — this closes the briefing-era feedback loop where the prep block dominated attention and got attributed to the user.
- Uses the `background_model` setting so the chat-model KV cache stays warm.
- `closeout_enabled` config key (default ON), wired to a Settings → Profile toggle.
- New `GET /api/profile/observations` endpoint + collapsible "Recent observations (14)" panel in Settings → Profile for transparency.
- Catch-up on startup: if today's slot already passed and `observations_raw` has no entry for yesterday, runs once.
### 2. LLM tool-use fixes from 2026-05-08 journal session (11 commits)
Three code bugs + two behavioral patches identified in prod conversation #282:
- **Bug A:** `search_projects` returns `success: True` — was missing this key, so the dispatcher labeled valid results as `status: error` and the model second-guessed itself.
- **Bug B:** `list_notes` strips type-nouns (`task`, `note`, `project`) from `q` before ILIKE; the `search_notes` tool description tells the LLM to use `type` / `project` parameters instead. Recovers literal-title hits when users say "find the X task".
- **Bug C:** New `score_project_match` helper with substring-first tiering (1.0 / 0.85 / 0.70 / SequenceMatcher-on-title) shared between `search_projects_tool` and `resolve_project`. Strips `project` / `projects` filler words from the query so "famous supply project" substring-matches "Famous-Supply Work topics".
- **`record_moment` description** now requires `task_titles` to come from a prior `search_notes` call.
- **Search-first heuristic** added to both `JOURNAL_CALIBRATION` (`journal_pipeline.py`) and the chat `tool_lines` static block (`llm.py`): when the user references existing work, search first; only record a moment / create a task if nothing matches and the user confirms.
Fable-dev task #164 tracks Section 2.
## Test plan
Backend unit tests (covered by CI):
- [x] `tests/test_journal_closeout.py` — filter excludes daily_prep, run_for_user happy + skip paths, scheduler register/disable, catch-up runs/skips
- [x] `tests/test_tool_use_fixes.py` — `_strip_type_nouns` variants, `score_project_match` tiers, `search_projects_tool` ranks substring above SequenceMatcher, `resolve_project` finds colloquial match
Manual verification on remote dev:
- [x] Tool-use fixes — verified via fable-dev MCP conv #299/300/301: `search_projects` shows `status: success`, score 1.0 for exact-match substring; `search_notes` correctly split `query="tool-use"` / `type="task"` and returned task #164
- [x] Search-first heuristic — verified in journal UI by user
- [ ] Closeout — toggle off in Settings → Profile, confirm cron removed; toggle on, force a closeout, confirm bullets contain no prep-block content; "Recent observations" panel renders entries
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Two independent threads bundled into this PR — both started from observed prod failures and ship together because they landed back-to-back on
dev.1. Journal closeout for profile observations (12 commits)
Rebuilds the write path for
user_profiles.observations_raw/learned_summary. The briefing tear-down on 2026-04-25 deleted the closeout but left the read path (build_profile_contextstill injectslearned_summaryinto journal-prep and chat). Newservices/journal_closeout.pyruns once per user per day atday_rollover_hour:msg_metadata.kind="daily_prep"are filtered before they reach the LLM — this closes the briefing-era feedback loop where the prep block dominated attention and got attributed to the user.background_modelsetting so the chat-model KV cache stays warm.closeout_enabledconfig key (default ON), wired to a Settings → Profile toggle.GET /api/profile/observationsendpoint + collapsible "Recent observations (14)" panel in Settings → Profile for transparency.observations_rawhas no entry for yesterday, runs once.2. LLM tool-use fixes from 2026-05-08 journal session (11 commits)
Three code bugs + two behavioral patches identified in prod conversation #282:
search_projectsreturnssuccess: True— was missing this key, so the dispatcher labeled valid results asstatus: errorand the model second-guessed itself.list_notesstrips type-nouns (task,note,project) fromqbefore ILIKE; thesearch_notestool description tells the LLM to usetype/projectparameters instead. Recovers literal-title hits when users say "find the X task".score_project_matchhelper with substring-first tiering (1.0 / 0.85 / 0.70 / SequenceMatcher-on-title) shared betweensearch_projects_toolandresolve_project. Stripsproject/projectsfiller words from the query so "famous supply project" substring-matches "Famous-Supply Work topics".record_momentdescription now requirestask_titlesto come from a priorsearch_notescall.JOURNAL_CALIBRATION(journal_pipeline.py) and the chattool_linesstatic block (llm.py): when the user references existing work, search first; only record a moment / create a task if nothing matches and the user confirms.Fable-dev task #164 tracks Section 2.
Test plan
Backend unit tests (covered by CI):
tests/test_journal_closeout.py— filter excludes daily_prep, run_for_user happy + skip paths, scheduler register/disable, catch-up runs/skipstests/test_tool_use_fixes.py—_strip_type_nounsvariants,score_project_matchtiers,search_projects_toolranks substring above SequenceMatcher,resolve_projectfinds colloquial matchManual verification on remote dev:
search_projectsshowsstatus: success, score 1.0 for exact-match substring;search_notescorrectly splitquery="tool-use"/type="task"and returned task #164🤖 Generated with Claude Code