feat(notifications): agent-callable push notification tool (cloud + mobile)#4627
Conversation
Code Review SummaryStatus: No Issues Found | Recommendation: Merge Executive SummaryReviewed the full PR diff (39 files: DB schema/migration, notifications service RPC/DO/push-sink, session-ingest dispatch/attention, web preferences router, mobile toggle) against current HEAD ( Files Reviewed (39 files)
Previous Review Summaries (2 snapshots, latest commit 1828f06)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 1828f06)Status: No Issues Found | Recommendation: Merge Executive SummaryAll 6 previously reported issues (rate-limit double-consumption, hardcoded sink Files Reviewed (5 files)
Previous review (commit 98a5358)Status: 6 Issues Found | Recommendation: Address before merge Executive SummaryThe highest-risk issue is a rate-limit accounting bug in Overview
Issue Details (click to expand)WARNING
SUGGESTION
Files Reviewed (37 files)
Reviewed by claude-sonnet-5 · Input: 78 · Output: 26.1K · Cached: 3.1M Review guidance: REVIEW.md from base branch |
| migrations: { | ||
| m0000, | ||
| m0001, | ||
| m0002, | ||
| m0003, | ||
| m0004 | ||
| } |
There was a problem hiding this comment.
nit: there's some funky formatting going on here.
| // writes a `pending` marker. A later send failure would otherwise leave | ||
| // the idempotency key empty and cause a replay to re-consume a quota | ||
| // slot. Mirror the badge path by marking `pending` before token/send. | ||
| if (!input.badge) { |
There was a problem hiding this comment.
Concurrent replays can send the same agent notification more than once. This writes pending before token lookup and Expo submission, but a request that observes pending is treated as immediately retryable. While the first request awaits external I/O, a concurrent replay can skip rate-limit consumption and enter the same send path, allowing both requests to submit before either records a terminal stage. Please introduce an in-flight claim or lease so a replay seeing a live claim waits or returns duplicate; only an expired or explicitly abandoned claim should retry.
| idempotencyKey: input.idempotencyKey, | ||
| payload: { | ||
| title: input.push.title, | ||
| body: input.push.body, |
There was a problem hiding this comment.
Push-sink mode logs the complete notification title, agent/user-controlled body, and data object. Redacting only the device token still allows sensitive session content, including credentials or private customer data, to be retained in local, CI, or aggregated Worker logs. Please keep console observability content-free (for example identifiers, outcome, fixed discriminators, and payload sizes), or route full-payload assertions through a dedicated test-only recorder with controlled storage and lifecycle.
Add a user_notification_preferences table (user_id PK/FK, agent_push_enabled default true) with migration 0190, plus getNotificationPreferences and setNotificationPreferences tRPC procedures scoped per user. Clean up the preference row in the soft-deletion path since soft delete has no FK cascade.
Add sendAgentSessionNotification RPC that resolves the session and title server-side, reads the per-user agent push preference (fail closed), and dispatches with presence context and a per-session sliding-window rate limit. NotificationChannelDO gains the rate-limit gate (prune-on-read, suppressed_rate_limit outcome, rl: GC via the cleanup alarm) and a default-off local push sink that records a token-redacted payload without calling Expo for local E2E. The existing sendCloudAgentSessionNotification contract is unchanged.
Add an Agent notifications row to the notifications card, bound to the per-user preference via getNotificationPreferences with an optimistic setNotificationPreferences mutation. The switch is editable whenever the preference query resolves, independent of this device's OS permission or push-token registration, since the preference is per-user and cross-device. Toggle cache and derived-state logic is extracted into a unit-tested hook.
Accept the new agent_notification ingest item on both the queue and direct paths, turn it into an AttentionSignal carrying the notification id and full message, and dispatch it via the new sendAgentSessionNotification RPC without the live-CLI gate (the user explicitly asked for the ping) while keeping root-session eligibility. A durable per-identity dispatch marker makes delivery replay-safe and loss-safe: the signal re-emits while pending and is marked dispatched only on a terminal local decision, so a thrown RPC or a failed session lookup leaves it pending rather than dropping or double-sending.
- NotificationChannelDO: skip rate-limit consumption on retries and write a pending idempotency marker for rate-limited no-badge dispatches so a replay of the same notification does not re-consume a rate-limit slot; report the sink tokenCount as tokens.length - mobile: guard getNotificationPreferences with enabled: isAuthenticated so a logged-out user does not hit a spurious error row - session-ingest: correct the agent_notification identity separator in a doc comment - mobile tests: make the preference-hook cases assert distinct real behaviors
…tion gate After rebasing onto main, agent_notification signals must bypass BOTH the per-user rollout gate and the live-CLI gate (explicit notify_user request), while legacy completed/needs_input signals remain gated. Regenerate the packages/db (0192) and session-ingest (0004) migrations against main since both original migrations collided with numbers already merged to main.
9bdcda1 to
99f1511
Compare
Summary
Adds an agent-callable push notification path so a
notify_usertool in the kilo CLI can send a push to the user's phone (Kilo mobile app) for explicitly requested pings and significant mid-run milestones. The platform already auto-notifies on needs-input and completion; this path is deliberately separate and gated so it cannot duplicate or bypass those.This is the cloud half. The CLI tool itself is in the companion kilocode PR (see Cross-repo dependency).
What's here
packages/db,apps/web): newuser_notification_preferencestable (per-user,agent_push_enableddefault true), migration0190,getNotificationPreferences/setNotificationPreferencesprocedures (per-user scoped), and cleanup of the row in the soft-deletion path.packages/notifications,services/notifications): newsendAgentSessionNotificationRPC that resolves the session + title server-side, reads the preference (fail-closed), and dispatches with presence context and a per-session sliding-window rate limit (5 / 600s) inNotificationChannelDO(suppressed_rate_limitoutcome, prune-on-read,rl:GC via the cleanup alarm). A default-off local push sink (PUSH_SINK_MODE, absent fromwrangler.jsonc) records a token-redacted payload without calling Expo, for local E2E. The existingsendCloudAgentSessionNotificationcontract is unchanged.services/session-ingest): accepts the newagent_notificationitem on both ingest paths, turns it into an AttentionSignal (carrying the notification id + full message), and dispatches via the new RPC without the live-CLI gate (the user explicitly asked for the ping) while keeping root-session eligibility. A durable per-identity dispatch marker makes delivery replay-safe and loss-safe.apps/mobile): an "Agent notifications" row in the notifications card bound to the preference with an optimistic mutation; the switch stays editable independent of this device's OS permission / push-token registration (per-user, cross-device).Verification
delivered; preference OFF →suppressed_preference/ ON →delivered(effective immediately); presence foreground →suppressed_presence/ background →delivered; per-session rate limit 5 dispatched, 6thsuppressed_rate_limit; tool-in-disconnected-session returns friendly failure text; the "Agent notifications" row is locatable by its accessibility label and its layout stays intact at accessibility text sizes.datareusestype: 'cloud_agent_session'so tap-routing to/(app)/agent-chat/<cliSessionId>uses the existing, unchanged deep-link map. Real-device banner delivery should be confirmed before production release.Cross-repo dependency
The
notify_userCLI tool lives in the kilocode PR (feat/agent-push-tool). Contracts (ingest item shape, RPC shape, tool policy text) are pinned across both PRs.Deploy ordering (§9.1)
DB migration → notifications worker → session-ingest worker → web (tRPC) / mobile app → then the kilocode CLI release. The kilocode CLI must not be released before this cloud deploy. session-ingest degrades gracefully if the RPC is not yet deployed (the dispatch error is caught only at the outer boundary; ingest never fails). Old servers handle the new item per existing unknown-item behavior; app versions without the toggle simply never write a preference row (default ON).
This PR is stacked on #4605; GitHub will retarget it to
mainwhen #4605 merges.