Merge 80cc0b9c07d7ded99451647d0c0b5d000f03f271 into 5e417b44e1540f528d2ae63e3e20229a902d1db2
This commit is contained in:
commit
d46ed1dd1b
@ -54,6 +54,7 @@ function fakeApi(overrides: Partial<OpenClawPluginApi> = {}): OpenClawPluginApi
|
|||||||
registerHttpRoute() {},
|
registerHttpRoute() {},
|
||||||
registerCommand() {},
|
registerCommand() {},
|
||||||
registerContextEngine() {},
|
registerContextEngine() {},
|
||||||
|
registerMemoryPromptSection() {},
|
||||||
on() {},
|
on() {},
|
||||||
resolvePath: (p) => p,
|
resolvePath: (p) => p,
|
||||||
...overrides,
|
...overrides,
|
||||||
|
|||||||
32
extensions/memory-core/index.test.ts
Normal file
32
extensions/memory-core/index.test.ts
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { buildPromptSection } from "./index.js";
|
||||||
|
|
||||||
|
describe("buildPromptSection", () => {
|
||||||
|
it("returns empty when no memory tools are available", () => {
|
||||||
|
expect(buildPromptSection({ availableTools: new Set() })).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns Memory Recall section when memory_search is available", () => {
|
||||||
|
const result = buildPromptSection({ availableTools: new Set(["memory_search"]) });
|
||||||
|
expect(result[0]).toBe("## Memory Recall");
|
||||||
|
expect(result).toContain(
|
||||||
|
"Citations: include Source: <path#line> when it helps the user verify memory snippets.",
|
||||||
|
);
|
||||||
|
expect(result.at(-1)).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns Memory Recall section when memory_get is available", () => {
|
||||||
|
const result = buildPromptSection({ availableTools: new Set(["memory_get"]) });
|
||||||
|
expect(result[0]).toBe("## Memory Recall");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("includes citations-off instruction when citationsMode is off", () => {
|
||||||
|
const result = buildPromptSection({
|
||||||
|
availableTools: new Set(["memory_search"]),
|
||||||
|
citationsMode: "off",
|
||||||
|
});
|
||||||
|
expect(result).toContain(
|
||||||
|
"Citations are disabled: do not mention file paths or line numbers in replies unless the user explicitly asks.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
@ -1,4 +1,29 @@
|
|||||||
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
import { definePluginEntry } from "openclaw/plugin-sdk/core";
|
||||||
|
import type { MemoryPromptSectionBuilder } from "openclaw/plugin-sdk/memory-core";
|
||||||
|
|
||||||
|
export const buildPromptSection: MemoryPromptSectionBuilder = ({
|
||||||
|
availableTools,
|
||||||
|
citationsMode,
|
||||||
|
}) => {
|
||||||
|
if (!availableTools.has("memory_search") && !availableTools.has("memory_get")) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const lines = [
|
||||||
|
"## Memory Recall",
|
||||||
|
"Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines. If low confidence after search, say you checked.",
|
||||||
|
];
|
||||||
|
if (citationsMode === "off") {
|
||||||
|
lines.push(
|
||||||
|
"Citations are disabled: do not mention file paths or line numbers in replies unless the user explicitly asks.",
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
lines.push(
|
||||||
|
"Citations: include Source: <path#line> when it helps the user verify memory snippets.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
lines.push("");
|
||||||
|
return lines;
|
||||||
|
};
|
||||||
|
|
||||||
export default definePluginEntry({
|
export default definePluginEntry({
|
||||||
id: "memory-core",
|
id: "memory-core",
|
||||||
@ -6,6 +31,8 @@ export default definePluginEntry({
|
|||||||
description: "File-backed memory search tools and CLI",
|
description: "File-backed memory search tools and CLI",
|
||||||
kind: "memory",
|
kind: "memory",
|
||||||
register(api) {
|
register(api) {
|
||||||
|
api.registerMemoryPromptSection(buildPromptSection);
|
||||||
|
|
||||||
api.registerTool(
|
api.registerTool(
|
||||||
(ctx) => {
|
(ctx) => {
|
||||||
const memorySearchTool = api.runtime.tools.createMemorySearchTool({
|
const memorySearchTool = api.runtime.tools.createMemorySearchTool({
|
||||||
|
|||||||
@ -309,6 +309,10 @@
|
|||||||
"types": "./dist/plugin-sdk/llm-task.d.ts",
|
"types": "./dist/plugin-sdk/llm-task.d.ts",
|
||||||
"default": "./dist/plugin-sdk/llm-task.js"
|
"default": "./dist/plugin-sdk/llm-task.js"
|
||||||
},
|
},
|
||||||
|
"./plugin-sdk/memory-core": {
|
||||||
|
"types": "./dist/plugin-sdk/memory-core.d.ts",
|
||||||
|
"default": "./dist/plugin-sdk/memory-core.js"
|
||||||
|
},
|
||||||
"./plugin-sdk/memory-lancedb": {
|
"./plugin-sdk/memory-lancedb": {
|
||||||
"types": "./dist/plugin-sdk/memory-lancedb.d.ts",
|
"types": "./dist/plugin-sdk/memory-lancedb.d.ts",
|
||||||
"default": "./dist/plugin-sdk/memory-lancedb.js"
|
"default": "./dist/plugin-sdk/memory-lancedb.js"
|
||||||
|
|||||||
@ -67,6 +67,7 @@
|
|||||||
"json-store",
|
"json-store",
|
||||||
"keyed-async-queue",
|
"keyed-async-queue",
|
||||||
"llm-task",
|
"llm-task",
|
||||||
|
"memory-core",
|
||||||
"memory-lancedb",
|
"memory-lancedb",
|
||||||
"provider-auth",
|
"provider-auth",
|
||||||
"provider-auth-api-key",
|
"provider-auth-api-key",
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { createHmac, createHash } from "node:crypto";
|
|||||||
import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js";
|
import type { ReasoningLevel, ThinkLevel } from "../auto-reply/thinking.js";
|
||||||
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
import { SILENT_REPLY_TOKEN } from "../auto-reply/tokens.js";
|
||||||
import type { MemoryCitationsMode } from "../config/types.memory.js";
|
import type { MemoryCitationsMode } from "../config/types.memory.js";
|
||||||
|
import { buildMemoryPromptSection } from "../memory/prompt-section.js";
|
||||||
import { listDeliverableMessageChannels } from "../utils/message-channel.js";
|
import { listDeliverableMessageChannels } from "../utils/message-channel.js";
|
||||||
import type { ResolvedTimeFormat } from "./date-time.js";
|
import type { ResolvedTimeFormat } from "./date-time.js";
|
||||||
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
|
import type { EmbeddedContextFile } from "./pi-embedded-helpers.js";
|
||||||
@ -43,24 +44,10 @@ function buildMemorySection(params: {
|
|||||||
if (params.isMinimal) {
|
if (params.isMinimal) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
if (!params.availableTools.has("memory_search") && !params.availableTools.has("memory_get")) {
|
return buildMemoryPromptSection({
|
||||||
return [];
|
availableTools: params.availableTools,
|
||||||
}
|
citationsMode: params.citationsMode,
|
||||||
const lines = [
|
});
|
||||||
"## Memory Recall",
|
|
||||||
"Before answering anything about prior work, decisions, dates, people, preferences, or todos: run memory_search on MEMORY.md + memory/*.md; then use memory_get to pull only the needed lines. If low confidence after search, say you checked.",
|
|
||||||
];
|
|
||||||
if (params.citationsMode === "off") {
|
|
||||||
lines.push(
|
|
||||||
"Citations are disabled: do not mention file paths or line numbers in replies unless the user explicitly asks.",
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
lines.push(
|
|
||||||
"Citations: include Source: <path#line> when it helps the user verify memory snippets.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
lines.push("");
|
|
||||||
return lines;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildUserIdentitySection(ownerLine: string | undefined, isMinimal: boolean) {
|
function buildUserIdentitySection(ownerLine: string | undefined, isMinimal: boolean) {
|
||||||
|
|||||||
64
src/memory/prompt-section.test.ts
Normal file
64
src/memory/prompt-section.test.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import { describe, it, expect, beforeEach } from "vitest";
|
||||||
|
import {
|
||||||
|
registerMemoryPromptSection,
|
||||||
|
buildMemoryPromptSection,
|
||||||
|
clearMemoryPromptSection,
|
||||||
|
_resetMemoryPromptSection,
|
||||||
|
} from "./prompt-section.js";
|
||||||
|
|
||||||
|
describe("memory prompt section registry", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
_resetMemoryPromptSection();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns empty array when no builder is registered", () => {
|
||||||
|
const result = buildMemoryPromptSection({
|
||||||
|
availableTools: new Set(["memory_search", "memory_get"]),
|
||||||
|
});
|
||||||
|
expect(result).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("delegates to the registered builder", () => {
|
||||||
|
registerMemoryPromptSection(({ availableTools }) => {
|
||||||
|
if (!availableTools.has("memory_search")) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
return ["## Custom Memory", "Use custom memory tools.", ""];
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = buildMemoryPromptSection({
|
||||||
|
availableTools: new Set(["memory_search"]),
|
||||||
|
});
|
||||||
|
expect(result).toEqual(["## Custom Memory", "Use custom memory tools.", ""]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("passes citationsMode to the builder", () => {
|
||||||
|
registerMemoryPromptSection(({ citationsMode }) => {
|
||||||
|
return [`citations: ${citationsMode ?? "default"}`];
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(
|
||||||
|
buildMemoryPromptSection({
|
||||||
|
availableTools: new Set(),
|
||||||
|
citationsMode: "off",
|
||||||
|
}),
|
||||||
|
).toEqual(["citations: off"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("last registration wins", () => {
|
||||||
|
registerMemoryPromptSection(() => ["first"]);
|
||||||
|
registerMemoryPromptSection(() => ["second"]);
|
||||||
|
|
||||||
|
const result = buildMemoryPromptSection({ availableTools: new Set() });
|
||||||
|
expect(result).toEqual(["second"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clearMemoryPromptSection resets the builder", () => {
|
||||||
|
registerMemoryPromptSection(() => ["stale section"]);
|
||||||
|
expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual(["stale section"]);
|
||||||
|
|
||||||
|
clearMemoryPromptSection();
|
||||||
|
|
||||||
|
expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
42
src/memory/prompt-section.ts
Normal file
42
src/memory/prompt-section.ts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import type { MemoryCitationsMode } from "../config/types.memory.js";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Callback that the active memory plugin provides to build
|
||||||
|
* its section of the agent system prompt.
|
||||||
|
*/
|
||||||
|
export type MemoryPromptSectionBuilder = (params: {
|
||||||
|
availableTools: Set<string>;
|
||||||
|
citationsMode?: MemoryCitationsMode;
|
||||||
|
}) => string[];
|
||||||
|
|
||||||
|
// Module-level singleton — only one memory plugin can be active (exclusive slot).
|
||||||
|
let _builder: MemoryPromptSectionBuilder | undefined;
|
||||||
|
|
||||||
|
export function registerMemoryPromptSection(builder: MemoryPromptSectionBuilder): void {
|
||||||
|
_builder = builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMemoryPromptSection(params: {
|
||||||
|
availableTools: Set<string>;
|
||||||
|
citationsMode?: MemoryCitationsMode;
|
||||||
|
}): string[] {
|
||||||
|
return _builder?.(params) ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Return the current builder (used by the plugin cache to snapshot state). */
|
||||||
|
export function getMemoryPromptSectionBuilder(): MemoryPromptSectionBuilder | undefined {
|
||||||
|
return _builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Restore a previously-snapshotted builder (used on plugin cache hits). */
|
||||||
|
export function restoreMemoryPromptSection(builder: MemoryPromptSectionBuilder | undefined): void {
|
||||||
|
_builder = builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Clear the registered builder (called on plugin reload and in tests). */
|
||||||
|
export function clearMemoryPromptSection(): void {
|
||||||
|
_builder = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use {@link clearMemoryPromptSection}. */
|
||||||
|
export const _resetMemoryPromptSection = clearMemoryPromptSection;
|
||||||
@ -2,4 +2,5 @@
|
|||||||
// Keep this list additive and scoped to symbols used under extensions/memory-core.
|
// Keep this list additive and scoped to symbols used under extensions/memory-core.
|
||||||
|
|
||||||
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
export { emptyPluginConfigSchema } from "../plugins/config-schema.js";
|
||||||
|
export type { MemoryPromptSectionBuilder } from "../memory/prompt-section.js";
|
||||||
export type { OpenClawPluginApi } from "../plugins/types.js";
|
export type { OpenClawPluginApi } from "../plugins/types.js";
|
||||||
|
|||||||
@ -23,12 +23,13 @@ async function importFreshPluginTestModules() {
|
|||||||
vi.doUnmock("./hooks.js");
|
vi.doUnmock("./hooks.js");
|
||||||
vi.doUnmock("./loader.js");
|
vi.doUnmock("./loader.js");
|
||||||
vi.doUnmock("jiti");
|
vi.doUnmock("jiti");
|
||||||
const [loader, hookRunnerGlobal, hooks, runtime, registry] = await Promise.all([
|
const [loader, hookRunnerGlobal, hooks, runtime, registry, promptSection] = await Promise.all([
|
||||||
import("./loader.js"),
|
import("./loader.js"),
|
||||||
import("./hook-runner-global.js"),
|
import("./hook-runner-global.js"),
|
||||||
import("./hooks.js"),
|
import("./hooks.js"),
|
||||||
import("./runtime.js"),
|
import("./runtime.js"),
|
||||||
import("./registry.js"),
|
import("./registry.js"),
|
||||||
|
import("../memory/prompt-section.js"),
|
||||||
]);
|
]);
|
||||||
return {
|
return {
|
||||||
...loader,
|
...loader,
|
||||||
@ -36,11 +37,13 @@ async function importFreshPluginTestModules() {
|
|||||||
...hooks,
|
...hooks,
|
||||||
...runtime,
|
...runtime,
|
||||||
...registry,
|
...registry,
|
||||||
|
...promptSection,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
__testing,
|
__testing,
|
||||||
|
buildMemoryPromptSection,
|
||||||
clearPluginLoaderCache,
|
clearPluginLoaderCache,
|
||||||
createHookRunner,
|
createHookRunner,
|
||||||
createEmptyPluginRegistry,
|
createEmptyPluginRegistry,
|
||||||
@ -48,6 +51,7 @@ const {
|
|||||||
getActivePluginRegistryKey,
|
getActivePluginRegistryKey,
|
||||||
getGlobalHookRunner,
|
getGlobalHookRunner,
|
||||||
loadOpenClawPlugins,
|
loadOpenClawPlugins,
|
||||||
|
registerMemoryPromptSection,
|
||||||
resetGlobalHookRunner,
|
resetGlobalHookRunner,
|
||||||
setActivePluginRegistry,
|
setActivePluginRegistry,
|
||||||
} = await importFreshPluginTestModules();
|
} = await importFreshPluginTestModules();
|
||||||
@ -1204,6 +1208,73 @@ module.exports = { id: "skipped-scoped-only", register() { throw new Error("skip
|
|||||||
clearPluginCommands();
|
clearPluginCommands();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("does not replace the active memory prompt section during non-activating loads", () => {
|
||||||
|
useNoBundledPlugins();
|
||||||
|
registerMemoryPromptSection(() => ["active memory section"]);
|
||||||
|
const plugin = writePlugin({
|
||||||
|
id: "snapshot-memory",
|
||||||
|
filename: "snapshot-memory.cjs",
|
||||||
|
body: `module.exports = {
|
||||||
|
id: "snapshot-memory",
|
||||||
|
kind: "memory",
|
||||||
|
register(api) {
|
||||||
|
api.registerMemoryPromptSection(() => ["snapshot memory section"]);
|
||||||
|
},
|
||||||
|
};`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const scoped = loadOpenClawPlugins({
|
||||||
|
cache: false,
|
||||||
|
activate: false,
|
||||||
|
workspaceDir: plugin.dir,
|
||||||
|
config: {
|
||||||
|
plugins: {
|
||||||
|
load: { paths: [plugin.file] },
|
||||||
|
allow: ["snapshot-memory"],
|
||||||
|
slots: { memory: "snapshot-memory" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onlyPluginIds: ["snapshot-memory"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(scoped.plugins.find((entry) => entry.id === "snapshot-memory")?.status).toBe("loaded");
|
||||||
|
expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([
|
||||||
|
"active memory section",
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("clears a newly-registered memory prompt section when plugin register fails", () => {
|
||||||
|
useNoBundledPlugins();
|
||||||
|
const plugin = writePlugin({
|
||||||
|
id: "failing-memory",
|
||||||
|
filename: "failing-memory.cjs",
|
||||||
|
body: `module.exports = {
|
||||||
|
id: "failing-memory",
|
||||||
|
kind: "memory",
|
||||||
|
register(api) {
|
||||||
|
api.registerMemoryPromptSection(() => ["stale failure section"]);
|
||||||
|
throw new Error("memory register failed");
|
||||||
|
},
|
||||||
|
};`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const registry = loadOpenClawPlugins({
|
||||||
|
cache: false,
|
||||||
|
workspaceDir: plugin.dir,
|
||||||
|
config: {
|
||||||
|
plugins: {
|
||||||
|
load: { paths: [plugin.file] },
|
||||||
|
allow: ["failing-memory"],
|
||||||
|
slots: { memory: "failing-memory" },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
onlyPluginIds: ["failing-memory"],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(registry.plugins.find((entry) => entry.id === "failing-memory")?.status).toBe("error");
|
||||||
|
expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it("throws when activate:false is used without cache:false", () => {
|
it("throws when activate:false is used without cache:false", () => {
|
||||||
expect(() => loadOpenClawPlugins({ activate: false })).toThrow(
|
expect(() => loadOpenClawPlugins({ activate: false })).toThrow(
|
||||||
"activate:false requires cache:false",
|
"activate:false requires cache:false",
|
||||||
@ -3787,3 +3858,16 @@ export const runtimeValue = helperValue;`,
|
|||||||
expect(resolved).toBe(expected === "dist" ? fixture.distFile : fixture.srcFile);
|
expect(resolved).toBe(expected === "dist" ? fixture.distFile : fixture.srcFile);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("clearPluginLoaderCache", () => {
|
||||||
|
it("resets the registered memory prompt section builder", () => {
|
||||||
|
registerMemoryPromptSection(() => ["stale memory section"]);
|
||||||
|
expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([
|
||||||
|
"stale memory section",
|
||||||
|
]);
|
||||||
|
|
||||||
|
clearPluginLoaderCache();
|
||||||
|
|
||||||
|
expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
@ -9,6 +9,11 @@ import type { PluginInstallRecord } from "../config/types.plugins.js";
|
|||||||
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
import type { GatewayRequestHandler } from "../gateway/server-methods/types.js";
|
||||||
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
import { openBoundaryFileSync } from "../infra/boundary-file-read.js";
|
||||||
import { createSubsystemLogger } from "../logging/subsystem.js";
|
import { createSubsystemLogger } from "../logging/subsystem.js";
|
||||||
|
import {
|
||||||
|
clearMemoryPromptSection,
|
||||||
|
getMemoryPromptSectionBuilder,
|
||||||
|
restoreMemoryPromptSection,
|
||||||
|
} from "../memory/prompt-section.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
import { inspectBundleMcpRuntimeSupport } from "./bundle-mcp.js";
|
import { inspectBundleMcpRuntimeSupport } from "./bundle-mcp.js";
|
||||||
import { clearPluginCommands } from "./commands.js";
|
import { clearPluginCommands } from "./commands.js";
|
||||||
@ -90,8 +95,13 @@ export class PluginLoadFailureError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type CachedPluginState = {
|
||||||
|
registry: PluginRegistry;
|
||||||
|
memoryPromptBuilder: ReturnType<typeof getMemoryPromptSectionBuilder>;
|
||||||
|
};
|
||||||
|
|
||||||
const MAX_PLUGIN_REGISTRY_CACHE_ENTRIES = 128;
|
const MAX_PLUGIN_REGISTRY_CACHE_ENTRIES = 128;
|
||||||
const registryCache = new Map<string, PluginRegistry>();
|
const registryCache = new Map<string, CachedPluginState>();
|
||||||
const openAllowlistWarningCache = new Set<string>();
|
const openAllowlistWarningCache = new Set<string>();
|
||||||
const LAZY_RUNTIME_REFLECTION_KEYS = [
|
const LAZY_RUNTIME_REFLECTION_KEYS = [
|
||||||
"version",
|
"version",
|
||||||
@ -113,6 +123,7 @@ const LAZY_RUNTIME_REFLECTION_KEYS = [
|
|||||||
export function clearPluginLoaderCache(): void {
|
export function clearPluginLoaderCache(): void {
|
||||||
registryCache.clear();
|
registryCache.clear();
|
||||||
openAllowlistWarningCache.clear();
|
openAllowlistWarningCache.clear();
|
||||||
|
clearMemoryPromptSection();
|
||||||
}
|
}
|
||||||
|
|
||||||
const defaultLogger = () => createSubsystemLogger("plugins");
|
const defaultLogger = () => createSubsystemLogger("plugins");
|
||||||
@ -209,7 +220,7 @@ export const __testing = {
|
|||||||
maxPluginRegistryCacheEntries: MAX_PLUGIN_REGISTRY_CACHE_ENTRIES,
|
maxPluginRegistryCacheEntries: MAX_PLUGIN_REGISTRY_CACHE_ENTRIES,
|
||||||
};
|
};
|
||||||
|
|
||||||
function getCachedPluginRegistry(cacheKey: string): PluginRegistry | undefined {
|
function getCachedPluginRegistry(cacheKey: string): CachedPluginState | undefined {
|
||||||
const cached = registryCache.get(cacheKey);
|
const cached = registryCache.get(cacheKey);
|
||||||
if (!cached) {
|
if (!cached) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@ -220,11 +231,11 @@ function getCachedPluginRegistry(cacheKey: string): PluginRegistry | undefined {
|
|||||||
return cached;
|
return cached;
|
||||||
}
|
}
|
||||||
|
|
||||||
function setCachedPluginRegistry(cacheKey: string, registry: PluginRegistry): void {
|
function setCachedPluginRegistry(cacheKey: string, state: CachedPluginState): void {
|
||||||
if (registryCache.has(cacheKey)) {
|
if (registryCache.has(cacheKey)) {
|
||||||
registryCache.delete(cacheKey);
|
registryCache.delete(cacheKey);
|
||||||
}
|
}
|
||||||
registryCache.set(cacheKey, registry);
|
registryCache.set(cacheKey, state);
|
||||||
while (registryCache.size > MAX_PLUGIN_REGISTRY_CACHE_ENTRIES) {
|
while (registryCache.size > MAX_PLUGIN_REGISTRY_CACHE_ENTRIES) {
|
||||||
const oldestKey = registryCache.keys().next().value;
|
const oldestKey = registryCache.keys().next().value;
|
||||||
if (!oldestKey) {
|
if (!oldestKey) {
|
||||||
@ -763,18 +774,20 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
|||||||
if (cacheEnabled) {
|
if (cacheEnabled) {
|
||||||
const cached = getCachedPluginRegistry(cacheKey);
|
const cached = getCachedPluginRegistry(cacheKey);
|
||||||
if (cached) {
|
if (cached) {
|
||||||
|
restoreMemoryPromptSection(cached.memoryPromptBuilder);
|
||||||
if (shouldActivate) {
|
if (shouldActivate) {
|
||||||
activatePluginRegistry(cached, cacheKey);
|
activatePluginRegistry(cached.registry, cacheKey);
|
||||||
}
|
}
|
||||||
return cached;
|
return cached.registry;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear previously registered plugin commands before reloading.
|
// Clear previously registered plugin state before reloading.
|
||||||
// Skip for non-activating (snapshot) loads to avoid wiping commands from other plugins.
|
// Skip for non-activating (snapshot) loads to avoid wiping commands from other plugins.
|
||||||
if (shouldActivate) {
|
if (shouldActivate) {
|
||||||
clearPluginCommands();
|
clearPluginCommands();
|
||||||
clearPluginInteractiveHandlers();
|
clearPluginInteractiveHandlers();
|
||||||
|
clearMemoryPromptSection();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Lazy: avoid creating the Jiti loader when all plugins are disabled (common in unit tests).
|
// Lazy: avoid creating the Jiti loader when all plugins are disabled (common in unit tests).
|
||||||
@ -1270,6 +1283,7 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
|||||||
hookPolicy: entry?.hooks,
|
hookPolicy: entry?.hooks,
|
||||||
registrationMode,
|
registrationMode,
|
||||||
});
|
});
|
||||||
|
const previousMemoryPromptBuilder = getMemoryPromptSectionBuilder();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = register(api);
|
const result = register(api);
|
||||||
@ -1281,9 +1295,14 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
|||||||
message: "plugin register returned a promise; async registration is ignored",
|
message: "plugin register returned a promise; async registration is ignored",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Snapshot loads should not replace process-global runtime prompt state.
|
||||||
|
if (!shouldActivate) {
|
||||||
|
restoreMemoryPromptSection(previousMemoryPromptBuilder);
|
||||||
|
}
|
||||||
registry.plugins.push(record);
|
registry.plugins.push(record);
|
||||||
seenIds.set(pluginId, candidate.origin);
|
seenIds.set(pluginId, candidate.origin);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
restoreMemoryPromptSection(previousMemoryPromptBuilder);
|
||||||
recordPluginError({
|
recordPluginError({
|
||||||
logger,
|
logger,
|
||||||
registry,
|
registry,
|
||||||
@ -1317,7 +1336,10 @@ export function loadOpenClawPlugins(options: PluginLoadOptions = {}): PluginRegi
|
|||||||
maybeThrowOnPluginLoadError(registry, options.throwOnLoadError);
|
maybeThrowOnPluginLoadError(registry, options.throwOnLoadError);
|
||||||
|
|
||||||
if (cacheEnabled) {
|
if (cacheEnabled) {
|
||||||
setCachedPluginRegistry(cacheKey, registry);
|
setCachedPluginRegistry(cacheKey, {
|
||||||
|
registry,
|
||||||
|
memoryPromptBuilder: getMemoryPromptSectionBuilder(),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
if (shouldActivate) {
|
if (shouldActivate) {
|
||||||
activatePluginRegistry(registry, cacheKey);
|
activatePluginRegistry(registry, cacheKey);
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import type {
|
|||||||
} from "../gateway/server-methods/types.js";
|
} from "../gateway/server-methods/types.js";
|
||||||
import { registerInternalHook } from "../hooks/internal-hooks.js";
|
import { registerInternalHook } from "../hooks/internal-hooks.js";
|
||||||
import type { HookEntry } from "../hooks/types.js";
|
import type { HookEntry } from "../hooks/types.js";
|
||||||
|
import { registerMemoryPromptSection } from "../memory/prompt-section.js";
|
||||||
import { resolveUserPath } from "../utils.js";
|
import { resolveUserPath } from "../utils.js";
|
||||||
import { registerPluginCommand, validatePluginCommandDefinition } from "./commands.js";
|
import { registerPluginCommand, validatePluginCommandDefinition } from "./commands.js";
|
||||||
import { normalizePluginHttpPath } from "./http-path.js";
|
import { normalizePluginHttpPath } from "./http-path.js";
|
||||||
@ -979,6 +980,21 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
registerMemoryPromptSection: (builder) => {
|
||||||
|
if (registrationMode !== "full") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (record.kind !== "memory") {
|
||||||
|
pushDiagnostic({
|
||||||
|
level: "error",
|
||||||
|
pluginId: record.id,
|
||||||
|
source: record.source,
|
||||||
|
message: "only memory plugins can register a memory prompt section",
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
registerMemoryPromptSection(builder);
|
||||||
|
},
|
||||||
resolvePath: (input: string) => resolveUserPath(input),
|
resolvePath: (input: string) => resolveUserPath(input),
|
||||||
on: (hookName, handler, opts) =>
|
on: (hookName, handler, opts) =>
|
||||||
registrationMode === "full"
|
registrationMode === "full"
|
||||||
|
|||||||
@ -1341,6 +1341,10 @@ export type OpenClawPluginApi = {
|
|||||||
id: string,
|
id: string,
|
||||||
factory: import("../context-engine/registry.js").ContextEngineFactory,
|
factory: import("../context-engine/registry.js").ContextEngineFactory,
|
||||||
) => void;
|
) => void;
|
||||||
|
/** Register the system prompt section builder for this memory plugin (exclusive slot). */
|
||||||
|
registerMemoryPromptSection: (
|
||||||
|
builder: import("../memory/prompt-section.js").MemoryPromptSectionBuilder,
|
||||||
|
) => void;
|
||||||
resolvePath: (input: string) => string;
|
resolvePath: (input: string) => string;
|
||||||
/** Register a lifecycle hook handler */
|
/** Register a lifecycle hook handler */
|
||||||
on: <K extends PluginHookName>(
|
on: <K extends PluginHookName>(
|
||||||
|
|||||||
@ -23,6 +23,7 @@ export function createTestPluginApi(api: TestPluginApiInput): OpenClawPluginApi
|
|||||||
onConversationBindingResolved() {},
|
onConversationBindingResolved() {},
|
||||||
registerCommand() {},
|
registerCommand() {},
|
||||||
registerContextEngine() {},
|
registerContextEngine() {},
|
||||||
|
registerMemoryPromptSection() {},
|
||||||
resolvePath(input: string) {
|
resolvePath(input: string) {
|
||||||
return input;
|
return input;
|
||||||
},
|
},
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user