Without this, await audio.play() resolves immediately after source.start(),
so the playQueue chains the next sentence before the current one finishes,
causing overlapping / interrupted playback.
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>
Chat section and inline response now have max-width 720px centered
within the page, and quick action chips are centered. Prevents the
widget from stretching the full 1200px content width on wide screens.
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>
- Parse multi-line SSE format correctly (event: on separate line, chunk in data.chunk)
- Retry stream GET up to 10x with 300ms backoff when 404 (buffer not ready yet)
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
POST .../messages to start generation, then stream from .../generation/stream.
The previous implementation used a non-existent /stream endpoint.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- stream_get now reads error responses before calling _raise_for_status
(httpx raises on .json()/.text access inside an unread stream context)
- Raise read timeout to 120s (was 30s) so generation streams don't timeout
- Document "generation" as a valid category for fable_get_app_logs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Three sources of unbounded growth removed:
- Drop cache-from/cache-to registry: on a persistent self-hosted runner the
local BuildKit layer cache already provides between-run reuse; the registry
cache was redundant and pushed ~2 GB of torch layers on every build
- Switch docker system prune -f → -af so old :SHA-tagged images are removed,
not just dangling ones (-f alone never touched named tags)
- Add docker builder prune --keep-storage 5g to bound the local BuildKit
cache; pip mount cache (torch etc.) is recently-used so survives, stale
intermediate layers are evicted first
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- renderMarkdown() accepts interactiveCheckboxes option: removes disabled=""
and stamps data-task-index on each checkbox in the marked HTML output
- NoteViewerView detects list notes by body content (- [ ] / - [x] pattern)
and passes interactiveCheckboxes: true when rendering
- onBodyChange() handles checkbox change events: toggles the matching line
in the body, optimistically updates the store, then PATCHes the note
- prose.css adds .prose--checklist rules for marked output: no bullet,
flex row, accent-color, line-through on checked items via :has()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
build-push-action@v7 generates OCI attestation manifests by default.
Forgejo's registry doesn't support OCI image index format with attestations,
causing the push to fail with "unknown". provenance: false disables this.
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>
- Drop max-width from .knowledge-root so the graph panel can use the full
viewport width without hitting a cap
- Drop max-width from .chat-page (message bubbles are already self-constraining)
- Increase normal graph panel width 420px → 500px
- Increase expanded width to min(960px, 60vw) so it scales with the viewport
and updates minichat right-offset to match
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
kokoro and transformers pull full nvidia CUDA wheels by default (~2 GB),
exhausting the runner disk. Pre-installing torch from the CPU wheel index
satisfies the dependency and prevents pip from selecting the CUDA variant.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add `# syntax=docker/dockerfile:1` to enable BuildKit cache mounts
- Replace `--no-cache-dir` pip installs with `--mount=type=cache,target=/root/.cache/pip`
so torch/CUDA wheels are reused across builds instead of re-downloaded every run
- Add `docker system prune -f` step before build to free dangling image/layer space
- Add `cache-from`/`cache-to` pointing to `:cache` tag so unchanged layers
(including the heavy voice-deps layer) are pulled from registry instead of rebuilt
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Layout:
- Chat and Knowledge views now max out at 1600px and center on wide
screens, consistent with the rest of the app; app-content overflow
is set to hidden for both so they manage their own scroll
Graph panel (Knowledge):
- Open/closed state persisted to localStorage (fa_knowledge_graph_open)
— stays open across navigation and page refreshes
- Expanded state persisted (fa_knowledge_graph_expanded): chevron button
in the panel header toggles between 420px (normal) and 700px (expanded)
- Minichat right offset follows the panel width with a matching transition
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>