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:
1 parent
1870b375b1
commit
83ec590ed8
3 files changed
+71
-6
No files matched your search
@@ -157,6 +157,15 @@ object HermesWsClient {
|
|||||||
/** Observable connection status */
|
/** Observable connection status */
|
||||||
val connectionStatus: StateFlow<ConnectionStatus> = _connectionStatus.asStateFlow()
|
val connectionStatus: StateFlow<ConnectionStatus> = _connectionStatus.asStateFlow()
|
||||||
|
|
||||||
|
// ── Reply-pending tracking ─────────────────────────────────────────
|
||||||
|
// True while at least one agent turn is in flight. Drives the chat
|
||||||
|
// notification foreground service: it only runs while a reply is
|
||||||
|
// actually pending, instead of for the whole time the app is
|
||||||
|
// backgrounded (issue #794).
|
||||||
|
@Volatile
|
||||||
|
var pendingReply: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
// ── Credential warning (issue #534) ─────────────────────────────────
|
// ── Credential warning (issue #534) ─────────────────────────────────
|
||||||
// Backend surfaces `credential_warning` in `gateway.ready` / `session.info`
|
// Backend surfaces `credential_warning` in `gateway.ready` / `session.info`
|
||||||
// WS payloads (desktop `requestDesktopOnboarding`). Mobile has no equivalent
|
// WS payloads (desktop `requestDesktopOnboarding`). Mobile has no equivalent
|
||||||
@@ -207,6 +216,25 @@ object HermesWsClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Track whether a reply is actually in flight so the chat notification
|
||||||
|
// foreground service only runs while the user is waiting for one
|
||||||
|
// (issue #794), instead of for the whole time the app is backgrounded.
|
||||||
|
// Also covers turns started from other devices on the same session.
|
||||||
|
wsScope.launch {
|
||||||
|
events.collect { event ->
|
||||||
|
when (event) {
|
||||||
|
is WsEvent.MessageStart,
|
||||||
|
is WsEvent.MessageToken,
|
||||||
|
is WsEvent.ThinkingDelta,
|
||||||
|
is WsEvent.ReasoningDelta,
|
||||||
|
is WsEvent.ToolStart,
|
||||||
|
-> pendingReply = true
|
||||||
|
|
||||||
|
is WsEvent.MessageComplete -> pendingReply = false
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Connection helpers ────────────────────────────────────────────────
|
// ── Connection helpers ────────────────────────────────────────────────
|
||||||
@@ -584,12 +612,17 @@ object HermesWsClient {
|
|||||||
sessionId: String,
|
sessionId: String,
|
||||||
text: String,
|
text: String,
|
||||||
onSent: ((String) -> Unit)? = null,
|
onSent: ((String) -> Unit)? = null,
|
||||||
): String =
|
): String {
|
||||||
send(
|
// A reply is now pending — even before the first MessageStart event
|
||||||
|
// (the agent may be in its tool phase), so backgrounding the app in
|
||||||
|
// that window still arms the notification service (issue #794).
|
||||||
|
pendingReply = true
|
||||||
|
return send(
|
||||||
method = WsMethods.PROMPT_SUBMIT,
|
method = WsMethods.PROMPT_SUBMIT,
|
||||||
params = mapOf("session_id" to sessionId, "text" to text),
|
params = mapOf("session_id" to sessionId, "text" to text),
|
||||||
onSent = onSent,
|
onSent = onSent,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Convenience: redirect the active model turn while it is still generating
|
* Convenience: redirect the active model turn while it is still generating
|
||||||
|
|||||||
@@ -42,11 +42,16 @@ import java.util.concurrent.atomic.AtomicBoolean
|
|||||||
* message notifications, which is correct.
|
* message notifications, which is correct.
|
||||||
*
|
*
|
||||||
* Lifecycle:
|
* Lifecycle:
|
||||||
* - Started by [NotificationHelper.start] when the user sends a message and
|
* - Started by [NotificationHelper.start] when the app goes to the
|
||||||
* the app is about to go to the background (called from ChatScreen's
|
* background while a reply is still pending (the user sent a message or
|
||||||
* onStop / onPause).
|
* 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
|
* - 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 service collects [WsEvent]s from [HermesWsClient] — the same stream
|
||||||
* the ChatViewModel collects — and watches for [WsEvent.MessageComplete]
|
* the ChatViewModel collects — and watches for [WsEvent.MessageComplete]
|
||||||
@@ -64,6 +69,8 @@ class ChatNotificationService : Service() {
|
|||||||
fun setAppForeground(foreground: Boolean) {
|
fun setAppForeground(foreground: Boolean) {
|
||||||
isAppInForeground.set(foreground)
|
isAppInForeground.set(foreground)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isAppInForeground(): Boolean = isAppInForeground.get()
|
||||||
}
|
}
|
||||||
|
|
||||||
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
private val serviceScope = CoroutineScope(Dispatchers.IO + SupervisorJob())
|
||||||
@@ -92,6 +99,15 @@ class ChatNotificationService : Service() {
|
|||||||
.replace("\n", " ")
|
.replace("\n", " ")
|
||||||
.ifBlank { getString(R.string.notif_new_message) }
|
.ifBlank { getString(R.string.notif_new_message) }
|
||||||
showReplyNotification(preview, event.sessionId)
|
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 -> {
|
is WsEvent.ClarifyRequest -> {
|
||||||
@@ -235,6 +251,11 @@ class ChatNotificationService : Service() {
|
|||||||
object NotificationHelper {
|
object NotificationHelper {
|
||||||
fun start(context: Context) {
|
fun start(context: Context) {
|
||||||
if (AuthManager.getToken().isNullOrBlank()) return
|
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)
|
val intent = Intent(context, ChatNotificationService::class.java)
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
context.startForegroundService(intent)
|
context.startForegroundService(intent)
|
||||||
@@ -253,4 +274,6 @@ object NotificationHelper {
|
|||||||
) {
|
) {
|
||||||
ChatNotificationService.setAppForeground(foreground)
|
ChatNotificationService.setAppForeground(foreground)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isAppInForeground(): Boolean = ChatNotificationService.isAppInForeground()
|
||||||
}
|
}
|
||||||
@@ -92,6 +92,15 @@ open class NotificationReplyReceiver : BroadcastReceiver() {
|
|||||||
val repliedNotification = buildReplyNotification(context)
|
val repliedNotification = buildReplyNotification(context)
|
||||||
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
manager.notify(ChatNotificationService.PENDING_NOTIFICATION_ID, repliedNotification)
|
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) {
|
} catch (e: Exception) {
|
||||||
|
|||||||
Reference in new issue
Block a user