fix(notifications): only run waiting-reply foreground service while a reply is pending (#795)

The chat notification FGS — and its mandatory persistent 'Waiting for
Hermes replies' notification — was started on every ON_STOP of the chat
screen, even when nothing was in flight, and lingered at app startup.

Track a pending-reply flag on HermesWsClient (set when a prompt is
submitted and on any in-flight turn event, cleared on MessageComplete),
gate NotificationHelper.start() on it, and have the service retire
itself after delivering the reply notification in the background.
Replies sent from the notification re-arm the service for the
follow-up turn.

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-08-04 16:59:42 +03:00
1 parent 1870b375b1
commit 83ec590ed8
3 files changed
+71 -6

No files matched your search

@@ -42,11 +42,16 @@ import java.util.concurrent.atomic.AtomicBoolean
* message notifications, which is correct.
*
* Lifecycle:
* - Started by [NotificationHelper.start] when the user sends a message and
* the app is about to go to the background (called from ChatScreen's
* onStop / onPause).
* - Started by [NotificationHelper.start] when the app goes to the
* background while a reply is still pending (the user sent a message or
* replied from a notification and the agent has not finished yet).
* [NotificationHelper.start] is a no-op when nothing is pending, so the
* service — and its mandatory persistent notification — only exists
* while the user is actually waiting for a reply (issue #794).
* - Stopped by [NotificationHelper.stop] when the app returns to the
* foreground (called from ChatScreen's onStart / onResume).
* foreground (called from ChatScreen's onStart / onResume), or by the
* service itself once the pending reply completes in the background
* (the reply notification replaces the persistent "waiting" one).
*
* The service collects [WsEvent]s from [HermesWsClient] — the same stream
* the ChatViewModel collects — and watches for [WsEvent.MessageComplete]
@@ -64,6 +69,8 @@ class ChatNotificationService : Service() {
fun setAppForeground(foreground: Boolean) {
isAppInForeground.set(foreground)
}
fun isAppInForeground(): Boolean = isAppInForeground.get()
}
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
@@ -92,6 +99,15 @@ class ChatNotificationService : Service() {
.replace("\n", " ")
.ifBlank { getString(R.string.notif_new_message) }
showReplyNotification(preview, event.sessionId)
// The wait is over — retire the foreground
// service. The reply notification above
// replaces the persistent "waiting" one,
// and the pendingReply flag is cleared by
// HermesWsClient's own collector, so the
// service is not restarted on the next
// ON_STOP (issue #794).
stopForeground(STOP_FOREGROUND_REMOVE)
stopSelf()
}
is WsEvent.ClarifyRequest -> {
@@ -235,6 +251,11 @@ class ChatNotificationService : Service() {
object NotificationHelper {
fun start(context: Context) {
if (AuthManager.getToken().isNullOrBlank()) return
// Only run the foreground service while a reply is actually pending
// (issue #794) — otherwise the mandatory persistent "Waiting for
// Hermes replies" notification appears on every background even
// when nothing is in flight.
if (!HermesWsClient.pendingReply) return
val intent = Intent(context, ChatNotificationService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.startForegroundService(intent)
@@ -253,4 +274,6 @@ object NotificationHelper {
) {
ChatNotificationService.setAppForeground(foreground)
}
fun isAppInForeground(): Boolean = ChatNotificationService.isAppInForeground()
}
@@ -92,6 +92,15 @@ open class NotificationReplyReceiver : BroadcastReceiver() {
val repliedNotification = buildReplyNotification(context)
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
manager.notify(ChatNotificationService.PENDING_NOTIFICATION_ID, repliedNotification)
// The follow-up turn is now pending — keep the
// foreground service alive if it retired after the
// previous reply completed, so the next
// MessageComplete still notifies while backgrounded
// (issue #794). No-op when the app is foreground.
if (!NotificationHelper.isAppInForeground()) {
NotificationHelper.start(context)
}
}
}
} catch (e: Exception) {