Push notification dedupe: one message = one notification — an offline message was notified twice (FCM push, then again when the app synced the outbox and mirrored the replayed frames). Fix: the gateway records the highest outbox cursor delivered per device via push (devices.last_pushed_cursor, advanced only on successful send) and returns it in hello.ack; sync-replayed frames carry their outbox cursor in the envelope; the app skips system notifications for replayed frames at/below the watermark (live frames never suppressed — that is the case where no push fired). Also: 5s per-chat push coalescing so a cron delivery (notification frame + message frame) pushes once, and the FCM handler no longer posts a redundant notification (skips when WS is connected or FCM already displayed the notification payload; data-only messages are the exception). Docs: frames.schema.json, 04-wire-protocol.md, 08-push.md §8.8
This commit is contained in:
1 parent
acd5fb4ad0
commit
9f3f9842c8
11 files changed
+301
-98
No files matched your search
@@ -439,6 +439,11 @@ def _strip_streaming_cursor(text: str) -> str:
|
||||
return text
|
||||
|
||||
|
||||
# M5: coalesce back-to-back pushes for the same chat (a cron delivery parks
|
||||
# a notification frame AND a message frame; only the first should push).
|
||||
_PUSH_COALESCE_S = 5.0
|
||||
|
||||
|
||||
def _push_preview(text: Any, limit: int = 120) -> str:
|
||||
"""Short single-line preview for push bodies (lock-screen privacy: no
|
||||
secrets, no full bodies -- full content arrives via ``sync``)."""
|
||||
@@ -1019,6 +1024,9 @@ class AndroidAdapter(BasePlatformAdapter):
|
||||
ntfy_auth_token=_get_scoped_secret("NTFY_AUTH_TOKEN"),
|
||||
)
|
||||
self._prune_notified_at = 0.0
|
||||
# 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] = {}
|
||||
|
||||
def _turn_state(self, chat_id: str) -> _TurnState:
|
||||
st = self._turns.get(chat_id)
|
||||
@@ -1537,6 +1545,16 @@ class AndroidAdapter(BasePlatformAdapter):
|
||||
summary = self._push_summary(frame)
|
||||
if summary is None:
|
||||
return
|
||||
# M5: coalesce back-to-back pushes for the same chat (cron delivery
|
||||
# = notification frame + message frame). The suppressed frame is
|
||||
# still synced when the app reconnects.
|
||||
now = time.time()
|
||||
if now - self._last_push_at.get(chat_id, 0.0) < _PUSH_COALESCE_S:
|
||||
logger.info(
|
||||
"android: push coalesced for %s (%s frame within %.0fs of last push)",
|
||||
chat_id, frame.type, _PUSH_COALESCE_S,
|
||||
)
|
||||
return
|
||||
title, body, kind, priority = summary
|
||||
backend = self._push
|
||||
if backend is None or not backend.token_field:
|
||||
@@ -1578,6 +1596,15 @@ class AndroidAdapter(BasePlatformAdapter):
|
||||
logger.warning("android: push via %s failed", backend.name, exc_info=True)
|
||||
continue
|
||||
if ok:
|
||||
# M5: remember that this cursor reached the device via push,
|
||||
# so the app can dedupe it on the next sync replay.
|
||||
try:
|
||||
self._devices.update_push_cursor(device_id, cursor)
|
||||
except Exception:
|
||||
logger.warning(
|
||||
"android: push cursor update failed for %s", device_id, exc_info=True
|
||||
)
|
||||
self._last_push_at[chat_id] = time.time()
|
||||
logger.info(
|
||||
"android: push via %s -> %s (%s, chat=%s)",
|
||||
backend.name, device_id, frame.type, chat_id,
|
||||
@@ -2362,6 +2389,10 @@ class AndroidAdapter(BasePlatformAdapter):
|
||||
id=raw.get("id") if isinstance(raw.get("id"), int) else None,
|
||||
chat_id=raw.get("chat_id") if isinstance(raw.get("chat_id"), str) else e.get("chat_id"),
|
||||
thread_id=raw.get("thread_id") if isinstance(raw.get("thread_id"), str) else None,
|
||||
# M5: tag replayed frames with their outbox cursor so the app
|
||||
# can skip re-notifying frames that already woke the device
|
||||
# via push (cursor <= last_pushed_cursor, docs/08 §8.7).
|
||||
cursor=e.get("cursor"),
|
||||
v=raw.get("v") if isinstance(raw.get("v"), int) else protocol.PROTOCOL_VERSION,
|
||||
)
|
||||
await self._ws_server.send_to(device_id, replayed)
|
||||
|
||||
Reference in new issue
Block a user