feat(mobile,api,host-service): keep the Lock Screen agent card current with APNs pushes

Merged
SA
2mo ago
+23205-43

Why

The Lock Screen Live Activity was fed only by the phone polling each host every 5s while the app was in the foreground. Lock the phone and the card froze two minutes later. A Lock Screen card that is only right while the app is open is not worth much.

What

Three hops, one per package:

  • host-service reports state transitions, not hook events. TerminalAgentStatusReporter diffs the store's derived card state (working / review / permission / failed / gone) against what it last reported, debounces 2s, and sends one host.reportAgentStatus per quiet period. A burst of tool calls sends nothing; every row goes gone on dispose. Measured on Satya's own transcripts this is ~60 calls/hour at the median for 5 agents, ~1/4 of the hook event volume.
  • API stores the latest state per terminal in v2_agent_status (rows exist only while the agent is on the card) and the phone's ActivityKit tokens in v2_live_activity_tokens, together with the card's status words in the phone's language so the API never translates. After a report it rewrites the card for every user who can see the host: update to running activities, start (push-to-start) when a phone has none, end when the fleet is empty. Working transitions go at APNs priority 5, everything a person cares about at 10, so Apple's per-device budget is spent on the right moments. The APNs client is Node http2 with an ES256 provider JWT from node:crypto; no new dependency. Without APNS_* configured every push is skipped and the card is foreground-only, as before.
  • mobile starts activities with pushType: .token, watches every activity's pushTokenUpdates, the app's pushToStartTokenUpdates and activity end, remembers the latest tokens so a late listener still sees them, and registers them via useLiveActivityPushTokens. Rows carry since (epoch ms) instead of a preformatted "12m" and the widget ticks the relative time itself, so a pushed card stays right for hours. The 2-minute stale timer goes.

Migration 0115_agent_status_and_live_activity_tokens was generated with drizzle-kit on the workspace Neon branch and applied there only.

Plan with decisions and measurements: plans/done/20260912-1838-live-activity-push.md.

Verification

  • Unit: 7 reporter tests, 2 card-builder tests; existing terminal-agents and trpc suites green.
  • API end to end: local API on the Neon branch, driven as a fresh user, against a fake APNs HTTP/2 server. Observed: start at priority 10 with rows ordered permission, failed, review, working and "+2 more"; update at priority 5 on token registration; end with a dismissal date and the token deleted; push-to-start again after an empty fleet; a 410 deletes the token. Provider JWT verified ES256 against the test key. This run caught one real bug (neon-http has no transactions, now dbWs).
  • iOS: expo prebuild + xcodebuild for the simulator succeeds for the app, the module and the widget extension. aps-environment is already in the entitlements via expo-notifications.
  • Typecheck, lint, sherif, check:i18n clean (no new strings: the labels reuse existing catalog entries).

Not verified: Apple delivering to a real phone. That needs the production secrets and a native build:

  1. APNS_KEY_ID, APNS_TEAM_ID (NV9657CS5A), APNS_PRIVATE_KEY (the .p8, newlines as \n) via gh secret set. EAS already holds a push key for the app (portal id BW3334AKHK); use its .p8 or create a new key.
  2. A development or TestFlight build; the widget and module are native.
  3. Lock the phone, start and stop an agent on the Mac, watch the card appear and change.

Follow-ups (deliberately out of scope)

  • Phone-side "seen" marks do not reach the server, so a reviewed session shows "Review" on the card until its next transition.
  • A host that crashes without reporting gone leaves its rows until it restarts; graceful shutdown is handled.

https://claude.ai/code/session_019R4jiQ8STC4BY594f4jjPc


Summary by cubic

Keeps the Lock Screen agent card current while the phone is locked. Hosts now report agent state transitions to the API, which stores the latest state per terminal and pushes start/update/end card rewrites to the phone over APNs. Previously the card was fed only by foreground polling and froze minutes after locking; without APNS_* secrets every push is skipped and that behavior remains.

  • Host-service sends one debounced reportAgentStatus per batch with only derived state changes; graceful shutdown reports rows gone, and a crash without that leaves them until the host restarts.
  • The API stores the latest state per terminal in v2_agent_status and a phone's APNs tokens with translated status words in v2_live_activity_tokens.
  • A push targets each phone by device_id: a phone with a running activity gets an update, one without gets a push-to-start, and an end deletes its token only after Apple confirms delivery.
  • The mobile app starts activities with pushType: .token; rows carry since (epoch ms), the widget ticks elapsed time itself, and a 30-minute stale deadline is cleared by every push, so it only fires when the API has no APNs key.
  • Migrations 0115_agent_status_and_live_activity_tokens and 0116_live_activity_token_device were generated with drizzle-kit and applied only on the workspace Neon branch.
  • Apple delivery to a real phone is unverified; it needs APNS_KEY_ID, APNS_TEAM_ID, and APNS_PRIVATE_KEY in production secrets and a native build.

Summary by CodeRabbit

  • New Features
    • Live Activity agent cards now stay updated on the Lock Screen when the mobile app is closed.
    • Cards support push-to-start, status updates, activity completion, and automatic removal of inactive activities.
    • Agent statuses display dynamically with clearer elapsed-time tracking, prioritized states, and “more items” summaries.
    • Live Activity notifications are registered per device for more reliable updates across phones.
  • Configuration
    • Added Apple Push Notification settings for deployments; without them, cards update only while the app is open.