fix(plugins): reset context engine slot on uninstall

This commit is contained in:
Peter Steinberger
2026-04-26 09:49:53 +01:00
parent 42487d0dac
commit c6b7444d16
8 changed files with 84 additions and 8 deletions

View File

@@ -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.

View File

@@ -594,6 +594,7 @@ export function resetPluginsCliTestState() {
allowlist: false,
loadPath: false,
memorySlot: false,
contextEngineSlot: false,
directory: false,
},
});

View File

@@ -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<string, unknown> | 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");
}

View File

@@ -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,
},
});

View File

@@ -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();

View File

@@ -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;
}

View File

@@ -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,

View File

@@ -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,