In the app's own store, nothing re-ran the reminder scheduler when a task was completed in the list, the detail screen or a widget, or when a sync pulled in a completion from another device. The alarm stayed armed until the next app start or the daily maintenance pass. A reminder that was already showing never came down, because only its own "Done" button cleared it. There was a second cause for repeating tasks. A ticked occurrence is saved as a row with its own id, but `occurrence()` searched the list by the series id. It never found the ticked occurrence, fell back to the next open one and posted the reminder anyway. That's probably the first half of #127. - `ReminderRefresher` runs one debounced `ReminderScheduler.sync()` per burst of changes to tasks, lists or alarms. Account bookkeeping doesn't trigger it. The refresher and `TaskWidgetUpdater` now share a `StoreChangeDebouncer`. - Each scheduler pass withdraws posted reminders whose occurrence is closed. It also runs when reminders are switched off. It only looks at reminders from the current storage mode. - A missing task counts as deleted only in Agendula's own store. A provider that fails a read returns nothing rather than throwing. - Reminders now record their occurrence in the notification extras. Reminders posted by the previous version are only withdrawn for tasks that don't repeat. - `occurrence()` looks up occurrences by their start time, so it finds ticked rows. It falls back to scanning the list for providers whose exception rows the direct lookup misses. - A fired reminder now stays marked as fired for the 6 h grace period, even after its task is ticked. Ticking a task and then tapping Undo no longer makes the reminder ring again. - After posting, the receivers check the task once more. That catches a completion that lands between the check and the post. - These callers no longer run the scheduler themselves, because the refresher now covers them: the edit screen, the Done action, a storage-mode switch, and the two import paths in Settings. `ProviderWriter` asks the refresher for a pass instead of syncing directly. - `SyncWorker` still syncs directly. Once the worker returns, nothing keeps the process alive for the debounce. Closes #127 Closes #129 Co-authored-by: Jean-Luc Makiola <business@jeanlucmakiola.de> Reviewed-on: https://codeberg.org/jlmakiola/agendula/pulls/130