From f72ac60b01f166d3b168f70a56211e11c8d19034 Mon Sep 17 00:00:00 2001 From: Mariano Belinky Date: Sat, 31 Jan 2026 20:49:16 +0100 Subject: [PATCH] iOS: streamline notify timeouts --- .../Gateway/GatewayConnectionController.swift | 1 - apps/ios/Sources/Model/NodeAppModel.swift | 14 ++++++++------ .../Sources/OpenClawKit/GatewayNodeSession.swift | 7 +++++-- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/apps/ios/Sources/Gateway/GatewayConnectionController.swift b/apps/ios/Sources/Gateway/GatewayConnectionController.swift index 25fd2ac2f8..febc529f64 100644 --- a/apps/ios/Sources/Gateway/GatewayConnectionController.swift +++ b/apps/ios/Sources/Gateway/GatewayConnectionController.swift @@ -2,7 +2,6 @@ import AVFoundation import Contacts import CoreLocation import CoreMotion -import Darwin import EventKit import Foundation import OpenClawKit diff --git a/apps/ios/Sources/Model/NodeAppModel.swift b/apps/ios/Sources/Model/NodeAppModel.swift index b8d2b46112..06c21e6a23 100644 --- a/apps/ios/Sources/Model/NodeAppModel.swift +++ b/apps/ios/Sources/Model/NodeAppModel.swift @@ -1,14 +1,15 @@ import OpenClawKit -import Network import Observation import SwiftUI import UIKit import UserNotifications +// Wrap errors without pulling non-Sendable types into async notification paths. private struct NotificationCallError: Error, Sendable { let message: String } +// Ensures notification requests return promptly even if the system prompt blocks. private final class NotificationInvokeLatch: @unchecked Sendable { private let lock = NSLock() private var continuation: CheckedContinuation, Never>? @@ -1011,7 +1012,12 @@ final class NodeAppModel { let latch = NotificationInvokeLatch() var opTask: Task? var timeoutTask: Task? - let result = await withCheckedContinuation { (cont: CheckedContinuation, Never>) in + defer { + opTask?.cancel() + timeoutTask?.cancel() + } + let clamped = max(0.0, timeoutSeconds) + return await withCheckedContinuation { (cont: CheckedContinuation, Never>) in latch.setContinuation(cont) opTask = Task { @MainActor in do { @@ -1022,16 +1028,12 @@ final class NodeAppModel { } } timeoutTask = Task.detached { - let clamped = max(0.0, timeoutSeconds) if clamped > 0 { try? await Task.sleep(nanoseconds: UInt64(clamped * 1_000_000_000)) } latch.resume(.failure(NotificationCallError(message: "notification request timed out"))) } } - opTask?.cancel() - timeoutTask?.cancel() - return result } private func handleDeviceInvoke(_ req: BridgeInvokeRequest) async throws -> BridgeInvokeResponse { diff --git a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift index 217e795a05..dbc7dba3d6 100644 --- a/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift +++ b/apps/shared/OpenClawKit/Sources/OpenClawKit/GatewayNodeSession.swift @@ -43,6 +43,7 @@ public actor GatewayNodeSession { return await onInvoke(request) } + // Use an explicit latch so timeouts win even if onInvoke blocks (e.g., permission prompts). final class InvokeLatch: @unchecked Sendable { private let lock = NSLock() private var continuation: CheckedContinuation? @@ -72,6 +73,10 @@ public actor GatewayNodeSession { let latch = InvokeLatch() var onInvokeTask: Task? var timeoutTask: Task? + defer { + onInvokeTask?.cancel() + timeoutTask?.cancel() + } let response = await withCheckedContinuation { (cont: CheckedContinuation) in latch.setContinuation(cont) onInvokeTask = Task.detached { @@ -90,8 +95,6 @@ public actor GatewayNodeSession { )) } } - onInvokeTask?.cancel() - timeoutTask?.cancel() timeoutLogger.info("node invoke race resolved id=\(request.id, privacy: .public) ok=\(response.ok, privacy: .public)") return response }