diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 0336f2fb14d..75d08a32676 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -1189,6 +1189,73 @@ module.exports = { id: "skipped-scoped-only", register() { throw new Error("skip clearPluginCommands(); }); + it("does not replace the active memory prompt section during non-activating loads", () => { + useNoBundledPlugins(); + registerMemoryPromptSection(() => ["active memory section"]); + const plugin = writePlugin({ + id: "snapshot-memory", + filename: "snapshot-memory.cjs", + body: `module.exports = { + id: "snapshot-memory", + kind: "memory", + register(api) { + api.registerMemoryPromptSection(() => ["snapshot memory section"]); + }, + };`, + }); + + const scoped = loadOpenClawPlugins({ + cache: false, + activate: false, + workspaceDir: plugin.dir, + config: { + plugins: { + load: { paths: [plugin.file] }, + allow: ["snapshot-memory"], + slots: { memory: "snapshot-memory" }, + }, + }, + onlyPluginIds: ["snapshot-memory"], + }); + + expect(scoped.plugins.find((entry) => entry.id === "snapshot-memory")?.status).toBe("loaded"); + expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([ + "active memory section", + ]); + }); + + it("clears a newly-registered memory prompt section when plugin register fails", () => { + useNoBundledPlugins(); + const plugin = writePlugin({ + id: "failing-memory", + filename: "failing-memory.cjs", + body: `module.exports = { + id: "failing-memory", + kind: "memory", + register(api) { + api.registerMemoryPromptSection(() => ["stale failure section"]); + throw new Error("memory register failed"); + }, + };`, + }); + + const registry = loadOpenClawPlugins({ + cache: false, + workspaceDir: plugin.dir, + config: { + plugins: { + load: { paths: [plugin.file] }, + allow: ["failing-memory"], + slots: { memory: "failing-memory" }, + }, + }, + onlyPluginIds: ["failing-memory"], + }); + + expect(registry.plugins.find((entry) => entry.id === "failing-memory")?.status).toBe("error"); + expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([]); + }); + 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 e0d5374a6d8..e0a0d746e1c 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -1252,6 +1252,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi hookPolicy: entry?.hooks, registrationMode, }); + const previousMemoryPromptBuilder = getMemoryPromptSectionBuilder(); try { const result = register(api); @@ -1263,9 +1264,14 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi message: "plugin register returned a promise; async registration is ignored", }); } + // Snapshot loads should not replace process-global runtime prompt state. + if (!shouldActivate) { + restoreMemoryPromptSection(previousMemoryPromptBuilder); + } registry.plugins.push(record); seenIds.set(pluginId, candidate.origin); } catch (err) { + restoreMemoryPromptSection(previousMemoryPromptBuilder); recordPluginError({ logger, registry,