mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -99,15 +99,43 @@ function resolveMemorySlotStartupPluginId(params: {
|
||||
return normalizePluginId(configuredSlot);
|
||||
}
|
||||
|
||||
function resolveContextEngineSlotStartupPluginId(params: {
|
||||
activationSourceConfig: OpenClawConfig;
|
||||
activationSourcePlugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
|
||||
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<string>;
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user