mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:40:44 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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") {
|
||||
|
||||
Reference in New Issue
Block a user