Skip to content

Commit 425fb15

Browse files
committed
fix(doctor): prune stale bundled plugin paths
1 parent 1a60c19 commit 425fb15

4 files changed

Lines changed: 139 additions & 6 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,7 @@ Docs: https://docs.openclaw.ai
233233
- CLI/update: pre-pack GitHub/git package update targets before the staged npm install, restoring `openclaw update --tag main` for one-off package updates. (#81296) Thanks @fuller-stack-dev.
234234
- Gateway: mirror successful same-source message-tool sends into session transcripts so delivered replies stay in later history/context. (#84837) Thanks @iFiras-Max1.
235235
- Media generation: keep image, music, and video completion delivery from duplicating or losing task ownership when generated media finishes through active session replies. (#84006) Thanks @fuller-stack-dev.
236+
- CLI/doctor: remove stale bundled plugin load paths from old versioned OpenClaw package roots after pnpm/npm upgrades. Fixes #58626. Thanks @solink7.
236237
- Infra/json: retry transient `File changed during read` races while loading JSON state so config and state reads recover instead of failing the turn. (#84285)
237238
- Plugins/providers: fail closed for workspace provider plugins during setup-mode discovery unless explicitly trusted, preventing untrusted workspace plugin code from running during provider setup. (#81069) Thanks @mmaps.
238239
- Providers/Ollama: resolve configured Ollama Cloud `OLLAMA_API_KEY` markers to the real discovery key so cloud provider entries keep authenticated model catalog access. (#85037)

src/commands/doctor/shared/bundled-plugin-load-paths.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,61 @@ describe("bundled plugin load path repair", () => {
103103
expect(result.config.plugins?.load?.paths).toStrictEqual([]);
104104
});
105105

106+
it("removes stale bundled paths from old versioned OpenClaw package roots", () => {
107+
const currentPackageRoot = path.resolve("node_modules", "openclaw");
108+
const stalePackageRoot = path.resolve(
109+
"pnpm-global",
110+
".pnpm",
111+
"openclaw@2026.3.28_@napi-rs+canvas@0.1.97",
112+
"node_modules",
113+
"openclaw",
114+
);
115+
const currentBundledPath = bundledDistPluginRootAt(currentPackageRoot, "feishu");
116+
const staleBundledPath = bundledDistPluginRootAt(stalePackageRoot, "feishu");
117+
mockBundledSource("feishu", currentBundledPath);
118+
119+
const result = maybeRepairBundledPluginLoadPaths(
120+
createPluginLoadPathConfig([staleBundledPath, "/custom/path"]),
121+
);
122+
123+
expect(result.changes).toEqual([
124+
`- plugins.load.paths: removed bundled feishu path alias ${staleBundledPath}`,
125+
]);
126+
expect(result.config.plugins?.load?.paths).toStrictEqual(["/custom/path"]);
127+
});
128+
129+
it("removes stale legacy bundled paths from old versioned OpenClaw package roots", () => {
130+
const currentPackageRoot = path.resolve("node_modules", "openclaw");
131+
const stalePackageRoot = path.resolve(
132+
"pnpm-global",
133+
".pnpm",
134+
"openclaw@2026.3.28_@napi-rs+canvas@0.1.97",
135+
"node_modules",
136+
"openclaw",
137+
);
138+
const currentBundledPath = bundledDistPluginRootAt(currentPackageRoot, "feishu");
139+
const staleLegacyPath = bundledPluginRootAt(stalePackageRoot, "feishu");
140+
mockBundledSource("feishu", currentBundledPath);
141+
142+
const result = maybeRepairBundledPluginLoadPaths(createPluginLoadPathConfig([staleLegacyPath]));
143+
144+
expect(result.changes).toEqual([
145+
`- plugins.load.paths: removed bundled feishu path alias ${staleLegacyPath}`,
146+
]);
147+
expect(result.config.plugins?.load?.paths).toStrictEqual([]);
148+
});
149+
150+
it("does not remove arbitrary missing paths that happen to use the bundled dist layout", () => {
151+
const currentPackageRoot = path.resolve("node_modules", "openclaw");
152+
const customPath = path.resolve("elsewhere", "dist", "extensions", "feishu");
153+
mockBundledSource("feishu", bundledDistPluginRootAt(currentPackageRoot, "feishu"));
154+
155+
const result = maybeRepairBundledPluginLoadPaths(createPluginLoadPathConfig([customPath]));
156+
157+
expect(result.changes).toEqual([]);
158+
expect(result.config).toEqual(createPluginLoadPathConfig([customPath]));
159+
});
160+
106161
it("derives legacy paths from the bundled directory name instead of plugin id", () => {
107162
const packageRoot = path.resolve("app-node-modules", "openclaw");
108163
const legacyPath = bundledPluginRootAt(packageRoot, "kimi-coding");

src/commands/doctor/shared/bundled-plugin-load-paths.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
1+
import path from "node:path";
12
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../../../agents/agent-scope.js";
23
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
34
import {
45
buildBundledPluginLoadPathAliases,
56
normalizeBundledLookupPath,
7+
parseLegacyBundledPluginPath,
8+
parsePackagedBundledPluginPath,
69
} from "../../../plugins/bundled-load-path-aliases.js";
710
import { resolveBundledPluginSources } from "../../../plugins/bundled-sources.js";
811
import { sanitizeForLog } from "../../../terminal/ansi.js";
@@ -20,6 +23,13 @@ function resolveBundledWorkspaceDir(cfg: OpenClawConfig): string | undefined {
2023
return resolveAgentWorkspaceDir(cfg, resolveDefaultAgentId(cfg)) ?? undefined;
2124
}
2225

26+
function isOpenClawNodeModulesPackageRoot(packageRoot: string): boolean {
27+
const normalized = normalizeBundledLookupPath(packageRoot);
28+
const packageDir = path.basename(normalized);
29+
const parentDir = path.basename(path.dirname(normalized));
30+
return packageDir === "openclaw" && parentDir === "node_modules";
31+
}
32+
2333
export function scanBundledPluginLoadPathMigrations(
2434
cfg: OpenClawConfig,
2535
env: NodeJS.ProcessEnv = process.env,
@@ -40,13 +50,21 @@ export function scanBundledPluginLoadPathMigrations(
4050
}
4151

4252
const bundledPathMap = new Map<string, { pluginId: string; toPath: string }>();
53+
const packagedBundledLeafMap = new Map<string, { pluginId: string; toPath: string }>();
4354
for (const source of bundled.values()) {
4455
for (const alias of buildBundledPluginLoadPathAliases(source.localPath)) {
4556
bundledPathMap.set(normalizeBundledLookupPath(alias.path), {
4657
pluginId: source.pluginId,
4758
toPath: source.localPath,
4859
});
4960
}
61+
const packaged = parsePackagedBundledPluginPath(source.localPath);
62+
if (packaged) {
63+
packagedBundledLeafMap.set(normalizeBundledLookupPath(packaged.bundledLeaf), {
64+
pluginId: source.pluginId,
65+
toPath: source.localPath,
66+
});
67+
}
5068
}
5169

5270
const hits: BundledPluginLoadPathHit[] = [];
@@ -57,6 +75,23 @@ export function scanBundledPluginLoadPathMigrations(
5775
const normalized = normalizeBundledLookupPath(resolveUserPath(rawPath, env));
5876
const match = bundledPathMap.get(normalized);
5977
if (!match) {
78+
const oldPackaged = parsePackagedBundledPluginPath(normalized);
79+
const oldLegacy = oldPackaged ? null : parseLegacyBundledPluginPath(normalized);
80+
const oldPackageRoot = oldPackaged?.packageRoot ?? oldLegacy?.packageRoot;
81+
const oldBundledLeaf = oldPackaged?.bundledLeaf ?? oldLegacy?.bundledLeaf;
82+
const oldPackageMatch =
83+
oldPackageRoot && oldBundledLeaf && isOpenClawNodeModulesPackageRoot(oldPackageRoot)
84+
? packagedBundledLeafMap.get(normalizeBundledLookupPath(oldBundledLeaf))
85+
: undefined;
86+
if (!oldPackageMatch) {
87+
continue;
88+
}
89+
hits.push({
90+
pluginId: oldPackageMatch.pluginId,
91+
fromPath: rawPath,
92+
toPath: oldPackageMatch.toPath,
93+
pathLabel: "plugins.load.paths",
94+
});
6095
continue;
6196
}
6297
hits.push({

src/plugins/bundled-load-path-aliases.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,18 @@ export type BundledPluginLoadPathAlias = {
88
path: string;
99
};
1010

11+
export type PackagedBundledPluginPath = {
12+
packageRoot: string;
13+
bundledRoot: string;
14+
bundledLeaf: string;
15+
};
16+
17+
export type LegacyBundledPluginPath = {
18+
packageRoot: string;
19+
legacyRoot: string;
20+
bundledLeaf: string;
21+
};
22+
1123
const PACKAGED_BUNDLED_ROOTS = [
1224
path.join("dist", "extensions"),
1325
path.join("dist-runtime", "extensions"),
@@ -46,24 +58,54 @@ function findPackagedBundledRoot(localPath: string): {
4658
return null;
4759
}
4860

49-
export function buildLegacyBundledPath(localPath: string): string | null {
61+
export function parsePackagedBundledPluginPath(
62+
localPath: string,
63+
): PackagedBundledPluginPath | null {
5064
const packaged = findPackagedBundledRoot(localPath);
5165
if (!packaged) {
5266
return null;
5367
}
5468
const normalized = normalizeBundledLookupPath(localPath);
55-
const bundledLeaf =
56-
normalized === packaged.bundledRoot
57-
? ""
58-
: normalized.slice(packaged.bundledRoot.length + path.sep.length);
59-
return bundledLeaf ? path.join(packaged.packageRoot, "extensions", bundledLeaf) : null;
69+
if (normalized === packaged.bundledRoot) {
70+
return null;
71+
}
72+
return {
73+
...packaged,
74+
bundledLeaf: normalized.slice(packaged.bundledRoot.length + path.sep.length),
75+
};
76+
}
77+
78+
export function buildLegacyBundledPath(localPath: string): string | null {
79+
const packaged = parsePackagedBundledPluginPath(localPath);
80+
if (!packaged) {
81+
return null;
82+
}
83+
return path.join(packaged.packageRoot, "extensions", packaged.bundledLeaf);
6084
}
6185

6286
export function buildLegacyBundledRootPath(localPath: string): string | null {
6387
const packaged = findPackagedBundledRoot(localPath);
6488
return packaged ? path.join(packaged.packageRoot, "extensions") : null;
6589
}
6690

91+
export function parseLegacyBundledPluginPath(localPath: string): LegacyBundledPluginPath | null {
92+
const normalized = normalizeBundledLookupPath(localPath);
93+
const marker = `${path.sep}extensions`;
94+
const markerIndex = normalized.lastIndexOf(marker);
95+
if (markerIndex === -1) {
96+
return null;
97+
}
98+
const markerEnd = markerIndex + marker.length;
99+
if (normalized.length === markerEnd || normalized[markerEnd] !== path.sep) {
100+
return null;
101+
}
102+
return {
103+
packageRoot: normalized.slice(0, markerIndex),
104+
legacyRoot: normalized.slice(0, markerEnd),
105+
bundledLeaf: normalized.slice(markerEnd + path.sep.length),
106+
};
107+
}
108+
67109
export function buildBundledPluginLoadPathAliases(localPath: string): BundledPluginLoadPathAlias[] {
68110
const legacyPath = buildLegacyBundledPath(localPath);
69111
if (!legacyPath) {

0 commit comments

Comments
 (0)