diff --git a/scripts/e2e/parallels-npm-update-smoke.sh b/scripts/e2e/parallels-npm-update-smoke.sh index 34d0f9ae065..503120c2efe 100755 --- a/scripts/e2e/parallels-npm-update-smoke.sh +++ b/scripts/e2e/parallels-npm-update-smoke.sh @@ -1083,6 +1083,15 @@ gateway_log_ready() { gateway_smoke_ready() { gateway_listener_ready && gateway_log_ready } +stop_openclaw_gateway_processes() { + OPENCLAW_DISABLE_BUNDLED_PLUGINS=1 /opt/homebrew/bin/openclaw gateway stop >/dev/null 2>&1 || true + /usr/bin/pkill -9 -f openclaw-gateway || true + /usr/bin/pkill -9 -f 'openclaw gateway run' || true + /usr/bin/pkill -9 -f 'openclaw.mjs gateway' || true + for pid in \$(/usr/sbin/lsof -tiTCP:18789 -sTCP:LISTEN 2>/dev/null || true); do + /bin/kill -9 "\$pid" 2>/dev/null || true + done +} if [ -n "\$busy" ]; then printf 'update still has active npm/pnpm/openclaw processes\n%s\n' "\$busy" >&2 exit 1 @@ -1118,6 +1127,18 @@ if [ "\$gateway_ready" != "1" ]; then done fi if [ "\$gateway_ready" != "1" ]; then + stop_openclaw_gateway_processes + /opt/homebrew/bin/openclaw gateway run --bind loopback --port 18789 --force >/tmp/openclaw-parallels-npm-update-macos-recover-gateway.log 2>&1 /dev/null || true echo "gateway did not become ready after transport recovery" >&2 exit 1 fi diff --git a/src/entry.compile-cache.test.ts b/src/entry.compile-cache.test.ts index 87af57b24e2..dd1b107301c 100644 --- a/src/entry.compile-cache.test.ts +++ b/src/entry.compile-cache.test.ts @@ -5,6 +5,7 @@ import { cleanupTempDirs, makeTempDir } from "../test/helpers/temp-dir.js"; import { buildOpenClawCompileCacheRespawnPlan, isSourceCheckoutInstallRoot, + resolveOpenClawCompileCacheDirectory, resolveEntryInstallRoot, shouldEnableOpenClawCompileCache, } from "./entry.compile-cache.js"; @@ -54,6 +55,21 @@ describe("entry compile cache", () => { ).toBe(false); }); + it("scopes packaged compile cache by package install metadata", async () => { + const root = makeTempDir(tempDirs, "openclaw-compile-cache-package-key-"); + const packageJsonPath = path.join(root, "package.json"); + await fs.writeFile(packageJsonPath, '{"version":"2026.4.27-beta.1"}\n', "utf8"); + + const directory = resolveOpenClawCompileCacheDirectory({ + env: { NODE_COMPILE_CACHE: path.join(root, ".node-cache") }, + installRoot: root, + }); + + expect(directory).toContain(path.join(".node-cache", "openclaw")); + expect(directory).toContain("2026.4.27-beta.1"); + expect(path.basename(directory)).toMatch(/^\d+-\d+$/); + }); + 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 }); diff --git a/src/entry.compile-cache.ts b/src/entry.compile-cache.ts index c03399b51bc..fb757ad14ad 100644 --- a/src/entry.compile-cache.ts +++ b/src/entry.compile-cache.ts @@ -1,6 +1,7 @@ import { spawnSync } from "node:child_process"; -import { existsSync } from "node:fs"; +import { existsSync, readFileSync, statSync } from "node:fs"; import { enableCompileCache, getCompileCacheDir } from "node:module"; +import os from "node:os"; import path from "node:path"; export function resolveEntryInstallRoot(entryFile: string): string { @@ -34,6 +35,55 @@ export function shouldEnableOpenClawCompileCache(params: { return !isSourceCheckoutInstallRoot(params.installRoot); } +function sanitizeCompileCachePathSegment(value: string): string { + const normalized = value.replace(/[^A-Za-z0-9._-]+/g, "_").replace(/^_+|_+$/g, ""); + return normalized.length > 0 ? normalized : "unknown"; +} + +function readPackageVersion(packageJsonPath: string): string { + try { + const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as unknown; + if ( + parsed && + typeof parsed === "object" && + "version" in parsed && + typeof parsed.version === "string" && + parsed.version.trim().length > 0 + ) { + return parsed.version; + } + } catch { + // Fall through to an install-metadata-only cache key. + } + return "unknown"; +} + +export function resolveOpenClawCompileCacheDirectory(params: { + env?: NodeJS.ProcessEnv; + installRoot: string; +}): string { + const env = params.env ?? process.env; + const packageJsonPath = path.join(params.installRoot, "package.json"); + const version = sanitizeCompileCachePathSegment(readPackageVersion(packageJsonPath)); + let installMarker = "no-package-json"; + try { + const stat = statSync(packageJsonPath); + installMarker = `${Math.trunc(stat.mtimeMs)}-${stat.size}`; + } catch { + // Package archives should always have package.json, but keep startup best-effort. + } + const baseDirectory = + env.NODE_COMPILE_CACHE && !isNodeCompileCacheDisabled(env) + ? env.NODE_COMPILE_CACHE + : path.join(os.tmpdir(), "node-compile-cache"); + return path.join( + baseDirectory, + "openclaw", + version, + sanitizeCompileCachePathSegment(installMarker), + ); +} + export type OpenClawCompileCacheRespawnPlan = { command: string; args: string[]; @@ -107,7 +157,7 @@ export function enableOpenClawCompileCache(params: { return; } try { - enableCompileCache(); + enableCompileCache(resolveOpenClawCompileCacheDirectory(params)); } catch { // Best-effort only; never block startup. }