From 8dfc031c4d50d02950344ff4d7f8afff723654e9 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 28 Dec 2025 09:24:43 +0000 Subject: [PATCH] fix: start gateway before control channel --- CHANGELOG.md | 1 + .../Clawdis/ConnectionModeCoordinator.swift | 15 ++++++++------- .../Sources/Clawdis/GatewayProcessManager.swift | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b506b84e..f1fd72d59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - Package contents: include Discord/hooks build outputs in the npm tarball to avoid missing module errors. - Heartbeat replies now drop any output containing `HEARTBEAT_OK`, preventing stray emoji/text from being delivered. - macOS menu now refreshes the control channel after the gateway starts and shows “Connecting to gateway…” while the gateway is coming up. +- macOS local mode now waits for the gateway to be ready before configuring the control channel. ## 2.0.0-beta3 — 2025-12-27 diff --git a/apps/macos/Sources/Clawdis/ConnectionModeCoordinator.swift b/apps/macos/Sources/Clawdis/ConnectionModeCoordinator.swift index 46866b655..d9abb5795 100644 --- a/apps/macos/Sources/Clawdis/ConnectionModeCoordinator.swift +++ b/apps/macos/Sources/Clawdis/ConnectionModeCoordinator.swift @@ -22,13 +22,6 @@ final class ConnectionModeCoordinator { case .local: await RemoteTunnelManager.shared.stopAll() WebChatManager.shared.resetTunnels() - do { - try await ControlChannel.shared.configure(mode: .local) - } catch { - // Control channel will mark itself degraded; nothing else to do here. - self.logger.error( - "control channel local configure failed: \(error.localizedDescription, privacy: .public)") - } let shouldStart = GatewayAutostartPolicy.shouldStartGateway(mode: .local, paused: paused) if shouldStart { GatewayProcessManager.shared.setActive(true) @@ -39,9 +32,17 @@ final class ConnectionModeCoordinator { { Task { await GatewayProcessManager.shared.ensureLaunchAgentEnabledIfNeeded() } } + _ = await GatewayProcessManager.shared.waitForGatewayReady() } else { GatewayProcessManager.shared.stop() } + do { + try await ControlChannel.shared.configure(mode: .local) + } catch { + // Control channel will mark itself degraded; nothing else to do here. + self.logger.error( + "control channel local configure failed: \(error.localizedDescription, privacy: .public)") + } Task.detached { await PortGuardian.shared.sweep(mode: .local) } case .remote: diff --git a/apps/macos/Sources/Clawdis/GatewayProcessManager.swift b/apps/macos/Sources/Clawdis/GatewayProcessManager.swift index 7c904d2cd..0b4a87c9f 100644 --- a/apps/macos/Sources/Clawdis/GatewayProcessManager.swift +++ b/apps/macos/Sources/Clawdis/GatewayProcessManager.swift @@ -320,6 +320,21 @@ final class GatewayProcessManager { Task { await ControlChannel.shared.configure() } } + func waitForGatewayReady(timeout: TimeInterval = 6) async -> Bool { + let deadline = Date().addingTimeInterval(timeout) + while Date() < deadline { + if !self.desiredActive { return false } + do { + _ = try await GatewayConnection.shared.requestRaw(method: .health, timeoutMs: 1500) + return true + } catch { + try? await Task.sleep(nanoseconds: 300_000_000) + } + } + self.appendLog("[gateway] readiness wait timed out\n") + return false + } + func clearLog() { self.log = "" try? FileManager.default.removeItem(atPath: LogLocator.launchdGatewayLogPath)