From deb7b821c195ca8c450fb234b4fec0b6c0bd54ea Mon Sep 17 00:00:00 2001 From: HCL Date: Sun, 3 May 2026 19:13:16 +0800 Subject: [PATCH] fix(plugins): include selected context-engine slot plugin in gateway startup (#76576) External context-engine plugins (e.g. lossless-claw) register via api.registerContextEngine at load time but ship without activation.onStartup in their manifest. The gateway startup planner only considered memory plugins and explicit sidecar plugins, so a selected non-legacy context engine was omitted from the startup load plan and never loaded before agent turns resolved the active engine, producing the "Context engine X is not registered; falling back to default engine legacy" warning. Fix: add resolveContextEngineSlotStartupPluginId mirroring the memory slot pattern; pass contextEngineSlotStartupPluginId into shouldConsiderForGatewayStartup so the selected context-engine plugin is included in pluginIds regardless of its manifest activation shape. Tests: added four regression cases covering include, exclude, legacy bypass, and id normalization. 82 tests pass. Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 1 + src/plugins/channel-plugin-ids.test.ts | 64 +++++++++++++++++++++-- src/plugins/gateway-startup-plugin-ids.ts | 41 +++++++++++++-- 3 files changed, 98 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 947b1b155d9..6b1d8805f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai - CLI/config: keep JSON dry-run patches validating touched channel configuration against bundled channel schemas even when the patch only contains SecretRef objects. - Plugins/tools: keep disabled bundled tool plugins out of explicit runtime allowlist ownership and fall back from loaded-but-empty channel registries to tool-bearing plugin registries, so Active Memory can use bundled `memory-core` search/get tools even when `memory-lancedb` is disabled. Fixes #76603. Thanks @jwong-art. - Plugins/install: run `npm install` from the managed npm-root manifest so installing one `@openclaw/*` plugin preserves already installed sibling plugins instead of pruning them. Fixes #76571. (#76602) Thanks @byungskers and @crpol. +- Plugins/context-engine: include the selected `plugins.slots.contextEngine` plugin in the gateway startup load plan so external context-engine plugins without `activation.onStartup` in their manifest are loaded before any agent turn resolves the active engine; prevents the "Context engine X is not registered; falling back to default engine legacy" warning after gateway startup. Fixes #76576. Thanks @hclsys. - Plugins/tools: restore on-demand registry load for path-based plugins (origin "config") so tool factories registered via `plugins.load.paths` are resolved at agent request time when no pre-warmed channel registry is present; prevents "unknown method" errors after gateway startup. Fixes #76598. Thanks @hclsys. - Channels/QQ Bot: resolve structured `clientSecret` SecretRefs before QQ token exchange, expose the QQ Bot secret contract to secrets tooling, and reject legacy `secretref:/...` marker strings. (#74772) Thanks @xialonglee. - Agents: keep active streamed provider replies alive by refreshing guarded fetch timeouts on raw body chunks and surface true prompt stream timeouts as explicit errors instead of partial assistant fragments. Fixes #76307. (#76633) Thanks @MkDev11. diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index 728863676b5..aa8c6175523 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -301,6 +301,18 @@ function createManifestRegistryFixture(): PluginManifestRegistry { providers: [], cliBackends: [], }, + { + id: "lossless-claw", + kind: "context-engine", + channels: [], + // No activation.onStartup — this is the bug scenario (#76576): + // external context-engine plugins do not set onStartup but must be + // included in gateway startup when selected via plugins.slots.contextEngine. + origin: "installed", + enabledByDefault: undefined, + providers: [], + cliBackends: [], + }, ].map(withManifestLoadPaths) as PluginManifestRecord[], diagnostics: [], }; @@ -436,7 +448,13 @@ function createStartupConfig(params: { allowPluginIds?: string[]; noConfiguredChannels?: boolean; memorySlot?: string; + contextEngine?: string; }) { + const slotsConfig = { + ...(params.memorySlot ? { memory: params.memorySlot } : {}), + ...(params.contextEngine ? { contextEngine: params.contextEngine } : {}), + }; + const hasSlots = Object.keys(slotsConfig).length > 0; return { ...(params.noConfiguredChannels ? { @@ -453,7 +471,7 @@ function createStartupConfig(params: { ? { plugins: { ...(params.allowPluginIds?.length ? { allow: params.allowPluginIds } : {}), - ...(params.memorySlot ? { slots: { memory: params.memorySlot } } : {}), + ...(hasSlots ? { slots: slotsConfig } : {}), entries: Object.fromEntries( params.enabledPluginIds.map((pluginId) => [pluginId, { enabled: true }]), ), @@ -465,12 +483,10 @@ function createStartupConfig(params: { allow: params.allowPluginIds, }, } - : params.memorySlot + : hasSlots ? { plugins: { - slots: { - memory: params.memorySlot, - }, + slots: slotsConfig, }, } : {}), @@ -1095,6 +1111,44 @@ describe("resolveGatewayStartupPluginIds", () => { }); }); + it("includes the selected context-engine slot plugin in startup scope even without activation.onStartup (#76576)", () => { + expectStartupPluginIdsCase({ + config: createStartupConfig({ + enabledPluginIds: ["lossless-claw"], + contextEngine: "lossless-claw", + }), + expected: ["demo-channel", "browser", "memory-core", "lossless-claw"], + }); + }); + + it("does not include context-engine plugins not selected via the slot", () => { + expectStartupPluginIdsCase({ + config: createStartupConfig({ + enabledPluginIds: ["lossless-claw"], + }), + expected: ["demo-channel", "browser", "memory-core"], + }); + }); + + it("does not include the context-engine slot plugin when it is the built-in legacy engine", () => { + expectStartupPluginIdsCase({ + config: createStartupConfig({ + contextEngine: "legacy", + }), + expected: ["demo-channel", "browser", "memory-core"], + }); + }); + + it("normalizes the context-engine slot id before startup filtering", () => { + expectStartupPluginIdsCase({ + config: createStartupConfig({ + enabledPluginIds: ["lossless-claw"], + contextEngine: "Lossless-Claw", + }), + expected: ["demo-channel", "browser", "memory-core", "lossless-claw"], + }); + }); + it("includes required agent harness owner plugins when the default runtime is forced", () => { expectStartupPluginIdsCase({ config: createStartupConfig({ diff --git a/src/plugins/gateway-startup-plugin-ids.ts b/src/plugins/gateway-startup-plugin-ids.ts index 47942a69c9b..ac39770d1e2 100644 --- a/src/plugins/gateway-startup-plugin-ids.ts +++ b/src/plugins/gateway-startup-plugin-ids.ts @@ -99,15 +99,43 @@ function resolveMemorySlotStartupPluginId(params: { return normalizePluginId(configuredSlot); } +function resolveContextEngineSlotStartupPluginId(params: { + activationSourceConfig: OpenClawConfig; + activationSourcePlugins: ReturnType; + normalizePluginId: (pluginId: string) => string; +}): string | undefined { + const { activationSourceConfig, activationSourcePlugins, normalizePluginId } = params; + const configuredSlot = activationSourceConfig.plugins?.slots?.contextEngine?.trim(); + if (!configuredSlot) { + return undefined; + } + const normalized = normalizePluginId(configuredSlot); + // "legacy" is the built-in default engine — no plugin startup needed. + if (normalized === "legacy") { + return undefined; + } + if (activationSourcePlugins.deny.includes(normalized)) { + return undefined; + } + if (activationSourcePlugins.entries[normalized]?.enabled === false) { + return undefined; + } + return normalized; +} + function shouldConsiderForGatewayStartup(params: { plugin: InstalledPluginIndexRecord; manifest: PluginManifestRecord | undefined; startupDreamingPluginIds: ReadonlySet; memorySlotStartupPluginId?: string; + contextEngineSlotStartupPluginId?: string; }): boolean { if (params.manifest?.activation?.onStartup === true) { return true; } + if (params.contextEngineSlotStartupPluginId === params.plugin.pluginId) { + return true; + } if (!isGatewayStartupMemoryPlugin(params.plugin)) { return false; } @@ -404,12 +432,18 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: { const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config); const manifestLookup = createManifestRegistryLookup(params.manifestRegistry); const configuredSpeechProviderIds = collectConfiguredSpeechProviderIds(activationSourceConfig); + const normalizePluginId = createPluginRegistryIdNormalizer(params.index, { + manifestRegistry: params.manifestRegistry, + }); const memorySlotStartupPluginId = resolveMemorySlotStartupPluginId({ activationSourceConfig, activationSourcePlugins, - normalizePluginId: createPluginRegistryIdNormalizer(params.index, { - manifestRegistry: params.manifestRegistry, - }), + normalizePluginId, + }); + const contextEngineSlotStartupPluginId = resolveContextEngineSlotStartupPluginId({ + activationSourceConfig, + activationSourcePlugins, + normalizePluginId, }); const pluginIds = params.index.plugins .filter((plugin) => { @@ -471,6 +505,7 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: { manifest, startupDreamingPluginIds, memorySlotStartupPluginId, + contextEngineSlotStartupPluginId, }) ) { return false;