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:
ARIA committed 2026-08-21 17:21:54 +02:00
1 parent acd5fb4ad0
commit 9f3f9842c8
11 files changed
+301 -98

No files matched your search

+2
View File
@@ -12,6 +12,7 @@
"type": { "type": "string", "description": "Frame type (see frame_types)." },
"chat_id": { "type": "string", "description": "Optional chat scope (e.g. android:default, android:chan_7)." },
"thread_id": { "type": "string", "description": "Optional thread scope within a chat_id." },
"cursor": { "type": "integer", "description": "Outbox cursor the frame was parked under. Present ONLY on frames replayed by sync (live frames carry none). The app skips re-notifying replayed frames with cursor <= last_pushed_cursor (docs/08 §8.7)." },
"payload": { "type": "object", "description": "Type-specific payload." }
}
},
@@ -22,6 +23,7 @@
"payload": {
"server_caps": { "type": "object", "properties": { "streaming": {"type":"boolean"}, "reasoning": {"type":"boolean"}, "tools": {"type":"boolean"}, "media": {"type":"boolean"}, "search": {"type":"boolean"}, "push": {"type":"string","enum":["fcm","ntfy","none"]}, "push_ntfy_server": {"type":"string","description":"ntfy server URL for the app's listener; empty string when the backend is not ntfy."}, "pickers": {"type":"boolean"} } },
"sync_cursor": { "type": "integer" },
"last_pushed_cursor": { "type": "integer", "description": "Highest outbox cursor already delivered to THIS device via the push backend (0 = never). The app skips system notifications for sync-replayed frames at/below it (dedupe, docs/08 §8.7)." },
"channels": { "type": "array", "items": { "$ref": "#/definitions/channel" } }
}
},