diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c493cdf831..3b1e6addd31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -147,6 +147,7 @@ Docs: https://docs.openclaw.ai - Sandbox/security: auto-derive CDP source-range from Docker network gateway and refuse to start the socat relay without one, so peer containers cannot reach CDP unauthenticated. (#61404) Thanks @dims. - Daemon/launchd: keep `openclaw gateway stop` persistent without uninstalling the macOS LaunchAgent, re-enable it on explicit restart or repair, and harden launchd label handling. (#64447) Thanks @ngutman. - Agents/Slack: preserve threaded announce delivery when `sessions.list` rows lack stored thread metadata by falling back to the thread id encoded in the session key. (#63143) Thanks @mariosousa-finn. +- Plugins/context engines: preserve `plugins.slots.contextEngine` through normalization and keep explicitly selected workspace context-engine plugins enabled, so loader diagnostics and plugin activation stop dropping that slot selection. (#64192) Thanks @hclsys. ## 2026.4.9 diff --git a/src/plugins/config-normalization-shared.ts b/src/plugins/config-normalization-shared.ts index 184f7eb2daf..4de6fc2263f 100644 --- a/src/plugins/config-normalization-shared.ts +++ b/src/plugins/config-normalization-shared.ts @@ -13,6 +13,7 @@ export type NormalizedPluginsConfig = { loadPaths: string[]; slots: { memory?: string | null; + contextEngine?: string | null; }; entries: Record< string, @@ -142,6 +143,7 @@ export function normalizePluginsConfigWithResolver( loadPaths: normalizeList(config?.load?.paths, identityNormalizePluginId), slots: { memory: memorySlot === undefined ? defaultSlotIdForKey("memory") : memorySlot, + contextEngine: normalizeSlotValue(config?.slots?.contextEngine), }, entries: normalizePluginEntries(config?.entries, normalizePluginId), }; diff --git a/src/plugins/config-policy.ts b/src/plugins/config-policy.ts index 68554d8322a..012a4eb4495 100644 --- a/src/plugins/config-policy.ts +++ b/src/plugins/config-policy.ts @@ -51,6 +51,9 @@ function resolveExplicitPluginSelection(params: { if (params.config.slots.memory === params.id) { return { explicitlyEnabled: true, reason: "selected memory slot" }; } + if (params.config.slots.contextEngine === params.id) { + return { explicitlyEnabled: true, reason: "selected context engine slot" }; + } if (params.origin !== "bundled" && params.config.allow.includes(params.id)) { return { explicitlyEnabled: true, reason: "selected in allowlist" }; } @@ -103,7 +106,12 @@ export function resolvePluginActivationState(params: { }; } const explicitlyAllowed = params.config.allow.includes(params.id); - if (params.origin === "workspace" && !explicitlyAllowed && entry?.enabled !== true) { + if ( + params.origin === "workspace" && + !explicitlyAllowed && + entry?.enabled !== true && + explicitSelection.reason !== "selected context engine slot" + ) { return { enabled: false, activated: false, @@ -121,6 +129,15 @@ export function resolvePluginActivationState(params: { reason: "selected memory slot", }; } + if (params.config.slots.contextEngine === params.id) { + return { + enabled: true, + activated: true, + explicitlyEnabled: true, + source: "explicit", + reason: "selected context engine slot", + }; + } if (params.config.allow.length > 0 && !explicitlyAllowed) { return { enabled: false, diff --git a/src/plugins/config-state.test.ts b/src/plugins/config-state.test.ts index 2ff9ff9632b..246a21bd29b 100644 --- a/src/plugins/config-state.test.ts +++ b/src/plugins/config-state.test.ts @@ -54,6 +54,16 @@ describe("normalizePluginsConfig", () => { expect(normalizePluginsConfig(config).slots.memory).toBe(expected); }); + it.each([ + [{}, undefined], + [{ slots: { contextEngine: "lossless-claw" } }, "lossless-claw"], + [{ slots: { contextEngine: "none" } }, null], + [{ slots: { contextEngine: " cortex " } }, "cortex"], + [{ slots: { contextEngine: "" } }, undefined], + ] as const)("preserves contextEngine slot for %o (#64170)", (config, expected) => { + expect(normalizePluginsConfig(config).slots.contextEngine).toBe(expected); + }); + it.each([ { name: "normalizes plugin hook policy flags", @@ -432,6 +442,32 @@ describe("resolveEffectivePluginActivationState", () => { reason: "enabled by effective config", }); }); + + it("treats an explicitly selected workspace context engine as explicit activation", () => { + const rawConfig = { + plugins: { + slots: { + contextEngine: "lossless-claw", + }, + }, + }; + + expect( + resolveEffectivePluginActivationState({ + id: "lossless-claw", + origin: "workspace", + config: normalizePluginsConfig(rawConfig.plugins), + rootConfig: rawConfig, + activationSource: createPluginActivationSource({ config: rawConfig }), + }), + ).toEqual({ + enabled: true, + activated: true, + explicitlyEnabled: true, + source: "explicit", + reason: "selected context engine slot", + }); + }); }); describe("resolveEnableState", () => { @@ -525,6 +561,20 @@ describe("resolveEnableState", () => { }, }); }); + + it("keeps an explicitly selected workspace context engine enabled when omitted from plugins.allow", () => { + expectNormalizedEnableState({ + id: "lossless-claw", + origin: "workspace", + config: { + allow: ["telegram"], + slots: { contextEngine: "lossless-claw" }, + }, + expected: { + enabled: true, + }, + }); + }); }); describe("resolveMemorySlotDecision", () => { diff --git a/src/plugins/config-state.ts b/src/plugins/config-state.ts index c7cd1d8b1ae..def6621a012 100644 --- a/src/plugins/config-state.ts +++ b/src/plugins/config-state.ts @@ -23,6 +23,7 @@ export type PluginExplicitSelectionCause = | "enabled-in-config" | "bundled-channel-enabled-in-config" | "selected-memory-slot" + | "selected-context-engine-slot" | "selected-in-allowlist"; export type PluginActivationCause = @@ -104,6 +105,7 @@ const PLUGIN_ACTIVATION_REASON_BY_CAUSE: Record = "enabled-in-config": "enabled in config", "bundled-channel-enabled-in-config": "channel enabled in config", "selected-memory-slot": "selected memory slot", + "selected-context-engine-slot": "selected context engine slot", "selected-in-allowlist": "selected in allowlist", "plugins-disabled": "plugins disabled", "blocked-by-denylist": "blocked by denylist", @@ -231,6 +233,9 @@ function resolveExplicitPluginSelection(params: { if (params.config.slots.memory === params.id) { return { explicitlyEnabled: true, cause: "selected-memory-slot" }; } + if (params.config.slots.contextEngine === params.id) { + return { explicitlyEnabled: true, cause: "selected-context-engine-slot" }; + } if (params.origin !== "bundled" && params.config.allow.includes(params.id)) { return { explicitlyEnabled: true, cause: "selected-in-allowlist" }; } @@ -288,7 +293,12 @@ export function resolvePluginActivationState(params: { }); } const explicitlyAllowed = params.config.allow.includes(params.id); - if (params.origin === "workspace" && !explicitlyAllowed && entry?.enabled !== true) { + if ( + params.origin === "workspace" && + !explicitlyAllowed && + entry?.enabled !== true && + explicitSelection.cause !== "selected-context-engine-slot" + ) { return toPluginActivationState({ enabled: false, activated: false, @@ -306,6 +316,15 @@ export function resolvePluginActivationState(params: { cause: "selected-memory-slot", }); } + if (params.config.slots.contextEngine === params.id) { + return toPluginActivationState({ + enabled: true, + activated: true, + explicitlyEnabled: true, + source: "explicit", + cause: "selected-context-engine-slot", + }); + } if (explicitSelection.cause === "bundled-channel-enabled-in-config") { return toPluginActivationState({ enabled: true,