From 5e2136c6aeed583f75493ce8ec3a9c4d13efc7f6 Mon Sep 17 00:00:00 2001 From: EronFan <50734013+EronFan@users.noreply.github.com> Date: Sat, 11 Apr 2026 05:02:44 +0800 Subject: [PATCH] fix: include memory plugins in gateway startup (openclaw#64423) Verified: - pnpm build - pnpm check - pnpm test -- src/plugins/channel-plugin-ids.test.ts Co-authored-by: EronFan <50734013+EronFan@users.noreply.github.com> --- CHANGELOG.md | 1 + src/plugins/channel-plugin-ids.test.ts | 105 +++++++++---------------- src/plugins/channel-plugin-ids.ts | 63 +++++++++++---- 3 files changed, 88 insertions(+), 81 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 476e1284b12..d1f7ec5542a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -44,6 +44,7 @@ Docs: https://docs.openclaw.ai - Security/nodes: keep `nodes` tool output paths inside the workspace boundary so model-driven node writes cannot escape the intended workspace. (#63551) Thanks @pgondhi987. - Security/QQBot: enforce media storage boundaries for all outbound local file paths and route image-size probes through SSRF-guarded media fetching instead of raw `fetch()`. (#63271, #63495) Thanks @pgondhi987. - Channel setup: ignore workspace plugin shadows when resolving trusted channel setup catalog entries so onboarding and setup flows keep using the bundled, trusted setup contract. +- Gateway/memory startup: load the explicitly selected memory-slot plugin during gateway startup, while keeping restrictive allowlists and implicit default memory slots from auto-starting unrelated memory plugins. (#64423) Thanks @EronFan. - Config/plugins: let config writes keep disabled plugin entries without forcing required plugin config schemas or crashing raw plugin validation, and avoid re-activating plugin registry state during schema checks. (#54971, #63296) Thanks @fuller-stack-dev. - Config validation: surface the actual offending field for strict-schema union failures in bindings, including top-level unexpected keys on the matching ACP branch. (#40841) Thanks @Hollychou924. - Wizard/plugin config: coerce integer-typed plugin config fields from interactive text input so integer schema values persist as numbers instead of failing validation. (#63346) Thanks @jalehman. diff --git a/src/plugins/channel-plugin-ids.test.ts b/src/plugins/channel-plugin-ids.test.ts index a250578a623..e823e4c7ad6 100644 --- a/src/plugins/channel-plugin-ids.test.ts +++ b/src/plugins/channel-plugin-ids.test.ts @@ -49,6 +49,14 @@ function createManifestRegistryFixture() { providers: ["demo-provider"], cliBackends: ["demo-cli"], }, + { + id: "voice-call", + channels: [], + origin: "bundled", + enabledByDefault: undefined, + providers: [], + cliBackends: [], + }, { id: "memory-core", kind: "memory", @@ -67,14 +75,6 @@ function createManifestRegistryFixture() { providers: [], cliBackends: [], }, - { - id: "voice-call", - channels: [], - origin: "bundled", - enabledByDefault: undefined, - providers: [], - cliBackends: [], - }, { id: "demo-global-sidecar", channels: [], @@ -121,6 +121,7 @@ function createStartupConfig(params: { channelIds?: string[]; allowPluginIds?: string[]; noConfiguredChannels?: boolean; + memorySlot?: string; }) { return { ...(params.noConfiguredChannels @@ -138,6 +139,7 @@ function createStartupConfig(params: { ? { plugins: { ...(params.allowPluginIds?.length ? { allow: params.allowPluginIds } : {}), + ...(params.memorySlot ? { slots: { memory: params.memorySlot } } : {}), entries: Object.fromEntries( params.enabledPluginIds.map((pluginId) => [pluginId, { enabled: true }]), ), @@ -149,6 +151,14 @@ function createStartupConfig(params: { allow: params.allowPluginIds, }, } + : params.memorySlot + ? { + plugins: { + slots: { + memory: params.memorySlot, + }, + }, + } : {}), ...(params.providerIds?.length ? { @@ -250,6 +260,9 @@ describe("resolveGatewayStartupPluginIds", () => { "voice-call": { enabled: true, }, + "memory-core": { + enabled: true, + }, }, }, } as OpenClawConfig; @@ -261,74 +274,32 @@ describe("resolveGatewayStartupPluginIds", () => { }); }); - it("includes memory-core at startup when dreaming is enabled", () => { + it("includes the explicitly selected memory slot plugin in startup scope", () => { expectStartupPluginIdsCase({ - config: { - channels: {}, - plugins: { - entries: { - "memory-core": { - enabled: true, - config: { - dreaming: { - enabled: true, - }, - }, - }, - }, - }, - } as OpenClawConfig, - expected: ["browser", "memory-core"], + config: createStartupConfig({ + enabledPluginIds: ["memory-lancedb"], + memorySlot: "memory-lancedb", + }), + expected: ["demo-channel", "browser", "memory-lancedb"], }); }); - it("includes the selected memory-slot plugin and memory-core when dreaming is enabled", () => { + it("normalizes the raw memory slot id before startup filtering", () => { expectStartupPluginIdsCase({ - config: { - plugins: { - slots: { - memory: "memory-lancedb", - }, - entries: { - "memory-core": { - enabled: true, - }, - "memory-lancedb": { - enabled: true, - config: { - dreaming: { - enabled: true, - }, - }, - }, - }, - }, - } as OpenClawConfig, - expected: ["demo-channel", "browser", "memory-core", "memory-lancedb"], + config: createStartupConfig({ + enabledPluginIds: ["memory-core"], + memorySlot: "Memory-Core", + }), + expected: ["demo-channel", "browser", "memory-core"], }); }); - it("does not bypass activation policy for dreaming startup owners", () => { + it("does not include non-selected memory plugins only because they are enabled", () => { expectStartupPluginIdsCase({ - config: { - channels: {}, - plugins: { - slots: { - memory: "memory-lancedb", - }, - entries: { - "memory-lancedb": { - enabled: false, - config: { - dreaming: { - enabled: true, - }, - }, - }, - }, - }, - } as OpenClawConfig, - expected: ["browser"], + config: createStartupConfig({ + enabledPluginIds: ["memory-lancedb"], + }), + expected: ["demo-channel", "browser"], }); }); }); diff --git a/src/plugins/channel-plugin-ids.ts b/src/plugins/channel-plugin-ids.ts index 41f44697507..18fdbc31fbb 100644 --- a/src/plugins/channel-plugin-ids.ts +++ b/src/plugins/channel-plugin-ids.ts @@ -7,6 +7,7 @@ import { } from "../memory-host-sdk/dreaming.js"; import { createPluginActivationSource, + normalizePluginId, normalizePluginsConfig, resolveEffectivePluginActivationState, } from "./config-state.js"; @@ -29,6 +30,10 @@ function hasRuntimeContractSurface(plugin: PluginManifestRecord): boolean { ); } +function isGatewayStartupMemoryPlugin(plugin: PluginManifestRecord): boolean { + return hasKind(plugin.kind, "memory"); +} + function isGatewayStartupSidecar(plugin: PluginManifestRecord): boolean { return plugin.channels.length === 0 && !hasRuntimeContractSurface(plugin); } @@ -44,6 +49,33 @@ function resolveGatewayStartupDreamingPluginIds(config: OpenClawConfig): Set; + explicitMemorySlotStartupPluginId?: string; +}): boolean { + if (isGatewayStartupSidecar(params.plugin)) { + return true; + } + if (!isGatewayStartupMemoryPlugin(params.plugin)) { + return false; + } + if (params.startupDreamingPluginIds.has(params.plugin.id)) { + return true; + } + return params.explicitMemorySlotStartupPluginId === params.plugin.id; +} + export function resolveChannelPluginIds(params: { config: OpenClawConfig; workspaceDir?: string; @@ -113,6 +145,9 @@ export function resolveGatewayStartupPluginIds(params: { config: params.activationSourceConfig ?? params.config, }); const startupDreamingPluginIds = resolveGatewayStartupDreamingPluginIds(params.config); + const explicitMemorySlotStartupPluginId = resolveExplicitMemorySlotStartupPluginId( + params.activationSourceConfig ?? params.config, + ); return loadPluginManifestRegistry({ config: params.config, workspaceDir: params.workspaceDir, @@ -122,6 +157,15 @@ export function resolveGatewayStartupPluginIds(params: { if (plugin.channels.some((channelId) => configuredChannelIds.has(channelId))) { return true; } + if ( + !shouldConsiderForGatewayStartup({ + plugin, + startupDreamingPluginIds, + explicitMemorySlotStartupPluginId, + }) + ) { + return false; + } const activationState = resolveEffectivePluginActivationState({ id: plugin.id, origin: plugin.origin, @@ -130,22 +174,13 @@ export function resolveGatewayStartupPluginIds(params: { enabledByDefault: plugin.enabledByDefault, activationSource, }); - const isAllowedStartupActivation = (): boolean => { - if (!activationState.enabled) { - return false; - } - if (plugin.origin !== "bundled") { - return activationState.explicitlyEnabled; - } - return activationState.source === "explicit" || activationState.source === "default"; - }; - if (startupDreamingPluginIds.has(plugin.id)) { - return isAllowedStartupActivation(); - } - if (!isGatewayStartupSidecar(plugin)) { + if (!activationState.enabled) { return false; } - return isAllowedStartupActivation(); + if (plugin.origin !== "bundled") { + return activationState.explicitlyEnabled; + } + return activationState.source === "explicit" || activationState.source === "default"; }) .map((plugin) => plugin.id); }