fix(plugins): prune legacy runtime deps roots

This commit is contained in:
Vincent Koc
2026-05-01 01:17:54 -07:00
parent 637525136e
commit 465d1b0d4b
4 changed files with 49 additions and 18 deletions

View File

@@ -6,7 +6,6 @@ Docs: https://docs.openclaw.ai
### Changes
- Package Acceptance: expand published upgrade-survivor coverage across release-history baselines and reported-issue scenarios, including Gateway `/healthz` and `/readyz` probes, so stale plugin runtime-deps upgrade regressions are caught before release. Thanks @vincentkoc.
- Voice Call/Google Meet: add Twilio Meet join phase logs around pre-connect DTMF, realtime stream setup, and initial greeting handoff for easier live-call debugging. Thanks @donkeykong91 and @PfanP.
- macOS app: move recent session context rows into a Context submenu while keeping usage and cost details root-level, so the menu bar companion stays compact with many active sessions. Thanks @guti.
- Gateway/SDK: add SDK-facing tools.invoke RPC with shared HTTP policy, typed approval/refusal results, and SDK helper support. Refs #74705. Thanks @BunsDev and @ai-hpc.
@@ -17,6 +16,7 @@ Docs: https://docs.openclaw.ai
### Fixes
- 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.
- Google Meet: interrupt Realtime provider output when local barge-in clears playback, so command-pair audio stops model speech instead of only restarting Chrome playback. Fixes #73850. (#73834) Thanks @shhtheonlyperson.
- Gateway/config: cap oversized plugin-owned schemas in the full `config.schema` response so large installed plugin sets cannot balloon Gateway RSS or crash schema clients. Thanks @vincentkoc.
- Gateway/sessions: use bounded tail reads for sessions-list transcript usage fallbacks and cap bulk title/last-message hydration, keeping large session stores responsive when rows request derived previews. Thanks @vincentkoc.

View File

@@ -13,6 +13,8 @@ import {
const DEFAULT_UNKNOWN_RUNTIME_DEPS_ROOTS_TO_KEEP = 20;
const DEFAULT_UNKNOWN_RUNTIME_DEPS_MIN_AGE_MS = 10 * 60_000;
const PACKAGE_KEY_PATH_HASH_RE = /^openclaw-.+-([0-9a-f]{12})$/u;
const LEGACY_VERSIONED_RUNTIME_DEPS_ROOT_RE =
/^openclaw-\d{4}\.\d+\.\d+(?:-[0-9A-Za-z.]+)*-[A-Za-z][A-Za-z0-9_-]*$/u;
export type BundledRuntimeDepsInstallRootPlan = {
installRoot: string;
@@ -143,6 +145,19 @@ export function pruneUnknownBundledRuntimeDepsRoots(
let scanned = 0;
let removed = 0;
let skippedLocked = 0;
const removeRoot = (root: string): void => {
const lockDir = path.join(root, BUNDLED_RUNTIME_DEPS_LOCK_DIR);
if (fs.existsSync(lockDir) && !removeRuntimeDepsLockIfStale(lockDir, nowMs)) {
skippedLocked += 1;
return;
}
try {
fs.rmSync(root, { recursive: true, force: true });
removed += 1;
} catch (error) {
params.warn?.(`failed to remove stale bundled runtime deps root ${root}: ${String(error)}`);
}
};
for (const baseDir of resolveBundledRuntimeDepsExternalBaseDirs(env)) {
let entries: fs.Dirent[];
@@ -163,32 +178,39 @@ export function pruneUnknownBundledRuntimeDepsRoots(
})
.filter((entry): entry is { root: string; mtimeMs: number } => entry !== null)
.toSorted((left, right) => right.mtimeMs - left.mtimeMs);
const legacyVersionedRoots = entries
.filter(
(entry) => entry.isDirectory() && isLegacyVersionedBundledRuntimeDepsRootName(entry.name),
)
.map((entry) => path.join(baseDir, entry.name))
.toSorted((left, right) => left.localeCompare(right));
scanned += unknownRoots.length;
scanned += legacyVersionedRoots.length;
for (const [index, entry] of unknownRoots.entries()) {
const ageMs = nowMs - entry.mtimeMs;
if (index < maxRootsToKeep && ageMs < minAgeMs) {
continue;
}
const lockDir = path.join(entry.root, BUNDLED_RUNTIME_DEPS_LOCK_DIR);
if (fs.existsSync(lockDir) && !removeRuntimeDepsLockIfStale(lockDir, nowMs)) {
skippedLocked += 1;
continue;
}
try {
fs.rmSync(entry.root, { recursive: true, force: true });
removed += 1;
} catch (error) {
params.warn?.(
`failed to remove stale bundled runtime deps root ${entry.root}: ${String(error)}`,
);
}
removeRoot(entry.root);
}
for (const root of legacyVersionedRoots) {
removeRoot(root);
}
}
return { scanned, removed, skippedLocked };
}
function isLegacyVersionedBundledRuntimeDepsRootName(name: string): boolean {
return (
name.startsWith("openclaw-") &&
readPackageKeyPathHash(name) === null &&
LEGACY_VERSIONED_RUNTIME_DEPS_ROOT_RE.test(name)
);
}
export function listSiblingExternalBundledRuntimeDepsRoots(params: {
installRoot: string;
env?: NodeJS.ProcessEnv;

View File

@@ -3418,7 +3418,7 @@ describe("ensureBundledPluginRuntimeDeps", () => {
},
);
it("prunes stale unknown external runtime roots while keeping newest and locked roots", () => {
it("prunes stale unknown and legacy versioned external runtime roots", () => {
const stageDir = makeTempDir();
const nowMs = Date.parse("2026-04-29T08:00:00.000Z");
const makeRoot = (name: string, ageMs: number, locked = false) => {
@@ -3440,7 +3440,9 @@ describe("ensureBundledPluginRuntimeDeps", () => {
const newest = makeRoot("openclaw-unknown-newest", 1_000);
const stale = makeRoot("openclaw-unknown-stale", 120_000);
const locked = makeRoot("openclaw-unknown-locked", 120_000, true);
const versioned = makeRoot("openclaw-2026.4.25-versioned", 120_000);
const legacyVersioned = makeRoot("openclaw-2026.4.25-discord", 1_000);
const lockedLegacyVersioned = makeRoot("openclaw-2026.4.25-telegram", 1_000, true);
const modernVersioned = makeRoot("openclaw-2026.4.25-abcdef123456", 120_000);
const result = pruneUnknownBundledRuntimeDepsRoots({
env: { OPENCLAW_PLUGIN_STAGE_DIR: stageDir },
@@ -3449,11 +3451,13 @@ describe("ensureBundledPluginRuntimeDeps", () => {
minAgeMs: 60_000,
});
expect(result).toEqual({ scanned: 3, removed: 1, skippedLocked: 1 });
expect(result).toEqual({ scanned: 5, removed: 2, skippedLocked: 2 });
expect(fs.existsSync(newest)).toBe(true);
expect(fs.existsSync(stale)).toBe(false);
expect(fs.existsSync(locked)).toBe(true);
expect(fs.existsSync(versioned)).toBe(true);
expect(fs.existsSync(legacyVersioned)).toBe(false);
expect(fs.existsSync(lockedLegacyVersioned)).toBe(true);
expect(fs.existsSync(modernVersioned)).toBe(true);
});
it("uses the plugin-local stage for source-checkout runtime deps", () => {

View File

@@ -25,6 +25,7 @@ import {
import {
isSourceCheckoutRoot,
listSiblingExternalBundledRuntimeDepsRoots,
pruneUnknownBundledRuntimeDepsRoots,
resolveBundledRuntimeDependencyInstallRootPlan,
resolveBundledRuntimeDependencyPackageInstallRootPlan,
resolveBundledRuntimeDependencyPackageRoot,
@@ -397,6 +398,10 @@ export async function repairBundledRuntimeDepsPackagePlanAsync(params: {
onProgress?: (message: string) => void;
warn?: (message: string) => void;
}): Promise<RepairBundledRuntimeDepsPackagePlanResult> {
pruneUnknownBundledRuntimeDepsRoots({
env: params.env,
...(params.warn ? { warn: params.warn } : {}),
});
const plan = createBundledRuntimeDepsPackagePlan(params);
if (plan.missingSpecs.length === 0) {
return { plan, repairedSpecs: [] };