Skip to content

feat(mobile): direct push notifications from each server - #231

Merged
jayleaton merged 1 commit into
mainfrom
feat/push-mobile-20261113
Nov 14, 2026
Merged

jayleaton merged 1 commit into
mainfrom
feat/push-mobile-20261113

Conversation

@jayleaton

@jayleaton jayleaton commented Nov 13, 2026 •

Copy link
Copy Markdown
Owner

Summary

Sits on #230. The iOS app registers with each server and shows its notifications. Tapping one opens the thread.

apps/mobile/src/features/notifications/
  pushRegistrationAction.ts   pure: (platform, permission, switch, capability, connected) → register | unregister | none
  pushRegistration.ts         per connected server: send on change, reconnect, foreground, token rotation;
                              unregisterPushBeforeRemoval (best effort, 3 s)
  notificationPayload/Navigation/ResponseConsumer/foregroundNotificationBehavior.ts
settings/SettingsNotificationsRouteScreen.tsx   permission row + one switch per server
connection/useConnectionController.ts, settings/EnvironmentRoutesSection.tsx   unregister before removing a server
  • expo-notifications returns, so this is a new native fingerprint and needs a new TestFlight build. Personal Team builds skip the plugin because they can't sign the push entitlement.
  • iOS only. Android shows the screen and never registers.
  • A switch reads off whenever it is disabled. A disconnected server says "Connect to this server to change this", because its cached config may predate push.
  • All copy says Ada. The iOS prompt reads "Ada Agents" would like to send you notifications. The brand guard passes.
  • Rewrites docs/user/mobile-notifications.md for the top-level-only rule.

Evidence

iPhone 17 Pro Simulator on the MacBook, dev clients built locally from origin/main and from this branch. Captures are in a secret gist.

Before (main) After: permission not set up After: allowed
Push banner (app backgrounded) Tap opens the thread
  • The banner was sent with simctl push using Expo's APNs shape, where data is nested under body. Tapping it opened "Fix the login bug" on its "User input needed" card. A payload with data at the top level opens the app without routing, but Expo never sends that shape.
  • Granting permission saved a push registration on the paired dev server.
  • Tests: pushRegistrationAction.test.ts (9 cases) and notificationNavigation.test.ts. vp run check:changed passes for this layer.
  • Not covered here: delivery from a real server through Expo to a real iPhone. A Simulator can't receive Expo pushes. That needs the TestFlight build, now that the APNs key (KVLSS6NV9L) is in EAS.

Merge Danger

Door: two-way for code. The native dependency ships in the next store build.

Blast Radius: mobile. App startup installs the notification listeners. Removing a server can wait up to 3 s for an unreachable one.

🤖 Generated with Claude Code

@github-actions github-actions Bot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. labels Nov 13, 2026
@github-actions

github-actions Bot commented Nov 13, 2026 •

Copy link
Copy Markdown

Thread transfer impact

✅ Thread transfer remains within every enforced ceiling.

ℹ️ No successful main baseline artifact is available yet. This run establishes the initial measurement.

Provider Metric Main baseline This PR Impact PR ceiling
Codex Total thread wire — 5.0 KiB — 6.8 KiB ✅
Codex Thread snapshot wire — 3.8 KiB — 4.9 KiB ✅
Codex Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Codex Live turn WebSocket decoded — 21.5 KiB — 24.4 KiB ✅
Codex Live turn messages — 1 — 8 ✅
Claude Total thread wire — 5.0 KiB — 6.8 KiB ✅
Claude Thread snapshot wire — 3.8 KiB — 4.9 KiB ✅
Claude Live turn WebSocket wire — 1.2 KiB — 2.0 KiB ✅
Claude Live turn WebSocket decoded — 21.9 KiB — 24.4 KiB ✅
Claude Live turn messages — 1 — 8 ✅

Baseline: unavailable · PR result: 900ec5d · Source CI: success

Scenario and decoded snapshot size

10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.

  • Codex decoded thread snapshot: 108.5 KiB
  • Claude decoded thread snapshot: 108.8 KiB

Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed.

With T3 Connect gone, the phone had no way to get notified. Each server
now sends Expo pushes itself, so the iOS app registers its Expo push token
with every connected server that advertises the pushNotifications
capability, again on each reconnect and on return to the foreground, and
unregisters when a server is switched off or removed (best effort, 3 s).

Settings > Notifications shows the iOS permission and one switch per
server, kept locally. Tapping a push opens its thread; a push for the
thread on screen stays quiet. Android shows the screen but never
registers yet. expo-notifications and its config plugin come back,
skipped for Personal Team builds that cannot sign the push entitlement.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jayleaton
jayleaton force-pushed the feat/push-mobile-20261113 branch from 081b6f0 to 900ec5d Compare November 14, 2026 04:07
@jayleaton
jayleaton marked this pull request as ready for review November 14, 2026 04:21
@jayleaton
jayleaton changed the base branch from feat/push-notifier-20261113 to main November 14, 2026 04:22
@jayleaton
jayleaton merged commit ba4a864 into main Nov 14, 2026
36 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📱 Native Change Changes the native fingerprint; merging blocks production OTAs until a new store build ships. size:XL vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant