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.
This commit is contained in:
Edward Abrams
2026-04-20 12:43:08 -07:00
committed by GitHub
parent a6aa028626
commit 8595e6c872
3 changed files with 85 additions and 0 deletions

View File

@@ -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.

View File

@@ -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",

View File

@@ -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,