@xdarkicex/openclaw-memory-libravdb 1.7.0 → 1.8.0
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 +17 -0
- package/dist/context-engine.d.ts +19 -3
- package/dist/context-engine.js +338 -0
- package/dist/index.js +1293 -42
- package/dist/libravdb-client.d.ts +4 -1
- package/dist/libravdb-client.js +12 -0
- package/dist/memory-provider.js +34 -21
- package/dist/memory-runtime.d.ts +2 -0
- package/dist/memory-runtime.js +8 -2
- package/dist/memory-tools.js +14 -0
- package/dist/tools/memory-recall.d.ts +144 -0
- package/dist/tools/memory-recall.js +421 -0
- package/dist/turn-cache.d.ts +21 -0
- package/dist/turn-cache.js +103 -0
- package/dist/types.d.ts +13 -0
- package/openclaw.plugin.json +16 -1
- package/package.json +2 -2
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
import { formatError } from "../format-error.js";
|
|
2
|
+
import { consumeSubagentBudget } from "../context-engine.js";
|
|
3
|
+
// ── Constants ──
|
|
4
|
+
const MAX_EXPAND_TOKENS = 8000;
|
|
5
|
+
const MAX_EXPAND_CHARS = MAX_EXPAND_TOKENS * 4;
|
|
6
|
+
const MAX_GREP_RESULTS = 50;
|
|
7
|
+
const MAX_GREP_CHARS = 40000;
|
|
8
|
+
const MAX_SNIPPET_CHARS = 200;
|
|
9
|
+
// ── Schemas ──
|
|
10
|
+
const MEMORY_DESCRIBE_SCHEMA = {
|
|
11
|
+
type: "object",
|
|
12
|
+
additionalProperties: false,
|
|
13
|
+
properties: {
|
|
14
|
+
summaryId: {
|
|
15
|
+
type: "string",
|
|
16
|
+
description: "A summary ID (sum_xxx format) returned by memory_search. Inspect metadata without expanding.",
|
|
17
|
+
},
|
|
18
|
+
sessionId: {
|
|
19
|
+
type: "string",
|
|
20
|
+
description: "Session ID the summary belongs to. If omitted, uses the current session.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
required: ["summaryId"],
|
|
24
|
+
};
|
|
25
|
+
const MEMORY_EXPAND_SCHEMA = {
|
|
26
|
+
type: "object",
|
|
27
|
+
additionalProperties: false,
|
|
28
|
+
properties: {
|
|
29
|
+
summaryIds: {
|
|
30
|
+
type: "array",
|
|
31
|
+
items: { type: "string" },
|
|
32
|
+
description: "Summary IDs (sum_xxx format) to expand. Use results from memory_search or memory_describe.",
|
|
33
|
+
},
|
|
34
|
+
maxDepth: {
|
|
35
|
+
type: "number",
|
|
36
|
+
minimum: 0,
|
|
37
|
+
maximum: 5,
|
|
38
|
+
description: "Max tree traversal depth per summary (default: 1). 0 returns only the cue/metadata.",
|
|
39
|
+
},
|
|
40
|
+
maxTokens: {
|
|
41
|
+
type: "number",
|
|
42
|
+
minimum: 100,
|
|
43
|
+
maximum: Number(MAX_EXPAND_TOKENS),
|
|
44
|
+
description: `Token budget cap for the expansion result (default: ${MAX_EXPAND_TOKENS}).`,
|
|
45
|
+
},
|
|
46
|
+
sessionId: {
|
|
47
|
+
type: "string",
|
|
48
|
+
description: "Session ID the summary belongs to. If omitted, uses the current session.",
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
required: ["summaryIds"],
|
|
52
|
+
};
|
|
53
|
+
const MEMORY_GREP_SCHEMA = {
|
|
54
|
+
type: "object",
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
properties: {
|
|
57
|
+
pattern: {
|
|
58
|
+
type: "string",
|
|
59
|
+
description: "Search pattern. Regex when mode=regex, plain text when mode=text.",
|
|
60
|
+
},
|
|
61
|
+
mode: {
|
|
62
|
+
type: "string",
|
|
63
|
+
enum: ["regex", "text"],
|
|
64
|
+
description: 'Search mode. Default: "text".',
|
|
65
|
+
},
|
|
66
|
+
scope: {
|
|
67
|
+
type: "string",
|
|
68
|
+
enum: ["messages", "summaries", "both"],
|
|
69
|
+
description: 'What to search. Default: "both".',
|
|
70
|
+
},
|
|
71
|
+
limit: {
|
|
72
|
+
type: "number",
|
|
73
|
+
minimum: 1,
|
|
74
|
+
maximum: 200,
|
|
75
|
+
description: `Max results (default: ${MAX_GREP_RESULTS}).`,
|
|
76
|
+
},
|
|
77
|
+
sessionId: {
|
|
78
|
+
type: "string",
|
|
79
|
+
description: "Session ID to search within. If omitted, uses the current session.",
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
required: ["pattern"],
|
|
83
|
+
};
|
|
84
|
+
// ── Helpers ──
|
|
85
|
+
function truncateSnippet(text, maxLen = MAX_SNIPPET_CHARS) {
|
|
86
|
+
const singleLine = text.replace(/\n/g, " ").trim();
|
|
87
|
+
if (singleLine.length <= maxLen)
|
|
88
|
+
return singleLine;
|
|
89
|
+
return singleLine.slice(0, maxLen - 3) + "...";
|
|
90
|
+
}
|
|
91
|
+
function jsonResult(details) {
|
|
92
|
+
return { content: [{ type: "text", text: JSON.stringify(details, null, 2) }], details };
|
|
93
|
+
}
|
|
94
|
+
function asParams(value) {
|
|
95
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
96
|
+
return {};
|
|
97
|
+
return value;
|
|
98
|
+
}
|
|
99
|
+
function readStr(params, key) {
|
|
100
|
+
const v = params[key];
|
|
101
|
+
if (typeof v !== "string")
|
|
102
|
+
return undefined;
|
|
103
|
+
const t = v.trim();
|
|
104
|
+
return t.length > 0 ? t : undefined;
|
|
105
|
+
}
|
|
106
|
+
function readNum(params, key, opts) {
|
|
107
|
+
const v = params[key];
|
|
108
|
+
const n = typeof v === "number" ? v : typeof v === "string" && v.trim() ? Number(v) : undefined;
|
|
109
|
+
if (n === undefined || !Number.isFinite(n))
|
|
110
|
+
return undefined;
|
|
111
|
+
const min = opts?.min ?? 1;
|
|
112
|
+
return opts?.integer ? Math.max(min, Math.floor(n)) : n;
|
|
113
|
+
}
|
|
114
|
+
function formatEvictionCueLine(cue, summaryId) {
|
|
115
|
+
if (!cue)
|
|
116
|
+
return `[Summary ${summaryId}]`;
|
|
117
|
+
const firstLine = cue.split("\n")[0] ?? "";
|
|
118
|
+
return `[Summary ${summaryId}]: ${firstLine}`;
|
|
119
|
+
}
|
|
120
|
+
function safeMatch(text, pattern, mode) {
|
|
121
|
+
if (mode === "text")
|
|
122
|
+
return text.toLowerCase().includes(pattern.toLowerCase());
|
|
123
|
+
try {
|
|
124
|
+
return new RegExp(pattern, "i").test(text);
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return text.toLowerCase().includes(pattern.toLowerCase());
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// ── Tool factories ──
|
|
131
|
+
export function createMemoryDescribeTool(getClient, logger = console) {
|
|
132
|
+
return {
|
|
133
|
+
name: "memory_describe",
|
|
134
|
+
label: "Memory Describe",
|
|
135
|
+
description: "Inspect a summary's metadata without expanding its full text. " +
|
|
136
|
+
"Returns eviction cues (anchors, decisions, constraints, signal counts), " +
|
|
137
|
+
"child summary count, and source turn range. Use this before memory_expand " +
|
|
138
|
+
"to decide whether the summary is worth the expansion cost.",
|
|
139
|
+
parameters: MEMORY_DESCRIBE_SCHEMA,
|
|
140
|
+
execute: async (_toolCallId, rawParams) => {
|
|
141
|
+
const params = asParams(rawParams);
|
|
142
|
+
const summaryId = readStr(params, "summaryId");
|
|
143
|
+
if (!summaryId)
|
|
144
|
+
throw new Error("memory_describe requires summaryId");
|
|
145
|
+
try {
|
|
146
|
+
const client = await getClient();
|
|
147
|
+
const sessionId = readStr(params, "sessionId") ?? "";
|
|
148
|
+
// Use ExpandSummary with maxDepth=0 to get metadata without expanding children.
|
|
149
|
+
// maxDepth=0 returns just the target summary's text + metadata_json.
|
|
150
|
+
const resp = await client.expandSummary({
|
|
151
|
+
sessionId,
|
|
152
|
+
summaryId,
|
|
153
|
+
maxDepth: 0,
|
|
154
|
+
});
|
|
155
|
+
let evictionCue;
|
|
156
|
+
let meta = {};
|
|
157
|
+
if (resp.metadataJson && resp.metadataJson.length > 0) {
|
|
158
|
+
try {
|
|
159
|
+
const decoder = new TextDecoder();
|
|
160
|
+
meta = JSON.parse(decoder.decode(resp.metadataJson));
|
|
161
|
+
evictionCue = typeof meta.eviction_cue === "string" ? meta.eviction_cue : undefined;
|
|
162
|
+
}
|
|
163
|
+
catch { /* metadata parse best-effort */ }
|
|
164
|
+
}
|
|
165
|
+
const lineage = (meta.continuity_lineage ?? {});
|
|
166
|
+
const sourceTurnIds = Array.isArray(lineage.source_turn_ids) ? lineage.source_turn_ids : [];
|
|
167
|
+
const parentSummaryIds = Array.isArray(lineage.parent_summary_ids) ? lineage.parent_summary_ids : [];
|
|
168
|
+
return jsonResult({
|
|
169
|
+
summaryId,
|
|
170
|
+
found: true,
|
|
171
|
+
evictionCue,
|
|
172
|
+
depth: typeof meta.compaction_generation === "number" ? meta.compaction_generation : undefined,
|
|
173
|
+
descendantCount: typeof meta.descendant_count === "number" ? meta.descendant_count : undefined,
|
|
174
|
+
sourceTurnCount: sourceTurnIds.length,
|
|
175
|
+
sourceTurnIds: sourceTurnIds.slice(0, 10),
|
|
176
|
+
parentSummaryIds: parentSummaryIds.slice(0, 10),
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
logger.warn?.(`memory_describe failed: ${formatError(error)}`);
|
|
181
|
+
return jsonResult({
|
|
182
|
+
summaryId,
|
|
183
|
+
found: false,
|
|
184
|
+
error: formatError(error),
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
export function createMemoryExpandTool(getClient, getSessionKey, logger = console) {
|
|
191
|
+
return {
|
|
192
|
+
name: "memory_expand",
|
|
193
|
+
label: "Memory Expand",
|
|
194
|
+
description: "Expand compacted summaries to recover full detail. Walks the summary tree " +
|
|
195
|
+
"up to maxDepth levels. For large expansions (>2500 tokens), spawns a " +
|
|
196
|
+
"sub-agent to protect context. Use memory_describe first to check if expansion " +
|
|
197
|
+
"is warranted — many questions can be answered from the eviction cue alone.",
|
|
198
|
+
parameters: MEMORY_EXPAND_SCHEMA,
|
|
199
|
+
execute: async (_toolCallId, rawParams) => {
|
|
200
|
+
const params = asParams(rawParams);
|
|
201
|
+
const rawIds = params.summaryIds;
|
|
202
|
+
const summaryIds = Array.isArray(rawIds) ? rawIds.filter((v) => typeof v === "string" && v.trim().length > 0) : [];
|
|
203
|
+
if (summaryIds.length === 0)
|
|
204
|
+
throw new Error("memory_expand requires at least one summaryId");
|
|
205
|
+
const maxDepth = readNum(params, "maxDepth", { integer: true, min: 0 }) ?? 1;
|
|
206
|
+
const maxTokens = readNum(params, "maxTokens", { integer: true }) ?? MAX_EXPAND_TOKENS;
|
|
207
|
+
const sessionId = readStr(params, "sessionId") ?? "";
|
|
208
|
+
// Subagent budget gate: if this is a subagent, check remaining expansion budget.
|
|
209
|
+
const sessionKey = getSessionKey();
|
|
210
|
+
if (sessionKey) {
|
|
211
|
+
const remaining = consumeSubagentBudget(sessionKey, maxTokens);
|
|
212
|
+
if (remaining === 0) {
|
|
213
|
+
return {
|
|
214
|
+
content: [{ type: "text", text: "[Subagent expansion budget exhausted. Narrow the query or request fewer summaries.]" }],
|
|
215
|
+
details: { summaryId: summaryIds[0] ?? "", depth: maxDepth, text: "", truncated: true, exceededBudget: true, parentCount: 0 },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (remaining > 0 && remaining < maxTokens) {
|
|
219
|
+
// Clamp to remaining budget.
|
|
220
|
+
logger.info?.(`subagent expansion budget clamped from ${maxTokens} to ${remaining} tokens`);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
try {
|
|
224
|
+
const client = await getClient();
|
|
225
|
+
const parts = [];
|
|
226
|
+
let totalChars = 0;
|
|
227
|
+
let truncated = false;
|
|
228
|
+
let parentCount = 0;
|
|
229
|
+
for (const sid of summaryIds) {
|
|
230
|
+
if (totalChars >= MAX_EXPAND_CHARS) {
|
|
231
|
+
truncated = true;
|
|
232
|
+
break;
|
|
233
|
+
}
|
|
234
|
+
const resp = await client.expandSummary({
|
|
235
|
+
sessionId,
|
|
236
|
+
summaryId: sid,
|
|
237
|
+
maxDepth,
|
|
238
|
+
});
|
|
239
|
+
if (resp.text) {
|
|
240
|
+
// Count children from metadata if available
|
|
241
|
+
let meta = {};
|
|
242
|
+
if (resp.metadataJson && resp.metadataJson.length > 0) {
|
|
243
|
+
try {
|
|
244
|
+
const decoder = new TextDecoder();
|
|
245
|
+
meta = JSON.parse(decoder.decode(resp.metadataJson));
|
|
246
|
+
}
|
|
247
|
+
catch { /* best-effort */ }
|
|
248
|
+
}
|
|
249
|
+
const lineage = (meta.continuity_lineage ?? {});
|
|
250
|
+
const parents = Array.isArray(lineage.parent_summary_ids) ? lineage.parent_summary_ids.length : 0;
|
|
251
|
+
parentCount += parents;
|
|
252
|
+
const remaining = MAX_EXPAND_CHARS - totalChars;
|
|
253
|
+
const text = resp.text.length > remaining ? resp.text.slice(0, remaining) + "\n...[truncated]" : resp.text;
|
|
254
|
+
parts.push(`## ${sid}\n${text}`);
|
|
255
|
+
totalChars += text.length;
|
|
256
|
+
if (resp.text.length > remaining) {
|
|
257
|
+
truncated = true;
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const text = parts.join("\n\n");
|
|
263
|
+
const exceededBudget = totalChars > maxTokens * 4;
|
|
264
|
+
if (exceededBudget) {
|
|
265
|
+
return {
|
|
266
|
+
content: [{
|
|
267
|
+
type: "text",
|
|
268
|
+
text: `[Expansion exceeds ${maxTokens}-token budget. Use memory_describe to navigate child summaries, or narrow with specific summaryIds.]`,
|
|
269
|
+
}],
|
|
270
|
+
details: { summaryId: summaryIds[0] ?? "", depth: maxDepth, text: "", truncated: true, exceededBudget: true, parentCount },
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
return jsonResult({
|
|
274
|
+
summaryId: summaryIds[0] ?? "",
|
|
275
|
+
depth: maxDepth,
|
|
276
|
+
text,
|
|
277
|
+
truncated,
|
|
278
|
+
exceededBudget,
|
|
279
|
+
parentCount,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
logger.warn?.(`memory_expand failed: ${formatError(error)}`);
|
|
284
|
+
return jsonResult({
|
|
285
|
+
summaryId: summaryIds[0] ?? "",
|
|
286
|
+
depth: maxDepth,
|
|
287
|
+
text: "",
|
|
288
|
+
truncated: false,
|
|
289
|
+
exceededBudget: false,
|
|
290
|
+
parentCount: 0,
|
|
291
|
+
error: formatError(error),
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
export function createMemoryGrepTool(getClient, logger = console) {
|
|
298
|
+
return {
|
|
299
|
+
name: "memory_grep",
|
|
300
|
+
label: "Memory Grep",
|
|
301
|
+
description: "Search compacted conversation history by text or regex pattern. " +
|
|
302
|
+
"Searches across session summaries and raw turns. Returns matching snippets " +
|
|
303
|
+
"with summary/turn IDs for follow-up with memory_describe or memory_expand.",
|
|
304
|
+
parameters: MEMORY_GREP_SCHEMA,
|
|
305
|
+
execute: async (_toolCallId, rawParams) => {
|
|
306
|
+
const params = asParams(rawParams);
|
|
307
|
+
const pattern = readStr(params, "pattern");
|
|
308
|
+
if (!pattern)
|
|
309
|
+
throw new Error("memory_grep requires pattern");
|
|
310
|
+
const mode = (params.mode === "regex" ? "regex" : "text");
|
|
311
|
+
const scope = (params.scope === "messages" ? "messages" : params.scope === "summaries" ? "summaries" : "both");
|
|
312
|
+
const limit = readNum(params, "limit", { integer: true }) ?? MAX_GREP_RESULTS;
|
|
313
|
+
const sessionId = readStr(params, "sessionId") ?? "";
|
|
314
|
+
try {
|
|
315
|
+
const client = await getClient();
|
|
316
|
+
const summaries = [];
|
|
317
|
+
const turns = [];
|
|
318
|
+
let totalChars = 0;
|
|
319
|
+
let totalMatches = 0;
|
|
320
|
+
if (scope === "summaries" || scope === "both") {
|
|
321
|
+
const searchK = Math.min(limit * 3, 200);
|
|
322
|
+
const summaryResults = await client.searchText({
|
|
323
|
+
collection: `session_summary:${sessionId}`,
|
|
324
|
+
text: pattern,
|
|
325
|
+
k: searchK,
|
|
326
|
+
});
|
|
327
|
+
for (const r of (summaryResults.results ?? [])) {
|
|
328
|
+
if (summaries.length >= limit || totalChars >= MAX_GREP_CHARS)
|
|
329
|
+
break;
|
|
330
|
+
if (!safeMatch(r.text, pattern, mode))
|
|
331
|
+
continue;
|
|
332
|
+
totalMatches++;
|
|
333
|
+
let evictionCue;
|
|
334
|
+
if (r.metadataJson && r.metadataJson.length > 0) {
|
|
335
|
+
try {
|
|
336
|
+
const decoder = new TextDecoder();
|
|
337
|
+
const meta = JSON.parse(decoder.decode(r.metadataJson));
|
|
338
|
+
evictionCue = typeof meta.eviction_cue === "string" ? meta.eviction_cue : undefined;
|
|
339
|
+
}
|
|
340
|
+
catch { /* best-effort */ }
|
|
341
|
+
}
|
|
342
|
+
const snippet = truncateSnippet(r.text);
|
|
343
|
+
summaries.push({ summaryId: r.id, snippet, score: r.score, evictionCue });
|
|
344
|
+
totalChars += snippet.length;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (scope === "messages" || scope === "both") {
|
|
348
|
+
const searchK = Math.min(limit * 3, 200);
|
|
349
|
+
const turnResults = await client.searchText({
|
|
350
|
+
collection: `session_raw:${sessionId}`,
|
|
351
|
+
text: pattern,
|
|
352
|
+
k: searchK,
|
|
353
|
+
});
|
|
354
|
+
for (const r of (turnResults.results ?? [])) {
|
|
355
|
+
if (turns.length >= limit || totalChars >= MAX_GREP_CHARS)
|
|
356
|
+
break;
|
|
357
|
+
if (!safeMatch(r.text, pattern, mode))
|
|
358
|
+
continue;
|
|
359
|
+
totalMatches++;
|
|
360
|
+
const snippet = truncateSnippet(r.text);
|
|
361
|
+
let role = "unknown";
|
|
362
|
+
if (r.metadataJson && r.metadataJson.length > 0) {
|
|
363
|
+
try {
|
|
364
|
+
const decoder = new TextDecoder();
|
|
365
|
+
const meta = JSON.parse(decoder.decode(r.metadataJson));
|
|
366
|
+
role = typeof meta.role === "string" ? meta.role : "unknown";
|
|
367
|
+
}
|
|
368
|
+
catch { /* best-effort */ }
|
|
369
|
+
}
|
|
370
|
+
turns.push({ turnId: r.id, snippet, role, score: r.score });
|
|
371
|
+
totalChars += snippet.length;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
return jsonResult({
|
|
375
|
+
pattern,
|
|
376
|
+
mode,
|
|
377
|
+
totalMatches,
|
|
378
|
+
summaries,
|
|
379
|
+
turns,
|
|
380
|
+
truncated: totalChars >= MAX_GREP_CHARS,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
logger.warn?.(`memory_grep failed: ${formatError(error)}`);
|
|
385
|
+
return jsonResult({
|
|
386
|
+
pattern,
|
|
387
|
+
mode,
|
|
388
|
+
totalMatches: 0,
|
|
389
|
+
summaries: [],
|
|
390
|
+
turns: [],
|
|
391
|
+
truncated: false,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
},
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
// ── Prompt guidance ──
|
|
398
|
+
const RECALL_GUIDANCE = [
|
|
399
|
+
"## LibraVDB Recall",
|
|
400
|
+
"",
|
|
401
|
+
"Summaries in context are compressed maps — not the details.",
|
|
402
|
+
"Active session recall and summary expansion tools are available:",
|
|
403
|
+
"",
|
|
404
|
+
"**Tool escalation (cheap → expensive):**",
|
|
405
|
+
"1. `memory_search` — semantic search across all memory/session collections.",
|
|
406
|
+
" Summary hits show `[Summary sum_xxx]: [cue with anchors, decisions, signals]`.",
|
|
407
|
+
" Use these cues to decide what's worth expanding.",
|
|
408
|
+
"2. `memory_describe` — inspect a summary's metadata (cheap, no expansion).",
|
|
409
|
+
" Returns eviction cues with child count and source turn range.",
|
|
410
|
+
"3. `memory_expand` — deep recall: walks the summary tree, returns full text.",
|
|
411
|
+
" Use this when the eviction cue suggests the detail you need is inside.",
|
|
412
|
+
"4. `memory_grep` — search compacted history by text or regex pattern.",
|
|
413
|
+
" Returns matching snippets with summary/turn IDs for follow-up.",
|
|
414
|
+
"",
|
|
415
|
+
"**Many questions can be answered from eviction cues alone.**",
|
|
416
|
+
"Only expand when the cue signals specific details worth the token cost.",
|
|
417
|
+
"",
|
|
418
|
+
];
|
|
419
|
+
export function memoryRecallPromptSection() {
|
|
420
|
+
return [...RECALL_GUIDANCE];
|
|
421
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
type AgentMessage = {
|
|
2
|
+
role: string;
|
|
3
|
+
content: string | unknown[];
|
|
4
|
+
id?: string;
|
|
5
|
+
};
|
|
6
|
+
export declare class TurnMemoryCache {
|
|
7
|
+
private cache;
|
|
8
|
+
constructor(maxSize?: number);
|
|
9
|
+
private cacheKey;
|
|
10
|
+
private normalize;
|
|
11
|
+
get(sessionId: string, queryHint: string): unknown | undefined;
|
|
12
|
+
set(sessionId: string, queryHint: string, value: unknown): void;
|
|
13
|
+
invalidateSession(sessionId: string): void;
|
|
14
|
+
get size(): number;
|
|
15
|
+
}
|
|
16
|
+
export declare function isNewUserTurn(messages: AgentMessage[]): boolean;
|
|
17
|
+
export declare function detectNewTurn(messages: AgentMessage[], lastUserMessageHash: {
|
|
18
|
+
current: string | null;
|
|
19
|
+
}): boolean;
|
|
20
|
+
export declare function extractQueryHint(messages: AgentMessage[], stripSenderMetadata: (text: string) => string): string | null;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
const DEFAULT_MAX_SIZE = 100;
|
|
2
|
+
class MemoryCache {
|
|
3
|
+
cache = new Map();
|
|
4
|
+
maxSize;
|
|
5
|
+
constructor(maxSize = DEFAULT_MAX_SIZE) {
|
|
6
|
+
this.maxSize = maxSize;
|
|
7
|
+
}
|
|
8
|
+
get(key) {
|
|
9
|
+
const entry = this.cache.get(key);
|
|
10
|
+
if (!entry)
|
|
11
|
+
return undefined;
|
|
12
|
+
this.cache.delete(key);
|
|
13
|
+
this.cache.set(key, entry);
|
|
14
|
+
return entry.value;
|
|
15
|
+
}
|
|
16
|
+
set(key, value) {
|
|
17
|
+
if (this.cache.has(key))
|
|
18
|
+
this.cache.delete(key);
|
|
19
|
+
if (this.cache.size >= this.maxSize) {
|
|
20
|
+
const firstKey = this.cache.keys().next().value;
|
|
21
|
+
if (firstKey !== undefined)
|
|
22
|
+
this.cache.delete(firstKey);
|
|
23
|
+
}
|
|
24
|
+
this.cache.set(key, { value, timestamp: Date.now() });
|
|
25
|
+
}
|
|
26
|
+
invalidate(prefix) {
|
|
27
|
+
for (const key of this.cache.keys()) {
|
|
28
|
+
if (key.startsWith(prefix))
|
|
29
|
+
this.cache.delete(key);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
get size() {
|
|
33
|
+
return this.cache.size;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export class TurnMemoryCache {
|
|
37
|
+
cache = new MemoryCache();
|
|
38
|
+
constructor(maxSize = DEFAULT_MAX_SIZE) {
|
|
39
|
+
this.cache = new MemoryCache(maxSize);
|
|
40
|
+
}
|
|
41
|
+
cacheKey(sessionId, queryHint) {
|
|
42
|
+
return `${sessionId}:${this.normalize(queryHint)}`;
|
|
43
|
+
}
|
|
44
|
+
normalize(text) {
|
|
45
|
+
return text.toLowerCase().replace(/\s+/g, " ").trim().slice(0, 200);
|
|
46
|
+
}
|
|
47
|
+
get(sessionId, queryHint) {
|
|
48
|
+
return this.cache.get(this.cacheKey(sessionId, queryHint));
|
|
49
|
+
}
|
|
50
|
+
set(sessionId, queryHint, value) {
|
|
51
|
+
this.cache.set(this.cacheKey(sessionId, queryHint), value);
|
|
52
|
+
}
|
|
53
|
+
invalidateSession(sessionId) {
|
|
54
|
+
this.cache.invalidate(sessionId + ":");
|
|
55
|
+
}
|
|
56
|
+
get size() {
|
|
57
|
+
return this.cache.size;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function contentHash(msg) {
|
|
61
|
+
const content = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content);
|
|
62
|
+
let hash = 0;
|
|
63
|
+
for (let i = 0; i < content.length; i++) {
|
|
64
|
+
const ch = content.charCodeAt(i);
|
|
65
|
+
hash = ((hash << 5) - hash) + ch;
|
|
66
|
+
hash |= 0;
|
|
67
|
+
}
|
|
68
|
+
return String(hash);
|
|
69
|
+
}
|
|
70
|
+
export function isNewUserTurn(messages) {
|
|
71
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
72
|
+
const role = messages[i].role;
|
|
73
|
+
if (role === "user")
|
|
74
|
+
return true;
|
|
75
|
+
if (role === "assistant" || role === "toolResult")
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
export function detectNewTurn(messages, lastUserMessageHash) {
|
|
81
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
82
|
+
if (messages[i].role === "user") {
|
|
83
|
+
const hash = contentHash(messages[i]);
|
|
84
|
+
if (hash !== lastUserMessageHash.current) {
|
|
85
|
+
lastUserMessageHash.current = hash;
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
export function extractQueryHint(messages, stripSenderMetadata) {
|
|
94
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
95
|
+
if (messages[i].role === "user") {
|
|
96
|
+
const raw = messages[i].content;
|
|
97
|
+
const content = typeof raw === "string" ? raw : JSON.stringify(raw) ?? "";
|
|
98
|
+
const cleaned = stripSenderMetadata(content);
|
|
99
|
+
return cleaned.slice(0, 200);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return null;
|
|
103
|
+
}
|
package/dist/types.d.ts
CHANGED
|
@@ -87,6 +87,10 @@ export interface PluginConfig {
|
|
|
87
87
|
compactThreshold?: number;
|
|
88
88
|
compactionThresholdFraction?: number;
|
|
89
89
|
compactSessionTokenBudget?: number;
|
|
90
|
+
/** Token budget cap for subagent memory_expand calls. Default 8000.
|
|
91
|
+
* Prevents a subagent from blowing its context window via repeated
|
|
92
|
+
* expansions. Set to 0 to disable the cap entirely. */
|
|
93
|
+
subagentTokenBudget?: number;
|
|
90
94
|
section7CoarseTopK?: number;
|
|
91
95
|
section7SecondPassTopK?: number;
|
|
92
96
|
section7Theta1?: number;
|
|
@@ -99,6 +103,7 @@ export interface PluginConfig {
|
|
|
99
103
|
section7AuthorityAuthoredWeight?: number;
|
|
100
104
|
section7AuthoritySalienceWeight?: number;
|
|
101
105
|
section7RecencyAccessLambda?: number;
|
|
106
|
+
section7AuthorityAccessWeight?: number;
|
|
102
107
|
recoveryFloorScore?: number;
|
|
103
108
|
recoveryMinTopK?: number;
|
|
104
109
|
recoveryMinConfidenceMean?: number;
|
|
@@ -124,6 +129,14 @@ export interface PluginConfig {
|
|
|
124
129
|
* "tls" — always use TLS regardless of address.
|
|
125
130
|
* "insecure" — always use plaintext (service mesh, tunnel). */
|
|
126
131
|
grpcEndpointTlsMode?: "auto" | "tls" | "insecure";
|
|
132
|
+
/** Whether BeforeTurnKernel retrieval is enabled. Default: true */
|
|
133
|
+
beforeTurnEnabled?: boolean;
|
|
134
|
+
/** Timeout in milliseconds for the BeforeTurnKernel gRPC call. Default: 5000 */
|
|
135
|
+
beforeTurnTimeoutMs?: number;
|
|
136
|
+
/** Maximum number of retrieved memories to inject per turn. Default: 5 */
|
|
137
|
+
beforeTurnMaxMemories?: number;
|
|
138
|
+
/** Minimum similarity score (0.0–1.0) for semantic search hits. Default: 0.4 */
|
|
139
|
+
beforeTurnMinScore?: number;
|
|
127
140
|
}
|
|
128
141
|
export interface SearchResult {
|
|
129
142
|
id: string;
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "libravdb-memory",
|
|
3
3
|
"name": "LibraVDB Memory",
|
|
4
4
|
"description": "Persistent vector memory with three-tier hybrid scoring",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.8.0",
|
|
6
6
|
"kind": [
|
|
7
7
|
"memory",
|
|
8
8
|
"context-engine"
|
|
@@ -166,6 +166,21 @@
|
|
|
166
166
|
"section7RecencyAccessLambda": {
|
|
167
167
|
"type": "number"
|
|
168
168
|
},
|
|
169
|
+
"section7AuthorityAccessWeight": {
|
|
170
|
+
"type": "number"
|
|
171
|
+
},
|
|
172
|
+
"subagentTokenBudget": {
|
|
173
|
+
"type": "number"
|
|
174
|
+
},
|
|
175
|
+
"beforeTurnEnabled": {
|
|
176
|
+
"type": "boolean"
|
|
177
|
+
},
|
|
178
|
+
"beforeTurnTimeoutMs": {
|
|
179
|
+
"type": "number"
|
|
180
|
+
},
|
|
181
|
+
"beforeTurnMaxMemories": {
|
|
182
|
+
"type": "number"
|
|
183
|
+
},
|
|
169
184
|
"embeddingRuntimePath": {
|
|
170
185
|
"type": "string",
|
|
171
186
|
"description": "Path to the ONNX Runtime library visible to the daemon, for example /opt/homebrew/opt/libravdbd/models/onnxruntime/lib/libonnxruntime.dylib or /home/node/.openclaw/libravdbd/models/onnxruntime/lib/libonnxruntime.so."
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xdarkicex/openclaw-memory-libravdb",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -79,6 +79,6 @@
|
|
|
79
79
|
"dependencies": {
|
|
80
80
|
"@connectrpc/connect": "^1.7.0",
|
|
81
81
|
"@connectrpc/connect-node": "^1.7.0",
|
|
82
|
-
"@xdarkicex/libravdb-contracts": "^2.0.
|
|
82
|
+
"@xdarkicex/libravdb-contracts": "^2.0.19"
|
|
83
83
|
}
|
|
84
84
|
}
|