Skip to content

feat(notifications): agent-callable push notification tool (cloud + mobile)#4627

Merged
iscekic merged 8 commits into
mainfrom
feat/agent-push-tool
Jul 21, 2026
Merged

feat(notifications): agent-callable push notification tool (cloud + mobile)#4627
iscekic merged 8 commits into
mainfrom
feat/agent-push-tool

Conversation

@iscekic

@iscekic iscekic commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds an agent-callable push notification path so a notify_user tool 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

  • Preference storage + tRPC (packages/db, apps/web): new user_notification_preferences table (per-user, agent_push_enabled default true), migration 0190, getNotificationPreferences / setNotificationPreferences procedures (per-user scoped), and cleanup of the row in the soft-deletion path.
  • Notifications service (packages/notifications, services/notifications): new sendAgentSessionNotification RPC 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) in NotificationChannelDO (suppressed_rate_limit outcome, prune-on-read, rl: GC via the cleanup alarm). A default-off local push sink (PUSH_SINK_MODE, absent from wrangler.jsonc) records a token-redacted payload without calling Expo, for local E2E. The existing sendCloudAgentSessionNotification contract is unchanged.
  • session-ingest (services/session-ingest): accepts the new agent_notification item 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.
  • Mobile toggle (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

  • Unit/integration: web router + deletion tests; notifications service tests (preference gate, rate-limit boundaries, DO-outcome→RPC mapping incl. thrown-DO propagation, sink off-by-default); session-ingest tests (item parse, identity, dispatch-marker lifecycle, both ingest paths, error boundary); mobile pure-hook tests.
  • Local iOS E2E against the full local stack: verified end-to-end the tool → ingest → signal → RPC (all gates) → DO → local sink recorded payload (title = session title, body = message, token redacted) with outcome delivered; preference OFF → suppressed_preference / ON → delivered (effective immediately); presence foreground → suppressed_presence / background → delivered; per-session rate limit 5 dispatched, 6th suppressed_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.
  • Real APNs banner delivery is not applicable on the iOS simulator (the dev-client cannot obtain notification authorization non-interactively); the notification data reuses type: '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_user CLI 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 main when #4605 merges.

@iscekic iscekic self-assigned this Jul 20, 2026
Comment thread services/notifications/src/dos/NotificationChannelDO.ts Outdated
Comment thread services/notifications/src/dos/NotificationChannelDO.ts Outdated
Comment thread apps/mobile/src/components/notifications-card.tsx Outdated
Comment thread services/session-ingest/src/db/sqlite-schema.ts
Comment thread apps/mobile/src/lib/hooks/agent-push-preference.test.ts Outdated
Comment thread apps/mobile/src/lib/hooks/agent-push-preference.test.ts Outdated
@kilo-code-bot

kilo-code-bot Bot commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Executive Summary

Reviewed 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 (2653bcf6f); no new high-confidence security, runtime, logic, or breaking-API issues were found in changed code.

Files Reviewed (39 files)
  • apps/mobile/src/components/notifications-card.tsx
  • apps/mobile/src/lib/hooks/agent-push-preference.ts
  • apps/mobile/src/lib/hooks/agent-push-preference.test.ts
  • apps/web/src/lib/user/index.ts
  • apps/web/src/lib/user/index.test.ts
  • apps/web/src/routers/user-router.ts
  • apps/web/src/routers/user-router.test.ts
  • packages/db/src/migrations/0192_big_doctor_doom.sql
  • packages/db/src/migrations/meta/0192_snapshot.json
  • packages/db/src/migrations/meta/_journal.json
  • packages/db/src/schema.ts
  • packages/notifications/src/rpc-schemas.ts
  • services/notifications/.dev.vars.example
  • services/notifications/src/__tests__/dispatch-push.test.ts
  • services/notifications/src/bindings.d.ts
  • services/notifications/src/dos/NotificationChannelDO.ts
  • services/notifications/src/index.ts
  • services/notifications/src/lib/agent-session-notification-push.ts
  • services/notifications/src/lib/agent-session-notification-push.test.ts
  • services/notifications/src/lib/cloud-agent-session-push.ts
  • services/notifications/src/lib/push-sink.ts
  • services/notifications/src/lib/push-sink.test.ts
  • services/session-ingest/drizzle/0004_giant_deathbird.sql
  • services/session-ingest/drizzle/meta/0004_snapshot.json
  • services/session-ingest/drizzle/meta/_journal.json
  • services/session-ingest/drizzle/migrations.js
  • services/session-ingest/src/db/sqlite-schema.ts
  • services/session-ingest/src/dos/SessionIngestDO.ts
  • services/session-ingest/src/dos/SessionIngestDO-agent-notification.test.ts
  • services/session-ingest/src/dos/session-ingest-attention.ts
  • services/session-ingest/src/ingest/direct-ingest.ts
  • services/session-ingest/src/ingest/direct-ingest.test.ts
  • services/session-ingest/src/notifications-binding.ts
  • services/session-ingest/src/queue-consumer.ts
  • services/session-ingest/src/queue-consumer.test.ts
  • services/session-ingest/src/remote-session-notifications.ts
  • services/session-ingest/src/remote-session-notifications.test.ts
  • services/session-ingest/src/types/session-sync.ts
  • services/session-ingest/src/types/session-sync.test.ts
  • services/session-ingest/src/util/compaction.ts
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 Summary

All 6 previously reported issues (rate-limit double-consumption, hardcoded sink tokenCount, missing enabled: isAuthenticated guard, wrong identity separator in a doc comment, and two misleading tests) are fixed in 1828f06f6 and verified against current code, with new test coverage for the rate-limit retry fix.

Files Reviewed (5 files)
  • apps/mobile/src/components/notifications-card.tsx
  • apps/mobile/src/lib/hooks/agent-push-preference.test.ts
  • services/notifications/src/__tests__/dispatch-push.test.ts
  • services/notifications/src/dos/NotificationChannelDO.ts
  • services/session-ingest/src/db/sqlite-schema.ts

Previous review (commit 98a5358)

Status: 6 Issues Found | Recommendation: Address before merge

Executive Summary

The highest-risk issue is a rate-limit accounting bug in NotificationChannelDO.checkAndConsumeRateLimit that can let retried agent-notification sends prematurely exhaust the per-session push quota.

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 4
Issue Details (click to expand)

WARNING

File Line Issue
services/notifications/src/dos/NotificationChannelDO.ts 99 New rate-limit check consumes quota before any idempotency marker exists for non-badge (agent-notification) dispatches; a retry after a transient Expo failure re-consumes a rate-limit slot for the same logical notification
apps/mobile/src/components/notifications-card.tsx 102 New getNotificationPreferences query lacks the enabled: isAuthenticated guard used by the sibling getMyPushTokens query, causing a failed query/permanent error row for logged-out users

SUGGESTION

File Line Issue
services/notifications/src/dos/NotificationChannelDO.ts 165 Local push-sink tokenCount hardcoded to 1 instead of tokens.length
services/session-ingest/src/db/sqlite-schema.ts 33 Doc comment gives the wrong separator (: vs /) for the agent_notification identity format
apps/mobile/src/lib/hooks/agent-push-preference.test.ts 37 Test claims to cover a "query error" state that deriveAgentPushEditable cannot express and duplicates an existing case
apps/mobile/src/lib/hooks/agent-push-preference.test.ts 108 Test assertion is identical to an earlier test and doesn't add coverage for the claimed behavior
Files Reviewed (37 files)
  • apps/mobile/src/components/notifications-card.tsx - 1 issue
  • apps/mobile/src/lib/hooks/agent-push-preference.ts
  • apps/mobile/src/lib/hooks/agent-push-preference.test.ts - 2 issues
  • apps/web/src/lib/user/index.ts
  • apps/web/src/lib/user/index.test.ts
  • apps/web/src/routers/user-router.ts
  • apps/web/src/routers/user-router.test.ts
  • packages/db/src/schema.ts
  • packages/db/src/migrations/0190_sudden_meltdown.sql
  • packages/db/src/migrations/meta/0190_snapshot.json
  • packages/db/src/migrations/meta/_journal.json
  • packages/notifications/src/rpc-schemas.ts
  • services/notifications/.dev.vars.example
  • services/notifications/src/index.ts
  • services/notifications/src/bindings.d.ts
  • services/notifications/src/dos/NotificationChannelDO.ts - 2 issues
  • services/notifications/src/lib/agent-session-notification-push.ts
  • services/notifications/src/lib/agent-session-notification-push.test.ts
  • services/notifications/src/lib/cloud-agent-session-push.ts
  • services/notifications/src/lib/push-sink.ts
  • services/notifications/src/lib/push-sink.test.ts
  • services/notifications/src/__tests__/dispatch-push.test.ts
  • services/session-ingest/drizzle/0003_agent_notification_dispatch.sql
  • services/session-ingest/drizzle/meta/0003_snapshot.json
  • services/session-ingest/drizzle/meta/_journal.json
  • services/session-ingest/drizzle/migrations.js
  • services/session-ingest/src/db/sqlite-schema.ts - 1 issue
  • services/session-ingest/src/dos/SessionIngestDO.ts
  • services/session-ingest/src/dos/SessionIngestDO-agent-notification.test.ts
  • services/session-ingest/src/dos/session-ingest-attention.ts
  • services/session-ingest/src/ingest/direct-ingest.ts
  • services/session-ingest/src/ingest/direct-ingest.test.ts
  • services/session-ingest/src/notifications-binding.ts
  • services/session-ingest/src/queue-consumer.ts
  • services/session-ingest/src/queue-consumer.test.ts
  • services/session-ingest/src/remote-session-notifications.ts
  • services/session-ingest/src/remote-session-notifications.test.ts
  • services/session-ingest/src/types/session-sync.ts
  • services/session-ingest/src/types/session-sync.test.ts
  • services/session-ingest/src/util/compaction.ts
  • services/session-ingest/src/util/compaction.test.ts

Fix these issues in Kilo Cloud


Reviewed by claude-sonnet-5 · Input: 78 · Output: 26.1K · Cached: 3.1M

Review guidance: REVIEW.md from base branch main

Comment on lines +10 to +16
migrations: {
m0000,
m0001,
m0002,
m0003,
m0004
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Base automatically changed from feature/mobile-agent-attention-notifications to main July 21, 2026 12:25
iscekic added 7 commits July 21, 2026 15:22
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.
@iscekic
iscekic force-pushed the feat/agent-push-tool branch from 9bdcda1 to 99f1511 Compare July 21, 2026 13:35
@iscekic
iscekic merged commit a7d42aa into main Jul 21, 2026
63 checks passed
@iscekic
iscekic deleted the feat/agent-push-tool branch July 21, 2026 13:57
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.

2 participants