import Foundation import WatchConnectivity struct WatchReplyDraft { var replyId: String var promptId: String var actionId: String var actionLabel: String? var sessionKey: String? var gatewayStableID: String? var note: String? var sentAtMs: Int64 } enum WatchReplyDeliveryState: Equatable { case delivered case queued case notSent } struct WatchReplySendResult: Equatable { var delivery: WatchReplyDeliveryState var transport: String var errorMessage: String? var requiresCanonicalReadback: Bool var deliveredImmediately: Bool { self.delivery == .delivered } var queuedForDelivery: Bool { self.delivery == .queued } } struct WatchExecApprovalSnapshotRequestToken: Hashable, Sendable { let requestId: String let gatewayStableID: String private let requestKey: WatchOpaqueUTF8Key private let gatewayKey: WatchOpaqueUTF8Key init?(requestId: String, gatewayStableID: String?) { guard !requestId.isEmpty, let gatewayStableID = WatchGatewayID.exact(gatewayStableID) else { return nil } self.requestId = requestId self.gatewayStableID = gatewayStableID self.requestKey = WatchOpaqueUTF8Key(requestId) self.gatewayKey = WatchOpaqueUTF8Key(gatewayStableID) } static func == (lhs: Self, rhs: Self) -> Bool { lhs.requestKey == rhs.requestKey && lhs.gatewayKey == rhs.gatewayKey } func hash(into hasher: inout Hasher) { hasher.combine(self.requestKey) hasher.combine(self.gatewayKey) } func matchesGatewayStableID(_ gatewayStableID: String?) -> Bool { WatchGatewayID.key(gatewayStableID) == self.gatewayKey } } final class WatchConnectivityReceiver: NSObject, @unchecked Sendable { private typealias MessageSendContinuation = CheckedContinuation private static let maxAcceptedExecApprovalSnapshotRequests = 32 private let store: WatchInboxStore private let session: WCSession? private let activationGate = WatchSessionActivationGate() private let execApprovalSnapshotAcknowledgmentLock = NSLock() private var acceptedExecApprovalSnapshotRequests: Set = [] private var acceptedExecApprovalSnapshotRequestOrder: [WatchExecApprovalSnapshotRequestToken] = [] private let directNodeSetupHandler: @MainActor @Sendable (String, Int64) -> Void init( store: WatchInboxStore, directNodeSetupHandler: @escaping @MainActor @Sendable (String, Int64) -> Void) { self.store = store self.directNodeSetupHandler = directNodeSetupHandler if WCSession.isSupported() { self.session = WCSession.default } else { self.session = nil } super.init() } func activate() { guard let session else { return } session.delegate = self self.beginActivation(session) } private func beginActivation(_ session: WCSession) { if self.activationGate.beginActivation() { session.activate() } } private func activatedSession() async throws -> WCSession { guard let session else { throw WatchSessionActivationError.failed("session unavailable") } if session.activationState == .activated { self.activationGate.complete(activated: true, errorDescription: nil) return session } self.beginActivation(session) try await self.activationGate.waitUntilActivated() guard session.activationState == .activated else { throw WatchSessionActivationError.failed("session stayed inactive") } return session } @discardableResult func requestExecApprovalSnapshot( gatewayStableID: String? = nil, heldApprovals: [WatchExecApprovalSnapshotRequestItem] = []) async -> WatchExecApprovalSnapshotRequestToken? { guard let session = try? await activatedSession() else { return nil } let requestId = UUID().uuidString let exactGatewayStableID = WatchGatewayID.exact(gatewayStableID) let request = WatchExecApprovalSnapshotRequestMessage( requestId: requestId, sentAtMs: Self.nowMs(), gatewayStableID: exactGatewayStableID, heldApprovals: heldApprovals) let token = WatchExecApprovalSnapshotRequestToken( requestId: requestId, gatewayStableID: exactGatewayStableID) let payload = Self.encodeSnapshotRequestPayload(request) if session.isReachable { do { try await Self.sendMessage(payload, through: session) return token } catch { // Fall through to queued delivery. } } _ = session.transferUserInfo(payload) return token } func requestAppSnapshot() async -> WatchReplySendResult { let session: WCSession do { session = try await self.activatedSession() } catch { return Self.unavailableResult(error) } let request = WatchAppSnapshotRequestMessage( requestId: UUID().uuidString, sentAtMs: Self.nowMs()) let payload = Self.encodeAppSnapshotRequestPayload(request) return await self.sendPayload(payload, session: session) } func sendReply(_ draft: WatchReplyDraft) async -> WatchReplySendResult { let session: WCSession do { session = try await self.activatedSession() } catch { return Self.unavailableResult(error) } var payload: [String: Any] = [ "type": WatchPayloadType.reply.rawValue, "replyId": draft.replyId, "promptId": draft.promptId, "actionId": draft.actionId, "sentAtMs": draft.sentAtMs, ] if let actionLabel = draft.actionLabel?.trimmingCharacters(in: .whitespacesAndNewlines), !actionLabel.isEmpty { payload["actionLabel"] = actionLabel } if let sessionKey = draft.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines), !sessionKey.isEmpty { payload["sessionKey"] = sessionKey } if let gatewayStableID = WatchGatewayID.exact(draft.gatewayStableID) { payload["gatewayStableID"] = gatewayStableID } if let note = draft.note?.trimmingCharacters(in: .whitespacesAndNewlines), !note.isEmpty { payload["note"] = note } return await self.sendPayload(payload, session: session) } func sendExecApprovalResolve( approvalId: String, gatewayStableID: String?, attemptID: String, decision: WatchExecApprovalDecision) async -> WatchReplySendResult { let session: WCSession do { session = try await self.activatedSession() } catch { return Self.unavailableResult(error) } let payload = Self.encodeExecApprovalResolvePayload( WatchExecApprovalResolveMessage( approvalId: approvalId, gatewayStableID: gatewayStableID, decision: decision, replyId: attemptID, sentAtMs: Self.nowMs())) return await self.sendPayload(payload, session: session) } func sendAppCommand(_ message: WatchAppCommandMessage) async -> WatchReplySendResult { let session: WCSession do { session = try await self.activatedSession() } catch { return Self.unavailableResult(error) } return await self.sendPayload(Self.encodeAppCommandPayload(message), session: session) } private func sendPayload(_ payload: [String: Any], session: WCSession) async -> WatchReplySendResult { var requiresCanonicalReadback = false if session.isReachable { do { try await Self.sendMessage(payload, through: session) return WatchReplySendResult( delivery: .delivered, transport: "sendMessage", errorMessage: nil, requiresCanonicalReadback: false) } catch { // The immediate send may have reached the iPhone before its reply path // failed. Queue a durable copy, but require canonical state readback. requiresCanonicalReadback = true // Fall through to queued delivery below. } } _ = session.transferUserInfo(payload) return WatchReplySendResult( delivery: .queued, transport: "transferUserInfo", errorMessage: nil, requiresCanonicalReadback: requiresCanonicalReadback) } private static func sendMessage(_ payload: [String: Any], through session: WCSession) async throws { try await withCheckedThrowingContinuation(isolation: nil) { (continuation: MessageSendContinuation) in session.sendMessage( payload, replyHandler: { _ in continuation.resume(returning: ()) }, errorHandler: { error in continuation.resume(throwing: error) }) } } private static func unavailableResult(_ error: any Error) -> WatchReplySendResult { // Activation failed before a payload could be handed to WatchConnectivity. // The closed notSent state lets callers safely offer an immediate retry. WatchReplySendResult( delivery: .notSent, transport: "none", errorMessage: error.localizedDescription, requiresCanonicalReadback: false) } private static func nowMs() -> Int64 { Int64(Date().timeIntervalSince1970 * 1000) } func consumeExecApprovalSnapshotAcknowledgment( for token: WatchExecApprovalSnapshotRequestToken) -> Bool { self.execApprovalSnapshotAcknowledgmentLock.withLock { guard self.acceptedExecApprovalSnapshotRequests.remove(token) != nil else { return false } self.acceptedExecApprovalSnapshotRequestOrder.removeAll { $0 == token } return true } } func discardExecApprovalSnapshotAcknowledgments(exceptGatewayStableID gatewayStableID: String?) { self.execApprovalSnapshotAcknowledgmentLock.withLock { self.acceptedExecApprovalSnapshotRequestOrder.removeAll { token in !token.matchesGatewayStableID(gatewayStableID) } self.acceptedExecApprovalSnapshotRequests = Set( self.acceptedExecApprovalSnapshotRequestOrder) } } private func recordAcceptedExecApprovalSnapshot(_ snapshot: WatchExecApprovalSnapshotMessage) { guard let requestId = snapshot.requestId, let token = WatchExecApprovalSnapshotRequestToken( requestId: requestId, gatewayStableID: snapshot.requestGatewayStableID), WatchGatewayID.key(snapshot.gatewayStableID) == WatchGatewayID.key(token.gatewayStableID) else { return } self.execApprovalSnapshotAcknowledgmentLock.withLock { guard self.acceptedExecApprovalSnapshotRequests.insert(token).inserted else { return } self.acceptedExecApprovalSnapshotRequestOrder.append(token) // Responses can arrive after their refresh task is cancelled. Bound retained // acknowledgments while keeping enough room for WatchConnectivity reordering. if self.acceptedExecApprovalSnapshotRequestOrder.count > Self.maxAcceptedExecApprovalSnapshotRequests { let evicted = self.acceptedExecApprovalSnapshotRequestOrder.removeFirst() self.acceptedExecApprovalSnapshotRequests.remove(evicted) } } } private static func normalizeObject(_ value: Any) -> [String: Any]? { if let object = value as? [String: Any] { return object } if let object = value as? [AnyHashable: Any] { var normalized: [String: Any] = [:] normalized.reserveCapacity(object.count) for (key, item) in object { guard let stringKey = key as? String else { continue } normalized[stringKey] = item } return normalized } return nil } private static func parseActions(_ value: Any?) -> [WatchPromptAction] { guard let raw = value as? [Any] else { return [] } return raw.compactMap { item in guard let obj = Self.normalizeObject(item) else { return nil } let id = (obj["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let label = (obj["label"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard !id.isEmpty, !label.isEmpty else { return nil } let style = (obj["style"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) return WatchPromptAction(id: id, label: label, style: style) } } private static func parseNotificationPayload(_ payload: [String: Any]) -> WatchNotifyMessage? { guard let type = payload["type"] as? String, type == WatchPayloadType.notify.rawValue else { return nil } let title = (payload["title"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let body = (payload["body"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard title.isEmpty == false || body.isEmpty == false else { return nil } let id = (payload["id"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) let sentAtMs = (payload["sentAtMs"] as? NSNumber)?.int64Value let promptId = (payload["promptId"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) let sessionKey = (payload["sessionKey"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) let gatewayStableID = WatchGatewayID.exact(payload["gatewayStableID"] as? String) let kind = (payload["kind"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) let details = (payload["details"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) let expiresAtMs = (payload["expiresAtMs"] as? NSNumber)?.int64Value let risk = (payload["risk"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) let actions = Self.parseActions(payload["actions"]) return WatchNotifyMessage( id: id, title: title, body: body, sentAtMs: sentAtMs, promptId: promptId, sessionKey: sessionKey, gatewayStableID: gatewayStableID, kind: kind, details: details, expiresAtMs: expiresAtMs, risk: risk, actions: actions) } private static func parseExecApprovalDecision(_ value: Any?) -> WatchExecApprovalDecision? { let raw = (value as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" return WatchExecApprovalDecision(rawValue: raw) } private static func parseExecApprovalItem(_ value: Any?) -> WatchExecApprovalItem? { guard let payload = value.flatMap(normalizeObject) else { return nil } guard let id = WatchApprovalID.exact(payload["id"] as? String) else { return nil } let commandText = (payload["commandText"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard !commandText.isEmpty else { return nil } let commandPreview = (payload["commandPreview"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) let warningText = (payload["warningText"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) let host = (payload["host"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) let nodeId = (payload["nodeId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) let agentId = (payload["agentId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) let gatewayStableID = WatchGatewayID.exact(payload["gatewayStableID"] as? String) let expiresAtMs = (payload["expiresAtMs"] as? NSNumber)?.int64Value let riskRaw = (payload["risk"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let risk = WatchRiskLevel(rawValue: riskRaw) let allowedDecisions = (payload["allowedDecisions"] as? [Any] ?? []).compactMap { Self.parseExecApprovalDecision($0) } return WatchExecApprovalItem( id: id, gatewayStableID: gatewayStableID, commandText: commandText, commandPreview: commandPreview, warningText: warningText?.isEmpty == false ? warningText : nil, host: host, nodeId: nodeId, agentId: agentId, expiresAtMs: expiresAtMs, allowedDecisions: allowedDecisions, risk: risk) } private static func parseExecApprovalPromptPayload( _ payload: [String: Any]) -> WatchExecApprovalPromptMessage? { guard let type = payload["type"] as? String, type == WatchPayloadType.execApprovalPrompt.rawValue, let approval = parseExecApprovalItem(payload["approval"]) else { return nil } let sentAtMs = (payload["sentAtMs"] as? NSNumber)?.int64Value let resetResolutionAttemptId = (payload["resetResolutionAttemptId"] as? String) .flatMap { $0.isEmpty ? nil : $0 } return WatchExecApprovalPromptMessage( approval: approval, sentAtMs: sentAtMs, resetResolutionAttemptId: resetResolutionAttemptId) } private static func parseExecApprovalResolvedPayload( _ payload: [String: Any]) -> WatchExecApprovalResolvedMessage? { guard let type = payload["type"] as? String, type == WatchPayloadType.execApprovalResolved.rawValue else { return nil } guard let approvalId = WatchApprovalID.exact(payload["approvalId"] as? String) else { return nil } let decision = Self.parseExecApprovalDecision(payload["decision"]) let gatewayStableID = WatchGatewayID.exact(payload["gatewayStableID"] as? String) let resolvedAtMs = (payload["resolvedAtMs"] as? NSNumber)?.int64Value let source = (payload["source"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) let outcomeText = (payload["outcomeText"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) return WatchExecApprovalResolvedMessage( approvalId: approvalId, gatewayStableID: gatewayStableID, decision: decision, resolvedAtMs: resolvedAtMs, source: source, outcomeText: outcomeText?.isEmpty == false ? outcomeText : nil) } private static func parseExecApprovalExpiredPayload( _ payload: [String: Any]) -> WatchExecApprovalExpiredMessage? { guard let type = payload["type"] as? String, type == WatchPayloadType.execApprovalExpired.rawValue else { return nil } guard let approvalId = WatchApprovalID.exact(payload["approvalId"] as? String) else { return nil } let rawReason = (payload["reason"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard let reason = WatchExecApprovalCloseReason(rawValue: rawReason) else { return nil } let expiredAtMs = (payload["expiredAtMs"] as? NSNumber)?.int64Value let gatewayStableID = WatchGatewayID.exact(payload["gatewayStableID"] as? String) return WatchExecApprovalExpiredMessage( approvalId: approvalId, gatewayStableID: gatewayStableID, reason: reason, expiredAtMs: expiredAtMs) } private static func parseExecApprovalSnapshotPayload( _ payload: [String: Any]) -> WatchExecApprovalSnapshotMessage? { guard let type = payload["type"] as? String, type == WatchPayloadType.execApprovalSnapshot.rawValue else { return nil } guard let rawApprovals = payload["approvals"] as? [Any] else { return nil } var approvals: [WatchExecApprovalItem] = [] approvals.reserveCapacity(rawApprovals.count) for item in rawApprovals { guard let approval = Self.parseExecApprovalItem(item) else { return nil } approvals.append(approval) } let gatewayStableID = WatchGatewayID.exact(payload["gatewayStableID"] as? String) let sentAtMs = (payload["sentAtMs"] as? NSNumber)?.int64Value let snapshotId = (payload["snapshotId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) let requestId = (payload["requestId"] as? String).flatMap { $0.isEmpty ? nil : $0 } let requestGatewayStableID = WatchGatewayID.exact(payload["requestGatewayStableID"] as? String) return WatchExecApprovalSnapshotMessage( approvals: approvals, gatewayStableID: gatewayStableID, sentAtMs: sentAtMs, snapshotId: snapshotId, requestId: requestId, requestGatewayStableID: requestGatewayStableID) } private static func parseAppSnapshotPayload(_ payload: [String: Any]) -> WatchAppSnapshotMessage? { guard let type = payload["type"] as? String, type == WatchPayloadType.appSnapshot.rawValue else { return nil } let gatewayStatusText = (payload["gatewayStatusText"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let agentName = (payload["agentName"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let agentAvatarURL = (payload["agentAvatarUrl"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) let agentAvatarText = (payload["agentAvatarText"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) let sessionKey = (payload["sessionKey"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let gatewayStableID = WatchGatewayID.exact(payload["gatewayStableID"] as? String) let talkStatusText = (payload["talkStatusText"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let pendingApprovalCount = (payload["pendingApprovalCount"] as? Int) ?? (payload["pendingApprovalCount"] as? NSNumber)?.intValue ?? 0 let sentAtMs = (payload["sentAtMs"] as? NSNumber)?.int64Value let snapshotId = (payload["snapshotId"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) let chatItems = (payload["chatItems"] as? [Any])?.compactMap(Self.parseChatItem) let chatStatusText = (payload["chatStatusText"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) return WatchAppSnapshotMessage( gatewayStatusText: gatewayStatusText.isEmpty ? "Unknown" : gatewayStatusText, gatewayConnected: Self.boolValue(payload["gatewayConnected"]), agentName: agentName.isEmpty ? "Main" : agentName, agentAvatarURL: agentAvatarURL?.isEmpty == false ? agentAvatarURL : nil, agentAvatarText: agentAvatarText?.isEmpty == false ? agentAvatarText : nil, sessionKey: sessionKey.isEmpty ? "main" : sessionKey, gatewayStableID: gatewayStableID, talkStatusText: talkStatusText.isEmpty ? "Off" : talkStatusText, talkEnabled: Self.boolValue(payload["talkEnabled"]), talkListening: Self.boolValue(payload["talkListening"]), talkSpeaking: Self.boolValue(payload["talkSpeaking"]), pendingApprovalCount: max(0, pendingApprovalCount), chatItems: chatItems, chatStatusText: chatStatusText?.isEmpty == false ? chatStatusText : nil, sentAtMs: sentAtMs, snapshotId: snapshotId) } private static func parseChatCompletionPayload( _ payload: [String: Any]) -> WatchChatCompletionMessage? { guard (payload["type"] as? String) == WatchPayloadType.chatCompletion.rawValue else { return nil } let commandId = (payload["commandId"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let replyText = (payload["replyText"] as? String)? .trimmingCharacters(in: .whitespacesAndNewlines) ?? "" guard !commandId.isEmpty, !replyText.isEmpty else { return nil } let sentAtMs = (payload["sentAtMs"] as? NSNumber)?.int64Value return WatchChatCompletionMessage( commandId: commandId, replyText: replyText, sentAtMs: sentAtMs) } private static func parseChatItem(_ item: Any) -> WatchChatItem? { guard let dict = item as? [String: Any] else { return nil } guard let id = (dict["id"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines), !id.isEmpty else { return nil } let trimmedRole = (dict["role"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) ?? "" let text = (dict["text"] as? String)?.trimmingCharacters(in: .whitespacesAndNewlines) guard let text, !text.isEmpty else { return nil } let timestampMs = (dict["timestampMs"] as? NSNumber)?.int64Value return WatchChatItem( id: id, role: trimmedRole.isEmpty ? "assistant" : trimmedRole, text: text, timestampMs: timestampMs) } private static func boolValue(_ value: Any?) -> Bool { if let bool = value as? Bool { return bool } if let number = value as? NSNumber { return number.boolValue } return false } private static func encodeAppSnapshotRequestPayload( _ request: WatchAppSnapshotRequestMessage) -> [String: Any] { var payload: [String: Any] = [ "type": WatchPayloadType.appSnapshotRequest.rawValue, "requestId": request.requestId, ] if let sentAtMs = request.sentAtMs { payload["sentAtMs"] = sentAtMs } return payload } private static func encodeAppCommandPayload(_ message: WatchAppCommandMessage) -> [String: Any] { var payload: [String: Any] = [ "type": WatchPayloadType.appCommand.rawValue, "command": message.command.rawValue, "commandId": message.commandId, ] if let sessionKey = message.sessionKey?.trimmingCharacters(in: .whitespacesAndNewlines), !sessionKey.isEmpty { payload["sessionKey"] = sessionKey } if let gatewayStableID = WatchGatewayID.exact(message.gatewayStableID) { payload["gatewayStableID"] = gatewayStableID } if let text = message.text?.trimmingCharacters(in: .whitespacesAndNewlines), !text.isEmpty { payload["text"] = text } if let sentAtMs = message.sentAtMs { payload["sentAtMs"] = sentAtMs } return payload } private static func encodeSnapshotRequestPayload( _ request: WatchExecApprovalSnapshotRequestMessage) -> [String: Any] { var payload: [String: Any] = [ "type": WatchPayloadType.execApprovalSnapshotRequest.rawValue, "requestId": request.requestId, "heldApprovals": request.heldApprovals.map { item in var encoded: [String: Any] = [ "approvalId": item.approvalId, ] if let attemptID = item.activeResolutionAttemptId, !attemptID.isEmpty { encoded["activeResolutionAttemptId"] = attemptID } return encoded }, ] if let sentAtMs = request.sentAtMs { payload["sentAtMs"] = sentAtMs } if let gatewayStableID = WatchGatewayID.exact(request.gatewayStableID) { payload["gatewayStableID"] = gatewayStableID } return payload } private static func encodeExecApprovalResolvePayload( _ message: WatchExecApprovalResolveMessage) -> [String: Any] { var payload: [String: Any] = [ "type": WatchPayloadType.execApprovalResolve.rawValue, "approvalId": message.approvalId, "decision": message.decision.rawValue, "replyId": message.replyId, ] if let gatewayStableID = WatchGatewayID.exact(message.gatewayStableID) { payload["gatewayStableID"] = gatewayStableID } if let sentAtMs = message.sentAtMs { payload["sentAtMs"] = sentAtMs } return payload } } extension WatchConnectivityReceiver: WCSessionDelegate { func session( _ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: (any Error)?) { self.activationGate.complete( activated: activationState == .activated, errorDescription: error?.localizedDescription) if activationState == .activated, !session.receivedApplicationContext.isEmpty { self.consumeIncomingPayload( session.receivedApplicationContext, transport: "receivedApplicationContext") } Task { @MainActor in let gatewayStableID = self.store.execApprovalReviewGatewayStableID await self.requestExecApprovalSnapshot( gatewayStableID: gatewayStableID, heldApprovals: self.store.execApprovalSnapshotRequestItems( gatewayStableID: gatewayStableID)) } } func session(_: WCSession, didReceiveMessage message: [String: Any]) { self.consumeIncomingPayload(message, transport: "sendMessage") } func session( _: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) { replyHandler(["ok": true]) self.consumeIncomingPayload(message, transport: "sendMessage") } func session(_: WCSession, didReceiveUserInfo userInfo: [String: Any]) { self.consumeIncomingPayload(userInfo, transport: "transferUserInfo") } func session(_: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) { self.consumeIncomingPayload(applicationContext, transport: "applicationContext") } private func consumeIncomingPayload(_ payload: [String: Any], transport: String) { if let type = payload["type"] as? String, type == WatchPayloadType.directNodeSetup.rawValue, let setupCode = payload["setupCode"] as? String, let sentAtMs = (payload["sentAtMs"] as? NSNumber)?.int64Value { Task { @MainActor in self.directNodeSetupHandler(setupCode, sentAtMs) } return } let appSnapshot = (payload[WatchPayloadType.appSnapshot.rawValue] as? [String: Any]) .flatMap(Self.parseAppSnapshotPayload) let execApprovalSnapshot = (payload[WatchPayloadType.execApprovalSnapshot.rawValue] as? [String: Any]) .flatMap(Self.parseExecApprovalSnapshotPayload) if appSnapshot != nil || execApprovalSnapshot != nil { // Owner state must land first so approvals are filtered against this context's route. Task { @MainActor in if let appSnapshot { self.store.consume(appSnapshot: appSnapshot) self.discardExecApprovalSnapshotAcknowledgments( exceptGatewayStableID: appSnapshot.gatewayStableID) } if let execApprovalSnapshot { if self.store.consume(execApprovalSnapshot: execApprovalSnapshot, transport: transport) { self.recordAcceptedExecApprovalSnapshot(execApprovalSnapshot) } } if appSnapshot != nil { for snapshot in self.store.replayDeferredGatewayPayloads() { self.recordAcceptedExecApprovalSnapshot(snapshot) } } } return } if let incoming = Self.parseNotificationPayload(payload) { Task { @MainActor in self.store.consume(message: incoming, transport: transport) } return } if let prompt = Self.parseExecApprovalPromptPayload(payload) { Task { @MainActor in self.store.consume(execApprovalPrompt: prompt, transport: transport) } return } if let resolved = Self.parseExecApprovalResolvedPayload(payload) { Task { @MainActor in self.store.consume(execApprovalResolved: resolved) } return } if let expired = Self.parseExecApprovalExpiredPayload(payload) { Task { @MainActor in self.store.consume(execApprovalExpired: expired) } return } if let snapshot = Self.parseExecApprovalSnapshotPayload(payload) { Task { @MainActor in if self.store.consume(execApprovalSnapshot: snapshot, transport: transport) { self.recordAcceptedExecApprovalSnapshot(snapshot) } } return } if let snapshot = Self.parseAppSnapshotPayload(payload) { Task { @MainActor in self.store.consume(appSnapshot: snapshot) self.discardExecApprovalSnapshotAcknowledgments( exceptGatewayStableID: snapshot.gatewayStableID) for snapshot in self.store.replayDeferredGatewayPayloads() { self.recordAcceptedExecApprovalSnapshot(snapshot) } } return } if let completion = Self.parseChatCompletionPayload(payload) { Task { @MainActor in self.store.consume(chatCompletion: completion) } } } }