From cd79e01be3644b8dc45d1eb393da95ab64f68636 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 26 Apr 2026 11:32:51 +0100 Subject: [PATCH] fix: load default memory plugin at startup --- CHANGELOG.md | 1 + src/plugins/channel-plugin-ids.test.ts | 42 ++++++++++------- src/plugins/gateway-startup-plugin-ids.ts | 46 +++++++++++++------ ...web-provider-resolution-candidates.test.ts | 27 ++++++++++- src/plugins/web-provider-resolution-shared.ts | 3 ++ 5 files changed, 88 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index efc9e9f9234..094bc06d8b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugins/startup: load the default `memory-core` slot during Gateway startup when permitted so active-memory recall can call `memory_search` and `memory_get` without requiring an explicit `plugins.slots.memory` entry, while preserving `plugins.slots.memory: "none"`. Thanks @codex. - Plugins/CLI: prefer native require for compiled bundled plugin JavaScript before jiti so read-only config, status, device, and node commands avoid unnecessary transform overhead on slow hosts. Fixes #62842. Thanks @Effet. - Plugins/compat: add missing dated compatibility records for legacy extension-api, memory registration, provider hook/type aliases, runtime aliases, channel SDK helpers, and approval/test utility shims. Thanks @vincentkoc. - Plugins/CLI: refresh the persisted registry after managed plugin files are removed so ClawHub uninstall cannot leave stale `plugins list` entries. Thanks @codex. diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index 8993fad34be..f9c655e9895 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -419,26 +419,26 @@ describe("resolveGatewayStartupPluginIds", () => { enabledPluginIds: ["voice-call"], modelId: "demo-cli/demo-model", }), - ["demo-channel", "browser", "voice-call"], + ["demo-channel", "browser", "voice-call", "memory-core"], ], [ "keeps bundled startup sidecars with enabledByDefault at idle startup", {} as OpenClawConfig, - ["demo-channel", "browser"], + ["demo-channel", "browser", "memory-core"], ], [ "keeps provider plugins out of idle startup when only provider config references them", createStartupConfig({ providerIds: ["demo-provider"], }), - ["demo-channel", "browser"], + ["demo-channel", "browser", "memory-core"], ], [ "includes explicitly enabled non-channel sidecars in startup scope", createStartupConfig({ enabledPluginIds: ["demo-global-sidecar", "voice-call"], }), - ["demo-channel", "browser", "voice-call", "demo-global-sidecar"], + ["demo-channel", "browser", "voice-call", "memory-core", "demo-global-sidecar"], ], [ "keeps default-enabled startup sidecars when a restrictive allowlist permits them", @@ -453,7 +453,7 @@ describe("resolveGatewayStartupPluginIds", () => { createStartupConfig({ channelIds: ["demo-channel", "demo-other-channel"], }), - ["demo-channel", "demo-other-channel", "browser"], + ["demo-channel", "demo-other-channel", "browser", "memory-core"], ], ] as const)("%s", (_name, config, expected) => { expectStartupPluginIdsCase({ config, expected }); @@ -501,7 +501,7 @@ describe("resolveGatewayStartupPluginIds", () => { env: { DEMO_CHANNEL_ANYTHING: "1", } as NodeJS.ProcessEnv, - expected: ["demo-channel", "browser"], + expected: ["demo-channel", "browser", "memory-core"], }); expect( resolveConfiguredDeferredChannelPluginIds({ @@ -564,7 +564,7 @@ describe("resolveGatewayStartupPluginIds", () => { }, } as OpenClawConfig, env: {}, - expected: ["browser"], + expected: ["browser", "memory-core"], }); }); @@ -582,7 +582,7 @@ describe("resolveGatewayStartupPluginIds", () => { env: { OPENCLAW_STATE_DIR: "/tmp/openclaw-with-persisted-demo-channel", } as NodeJS.ProcessEnv, - expected: ["browser"], + expected: ["browser", "memory-core"], }); }); @@ -657,12 +657,22 @@ describe("resolveGatewayStartupPluginIds", () => { }); }); + it("includes the default memory slot plugin when the allowlist permits it", () => { + expectStartupPluginIdsCase({ + config: createStartupConfig({ + allowPluginIds: ["browser", "memory-core"], + noConfiguredChannels: true, + }), + expected: ["browser", "memory-core"], + }); + }); + it("does not include non-selected memory plugins only because they are enabled", () => { expectStartupPluginIdsCase({ config: createStartupConfig({ enabledPluginIds: ["memory-lancedb"], }), - expected: ["demo-channel", "browser"], + expected: ["demo-channel", "browser", "memory-core"], }); }); @@ -672,7 +682,7 @@ describe("resolveGatewayStartupPluginIds", () => { agentRuntimeId: "codex", enabledPluginIds: ["codex"], }), - expected: ["demo-channel", "browser", "codex"], + expected: ["demo-channel", "browser", "codex", "memory-core"], }); }); @@ -682,7 +692,7 @@ describe("resolveGatewayStartupPluginIds", () => { agentRuntimeIds: ["codex"], enabledPluginIds: ["codex"], }), - expected: ["demo-channel", "browser", "codex"], + expected: ["demo-channel", "browser", "codex", "memory-core"], }); }); @@ -692,7 +702,7 @@ describe("resolveGatewayStartupPluginIds", () => { enabledPluginIds: ["codex"], }), env: { OPENCLAW_AGENT_RUNTIME: "codex" }, - expected: ["demo-channel", "browser", "codex"], + expected: ["demo-channel", "browser", "codex", "memory-core"], }); }); @@ -702,7 +712,7 @@ describe("resolveGatewayStartupPluginIds", () => { agentRuntimeId: "demo-cli", enabledPluginIds: ["demo-provider-plugin"], }), - expected: ["demo-channel", "browser", "demo-provider-plugin"], + expected: ["demo-channel", "browser", "demo-provider-plugin", "memory-core"], }); }); @@ -715,7 +725,7 @@ describe("resolveGatewayStartupPluginIds", () => { config: createStartupConfig({ agentRuntimeId: runtime, }), - expected: ["demo-channel", "browser", pluginId], + expected: ["demo-channel", "browser", pluginId, "memory-core"], }); }); @@ -738,7 +748,7 @@ describe("resolveGatewayStartupPluginIds", () => { }, }, } as OpenClawConfig, - expected: ["demo-channel", "browser"], + expected: ["demo-channel", "browser", "memory-core"], }); }); @@ -761,7 +771,7 @@ describe("resolveGatewayStartupPluginIds", () => { }, }, } as OpenClawConfig, - expected: ["demo-channel", "browser"], + expected: ["demo-channel", "browser", "memory-core"], }); }); }); diff --git a/src/plugins/gateway-startup-plugin-ids.ts b/src/plugins/gateway-startup-plugin-ids.ts index 86bed0d9b8f..09e7b627a73 100644 --- a/src/plugins/gateway-startup-plugin-ids.ts +++ b/src/plugins/gateway-startup-plugin-ids.ts @@ -65,21 +65,36 @@ function resolveGatewayStartupDreamingPluginIds(config: OpenClawConfig): Set string, -): string | undefined { - const configuredSlot = config.plugins?.slots?.memory?.trim(); - if (!configuredSlot || configuredSlot.toLowerCase() === "none") { +function resolveMemorySlotStartupPluginId(params: { + activationSourceConfig: OpenClawConfig; + activationSourcePlugins: ReturnType; + normalizePluginId: (pluginId: string) => string; +}): string | undefined { + const { activationSourceConfig, activationSourcePlugins, normalizePluginId } = params; + const configuredSlot = activationSourceConfig.plugins?.slots?.memory?.trim(); + if (configuredSlot?.toLowerCase() === "none") { return undefined; } + if (!configuredSlot) { + const defaultSlot = activationSourcePlugins.slots.memory; + if (typeof defaultSlot !== "string") { + return undefined; + } + if ( + activationSourcePlugins.allow.length > 0 && + !activationSourcePlugins.allow.includes(defaultSlot) + ) { + return undefined; + } + return defaultSlot; + } return normalizePluginId(configuredSlot); } function shouldConsiderForGatewayStartup(params: { plugin: InstalledPluginIndexRecord; startupDreamingPluginIds: ReadonlySet; - explicitMemorySlotStartupPluginId?: string; + memorySlotStartupPluginId?: string; }): boolean { if (isGatewayStartupSidecar(params.plugin)) { return true; @@ -90,7 +105,7 @@ function shouldConsiderForGatewayStartup(params: { if (params.startupDreamingPluginIds.has(params.plugin.pluginId)) { return true; } - return params.explicitMemorySlotStartupPluginId === params.plugin.pluginId; + return params.memorySlotStartupPluginId === params.plugin.pluginId; } function hasConfiguredStartupChannel(params: { @@ -246,18 +261,23 @@ export function resolveGatewayStartupPluginIds(params: { // not the auto-enabled effective snapshot, or configured-only channels can be // misclassified as explicit enablement. const activationSourceConfig = params.activationSourceConfig ?? params.config; + const activationSourcePlugins = normalizePluginsConfigWithRegistry( + activationSourceConfig.plugins, + index, + ); const activationSource = { - plugins: normalizePluginsConfigWithRegistry(activationSourceConfig.plugins, index), + plugins: activationSourcePlugins, rootConfig: activationSourceConfig, }; const requiredAgentHarnessRuntimes = new Set( collectConfiguredAgentHarnessRuntimes(activationSourceConfig, params.env), ); const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config); - const explicitMemorySlotStartupPluginId = resolveExplicitMemorySlotStartupPluginId( + const memorySlotStartupPluginId = resolveMemorySlotStartupPluginId({ activationSourceConfig, - createPluginRegistryIdNormalizer(index), - ); + activationSourcePlugins, + normalizePluginId: createPluginRegistryIdNormalizer(index), + }); return index.plugins .filter((plugin) => { if (hasConfiguredStartupChannel({ plugin, manifestRegistry, configuredChannelIds })) { @@ -286,7 +306,7 @@ export function resolveGatewayStartupPluginIds(params: { !shouldConsiderForGatewayStartup({ plugin, startupDreamingPluginIds, - explicitMemorySlotStartupPluginId, + memorySlotStartupPluginId, }) ) { return false; diff --git a/src/plugins/web-provider-resolution-candidates.test.ts b/src/plugins/web-provider-resolution-candidates.test.ts index 20609003ca8..20ee5604902 100644 --- a/src/plugins/web-provider-resolution-candidates.test.ts +++ b/src/plugins/web-provider-resolution-candidates.test.ts @@ -65,14 +65,14 @@ describe("resolveManifestDeclaredWebProviderCandidatePluginIds", () => { expect(mocks.loadPluginManifestRegistryForInstalledIndex).not.toHaveBeenCalled(); }); - it("keeps runtime fallback for scoped plugins with no declared web candidates", () => { + it("keeps scoped plugins with no declared web candidates scoped-empty", () => { expect( resolveManifestDeclaredWebProviderCandidatePluginIds({ contract: "webSearchProviders", configKey: "webSearch", onlyPluginIds: ["missing-plugin"], }), - ).toBeUndefined(); + ).toEqual([]); expect(mocks.loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledWith( expect.objectContaining({ pluginIds: ["missing-plugin"], @@ -80,6 +80,29 @@ describe("resolveManifestDeclaredWebProviderCandidatePluginIds", () => { ); }); + it("keeps origin filters with no declared web candidates scoped-empty", () => { + mocks.loadPluginManifestRegistryForInstalledIndex.mockReturnValue({ + plugins: [ + { + id: "workspace-tool", + origin: "workspace", + configSchema: { + properties: {}, + }, + }, + ], + diagnostics: [], + }); + + expect( + resolveManifestDeclaredWebProviderCandidatePluginIds({ + contract: "webSearchProviders", + configKey: "webSearch", + origin: "bundled", + }), + ).toEqual([]); + }); + it("derives provider candidates from a single manifest-registry read", () => { expect( resolveManifestDeclaredWebProviderCandidatePluginIds({ diff --git a/src/plugins/web-provider-resolution-shared.ts b/src/plugins/web-provider-resolution-shared.ts index aa2a60678a6..6f2ff99cf58 100644 --- a/src/plugins/web-provider-resolution-shared.ts +++ b/src/plugins/web-provider-resolution-shared.ts @@ -105,6 +105,9 @@ export function resolveManifestDeclaredWebProviderCandidatePluginIds(params: { if (ids.length > 0) { return ids; } + if (params.origin || scopedPluginIds !== undefined) { + return []; + } return undefined; }