Skip to content

Commit 465d1b0

Browse files
committed
fix(plugins): prune legacy runtime deps roots
1 parent 6375251 commit 465d1b0

4 files changed

Lines changed: 49 additions & 18 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ Docs: https://docs.openclaw.ai
66

77
### Changes
88

9-
- 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.
109
- 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.
1110
- 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.
1211
- 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
1716

1817
### Fixes
1918

19+
- 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.
2020
- 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.
2121
- 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.
2222
- 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.

src/plugins/bundled-runtime-deps-roots.ts

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ import {
1313
const DEFAULT_UNKNOWN_RUNTIME_DEPS_ROOTS_TO_KEEP = 20;
1414
const DEFAULT_UNKNOWN_RUNTIME_DEPS_MIN_AGE_MS = 10 * 60_000;
1515
const PACKAGE_KEY_PATH_HASH_RE = /^openclaw-.+-([0-9a-f]{12})$/u;
16+
const LEGACY_VERSIONED_RUNTIME_DEPS_ROOT_RE =
17+
/^openclaw-\d{4}\.\d+\.\d+(?:-[0-9A-Za-z.]+)*-[A-Za-z][A-Za-z0-9_-]*$/u;
1618

1719
export type BundledRuntimeDepsInstallRootPlan = {
1820
installRoot: string;
@@ -143,6 +145,19 @@ export function pruneUnknownBundledRuntimeDepsRoots(
143145
let scanned = 0;
144146
let removed = 0;
145147
let skippedLocked = 0;
148+
const removeRoot = (root: string): void => {
149+
const lockDir = path.join(root, BUNDLED_RUNTIME_DEPS_LOCK_DIR);
150+
if (fs.existsSync(lockDir) && !removeRuntimeDepsLockIfStale(lockDir, nowMs)) {
151+
skippedLocked += 1;
152+
return;
153+
}
154+
try {
155+
fs.rmSync(root, { recursive: true, force: true });
156+
removed += 1;
157+
} catch (error) {
158+
params.warn?.(`failed to remove stale bundled runtime deps root ${root}: ${String(error)}`);
159+
}
160+
};
146161

147162
for (const baseDir of resolveBundledRuntimeDepsExternalBaseDirs(env)) {
148163
let entries: fs.Dirent[];
@@ -163,32 +178,39 @@ export function pruneUnknownBundledRuntimeDepsRoots(
163178
})
164179
.filter((entry): entry is { root: string; mtimeMs: number } => entry !== null)
165180
.toSorted((left, right) => right.mtimeMs - left.mtimeMs);
181+
const legacyVersionedRoots = entries
182+
.filter(
183+
(entry) => entry.isDirectory() && isLegacyVersionedBundledRuntimeDepsRootName(entry.name),
184+
)
185+
.map((entry) => path.join(baseDir, entry.name))
186+
.toSorted((left, right) => left.localeCompare(right));
166187
scanned += unknownRoots.length;
188+
scanned += legacyVersionedRoots.length;
167189

168190
for (const [index, entry] of unknownRoots.entries()) {
169191
const ageMs = nowMs - entry.mtimeMs;
170192
if (index < maxRootsToKeep && ageMs < minAgeMs) {
171193
continue;
172194
}
173-
const lockDir = path.join(entry.root, BUNDLED_RUNTIME_DEPS_LOCK_DIR);
174-
if (fs.existsSync(lockDir) && !removeRuntimeDepsLockIfStale(lockDir, nowMs)) {
175-
skippedLocked += 1;
176-
continue;
177-
}
178-
try {
179-
fs.rmSync(entry.root, { recursive: true, force: true });
180-
removed += 1;
181-
} catch (error) {
182-
params.warn?.(
183-
`failed to remove stale bundled runtime deps root ${entry.root}: ${String(error)}`,
184-
);
185-
}
195+
removeRoot(entry.root);
196+
}
197+
198+
for (const root of legacyVersionedRoots) {
199+
removeRoot(root);
186200
}
187201
}
188202

189203
return { scanned, removed, skippedLocked };
190204
}
191205

206+
function isLegacyVersionedBundledRuntimeDepsRootName(name: string): boolean {
207+
return (
208+
name.startsWith("openclaw-") &&
209+
readPackageKeyPathHash(name) === null &&
210+
LEGACY_VERSIONED_RUNTIME_DEPS_ROOT_RE.test(name)
211+
);
212+
}
213+
192214
export function listSiblingExternalBundledRuntimeDepsRoots(params: {
193215
installRoot: string;
194216
env?: NodeJS.ProcessEnv;

src/plugins/bundled-runtime-deps.test.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3418,7 +3418,7 @@ describe("ensureBundledPluginRuntimeDeps", () => {
34183418
},
34193419
);
34203420

3421-
it("prunes stale unknown external runtime roots while keeping newest and locked roots", () => {
3421+
it("prunes stale unknown and legacy versioned external runtime roots", () => {
34223422
const stageDir = makeTempDir();
34233423
const nowMs = Date.parse("2026-04-29T08:00:00.000Z");
34243424
const makeRoot = (name: string, ageMs: number, locked = false) => {
@@ -3440,7 +3440,9 @@ describe("ensureBundledPluginRuntimeDeps", () => {
34403440
const newest = makeRoot("openclaw-unknown-newest", 1_000);
34413441
const stale = makeRoot("openclaw-unknown-stale", 120_000);
34423442
const locked = makeRoot("openclaw-unknown-locked", 120_000, true);
3443-
const versioned = makeRoot("openclaw-2026.4.25-versioned", 120_000);
3443+
const legacyVersioned = makeRoot("openclaw-2026.4.25-discord", 1_000);
3444+
const lockedLegacyVersioned = makeRoot("openclaw-2026.4.25-telegram", 1_000, true);
3445+
const modernVersioned = makeRoot("openclaw-2026.4.25-abcdef123456", 120_000);
34443446

34453447
const result = pruneUnknownBundledRuntimeDepsRoots({
34463448
env: { OPENCLAW_PLUGIN_STAGE_DIR: stageDir },
@@ -3449,11 +3451,13 @@ describe("ensureBundledPluginRuntimeDeps", () => {
34493451
minAgeMs: 60_000,
34503452
});
34513453

3452-
expect(result).toEqual({ scanned: 3, removed: 1, skippedLocked: 1 });
3454+
expect(result).toEqual({ scanned: 5, removed: 2, skippedLocked: 2 });
34533455
expect(fs.existsSync(newest)).toBe(true);
34543456
expect(fs.existsSync(stale)).toBe(false);
34553457
expect(fs.existsSync(locked)).toBe(true);
3456-
expect(fs.existsSync(versioned)).toBe(true);
3458+
expect(fs.existsSync(legacyVersioned)).toBe(false);
3459+
expect(fs.existsSync(lockedLegacyVersioned)).toBe(true);
3460+
expect(fs.existsSync(modernVersioned)).toBe(true);
34573461
});
34583462

34593463
it("uses the plugin-local stage for source-checkout runtime deps", () => {

src/plugins/bundled-runtime-deps.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import {
2525
import {
2626
isSourceCheckoutRoot,
2727
listSiblingExternalBundledRuntimeDepsRoots,
28+
pruneUnknownBundledRuntimeDepsRoots,
2829
resolveBundledRuntimeDependencyInstallRootPlan,
2930
resolveBundledRuntimeDependencyPackageInstallRootPlan,
3031
resolveBundledRuntimeDependencyPackageRoot,
@@ -397,6 +398,10 @@ export async function repairBundledRuntimeDepsPackagePlanAsync(params: {
397398
onProgress?: (message: string) => void;
398399
warn?: (message: string) => void;
399400
}): Promise<RepairBundledRuntimeDepsPackagePlanResult> {
401+
pruneUnknownBundledRuntimeDepsRoots({
402+
env: params.env,
403+
...(params.warn ? { warn: params.warn } : {}),
404+
});
400405
const plan = createBundledRuntimeDepsPackagePlan(params);
401406
if (plan.missingSpecs.length === 0) {
402407
return { plan, repairedSpecs: [] };

0 commit comments

Comments
 (0)