From 702cef384c673f82e1ab6b648ddd56d2096776cb Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 31 May 2026 07:43:29 -0700 Subject: [PATCH] perf: bound notification cooldown tracking (#4194) --- src/main/ipc/notifications.test.ts | 32 +++++++++++++++++++++++++++++ src/main/ipc/notifications.ts | 33 ++++++++++++++++++++++-------- 2 files changed, 57 insertions(+), 8 deletions(-) diff --git a/src/main/ipc/notifications.test.ts b/src/main/ipc/notifications.test.ts index 1fedb2a7a21..e2785ce73f5 100644 --- a/src/main/ipc/notifications.test.ts +++ b/src/main/ipc/notifications.test.ts @@ -965,6 +965,38 @@ describe('registerNotificationHandlers', () => { expect(notificationShowMock).toHaveBeenCalledTimes(2) }) + it('bounds notification cooldown keys during unique worktree bursts', () => { + notificationIsSupportedMock.mockReturnValue(false) + registerNotificationHandlers({ + getSettings: () => ({ + notifications: { + enabled: true, + agentTaskComplete: true, + terminalBell: true, + suppressWhenFocused: false + } + }) + } as never) + + const handler = getDispatchHandler() + for (let i = 0; i < 75; i++) { + expect(handler({}, { source: 'terminal-bell', worktreeId: `repo::wt-${i}` })).toEqual({ + delivered: false, + reason: 'not-supported' + }) + } + + expect(handler({}, { source: 'terminal-bell', worktreeId: 'repo::wt-0' })).toEqual({ + delivered: false, + reason: 'not-supported' + }) + expect(handler({}, { source: 'terminal-bell', worktreeId: 'repo::wt-74' })).toEqual({ + delivered: false, + reason: 'cooldown' + }) + expect(notificationCtorMock).not.toHaveBeenCalled() + }) + it('deduplicates agent-task-complete and terminal-bell for the same worktree', () => { registerNotificationHandlers({ getSettings: () => ({ diff --git a/src/main/ipc/notifications.ts b/src/main/ipc/notifications.ts index a7b6e95029e..f35be06f73f 100644 --- a/src/main/ipc/notifications.ts +++ b/src/main/ipc/notifications.ts @@ -25,6 +25,7 @@ import { buildNotificationOptions } from './notification-options' import { parsePaneKey } from '../../shared/stable-pane-id' const NOTIFICATION_COOLDOWN_MS = 5000 +const MAX_RECENT_NOTIFICATION_KEYS = 50 const NOTIFICATION_DISPLAY_CONFIRMATION_TIMEOUT_MS = 2500 const NOTIFICATION_RELEASE_FALLBACK_MS = 5 * 60 * 1000 const MAX_NOTIFICATION_SOUND_BYTES = 10 * 1024 * 1024 @@ -175,6 +176,26 @@ function logNativeNotificationFailure(context: string, error?: string): void { ) } +function pruneRecentNotifications(recentNotifications: Map, now: number): void { + if (recentNotifications.size <= MAX_RECENT_NOTIFICATION_KEYS) { + return + } + + for (const [key, ts] of recentNotifications) { + if (now - ts >= NOTIFICATION_COOLDOWN_MS) { + recentNotifications.delete(key) + } + } + + while (recentNotifications.size > MAX_RECENT_NOTIFICATION_KEYS) { + const oldest = recentNotifications.keys().next() + if (oldest.done) { + break + } + recentNotifications.delete(oldest.value) + } +} + export function registerNotificationHandlers(store: Store, runtime?: OrcaRuntimeService): void { const recentNotifications = new Map() @@ -245,16 +266,12 @@ export function registerNotificationHandlers(store: Store, runtime?: OrcaRuntime if (now - lastSentAt < NOTIFICATION_COOLDOWN_MS) { return { delivered: false, reason: 'cooldown' } } + recentNotifications.delete(dedupeKey) recentNotifications.set(dedupeKey, now) - // Evict stale entries so the map doesn't grow unbounded. - if (recentNotifications.size > 50) { - for (const [key, ts] of recentNotifications) { - if (now - ts >= NOTIFICATION_COOLDOWN_MS) { - recentNotifications.delete(key) - } - } - } + // Why: a storm across many worktrees should not make every + // notification dispatch scan an ever-growing cooldown table. + pruneRecentNotifications(recentNotifications, now) } const notificationOptions = buildNotificationOptions(args)