fix(macos): stop idle node setup CPU loop (#124599)

Make validated CLI cache writes idempotent and reuse the startup-scoped node worker launch across route retries, keeping the signed menu-bar app near-zero CPU while idle.\n\nCloses #124592
This commit is contained in:
Peter Steinberger
2026-08-16 06:40:56 -07:00
committed by GitHub
parent 147fa7e4c3
commit c82dfdb43a
3 changed files with 56 additions and 5 deletions
@@ -201,7 +201,7 @@ enum CLIInstaller {
expectedVersion: GatewayEnvironment.expectedGatewayVersionString(),
preferredPaths: preferredPaths)
if status.isReady {
self.rememberValidated(status)
self.rememberValidated(status, defaults: AppDefaults.standard)
return status
}
fallbackStatus = fallbackStatus ?? status
@@ -225,7 +225,7 @@ enum CLIInstaller {
expectedVersion: expectedVersion,
preferredPaths: preferredPaths)
if status.isReady {
self.rememberValidated(status)
self.rememberValidated(status, defaults: AppDefaults.standard)
}
return status
}
@@ -316,10 +316,14 @@ enum CLIInstaller {
return environment
}
private static func rememberValidated(_ status: Status) {
static func rememberValidated(_ status: Status, defaults: UserDefaults) {
guard case let .ready(location, version) = status else { return }
AppDefaults.standard.set(location, forKey: cliValidatedExecutableKey)
AppDefaults.standard.set(version, forKey: cliValidatedVersionKey)
if defaults.string(forKey: cliValidatedExecutableKey) != location {
defaults.set(location, forKey: cliValidatedExecutableKey)
}
if defaults.string(forKey: cliValidatedVersionKey) != version {
defaults.set(version, forKey: cliValidatedVersionKey)
}
}
@discardableResult
@@ -936,6 +936,12 @@ extension MacNodeModeCoordinator {
guard self.nodeHostWorkerRetryTask == nil else {
throw MacNodeHostWorkerRetryPolicy.RetryBackoffPending()
}
if let activeInput = self.activeNodeHostWorkerInput {
// Worker launch metadata is startup-scoped. Route retries reuse it instead of
// repeating CLI and runtime discovery until an explicit restart resets state.
try self.nodeHostWorkerRetryPolicy.prepareForStart(activeInput)
return try await nodeHostWorker.start(launch: activeInput.launch)
}
let launch: MacNodeHostWorkerLaunch
do {
if let projectLaunch = try await CommandResolver.projectNodeHostWorkerLaunch() {
@@ -1,4 +1,5 @@
import Foundation
import os
import Testing
@testable import OpenClaw
@@ -291,6 +292,46 @@ struct CLIInstallerTests {
defaults: defaults) == "2026.7.2")
}
@Test func `validated CLI cache changes only when the ready tuple changes`() throws {
let suite = "CLIInstallerTests.validated-cache.\(UUID().uuidString)"
let defaults = try #require(UserDefaults(suiteName: suite))
defer { defaults.removePersistentDomain(forName: suite) }
let notificationCount = OSAllocatedUnfairLock(initialState: 0)
let initialLocation = "/Users/test/.local/bin/openclaw"
defaults.set(initialLocation, forKey: cliValidatedExecutableKey)
defaults.set("2026.8.1", forKey: cliValidatedVersionKey)
let observer = NotificationCenter.default.addObserver(
forName: UserDefaults.didChangeNotification,
object: defaults,
queue: nil)
{ _ in
notificationCount.withLock { $0 += 1 }
}
defer { NotificationCenter.default.removeObserver(observer) }
CLIInstaller.rememberValidated(
.ready(location: initialLocation, version: "2026.8.1"),
defaults: defaults)
#expect(notificationCount.withLock { $0 } == 0)
let updatedLocation = "/opt/homebrew/bin/openclaw"
CLIInstaller.rememberValidated(
.ready(location: updatedLocation, version: "2026.8.1"),
defaults: defaults)
#expect(notificationCount.withLock { $0 } == 1)
#expect(defaults.string(forKey: cliValidatedExecutableKey) == updatedLocation)
#expect(defaults.string(forKey: cliValidatedVersionKey) == "2026.8.1")
CLIInstaller.rememberValidated(
.ready(location: updatedLocation, version: "2026.8.2"),
defaults: defaults)
#expect(notificationCount.withLock { $0 } == 2)
#expect(defaults.string(forKey: cliValidatedVersionKey) == "2026.8.2")
}
@Test func `managed setup requires a parseable compatible version`() {
let location = "/Users/test/.openclaw/bin/openclaw"