From c6b7444d168e01aadc0d9ee3990d7322669a7992 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 09:49:53 +0100 Subject: [PATCH] fix(plugins): reset context engine slot on uninstall --- CHANGELOG.md | 1 + src/cli/plugins-cli-test-helpers.ts | 1 + src/cli/plugins-cli.ts | 8 ++++++ src/cli/plugins-cli.uninstall.test.ts | 6 +++++ src/plugins/uninstall.test.ts | 16 ++++++++++++ src/plugins/uninstall.ts | 11 ++++++++- src/plugins/update.test.ts | 35 +++++++++++++++++++++++++++ src/plugins/update.ts | 14 +++++------ 8 files changed, 84 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bbd2cbd4a52..70450d05fe2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai - Doctor: honor `OPENCLAW_SERVICE_REPAIR_POLICY=external` by reporting gateway service health while skipping service install/start/restart/bootstrap, supervisor rewrites, and legacy service cleanup for externally managed environments. Thanks @shakkernerd. - CLI/update: run package post-update doctor with `--fix` so package updates repair config migrations before restart. Thanks @shakkernerd. - CLI/update: retry failed npm global updates with `--omit=optional` and ignore the superseded first failure when the fallback succeeds. Thanks @shakkernerd. +- Plugins/uninstall: migrate and reset `plugins.slots.contextEngine` alongside memory slots when plugin ids change or selected plugins are removed. Thanks @shakkernerd. - Agents/Discord: keep raw `Agent failed before reply` runner failures out of Discord group/channel chats and show detailed runner errors in direct chats only when `/verbose` is enabled. Thanks @codex. - Package: include patched dependency files in the published npm package so downstream installs can resolve `patchedDependencies`. (#69224) Thanks @gucasbrg and @vincentkoc. - Plugins/channels: treat malformed bundled channel plugin loaders that return `undefined` as unavailable instead of crashing config and help paths. Fixes #69044. Thanks @frankhli843 and @vincentkoc. diff --git a/src/cli/plugins-cli-test-helpers.ts b/src/cli/plugins-cli-test-helpers.ts index a02fd04110b..975ccd839fd 100644 --- a/src/cli/plugins-cli-test-helpers.ts +++ b/src/cli/plugins-cli-test-helpers.ts @@ -594,6 +594,7 @@ export function resetPluginsCliTestState() { allowlist: false, loadPath: false, memorySlot: false, + contextEngineSlot: false, directory: false, }, }); diff --git a/src/cli/plugins-cli.ts b/src/cli/plugins-cli.ts index c390e4f9c4e..dbec8169d90 100644 --- a/src/cli/plugins-cli.ts +++ b/src/cli/plugins-cli.ts @@ -634,6 +634,11 @@ export function registerPluginsCli(program: Command) { if (cfg.plugins?.slots?.memory === pluginId) { preview.push(`memory slot (will reset to "${defaultSlotIdForKey("memory")}")`); } + if (cfg.plugins?.slots?.contextEngine === pluginId) { + preview.push( + `context engine slot (will reset to "${defaultSlotIdForKey("contextEngine")}")`, + ); + } const channelIds = plugin?.status === "loaded" ? plugin.channelIds : undefined; const channels = cfg.channels as Record | undefined; if (hasInstall && channels) { @@ -723,6 +728,9 @@ export function registerPluginsCli(program: Command) { if (result.actions.memorySlot) { removed.push("memory slot"); } + if (result.actions.contextEngineSlot) { + removed.push("context engine slot"); + } if (result.actions.channelConfig) { removed.push("channel config"); } diff --git a/src/cli/plugins-cli.uninstall.test.ts b/src/cli/plugins-cli.uninstall.test.ts index d98f10ad34e..91452a5d114 100644 --- a/src/cli/plugins-cli.uninstall.test.ts +++ b/src/cli/plugins-cli.uninstall.test.ts @@ -40,6 +40,9 @@ describe("plugins cli uninstall", () => { installPath: ALPHA_INSTALL_PATH, }, }, + slots: { + contextEngine: "alpha", + }, }, } as OpenClawConfig); buildPluginDiagnosticsReport.mockReturnValue({ @@ -53,6 +56,7 @@ describe("plugins cli uninstall", () => { expect(writeConfigFile).not.toHaveBeenCalled(); expect(refreshPluginRegistry).not.toHaveBeenCalled(); expect(runtimeLogs.some((line) => line.includes("Dry run, no changes made."))).toBe(true); + expect(runtimeLogs.some((line) => line.includes("context engine slot"))).toBe(true); }); it("uninstalls with --force and --keep-files without prompting", async () => { @@ -93,6 +97,7 @@ describe("plugins cli uninstall", () => { allowlist: false, loadPath: false, memorySlot: false, + contextEngineSlot: false, directory: false, }, }); @@ -162,6 +167,7 @@ describe("plugins cli uninstall", () => { allowlist: false, loadPath: false, memorySlot: false, + contextEngineSlot: false, directory: false, }, }); diff --git a/src/plugins/uninstall.test.ts b/src/plugins/uninstall.test.ts index dae137d81c6..5ad61a0f4e6 100644 --- a/src/plugins/uninstall.test.ts +++ b/src/plugins/uninstall.test.ts @@ -365,6 +365,22 @@ describe("removePluginFromConfig", () => { expect(actions.memorySlot).toBe(expectedChanged); }); + it("clears context engine slot when uninstalling active context engine plugin", () => { + const config = createPluginConfig({ + entries: { + "context-plugin": { enabled: true }, + }, + slots: { + contextEngine: "context-plugin", + }, + }); + + const { config: result, actions } = removePluginFromConfig(config, "context-plugin"); + + expect(result.plugins?.slots?.contextEngine).toBe("legacy"); + expect(actions.contextEngineSlot).toBe(true); + }); + it("removes plugins object when uninstall leaves only empty slots", () => { const config = createSinglePluginWithEmptySlotsConfig(); diff --git a/src/plugins/uninstall.ts b/src/plugins/uninstall.ts index 6badad6e019..b1f054941b6 100644 --- a/src/plugins/uninstall.ts +++ b/src/plugins/uninstall.ts @@ -13,6 +13,7 @@ export type UninstallActions = { allowlist: boolean; loadPath: boolean; memorySlot: boolean; + contextEngineSlot: boolean; channelConfig: boolean; directory: boolean; }; @@ -155,6 +156,7 @@ export function removePluginFromConfig( allowlist: false, loadPath: false, memorySlot: false, + contextEngineSlot: false, channelConfig: false, }; @@ -204,7 +206,7 @@ export function removePluginFromConfig( } } - // Reset memory slot if this plugin was selected + // Reset slots if this plugin was selected. let slots = pluginsConfig.slots; if (slots?.memory === pluginId) { slots = { @@ -213,6 +215,13 @@ export function removePluginFromConfig( }; actions.memorySlot = true; } + if (slots?.contextEngine === pluginId) { + slots = { + ...slots, + contextEngine: defaultSlotIdForKey("contextEngine"), + }; + actions.contextEngineSlot = true; + } if (slots && Object.keys(slots).length === 0) { slots = undefined; } diff --git a/src/plugins/update.test.ts b/src/plugins/update.test.ts index 01be9f10307..69c9995b448 100644 --- a/src/plugins/update.test.ts +++ b/src/plugins/update.test.ts @@ -876,6 +876,41 @@ describe("updateNpmInstalledPlugins", () => { expect(result.config.plugins?.installs?.["voice-call"]).toBeUndefined(); }); + it("migrates context engine slot when a plugin id changes during update", async () => { + installPluginFromNpmSpecMock.mockResolvedValue({ + ok: true, + pluginId: "@openclaw/context-engine", + targetDir: "/tmp/openclaw-context-engine", + version: "0.0.2", + extensions: ["index.ts"], + }); + + const result = await updateNpmInstalledPlugins({ + config: { + plugins: { + slots: { contextEngine: "context-engine" }, + installs: { + "context-engine": { + source: "npm", + spec: "@openclaw/context-engine", + installPath: "/tmp/context-engine", + }, + }, + }, + } as OpenClawConfig, + pluginIds: ["context-engine"], + }); + + expect(result.config.plugins?.slots?.contextEngine).toBe("@openclaw/context-engine"); + expect(result.config.plugins?.installs?.["@openclaw/context-engine"]).toMatchObject({ + source: "npm", + spec: "@openclaw/context-engine", + installPath: "/tmp/openclaw-context-engine", + version: "0.0.2", + }); + expect(result.config.plugins?.installs?.["context-engine"]).toBeUndefined(); + }); + it("checks marketplace installs during dry-run updates", async () => { installPluginFromMarketplaceMock.mockResolvedValue({ ok: true, diff --git a/src/plugins/update.ts b/src/plugins/update.ts index 32e74c051ef..226844485a7 100644 --- a/src/plugins/update.ts +++ b/src/plugins/update.ts @@ -417,13 +417,13 @@ function migratePluginConfigId(cfg: OpenClawConfig, fromId: string, toId: string delete nextEntries[fromId]; } - const nextSlots = - slots?.memory === fromId - ? { - ...slots, - memory: toId, - } - : slots; + const nextSlots = slots + ? { + ...slots, + ...(slots.memory === fromId ? { memory: toId } : {}), + ...(slots.contextEngine === fromId ? { contextEngine: toId } : {}), + } + : undefined; return { ...cfg,