iOS/Gateway: wake disconnected iOS nodes via APNs before invoke#20332
iOS/Gateway: wake disconnected iOS nodes via APNs before invoke#20332
Conversation
| inFlight?: Promise<boolean>; | ||
| }; | ||
|
|
||
| const nodeWakeById = new Map<string, NodeWakeState>(); |
There was a problem hiding this 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.
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.| .task { | ||
| self.appDelegate.appModel = self.appModel | ||
| } |
There was a problem hiding this 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.
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.2001b84 to
7751f9c
Compare
…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
…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
…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
Summary
node.invokefails immediately withnode not connected.Change Type (select all)
Scope (select all touched areas)
Linked Issue/PR
User-visible / Behavior Changes
node.invokenow attempts APNs background wake for disconnected iOS nodes (throttled), waits up to ~3s for reconnect, then proceeds.NO TEST FLIGHT AVAILABLE AT THIS POINTand documents manual Xcode deploy + current alpha limitations.Security Impact (required)
No)No)Yes)No)No)Yes, explain risk + mitigation:Repro + Verification
Environment
OPENCLAW_APNS_TEAM_ID,OPENCLAW_APNS_KEY_ID,OPENCLAW_APNS_PRIVATE_KEY_PATHSteps
node.invokeagainst iOS node.Expected
Actual
Evidence
Human Verification (required)
pnpm test -- src/infra/push-apns.test.ts src/gateway/server-methods/nodes.invoke-wake.test.tsnode not connectedbehavior.Compatibility / Migration
Yes)No)No)Failure Recovery (if this breaks)
src/gateway/server-methods/nodes.tssrc/infra/push-apns.tsapps/ios/Sources/OpenClawApp.swiftandapps/ios/Sources/Model/NodeAppModel.swiftRisks and Mitigations
node not connectedfailure mode.Greptile Summary
This PR adds APNs silent push wake support so the gateway can wake disconnected iOS nodes before
node.invokefails. The implementation spans three layers:nodes.ts,push-apns.ts): Whennode.invokefinds 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 newpush.testgateway method and CLI command are also added for debugging APNs delivery.OpenClawApp.swift,NodeAppModel.swift): The app registers for remote notifications at launch, sends the APNs device token to the gateway viapush.apns.registernode event on connect, and handles silent push wakes by triggering a gateway reconnect when backgrounded."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:
nodeWakeByIdthrottle map innodes.tsnever evicts entries — acceptable for personal gateway usage but worth noting for long-lived processes.appDelegate.appModelcould be missed on first-ever launch since the model is assigned asynchronously via.task {}. Recovery viaUserDefaultsandonNodeGatewayConnectedmitigates this.replyHandleris now called before dispatching to@MainActor, fixing a potential WatchKit timeout.Confidence Score: 4/5
apps/ios/Sources/OpenClawApp.swift(APNs token delivery race) andsrc/gateway/server-methods/nodes.ts(unbounded wake state map)Last reviewed commit: 2001b84