Deletes ~760 lines of legacy briefing code: format_task, compute_task_hash,
upsert_task_snapshots, _gather_internal, _gather_weekly_review,
_llm_synthesise, and the unified prompt helpers. run_compilation and
run_slot_injection are now agentic-tool-use-loop only.
briefing_scheduler and user_profile migrated from the deleted helper to
services.llm.generate_completion (retry + keep_alive baked in).
routes/briefing.manual_trigger now persists agentic tool-call receipts
via _persist_agentic_messages (previously silently dropped them) and
adds POST /api/briefing/reset-today to wipe today's briefing messages.
BREAKING: briefing_mode setting no longer honored; no legacy fallback.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
list_notes gains exclude_paused_projects; list_tasks tool sets it when
no explicit project filter is given. Paused projects no longer leak
tasks into briefings or list_tasks results.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Applies the grounding discipline from the agentic briefing work to the
main chat system prompt. The regular chat pipeline was already agentic
(it uses stream_chat_with_tools), but its system prompt never told the
model "only assert facts from tool results" or "if a tool returns
nothing, say so honestly." That left room for the same class of
hallucinations the briefings had — calling list_events, getting an
empty array, and then confidently mentioning a meeting anyway.
Adds two new static rules to the tool guidance block in llm.build_context:
GROUNDING — when the user asks about their own data, call the relevant
tool to see what exists. Never assert from memory or assumption.
HONESTY WHEN EMPTY — if a tool returns empty results, tell the user
plainly. No fabricated example items, no invented meetings, no generic
suggestions dressed up as real data.
Both rules are in the static (KV-cache-stable) portion of the system
prompt so they cost nothing on repeated requests for the same user.
Carries the hallucination fix from the briefing work directly into
every chat turn, not just chat that happens inside a briefing thread.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
PR 1.5 + PR 2 of the agentic briefing rollout. Extends the agentic
path to cover the morning/midday/afternoon check-in slots in addition
to the 4am compilation, and persists the full tool-call sequence
into the briefing conversation so chat follow-ups see the receipts.
run_slot_injection now honors the briefing_mode setting the same way
run_compilation does: agentic mode routes through run_agentic_briefing
with a check-in system prompt variant, legacy falls back automatically
if the new path returns empty. Signature changed from str to
(str, dict) to surface the agentic message list to the caller.
_agentic_system_prompt now takes the user's local-day window and
timezone, pre-computed by run_agentic_briefing, and embeds them in
the prompt. This eliminates a whole class of "wrong day" bugs that
would otherwise happen when the model tried to translate "today" into
ISO 8601 ranges without knowing the user's timezone.
briefing_scheduler._run_slot_for_user now calls _persist_agentic_messages
after each slot, which walks the agentic message list and stores
every intermediate assistant turn (with its tool calls and results
folded into the flat storage format the existing chat loader expects)
as a real message row. The synthetic "[Midday briefing update]"
user-role messages are no longer created — final prose is written
with metadata.briefing_slot so the UI can still identify it as a
scheduled entry. The agentic user-trigger ("Generate my morning
briefing…") is deliberately skipped so it doesn't recreate the same
fake-user-message problem we're trying to remove.
briefing_conversations.post_message now accepts tool_calls, matching
the schema the Message model already supports. This lets scheduled
briefings write structured tool-use history without reaching into
the model layer.
Net effect with briefing_mode="agentic" on:
- All four slots are grounded in tool results, no more hallucinated
events or tasks
- Chat follow-ups in the briefing conversation see morning's
list_events → [] receipt (and everything else), so "what meeting?"
gets an honest "nothing on the calendar" reply grounded in data
- No more fake [Briefing update] user messages in the chat scroll
Still to come (PR 3): UI polish — tool-call status row, inline cards
for list_events/list_tasks results, visual treatment for slot messages.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
services/events.py::list_events treated any event with end_dt IS NULL
as perpetually matching, so a point-event created last week would be
returned as "happening today" whenever the briefing or list_events
tool queried today's window. That manifested as briefings claiming
past events were scheduled for the current day.
Split the null-end branch: point events now only match when start_dt
falls within [date_from, date_to]. Events with an end_dt continue to
use standard overlap logic.
This bug affected both the legacy briefing synthesis and the agentic
list_events tool path, since both call the same events service.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
First cut of the agentic briefing redesign. Morning compilation can now
route through a tool-call loop that grounds every factual claim in an
actual tool result, eliminating the hallucinated meetings, tasks, and
news items the legacy one-shot path was producing. Behind a per-user
`briefing_mode` setting (default "legacy"); falls back to the legacy
path automatically if the new path returns empty (e.g. model too weak
to drive tool calls reliably).
New: services/briefing_tools.py — explicit read-only allowlist of 10
tools (tasks, events, weather, rss, projects, notes). New tools added
to tools.py must be opted in by name. Excludes all mutating tools and
external search tools (search_images, search_web, research_topic) which
are neither useful nor safe for a scheduled background job.
New: briefing_pipeline.run_agentic_briefing — wraps the existing
stream_chat_with_tools loop with slot-specific system prompts that tell
the model to only assert facts from tool results and to be honest when
tools return nothing. Max 8 rounds, per-round exception handling,
returns the full message list so tool-call receipts can be persisted
alongside the prose in a later PR.
Sentence-count floors bumped: compilation 6–10 (was 4–8), check-ins
3–5 (was 2–3). Weekly review 5–8.
Design doc: docs/2026-04-10-agentic-briefing-design.md
Out of scope for this PR (future PRs): slot-injection migration,
persisting tool-call receipts into the conversation so chat follow-ups
see them, UI polish for tool-call status, sidecar storage for
briefings. See the design doc's migration path for details.
Enable on an account with:
UPDATE settings SET value='agentic'
WHERE user_id=<id> AND key='briefing_mode';
or insert the row if missing.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace the hardcoded "2h" keep_alive everywhere with a helper that
returns OLLAMA_KEEP_ALIVE_MAIN (default 30m) for the interactive model
and OLLAMA_KEEP_ALIVE_BACKGROUND (default 10m) for the background
model. Lets the main model release VRAM during long idle periods
while keeping it warm enough for bursty chat use, and stops the
sporadic background model from camping VRAM it rarely needs.
Seven call sites updated to route through llm.keep_alive_for(model):
the streaming helpers, generate_completion, the two startup warmers,
the settings KV-cache primer, and the chat warmer endpoint.
Override via env vars: OLLAMA_KEEP_ALIVE_MAIN, OLLAMA_KEEP_ALIVE_BACKGROUND.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace indigo (#6366f1) with deep violet (#7c3aed) gradient header,
violet-tinted card border and background, refined border radius and
spacing, and violet-accented footer to match the app's design language.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exposes birthday, organization, address (person) and website, category (place)
from entity_meta in the knowledge API response; updates KnowledgeView cards
to display organization and category as visible meta chips/text.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The model was hallucinating project names from task/note content (e.g.
inferring "Vehicle Maintenance" from "purchase wheel bearings"). Added
explicit guidance to both project field descriptions: only set if the
user explicitly named a project, never infer from content.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- build_context: when conversation_type is 'briefing', inject a system
prompt instruction telling the model to answer from conversation history
and article context instead of searching the web
- Consolidate briefing conversation type detection to one DB query (was
being checked twice — once for the system prompt addition, once for
article context injection)
- ChatPanel: render a visual 'New Briefing Update' separator line before
2nd+ briefing slot messages (identified by metadata.rss_item_ids)
- types/chat.ts: add metadata field to Message interface
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the single monolithic research note with topic-driven section notes
plus an index note. Two new LLM calls: _generate_outline (JSON outline, 3-8
sections) and _synthesize_section (300-600 word focused note per section,
parallelised via asyncio.gather). Public signature of run_research_pipeline
unchanged; falls back to single-note synthesis on outline failure or if all
sections fail.
Also extracts _build_sources_block helper and adds full test suite.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Route now logs every synthesis request (char count, voice, speed)
- Route logs char count + text preview when the 8000-char limit is hit
- Route logs empty audio with preview (helps spot no-chunk-produced edge case)
- Route logs success with byte count and duration
- Kokoro synthesise() logs per-call: samples produced, elapsed, chars/s
- Kokoro synthesise() logs warning when zero audio chunks returned with preview
- Kokoro synthesise() catches and logs pipeline-internal errors with preview
- Frontend: console.warn now includes char count + 80-char preview on failure and retry
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
CONTENT_MAX_CHARS was removed from rss.py when the article content cap
was lifted. backfill_rss_article_content still referenced it, causing an
ImportError on startup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- _add_user_jobs now accepts config dict and skips disabled slots
- _get_briefing_enabled_users returns 3-tuple (user_id, tz, config)
- update_user_schedule passes config through to _add_user_jobs
- _run_slot_for_user skips morning slot on non-work days via profile.work_schedule
- Add tests for slot gating and work-day gate
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes the custom classify_capture_intent + _process_note two-pass
approach. The LLM now picks the right tool directly via Ollama's native
tool_calls API (same path as the main chat pipeline). _should_think
decides whether extended reasoning is needed based on input length/
complexity. intent.py deleted — no longer needed.
Android app and response format unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Needed by the Discuss endpoint to persist synthetic read_article
tool exchanges before triggering generation.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Trafilatura extracts only article body text (typically 2K–15K chars),
so storing the full content is safe without an artificial ceiling.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- PUT/PATCH/DELETE /api/tasks/:id now use get_note_for_user + can_write_note
so shared project editors can mutate tasks; owners unaffected
- PATCH /api/tasks/:id/status gets same treatment
- All write routes call update_note/delete_note with note.user_id (owner)
not the accessing user's uid, matching the milestone fix pattern
- create_task tool gains tags (array) and status (enum) parameters;
handler now passes tags to create_note and respects initial status
- create_task tool response now includes milestone_id and parent_id
- update_note tool gains milestone parameter; handler resolves the
milestone by title within the note's current (or newly set) project,
clears milestone_id when project is cleared
- list_tasks tool gains q keyword search parameter; passed through
to list_notes
- TaskEditorView: replace window.location.reload() with
router.push('/tasks/:id') after save
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Project.to_dict() now includes user_id and auto_summary
- Status validation unified to (active/completed/archived) on both
create and update project routes; update route previously had none
- Milestone routes: replace get_project (ownership-only) with
get_project_for_user so shared viewers/editors can access milestones
- Add get_milestone_in_project() to milestones service for project-
scoped lookup without user_id filter; all milestone routes use it
- Milestone PATCH now validates status as 'active'|'done'; fix tool
enum which was wrongly ['active','completed','cancelled']
- Write mutation routes (POST/PATCH/DELETE milestones) now check
can_write_project() and return 403 for read-only shared users
- update_project tool now exposes title and color fields so projects
can be renamed or recolored via chat
- create_project tool now exposes color field
- GET /api/projects?include_summary=true embeds summaries in one
backend pass; ProjectListView switches to this, eliminating N+1
per-project fetches
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- cleanup_old_conversations now excludes briefing conversations (was
silently deleting briefing history after the retention window)
- list_conversations response now includes rag_project_id, matching the
shape returned by the single-conversation GET endpoint
- create_conversation_from_article: removed duplicate async_session import
(_session2 was a copy of the same import); consolidated into one
- MAX_TOOL_ROUNDS fixed from 5→6 to match the actual range(6) loop;
loop updated to range(MAX_TOOL_ROUNDS) so the constant is accurate
- Chat retention cleanup moved from per-request (every GET /conversations)
to a daily scheduled job in event_scheduler.py; route no longer runs
a DB write on every read
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- RRULE expansion: list_events now expands recurring events into
individual occurrences within the query window using python-dateutil
- CalDAV pull sync: new caldav_sync.py + POST /api/events/sync route;
imports remote events into the internal store by caldav_uid
- Past event search: search_events accepts include_past=true to search
historical events; exposed in the LLM tool definition
- Internal reminders: migration 0037 adds reminder_minutes +
reminder_sent_at columns; event_scheduler.py checks every 5 min and
fires push notifications; CalDAV sync job runs hourly
- reminder_minutes now stored and returned in create/update routes + tools
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- tools.py: apply UTC normalization to update_event datetime fields
(matched create_event which already did this)
- events.py service: allow end_dt/recurrence/project_id to be cleared
via update_event by permitting None for nullable fields
- events.py service: find_events_by_query now returns upcoming events
first, falling back to past — prevents AI tools from mutating stale
past events when a future match exists
- events.py service: list_events now uses overlap logic (start <= to
AND end >= from) so multi-day events spanning the query boundary
are included; previously only start_dt was checked
Frontend:
- ToolCallCard: fire fable:calendar-changed on created/updated/deleted
so CalendarView refetches without requiring a manual page refresh
- KnowledgeView: replace raw apiGet('/api/events') with listEvents()
client function; also fix today bar which was reading .events off a
flat array (always empty) — now correctly receives EventEntry[]
- HomeView: use full ISO strings for event date range instead of naive
UTC-midnight strings; deduplicate inline date math via _dateRange()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Backend:
- GET /api/knowledge/ids: returns up to 100 note IDs cheaply (no body
parsing), supports same filters as /api/knowledge, includes has_more
- GET /api/knowledge/batch?ids=...: fetches full items for given IDs in
order; used by frontend to load content in controlled batches
Frontend (KnowledgeView):
- Fetch 100 IDs upfront, load first 50 as content on mount
- IntersectionObserver sentinel (root: null) triggers 24-item content
batches as user scrolls
- Proactive ID refill when queue drops below 48 unloaded IDs
- fetchGen counter invalidates stale in-flight responses on filter reset
- IDs claimed before async fetch to prevent double-loading
- sentinelVisible ref drives post-load re-check when content doesn't
push sentinel off screen
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KnowledgeView: sentinel was OUTSIDE the card-grid div, making
IntersectionObserver (root: cardGridEl) never fire since the target must
be a descendant of the root. Moved sentinel inside card-grid with
grid-column:1/-1 + order:9999 so it spans all columns and sits at the
bottom. Fixed backup check to compare against container bounds not viewport.
tools.py list_events: apply same UTC normalization as create_event (treat
naive datetimes as UTC, handle Z suffix). Update tool description to
explicitly request full-day UTC ranges so the LLM doesn't send local time
without offsets, which caused the recall query to miss UTC-stored events.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- KnowledgeView: IntersectionObserver was watching the viewport instead
of the card-grid scroll container, causing infinite scroll to stop
loading after only ~29 items. Pass card-grid element as `root`.
- CalendarView: listen for 'fable:calendar-changed' custom event and
call refetchEvents() so tool-created events appear without navigation.
- ChatPanel: dispatch 'fable:calendar-changed' when create_event,
update_event, or delete_event tool calls succeed.
- tools.py: normalize naive datetimes to UTC before storing events so
timezone comparisons in list_events queries are always consistent.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds pick_num_ctx() which selects the smallest context window tier
(8192, 16384, 32768) that fits the current messages with 25% headroom,
capped at OLLAMA_NUM_CTX. Threads num_ctx through generation_task.py so
every chat request uses the computed tier rather than a fixed 16384.
Fixes a critical cache miss bug: KV cache priming in app.py and
settings.py was sending requests without num_ctx, so Ollama sized the
cache at its model default (different from the 16384 real requests used),
forcing a full model reload on the first real user message. Both priming
sites now call pick_num_ctx() and pass the matching value.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Removes verbose redundant text from tool descriptions and system prompt
guidance: multi-line recurrence_rule JSON examples, CAPS warnings that
duplicate system prompt instructions, and wordy descriptions that don't
add model understanding.
Saves ~990 tokens per request (~17% reduction, 5,639 → ~4,650 tokens),
reducing prefill time on cache misses and lowering KV memory pressure.
No functional changes — parameter names, types, enums, and required
fields are unchanged.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>