fix(cli): disable source checkout compile cache

This commit is contained in:
Peter Steinberger
2026-04-27 23:27:59 +01:00
parent 6e77c10c6c
commit 36d3722a96
6 changed files with 401 additions and 11 deletions

View File

@@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai
- CLI/model probes: fail local `infer model run` probes when the provider returns no text output, so unreachable local providers and empty completions no longer look like successful smoke tests. Refs #73023. Thanks @pavelyortho-cyber.
- CLI/Ollama: run local `infer model run` through the lean provider completion path and skip global model discovery for one-shot local probes, so Ollama smoke tests no longer pay full chat-agent/tool startup cost or hang before the native `/api/chat` request. Fixes #72851. Thanks @TotalRes2020.
- Daemon/service: only emit hard-coded version-manager paths such as `~/.volta/bin`, `~/.asdf/shims`, `~/.bun/bin`, and fnm/pnpm fallbacks into gateway and node service PATHs when the directories exist, so `openclaw doctor` no longer flags `gateway.path.non-minimal` against a PATH the daemon just wrote. Env-driven roots and stable user-bin dirs remain unconditional. Fixes #71944; carries forward #71964. Thanks @Sanjays2402.
- CLI/startup: disable Node's module compile cache automatically for live source-checkout launchers so in-place `pnpm build` updates are visible to the next `openclaw` CLI invocation. Fixes #73037. Thanks @LouisGameDev.
- Channels/commands: make generated `/dock-*` commands switch the active session reply route through `session.identityLinks` instead of falling through to normal chat. Fixes #69206; carries forward #73033. Thanks @clawbones and @michaelatamuk.
- Providers/Cloudflare AI Gateway: strip assistant prefill turns from Anthropic Messages payloads when thinking is enabled, so Claude requests through Cloudflare AI Gateway no longer fail Anthropic conversation-ending validation. Fixes #72905; carries forward #73005. Thanks @AaronFaby and @sahilsatralkar.
- Gateway/startup: keep primary-model startup prewarm on scoped metadata preparation, let native approval bootstraps retry outside channel startup, and skip the global hook runner when no `gateway_start` hook is registered, so clean post-ready sidecar work stays off the critical path. Refs #72846. Thanks @RayWoo, @livekm0309, and @mrz1836.

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env node
import { readFileSync } from "node:fs";
import { spawnSync } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { access } from "node:fs/promises";
import module from "node:module";
import { fileURLToPath } from "node:url";
@@ -38,8 +39,48 @@ const ensureSupportedNodeVersion = () => {
ensureSupportedNodeVersion();
const isSourceCheckoutLauncher = () =>
existsSync(new URL("./.git", import.meta.url)) ||
existsSync(new URL("./src/entry.ts", import.meta.url));
const isNodeCompileCacheDisabled = () => process.env.NODE_DISABLE_COMPILE_CACHE !== undefined;
const isNodeCompileCacheRequested = () =>
process.env.NODE_COMPILE_CACHE !== undefined && !isNodeCompileCacheDisabled();
const respawnWithoutCompileCacheIfNeeded = () => {
if (!isSourceCheckoutLauncher()) {
return false;
}
if (process.env.OPENCLAW_SOURCE_COMPILE_CACHE_RESPAWNED === "1") {
return false;
}
if (!module.getCompileCacheDir?.() && !isNodeCompileCacheRequested()) {
return false;
}
const env = {
...process.env,
NODE_DISABLE_COMPILE_CACHE: "1",
OPENCLAW_SOURCE_COMPILE_CACHE_RESPAWNED: "1",
};
delete env.NODE_COMPILE_CACHE;
const result = spawnSync(
process.execPath,
[...process.execArgv, fileURLToPath(import.meta.url), ...process.argv.slice(2)],
{
stdio: "inherit",
env,
},
);
if (result.error) {
throw result.error;
}
process.exit(result.status ?? 1);
};
respawnWithoutCompileCacheIfNeeded();
// https://nodejs.org/api/module.html#module-compile-cache
if (module.enableCompileCache && !process.env.NODE_DISABLE_COMPILE_CACHE) {
if (module.enableCompileCache && !isNodeCompileCacheDisabled() && !isSourceCheckoutLauncher()) {
try {
module.enableCompileCache();
} catch {

View File

@@ -0,0 +1,109 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { cleanupTempDirs, makeTempDir } from "../test/helpers/temp-dir.js";
import {
buildOpenClawCompileCacheRespawnPlan,
isSourceCheckoutInstallRoot,
resolveEntryInstallRoot,
shouldEnableOpenClawCompileCache,
} from "./entry.compile-cache.js";
describe("entry compile cache", () => {
const tempDirs: string[] = [];
afterEach(() => {
cleanupTempDirs(tempDirs);
});
it("resolves install roots from source and dist entry paths", () => {
expect(resolveEntryInstallRoot("/repo/openclaw/src/entry.ts")).toBe("/repo/openclaw");
expect(resolveEntryInstallRoot("/repo/openclaw/dist/entry.js")).toBe("/repo/openclaw");
expect(resolveEntryInstallRoot("/pkg/openclaw/entry.js")).toBe("/pkg/openclaw");
});
it("treats git and source entry markers as source checkouts", async () => {
const root = makeTempDir(tempDirs, "openclaw-compile-cache-source-");
await fs.writeFile(path.join(root, ".git"), "gitdir: .git/worktrees/openclaw\n", "utf8");
expect(isSourceCheckoutInstallRoot(root)).toBe(true);
});
it("disables compile cache for source-checkout installs", async () => {
const root = makeTempDir(tempDirs, "openclaw-compile-cache-src-entry-");
await fs.mkdir(path.join(root, "src"), { recursive: true });
await fs.writeFile(path.join(root, "src", "entry.ts"), "export {};\n", "utf8");
expect(
shouldEnableOpenClawCompileCache({
env: {},
installRoot: root,
}),
).toBe(false);
});
it("keeps compile cache enabled for packaged installs unless disabled by env", () => {
const root = makeTempDir(tempDirs, "openclaw-compile-cache-package-");
expect(shouldEnableOpenClawCompileCache({ env: {}, installRoot: root })).toBe(true);
expect(
shouldEnableOpenClawCompileCache({
env: { NODE_DISABLE_COMPILE_CACHE: "1" },
installRoot: root,
}),
).toBe(false);
});
it("builds a one-shot no-cache respawn plan when source checkout inherits NODE_COMPILE_CACHE", async () => {
const root = makeTempDir(tempDirs, "openclaw-compile-cache-respawn-");
await fs.mkdir(path.join(root, "src"), { recursive: true });
await fs.writeFile(path.join(root, "src", "entry.ts"), "export {};\n", "utf8");
const plan = buildOpenClawCompileCacheRespawnPlan({
currentFile: path.join(root, "dist", "entry.js"),
env: { NODE_COMPILE_CACHE: "/tmp/openclaw-cache" },
execArgv: ["--no-warnings"],
execPath: "/usr/bin/node",
installRoot: root,
argv: ["/usr/bin/node", path.join(root, "dist", "entry.js"), "status", "--json"],
});
expect(plan).toEqual({
command: "/usr/bin/node",
args: ["--no-warnings", path.join(root, "dist", "entry.js"), "status", "--json"],
env: {
NODE_DISABLE_COMPILE_CACHE: "1",
OPENCLAW_SOURCE_COMPILE_CACHE_RESPAWNED: "1",
},
});
});
it("does not respawn packaged installs when NODE_COMPILE_CACHE is configured", () => {
const root = makeTempDir(tempDirs, "openclaw-compile-cache-package-respawn-");
expect(
buildOpenClawCompileCacheRespawnPlan({
currentFile: path.join(root, "dist", "entry.js"),
env: { NODE_COMPILE_CACHE: "/tmp/openclaw-cache" },
installRoot: root,
}),
).toBeUndefined();
});
it("does not respawn source checkouts twice", async () => {
const root = makeTempDir(tempDirs, "openclaw-compile-cache-respawn-once-");
await fs.mkdir(path.join(root, "src"), { recursive: true });
await fs.writeFile(path.join(root, "src", "entry.ts"), "export {};\n", "utf8");
expect(
buildOpenClawCompileCacheRespawnPlan({
currentFile: path.join(root, "dist", "entry.js"),
env: {
NODE_COMPILE_CACHE: "/tmp/openclaw-cache",
OPENCLAW_SOURCE_COMPILE_CACHE_RESPAWNED: "1",
},
installRoot: root,
}),
).toBeUndefined();
});
});

114
src/entry.compile-cache.ts Normal file
View File

@@ -0,0 +1,114 @@
import { spawnSync } from "node:child_process";
import { existsSync } from "node:fs";
import { enableCompileCache, getCompileCacheDir } from "node:module";
import path from "node:path";
export function resolveEntryInstallRoot(entryFile: string): string {
const entryDir = path.dirname(entryFile);
const entryParent = path.basename(entryDir);
return entryParent === "dist" || entryParent === "src" ? path.dirname(entryDir) : entryDir;
}
export function isSourceCheckoutInstallRoot(installRoot: string): boolean {
return (
existsSync(path.join(installRoot, ".git")) ||
existsSync(path.join(installRoot, "src", "entry.ts"))
);
}
function isNodeCompileCacheDisabled(env: NodeJS.ProcessEnv | undefined): boolean {
return env?.NODE_DISABLE_COMPILE_CACHE !== undefined;
}
function isNodeCompileCacheRequested(env: NodeJS.ProcessEnv | undefined): boolean {
return env?.NODE_COMPILE_CACHE !== undefined && !isNodeCompileCacheDisabled(env);
}
export function shouldEnableOpenClawCompileCache(params: {
env?: NodeJS.ProcessEnv;
installRoot: string;
}): boolean {
if (isNodeCompileCacheDisabled(params.env)) {
return false;
}
return !isSourceCheckoutInstallRoot(params.installRoot);
}
export type OpenClawCompileCacheRespawnPlan = {
command: string;
args: string[];
env: NodeJS.ProcessEnv;
};
export function buildOpenClawCompileCacheRespawnPlan(params: {
currentFile: string;
env?: NodeJS.ProcessEnv;
execArgv?: string[];
execPath?: string;
installRoot: string;
argv?: string[];
compileCacheDir?: string;
}): OpenClawCompileCacheRespawnPlan | undefined {
const env = params.env ?? process.env;
if (!isSourceCheckoutInstallRoot(params.installRoot)) {
return undefined;
}
if (env.OPENCLAW_SOURCE_COMPILE_CACHE_RESPAWNED === "1") {
return undefined;
}
if (!params.compileCacheDir && !isNodeCompileCacheRequested(env)) {
return undefined;
}
const nextEnv: NodeJS.ProcessEnv = {
...env,
NODE_DISABLE_COMPILE_CACHE: "1",
OPENCLAW_SOURCE_COMPILE_CACHE_RESPAWNED: "1",
};
delete nextEnv.NODE_COMPILE_CACHE;
return {
command: params.execPath ?? process.execPath,
args: [
...(params.execArgv ?? process.execArgv),
params.currentFile,
...(params.argv ?? process.argv).slice(2),
],
env: nextEnv,
};
}
export function respawnWithoutOpenClawCompileCacheIfNeeded(params: {
currentFile: string;
installRoot: string;
}): boolean {
const plan = buildOpenClawCompileCacheRespawnPlan({
currentFile: params.currentFile,
installRoot: params.installRoot,
compileCacheDir: getCompileCacheDir?.(),
});
if (!plan) {
return false;
}
const result = spawnSync(plan.command, plan.args, {
stdio: "inherit",
env: plan.env,
});
if (result.error) {
throw result.error;
}
process.exit(result.status ?? 1);
return true;
}
export function enableOpenClawCompileCache(params: {
env?: NodeJS.ProcessEnv;
installRoot: string;
}): void {
if (!shouldEnableOpenClawCompileCache(params)) {
return;
}
try {
enableCompileCache();
} catch {
// Best-effort only; never block startup.
}
}

View File

@@ -1,15 +1,19 @@
#!/usr/bin/env node
import { spawn } from "node:child_process";
import { enableCompileCache } from "node:module";
import process from "node:process";
import { fileURLToPath } from "node:url";
import { isRootHelpInvocation } from "./cli/argv.js";
import { parseCliContainerArgs, resolveCliContainerTarget } from "./cli/container-target.js";
import { applyCliProfileEnv, parseCliProfileArgs } from "./cli/profile.js";
import { normalizeWindowsArgv } from "./cli/windows-argv.js";
import {
enableOpenClawCompileCache,
resolveEntryInstallRoot,
respawnWithoutOpenClawCompileCacheIfNeeded,
} from "./entry.compile-cache.js";
import { buildCliRespawnPlan } from "./entry.respawn.js";
import { tryHandleRootVersionFastPath } from "./entry.version-fast-path.js";
import { isTruthyEnvValue, normalizeEnv } from "./infra/env.js";
import { normalizeEnv } from "./infra/env.js";
import { isMainModule } from "./infra/is-main.js";
import { ensureOpenClawExecMarkerOnProcess } from "./infra/openclaw-exec-env.js";
import { installProcessWarningFilter } from "./infra/warning-filter.js";
@@ -43,17 +47,19 @@ if (
) {
// Imported as a dependency — skip all entry-point side effects.
} else {
const entryFile = fileURLToPath(import.meta.url);
const installRoot = resolveEntryInstallRoot(entryFile);
respawnWithoutOpenClawCompileCacheIfNeeded({
currentFile: entryFile,
installRoot,
});
process.title = "openclaw";
ensureOpenClawExecMarkerOnProcess();
installProcessWarningFilter();
normalizeEnv();
if (!isTruthyEnvValue(process.env.NODE_DISABLE_COMPILE_CACHE)) {
try {
enableCompileCache();
} catch {
// Best-effort only; never block startup.
}
}
enableOpenClawCompileCache({
installRoot,
});
if (shouldForceReadOnlyAuthStore(process.argv)) {
process.env.OPENCLAW_AUTH_STORE_READONLY = "1";

View File

@@ -19,6 +19,37 @@ async function addSourceTreeMarker(fixtureRoot: string): Promise<void> {
await fs.writeFile(path.join(fixtureRoot, "src", "entry.ts"), "export {};\n", "utf8");
}
async function addGitMarker(fixtureRoot: string): Promise<void> {
await fs.writeFile(path.join(fixtureRoot, ".git"), "gitdir: .git/worktrees/openclaw\n", "utf8");
}
async function addCompileCacheProbe(fixtureRoot: string): Promise<void> {
await fs.writeFile(
path.join(fixtureRoot, "dist", "entry.js"),
[
'import module from "node:module";',
"process.stdout.write(",
' `${module.getCompileCacheDir?.() ? "cache:enabled" : "cache:disabled"};respawn:${process.env.OPENCLAW_SOURCE_COMPILE_CACHE_RESPAWNED ?? "0"}`',
");",
].join("\n"),
"utf8",
);
}
function launcherEnv(extra: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
const env = { ...process.env, ...extra };
delete env.NODE_COMPILE_CACHE;
delete env.NODE_DISABLE_COMPILE_CACHE;
for (const [key, value] of Object.entries(extra)) {
if (value === undefined) {
delete env[key];
} else {
env[key] = value;
}
}
return env;
}
describe("openclaw launcher", () => {
const fixtureRoots: string[] = [];
@@ -36,6 +67,7 @@ describe("openclaw launcher", () => {
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs"), "--help"], {
cwd: fixtureRoot,
env: launcherEnv(),
encoding: "utf8",
});
@@ -49,6 +81,7 @@ describe("openclaw launcher", () => {
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs"), "--help"], {
cwd: fixtureRoot,
env: launcherEnv(),
encoding: "utf8",
});
@@ -62,6 +95,7 @@ describe("openclaw launcher", () => {
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs"), "--help"], {
cwd: fixtureRoot,
env: launcherEnv(),
encoding: "utf8",
});
@@ -71,4 +105,89 @@ describe("openclaw launcher", () => {
expect(result.stderr).toContain("pnpm install && pnpm build");
expect(result.stderr).toContain("github:openclaw/openclaw#<ref>");
});
it("keeps compile cache off for source-checkout launchers", async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
await addSourceTreeMarker(fixtureRoot);
await addCompileCacheProbe(fixtureRoot);
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs")], {
cwd: fixtureRoot,
env: launcherEnv(),
encoding: "utf8",
});
expect(result.status).toBe(0);
expect(result.stdout).toBe("cache:disabled;respawn:0");
});
it("respawns source-checkout launchers without inherited NODE_COMPILE_CACHE", async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
await addGitMarker(fixtureRoot);
await addCompileCacheProbe(fixtureRoot);
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs")], {
cwd: fixtureRoot,
env: launcherEnv({
NODE_COMPILE_CACHE: path.join(fixtureRoot, ".node-compile-cache"),
}),
encoding: "utf8",
});
expect(result.status).toBe(0);
expect(result.stdout).toBe("cache:disabled;respawn:1");
});
it.runIf(process.platform !== "win32")(
"respawns symlinked source-checkout launchers without inherited NODE_COMPILE_CACHE",
async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
await addGitMarker(fixtureRoot);
await addCompileCacheProbe(fixtureRoot);
const linkParent = makeTempDir(fixtureRoots, "openclaw-launcher-link-");
const linkedRoot = path.join(linkParent, "openclaw-linked");
await fs.symlink(fixtureRoot, linkedRoot, "dir");
const result = spawnSync(process.execPath, [path.join(linkedRoot, "openclaw.mjs")], {
cwd: linkParent,
env: launcherEnv({
NODE_COMPILE_CACHE: path.join(linkParent, ".node-compile-cache"),
}),
encoding: "utf8",
});
expect(result.status).toBe(0);
expect(result.stdout).toBe("cache:disabled;respawn:1");
},
);
it("does not respawn packaged launchers when NODE_COMPILE_CACHE is configured", async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
await addCompileCacheProbe(fixtureRoot);
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs")], {
cwd: fixtureRoot,
env: launcherEnv({
NODE_COMPILE_CACHE: path.join(fixtureRoot, ".node-compile-cache"),
}),
encoding: "utf8",
});
expect(result.status).toBe(0);
expect(result.stdout).toBe("cache:enabled;respawn:0");
});
it("enables compile cache for packaged launchers", async () => {
const fixtureRoot = await makeLauncherFixture(fixtureRoots);
await addCompileCacheProbe(fixtureRoot);
const result = spawnSync(process.execPath, [path.join(fixtureRoot, "openclaw.mjs")], {
cwd: fixtureRoot,
env: launcherEnv(),
encoding: "utf8",
});
expect(result.status).toBe(0);
expect(result.stdout).toBe("cache:enabled;respawn:0");
});
});