fix(plugins): exempt dreaming engine from memory slot fast-path in loader (#65411)

* fix(plugins): exempt dreaming engine from memory slot fast-path in loader

* fix(plugins): handle dreaming engine as slot + add tests for slot coexistence

* fix(plugins): narrow dreaming sidecar loading

* Update CHANGELOG.md

* Update CHANGELOG.md

---------

Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
pradeep7127
2026-04-12 19:41:25 +04:00
committed by GitHub
parent 559de69488
commit 5fde14b844
3 changed files with 257 additions and 52 deletions

View File

@@ -13,6 +13,7 @@ Docs: https://docs.openclaw.ai
- Matrix/mentions: keep room mention gating strict while accepting visible `@displayName` Matrix URI labels, so `requireMention` works for non-OpenClaw Matrix clients again. (#64796) Thanks @hclsys.
- Doctor: warn when on-disk agent directories still exist under `~/.openclaw/agents/<id>/agent` but the matching `agents.list[]` entries are missing from config. (#65113) Thanks @neeravmakwana.
- Telegram: route approval button callback queries onto a separate sequentializer lane so plugin approval clicks can resolve immediately instead of deadlocking behind the blocked agent turn. (#64979) Thanks @nk3750.
- Plugins/memory-core dreaming: keep bundled `memory-core` loaded alongside an explicit external memory slot owner only when that owner enables dreaming, while preserving `plugins.slots.memory = "none"` disable semantics. (#65411) Thanks @pradeep7127 and @vincentkoc.
- Agents/Anthropic replay: preserve immutable signed-thinking replay safety across stored and live reruns, keep non-thinking embedded `tool_result` user blocks intact, and drop conflicting preserved tool IDs before validation so retries stop degrading into omitted tool calls. (#65126) Thanks @shakkernerd.
- Telegram/direct sessions: keep commentary-only assistant fallback payloads out of visible direct delivery, so Codex planning chatter cannot leak into Telegram DMs when a run has no `final_answer` text. (#65112) Thanks @vincentkoc.
- Infra/net: fix multipart FormData fields (including `model`) being silently dropped when a guarded runtime fetch body crosses a FormData implementation boundary, restoring OpenAI audio transcription requests that failed with HTTP 400. (#64349) Thanks @petr-sloup.

View File

@@ -3441,6 +3441,173 @@ module.exports = {
expect(b?.status).toBe("loaded");
},
},
{
label:
"loads dreaming engine alongside a different memory slot plugin when dreaming is enabled",
loadRegistry: () => {
const bundledDir = makeTempDir();
const memoryCoreDir = path.join(bundledDir, "memory-core");
const memoryLanceDir = path.join(bundledDir, "memory-lancedb");
mkdirSafe(memoryCoreDir);
mkdirSafe(memoryLanceDir);
writePlugin({
id: "memory-core",
dir: memoryCoreDir,
filename: "index.cjs",
body: memoryPluginBody("memory-core"),
});
writePlugin({
id: "memory-lancedb",
dir: memoryLanceDir,
filename: "index.cjs",
body: memoryPluginBody("memory-lancedb"),
});
const openSchema = { type: "object", additionalProperties: true };
fs.writeFileSync(
path.join(memoryCoreDir, "openclaw.plugin.json"),
JSON.stringify(
{ id: "memory-core", kind: "memory", configSchema: EMPTY_PLUGIN_SCHEMA },
null,
2,
),
"utf-8",
);
fs.writeFileSync(
path.join(memoryLanceDir, "openclaw.plugin.json"),
JSON.stringify(
{ id: "memory-lancedb", kind: "memory", configSchema: openSchema },
null,
2,
),
"utf-8",
);
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;
return loadOpenClawPlugins({
cache: false,
config: {
plugins: {
allow: ["memory-core", "memory-lancedb"],
slots: { memory: "memory-lancedb" },
entries: {
"memory-core": { enabled: true },
"memory-lancedb": { enabled: true, config: { dreaming: { enabled: true } } },
},
},
},
});
},
assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
const core = registry.plugins.find((entry) => entry.id === "memory-core");
const lance = registry.plugins.find((entry) => entry.id === "memory-lancedb");
expect(core?.status).toBe("loaded");
expect(lance?.status).toBe("loaded");
expect(lance?.memorySlotSelected).toBe(true);
expect(core?.memorySlotSelected).toBeFalsy();
},
},
{
label: "excludes dreaming engine when dreaming is disabled and it is not the slot",
loadRegistry: () => {
const bundledDir = makeTempDir();
const memoryCoreDir = path.join(bundledDir, "memory-core");
const memoryLanceDir = path.join(bundledDir, "memory-lancedb");
mkdirSafe(memoryCoreDir);
mkdirSafe(memoryLanceDir);
writePlugin({
id: "memory-core",
dir: memoryCoreDir,
filename: "index.cjs",
body: `throw new Error("memory-core should not load when dreaming is disabled");`,
});
writePlugin({
id: "memory-lancedb",
dir: memoryLanceDir,
filename: "index.cjs",
body: memoryPluginBody("memory-lancedb"),
});
fs.writeFileSync(
path.join(memoryCoreDir, "openclaw.plugin.json"),
JSON.stringify(
{ id: "memory-core", kind: "memory", configSchema: EMPTY_PLUGIN_SCHEMA },
null,
2,
),
"utf-8",
);
fs.writeFileSync(
path.join(memoryLanceDir, "openclaw.plugin.json"),
JSON.stringify(
{ id: "memory-lancedb", kind: "memory", configSchema: EMPTY_PLUGIN_SCHEMA },
null,
2,
),
"utf-8",
);
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;
return loadOpenClawPlugins({
cache: false,
config: {
plugins: {
allow: ["memory-core", "memory-lancedb"],
slots: { memory: "memory-lancedb" },
entries: {
"memory-core": { enabled: true },
"memory-lancedb": { enabled: true },
},
},
},
});
},
assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
const core = registry.plugins.find((entry) => entry.id === "memory-core");
const lance = registry.plugins.find((entry) => entry.id === "memory-lancedb");
expect(core?.status).toBe("disabled");
expect(lance?.status).toBe("loaded");
},
},
{
label: 'keeps memory slot "none" disabled even with stale memory-core dreaming config',
loadRegistry: () => {
const bundledDir = makeTempDir();
const memoryCoreDir = path.join(bundledDir, "memory-core");
mkdirSafe(memoryCoreDir);
writePlugin({
id: "memory-core",
dir: memoryCoreDir,
filename: "index.cjs",
body: `throw new Error("memory-core should not load when memory slot is none");`,
});
fs.writeFileSync(
path.join(memoryCoreDir, "openclaw.plugin.json"),
JSON.stringify(
{ id: "memory-core", kind: "memory", configSchema: EMPTY_PLUGIN_SCHEMA },
null,
2,
),
"utf-8",
);
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = bundledDir;
return loadOpenClawPlugins({
cache: false,
config: {
plugins: {
allow: ["memory-core"],
slots: { memory: "none" },
entries: {
"memory-core": { enabled: true, config: { dreaming: { enabled: true } } },
},
},
},
});
},
assert: (registry: ReturnType<typeof loadOpenClawPlugins>) => {
const core = registry.plugins.find((entry) => entry.id === "memory-core");
expect(core?.status).toBe("disabled");
},
},
{
label: "disables memory plugins when slot is none",
loadRegistry: () => {

View File

@@ -14,7 +14,15 @@ import type { PluginInstallRecord } from "../config/types.plugins.js";
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
import { createSubsystemLogger } from "../logging/subsystem.js";
import { normalizeOptionalString } from "../shared/string-coerce.js";
import {
DEFAULT_MEMORY_DREAMING_PLUGIN_ID,
resolveMemoryDreamingConfig,
resolveMemoryDreamingPluginConfig,
} from "../memory-host-sdk/dreaming.js";
import {
normalizeLowercaseStringOrEmpty,
normalizeOptionalString,
} from "../shared/string-coerce.js";
import { resolveUserPath } from "../utils.js";
import { buildPluginApi } from "./api-builder.js";
import { inspectBundleMcpRuntimeSupport } from "./bundle-mcp.js";
@@ -127,6 +135,25 @@ const CLI_METADATA_ENTRY_BASENAMES = [
"cli-metadata.cjs",
] as const;
function resolveDreamingSidecarEngineId(params: {
cfg: OpenClawConfig;
memorySlot: string | null | undefined;
}): string | null {
const normalizedMemorySlot = normalizeLowercaseStringOrEmpty(params.memorySlot);
if (
!normalizedMemorySlot ||
normalizedMemorySlot === "none" ||
normalizedMemorySlot === DEFAULT_MEMORY_DREAMING_PLUGIN_ID
) {
return null;
}
const dreamingConfig = resolveMemoryDreamingConfig({
pluginConfig: resolveMemoryDreamingPluginConfig(params.cfg),
cfg: params.cfg,
});
return dreamingConfig.enabled ? DEFAULT_MEMORY_DREAMING_PLUGIN_ID : null;
}
export class PluginLoadFailureError extends Error {
readonly pluginIds: string[];
readonly registry: PluginRegistry;
@@ -1286,6 +1313,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
const memorySlot = normalized.slots.memory;
let selectedMemoryPluginId: string | null = null;
let memorySlotMatched = false;
const dreamingEngineId = resolveDreamingSidecarEngineId({ cfg, memorySlot });
for (const candidate of orderedCandidates) {
const manifestRecord = manifestByRoot.get(candidate.rootDir);
@@ -1477,25 +1505,29 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
}
// Fast-path bundled memory plugins that are guaranteed disabled by slot policy.
// This avoids opening/importing heavy memory plugin modules that will never register.
// Exception: the dreaming engine (memory-core by default) must load alongside the
// selected memory slot plugin so dreaming can run even when lancedb holds the slot.
if (
registrationMode === "full" &&
candidate.origin === "bundled" &&
hasKind(manifestRecord.kind, "memory")
) {
const earlyMemoryDecision = resolveMemorySlotDecision({
id: record.id,
kind: manifestRecord.kind,
slot: memorySlot,
selectedId: selectedMemoryPluginId,
});
if (!earlyMemoryDecision.enabled) {
record.enabled = false;
record.status = "disabled";
record.error = earlyMemoryDecision.reason;
markPluginActivationDisabled(record, earlyMemoryDecision.reason);
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
continue;
if (pluginId !== dreamingEngineId) {
const earlyMemoryDecision = resolveMemorySlotDecision({
id: record.id,
kind: manifestRecord.kind,
slot: memorySlot,
selectedId: selectedMemoryPluginId,
});
if (!earlyMemoryDecision.enabled) {
record.enabled = false;
record.status = "disabled";
record.error = earlyMemoryDecision.reason;
markPluginActivationDisabled(record, earlyMemoryDecision.reason);
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
continue;
}
}
}
@@ -1512,7 +1544,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
selectedId: selectedMemoryPluginId,
});
if (!memoryDecision.enabled) {
if (!memoryDecision.enabled && pluginId !== dreamingEngineId) {
record.enabled = false;
record.status = "disabled";
record.error = memoryDecision.reason;
@@ -1653,26 +1685,28 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
}
if (registrationMode === "full") {
const memoryDecision = resolveMemorySlotDecision({
id: record.id,
kind: record.kind,
slot: memorySlot,
selectedId: selectedMemoryPluginId,
});
if (pluginId !== dreamingEngineId) {
const memoryDecision = resolveMemorySlotDecision({
id: record.id,
kind: record.kind,
slot: memorySlot,
selectedId: selectedMemoryPluginId,
});
if (!memoryDecision.enabled) {
record.enabled = false;
record.status = "disabled";
record.error = memoryDecision.reason;
markPluginActivationDisabled(record, memoryDecision.reason);
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
continue;
}
if (!memoryDecision.enabled) {
record.enabled = false;
record.status = "disabled";
record.error = memoryDecision.reason;
markPluginActivationDisabled(record, memoryDecision.reason);
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
continue;
}
if (memoryDecision.selected && hasKind(record.kind, "memory")) {
selectedMemoryPluginId = record.id;
record.memorySlotSelected = true;
if (memoryDecision.selected && hasKind(record.kind, "memory")) {
selectedMemoryPluginId = record.id;
record.memorySlotSelected = true;
}
}
}
@@ -1889,6 +1923,7 @@ export async function loadOpenClawPluginCliRegistry(
const seenIds = new Map<string, PluginRecord["origin"]>();
const memorySlot = normalized.slots.memory;
let selectedMemoryPluginId: string | null = null;
const dreamingEngineId = resolveDreamingSidecarEngineId({ cfg, memorySlot });
for (const candidate of orderedCandidates) {
const manifestRecord = manifestByRoot.get(candidate.rootDir);
@@ -2089,24 +2124,26 @@ export async function loadOpenClawPluginCliRegistry(
}
record.kind = definition?.kind ?? record.kind;
const memoryDecision = resolveMemorySlotDecision({
id: record.id,
kind: record.kind,
slot: memorySlot,
selectedId: selectedMemoryPluginId,
});
if (!memoryDecision.enabled) {
record.enabled = false;
record.status = "disabled";
record.error = memoryDecision.reason;
markPluginActivationDisabled(record, memoryDecision.reason);
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
continue;
}
if (memoryDecision.selected && hasKind(record.kind, "memory")) {
selectedMemoryPluginId = record.id;
record.memorySlotSelected = true;
if (pluginId !== dreamingEngineId) {
const memoryDecision = resolveMemorySlotDecision({
id: record.id,
kind: record.kind,
slot: memorySlot,
selectedId: selectedMemoryPluginId,
});
if (!memoryDecision.enabled) {
record.enabled = false;
record.status = "disabled";
record.error = memoryDecision.reason;
markPluginActivationDisabled(record, memoryDecision.reason);
registry.plugins.push(record);
seenIds.set(pluginId, candidate.origin);
continue;
}
if (memoryDecision.selected && hasKind(record.kind, "memory")) {
selectedMemoryPluginId = record.id;
record.memorySlotSelected = true;
}
}
if (typeof register !== "function") {