Skip to content

Mobile Push Relay Platform#705

Merged
arul28 merged 1 commit into
mainfrom
ade/mobile-push-relay-platform-c1ebe009
Jul 5, 2026
Merged

Mobile Push Relay Platform#705
arul28 merged 1 commit into
mainfrom
ade/mobile-push-relay-platform-c1ebe009

Conversation

@arul28

@arul28 arul28 commented Jul 5, 2026

Copy link
Copy Markdown
Owner

Mobile Push & Relay Platform

Cloud connectivity for ADE mobile: APNs push + Live Activities, a Cloudflare push relay, a Durable Objects WebSocket tunnel relay, DPoP pairing hardening, one-QR smart pairing, and an offline pending-chat row.

What's here

  • apps/push-relay/ (new CF Worker, deployed) — D1 device registrations, machine-claim + HMAC-signed API, APNs HTTP/2 (ES256 .p8 JWT), phase TTLs (2h running / 24h waiting), content-hash suppression, dead-token cleanup, Live Activity start/update/end routing (input-push-token minting, 10-min stale default).
  • Brain publisher (apps/ade-cli/src/services/push/) — subscribes agentChatService events, ptyService.onExit, and prPollingService pr-notifications; debounced + double-deduped; per-device prefs (mute, quiet hours in device timezone) enforced brain-side; push.* sync commands for phone registration.
  • iOS — remote-notification registration after pairing, alert pushes with ade:// deep links, ADEAgentRunsAttributes Live Activity (lock screen + Dynamic Island, ≤3 runs) with push-to-start + foreground re-registration + orphan reaping, and a Settings "Push delivery" panel (status, toggles, quiet hours).
  • apps/tunnel-relay/ (new DO Worker, deployed) — phone→relay→brain WS tunnel (per-connection pipe sockets, early-frame buffering, idle sweep); brain tunnel client with backoff; "Cloud relay fallback" toggle (desktop + iOS, default off); relay advertised as lowest-priority address candidate.
  • DPoP pairing hardening — Secure Enclave P-256 key registered at pairing; every paired hello signs a fresh secret-bound challenge (nonce + timestamp); TOFU upgrade for legacy devices; fail-closed once a key is on record; bootstrap-token downgrade blocked; requireDpop machine setting (+ ade sync security require-dpop).
  • One-QR smart pairing — desktop QR encodes https://ade-app.dev/pair#<payload> (identity + port + candidates + relay URL); iOS scanner: already-paired → instant reconnect, new machine → PIN-only.
  • Offline outbox — queued chat.create persists a creation snapshot and renders a pending Work-list row that reconciles on drain.

Verification

  • ade-cli 1507 tests, desktop sync suites (208), push-relay 8, tunnel-relay 15 — all green; typechecks clean across desktop/ade-cli/workers.
  • iOS app+widget builds clean (iPhone 17 Pro sim); 12 new Swift unit tests.
  • Tunnel e2e smoke verified against production (phone→relay→pipe→echo).
  • Quality gate: dual-track review + fixes applied (incl. requireDpop brain-ingress enforcement); cross-app HMAC golden vectors pinned in both apps.

Needs a physical device / operator

  • Real APNs delivery, Live Activity push-to-start, background LA updates (sim can't).
  • APNs auth key upload: wrangler secret put APNS_KEY / APNS_KEY_ID / APNS_TEAM_ID in apps/push-relay (relay /health reports apnsConfigured:false until then).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added cloud relay fallback for sync connections, including updated pairing QR handling, desktop/mobile UI, and an iOS pairing-code scanner flow.
    • Introduced DPoP-based paired authentication controls (including optional enforcement) plus new CLI commands for relay status and DPoP requirements.
    • Added push relay–backed notifications with quiet-hours gating and Live Activity updates, plus offline “pending sync” chat-creation handling.
  • Bug Fixes
    • Hardened parsing/validation and relay/security command handling; improved sync reconnect behavior and pairing/auth posture checks.
  • Chores / Tests
    • Expanded CI coverage for push-relay and tunnel-relay, along with additional unit tests.

Greptile Summary

This PR adds mobile push delivery and cloud relay support for ADE. The main changes are:

  • New Cloudflare push relay with D1 device registration, HMAC-signed machine APIs, APNs alert delivery, and Live Activity routing.
  • New brain-side push publisher for agent, terminal, and PR notifications with quiet-hours and per-device preference handling.
  • New Durable Object tunnel relay plus brain tunnel client for phone-to-brain WebSocket fallback.
  • DPoP pairing hardening with Secure Enclave key registration, nonce-bound hello proofs, TOFU upgrades, and requireDpop controls.
  • Smart pairing QR payloads with direct and relay candidates across desktop and iOS.
  • iOS push registration, Live Activity support, settings UI, deep links, and offline pending-chat handling.

Confidence Score: 5/5

This PR appears safe to merge from the reviewed changed paths.

No review findings were identified in the core push relay, tunnel relay, DPoP authentication, push publisher, QR, and iOS routing changes. Previously reported issues are addressed in the current diff, including relay buffer overflow closure, pre-open pipe buffering, delivery outcome checks, and post-delivery suppression recording.

No files require special attention.

T-Rex T-Rex Logs

What T-Rex did

  • Ran the tunnel-relay unit test suite for the npm package in apps/tunnel-relay and confirmed all tests passed (1 test file, 15 tests), with exit code 0.
  • Executed the tunnel-relay smoke path by starting the dev server and running the smoke script; the run completed with exit code 0 and reported a successful SMOKE PASS for the phone → relay → pipe → echo → phone round trip, including the wrangler handshake '101 Switching Protocols'.

View all artifacts

T-Rex Ran code and verified through T-Rex

Important Files Changed

Filename Overview
apps/push-relay/src/relay.ts Adds the D1-backed push relay request handling, APNs delivery routing, HMAC authorization, suppression, and token cleanup.
apps/push-relay/src/apns.ts Implements APNs JWT creation and push sending with timeout/error handling.
apps/tunnel-relay/src/tunnelDo.ts Adds the Durable Object WebSocket pairing and forwarding relay with bounded pre-pipe buffering and idle cleanup.
apps/ade-cli/src/services/sync/syncTunnelClientService.ts Adds the brain-side tunnel client with signed host and pipe sockets, reconnect backoff, and pre-open buffering.
apps/ade-cli/src/services/sync/syncDpop.ts Adds DPoP challenge verification, nonce replay protection, and paired-hello policy.
apps/ade-cli/src/services/push/pushPublisherService.ts Adds push publishing from chat, PTY, and PR events with preference gating, Live Activity aggregation, dedupe, and retry handling.
apps/ios/ADE/Services/SyncService.swift Updates iOS sync routing for full WebSocket relay URLs and offline command queuing behavior.
apps/desktop/src/shared/pairingQr.ts Adds smart pairing QR encoding and parsing with relay candidates.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
  participant Phone as iOS phone
  participant Brain as ADE brain
  participant Push as Cloudflare push relay
  participant APNs as APNs
  participant Tunnel as Tunnel Durable Object

  Phone->>Brain: Pair via QR/PIN and register DPoP public key
  Phone->>Brain: push.registerDevice / prefs / activity token
  Brain->>Push: Claim machine and HMAC-signed device registration
  Brain->>Push: Publish alert / Live Activity updates
  Push->>APNs: APNs HTTP/2 alert or liveactivity push
  APNs-->>Phone: Notification / Live Activity update
  Brain->>Tunnel: Signed host control WebSocket
  Phone->>Tunnel: WebSocket connect using relay candidate
  Tunnel-->>Brain: open(id)
  Brain->>Tunnel: Signed pipe WebSocket
  Phone<<->>Brain: ADE sync protocol frames relayed byte-for-byte
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
  participant Phone as iOS phone
  participant Brain as ADE brain
  participant Push as Cloudflare push relay
  participant APNs as APNs
  participant Tunnel as Tunnel Durable Object

  Phone->>Brain: Pair via QR/PIN and register DPoP public key
  Phone->>Brain: push.registerDevice / prefs / activity token
  Brain->>Push: Claim machine and HMAC-signed device registration
  Brain->>Push: Publish alert / Live Activity updates
  Push->>APNs: APNs HTTP/2 alert or liveactivity push
  APNs-->>Phone: Notification / Live Activity update
  Brain->>Tunnel: Signed host control WebSocket
  Phone->>Tunnel: WebSocket connect using relay candidate
  Tunnel-->>Brain: open(id)
  Brain->>Tunnel: Signed pipe WebSocket
  Phone<<->>Brain: ADE sync protocol frames relayed byte-for-byte
Loading

Reviews (8): Last reviewed commit: "Mobile push & relay platform: APNs + Liv..." | Re-trigger Greptile

@cursor

cursor Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@vercel

vercel Bot commented Jul 5, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
ade Ignored Ignored Preview Jul 5, 2026 6:22pm

@coderabbitai

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This PR adds push-relay and tunnel-relay workers, expands ADE CLI sync/push/DPoP/cloud-relay support, and updates desktop and iOS clients for pairing, notifications, Live Activities, and offline chat state.

Changes

CI and repository plumbing

Layer / File(s) Summary
Workflow and install updates
.github/workflows/ci.yml, .gitignore, package.json
Extends workspace installs, cache keys, and CI jobs for apps/push-relay and apps/tunnel-relay, and keeps their lockfiles tracked.

Push Relay Cloudflare Worker

Layer / File(s) Summary
Worker config and schema
apps/push-relay/package.json, apps/push-relay/tsconfig.json, apps/push-relay/wrangler.jsonc, apps/push-relay/migrations/0001_push_registrations.sql, apps/push-relay/README.md
Adds the worker package/config, D1 schema, and worker documentation.
APNs client
apps/push-relay/src/apns.ts
Implements APNs JWT creation, caching, and push sending with invalid-token handling.
Relay request handling
apps/push-relay/src/relay.ts, apps/push-relay/src/index.ts
Implements machine auth, device/live-activity persistence, suppression, APNs delivery, routing, and worker entrypoint wiring.
Tests
apps/push-relay/test/relay.test.ts
Adds D1-backed relay tests for signing, claim, registration, publish, suppression, and live-activity behavior.

Tunnel Relay Cloudflare Worker

Layer / File(s) Summary
Worker config and docs
apps/tunnel-relay/package.json, apps/tunnel-relay/tsconfig.json, apps/tunnel-relay/wrangler.jsonc, apps/tunnel-relay/.gitignore, apps/tunnel-relay/README.md
Adds the worker package/config and documents the tunnel relay architecture and constraints.
Signed routing and Durable Object handling
apps/tunnel-relay/src/relay.ts, apps/tunnel-relay/src/index.ts, apps/tunnel-relay/src/tunnelDo.ts
Implements route parsing, HMAC verification, connection ids, the worker entrypoint, and the tunnel Durable Object lifecycle.
Tests and smoke test
apps/tunnel-relay/test/relay.test.ts, apps/tunnel-relay/test/smoke.mjs
Adds unit tests and an end-to-end echo smoke test.

ADE CLI push, DPoP, and cloud relay

Layer / File(s) Summary
Shared push DTOs
apps/desktop/src/shared/types/push.ts
Defines shared push registration, delivery status, and action-name types used by CLI and desktop.
Push runtime
apps/ade-cli/src/services/push/pushRegistrationStore.ts, apps/ade-cli/src/services/push/pushRelayClient.ts, apps/ade-cli/src/services/push/pushPublisherService.ts, apps/ade-cli/src/services/push/pushPublisherService.test.ts
Implements the push registration store, signed relay client, publisher service, and tests.
Bootstrap wiring
apps/ade-cli/src/bootstrap.ts
Starts the push publisher and cloud relay tunnel client and wires PR notification fanout.
DPoP and security state
apps/ade-cli/src/services/sync/syncDpop.ts, apps/ade-cli/src/services/sync/syncDpop.test.ts, apps/ade-cli/src/services/sync/syncPairingStore.ts, apps/ade-cli/src/services/sync/syncSecurityStore.ts, apps/ade-cli/src/services/sync/brainProjectActionsSyncHandler.ts, apps/ade-cli/src/services/sync/syncHostService.ts
Implements DPoP proof verification, pairing-store key persistence, security posture storage, and DPoP-aware pairing/hello handling.
Cloud relay store and tunnel client
apps/ade-cli/src/services/sync/syncCloudRelayStore.ts, apps/ade-cli/src/services/sync/syncTunnelClientService.ts, apps/ade-cli/src/services/sync/syncTunnelClientService.test.ts
Persists relay identity/config and maintains signed control/pipe tunnel connections.
Runtime wiring, RPC, and commands
apps/ade-cli/src/services/sync/syncRemoteCommandService.ts, apps/ade-cli/src/services/sync/syncService.ts, apps/ade-cli/src/multiProjectRpcServer.ts, apps/ade-cli/src/cli.ts, apps/ade-cli/src/cli.test.ts, apps/ade-cli/README.md
Registers push commands and exposes security/cloud-relay sync APIs and CLI commands/docs.

Desktop cloud relay UI, IPC, and pairing QR

Layer / File(s) Summary
Shared types and IPC registry
apps/desktop/src/shared/types/sync.ts, apps/desktop/src/shared/ipc.ts, apps/desktop/src/shared/types/push.ts
Adds relay candidate, DPoP, cloud relay status, and pairing QR types plus new IPC channels.
Main and preload IPC wiring
apps/desktop/src/main/services/ipc/registerIpc.ts, apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.ts, apps/desktop/src/preload/global.d.ts, apps/desktop/src/preload/preload.ts, apps/desktop/src/main/services/shared/utils.ts
Exposes cloud relay status and enable/disable calls through main-process handlers and preload APIs.
Pairing QR codec and tests
apps/desktop/src/shared/pairingQr.ts, apps/desktop/src/shared/pairingQr.test.ts
Encodes and parses pairing QR payloads with validation and round-trip tests.
Sync devices settings UI
apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx
Adds cloud relay status refresh/toggle, QR pairing display, and connection details to the settings panel.

iOS push, Live Activities, QR pairing, and offline chat

Layer / File(s) Summary
App setup and notification bridge
apps/ios/ADE/App/*.swift, apps/ios/ADE/ADE.entitlements, apps/ios/ADE/Info.plist
Adds APNs, camera, Live Activity, and background capabilities and bridges notification callbacks into PushNotificationService.
DPoP key and pairing QR models
apps/ios/ADE/Services/DpopKeyService.swift, apps/ios/ADE/Services/PairingQrPayload.swift, apps/ios/ADE/Models/RemoteModels.swift
Adds Secure Enclave/software-backed DPoP keys and QR payload parsing/model storage.
Push and Live Activity services
apps/ios/ADE/Services/PushNotificationService.swift, apps/ios/ADE/Services/LiveActivityService.swift, apps/ios/ADE/Shared/ADEAgentActivityAttributes.swift, apps/ios/ADEWidgets/*.swift
Implements push registration, preferences/status sync, Live Activity observation/reporting, shared run-state attributes, and widget rendering.
SyncService relay, pairing, DPoP, and offline chat
apps/ios/ADE/Services/SyncService.swift
Adds relay-aware routing, QR relay candidates, DPoP hello handling, cloud relay fallback, and pending chat creation persistence.
Settings UI
apps/ios/ADE/Views/Settings/*.swift
Adds the cloud relay toggle, QR scanner sheet, push delivery section, and QR-aware pairing/pin routes.
Work screens offline chat UI
apps/ios/ADE/Views/Work/*.swift
Renders pending-sync sessions and handles offline chat creation actions.
Tests
apps/ios/ADETests/*.swift
Adds websocket URL, pairing QR, and DPoP verification coverage.

Estimated code review effort: 5 (Critical) | ~150 minutes

Possibly related PRs

  • arul28/ADE#579: Both PRs modify apps/ade-cli/src/services/sync/brainProjectActionsSyncHandler.ts, specifically extending the brain sync connection handshake/auth logic around hello/pairing_request payload parsing and validation.
  • arul28/ADE#592: This PR wires new push-relay publishing into ADE runtime/sync command plumbing, and the retrieved PR removes older push/notification wiring in the same integration points.

Suggested labels: ci, desktop, ios

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and captures the core mobile push/relay expansion, even though the PR also includes broader sync and pairing work.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ade/mobile-push-relay-platform-c1ebe009

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@arul28

arul28 commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

@copilot review but do not make fixes

Comment thread apps/ade-cli/src/services/sync/syncTunnelClientService.ts Outdated
@arul28 arul28 force-pushed the ade/mobile-push-relay-platform-c1ebe009 branch from ddf9e3d to d0e1451 Compare July 5, 2026 15:27
Comment thread apps/push-relay/src/relay.ts Outdated
@arul28 arul28 force-pushed the ade/mobile-push-relay-platform-c1ebe009 branch from d0e1451 to 430acac Compare July 5, 2026 15:46
@arul28

arul28 commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 15

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
apps/ios/ADE/Views/Work/WorkRootComponents.swift (1)

1066-1085: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

VoiceOver announces "running" for pending-sync rows.

accessibilityLabel still derives its status text from sessionStatusLabel(for: status), but pending-sync optimistic sessions are built with status: "running" (see WorkStatusAndFormattingHelpers.swift line 37). VoiceOver users will hear an active/running status while sighted users see "Pending sync" — a real mismatch introduced by this change.

♿️ Proposed fix
   var accessibilityLabel: String {
-    var parts = [chatSummary?.title ?? session.title, session.laneName, sessionStatusLabel(for: status)]
+    var parts = [
+      chatSummary?.title ?? session.title,
+      session.laneName,
+      isPendingSyncCreation ? "pending sync" : sessionStatusLabel(for: status)
+    ]
     if session.pinned {
       parts.append("pinned")
     }
     if isArchived {
       parts.append("archived")
     }
     return parts.joined(separator: ", ")
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Views/Work/WorkRootComponents.swift` around lines 1066 - 1085,
The accessibility label for pending-sync rows is still using the normal status
text, so VoiceOver announces “running” instead of the visible pending-sync
state. Update `accessibilityLabel` in `WorkRootComponents` to special-case
`isPendingSyncCreation` and use a pending-sync-friendly label rather than
`sessionStatusLabel(for: status)`, while leaving the existing pinned/archived
parts unchanged.
apps/ios/ADE/Services/SyncService.swift (1)

4089-4099: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Await the unregister before disconnecting
apps/ios/ADE/Services/SyncService.swift:4089-4093
handleUnpair() launches push.unregisterDevice in a Task, then disconnect(clearCredentials:) tears down the socket immediately. sendPushCommand only sends while canSendLiveRequests() is true, so this unregister can be dropped before it reaches the relay.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Services/SyncService.swift` around lines 4089 - 4099,
`forgetHost()` is disconnecting before the unpair/unregister request has a
chance to be sent, so the relay cleanup can be dropped. Update the flow so
`PushNotificationService.shared.handleUnpair()` is awaited or otherwise
completed before calling `disconnect(clearCredentials:)`, and keep the rest of
the teardown in `forgetHost()` afterward. Use the `handleUnpair()` path in
`PushNotificationService` and the `disconnect(clearCredentials:)` call in
`SyncService` as the key points to adjust.
🧹 Nitpick comments (10)
apps/tunnel-relay/README.md (1)

118-122: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Personal account name embedded in public docs.

The deploy example hardcodes ade-tunnel-relay.arulsharma1028.workers.dev, exposing a personal identifier in a checked-in README rather than a generic placeholder.

✏️ Suggested fix
-curl https://ade-tunnel-relay.arulsharma1028.workers.dev/health
+curl https://ade-tunnel-relay.<your-workers-subdomain>.workers.dev/health
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/tunnel-relay/README.md` around lines 118 - 122, The deploy example in
the README hardcodes a personal worker subdomain in the curl URL, which should
be replaced with a generic placeholder or example domain. Update the example
near the deploy/health-check snippet to use a neutral identifier derived from
the app name instead of the embedded personal account name, keeping the rest of
the workflow unchanged.
apps/tunnel-relay/package.json (1)

16-16: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Vitest is still several majors behind. apps/tunnel-relay/package.json pins vitest@^0.34.6; the relay packages are already aligned on that version, but it’s far behind current Vitest 4.x and will need a migration plan to upgrade cleanly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/tunnel-relay/package.json` at line 16, The tunnel-relay package is still
pinned to an old Vitest version, so update the vitest dependency in the
package.json for the relay app to match the newer workspace standard and plan
the migration to a current 4.x release. Check any related test config or scripts
that reference Vitest directly so they remain compatible with the newer version.
apps/ade-cli/src/multiProjectRpcServer.ts (1)

770-776: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Inconsistent response shape for sync.getRequireDpop/sync.setRequireDpop.

Every other simple-scalar getter/setter in this handler wraps its primitive in a named field — e.g. sync.getPin returns { pin: ... } (Line 733) and sync.getRuntimeName returns { runtimeName: ... } (Line 750) — but sync.getRequireDpop/sync.setRequireDpop return a bare boolean. This diverges from the established RPC response convention and may not render correctly wherever the CLI/desktop generically expects an object with named fields (e.g., --text/--json formatting, or future desktop consumers keyed on a field name).

♻️ Proposed fix: wrap for consistency
     if (method === "sync.getRequireDpop") {
-      return (await getSyncService()).getRequireDpop();
+      return { requireDpop: (await getSyncService()).getRequireDpop() };
     }

     if (method === "sync.setRequireDpop") {
-      return (await getSyncService()).setRequireDpop(params.requireDpop === true);
+      return {
+        requireDpop: (await getSyncService()).setRequireDpop(params.requireDpop === true),
+      };
     }

Note: this also requires updating syncService.test.ts/cli.ts consumers if the shape changes (cli.test.ts currently expects the plan steps, not the RPC result shape, so should be unaffected).

Please verify how cli.ts renders --text output for this step's result to confirm the raw boolean isn't silently mis-rendered.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ade-cli/src/multiProjectRpcServer.ts` around lines 770 - 776, The
sync.getRequireDpop and sync.setRequireDpop branches in multiProjectRpcServer.ts
return a bare boolean, unlike the other getter/setter handlers such as
sync.getPin and sync.getRuntimeName that wrap primitives in named fields. Update
the getSyncService() call sites so both methods return an object with a stable
field name (for example matching the existing RPC convention), and then adjust
any tests or consumers that assert the raw boolean shape, including
syncService.test.ts if needed. Also verify cli.ts text/json rendering still
reads the new wrapped response correctly so the output remains consistent with
the other sync RPCs.
apps/ios/ADE/Views/Settings/SettingsPairingScannerSheet.swift (1)

270-273: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: SwiftLint flags override class varoverride static var in this final class.

Since PreviewView is final, SwiftLint's static_over_final_class rule suggests using static instead of class for the layerClass override. This is a style-only change with no behavior impact; there are also known false-positive reports for this rule on overrides of inherited class properties, so it's worth a quick compile check before applying.

💡 Proposed fix
-    override class var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
+    override static var layerClass: AnyClass { AVCaptureVideoPreviewLayer.self }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Views/Settings/SettingsPairingScannerSheet.swift` around lines
270 - 273, SwiftLint is flagging the `PreviewView` override because the class is
`final`, so update the `layerClass` declaration in `PreviewView` from a class
override to the `static` form SwiftLint expects, while keeping the
`previewLayer` accessor unchanged. Since this is a style-only
`static_over_final_class` change, do a quick compile check after adjusting the
`layerClass` symbol to confirm the inherited `UIView` override still behaves
correctly.

Source: Linters/SAST tools

apps/ios/ADE/Views/Settings/SettingsPushDeliverySection.swift (1)

30-32: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

"Enable notifications" prompt shows before pairing, but tapping it silently no-ops.

needsPermissionPrompt only checks permissionStatus, so on an unpaired device the panel shows both "Pair a machine first" (Status row) and an actionable "Enable notifications" button. Per PushNotificationService.enableIfPaired() (provided snippet), tapping it when hasPairedHost == false just sets state to .unsupported without ever prompting for system permission — a dead-end tap.

🛠️ Proposed fix
     var needsPermissionPrompt: Bool {
-        permissionStatus == .notDetermined || permissionStatus == .denied
+        registrationState != .unsupported
+            && (permissionStatus == .notDetermined || permissionStatus == .denied)
     }

Also applies to: 114-116

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Views/Settings/SettingsPushDeliverySection.swift` around lines
30 - 32, The notification prompt logic in SettingsPushDeliverySection.swift is
too permissive because needsPermissionPrompt only checks permissionStatus, so
the UI can show “Enable notifications” even when no machine is paired. Update
the conditions that drive the prompt/button so they also require a paired host
state, and make sure the action path through
PushNotificationService.enableIfPaired() is only exposed when hasPairedHost is
true. Also review the related display logic near the other referenced section to
keep the status row and the actionable prompt consistent.
apps/ios/ADE/Views/Work/WorkStatusAndFormattingHelpers.swift (1)

5-51: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

LGTM overall; consider deduplicating optimistic-session construction.

Correct and matches the offline-create contract in SyncService.swift. Minor: this function and makeOptimisticSession(for:) in WorkRootScreen+Actions.swift both hand-build a TerminalSessionSummary with ~10 identical placeholder fields (ptyId: nil, tracked: true, pinned: false, manuallyNamed: nil, exitCode: nil, transcriptPath: "", headShaStart: nil, headShaEnd: nil, resumeCommand: nil, resumeMetadata: nil). Factoring these shared defaults into a small factory/extension would reduce future drift risk when TerminalSessionSummary gains fields.

♻️ Sketch of a shared default factory
extension TerminalSessionSummary {
  static func optimisticPlaceholder(
    id: String,
    laneId: String,
    laneName: String,
    toolType: String,
    title: String,
    status: String,
    startedAt: String?
  ) -> TerminalSessionSummary {
    TerminalSessionSummary(
      id: id, laneId: laneId, laneName: laneName, ptyId: nil, tracked: true,
      pinned: false, manuallyNamed: nil, goal: nil, toolType: toolType,
      title: title, status: status, startedAt: startedAt, endedAt: nil,
      exitCode: nil, transcriptPath: "", headShaStart: nil, headShaEnd: nil,
      lastOutputPreview: nil, summary: nil, runtimeState: status,
      resumeCommand: nil, resumeMetadata: nil, chatIdleSinceAt: nil
    )
  }
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Views/Work/WorkStatusAndFormattingHelpers.swift` around lines 5
- 51, The optimistic-session construction is duplicated between
workPendingChatCreationOptimisticSession and makeOptimisticSession(for:), so
factor the shared placeholder TerminalSessionSummary fields into a common
factory or extension. Create a reusable helper on TerminalSessionSummary (or a
small builder) that centralizes the repeated defaults like ptyId, tracked,
pinned, manuallyNamed, exitCode, transcriptPath, headShaStart, headShaEnd,
resumeCommand, and resumeMetadata, then have both functions call it and only
supply the per-row values such as id, laneId, laneName, toolType, title, status,
and startedAt.
apps/push-relay/migrations/0001_push_registrations.sql (1)

44-53: 🧹 Nitpick | 🔵 Trivial

No retention/cleanup policy for publish_suppression rows.

The suppression table grows unbounded with one row per (machine_key, suppression_key) and only an index on published_at; there's no scheduled job referenced in this migration/cohort to prune stale rows. Consider a Cron Trigger or TTL-style cleanup to bound table growth over time.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/push-relay/migrations/0001_push_registrations.sql` around lines 44 - 53,
The publish_suppression table is created without any retention mechanism, so
rows can accumulate indefinitely. Update the push-relay migration cohort around
publish_suppression to add a cleanup strategy, such as a Cron Trigger or
TTL-style pruning job, and reference the relevant table/index definitions so
stale rows are periodically removed and table growth stays bounded.
apps/push-relay/src/relay.ts (2)

431-444: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Consider deferring pruneRelayState off the response path.

Both handleDeviceUpsert (764) and handlePublish (869) await pruneRelayState, adding two blocking DELETE round-trips to every write request. Since pruning is best-effort maintenance and not something the caller needs to wait on, threading ExecutionContext through from index.ts and using ctx.waitUntil(pruneRelayState(env)) would cut this latency from the response path without changing behavior.

Also applies to: 841-878

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/push-relay/src/relay.ts` around lines 431 - 444, pruneRelayState is
currently awaited on the critical path, blocking write requests with maintenance
DELETEs. Update handleDeviceUpsert and handlePublish to stop awaiting
pruneRelayState and instead trigger it via ExecutionContext using
ctx.waitUntil(pruneRelayState(env)) after threading ctx through from index.ts.
Keep pruneRelayState unchanged, but make sure the call sites use the existing
handleDeviceUpsert/handlePublish flow to defer pruning off the response path.

536-563: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Sequential per-device APNs delivery.

Devices are delivered to one at a time via a for loop with await inside. Parallelizing with Promise.all (mindful of the per-device clearInvalidToken/deleteActivityToken side effects) would reduce publish latency, especially since MAX_DEVICES_PER_MACHINE is configurable above the default of 16.

Also applies to: 595-668

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/push-relay/src/relay.ts` around lines 536 - 563, The APNs delivery logic
in relay.ts is sequential because the per-device send happens inside a for loop
with await, which increases publish latency. Refactor the device processing in
the APNs send path (the alert flow and the other matching device-delivery
block(s) in relay.ts) to run requests in parallel with Promise.all or a similar
concurrency pattern, while still preserving the per-device side effects like
clearInvalidToken and deleteActivityToken for each result. Keep the existing
outcome shaping and use the same sendApnsPush, clearInvalidToken, and
outcomes.push logic per device.
apps/ios/ADE/Services/PushNotificationService.swift (1)

133-164: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Concurrent registration triggers can silently drop updates.

registerInFlight causes registerDevice() to return immediately if a call is already in progress, with no mechanism to re-run once it completes. If updateLiveActivityPushToStartToken (Line 127) or didRegisterForRemoteNotifications fire while a prior registerDevice() call is still awaiting the network round-trip, that update is dropped — the brain never learns the latest token/prefs until some unrelated trigger fires again.

♻️ Proposed fix: re-run registration if a change arrived mid-flight
     private var registerInFlight = false
+    private var registerDirty = false
 
     private func registerDevice() async {
         guard let sync = SyncService.shared, sync.hasPairedHost else {
             setRegistrationState(.unsupported)
             return
         }
-        guard !registerInFlight else { return }
+        guard !registerInFlight else {
+            registerDirty = true
+            return
+        }
         registerInFlight = true
-        defer { registerInFlight = false }
+        defer {
+            registerInFlight = false
+            if registerDirty {
+                registerDirty = false
+                Task { await registerDevice() }
+            }
+        }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/ios/ADE/Services/PushNotificationService.swift` around lines 133 - 164,
`registerDevice()` can drop newer token/prefs changes when `registerInFlight` is
already true, so add a follow-up registration pass after the in-flight request
completes if a change arrived during the await. Use the existing
`registerDevice()` and `registerInFlight` flow in `PushNotificationService` to
track that a retry is needed, then re-invoke registration once the current call
finishes so updates from `updateLiveActivityPushToStartToken` and
`didRegisterForRemoteNotifications` are not lost.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Around line 146-164: The new checkout steps in the push-relay CI jobs are
missing the credential hardening used elsewhere. Update the actions/checkout@v4
step in typecheck-push-relay and the matching checkout in test-push-relay to set
persist-credentials: false, consistent with validate-docs, so the GITHUB_TOKEN
is not left in git config during npm execution.
- Around line 266-285: The checkout step in the tunnel-relay GitHub Actions jobs
is missing the credential hardening setting, so update both
`typecheck-tunnel-relay` and `test-tunnel-relay` to use `actions/checkout@v4`
with `persist-credentials: false`, matching the secure checkout configuration
already expected for the `npm ci` steps in these jobs.

In `@apps/ade-cli/src/bootstrap.ts`:
- Line 1390: The shutdown cleanup for syncTunnelClientService.dispose() is
letting an async rejection escape because swallow only handles synchronous
errors. Update the disposal call in bootstrap.ts to follow the same pattern as
configReloadService.dispose(), using the dispose() promise’s .catch() handling
instead of void-ing it so failures are swallowed during shutdown. Use the
syncTunnelClientService.dispose() and configReloadService.dispose() calls as the
nearby reference points.

In `@apps/ade-cli/src/services/push/pushPublisherService.ts`:
- Around line 679-694: The relay APNS health check in relayApnsConfigured is
caching failures for the full APNS_HEALTH_CACHE_MS window because the catch
block writes { value: null, at: nowMs } and the freshness guard treats it as
valid; change this so a transient deps.relayClient.health() error does not
suppress re-checks for 24 hours. Update the caching logic in relayApnsConfigured
to either avoid storing failed results at all or use a much shorter negative
TTL, while keeping the successful health path and deps.store.recordRelayContact
behavior unchanged.

In `@apps/ade-cli/src/services/sync/syncTunnelClientService.ts`:
- Around line 191-224: The tunnel forwarding in syncTunnelClientService
currently drops frames in forward() whenever the peer socket is still
CONNECTING, which can lose the first bytes during startup. Update the message
handling around the pipe/local WebSocket pair to queue outgoing data per
direction until the opposite socket emits open, then flush the buffered frames
before resuming live forwarding. Use the existing forward, pipe, local, and
closeBoth logic to locate the change and make sure the buffering is cleared on
close/error.

In `@apps/ios/ADE/App/ADEAppDelegate.swift`:
- Around line 37-43: The didReceiveRemoteNotification callback in ADEAppDelegate
only records push diagnostics via
PushNotificationService.shared.notePushReceived(), so it should not claim that
new content was fetched. Update application(_:didReceiveRemoteNotification:) to
return .noData instead of .newData, keeping the MainActor update unchanged.

In `@apps/ios/ADE/Services/DpopKeyService.swift`:
- Around line 134-146: Avoid force-casting the keychain result in
loadPrivateKey(); the `item as! SecKey` in DpopKeyService can crash if Keychain
returns an unexpected type. Update the `SecItemCopyMatching` handling to safely
cast the returned `item` to `SecKey` with a conditional cast and return nil on
failure, keeping the method’s optional-safe behavior consistent with the rest of
DpopKeyService.

In `@apps/ios/ADE/Services/LiveActivityService.swift`:
- Around line 87-100: The observePushToken(for:) task in LiveActivityService can
still run its post-loop cleanup after being cancelled, which may let an old
observer call report(token: "") and clear perActivityTokenTasks[activity.id]
after a replacement task is already active. Update the Task block to guard the
cleanup with Task.isCancelled or a task-identity check before sending the empty
token and niling the dictionary entry, so only the currently active observer
performs that final cleanup.

In `@apps/ios/ADE/Services/SyncService.swift`:
- Line 4080: `clearPendingChatCreations()` currently removes only the in-memory
pending chat state, but leaves matching queued `chat.create` operations behind.
Update `clearPendingChatCreations()` (and the `disconnect(clearCredentials:)`
forget-machine path that calls it) to also purge any persisted queued create
commands from the pending operations store, similar to how
`cancelPendingChatCreation(id:)` clears both the operation entry and UI
snapshot. Make sure the cleanup covers the
`loadPendingOperations()`/`savePendingOperations()` queue so
`flushPendingOperations()` cannot replay stale chat creates after re-pairing.

In `@apps/push-relay/src/apns.ts`:
- Around line 119-134: The APNs request in sendApnsPush is unbounded and can
stall the sequential delivery path in relay.ts. Update the fetch call in
sendApnsPush to use a timeout-backed AbortSignal (for example via
AbortSignal.timeout or equivalent) so hanging APNs connections are canceled
after a reasonable limit, and make sure the timeout error is handled or
propagated cleanly through ApnsSendResult.

In `@apps/push-relay/src/relay.ts`:
- Around line 146-149: Update readNumber in relay.ts so it distinguishes
explicit JSON null from a missing value instead of coercing null through
Number(). Treat null as null and only convert actual numeric-looking inputs,
then keep liveActivityApnsPayload using that helper so stale-date,
dismissalDate, and relevanceScore preserve “absent” semantics; mirror the
explicit-null handling already used by the sound logic.
- Around line 285-293: The readDeviceIds helper is collapsing two different
cases: missing deviceIds and provided-but-empty/invalid deviceIds, which causes
selectTargets to treat invalid input as “all devices.” Update readDeviceIds in
relay.ts to preserve the absent-vs-empty distinction by returning null only when
deviceIds is not present, and returning an empty array when the property exists
but contains no valid IDs; then ensure selectTargets handles an empty array as
“target none” rather than falling back to the unrestricted path.
- Around line 671-696: `handleMachineClaim` currently does a read-then-insert on
`machines.machine_key`, which can race under concurrent first claims and surface
a unique-constraint failure instead of the intended 201/409 response. Update the
claim flow in `handleMachineClaim` to use a single atomic `INSERT ... ON
CONFLICT(machine_key)` path, and preserve the existing semantics by
distinguishing same-secret idempotent claims from conflicting secrets via the
conflict handling result rather than a separate `loadMachine()` lookup.
- Around line 893-895: The body size check in relay.ts only relies on
contentLengthExceeds(), which can be bypassed when the content-length header is
missing or invalid. Update the request handling flow around the body read and
arrayBuffer() usage to validate the actual body.byteLength after reading, or
reject requests without a trustworthy length header before buffering the
payload. Use the existing contentLengthExceeds helper and the surrounding
request handler logic to keep the 256 KB limit enforced.

In `@apps/tunnel-relay/src/relay.ts`:
- Around line 129-146: routeTunnelPath currently decodes each path segment with
decodeURIComponent without handling malformed escape sequences, so bad input can
throw and escape into handleRequest. Update routeTunnelPath to fail closed by
catching decodeURIComponent errors (or otherwise validating before decoding) and
returning null for malformed paths, while keeping the existing route matching
for valid cases in routeTunnelPath, validKey, and CONNECTION_ID_PATTERN.

---

Outside diff comments:
In `@apps/ios/ADE/Services/SyncService.swift`:
- Around line 4089-4099: `forgetHost()` is disconnecting before the
unpair/unregister request has a chance to be sent, so the relay cleanup can be
dropped. Update the flow so `PushNotificationService.shared.handleUnpair()` is
awaited or otherwise completed before calling `disconnect(clearCredentials:)`,
and keep the rest of the teardown in `forgetHost()` afterward. Use the
`handleUnpair()` path in `PushNotificationService` and the
`disconnect(clearCredentials:)` call in `SyncService` as the key points to
adjust.

In `@apps/ios/ADE/Views/Work/WorkRootComponents.swift`:
- Around line 1066-1085: The accessibility label for pending-sync rows is still
using the normal status text, so VoiceOver announces “running” instead of the
visible pending-sync state. Update `accessibilityLabel` in `WorkRootComponents`
to special-case `isPendingSyncCreation` and use a pending-sync-friendly label
rather than `sessionStatusLabel(for: status)`, while leaving the existing
pinned/archived parts unchanged.

---

Nitpick comments:
In `@apps/ade-cli/src/multiProjectRpcServer.ts`:
- Around line 770-776: The sync.getRequireDpop and sync.setRequireDpop branches
in multiProjectRpcServer.ts return a bare boolean, unlike the other
getter/setter handlers such as sync.getPin and sync.getRuntimeName that wrap
primitives in named fields. Update the getSyncService() call sites so both
methods return an object with a stable field name (for example matching the
existing RPC convention), and then adjust any tests or consumers that assert the
raw boolean shape, including syncService.test.ts if needed. Also verify cli.ts
text/json rendering still reads the new wrapped response correctly so the output
remains consistent with the other sync RPCs.

In `@apps/ios/ADE/Services/PushNotificationService.swift`:
- Around line 133-164: `registerDevice()` can drop newer token/prefs changes
when `registerInFlight` is already true, so add a follow-up registration pass
after the in-flight request completes if a change arrived during the await. Use
the existing `registerDevice()` and `registerInFlight` flow in
`PushNotificationService` to track that a retry is needed, then re-invoke
registration once the current call finishes so updates from
`updateLiveActivityPushToStartToken` and `didRegisterForRemoteNotifications` are
not lost.

In `@apps/ios/ADE/Views/Settings/SettingsPairingScannerSheet.swift`:
- Around line 270-273: SwiftLint is flagging the `PreviewView` override because
the class is `final`, so update the `layerClass` declaration in `PreviewView`
from a class override to the `static` form SwiftLint expects, while keeping the
`previewLayer` accessor unchanged. Since this is a style-only
`static_over_final_class` change, do a quick compile check after adjusting the
`layerClass` symbol to confirm the inherited `UIView` override still behaves
correctly.

In `@apps/ios/ADE/Views/Settings/SettingsPushDeliverySection.swift`:
- Around line 30-32: The notification prompt logic in
SettingsPushDeliverySection.swift is too permissive because
needsPermissionPrompt only checks permissionStatus, so the UI can show “Enable
notifications” even when no machine is paired. Update the conditions that drive
the prompt/button so they also require a paired host state, and make sure the
action path through PushNotificationService.enableIfPaired() is only exposed
when hasPairedHost is true. Also review the related display logic near the other
referenced section to keep the status row and the actionable prompt consistent.

In `@apps/ios/ADE/Views/Work/WorkStatusAndFormattingHelpers.swift`:
- Around line 5-51: The optimistic-session construction is duplicated between
workPendingChatCreationOptimisticSession and makeOptimisticSession(for:), so
factor the shared placeholder TerminalSessionSummary fields into a common
factory or extension. Create a reusable helper on TerminalSessionSummary (or a
small builder) that centralizes the repeated defaults like ptyId, tracked,
pinned, manuallyNamed, exitCode, transcriptPath, headShaStart, headShaEnd,
resumeCommand, and resumeMetadata, then have both functions call it and only
supply the per-row values such as id, laneId, laneName, toolType, title, status,
and startedAt.

In `@apps/push-relay/migrations/0001_push_registrations.sql`:
- Around line 44-53: The publish_suppression table is created without any
retention mechanism, so rows can accumulate indefinitely. Update the push-relay
migration cohort around publish_suppression to add a cleanup strategy, such as a
Cron Trigger or TTL-style pruning job, and reference the relevant table/index
definitions so stale rows are periodically removed and table growth stays
bounded.

In `@apps/push-relay/src/relay.ts`:
- Around line 431-444: pruneRelayState is currently awaited on the critical
path, blocking write requests with maintenance DELETEs. Update
handleDeviceUpsert and handlePublish to stop awaiting pruneRelayState and
instead trigger it via ExecutionContext using
ctx.waitUntil(pruneRelayState(env)) after threading ctx through from index.ts.
Keep pruneRelayState unchanged, but make sure the call sites use the existing
handleDeviceUpsert/handlePublish flow to defer pruning off the response path.
- Around line 536-563: The APNs delivery logic in relay.ts is sequential because
the per-device send happens inside a for loop with await, which increases
publish latency. Refactor the device processing in the APNs send path (the alert
flow and the other matching device-delivery block(s) in relay.ts) to run
requests in parallel with Promise.all or a similar concurrency pattern, while
still preserving the per-device side effects like clearInvalidToken and
deleteActivityToken for each result. Keep the existing outcome shaping and use
the same sendApnsPush, clearInvalidToken, and outcomes.push logic per device.

In `@apps/tunnel-relay/package.json`:
- Line 16: The tunnel-relay package is still pinned to an old Vitest version, so
update the vitest dependency in the package.json for the relay app to match the
newer workspace standard and plan the migration to a current 4.x release. Check
any related test config or scripts that reference Vitest directly so they remain
compatible with the newer version.

In `@apps/tunnel-relay/README.md`:
- Around line 118-122: The deploy example in the README hardcodes a personal
worker subdomain in the curl URL, which should be replaced with a generic
placeholder or example domain. Update the example near the deploy/health-check
snippet to use a neutral identifier derived from the app name instead of the
embedded personal account name, keeping the rest of the workflow unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3715f065-0d4c-4a0a-bd1d-92ededec24b6

📥 Commits

Reviewing files that changed from the base of the PR and between e1cb43d and d0e1451.

⛔ Files ignored due to path filters (7)
  • apps/ios/ADE.xcodeproj/project.pbxproj is excluded by !**/*.xcodeproj/project.pbxproj
  • apps/push-relay/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • apps/tunnel-relay/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • docs/ARCHITECTURE.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/ios-companion.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/push-notifications.md is excluded by !docs/**
📒 Files selected for processing (79)
  • .github/workflows/ci.yml
  • .gitignore
  • apps/ade-cli/README.md
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/cli.test.ts
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/multiProjectRpcServer.ts
  • apps/ade-cli/src/services/push/pushPublisherService.test.ts
  • apps/ade-cli/src/services/push/pushPublisherService.ts
  • apps/ade-cli/src/services/push/pushRegistrationStore.ts
  • apps/ade-cli/src/services/push/pushRelayClient.ts
  • apps/ade-cli/src/services/sync/brainProjectActionsSyncHandler.ts
  • apps/ade-cli/src/services/sync/syncCloudRelayStore.ts
  • apps/ade-cli/src/services/sync/syncDpop.test.ts
  • apps/ade-cli/src/services/sync/syncDpop.ts
  • apps/ade-cli/src/services/sync/syncHostService.ts
  • apps/ade-cli/src/services/sync/syncPairingStore.ts
  • apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
  • apps/ade-cli/src/services/sync/syncSecurityStore.ts
  • apps/ade-cli/src/services/sync/syncService.ts
  • apps/ade-cli/src/services/sync/syncTunnelClientService.test.ts
  • apps/ade-cli/src/services/sync/syncTunnelClientService.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.ts
  • apps/desktop/src/preload/global.d.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx
  • apps/desktop/src/shared/ipc.ts
  • apps/desktop/src/shared/pairingQr.test.ts
  • apps/desktop/src/shared/pairingQr.ts
  • apps/desktop/src/shared/types/push.ts
  • apps/desktop/src/shared/types/sync.ts
  • apps/ios/ADE/ADE.entitlements
  • apps/ios/ADE/App/ADEApp.swift
  • apps/ios/ADE/App/ADEAppDelegate.swift
  • apps/ios/ADE/App/DeepLinkRouter.swift
  • apps/ios/ADE/Info.plist
  • apps/ios/ADE/Models/RemoteModels.swift
  • apps/ios/ADE/Services/DpopKeyService.swift
  • apps/ios/ADE/Services/LiveActivityService.swift
  • apps/ios/ADE/Services/PairingQrPayload.swift
  • apps/ios/ADE/Services/PushNotificationService.swift
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Shared/ADEAgentActivityAttributes.swift
  • apps/ios/ADE/Views/Settings/ConnectionSettingsView.swift
  • apps/ios/ADE/Views/Settings/SettingsPairingScannerSheet.swift
  • apps/ios/ADE/Views/Settings/SettingsPairingSection.swift
  • apps/ios/ADE/Views/Settings/SettingsPinSheet.swift
  • apps/ios/ADE/Views/Settings/SettingsPushDeliverySection.swift
  • apps/ios/ADE/Views/Settings/SettingsSupportTypes.swift
  • apps/ios/ADE/Views/Work/WorkNewChatScreen.swift
  • apps/ios/ADE/Views/Work/WorkRootComponents.swift
  • apps/ios/ADE/Views/Work/WorkRootScreen+Actions.swift
  • apps/ios/ADE/Views/Work/WorkRootScreen.swift
  • apps/ios/ADE/Views/Work/WorkStatusAndFormattingHelpers.swift
  • apps/ios/ADETests/ADETests.swift
  • apps/ios/ADETests/PairingAndDpopTests.swift
  • apps/ios/ADEWidgets/ADEAgentActivityWidget.swift
  • apps/ios/ADEWidgets/ADEWidgetBundle.swift
  • apps/push-relay/README.md
  • apps/push-relay/migrations/0001_push_registrations.sql
  • apps/push-relay/package.json
  • apps/push-relay/src/apns.ts
  • apps/push-relay/src/index.ts
  • apps/push-relay/src/relay.ts
  • apps/push-relay/test/relay.test.ts
  • apps/push-relay/tsconfig.json
  • apps/push-relay/wrangler.jsonc
  • apps/tunnel-relay/.gitignore
  • apps/tunnel-relay/README.md
  • apps/tunnel-relay/package.json
  • apps/tunnel-relay/src/index.ts
  • apps/tunnel-relay/src/relay.ts
  • apps/tunnel-relay/src/tunnelDo.ts
  • apps/tunnel-relay/test/relay.test.ts
  • apps/tunnel-relay/test/smoke.mjs
  • apps/tunnel-relay/tsconfig.json
  • apps/tunnel-relay/wrangler.jsonc
  • package.json

Comment thread .github/workflows/ci.yml
Comment thread .github/workflows/ci.yml
Comment thread apps/ade-cli/src/bootstrap.ts Outdated
Comment thread apps/ade-cli/src/services/push/pushPublisherService.ts
Comment thread apps/ade-cli/src/services/sync/syncTunnelClientService.ts
Comment thread apps/push-relay/src/relay.ts
Comment thread apps/push-relay/src/relay.ts
Comment thread apps/push-relay/src/relay.ts
Comment thread apps/push-relay/src/relay.ts
Comment thread apps/tunnel-relay/src/relay.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 430acac652

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +377 to +381
let event: "start" | "update" | "end";
if (activeCount > 0 && !liveActivityStarted) event = "start";
else if (activeCount === 0 && liveActivityStarted) event = "end";
else if (activeCount === 0 && !liveActivityStarted) return null; // nothing live to report
else event = "update";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Start live activities per newly registered device

When a second phone registers while an agent run is already active, handleDeviceRegistered() calls poke(), but this machine-global liveActivityStarted flag makes the next payload an update instead of a start. The relay's update/end path targets per-activity tokens, and the new phone only has a push-to-start token at that point, so it never gets a Live Activity for the current run until the aggregate ends and a later run starts.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Valid but deliberately deferred: starting the Live Activity per newly-registered device needs per-device start tracking and a start/update split in the aggregate planner — a design change, not a review fix. Tracked as a follow-up.

Comment on lines +485 to +489
try {
await deps.relayClient.publish(payload);
for (const [key, fingerprint] of alertCommits) lastAlertFingerprintByKey.set(key, fingerprint);
laPlan?.commit();
deps.store.recordPublishResult({ at: new Date().toISOString() });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Treat relay delivery failures as retryable

If APNs rejects a publish for a transient per-device reason, the relay still returns HTTP 200 with a failed count in apps/push-relay/src/relay.ts::handlePublish; this call ignores that body and immediately commits alert dedupe/live-activity state. In that scenario the alert is dropped and future identical content is suppressed locally instead of being retried, even though no device received it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c50d7a2 (same change as the Greptile thread): delivery failures no longer count as published.

Comment on lines +133 to +139
private func registerDevice() async {
guard let sync = SyncService.shared, sync.hasPairedHost else {
setRegistrationState(.unsupported)
return
}
guard !registerInFlight else { return }
registerInFlight = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Queue a second push registration while in flight

During first pairing/foreground, APNs registration and LiveActivityService push-to-start token reporting can overlap; if the token update calls registerDevice() while the APNs registration is still in flight, this guard drops the second registration. When the first request was built before liveActivityPushToStartTokenHex was set, the relay never receives the push-to-start token until another foreground/token change, so remote Live Activity starts silently do nothing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in c50d7a2: registerDevice() coalesces overlapping registrations (pending flag + re-run loop) so a mid-flight push-to-start token still reaches the relay.

@arul28 arul28 force-pushed the ade/mobile-push-relay-platform-c1ebe009 branch from 430acac to 14e5c0d Compare July 5, 2026 16:01
Comment thread apps/ade-cli/src/services/push/pushPublisherService.ts Outdated
@arul28 arul28 force-pushed the ade/mobile-push-relay-platform-c1ebe009 branch from 14e5c0d to c50d7a2 Compare July 5, 2026 16:37
@cursor

cursor Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@arul28

arul28 commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c50d7a2558

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread apps/ade-cli/src/bootstrap.ts Outdated
Comment on lines +1181 to +1185
const pushPublisherService = createPushPublisherService({
logger,
store: pushRegistrationStore,
relayClient: pushRelayClient,
machineName: os.hostname(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Keep the push publisher machine-scoped

In the multi-project daemon, ProjectScopeRegistry.get() calls createAdeRuntime() for every opened project, so this instantiates a separate publisher per project while all of them share ~/.ade/secrets/push-relay.json and publish the fixed agent-runs Live Activity. With two booted projects, each publisher has its own runs/liveActivityStarted state; a run event from project B overwrites or ends the phone's single machine activity with only B's state, hiding active runs from project A. Gate this to the active sync host or make the publisher a singleton per machine.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 36737c2: the publisher is now a machine-level singleton (memoized by identity file) with per-project attachSources/detach — runs merge across scopes into the one agent-runs aggregate, and a detached project's runs drop out.

return;
}
for (const [key, fingerprint] of alertCommits) lastAlertFingerprintByKey.set(key, fingerprint);
laPlan?.commit();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Commit Live Activity state only after its delivery succeeds

When a publish contains both an alert and a Live Activity item (for example the first event is an approval request), the relay can return delivered > 0 because the alert succeeded while the Live Activity push-to-start failed. This line still runs laPlan.commit(), setting liveActivityStarted/dedupe state, so subsequent flushes send updates instead of retrying the start and the phone never shows the activity. Inspect the relay outcomes for the liveactivity item before committing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 36737c2: the Live Activity plan commits only off its own outcomes (delivered/suppressed liveactivity entries); an alert-only success no longer commits LA state, so the start retries.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ade-cli/src/services/push/pushRegistrationStore.ts`:
- Around line 107-115: The write path in pushRegistrationStore’s write function
currently relies on a post-write chmod after writeTextAtomic, which can leave
machineKey and machineSecret briefly exposed with broader permissions. Update
the file creation flow so the secrets file is created with 0600 immediately,
either by passing the mode into writeTextAtomic or by setting that default
inside the helper, and keep the existing cache update in write.

In `@apps/ade-cli/src/services/sync/syncCloudRelayStore.ts`:
- Around line 84-100: The load() path in syncCloudRelayStore is racing on first
run because it reads, generates, and writes machineKey/secret without any
exclusive first-write guard. Update load() (and the read/write helpers it uses)
so the initial config creation is atomic across processes, using an exclusive
file creation or equivalent lock before persisting the identity values. Keep the
existing validation and normalization logic, but ensure only one caller can mint
and write the first SyncCloudRelayConfig so every accessor returns the same
machineKey/secret pair.

In `@apps/ios/ADE/Views/Settings/SettingsPairingScannerSheet.swift`:
- Around line 241-248: The setTorch(_:) method currently ignores failures from
lockForConfiguration() and still changes torchMode and calls
unlockForConfiguration(), which can run without a valid lock. Update the
AVCaptureDevice handling in SettingsPairingScannerSheet.setTorch(_:) to attempt
the lock first, bail out immediately if locking fails, and only then set
torchMode and unlock the device.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 61f98e73-9073-44b7-b619-d1642a21ff78

📥 Commits

Reviewing files that changed from the base of the PR and between d0e1451 and c50d7a2.

⛔ Files ignored due to path filters (7)
  • apps/ios/ADE.xcodeproj/project.pbxproj is excluded by !**/*.xcodeproj/project.pbxproj
  • apps/push-relay/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • apps/tunnel-relay/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • docs/ARCHITECTURE.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/ios-companion.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/push-notifications.md is excluded by !docs/**
📒 Files selected for processing (79)
  • .github/workflows/ci.yml
  • .gitignore
  • apps/ade-cli/README.md
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/cli.test.ts
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/multiProjectRpcServer.ts
  • apps/ade-cli/src/services/push/pushPublisherService.test.ts
  • apps/ade-cli/src/services/push/pushPublisherService.ts
  • apps/ade-cli/src/services/push/pushRegistrationStore.ts
  • apps/ade-cli/src/services/push/pushRelayClient.ts
  • apps/ade-cli/src/services/sync/brainProjectActionsSyncHandler.ts
  • apps/ade-cli/src/services/sync/syncCloudRelayStore.ts
  • apps/ade-cli/src/services/sync/syncDpop.test.ts
  • apps/ade-cli/src/services/sync/syncDpop.ts
  • apps/ade-cli/src/services/sync/syncHostService.ts
  • apps/ade-cli/src/services/sync/syncPairingStore.ts
  • apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
  • apps/ade-cli/src/services/sync/syncSecurityStore.ts
  • apps/ade-cli/src/services/sync/syncService.ts
  • apps/ade-cli/src/services/sync/syncTunnelClientService.test.ts
  • apps/ade-cli/src/services/sync/syncTunnelClientService.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.ts
  • apps/desktop/src/preload/global.d.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx
  • apps/desktop/src/shared/ipc.ts
  • apps/desktop/src/shared/pairingQr.test.ts
  • apps/desktop/src/shared/pairingQr.ts
  • apps/desktop/src/shared/types/push.ts
  • apps/desktop/src/shared/types/sync.ts
  • apps/ios/ADE/ADE.entitlements
  • apps/ios/ADE/App/ADEApp.swift
  • apps/ios/ADE/App/ADEAppDelegate.swift
  • apps/ios/ADE/App/DeepLinkRouter.swift
  • apps/ios/ADE/Info.plist
  • apps/ios/ADE/Models/RemoteModels.swift
  • apps/ios/ADE/Services/DpopKeyService.swift
  • apps/ios/ADE/Services/LiveActivityService.swift
  • apps/ios/ADE/Services/PairingQrPayload.swift
  • apps/ios/ADE/Services/PushNotificationService.swift
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Shared/ADEAgentActivityAttributes.swift
  • apps/ios/ADE/Views/Settings/ConnectionSettingsView.swift
  • apps/ios/ADE/Views/Settings/SettingsPairingScannerSheet.swift
  • apps/ios/ADE/Views/Settings/SettingsPairingSection.swift
  • apps/ios/ADE/Views/Settings/SettingsPinSheet.swift
  • apps/ios/ADE/Views/Settings/SettingsPushDeliverySection.swift
  • apps/ios/ADE/Views/Settings/SettingsSupportTypes.swift
  • apps/ios/ADE/Views/Work/WorkNewChatScreen.swift
  • apps/ios/ADE/Views/Work/WorkRootComponents.swift
  • apps/ios/ADE/Views/Work/WorkRootScreen+Actions.swift
  • apps/ios/ADE/Views/Work/WorkRootScreen.swift
  • apps/ios/ADE/Views/Work/WorkStatusAndFormattingHelpers.swift
  • apps/ios/ADETests/ADETests.swift
  • apps/ios/ADETests/PairingAndDpopTests.swift
  • apps/ios/ADEWidgets/ADEAgentActivityWidget.swift
  • apps/ios/ADEWidgets/ADEWidgetBundle.swift
  • apps/push-relay/README.md
  • apps/push-relay/migrations/0001_push_registrations.sql
  • apps/push-relay/package.json
  • apps/push-relay/src/apns.ts
  • apps/push-relay/src/index.ts
  • apps/push-relay/src/relay.ts
  • apps/push-relay/test/relay.test.ts
  • apps/push-relay/tsconfig.json
  • apps/push-relay/wrangler.jsonc
  • apps/tunnel-relay/.gitignore
  • apps/tunnel-relay/README.md
  • apps/tunnel-relay/package.json
  • apps/tunnel-relay/src/index.ts
  • apps/tunnel-relay/src/relay.ts
  • apps/tunnel-relay/src/tunnelDo.ts
  • apps/tunnel-relay/test/relay.test.ts
  • apps/tunnel-relay/test/smoke.mjs
  • apps/tunnel-relay/tsconfig.json
  • apps/tunnel-relay/wrangler.jsonc
  • package.json
✅ Files skipped from review due to trivial changes (8)
  • apps/push-relay/package.json
  • apps/tunnel-relay/.gitignore
  • apps/push-relay/wrangler.jsonc
  • apps/tunnel-relay/tsconfig.json
  • apps/push-relay/tsconfig.json
  • apps/ade-cli/README.md
  • apps/tunnel-relay/README.md
  • apps/push-relay/README.md
🚧 Files skipped from review as they are similar to previous changes (59)
  • apps/tunnel-relay/package.json
  • apps/tunnel-relay/wrangler.jsonc
  • package.json
  • apps/tunnel-relay/src/index.ts
  • apps/push-relay/src/index.ts
  • apps/desktop/src/preload/preload.ts
  • apps/ios/ADE/Views/Work/WorkRootComponents.swift
  • apps/ios/ADE/App/DeepLinkRouter.swift
  • apps/desktop/src/shared/ipc.ts
  • apps/ios/ADE/ADE.entitlements
  • apps/desktop/src/preload/global.d.ts
  • apps/ios/ADE/Views/Work/WorkStatusAndFormattingHelpers.swift
  • apps/tunnel-relay/test/relay.test.ts
  • apps/ade-cli/src/multiProjectRpcServer.ts
  • apps/ios/ADE/Views/Work/WorkRootScreen+Actions.swift
  • apps/ios/ADE/Views/Settings/SettingsPinSheet.swift
  • apps/ios/ADEWidgets/ADEWidgetBundle.swift
  • apps/ios/ADE/Views/Work/WorkNewChatScreen.swift
  • apps/ade-cli/src/services/sync/syncTunnelClientService.test.ts
  • apps/ios/ADE/Models/RemoteModels.swift
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/ios/ADE/Info.plist
  • apps/ios/ADE/App/ADEApp.swift
  • apps/tunnel-relay/test/smoke.mjs
  • apps/ade-cli/src/cli.ts
  • apps/desktop/src/shared/pairingQr.test.ts
  • apps/ade-cli/src/services/sync/syncPairingStore.ts
  • apps/ios/ADE/Views/Settings/SettingsPairingSection.swift
  • apps/ade-cli/src/services/sync/brainProjectActionsSyncHandler.ts
  • apps/ios/ADE/Views/Settings/SettingsSupportTypes.swift
  • apps/ios/ADE/Shared/ADEAgentActivityAttributes.swift
  • apps/desktop/src/shared/types/sync.ts
  • apps/ios/ADE/Services/PairingQrPayload.swift
  • apps/desktop/src/shared/types/push.ts
  • apps/ios/ADE/App/ADEAppDelegate.swift
  • apps/ios/ADEWidgets/ADEAgentActivityWidget.swift
  • apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
  • apps/ios/ADE/Services/DpopKeyService.swift
  • apps/push-relay/migrations/0001_push_registrations.sql
  • apps/ios/ADETests/ADETests.swift
  • apps/ios/ADE/Views/Work/WorkRootScreen.swift
  • apps/push-relay/src/apns.ts
  • apps/ios/ADE/Views/Settings/ConnectionSettingsView.swift
  • apps/tunnel-relay/src/relay.ts
  • apps/ade-cli/src/services/sync/syncDpop.ts
  • apps/tunnel-relay/src/tunnelDo.ts
  • apps/push-relay/test/relay.test.ts
  • apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx
  • apps/desktop/src/shared/pairingQr.ts
  • apps/ade-cli/src/services/sync/syncHostService.ts
  • apps/ios/ADE/Services/LiveActivityService.swift
  • apps/ios/ADE/Views/Settings/SettingsPushDeliverySection.swift
  • apps/ade-cli/src/services/push/pushRelayClient.ts
  • apps/ade-cli/src/services/push/pushPublisherService.test.ts
  • .github/workflows/ci.yml
  • apps/ios/ADE/Services/PushNotificationService.swift
  • apps/push-relay/src/relay.ts
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ade-cli/src/services/sync/syncService.ts

Comment thread apps/ade-cli/src/services/push/pushRegistrationStore.ts
Comment thread apps/ade-cli/src/services/sync/syncCloudRelayStore.ts
Comment thread apps/ios/ADE/Views/Settings/SettingsPairingScannerSheet.swift
@arul28

arul28 commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@arul28 arul28 force-pushed the ade/mobile-push-relay-platform-c1ebe009 branch from c50d7a2 to 36737c2 Compare July 5, 2026 17:20
Comment thread apps/ade-cli/src/cli.ts

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 36737c2f17

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +550 to +552
const landed = laOutcomes.length === 0 || laOutcomes.some((o) => o.delivered || o.suppressed);
if (landed) {
laPlan.commit();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Don't commit Live Activity updates with no APNs target

When the relay returns no liveactivity outcomes, this treats the publish as landed and commits the Live Activity fingerprint. That happens for update/end publishes before the phone has reported the per-activity token (or after the relay has no matching device row), so no APNs update is sent, the new state is locally deduped, and handleLiveActivityToken does not trigger a resend when the token arrives; the Lock Screen activity can stay stuck on the old content until another distinct agent event occurs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 8870601: a real outcomes array with zero landed liveactivity entries no longer commits (legacy/mock responses without an outcomes field keep the old behavior), and handleLiveActivityToken pokes a flush when a token arrives so the activity catches up immediately.

Comment on lines 4185 to +4188
// be paired; unknown devices must pair via the PIN flow. Existing
// paired phones from older releases may not have a host PIN configured
// yet, and should still be able to reconnect with their stored token.
return !alreadyPaired;
return bootstrapPairingRecord == null;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Honor require-DPoP on bootstrap reconnects

When the require-DPoP setting/env is enabled, a legacy paired device with no stored DPoP key can still authenticate through this bootstrap-token branch because only records that already have dpopPublicKey are rejected. That bypasses the documented force-repair/force-re-pair posture for previously paired devices on the LAN (the brain ingress mirrors the same check), leaving the shared bootstrap token sufficient for those device IDs instead of requiring a DPoP-bound pairing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Fixed in 8870601: with require-DPoP on, the shared bootstrap token no longer satisfies a LAN hello for ANY device (keyless legacy pairings included) — enforced on both the project host and the brain ingress; loopback-only mode retains the historical local-process behavior.

@arul28 arul28 force-pushed the ade/mobile-push-relay-platform-c1ebe009 branch from 36737c2 to 8870601 Compare July 5, 2026 17:51
@arul28

arul28 commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@cursor

cursor Bot commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8870601211

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

logger,
store,
relayClient: createPushRelayClient({ store, logger }),
machineName: os.hostname(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Use the paired display name for Live Activity attributes

On macOS this uses the network hostname, but the phone stores the paired machine as the sync host identity name (the device display name/ComputerName) and LiveActivityService.endOrphanedActivities compares activity.attributes.machineName against SyncService.hostName. When those names differ (common on macOS), foregrounding the app treats the current machine's valid activity as orphaned and ends it immediately, so remote Live Activities disappear after the next foreground transition.

Useful? React with 👍 / 👎.

Comment on lines +129 to +130
if apnsTokenHex != nil {
Task { await registerDevice() }

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Register push-to-start tokens without requiring APNs

When ActivityKit yields a push-to-start token while apnsTokenHex is still nil (for example the user denied alert notification permission, or APNs has not called back yet), this guard skips registerDevice() and the token is never sent to the brain. The new push DTO/store explicitly support push-to-start-only registrations, so these devices cannot receive remote Live Activity starts until an alert APNs token happens to arrive.

Useful? React with 👍 / 👎.

Comment thread apps/tunnel-relay/src/tunnelDo.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/ade-cli/src/services/push/pushPublisherService.ts`:
- Around line 529-545: The publish flow in pushPublisherService currently only
retries when relayClient.publish returns zero delivered and some failed, but
partial failures still commit alert fingerprints and block retries. Update the
publish handling around readOutcomeCount, alertCommits, and scheduleRetry so
that any failed target with delivered > 0 also re-queues the affected alerts
instead of committing dedupe/Live-Activity state immediately. Keep the existing
all-failed path, but expand the logic to detect partial relay failures, preserve
pendingAlerts for retry, and only update lastAlertFingerprintByKey after a fully
successful publish.

In `@apps/ade-cli/src/services/push/pushRegistrationStore.ts`:
- Around line 129-133: The device normalization in pushRegistrationStore’s load
path can crash when a persisted entry is malformed because `isValid()` only
validates the `devices` container, not each entry. Update the
`Object.entries(parsed.devices).map(...)` logic to guard each `device` before
dereferencing `device.prefs`, and either skip invalid entries or filter them out
before building `devices` so `normalizePrefs` only runs on valid objects.
- Line 93: The in-memory cache in PushRegistrationStore can become stale across
multiple ADE runtimes and overwrite newer data in push-relay.json. Update the
mutation path in PushRegistrationStore so each write re-reads and merges the
latest file state before saving, or add a file lock around the write flow to
serialize updates. Use the existing PushRegistrationStore cache and the methods
that persist the registration data to ensure device/prefs changes from other
processes are not clobbered.

In `@apps/ade-cli/src/services/sync/syncPairingStore.ts`:
- Around line 79-89: The pairing record write in pairPeer currently stores any
non-empty dpopPublicKey value, which can leave invalid key material in
syncPairingStore. Validate the incoming key before assigning it to
records[peer.deviceId], using the same Base64 X9.63 P-256 expectations already
implied by adoptDpopPublicKey and the rest of the sync flow, and reject/throw on
malformed input so only usable DPoP keys are persisted.

In `@apps/ade-cli/src/services/sync/syncTunnelClientService.ts`:
- Around line 126-157: `claimOnce()` and the tunnel connect flow in
`connectControl()` can hang indefinitely because the relay POST and subsequent
WebSocket opens have no deadline. Add abort/open timeouts to the claim request
and to each relay/local WebSocket connection path, then treat timeout as a
failure that updates `lastError`, logs via
`log.warn?.("sync_tunnel.claim_failed", ...)` or the equivalent connect error
handler, and triggers `scheduleReconnect()`. Make the same timeout handling
consistent across the control, pipe, and local connect helpers so `start()`
never stalls on a dead relay or sync port.

In `@apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx`:
- Around line 677-678: The pairing QR payload is missing the enabled cloud relay
route because PairQrPanel only gets connectInfo while buildPairingQrPayload can
also take relayUrl. Update the PairQrPanel usage and its props so it receives
the active relay URL when cloud relay fallback is enabled, and pass that value
through to buildPairingQrPayload alongside connectInfo. Check the PairQrPanel
component and any callers in SyncDevicesSection so the scanned QR matches the
relay the UI advertises.

In `@apps/tunnel-relay/test/smoke.mjs`:
- Around line 49-52: The smoke test WebSocket bridge in the local WebSocket
setup can drop the first frame if either side receives a message before the
other socket reaches OPEN. Update the bridging logic around pipe and local so
messages are buffered until both connections are open, then flushed in order;
use the existing closeBoth, pipe.on("message"), and local.on("message") wiring
in smoke.mjs as the place to add the buffering state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e19dfb08-9765-4e4c-a69b-2c809a731982

📥 Commits

Reviewing files that changed from the base of the PR and between c50d7a2 and 8870601.

⛔ Files ignored due to path filters (7)
  • apps/ios/ADE.xcodeproj/project.pbxproj is excluded by !**/*.xcodeproj/project.pbxproj
  • apps/push-relay/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • apps/tunnel-relay/package-lock.json is excluded by !**/package-lock.json, !**/package-lock.json
  • docs/ARCHITECTURE.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/README.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/ios-companion.md is excluded by !docs/**
  • docs/features/sync-and-multi-device/push-notifications.md is excluded by !docs/**
📒 Files selected for processing (80)
  • .github/workflows/ci.yml
  • .gitignore
  • apps/ade-cli/README.md
  • apps/ade-cli/src/bootstrap.ts
  • apps/ade-cli/src/cli.test.ts
  • apps/ade-cli/src/cli.ts
  • apps/ade-cli/src/multiProjectRpcServer.ts
  • apps/ade-cli/src/services/push/pushPublisherService.test.ts
  • apps/ade-cli/src/services/push/pushPublisherService.ts
  • apps/ade-cli/src/services/push/pushRegistrationStore.ts
  • apps/ade-cli/src/services/push/pushRelayClient.ts
  • apps/ade-cli/src/services/sync/brainProjectActionsSyncHandler.ts
  • apps/ade-cli/src/services/sync/syncCloudRelayStore.ts
  • apps/ade-cli/src/services/sync/syncDpop.test.ts
  • apps/ade-cli/src/services/sync/syncDpop.ts
  • apps/ade-cli/src/services/sync/syncHostService.ts
  • apps/ade-cli/src/services/sync/syncPairingStore.ts
  • apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
  • apps/ade-cli/src/services/sync/syncSecurityStore.ts
  • apps/ade-cli/src/services/sync/syncService.ts
  • apps/ade-cli/src/services/sync/syncTunnelClientService.test.ts
  • apps/ade-cli/src/services/sync/syncTunnelClientService.ts
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/desktop/src/main/services/localRuntime/localRuntimeConnectionPool.ts
  • apps/desktop/src/main/services/shared/utils.ts
  • apps/desktop/src/preload/global.d.ts
  • apps/desktop/src/preload/preload.ts
  • apps/desktop/src/renderer/components/settings/SyncDevicesSection.tsx
  • apps/desktop/src/shared/ipc.ts
  • apps/desktop/src/shared/pairingQr.test.ts
  • apps/desktop/src/shared/pairingQr.ts
  • apps/desktop/src/shared/types/push.ts
  • apps/desktop/src/shared/types/sync.ts
  • apps/ios/ADE/ADE.entitlements
  • apps/ios/ADE/App/ADEApp.swift
  • apps/ios/ADE/App/ADEAppDelegate.swift
  • apps/ios/ADE/App/DeepLinkRouter.swift
  • apps/ios/ADE/Info.plist
  • apps/ios/ADE/Models/RemoteModels.swift
  • apps/ios/ADE/Services/DpopKeyService.swift
  • apps/ios/ADE/Services/LiveActivityService.swift
  • apps/ios/ADE/Services/PairingQrPayload.swift
  • apps/ios/ADE/Services/PushNotificationService.swift
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Shared/ADEAgentActivityAttributes.swift
  • apps/ios/ADE/Views/Settings/ConnectionSettingsView.swift
  • apps/ios/ADE/Views/Settings/SettingsPairingScannerSheet.swift
  • apps/ios/ADE/Views/Settings/SettingsPairingSection.swift
  • apps/ios/ADE/Views/Settings/SettingsPinSheet.swift
  • apps/ios/ADE/Views/Settings/SettingsPushDeliverySection.swift
  • apps/ios/ADE/Views/Settings/SettingsSupportTypes.swift
  • apps/ios/ADE/Views/Work/WorkNewChatScreen.swift
  • apps/ios/ADE/Views/Work/WorkRootComponents.swift
  • apps/ios/ADE/Views/Work/WorkRootScreen+Actions.swift
  • apps/ios/ADE/Views/Work/WorkRootScreen.swift
  • apps/ios/ADE/Views/Work/WorkStatusAndFormattingHelpers.swift
  • apps/ios/ADETests/ADETests.swift
  • apps/ios/ADETests/PairingAndDpopTests.swift
  • apps/ios/ADEWidgets/ADEAgentActivityWidget.swift
  • apps/ios/ADEWidgets/ADEWidgetBundle.swift
  • apps/push-relay/README.md
  • apps/push-relay/migrations/0001_push_registrations.sql
  • apps/push-relay/package.json
  • apps/push-relay/src/apns.ts
  • apps/push-relay/src/index.ts
  • apps/push-relay/src/relay.ts
  • apps/push-relay/test/relay.test.ts
  • apps/push-relay/tsconfig.json
  • apps/push-relay/wrangler.jsonc
  • apps/tunnel-relay/.gitignore
  • apps/tunnel-relay/README.md
  • apps/tunnel-relay/package.json
  • apps/tunnel-relay/src/index.ts
  • apps/tunnel-relay/src/relay.ts
  • apps/tunnel-relay/src/tunnelDo.ts
  • apps/tunnel-relay/test/relay.test.ts
  • apps/tunnel-relay/test/smoke.mjs
  • apps/tunnel-relay/tsconfig.json
  • apps/tunnel-relay/wrangler.jsonc
  • package.json
✅ Files skipped from review due to trivial changes (5)
  • apps/tunnel-relay/.gitignore
  • apps/push-relay/README.md
  • apps/ios/ADE/ADE.entitlements
  • .gitignore
  • apps/tunnel-relay/README.md
🚧 Files skipped from review as they are similar to previous changes (61)
  • apps/tunnel-relay/package.json
  • apps/push-relay/src/index.ts
  • apps/ios/ADEWidgets/ADEWidgetBundle.swift
  • apps/desktop/src/preload/global.d.ts
  • apps/tunnel-relay/tsconfig.json
  • apps/ios/ADE/App/ADEApp.swift
  • apps/tunnel-relay/src/index.ts
  • apps/tunnel-relay/wrangler.jsonc
  • apps/push-relay/wrangler.jsonc
  • apps/ios/ADE/Models/RemoteModels.swift
  • apps/ios/ADE/Info.plist
  • apps/ios/ADE/Views/Work/WorkRootScreen+Actions.swift
  • apps/desktop/src/main/services/ipc/registerIpc.ts
  • apps/ios/ADE/Views/Work/WorkRootScreen.swift
  • apps/tunnel-relay/test/relay.test.ts
  • apps/push-relay/tsconfig.json
  • apps/ade-cli/src/multiProjectRpcServer.ts
  • apps/desktop/src/shared/types/push.ts
  • apps/ade-cli/README.md
  • apps/ade-cli/src/services/sync/syncTunnelClientService.test.ts
  • apps/desktop/src/preload/preload.ts
  • apps/ios/ADE/App/DeepLinkRouter.swift
  • apps/ios/ADE/Views/Settings/SettingsPairingSection.swift
  • apps/ios/ADE/App/ADEAppDelegate.swift
  • apps/ios/ADE/Views/Work/WorkRootComponents.swift
  • apps/push-relay/package.json
  • apps/ios/ADE/Views/Work/WorkStatusAndFormattingHelpers.swift
  • apps/ios/ADE/Views/Work/WorkNewChatScreen.swift
  • apps/desktop/src/shared/types/sync.ts
  • apps/ade-cli/src/cli.ts
  • apps/ios/ADETests/ADETests.swift
  • apps/ios/ADEWidgets/ADEAgentActivityWidget.swift
  • apps/ios/ADE/Views/Settings/SettingsPinSheet.swift
  • apps/desktop/src/shared/ipc.ts
  • apps/ade-cli/src/services/sync/syncHostService.ts
  • apps/ios/ADE/Services/DpopKeyService.swift
  • package.json
  • apps/ios/ADE/Views/Settings/SettingsSupportTypes.swift
  • apps/push-relay/migrations/0001_push_registrations.sql
  • apps/ade-cli/src/services/push/pushPublisherService.test.ts
  • apps/desktop/src/shared/pairingQr.ts
  • apps/ade-cli/src/bootstrap.ts
  • apps/push-relay/test/relay.test.ts
  • apps/ios/ADE/Views/Settings/ConnectionSettingsView.swift
  • apps/desktop/src/shared/pairingQr.test.ts
  • apps/ade-cli/src/services/push/pushRelayClient.ts
  • apps/ios/ADE/Views/Settings/SettingsPushDeliverySection.swift
  • apps/push-relay/src/apns.ts
  • apps/ios/ADE/Shared/ADEAgentActivityAttributes.swift
  • apps/ade-cli/src/services/sync/syncDpop.ts
  • apps/ade-cli/src/services/sync/brainProjectActionsSyncHandler.ts
  • apps/ios/ADE/Services/PairingQrPayload.swift
  • apps/tunnel-relay/src/tunnelDo.ts
  • .github/workflows/ci.yml
  • apps/tunnel-relay/src/relay.ts
  • apps/ade-cli/src/services/sync/syncService.ts
  • apps/ios/ADE/Services/PushNotificationService.swift
  • apps/ade-cli/src/services/sync/syncRemoteCommandService.ts
  • apps/push-relay/src/relay.ts
  • apps/ios/ADE/Services/SyncService.swift
  • apps/ios/ADE/Services/LiveActivityService.swift

Comment thread apps/ade-cli/src/services/push/pushPublisherService.ts
Comment thread apps/ade-cli/src/services/push/pushRegistrationStore.ts
Comment thread apps/ade-cli/src/services/push/pushRegistrationStore.ts Outdated
Comment thread apps/ade-cli/src/services/sync/syncPairingStore.ts Outdated
Comment thread apps/ade-cli/src/services/sync/syncTunnelClientService.ts
Comment thread apps/tunnel-relay/test/smoke.mjs Outdated
… DPoP + smart QR

- apps/push-relay (new CF Worker, deployed): D1 device registrations,
  machine-claim + HMAC-signed publish API, APNs HTTP/2 (ES256 .p8 JWT),
  phase TTLs (2h running / 24h waiting), delivery-gated content-hash
  suppression, dead-token cleanup, Live Activity start/update/end routing.
- Brain publisher (ade-cli services/push): subscribes agentChatService /
  ptyService.onExit / prPollingService pr-notifications; debounced +
  deduped; per-device prefs enforced brain-side (mute, quiet hours in
  device timezone); push.* runtime sync commands for phone registration.
- iOS: aps-environment + remote-notification; registration after pairing;
  alert pushes deep-link via ade://; ADEAgentRunsAttributes Live Activity
  (lock screen + Dynamic Island, up to 3 runs) with push-to-start +
  foreground re-registration + orphan reaping; Push delivery settings
  panel (status, toggles, quiet hours).
- apps/tunnel-relay (new DO Worker, deployed): phone->relay->brain WS
  tunnel with per-connection pipe sockets and pre-open frame buffering on
  both hops; brain tunnel client with backoff; "Cloud relay fallback"
  toggle (desktop + iOS, default off); relay advertised as the
  lowest-priority address candidate.
- DPoP pairing hardening: Secure Enclave P-256 key registered at pairing;
  every paired hello signs a fresh secret-bound challenge; TOFU upgrade
  for legacy devices; fail-closed once a key is on record; bootstrap
  downgrade path blocked; requireDpop machine setting enforced on both
  the project host and brain ingress (+ ade sync security/relay CLI).
- One-QR smart pairing: desktop QR encodes https://ade-app.dev/pair
  payload (identity + port + candidates + relay URL); iOS scanner with
  already-paired instant reconnect or PIN-only pair; universal-link route.
- Offline outbox: queued chat.create persists a creation snapshot and
  renders a pending Work-list row that reconciles on drain.
- Tests: syncDpop contract suite (replay/skew/TOFU/fail-closed +
  requireDpop regression), cross-app HMAC golden vectors, consolidated
  push/cloud-relay suites; docs updated (sync README, ios-companion,
  ARCHITECTURE relay-workers section).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@arul28 arul28 force-pushed the ade/mobile-push-relay-platform-c1ebe009 branch from 8870601 to 1e8c9b6 Compare July 5, 2026 18:22
@arul28

arul28 commented Jul 5, 2026

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1e8c9b66d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +4092 to 4093
PushNotificationService.shared.handleUnpair()
disconnect(clearCredentials: true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Await push unregister before tearing down pairing

When a user forgets a paired host, this schedules push.unregisterDevice inside handleUnpair() but immediately calls disconnect(clearCredentials: true). Because push.unregisterDevice is a live, non-queued sync command (sendPushCommand throws once offline), the task can run after the socket has been torn down and the desktop keeps this device's APNs registration, so an unpaired phone can continue receiving pushes from that machine. Await the unregister (or otherwise make it complete before closing the sync connection) on the connected path.

Useful? React with 👍 / 👎.

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.

1 participant