Menu ▾ ▴

#202 Stop replaying notifications after a self-perpetuating reload loop

closed
nobody
None
2026-07-26
2026-07-26
Anonymous
No

Originally created by: kristofferR

Duplicate notifications for a single message, repeating hours apart, reported on 1.6.0.

Evidence

Log timestamps are UTC (local +2), so all four banners in the report are in Carrier.log, and all four came from the same path:

Reported Log (UTC) Path
23:10 21:10:53 notify.fallback
01:42 23:42:17 notify.fallback
01:58 23:57:16 notify.fallback
02:06 00:05:40 notify.fallback

None was preceded by notify.recovered — the only route from a mismatched fingerprint to a notification. So reconcileFingerprint returned "missing" each time: the persisted entry was gone. Decoding the UTF-16 values out of the WebKit localStorage WAL confirmed the store itself works and holds a single entry, so it was being erased and rewritten between deliveries.

From 23:41 to 00:11 the log shows reloading stale Messenger view after realtime every ~65 seconds without a break — 21 reloads in one hour, 11 in the next. Each duplicate fires just after one.

Bug 1 — the trigger: an endless 60-second reload loop

RealtimeHealthWatchdog.health() set recoveryStartedAt when the last page-owned socket closed and never cleared it, returning "stale" permanently. But current Messenger runs sync in a worker and stops replacing the page socket — as the monitor's own docs note. Messages were plainly still arriving.

RealtimeRecoveryTracker.needsRecovery() OR'd its sources, so a healthy worker could never clear a stale socket, and realtimeRecoveryDelay() reads its floor from sessionStorage, which survives the reload — re-arming at exactly 60s forever.

  • Owning no socket now reports "starting" (unobservable) instead of "stale". A socket stuck connecting past the grace period is still a genuine failure and still reports stale, as does an open-but-silent one.
  • Sources can withdraw() a verdict they can no longer form, so a stale opinion isn't latched after the source goes blind.
  • A source healthy within REALTIME_CORROBORATION_MS (30s) vouches for one reporting stale, so a dead page socket beside a responsive worker no longer forces a reload.
  • The pending reload re-checks needsRecovery() at the moment of truth, so a page that recovered during the 60s gap is never yanked out from under you.

Bug 2 — the hole it exposed: the dedupe entry was discarded

READ_TRANSITION_CONFIRM_MS was 1s. Once a document had seen a row unread even once, one second of that row scraping as non-unread deleted the delivered fingerprint for good. That scrape is getComputedStyle(span).fontWeight over a React-virtualized list that re-renders constantly, so a sub-second styling flap was enough. The entry died, the next reload primed fresh trackers, and the unread row looked new.

The native deduper couldn't catch it either: its window is 30s and these replays were 8–15 minutes apart.

Forgetting now requires sustained, corroborated evidence — a 20s bound, at least three consecutive read observations, and a fully hydrated list. A row's text can hydrate ahead of its unread styling, so absence of bold is unreliable in exactly that window; presence of bold is not, and is still trusted.

Dropping late is nearly free: it only delays re-notifying a new message whose text is byte-identical to the one just read. Dropping early is the duplicate-banner bug.

Validation

bunx biome check inject, bun run typecheck, bun test (259 pass), bun run build:inject — all clean, bundles regenerated. No Rust changed. crq preflight findings verified and all three applied.

Note: bun run lint currently fails repo-wide because stale directories under the gitignored .worktrees/ contain nested Biome root configs; lint was scoped to inject instead. Unrelated to this change and left alone, since one of those worktrees is active.

Ref [#135]

Summary by CodeRabbit

  • Bug Fixes
  • Improved Messenger auto-refresh recovery to avoid unnecessary reloads and better handle temporarily unobservable realtime connections.
  • Reduced false notification read-state changes caused by virtualized or partially loaded conversation lists.
  • Improved detection of genuinely stable unread and read states.
  • Reliability
  • Added stronger realtime health corroboration and recovery tracking across connection states.
  • Improved handling of connections that are starting, stale, or temporarily unavailable.

Related

Tickets: #135

Discussion

  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: kristofferR

    @codex review

     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: kristofferR

    @coderabbitai review

     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: coderabbitai[bot]

    ✅ Action performed Review finished. > Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.
     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: coderabbitai[bot]

    Review Change Stack

    No actionable comments were generated in the recent review. 🎉

    ℹ️ Recent review info
    ⚙️ Run configuration **Configuration used**: Path: .coderabbit.yaml **Review profile**: ASSERTIVE **Plan**: Pro Plus **Run ID**: `17f4bbfb-ecfb-4029-ac19-c65a822cb930`
    📥 Commits Reviewing files that changed from the base of the PR and between [2571bb] and b5d6d4f4ace247486f9b31415fddf217454f94d5.
    📒 Files selected for processing (8) * `inject/src/messenger/features/auto-refresh.ts` * `inject/src/messenger/features/notifications.ts` * `inject/src/messenger/features/realtime-health.ts` * `inject/src/messenger/lib/notification-fallback.test.ts` * `inject/src/messenger/lib/notification-fallback.ts` * `inject/src/messenger/lib/realtime-health.test.ts` * `inject/src/messenger/lib/realtime-health.ts` * `src-tauri/inject/messenger.js`

    📝 Walkthrough ## Walkthrough The PR updates realtime recovery tracking and auto-refresh scheduling, and changes notification read-state detection to use hydration-aware consecutive observations. The generated Messenger bundle mirrors both source changes, with expanded tests for recovery states and read-drop timing. ### Changes **Realtime recovery** |Layer / File(s)|Summary| |---|---| |**Timestamped realtime health model**
    `inject/src/messenger/lib/realtime-health.ts`, `inject/src/messenger/features/realtime-health.ts`, `inject/src/messenger/lib/realtime-health.test.ts`, `src-tauri/inject/messenger.js`|Realtime recovery now tracks timestamped health, corroboration windows, withdrawn sources, unobserved transport, and explicit `starting` states. Tests cover these status and timing transitions.| |**Realtime reload scheduling**
    `inject/src/messenger/features/auto-refresh.ts`, `src-tauri/inject/messenger.js`|Auto-refresh timestamps health reports, cancels obsolete realtime reloads, clears settled recovery, and schedules reloads for unobserved recovery states.| **Notification read evidence** |Layer / File(s)|Summary| |---|---| |**Hydrated read streak tracking**
    `inject/src/messenger/lib/notification-fallback.ts`, `src-tauri/inject/messenger.js`|`NotifiedSignatureStore` replaces timestamp-only read tracking with hydration-aware streaks requiring elapsed time and consecutive observations before forgetting delivered fingerprints.| |**Unread scan integration**
    `inject/src/messenger/features/notifications.ts`, `inject/src/messenger/lib/notification-fallback.test.ts`, `src-tauri/inject/messenger.js`|Unread scanning passes list hydration into read tracking and uses hydrated, unread-row-free observations for zero-unread corroboration. Tests cover timing boundaries, read flaps, observation counts, and partial hydration.| **Estimated code review effort:** 4 (Complex) | ~45 minutes ### Sequence Diagram(s) :::mermaid sequenceDiagram participant SocketWatchdog participant RealtimeRecoveryTracker participant AutoRefresh SocketWatchdog->>RealtimeRecoveryTracker: report healthy, stale, or unknown RealtimeRecoveryTracker->>RealtimeRecoveryTracker: evaluate recovery windows RealtimeRecoveryTracker->>AutoRefresh: expose recovery status AutoRefresh->>AutoRefresh: schedule or clear reload **Possibly related PRs** - [[kristofferR/Carrier#99](https://github.com/kristofferR/Carrier/issues/99)](https://github.com/kristofferR/Carrier/pull/99): Related changes to `NotifiedSignatureStore` and unread notification replay handling. - [[kristofferR/Carrier#156](https://github.com/kristofferR/Carrier/issues/156)](https://github.com/kristofferR/Carrier/pull/156): Related realtime watchdog and reload recovery flow. - [[kristofferR/Carrier#165](https://github.com/kristofferR/Carrier/issues/165)](https://github.com/kristofferR/Carrier/pull/165): Related realtime recovery and heartbeat signaling changes.
    🚥 Pre-merge checks | ✅ 4 | ❌ 1 ### ❌ Failed checks (1 warning) | Check name | Status | Explanation | Resolution | | :----------------: | :--------- | :------------------------------------------------------------------------------------ | :--------------------------------------------------------------------------------- | | Docstring Coverage | ⚠️ Warning | Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. | Write docstrings for the functions missing them to satisfy the coverage threshold. |
    ✅ Passed checks (4 passed) | Check name | Status | Explanation | | :------------------------: | :------- | :------------------------------------------------------------------------------------------------- | | Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled. | | Title check | ✅ Passed | The title captures the core fix: breaking the reload loop that was causing repeated notifications. | | Linked Issues check | ✅ Passed | Check skipped because no linked issues were found for this pull request. | | Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request. |
    ✨ Finishing Touches
    📝 Generate docstrings - [ ] Create stacked PR - [ ] Commit on current branch
    🧪 Generate unit tests (beta) - [ ] Create PR with unit tests - [ ] Commit unit tests in branch `fix/notification-replay-reload-loop`

    Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

    ❤️ Share - [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai) - [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai) - [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai) - [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

    Comment @coderabbitai help to get the list of available commands.

     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: kristofferR

    @coderabbitai review

     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: coderabbitai[bot]

    ✅ Action performed Review finished. > Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.
     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: kristofferR

    @codex review

     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: kristofferR

    @coderabbitai review

     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: coderabbitai[bot]

    ✅ Action performed Review finished. > Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.
     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: kristofferR

    @codex review

     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: kristofferR

    @coderabbitai review

     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: coderabbitai[bot]

    ✅ Action performed Review finished. > Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.
     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: kristofferR

    @codex review

     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: chatgpt-codex-connector[bot]

    Codex Review: Didn't find any major issues. You're on a roll.

    Reviewed commit: b5d6d4f4ac

    ℹ️ About Codex in GitHub
    [Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you - Open a pull request for review - Mark a draft as ready - Comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: kristofferR

    @coderabbitai review

     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: coderabbitai[bot]

    ✅ Action performed Review finished. > Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.
     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: kristofferR

    @coderabbitai review

     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: coderabbitai[bot]

    ✅ Action performed Review finished. > Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.
     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: kristofferR

    @coderabbitai review

     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: coderabbitai[bot]

    ✅ Action performed Review finished. > Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.
     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: kristofferR

    @codex review

     
  • Anonymous

    Anonymous - 2026-07-26

    Ticket changed by: kristofferR

    • status: open --> closed
     
  • Anonymous

    Anonymous - 2026-07-26

    Originally posted by: chatgpt-codex-connector[bot]

    Codex Review: Didn't find any major issues. Already looking forward to the next diff.

    Reviewed commit: 2239632c28

    ℹ️ About Codex in GitHub
    [Your team has set up Codex to review pull requests in this repo](https://chatgpt.com/codex/cloud/settings/general). Reviews are triggered when you - Open a pull request for review - Mark a draft as ready - Comment "@codex review". If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
     

Log in to post a comment.