Skip to content

iOS/Gateway: wake disconnected iOS nodes via APNs before invoke#20332

Merged
mbelinky merged 4 commits intomainfrom
feat/ios-apns-wake-reconnect
Feb 18, 2026
Merged

iOS/Gateway: wake disconnected iOS nodes via APNs before invoke#20332
mbelinky merged 4 commits intomainfrom
feat/ios-apns-wake-reconnect

Conversation

@mbelinky
Copy link
Contributor

@mbelinky mbelinky commented Feb 18, 2026

Summary

  • Problem: iOS nodes disconnect in background and node.invoke fails immediately with node not connected.
  • Why it matters: node-targeted commands fail unless users foreground the app manually.
  • What changed: gateway now sends APNs silent wake for disconnected iOS nodes before invoke, waits briefly for reconnect, and then retries invoke; iOS app now handles silent push wake to trigger reconnect; iOS README rewritten with alpha/manual deploy/push guidance.
  • What did NOT change (scope boundary): no Android work, no TestFlight/App Store distribution pipeline, no changes to command allowlist policy.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

  • Closes #
  • Related #

User-visible / Behavior Changes

  • node.invoke now attempts APNs background wake for disconnected iOS nodes (throttled), waits up to ~3s for reconnect, then proceeds.
  • iOS app now responds to silent APNs wake notifications and triggers gateway reconnect while backgrounded.
  • iOS README now explicitly states NO TEST FLIGHT AVAILABLE AT THIS POINT and documents manual Xcode deploy + current alpha limitations.

Security Impact (required)

  • New permissions/capabilities? (No)
  • Secrets/tokens handling changed? (No)
  • New/changed network calls? (Yes)
  • Command/tool execution surface changed? (No)
  • Data access scope changed? (No)
  • If any Yes, explain risk + mitigation:
    • Gateway now sends APNs background pushes in addition to alert pushes. Mitigations: existing APNs auth config is reused, wake attempts are best-effort, and per-node wake attempts are throttled.

Repro + Verification

Environment

  • OS: macOS host + physical iPhone
  • Runtime/container: Node 22+/pnpm
  • Model/provider: N/A
  • Integration/channel (if any): APNs sandbox
  • Relevant config (redacted): OPENCLAW_APNS_TEAM_ID, OPENCLAW_APNS_KEY_ID, OPENCLAW_APNS_PRIVATE_KEY_PATH

Steps

  1. Put paired iOS app in background so node is disconnected.
  2. Call node.invoke against iOS node.
  3. Observe APNs wake + reconnect path and invoke result.

Expected

  • Gateway attempts wake, node reconnects, invoke succeeds when reconnect lands in wait window.

Actual

  • Verified on physical device; invoke succeeded after wake/reconnect.

Evidence

  • Failing test/log before + passing after
  • Trace/log snippets
  • Screenshot/recording
  • Perf numbers (if relevant)

Human Verification (required)

  • Verified scenarios:
    • Targeted tests pass:
      • pnpm test -- src/infra/push-apns.test.ts src/gateway/server-methods/nodes.invoke-wake.test.ts
    • Physical iPhone redeploy and background wake test succeeded end-to-end.
  • Edge cases checked:
    • No APNs registration -> retains existing node not connected behavior.
    • Wake throttling avoids repeated APNs spam for same disconnected node.
  • What you did not verify:
    • Production APNs env behavior.
    • Long background suspension / low-power edge behavior.

Compatibility / Migration

  • Backward compatible? (Yes)
  • Config/env changes? (No)
  • Migration needed? (No)
  • If yes, exact upgrade steps:

Failure Recovery (if this breaks)

  • How to disable/revert this change quickly:
    • Revert this PR or disable APNs env vars so wake path becomes no-op and existing behavior remains.
  • Files/config to restore:
    • src/gateway/server-methods/nodes.ts
    • src/infra/push-apns.ts
    • iOS app changes in apps/ios/Sources/OpenClawApp.swift and apps/ios/Sources/Model/NodeAppModel.swift
  • Known bad symptoms reviewers should watch for:
    • Unexpected repeated APNs wake attempts.
    • iOS app reconnect loops while foregrounded.

Risks and Mitigations

  • Risk: silent push may not be delivered under some iOS background conditions.
    • Mitigation: behavior remains best-effort and falls back to current node not connected failure mode.
  • Risk: wake path might add latency to failed invokes on disconnected nodes.
    • Mitigation: reconnect wait window is capped (~3s) and wake attempts are throttled.

Greptile Summary

This PR adds APNs silent push wake support so the gateway can wake disconnected iOS nodes before node.invoke fails. The implementation spans three layers:

  • Gateway (nodes.ts, push-apns.ts): When node.invoke finds a disconnected node, it sends an APNs background push, then polls for up to ~3s for the node to reconnect. Wake attempts are throttled per-node (15s). A new push.test gateway method and CLI command are also added for debugging APNs delivery.
  • iOS app (OpenClawApp.swift, NodeAppModel.swift): The app registers for remote notifications at launch, sends the APNs device token to the gateway via push.apns.register node event on connect, and handles silent push wakes by triggering a gateway reconnect when backgrounded.
  • Supporting changes: session key alias matching ("main""agent:main:main") fixes chat event routing, A2UI asset resolution is improved with retry-on-null caching and additional path candidates, Watch bundle IDs are parameterized via xcconfig, and the iOS README is rewritten for the current alpha state.

Key observations:

  • The nodeWakeById throttle map in nodes.ts never evicts entries — acceptable for personal gateway usage but worth noting for long-lived processes.
  • There is a race condition where the APNs device token delivery to appDelegate.appModel could be missed on first-ever launch since the model is assigned asynchronously via .task {}. Recovery via UserDefaults and onNodeGatewayConnected mitigates this.
  • The WatchConnectivity replyHandler is now called before dispatching to @MainActor, fixing a potential WatchKit timeout.
  • Test coverage is solid with dedicated tests for the wake path, throttling, push.test handler, and APNs send semantics.

Confidence Score: 4/5

  • This PR is safe to merge with minor concerns around a timing race and unbounded map growth, both of which have reasonable mitigations.
  • The core wake-before-invoke logic is well-structured with throttling, best-effort semantics, and proper fallback to existing behavior. The APNs integration follows Apple's documented patterns correctly. Test coverage is thorough. The two issues flagged (unbounded map, token race) are low-severity and have built-in recovery paths. The overall design is defensive and backward-compatible.
  • apps/ios/Sources/OpenClawApp.swift (APNs token delivery race) and src/gateway/server-methods/nodes.ts (unbounded wake state map)

Last reviewed commit: 2001b84

@openclaw-barnacle openclaw-barnacle bot added app: ios App: ios app: web-ui App: web-ui gateway Gateway runtime cli CLI command changes size: XL maintainer Maintainer-authored PR labels Feb 18, 2026
Copy link
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

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

33 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

inFlight?: Promise<boolean>;
};

const nodeWakeById = new Map<string, NodeWakeState>();
Copy link
Contributor

Choose a reason for hiding this comment

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

Unbounded nodeWakeById map growth

Entries are added to nodeWakeById on each unique nodeId wake attempt but never removed. Over a long gateway lifetime with many different node IDs, this map grows without bound. In practice this is low-risk for a personal gateway with a handful of iOS nodes, but consider adding periodic cleanup (e.g., pruning entries older than NODE_WAKE_THROTTLE_MS) or using a bounded LRU-style eviction to prevent a slow memory leak in longer-running deployments.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/gateway/server-methods/nodes.ts
Line: 51:51

Comment:
**Unbounded `nodeWakeById` map growth**

Entries are added to `nodeWakeById` on each unique `nodeId` wake attempt but never removed. Over a long gateway lifetime with many different node IDs, this map grows without bound. In practice this is low-risk for a personal gateway with a handful of iOS nodes, but consider adding periodic cleanup (e.g., pruning entries older than `NODE_WAKE_THROTTLE_MS`) or using a bounded LRU-style eviction to prevent a slow memory leak in longer-running deployments.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines 69 to 71
.task {
self.appDelegate.appModel = self.appModel
}
Copy link
Contributor

Choose a reason for hiding this comment

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

Race between delegate callback and appModel assignment

appDelegate.appModel is set inside a .task {} modifier, which runs asynchronously after the view appears. However, registerForRemoteNotifications() is called synchronously in didFinishLaunchingWithOptions, and iOS may invoke didRegisterForRemoteNotificationsWithDeviceToken before this .task runs — especially if a cached token is available. In that case, self.appModel is still nil and the token delivery is silently dropped.

The token is recovered from UserDefaults on subsequent launches (since NodeAppModel.init() reads it at line 167), and registerAPNsTokenIfNeeded() is also called from onNodeGatewayConnected(), so the practical impact is limited. But on a first-ever launch, the initial APNs token could be missed until the next app restart. Consider assigning appDelegate.appModel earlier (e.g., in OpenClawApp.init()) to close this window.

Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/ios/Sources/OpenClawApp.swift
Line: 69:71

Comment:
**Race between delegate callback and `appModel` assignment**

`appDelegate.appModel` is set inside a `.task {}` modifier, which runs asynchronously after the view appears. However, `registerForRemoteNotifications()` is called synchronously in `didFinishLaunchingWithOptions`, and iOS may invoke `didRegisterForRemoteNotificationsWithDeviceToken` before this `.task` runs — especially if a cached token is available. In that case, `self.appModel` is still `nil` and the token delivery is silently dropped.

The token is recovered from `UserDefaults` on subsequent launches (since `NodeAppModel.init()` reads it at line 167), and `registerAPNsTokenIfNeeded()` is also called from `onNodeGatewayConnected()`, so the practical impact is limited. But on a first-ever launch, the initial APNs token could be missed until the next app restart. Consider assigning `appDelegate.appModel` earlier (e.g., in `OpenClawApp.init()`) to close this window.

How can I resolve this? If you propose a fix, please make it concise.

@mbelinky mbelinky force-pushed the feat/ios-apns-wake-reconnect branch from 2001b84 to 7751f9c Compare February 18, 2026 20:59
@openclaw-barnacle openclaw-barnacle bot added size: L and removed app: web-ui App: web-ui cli CLI command changes size: XL labels Feb 18, 2026
@mbelinky mbelinky merged commit e67da15 into main Feb 18, 2026
10 checks passed
@mbelinky mbelinky deleted the feat/ios-apns-wake-reconnect branch February 18, 2026 21:00
@mbelinky
Copy link
Contributor Author

Merged via squash.

Thanks @mbelinky!

anschmieg pushed a commit to anschmieg/openclaw that referenced this pull request Feb 19, 2026
…claw#20332)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7751f9c
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky
yneth-ray-openclaw pushed a commit to yneth-ray-openclaw/openclaw that referenced this pull request Feb 19, 2026
…claw#20332)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7751f9c
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky
HenryChenV pushed a commit to HenryChenV/openclaw that referenced this pull request Feb 20, 2026
…claw#20332)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 7751f9c
Co-authored-by: mbelinky <[email protected]>
Co-authored-by: mbelinky <[email protected]>
Reviewed-by: @mbelinky
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

app: ios App: ios gateway Gateway runtime maintainer Maintainer-authored PR size: L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments