From f66c162ba63b85e2f96299867435fa3c84816a63 Mon Sep 17 00:00:00 2001 From: ToToKr Date: Thu, 27 Aug 2026 16:35:15 +0900 Subject: [PATCH] fix(daemon): prevent macOS node startup hangs (#130097) Prevent macOS node LaunchAgents from activating inherited Node compile caches while keeping the managed cache for worker processes. Co-authored-by: Ayaan Zaidi --- src/daemon/service-env.test.ts | 39 +++++++++++++++++++ src/daemon/service-env.ts | 3 ++ .../node-worker-bundle-installer.test.ts | 25 ++++++++++++ src/node-host/node-worker-environment.ts | 12 ++++-- src/node-host/node-worker-supervisor.test.ts | 7 ++++ 5 files changed, 83 insertions(+), 3 deletions(-) diff --git a/src/daemon/service-env.test.ts b/src/daemon/service-env.test.ts index f8481ba3918b..51d4816a1b3e 100644 --- a/src/daemon/service-env.test.ts +++ b/src/daemon/service-env.test.ts @@ -5,6 +5,7 @@ import path from "node:path"; import { describe, expect, it, vi } from "vitest"; import { resolveAutoNodeExtraCaCerts } from "../bootstrap/node-extra-ca-certs.js"; import { inspectGatewayHeapLimit } from "./gateway-heap.js"; +import { buildLaunchAgentPlist } from "./launchd-plist.js"; import { resolveGatewayStateDir } from "./paths.js"; import { buildNodeServiceEnvironment, @@ -784,6 +785,44 @@ describe("buildNodeServiceEnvironment", () => { expect(env.OPENCLAW_LAUNCHD_LABEL).toBe("ai.openclaw.node"); }); + it("fences inherited Node compile cache in macOS node LaunchAgents", () => { + const environment = buildNodeServiceEnvironment({ + env: { + HOME: "/Users/user", + NODE_COMPILE_CACHE: "/tmp/ambient-node-compile-cache", + }, + platform: "darwin", + }); + const plist = buildLaunchAgentPlist({ + label: "ai.openclaw.node", + programArguments: ["/usr/local/bin/node", "dist/index.js", "node", "run"], + stdoutPath: "/tmp/openclaw-node.log", + stderrPath: "/tmp/openclaw-node.err.log", + environment, + }); + + expect(environment.NODE_DISABLE_COMPILE_CACHE).toBe("1"); + expect(environment.NODE_COMPILE_CACHE).toBeUndefined(); + expect(plist).toContain("NODE_DISABLE_COMPILE_CACHE"); + expect(plist).not.toContain("NODE_COMPILE_CACHE"); + }); + + it.each(["linux", "win32"] as const)( + "does not force Node compile cache off for %s node services", + (platform) => { + const environment = buildNodeServiceEnvironment({ + env: { + HOME: platform === "win32" ? "C:\\Users\\user" : "/home/user", + NODE_COMPILE_CACHE: "/tmp/ambient-node-compile-cache", + }, + platform, + }); + + expect(environment.NODE_DISABLE_COMPILE_CACHE).toBeUndefined(); + expect(environment.NODE_COMPILE_CACHE).toBeUndefined(); + }, + ); + it("passes through OPENCLAW_GATEWAY_TOKEN for node services", () => { const env = buildNodeServiceEnvironment({ env: { HOME: "/home/user", OPENCLAW_GATEWAY_TOKEN: " node-token " }, diff --git a/src/daemon/service-env.ts b/src/daemon/service-env.ts index 45effaba6203..1cc2cabed988 100644 --- a/src/daemon/service-env.ts +++ b/src/daemon/service-env.ts @@ -392,6 +392,9 @@ export function buildNodeServiceEnvironment(params: { CF_ACCESS_CLIENT_ID: cloudflareAccessClientId, CF_ACCESS_CLIENT_SECRET: cloudflareAccessClientSecret, OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: allowInsecurePrivateWs, + // launchd manager variables outlive the installer. Worker snapshots scope + // this host fence by the canonical managed-node service identity. + NODE_DISABLE_COMPILE_CACHE: platform === "darwin" ? "1" : undefined, ...resolveNodeServiceIdentityEnvironment(), }; } diff --git a/src/node-host/node-worker-bundle-installer.test.ts b/src/node-host/node-worker-bundle-installer.test.ts index 11fbd667983a..8e2e7b6f22c1 100644 --- a/src/node-host/node-worker-bundle-installer.test.ts +++ b/src/node-host/node-worker-bundle-installer.test.ts @@ -180,6 +180,31 @@ describe("node worker bundle installer", () => { await expect(fs.readFile(prewarmMarker, "utf8")).resolves.toBe("ready"); }); + it("prewarms bundles with managed cache behind a host compile-cache fence", async () => { + const prewarmMarker = path.join(root, "worker-prewarmed-with-managed-cache"); + const fixture = await bundleFixture({ + prewarmMarker, + bundlePrewarm: 1, + compileCacheDisabled: false, + }); + const served = await serve(fixture.archive, fixture.input.archive.token); + const installer = new NodeWorkerBundleInstaller({ + root, + env: { + ...process.env, + NODE_COMPILE_CACHE: "/tmp/ambient-host-compile-cache", + NODE_DISABLE_COMPILE_CACHE: "1", + OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.node", + OPENCLAW_SERVICE_KIND: "node", + }, + }); + + await expect( + installer.ensure({ input: fixture.input, gatewayUrl: served.gatewayUrl }), + ).resolves.toEqual(fixture.input.build); + await expect(fs.readFile(prewarmMarker, "utf8")).resolves.toBe("ready"); + }); + it("reuses a v1 install when Windows cannot retain Unix artifact modes", async () => { const fixture = await bundleFixture(); const served = await serve(fixture.archive, fixture.input.archive.token); diff --git a/src/node-host/node-worker-environment.ts b/src/node-host/node-worker-environment.ts index ecbab646b781..479bc0af8864 100644 --- a/src/node-host/node-worker-environment.ts +++ b/src/node-host/node-worker-environment.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import { NODE_SERVICE_KIND, resolveNodeLaunchAgentLabel } from "../daemon/constants.js"; import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js"; const POSIX_WORKER_ENV_KEYS = new Set([ @@ -50,10 +51,15 @@ export function snapshotNodeWorkerEnv(source: NodeJS.ProcessEnv): NodeJS.Process } snapshot[key] = value; } - if (source.NODE_DISABLE_COMPILE_CACHE === undefined) { + const hostCacheFenced = + source.NODE_DISABLE_COMPILE_CACHE !== undefined && + source.OPENCLAW_SERVICE_KIND === NODE_SERVICE_KIND && + source.OPENCLAW_LAUNCHD_LABEL === resolveNodeLaunchAgentLabel(); + const workerCacheDisabled = source.NODE_DISABLE_COMPILE_CACHE !== undefined && !hostCacheFenced; + if (!workerCacheDisabled) { + const requestedCache = hostCacheFenced ? undefined : source.NODE_COMPILE_CACHE?.trim(); snapshot.NODE_COMPILE_CACHE = - source.NODE_COMPILE_CACHE?.trim() || - path.join(resolvePreferredOpenClawTmpDir(), "node-worker-compile-cache"); + requestedCache || path.join(resolvePreferredOpenClawTmpDir(), "node-worker-compile-cache"); } else { snapshot.NODE_DISABLE_COMPILE_CACHE = "1"; } diff --git a/src/node-host/node-worker-supervisor.test.ts b/src/node-host/node-worker-supervisor.test.ts index e5529a46b1de..7096289cb4b7 100644 --- a/src/node-host/node-worker-supervisor.test.ts +++ b/src/node-host/node-worker-supervisor.test.ts @@ -445,9 +445,13 @@ describe("node worker supervisor", () => { HOME: path.join(root, "worker-home"), LANG: "en_US.UTF-8", LC_TIME: "de_DE.UTF-8", + NODE_COMPILE_CACHE: path.join(root, "host-compile-cache"), + NODE_DISABLE_COMPILE_CACHE: "1", NODE_EXTRA_CA_CERTS: path.join(root, "private-ca.pem"), NODE_USE_SYSTEM_CA: "1", OPENCLAW_ALLOW_INSECURE_PRIVATE_WS: "1", + OPENCLAW_LAUNCHD_LABEL: "ai.openclaw.node", + OPENCLAW_SERVICE_KIND: "node", OPENCLAW_SUPPLIED_SECRET: "supplied-openclaw-secret", NODE_OPTIONS: "--title=forbidden-worker-title", BASH_ENV: path.join(root, "forbidden-shell-init"), @@ -486,8 +490,11 @@ describe("node worker supervisor", () => { expect(workerEnv).toMatchObject(expectedWorkerEnv); expect(workerEnv).not.toHaveProperty("AMBIENT_SECRET"); expect(workerEnv).not.toHaveProperty("OPENCLAW_AMBIENT_SECRET"); + expect(workerEnv).not.toHaveProperty("OPENCLAW_LAUNCHD_LABEL"); + expect(workerEnv).not.toHaveProperty("OPENCLAW_SERVICE_KIND"); expect(workerEnv).not.toHaveProperty("OPENCLAW_STATE_DIR"); expect(workerEnv).not.toHaveProperty("OPENCLAW_SUPPLIED_SECRET"); + expect(workerEnv).not.toHaveProperty("NODE_DISABLE_COMPILE_CACHE"); expect(workerEnv).not.toHaveProperty("NODE_OPTIONS"); expect(workerEnv).not.toHaveProperty("BASH_ENV"); expect(workerEnv).not.toHaveProperty("DYLD_INSERT_LIBRARIES");