Skip to content

feat(notifications): optional Android notification capture → MCP rollup#19

Merged
teslashibe merged 1 commit intomainfrom
feat/notification-capture
Apr 22, 2026
Merged

feat(notifications): optional Android notification capture → MCP rollup#19
teslashibe merged 1 commit intomainfrom
feat/notification-capture

Conversation

@teslashibe
Copy link
Copy Markdown
Owner

Summary

Adds an opt-in feature that captures device notifications on Android via NotificationListenerService, ships them to the backend in a Timescale hypertable, and exposes 5 MCP tools so the agent can produce daily "what happened / what needs attention" rollups across SMS, WhatsApp, Zillow, email and any other allowlisted app.

Designed for the real-estate-agent persona but template-generic — every surface (DB rows, API routes, MCP plugin, system prompt, mobile UI) is gated by a single NOTIFICATIONS_ENABLED flag (server) and EXPO_PUBLIC_NOTIFICATIONS_ENABLED (client), default OFF, so forks that don't need it pay zero overhead.

Backend

  • migrations/00003 creates notification_events Timescale hypertable + dedup unique constraint + content FTS index. Inert when feature disabled.
  • internal/notifications: store / service / handler with limit clamping, classification heuristics for action-item ranking, full-text search via plainto_tsquery.
  • internal/notifications/mcp: 5 tools — list, search, threads, apps, pending_actions — registered as the credential-less notifications platform.
  • internal/mcp: PlatformBinding.NoCredentials + context-based user_id propagation so credential-less plugins can scope queries without a creds blob.
  • agent.NotificationsSystemPrompt() appends rollup instructions only when the flag is on.
  • Per-user rate limiter on /notifications/batch.

Mobile (Android)

  • Local Expo module mobile/modules/notification-capture — Kotlin NotificationListenerService + SharedPreferences buffer + JS bridge. Autolinking confirmed.
  • services/notificationSync.ts — foreground 5-min flush loop + AppState-driven sync. Network-loss-safe: failed chunks requeue into the native buffer via a new requeueEvents API so flaky cell coverage never drops data.
  • providers/NotificationCaptureProvider lifts native state into React, orchestrates start/stop based on master switch + auth.
  • app/(app)/capture.tsx settings screen with permission CTA, master toggle, sync stats, allowlist editor (pre-seeded with Messages / WhatsApp / Gmail / Zillow / iMessage / Outlook / Calendar), and live captured-apps stat.
  • eas.json with preview + production-apk profiles + npm run build:apk:* scripts so the APK ships via eas build without local Android SDK.

Audit

  • .cursor/tickets/notification-capture-{scope,audit}.md per issue-audit-user-stories.mdc — findings, gaps, 5 user stories, full Given/When/Then ACs, risks, ship checklist.
  • Audit caught & fixed two medium findings before merge:
    • M1: flushNow previously lost data on network failure (drained store before chunks uploaded). Fixed end-to-end via requeueEvents.
    • M2: mcp.Server.resolveClient would 500 on credential-less platforms. Fixed via NoCredentials flag + context-based user-id propagation.

Out of scope (V2)

iOS (OS-level restriction), background WorkManager sync, encrypted-at-rest buffer (needed if we go enterprise).

Test plan

  • cd backend && go vet ./... — clean (11 packages)
  • cd backend && go build ./... — clean
  • cd backend && go test ./... — all packages pass (incl. new internal/notifications/..., internal/mcp/... tests)
  • cd mobile && npm run typecheck — clean
  • cd mobile && npx expo prebuild --platform android --no-install --clean — succeeds
  • cd mobile && npx expo-modules-autolinking search --platform androidnotification-capture listed
  • Smoke test: cd mobile && eas build --platform android --profile preview after eas login (requires cloud)
  • Smoke test: install APK on Android device, grant Notification Access, post test notification, verify it appears in notification_events
  • Smoke test: ask the agent "what happened today" and verify the rollup is structured per the system prompt

Adds an opt-in feature that captures device notifications on Android via
NotificationListenerService, ships them to the backend in a Timescale
hypertable, and exposes 5 MCP tools so the agent can produce daily
"what happened / what needs attention" rollups across SMS, WhatsApp,
Zillow, email and any other allowlisted app.

Designed for the real-estate-agent persona but template-generic — every
surface (DB rows, API routes, MCP plugin, system prompt, mobile UI) is
gated by a single NOTIFICATIONS_ENABLED flag (server) and
EXPO_PUBLIC_NOTIFICATIONS_ENABLED (client), default OFF, so forks that
don't need it pay zero overhead.

Backend
- migrations/00003 creates notification_events hypertable + dedup
  unique constraint + content FTS index. Inert when feature disabled.
- internal/notifications: store/service/handler with limit clamping,
  classification heuristics for action-item ranking, and full-text
  search via plainto_tsquery.
- internal/notifications/mcp: 5 tools — list, search, threads, apps,
  pending_actions — registered as the credential-less "notifications"
  platform.
- internal/mcp: PlatformBinding gains NoCredentials; resolveClient
  threads user_id through context for credential-less plugins so the
  notification tools can scope queries without a creds blob.
- agent.NotificationsSystemPrompt() appends rollup instructions to the
  default prompt — only attached when the flag is on.
- Per-user rate limiter on /notifications/batch.

Mobile (Android)
- Local Expo module modules/notification-capture (Kotlin) implements
  NotificationListenerService + SharedPreferences buffer + JS bridge.
- services/notificationSync.ts runs a foreground flush loop on a 5-min
  cadence and on every app-foreground transition. On upload failure,
  unsent events are requeued into the native buffer via a new
  requeueEvents API so flaky cell coverage never drops data.
- providers/NotificationCaptureProvider lifts native state into React
  and orchestrates start/stop based on master switch + auth status.
- app/(app)/capture.tsx settings screen with master toggle, permission
  CTA, sync stats, allowlist editor (pre-populated with Messages /
  WhatsApp / Gmail / Zillow / etc.), and live captured-apps list.
- eas.json with preview + production-apk profiles + build:apk:* npm
  scripts so the APK ships via `eas build` without local Android SDK.

Audit + tests
- .cursor/tickets/notification-capture-{scope,audit}.md document intent,
  decisions, user stories, ACs, risks, and the ship checklist per
  .claude/rules/issue-audit-user-stories.mdc.
- Unit tests cover service classification + ranking, MCP tool schema +
  user-id propagation, mcp context plumbing, and platform composition.
- Verification: go vet + go test ./internal/notifications/... clean,
  npm run typecheck clean, npx expo prebuild --platform android dry
  run succeeds, autolinking confirms the local module is discovered.

Out of scope: iOS (OS-level restriction), background WorkManager sync
(V2), encrypted-at-rest buffer (V2 — needed if we go enterprise).
@teslashibe teslashibe merged commit 8d01ba9 into main Apr 22, 2026
1 check passed
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