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 <hi@obviy.us>
This commit is contained in:
ToToKr
2026-08-27 16:35:15 +09:00
committed by GitHub
parent 8d82e26e5f
commit f66c162ba6
5 changed files with 83 additions and 3 deletions
+39
View File
@@ -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("<key>NODE_DISABLE_COMPILE_CACHE</key>");
expect(plist).not.toContain("<key>NODE_COMPILE_CACHE</key>");
});
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 " },
+3
View File
@@ -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(),
};
}
@@ -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);
+9 -3
View File
@@ -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";
}
@@ -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");