From 8595e6c872c8a315c0583b0da6053511ec99bd48 Mon Sep 17 00:00:00 2001 From: Edward Abrams Date: Mon, 20 Apr 2026 12:43:08 -0700 Subject: [PATCH] fix(plugins): preserve memory capability across snapshot plugin loads Preserve the active memory capability when non-activating plugin snapshot loads run, and add a regression test.\n\nThanks @zeroaltitude. --- CHANGELOG.md | 1 + src/plugins/loader.test.ts | 81 ++++++++++++++++++++++++++++++++++++++ src/plugins/loader.ts | 3 ++ 3 files changed, 85 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4b34636ce95..9f7024ec963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Plugins/memory: preserve the active memory capability when read-only snapshot plugin loads run, so status and provider discovery paths no longer wipe memory public artifacts. (#69219) Thanks @zeroaltitude. - fix(security): block MINIMAX_API_HOST workspace env injection and remove env-driven URL routing [AI-assisted]. (#67300) Thanks @pgondhi987. - Cron/delivery: treat explicit `delivery.mode: "none"` runs as not requested even if the runner reports `delivered: false`, so no-delivery cron jobs no longer persist false delivery failures or errors. (#69285) Thanks @matsuri1987. - Plugins/install: repair active and default-enabled bundled plugin runtime dependencies before import in packaged installs, so bundled Discord, WhatsApp, Slack, Telegram, and provider plugins work without putting their dependency trees in core. diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 73106fc0fe4..19a61f900c5 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -2527,6 +2527,87 @@ module.exports = { id: "throws-after-import", register() {} };`, ); }); + it("preserves previously registered memory capability across activate:false snapshot loads", async () => { + useNoBundledPlugins(); + const workspaceDir = makeTempDir(); + const absolutePath = path.join(workspaceDir, "MEMORY.md"); + fs.writeFileSync(absolutePath, "# Memory\n"); + const memoryPlugin = writePlugin({ + id: "capability-survives-memory", + filename: "capability-survives-memory.cjs", + body: `module.exports = { + id: "capability-survives-memory", + kind: "memory", + register(api) { + api.registerMemoryCapability({ + publicArtifacts: { + async listArtifacts() { + return [{ + kind: "memory-root", + workspaceDir: ${JSON.stringify(workspaceDir)}, + relativePath: "MEMORY.md", + absolutePath: ${JSON.stringify(absolutePath)}, + agentIds: ["main"], + contentType: "markdown", + }]; + }, + }, + }); + }, + };`, + }); + const sidecarPlugin = writePlugin({ + id: "capability-survives-sidecar", + filename: "capability-survives-sidecar.cjs", + body: `module.exports = { + id: "capability-survives-sidecar", + register() {}, + };`, + }); + + const activateConfig = { + plugins: { + load: { paths: [memoryPlugin.file, sidecarPlugin.file] }, + allow: ["capability-survives-memory", "capability-survives-sidecar"], + slots: { memory: "capability-survives-memory" }, + }, + }; + loadOpenClawPlugins({ + cache: false, + workspaceDir: memoryPlugin.dir, + config: activateConfig, + }); + + const expectedArtifacts = [ + { + kind: "memory-root", + workspaceDir, + relativePath: "MEMORY.md", + absolutePath, + agentIds: ["main"], + contentType: "markdown" as const, + }, + ]; + + await expect(listActiveMemoryPublicArtifacts({ cfg: {} as never })).resolves.toEqual( + expectedArtifacts, + ); + + // Simulate what resolvePluginWebSearchProviders and similar read-only paths do: + // load plugins again with activate:false. Each per-plugin snapshot/rollback must + // preserve the previously registered memory capability. + loadOpenClawPlugins({ + cache: false, + activate: false, + workspaceDir: memoryPlugin.dir, + config: activateConfig, + }); + + await expect(listActiveMemoryPublicArtifacts({ cfg: {} as never })).resolves.toEqual( + expectedArtifacts, + ); + }); + it("throws when activate:false is used without cache:false", () => { expect(() => loadOpenClawPlugins({ activate: false })).toThrow( "activate:false requires cache:false", diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 2d6df8dd4f4..8a53494e751 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -2253,6 +2253,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi const previousAgentHarnesses = listRegisteredAgentHarnesses(); const previousCompactionProviders = listRegisteredCompactionProviders(); const previousDetachedTaskRuntimeRegistration = getDetachedTaskLifecycleRuntimeRegistration(); + const previousMemoryCapability = getMemoryCapabilityRegistration(); const previousMemoryEmbeddingProviders = listRegisteredMemoryEmbeddingProviders(); const previousMemoryFlushPlanResolver = getMemoryFlushPlanResolver(); const previousMemoryPromptBuilder = getMemoryPromptSectionBuilder(); @@ -2269,6 +2270,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi restoreDetachedTaskLifecycleRuntimeRegistration(previousDetachedTaskRuntimeRegistration); restoreRegisteredMemoryEmbeddingProviders(previousMemoryEmbeddingProviders); restoreMemoryPluginState({ + capability: previousMemoryCapability, corpusSupplements: previousMemoryCorpusSupplements, promptBuilder: previousMemoryPromptBuilder, promptSupplements: previousMemoryPromptSupplements, @@ -2286,6 +2288,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi restoreDetachedTaskLifecycleRuntimeRegistration(previousDetachedTaskRuntimeRegistration); restoreRegisteredMemoryEmbeddingProviders(previousMemoryEmbeddingProviders); restoreMemoryPluginState({ + capability: previousMemoryCapability, corpusSupplements: previousMemoryCorpusSupplements, promptBuilder: previousMemoryPromptBuilder, promptSupplements: previousMemoryPromptSupplements,