Skip to content

feat: a fork server can be the Android push host for itself and linked environments - #45

Merged
tebiggs merged 18 commits into
mainfrom
feature/fork-push-host
Nov 13, 2026
Merged

tebiggs merged 18 commits into
mainfrom
feature/fork-push-host

Conversation

@tebiggs

@tebiggs tebiggs commented Nov 12, 2026

Copy link
Copy Markdown
Owner

The fork's Android app gets no notifications. Its FCM tokens belong to the fork's own Firebase project, and upstream's relay can only send as upstream's project.

This makes one T3 server the push host. A server started with T3CODE_FCM_SERVICE_ACCOUNT_FILE sends the app its alerts and ongoing activity card, for itself and for every environment linked to it. It reuses upstream's relay payload and alert code from infra/relay, so the app's native module and alert rules are unchanged.

  • Host: apps/server/src/fork/push/ stores devices and the latest thread rows in a JSON file under the state directory. Each device gets its own delivery fiber that retries FCM failures, with at most one pending entry per thread.
  • Peers: a server with a link to the host publishes over POST /api/fork/push/v1/…/agent-activity, using the link's token. Over T3 Connect this works off the LAN. With no key and no host link, AgentAwarenessRelay behaves as upstream.
  • App: the registrar finds the host among connected environments, configures the native module with a fork identity, and registers the FCM token. Upstream's Clerk configure and clear calls are skipped while the fork owns push. Settings → Notifications shows the host and each environment's link state, and has Send test and Stop buttons.
  • Docs: docs/user/push-host.md.

Upstream files with small hunks: AgentAwarenessRelay.ts, server.ts, ws.ts, rpc.ts, clientRpcPermissions.ts, RpcAuthorization.ts, androidNotifications.ts, and the mobile settings screen and app root.

Verified end to end up to FCM with two linked dev servers and T3CODE_FCM_VALIDATE_ONLY=1. A phone test with the rebranded APK from #32 is still to do. Four rounds of independent review by GPT Sol (Codex); the last one approved 163a47a with no findings.

Built with Claude Opus in Claude Code (T3 Code), reviewed by GPT Sol in Codex.

🤖 Generated with Claude Code

tebiggs and others added 18 commits November 12, 2026 09:59
…M key

The fork's Android app uses its own Firebase project, so upstream's relay
cannot reach it. A server started with T3CODE_FCM_SERVICE_ACCOUNT_FILE now
keeps the latest agent activity, a device registry and its peers in one JSON
file, and sends devices the same FCM payloads upstream's relay sends. It
reuses the relay's aggregate, alert, payload and FCM client code from
infra/relay by relative import; the two alert functions that live beside the
relay's database are copied, with a test that compares them to upstream's.
T3CODE_FCM_VALIDATE_ONLY makes FCM check every message without delivering it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…host

Each server looks among its linked environments for one that answers as a
push host, and AgentAwarenessRelay then publishes there with that link's
credential instead of to the T3 Connect relay, keeping its retries. The
push host publishes its own threads in process. Its routes take updates
only from a linked environment's session and refuse its own environment.
A new or lost route republishes active threads.

Publishing to the push host respects an explicit off in "Publish agent
activity", and runs when the toggle was never set, since T3 Connect sets it.
Without a key or a linked push host, publishing is upstream's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…nd read its status

forkPush.getStatus reports each server's role, its push host and link expiry,
the registered devices and the peers last heard from. registerDevice and
unregisterDevice need settings access; getStatus and sendTest need read access.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…its status

The app finds the connected environment that holds the FCM key, configures
the native notification handler with an identity that names it, and
registers its FCM token there again on rotation and on return. While the push
host owns the handler, upstream's T3 Connect registration can no longer
reconfigure or clear it. Settings → Notifications shows the push host, whether
each other environment sends to it and when its link expires, a test
notification, and stopping or resuming notifications on this phone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…egistration

A send that FCM answered UNREGISTERED removed the device by its ID, so a token
registered while the old token's send was pending was lost too. Results now
apply only while the registration still has the token they were sent to.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A send that failed in transport, timed out, was rate limited or met an FCM
server error was recorded and dropped, after ingest had already answered the
peer, so the alert was lost. Such sends now retry three times over about 21
seconds, while the registration still holds the same token.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every delivery advanced the device's card baseline, so a deletion processed
after another thread had changed acknowledged that change, and the change's
own delivery then saw no transition and never alerted. Deletions now repaint
the card without moving the baseline, as upstream's relay does; its
regression case is ported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stop was checked only when a registration began, so one waiting on the
notification permission or FCM token configured the native handler and
registered again after Stop had cleared and unregistered. Stop now bumps a
generation that such an attempt checks before each step, and waits for a
registration already sent before it unregisters.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each connected host started its own registration, and one that lost the race
stayed idle for good, so the phone stopped registering once the first host
disconnected. One place now picks among the connected hosts: it keeps the
current one while it stays, otherwise takes the first by environment ID.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s headers

FcmClient keeps the response status when reading the body times out, so a
token answer that stalled after a 200 looked permanent and its alert was
dropped. A timeout or transport cause now counts as transient whatever the
status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…cklog

One serial worker carried every device's deliveries, so a device FCM kept
rate limiting held up the others for its retries, and updates queued faster
than a failing device drained without limit. Each device now delivers on its
own fiber, and while it is busy its pending work keeps one entry per thread
plus a replay. Delivery reads the rows and compares them with the device's
baseline when it runs, as upstream's relay does when its queue lags, so a
later update to a thread decides the same alerts as the updates it replaces,
and deletions still keep the baseline.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…proval alert

A replay moves the device's card baseline, and with per-device coalescing it
can run before a thread's pending delivery. It then acknowledged that thread's
transition silently, so the alert never fired. A replay now picks its alert
from the baseline the same way a thread delivery does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Release installs skip infra/relay's dependencies, so effect could not be
resolved from the relay sources the push host bundles and the executable
build refused the leftover external imports. The server's pack config now
falls back to its own node_modules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tebiggs
tebiggs merged commit 087920a into main Nov 13, 2026
12 checks passed
@tebiggs
tebiggs deleted the feature/fork-push-host branch November 14, 2026 02:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant