feat(memory): pluggable system prompt section for memory plugins (#40126)

Merged via squash.

Prepared head SHA: 5228d1937f
Co-authored-by: jarimustonen <1272053+jarimustonen@users.noreply.github.com>
Co-authored-by: jalehman <550978+jalehman@users.noreply.github.com>
Reviewed-by: @jalehman
This commit is contained in:
Jari Mustonen
2026-03-21 20:20:45 +02:00
committed by GitHub
parent ebfc5f8240
commit fd2b3ed6af
17 changed files with 319 additions and 31 deletions

View File

@@ -23,12 +23,13 @@ async function importFreshPluginTestModules() {
vi.doUnmock("./hooks.js");
vi.doUnmock("./loader.js");
vi.doUnmock("jiti");
const [loader, hookRunnerGlobal, hooks, runtime, registry] = await Promise.all([
const [loader, hookRunnerGlobal, hooks, runtime, registry, promptSection] = await Promise.all([
import("./loader.js"),
import("./hook-runner-global.js"),
import("./hooks.js"),
import("./runtime.js"),
import("./registry.js"),
import("../memory/prompt-section.js"),
]);
return {
...loader,
@@ -36,11 +37,13 @@ async function importFreshPluginTestModules() {
...hooks,
...runtime,
...registry,
...promptSection,
};
}
const {
__testing,
buildMemoryPromptSection,
clearPluginLoaderCache,
createHookRunner,
createEmptyPluginRegistry,
@@ -48,6 +51,7 @@ const {
getActivePluginRegistryKey,
getGlobalHookRunner,
loadOpenClawPlugins,
registerMemoryPromptSection,
resetGlobalHookRunner,
setActivePluginRegistry,
} = await importFreshPluginTestModules();
@@ -1204,6 +1208,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",
@@ -3787,3 +3858,16 @@ export const runtimeValue = helperValue;`,
expect(resolved).toBe(expected === "dist" ? fixture.distFile : fixture.srcFile);
});
});
describe("clearPluginLoaderCache", () => {
it("resets the registered memory prompt section builder", () => {
registerMemoryPromptSection(() => ["stale memory section"]);
expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([
"stale memory section",
]);
clearPluginLoaderCache();
expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([]);
});
});