From 5fde14b844019621a32ee1a43d7510b7931fc384 Mon Sep 17 00:00:00 2001 From: pradeep7127 Date: Sun, 12 Apr 2026 19:41:25 +0400 Subject: [PATCH] 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 --- CHANGELOG.md | 1 + src/plugins/loader.test.ts | 167 +++++++++++++++++++++++++++++++++++++ src/plugins/loader.ts | 141 +++++++++++++++++++------------ 3 files changed, 257 insertions(+), 52 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f445db8d10..5fb23444b8a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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//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. diff --git a/src/plugins/loader.test.ts b/src/plugins/loader.test.ts index 93eafabe015..c8ff13cd30f 100644 --- a/src/plugins/loader.test.ts +++ b/src/plugins/loader.test.ts @@ -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) => { + 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) => { + 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) => { + const core = registry.plugins.find((entry) => entry.id === "memory-core"); + expect(core?.status).toBe("disabled"); + }, + }, { label: "disables memory plugins when slot is none", loadRegistry: () => { diff --git a/src/plugins/loader.ts b/src/plugins/loader.ts index 72b348e124e..8e9ce34261b 100644 --- a/src/plugins/loader.ts +++ b/src/plugins/loader.ts @@ -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(); 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") {