Skip to content

Mobile push notifications for agent alerts#84

Merged
TraderSamwise merged 6 commits into
masterfrom
polish
Jun 6, 2026
Merged

Mobile push notifications for agent alerts#84
TraderSamwise merged 6 commits into
masterfrom
polish

Conversation

@TraderSamwise

@TraderSamwise TraderSamwise commented Jun 6, 2026

Copy link
Copy Markdown
Owner

What

Bridges agent notifications to mobile push so the same alerts that fire on desktop/CLI also reach the iOS/Android app, end to end (daemon → relay → Expo → APNs).

How

  • Single choke point. The mobile forward lives inside notifyAlert, so desktop and mobile alerts fire together by the exact same gate (config flags + focus suppression). No more "remember to wire the forward at each call site" — the previous approach hooked one of two notifyAlert callers and silently skipped the other.
  • Daemon → relay → Expo. Project services hand alerts to the daemon's single relay connection (/internal/push, loopback-only); the relay (deliverNotificationPush) sends to the owner's own iOS/Android tokens via Expo.
  • Throttle. MobilePushThrottle at the daemon funnel collapses identical alerts within a TTL window and caps pushes per session, so a chatty/looping agent can't flood the device.
  • Native + app. expo-notifications config plugin (entitlement), a foreground handler, and tap-to-route into the session chat.
  • Docs. Standardized the eas-release OTA-vs-native release workflow.

Criteria

Mobile fires iff notifyAlert fires: master switch, per-category gates (onPrompt/onComplete/onError), and focus suppression — identical to desktop/CLI.

Tests

  • notify.test.ts — desktop+mobile fire together and respect every gate.
  • mobile-push-throttle.test.ts — dedup window, per-session rate cap, independence.
  • security-delivery.test.ts — owner-only token filtering.
  • Full suite green (678).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Native push notifications with tap routing into the correct screen and expo notification configuration.
    • Mobile push relay with forwarding, dedupe, and per-session rate limiting to reduce alert noise.
  • Documentation

    • Split release pipeline into OTA vs native build flows and added clear bump/build commands and decision rules.
    • Updated mobile deployment and release instructions.
  • Chores

    • Bumped app OTA version.
  • Tests

    • Added tests covering push delivery, throttling, and notify behavior.

test and others added 5 commits June 6, 2026 14:19
Adds a daemon->relay->Expo path so agent notifications (needs-input, task-complete) reach mobile, matching desktop gating via notifyAlert. Includes native foreground handler + tap routing and owner-only push filtering.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…imit

Move the mobile-push forward into notifyAlert so desktop and mobile alerts fire together by the same gate, instead of being wired at one of two notifyAlert call sites. Add MobilePushThrottle at the daemon funnel: collapse identical alerts within a TTL window and cap pushes per session, preventing chatty agents from flooding the device.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
app Ready Ready Preview, Comment Jun 6, 2026 1:12pm

@TraderSamwise

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d7d99029-4e04-4f66-a6b7-2398563a6b31

📥 Commits

Reviewing files that changed from the base of the PR and between 41eab72 and c33ff58.

📒 Files selected for processing (4)
  • relay/src/relay-object.ts
  • relay/src/security-delivery.test.ts
  • relay/src/security-delivery.ts
  • src/daemon.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/daemon.ts
  • relay/src/security-delivery.ts
  • relay/src/relay-object.ts

📝 Walkthrough

Walkthrough

Adds an end-to-end mobile push pipeline: loopback daemon endpoint → MobilePushThrottle dedupe/rate limit → RelayClient push API → relay delivery via deliverNotificationPush to Expo → device notification taps routed by NativeNotificationRouter; updates docs and bumps OTA version.

Changes

Mobile Push Notification Pipeline

Layer / File(s) Summary
Notification type contracts
relay/src/types.ts, src/relay-client.ts, src/mobile-push-throttle.ts
RelayNotificationPushControl with notification_push discriminator, RelayNotificationPush interface, and ThrottleInput shape define message and throttle contracts.
Push throttling and deduplication
src/mobile-push-throttle.ts, src/mobile-push-throttle.test.ts
MobilePushThrottle implements dedupe key TTL and per-session rate caps; tests cover dedupe precedence, content vs dedupeKey, session limits, and window expiry.
Relay WebSocket daemon notification handling
relay/src/relay-object.ts
Daemon connections may carry user:<id> tags from X-Aimux-User-Id; incoming notification_push messages are routed to a handler that resolves owner push tokens and calls delivery.
Shared Expo push delivery
relay/src/security-delivery.ts, relay/src/security-delivery.test.ts
NotificationPushInput and deliverNotificationPush filter tokens by user/platform, build Expo payloads with metadata/dedupeKey, and send via sendExpoPush; existing security push flow now reuses the shared sender. Tests verify success, no-mobile-tokens path, and non-2xx error handling.
Daemon push HTTP endpoint with throttling
src/daemon.ts
Adds loopback-only POST /internal/push that validates title, enforces pushThrottle.allow(), returns suppressed vs forwarded JSON, and forwards allowed payloads to relayClient.pushNotification().
Relay client notification push method
src/relay-client.ts
RelayClient.pushNotification() guards on notification.title and sends a notification_push control message via WebSocket.
Alert-to-mobile-push forwarding bridge
src/mobile-push-bridge.ts, src/notify.ts, src/notify.test.ts
forwardAlertToMobilePush posts AlertEvent fields to daemon /internal/push fire-and-forget; notifyAlert now returns boolean, performs early exits for disabled/suppressed/misconfigured alerts, forwards allowed alerts to mobile, and is covered by tests for gating logic.
Frontend native notification routing and config
app/app.config.js, app/app/(main)/_layout.tsx, app/components/NativeNotificationRouter.tsx
Adds expo-notifications plugin (icon/color), NativeNotificationRouter to handle cold-start and tap responses (extracts projectRoot/sessionId, updates Jotai atoms, navigates to session or inbox), and wires it into main layout.
Release pipeline documentation and version
AGENTS.md, README.md, docs/deployment.md, app/lib/version.ts
Documentation updated to present OTA vs native build release paths using @tradersamwise/eas-release; adds yarn version:bump-ota vs bump-build flows and runtime gating; increments OTA version and timestamp.

Sequence Diagram

sequenceDiagram
  participant Daemon as Daemon
  participant Push as POST /internal/push
  participant Throttle as MobilePushThrottle
  participant Relay as RelayClient
  participant RelayWS as Relay WebSocket
  participant Device as Device
  participant App as NativeNotificationRouter

  Daemon->>Push: POST {title, body, kind, sessionId, projectId, projectRoot}
  Push->>Throttle: allow({dedupeKey?, title, body, sessionId})
  alt Throttled
    Throttle-->>Push: false
    Push-->>Daemon: {ok: true, suppressed: true}
  else Allowed
    Throttle-->>Push: true
    Push->>Relay: pushNotification({title, body, ...})
    Relay->>RelayWS: send notification_push control message to user
    RelayWS->>Device: Expo push delivered
    Device->>App: notification tapped (cold-start or foreground)
    App->>App: extract projectRoot, sessionId
    App->>App: setSelectedProject/setSelectedSession and navigate
    Push-->>Daemon: {ok: true}
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • TraderSamwise/aimux#27: Related changes to Expo push payload construction and delivery in security-delivery.
  • TraderSamwise/aimux#15: Prior work adding NotificationProvider to the main layout; this PR adds native routing integration.
  • TraderSamwise/aimux#4: Earlier relay protocol changes that this PR extends with notification_push handling.

Poem

🐰 I’m a rabbit routing pings so fleet,

From daemon calls to mobile beat,
Throttles hush the noisy throng,
Expo sings the push along,
Tap the bell — the chat’s complete.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Mobile push notifications for agent alerts' directly and accurately summarizes the main change: implementing push notifications for mobile platforms triggered by agent alerts.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch polish

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
src/notify.test.ts (1)

32-41: ⚡ Quick win

Use a fully valid AlertEvent fixture instead of casting.

The as AlertEvent cast currently hides missing required fields (projectId), which can mask contract regressions.

Proposed refactor
 function alert(overrides: Partial<AlertEvent> = {}): AlertEvent {
   return {
     type: "alert",
     kind: "needs_input",
+    projectId: "project-1",
     sessionId: "claude-1",
     title: "claude-1 needs input",
     message: "waiting for input",
     ts: "2026-06-06T00:00:00.000Z",
     ...overrides,
-  } as AlertEvent;
+  };
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/notify.test.ts` around lines 32 - 41, The fixture function alert
currently returns an object cast with "as AlertEvent", hiding missing required
fields (e.g., projectId) and masking contract regressions; change the alert
function to construct and return a fully valid AlertEvent object (no "as" cast)
by including all required properties from the AlertEvent type—at minimum add
projectId and any other required fields—and preserve overrides via the overrides
parameter so tests use a true AlertEvent instance.
src/mobile-push-bridge.ts (1)

23-23: ⚡ Quick win

Avoid fully silent push-forward failures.

Swallowing all errors without even debug logging makes delivery regressions hard to diagnose.

Proposed refactor
 import { requestDaemonJson } from "./daemon.js";
 import type { AlertEvent } from "./project-events.js";
+import { debug } from "./debug.js";
@@
-  }).catch(() => {});
+  }).catch((error) => {
+    debug(`mobile push forward failed: ${error instanceof Error ? error.message : String(error)}`, "notify");
+  });
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/mobile-push-bridge.ts` at line 23, The empty promise rejection handler
"}).catch(() => {});" in mobile-push-bridge.ts swallows all errors silently;
change it to capture the error (e.g., .catch((err) => { ... })) and log a
helpful diagnostic using the existing logger (or console) including context such
as the push payload/recipient and a short message (e.g., "Push forward failed"),
while still avoiding crashing the process — do not remove error handling, just
replace the empty catch with a call to processLogger.error/console.error that
includes the error and relevant identifiers.
app/components/NativeNotificationRouter.tsx (1)

20-24: ⚡ Quick win

Return trimmed value to prevent whitespace bugs.

The stringField helper validates that value.trim() is non-empty but returns the original untrimmed value. If the notification data contains leading or trailing whitespace (e.g., "session-123 "), this could cause mismatches when comparing with stored values or route parameters.

♻️ Proposed fix
 function stringField(data: unknown, key: string): string | undefined {
   if (!data || typeof data !== "object") return undefined;
   const value = (data as Record<string, unknown>)[key];
-  return typeof value === "string" && value.trim() ? value : undefined;
+  const trimmed = typeof value === "string" ? value.trim() : "";
+  return trimmed ? trimmed : undefined;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/components/NativeNotificationRouter.tsx` around lines 20 - 24, The helper
stringField validates by calling value.trim() but returns the original untrimmed
value, which can cause mismatches; update stringField to return the trimmed
string when the check passes (e.g., compute const trimmed = (value as
string).trim() and return trimmed) so callers get a normalized, whitespace‑free
string; keep the same type signature and undefined behavior for non-strings or
empty trimmed values.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@relay/src/relay-object.ts`:
- Around line 254-273: The handleDaemonNotificationPush path currently awaits
deliverNotificationPush which can throw on network/Expo/API failures and bubble
out; wrap the deliverNotificationPush call in a try/catch inside
handleDaemonNotificationPush (after loadSecurityState and before returning) so
errors are caught and do not break daemon message processing—on catch log the
error (include context like ownerUserId and notification) via this.ctx.logger or
equivalent and do not rethrow; keep the existing early-return checks
(ownerUserId, notification.title) and still await loadSecurityState as before.

In `@relay/src/security-delivery.ts`:
- Around line 59-69: sendExpoPush currently assumes any resolved fetch is
success, so non-2xx responses (rate limits/errors) and network exceptions are
ignored; wrap the fetch in try/catch, await the Response and check response.ok,
and if not ok parse the response body (JSON) for error details and either throw
or log a descriptive error including status and body; also handle fetch
exceptions in the catch and surface them to the caller (or implement
retry/backoff for 429 if desired) so callers can react to failures. Use the
sendExpoPush function name to locate where to add the try/catch, response.ok
check, response.json() parsing, and appropriate error propagation/logging.

In `@src/daemon.ts`:
- Around line 731-733: The throttle check is executed before verifying relay
availability, so calling this.pushThrottle.allow(payload) consumes quota even
when the relay is null/disconnected; change the logic to first verify relay
availability (check this.relayClient exists and any connection/ready flag it
exposes) and only if the relay is available call
this.pushThrottle.allow(payload) and then
this.relayClient.pushNotification(payload); if the relay is unavailable return
the same 200 response without calling pushThrottle.allow to avoid burning
dedupe/session quota. Ensure you update the branch that currently calls
this.relayClient?.pushNotification after allow so that pushThrottle.allow and
pushNotification are only invoked when relayClient is present/ready.

---

Nitpick comments:
In `@app/components/NativeNotificationRouter.tsx`:
- Around line 20-24: The helper stringField validates by calling value.trim()
but returns the original untrimmed value, which can cause mismatches; update
stringField to return the trimmed string when the check passes (e.g., compute
const trimmed = (value as string).trim() and return trimmed) so callers get a
normalized, whitespace‑free string; keep the same type signature and undefined
behavior for non-strings or empty trimmed values.

In `@src/mobile-push-bridge.ts`:
- Line 23: The empty promise rejection handler "}).catch(() => {});" in
mobile-push-bridge.ts swallows all errors silently; change it to capture the
error (e.g., .catch((err) => { ... })) and log a helpful diagnostic using the
existing logger (or console) including context such as the push
payload/recipient and a short message (e.g., "Push forward failed"), while still
avoiding crashing the process — do not remove error handling, just replace the
empty catch with a call to processLogger.error/console.error that includes the
error and relevant identifiers.

In `@src/notify.test.ts`:
- Around line 32-41: The fixture function alert currently returns an object cast
with "as AlertEvent", hiding missing required fields (e.g., projectId) and
masking contract regressions; change the alert function to construct and return
a fully valid AlertEvent object (no "as" cast) by including all required
properties from the AlertEvent type—at minimum add projectId and any other
required fields—and preserve overrides via the overrides parameter so tests use
a true AlertEvent instance.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 99be8a93-91a0-4997-a256-a49ab1f787c1

📥 Commits

Reviewing files that changed from the base of the PR and between e2cf0c3 and 41eab72.

📒 Files selected for processing (18)
  • AGENTS.md
  • README.md
  • app/app.config.js
  • app/app/(main)/_layout.tsx
  • app/components/NativeNotificationRouter.tsx
  • app/lib/version.ts
  • docs/deployment.md
  • relay/src/relay-object.ts
  • relay/src/security-delivery.test.ts
  • relay/src/security-delivery.ts
  • relay/src/types.ts
  • src/daemon.ts
  • src/mobile-push-bridge.ts
  • src/mobile-push-throttle.test.ts
  • src/mobile-push-throttle.ts
  • src/notify.test.ts
  • src/notify.ts
  • src/relay-client.ts

Comment thread relay/src/relay-object.ts
Comment thread relay/src/security-delivery.ts
Comment thread src/daemon.ts
- sendExpoPush throws on non-2xx so Expo rejections/rate-limits aren't silently dropped (security path is allSettled, so unaffected).
- handleDaemonNotificationPush guards delivery in try/catch so a push failure can't break relay WS message processing.
- /internal/push gates on the relay being connected before consuming throttle quota, so dropped pushes don't burn dedupe/rate budget.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@TraderSamwise

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jun 6, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

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