From bfdfd35e5d68ba363b94332f293ccfdd9d4911a7 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 13 Jul 2026 05:00:48 -0700 Subject: [PATCH] fix(mac): allow slow node worker startup (#106281) * fix(mac): allow slow node worker startup * chore: leave release notes to release flow --- .../OpenClaw/NodeMode/MacNodeHostWorker.swift | 9 ++++++++- .../OpenClawIPCTests/MacNodeHostWorkerTests.swift | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/apps/macos/Sources/OpenClaw/NodeMode/MacNodeHostWorker.swift b/apps/macos/Sources/OpenClaw/NodeMode/MacNodeHostWorker.swift index 48d55390dafe..dff70cd16e49 100644 --- a/apps/macos/Sources/OpenClaw/NodeMode/MacNodeHostWorker.swift +++ b/apps/macos/Sources/OpenClaw/NodeMode/MacNodeHostWorker.swift @@ -27,6 +27,8 @@ protocol MacNodeHostWorking: Sendable { /// The worker never connects to Gateway; this app remains the sole node identity /// and keeps TCC-sensitive execution behind the native exec-host socket. final class MacNodeHostWorker: MacNodeHostWorking, @unchecked Sendable { + nonisolated static let defaultStartupTimeout: TimeInterval = 120 + enum WorkerError: LocalizedError { case unavailable(String) @@ -41,6 +43,7 @@ final class MacNodeHostWorker: MacNodeHostWorking, @unchecked Sendable { private let queue = DispatchQueue(label: "ai.openclaw.node-host-worker") private let writerQueue = DispatchQueue(label: "ai.openclaw.node-host-worker.writer") private let session: GatewayNodeSession + private let startupTimeout: TimeInterval private let onUnexpectedExit: @Sendable () -> Void private var process: Process? private var stdinPipe: Pipe? @@ -65,9 +68,11 @@ final class MacNodeHostWorker: MacNodeHostWorking, @unchecked Sendable { init( session: GatewayNodeSession, + startupTimeout: TimeInterval = MacNodeHostWorker.defaultStartupTimeout, onUnexpectedExit: @escaping @Sendable () -> Void = {}) { self.session = session + self.startupTimeout = startupTimeout self.onUnexpectedExit = onUnexpectedExit } @@ -228,7 +233,9 @@ final class MacNodeHostWorker: MacNodeHostWorking, @unchecked Sendable { } let timer = DispatchSource.makeTimerSource(queue: self.queue) - timer.schedule(deadline: .now() + 20) + // Cold config and plugin discovery can exceed the old 20-second bound. + // Keep a hard deadline, but leave enough room for a cold CLI worker start. + timer.schedule(deadline: .now() + self.startupTimeout) timer.setEventHandler { [weak self] in guard let self else { return } let state = self.process?.isRunning == true ? "running" : "exited" diff --git a/apps/macos/Tests/OpenClawIPCTests/MacNodeHostWorkerTests.swift b/apps/macos/Tests/OpenClawIPCTests/MacNodeHostWorkerTests.swift index 7bd64ccbf5bc..aa806e169e34 100644 --- a/apps/macos/Tests/OpenClawIPCTests/MacNodeHostWorkerTests.swift +++ b/apps/macos/Tests/OpenClawIPCTests/MacNodeHostWorkerTests.swift @@ -29,6 +29,21 @@ private actor StubMacNodeHostWorker: MacNodeHostWorking { @Suite(.serialized) struct MacNodeHostWorkerTests { + @Test func `worker allows a generous cold-start window`() async throws { + #expect(MacNodeHostWorker.defaultStartupTimeout == 120) + + let worker = MacNodeHostWorker(session: GatewayNodeSession(), startupTimeout: 1) + let script = """ + sleep 0.1 + printf '%s\\n' '{"type":"ready","version":"test","manifest":{"caps":[],"commands":[],"pathEnv":"/usr/bin:/bin"}}' + while IFS= read -r line; do :; done + """ + + let manifest = try await worker.start(command: ["/bin/sh", "-c", script]) + #expect(manifest.version == "test") + await worker.stop() + } + @Test func `Mac runtime forwards CLI node commands to the shared worker`() async { let worker = StubMacNodeHostWorker() let runtime = MacNodeRuntime(nodeHostWorker: worker)