mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 00:52:57 +00:00
fix(memory-lancedb): expose public memory artifacts
This commit is contained in:
committed by
Peter Steinberger
parent
6657b493e2
commit
aac1abeaff
@@ -1,96 +1,11 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { resolveMemoryDreamingWorkspaces } from "openclaw/plugin-sdk/memory-core-host-status";
|
||||
import type { MemoryPluginPublicArtifact } from "openclaw/plugin-sdk/memory-host-core";
|
||||
import { resolveMemoryHostEventLogPath } from "openclaw/plugin-sdk/memory-host-events";
|
||||
import { pathExists } from "openclaw/plugin-sdk/security-runtime";
|
||||
import {
|
||||
listMemoryHostPublicArtifacts,
|
||||
type MemoryPluginPublicArtifact,
|
||||
} from "openclaw/plugin-sdk/memory-host-core";
|
||||
import type { OpenClawConfig } from "../api.js";
|
||||
|
||||
async function listMarkdownFilesRecursive(rootDir: string): Promise<string[]> {
|
||||
const entries = await fs.readdir(rootDir, { withFileTypes: true }).catch(() => []);
|
||||
const files: string[] = [];
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(rootDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await listMarkdownFilesRecursive(fullPath)));
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && entry.name.endsWith(".md")) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
async function collectWorkspaceArtifacts(params: {
|
||||
workspaceDir: string;
|
||||
agentIds: string[];
|
||||
}): Promise<MemoryPluginPublicArtifact[]> {
|
||||
const artifacts: MemoryPluginPublicArtifact[] = [];
|
||||
const workspaceEntries = new Set(
|
||||
(await fs.readdir(params.workspaceDir, { withFileTypes: true }).catch(() => []))
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => entry.name),
|
||||
);
|
||||
for (const relativePath of ["MEMORY.md"]) {
|
||||
if (!workspaceEntries.has(relativePath)) {
|
||||
continue;
|
||||
}
|
||||
const absolutePath = path.join(params.workspaceDir, relativePath);
|
||||
artifacts.push({
|
||||
kind: "memory-root",
|
||||
workspaceDir: params.workspaceDir,
|
||||
relativePath,
|
||||
absolutePath,
|
||||
agentIds: [...params.agentIds],
|
||||
contentType: "markdown",
|
||||
});
|
||||
}
|
||||
|
||||
const memoryDir = path.join(params.workspaceDir, "memory");
|
||||
for (const absolutePath of await listMarkdownFilesRecursive(memoryDir)) {
|
||||
const relativePath = path.relative(params.workspaceDir, absolutePath).replace(/\\/g, "/");
|
||||
artifacts.push({
|
||||
kind: relativePath.startsWith("memory/dreaming/") ? "dream-report" : "daily-note",
|
||||
workspaceDir: params.workspaceDir,
|
||||
relativePath,
|
||||
absolutePath,
|
||||
agentIds: [...params.agentIds],
|
||||
contentType: "markdown",
|
||||
});
|
||||
}
|
||||
|
||||
const eventLogPath = resolveMemoryHostEventLogPath(params.workspaceDir);
|
||||
if (await pathExists(eventLogPath)) {
|
||||
artifacts.push({
|
||||
kind: "event-log",
|
||||
workspaceDir: params.workspaceDir,
|
||||
relativePath: path.relative(params.workspaceDir, eventLogPath).replace(/\\/g, "/"),
|
||||
absolutePath: eventLogPath,
|
||||
agentIds: [...params.agentIds],
|
||||
contentType: "json",
|
||||
});
|
||||
}
|
||||
|
||||
const deduped = new Map<string, MemoryPluginPublicArtifact>();
|
||||
for (const artifact of artifacts) {
|
||||
deduped.set(`${artifact.workspaceDir}\0${artifact.relativePath}\0${artifact.kind}`, artifact);
|
||||
}
|
||||
return [...deduped.values()];
|
||||
}
|
||||
|
||||
export async function listMemoryCorePublicArtifacts(params: {
|
||||
cfg: OpenClawConfig;
|
||||
}): Promise<MemoryPluginPublicArtifact[]> {
|
||||
const workspaces = resolveMemoryDreamingWorkspaces(params.cfg);
|
||||
const artifacts: MemoryPluginPublicArtifact[] = [];
|
||||
for (const workspace of workspaces) {
|
||||
artifacts.push(
|
||||
...(await collectWorkspaceArtifacts({
|
||||
workspaceDir: workspace.workspaceDir,
|
||||
agentIds: workspace.agentIds,
|
||||
})),
|
||||
);
|
||||
}
|
||||
return artifacts;
|
||||
return await listMemoryHostPublicArtifacts(params);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,16 @@
|
||||
*/
|
||||
|
||||
import { Buffer } from "node:buffer";
|
||||
import { describe, test, expect, vi } from "vitest";
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import {
|
||||
clearMemoryPluginState,
|
||||
getMemoryCapabilityRegistration,
|
||||
listActiveMemoryPublicArtifacts,
|
||||
registerMemoryCapability,
|
||||
type MemoryPluginCapability,
|
||||
} from "openclaw/plugin-sdk/memory-host-core";
|
||||
import { afterEach, describe, test, expect, vi } from "vitest";
|
||||
import memoryPlugin, {
|
||||
detectCategory,
|
||||
formatRelevantMemoriesContext,
|
||||
@@ -164,7 +173,11 @@ async function withMockedOpenAiMemoryPlugin<T>(params: {
|
||||
}
|
||||
|
||||
describe("memory plugin e2e", () => {
|
||||
const { getDbPath } = installTmpDirHarness({ prefix: "openclaw-memory-test-" });
|
||||
const { getDbPath, getTmpDir } = installTmpDirHarness({ prefix: "openclaw-memory-test-" });
|
||||
|
||||
afterEach(() => {
|
||||
clearMemoryPluginState();
|
||||
});
|
||||
|
||||
function parseConfig(overrides: Record<string, unknown> = {}) {
|
||||
return memoryPlugin.configSchema?.parse?.({
|
||||
@@ -340,6 +353,165 @@ describe("memory plugin e2e", () => {
|
||||
expectHookNotRegistered(on, "before_agent_start");
|
||||
});
|
||||
|
||||
test("registers memory public artifact provider for memory-wiki bridge parity", async () => {
|
||||
const workspaceDir = path.join(getTmpDir(), "workspace-public-artifacts");
|
||||
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
|
||||
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "# Durable Memory\n", "utf8");
|
||||
await fs.writeFile(path.join(workspaceDir, "memory", "2026-05-18.md"), "# Daily\n", "utf8");
|
||||
const registerMemoryCapability = vi.fn();
|
||||
const mockApi = {
|
||||
id: "memory-lancedb",
|
||||
name: "Memory (LanceDB)",
|
||||
source: "test",
|
||||
config: {},
|
||||
pluginConfig: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
autoCapture: false,
|
||||
autoRecall: false,
|
||||
},
|
||||
runtime: {},
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
registerMemoryCapability,
|
||||
registerTool: vi.fn(),
|
||||
registerCli: vi.fn(),
|
||||
registerService: vi.fn(),
|
||||
on: vi.fn(),
|
||||
resolvePath: (filePath: string) => filePath,
|
||||
};
|
||||
|
||||
memoryPlugin.register(mockApi as any);
|
||||
const capability = firstObjectArg(
|
||||
registerMemoryCapability as unknown as MockCallSource,
|
||||
"memory capability",
|
||||
);
|
||||
const publicArtifacts = capability.publicArtifacts as
|
||||
| { listArtifacts?: (params: { cfg: unknown }) => Promise<unknown> }
|
||||
| undefined;
|
||||
expect(publicArtifacts?.listArtifacts).toBeTypeOf("function");
|
||||
|
||||
await expect(
|
||||
publicArtifacts?.listArtifacts?.({
|
||||
cfg: {
|
||||
agents: {
|
||||
list: [{ id: "main", default: true, workspace: workspaceDir }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
kind: "memory-root",
|
||||
workspaceDir,
|
||||
relativePath: "MEMORY.md",
|
||||
absolutePath: path.join(workspaceDir, "MEMORY.md"),
|
||||
agentIds: ["main"],
|
||||
contentType: "markdown",
|
||||
},
|
||||
{
|
||||
kind: "daily-note",
|
||||
workspaceDir,
|
||||
relativePath: "memory/2026-05-18.md",
|
||||
absolutePath: path.join(workspaceDir, "memory", "2026-05-18.md"),
|
||||
agentIds: ["main"],
|
||||
contentType: "markdown",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("preserves memory-core sidecar capability when registering public artifacts", async () => {
|
||||
const workspaceDir = path.join(getTmpDir(), "workspace-sidecar-public-artifacts");
|
||||
await fs.mkdir(path.join(workspaceDir, "memory"), { recursive: true });
|
||||
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "# Durable Memory\n", "utf8");
|
||||
await fs.writeFile(path.join(workspaceDir, "memory", "2026-05-18.md"), "# Daily\n", "utf8");
|
||||
const runtime = {
|
||||
async getMemorySearchManager() {
|
||||
return { manager: null, error: "test" };
|
||||
},
|
||||
resolveMemoryBackendConfig() {
|
||||
return { backend: "builtin" as const };
|
||||
},
|
||||
};
|
||||
const flushPlanResolver = vi.fn(() => ({
|
||||
softThresholdTokens: 1,
|
||||
forceFlushTranscriptBytes: 2,
|
||||
reserveTokensFloor: 3,
|
||||
prompt: "flush",
|
||||
systemPrompt: "flush",
|
||||
relativePath: "memory/sidecar.md",
|
||||
}));
|
||||
registerMemoryCapability("memory-core", {
|
||||
flushPlanResolver,
|
||||
runtime,
|
||||
});
|
||||
const registerMemoryCapabilityForPlugin = vi.fn((capability: MemoryPluginCapability) => {
|
||||
registerMemoryCapability("memory-lancedb", capability);
|
||||
});
|
||||
const mockApi = {
|
||||
id: "memory-lancedb",
|
||||
name: "Memory (LanceDB)",
|
||||
source: "test",
|
||||
config: {},
|
||||
pluginConfig: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
autoCapture: false,
|
||||
autoRecall: false,
|
||||
},
|
||||
runtime: {},
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
registerMemoryCapability: registerMemoryCapabilityForPlugin,
|
||||
registerTool: vi.fn(),
|
||||
registerCli: vi.fn(),
|
||||
registerService: vi.fn(),
|
||||
on: vi.fn(),
|
||||
resolvePath: (filePath: string) => filePath,
|
||||
};
|
||||
|
||||
memoryPlugin.register(mockApi as any);
|
||||
|
||||
expect(registerMemoryCapabilityForPlugin).toHaveBeenCalledOnce();
|
||||
expect(
|
||||
getMemoryCapabilityRegistration()?.capability.flushPlanResolver?.({})?.relativePath,
|
||||
).toBe("memory/sidecar.md");
|
||||
expect(getMemoryCapabilityRegistration()?.capability.runtime).toBe(runtime);
|
||||
await expect(
|
||||
listActiveMemoryPublicArtifacts({
|
||||
cfg: {
|
||||
agents: {
|
||||
list: [{ id: "main", default: true, workspace: workspaceDir }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toMatchObject([
|
||||
{
|
||||
kind: "memory-root",
|
||||
workspaceDir,
|
||||
relativePath: "MEMORY.md",
|
||||
},
|
||||
{
|
||||
kind: "daily-note",
|
||||
workspaceDir,
|
||||
relativePath: "memory/2026-05-18.md",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("uses provider adapter auth when embedding apiKey is omitted", async () => {
|
||||
const embedQuery = vi.fn(async () => [0.1, 0.2, 0.3]);
|
||||
const createProvider = vi.fn(async (options: Record<string, unknown>) => ({
|
||||
|
||||
@@ -11,6 +11,7 @@ import { randomUUID } from "node:crypto";
|
||||
import type * as LanceDB from "@lancedb/lancedb";
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts";
|
||||
import type { MemoryEmbeddingProvider } from "openclaw/plugin-sdk/memory-core-host-engine-embeddings";
|
||||
import { getMemoryCapabilityRegistration } from "openclaw/plugin-sdk/memory-host-core";
|
||||
import { resolveLivePluginConfigObject } from "openclaw/plugin-sdk/plugin-config-runtime";
|
||||
import { ensureGlobalUndiciEnvProxyDispatcher } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
@@ -694,6 +695,16 @@ export default definePluginEntry({
|
||||
};
|
||||
|
||||
api.logger.info(`memory-lancedb: plugin registered (db: ${resolvedDbPath}, lazy init)`);
|
||||
const existingMemoryCapability = getMemoryCapabilityRegistration()?.capability;
|
||||
api.registerMemoryCapability?.({
|
||||
...existingMemoryCapability,
|
||||
publicArtifacts: {
|
||||
async listArtifacts(params) {
|
||||
const { listMemoryHostPublicArtifacts } = await loadMemoryHostCoreModule();
|
||||
return await listMemoryHostPublicArtifacts(params);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// ========================================================================
|
||||
// Tools
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
import fs from "node:fs/promises";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import {
|
||||
clearMemoryPluginState,
|
||||
@@ -7,8 +10,10 @@ import {
|
||||
import * as memoryCoreAlias from "./memory-core.js";
|
||||
import {
|
||||
buildActiveMemoryPromptSection,
|
||||
listMemoryHostPublicArtifacts,
|
||||
listActiveMemoryPublicArtifacts,
|
||||
} from "./memory-host-core.js";
|
||||
import { appendMemoryHostEvent, resolveMemoryHostEventLogPath } from "./memory-host-events.js";
|
||||
|
||||
describe("memory-host-core helpers", () => {
|
||||
afterEach(() => {
|
||||
@@ -60,6 +65,77 @@ describe("memory-host-core helpers", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("lists shared public artifacts from memory workspaces", async () => {
|
||||
const fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), "memory-host-public-artifacts-"));
|
||||
try {
|
||||
const workspaceDir = path.join(fixtureRoot, "workspace");
|
||||
await fs.mkdir(path.join(workspaceDir, "memory", "dreaming"), { recursive: true });
|
||||
await fs.writeFile(path.join(workspaceDir, "MEMORY.md"), "# Durable Memory\n", "utf8");
|
||||
await fs.writeFile(
|
||||
path.join(workspaceDir, "memory", "2026-05-18.md"),
|
||||
"# Daily Note\n",
|
||||
"utf8",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(workspaceDir, "memory", "dreaming", "2026-05-18.md"),
|
||||
"# Dream Report\n",
|
||||
"utf8",
|
||||
);
|
||||
await appendMemoryHostEvent(workspaceDir, {
|
||||
type: "memory.recall.recorded",
|
||||
timestamp: "2026-05-18T12:00:00.000Z",
|
||||
query: "bridge",
|
||||
resultCount: 0,
|
||||
results: [],
|
||||
});
|
||||
|
||||
await expect(
|
||||
listMemoryHostPublicArtifacts({
|
||||
cfg: {
|
||||
agents: {
|
||||
list: [{ id: "main", default: true, workspace: workspaceDir }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual([
|
||||
{
|
||||
kind: "memory-root",
|
||||
workspaceDir,
|
||||
relativePath: "MEMORY.md",
|
||||
absolutePath: path.join(workspaceDir, "MEMORY.md"),
|
||||
agentIds: ["main"],
|
||||
contentType: "markdown",
|
||||
},
|
||||
{
|
||||
kind: "daily-note",
|
||||
workspaceDir,
|
||||
relativePath: "memory/2026-05-18.md",
|
||||
absolutePath: path.join(workspaceDir, "memory", "2026-05-18.md"),
|
||||
agentIds: ["main"],
|
||||
contentType: "markdown",
|
||||
},
|
||||
{
|
||||
kind: "dream-report",
|
||||
workspaceDir,
|
||||
relativePath: "memory/dreaming/2026-05-18.md",
|
||||
absolutePath: path.join(workspaceDir, "memory", "dreaming", "2026-05-18.md"),
|
||||
agentIds: ["main"],
|
||||
contentType: "markdown",
|
||||
},
|
||||
{
|
||||
kind: "event-log",
|
||||
workspaceDir,
|
||||
relativePath: "memory/.dreams/events.jsonl",
|
||||
absolutePath: resolveMemoryHostEventLogPath(workspaceDir),
|
||||
agentIds: ["main"],
|
||||
contentType: "json",
|
||||
},
|
||||
]);
|
||||
} finally {
|
||||
await fs.rm(fixtureRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps the deprecated memory-core alias wired to memory-host-core", () => {
|
||||
expect(memoryCoreAlias.buildActiveMemoryPromptSection).toBe(buildActiveMemoryPromptSection);
|
||||
expect(memoryCoreAlias.listActiveMemoryPublicArtifacts).toBe(listActiveMemoryPublicArtifacts);
|
||||
|
||||
@@ -1 +1,104 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import type { MemoryPluginPublicArtifact } from "../plugins/memory-state.js";
|
||||
import { resolveMemoryDreamingWorkspaces } from "./memory-core-host-status.js";
|
||||
import { resolveMemoryHostEventLogPath } from "./memory-host-events.js";
|
||||
|
||||
export * from "./memory-core-host-runtime-core.js";
|
||||
|
||||
async function pathExists(filePath: string): Promise<boolean> {
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function listMarkdownFilesRecursive(rootDir: string): Promise<string[]> {
|
||||
const entries = await fs.readdir(rootDir, { withFileTypes: true }).catch(() => []);
|
||||
const files: string[] = [];
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(rootDir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
files.push(...(await listMarkdownFilesRecursive(fullPath)));
|
||||
continue;
|
||||
}
|
||||
if (entry.isFile() && entry.name.endsWith(".md")) {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
return files.toSorted((left, right) => left.localeCompare(right));
|
||||
}
|
||||
|
||||
export async function listMemoryWorkspacePublicArtifacts(params: {
|
||||
workspaceDir: string;
|
||||
agentIds: string[];
|
||||
}): Promise<MemoryPluginPublicArtifact[]> {
|
||||
const artifacts: MemoryPluginPublicArtifact[] = [];
|
||||
const workspaceEntries = new Set(
|
||||
(await fs.readdir(params.workspaceDir, { withFileTypes: true }).catch(() => []))
|
||||
.filter((entry) => entry.isFile())
|
||||
.map((entry) => entry.name),
|
||||
);
|
||||
|
||||
if (workspaceEntries.has("MEMORY.md")) {
|
||||
const absolutePath = path.join(params.workspaceDir, "MEMORY.md");
|
||||
artifacts.push({
|
||||
kind: "memory-root",
|
||||
workspaceDir: params.workspaceDir,
|
||||
relativePath: "MEMORY.md",
|
||||
absolutePath,
|
||||
agentIds: [...params.agentIds],
|
||||
contentType: "markdown",
|
||||
});
|
||||
}
|
||||
|
||||
const memoryDir = path.join(params.workspaceDir, "memory");
|
||||
for (const absolutePath of await listMarkdownFilesRecursive(memoryDir)) {
|
||||
const relativePath = path.relative(params.workspaceDir, absolutePath).replace(/\\/g, "/");
|
||||
artifacts.push({
|
||||
kind: relativePath.startsWith("memory/dreaming/") ? "dream-report" : "daily-note",
|
||||
workspaceDir: params.workspaceDir,
|
||||
relativePath,
|
||||
absolutePath,
|
||||
agentIds: [...params.agentIds],
|
||||
contentType: "markdown",
|
||||
});
|
||||
}
|
||||
|
||||
const eventLogPath = resolveMemoryHostEventLogPath(params.workspaceDir);
|
||||
if (await pathExists(eventLogPath)) {
|
||||
artifacts.push({
|
||||
kind: "event-log",
|
||||
workspaceDir: params.workspaceDir,
|
||||
relativePath: path.relative(params.workspaceDir, eventLogPath).replace(/\\/g, "/"),
|
||||
absolutePath: eventLogPath,
|
||||
agentIds: [...params.agentIds],
|
||||
contentType: "json",
|
||||
});
|
||||
}
|
||||
|
||||
const deduped = new Map<string, MemoryPluginPublicArtifact>();
|
||||
for (const artifact of artifacts) {
|
||||
deduped.set(`${artifact.workspaceDir}\0${artifact.relativePath}\0${artifact.kind}`, artifact);
|
||||
}
|
||||
return [...deduped.values()];
|
||||
}
|
||||
|
||||
export async function listMemoryHostPublicArtifacts(params: {
|
||||
cfg: OpenClawConfig;
|
||||
}): Promise<MemoryPluginPublicArtifact[]> {
|
||||
const workspaces = resolveMemoryDreamingWorkspaces(params.cfg);
|
||||
const artifacts: MemoryPluginPublicArtifact[] = [];
|
||||
for (const workspace of workspaces) {
|
||||
artifacts.push(
|
||||
...(await listMemoryWorkspacePublicArtifacts({
|
||||
workspaceDir: workspace.workspaceDir,
|
||||
agentIds: workspace.agentIds,
|
||||
})),
|
||||
);
|
||||
}
|
||||
return artifacts;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user