@ngo-a/native-memory-citations 2026.6.7 → 2026.6.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +126 -6
- package/dist/core.d.ts +69 -0
- package/dist/core.js +194 -1
- package/dist/enhanced.d.ts +48 -0
- package/dist/enhanced.js +205 -0
- package/dist/health.d.ts +1 -0
- package/dist/health.js +164 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +159 -36
- package/openclaw.plugin.json +157 -2
- package/package.json +8 -8
package/dist/enhanced.js
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { appendFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { modeFromConfig, workspaceFromConfig } from "./core.js";
|
|
4
|
+
const PLUGIN_ID = "native-memory-citations";
|
|
5
|
+
const DEFAULT_TOKEN_CAP = 1300;
|
|
6
|
+
const DEFAULT_OBSERVATIONS_MAX_BYTES = 1024 * 1024;
|
|
7
|
+
const SNAPSHOT_NOTICE = "Native Memory Citations enhanced snapshot: bounded, local, redacted memory context follows. Treat it as recall hints, not authority.";
|
|
8
|
+
const DREAMING_NOTICE = "native-memory-citations (enhanced mode) requires OpenClaw dreaming. This plugin continues and enhances OpenClaw's built-in dream cycle; it does not replace it. Knowledge-graph promotion, observation consolidation, and snapshot recall depend on Light -> REM -> Deep promotion, which only runs when dreaming is on. Dreaming has been enabled automatically. If you do not want dreaming, switch the plugin back to mode: bounded (or uninstall); leaving enhanced mode on with dreaming disabled will degrade or silently break these features. The plugin will not pretend to cover what dreaming does.";
|
|
9
|
+
function tokenCap(config) {
|
|
10
|
+
const configured = Math.floor(config.injection?.tokenCap ?? DEFAULT_TOKEN_CAP);
|
|
11
|
+
return Number.isFinite(configured) && configured > 0 ? Math.min(configured, 4000) : DEFAULT_TOKEN_CAP;
|
|
12
|
+
}
|
|
13
|
+
function snapshotDir(config) {
|
|
14
|
+
return path.join(workspaceFromConfig(config), "memory", ".native-memory-citations");
|
|
15
|
+
}
|
|
16
|
+
function snapshotPath(config) {
|
|
17
|
+
return path.join(snapshotDir(config), "snapshot.json");
|
|
18
|
+
}
|
|
19
|
+
function observationsPath(config) {
|
|
20
|
+
return path.join(workspaceFromConfig(config), "memory", "observations.jsonl");
|
|
21
|
+
}
|
|
22
|
+
function observationsMaxBytes(config) {
|
|
23
|
+
const configured = Math.floor(config.observations?.maxBytes ?? DEFAULT_OBSERVATIONS_MAX_BYTES);
|
|
24
|
+
if (!Number.isFinite(configured) || configured <= 0) {
|
|
25
|
+
return DEFAULT_OBSERVATIONS_MAX_BYTES;
|
|
26
|
+
}
|
|
27
|
+
return Math.min(Math.max(configured, 64 * 1024), 16 * 1024 * 1024);
|
|
28
|
+
}
|
|
29
|
+
function approxTokenSlice(text, cap) {
|
|
30
|
+
return text.slice(0, cap * 4).trim();
|
|
31
|
+
}
|
|
32
|
+
function requiresSnapshot(config) {
|
|
33
|
+
return config.injection?.enabled === true || config.recall?.snapshotFirst === true;
|
|
34
|
+
}
|
|
35
|
+
function hookOptions(priority, timeoutMs) {
|
|
36
|
+
return {
|
|
37
|
+
entry: {
|
|
38
|
+
priority,
|
|
39
|
+
timeoutMs,
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function registerHook(api, event, handler, opts) {
|
|
44
|
+
if (typeof api.on === "function") {
|
|
45
|
+
api.on(event, handler, opts);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
if (typeof api.registerHook === "function") {
|
|
49
|
+
api.registerHook(event, handler, opts);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function memoryCoreEntry(draft) {
|
|
53
|
+
const plugins = typeof draft.plugins === "object" && draft.plugins !== null
|
|
54
|
+
? draft.plugins
|
|
55
|
+
: {};
|
|
56
|
+
const entries = typeof plugins.entries === "object" && plugins.entries !== null
|
|
57
|
+
? plugins.entries
|
|
58
|
+
: {};
|
|
59
|
+
const entry = typeof entries["memory-core"] === "object" && entries["memory-core"] !== null
|
|
60
|
+
? entries["memory-core"]
|
|
61
|
+
: {};
|
|
62
|
+
plugins.entries = entries;
|
|
63
|
+
entries["memory-core"] = entry;
|
|
64
|
+
draft.plugins = plugins;
|
|
65
|
+
return entry;
|
|
66
|
+
}
|
|
67
|
+
function setDreamingEnabledOnConfig(draft) {
|
|
68
|
+
const entry = memoryCoreEntry(draft);
|
|
69
|
+
const config = typeof entry.config === "object" && entry.config !== null
|
|
70
|
+
? entry.config
|
|
71
|
+
: {};
|
|
72
|
+
const dreaming = typeof config.dreaming === "object" && config.dreaming !== null
|
|
73
|
+
? config.dreaming
|
|
74
|
+
: {};
|
|
75
|
+
dreaming.enabled = true;
|
|
76
|
+
config.dreaming = dreaming;
|
|
77
|
+
entry.config = config;
|
|
78
|
+
}
|
|
79
|
+
async function buildSnapshot(config, logger) {
|
|
80
|
+
const workspace = workspaceFromConfig(config);
|
|
81
|
+
const parts = [];
|
|
82
|
+
for (const source of ["MEMORY.md", "DREAMS.md"]) {
|
|
83
|
+
const text = await readFile(path.join(workspace, source), "utf8").catch(() => "");
|
|
84
|
+
if (text.trim()) {
|
|
85
|
+
parts.push(`## ${source}\n${text.trim()}`);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
const content = approxTokenSlice(parts.join("\n\n"), tokenCap(config));
|
|
89
|
+
await mkdir(snapshotDir(config), { recursive: true });
|
|
90
|
+
await writeFile(snapshotPath(config), `${JSON.stringify({
|
|
91
|
+
createdAt: new Date().toISOString(),
|
|
92
|
+
tokenCap: tokenCap(config),
|
|
93
|
+
content,
|
|
94
|
+
})}\n`, "utf8");
|
|
95
|
+
logger?.debug?.(`native-memory-citations: refreshed enhanced snapshot (${content.length} chars)`);
|
|
96
|
+
}
|
|
97
|
+
async function readSnapshot(config) {
|
|
98
|
+
const raw = await readFile(snapshotPath(config), "utf8").catch(() => "");
|
|
99
|
+
if (!raw.trim()) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
const parsed = JSON.parse(raw);
|
|
103
|
+
return typeof parsed.content === "string" && parsed.content.trim() ? parsed.content.trim() : undefined;
|
|
104
|
+
}
|
|
105
|
+
async function appendObservation(config, event) {
|
|
106
|
+
const file = observationsPath(config);
|
|
107
|
+
const record = {
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
turn: event && typeof event === "object" && "runId" in event ? String(event.runId ?? "") : "",
|
|
110
|
+
type: config.observations?.extraction === false ? "raw" : "none",
|
|
111
|
+
content: config.observations?.extraction === false ? JSON.stringify(event).slice(0, 4000) : "",
|
|
112
|
+
confidence: config.observations?.extraction === false ? 0.2 : 0,
|
|
113
|
+
sources: [],
|
|
114
|
+
action: config.observations?.extraction === false ? "ADD" : "NONE",
|
|
115
|
+
};
|
|
116
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
117
|
+
await appendFile(file, `${JSON.stringify(record)}\n`, "utf8");
|
|
118
|
+
await enforceObservationLimit(file, observationsMaxBytes(config));
|
|
119
|
+
}
|
|
120
|
+
async function enforceObservationLimit(file, maxBytes) {
|
|
121
|
+
const size = (await stat(file).catch(() => null))?.size ?? 0;
|
|
122
|
+
if (size <= maxBytes) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const content = await readFile(file, "utf8");
|
|
126
|
+
if (Buffer.byteLength(content, "utf8") <= maxBytes) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let retained = Buffer.from(content, "utf8").subarray(-maxBytes).toString("utf8");
|
|
130
|
+
const firstNewline = retained.indexOf("\n");
|
|
131
|
+
if (firstNewline >= 0) {
|
|
132
|
+
retained = retained.slice(firstNewline + 1);
|
|
133
|
+
}
|
|
134
|
+
await writeFile(file, retained, "utf8");
|
|
135
|
+
}
|
|
136
|
+
function hostDreamingEnabled(api) {
|
|
137
|
+
const current = (typeof api.runtime?.config?.current === "function" ? api.runtime.config.current() : api.config);
|
|
138
|
+
if (current?.memory?.dreaming?.enabled === true) {
|
|
139
|
+
return true;
|
|
140
|
+
}
|
|
141
|
+
const memoryCore = current?.plugins?.entries?.["memory-core"];
|
|
142
|
+
if (memoryCore && typeof memoryCore === "object") {
|
|
143
|
+
return memoryCore.config?.dreaming?.enabled === true;
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
async function runDreamingGuard(api, config) {
|
|
148
|
+
if (modeFromConfig(config) !== "enhanced" || config.dreaming?.enforce === false) {
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (hostDreamingEnabled(api)) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
if (config.dreaming?.autoEnable === false || typeof api.runtime?.config?.mutateConfigFile !== "function") {
|
|
155
|
+
api.logger?.warn?.("native-memory-citations enhanced mode is enabled, but OpenClaw memory-core dreaming is not true.");
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
await api.runtime.config.mutateConfigFile({
|
|
159
|
+
afterWrite: { mode: "auto" },
|
|
160
|
+
mutate: setDreamingEnabledOnConfig,
|
|
161
|
+
});
|
|
162
|
+
api.logger?.warn?.(DREAMING_NOTICE);
|
|
163
|
+
}
|
|
164
|
+
export function registerEnhancedLifecycle(api) {
|
|
165
|
+
const config = api.pluginConfig ?? {};
|
|
166
|
+
if (modeFromConfig(config) !== "enhanced") {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
void runDreamingGuard(api, config).catch((error) => {
|
|
170
|
+
api.logger?.warn?.(`native-memory-citations dreaming guard failed open: ${String(error)}`);
|
|
171
|
+
});
|
|
172
|
+
registerHook(api, "cron_changed", () => {
|
|
173
|
+
void runDreamingGuard(api, config).catch((error) => {
|
|
174
|
+
api.logger?.warn?.(`native-memory-citations dreaming guard failed open: ${String(error)}`);
|
|
175
|
+
});
|
|
176
|
+
}, hookOptions(100, 3000));
|
|
177
|
+
if (requiresSnapshot(config)) {
|
|
178
|
+
registerHook(api, "session_start", async () => {
|
|
179
|
+
await buildSnapshot(config, api.logger);
|
|
180
|
+
}, hookOptions(90, 5000));
|
|
181
|
+
}
|
|
182
|
+
if (config.injection?.enabled === true) {
|
|
183
|
+
registerHook(api, "before_prompt_build", async () => {
|
|
184
|
+
const snapshot = await readSnapshot(config);
|
|
185
|
+
if (!snapshot) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
prependContext: `${SNAPSHOT_NOTICE}\n\n${snapshot}`,
|
|
190
|
+
};
|
|
191
|
+
}, hookOptions(80, 5000));
|
|
192
|
+
}
|
|
193
|
+
if (config.observations?.enabled === true) {
|
|
194
|
+
registerHook(api, "agent_end", (event) => {
|
|
195
|
+
void appendObservation(config, event).catch((error) => {
|
|
196
|
+
api.logger?.warn?.(`native-memory-citations observation tagging failed open: ${String(error)}`);
|
|
197
|
+
});
|
|
198
|
+
}, hookOptions(10, 3000));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
export const enhancedLifecycleForTest = {
|
|
202
|
+
DREAMING_NOTICE,
|
|
203
|
+
PLUGIN_ID,
|
|
204
|
+
setDreamingEnabledOnConfig,
|
|
205
|
+
};
|
package/dist/health.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function registerNativeMemoryHealthChecks(): void;
|
package/dist/health.js
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { readFile, readdir, stat } from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { registerHealthCheck } from "openclaw/plugin-sdk/health";
|
|
5
|
+
import { graphEnabled, modeFromConfig, workspaceFromConfig } from "./core.js";
|
|
6
|
+
const PLUGIN_ID = "native-memory-citations";
|
|
7
|
+
const EXPECTED_TOOLS = [
|
|
8
|
+
"native_memory_answer",
|
|
9
|
+
"native_memory_extract",
|
|
10
|
+
"native_memory_fetch",
|
|
11
|
+
"native_memory_graph",
|
|
12
|
+
"native_memory_search",
|
|
13
|
+
];
|
|
14
|
+
let registered = false;
|
|
15
|
+
function memoryDreamingEnabled(cfg) {
|
|
16
|
+
const record = cfg;
|
|
17
|
+
if (record.memory?.dreaming?.enabled === true) {
|
|
18
|
+
return true;
|
|
19
|
+
}
|
|
20
|
+
const memoryCore = record.plugins?.entries?.["memory-core"];
|
|
21
|
+
if (memoryCore && typeof memoryCore === "object") {
|
|
22
|
+
return memoryCore.config?.dreaming?.enabled === true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
function pluginConfigFromOpenClawConfig(cfg) {
|
|
27
|
+
const record = cfg;
|
|
28
|
+
const entry = record.plugins?.entries?.[PLUGIN_ID];
|
|
29
|
+
if (entry && typeof entry === "object") {
|
|
30
|
+
const maybeConfig = entry.config;
|
|
31
|
+
if (maybeConfig && typeof maybeConfig === "object") {
|
|
32
|
+
return maybeConfig;
|
|
33
|
+
}
|
|
34
|
+
return entry;
|
|
35
|
+
}
|
|
36
|
+
return {};
|
|
37
|
+
}
|
|
38
|
+
async function manifestTools() {
|
|
39
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
40
|
+
const manifestPath = path.resolve(here, "..", "openclaw.plugin.json");
|
|
41
|
+
const manifest = JSON.parse(await readFile(manifestPath, "utf8"));
|
|
42
|
+
return manifest.contracts?.tools ?? [];
|
|
43
|
+
}
|
|
44
|
+
async function collectMemoryMtimes(root) {
|
|
45
|
+
const info = await stat(root).catch(() => null);
|
|
46
|
+
if (!info) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
if (info.isFile()) {
|
|
50
|
+
return [info.mtimeMs];
|
|
51
|
+
}
|
|
52
|
+
if (!info.isDirectory()) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
const entries = await readdir(root, { withFileTypes: true }).catch(() => []);
|
|
56
|
+
const mtimes = [];
|
|
57
|
+
for (const entry of entries) {
|
|
58
|
+
if (entry.name.startsWith(".") || entry.name === "node_modules") {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const child = path.join(root, entry.name);
|
|
62
|
+
if (entry.isDirectory()) {
|
|
63
|
+
mtimes.push(...await collectMemoryMtimes(child));
|
|
64
|
+
}
|
|
65
|
+
else if (entry.isFile()) {
|
|
66
|
+
const childInfo = await stat(child).catch(() => null);
|
|
67
|
+
if (childInfo) {
|
|
68
|
+
mtimes.push(childInfo.mtimeMs);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return mtimes;
|
|
73
|
+
}
|
|
74
|
+
export function registerNativeMemoryHealthChecks() {
|
|
75
|
+
if (registered) {
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
registered = true;
|
|
79
|
+
registerHealthCheck({
|
|
80
|
+
id: "native-memory-citations/manifest-tools",
|
|
81
|
+
kind: "plugin",
|
|
82
|
+
source: PLUGIN_ID,
|
|
83
|
+
description: "Native Memory Citations generated manifest lists the registered tools.",
|
|
84
|
+
async detect() {
|
|
85
|
+
const tools = await manifestTools().catch(() => []);
|
|
86
|
+
const missing = EXPECTED_TOOLS.filter((tool) => !tools.includes(tool));
|
|
87
|
+
if (missing.length === 0) {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
return [{
|
|
91
|
+
checkId: "native-memory-citations/manifest-tools",
|
|
92
|
+
severity: "error",
|
|
93
|
+
message: `Generated manifest is missing tools: ${missing.join(", ")}`,
|
|
94
|
+
source: PLUGIN_ID,
|
|
95
|
+
fixHint: "Run npm run plugin:build and commit the regenerated openclaw.plugin.json.",
|
|
96
|
+
}];
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
registerHealthCheck({
|
|
100
|
+
id: "native-memory-citations/dreaming-required",
|
|
101
|
+
kind: "plugin",
|
|
102
|
+
source: PLUGIN_ID,
|
|
103
|
+
description: "Enhanced mode warns when OpenClaw dreaming is disabled.",
|
|
104
|
+
async detect(ctx) {
|
|
105
|
+
const config = pluginConfigFromOpenClawConfig(ctx.cfg);
|
|
106
|
+
if (modeFromConfig(config) !== "enhanced") {
|
|
107
|
+
return [];
|
|
108
|
+
}
|
|
109
|
+
if (memoryDreamingEnabled(ctx.cfg)) {
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
return [{
|
|
113
|
+
checkId: "native-memory-citations/dreaming-required",
|
|
114
|
+
severity: config.dreaming?.blockToolsWhenOff ? "error" : "warning",
|
|
115
|
+
message: "native-memory-citations enhanced mode is enabled but OpenClaw memory-core dreaming is not true.",
|
|
116
|
+
source: PLUGIN_ID,
|
|
117
|
+
ocPath: "plugins.entries.memory-core.config.dreaming.enabled",
|
|
118
|
+
fixHint: "Enable OpenClaw dreaming or switch native-memory-citations back to mode: bounded.",
|
|
119
|
+
}];
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
registerHealthCheck({
|
|
123
|
+
id: "native-memory-citations/graph-fresh",
|
|
124
|
+
kind: "plugin",
|
|
125
|
+
source: PLUGIN_ID,
|
|
126
|
+
description: "Enhanced graph sidecar exists and is fresh enough for configured memory files.",
|
|
127
|
+
async detect(ctx) {
|
|
128
|
+
const config = pluginConfigFromOpenClawConfig(ctx.cfg);
|
|
129
|
+
if (!graphEnabled(config)) {
|
|
130
|
+
return [];
|
|
131
|
+
}
|
|
132
|
+
const workspace = workspaceFromConfig(config);
|
|
133
|
+
const graphPath = path.join(workspace, "memory", "graph.jsonl");
|
|
134
|
+
const graphInfo = await stat(graphPath).catch(() => null);
|
|
135
|
+
if (!graphInfo?.isFile()) {
|
|
136
|
+
return [{
|
|
137
|
+
checkId: "native-memory-citations/graph-fresh",
|
|
138
|
+
severity: "warning",
|
|
139
|
+
message: "Enhanced graph mode is enabled but memory/graph.jsonl does not exist yet.",
|
|
140
|
+
source: PLUGIN_ID,
|
|
141
|
+
path: graphPath,
|
|
142
|
+
fixHint: "Run native_memory_extract to build the graph sidecar.",
|
|
143
|
+
}];
|
|
144
|
+
}
|
|
145
|
+
const memoryMtimes = [
|
|
146
|
+
...await collectMemoryMtimes(path.join(workspace, "memory")),
|
|
147
|
+
...await collectMemoryMtimes(path.join(workspace, "MEMORY.md")),
|
|
148
|
+
];
|
|
149
|
+
const newestMemory = Math.max(0, ...memoryMtimes);
|
|
150
|
+
const findings = [];
|
|
151
|
+
if (newestMemory > graphInfo.mtimeMs + 1000) {
|
|
152
|
+
findings.push({
|
|
153
|
+
checkId: "native-memory-citations/graph-fresh",
|
|
154
|
+
severity: "warning",
|
|
155
|
+
message: "memory/graph.jsonl is older than at least one memory source file.",
|
|
156
|
+
source: PLUGIN_ID,
|
|
157
|
+
path: graphPath,
|
|
158
|
+
fixHint: "Run native_memory_extract to refresh the graph sidecar.",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
return findings;
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const
|
|
2
|
-
export default
|
|
1
|
+
declare const plugin: import("openclaw/plugin-sdk/tool-plugin").DefinedToolPluginEntry;
|
|
2
|
+
export default plugin;
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,88 @@
|
|
|
1
|
-
import { Type } from "typebox";
|
|
2
1
|
import { defineToolPlugin } from "openclaw/plugin-sdk/tool-plugin";
|
|
3
|
-
import { answerFromMemory, fetchMemorySource, searchMemory } from "./core.js";
|
|
2
|
+
import { answerFromMemory, extractMemoryGraph, fetchMemorySource, modeFromConfig, queryMemoryGraph, searchMemory, } from "./core.js";
|
|
3
|
+
import { registerEnhancedLifecycle } from "./enhanced.js";
|
|
4
|
+
import { registerNativeMemoryHealthChecks } from "./health.js";
|
|
5
|
+
registerNativeMemoryHealthChecks();
|
|
6
|
+
function objectSchema(properties, required = []) {
|
|
7
|
+
return {
|
|
8
|
+
type: "object",
|
|
9
|
+
properties,
|
|
10
|
+
...(required.length > 0 ? { required } : {}),
|
|
11
|
+
additionalProperties: false,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function stringSchema(description) {
|
|
15
|
+
return { type: "string", description };
|
|
16
|
+
}
|
|
17
|
+
function numberSchema(description) {
|
|
18
|
+
return { type: "number", description };
|
|
19
|
+
}
|
|
20
|
+
function booleanSchema(description) {
|
|
21
|
+
return { type: "boolean", description };
|
|
22
|
+
}
|
|
23
|
+
function stringUnionSchema(values, description) {
|
|
24
|
+
return {
|
|
25
|
+
anyOf: values.map((value) => ({ type: "string", const: value })),
|
|
26
|
+
...(description ? { description } : {}),
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
const graphEdgeType = stringUnionSchema([
|
|
30
|
+
"works_at",
|
|
31
|
+
"invested_in",
|
|
32
|
+
"founded",
|
|
33
|
+
"advises",
|
|
34
|
+
"attended",
|
|
35
|
+
"mentions",
|
|
36
|
+
]);
|
|
4
37
|
// Declared config schema. Without this, OpenClaw applies a strict empty-object
|
|
5
38
|
// schema and rejects every config key below. The generated manifest is produced
|
|
6
39
|
// from this by `openclaw plugins build`; do not hand-edit it.
|
|
7
|
-
const configSchema =
|
|
8
|
-
workspace:
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
40
|
+
const configSchema = objectSchema({
|
|
41
|
+
workspace: stringSchema("Absolute path to the OpenClaw workspace. Defaults to $OPENCLAW_WORKSPACE or ~/.openclaw/workspace."),
|
|
42
|
+
allowedRoots: {
|
|
43
|
+
type: "array",
|
|
44
|
+
items: { type: "string" },
|
|
12
45
|
description: "Workspace-relative memory roots to search. Overrides the built-in default set.",
|
|
13
|
-
}
|
|
14
|
-
sharedMode:
|
|
15
|
-
maxFileBytes:
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
46
|
+
},
|
|
47
|
+
sharedMode: booleanSchema("When true, exclude the private MEMORY.md from the default root set."),
|
|
48
|
+
maxFileBytes: numberSchema("Per-file size cap in bytes. Files larger than this are skipped. Default 1048576."),
|
|
49
|
+
mode: stringUnionSchema(["bounded", "enhanced"], "Operating mode. bounded is default and preserves 2026.6.x behavior; enhanced enables explicitly configured pillars."),
|
|
50
|
+
dreaming: objectSchema({
|
|
51
|
+
autoEnable: booleanSchema("In enhanced mode, allow the dreaming guard to enable OpenClaw dreaming."),
|
|
52
|
+
enforce: booleanSchema("In enhanced mode, warn when dreaming-dependent features run without host dreaming."),
|
|
53
|
+
blockToolsWhenOff: booleanSchema("In enhanced mode, make dreaming-dependent tools fail hard when dreaming is off."),
|
|
54
|
+
}),
|
|
55
|
+
graph: objectSchema({
|
|
56
|
+
enabled: booleanSchema("Enable the local zero-LLM graph sidecar tools. Default false."),
|
|
57
|
+
edgeTypes: {
|
|
58
|
+
type: "array",
|
|
59
|
+
items: graphEdgeType,
|
|
60
|
+
description: "Typed graph edges to extract.",
|
|
61
|
+
},
|
|
62
|
+
maxDepth: numberSchema("Maximum traversal depth for native_memory_graph. Default 3."),
|
|
63
|
+
}),
|
|
64
|
+
recall: objectSchema({
|
|
65
|
+
semantic: booleanSchema("Enhanced mode placeholder for host semantic recall fusion. Default false."),
|
|
66
|
+
rerank: booleanSchema("Enhanced mode placeholder for reranking fused candidates. Default false."),
|
|
67
|
+
snapshotFirst: booleanSchema("Enhanced mode placeholder for tier-0 snapshot recall. Default false."),
|
|
68
|
+
intentClassifier: booleanSchema("Enhanced mode placeholder for intent classification. Default false."),
|
|
69
|
+
}),
|
|
70
|
+
injection: objectSchema({
|
|
71
|
+
enabled: booleanSchema("Enhanced mode placeholder for snapshot prompt injection. Default false."),
|
|
72
|
+
tokenCap: numberSchema("Maximum injected snapshot budget. Default 1300."),
|
|
73
|
+
}),
|
|
74
|
+
observations: objectSchema({
|
|
75
|
+
enabled: booleanSchema("Enhanced mode placeholder for observation tagging. Default false."),
|
|
76
|
+
model: stringSchema("Optional host model profile for future observation extraction. When omitted, use the host configured summarization or fast model."),
|
|
77
|
+
extraction: booleanSchema("When false, observation tagging uses raw append fallback. Default true."),
|
|
78
|
+
maxBytes: numberSchema("Maximum retained observations.jsonl size in bytes. Default 1048576."),
|
|
79
|
+
}),
|
|
80
|
+
wikiBridge: objectSchema({
|
|
81
|
+
enabled: booleanSchema("Enable optional memory-wiki bridge when present. Default false."),
|
|
82
|
+
}),
|
|
83
|
+
});
|
|
84
|
+
const ENHANCED_TOOL_NAMES = new Set(["native_memory_graph", "native_memory_extract"]);
|
|
85
|
+
const plugin = defineToolPlugin({
|
|
20
86
|
id: "native-memory-citations",
|
|
21
87
|
name: "Native Memory Citations",
|
|
22
88
|
description: "Search and fetch local OpenClaw memory with source citations and extractive cited answers.",
|
|
@@ -26,29 +92,34 @@ export default defineToolPlugin({
|
|
|
26
92
|
name: "native_memory_search",
|
|
27
93
|
label: "Memory Search (cited)",
|
|
28
94
|
description: "Search approved local memory roots and return snippets with source paths and line numbers.",
|
|
29
|
-
parameters:
|
|
30
|
-
query:
|
|
31
|
-
limit:
|
|
32
|
-
contextLines:
|
|
33
|
-
}),
|
|
34
|
-
execute: async (
|
|
95
|
+
parameters: objectSchema({
|
|
96
|
+
query: stringSchema("Search query."),
|
|
97
|
+
limit: numberSchema("Maximum hits to return. Default 8, max 50."),
|
|
98
|
+
contextLines: numberSchema("Context lines around each hit. Default 2, max 8."),
|
|
99
|
+
}, ["query"]),
|
|
100
|
+
execute: async (input, config, context) => {
|
|
101
|
+
const { query, limit, contextLines } = input;
|
|
35
102
|
context.signal?.throwIfAborted();
|
|
36
|
-
return searchMemory(query, {
|
|
103
|
+
return searchMemory(query, {
|
|
104
|
+
limit,
|
|
105
|
+
contextLines,
|
|
106
|
+
config: config,
|
|
107
|
+
signal: context.signal,
|
|
108
|
+
logger: context.api.logger,
|
|
109
|
+
});
|
|
37
110
|
},
|
|
38
111
|
}),
|
|
39
112
|
tool({
|
|
40
113
|
name: "native_memory_fetch",
|
|
41
114
|
label: "Memory Fetch (cited)",
|
|
42
115
|
description: "Fetch cited local memory content by sourceId/path and optional line range.",
|
|
43
|
-
parameters:
|
|
44
|
-
sourceId:
|
|
45
|
-
filePath:
|
|
46
|
-
lineStart:
|
|
47
|
-
lineEnd:
|
|
48
|
-
maxChars:
|
|
49
|
-
expectedSha256:
|
|
50
|
-
description: "Optional SHA-256 from a prior citation. When it differs from the current file hash, the result is marked stale.",
|
|
51
|
-
})),
|
|
116
|
+
parameters: objectSchema({
|
|
117
|
+
sourceId: stringSchema("Source id returned by native_memory_search."),
|
|
118
|
+
filePath: stringSchema("Workspace-relative path inside an allowed memory root."),
|
|
119
|
+
lineStart: numberSchema("1-based starting line."),
|
|
120
|
+
lineEnd: numberSchema("1-based ending line."),
|
|
121
|
+
maxChars: numberSchema("Maximum characters returned. Default 8000."),
|
|
122
|
+
expectedSha256: stringSchema("Optional SHA-256 from a prior citation. When it differs from the current file hash, the result is marked stale."),
|
|
52
123
|
}),
|
|
53
124
|
execute: async (input, config, context) => {
|
|
54
125
|
context.signal?.throwIfAborted();
|
|
@@ -59,14 +130,66 @@ export default defineToolPlugin({
|
|
|
59
130
|
name: "native_memory_answer",
|
|
60
131
|
label: "Memory Answer (cited)",
|
|
61
132
|
description: "Answer from approved local memory using extractive snippets with citations. Says when no cited memory is found.",
|
|
62
|
-
parameters:
|
|
63
|
-
query:
|
|
64
|
-
limit:
|
|
65
|
-
}),
|
|
66
|
-
execute: async (
|
|
133
|
+
parameters: objectSchema({
|
|
134
|
+
query: stringSchema("Question to answer from local memory."),
|
|
135
|
+
limit: numberSchema("Maximum cited search hits to consider. Default 6."),
|
|
136
|
+
}, ["query"]),
|
|
137
|
+
execute: async (input, config, context) => {
|
|
138
|
+
const { query, limit } = input;
|
|
139
|
+
context.signal?.throwIfAborted();
|
|
140
|
+
return answerFromMemory(query, {
|
|
141
|
+
limit,
|
|
142
|
+
config: config,
|
|
143
|
+
signal: context.signal,
|
|
144
|
+
logger: context.api.logger,
|
|
145
|
+
});
|
|
146
|
+
},
|
|
147
|
+
}),
|
|
148
|
+
tool({
|
|
149
|
+
name: "native_memory_graph",
|
|
150
|
+
label: "Memory Graph (enhanced)",
|
|
151
|
+
description: "Query the opt-in local memory graph sidecar. Returns no data unless mode is enhanced and graph.enabled is true.",
|
|
152
|
+
optional: true,
|
|
153
|
+
parameters: objectSchema({
|
|
154
|
+
query: stringSchema("Entity or phrase to start traversal from."),
|
|
155
|
+
maxDepth: numberSchema("Maximum graph traversal depth. Default 3, max 6."),
|
|
156
|
+
}, ["query"]),
|
|
157
|
+
execute: async (input, config, context) => {
|
|
158
|
+
const { query, maxDepth } = input;
|
|
159
|
+
context.signal?.throwIfAborted();
|
|
160
|
+
return queryMemoryGraph(query, { maxDepth, config: config });
|
|
161
|
+
},
|
|
162
|
+
}),
|
|
163
|
+
tool({
|
|
164
|
+
name: "native_memory_extract",
|
|
165
|
+
label: "Memory Graph Extract (maintenance)",
|
|
166
|
+
description: "Rebuild the opt-in deterministic zero-LLM memory graph sidecar. Writes only when mode is enhanced and graph.enabled is true.",
|
|
167
|
+
optional: true,
|
|
168
|
+
parameters: objectSchema({}),
|
|
169
|
+
execute: async (_input, config, context) => {
|
|
67
170
|
context.signal?.throwIfAborted();
|
|
68
|
-
return
|
|
171
|
+
return extractMemoryGraph(config, { logger: context.api.logger });
|
|
69
172
|
},
|
|
70
173
|
}),
|
|
71
174
|
],
|
|
72
175
|
});
|
|
176
|
+
const registerAllTools = plugin.register.bind(plugin);
|
|
177
|
+
plugin.register = (api) => {
|
|
178
|
+
const boundedMode = modeFromConfig(api.pluginConfig) === "bounded";
|
|
179
|
+
const filteredApi = {
|
|
180
|
+
...api,
|
|
181
|
+
registerTool(toolDefinition, options) {
|
|
182
|
+
const name = typeof toolDefinition === "function"
|
|
183
|
+
? options?.name
|
|
184
|
+
: toolDefinition?.name;
|
|
185
|
+
if (boundedMode && name && ENHANCED_TOOL_NAMES.has(name)) {
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
return api.registerTool(toolDefinition, options);
|
|
189
|
+
},
|
|
190
|
+
};
|
|
191
|
+
const result = registerAllTools(filteredApi);
|
|
192
|
+
registerEnhancedLifecycle(api);
|
|
193
|
+
return result;
|
|
194
|
+
};
|
|
195
|
+
export default plugin;
|