mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-09 06:33:56 +00:00
fix(memory-wiki): drop malformed public artifacts and honor readMemoryArtifacts in status (#100900)
Two crashes when a memory plugin misbehaves in bridge mode: - listActiveMemoryPublicArtifacts sorted plugin-returned artifacts without validating them; an artifact missing any of the string fields the comparator dereferences (kind, workspaceDir, relativePath, absolutePath, contentType) crashed wiki status and every other bridge consumer with "Cannot read properties of undefined (reading 'localeCompare')". @mem0/openclaw-mem0 <= 1.0.14 shipped record-shaped artifacts with none of those fields, typed against a drifted SDK stub. Validate the shape, drop malformed entries (and non-array listings), and warn once naming the offending plugin -- the same treatment agentIds already got. - resolveMemoryWikiStatus gated artifact counting on vaultMode/enabled but not bridge.readMemoryArtifacts, so the wiki.status gateway method still enumerated artifacts (and hit the crash above) with the flag off, even though the sync path (bridge.ts) and CLI gateway routing honor it. The documented workaround therefore never worked for the wiki_status agent tool. Add the flag to the gate; the count reports null when imports are disabled, matching non-bridge modes.
This commit is contained in:
@@ -92,6 +92,40 @@ describe("resolveMemoryWikiStatus", () => {
|
||||
expect(status.warnings.map((warning) => warning.code)).toContain("bridge-artifacts-missing");
|
||||
});
|
||||
|
||||
it("skips artifact enumeration when readMemoryArtifacts is disabled", async () => {
|
||||
const config = resolveMemoryWikiConfig(
|
||||
{
|
||||
vaultMode: "bridge",
|
||||
bridge: {
|
||||
enabled: true,
|
||||
readMemoryArtifacts: false,
|
||||
},
|
||||
},
|
||||
{ homedir: "/Users/tester" },
|
||||
);
|
||||
|
||||
let listCalls = 0;
|
||||
const status = await resolveMemoryWikiStatus(config, {
|
||||
appConfig: {
|
||||
agents: {
|
||||
list: [{ id: "main", default: true, workspace: "/tmp/workspace" }],
|
||||
},
|
||||
} as OpenClawConfig,
|
||||
listPublicArtifacts: async () => {
|
||||
listCalls += 1;
|
||||
return [];
|
||||
},
|
||||
pathExists: async () => true,
|
||||
resolveCommand: async () => null,
|
||||
});
|
||||
|
||||
expect(listCalls).toBe(0);
|
||||
expect(status.bridgePublicArtifactCount).toBeNull();
|
||||
expect(status.warnings.map((warning) => warning.code)).not.toContain(
|
||||
"bridge-artifacts-missing",
|
||||
);
|
||||
});
|
||||
|
||||
it("discovers pages in nested subdirectories", async () => {
|
||||
const { rootDir, config } = await createVault({
|
||||
prefix: "memory-wiki-nested-",
|
||||
|
||||
@@ -211,7 +211,10 @@ export async function resolveMemoryWikiStatus(
|
||||
const exists = deps?.pathExists ?? pathExists;
|
||||
const vaultExists = await exists(config.vault.path);
|
||||
const bridgePublicArtifactCount =
|
||||
deps?.appConfig && config.vaultMode === "bridge" && config.bridge.enabled
|
||||
deps?.appConfig &&
|
||||
config.vaultMode === "bridge" &&
|
||||
config.bridge.enabled &&
|
||||
config.bridge.readMemoryArtifacts
|
||||
? (
|
||||
await (deps.listPublicArtifacts ?? listActiveMemoryPublicArtifacts)({
|
||||
cfg: deps.appConfig,
|
||||
|
||||
@@ -234,6 +234,58 @@ describe("memory plugin state", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("drops malformed public memory artifacts instead of crashing the sort", async () => {
|
||||
// Record-shaped artifact as shipped by @mem0/openclaw-mem0 <= 1.0.14 —
|
||||
// none of the file-backed fields the sort dereferences.
|
||||
const recordShapedArtifact = {
|
||||
id: "mem0:memory:1",
|
||||
type: "memory",
|
||||
title: "A memory",
|
||||
content: "memory text",
|
||||
} as unknown as MemoryPluginPublicArtifact;
|
||||
|
||||
registerMemoryCapability("openclaw-mem0", {
|
||||
publicArtifacts: {
|
||||
async listArtifacts() {
|
||||
return [
|
||||
recordShapedArtifact,
|
||||
{
|
||||
kind: "memory-root",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
relativePath: "MEMORY.md",
|
||||
absolutePath: "/tmp/workspace/MEMORY.md",
|
||||
agentIds: ["main"],
|
||||
contentType: "markdown" as const,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(listActiveMemoryPublicArtifacts({ cfg: {} as never })).resolves.toEqual([
|
||||
{
|
||||
kind: "memory-root",
|
||||
workspaceDir: "/tmp/workspace",
|
||||
relativePath: "MEMORY.md",
|
||||
absolutePath: "/tmp/workspace/MEMORY.md",
|
||||
agentIds: ["main"],
|
||||
contentType: "markdown",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("ignores a non-array public artifact listing", async () => {
|
||||
registerMemoryCapability("openclaw-mem0", {
|
||||
publicArtifacts: {
|
||||
async listArtifacts() {
|
||||
return { artifacts: [] } as unknown as MemoryPluginPublicArtifact[];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(listActiveMemoryPublicArtifacts({ cfg: {} as never })).resolves.toEqual([]);
|
||||
});
|
||||
|
||||
it("preserves sidecar runtime fields when a memory plugin adds public artifacts only", async () => {
|
||||
const runtime = createMemoryRuntime();
|
||||
const flushPlanResolver = () => createMemoryFlushPlan("memory/sidecar.md");
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
/** Registry state for plugin memory runtimes, prompt supplements, and flush planning. */
|
||||
import type { MemoryCitationsMode } from "../config/types.memory.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||
import type { MemorySearchManager } from "../memory-host-sdk/host/types.js";
|
||||
|
||||
const log = createSubsystemLogger("plugins/memory-state");
|
||||
|
||||
export type MemoryPromptSectionBuilder = (params: {
|
||||
availableTools: Set<string>;
|
||||
citationsMode?: MemoryCitationsMode;
|
||||
@@ -317,11 +320,36 @@ function cloneMemoryPublicArtifact(
|
||||
};
|
||||
}
|
||||
|
||||
// The sort below dereferences these fields, so a plugin-supplied artifact
|
||||
// missing any of them would crash every status/bridge consumer.
|
||||
function isValidMemoryPublicArtifact(
|
||||
artifact: MemoryPluginPublicArtifact | null | undefined,
|
||||
): artifact is MemoryPluginPublicArtifact {
|
||||
return (
|
||||
typeof artifact?.kind === "string" &&
|
||||
typeof artifact.workspaceDir === "string" &&
|
||||
typeof artifact.relativePath === "string" &&
|
||||
typeof artifact.absolutePath === "string" &&
|
||||
typeof artifact.contentType === "string"
|
||||
);
|
||||
}
|
||||
|
||||
export async function listActiveMemoryPublicArtifacts(params: {
|
||||
cfg: OpenClawConfig;
|
||||
}): Promise<MemoryPluginPublicArtifact[]> {
|
||||
const artifacts =
|
||||
const pluginId = memoryPluginState.capability?.pluginId;
|
||||
const listed =
|
||||
(await memoryPluginState.capability?.capability.publicArtifacts?.listArtifacts(params)) ?? [];
|
||||
if (!Array.isArray(listed)) {
|
||||
log.warn(`ignoring public memory artifacts from plugin "${pluginId}": not an array`);
|
||||
return [];
|
||||
}
|
||||
const artifacts = listed.filter(isValidMemoryPublicArtifact);
|
||||
if (artifacts.length < listed.length) {
|
||||
log.warn(
|
||||
`ignoring ${listed.length - artifacts.length} malformed public memory artifact(s) from plugin "${pluginId}": artifacts must include string kind, workspaceDir, relativePath, absolutePath, and contentType`,
|
||||
);
|
||||
}
|
||||
return artifacts.map(cloneMemoryPublicArtifact).toSorted((left, right) => {
|
||||
const workspaceOrder = left.workspaceDir.localeCompare(right.workspaceDir);
|
||||
if (workspaceOrder !== 0) {
|
||||
|
||||
Reference in New Issue
Block a user