clawvault 3.3.0 → 3.4.1
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 +26 -26
- package/bin/command-registration.test.js +2 -1
- package/bin/help-contract.test.js +2 -0
- package/bin/register-maintenance-commands.js +27 -1
- package/bin/register-query-commands.js +35 -0
- package/bin/register-query-commands.test.js +15 -0
- package/dist/chunk-35JCYSRR.js +158 -0
- package/dist/{chunk-TDWFBDAQ.js → chunk-D5U3Q4N5.js} +7 -151
- package/dist/{chunk-YCUVAOFC.js → chunk-DCF4KMFD.js} +4 -4
- package/dist/chunk-NSXYM6EZ.js +255 -0
- package/dist/chunk-PLNK37JD.js +2223 -0
- package/dist/chunk-RL2L6I6K.js +223 -0
- package/dist/chunk-YTRZNA64.js +37 -0
- package/dist/cli/index.js +6 -5
- package/dist/commands/entities.d.ts +8 -1
- package/dist/commands/entities.js +44 -1
- package/dist/commands/link.js +5 -5
- package/dist/commands/maintain.js +2 -1
- package/dist/commands/recall.d.ts +14 -0
- package/dist/commands/recall.js +15 -0
- package/dist/index.d.ts +59 -1
- package/dist/index.js +56 -11
- package/dist/openclaw-plugin--gqA2BZw.d.ts +267 -0
- package/dist/openclaw-plugin.d.ts +4 -8
- package/dist/openclaw-plugin.js +16 -10
- package/dist/types-CbL-wIKi.d.ts +36 -0
- package/openclaw.plugin.json +39 -0
- package/package.json +4 -4
|
@@ -0,0 +1,2223 @@
|
|
|
1
|
+
import {
|
|
2
|
+
buildRecallResult
|
|
3
|
+
} from "./chunk-RL2L6I6K.js";
|
|
4
|
+
import {
|
|
5
|
+
synthesizeEntityProfiles
|
|
6
|
+
} from "./chunk-NSXYM6EZ.js";
|
|
7
|
+
import {
|
|
8
|
+
normalizeForDedup,
|
|
9
|
+
similarityScore
|
|
10
|
+
} from "./chunk-35JCYSRR.js";
|
|
11
|
+
import {
|
|
12
|
+
FactStore,
|
|
13
|
+
extractFactsRuleBased
|
|
14
|
+
} from "./chunk-BSJ6RIT7.js";
|
|
15
|
+
import {
|
|
16
|
+
ClawVault
|
|
17
|
+
} from "./chunk-ECGJYWNA.js";
|
|
18
|
+
import {
|
|
19
|
+
hasQmd
|
|
20
|
+
} from "./chunk-PTWPPVC7.js";
|
|
21
|
+
import {
|
|
22
|
+
resolveVaultPath
|
|
23
|
+
} from "./chunk-GJO3CFUN.js";
|
|
24
|
+
|
|
25
|
+
// src/capture/extractor.ts
|
|
26
|
+
var MEMORY_NOTE_PATTERN = /<memory_note([^>]*)>([\s\S]*?)<\/memory_note>/gi;
|
|
27
|
+
var WIKI_LINK_PATTERN = /\[\[([^\]|]+)(?:\|[^\]]+)?\]\]/g;
|
|
28
|
+
var SENTENCE_SPLIT_PATTERN = /\r?\n|(?<=[.?!])\s+/;
|
|
29
|
+
var CLASSIFIER_RULES = [
|
|
30
|
+
{ type: "decision", confidence: 0.74, pattern: /\b(decid(?:e|ed|ing|ion)|chose|selected|opted|agreed|we will|i will)\b/i },
|
|
31
|
+
{ type: "preference", confidence: 0.72, pattern: /\b(prefer(?:ence|red|s)?|like(?:s|d)?|dislike|always use|never use|default to)\b/i },
|
|
32
|
+
{ type: "lesson", confidence: 0.76, pattern: /\b(learn(?:ed|ing|t)|lesson|insight|takeaway|next time|mistake|realized)\b/i },
|
|
33
|
+
{ type: "relationship", confidence: 0.69, pattern: /\b(works with|reports to|collaborates with|partnered with|depends on|related to)\b/i },
|
|
34
|
+
{ type: "episode", confidence: 0.64, pattern: /\b(today|yesterday|this morning|this afternoon|during|after|before|in the meeting)\b/i },
|
|
35
|
+
{ type: "entity", confidence: 0.62, pattern: /\[\[[^\]]+\]\]/i }
|
|
36
|
+
];
|
|
37
|
+
function normalizeWhitespace(value) {
|
|
38
|
+
return value.replace(/\s+/g, " ").trim();
|
|
39
|
+
}
|
|
40
|
+
function parseNoteAttributes(raw) {
|
|
41
|
+
const attributes = {};
|
|
42
|
+
const attributePattern = /([a-zA-Z_][\w-]*)\s*=\s*"([^"]*)"/g;
|
|
43
|
+
let match = attributePattern.exec(raw);
|
|
44
|
+
while (match) {
|
|
45
|
+
attributes[match[1]] = match[2];
|
|
46
|
+
match = attributePattern.exec(raw);
|
|
47
|
+
}
|
|
48
|
+
return attributes;
|
|
49
|
+
}
|
|
50
|
+
function sanitizeType(value) {
|
|
51
|
+
if (!value) return null;
|
|
52
|
+
const normalized = value.trim().toLowerCase();
|
|
53
|
+
const allowed = [
|
|
54
|
+
"fact",
|
|
55
|
+
"preference",
|
|
56
|
+
"decision",
|
|
57
|
+
"lesson",
|
|
58
|
+
"entity",
|
|
59
|
+
"episode",
|
|
60
|
+
"relationship"
|
|
61
|
+
];
|
|
62
|
+
return allowed.includes(normalized) ? normalized : null;
|
|
63
|
+
}
|
|
64
|
+
function parseConfidence(value, fallback = 0.85) {
|
|
65
|
+
if (!value) return fallback;
|
|
66
|
+
const parsed = Number.parseFloat(value);
|
|
67
|
+
if (!Number.isFinite(parsed)) return fallback;
|
|
68
|
+
return Math.min(1, Math.max(0, parsed));
|
|
69
|
+
}
|
|
70
|
+
function extractEntities(text) {
|
|
71
|
+
const entities = /* @__PURE__ */ new Set();
|
|
72
|
+
WIKI_LINK_PATTERN.lastIndex = 0;
|
|
73
|
+
let linkMatch = WIKI_LINK_PATTERN.exec(text);
|
|
74
|
+
while (linkMatch) {
|
|
75
|
+
entities.add(linkMatch[1].trim());
|
|
76
|
+
linkMatch = WIKI_LINK_PATTERN.exec(text);
|
|
77
|
+
}
|
|
78
|
+
const properNounPattern = /\b([A-Z][a-z]+(?:\s+[A-Z][a-z]+){0,2})\b/g;
|
|
79
|
+
let nounMatch = properNounPattern.exec(text);
|
|
80
|
+
while (nounMatch) {
|
|
81
|
+
const value = nounMatch[1].trim();
|
|
82
|
+
if (value.length >= 3) {
|
|
83
|
+
entities.add(value);
|
|
84
|
+
}
|
|
85
|
+
nounMatch = properNounPattern.exec(text);
|
|
86
|
+
}
|
|
87
|
+
return [...entities];
|
|
88
|
+
}
|
|
89
|
+
function inferTypeFromSentence(sentence) {
|
|
90
|
+
for (const rule of CLASSIFIER_RULES) {
|
|
91
|
+
if (rule.pattern.test(sentence)) {
|
|
92
|
+
return { type: rule.type, confidence: rule.confidence };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { type: "fact", confidence: 0.55 };
|
|
96
|
+
}
|
|
97
|
+
function titleFromContent(type, content) {
|
|
98
|
+
const words = normalizeWhitespace(content).split(" ").filter(Boolean).slice(0, 8);
|
|
99
|
+
const stem = words.join(" ");
|
|
100
|
+
return `${type}: ${stem}`.slice(0, 90);
|
|
101
|
+
}
|
|
102
|
+
function dedupeCandidates(candidates) {
|
|
103
|
+
const seen = /* @__PURE__ */ new Set();
|
|
104
|
+
const deduped = [];
|
|
105
|
+
for (const candidate of candidates) {
|
|
106
|
+
const key = normalizeWhitespace(candidate.content).toLowerCase();
|
|
107
|
+
if (!key || seen.has(key)) {
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
seen.add(key);
|
|
111
|
+
deduped.push(candidate);
|
|
112
|
+
}
|
|
113
|
+
return deduped;
|
|
114
|
+
}
|
|
115
|
+
function extractTaggedMemoryNotes(text) {
|
|
116
|
+
const candidates = [];
|
|
117
|
+
MEMORY_NOTE_PATTERN.lastIndex = 0;
|
|
118
|
+
let match = MEMORY_NOTE_PATTERN.exec(text);
|
|
119
|
+
while (match) {
|
|
120
|
+
const attributes = parseNoteAttributes(match[1] ?? "");
|
|
121
|
+
const body = normalizeWhitespace(match[2] ?? "");
|
|
122
|
+
if (!body) {
|
|
123
|
+
match = MEMORY_NOTE_PATTERN.exec(text);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
const type = sanitizeType(attributes.type) ?? inferTypeFromSentence(body).type;
|
|
127
|
+
const confidence = parseConfidence(attributes.confidence, 0.9);
|
|
128
|
+
const entities = extractEntities(body);
|
|
129
|
+
candidates.push({
|
|
130
|
+
content: body,
|
|
131
|
+
type,
|
|
132
|
+
confidence,
|
|
133
|
+
title: attributes.title || titleFromContent(type, body),
|
|
134
|
+
tags: attributes.tags ? attributes.tags.split(",").map((item) => item.trim()).filter(Boolean) : void 0,
|
|
135
|
+
entities,
|
|
136
|
+
source: "memory_note",
|
|
137
|
+
metadata: {
|
|
138
|
+
taggedType: attributes.type,
|
|
139
|
+
rawAttributes: attributes
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
match = MEMORY_NOTE_PATTERN.exec(text);
|
|
143
|
+
}
|
|
144
|
+
return dedupeCandidates(candidates);
|
|
145
|
+
}
|
|
146
|
+
function stripMemoryNotes(text) {
|
|
147
|
+
return text.replace(MEMORY_NOTE_PATTERN, " ");
|
|
148
|
+
}
|
|
149
|
+
function extractHeuristicMemories(text) {
|
|
150
|
+
const sanitized = stripMemoryNotes(text);
|
|
151
|
+
const sentences = sanitized.split(SENTENCE_SPLIT_PATTERN).map((line) => normalizeWhitespace(line)).filter((line) => line.length >= 20 && line.length <= 400);
|
|
152
|
+
const candidates = [];
|
|
153
|
+
for (const sentence of sentences) {
|
|
154
|
+
const classification = inferTypeFromSentence(sentence);
|
|
155
|
+
const entities = extractEntities(sentence);
|
|
156
|
+
const confidenceBoost = entities.length > 0 ? 0.08 : 0;
|
|
157
|
+
candidates.push({
|
|
158
|
+
content: sentence,
|
|
159
|
+
type: classification.type,
|
|
160
|
+
confidence: Math.min(1, classification.confidence + confidenceBoost),
|
|
161
|
+
title: titleFromContent(classification.type, sentence),
|
|
162
|
+
entities,
|
|
163
|
+
source: "heuristic"
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return dedupeCandidates(candidates);
|
|
167
|
+
}
|
|
168
|
+
function extractMemoriesFromAssistantResponse(text) {
|
|
169
|
+
const fromTags = extractTaggedMemoryNotes(text);
|
|
170
|
+
const fromHeuristics = extractHeuristicMemories(text);
|
|
171
|
+
return dedupeCandidates([...fromTags, ...fromHeuristics]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/capture/quality.ts
|
|
175
|
+
var SECRET_PATTERNS = [
|
|
176
|
+
/sk-[A-Za-z0-9]{20,}/,
|
|
177
|
+
/AKIA[0-9A-Z]{16}/,
|
|
178
|
+
/-----BEGIN (?:RSA|EC|OPENSSH|PGP|DSA) PRIVATE KEY-----/,
|
|
179
|
+
/(?:api[-_ ]?key|access[-_ ]?token|secret)(?:\s*[:=]\s*|\s+)[A-Za-z0-9_\-]{12,}/i
|
|
180
|
+
];
|
|
181
|
+
var NOISE_PATTERNS = [
|
|
182
|
+
/^\s*[{[][\s\S]{30,}[}\]]\s*$/m,
|
|
183
|
+
/^\s*\$?\s*(?:npm|pnpm|yarn|bun|git|docker|kubectl|curl|node)\b/m,
|
|
184
|
+
/(?:stdout|stderr|exit code|stack trace|traceback|at\s+[A-Za-z0-9_.$]+\s+\()/i,
|
|
185
|
+
/^\s*[|+]{2,}[-+| ]+\s*$/m
|
|
186
|
+
];
|
|
187
|
+
function countAlphaWords(value) {
|
|
188
|
+
const words = value.match(/[A-Za-z][A-Za-z0-9'-]*/g) ?? [];
|
|
189
|
+
return words.length;
|
|
190
|
+
}
|
|
191
|
+
function symbolRatio(value) {
|
|
192
|
+
if (!value) return 1;
|
|
193
|
+
const symbols = value.replace(/[A-Za-z0-9\s]/g, "").length;
|
|
194
|
+
return symbols / value.length;
|
|
195
|
+
}
|
|
196
|
+
function lineDensity(value) {
|
|
197
|
+
const lines = value.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
198
|
+
if (lines.length === 0) return 0;
|
|
199
|
+
const averageLength = lines.reduce((sum, line) => sum + line.length, 0) / lines.length;
|
|
200
|
+
return averageLength;
|
|
201
|
+
}
|
|
202
|
+
function isLikelyJunkMemory(content) {
|
|
203
|
+
if (!content || content.trim().length < 12) {
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
if (SECRET_PATTERNS.some((pattern) => pattern.test(content))) {
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
if (NOISE_PATTERNS.some((pattern) => pattern.test(content))) {
|
|
210
|
+
return true;
|
|
211
|
+
}
|
|
212
|
+
if (symbolRatio(content) > 0.3) {
|
|
213
|
+
return true;
|
|
214
|
+
}
|
|
215
|
+
if (lineDensity(content) > 180) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
return false;
|
|
219
|
+
}
|
|
220
|
+
function plausibilityScore(content) {
|
|
221
|
+
const trimmed = content.trim();
|
|
222
|
+
if (trimmed.length === 0) return 0;
|
|
223
|
+
const alphaWords = countAlphaWords(trimmed);
|
|
224
|
+
const hasSentencePunctuation = /[.!?]$/.test(trimmed) || trimmed.length > 40;
|
|
225
|
+
const hasVerbLikeWord = /\b(is|are|was|were|has|have|will|should|decided|prefer|learned|met|works|uses)\b/i.test(trimmed);
|
|
226
|
+
const hasBalancedLength = trimmed.length >= 20 && trimmed.length <= 400;
|
|
227
|
+
let score = 0;
|
|
228
|
+
if (alphaWords >= 4) score += 0.3;
|
|
229
|
+
if (alphaWords >= 8) score += 0.2;
|
|
230
|
+
if (hasSentencePunctuation) score += 0.2;
|
|
231
|
+
if (hasVerbLikeWord) score += 0.2;
|
|
232
|
+
if (hasBalancedLength) score += 0.1;
|
|
233
|
+
return Math.min(1, score);
|
|
234
|
+
}
|
|
235
|
+
function maxJaccardSimilarity(candidate, existingContents) {
|
|
236
|
+
if (existingContents.length === 0) return 0;
|
|
237
|
+
let best = 0;
|
|
238
|
+
const normalizedCandidate = normalizeForDedup(candidate);
|
|
239
|
+
for (const entry of existingContents) {
|
|
240
|
+
const score = similarityScore(normalizedCandidate, normalizeForDedup(entry));
|
|
241
|
+
if (score > best) {
|
|
242
|
+
best = score;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return best;
|
|
246
|
+
}
|
|
247
|
+
function evaluateCandidateQuality(candidate, existingContents, stagedContents = [], options = {}) {
|
|
248
|
+
const minConfidence = options.minConfidence ?? 0.4;
|
|
249
|
+
const minQualityScore = options.minQualityScore ?? 0.4;
|
|
250
|
+
const dedupThreshold = options.dedupThreshold ?? 0.82;
|
|
251
|
+
if (candidate.confidence < minConfidence) {
|
|
252
|
+
return {
|
|
253
|
+
accepted: false,
|
|
254
|
+
reason: `confidence below threshold (${candidate.confidence.toFixed(2)} < ${minConfidence.toFixed(2)})`,
|
|
255
|
+
qualityScore: candidate.confidence,
|
|
256
|
+
plausibilityScore: 0,
|
|
257
|
+
confidenceScore: candidate.confidence,
|
|
258
|
+
maxSimilarity: 0
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
if (isLikelyJunkMemory(candidate.content)) {
|
|
262
|
+
return {
|
|
263
|
+
accepted: false,
|
|
264
|
+
reason: "candidate flagged as junk/tool noise",
|
|
265
|
+
qualityScore: 0,
|
|
266
|
+
plausibilityScore: 0,
|
|
267
|
+
confidenceScore: candidate.confidence,
|
|
268
|
+
maxSimilarity: 0
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
const plausibility = plausibilityScore(candidate.content);
|
|
272
|
+
const maxSimilarity = maxJaccardSimilarity(candidate.content, [...existingContents, ...stagedContents]);
|
|
273
|
+
if (maxSimilarity >= dedupThreshold) {
|
|
274
|
+
return {
|
|
275
|
+
accepted: false,
|
|
276
|
+
reason: `candidate too similar to existing memory (similarity=${maxSimilarity.toFixed(2)})`,
|
|
277
|
+
qualityScore: 0,
|
|
278
|
+
plausibilityScore: plausibility,
|
|
279
|
+
confidenceScore: candidate.confidence,
|
|
280
|
+
maxSimilarity
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
const qualityScore = candidate.confidence * 0.6 + plausibility * 0.4;
|
|
284
|
+
if (qualityScore < minQualityScore) {
|
|
285
|
+
return {
|
|
286
|
+
accepted: false,
|
|
287
|
+
reason: `quality score below threshold (${qualityScore.toFixed(2)} < ${minQualityScore.toFixed(2)})`,
|
|
288
|
+
qualityScore,
|
|
289
|
+
plausibilityScore: plausibility,
|
|
290
|
+
confidenceScore: candidate.confidence,
|
|
291
|
+
maxSimilarity
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
return {
|
|
295
|
+
accepted: true,
|
|
296
|
+
qualityScore,
|
|
297
|
+
plausibilityScore: plausibility,
|
|
298
|
+
confidenceScore: candidate.confidence,
|
|
299
|
+
maxSimilarity
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// src/capture/service.ts
|
|
304
|
+
var CATEGORY_BY_MEMORY_TYPE = {
|
|
305
|
+
fact: "facts",
|
|
306
|
+
preference: "preferences",
|
|
307
|
+
decision: "decisions",
|
|
308
|
+
lesson: "lessons",
|
|
309
|
+
entity: "people",
|
|
310
|
+
episode: "transcripts",
|
|
311
|
+
relationship: "people"
|
|
312
|
+
};
|
|
313
|
+
function normalizeMessageContent(content) {
|
|
314
|
+
if (typeof content === "string") {
|
|
315
|
+
return content;
|
|
316
|
+
}
|
|
317
|
+
if (Array.isArray(content)) {
|
|
318
|
+
const parts = content.map((item) => {
|
|
319
|
+
if (typeof item === "string") return item;
|
|
320
|
+
if (!item || typeof item !== "object") return "";
|
|
321
|
+
const record = item;
|
|
322
|
+
if (typeof record.text === "string") return record.text;
|
|
323
|
+
if (typeof record.content === "string") return record.content;
|
|
324
|
+
return "";
|
|
325
|
+
}).filter(Boolean);
|
|
326
|
+
return parts.join("\n").trim();
|
|
327
|
+
}
|
|
328
|
+
if (content && typeof content === "object") {
|
|
329
|
+
const record = content;
|
|
330
|
+
if (typeof record.text === "string") return record.text;
|
|
331
|
+
if (typeof record.content === "string") return record.content;
|
|
332
|
+
}
|
|
333
|
+
return "";
|
|
334
|
+
}
|
|
335
|
+
function normalizeIncomingMessage(value) {
|
|
336
|
+
if (!value || typeof value !== "object") {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
const record = value;
|
|
340
|
+
const roleCandidate = record.role ?? record.authorRole ?? record.speaker ?? record.type;
|
|
341
|
+
const role = typeof roleCandidate === "string" ? roleCandidate.toLowerCase() : "assistant";
|
|
342
|
+
const content = normalizeMessageContent(record.content ?? record.text ?? record.message);
|
|
343
|
+
if (!content.trim()) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
const timestamp = typeof record.timestamp === "string" ? record.timestamp : typeof record.createdAt === "string" ? record.createdAt : void 0;
|
|
347
|
+
return { role, content, timestamp };
|
|
348
|
+
}
|
|
349
|
+
function titleForCandidate(candidate) {
|
|
350
|
+
if (candidate.title && candidate.title.trim()) {
|
|
351
|
+
return candidate.title.trim();
|
|
352
|
+
}
|
|
353
|
+
const stem = candidate.content.replace(/\s+/g, " ").trim().split(" ").slice(0, 8).join(" ");
|
|
354
|
+
return `${candidate.type}: ${stem}`.slice(0, 90);
|
|
355
|
+
}
|
|
356
|
+
function withCollisionSuffix(title) {
|
|
357
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
358
|
+
return `${title} ${stamp}`;
|
|
359
|
+
}
|
|
360
|
+
var LiveCaptureService = class {
|
|
361
|
+
async captureTurn(messages, options = {}) {
|
|
362
|
+
const normalizedMessages = messages.map((message) => normalizeIncomingMessage(message)).filter((message) => Boolean(message)).filter((message) => message.role === "assistant");
|
|
363
|
+
if (normalizedMessages.length === 0) {
|
|
364
|
+
return {
|
|
365
|
+
stored: 0,
|
|
366
|
+
rejected: 0,
|
|
367
|
+
storedDocuments: [],
|
|
368
|
+
acceptedCandidates: [],
|
|
369
|
+
rejectedCandidates: []
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
const vaultPath = resolveVaultPath({
|
|
373
|
+
explicitPath: options.vaultPath,
|
|
374
|
+
agentId: options.agentId,
|
|
375
|
+
pluginConfig: options.pluginConfig
|
|
376
|
+
});
|
|
377
|
+
const vault = new ClawVault(vaultPath);
|
|
378
|
+
await vault.load();
|
|
379
|
+
const extractedCandidates = normalizedMessages.flatMap(
|
|
380
|
+
(message) => extractMemoriesFromAssistantResponse(message.content)
|
|
381
|
+
);
|
|
382
|
+
if (extractedCandidates.length === 0) {
|
|
383
|
+
return {
|
|
384
|
+
stored: 0,
|
|
385
|
+
rejected: 0,
|
|
386
|
+
storedDocuments: [],
|
|
387
|
+
acceptedCandidates: [],
|
|
388
|
+
rejectedCandidates: []
|
|
389
|
+
};
|
|
390
|
+
}
|
|
391
|
+
const maxPerTurn = options.maxPerTurn ?? 8;
|
|
392
|
+
const existingDocs = await vault.list();
|
|
393
|
+
const existingContents = existingDocs.map((doc) => doc.content);
|
|
394
|
+
const stagedAcceptedContents = [];
|
|
395
|
+
const acceptedCandidates = [];
|
|
396
|
+
const rejectedCandidates = [];
|
|
397
|
+
for (const candidate of extractedCandidates) {
|
|
398
|
+
if (acceptedCandidates.length >= maxPerTurn) {
|
|
399
|
+
break;
|
|
400
|
+
}
|
|
401
|
+
const quality = evaluateCandidateQuality(
|
|
402
|
+
candidate,
|
|
403
|
+
existingContents,
|
|
404
|
+
stagedAcceptedContents,
|
|
405
|
+
{
|
|
406
|
+
minConfidence: options.minConfidence,
|
|
407
|
+
dedupThreshold: options.dedupThreshold
|
|
408
|
+
}
|
|
409
|
+
);
|
|
410
|
+
if (!quality.accepted) {
|
|
411
|
+
rejectedCandidates.push({
|
|
412
|
+
candidate,
|
|
413
|
+
reason: quality.reason ?? "quality gate rejected candidate"
|
|
414
|
+
});
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
stagedAcceptedContents.push(candidate.content);
|
|
418
|
+
acceptedCandidates.push(candidate);
|
|
419
|
+
}
|
|
420
|
+
const storedDocuments = [];
|
|
421
|
+
for (const candidate of acceptedCandidates) {
|
|
422
|
+
const doc = await this.persistCandidate(vault, candidate, options);
|
|
423
|
+
storedDocuments.push(doc.id);
|
|
424
|
+
}
|
|
425
|
+
const hasEntityMentions = acceptedCandidates.some((candidate) => (candidate.entities?.length ?? 0) > 0);
|
|
426
|
+
if (hasEntityMentions) {
|
|
427
|
+
await synthesizeEntityProfiles(vaultPath, { writeFiles: true });
|
|
428
|
+
}
|
|
429
|
+
return {
|
|
430
|
+
stored: storedDocuments.length,
|
|
431
|
+
rejected: rejectedCandidates.length,
|
|
432
|
+
storedDocuments,
|
|
433
|
+
acceptedCandidates,
|
|
434
|
+
rejectedCandidates
|
|
435
|
+
};
|
|
436
|
+
}
|
|
437
|
+
async persistCandidate(vault, candidate, options) {
|
|
438
|
+
const category = CATEGORY_BY_MEMORY_TYPE[candidate.type] ?? "inbox";
|
|
439
|
+
const title = titleForCandidate(candidate);
|
|
440
|
+
const frontmatter = {
|
|
441
|
+
memoryType: candidate.type,
|
|
442
|
+
captureSource: candidate.source,
|
|
443
|
+
confidence: Number(candidate.confidence.toFixed(3)),
|
|
444
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
445
|
+
};
|
|
446
|
+
if (candidate.tags && candidate.tags.length > 0) {
|
|
447
|
+
frontmatter.tags = candidate.tags;
|
|
448
|
+
}
|
|
449
|
+
if (candidate.entities && candidate.entities.length > 0) {
|
|
450
|
+
frontmatter.entities = candidate.entities;
|
|
451
|
+
}
|
|
452
|
+
if (options.sourceSessionId) {
|
|
453
|
+
frontmatter.sessionId = options.sourceSessionId;
|
|
454
|
+
}
|
|
455
|
+
if (candidate.metadata) {
|
|
456
|
+
frontmatter.captureMetadata = candidate.metadata;
|
|
457
|
+
}
|
|
458
|
+
try {
|
|
459
|
+
return await vault.store({
|
|
460
|
+
category,
|
|
461
|
+
title,
|
|
462
|
+
content: candidate.content,
|
|
463
|
+
frontmatter
|
|
464
|
+
});
|
|
465
|
+
} catch (err) {
|
|
466
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
467
|
+
if (!message.includes("Document already exists")) {
|
|
468
|
+
throw err;
|
|
469
|
+
}
|
|
470
|
+
return vault.store({
|
|
471
|
+
category,
|
|
472
|
+
title: withCollisionSuffix(title),
|
|
473
|
+
content: candidate.content,
|
|
474
|
+
frontmatter
|
|
475
|
+
});
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
// src/plugin/slot.ts
|
|
481
|
+
var CATEGORY_BY_TYPE = {
|
|
482
|
+
fact: "facts",
|
|
483
|
+
preference: "preferences",
|
|
484
|
+
decision: "decisions",
|
|
485
|
+
lesson: "lessons",
|
|
486
|
+
entity: "people",
|
|
487
|
+
episode: "transcripts",
|
|
488
|
+
relationship: "people"
|
|
489
|
+
};
|
|
490
|
+
function resolveSlotVaultPath(defaults, options) {
|
|
491
|
+
return resolveVaultPath({
|
|
492
|
+
explicitPath: options?.vaultPath ?? defaults.vaultPath,
|
|
493
|
+
pluginConfig: options?.pluginConfig ?? defaults.pluginConfig,
|
|
494
|
+
agentId: options?.agentId ?? defaults.agentId
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
function normalizeTitle(content, fallbackPrefix = "memory") {
|
|
498
|
+
const stem = content.replace(/\s+/g, " ").trim().split(" ").slice(0, 8).join(" ");
|
|
499
|
+
const value = stem.length > 0 ? stem : `${fallbackPrefix}-${Date.now()}`;
|
|
500
|
+
return value.slice(0, 90);
|
|
501
|
+
}
|
|
502
|
+
function createMemorySlot(defaults = {}) {
|
|
503
|
+
const captureService = new LiveCaptureService();
|
|
504
|
+
async function getVault(options) {
|
|
505
|
+
const vaultPath = resolveSlotVaultPath(defaults, options);
|
|
506
|
+
const vault = new ClawVault(vaultPath);
|
|
507
|
+
await vault.load();
|
|
508
|
+
return vault;
|
|
509
|
+
}
|
|
510
|
+
return {
|
|
511
|
+
async search(query, options = {}) {
|
|
512
|
+
const vault = await getVault(options);
|
|
513
|
+
const { vaultPath: _vaultPath, pluginConfig: _pluginConfig, agentId: _agentId, ...searchOptions } = options;
|
|
514
|
+
return vault.find(query, searchOptions);
|
|
515
|
+
},
|
|
516
|
+
async recall(query, options = {}) {
|
|
517
|
+
const vault = await getVault(options);
|
|
518
|
+
const result = await buildRecallResult(vault, query, {
|
|
519
|
+
...options,
|
|
520
|
+
includeSources: options.includeSources ?? true
|
|
521
|
+
});
|
|
522
|
+
return result.context;
|
|
523
|
+
},
|
|
524
|
+
async capture(messages, options = {}) {
|
|
525
|
+
return captureService.captureTurn(messages, {
|
|
526
|
+
...options,
|
|
527
|
+
vaultPath: options.vaultPath ?? defaults.vaultPath,
|
|
528
|
+
pluginConfig: options.pluginConfig ?? defaults.pluginConfig,
|
|
529
|
+
agentId: options.agentId ?? defaults.agentId
|
|
530
|
+
});
|
|
531
|
+
},
|
|
532
|
+
async store(content, metadata = {}) {
|
|
533
|
+
const vault = await getVault(metadata);
|
|
534
|
+
const category = metadata.category ?? (metadata.type ? CATEGORY_BY_TYPE[metadata.type] : "inbox");
|
|
535
|
+
const title = metadata.title ?? normalizeTitle(content, metadata.type ?? "memory");
|
|
536
|
+
const frontmatter = {
|
|
537
|
+
...metadata.frontmatter ?? {}
|
|
538
|
+
};
|
|
539
|
+
if (metadata.tags && metadata.tags.length > 0) {
|
|
540
|
+
frontmatter.tags = metadata.tags;
|
|
541
|
+
}
|
|
542
|
+
if (metadata.type) {
|
|
543
|
+
frontmatter.memoryType = metadata.type;
|
|
544
|
+
}
|
|
545
|
+
return vault.store({
|
|
546
|
+
category,
|
|
547
|
+
title,
|
|
548
|
+
content,
|
|
549
|
+
frontmatter
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
}
|
|
554
|
+
function createMemorySlotPlugin(defaults = {}) {
|
|
555
|
+
return {
|
|
556
|
+
plugins: {
|
|
557
|
+
slots: {
|
|
558
|
+
memory: createMemorySlot(defaults)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function registerMemorySlot(registry, defaults = {}) {
|
|
564
|
+
if (!registry.plugins || typeof registry.plugins !== "object") {
|
|
565
|
+
registry.plugins = {};
|
|
566
|
+
}
|
|
567
|
+
if (!registry.plugins.slots || typeof registry.plugins.slots !== "object") {
|
|
568
|
+
registry.plugins.slots = {};
|
|
569
|
+
}
|
|
570
|
+
registry.plugins.slots.memory = createMemorySlot(defaults);
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
// src/plugin/config.ts
|
|
574
|
+
import * as fs from "fs";
|
|
575
|
+
import * as os from "os";
|
|
576
|
+
import * as path from "path";
|
|
577
|
+
var SESSION_KEY_RE = /^agent:[a-zA-Z0-9_-]+:[a-zA-Z0-9:_-]+$/;
|
|
578
|
+
var AGENT_ID_RE = /^[a-zA-Z0-9_-]{1,100}$/;
|
|
579
|
+
function isRecord(value) {
|
|
580
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
581
|
+
}
|
|
582
|
+
function readPluginConfig(api) {
|
|
583
|
+
if (!isRecord(api.pluginConfig)) {
|
|
584
|
+
return {};
|
|
585
|
+
}
|
|
586
|
+
return api.pluginConfig;
|
|
587
|
+
}
|
|
588
|
+
function isOptInEnabled(pluginConfig, ...keys) {
|
|
589
|
+
return keys.some((key) => pluginConfig[key] === true);
|
|
590
|
+
}
|
|
591
|
+
function isFeatureEnabled(pluginConfig, key, defaultValue) {
|
|
592
|
+
const value = pluginConfig[key];
|
|
593
|
+
if (typeof value === "boolean") return value;
|
|
594
|
+
return defaultValue;
|
|
595
|
+
}
|
|
596
|
+
function allowsEnvAccess(pluginConfig) {
|
|
597
|
+
return isOptInEnabled(pluginConfig, "allowEnvAccess");
|
|
598
|
+
}
|
|
599
|
+
function sanitizeSessionKey(value) {
|
|
600
|
+
if (typeof value !== "string") return "";
|
|
601
|
+
const trimmed = value.trim();
|
|
602
|
+
if (!SESSION_KEY_RE.test(trimmed)) return "";
|
|
603
|
+
return trimmed.slice(0, 200);
|
|
604
|
+
}
|
|
605
|
+
function sanitizeAgentId(value) {
|
|
606
|
+
if (typeof value !== "string") return "";
|
|
607
|
+
const trimmed = value.trim();
|
|
608
|
+
if (!AGENT_ID_RE.test(trimmed)) return "";
|
|
609
|
+
return trimmed;
|
|
610
|
+
}
|
|
611
|
+
function extractAgentIdFromSessionKey(sessionKey) {
|
|
612
|
+
const match = /^agent:([^:]+):/.exec(sessionKey);
|
|
613
|
+
if (!match?.[1]) return "";
|
|
614
|
+
return sanitizeAgentId(match[1]);
|
|
615
|
+
}
|
|
616
|
+
function resolveAgentId(ctx, pluginConfig) {
|
|
617
|
+
const fromContext = sanitizeAgentId(ctx.agentId);
|
|
618
|
+
if (fromContext) return fromContext;
|
|
619
|
+
const fromSessionKey = extractAgentIdFromSessionKey(sanitizeSessionKey(ctx.sessionKey));
|
|
620
|
+
if (fromSessionKey) return fromSessionKey;
|
|
621
|
+
if (allowsEnvAccess(pluginConfig)) {
|
|
622
|
+
const fromEnv = sanitizeAgentId(process.env.OPENCLAW_AGENT_ID);
|
|
623
|
+
if (fromEnv) return fromEnv;
|
|
624
|
+
}
|
|
625
|
+
return "main";
|
|
626
|
+
}
|
|
627
|
+
function getConfiguredExecutablePath(pluginConfig) {
|
|
628
|
+
if (typeof pluginConfig.clawvaultBinaryPath !== "string") return null;
|
|
629
|
+
const trimmed = pluginConfig.clawvaultBinaryPath.trim();
|
|
630
|
+
return trimmed || null;
|
|
631
|
+
}
|
|
632
|
+
function getConfiguredExecutableSha256(pluginConfig) {
|
|
633
|
+
if (typeof pluginConfig.clawvaultBinarySha256 !== "string") return null;
|
|
634
|
+
const trimmed = pluginConfig.clawvaultBinarySha256.trim().toLowerCase();
|
|
635
|
+
return trimmed || null;
|
|
636
|
+
}
|
|
637
|
+
function normalizeAbsoluteEnvPath(value) {
|
|
638
|
+
if (typeof value !== "string") return null;
|
|
639
|
+
const trimmed = value.trim();
|
|
640
|
+
if (!trimmed) return null;
|
|
641
|
+
const resolved = path.resolve(trimmed);
|
|
642
|
+
if (!path.isAbsolute(resolved)) return null;
|
|
643
|
+
return resolved;
|
|
644
|
+
}
|
|
645
|
+
function getOpenClawAgentsDir(pluginConfig) {
|
|
646
|
+
if (allowsEnvAccess(pluginConfig)) {
|
|
647
|
+
const stateDir = normalizeAbsoluteEnvPath(process.env.OPENCLAW_STATE_DIR);
|
|
648
|
+
if (stateDir) {
|
|
649
|
+
return path.join(stateDir, "agents");
|
|
650
|
+
}
|
|
651
|
+
const openClawHome = normalizeAbsoluteEnvPath(process.env.OPENCLAW_HOME);
|
|
652
|
+
if (openClawHome) {
|
|
653
|
+
return path.join(openClawHome, "agents");
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
return path.join(os.homedir(), ".openclaw", "agents");
|
|
657
|
+
}
|
|
658
|
+
function validateVaultPath(vaultPath) {
|
|
659
|
+
if (!vaultPath || typeof vaultPath !== "string") return null;
|
|
660
|
+
const resolved = path.resolve(vaultPath);
|
|
661
|
+
if (!path.isAbsolute(resolved)) return null;
|
|
662
|
+
try {
|
|
663
|
+
const stat = fs.statSync(resolved);
|
|
664
|
+
if (!stat.isDirectory()) return null;
|
|
665
|
+
} catch {
|
|
666
|
+
return null;
|
|
667
|
+
}
|
|
668
|
+
const configPath = path.join(resolved, ".clawvault.json");
|
|
669
|
+
if (!fs.existsSync(configPath)) return null;
|
|
670
|
+
return resolved;
|
|
671
|
+
}
|
|
672
|
+
function resolveAgentVaultPath(pluginConfig, agentId) {
|
|
673
|
+
if (!agentId) return null;
|
|
674
|
+
if (!isRecord(pluginConfig.agentVaults)) return null;
|
|
675
|
+
const configured = pluginConfig.agentVaults[agentId];
|
|
676
|
+
if (typeof configured !== "string") return null;
|
|
677
|
+
return validateVaultPath(configured);
|
|
678
|
+
}
|
|
679
|
+
function findVaultPath(pluginConfig, options = {}) {
|
|
680
|
+
const agentId = sanitizeAgentId(options.agentId);
|
|
681
|
+
if (agentId) {
|
|
682
|
+
const perAgent = resolveAgentVaultPath(pluginConfig, agentId);
|
|
683
|
+
if (perAgent) return perAgent;
|
|
684
|
+
}
|
|
685
|
+
const configured = validateVaultPath(pluginConfig.vaultPath ?? null);
|
|
686
|
+
if (configured) return configured;
|
|
687
|
+
if (allowsEnvAccess(pluginConfig)) {
|
|
688
|
+
const fromPluginEnv = validateVaultPath(process.env.OPENCLAW_PLUGIN_CLAWVAULT_VAULTPATH ?? null);
|
|
689
|
+
if (fromPluginEnv) return fromPluginEnv;
|
|
690
|
+
const fromClawVaultEnv = validateVaultPath(process.env.CLAWVAULT_PATH ?? null);
|
|
691
|
+
if (fromClawVaultEnv) return fromClawVaultEnv;
|
|
692
|
+
}
|
|
693
|
+
let dir = path.resolve(options.cwd ?? process.cwd());
|
|
694
|
+
const root = path.parse(dir).root;
|
|
695
|
+
while (dir !== root) {
|
|
696
|
+
const direct = validateVaultPath(dir);
|
|
697
|
+
if (direct) return direct;
|
|
698
|
+
const memorySubdir = validateVaultPath(path.join(dir, "memory"));
|
|
699
|
+
if (memorySubdir) return memorySubdir;
|
|
700
|
+
dir = path.dirname(dir);
|
|
701
|
+
}
|
|
702
|
+
return null;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
// src/plugin/memory-manager.ts
|
|
706
|
+
import * as fs4 from "fs";
|
|
707
|
+
import * as path4 from "path";
|
|
708
|
+
|
|
709
|
+
// src/plugin/clawvault-cli.ts
|
|
710
|
+
import { execFileSync } from "child_process";
|
|
711
|
+
import * as fs3 from "fs";
|
|
712
|
+
import * as path3 from "path";
|
|
713
|
+
|
|
714
|
+
// src/plugin/integrity.ts
|
|
715
|
+
import { createHash } from "crypto";
|
|
716
|
+
import * as fs2 from "fs";
|
|
717
|
+
import * as path2 from "path";
|
|
718
|
+
var WINDOWS_PATHEXT_DEFAULT = ".EXE;.CMD;.BAT;.COM";
|
|
719
|
+
function splitPathEnv(pathEnv) {
|
|
720
|
+
if (typeof pathEnv !== "string" || !pathEnv.trim()) {
|
|
721
|
+
return [];
|
|
722
|
+
}
|
|
723
|
+
return pathEnv.split(path2.delimiter).map((entry) => entry.trim()).filter(Boolean);
|
|
724
|
+
}
|
|
725
|
+
function listExecutableCandidates(commandName) {
|
|
726
|
+
if (process.platform !== "win32") {
|
|
727
|
+
return [commandName];
|
|
728
|
+
}
|
|
729
|
+
const ext = path2.extname(commandName);
|
|
730
|
+
if (ext) {
|
|
731
|
+
return [commandName];
|
|
732
|
+
}
|
|
733
|
+
const pathext = splitPathEnv(process.env.PATHEXT || WINDOWS_PATHEXT_DEFAULT).map((candidate) => candidate.toLowerCase());
|
|
734
|
+
if (pathext.length === 0) {
|
|
735
|
+
return [commandName];
|
|
736
|
+
}
|
|
737
|
+
return pathext.map((candidate) => `${commandName}${candidate}`);
|
|
738
|
+
}
|
|
739
|
+
function isExecutablePath(candidatePath) {
|
|
740
|
+
try {
|
|
741
|
+
const stats = fs2.statSync(candidatePath);
|
|
742
|
+
if (!stats.isFile()) return false;
|
|
743
|
+
if (process.platform === "win32") return true;
|
|
744
|
+
return (stats.mode & 73) !== 0;
|
|
745
|
+
} catch {
|
|
746
|
+
return false;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
function resolveFromPath(commandName, pathEnv) {
|
|
750
|
+
const candidates = listExecutableCandidates(commandName);
|
|
751
|
+
const directories = splitPathEnv(pathEnv);
|
|
752
|
+
for (const directory of directories) {
|
|
753
|
+
for (const candidate of candidates) {
|
|
754
|
+
const absoluteCandidate = path2.resolve(directory, candidate);
|
|
755
|
+
if (isExecutablePath(absoluteCandidate)) {
|
|
756
|
+
return absoluteCandidate;
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
return null;
|
|
761
|
+
}
|
|
762
|
+
function resolveExecutablePath(commandName, options = {}) {
|
|
763
|
+
if (typeof commandName !== "string" || !commandName.trim()) {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
const explicitPath = typeof options.explicitPath === "string" ? options.explicitPath.trim() : "";
|
|
767
|
+
if (explicitPath) {
|
|
768
|
+
const absolute = path2.resolve(explicitPath);
|
|
769
|
+
return isExecutablePath(absolute) ? absolute : null;
|
|
770
|
+
}
|
|
771
|
+
if (commandName.includes(path2.sep)) {
|
|
772
|
+
const absolute = path2.resolve(commandName);
|
|
773
|
+
return isExecutablePath(absolute) ? absolute : null;
|
|
774
|
+
}
|
|
775
|
+
return resolveFromPath(commandName, process.env.PATH);
|
|
776
|
+
}
|
|
777
|
+
function sanitizeExecArgs(args) {
|
|
778
|
+
if (!Array.isArray(args)) {
|
|
779
|
+
throw new Error("Arguments must be an array");
|
|
780
|
+
}
|
|
781
|
+
return args.map((value, index) => {
|
|
782
|
+
if (typeof value !== "string") {
|
|
783
|
+
throw new Error(`Argument ${index} is not a string`);
|
|
784
|
+
}
|
|
785
|
+
if (value.includes("\0")) {
|
|
786
|
+
throw new Error(`Argument ${index} contains a null byte`);
|
|
787
|
+
}
|
|
788
|
+
return value;
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
function verifyExecutableIntegrity(executablePath, expectedSha256) {
|
|
792
|
+
if (typeof expectedSha256 !== "string" || !expectedSha256.trim()) {
|
|
793
|
+
return { ok: true, actualSha256: null };
|
|
794
|
+
}
|
|
795
|
+
const normalizedExpected = expectedSha256.trim().toLowerCase();
|
|
796
|
+
if (!/^[a-f0-9]{64}$/.test(normalizedExpected)) {
|
|
797
|
+
return { ok: false, actualSha256: null };
|
|
798
|
+
}
|
|
799
|
+
const payload = fs2.readFileSync(executablePath);
|
|
800
|
+
const actualSha256 = createHash("sha256").update(payload).digest("hex").toLowerCase();
|
|
801
|
+
return {
|
|
802
|
+
ok: actualSha256 === normalizedExpected,
|
|
803
|
+
actualSha256
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
|
|
807
|
+
// src/plugin/clawvault-cli.ts
|
|
808
|
+
var MAX_CONTEXT_PROMPT_LENGTH = 500;
|
|
809
|
+
var MAX_CONTEXT_SNIPPET_LENGTH = 220;
|
|
810
|
+
var MAX_RECAP_SNIPPET_LENGTH = 220;
|
|
811
|
+
var CLAWVAULT_EXECUTABLE = "clawvault";
|
|
812
|
+
var ONE_KIB = 1024;
|
|
813
|
+
var ONE_MIB = ONE_KIB * ONE_KIB;
|
|
814
|
+
var SMALL_SESSION_THRESHOLD_BYTES = 50 * ONE_KIB;
|
|
815
|
+
var MEDIUM_SESSION_THRESHOLD_BYTES = 150 * ONE_KIB;
|
|
816
|
+
var LARGE_SESSION_THRESHOLD_BYTES = 300 * ONE_KIB;
|
|
817
|
+
var OBSERVE_CURSOR_FILE = "observe-cursors.json";
|
|
818
|
+
function sanitizeForDisplay(value) {
|
|
819
|
+
if (typeof value !== "string") return "";
|
|
820
|
+
return value.replace(/[\x00-\x1f\x7f]/g, "").replace(/[`*_~\[\]]/g, "\\$&").slice(0, 240);
|
|
821
|
+
}
|
|
822
|
+
function sanitizePromptForContext(value) {
|
|
823
|
+
if (typeof value !== "string") return "";
|
|
824
|
+
return value.replace(/[\x00-\x1f\x7f]/g, " ").replace(/\s+/g, " ").trim().slice(0, MAX_CONTEXT_PROMPT_LENGTH);
|
|
825
|
+
}
|
|
826
|
+
function truncateSnippet(snippet) {
|
|
827
|
+
const safe = sanitizeForDisplay(snippet).replace(/\s+/g, " ").trim();
|
|
828
|
+
if (safe.length <= MAX_CONTEXT_SNIPPET_LENGTH) return safe;
|
|
829
|
+
return `${safe.slice(0, MAX_CONTEXT_SNIPPET_LENGTH - 3).trimEnd()}...`;
|
|
830
|
+
}
|
|
831
|
+
function truncateRecapSnippet(snippet) {
|
|
832
|
+
const safe = sanitizeForDisplay(snippet).replace(/\s+/g, " ").trim();
|
|
833
|
+
if (safe.length <= MAX_RECAP_SNIPPET_LENGTH) return safe;
|
|
834
|
+
return `${safe.slice(0, MAX_RECAP_SNIPPET_LENGTH - 3).trimEnd()}...`;
|
|
835
|
+
}
|
|
836
|
+
function isRecord2(value) {
|
|
837
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
838
|
+
}
|
|
839
|
+
function parseTopLevelJson(output) {
|
|
840
|
+
const text = output.trim();
|
|
841
|
+
if (!text) return null;
|
|
842
|
+
const tryParse = (candidate) => {
|
|
843
|
+
try {
|
|
844
|
+
const parsed = JSON.parse(candidate);
|
|
845
|
+
return isRecord2(parsed) ? parsed : null;
|
|
846
|
+
} catch {
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
};
|
|
850
|
+
const direct = tryParse(text);
|
|
851
|
+
if (direct) return direct;
|
|
852
|
+
const findJsonEnd = (start) => {
|
|
853
|
+
const open = text[start];
|
|
854
|
+
if (open !== "{" && open !== "[") return -1;
|
|
855
|
+
const stack = [open];
|
|
856
|
+
let inString = false;
|
|
857
|
+
let escaped = false;
|
|
858
|
+
for (let i = start + 1; i < text.length; i += 1) {
|
|
859
|
+
const ch = text[i];
|
|
860
|
+
if (inString) {
|
|
861
|
+
if (escaped) {
|
|
862
|
+
escaped = false;
|
|
863
|
+
continue;
|
|
864
|
+
}
|
|
865
|
+
if (ch === "\\") {
|
|
866
|
+
escaped = true;
|
|
867
|
+
continue;
|
|
868
|
+
}
|
|
869
|
+
if (ch === '"') {
|
|
870
|
+
inString = false;
|
|
871
|
+
}
|
|
872
|
+
continue;
|
|
873
|
+
}
|
|
874
|
+
if (ch === '"') {
|
|
875
|
+
inString = true;
|
|
876
|
+
continue;
|
|
877
|
+
}
|
|
878
|
+
if (ch === "{" || ch === "[") {
|
|
879
|
+
stack.push(ch);
|
|
880
|
+
continue;
|
|
881
|
+
}
|
|
882
|
+
if (ch === "}" || ch === "]") {
|
|
883
|
+
const expected = ch === "}" ? "{" : "[";
|
|
884
|
+
const top = stack[stack.length - 1];
|
|
885
|
+
if (top !== expected) return -1;
|
|
886
|
+
stack.pop();
|
|
887
|
+
if (stack.length === 0) {
|
|
888
|
+
return i;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
return -1;
|
|
893
|
+
};
|
|
894
|
+
for (let start = 0; start < text.length; start += 1) {
|
|
895
|
+
const ch = text[start];
|
|
896
|
+
if (ch !== "{" && ch !== "[") continue;
|
|
897
|
+
const end = findJsonEnd(start);
|
|
898
|
+
if (end < 0) continue;
|
|
899
|
+
const parsed = tryParse(text.slice(start, end + 1));
|
|
900
|
+
if (parsed) return parsed;
|
|
901
|
+
}
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
904
|
+
function resolveEntryArray(parsed, keys) {
|
|
905
|
+
for (const key of keys) {
|
|
906
|
+
const value = parsed[key];
|
|
907
|
+
if (!Array.isArray(value)) continue;
|
|
908
|
+
return value.filter((item) => isRecord2(item));
|
|
909
|
+
}
|
|
910
|
+
return [];
|
|
911
|
+
}
|
|
912
|
+
function parseContextJson(output, maxResults) {
|
|
913
|
+
const parsed = parseTopLevelJson(output);
|
|
914
|
+
if (!parsed) {
|
|
915
|
+
return [];
|
|
916
|
+
}
|
|
917
|
+
const rows = resolveEntryArray(parsed, ["context", "results", "entries", "memories"]);
|
|
918
|
+
return rows.slice(0, maxResults).map((entry) => {
|
|
919
|
+
const nestedDocument = isRecord2(entry.document) ? entry.document : null;
|
|
920
|
+
const title = sanitizeForDisplay(
|
|
921
|
+
entry.title ?? nestedDocument?.title ?? nestedDocument?.id ?? entry.path ?? "Untitled"
|
|
922
|
+
);
|
|
923
|
+
const resolvedPath = sanitizeForDisplay(
|
|
924
|
+
entry.path ?? entry.relPath ?? nestedDocument?.path ?? ""
|
|
925
|
+
);
|
|
926
|
+
const resolvedAge = sanitizeForDisplay(entry.age ?? entry.modified ?? "unknown age");
|
|
927
|
+
const snippetSource = String(
|
|
928
|
+
entry.snippet ?? entry.text ?? entry.content ?? nestedDocument?.snippet ?? nestedDocument?.content ?? ""
|
|
929
|
+
);
|
|
930
|
+
return {
|
|
931
|
+
title,
|
|
932
|
+
path: resolvedPath,
|
|
933
|
+
age: resolvedAge,
|
|
934
|
+
snippet: truncateSnippet(snippetSource),
|
|
935
|
+
score: Number.isFinite(Number(entry.score)) ? Number(entry.score) : 0
|
|
936
|
+
};
|
|
937
|
+
}).filter((entry) => entry.snippet.length > 0);
|
|
938
|
+
}
|
|
939
|
+
function parseSessionRecapJson(output, maxResults) {
|
|
940
|
+
const parsed = parseTopLevelJson(output);
|
|
941
|
+
if (!parsed) {
|
|
942
|
+
return [];
|
|
943
|
+
}
|
|
944
|
+
const rows = resolveEntryArray(parsed, ["messages", "turns", "recap"]);
|
|
945
|
+
return rows.map((entry) => {
|
|
946
|
+
const role = typeof entry.role === "string" ? entry.role.toLowerCase() : "";
|
|
947
|
+
const normalizedRole = role === "user" || role === "human" ? "User" : role === "assistant" || role === "ai" ? "Assistant" : "";
|
|
948
|
+
if (!normalizedRole) return null;
|
|
949
|
+
const text = truncateRecapSnippet(
|
|
950
|
+
typeof entry.text === "string" ? entry.text : typeof entry.content === "string" ? entry.content : ""
|
|
951
|
+
);
|
|
952
|
+
if (!text) return null;
|
|
953
|
+
return {
|
|
954
|
+
role: normalizedRole,
|
|
955
|
+
text
|
|
956
|
+
};
|
|
957
|
+
}).filter((entry) => Boolean(entry)).slice(-maxResults);
|
|
958
|
+
}
|
|
959
|
+
function formatSessionContextInjection(recapEntries, memoryEntries) {
|
|
960
|
+
const lines = [
|
|
961
|
+
"[ClawVault] Session context restored:",
|
|
962
|
+
"",
|
|
963
|
+
"Recent conversation:"
|
|
964
|
+
];
|
|
965
|
+
if (recapEntries.length === 0) {
|
|
966
|
+
lines.push("- No recent user/assistant turns found for this session.");
|
|
967
|
+
} else {
|
|
968
|
+
for (const entry of recapEntries) {
|
|
969
|
+
lines.push(`- ${entry.role}: ${entry.text}`);
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
lines.push("", "Relevant memories:");
|
|
973
|
+
if (memoryEntries.length === 0) {
|
|
974
|
+
lines.push("- No relevant vault memories found for the current prompt.");
|
|
975
|
+
} else {
|
|
976
|
+
for (const entry of memoryEntries) {
|
|
977
|
+
const pathSuffix = entry.path ? ` [${entry.path}]` : "";
|
|
978
|
+
lines.push(`- ${entry.title} (${entry.age}${pathSuffix}): ${entry.snippet}`);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
return lines.join("\n");
|
|
982
|
+
}
|
|
983
|
+
function resolveVaultPathForAgent(pluginConfig, options = {}) {
|
|
984
|
+
return findVaultPath(pluginConfig, options);
|
|
985
|
+
}
|
|
986
|
+
function runClawvault(args, pluginConfig, options = {}) {
|
|
987
|
+
if (!isOptInEnabled(pluginConfig, "allowClawvaultExec")) {
|
|
988
|
+
return {
|
|
989
|
+
success: false,
|
|
990
|
+
skipped: true,
|
|
991
|
+
output: "ClawVault CLI execution is disabled. Set allowClawvaultExec=true to enable.",
|
|
992
|
+
code: 0
|
|
993
|
+
};
|
|
994
|
+
}
|
|
995
|
+
const timeoutMs = Number.isFinite(options.timeoutMs) ? Math.max(1e3, Number(options.timeoutMs)) : 15e3;
|
|
996
|
+
const executablePath = resolveExecutablePath(CLAWVAULT_EXECUTABLE, {
|
|
997
|
+
explicitPath: getConfiguredExecutablePath(pluginConfig)
|
|
998
|
+
});
|
|
999
|
+
if (!executablePath) {
|
|
1000
|
+
return {
|
|
1001
|
+
success: false,
|
|
1002
|
+
output: "Unable to resolve clawvault executable path.",
|
|
1003
|
+
code: 1
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
const expectedSha256 = getConfiguredExecutableSha256(pluginConfig);
|
|
1007
|
+
const integrityResult = verifyExecutableIntegrity(executablePath, expectedSha256);
|
|
1008
|
+
if (!integrityResult.ok) {
|
|
1009
|
+
return {
|
|
1010
|
+
success: false,
|
|
1011
|
+
output: `Executable integrity verification failed for ${executablePath}.`,
|
|
1012
|
+
code: 1
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
let sanitizedArgs;
|
|
1016
|
+
try {
|
|
1017
|
+
sanitizedArgs = sanitizeExecArgs(args);
|
|
1018
|
+
} catch (error) {
|
|
1019
|
+
return {
|
|
1020
|
+
success: false,
|
|
1021
|
+
output: error instanceof Error ? error.message : "Invalid command arguments",
|
|
1022
|
+
code: 1
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
try {
|
|
1026
|
+
const output = execFileSync(executablePath, sanitizedArgs, {
|
|
1027
|
+
encoding: "utf-8",
|
|
1028
|
+
timeout: timeoutMs,
|
|
1029
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1030
|
+
shell: false
|
|
1031
|
+
});
|
|
1032
|
+
return {
|
|
1033
|
+
success: true,
|
|
1034
|
+
output: output.trim(),
|
|
1035
|
+
code: 0
|
|
1036
|
+
};
|
|
1037
|
+
} catch (error) {
|
|
1038
|
+
const details = error;
|
|
1039
|
+
return {
|
|
1040
|
+
success: false,
|
|
1041
|
+
output: details.stderr?.toString() || details.message || "unknown error",
|
|
1042
|
+
code: details.status || 1
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
function parseRecoveryOutput(output) {
|
|
1047
|
+
if (!output || typeof output !== "string") {
|
|
1048
|
+
return { hadDeath: false, workingOn: null };
|
|
1049
|
+
}
|
|
1050
|
+
const hadDeath = output.includes("Context death detected") || output.includes("died") || output.includes("\u26A0\uFE0F");
|
|
1051
|
+
if (!hadDeath) {
|
|
1052
|
+
return { hadDeath: false, workingOn: null };
|
|
1053
|
+
}
|
|
1054
|
+
const workingOnLine = output.split("\n").find((line) => line.toLowerCase().includes("working on"));
|
|
1055
|
+
if (!workingOnLine) {
|
|
1056
|
+
return { hadDeath: true, workingOn: null };
|
|
1057
|
+
}
|
|
1058
|
+
const parts = workingOnLine.split(":");
|
|
1059
|
+
const workingOn = parts.length > 1 ? sanitizeForDisplay(parts.slice(1).join(":").trim()) : null;
|
|
1060
|
+
return { hadDeath: true, workingOn: workingOn || null };
|
|
1061
|
+
}
|
|
1062
|
+
function getObserveCursorPath(vaultPath) {
|
|
1063
|
+
return path3.join(vaultPath, ".clawvault", OBSERVE_CURSOR_FILE);
|
|
1064
|
+
}
|
|
1065
|
+
function loadObserveCursors(vaultPath) {
|
|
1066
|
+
const cursorPath = getObserveCursorPath(vaultPath);
|
|
1067
|
+
if (!fs3.existsSync(cursorPath)) {
|
|
1068
|
+
return {};
|
|
1069
|
+
}
|
|
1070
|
+
try {
|
|
1071
|
+
const parsed = JSON.parse(fs3.readFileSync(cursorPath, "utf-8"));
|
|
1072
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1073
|
+
return {};
|
|
1074
|
+
}
|
|
1075
|
+
return parsed;
|
|
1076
|
+
} catch {
|
|
1077
|
+
return {};
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
function getScaledObservationThresholdBytes(fileSizeBytes) {
|
|
1081
|
+
if (!Number.isFinite(fileSizeBytes) || fileSizeBytes <= 0) {
|
|
1082
|
+
return SMALL_SESSION_THRESHOLD_BYTES;
|
|
1083
|
+
}
|
|
1084
|
+
if (fileSizeBytes < ONE_MIB) {
|
|
1085
|
+
return SMALL_SESSION_THRESHOLD_BYTES;
|
|
1086
|
+
}
|
|
1087
|
+
if (fileSizeBytes <= 5 * ONE_MIB) {
|
|
1088
|
+
return MEDIUM_SESSION_THRESHOLD_BYTES;
|
|
1089
|
+
}
|
|
1090
|
+
return LARGE_SESSION_THRESHOLD_BYTES;
|
|
1091
|
+
}
|
|
1092
|
+
function parseSessionIndex(agentId, pluginConfig) {
|
|
1093
|
+
const sessionsDir = path3.join(getOpenClawAgentsDir(pluginConfig), agentId, "sessions");
|
|
1094
|
+
const sessionsJsonPath = path3.join(sessionsDir, "sessions.json");
|
|
1095
|
+
if (!fs3.existsSync(sessionsJsonPath)) {
|
|
1096
|
+
return { sessionsDir, index: {} };
|
|
1097
|
+
}
|
|
1098
|
+
try {
|
|
1099
|
+
const parsed = JSON.parse(fs3.readFileSync(sessionsJsonPath, "utf-8"));
|
|
1100
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
1101
|
+
return { sessionsDir, index: {} };
|
|
1102
|
+
}
|
|
1103
|
+
return {
|
|
1104
|
+
sessionsDir,
|
|
1105
|
+
index: parsed
|
|
1106
|
+
};
|
|
1107
|
+
} catch {
|
|
1108
|
+
return { sessionsDir, index: {} };
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
function shouldObserveActiveSessions(vaultPath, agentId, pluginConfig) {
|
|
1112
|
+
const cursors = loadObserveCursors(vaultPath);
|
|
1113
|
+
const { sessionsDir, index } = parseSessionIndex(agentId, pluginConfig);
|
|
1114
|
+
const entries = Object.entries(index);
|
|
1115
|
+
if (entries.length === 0) {
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
for (const [sessionKey, value] of entries) {
|
|
1119
|
+
const sessionId = typeof value?.sessionId === "string" ? value.sessionId.trim() : "";
|
|
1120
|
+
if (!/^[a-zA-Z0-9._-]{1,200}$/.test(sessionId)) continue;
|
|
1121
|
+
const filePath = path3.join(sessionsDir, `${sessionId}.jsonl`);
|
|
1122
|
+
let stat;
|
|
1123
|
+
try {
|
|
1124
|
+
stat = fs3.statSync(filePath);
|
|
1125
|
+
} catch {
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
if (!stat.isFile()) continue;
|
|
1129
|
+
const fileSize = stat.size;
|
|
1130
|
+
const cursorEntry = cursors[sessionId];
|
|
1131
|
+
const previousOffset = Number.isFinite(cursorEntry?.lastObservedOffset) ? Math.max(0, Number(cursorEntry.lastObservedOffset)) : 0;
|
|
1132
|
+
const startOffset = previousOffset <= fileSize ? previousOffset : 0;
|
|
1133
|
+
const newBytes = Math.max(0, fileSize - startOffset);
|
|
1134
|
+
const thresholdBytes = getScaledObservationThresholdBytes(fileSize);
|
|
1135
|
+
if (newBytes >= thresholdBytes) {
|
|
1136
|
+
return true;
|
|
1137
|
+
}
|
|
1138
|
+
if (sessionKey === "main" && newBytes > 0) {
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
return false;
|
|
1142
|
+
}
|
|
1143
|
+
function runObserverCron(vaultPath, agentId, pluginConfig, options = {}) {
|
|
1144
|
+
const args = ["observe", "--cron", "--agent", agentId, "-v", vaultPath];
|
|
1145
|
+
if (Number.isFinite(options.minNewBytes) && Number(options.minNewBytes) > 0) {
|
|
1146
|
+
args.push("--min-new", String(Math.floor(Number(options.minNewBytes))));
|
|
1147
|
+
}
|
|
1148
|
+
const result = runClawvault(args, pluginConfig, { timeoutMs: 12e4 });
|
|
1149
|
+
return !result.skipped && result.success;
|
|
1150
|
+
}
|
|
1151
|
+
function resolveSessionKey(input) {
|
|
1152
|
+
return sanitizeSessionKey(input);
|
|
1153
|
+
}
|
|
1154
|
+
|
|
1155
|
+
// src/plugin/memory-manager.ts
|
|
1156
|
+
var DEFAULT_MAX_RESULTS = 6;
|
|
1157
|
+
var DEFAULT_MIN_SCORE = 0.2;
|
|
1158
|
+
function clamp(value, min, max) {
|
|
1159
|
+
return Math.max(min, Math.min(max, value));
|
|
1160
|
+
}
|
|
1161
|
+
function normalizeRelPath(relPath) {
|
|
1162
|
+
return relPath.replace(/\\/g, "/").replace(/^\.\/+/, "").replace(/^\/+/, "");
|
|
1163
|
+
}
|
|
1164
|
+
function estimateLineRange(content, snippet) {
|
|
1165
|
+
const cleanedSnippet = snippet.replace(/\s+/g, " ").trim();
|
|
1166
|
+
if (!cleanedSnippet) {
|
|
1167
|
+
return { startLine: 1, endLine: 1 };
|
|
1168
|
+
}
|
|
1169
|
+
const normalizedContent = content.replace(/\s+/g, " ");
|
|
1170
|
+
const index = normalizedContent.toLowerCase().indexOf(cleanedSnippet.toLowerCase());
|
|
1171
|
+
if (index < 0) {
|
|
1172
|
+
return { startLine: 1, endLine: Math.max(1, cleanedSnippet.split(/\r?\n/).length) };
|
|
1173
|
+
}
|
|
1174
|
+
const upToIndex = normalizedContent.slice(0, index);
|
|
1175
|
+
const startLine = upToIndex.split(/\r?\n/).length;
|
|
1176
|
+
const endLine = startLine + Math.max(1, cleanedSnippet.split(/\r?\n/).length) - 1;
|
|
1177
|
+
return { startLine, endLine };
|
|
1178
|
+
}
|
|
1179
|
+
function mapSearchResult(vaultPath, result) {
|
|
1180
|
+
const relPath = normalizeRelPath(path4.relative(vaultPath, result.document.path));
|
|
1181
|
+
const { startLine, endLine } = estimateLineRange(result.document.content, result.snippet);
|
|
1182
|
+
const source = relPath === "MEMORY.md" || relPath.startsWith("memory/") ? "memory" : "sessions";
|
|
1183
|
+
return {
|
|
1184
|
+
path: relPath || path4.basename(result.document.path),
|
|
1185
|
+
startLine,
|
|
1186
|
+
endLine,
|
|
1187
|
+
score: result.score,
|
|
1188
|
+
snippet: result.snippet,
|
|
1189
|
+
source,
|
|
1190
|
+
citation: `${relPath || path4.basename(result.document.path)}#L${startLine}-L${endLine}`
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
function countMarkdownFiles(root) {
|
|
1194
|
+
if (!fs4.existsSync(root)) return 0;
|
|
1195
|
+
let count = 0;
|
|
1196
|
+
const stack = [root];
|
|
1197
|
+
while (stack.length > 0) {
|
|
1198
|
+
const current = stack.pop();
|
|
1199
|
+
if (!current) continue;
|
|
1200
|
+
let entries;
|
|
1201
|
+
try {
|
|
1202
|
+
entries = fs4.readdirSync(current, { withFileTypes: true });
|
|
1203
|
+
} catch {
|
|
1204
|
+
continue;
|
|
1205
|
+
}
|
|
1206
|
+
for (const entry of entries) {
|
|
1207
|
+
const fullPath = path4.join(current, entry.name);
|
|
1208
|
+
if (entry.isDirectory()) {
|
|
1209
|
+
stack.push(fullPath);
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) {
|
|
1213
|
+
count += 1;
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
return count;
|
|
1218
|
+
}
|
|
1219
|
+
function toSafeFilePath(vaultPath, relPath) {
|
|
1220
|
+
const normalized = normalizeRelPath(relPath);
|
|
1221
|
+
const mapped = normalized.startsWith("qmd/") ? normalized.split("/").slice(2).join("/") : normalized;
|
|
1222
|
+
if (!mapped || mapped.includes("..")) {
|
|
1223
|
+
throw new Error("Invalid memory path");
|
|
1224
|
+
}
|
|
1225
|
+
if (!mapped.toLowerCase().endsWith(".md")) {
|
|
1226
|
+
throw new Error("memory_get only allows Markdown note paths inside the vault");
|
|
1227
|
+
}
|
|
1228
|
+
const absolute = path4.resolve(vaultPath, mapped);
|
|
1229
|
+
const vaultRootWithSep = vaultPath.endsWith(path4.sep) ? vaultPath : `${vaultPath}${path4.sep}`;
|
|
1230
|
+
if (absolute !== vaultPath && !absolute.startsWith(vaultRootWithSep)) {
|
|
1231
|
+
throw new Error("Path escapes vault root");
|
|
1232
|
+
}
|
|
1233
|
+
return absolute;
|
|
1234
|
+
}
|
|
1235
|
+
function resolveManagerVaultPath(options, sessionKey) {
|
|
1236
|
+
const derivedAgentId = sessionKey ? extractAgentIdFromSessionKey(sessionKey) : "";
|
|
1237
|
+
const agentId = derivedAgentId || options.defaultAgentId;
|
|
1238
|
+
return resolveVaultPathForAgent(options.pluginConfig, {
|
|
1239
|
+
agentId,
|
|
1240
|
+
cwd: options.workspaceDir
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
var ClawVaultMemoryManager = class {
|
|
1244
|
+
options;
|
|
1245
|
+
constructor(options) {
|
|
1246
|
+
this.options = options;
|
|
1247
|
+
}
|
|
1248
|
+
async search(query, opts = {}) {
|
|
1249
|
+
const normalizedQuery = sanitizePromptForContext(query);
|
|
1250
|
+
if (!normalizedQuery) return [];
|
|
1251
|
+
const vaultPath = resolveManagerVaultPath(this.options, opts.sessionKey);
|
|
1252
|
+
if (!vaultPath) return [];
|
|
1253
|
+
const maxResults = Number.isFinite(opts.maxResults) ? clamp(Math.floor(Number(opts.maxResults)), 1, 20) : DEFAULT_MAX_RESULTS;
|
|
1254
|
+
const minScore = Number.isFinite(opts.minScore) ? clamp(Number(opts.minScore), 0, 1) : DEFAULT_MIN_SCORE;
|
|
1255
|
+
try {
|
|
1256
|
+
const vault = new ClawVault(vaultPath);
|
|
1257
|
+
await vault.load();
|
|
1258
|
+
const results = await vault.find(normalizedQuery, {
|
|
1259
|
+
limit: maxResults,
|
|
1260
|
+
minScore,
|
|
1261
|
+
temporalBoost: true
|
|
1262
|
+
});
|
|
1263
|
+
return results.map((result) => mapSearchResult(vaultPath, result));
|
|
1264
|
+
} catch (error) {
|
|
1265
|
+
this.options.logger?.warn(
|
|
1266
|
+
`[clawvault] memory_search fallback error: ${error instanceof Error ? error.message : String(error)}`
|
|
1267
|
+
);
|
|
1268
|
+
return [];
|
|
1269
|
+
}
|
|
1270
|
+
}
|
|
1271
|
+
async readFile(params) {
|
|
1272
|
+
const vaultPath = resolveManagerVaultPath(this.options);
|
|
1273
|
+
const normalizedPath = normalizeRelPath(params.relPath);
|
|
1274
|
+
if (!vaultPath) {
|
|
1275
|
+
return { text: "", path: normalizedPath };
|
|
1276
|
+
}
|
|
1277
|
+
let absolutePath;
|
|
1278
|
+
try {
|
|
1279
|
+
absolutePath = toSafeFilePath(vaultPath, normalizedPath);
|
|
1280
|
+
} catch (error) {
|
|
1281
|
+
throw new Error(error instanceof Error ? error.message : "Invalid memory path");
|
|
1282
|
+
}
|
|
1283
|
+
if (!fs4.existsSync(absolutePath)) {
|
|
1284
|
+
return { text: "", path: normalizedPath };
|
|
1285
|
+
}
|
|
1286
|
+
const raw = fs4.readFileSync(absolutePath, "utf-8");
|
|
1287
|
+
if (!Number.isFinite(params.from) && !Number.isFinite(params.lines)) {
|
|
1288
|
+
return { text: raw, path: normalizedPath };
|
|
1289
|
+
}
|
|
1290
|
+
const from = Number.isFinite(params.from) ? Math.max(1, Math.floor(Number(params.from))) : 1;
|
|
1291
|
+
const lines = Number.isFinite(params.lines) ? Math.max(1, Math.floor(Number(params.lines))) : 120;
|
|
1292
|
+
const chunks = raw.split(/\r?\n/);
|
|
1293
|
+
const startIndex = from - 1;
|
|
1294
|
+
const sliced = chunks.slice(startIndex, startIndex + lines);
|
|
1295
|
+
return {
|
|
1296
|
+
text: sliced.join("\n"),
|
|
1297
|
+
path: normalizedPath
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
status() {
|
|
1301
|
+
const vaultPath = resolveManagerVaultPath(this.options);
|
|
1302
|
+
const markdownFiles = vaultPath ? countMarkdownFiles(path4.join(vaultPath, "memory")) : 0;
|
|
1303
|
+
return {
|
|
1304
|
+
backend: "builtin",
|
|
1305
|
+
provider: "clawvault",
|
|
1306
|
+
workspaceDir: vaultPath ?? this.options.workspaceDir,
|
|
1307
|
+
files: markdownFiles,
|
|
1308
|
+
sources: ["memory", "sessions"],
|
|
1309
|
+
vector: {
|
|
1310
|
+
enabled: true,
|
|
1311
|
+
available: hasQmd()
|
|
1312
|
+
}
|
|
1313
|
+
};
|
|
1314
|
+
}
|
|
1315
|
+
async sync(params) {
|
|
1316
|
+
params?.progress?.({ completed: 0, total: 1, label: "syncing" });
|
|
1317
|
+
const vaultPath = resolveManagerVaultPath(this.options);
|
|
1318
|
+
if (vaultPath) {
|
|
1319
|
+
const vault = new ClawVault(vaultPath);
|
|
1320
|
+
await vault.load();
|
|
1321
|
+
}
|
|
1322
|
+
params?.progress?.({ completed: 1, total: 1, label: "done" });
|
|
1323
|
+
}
|
|
1324
|
+
async probeEmbeddingAvailability() {
|
|
1325
|
+
try {
|
|
1326
|
+
const sample = await this.search("health probe", { maxResults: 1, minScore: 0 });
|
|
1327
|
+
if (sample.length >= 0) {
|
|
1328
|
+
return { ok: true };
|
|
1329
|
+
}
|
|
1330
|
+
return { ok: true };
|
|
1331
|
+
} catch (error) {
|
|
1332
|
+
return {
|
|
1333
|
+
ok: false,
|
|
1334
|
+
error: error instanceof Error ? error.message : String(error)
|
|
1335
|
+
};
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
async probeVectorAvailability() {
|
|
1339
|
+
return hasQmd();
|
|
1340
|
+
}
|
|
1341
|
+
async close() {
|
|
1342
|
+
return Promise.resolve();
|
|
1343
|
+
}
|
|
1344
|
+
};
|
|
1345
|
+
function buildToolSchema(properties, required = []) {
|
|
1346
|
+
return {
|
|
1347
|
+
type: "object",
|
|
1348
|
+
properties,
|
|
1349
|
+
required,
|
|
1350
|
+
additionalProperties: false
|
|
1351
|
+
};
|
|
1352
|
+
}
|
|
1353
|
+
function resolveToolInput(toolCallIdOrInput, maybeInput) {
|
|
1354
|
+
if (maybeInput && typeof maybeInput === "object" && !Array.isArray(maybeInput)) {
|
|
1355
|
+
return maybeInput;
|
|
1356
|
+
}
|
|
1357
|
+
if (toolCallIdOrInput && typeof toolCallIdOrInput === "object" && !Array.isArray(toolCallIdOrInput)) {
|
|
1358
|
+
return toolCallIdOrInput;
|
|
1359
|
+
}
|
|
1360
|
+
return {};
|
|
1361
|
+
}
|
|
1362
|
+
function createMemorySearchToolFactory(memoryManager) {
|
|
1363
|
+
return (_toolContext) => {
|
|
1364
|
+
const inputSchema = buildToolSchema({
|
|
1365
|
+
query: {
|
|
1366
|
+
type: "string",
|
|
1367
|
+
description: "Natural-language query for memory recall."
|
|
1368
|
+
},
|
|
1369
|
+
maxResults: {
|
|
1370
|
+
type: "number",
|
|
1371
|
+
minimum: 1,
|
|
1372
|
+
maximum: 20,
|
|
1373
|
+
description: "Maximum number of snippets to return."
|
|
1374
|
+
},
|
|
1375
|
+
minScore: {
|
|
1376
|
+
type: "number",
|
|
1377
|
+
minimum: 0,
|
|
1378
|
+
maximum: 1,
|
|
1379
|
+
description: "Minimum score threshold."
|
|
1380
|
+
},
|
|
1381
|
+
sessionKey: {
|
|
1382
|
+
type: "string",
|
|
1383
|
+
description: "Optional OpenClaw session key for scoped recall."
|
|
1384
|
+
}
|
|
1385
|
+
}, ["query"]);
|
|
1386
|
+
const execute = async (toolCallIdOrInput, maybeInput) => {
|
|
1387
|
+
const input = resolveToolInput(toolCallIdOrInput, maybeInput);
|
|
1388
|
+
const query = typeof input.query === "string" ? input.query : "";
|
|
1389
|
+
if (!query.trim()) {
|
|
1390
|
+
return { query, count: 0, results: [] };
|
|
1391
|
+
}
|
|
1392
|
+
const results = await memoryManager.search(query, {
|
|
1393
|
+
maxResults: Number.isFinite(Number(input.maxResults)) ? Number(input.maxResults) : void 0,
|
|
1394
|
+
minScore: Number.isFinite(Number(input.minScore)) ? Number(input.minScore) : void 0,
|
|
1395
|
+
sessionKey: typeof input.sessionKey === "string" ? input.sessionKey : void 0
|
|
1396
|
+
});
|
|
1397
|
+
return {
|
|
1398
|
+
query,
|
|
1399
|
+
count: results.length,
|
|
1400
|
+
results
|
|
1401
|
+
};
|
|
1402
|
+
};
|
|
1403
|
+
return {
|
|
1404
|
+
label: "Memory Search",
|
|
1405
|
+
name: "memory_search",
|
|
1406
|
+
description: "Search ClawVault memory for relevant snippets before answering.",
|
|
1407
|
+
inputSchema,
|
|
1408
|
+
input_schema: inputSchema,
|
|
1409
|
+
parameters: inputSchema,
|
|
1410
|
+
execute,
|
|
1411
|
+
run: execute,
|
|
1412
|
+
handler: execute
|
|
1413
|
+
};
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
function createMemoryGetToolFactory(memoryManager) {
|
|
1417
|
+
return (_toolContext) => {
|
|
1418
|
+
const inputSchema = buildToolSchema({
|
|
1419
|
+
path: {
|
|
1420
|
+
type: "string",
|
|
1421
|
+
description: "Relative path from memory_search result (for OpenClaw compatibility)."
|
|
1422
|
+
},
|
|
1423
|
+
relPath: {
|
|
1424
|
+
type: "string",
|
|
1425
|
+
description: "Alias of path (e.g. memory/2026-01-01.md)."
|
|
1426
|
+
},
|
|
1427
|
+
from: {
|
|
1428
|
+
type: "number",
|
|
1429
|
+
minimum: 1,
|
|
1430
|
+
description: "Optional start line (1-indexed)."
|
|
1431
|
+
},
|
|
1432
|
+
lines: {
|
|
1433
|
+
type: "number",
|
|
1434
|
+
minimum: 1,
|
|
1435
|
+
maximum: 400,
|
|
1436
|
+
description: "Optional number of lines to read."
|
|
1437
|
+
}
|
|
1438
|
+
});
|
|
1439
|
+
inputSchema.anyOf = [{ required: ["path"] }, { required: ["relPath"] }];
|
|
1440
|
+
const execute = async (toolCallIdOrInput, maybeInput) => {
|
|
1441
|
+
const input = resolveToolInput(toolCallIdOrInput, maybeInput);
|
|
1442
|
+
const relPath = typeof input.path === "string" ? input.path : typeof input.relPath === "string" ? input.relPath : "";
|
|
1443
|
+
if (!relPath.trim()) {
|
|
1444
|
+
return { path: relPath, text: "" };
|
|
1445
|
+
}
|
|
1446
|
+
return memoryManager.readFile({
|
|
1447
|
+
relPath,
|
|
1448
|
+
from: Number.isFinite(Number(input.from)) ? Number(input.from) : void 0,
|
|
1449
|
+
lines: Number.isFinite(Number(input.lines)) ? Number(input.lines) : void 0
|
|
1450
|
+
});
|
|
1451
|
+
};
|
|
1452
|
+
return {
|
|
1453
|
+
label: "Memory Get",
|
|
1454
|
+
name: "memory_get",
|
|
1455
|
+
description: "Read a specific memory file or line range from ClawVault.",
|
|
1456
|
+
inputSchema,
|
|
1457
|
+
input_schema: inputSchema,
|
|
1458
|
+
parameters: inputSchema,
|
|
1459
|
+
execute,
|
|
1460
|
+
run: execute,
|
|
1461
|
+
handler: execute
|
|
1462
|
+
};
|
|
1463
|
+
};
|
|
1464
|
+
}
|
|
1465
|
+
|
|
1466
|
+
// src/plugin/runtime-state.ts
|
|
1467
|
+
var ClawVaultPluginRuntimeState = class {
|
|
1468
|
+
startupRecoveryNotice = null;
|
|
1469
|
+
sessionContextByKey = /* @__PURE__ */ new Map();
|
|
1470
|
+
lastWeeklyReflectionWeekKey = null;
|
|
1471
|
+
setStartupRecoveryNotice(message) {
|
|
1472
|
+
const trimmed = message.trim();
|
|
1473
|
+
this.startupRecoveryNotice = trimmed || null;
|
|
1474
|
+
}
|
|
1475
|
+
consumeStartupRecoveryNotice() {
|
|
1476
|
+
const notice = this.startupRecoveryNotice;
|
|
1477
|
+
this.startupRecoveryNotice = null;
|
|
1478
|
+
return notice;
|
|
1479
|
+
}
|
|
1480
|
+
setSessionRecap(sessionKey, recapText) {
|
|
1481
|
+
const normalizedSessionKey = sessionKey.trim();
|
|
1482
|
+
if (!normalizedSessionKey) return;
|
|
1483
|
+
this.sessionContextByKey.set(normalizedSessionKey, {
|
|
1484
|
+
recapText: recapText.trim(),
|
|
1485
|
+
initializedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1486
|
+
recapInjected: false
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
getSessionRecap(sessionKey) {
|
|
1490
|
+
if (!sessionKey) return null;
|
|
1491
|
+
return this.sessionContextByKey.get(sessionKey) ?? null;
|
|
1492
|
+
}
|
|
1493
|
+
markSessionRecapInjected(sessionKey) {
|
|
1494
|
+
const current = this.sessionContextByKey.get(sessionKey);
|
|
1495
|
+
if (!current) return;
|
|
1496
|
+
this.sessionContextByKey.set(sessionKey, { ...current, recapInjected: true });
|
|
1497
|
+
}
|
|
1498
|
+
clearSession(sessionKey) {
|
|
1499
|
+
if (!sessionKey) return;
|
|
1500
|
+
this.sessionContextByKey.delete(sessionKey);
|
|
1501
|
+
}
|
|
1502
|
+
shouldRunWeeklyReflection(weekKey) {
|
|
1503
|
+
return this.lastWeeklyReflectionWeekKey !== weekKey;
|
|
1504
|
+
}
|
|
1505
|
+
markWeeklyReflectionRun(weekKey) {
|
|
1506
|
+
this.lastWeeklyReflectionWeekKey = weekKey;
|
|
1507
|
+
}
|
|
1508
|
+
};
|
|
1509
|
+
|
|
1510
|
+
// src/plugin/communication-protocol.ts
|
|
1511
|
+
var BANNED_PHRASE_PATTERNS = [
|
|
1512
|
+
{ id: "good-catch", regex: /\bgood catch\b[:,]?\s*/gi, replacement: "" },
|
|
1513
|
+
{ id: "great-question", regex: /\bgreat question\b[:,]?\s*/gi, replacement: "" },
|
|
1514
|
+
{ id: "right-to-call-out", regex: /\byou(?:'|’)re right to call that out\b[:,]?\s*/gi, replacement: "" }
|
|
1515
|
+
];
|
|
1516
|
+
var RABBIT_HOLE_PATTERNS = [
|
|
1517
|
+
/\bif (?:you(?:'|’)d like|you want(?: me)?(?: to)?|that would help),?\s*i can\b[^.!?]*(?:[.!?]|$)/gi,
|
|
1518
|
+
/\blet me know if you(?:'|’)d like\b[^.!?]*(?:[.!?]|$)/gi
|
|
1519
|
+
];
|
|
1520
|
+
var QUESTION_OPENERS = /\b(what|why|who|where|when|which|can you|could you|do you|did you|would you|should we)\b/i;
|
|
1521
|
+
function buildCommunicationProtocolAppendix() {
|
|
1522
|
+
return [
|
|
1523
|
+
"ClawVault Communication Protocol:",
|
|
1524
|
+
`- Never say: "good catch", "great question", or "you're right to call that out".`,
|
|
1525
|
+
`- Never offer rabbit-hole phrasing such as "if you'd like I can ...".`,
|
|
1526
|
+
"- Do not ask questions when memory already contains the answer.",
|
|
1527
|
+
"- Use memory tools proactively before answering memory-sensitive prompts."
|
|
1528
|
+
].join("\n");
|
|
1529
|
+
}
|
|
1530
|
+
function normalizeWhitespace2(value) {
|
|
1531
|
+
return value.replace(/[ \t]+\n/g, "\n").replace(/\n{3,}/g, "\n\n").replace(/[ \t]{2,}/g, " ").trim();
|
|
1532
|
+
}
|
|
1533
|
+
function rewriteOutboundMessage(content) {
|
|
1534
|
+
let rewritten = content;
|
|
1535
|
+
const violations = [];
|
|
1536
|
+
for (const pattern of BANNED_PHRASE_PATTERNS) {
|
|
1537
|
+
if (pattern.regex.test(rewritten)) {
|
|
1538
|
+
violations.push(pattern.id);
|
|
1539
|
+
rewritten = rewritten.replace(pattern.regex, pattern.replacement);
|
|
1540
|
+
}
|
|
1541
|
+
}
|
|
1542
|
+
for (const regex of RABBIT_HOLE_PATTERNS) {
|
|
1543
|
+
if (regex.test(rewritten)) {
|
|
1544
|
+
violations.push("rabbit-hole-offer");
|
|
1545
|
+
rewritten = rewritten.replace(regex, "");
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1548
|
+
rewritten = rewritten.replace(/^[,\-:; ]+/, "").replace(/[ ]+([,.!?;:])/g, "$1");
|
|
1549
|
+
rewritten = normalizeWhitespace2(rewritten);
|
|
1550
|
+
if (!rewritten) {
|
|
1551
|
+
rewritten = "Understood.";
|
|
1552
|
+
}
|
|
1553
|
+
return { content: rewritten, violations };
|
|
1554
|
+
}
|
|
1555
|
+
function containsQuestion(content) {
|
|
1556
|
+
if (!content.includes("?")) return false;
|
|
1557
|
+
return QUESTION_OPENERS.test(content.toLowerCase()) || content.trim().endsWith("?");
|
|
1558
|
+
}
|
|
1559
|
+
function rewriteQuestionWithMemoryEvidence(originalContent, memoryHits) {
|
|
1560
|
+
const cleaned = originalContent.replace(/\?/g, ".").replace(/\s+\./g, ".").trim();
|
|
1561
|
+
if (memoryHits.length === 0) {
|
|
1562
|
+
return cleaned || "Proceeding with the available context.";
|
|
1563
|
+
}
|
|
1564
|
+
const topHits = memoryHits.slice(0, 2).map((hit) => {
|
|
1565
|
+
const citation = hit.citation ? ` (${hit.citation})` : "";
|
|
1566
|
+
return `- ${hit.snippet}${citation}`;
|
|
1567
|
+
});
|
|
1568
|
+
const summary = [
|
|
1569
|
+
cleaned || "I checked ClawVault memory before responding.",
|
|
1570
|
+
"",
|
|
1571
|
+
"Memory already contains relevant details:",
|
|
1572
|
+
...topHits
|
|
1573
|
+
].join("\n");
|
|
1574
|
+
return normalizeWhitespace2(summary);
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
// src/plugin/vault-context-injector.ts
|
|
1578
|
+
var DEFAULT_MAX_CONTEXT_RESULTS = 4;
|
|
1579
|
+
var DEFAULT_MAX_RECAP_RESULTS = 6;
|
|
1580
|
+
async function fetchSessionRecapEntries(options) {
|
|
1581
|
+
const sessionKey = resolveSessionKey(options.sessionKey);
|
|
1582
|
+
if (!sessionKey) return [];
|
|
1583
|
+
const recapArgs = ["session-recap", sessionKey, "--format", "json"];
|
|
1584
|
+
if (options.agentId) {
|
|
1585
|
+
recapArgs.push("--agent", options.agentId);
|
|
1586
|
+
}
|
|
1587
|
+
const recapResult = runClawvault(recapArgs, options.pluginConfig, { timeoutMs: 2e4 });
|
|
1588
|
+
if (!recapResult.success) {
|
|
1589
|
+
return [];
|
|
1590
|
+
}
|
|
1591
|
+
return parseSessionRecapJson(recapResult.output, DEFAULT_MAX_RECAP_RESULTS);
|
|
1592
|
+
}
|
|
1593
|
+
async function fetchMemoryContextEntries(options) {
|
|
1594
|
+
const prompt = sanitizePromptForContext(options.prompt);
|
|
1595
|
+
if (!prompt) {
|
|
1596
|
+
return { entries: [], vaultPath: null };
|
|
1597
|
+
}
|
|
1598
|
+
const vaultPath = resolveVaultPathForAgent(options.pluginConfig, {
|
|
1599
|
+
agentId: options.agentId,
|
|
1600
|
+
cwd: options.workspaceDir
|
|
1601
|
+
});
|
|
1602
|
+
if (!vaultPath) {
|
|
1603
|
+
return { entries: [], vaultPath: null };
|
|
1604
|
+
}
|
|
1605
|
+
const maxResults = Number.isFinite(options.maxResults) ? Math.max(1, Math.min(20, Number(options.maxResults))) : DEFAULT_MAX_CONTEXT_RESULTS;
|
|
1606
|
+
const profile = options.contextProfile ?? options.pluginConfig.contextProfile ?? "auto";
|
|
1607
|
+
const contextArgs = [
|
|
1608
|
+
"context",
|
|
1609
|
+
prompt,
|
|
1610
|
+
"--format",
|
|
1611
|
+
"json",
|
|
1612
|
+
"--profile",
|
|
1613
|
+
profile,
|
|
1614
|
+
"--limit",
|
|
1615
|
+
String(maxResults),
|
|
1616
|
+
"-v",
|
|
1617
|
+
vaultPath
|
|
1618
|
+
];
|
|
1619
|
+
const contextResult = runClawvault(contextArgs, options.pluginConfig, { timeoutMs: 25e3 });
|
|
1620
|
+
if (contextResult.success) {
|
|
1621
|
+
return {
|
|
1622
|
+
entries: parseContextJson(contextResult.output, maxResults),
|
|
1623
|
+
vaultPath
|
|
1624
|
+
};
|
|
1625
|
+
}
|
|
1626
|
+
const fallbackSearchArgs = [
|
|
1627
|
+
"search",
|
|
1628
|
+
prompt,
|
|
1629
|
+
"--json",
|
|
1630
|
+
"-n",
|
|
1631
|
+
String(maxResults),
|
|
1632
|
+
"-v",
|
|
1633
|
+
vaultPath
|
|
1634
|
+
];
|
|
1635
|
+
const fallbackSearchResult = runClawvault(fallbackSearchArgs, options.pluginConfig, { timeoutMs: 25e3 });
|
|
1636
|
+
if (!fallbackSearchResult.success) {
|
|
1637
|
+
return { entries: [], vaultPath };
|
|
1638
|
+
}
|
|
1639
|
+
return {
|
|
1640
|
+
entries: parseContextJson(fallbackSearchResult.output, maxResults),
|
|
1641
|
+
vaultPath
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
async function buildVaultContextInjection(options) {
|
|
1645
|
+
const [recapEntries, memoryResult] = await Promise.all([
|
|
1646
|
+
fetchSessionRecapEntries(options),
|
|
1647
|
+
fetchMemoryContextEntries(options)
|
|
1648
|
+
]);
|
|
1649
|
+
if (recapEntries.length === 0 && memoryResult.entries.length === 0) {
|
|
1650
|
+
return {
|
|
1651
|
+
prependSystemContext: "",
|
|
1652
|
+
memoryEntries: [],
|
|
1653
|
+
recapEntries: [],
|
|
1654
|
+
vaultPath: memoryResult.vaultPath
|
|
1655
|
+
};
|
|
1656
|
+
}
|
|
1657
|
+
return {
|
|
1658
|
+
prependSystemContext: formatSessionContextInjection(recapEntries, memoryResult.entries),
|
|
1659
|
+
memoryEntries: memoryResult.entries,
|
|
1660
|
+
recapEntries,
|
|
1661
|
+
vaultPath: memoryResult.vaultPath
|
|
1662
|
+
};
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
// src/plugin/hooks/before-prompt-build.ts
|
|
1666
|
+
var MEMORY_RECALL_MANDATE = [
|
|
1667
|
+
"ClawVault Memory Recall Policy:",
|
|
1668
|
+
"- Before answering anything about prior work, people, decisions, preferences, todos, or historical context, call memory_search first.",
|
|
1669
|
+
"- If memory_search returns relevant snippets, ground your answer in those snippets and use memory_get when details are needed.",
|
|
1670
|
+
"- Do not guess from stale context when memory lookup is available."
|
|
1671
|
+
].join("\n");
|
|
1672
|
+
function appendSection(target, section) {
|
|
1673
|
+
if (!section) return;
|
|
1674
|
+
const trimmed = section.trim();
|
|
1675
|
+
if (!trimmed) return;
|
|
1676
|
+
target.push(trimmed);
|
|
1677
|
+
}
|
|
1678
|
+
function createBeforePromptBuildHandler(dependencies) {
|
|
1679
|
+
const contextInjector = dependencies.contextInjector ?? buildVaultContextInjection;
|
|
1680
|
+
return async (event, ctx) => {
|
|
1681
|
+
const prependSections = [];
|
|
1682
|
+
const appendSections = [];
|
|
1683
|
+
const recallEnabled = isFeatureEnabled(dependencies.pluginConfig, "enableBeforePromptRecall", true);
|
|
1684
|
+
const protocolEnabled = isFeatureEnabled(dependencies.pluginConfig, "enforceCommunicationProtocol", true);
|
|
1685
|
+
const contextInjectionEnabled = isFeatureEnabled(dependencies.pluginConfig, "enableSessionContextInjection", true);
|
|
1686
|
+
if (recallEnabled) {
|
|
1687
|
+
prependSections.push(MEMORY_RECALL_MANDATE);
|
|
1688
|
+
}
|
|
1689
|
+
const startupNotice = dependencies.runtimeState.consumeStartupRecoveryNotice();
|
|
1690
|
+
appendSection(prependSections, startupNotice ? `[ClawVault Recovery]
|
|
1691
|
+
${startupNotice}` : "");
|
|
1692
|
+
if (ctx.sessionKey) {
|
|
1693
|
+
const sessionCacheEntry = dependencies.runtimeState.getSessionRecap(ctx.sessionKey);
|
|
1694
|
+
if (sessionCacheEntry?.recapText && !sessionCacheEntry.recapInjected) {
|
|
1695
|
+
appendSection(prependSections, sessionCacheEntry.recapText);
|
|
1696
|
+
dependencies.runtimeState.markSessionRecapInjected(ctx.sessionKey);
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
if (contextInjectionEnabled) {
|
|
1700
|
+
const injection = await contextInjector({
|
|
1701
|
+
prompt: event.prompt,
|
|
1702
|
+
sessionKey: ctx.sessionKey,
|
|
1703
|
+
agentId: ctx.agentId,
|
|
1704
|
+
workspaceDir: ctx.workspaceDir,
|
|
1705
|
+
pluginConfig: dependencies.pluginConfig,
|
|
1706
|
+
contextProfile: dependencies.pluginConfig.contextProfile,
|
|
1707
|
+
maxResults: dependencies.pluginConfig.maxContextResults
|
|
1708
|
+
});
|
|
1709
|
+
appendSection(prependSections, injection.prependSystemContext);
|
|
1710
|
+
}
|
|
1711
|
+
if (protocolEnabled) {
|
|
1712
|
+
appendSections.push(buildCommunicationProtocolAppendix());
|
|
1713
|
+
}
|
|
1714
|
+
if (prependSections.length === 0 && appendSections.length === 0) {
|
|
1715
|
+
return;
|
|
1716
|
+
}
|
|
1717
|
+
return {
|
|
1718
|
+
prependSystemContext: prependSections.join("\n\n"),
|
|
1719
|
+
appendSystemContext: appendSections.join("\n\n")
|
|
1720
|
+
};
|
|
1721
|
+
};
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
// src/plugin/hooks/message-sending.ts
|
|
1725
|
+
var DEFAULT_QUESTION_RECALL_MIN_SCORE = 0.35;
|
|
1726
|
+
function normalizeScoreThreshold(config) {
|
|
1727
|
+
const raw = config.minQuestionRecallScore;
|
|
1728
|
+
if (!Number.isFinite(raw)) return DEFAULT_QUESTION_RECALL_MIN_SCORE;
|
|
1729
|
+
return Math.max(0, Math.min(1, Number(raw)));
|
|
1730
|
+
}
|
|
1731
|
+
function createMessageSendingHandler(dependencies) {
|
|
1732
|
+
return async (event, _ctx) => {
|
|
1733
|
+
const filterEnabled = isFeatureEnabled(dependencies.pluginConfig, "enableMessageSendingFilter", true);
|
|
1734
|
+
if (!filterEnabled) {
|
|
1735
|
+
return;
|
|
1736
|
+
}
|
|
1737
|
+
const rewritten = rewriteOutboundMessage(event.content);
|
|
1738
|
+
let content = rewritten.content;
|
|
1739
|
+
let shouldCancel = false;
|
|
1740
|
+
if (containsQuestion(content)) {
|
|
1741
|
+
const hits = await dependencies.memoryManager.search(content, {
|
|
1742
|
+
maxResults: 2,
|
|
1743
|
+
minScore: normalizeScoreThreshold(dependencies.pluginConfig)
|
|
1744
|
+
});
|
|
1745
|
+
if (hits.length > 0) {
|
|
1746
|
+
content = rewriteQuestionWithMemoryEvidence(content, hits);
|
|
1747
|
+
if (containsQuestion(content)) {
|
|
1748
|
+
shouldCancel = true;
|
|
1749
|
+
}
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
if (!shouldCancel && content === event.content) {
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (shouldCancel) {
|
|
1756
|
+
return { cancel: true };
|
|
1757
|
+
}
|
|
1758
|
+
return { content };
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// src/plugin/fact-extractor.ts
|
|
1763
|
+
import * as fs5 from "fs";
|
|
1764
|
+
import * as path5 from "path";
|
|
1765
|
+
import { createHash as createHash2 } from "crypto";
|
|
1766
|
+
var FACTS_FILE = "facts.jsonl";
|
|
1767
|
+
var ENTITY_GRAPH_FILE = "entity-graph.json";
|
|
1768
|
+
var MAX_TEXT_LENGTH = 6e3;
|
|
1769
|
+
function ensureClawVaultDir(vaultPath) {
|
|
1770
|
+
const dir = path5.join(vaultPath, ".clawvault");
|
|
1771
|
+
if (!fs5.existsSync(dir)) {
|
|
1772
|
+
fs5.mkdirSync(dir, { recursive: true });
|
|
1773
|
+
}
|
|
1774
|
+
return dir;
|
|
1775
|
+
}
|
|
1776
|
+
function sanitizeText(value, maxLength = MAX_TEXT_LENGTH) {
|
|
1777
|
+
if (typeof value !== "string") return "";
|
|
1778
|
+
return value.replace(/[\x00-\x1f\x7f]/g, " ").replace(/\s+/g, " ").trim().slice(0, maxLength);
|
|
1779
|
+
}
|
|
1780
|
+
function collectTextFragments(target, input, depth = 0) {
|
|
1781
|
+
if (depth > 3 || input === null || input === void 0) return;
|
|
1782
|
+
if (typeof input === "string") {
|
|
1783
|
+
const cleaned = sanitizeText(input);
|
|
1784
|
+
if (cleaned) target.push(cleaned);
|
|
1785
|
+
return;
|
|
1786
|
+
}
|
|
1787
|
+
if (Array.isArray(input)) {
|
|
1788
|
+
for (const item of input) {
|
|
1789
|
+
collectTextFragments(target, item, depth + 1);
|
|
1790
|
+
}
|
|
1791
|
+
return;
|
|
1792
|
+
}
|
|
1793
|
+
if (typeof input !== "object") return;
|
|
1794
|
+
const record = input;
|
|
1795
|
+
const directKeys = ["text", "message", "content", "rawText", "prompt", "observation"];
|
|
1796
|
+
for (const key of directKeys) {
|
|
1797
|
+
const cleaned = sanitizeText(record[key]);
|
|
1798
|
+
if (cleaned) target.push(cleaned);
|
|
1799
|
+
}
|
|
1800
|
+
const nestedKeys = ["messages", "history", "entries", "items", "events", "payload", "context"];
|
|
1801
|
+
for (const key of nestedKeys) {
|
|
1802
|
+
collectTextFragments(target, record[key], depth + 1);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
function dedupeTexts(items) {
|
|
1806
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1807
|
+
const deduped = [];
|
|
1808
|
+
for (const item of items) {
|
|
1809
|
+
const cleaned = sanitizeText(item);
|
|
1810
|
+
if (!cleaned || seen.has(cleaned)) continue;
|
|
1811
|
+
seen.add(cleaned);
|
|
1812
|
+
deduped.push(cleaned);
|
|
1813
|
+
}
|
|
1814
|
+
return deduped;
|
|
1815
|
+
}
|
|
1816
|
+
function collectObservedTextsForFactExtraction(event) {
|
|
1817
|
+
const collected = [];
|
|
1818
|
+
collectTextFragments(collected, event);
|
|
1819
|
+
return dedupeTexts(collected);
|
|
1820
|
+
}
|
|
1821
|
+
function extractTimestamp(event) {
|
|
1822
|
+
if (!event || typeof event !== "object") {
|
|
1823
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1824
|
+
}
|
|
1825
|
+
const record = event;
|
|
1826
|
+
const candidates = [
|
|
1827
|
+
record.timestamp,
|
|
1828
|
+
record.scheduledAt,
|
|
1829
|
+
record.time,
|
|
1830
|
+
record.context?.timestamp
|
|
1831
|
+
];
|
|
1832
|
+
for (const candidate of candidates) {
|
|
1833
|
+
if (!candidate) continue;
|
|
1834
|
+
const date = new Date(String(candidate));
|
|
1835
|
+
if (!Number.isNaN(date.getTime())) {
|
|
1836
|
+
return date.toISOString();
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
1840
|
+
}
|
|
1841
|
+
function buildEntityGraph(facts) {
|
|
1842
|
+
const nodes = /* @__PURE__ */ new Map();
|
|
1843
|
+
const edges = [];
|
|
1844
|
+
for (const fact of facts) {
|
|
1845
|
+
const sourceId = `entity:${fact.entityNorm || fact.entity.toLowerCase()}`;
|
|
1846
|
+
const sourceNode = nodes.get(sourceId) ?? {
|
|
1847
|
+
id: sourceId,
|
|
1848
|
+
name: fact.entityNorm || fact.entity.toLowerCase(),
|
|
1849
|
+
displayName: fact.entity,
|
|
1850
|
+
type: "person",
|
|
1851
|
+
attributes: { entityNorm: fact.entityNorm || fact.entity.toLowerCase() },
|
|
1852
|
+
lastSeen: fact.validFrom
|
|
1853
|
+
};
|
|
1854
|
+
if (new Date(fact.validFrom).getTime() > new Date(sourceNode.lastSeen).getTime()) {
|
|
1855
|
+
sourceNode.lastSeen = fact.validFrom;
|
|
1856
|
+
}
|
|
1857
|
+
nodes.set(sourceId, sourceNode);
|
|
1858
|
+
const normalizedValue = fact.value.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1859
|
+
const targetId = `value:${fact.relation}:${normalizedValue || "unknown"}`;
|
|
1860
|
+
const targetNode = nodes.get(targetId) ?? {
|
|
1861
|
+
id: targetId,
|
|
1862
|
+
name: normalizedValue || "unknown",
|
|
1863
|
+
displayName: fact.value,
|
|
1864
|
+
type: "attribute",
|
|
1865
|
+
attributes: { relation: fact.relation },
|
|
1866
|
+
lastSeen: fact.validFrom
|
|
1867
|
+
};
|
|
1868
|
+
if (new Date(fact.validFrom).getTime() > new Date(targetNode.lastSeen).getTime()) {
|
|
1869
|
+
targetNode.lastSeen = fact.validFrom;
|
|
1870
|
+
}
|
|
1871
|
+
nodes.set(targetId, targetNode);
|
|
1872
|
+
const edgeHash = createHash2("sha1").update(`${fact.id}|${sourceId}|${targetId}|${fact.validFrom}`).digest("hex").slice(0, 18);
|
|
1873
|
+
edges.push({
|
|
1874
|
+
id: `edge:${edgeHash}`,
|
|
1875
|
+
source: sourceId,
|
|
1876
|
+
target: targetId,
|
|
1877
|
+
relation: fact.relation,
|
|
1878
|
+
validFrom: fact.validFrom,
|
|
1879
|
+
validUntil: fact.validUntil,
|
|
1880
|
+
confidence: Math.max(0, Math.min(1, fact.confidence ?? 0.7))
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
return {
|
|
1884
|
+
version: 1,
|
|
1885
|
+
nodes: [...nodes.values()].sort((a, b) => a.id.localeCompare(b.id)),
|
|
1886
|
+
edges: edges.sort((a, b) => a.id.localeCompare(b.id))
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
function ensureFactsLogFile(vaultPath) {
|
|
1890
|
+
const filePath = path5.join(ensureClawVaultDir(vaultPath), FACTS_FILE);
|
|
1891
|
+
if (!fs5.existsSync(filePath)) {
|
|
1892
|
+
fs5.writeFileSync(filePath, "", "utf-8");
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
function persistFactsAndGraph(vaultPath, extractedFacts) {
|
|
1896
|
+
ensureFactsLogFile(vaultPath);
|
|
1897
|
+
const store = new FactStore(vaultPath);
|
|
1898
|
+
store.load();
|
|
1899
|
+
const conflictsResolved = store.addFacts(extractedFacts);
|
|
1900
|
+
store.save();
|
|
1901
|
+
const allFacts = store.getAllFacts();
|
|
1902
|
+
const graph = buildEntityGraph(allFacts);
|
|
1903
|
+
const graphPath = path5.join(ensureClawVaultDir(vaultPath), ENTITY_GRAPH_FILE);
|
|
1904
|
+
fs5.writeFileSync(graphPath, `${JSON.stringify(graph, null, 2)}
|
|
1905
|
+
`, "utf-8");
|
|
1906
|
+
return {
|
|
1907
|
+
extracted: extractedFacts.length,
|
|
1908
|
+
added: Math.max(0, extractedFacts.length - conflictsResolved),
|
|
1909
|
+
conflictsResolved,
|
|
1910
|
+
totalFacts: allFacts.length
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
function runFactExtractionForEvent(vaultPath, event, sourceLabel) {
|
|
1914
|
+
const observedTexts = collectObservedTextsForFactExtraction(event);
|
|
1915
|
+
if (observedTexts.length === 0) {
|
|
1916
|
+
return { extracted: 0, added: 0, conflictsResolved: 0, totalFacts: 0 };
|
|
1917
|
+
}
|
|
1918
|
+
const validFrom = extractTimestamp(event);
|
|
1919
|
+
const source = `hook:${sourceLabel}`;
|
|
1920
|
+
const facts = [];
|
|
1921
|
+
for (const text of observedTexts) {
|
|
1922
|
+
facts.push(...extractFactsRuleBased(text, source, validFrom));
|
|
1923
|
+
}
|
|
1924
|
+
if (facts.length === 0) {
|
|
1925
|
+
return { extracted: 0, added: 0, conflictsResolved: 0, totalFacts: 0 };
|
|
1926
|
+
}
|
|
1927
|
+
return persistFactsAndGraph(vaultPath, facts);
|
|
1928
|
+
}
|
|
1929
|
+
|
|
1930
|
+
// src/plugin/hooks/session-lifecycle.ts
|
|
1931
|
+
function getWeekKey(date) {
|
|
1932
|
+
const year = date.getUTCFullYear();
|
|
1933
|
+
const start = Date.UTC(year, 0, 1);
|
|
1934
|
+
const days = Math.floor((date.getTime() - start) / (24 * 60 * 60 * 1e3));
|
|
1935
|
+
const week = Math.floor(days / 7) + 1;
|
|
1936
|
+
return `${year}-W${String(week).padStart(2, "0")}`;
|
|
1937
|
+
}
|
|
1938
|
+
function isSundayMidnightUtc(date) {
|
|
1939
|
+
return date.getUTCDay() === 0 && date.getUTCHours() === 0 && date.getUTCMinutes() === 0;
|
|
1940
|
+
}
|
|
1941
|
+
async function runWeeklyReflectionIfNeeded(deps, agentId, workspaceDir) {
|
|
1942
|
+
if (!isOptInEnabled(deps.pluginConfig, "enableWeeklyReflection", "weeklyReflection")) {
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
const now = /* @__PURE__ */ new Date();
|
|
1946
|
+
if (!isSundayMidnightUtc(now)) {
|
|
1947
|
+
return;
|
|
1948
|
+
}
|
|
1949
|
+
const weekKey = getWeekKey(now);
|
|
1950
|
+
if (!deps.runtimeState.shouldRunWeeklyReflection(weekKey)) {
|
|
1951
|
+
return;
|
|
1952
|
+
}
|
|
1953
|
+
const vaultPath = resolveVaultPathForAgent(deps.pluginConfig, {
|
|
1954
|
+
agentId,
|
|
1955
|
+
cwd: workspaceDir
|
|
1956
|
+
});
|
|
1957
|
+
if (!vaultPath) {
|
|
1958
|
+
return;
|
|
1959
|
+
}
|
|
1960
|
+
const result = runClawvault(["reflect", "-v", vaultPath], deps.pluginConfig, {
|
|
1961
|
+
timeoutMs: 12e4
|
|
1962
|
+
});
|
|
1963
|
+
if (result.success) {
|
|
1964
|
+
deps.runtimeState.markWeeklyReflectionRun(weekKey);
|
|
1965
|
+
deps.logger?.info("[clawvault] Weekly reflection complete");
|
|
1966
|
+
} else if (!result.skipped) {
|
|
1967
|
+
deps.logger?.warn("[clawvault] Weekly reflection failed");
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
async function handleGatewayStart(event, ctx, deps) {
|
|
1971
|
+
const shouldRecover = isOptInEnabled(deps.pluginConfig, "enableStartupRecovery");
|
|
1972
|
+
if (!shouldRecover) {
|
|
1973
|
+
return;
|
|
1974
|
+
}
|
|
1975
|
+
const vaultPath = resolveVaultPathForAgent(deps.pluginConfig, { cwd: process.cwd(), agentId: "main" });
|
|
1976
|
+
if (!vaultPath) {
|
|
1977
|
+
deps.logger?.warn("[clawvault] No vault found, skipping startup recovery");
|
|
1978
|
+
return;
|
|
1979
|
+
}
|
|
1980
|
+
const result = runClawvault(["recover", "--clear", "-v", vaultPath], deps.pluginConfig, {
|
|
1981
|
+
timeoutMs: 2e4
|
|
1982
|
+
});
|
|
1983
|
+
if (result.skipped) {
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
if (!result.success) {
|
|
1987
|
+
deps.logger?.warn("[clawvault] Startup recovery command failed");
|
|
1988
|
+
return;
|
|
1989
|
+
}
|
|
1990
|
+
const parsed = parseRecoveryOutput(result.output);
|
|
1991
|
+
if (parsed.hadDeath) {
|
|
1992
|
+
const message = parsed.workingOn ? `[ClawVault] Context death detected. Last working on: ${parsed.workingOn}. Run \`clawvault wake\` for full recovery context.` : "[ClawVault] Context death detected. Run `clawvault wake` for full recovery context.";
|
|
1993
|
+
deps.runtimeState.setStartupRecoveryNotice(message);
|
|
1994
|
+
deps.logger?.warn("[clawvault] Context death detected at startup");
|
|
1995
|
+
}
|
|
1996
|
+
if (ctx.port || event.port) {
|
|
1997
|
+
}
|
|
1998
|
+
}
|
|
1999
|
+
async function handleSessionStart(event, ctx, deps) {
|
|
2000
|
+
const sessionKey = sanitizeSessionKey(ctx.sessionKey ?? event.sessionKey);
|
|
2001
|
+
const agentId = resolveAgentId({ agentId: ctx.agentId, sessionKey }, deps.pluginConfig);
|
|
2002
|
+
if (isOptInEnabled(deps.pluginConfig, "enableSessionContextInjection")) {
|
|
2003
|
+
const recapEntries = await fetchSessionRecapEntries({
|
|
2004
|
+
sessionKey,
|
|
2005
|
+
agentId: extractAgentIdFromSessionKey(sessionKey) || agentId,
|
|
2006
|
+
pluginConfig: deps.pluginConfig
|
|
2007
|
+
});
|
|
2008
|
+
if (recapEntries.length > 0) {
|
|
2009
|
+
const recapInjection = formatSessionContextInjection(recapEntries, []);
|
|
2010
|
+
deps.runtimeState.setSessionRecap(sessionKey, recapInjection);
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
await runWeeklyReflectionIfNeeded(deps, agentId, void 0);
|
|
2014
|
+
}
|
|
2015
|
+
function sanitizeForCheckpoint(value, maxLength) {
|
|
2016
|
+
if (typeof value !== "string") return "unknown";
|
|
2017
|
+
const cleaned = value.replace(/[^a-zA-Z0-9:_ -]/g, "").trim();
|
|
2018
|
+
return cleaned.slice(0, maxLength) || "unknown";
|
|
2019
|
+
}
|
|
2020
|
+
async function handleBeforeReset(event, ctx, deps) {
|
|
2021
|
+
const autoCheckpointEnabled = isOptInEnabled(deps.pluginConfig, "enableAutoCheckpoint", "autoCheckpoint");
|
|
2022
|
+
const observerOnResetEnabled = isOptInEnabled(deps.pluginConfig, "enableObserveOnNew");
|
|
2023
|
+
const factExtractionEnabled = isOptInEnabled(deps.pluginConfig, "enableFactExtraction");
|
|
2024
|
+
if (!autoCheckpointEnabled && !observerOnResetEnabled && !factExtractionEnabled) {
|
|
2025
|
+
return;
|
|
2026
|
+
}
|
|
2027
|
+
const sessionKey = sanitizeSessionKey(ctx.sessionKey);
|
|
2028
|
+
const agentId = resolveAgentId(ctx, deps.pluginConfig);
|
|
2029
|
+
const vaultPath = resolveVaultPathForAgent(deps.pluginConfig, {
|
|
2030
|
+
agentId,
|
|
2031
|
+
cwd: ctx.workspaceDir
|
|
2032
|
+
});
|
|
2033
|
+
if (!vaultPath) {
|
|
2034
|
+
return;
|
|
2035
|
+
}
|
|
2036
|
+
if (autoCheckpointEnabled) {
|
|
2037
|
+
const safeSessionKey = sanitizeForCheckpoint(sessionKey, 120);
|
|
2038
|
+
const safeReason = sanitizeForCheckpoint(event.reason ?? "before_reset", 80);
|
|
2039
|
+
const checkpointResult = runClawvault([
|
|
2040
|
+
"checkpoint",
|
|
2041
|
+
"--working-on",
|
|
2042
|
+
`Session reset via ${safeReason}`,
|
|
2043
|
+
"--focus",
|
|
2044
|
+
`Pre-reset checkpoint, session: ${safeSessionKey}`,
|
|
2045
|
+
"-v",
|
|
2046
|
+
vaultPath
|
|
2047
|
+
], deps.pluginConfig, { timeoutMs: 3e4 });
|
|
2048
|
+
if (!checkpointResult.success && !checkpointResult.skipped) {
|
|
2049
|
+
deps.logger?.warn("[clawvault] Auto-checkpoint before reset failed");
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
if (observerOnResetEnabled) {
|
|
2053
|
+
runObserverCron(vaultPath, agentId, deps.pluginConfig, {
|
|
2054
|
+
minNewBytes: 1,
|
|
2055
|
+
reason: "before_reset"
|
|
2056
|
+
});
|
|
2057
|
+
}
|
|
2058
|
+
if (factExtractionEnabled) {
|
|
2059
|
+
runFactExtractionForEvent(vaultPath, event, "before_reset");
|
|
2060
|
+
}
|
|
2061
|
+
}
|
|
2062
|
+
async function handleSessionEnd(event, ctx, deps) {
|
|
2063
|
+
deps.runtimeState.clearSession(ctx.sessionKey ?? event.sessionKey);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
// src/plugin/hooks/observation.ts
|
|
2067
|
+
async function handleAgentEndHeartbeat(event, ctx, deps) {
|
|
2068
|
+
if (!isOptInEnabled(deps.pluginConfig, "enableHeartbeatObservation", "observeOnHeartbeat")) {
|
|
2069
|
+
return;
|
|
2070
|
+
}
|
|
2071
|
+
const agentId = resolveAgentId(ctx, deps.pluginConfig);
|
|
2072
|
+
const vaultPath = resolveVaultPathForAgent(deps.pluginConfig, {
|
|
2073
|
+
agentId,
|
|
2074
|
+
cwd: ctx.workspaceDir
|
|
2075
|
+
});
|
|
2076
|
+
if (!vaultPath) {
|
|
2077
|
+
return;
|
|
2078
|
+
}
|
|
2079
|
+
if (!shouldObserveActiveSessions(vaultPath, agentId, deps.pluginConfig)) {
|
|
2080
|
+
return;
|
|
2081
|
+
}
|
|
2082
|
+
const observed = runObserverCron(vaultPath, agentId, deps.pluginConfig, {
|
|
2083
|
+
reason: "agent_end heartbeat"
|
|
2084
|
+
});
|
|
2085
|
+
if (!observed) {
|
|
2086
|
+
deps.logger?.warn("[clawvault] Heartbeat observation trigger failed");
|
|
2087
|
+
}
|
|
2088
|
+
if (!event.success && event.error) {
|
|
2089
|
+
deps.logger?.info(`[clawvault] Agent ended with error: ${event.error}`);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
async function handleBeforeCompactionObservation(event, ctx, deps) {
|
|
2093
|
+
const compactionObserveEnabled = isOptInEnabled(deps.pluginConfig, "enableCompactionObservation");
|
|
2094
|
+
const factExtractionEnabled = isOptInEnabled(deps.pluginConfig, "enableFactExtraction");
|
|
2095
|
+
if (!compactionObserveEnabled && !factExtractionEnabled) {
|
|
2096
|
+
return;
|
|
2097
|
+
}
|
|
2098
|
+
const agentId = resolveAgentId(ctx, deps.pluginConfig);
|
|
2099
|
+
const vaultPath = resolveVaultPathForAgent(deps.pluginConfig, {
|
|
2100
|
+
agentId,
|
|
2101
|
+
cwd: ctx.workspaceDir
|
|
2102
|
+
});
|
|
2103
|
+
if (!vaultPath) {
|
|
2104
|
+
return;
|
|
2105
|
+
}
|
|
2106
|
+
if (compactionObserveEnabled) {
|
|
2107
|
+
runObserverCron(vaultPath, agentId, deps.pluginConfig, {
|
|
2108
|
+
minNewBytes: 1,
|
|
2109
|
+
reason: "before_compaction"
|
|
2110
|
+
});
|
|
2111
|
+
}
|
|
2112
|
+
if (factExtractionEnabled) {
|
|
2113
|
+
runFactExtractionForEvent(vaultPath, event, "before_compaction");
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
|
|
2117
|
+
// src/openclaw-plugin.ts
|
|
2118
|
+
function isOpenClawPluginApi(value) {
|
|
2119
|
+
if (!value || typeof value !== "object") return false;
|
|
2120
|
+
const record = value;
|
|
2121
|
+
return typeof record.on === "function" && typeof record.registerTool === "function" && typeof record.logger === "object";
|
|
2122
|
+
}
|
|
2123
|
+
async function registerOpenClawPlugin(api) {
|
|
2124
|
+
const pluginConfig = readPluginConfig(api);
|
|
2125
|
+
const runtimeState = new ClawVaultPluginRuntimeState();
|
|
2126
|
+
const memoryManager = new ClawVaultMemoryManager({
|
|
2127
|
+
pluginConfig,
|
|
2128
|
+
defaultAgentId: "main",
|
|
2129
|
+
logger: {
|
|
2130
|
+
debug: api.logger.debug,
|
|
2131
|
+
warn: api.logger.warn
|
|
2132
|
+
}
|
|
2133
|
+
});
|
|
2134
|
+
const memorySearchTool = createMemorySearchToolFactory(memoryManager)();
|
|
2135
|
+
const memoryGetTool = createMemoryGetToolFactory(memoryManager)();
|
|
2136
|
+
api.registerTool(memorySearchTool, { name: "memory_search" });
|
|
2137
|
+
api.registerTool(memoryGetTool, { name: "memory_get" });
|
|
2138
|
+
api.on("before_prompt_build", createBeforePromptBuildHandler({
|
|
2139
|
+
pluginConfig,
|
|
2140
|
+
runtimeState
|
|
2141
|
+
}), { priority: 30 });
|
|
2142
|
+
api.on("message_sending", createMessageSendingHandler({
|
|
2143
|
+
pluginConfig,
|
|
2144
|
+
memoryManager
|
|
2145
|
+
}), { priority: 20 });
|
|
2146
|
+
api.on("gateway_start", async (event, ctx) => {
|
|
2147
|
+
await handleGatewayStart(event, ctx, {
|
|
2148
|
+
pluginConfig,
|
|
2149
|
+
runtimeState,
|
|
2150
|
+
logger: api.logger
|
|
2151
|
+
});
|
|
2152
|
+
});
|
|
2153
|
+
api.on("session_start", async (event, ctx) => {
|
|
2154
|
+
await handleSessionStart(event, ctx, {
|
|
2155
|
+
pluginConfig,
|
|
2156
|
+
runtimeState,
|
|
2157
|
+
logger: api.logger
|
|
2158
|
+
});
|
|
2159
|
+
});
|
|
2160
|
+
api.on("session_end", async (event, ctx) => {
|
|
2161
|
+
await handleSessionEnd(event, ctx, {
|
|
2162
|
+
pluginConfig,
|
|
2163
|
+
runtimeState,
|
|
2164
|
+
logger: api.logger
|
|
2165
|
+
});
|
|
2166
|
+
});
|
|
2167
|
+
api.on("before_reset", async (event, ctx) => {
|
|
2168
|
+
await handleBeforeReset(event, ctx, {
|
|
2169
|
+
pluginConfig,
|
|
2170
|
+
runtimeState,
|
|
2171
|
+
logger: api.logger
|
|
2172
|
+
});
|
|
2173
|
+
});
|
|
2174
|
+
api.on("before_compaction", async (event, ctx) => {
|
|
2175
|
+
await handleBeforeCompactionObservation(event, ctx, {
|
|
2176
|
+
pluginConfig,
|
|
2177
|
+
logger: api.logger
|
|
2178
|
+
});
|
|
2179
|
+
});
|
|
2180
|
+
api.on("agent_end", async (event, ctx) => {
|
|
2181
|
+
await handleAgentEndHeartbeat(event, ctx, {
|
|
2182
|
+
pluginConfig,
|
|
2183
|
+
logger: api.logger
|
|
2184
|
+
});
|
|
2185
|
+
});
|
|
2186
|
+
return {
|
|
2187
|
+
plugins: {
|
|
2188
|
+
slots: {
|
|
2189
|
+
memory: memoryManager
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
2193
|
+
}
|
|
2194
|
+
var clawvaultPlugin = {
|
|
2195
|
+
id: "clawvault",
|
|
2196
|
+
name: "ClawVault",
|
|
2197
|
+
kind: "memory",
|
|
2198
|
+
description: "Structured memory system for AI agents with proactive recall and protocol-safe messaging",
|
|
2199
|
+
async register(apiOrRuntime) {
|
|
2200
|
+
if (isOpenClawPluginApi(apiOrRuntime)) {
|
|
2201
|
+
return registerOpenClawPlugin(apiOrRuntime);
|
|
2202
|
+
}
|
|
2203
|
+
if (apiOrRuntime && typeof apiOrRuntime === "object") {
|
|
2204
|
+
registerMemorySlot(apiOrRuntime);
|
|
2205
|
+
}
|
|
2206
|
+
return createMemorySlotPlugin();
|
|
2207
|
+
}
|
|
2208
|
+
};
|
|
2209
|
+
var openclaw_plugin_default = clawvaultPlugin;
|
|
2210
|
+
|
|
2211
|
+
export {
|
|
2212
|
+
extractTaggedMemoryNotes,
|
|
2213
|
+
extractHeuristicMemories,
|
|
2214
|
+
extractMemoriesFromAssistantResponse,
|
|
2215
|
+
isLikelyJunkMemory,
|
|
2216
|
+
plausibilityScore,
|
|
2217
|
+
evaluateCandidateQuality,
|
|
2218
|
+
LiveCaptureService,
|
|
2219
|
+
createMemorySlot,
|
|
2220
|
+
createMemorySlotPlugin,
|
|
2221
|
+
registerMemorySlot,
|
|
2222
|
+
openclaw_plugin_default
|
|
2223
|
+
};
|