Reminders & Notifications Architecture¶
State Contract
The notification system is a reactive consumer of the EventCache's time-tick stream. It maintains zero persistent state of its own, relying entirely on the canonical event data, runtime deduplication, and centralized trigger evaluations.
The Notification Pipeline¶
- Subscription: The
NotificationManagersubscribes to the high-frequencytime-tickevent from theEventCache(ticking every 60 seconds). - Lookahead Filtering: On every tick, the manager filters the cache for events starting within the next 48 hours to minimize processing overhead.
- Centralized Trigger Evaluation:
- The manager calculates a trigger time for each occurrence using the public helper
getTriggerTime(occurrence: EnrichedOFCEvent). - Custom Priority: If an event has a
notifyproperty in its metadata (e.g.notify: 15), the trigger point is computed asstart.minus({ minutes: event.notify.value }). - Default Fallback: If no custom value exists and global default reminders are enabled, the trigger point is computed as
start.minus({ minutes: defaultReminderMinutes }).
- The manager calculates a trigger time for each occurrence using the public helper
- Mutex and FCR Takeover:
- Before firing local OS notifications or launching the interactive snooze/dismiss modal, the manager checks if the FCR Reminder Companion is enabled (
fcrReminderCompanion.enabled). - If enabled, the native toast notification is bypassed (returning immediately), allowing the background daemon to handle alerting. For details on how the daemon takes over active alerting, see the Alert Mutex (Obsidian Bypass) specification.
- Before firing local OS notifications or launching the interactive snooze/dismiss modal, the manager checks if the FCR Reminder Companion is enabled (
- Deduplication: To prevent "notification storms" (especially during startup or timezone shifts), every locally triggered notification is keyed by
sessionId::type::triggerTime. Once a key is added to the runtimenotifiedEventsset, it cannot trigger again in the current session.
Destructive Snooze Implementation¶
The decision to use a destructive snooze (modifying source data) rather than a runtime-only snooze was made to solve the Multi-Device Conflict problem.
Rationale¶
If snooze was runtime-only, snoozing on a Desktop would not prevent a mobile device (or a Google Calendar notification) from firing at the original time. By modifying the source YAML or Google Event, the "snooze" state is synchronized across the entire ecosystem.
Logic¶
- Time Shift: For events without custom
notifyvalues, thestartTimeis incremented. - Threshold Shift: For events with
notifyvalues, thenotifyinteger is decremented.
Provider Alarm Contract¶
Full Calendar keeps two reminder concepts separate:
notifyis the Obsidian/local reminder value consumed byNotificationManager.alarmsis provider-owned reminder data that syncs to remote calendar services.
Providers that can persist provider alarms set CalendarProviderCapabilities.supportsAlarms. The edit modal reads that capability and shows the provider reminder control only for those calendars.
Provider mappings:
- CalDAV serializes
alarmsas ICSVALARMcomponents. - Google serializes
alarmsas popup reminder overrides. - Outlook serializes
alarmsas reminder minutes.
Recurring instance overrides inherit notify and alarms from the master event unless the override explicitly supplies its own values. For CalDAV, override alarm edits are written by replacing the matching recurrence override component in the same .ics resource as the recurring master.
Startup Safety (Recency Cutoff)¶
The manager implements a 5-minute recency cutoff. If a reminder's trigger point was more than 5 minutes in the past (e.g., you open Obsidian at 14:05 for a 14:00 event with a 10-minute reminder), the notification is suppressed. This prevents a "spam" of missed notifications when starting the app after a long break.
FCR Reminder Companion Architecture · Event Cache · Timezone Architecture · API Architecture