From 9799e412f863b0eca3df6a33ea2d6b0b61552b4f Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 17:25:16 -0700 Subject: [PATCH] fix(plugins): clean pinned externalized load paths --- CHANGELOG.md | 1 + src/plugins/update.test.ts | 89 ++++++++++++++++++++++++++++++++++++++ src/plugins/update.ts | 19 ++++++-- 3 files changed, 106 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c5cd5ec076..42ea93c8455 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai - Gateway/performance: keep raw channel-config schema parsing from discovering bundled plugin runtime metadata, and add `pnpm gateway:watch --benchmark-no-force` for profiling startup without the default port cleanup. - Plugins/onboarding: let Manual setup install optional official plugins, including ClawHub-backed diagnostics with npm fallback, and expose the external Codex plugin as a selectable provider setup choice. Thanks @vincentkoc. - Plugins/update: treat official externalized bundled npm migrations and ClawHub-to-npm fallbacks as trusted source-linked installs, so prerelease-only official plugin packages can migrate from bundled builds without being rejected as unsafe prerelease resolutions. Thanks @vincentkoc. +- Plugins/update: clean stale bundled load paths for already-externalized pinned npm and ClawHub plugin installs, so release-channel sync does not leave removed bundled paths ahead of the installed external package. Thanks @vincentkoc. - Plugins/CLI: include package dependency install state in `openclaw plugins list --json` so scripts can spot missing plugin dependencies without runtime-loading plugins. - Discord/status: add degraded Discord transport and gateway event-loop starvation signals to `openclaw channels status`, `openclaw status --deep`, and fetch-timeout logs so intermittent socket resets do not look like a healthy running channel. (#76327) Thanks @joshavant. - Providers/OpenRouter: add opt-in response caching params that send OpenRouter's `X-OpenRouter-Cache`, `X-OpenRouter-Cache-TTL`, and cache-clear headers only on verified OpenRouter routes. Thanks @vincentkoc. diff --git a/src/plugins/update.test.ts b/src/plugins/update.test.ts index 8682ba0551a..f2e1234b0aa 100644 --- a/src/plugins/update.test.ts +++ b/src/plugins/update.test.ts @@ -2619,4 +2619,93 @@ describe("syncPluginsForUpdateChannel", () => { spec: "@openclaw/legacy-chat", }); }); + + it("removes stale bundled load paths for already-externalized pinned npm installs", async () => { + resolveBundledPluginSourcesMock.mockReturnValue(new Map()); + + const result = await syncPluginsForUpdateChannel({ + channel: "stable", + externalizedBundledPluginBridges: [ + { + bundledPluginId: "legacy-chat", + npmSpec: "@openclaw/legacy-chat", + channelIds: ["legacy-chat"], + }, + ], + config: { + channels: { + "legacy-chat": { + enabled: true, + }, + }, + plugins: { + load: { + paths: [appBundledPluginRoot("legacy-chat"), "/workspace/plugins/other"], + }, + installs: { + "legacy-chat": { + source: "npm", + spec: "@openclaw/legacy-chat@1.2.3", + resolvedSpec: "@openclaw/legacy-chat@1.2.3", + installPath: "/tmp/openclaw-plugins/legacy-chat", + }, + }, + }, + }, + }); + + expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled(); + expect(result.changed).toBe(true); + expect(result.config.plugins?.load?.paths).toEqual(["/workspace/plugins/other"]); + expect(result.config.plugins?.installs?.["legacy-chat"]).toMatchObject({ + source: "npm", + spec: "@openclaw/legacy-chat@1.2.3", + }); + }); + + it("removes stale bundled load paths for already-externalized pinned ClawHub installs", async () => { + resolveBundledPluginSourcesMock.mockReturnValue(new Map()); + + const result = await syncPluginsForUpdateChannel({ + channel: "stable", + externalizedBundledPluginBridges: [ + { + bundledPluginId: "legacy-chat", + preferredSource: "clawhub", + clawhubSpec: "clawhub:legacy-chat", + npmSpec: "@openclaw/legacy-chat", + channelIds: ["legacy-chat"], + }, + ], + config: { + channels: { + "legacy-chat": { + enabled: true, + }, + }, + plugins: { + load: { + paths: [appBundledPluginRoot("legacy-chat"), "/workspace/plugins/other"], + }, + installs: { + "legacy-chat": { + source: "clawhub", + spec: "clawhub:legacy-chat@2026.5.1", + clawhubPackage: "legacy-chat", + installPath: "/tmp/openclaw-plugins/legacy-chat", + }, + }, + }, + }, + }); + + expect(installPluginFromClawHubMock).not.toHaveBeenCalled(); + expect(installPluginFromNpmSpecMock).not.toHaveBeenCalled(); + expect(result.changed).toBe(true); + expect(result.config.plugins?.load?.paths).toEqual(["/workspace/plugins/other"]); + expect(result.config.plugins?.installs?.["legacy-chat"]).toMatchObject({ + source: "clawhub", + spec: "clawhub:legacy-chat@2026.5.1", + }); + }); }); diff --git a/src/plugins/update.ts b/src/plugins/update.ts index 68fec4c02fa..d0cf3c0ef3a 100644 --- a/src/plugins/update.ts +++ b/src/plugins/update.ts @@ -577,12 +577,25 @@ function isBridgeAlreadyInstalledFromPreferredSource(params: { record: PluginInstallRecord; }): boolean { const npmSpec = getExternalizedBundledPluginNpmSpec(params.bridge); - if (npmSpec && params.record.source === "npm" && params.record.spec === npmSpec) { - return true; + if (npmSpec && params.record.source === "npm") { + const bridgePackageName = resolveNpmSpecPackageName(npmSpec); + const recordPackageName = + resolveNpmSpecPackageName(params.record.spec) ?? + resolveNpmSpecPackageName(params.record.resolvedSpec); + if (bridgePackageName && recordPackageName === bridgePackageName) { + return true; + } } const clawhubSpec = getExternalizedBundledPluginClawHubSpec(params.bridge); + const bridgeClawHubPackage = clawhubSpec ? parseClawHubPluginSpec(clawhubSpec)?.name : undefined; + const recordClawHubPackage = + params.record.source === "clawhub" + ? (params.record.clawhubPackage ?? parseClawHubPluginSpec(params.record.spec ?? "")?.name) + : undefined; return Boolean( - clawhubSpec && params.record.source === "clawhub" && params.record.spec === clawhubSpec, + bridgeClawHubPackage && + params.record.source === "clawhub" && + recordClawHubPackage === bridgeClawHubPackage, ); }