@@ -464,7 +464,9 @@ void Invoke(Action action)
464464 try
465465 {
466466 var currentSettings = ConnectionSettings . Load ( ) ;
467- if ( ! currentSettings . EnableSessionNotifications ) return ;
467+ var notifyGlobal = currentSettings . EnableSessionNotifications ;
468+ var notifySession = state . Info . NotifyOnComplete ;
469+ if ( ! notifyGlobal && ! notifySession ) return ;
468470 var notifService = _serviceProvider ? . GetService < INotificationManagerService > ( ) ;
469471 if ( notifService == null || ! notifService . HasPermission ) return ;
470472 var lastMsg = state . Info . History . LastOrDefault ( m => m . Role == "assistant" ) ;
@@ -1366,6 +1368,11 @@ private async Task RunProcessingWatchdogAsync(SessionState state, string session
13661368 : 0 ;
13671369 var exceededMaxTime = totalProcessingSeconds >= WatchdogMaxProcessingTimeSeconds ;
13681370
1371+ // Send periodic "still running" reminder if configured — load settings once per check
1372+ // (not inside the helper) to avoid redundant disk reads per watchdog iteration.
1373+ var watchdogSettings = ConnectionSettings . Load ( ) ;
1374+ _ = SendReminderNotificationIfDueAsync ( state , sessionName , totalProcessingSeconds , watchdogSettings . NotificationReminderIntervalMinutes ) ;
1375+
13691376 if ( elapsed >= effectiveTimeout || exceededMaxTime )
13701377 {
13711378 var timeoutDisplay = exceededMaxTime
@@ -1421,4 +1428,39 @@ private async Task RunProcessingWatchdogAsync(SessionState state, string session
14211428 catch ( OperationCanceledException ) { /* Normal cancellation when response completes */ }
14221429 catch ( Exception ex ) { Debug ( $ "Watchdog error for '{ sessionName } ': { ex . Message } ") ; }
14231430 }
1431+
1432+ /// <summary>
1433+ /// Fires a "still running" reminder notification if the configured interval has elapsed
1434+ /// since the last reminder. Safe to call from the watchdog background thread.
1435+ /// </summary>
1436+ private async Task SendReminderNotificationIfDueAsync ( SessionState state , string sessionName , double totalProcessingSeconds , int intervalMinutes )
1437+ {
1438+ try
1439+ {
1440+ if ( intervalMinutes <= 0 ) return ;
1441+ var notifService = _serviceProvider ? . GetService < INotificationManagerService > ( ) ;
1442+ if ( notifService == null || ! notifService . HasPermission ) return ;
1443+
1444+ var elapsedMinutes = ( int ) ( totalProcessingSeconds / 60 ) ;
1445+ if ( elapsedMinutes < intervalMinutes ) return ;
1446+
1447+ // Compute how many complete intervals have elapsed
1448+ var intervalsDone = elapsedMinutes / intervalMinutes ;
1449+ var lastSent = Volatile . Read ( ref state . LastReminderSentAtMinutes ) ;
1450+ // Only send once per interval window
1451+ if ( intervalsDone <= lastSent ) return ;
1452+
1453+ // Atomically claim this interval so concurrent checks don't double-fire
1454+ if ( Interlocked . CompareExchange ( ref state . LastReminderSentAtMinutes , intervalsDone , lastSent ) != lastSent ) return ;
1455+
1456+ var elapsed = elapsedMinutes >= 60
1457+ ? $ "{ elapsedMinutes / 60 } h { elapsedMinutes % 60 } m"
1458+ : $ "{ elapsedMinutes } m";
1459+ await notifService . SendNotificationAsync (
1460+ sessionName ,
1461+ $ "⏱ Still running · { elapsed } elapsed",
1462+ state . Info . SessionId ) ;
1463+ }
1464+ catch ( Exception ex ) { Debug ( $ "Reminder notification failed for '{ sessionName } ': { ex . Message } ") ; }
1465+ }
14241466}
0 commit comments