fix FCM push notifications
CI / Gateway plugin tests (push) Failing after 6m27s
CI / Kotlin tests (android host + desktop) (push) Successful in 7m2s

This commit is contained in:
ARIA committed 2026-08-23 14:32:40 +02:00
1 parent 560d9c19b3
commit d801a18db5
4 files changed
+177 -6

No files matched your search

+60 -2
View File
@@ -1360,6 +1360,16 @@ class IrisAdapter(BasePlatformAdapter):
# M5: per-chat push throttle (epoch seconds of the last successful
# push). The coalesced frame still reaches the app via sync.
self._last_push_at: dict[str, float] = {}
# M5: turn-aware push. chat_ids whose agent turn is in flight, per
# the typing indicator (hermes turns typing on at turn start and off
# in the handler's finally at turn end). While a turn is active and
# the device is offline, pushable message/media frames are held back
# in _pending_push instead of pushing per intermediate status
# message; the latest one is pushed when the turn ends, so an
# offline user gets ONE push with the final answer. High-priority
# notifications (approval/clarify/cron) always push immediately.
self._typing_turns: set[str] = set()
self._pending_push: dict[str, tuple[protocol.Frame, int]] = {}
# Live todo lists (lane key -> items): the agent's planning state,
# re-served as a snapshot when a device opens its event stream.
# In-memory only — a gateway restart drops it (the agent re-emits on
@@ -1948,8 +1958,27 @@ class IrisAdapter(BasePlatformAdapter):
frame.type,
cursor,
)
# M5: wake the offline device(s) via the push backend.
await self._maybe_push(chat_id, frame, cursor)
# M5: wake the offline device(s) via the push backend -- unless
# this is a normal-priority message/media frame while the
# agent's turn is still in flight: hold it back and push the
# latest one when the turn ends (stop_typing), so an offline
# user gets one push with the final answer instead of one per
# intermediate status message. High-priority notifications
# (approval/clarify/cron) still push immediately.
if chat_id in self._typing_turns and frame.type in (
protocol.TYPE_MESSAGE,
protocol.TYPE_MESSAGE_STOP,
protocol.TYPE_MEDIA_OFFER,
):
self._pending_push[chat_id] = (frame, cursor)
logger.info(
"iris: push deferred for %s (turn active; %s frame cursor=%s)",
chat_id,
frame.type,
cursor,
)
else:
await self._maybe_push(chat_id, frame, cursor)
await self._maybe_notify_outbox_prune(chat_id)
elif (
frame.type == protocol.TYPE_NOTIFICATION
@@ -2099,6 +2128,9 @@ class IrisAdapter(BasePlatformAdapter):
tid = metadata.get("thread_id")
if isinstance(tid, str) and tid:
thread_id = tid
# M5: typing on marks the chat's agent turn as in flight (hermes
# turns typing on at turn start, before the first output).
self._typing_turns.add(chat_id)
frame = protocol.typing(chat_id, True, thread_id=thread_id)
await self._http_server.fanout(frame, cursor=None)
@@ -2106,6 +2138,32 @@ class IrisAdapter(BasePlatformAdapter):
"""Clear the typing indicator (``typing`` frame, on=false)."""
frame = protocol.typing(chat_id, False)
await self._http_server.fanout(frame, cursor=None)
# M5: turn ended (hermes fires stop_typing in the handler's finally,
# after the final send). Flush the held-back push -- the final
# answer -- but only while the device is still offline; a live
# device already got the frames via its event stream / sync.
# Idempotent: hermes may call stop_typing more than once per turn.
self._typing_turns.discard(chat_id)
pending = self._pending_push.pop(chat_id, None)
if pending is None:
return
if self._http_server.has_devices():
logger.info("iris: deferred push dropped for %s (device back online)", chat_id)
return
held_frame, held_cursor = pending
await self._maybe_push(chat_id, held_frame, held_cursor)
def on_device_online(self) -> None:
"""A device opened its event stream (SSE/long-poll): it will sync
the outbox, so drop any held-back pushes -- flushing them later
would duplicate what the app already shows. Called from the HTTP
server's handler thread; dict.clear() is atomic under the GIL."""
if self._pending_push:
logger.info(
"iris: device online; dropping %d deferred push(es)",
len(self._pending_push),
)
self._pending_push.clear()
# ── M4: outbound media (agent -> app) ─────────────────────────────────
#