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");