fix(plugins): preserve contextEngine slot through config normalization (#64192)

Merged via squash.

Prepared head SHA: ae8bd9f09d
Co-authored-by: hclsys <7755017+hclsys@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
hcl
2026-04-11 06:58:27 +08:00
committed by GitHub
parent c0dc3b3cb7
commit 8a28a3b056
5 changed files with 91 additions and 2 deletions

View File

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

View File

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

View File

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

View File

@@ -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", () => {

View File

@@ -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<PluginActivationCause, string> =
"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,