@@ -57,6 +57,7 @@ final class NodeAppModel {
5757
5858 private let deepLinkLogger = Logger ( subsystem: " ai.openclaw.ios " , category: " DeepLink " )
5959 private let pushWakeLogger = Logger ( subsystem: " ai.openclaw.ios " , category: " PushWake " )
60+ private let pendingActionLogger = Logger ( subsystem: " ai.openclaw.ios " , category: " PendingAction " )
6061 private let locationWakeLogger = Logger ( subsystem: " ai.openclaw.ios " , category: " LocationWake " )
6162 private let watchReplyLogger = Logger ( subsystem: " ai.openclaw.ios " , category: " WatchReply " )
6263 enum CameraHUDKind {
@@ -130,6 +131,7 @@ final class NodeAppModel {
130131 private var backgroundReconnectLeaseUntil : Date ?
131132 private var lastSignificantLocationWakeAt : Date ?
132133 @ObservationIgnored private let watchReplyCoordinator = WatchReplyCoordinator ( )
134+ private var pendingForegroundActionDrainInFlight = false
133135
134136 private var gatewayConnected = false
135137 private var operatorConnected = false
@@ -329,6 +331,9 @@ final class NodeAppModel {
329331 }
330332 await self . talkMode. resumeAfterBackground ( wasSuspended: suspended, wasKeptActive: keptActive)
331333 }
334+ Task { [ weak self] in
335+ await self ? . resumePendingForegroundNodeActionsIfNeeded ( trigger: " scene_active " )
336+ }
332337 }
333338 if phase == . active, self . reconnectAfterBackgroundArmed {
334339 self . reconnectAfterBackgroundArmed = false
@@ -2098,6 +2103,22 @@ private extension NodeAppModel {
20982103}
20992104
21002105extension NodeAppModel {
2106+ private struct PendingForegroundNodeAction : Decodable {
2107+ var id : String
2108+ var command : String
2109+ var paramsJSON : String ?
2110+ var enqueuedAtMs : Int ?
2111+ }
2112+
2113+ private struct PendingForegroundNodeActionsResponse : Decodable {
2114+ var nodeId : String ?
2115+ var actions : [ PendingForegroundNodeAction ]
2116+ }
2117+
2118+ private struct PendingForegroundNodeActionsAckRequest : Encodable {
2119+ var ids : [ String ]
2120+ }
2121+
21012122 private func refreshShareRouteFromGateway( ) async {
21022123 struct Params : Codable {
21032124 var includeGlobal : Bool
@@ -2195,6 +2216,83 @@ extension NodeAppModel {
21952216 func onNodeGatewayConnected( ) async {
21962217 await self . registerAPNsTokenIfNeeded ( )
21972218 await self . flushQueuedWatchRepliesIfConnected ( )
2219+ await self . resumePendingForegroundNodeActionsIfNeeded ( trigger: " node_connected " )
2220+ }
2221+
2222+ private func resumePendingForegroundNodeActionsIfNeeded( trigger: String ) async {
2223+ guard !self . isBackgrounded else { return }
2224+ guard await self . isGatewayConnected ( ) else { return }
2225+ guard !self . pendingForegroundActionDrainInFlight else { return }
2226+
2227+ self . pendingForegroundActionDrainInFlight = true
2228+ defer { self . pendingForegroundActionDrainInFlight = false }
2229+
2230+ do {
2231+ let payload = try await self . nodeGateway. request (
2232+ method: " node.pending.pull " ,
2233+ paramsJSON: " {} " ,
2234+ timeoutSeconds: 6 )
2235+ let decoded = try JSONDecoder ( ) . decode (
2236+ PendingForegroundNodeActionsResponse . self,
2237+ from: payload)
2238+ guard !decoded. actions. isEmpty else { return }
2239+ self . pendingActionLogger. info (
2240+ " Pending actions pulled trigger= \( trigger, privacy: . public) "
2241+ + " count= \( decoded. actions. count, privacy: . public) " )
2242+ await self . applyPendingForegroundNodeActions ( decoded. actions, trigger: trigger)
2243+ } catch {
2244+ // Best-effort only.
2245+ }
2246+ }
2247+
2248+ private func applyPendingForegroundNodeActions(
2249+ _ actions: [ PendingForegroundNodeAction ] ,
2250+ trigger: String ) async
2251+ {
2252+ for action in actions {
2253+ guard !self . isBackgrounded else {
2254+ self . pendingActionLogger. info (
2255+ " Pending action replay paused trigger= \( trigger, privacy: . public) : app backgrounded " )
2256+ return
2257+ }
2258+ let req = BridgeInvokeRequest (
2259+ id: action. id,
2260+ command: action. command,
2261+ paramsJSON: action. paramsJSON)
2262+ let result = await self . handleInvoke ( req)
2263+ self . pendingActionLogger. info (
2264+ " Pending action replay trigger= \( trigger, privacy: . public) "
2265+ + " id= \( action. id, privacy: . public) command= \( action. command, privacy: . public) "
2266+ + " ok= \( result. ok, privacy: . public) " )
2267+ guard result. ok else { return }
2268+ let acked = await self . ackPendingForegroundNodeAction (
2269+ id: action. id,
2270+ trigger: trigger,
2271+ command: action. command)
2272+ guard acked else { return }
2273+ }
2274+ }
2275+
2276+ private func ackPendingForegroundNodeAction(
2277+ id: String ,
2278+ trigger: String ,
2279+ command: String ) async -> Bool
2280+ {
2281+ do {
2282+ let payload = try JSONEncoder ( ) . encode ( PendingForegroundNodeActionsAckRequest ( ids: [ id] ) )
2283+ let paramsJSON = String ( decoding: payload, as: UTF8 . self)
2284+ _ = try await self . nodeGateway. request (
2285+ method: " node.pending.ack " ,
2286+ paramsJSON: paramsJSON,
2287+ timeoutSeconds: 6 )
2288+ return true
2289+ } catch {
2290+ self . pendingActionLogger. error (
2291+ " Pending action ack failed trigger= \( trigger, privacy: . public) "
2292+ + " id= \( id, privacy: . public) command= \( command, privacy: . public) "
2293+ + " error= \( String ( describing: error) , privacy: . public) " )
2294+ return false
2295+ }
21982296 }
21992297
22002298 private func handleWatchQuickReply( _ event: WatchQuickReplyEvent ) async {
@@ -2843,6 +2941,19 @@ extension NodeAppModel {
28432941 self . gatewayConnected = connected
28442942 }
28452943
2944+ func _test_applyPendingForegroundNodeActions(
2945+ _ actions: [ ( id: String , command: String , paramsJSON: String ? ) ] ) async
2946+ {
2947+ let mapped = actions. map { action in
2948+ PendingForegroundNodeAction (
2949+ id: action. id,
2950+ command: action. command,
2951+ paramsJSON: action. paramsJSON,
2952+ enqueuedAtMs: nil )
2953+ }
2954+ await self . applyPendingForegroundNodeActions ( mapped, trigger: " test " )
2955+ }
2956+
28462957 static func _test_currentDeepLinkKey( ) -> String {
28472958 self . expectedDeepLinkKey ( )
28482959 }
0 commit comments