diff --git a/apps/macos/Sources/Clawdis/GatewayChannel.swift b/apps/macos/Sources/Clawdis/GatewayChannel.swift index f12a0f103..b85cf54d0 100644 --- a/apps/macos/Sources/Clawdis/GatewayChannel.swift +++ b/apps/macos/Sources/Clawdis/GatewayChannel.swift @@ -23,6 +23,8 @@ private actor GatewayChannelActor { private var task: URLSessionWebSocketTask? private var pending: [String: CheckedContinuation] = [:] private var connected = false + private var isConnecting = false + private var connectWaiters: [CheckedContinuation] = [] private var url: URL private var token: String? private let session = URLSession(configuration: .default) @@ -68,6 +70,15 @@ private actor GatewayChannelActor { func connect() async throws { if self.connected, self.task?.state == .running { return } + if self.isConnecting { + try await withCheckedThrowingContinuation { cont in + self.connectWaiters.append(cont) + } + return + } + self.isConnecting = true + defer { self.isConnecting = false } + self.task?.cancel(with: .goingAway, reason: nil) self.task = self.session.webSocketTask(with: self.url) self.task?.resume() @@ -75,12 +86,26 @@ private actor GatewayChannelActor { try await self.sendHello() } catch { let wrapped = self.wrap(error, context: "connect to gateway @ \(self.url.absoluteString)") + self.connected = false + self.task?.cancel(with: .goingAway, reason: nil) + let waiters = self.connectWaiters + self.connectWaiters.removeAll() + for waiter in waiters { + waiter.resume(throwing: wrapped) + } + self.logger.error("gateway ws connect failed \(wrapped.localizedDescription, privacy: .public)") throw wrapped } self.listen() self.connected = true self.backoffMs = 500 self.lastSeq = nil + + let waiters = self.connectWaiters + self.connectWaiters.removeAll() + for waiter in waiters { + waiter.resume(returning: ()) + } } private func sendHello() async throws { @@ -141,7 +166,7 @@ private actor GatewayChannelActor { return } if let err = try? decoder.decode(HelloError.self, from: data) { - let reason = err.reason ?? "unknown" + let reason = err.reason // Log and throw a detailed error so UI can surface token/hello issues. self.logger.error("gateway hello-error: \(reason, privacy: .public)") throw NSError( @@ -284,6 +309,13 @@ private actor GatewayChannelActor { try await self.task?.send(.data(data)) } catch { self.pending.removeValue(forKey: id) + // Treat send failures as a broken socket: mark disconnected and trigger reconnect. + self.connected = false + self.task?.cancel(with: .goingAway, reason: nil) + Task { [weak self] in + guard let self else { return } + await self.scheduleReconnect() + } cont.resume(throwing: self.wrap(error, context: "gateway send \(method)")) } }