From ac00d7882a2c6d986fadd72ee1c50940c2b4dc76 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 19:48:30 -0700 Subject: [PATCH] fix(plugins): clean resolved npm load paths --- CHANGELOG.md | 1 + src/plugins/update.test.ts | 42 ++++++++++++++++++++++++++++++++++++++ src/plugins/update.ts | 1 + 3 files changed, 44 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b11221d221..af6432bff4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -75,6 +75,7 @@ Docs: https://docs.openclaw.ai - OpenAI Codex: let SSRF-guarded provider requests inherit OpenClaw's undici IPv4/IPv6 fallback policy, so ChatGPT-backed Codex runs recover on IPv4-working hosts when DNS still returns unreachable IPv6 addresses. Fixes #76857. Thanks @jplavoiemtl and @SymbolStar. - Gateway/systemd: preserve operator-added secrets in the Gateway env file across re-stage while clearing OpenClaw-managed keys (such as `OPENCLAW_GATEWAY_TOKEN`) so a fresh staging value is never shadowed by a stale env-file copy; operator secrets are also retained when the state-dir `.env` is empty. Fixes #76860. Thanks @hclsys. - Plugin updates: do not short-circuit trusted official npm updates as unchanged when the default/latest spec still resolves to an already-installed prerelease that the installer should replace with a stable fallback. Thanks @vincentkoc. +- Plugin updates: clean stale bundled load paths for already-externalized npm installs whose legacy install record only preserved the resolved package name. Thanks @vincentkoc. - Plugin tools: keep auth-unavailable optional tools hidden even when another default tool from the same plugin is available and `tools.alsoAllow` names the optional tool. Thanks @vincentkoc. - Realtime transcription: report socket closes before provider readiness as closed-before-ready failures instead of mislabeling them as connection timeouts for OpenAI, xAI, and Deepgram streaming transcription. Thanks @vincentkoc. - OpenAI/Google Meet: fail realtime voice connection attempts when the socket closes before `session.updated`, avoiding stuck Meet joins waiting on a bridge that never became ready. Thanks @vincentkoc. diff --git a/src/plugins/update.test.ts b/src/plugins/update.test.ts index f2e1234b0aa..46ff7e97354 100644 --- a/src/plugins/update.test.ts +++ b/src/plugins/update.test.ts @@ -2620,6 +2620,48 @@ describe("syncPluginsForUpdateChannel", () => { }); }); + it("removes stale bundled load paths for already-externalized resolved-name-only 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", + resolvedName: "@openclaw/legacy-chat", + 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", + resolvedName: "@openclaw/legacy-chat", + }); + }); + it("removes stale bundled load paths for already-externalized pinned npm installs", async () => { resolveBundledPluginSourcesMock.mockReturnValue(new Map()); diff --git a/src/plugins/update.ts b/src/plugins/update.ts index d0cf3c0ef3a..787de6d10be 100644 --- a/src/plugins/update.ts +++ b/src/plugins/update.ts @@ -580,6 +580,7 @@ function isBridgeAlreadyInstalledFromPreferredSource(params: { if (npmSpec && params.record.source === "npm") { const bridgePackageName = resolveNpmSpecPackageName(npmSpec); const recordPackageName = + params.record.resolvedName ?? resolveNpmSpecPackageName(params.record.spec) ?? resolveNpmSpecPackageName(params.record.resolvedSpec); if (bridgePackageName && recordPackageName === bridgePackageName) {