Discuss flow was hallucinating unrelated content when article
extraction returned empty or RAG pulled in orphan notes that looked
more relevant than the generic seed prompt.
- seed_article_discussion raises EmptyArticleError on empty body;
briefing and /news routes return 422 instead of staging an empty
synthetic tool result.
- build_context skips RAG auto-injection when user_message matches
ARTICLE_DISCUSS_SEED so the article IS the context on turn one;
follow-up turns keep RAG on.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Both the /news discuss button and the briefing discuss button now call a
shared seed_article_discussion() helper that stages the synthetic
read_article tool exchange and the conversational seed prompt — behavior
stays byte-identical across entry points. /news also auto-starts
generation so the chat screen lands on an in-flight stream.
First assistant reply in a seeded article conversation is persisted as a
Note (tags: article-summary + article topics) and backlinked via
rss_items.discussion_note_id, so the knowledge base stops being amnesiac
about articles the user has engaged with.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The Discuss button on news cards was producing one-shot replies because
the model got the whole trafilatura blob dropped into history with a
canned "summarize and discuss this article" prompt — no length guard, no
prep, no invitation to converse. Large articles got silently truncated by
Ollama; small articles got a tepid reply.
This reworks discuss_article around a three-layer cache:
context_prepared → content_full → fresh trafilatura fetch
First click on a small article fetches once, writes through to both
caches, and passes the body straight into the synthetic read_article
tool-result. First click on a large article additionally runs a parallel
map step (services/article_context.py) that chunks the body on paragraph
boundaries, summarizes each ~8k chunk to ~300 words of dense factual
prose via the background model, and concatenates the summaries under
section headers — all pinned to num_ctx=16384 so the map step doesn't
itself fall victim to silent truncation. Repeat clicks on either path
skip straight to the chat turn.
The canned summary prompt is replaced with a conversational seed that
invites the user into an actual discussion rather than a one-shot
synopsis, matching the goal of "have a conversation about an article,
not just read it."
discuss_topic is intentionally left untouched — it's the multi-article
aggregation path and needs a separate rework. Follow-up task will decide
whether to retire it or rework it on the cached-context approach.
Closes task #106.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Two related bugs where the server defaulted naive datetimes to UTC instead
of the configured user timezone, causing all-day events to land on the
previous day and briefings to "disappear" at UTC midnight.
- New services/tz.py helpers: get_user_tz, user_today, user_briefing_date
(the briefing day flips at 4am local to align with the compilation slot,
so the 00:00-04:00 local window still shows yesterday's briefing until
the new one is generated).
- calendar create/list/update tools now parse naive datetimes in the
user's TZ before converting to UTC for storage, and tool descriptions
tell the model to pass plain local dates.
- briefing_conversations.get_or_create_today_conversation and the
reset-today route use user_briefing_date so the in-progress briefing
doesn't get replaced at 19:00 NY / UTC midnight.
- _run_profile_closeout targets user-local "yesterday" for consistency.
Regression tests added for the TZ helpers and the calendar tool.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Adds 38 parametrized tests for the _should_think classifier covering the
explicit-override path, empty/whitespace content, short/medium/long length
boundaries, case-insensitive keyword matching, and a chatty-message negative
set. These pin the content-based semantics so future tweaks to the keyword
list or length thresholds surface regressions immediately instead of going
unnoticed behind subtle latency changes.
Also drops the `think=True` overrides from the briefing /discuss-article
and /discuss-topic entry points. With `"discuss"` added to _THINK_KEYWORDS,
those canned prompts trip the classifier naturally, so the overrides were
redundant — keeping a uniform "classifier is authoritative" rule makes the
code easier to reason about.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
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>
POST /api/briefing/articles/<id>/discuss injects stored article content
as a persisted read_article tool exchange before triggering generation.
The LLM sees the article as already read; follow-ups retain context via
the fixed history builder. Frontend Discuss button now calls the new
endpoint instead of inlining article text in the user message.
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>
Browser timezone is now synced to user_settings["user_timezone"] on
every login/page load (App.vue). The briefing scheduler and LLM context
both read from this single source, falling back to the legacy
briefing_config.timezone for existing users during migration.
- App.vue: PUT /api/settings with browser IANA timezone on startAppServices
- routes/chat.py: fall back to stored user_timezone when not sent in request
- briefing_scheduler: read user_timezone setting; briefing_config.timezone
kept as fallback only
- routes/briefing.py: pass tz_override from user_timezone to live-patched scheduler
- Remove timezone field from BriefingConfig interface and all briefing UI
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Task change detection via snapshot diff
- RSS scoring/filtering via briefing_preferences
- Weather card via parse_weather_card_data (staleness-gated)
- News card markdown format with ordering constraint
- Metadata stored on Message record via post_message()
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Briefing slots (4am/8am/12pm/4pm) now fire in each user's local timezone
rather than UTC, so the schedule matches the user's actual day.
Backend:
- briefing_scheduler: replaced 4 global UTC cron jobs with per-user
CronTrigger jobs keyed to the user's IANA timezone; update_user_schedule()
live-patches the scheduler when config is saved (no restart needed)
- Catchup logic evaluates missed slots in the user's local timezone
- put_config route calls update_user_schedule() after saving
Frontend:
- New Timezone section in Briefing settings: text input pre-filled from
browser (Intl.DateTimeFormat) with a Detect button for re-detection
- Slot times shown as fixed local times (4:00 am etc.) with the configured
timezone displayed beneath, replacing the old UTC-conversion display
- timezone field added to BriefingConfig type and default
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>