feat(mobile): deliver glanceable snapshot updates - #5620
Conversation
Add activity-token registration and background delivery so eligible work updates the iOS Live Activity, Android ongoing notification, and Home widgets without opening the app. Logout unregisters tokens. Old clients ignore the new payload type.
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Executive SummaryAPNs team ID, key ID, and private-key Secrets Store binding are now declared on the notifications worker; remaining changes are docs, generated types, and import formatting. Files Reviewed (5 files)
Previous Review Summaries (15 snapshots, latest commit 33674ef)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 33674ef)Status: No Issues Found | Recommendation: Merge Executive SummaryActive Agents master switch blanks surfaces and drops headless snapshots when off; APNs topic is declared in worker vars. Files Reviewed (21 files)
Previous review (commit 421a090)Status: No Issues Found | Recommendation: Merge Executive SummaryCredential-shaped glanceable-refresh error fixture was replaced with a generic string; the test still asserts queue ack and logs only the session id. Files Reviewed (1 file)
Previous review (commit cee6acb)Status: No Issues Found | Recommendation: Merge Executive SummaryTerminal Live Activity ends now retire APNs targets under a generation fence before fresh work can start, with failed refresh isolated from queue ack. Files Reviewed (6 files)
Previous review (commit 35cf697)Status: No Issues Found | Recommendation: Merge Executive SummaryCommitted Cloud Agent run reports now refresh glanceable counts after the report transaction, with failed refresh isolated so the queue still acks. Files Reviewed (3 files)
Previous review (commit 7428272)Status: No Issues Found | Recommendation: Merge Executive SummaryGeneration-fenced glanceable refresh now runs from session metadata, heartbeat, and disconnect transitions, with APNs timestamp ordering and Expo supersession checks covering the concurrent-delivery races. Files Reviewed (18 files)
Previous review (commit 453669e)Status: No Issues Found | Recommendation: Merge Executive SummaryIdle ends now keep scope delivery registered so rotated Live Activity and Android tokens still update widgets after the visible surface goes away. Files Reviewed (15 files)
Previous review (commit 2864988)Status: No Issues Found | Recommendation: Merge Executive SummaryIdle ActivityKit recovery now clears the denied latch after a verified identity read even when Agents is empty, and Android native deadlines hide expired counts without a JS timer. Files Reviewed (15 files)
Previous review (commit a07b005)Status: 1 Issue Found | Recommendation: Address before merge Executive SummaryActivityKit recovery no longer clears the denied latch unless eligible work is already present, so re-enabling Live Activities with an empty Agents tab never starts a later activity on that same focus. Overview
Issue Details (click to expand)WARNING
Files Reviewed (5 files)
Fix these issues in Kilo Cloud Previous review (commit 609fc68)Status: 1 Issue Found | Recommendation: Address before merge Executive SummaryActivityKit recovery no longer clears the denied latch unless eligible work is already present, so re-enabling Live Activities with an empty Agents tab never starts a later activity on that same focus. Overview
Issue Details (click to expand)WARNING
Files Reviewed (12 files)
Fix these issues in Kilo Cloud Previous review (commit fa8897d)Status: No Issues Found | Recommendation: Merge Executive SummaryIncremental i18n-only change since the last review: glanceable mobile status keys and Files Reviewed (172 files)
Previous review (commit 5853a07)Status: No Issues Found | Recommendation: Merge Files Reviewed (1 file)
Previous review (commit 05664ca)Status: No Issues Found | Recommendation: Merge Files Reviewed (6 files)
Previous review (commit fe6f60a)Status: 3 Issues Found | Recommendation: Address before merge Executive SummaryFailed activity-token unregister on account/org switch still overwrites a pending push-token tombstone, and org-switch registration can still race the fire-and-forget delete. Overview
Issue Details (click to expand)WARNING
Files Reviewed (6 files)
Fix these issues in Kilo Cloud Previous review (commit caf4017)Status: 5 Issues Found | Recommendation: Address before merge Executive SummaryFailed activity-token unregister on account/org switch still overwrites a pending push-token tombstone, and org-switch registration can still race the fire-and-forget delete. Overview
Issue Details (click to expand)WARNING
Files Reviewed (8 files)
Fix these issues in Kilo Cloud Previous review (commit 3367927)Status: 8 Issues Found | Recommendation: Address before merge Executive SummaryFailed activity-token unregister on account/org switch overwrites a pending push-token tombstone, and org-switch registration can race the fire-and-forget delete. Overview
Issue Details (click to expand)WARNING
[Snapshot truncated.] Additional previous summary content was truncated to keep this comment within platform limits. Reviewed by grok-4.6 · Input: 100.1K · Output: 6.9K · Cached: 202.4K Review guidance: REVIEW.md from base branch |
…activities-3cf8-s4
…activities-3cf8-s4 # Conflicts: # apps/mobile/src/lib/auth/logout-cleanup.ts # apps/mobile/src/lib/notifications.test.ts # apps/mobile/src/lib/notifications.ts # packages/db/src/migrations/meta/0233_snapshot.json # packages/db/src/migrations/meta/_journal.json
…activities-3cf8-s4
…activities-3cf8-s4
…activities-3cf8-s4
…activities-3cf8-s4
…activities-3cf8-s4
…activities-3cf8-s4
…activities-3cf8-s4
Submit the Live Activity end during publish so ActivityKit owns removal after the push handler returns. Track pending native ends by ActivityKit ID, not by a JS counter, so a recreated wrapper cannot restart a dying activity. Patch expo-widgets for getInfo() and getInstances(includeEnded) to read native state and reach terminal-retained instances on privacy.
…activities-3cf8-s4 # Conflicts: # apps/mobile/package.json # apps/mobile/src/lib/notifications.test.ts # apps/mobile/src/lib/organization-context.tsx # packages/db/src/migrations/meta/0234_snapshot.json # packages/db/src/migrations/meta/_journal.json # pnpm-lock.yaml # services/session-ingest/src/ingest/metadata.test.ts
421a090 to
e9bd7b6
Compare
Add a master switch for the glanceable Active Agents surfaces. Off blanks every surface, unregisters its push tokens, refuses the in-app publisher, drops remote snapshots in the headless push handler, and never prompts for ActivityKit permission. Declare the APNs Live Activity vars in the notifications worker config and document what still has to land before the deploy: the .p8 auth key in the Secrets Store, then its binding. Fix the session-ingest typecheck. Its DO tests import the notifications sources, which read bindings off a global `Env` this package does not have.
Register a team-scoped APNs auth key (KRYMZL626P, sandbox and production) and store the .p8 in the Secrets Store. Declare the team and key ids as vars, bind the private key, and regenerate the Worker bindings. Live Activity push now has every value it needs. A backup of the .p8 is in the 1Password "Eng / Product" vault; Apple never serves it a second time.
The app-side consumer of `dev_session_token` was deleted in #5641, so the script printed a successful sign-in and the app stayed on the login screen. Nothing else imports it.
Replace the four-state count model with three states the user named: needs input (orange), working (green), and idle (white). The `retry` status folds into needs input, because it means one thing to the user: the agent waits and cannot go on alone. The "Reconnecting" label is gone. The Dynamic Island now shows the Kilo mark and one number, colored by the state it counts and ranked needs-input, working, idle. The Lock Screen banner and every widget family show one row per non-zero state, each with a glyph whose shape differs as well as its color, so the state reads in the accented Lock Screen rendering too. The "Open agents" line is gone from every surface; the whole surface already deep-links there, so the label stays in the spoken text. Copy the Kilo mark into the shared app group and bake its path into the stringified `'widget'` layouts, because the widget extension cannot resolve a bundle asset and the notifications Worker cannot know a device path. Fix two defects the redesign uncovered: - The layouts referenced the imported `WIDGET_LOGO_PLACEHOLDER`, which is an undefined global once the layout source is re-evaluated in the widget process. Each layout now repeats the literal, and a test keeps the copies equal. - Widget props carried null fields. `updateTimeline` writes them to the shared `UserDefaults`, which rejects a null and threw out through the host function, so no widget family ever rendered. `toWidgetProps` omits them. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AGsBUddy4TP7xqJ58X8xiY
Summary
The iOS app now keeps the Active Agents Live Activity and the Lock Screen and Home Screen widgets current while the app is backgrounded or closed. They show how many agents are running, need input, or are reconnecting, a status line such as "No work in progress", and how long eligible work has run.
The Android Active Agents notification now updates in the background instead of only while the app is open.
If the device cannot show Live Activities, the Agents tab shows a one-time prompt to open Settings and enable them.
Signing out now also removes the device's background-update registration, so the signed-out account stops updating the Live Activity and widgets.
Reviewer Notes
A
user_activity_tokenstable stores device activity tokens for the glanceable surfaces. It holds the iOS push-to-start, iOS activity, and Android ongoing tokens, keyed by user and an optional organization, with a unique constraint on the token and cascade deletion with the user. Account soft-delete now also deletes these rows so a deleted user stops receiving glanceable deliveries.Files
packages/db/src/schema.ts— adds theuser_activity_tokenstable, its unique token index, and its user-plus-organization index.packages/db/src/migrations/0233_square_daimon_hellstrom.sql— creates the table, the user foreign key, and both indexes.apps/web/src/lib/user/index.ts— deletes the user's activity-token rows in the soft-delete path.Two new user mutations register and unregister activity tokens.
registerActivityTokenupserts on the token so a re-registration replaces the row, andunregisterActivityTokendeletes only a row owned by the caller.Files
apps/web/src/routers/user-router.ts— addsregisterActivityTokenandunregisterActivityTokenand imports the new table.The active-sessions core moves into a reusable server function. The tRPC procedure now delegates to
listActiveSessions, which keeps the worker fetch, the enrichment, and the cloud-agent merge, so the snapshot builder can reuse it without a tRPC context.Files
apps/web/src/lib/active-sessions-list.ts— new module holding the fetch, parse, enrich, and cloud-merge logic.apps/web/src/routers/active-sessions-router.ts— thelistprocedure now callslistActiveSessionsand re-exports its types.A new internal route builds the privacy-minimal glanceable snapshot for a user. It requires the internal secret, re-checks organization membership when an organization is named, and returns the same versioned shape the mobile publisher derives locally. The builder reads only each session's status, so titles, ids, and other raw fields never leave the server.
Files
apps/web/src/app/api/internal/glanceable-agents-snapshot/route.ts— the secret-authenticated route with the organization re-check.apps/web/src/lib/glanceable-agents-snapshot-server.ts— builds the snapshot from the active-sessions list through the shared builder.A new push data variant
active_agents_glanceablecarries the aggregate snapshot. It has a versioned revision, an opaque scope key, counts, status, and safe timestamps, and never a title or id. A newactive-agentsAndroid channel and count-free generic copy route this payload.Files
packages/notifications/src/push-data.ts— adds the variant and theGlanceableLiveActivityContentStatetype.packages/notifications/src/push-presentation.ts— adds the channel and the routing and generic-copy cases.packages/notifications/src/locales/en.json— adds the generic body string for the glanceable push.The notifications worker now sends Live Activity APNs pushes and aggregate glanceable deliveries. A token-based APNs client signs a provider JWT and sends start or update pushes, and a pure orchestrator builds the snapshot and pushes it to iOS activity tokens and Expo tokens. After each cloud-agent session push, the worker delivers the fresh snapshot best-effort.
Files
services/notifications/src/lib/apns-live-activity.ts— new APNs client with JWT signing, request building, and parallel sending.services/notifications/src/lib/glanceable-delivery.ts— new orchestrator whose IO is injected for tests.services/notifications/src/index.ts— wires the dependencies, reads the credentials, and delivers after the session push.The worker gains optional configuration for the snapshot and APNs delivery.
KILO_WEB_API_BASE_URLnames the web origin, and the four APNs values name the signing key and the topic. A missing value skips that part of delivery instead of failing the push.Files
services/notifications/src/bindings.d.ts— declares the new optional bindings.services/notifications/wrangler.jsonc— setsKILO_WEB_API_BASE_URLto the production origin.ENVIRONMENT.md— documents the five new values.The mobile app applies
active_agents_glanceablepushes in the foreground and in a headless background task. A scope-key fence and anupdatedAtcheck discard stale or foreign pushes, and the applied snapshot rebases its revision onto the local sequence and its local account epoch. The foreground handler suppresses these pushes as banners, and taps land on the Agents tab.Files
apps/mobile/src/lib/notifications.ts— adds the apply logic, the background task, and the foreground suppression.apps/mobile/src/lib/notification-path.ts— routes a glanceable tap to the Agents tab.apps/mobile/src/app/_layout.tsx— registers the background handler at module scope.apps/mobile/app.config.ts— enables background remote notifications.apps/mobile/package.json— addsexpo-task-manager.An iOS delivery registrar registers and unregisters the Live Activity and push-to-start tokens. Registration waits for any in-flight logout unregister to settle, and unregistration reports the tokens it attempted so logout can tombstone them.
Files
apps/mobile/src/lib/glanceable/delivery-registration.ts— new registrar wiring the two token mutations.apps/mobile/src/lib/glanceable/sink-registry.ts— adds the delivery hook and the token-only context.apps/mobile/src/lib/auth/push-registration-reconciliation.ts— imports the registrar as a side effect.The iOS Live Activity renders status, counts, and an elapsed timer from the shared content-state. The sink adopts an existing activity after a process restart, guards revisions, marks the surface denied only on a permanent ActivityKit failure, and triggers a one-time Settings prompt from the Agents tab.
Files
apps/mobile/src/glanceable-ios/active-agents-live-activity.tsx— the Live Activity layout with inlined English copy.apps/mobile/src/glanceable-ios/ios-sink.ts— start, update, adopt, end, and token registration.apps/mobile/src/glanceable-ios/view-props.ts— builds the surface props and the content-state.apps/mobile/src/lib/glanceable/activity-kit-prompt.ts— the one-time Settings alert.apps/mobile/src/app/(app)/(tabs)/(2_agents)/index.tsx— shows the alert when the tab regains focus.Logout now awaits activity-token unregistration and tombstones a failed attempt. Reconciliation retries only the recorded tokens, marks the activity part done when it succeeds, and orders registration against the unregister so a new session's tokens are never re-deleted.
Files
apps/mobile/src/lib/auth/logout-cleanup.ts— extends the tombstone with activity tokens and awaits unregistration.apps/mobile/src/lib/auth/logout-reconciliation.ts— retries the recorded tokens and marks the activity part done.Tests: 14 test files updated across the mobile, web, and notifications suites.
Generated: 1 lockfile updated and 2 Drizzle migration metadata files regenerated.
Verification
No E2E report was attached for this section, and no manual tests were run.
Visual Changes
Visual Changes: N/A
Human steps:
Notes: none.
Stacked PRs — merge bottom to top. Each level shows only its own diff.
Runtime verification (E2E, user advocacy, simplify) runs on the tip PR over every level.
Every level keeps its own checks, its own bot review, and its own threads; each one is answered on its own PR.
Each level is its own deliverable: it builds and passes its own checks alone.
A finding on a level is repaired on that level, then carried upward with stack.sh forward.
audit-w8b-live-activities-3cf8— feat(mobile): add glanceable agents snapshot contract #5535audit-w8b-live-activities-3cf8-s2— feat(mobile): add iOS Live Activity and widgets #5553audit-w8b-live-activities-3cf8-s3— feat(mobile): add Android agents widget and live update #5585audit-w8b-live-activities-3cf8-s4— feat(mobile): deliver glanceable snapshot updates #5620 ← this PR (tip)