@@ -43,6 +43,7 @@ final class NodeAppModel {
4343 private let deepLinkLogger = Logger ( subsystem: " ai.openclaw.ios " , category: " DeepLink " )
4444 private let pushWakeLogger = Logger ( subsystem: " ai.openclaw.ios " , category: " PushWake " )
4545 private let locationWakeLogger = Logger ( subsystem: " ai.openclaw.ios " , category: " LocationWake " )
46+ private let watchReplyLogger = Logger ( subsystem: " ai.openclaw.ios " , category: " WatchReply " )
4647 enum CameraHUDKind {
4748 case photo
4849 case recording
@@ -109,6 +110,8 @@ final class NodeAppModel {
109110 private var backgroundReconnectSuppressed = false
110111 private var backgroundReconnectLeaseUntil : Date ?
111112 private var lastSignificantLocationWakeAt : Date ?
113+ private var queuedWatchReplies : [ WatchQuickReplyEvent ] = [ ]
114+ private var seenWatchReplyIds = Set < String > ( )
112115
113116 private var gatewayConnected = false
114117 private var operatorConnected = false
@@ -155,6 +158,11 @@ final class NodeAppModel {
155158 self . talkMode = talkMode
156159 self . apnsDeviceTokenHex = UserDefaults . standard. string ( forKey: Self . apnsDeviceTokenUserDefaultsKey)
157160 GatewayDiagnostics . bootstrap ( )
161+ self . watchMessagingService. setReplyHandler { [ weak self] event in
162+ Task { @MainActor in
163+ await self ? . handleWatchQuickReply ( event)
164+ }
165+ }
158166
159167 self . voiceWake. configure { [ weak self] cmd in
160168 guard let self else { return }
@@ -1608,9 +1616,7 @@ private extension NodeAppModel {
16081616 do {
16091617 let result = try await self . watchMessagingService. sendNotification (
16101618 id: req. id,
1611- title: title,
1612- body: body,
1613- priority: params. priority)
1619+ params: params)
16141620 let payload = OpenClawWatchNotifyPayload (
16151621 deliveredImmediately: result. deliveredImmediately,
16161622 queuedForDelivery: result. queuedForDelivery,
@@ -2255,6 +2261,90 @@ extension NodeAppModel {
22552261 /// Back-compat hook retained for older gateway-connect flows.
22562262 func onNodeGatewayConnected( ) async {
22572263 await self . registerAPNsTokenIfNeeded ( )
2264+ await self . flushQueuedWatchRepliesIfConnected ( )
2265+ }
2266+
2267+ private func handleWatchQuickReply( _ event: WatchQuickReplyEvent ) async {
2268+ let replyId = event. replyId. trimmingCharacters ( in: . whitespacesAndNewlines)
2269+ let actionId = event. actionId. trimmingCharacters ( in: . whitespacesAndNewlines)
2270+ if replyId. isEmpty || actionId. isEmpty {
2271+ self . watchReplyLogger. info ( " watch reply dropped: missing replyId/actionId " )
2272+ return
2273+ }
2274+
2275+ if self . seenWatchReplyIds. contains ( replyId) {
2276+ self . watchReplyLogger. debug (
2277+ " watch reply deduped replyId= \( replyId, privacy: . public) " )
2278+ return
2279+ }
2280+ self . seenWatchReplyIds. insert ( replyId)
2281+
2282+ if await !self . isGatewayConnected ( ) {
2283+ self . queuedWatchReplies. append ( event)
2284+ self . watchReplyLogger. info (
2285+ " watch reply queued replyId= \( replyId, privacy: . public) action= \( actionId, privacy: . public) " )
2286+ return
2287+ }
2288+
2289+ await self . forwardWatchReplyToAgent ( event)
2290+ }
2291+
2292+ private func flushQueuedWatchRepliesIfConnected( ) async {
2293+ guard await self . isGatewayConnected ( ) else { return }
2294+ guard !self . queuedWatchReplies. isEmpty else { return }
2295+
2296+ let pending = self . queuedWatchReplies
2297+ self . queuedWatchReplies. removeAll ( )
2298+ for event in pending {
2299+ await self . forwardWatchReplyToAgent ( event)
2300+ }
2301+ }
2302+
2303+ private func forwardWatchReplyToAgent( _ event: WatchQuickReplyEvent ) async {
2304+ let sessionKey = event. sessionKey? . trimmingCharacters ( in: . whitespacesAndNewlines)
2305+ let effectiveSessionKey = ( sessionKey? . isEmpty == false ) ? sessionKey : self . mainSessionKey
2306+ let message = Self . makeWatchReplyAgentMessage ( event)
2307+ let link = AgentDeepLink (
2308+ message: message,
2309+ sessionKey: effectiveSessionKey,
2310+ thinking: " low " ,
2311+ deliver: false ,
2312+ to: nil ,
2313+ channel: nil ,
2314+ timeoutSeconds: nil ,
2315+ key: event. replyId)
2316+ do {
2317+ try await self . sendAgentRequest ( link: link)
2318+ self . watchReplyLogger. info (
2319+ " watch reply forwarded replyId= \( event. replyId, privacy: . public) action= \( event. actionId, privacy: . public) " )
2320+ self . openChatRequestID &+= 1
2321+ } catch {
2322+ self . watchReplyLogger. error (
2323+ " watch reply forwarding failed replyId= \( event. replyId, privacy: . public) error= \( error. localizedDescription, privacy: . public) " )
2324+ self . queuedWatchReplies. insert ( event, at: 0 )
2325+ }
2326+ }
2327+
2328+ private static func makeWatchReplyAgentMessage( _ event: WatchQuickReplyEvent ) -> String {
2329+ let actionLabel = event. actionLabel? . trimmingCharacters ( in: . whitespacesAndNewlines)
2330+ let promptId = event. promptId. trimmingCharacters ( in: . whitespacesAndNewlines)
2331+ let transport = event. transport. trimmingCharacters ( in: . whitespacesAndNewlines)
2332+ let summary = actionLabel? . isEmpty == false ? actionLabel! : event. actionId
2333+ var lines : [ String ] = [ ]
2334+ lines. append ( " Watch reply: \( summary) " )
2335+ lines. append ( " promptId= \( promptId. isEmpty ? " unknown " : promptId) " )
2336+ lines. append ( " actionId= \( event. actionId) " )
2337+ lines. append ( " replyId= \( event. replyId) " )
2338+ if !transport. isEmpty {
2339+ lines. append ( " transport= \( transport) " )
2340+ }
2341+ if let sentAtMs = event. sentAtMs {
2342+ lines. append ( " sentAtMs= \( sentAtMs) " )
2343+ }
2344+ if let note = event. note? . trimmingCharacters ( in: . whitespacesAndNewlines) , !note. isEmpty {
2345+ lines. append ( " note= \( note) " )
2346+ }
2347+ return lines. joined ( separator: " \n " )
22582348 }
22592349
22602350 func handleSilentPushWake( _ userInfo: [ AnyHashable : Any ] ) async -> Bool {
@@ -2497,5 +2587,9 @@ extension NodeAppModel {
24972587 func _test_applyTalkModeSync( enabled: Bool , phase: String ? = nil ) {
24982588 self . applyTalkModeSync ( enabled: enabled, phase: phase)
24992589 }
2590+
2591+ func _test_queuedWatchReplyCount( ) -> Int {
2592+ self . queuedWatchReplies. count
2593+ }
25002594}
25012595#endif
0 commit comments