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

+37 -4
View File
@@ -5,13 +5,44 @@ relay. **Decision: FCM primary, ntfy fallback** (`IRIS_PUSH_BACKEND`).
## 8.1 When push fires
- A frame targets a `chat_id` whose device is **disconnected** (WS closed) →
drop to **outbox** + fire **push**.
- A frame targets a `chat_id` whose device is **disconnected** (no live
SSE/long-poll subscriber) → drop to **outbox** + fire **push** — with one
refinement, *turn-aware push* (below).
- Also fire push for high-priority foreground events the user should see even if
the app is backgrounded (approvals, clarifies, cron completions) — the app
decides whether to also show an in-app banner.
- If the device is **connected**, no push (the live frame is enough).
### 8.1.1 Turn-aware push (one push per turn, final answer as body)
An agent turn can span minutes and emit several completed status messages
("researching X…", "found Y…", "writing findings…", final answer). Pushing each
parked message would spam an offline user with the steps in between. So while
the agent's turn is **in flight** (hermes holds the typing indicator on from
turn start until the handler's `finally` at turn end), normal-priority
`message` / `message.stop` / `media.offer` frames that park with no live
device are **held back** per chat instead of pushing; the latest one is pushed
when the turn ends (`stop_typing`), so the offline user gets **one push with
the final answer**. Details:
- Turn state is tracked per `chat_id` from the typing indicator
(`send_typing` → in flight, `stop_typing` → ended; hermes fires
`stop_typing` in the handler's `finally`, after the final send, so the
flush always sees the final frame).
- The held-back frame is still parked in the outbox — sync catch-up is
unaffected; only the push is deferred.
- **High-priority notifications** (approval/clarify/cron) push immediately,
even mid-turn — they need user action.
- If the device **reconnects mid-turn** (SSE/long-poll open), the held-back
push is dropped: the app syncs the parked frames and must not get a
duplicate push at turn end.
- If the turn ends while the device is live, nothing is pushed (the frames
were delivered live / synced).
- Turns without a typing indicator (e.g. typing disabled in config) and
non-turn deliveries (cron, standalone sends) push immediately as before.
- Best-effort: a gateway crash mid-turn loses the held-back push (the frames
remain in the outbox and sync on reconnect).
## 8.2 `PushBackend` interface (`push.py`)
```python
@@ -26,9 +57,10 @@ class PushBackend(Protocol):
Selected at adapter init by `IRIS_PUSH_BACKEND` (`fcm` default, `ntfy`).
### 8.2.1 `FcmBackend` (primary)
- **FCM HTTP v1 API** via `httpx` (core dep). Auth = Firebase **service
account** (`IRIS_FCM_SERVICE_ACCOUNT` JSON path) → mint a short-lived
OAuth2 access token (cached, refreshed before expiry).
OAuth2 access token (cached, refreshed before expiry).
- Fallback: legacy **server key** (`IRIS_FCM_SERVER_KEY`) if no service
account (simpler, but legacy).
- Target = the device's **FCM token** (registered via `hello` /
@@ -43,6 +75,7 @@ Selected at adapter init by `IRIS_PUSH_BACKEND` (`fcm` default, `ntfy`).
devices).
### 8.2.2 `NtfyBackend` (fallback, self-host friendly)
- Reuses hermes's existing ntfy publish path (hermes ships an ntfy adapter).
- Publish to `NTFY_TOPIC` on `NTFY_SERVER_URL` (default `https://ntfy.sh`) via
`httpx` POST, with an `X-Title` / `X-Message` / `X-Tag` / `X-Priority` and a
@@ -132,4 +165,4 @@ device push watermark:
Residual edge: FCM is at-least-once, so a lost device ack can still produce a
duplicate *system-displayed* notification (two `FCM-Notification:*` ids). The
designed evolution is the data-only push option (§8.2.1), which moves display
into the app and lets it use a stable per-message notification id.
into the app and lets it use a stable per-message notification id.