- Startup now pulls Config.OLLAMA_MODEL (system default chat model) — previously only
embedding and background models were pulled; the primary chat model was skipped
- _warm_user_models expanded to also pull user-configured default_model and
background_model overrides that are missing from Ollama, rather than logging and
skipping them; pulls run before warm/KV-cache priming
- Add background_model to _MODEL_KEYS in settings route so clearing the dropdown
deletes the DB row instead of saving "", which caused Ollama failures in tag
suggestions, title generation, project summaries, and RSS classification
- Add http/https scheme validation to PUT /api/admin/base-url matching the CalDAV
route pattern; a bad value no longer silently breaks invite/password-reset links
- Update admin voice config description: "Reload models" button exists to avoid
a server restart, so the old "restart required" text was misleading
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>
All datetime parsing now uses _parse_dt() which adds UTC tzinfo when
none is present, matching the fix already applied in tools.py. This
prevents asyncpg errors when comparing naive datetimes against
TIMESTAMPTZ columns — the root cause of events not appearing in the
calendar view.
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>
When a user saves a new default_model in Settings, fire a background
cache-prime request so the first message with the new model is fast
rather than paying the full cold-start prefill cost.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
After loading each user's chat model into VRAM, send a minimal chat
request with the real system prompt (num_predict=1) to populate the
KV cache. The first real user message then only needs to process its
own tokens rather than the full ~5,600-token system prompt, reducing
cold-start TTFT from ~25s to <1s.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Exposes OLLAMA_BACKGROUND_MODEL as a per-user setting in General settings,
alongside the Chat Model selector. Includes an inline warning when the same
model is selected for both, explaining the KV cache performance impact.
All background task callers (title generation, tag suggestions, project
summaries, RSS classification) now read background_model from user settings,
falling back to OLLAMA_BACKGROUND_MODEL env var.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Background tasks (title generation, tag suggestions, project summaries,
RSS classification) were using qwen3:8b and wiping its KV cache after
every response, preventing prefix cache hits on subsequent user messages.
Adds OLLAMA_BACKGROUND_MODEL (default: qwen2.5:0.5b) config var and
routes all background LLM calls to it, keeping qwen3:8b's KV cache
warm between user messages for consistent sub-second TTFT.
Also adds infinite scroll to KnowledgeView (replaces load-more button)
and bakes spaCy en_core_web_sm into the Docker image to eliminate the
pip install on every startup.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
get_weather now returns 1 day by default (today) instead of a full 7-day
forecast. A new optional `days` parameter (1–8) lets the model request
more days when the user explicitly asks for a weekly forecast or specific
date range. Tool description updated to guide the model accordingly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- KnowledgeView minichat: render assistant messages through renderMarkdown
so headers, bold, lists etc. display correctly instead of raw markdown
- get_weather tool: read user's temp_unit from briefing_config and convert
temperatures to °F when preferred; also include temp_unit in the
returned payload so the model can label values correctly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- KnowledgeView minichat: add margin-inline: auto so the widget centers
within the content area when max-width is reached on wide screens
- weather get_weather tool: return success: true on both the arbitrary
location path and the cached locations path so ToolCallCard shows
the correct success state instead of always flagging as error
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
RAG notes, RSS news, current note, URL content, and briefing articles
are now prepended to the user message rather than appended to the system
message. The system message now contains only stable content (persona,
tool guidance, date, profile, workspace, history summary), making its
token sequence identical across consecutive requests and allowing
Ollama's KV prefix cache to fire reliably every time.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Routes simple/conversational messages to think=false automatically,
even when the user has thinking enabled. Patterns checked: word count
thresholds, complexity keywords, code blocks, skip patterns for greetings
and simple CRUD. Workspace mode (think=true from frontend) still benefits
from the classifier on short messages.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
wait_for_model_loaded() polled /api/ps for up to 180s waiting for the
model to appear as loaded. But Ollama lazy-loads models on the first
/api/chat request, so the poll will never succeed — it just blocks for
the full 180s after every Ollama restart before proceeding.
Removed the wait entirely. Ollama handles on-demand loading correctly.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
65536 was causing Ollama to allocate a ~50GB KV cache, spilling 77% of
the model to CPU RAM and making prefill extremely slow (35-125s TTFT).
16384 covers 30+ message conversations comfortably while keeping the KV
cache small enough to stay on GPU.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Move static content (persona + tool guidance) to a fixed prefix and
append all dynamic content (date, timezone, profile, entities) as a tail.
Ollama prefix caching requires byte-for-byte token match from the start
of the prompt. Previously, Today's date + user profile were embedded
mid-prompt, invalidating the cache on every request/day and causing
~20s TTFT regardless of model warmth.
With this change the static prefix (~5500 tokens) should be cached
after the first request each session, reducing TTFT to ~2-5s for the
~200-token dynamic tail only.
Also removed inline user_timezone from tool_lines (timezone is now
stated once in the dynamic tail, which the model reads).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
KnowledgeView:
- Watch streamingToolCalls; call fetchItems+fetchTags on create/update
note or task so the card grid reflects changes made via the minichat
- Cap minichat to max-width: var(--page-max-width) so it matches chat column width
WorkspaceView + WorkspaceNoteEditor:
- Expose reload() from WorkspaceNoteEditor via defineExpose
- Call noteEditorRef.reload() alongside taskPanelRef.reload() when
create_note/update_note tools succeed in the SSE watcher
KnowledgeView list cards:
- Backend: parse markdown task list into list_items [{text, checked}] + body
- Card renders up to 6 items with real checkboxes; toggleListItem()
does an optimistic update then PATCHes /api/notes/:id
- Progress bar kept below items; "+N more" shown when list is long
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Two bugs:
1. Manual 'Refresh' button didn't refresh weather/RSS before compiling.
The daily scheduler calls refresh_all_feeds + refresh_location_cache
before run_compilation; the manual trigger route called run_compilation
directly, reading a stale cache. Manual trigger now mirrors the
scheduler: refresh feeds and all configured weather locations first.
2. Navigating to WorkspaceView during a long briefing compilation caused
the workspace chat to show the briefing content. triggerNow awaits
~30-60s; on completion it called loadAll() → chatStore.fetchConversation
which overwrote the workspace's currentConversation in the shared store.
Fixed with a _mounted flag — all post-async state writes in BriefingView
are now guarded so they no-op if the component has been unmounted.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three bugs in discussArticle():
- Scroll selector was '.briefing-chat' (doesn't exist) → '.briefing-center';
the panel never scrolled into view so the response was invisible until refresh
- Only 300-char snippet was sent to the LLM; now passes the full stored
content (up to 2000 chars) from the backend
- User message wasn't shown until streaming ended; now added optimistically
to messages[] immediately on click so it appears straight away
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- KnowledgeView mini-chat: replace harsh border-top with upward box-shadow
and rounded top corners (16px); remove padding-bottom from content area
so widget truly overlays cards without pushing layout; add collapse
toggle (chevron) that hides messages without closing the conversation
- WeatherCard: show precip_mm as fallback when precipitation_probability_max
is null but actual rainfall is expected (Open-Meteo omits probability for
some forecast days even when rain is shown)
- Pass precip_mm through weather service → frontend type definitions
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
SQLAlchemy reserves 'metadata' as a class attribute on declarative models.
Renamed to 'entity_meta' with explicit column name 'metadata' so the DB
column is unchanged but the Python attribute no longer conflicts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- On first load: model runs online (downloads .pt files), then stores the
current HF commit SHA to /data/kokoro_commit_hash.txt and switches the
process to offline mode (HF_HUB_OFFLINE) for all future requests
- On subsequent restarts: presence of the commit file triggers offline mode
before the pipeline loads, skipping all HuggingFace network validation
- Daily at 03:00 UTC: scheduler temporarily lifts offline mode, fetches the
latest commit SHA from HF, and only reloads the pipeline if the model has
actually changed — then restores offline mode
- Removed HF_HUB_OFFLINE from docker-compose.yml; behaviour is now automatic
and not a hoster/user concern
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- ChatView: listen mode toggle (auto-reads new responses via TTS), volume popup
with range slider persisted per-device in localStorage via GainNode
- useVoiceAudio: shared module-level _volume ref with localStorage persistence,
GainNode for volume control, exported setVoiceVolume()
- tts.py: pre-warm all Kokoro voices at pipeline load to eliminate HuggingFace
HEAD requests at synthesis time (reduces TTS latency)
- BriefingView: discuss article button now auto-sends instead of just filling input;
prompt capped to 15 sentences; send() accepts optional overrideText
- llm.py: instruct LLM not to proactively search notes or comment on note absence
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
synthesiseSpeech() called without explicit params now omits voice/speed/
blend from the request body. The backend detects this and auto-loads all
three from the user's saved settings (voice_tts_voice, voice_tts_speed,
voice_tts_blend), so briefing listen mode respects the voice the user
configured in Settings.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fetch precipitation_probability_max from Open-Meteo (replaces precip_sum
in the card display — probability is more useful at a glance than mm)
- Show WMO condition description text on each forecast day
- Convert wind speed to mph when temp unit is F; pass wind_unit in response
- Display 💧 X% chance of rain; 💨 X mph/km/h wind per day
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- WeatherCard: show precipitation (mm) and max wind speed per forecast day
- DashboardChatInput: add PTT mic button (transcribe-to-input, voice-gated)
- Remove global VoiceOverlay floating button and Space PTT shortcut from
App.vue — inline mic buttons in chat/briefing/dashboard are the right UX;
global overlay had focus/latency/context issues
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add voice blend support to TTS pipeline and settings UI. Users can mix
2–5 Kokoro voices with per-voice weight sliders; the blended style tensor
replaces the single voice when enabled. Settings persist as JSON and auto-
load on synthesis when no explicit voice is supplied in the request.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add trafilatura + html2text to dependencies
- Replace custom HTMLStripper with html2text for RSS feed content
- Fetch full article text via httpx + trafilatura after each new item is stored;
falls back to RSS-provided content if fetch/extraction fails
- Raise CONTENT_MAX_CHARS from 2000 to 50000 (TEXT column, no migration needed)
- Re-embed items with full article content once enrichment completes
- Startup backfill enriches existing items with short content (<1000 chars)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
feedparser returns HTML in content/summary fields for many feeds.
Raw tags were being stored in the DB and passed to the LLM/embeddings.
Added a stdlib HTMLParser-based stripper in extract_item() — block elements
become newlines, script/style content is dropped, plain text passes through.
No new dependencies required.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Embed RSS items at fetch time (nomic-embed-text); backfill at startup
- Semantic news search injected into chat system prompt ("Recent News You've Seen")
when items match query above 0.55 cosine threshold (independent of note RAG)
- "Discuss in chat" button on news cards — creates a seeded conversation with
the article title + full content, navigates directly to the new chat
- Briefing compilation now passes 500-char article excerpts (not just headlines)
to the LLM and uses 8192 num_ctx to accommodate the larger prompt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replaces the freeform briefing-profile note with a DB-backed user_profiles
table. Users can edit job/industry/expertise/response preferences/interests/
work schedule via a new Settings → Profile tab. The LLM appends nightly
observations; at 14+ entries they are auto-consolidated into a learned_summary.
Profile context is injected into both briefing and chat system prompts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace two-pass structured LLM synthesis (## Your Day / ## The World
sections with bullets and formatted news cards) with a single
conversational pass. The new prompt instructs the model to write 3-5
flowing sentences covering weather, today's tasks/events, and 1-2 news
highlights — no markdown, no headers, no lists. Full news detail stays
in the right panel; weather detail stays in the weather card.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Prevents models from sitting in VRAM indefinitely. Applies to both
streaming chat calls and the non-streaming generate_completion path,
as well as the startup warm-up request.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Voice enabled/STT model are now DB-backed (admin settings), not env
vars. Added reload_stt_model()/reload_tts_model() that clear singletons
under lock and re-trigger loading. POST /api/admin/voice/reload triggers
both in background tasks. Settings UI polls /api/voice/status every 2.5s
until models are ready, with spinner feedback.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace VOICE_ENABLED env var gate with DB-backed admin setting.
- services/voice_config.py: reads voice_enabled + voice_stt_model from
admin user's settings row (falls back to env var defaults)
- routes/admin.py: GET/PUT /api/admin/voice for admin configuration
- routes/voice.py, services/stt.py, services/tts.py: read enabled/model
from DB via voice_config instead of Config directly
- app.py: always schedule model loaders at startup; they self-gate on
the DB setting so no conditional needed at the call site
- SettingsView.vue: Voice section in Admin → Config tab (enable toggle +
STT model dropdown); user Voice tab now points to admin panel when disabled
No env var required to test — enable via Settings → Admin → Config → Voice.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>