mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 16:30:57 +00:00
fix(plugins): canonicalize packageRoot before hashing runtime-deps stage key (#75048)
Merged via squash.
Prepared head SHA: 324859fc69
Co-authored-by: openperf <80630709+openperf@users.noreply.github.com>
Co-authored-by: openperf <80630709+openperf@users.noreply.github.com>
Reviewed-by: @openperf
This commit is contained in:
@@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/compaction: add an opt-in `agents.defaults.compaction.midTurnPrecheck` mid-turn precheck that detects tool-loop context pressure and triggers compaction before the next tool call instead of waiting for end-of-turn. (#73499) Thanks @marchpure and @haoxingjun.
|
||||
- Gateway/approvals: let loopback token/password-backed native approval clients resolve exec approvals without attaching stale paired Gateway identities, while remote and unauthenticated approval clients keep normal device identity behavior. (#74472)
|
||||
- Gateway/config: include rejected validation paths in foreground and service last-known-good recovery logs plus main-agent notices, so unsupported direct edits explain which key caused restore instead of looking like silent reversion. Fixes #75060. Thanks @amknight.
|
||||
- Plugins/runtime-deps: hash the OS-canonical `packageRoot` via `fs.realpathSync.native` (with `path.resolve` fallback) when computing the bundled runtime-deps stage key, so loader and channel `bundled-root` callers no longer derive divergent stage directories under `~/.openclaw/plugin-runtime-deps/openclaw-<version>-<hash>/` and bundled channels stop failing with `ENOENT` on shared dist chunks under Windows npm symlinks, junctions, or PM2 multi-instance worker layouts. Fixes #74963. (#75048) Thanks @openperf and @vincentkoc.
|
||||
|
||||
## 2026.4.29
|
||||
|
||||
|
||||
@@ -53,7 +53,9 @@ function isPackagedBundledPluginRoot(pluginRoot: string): boolean {
|
||||
}
|
||||
|
||||
function createPathHash(value: string): string {
|
||||
return createHash("sha256").update(path.resolve(value)).digest("hex").slice(0, 12);
|
||||
// Hash the OS-canonical (realpath) form so symlinked / junctioned
|
||||
// packageRoots converge on a single staging directory across call sites.
|
||||
return createHash("sha256").update(realpathOrResolve(value)).digest("hex").slice(0, 12);
|
||||
}
|
||||
|
||||
function sanitizePathSegment(value: string): string {
|
||||
|
||||
@@ -2645,6 +2645,41 @@ describe("ensureBundledPluginRuntimeDeps", () => {
|
||||
expect(path.basename(resolved).startsWith("openclaw-unknown-")).toBe(false);
|
||||
});
|
||||
|
||||
const itSupportsPackageRootSymlinks = process.platform === "win32" ? it.skip : it;
|
||||
itSupportsPackageRootSymlinks(
|
||||
"stages bundled runtime deps to the same root for symlinked packageRoot views (issue #74963)",
|
||||
() => {
|
||||
const realParent = makeTempDir();
|
||||
const stageDir = makeTempDir();
|
||||
const realPackageRoot = path.join(realParent, "openclaw-real");
|
||||
fs.mkdirSync(realPackageRoot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(realPackageRoot, "package.json"),
|
||||
JSON.stringify({ name: "openclaw", version: "2026.4.27" }),
|
||||
);
|
||||
const realPluginRoot = path.join(realPackageRoot, "dist", "extensions", "discord");
|
||||
fs.mkdirSync(realPluginRoot, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(realPluginRoot, "package.json"),
|
||||
JSON.stringify({ dependencies: {} }),
|
||||
);
|
||||
const linkedPackageRoot = path.join(realParent, "openclaw-linked");
|
||||
fs.symlinkSync(realPackageRoot, linkedPackageRoot, "dir");
|
||||
const linkedPluginRoot = path.join(linkedPackageRoot, "dist", "extensions", "discord");
|
||||
const env = { OPENCLAW_PLUGIN_STAGE_DIR: stageDir };
|
||||
|
||||
const installRootViaReal = resolveBundledRuntimeDependencyInstallRoot(realPluginRoot, {
|
||||
env,
|
||||
});
|
||||
const installRootViaLink = resolveBundledRuntimeDependencyInstallRoot(linkedPluginRoot, {
|
||||
env,
|
||||
});
|
||||
|
||||
expect(installRootViaLink).toBe(installRootViaReal);
|
||||
expect(path.basename(installRootViaReal)).toMatch(/^openclaw-2026\.4\.27-[0-9a-f]{12}$/);
|
||||
},
|
||||
);
|
||||
|
||||
it("prunes stale unknown external runtime roots while keeping newest and locked roots", () => {
|
||||
const stageDir = makeTempDir();
|
||||
const nowMs = Date.parse("2026-04-29T08:00:00.000Z");
|
||||
|
||||
@@ -918,13 +918,25 @@ describe("bundledRuntimeDependencySentinelCandidates", () => {
|
||||
"playwright-core",
|
||||
{ HOME: homeRoot } as NodeJS.ProcessEnv,
|
||||
);
|
||||
const realRootCandidates = bundledRuntimeDependencySentinelCandidates(
|
||||
packageRoot,
|
||||
"browser",
|
||||
"playwright-core",
|
||||
{ HOME: homeRoot } as NodeJS.ProcessEnv,
|
||||
);
|
||||
const externalCandidates = candidates.filter(
|
||||
(candidate) =>
|
||||
candidate.startsWith(join(homeRoot, ".openclaw", "plugin-runtime-deps")) &&
|
||||
candidate.endsWith(join("node_modules", "playwright-core", "package.json")),
|
||||
);
|
||||
const realRootExternalCandidates = realRootCandidates.filter(
|
||||
(candidate) =>
|
||||
candidate.startsWith(join(homeRoot, ".openclaw", "plugin-runtime-deps")) &&
|
||||
candidate.endsWith(join("node_modules", "playwright-core", "package.json")),
|
||||
);
|
||||
|
||||
expect(externalCandidates.length).toBeGreaterThanOrEqual(2);
|
||||
expect(externalCandidates).toEqual(realRootExternalCandidates);
|
||||
expect(externalCandidates).toHaveLength(1);
|
||||
} finally {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user