Skip to content

fix(extension): re-arm reminder alarms after an update, and log reminder activity - #354

Merged
kYem merged 4 commits into
mainfrom
kes/eng-118-reminders-dont-fire-chromealarms-are-never-re-armed-after-an
Sep 20, 2026
Merged

kYem merged 4 commits into
mainfrom
kes/eng-118-reminders-dont-fire-chromealarms-are-never-re-armed-after-an

Conversation

@kYem

@kYem kYem commented Sep 19, 2026

Copy link
Copy Markdown
Owner

Fixes ENG-118, ENG-124

The bug

Chrome clears every chrome.alarms entry when an extension updates (documented: alarms "persist until the extension updates"), and the service worker never re-armed them from storage — ChromeScheduler.persistsAcrossRestarts = true switched the only re-arm loop off. Every release, and every crxjs dev reload, silently killed every pending reminder until the user happened to edit one.

Reproduced against the real built extension: e2e/reminder-alarms-survive-update.spec.ts seeds a reminder, arms it, stages a version bump, relaunches — red before, green after. (In Playwright's Chromium the alarm is gone after a plain relaunch too, with storage intact.)

The fix

armMissingReminderAlarms() in the shared reminder service, run by the worker on onInstalled + onStartup, seeded from chrome.alarms.getAll(). Ensure-exists, not recreate, so a Chrome that does keep an alarm can't fire it twice. Deliberately not at worker top level — that runs on every alarm wake and would re-create an alarm mid-fire. The macOS page-load re-arm now shares the same function.

The activity log (ENG-124)

The logger is console-only and the worker's console dies with the idle worker, so a missed fire was undiagnosable. recordReminderActivity() keeps the last 20 events in chrome.storage.local.reminderActivityarmed / cancelled / fired / skipped / toasted / done / snoozed / reconciled / advanced, tagged with the realm — under the collection lock, never throwing into the reminder path. Local-only, never synced.

e2e/reminder-lifecycle.spec.ts drives one reminder through the real UI — add → fire → snooze → staged update → done — asserts the trace in order, and prints it:

worker  reconciled   re-armed 0 of 0 pending
page    armed        …  due 05:45:02.000
worker  fired        Lifecycle probe
page    cancelled / armed  due 05:50:02
worker  reconciled   re-armed 1 of 1 pending
page    cancelled

Still open on ENG-118

Re-arm after a sync pull (the reminder-store.ts known gap), what an overdue-on-load reminder should do, and the page-toast + OS-notification double. Each is its own decision.

Verification

  • lint, type-check (12/12), 3030 unit tests across shared / storage / app / extension
  • both e2e specs green against the built extension

…der activity

Chrome clears every chrome.alarms entry when an extension updates, and the
service worker never re-armed them from storage, so each release silently
killed every pending reminder until the user edited one. The worker now
reconciles armed alarms against stored reminders on install/update and
browser start, arming only the missing ones so a kept alarm cannot fire twice.

A 20-entry reminderActivity ring buffer in chrome.storage.local records
armed/fired/skipped/snoozed/done/reconciled/advanced from both realms, so a
missed fire can be diagnosed after the fact. The reminder-lifecycle e2e drives
one reminder through the real extension and prints that trace.

Fixes ENG-118, ENG-124
…s once per start

Review round 1. The activity log exists to diagnose a missed fire, yet every
failure path wrote nothing to it: a notify, re-arm, snooze or button write
that failed now records a 'failed' entry naming the step; the reconcile
summary names the ids it could not arm and records its own failure; a read
that fails no longer wipes the log, and a corrupt value is replaced.

onInstalled and onStartup both fire on an update applied at launch, so the
reconcile is deduped behind one in-flight promise. The page's load-time
re-arm only moves the wakes it advanced, and the macOS launch re-arm warns
again when one fails. The e2e specs share one harness, settle each launch's
reconcile before seeding, and assert the reconcile record rather than the
alarm's mere presence. Comments say what the code does, in two lines.
…g at start

Review round 2. Chrome drops a one-shot alarm before dispatching it, so at
start an overdue reminder's fire is exactly the alarm getAll() no longer
lists; the reconcile re-armed it in the past and the user got it twice. The
worker now tracks fires in flight and treats them as armed.

The page's load-time advance decides which reminders moved inside the lock,
not from the snapshot a pull may have overtaken, so it neither logs a false
'advanced' nor re-arms a stale date. A reconcile or advance that fails is a
'failed' entry like every other failure, and the fire path's lookup, persist
and multi-failure cases are pinned.
…ncile's discriminator

Review round 3. A second overdue reminder not in flight is re-armed while
the firing one is left alone; the cancel-failure trace has its own test;
one comment split, one docstring reworded.
@kYem
kYem merged commit 3323938 into main Sep 20, 2026
10 checks passed
@kYem
kYem deleted the kes/eng-118-reminders-dont-fire-chromealarms-are-never-re-armed-after-an branch September 20, 2026 02:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant