@@ -130,6 +130,8 @@ export function createSessionNotification(
130130 const sessionActivitySinceIdle = new Set < string > ( )
131131 // Track notification execution version to handle race conditions
132132 const notificationVersions = new Map < string , number > ( )
133+ // Track sessions currently executing notification (prevents duplicate execution)
134+ const executingNotifications = new Set < string > ( )
133135
134136 function cleanupOldSessions ( ) {
135137 const maxSessions = mergedConfig . maxTrackedSessions
@@ -145,6 +147,10 @@ export function createSessionNotification(
145147 const sessionsToRemove = Array . from ( notificationVersions . keys ( ) ) . slice ( 0 , notificationVersions . size - maxSessions )
146148 sessionsToRemove . forEach ( id => notificationVersions . delete ( id ) )
147149 }
150+ if ( executingNotifications . size > maxSessions ) {
151+ const sessionsToRemove = Array . from ( executingNotifications ) . slice ( 0 , executingNotifications . size - maxSessions )
152+ sessionsToRemove . forEach ( id => executingNotifications . delete ( id ) )
153+ }
148154 }
149155
150156 function cancelPendingNotification ( sessionID : string ) {
@@ -164,42 +170,57 @@ export function createSessionNotification(
164170 }
165171
166172 async function executeNotification ( sessionID : string , version : number ) {
167- pendingTimers . delete ( sessionID )
173+ if ( executingNotifications . has ( sessionID ) ) {
174+ pendingTimers . delete ( sessionID )
175+ return
176+ }
168177
169- // Race condition fix: check if version matches (activity happened during async wait)
170178 if ( notificationVersions . get ( sessionID ) !== version ) {
179+ pendingTimers . delete ( sessionID )
171180 return
172181 }
173182
174183 if ( sessionActivitySinceIdle . has ( sessionID ) ) {
175184 sessionActivitySinceIdle . delete ( sessionID )
185+ pendingTimers . delete ( sessionID )
176186 return
177187 }
178188
179- if ( notifiedSessions . has ( sessionID ) ) return
189+ if ( notifiedSessions . has ( sessionID ) ) {
190+ pendingTimers . delete ( sessionID )
191+ return
192+ }
193+
194+ executingNotifications . add ( sessionID )
195+ try {
196+ if ( mergedConfig . skipIfIncompleteTodos ) {
197+ const hasPendingWork = await hasIncompleteTodos ( ctx , sessionID )
198+ if ( notificationVersions . get ( sessionID ) !== version ) {
199+ return
200+ }
201+ if ( hasPendingWork ) return
202+ }
180203
181- if ( mergedConfig . skipIfIncompleteTodos ) {
182- const hasPendingWork = await hasIncompleteTodos ( ctx , sessionID )
183- // Re-check version after async call (race condition fix)
184204 if ( notificationVersions . get ( sessionID ) !== version ) {
185205 return
186206 }
187- if ( hasPendingWork ) return
188- }
189207
190- if ( notificationVersions . get ( sessionID ) !== version ) {
191- return
192- }
208+ if ( sessionActivitySinceIdle . has ( sessionID ) ) {
209+ sessionActivitySinceIdle . delete ( sessionID )
210+ return
211+ }
193212
194- notifiedSessions . add ( sessionID )
213+ notifiedSessions . add ( sessionID )
195214
196- try {
197215 await sendNotification ( ctx , currentPlatform , mergedConfig . title , mergedConfig . message )
198216
199217 if ( mergedConfig . playSound && mergedConfig . soundPath ) {
200218 await playSound ( ctx , currentPlatform , mergedConfig . soundPath )
201219 }
202- } catch { }
220+ } finally {
221+ executingNotifications . delete ( sessionID )
222+ pendingTimers . delete ( sessionID )
223+ }
203224 }
204225
205226 return async ( { event } : { event : { type : string ; properties ?: unknown } } ) => {
@@ -224,6 +245,7 @@ export function createSessionNotification(
224245
225246 if ( notifiedSessions . has ( sessionID ) ) return
226247 if ( pendingTimers . has ( sessionID ) ) return
248+ if ( executingNotifications . has ( sessionID ) ) return
227249
228250 sessionActivitySinceIdle . delete ( sessionID )
229251
@@ -263,6 +285,7 @@ export function createSessionNotification(
263285 notifiedSessions . delete ( sessionInfo . id )
264286 sessionActivitySinceIdle . delete ( sessionInfo . id )
265287 notificationVersions . delete ( sessionInfo . id )
288+ executingNotifications . delete ( sessionInfo . id )
266289 }
267290 }
268291 }
0 commit comments