Skip to content

fix(macos): suppress notification storm during backend re-init loop (MCP-2328) - #659

Merged
Dumbris merged 1 commit into
mainfrom
fix/mcp-2328-notification-replay-suppression
Jun 15, 2026
Merged

fix(macos): suppress notification storm during backend re-init loop (MCP-2328)#659
Dumbris merged 1 commit into
mainfrom
fix/mcp-2328-notification-replay-suppression

Conversation

@Dumbris

@Dumbris Dumbris commented Jun 14, 2026

Copy link
Copy Markdown
Member

Summary

Fixes the macOS tray notification storm (tens of Notification Center alerts top-right) that appears while the backend core is stuck in a ~10s re-init / restart loop.

PR #656 (MCP-2215) proved the Vue Web UI emits zero toasts on SSE reconnect/replay — the frontend is exonerated. The user-visible symptom is native macOS Notification Center alerts, not browser toasts.

Root cause

sendQuarantineAlert (on servers.changed) and sendSensitiveDataAlert (on activity) fire on a "count went up vs the last-seen value" heuristic in CoreProcessManager.handleSSEEvent. During a re-init loop each cycle disconnects then replays the full server/activity state, so the tracked count transiently drops to 0 and is re-established — making every cycle look like a brand-new event. The existing 5-min per-key rate limit doesn't fully tame it across the loop, and the alerts are spurious regardless.

Fix

Add ConnectionSettleGate to NotificationService:

  • Replay-driven notifications (quarantine, sensitive-data) are suppressed until the connection has been free of instability for a settle interval (12s, chosen above the observed ~10s loop period).
  • CoreProcessManager marks the connection unsettled on every transient/error state transition (.launching / .waitingForCore / .reconnecting / .error) and on config.reloaded.
  • An active loop re-arms the gate each cycle → it never settles → no spurious alert. A genuinely stable connection settles after 12s → legitimate events fire unchanged.

.connected (steady state) never marks unsettled, so once the loop ends real notifications resume.

Acceptance criteria

  • AC-1 ✅ No Notification Center alert for passive SSE replay (servers.changed / config.reloaded / status) during a re-init loop — the gate stays unsettled for the loop's duration.
  • AC-2 ✅ Legitimate notifications still fire — steady-state quarantine/sensitive events occur outside the settle window and pass the gate; non-replay paths (OAuth expiry, update, core error) are untouched.
  • AC-3NotificationReplaySuppressionTests guards the loop-never-settles invariant against the real production ConnectionSettleGate type (deterministic injected clock).
  • AC-4 ✅ No production behavior change beyond suppressing the spurious burst.

Verification

  • swift test --filter "NotificationReplaySuppressionTests|NotificationRateLimitTests"20 passed, 0 failures.
  • Full tray binary builds clean (swiftc -O, exit 0).
  • The 7 failures in the full swift test run (SSEParserTests / AutoStartTests / MergePatchEncodingTests / ModelsTests) are pre-existing on clean origin/main (verified by stashing this change and re-running) — unrelated to notifications.

Note: the storm requires a live backend crash-loop to reproduce; behavior is timing-based and not screenshot-verifiable, so the unit test (AC-3) is the regression guard.

Related MCP-2328 · refs #656 (MCP-2215), MCP-2207

Quarantine and sensitive-data notifications fire on a "count went up vs
the last-seen value" heuristic. When the core is stuck in a ~10s re-init /
restart loop, each cycle disconnects then replays the full server/activity
state, so the tracked count transiently drops and is re-established —
making every cycle look like a brand-new event and producing tens of
Notification Center alerts.

Add ConnectionSettleGate: replay-driven notifications are suppressed until
the connection has been free of instability (reconnect / relaunch / error
transition / config reload) for a settle interval (12s, above the observed
loop period). An active loop re-arms the gate each cycle and never settles,
so no spurious alert fires; a genuinely stable connection settles and lets
legitimate events through unchanged.

- NotificationService: gate guards sendQuarantineAlert / sendSensitiveDataAlert
- CoreProcessManager: marks the connection unsettled on transient/error state
  transitions and on config.reloaded
- NotificationReplaySuppressionTests: unit-tests the loop-never-settles
  invariant against the real production type

Related MCP-2328
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying mcpproxy-docs with  Cloudflare Pages  Cloudflare Pages

Latest commit: f2a97f3
Status: ✅  Deploy successful!
Preview URL: https://34806d0c.mcpproxy-docs.pages.dev
Branch Preview URL: https://fix-mcp-2328-notification-re.mcpproxy-docs.pages.dev

View logs

@codecov-commenter

Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@github-actions

Copy link
Copy Markdown

📦 Build Artifacts

Workflow Run: View Run
Branch: fix/mcp-2328-notification-replay-suppression

Available Artifacts

  • archive-darwin-amd64 (28 MB)
  • archive-darwin-arm64 (25 MB)
  • archive-linux-amd64 (16 MB)
  • archive-linux-arm64 (14 MB)
  • archive-windows-amd64 (28 MB)
  • archive-windows-arm64 (24 MB)
  • frontend-dist-pr (0 MB)
  • installer-dmg-darwin-amd64 (21 MB)
  • installer-dmg-darwin-arm64 (19 MB)

How to Download

Option 1: GitHub Web UI (easiest)

  1. Go to the workflow run page linked above
  2. Scroll to the bottom "Artifacts" section
  3. Click on the artifact you want to download

Option 2: GitHub CLI

gh run download 27495449138 --repo smart-mcp-proxy/mcpproxy-go

Note: Artifacts expire in 14 days.

@mcpproxy-gatekeeper mcpproxy-gatekeeper Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Gatekeeper approval — Codex review verdict: ACCEPT.

This approval is posted automatically by the MCPProxy Gatekeeper App on behalf of the Codex reviewer (verdict of record lives in the Paperclip review thread). Author≠approver satisfied; QA + CI gates enforced separately.

Auto-approved per Model B (MCP-1249).

@Dumbris
Dumbris merged commit be6e1ee into main Jun 15, 2026
35 checks passed
Dumbris added a commit that referenced this pull request Jul 2, 2026
…r server

Related #786

Spec 077 US4 (MCP-2207): the security-scan notification storm came from
per-scanner scan_started/progress/completed/failed SSE events multiplied by
reconnect storms (prior partial fixes: #659, MCP-2223). Replace those per-scanner
lifecycle emissions with a single debounced security.scan_settled event per
server per scan.

## Changes
- Add scanNotifyDebouncer (internal/runtime/scan_notify.go): terminal-triggered
  per-server debounce with a generation counter guarding the AfterFunc race;
  only completed/failed arm the timer, started/progress are dropped.
- Add EventTypeSecurityScanSettled; route runtime EmitSecurityScan* through the
  debouncer (started/progress become no-ops, completed/failed record terminal
  state) and publish one settled event carrying the terminal findings summary.
- Wire scanNotify (750ms) into newRuntime alongside the existing coalescer.
- Collapse the activity log to one handleSecurityScanSettled record per scan,
  removing the former started/completed/failed handlers.

## Testing
- scan_notify_test.go: a reconnect storm across N servers yields <= N settled
  events (exactly one per server) and zero per-scanner lifecycle events;
  settled event carries the terminal summary.

Related: Spec 077 (specs/077-scanner-simplification)
Dumbris added a commit that referenced this pull request Jul 2, 2026
… event (Spec 077, MCP-2207) (#794)

* feat(security): debounce scan notifications into one settled event per server

Related #786

Spec 077 US4 (MCP-2207): the security-scan notification storm came from
per-scanner scan_started/progress/completed/failed SSE events multiplied by
reconnect storms (prior partial fixes: #659, MCP-2223). Replace those per-scanner
lifecycle emissions with a single debounced security.scan_settled event per
server per scan.

## Changes
- Add scanNotifyDebouncer (internal/runtime/scan_notify.go): terminal-triggered
  per-server debounce with a generation counter guarding the AfterFunc race;
  only completed/failed arm the timer, started/progress are dropped.
- Add EventTypeSecurityScanSettled; route runtime EmitSecurityScan* through the
  debouncer (started/progress become no-ops, completed/failed record terminal
  state) and publish one settled event carrying the terminal findings summary.
- Wire scanNotify (750ms) into newRuntime alongside the existing coalescer.
- Collapse the activity log to one handleSecurityScanSettled record per scan,
  removing the former started/completed/failed handlers.

## Testing
- scan_notify_test.go: a reconnect storm across N servers yields <= N settled
  events (exactly one per server) and zero per-scanner lifecycle events;
  settled event carries the terminal summary.

Related: Spec 077 (specs/077-scanner-simplification)

* feat(web-ui): consume debounced scan.settled event; drop per-scanner lifecycle

Related #786

Spec 077 US4 (MCP-2207): forward the new security.scan_settled SSE event from the
system store as a mcpproxy:scan-settled window event, and have
useSecurityScannerStatus refresh its cached scan totals off that single settled
signal instead of tracking per-scanner lifecycle events.

## Changes
- stores/system.ts: add a security.scan_settled SSE listener that dispatches
  mcpproxy:scan-settled.
- composables/useSecurityScannerStatus.ts: register a module-scope
  mcpproxy:scan-settled listener that triggers a status refresh.

## Testing
- frontend vue-tsc --noEmit clean; vite build succeeds.

Related: Spec 077 (specs/077-scanner-simplification)
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.

2 participants