Skip to content

Commit 0db6c28

Browse files
HCLclaude
authored andcommitted
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>
1 parent 4458c27 commit 0db6c28

3 files changed

Lines changed: 98 additions & 8 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
2323
- Active Memory: apply `setupGraceTimeoutMs` to the embedded recall runner as well as the outer prompt-build watchdog, so very-cold first recalls keep the configured setup grace end-to-end. (#74480) Thanks @volcano303.
2424
- 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.
2525
- 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.
26+
- 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.
2627
- 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.
2728
- 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.
2829
- Plugins/externalization: keep official ACPX, Google Chat, and LINE install specs on production package names, leaving beta-tag probing to the explicit OpenClaw beta update channel. Thanks @vincentkoc.

src/plugins/channel-plugin-ids.test.ts

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,18 @@ function createManifestRegistryFixture(): PluginManifestRegistry {
301301
providers: [],
302302
cliBackends: [],
303303
},
304+
{
305+
id: "lossless-claw",
306+
kind: "context-engine",
307+
channels: [],
308+
// No activation.onStartup — this is the bug scenario (#76576):
309+
// external context-engine plugins do not set onStartup but must be
310+
// included in gateway startup when selected via plugins.slots.contextEngine.
311+
origin: "installed",
312+
enabledByDefault: undefined,
313+
providers: [],
314+
cliBackends: [],
315+
},
304316
].map(withManifestLoadPaths) as PluginManifestRecord[],
305317
diagnostics: [],
306318
};
@@ -436,7 +448,13 @@ function createStartupConfig(params: {
436448
allowPluginIds?: string[];
437449
noConfiguredChannels?: boolean;
438450
memorySlot?: string;
451+
contextEngine?: string;
439452
}) {
453+
const slotsConfig = {
454+
...(params.memorySlot ? { memory: params.memorySlot } : {}),
455+
...(params.contextEngine ? { contextEngine: params.contextEngine } : {}),
456+
};
457+
const hasSlots = Object.keys(slotsConfig).length > 0;
440458
return {
441459
...(params.noConfiguredChannels
442460
? {
@@ -453,7 +471,7 @@ function createStartupConfig(params: {
453471
? {
454472
plugins: {
455473
...(params.allowPluginIds?.length ? { allow: params.allowPluginIds } : {}),
456-
...(params.memorySlot ? { slots: { memory: params.memorySlot } } : {}),
474+
...(hasSlots ? { slots: slotsConfig } : {}),
457475
entries: Object.fromEntries(
458476
params.enabledPluginIds.map((pluginId) => [pluginId, { enabled: true }]),
459477
),
@@ -465,12 +483,10 @@ function createStartupConfig(params: {
465483
allow: params.allowPluginIds,
466484
},
467485
}
468-
: params.memorySlot
486+
: hasSlots
469487
? {
470488
plugins: {
471-
slots: {
472-
memory: params.memorySlot,
473-
},
489+
slots: slotsConfig,
474490
},
475491
}
476492
: {}),
@@ -1033,6 +1049,44 @@ describe("resolveGatewayStartupPluginIds", () => {
10331049
});
10341050
});
10351051

1052+
it("includes the selected context-engine slot plugin in startup scope even without activation.onStartup (#76576)", () => {
1053+
expectStartupPluginIdsCase({
1054+
config: createStartupConfig({
1055+
enabledPluginIds: ["lossless-claw"],
1056+
contextEngine: "lossless-claw",
1057+
}),
1058+
expected: ["demo-channel", "browser", "memory-core", "lossless-claw"],
1059+
});
1060+
});
1061+
1062+
it("does not include context-engine plugins not selected via the slot", () => {
1063+
expectStartupPluginIdsCase({
1064+
config: createStartupConfig({
1065+
enabledPluginIds: ["lossless-claw"],
1066+
}),
1067+
expected: ["demo-channel", "browser", "memory-core"],
1068+
});
1069+
});
1070+
1071+
it("does not include the context-engine slot plugin when it is the built-in legacy engine", () => {
1072+
expectStartupPluginIdsCase({
1073+
config: createStartupConfig({
1074+
contextEngine: "legacy",
1075+
}),
1076+
expected: ["demo-channel", "browser", "memory-core"],
1077+
});
1078+
});
1079+
1080+
it("normalizes the context-engine slot id before startup filtering", () => {
1081+
expectStartupPluginIdsCase({
1082+
config: createStartupConfig({
1083+
enabledPluginIds: ["lossless-claw"],
1084+
contextEngine: "Lossless-Claw",
1085+
}),
1086+
expected: ["demo-channel", "browser", "memory-core", "lossless-claw"],
1087+
});
1088+
});
1089+
10361090
it("includes required agent harness owner plugins when the default runtime is forced", () => {
10371091
expectStartupPluginIdsCase({
10381092
config: createStartupConfig({

src/plugins/gateway-startup-plugin-ids.ts

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,43 @@ function resolveMemorySlotStartupPluginId(params: {
9999
return normalizePluginId(configuredSlot);
100100
}
101101

102+
function resolveContextEngineSlotStartupPluginId(params: {
103+
activationSourceConfig: OpenClawConfig;
104+
activationSourcePlugins: ReturnType<typeof normalizePluginsConfigWithRegistry>;
105+
normalizePluginId: (pluginId: string) => string;
106+
}): string | undefined {
107+
const { activationSourceConfig, activationSourcePlugins, normalizePluginId } = params;
108+
const configuredSlot = activationSourceConfig.plugins?.slots?.contextEngine?.trim();
109+
if (!configuredSlot) {
110+
return undefined;
111+
}
112+
const normalized = normalizePluginId(configuredSlot);
113+
// "legacy" is the built-in default engine — no plugin startup needed.
114+
if (normalized === "legacy") {
115+
return undefined;
116+
}
117+
if (activationSourcePlugins.deny.includes(normalized)) {
118+
return undefined;
119+
}
120+
if (activationSourcePlugins.entries[normalized]?.enabled === false) {
121+
return undefined;
122+
}
123+
return normalized;
124+
}
125+
102126
function shouldConsiderForGatewayStartup(params: {
103127
plugin: InstalledPluginIndexRecord;
104128
manifest: PluginManifestRecord | undefined;
105129
startupDreamingPluginIds: ReadonlySet<string>;
106130
memorySlotStartupPluginId?: string;
131+
contextEngineSlotStartupPluginId?: string;
107132
}): boolean {
108133
if (params.manifest?.activation?.onStartup === true) {
109134
return true;
110135
}
136+
if (params.contextEngineSlotStartupPluginId === params.plugin.pluginId) {
137+
return true;
138+
}
111139
if (!isGatewayStartupMemoryPlugin(params.plugin)) {
112140
return false;
113141
}
@@ -404,12 +432,18 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: {
404432
const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config);
405433
const manifestLookup = createManifestRegistryLookup(params.manifestRegistry);
406434
const configuredSpeechProviderIds = collectConfiguredSpeechProviderIds(activationSourceConfig);
435+
const normalizePluginId = createPluginRegistryIdNormalizer(params.index, {
436+
manifestRegistry: params.manifestRegistry,
437+
});
407438
const memorySlotStartupPluginId = resolveMemorySlotStartupPluginId({
408439
activationSourceConfig,
409440
activationSourcePlugins,
410-
normalizePluginId: createPluginRegistryIdNormalizer(params.index, {
411-
manifestRegistry: params.manifestRegistry,
412-
}),
441+
normalizePluginId,
442+
});
443+
const contextEngineSlotStartupPluginId = resolveContextEngineSlotStartupPluginId({
444+
activationSourceConfig,
445+
activationSourcePlugins,
446+
normalizePluginId,
413447
});
414448
const pluginIds = params.index.plugins
415449
.filter((plugin) => {
@@ -471,6 +505,7 @@ export function resolveGatewayStartupPluginPlanFromRegistry(params: {
471505
manifest,
472506
startupDreamingPluginIds,
473507
memorySlotStartupPluginId,
508+
contextEngineSlotStartupPluginId,
474509
})
475510
) {
476511
return false;

0 commit comments

Comments
 (0)