mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-18 12:41:12 +00:00
feat(memory-wiki): add belief-layer digests and compat migration
This commit is contained in:
@@ -45,6 +45,7 @@ export type BuildPluginApiParams = {
|
||||
| "onConversationBindingResolved"
|
||||
| "registerCommand"
|
||||
| "registerContextEngine"
|
||||
| "registerMemoryCapability"
|
||||
| "registerMemoryPromptSection"
|
||||
| "registerMemoryPromptSupplement"
|
||||
| "registerMemoryCorpusSupplement"
|
||||
@@ -91,6 +92,7 @@ const noopOnConversationBindingResolved: OpenClawPluginApi["onConversationBindin
|
||||
() => {};
|
||||
const noopRegisterCommand: OpenClawPluginApi["registerCommand"] = () => {};
|
||||
const noopRegisterContextEngine: OpenClawPluginApi["registerContextEngine"] = () => {};
|
||||
const noopRegisterMemoryCapability: OpenClawPluginApi["registerMemoryCapability"] = () => {};
|
||||
const noopRegisterMemoryPromptSection: OpenClawPluginApi["registerMemoryPromptSection"] = () => {};
|
||||
const noopRegisterMemoryPromptSupplement: OpenClawPluginApi["registerMemoryPromptSupplement"] =
|
||||
() => {};
|
||||
@@ -152,6 +154,7 @@ export function buildPluginApi(params: BuildPluginApiParams): OpenClawPluginApi
|
||||
handlers.onConversationBindingResolved ?? noopOnConversationBindingResolved,
|
||||
registerCommand: handlers.registerCommand ?? noopRegisterCommand,
|
||||
registerContextEngine: handlers.registerContextEngine ?? noopRegisterContextEngine,
|
||||
registerMemoryCapability: handlers.registerMemoryCapability ?? noopRegisterMemoryCapability,
|
||||
registerMemoryPromptSection:
|
||||
handlers.registerMemoryPromptSection ?? noopRegisterMemoryPromptSection,
|
||||
registerMemoryPromptSupplement:
|
||||
|
||||
@@ -3,11 +3,15 @@ import {
|
||||
_resetMemoryPluginState,
|
||||
buildMemoryPromptSection,
|
||||
clearMemoryPluginState,
|
||||
getMemoryCapabilityRegistration,
|
||||
getMemoryFlushPlanResolver,
|
||||
getMemoryPromptSectionBuilder,
|
||||
getMemoryRuntime,
|
||||
hasMemoryRuntime,
|
||||
listMemoryCorpusSupplements,
|
||||
listMemoryPromptSupplements,
|
||||
listActiveMemoryPublicArtifacts,
|
||||
registerMemoryCapability,
|
||||
registerMemoryCorpusSupplement,
|
||||
registerMemoryFlushPlanResolver,
|
||||
registerMemoryPromptSupplement,
|
||||
@@ -48,6 +52,7 @@ function expectClearedMemoryState() {
|
||||
|
||||
function createMemoryStateSnapshot() {
|
||||
return {
|
||||
capability: getMemoryCapabilityRegistration(),
|
||||
corpusSupplements: listMemoryCorpusSupplements(),
|
||||
promptBuilder: getMemoryPromptSectionBuilder(),
|
||||
promptSupplements: listMemoryPromptSupplements(),
|
||||
@@ -97,6 +102,85 @@ describe("memory plugin state", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("prefers the registered memory capability over legacy split state", async () => {
|
||||
const runtime = createMemoryRuntime();
|
||||
|
||||
registerMemoryPromptSection(() => ["legacy prompt"]);
|
||||
registerMemoryFlushPlanResolver(() => createMemoryFlushPlan("memory/legacy.md"));
|
||||
registerMemoryRuntime({
|
||||
async getMemorySearchManager() {
|
||||
return { manager: null, error: "legacy" };
|
||||
},
|
||||
resolveMemoryBackendConfig() {
|
||||
return { backend: "builtin" as const };
|
||||
},
|
||||
});
|
||||
registerMemoryCapability("memory-core", {
|
||||
promptBuilder: () => ["capability prompt"],
|
||||
flushPlanResolver: () => createMemoryFlushPlan("memory/capability.md"),
|
||||
runtime,
|
||||
});
|
||||
|
||||
expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual(["capability prompt"]);
|
||||
expect(resolveMemoryFlushPlan({})?.relativePath).toBe("memory/capability.md");
|
||||
await expect(
|
||||
getMemoryRuntime()?.getMemorySearchManager({
|
||||
cfg: {} as never,
|
||||
agentId: "main",
|
||||
}),
|
||||
).resolves.toEqual({ manager: null, error: "missing" });
|
||||
expect(hasMemoryRuntime()).toBe(true);
|
||||
expect(getMemoryCapabilityRegistration()).toMatchObject({
|
||||
pluginId: "memory-core",
|
||||
});
|
||||
});
|
||||
|
||||
it("lists active public memory artifacts in deterministic order", async () => {
|
||||
registerMemoryCapability("memory-core", {
|
||||
publicArtifacts: {
|
||||
async listArtifacts() {
|
||||
return [
|
||||
{
|
||||
kind: "daily-note",
|
||||
workspaceDir: "/tmp/workspace-b",
|
||||
relativePath: "memory/2026-04-06.md",
|
||||
absolutePath: "/tmp/workspace-b/memory/2026-04-06.md",
|
||||
agentIds: ["beta"],
|
||||
contentType: "markdown" as const,
|
||||
},
|
||||
{
|
||||
kind: "memory-root",
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
relativePath: "MEMORY.md",
|
||||
absolutePath: "/tmp/workspace-a/MEMORY.md",
|
||||
agentIds: ["main"],
|
||||
contentType: "markdown" as const,
|
||||
},
|
||||
];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await expect(listActiveMemoryPublicArtifacts({ cfg: {} as never })).resolves.toEqual([
|
||||
{
|
||||
kind: "memory-root",
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
relativePath: "MEMORY.md",
|
||||
absolutePath: "/tmp/workspace-a/MEMORY.md",
|
||||
agentIds: ["main"],
|
||||
contentType: "markdown",
|
||||
},
|
||||
{
|
||||
kind: "daily-note",
|
||||
workspaceDir: "/tmp/workspace-b",
|
||||
relativePath: "memory/2026-04-06.md",
|
||||
absolutePath: "/tmp/workspace-b/memory/2026-04-06.md",
|
||||
agentIds: ["beta"],
|
||||
contentType: "markdown",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("passes citations mode through to the prompt builder", () => {
|
||||
registerMemoryPromptSection(({ citationsMode }) => [
|
||||
`citations: ${citationsMode ?? "default"}`,
|
||||
|
||||
@@ -109,12 +109,46 @@ export type MemoryPluginRuntime = {
|
||||
closeAllMemorySearchManagers?(): Promise<void>;
|
||||
};
|
||||
|
||||
type MemoryPluginState = {
|
||||
corpusSupplements: MemoryCorpusSupplementRegistration[];
|
||||
export type MemoryPluginPublicArtifactContentType = "markdown" | "json" | "text";
|
||||
|
||||
export type MemoryPluginPublicArtifact = {
|
||||
kind: string;
|
||||
workspaceDir: string;
|
||||
relativePath: string;
|
||||
absolutePath: string;
|
||||
agentIds: string[];
|
||||
contentType: MemoryPluginPublicArtifactContentType;
|
||||
};
|
||||
|
||||
export type MemoryPluginPublicArtifactsProvider = {
|
||||
listArtifacts(params: { cfg: OpenClawConfig }): Promise<MemoryPluginPublicArtifact[]>;
|
||||
};
|
||||
|
||||
export type MemoryPluginCapability = {
|
||||
promptBuilder?: MemoryPromptSectionBuilder;
|
||||
promptSupplements: MemoryPromptSupplementRegistration[];
|
||||
flushPlanResolver?: MemoryFlushPlanResolver;
|
||||
runtime?: MemoryPluginRuntime;
|
||||
publicArtifacts?: MemoryPluginPublicArtifactsProvider;
|
||||
};
|
||||
|
||||
export type MemoryPluginCapabilityRegistration = {
|
||||
pluginId: string;
|
||||
capability: MemoryPluginCapability;
|
||||
};
|
||||
|
||||
type MemoryPluginState = {
|
||||
capability?: MemoryPluginCapabilityRegistration;
|
||||
corpusSupplements: MemoryCorpusSupplementRegistration[];
|
||||
promptSupplements: MemoryPromptSupplementRegistration[];
|
||||
// LEGACY(memory-v1): kept for external plugins still registering the older
|
||||
// split memory surfaces. Prefer `registerMemoryCapability(...)`.
|
||||
promptBuilder?: MemoryPromptSectionBuilder;
|
||||
// LEGACY(memory-v1): remove after external memory plugins migrate to the
|
||||
// unified capability registration path.
|
||||
flushPlanResolver?: MemoryFlushPlanResolver;
|
||||
// LEGACY(memory-v1): remove after external memory plugins migrate to the
|
||||
// unified capability registration path.
|
||||
runtime?: MemoryPluginRuntime;
|
||||
};
|
||||
|
||||
const memoryPluginState: MemoryPluginState = {
|
||||
@@ -133,10 +167,27 @@ export function registerMemoryCorpusSupplement(
|
||||
memoryPluginState.corpusSupplements = next;
|
||||
}
|
||||
|
||||
export function registerMemoryCapability(
|
||||
pluginId: string,
|
||||
capability: MemoryPluginCapability,
|
||||
): void {
|
||||
memoryPluginState.capability = { pluginId, capability: { ...capability } };
|
||||
}
|
||||
|
||||
export function getMemoryCapabilityRegistration(): MemoryPluginCapabilityRegistration | undefined {
|
||||
return memoryPluginState.capability
|
||||
? {
|
||||
pluginId: memoryPluginState.capability.pluginId,
|
||||
capability: { ...memoryPluginState.capability.capability },
|
||||
}
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function listMemoryCorpusSupplements(): MemoryCorpusSupplementRegistration[] {
|
||||
return [...memoryPluginState.corpusSupplements];
|
||||
}
|
||||
|
||||
/** @deprecated Use registerMemoryCapability(pluginId, { promptBuilder }) instead. */
|
||||
export function registerMemoryPromptSection(builder: MemoryPromptSectionBuilder): void {
|
||||
memoryPluginState.promptBuilder = builder;
|
||||
}
|
||||
@@ -156,7 +207,10 @@ export function buildMemoryPromptSection(params: {
|
||||
availableTools: Set<string>;
|
||||
citationsMode?: MemoryCitationsMode;
|
||||
}): string[] {
|
||||
const primary = memoryPluginState.promptBuilder?.(params) ?? [];
|
||||
const primary =
|
||||
memoryPluginState.capability?.capability.promptBuilder?.(params) ??
|
||||
memoryPluginState.promptBuilder?.(params) ??
|
||||
[];
|
||||
const supplements = memoryPluginState.promptSupplements
|
||||
// Keep supplement order stable even if plugin registration order changes.
|
||||
.toSorted((left, right) => left.pluginId.localeCompare(right.pluginId))
|
||||
@@ -165,13 +219,14 @@ export function buildMemoryPromptSection(params: {
|
||||
}
|
||||
|
||||
export function getMemoryPromptSectionBuilder(): MemoryPromptSectionBuilder | undefined {
|
||||
return memoryPluginState.promptBuilder;
|
||||
return memoryPluginState.capability?.capability.promptBuilder ?? memoryPluginState.promptBuilder;
|
||||
}
|
||||
|
||||
export function listMemoryPromptSupplements(): MemoryPromptSupplementRegistration[] {
|
||||
return [...memoryPluginState.promptSupplements];
|
||||
}
|
||||
|
||||
/** @deprecated Use registerMemoryCapability(pluginId, { flushPlanResolver }) instead. */
|
||||
export function registerMemoryFlushPlanResolver(resolver: MemoryFlushPlanResolver): void {
|
||||
memoryPluginState.flushPlanResolver = resolver;
|
||||
}
|
||||
@@ -180,26 +235,79 @@ export function resolveMemoryFlushPlan(params: {
|
||||
cfg?: OpenClawConfig;
|
||||
nowMs?: number;
|
||||
}): MemoryFlushPlan | null {
|
||||
return memoryPluginState.flushPlanResolver?.(params) ?? null;
|
||||
return (
|
||||
memoryPluginState.capability?.capability.flushPlanResolver?.(params) ??
|
||||
memoryPluginState.flushPlanResolver?.(params) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
|
||||
export function getMemoryFlushPlanResolver(): MemoryFlushPlanResolver | undefined {
|
||||
return memoryPluginState.flushPlanResolver;
|
||||
return (
|
||||
memoryPluginState.capability?.capability.flushPlanResolver ??
|
||||
memoryPluginState.flushPlanResolver
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated Use registerMemoryCapability(pluginId, { runtime }) instead. */
|
||||
export function registerMemoryRuntime(runtime: MemoryPluginRuntime): void {
|
||||
memoryPluginState.runtime = runtime;
|
||||
}
|
||||
|
||||
export function getMemoryRuntime(): MemoryPluginRuntime | undefined {
|
||||
return memoryPluginState.runtime;
|
||||
return memoryPluginState.capability?.capability.runtime ?? memoryPluginState.runtime;
|
||||
}
|
||||
|
||||
export function hasMemoryRuntime(): boolean {
|
||||
return memoryPluginState.runtime !== undefined;
|
||||
return getMemoryRuntime() !== undefined;
|
||||
}
|
||||
|
||||
function cloneMemoryPublicArtifact(
|
||||
artifact: MemoryPluginPublicArtifact,
|
||||
): MemoryPluginPublicArtifact {
|
||||
return {
|
||||
...artifact,
|
||||
agentIds: [...artifact.agentIds],
|
||||
};
|
||||
}
|
||||
|
||||
export async function listActiveMemoryPublicArtifacts(params: {
|
||||
cfg: OpenClawConfig;
|
||||
}): Promise<MemoryPluginPublicArtifact[]> {
|
||||
const artifacts =
|
||||
(await memoryPluginState.capability?.capability.publicArtifacts?.listArtifacts(params)) ?? [];
|
||||
return artifacts.map(cloneMemoryPublicArtifact).toSorted((left, right) => {
|
||||
const workspaceOrder = left.workspaceDir.localeCompare(right.workspaceDir);
|
||||
if (workspaceOrder !== 0) {
|
||||
return workspaceOrder;
|
||||
}
|
||||
const relativePathOrder = left.relativePath.localeCompare(right.relativePath);
|
||||
if (relativePathOrder !== 0) {
|
||||
return relativePathOrder;
|
||||
}
|
||||
const kindOrder = left.kind.localeCompare(right.kind);
|
||||
if (kindOrder !== 0) {
|
||||
return kindOrder;
|
||||
}
|
||||
const contentTypeOrder = left.contentType.localeCompare(right.contentType);
|
||||
if (contentTypeOrder !== 0) {
|
||||
return contentTypeOrder;
|
||||
}
|
||||
const agentOrder = left.agentIds.join("\0").localeCompare(right.agentIds.join("\0"));
|
||||
if (agentOrder !== 0) {
|
||||
return agentOrder;
|
||||
}
|
||||
return left.absolutePath.localeCompare(right.absolutePath);
|
||||
});
|
||||
}
|
||||
|
||||
export function restoreMemoryPluginState(state: MemoryPluginState): void {
|
||||
memoryPluginState.capability = state.capability
|
||||
? {
|
||||
pluginId: state.capability.pluginId,
|
||||
capability: { ...state.capability.capability },
|
||||
}
|
||||
: undefined;
|
||||
memoryPluginState.corpusSupplements = [...state.corpusSupplements];
|
||||
memoryPluginState.promptBuilder = state.promptBuilder;
|
||||
memoryPluginState.promptSupplements = [...state.promptSupplements];
|
||||
@@ -208,6 +316,7 @@ export function restoreMemoryPluginState(state: MemoryPluginState): void {
|
||||
}
|
||||
|
||||
export function clearMemoryPluginState(): void {
|
||||
memoryPluginState.capability = undefined;
|
||||
memoryPluginState.corpusSupplements = [];
|
||||
memoryPluginState.promptBuilder = undefined;
|
||||
memoryPluginState.promptSupplements = [];
|
||||
|
||||
@@ -5,7 +5,11 @@ import {
|
||||
registerVirtualTestPlugin,
|
||||
} from "../../test/helpers/plugins/contracts-testkit.js";
|
||||
import { clearMemoryEmbeddingProviders } from "./memory-embedding-providers.js";
|
||||
import { _resetMemoryPluginState, getMemoryRuntime } from "./memory-state.js";
|
||||
import {
|
||||
_resetMemoryPluginState,
|
||||
getMemoryCapabilityRegistration,
|
||||
getMemoryRuntime,
|
||||
} from "./memory-state.js";
|
||||
import { createPluginRecord } from "./status.test-helpers.js";
|
||||
|
||||
afterEach(() => {
|
||||
@@ -92,4 +96,30 @@ describe("dual-kind memory registration gate", () => {
|
||||
|
||||
expect(getMemoryRuntime()).toBeDefined();
|
||||
});
|
||||
|
||||
it("allows selected dual-kind plugins to register the unified memory capability", () => {
|
||||
const { config, registry } = createPluginRegistryFixture();
|
||||
|
||||
registerTestPlugin({
|
||||
registry,
|
||||
config,
|
||||
record: createPluginRecord({
|
||||
id: "dual-plugin",
|
||||
name: "Dual Plugin",
|
||||
kind: ["memory", "context-engine"],
|
||||
memorySlotSelected: true,
|
||||
}),
|
||||
register(api) {
|
||||
api.registerMemoryCapability({
|
||||
runtime: createStubMemoryRuntime(),
|
||||
promptBuilder: () => ["memory capability"],
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
expect(getMemoryCapabilityRegistration()).toMatchObject({
|
||||
pluginId: "dual-plugin",
|
||||
});
|
||||
expect(getMemoryRuntime()).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
registerMemoryEmbeddingProvider,
|
||||
} from "./memory-embedding-providers.js";
|
||||
import {
|
||||
registerMemoryCapability,
|
||||
registerMemoryCorpusSupplement,
|
||||
registerMemoryFlushPlanResolver,
|
||||
registerMemoryPromptSupplement,
|
||||
@@ -1296,6 +1297,32 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
});
|
||||
}
|
||||
},
|
||||
registerMemoryCapability: (capability) => {
|
||||
if (!hasKind(record.kind, "memory")) {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message: "only memory plugins can register a memory capability",
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (
|
||||
Array.isArray(record.kind) &&
|
||||
record.kind.length > 1 &&
|
||||
!record.memorySlotSelected
|
||||
) {
|
||||
pushDiagnostic({
|
||||
level: "warn",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message:
|
||||
"dual-kind plugin not selected for memory slot; skipping memory capability registration",
|
||||
});
|
||||
return;
|
||||
}
|
||||
registerMemoryCapability(record.id, capability);
|
||||
},
|
||||
registerMemoryPromptSection: (builder) => {
|
||||
if (!hasKind(record.kind, "memory")) {
|
||||
pushDiagnostic({
|
||||
|
||||
@@ -2184,7 +2184,14 @@ export type OpenClawPluginApi = {
|
||||
id: string,
|
||||
factory: import("../context-engine/registry.js").ContextEngineFactory,
|
||||
) => void;
|
||||
/** Register the system prompt section builder for this memory plugin (exclusive slot). */
|
||||
/** Register the active memory capability for this memory plugin (exclusive slot). */
|
||||
registerMemoryCapability: (
|
||||
capability: import("./memory-state.js").MemoryPluginCapability,
|
||||
) => void;
|
||||
/**
|
||||
* Register the system prompt section builder for this memory plugin (exclusive slot).
|
||||
* @deprecated Use registerMemoryCapability({ promptBuilder }) instead.
|
||||
*/
|
||||
registerMemoryPromptSection: (
|
||||
builder: import("./memory-state.js").MemoryPromptSectionBuilder,
|
||||
) => void;
|
||||
@@ -2196,9 +2203,15 @@ export type OpenClawPluginApi = {
|
||||
registerMemoryCorpusSupplement: (
|
||||
supplement: import("./memory-state.js").MemoryCorpusSupplement,
|
||||
) => void;
|
||||
/** Register the pre-compaction flush plan resolver for this memory plugin (exclusive slot). */
|
||||
/**
|
||||
* Register the pre-compaction flush plan resolver for this memory plugin (exclusive slot).
|
||||
* @deprecated Use registerMemoryCapability({ flushPlanResolver }) instead.
|
||||
*/
|
||||
registerMemoryFlushPlan: (resolver: import("./memory-state.js").MemoryFlushPlanResolver) => void;
|
||||
/** Register the active memory runtime adapter for this memory plugin (exclusive slot). */
|
||||
/**
|
||||
* Register the active memory runtime adapter for this memory plugin (exclusive slot).
|
||||
* @deprecated Use registerMemoryCapability({ runtime }) instead.
|
||||
*/
|
||||
registerMemoryRuntime: (runtime: import("./memory-state.js").MemoryPluginRuntime) => void;
|
||||
/** Register a memory embedding provider adapter. Multiple adapters may coexist. */
|
||||
registerMemoryEmbeddingProvider: (
|
||||
|
||||
Reference in New Issue
Block a user