diff --git a/CHANGELOG.md b/CHANGELOG.md index 35cdc4b6b03..3ecfdcf2738 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai ### Fixes - Discord/voice: run voice-channel turns under a voice-output policy that hides the agent `tts` tool and asks for spoken reply text, so `/vc join` sessions synthesize and play agent replies instead of ending with `NO_REPLY`. Fixes #61536. Thanks @aounakram. +- Plugins/runtime-deps: prune inactive same-package versioned runtime-deps roots after bundled dependency repair, so upgrades do not leave old `openclaw--` package caches behind after doctor runs. Thanks @vincentkoc. - Plugins/runtime-deps: prune legacy version-scoped plugin runtime-deps roots during bundled dependency repair and cover the path in Package Acceptance's upgrade-survivor matrix, so upgrades from 2026.4.x no longer leave stale per-plugin runtime trees after doctor runs. Thanks @vincentkoc. - Plugins/runtime-deps: keep Gateway startup plugin imports and runtime plugin fallback loads verify-only after startup/config repair planning, so packaged installs no longer spawn package-manager repair from hot paths after readiness. Refs #75283 and #75069. Thanks @brokemac79 and @xiaohuaxi. - Voice Call/realtime: add default-off fast memory/session context for `openclaw_agent_consult`, giving live calls a bounded answer-or-miss path before the full agent consult. Fixes #71849. Thanks @amzzzzzzz. diff --git a/src/plugins/bundled-runtime-deps-roots.ts b/src/plugins/bundled-runtime-deps-roots.ts index d37a7fb7f77..e71ddf820aa 100644 --- a/src/plugins/bundled-runtime-deps-roots.ts +++ b/src/plugins/bundled-runtime-deps-roots.ts @@ -269,6 +269,57 @@ export function listSiblingExternalBundledRuntimeDepsRoots(params: { .map((entry) => entry.root); } +export function pruneSiblingExternalBundledRuntimeDepsRoots(params: { + installRoot: string; + nowMs?: number; + warn?: (message: string) => void; +}): { scanned: number; removed: number; skippedLocked: number } { + const installRoot = path.resolve(params.installRoot); + const installRootHash = readPackageKeyPathHash(path.basename(installRoot)); + if (!installRootHash) { + return { scanned: 0, removed: 0, skippedLocked: 0 }; + } + const parentDir = path.dirname(installRoot); + const nowMs = params.nowMs ?? Date.now(); + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(parentDir, { withFileTypes: true }); + } catch { + return { scanned: 0, removed: 0, skippedLocked: 0 }; + } + + let scanned = 0; + let removed = 0; + let skippedLocked = 0; + for (const entry of entries) { + if ( + !entry.isDirectory() || + !entry.name.startsWith("openclaw-") || + readPackageKeyPathHash(entry.name) !== installRootHash + ) { + continue; + } + const root = path.join(parentDir, entry.name); + if (path.resolve(root) === installRoot) { + continue; + } + scanned += 1; + const lockDir = path.join(root, BUNDLED_RUNTIME_DEPS_LOCK_DIR); + if (fs.existsSync(lockDir) && !removeRuntimeDepsLockIfStale(lockDir, nowMs)) { + skippedLocked += 1; + continue; + } + try { + fs.rmSync(root, { recursive: true, force: true }); + removed += 1; + } catch (error) { + params.warn?.(`failed to remove sibling bundled runtime deps root ${root}: ${String(error)}`); + } + } + + return { scanned, removed, skippedLocked }; +} + function readPackageKeyPathHash(packageKey: string): string | null { return PACKAGE_KEY_PATH_HASH_RE.exec(packageKey)?.[1] ?? null; } diff --git a/src/plugins/bundled-runtime-deps.test.ts b/src/plugins/bundled-runtime-deps.test.ts index e405ca1c440..fc54d4cf520 100644 --- a/src/plugins/bundled-runtime-deps.test.ts +++ b/src/plugins/bundled-runtime-deps.test.ts @@ -1860,6 +1860,7 @@ describe("createBundledRuntimeDepsPackagePlan config policy", () => { ]); expect(fs.lstatSync(path.join(installRoot, "node_modules")).isSymbolicLink()).toBe(true); expect(isRuntimeDepsPlanMaterialized(installRoot, ["alpha-runtime@1.0.0"])).toBe(true); + expect(fs.existsSync(previousRoot)).toBe(true); expect(JSON.parse(fs.readFileSync(path.join(installRoot, "package.json"), "utf8"))).toEqual({ name: "openclaw-runtime-deps-install", private: true, @@ -1904,6 +1905,7 @@ describe("createBundledRuntimeDepsPackagePlan config policy", () => { }, ]); expect(fs.lstatSync(path.join(installRoot, "node_modules")).isSymbolicLink()).toBe(false); + expect(fs.existsSync(previousRoot)).toBe(true); }); it("does not create a reuse symlink when an earlier configured layer already satisfies the plan", async () => { @@ -1974,6 +1976,7 @@ describe("createBundledRuntimeDepsPackagePlan config policy", () => { }, ]); expect(fs.lstatSync(path.join(installRoot, "node_modules")).isSymbolicLink()).toBe(false); + expect(fs.existsSync(previousRoot)).toBe(false); }); it("does not reuse a compatible external runtime deps root from a different package key", async () => { diff --git a/src/plugins/bundled-runtime-deps.ts b/src/plugins/bundled-runtime-deps.ts index 656231383a3..a9da526fdcf 100644 --- a/src/plugins/bundled-runtime-deps.ts +++ b/src/plugins/bundled-runtime-deps.ts @@ -25,6 +25,7 @@ import { import { isSourceCheckoutRoot, listSiblingExternalBundledRuntimeDepsRoots, + pruneSiblingExternalBundledRuntimeDepsRoots, pruneUnknownBundledRuntimeDepsRoots, resolveBundledRuntimeDependencyInstallRootPlan, resolveBundledRuntimeDependencyPackageInstallRootPlan, @@ -404,6 +405,10 @@ export async function repairBundledRuntimeDepsPackagePlanAsync(params: { }); const plan = createBundledRuntimeDepsPackagePlan(params); if (plan.missingSpecs.length === 0) { + pruneSiblingExternalBundledRuntimeDepsRoots({ + installRoot: plan.installRootPlan.installRoot, + ...(params.warn ? { warn: params.warn } : {}), + }); return { plan, repairedSpecs: [] }; } const reuseResult = withBundledRuntimeDepsInstallRootLock(plan.installRootPlan.installRoot, () => @@ -416,6 +421,12 @@ export async function repairBundledRuntimeDepsPackagePlanAsync(params: { ); if (reuseResult) { const refreshedPlan = createBundledRuntimeDepsPackagePlan(params); + if (reuseResult.status === "materialized") { + pruneSiblingExternalBundledRuntimeDepsRoots({ + installRoot: refreshedPlan.installRootPlan.installRoot, + ...(params.warn ? { warn: params.warn } : {}), + }); + } return { plan: refreshedPlan, repairedSpecs: [], @@ -442,6 +453,10 @@ export async function repairBundledRuntimeDepsPackagePlanAsync(params: { ...(params.onProgress ? { onProgress: params.onProgress } : {}), ...(params.warn ? { warn: params.warn } : {}), }); + pruneSiblingExternalBundledRuntimeDepsRoots({ + installRoot: plan.installRootPlan.installRoot, + ...(params.warn ? { warn: params.warn } : {}), + }); return { plan, repairedSpecs: result.installSpecs }; }