feat(mobile,api,host-service): keep the Lock Screen agent card current with APNs pushes
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.
TerminalAgentStatusReporterdiffs the store's derived card state (working / review / permission / failed / gone) against what it last reported, debounces 2s, and sends onehost.reportAgentStatusper quiet period. A burst of tool calls sends nothing; every row goesgoneon 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 inv2_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:updateto running activities,start(push-to-start) when a phone has none,endwhen 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 Nodehttp2with an ES256 provider JWT fromnode:crypto; no new dependency. WithoutAPNS_*configured every push is skipped and the card is foreground-only, as before. - mobile starts activities with
pushType: .token, watches every activity'spushTokenUpdates, the app'spushToStartTokenUpdatesand activity end, remembers the latest tokens so a late listener still sees them, and registers them viauseLiveActivityPushTokens. Rows carrysince(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:
startat priority 10 with rows ordered permission, failed, review, working and "+2 more";updateat priority 5 on token registration;endwith 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, nowdbWs). - iOS:
expo prebuild+xcodebuildfor the simulator succeeds for the app, the module and the widget extension.aps-environmentis 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:
APNS_KEY_ID,APNS_TEAM_ID(NV9657CS5A),APNS_PRIVATE_KEY(the.p8, newlines as\n) viagh secret set. EAS already holds a push key for the app (portal id BW3334AKHK); use its.p8or create a new key.- A development or TestFlight build; the widget and module are native.
- 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
goneleaves 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
reportAgentStatusper batch with only derived state changes; graceful shutdown reports rowsgone, and a crash without that leaves them until the host restarts. - The API stores the latest state per terminal in
v2_agent_statusand a phone's APNs tokens with translated status words inv2_live_activity_tokens. - A push targets each phone by
device_id: a phone with a running activity gets anupdate, one without gets a push-to-start, and anenddeletes its token only after Apple confirms delivery. - The mobile app starts activities with
pushType: .token; rows carrysince(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_tokensand0116_live_activity_token_devicewere 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, andAPNS_PRIVATE_KEYin 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.