Drift-audit Group 8 (final item). _fire_reminders previously gated on the
base row (reminder_sent_at IS NULL AND start_dt > now), so a recurring event
reminded at most once ever — once the first occurrence passed, no future
occurrence qualified.
Now recurring events are evaluated every sweep against their next occurrence
(rrulestr.after(now)), and reminder_sent_at stores the start of the occurrence
last reminded about. Each new occurrence has a distinct marker, so it re-arms
and fires exactly once per occurrence. One-shot events keep the classic
NULL gate. Also adds the deleted_at filter so trashed events stop reminding.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drift-audit Group 2 (Phase-8 amputation — live wiring, no consumer):
- Recurring tasks never recurred: spawn_recurring_tasks() had no caller.
Register it as a 15-min interval job in the event scheduler (which
app.py already starts/stops). Also add a deleted_at IS NULL guard to
the spawn query in the same change, so a trashed recurring parent can
never resurrect children once the sweep is live.
- Event reminders were stamped reminder_sent_at but never delivered.
_fire_reminders now creates an 'event_reminder' in-app notification
before stamping, so a delivery failure stays retryable. Frontend
NotificationsPanel renders the new type (⏰ + message); message logic
pulled into a notifMessage() helper.
- Remove the dead _fire_push_notif no-op stub (push left in Phase 8) and
its three create_task call sites — no more throwaway tasks per share.
Co-Authored-By: Claude Opus 4.8 (1M context) <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>