@xdarkicex/openclaw-memory-libravdb 1.4.3 → 1.4.5
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 +76 -16
- package/docs/README.md +3 -12
- package/docs/architecture.md +68 -153
- package/docs/contributing.md +1 -2
- package/openclaw.plugin.json +64 -1
- package/package.json +2 -2
- package/src/cli.ts +34 -0
- package/src/comparison-experiments.ts +128 -0
- package/src/context-engine.ts +286 -72
- package/src/dream-promotion.ts +492 -0
- package/src/dream-routing.ts +40 -0
- package/src/index.ts +16 -1
- package/src/markdown-hash.ts +104 -0
- package/src/markdown-ingest.ts +627 -0
- package/src/memory-runtime.ts +32 -9
- package/src/scoring.ts +6 -3
- package/src/temporal.ts +657 -80
- package/src/types.ts +48 -0
- package/docs/ast-v2.md +0 -167
- package/docs/ast.md +0 -70
- package/docs/compaction-evaluation.md +0 -182
- package/docs/continuity.md +0 -708
- package/docs/elevated-guidance.md +0 -258
- package/docs/gating.md +0 -134
- package/docs/implementation.md +0 -447
- package/docs/mathematics-v2.md +0 -1879
- package/docs/mathematics.md +0 -695
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import fsp from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { getHashBackendName, hashBytes } from "./markdown-hash.js";
|
|
6
|
+
import type { LoggerLike, PluginConfig } from "./types.js";
|
|
7
|
+
|
|
8
|
+
const DEFAULT_DEBOUNCE_MS = 150;
|
|
9
|
+
const DEFAULT_MIN_SCORE = 0.6;
|
|
10
|
+
const DEFAULT_MIN_RECALL_COUNT = 2;
|
|
11
|
+
const DEFAULT_MIN_UNIQUE_QUERIES = 2;
|
|
12
|
+
const DREAM_PROMOTION_VERSION = 1;
|
|
13
|
+
const DREAM_SOURCE_KIND = "dream";
|
|
14
|
+
|
|
15
|
+
type Disposable = { close(): void };
|
|
16
|
+
|
|
17
|
+
interface RpcLike {
|
|
18
|
+
call<T>(method: string, params: unknown): Promise<T>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
type RpcGetterLike = () => Promise<RpcLike>;
|
|
22
|
+
|
|
23
|
+
interface FsWatcherLike extends Disposable {
|
|
24
|
+
on(event: "error", handler: (error: Error) => void): void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface FsApi {
|
|
28
|
+
readFile(file: string): Promise<Uint8Array>;
|
|
29
|
+
stat(file: string): Promise<{ size: number; mtimeMs: number }>;
|
|
30
|
+
watch(dir: string, onChange: (event: string, filename: string | Buffer | null) => void): FsWatcherLike;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface DreamPromotionHandle {
|
|
34
|
+
start(): Promise<void>;
|
|
35
|
+
refresh(): Promise<void>;
|
|
36
|
+
stop(): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface DreamPromotionCandidate {
|
|
40
|
+
text: string;
|
|
41
|
+
score: number;
|
|
42
|
+
recallCount: number;
|
|
43
|
+
uniqueQueries: number;
|
|
44
|
+
section: string;
|
|
45
|
+
line: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface DreamPromotionEntry extends DreamPromotionCandidate {
|
|
49
|
+
sourceLine: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface DreamPromotionParams {
|
|
53
|
+
userId: string;
|
|
54
|
+
sourceDoc: string;
|
|
55
|
+
sourceRoot: string;
|
|
56
|
+
sourcePath: string;
|
|
57
|
+
sourceKind: string;
|
|
58
|
+
fileHash: string;
|
|
59
|
+
sourceSize: number;
|
|
60
|
+
sourceMtimeMs: number;
|
|
61
|
+
ingestVersion: number;
|
|
62
|
+
hashBackend: string;
|
|
63
|
+
entries: DreamPromotionEntry[];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface DreamPromotionResult {
|
|
67
|
+
promoted?: number;
|
|
68
|
+
rejected?: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
interface DreamFileState {
|
|
72
|
+
size: number;
|
|
73
|
+
mtimeMs: number;
|
|
74
|
+
fileHash: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface DreamPromotionState {
|
|
78
|
+
watching: boolean;
|
|
79
|
+
dirty: boolean;
|
|
80
|
+
timer: ReturnType<typeof setTimeout> | null;
|
|
81
|
+
watcher: FsWatcherLike | null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function createDreamPromotionHandle(
|
|
85
|
+
cfg: PluginConfig,
|
|
86
|
+
getRpc: RpcGetterLike,
|
|
87
|
+
logger: LoggerLike = console,
|
|
88
|
+
fsApi: FsApi = createRealFsApi(),
|
|
89
|
+
): DreamPromotionHandle {
|
|
90
|
+
const diaryPath = normalizeDiaryPath(cfg.dreamPromotionDiaryPath);
|
|
91
|
+
const userId = cfg.dreamPromotionUserId?.trim() ?? "";
|
|
92
|
+
if (cfg.dreamPromotionEnabled !== true || !diaryPath || !userId) {
|
|
93
|
+
return {
|
|
94
|
+
async start() {},
|
|
95
|
+
async refresh() {},
|
|
96
|
+
async stop() {},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const state: DreamPromotionState = {
|
|
101
|
+
watching: false,
|
|
102
|
+
dirty: false,
|
|
103
|
+
timer: null,
|
|
104
|
+
watcher: null,
|
|
105
|
+
};
|
|
106
|
+
let lastFileState: DreamFileState | null = null;
|
|
107
|
+
const debounceMs = cfg.dreamPromotionDebounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
108
|
+
|
|
109
|
+
return {
|
|
110
|
+
async start(): Promise<void> {
|
|
111
|
+
if (state.watching) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
state.watching = true;
|
|
115
|
+
await refreshDiary();
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
async refresh(): Promise<void> {
|
|
119
|
+
await refreshDiary();
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
async stop(): Promise<void> {
|
|
123
|
+
state.watching = false;
|
|
124
|
+
if (state.timer) {
|
|
125
|
+
clearTimeout(state.timer);
|
|
126
|
+
state.timer = null;
|
|
127
|
+
}
|
|
128
|
+
if (state.watcher) {
|
|
129
|
+
state.watcher.close();
|
|
130
|
+
state.watcher = null;
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
async function refreshDiary(): Promise<void> {
|
|
136
|
+
if (!state.watching) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (state.timer) {
|
|
140
|
+
state.dirty = true;
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
state.timer = setTimeout(() => {
|
|
144
|
+
state.timer = null;
|
|
145
|
+
void scanDiary().catch((error) => {
|
|
146
|
+
logger.warn?.(`[dream-promotion] refresh failed for ${diaryPath}: ${formatError(error)}`);
|
|
147
|
+
});
|
|
148
|
+
}, debounceMs);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function scanDiary(): Promise<void> {
|
|
152
|
+
if (!state.watching) {
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
await ensureWatcher();
|
|
156
|
+
|
|
157
|
+
const stat = await safeStat(diaryPath);
|
|
158
|
+
if (!stat) {
|
|
159
|
+
lastFileState = null;
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (lastFileState && lastFileState.size === stat.size && lastFileState.mtimeMs === stat.mtimeMs) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const bytes = await safeReadFile(diaryPath);
|
|
168
|
+
if (!bytes) {
|
|
169
|
+
lastFileState = null;
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const fileHash = hashBytes(bytes);
|
|
174
|
+
if (lastFileState && lastFileState.fileHash === fileHash) {
|
|
175
|
+
lastFileState = {
|
|
176
|
+
size: stat.size,
|
|
177
|
+
mtimeMs: stat.mtimeMs,
|
|
178
|
+
fileHash,
|
|
179
|
+
};
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const text = textDecoder.decode(bytes);
|
|
184
|
+
const candidates = parseDreamPromotionCandidates(text);
|
|
185
|
+
if (candidates.length === 0) {
|
|
186
|
+
lastFileState = {
|
|
187
|
+
size: stat.size,
|
|
188
|
+
mtimeMs: stat.mtimeMs,
|
|
189
|
+
fileHash,
|
|
190
|
+
};
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const rpc = await getRpc();
|
|
195
|
+
const params: DreamPromotionParams = {
|
|
196
|
+
userId,
|
|
197
|
+
sourceDoc: diaryPath,
|
|
198
|
+
sourceRoot: path.dirname(diaryPath),
|
|
199
|
+
sourcePath: path.basename(diaryPath),
|
|
200
|
+
sourceKind: DREAM_SOURCE_KIND,
|
|
201
|
+
fileHash,
|
|
202
|
+
sourceSize: stat.size,
|
|
203
|
+
sourceMtimeMs: stat.mtimeMs,
|
|
204
|
+
ingestVersion: DREAM_PROMOTION_VERSION,
|
|
205
|
+
hashBackend: getHashBackendName(),
|
|
206
|
+
entries: candidates.map((candidate, index) => ({
|
|
207
|
+
...candidate,
|
|
208
|
+
sourceLine: candidate.line,
|
|
209
|
+
line: index + 1,
|
|
210
|
+
})),
|
|
211
|
+
};
|
|
212
|
+
await rpc.call<DreamPromotionResult>("promote_dream_entries", params);
|
|
213
|
+
|
|
214
|
+
lastFileState = {
|
|
215
|
+
size: stat.size,
|
|
216
|
+
mtimeMs: stat.mtimeMs,
|
|
217
|
+
fileHash,
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
if (state.dirty) {
|
|
221
|
+
state.dirty = false;
|
|
222
|
+
await refreshDiary();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function safeStat(filePath: string): Promise<{ size: number; mtimeMs: number } | null> {
|
|
227
|
+
try {
|
|
228
|
+
return await fsApi.stat(filePath);
|
|
229
|
+
} catch {
|
|
230
|
+
return null;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
async function safeReadFile(filePath: string): Promise<Uint8Array | null> {
|
|
235
|
+
try {
|
|
236
|
+
return await fsApi.readFile(filePath);
|
|
237
|
+
} catch {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
async function ensureWatcher(): Promise<void> {
|
|
243
|
+
if (state.watcher) {
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
const parentDir = path.dirname(diaryPath);
|
|
247
|
+
try {
|
|
248
|
+
const watcher = fsApi.watch(parentDir, (_event, filename) => {
|
|
249
|
+
if (filename && path.basename(String(filename)) !== path.basename(diaryPath)) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
state.dirty = true;
|
|
253
|
+
void refreshDiary();
|
|
254
|
+
});
|
|
255
|
+
watcher.on("error", (error) => {
|
|
256
|
+
logger.warn?.(`[dream-promotion] watch error for ${parentDir}: ${formatError(error)}`);
|
|
257
|
+
});
|
|
258
|
+
state.watcher = watcher;
|
|
259
|
+
} catch (error) {
|
|
260
|
+
logger.warn?.(`[dream-promotion] watch unavailable for ${parentDir}: ${formatError(error)}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
export async function promoteDreamDiaryFile(
|
|
266
|
+
rpc: RpcLike,
|
|
267
|
+
opts: {
|
|
268
|
+
userId: string;
|
|
269
|
+
diaryPath: string;
|
|
270
|
+
text?: string;
|
|
271
|
+
fileHash?: string;
|
|
272
|
+
sourceSize?: number;
|
|
273
|
+
sourceMtimeMs?: number;
|
|
274
|
+
},
|
|
275
|
+
): Promise<DreamPromotionResult> {
|
|
276
|
+
const diaryPath = normalizeDiaryPath(opts.diaryPath);
|
|
277
|
+
if (!diaryPath) {
|
|
278
|
+
throw new Error("dream diary path is required");
|
|
279
|
+
}
|
|
280
|
+
const userId = opts.userId.trim();
|
|
281
|
+
if (!userId) {
|
|
282
|
+
throw new Error("user id is required");
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
let text = opts.text;
|
|
286
|
+
let fileHash = opts.fileHash;
|
|
287
|
+
let sourceSize = opts.sourceSize;
|
|
288
|
+
let sourceMtimeMs = opts.sourceMtimeMs;
|
|
289
|
+
if (text == null) {
|
|
290
|
+
const bytes = await fsp.readFile(diaryPath);
|
|
291
|
+
text = textDecoder.decode(bytes);
|
|
292
|
+
fileHash = fileHash ?? hashBytes(bytes);
|
|
293
|
+
const stat = await fsp.stat(diaryPath);
|
|
294
|
+
sourceSize = sourceSize ?? stat.size;
|
|
295
|
+
sourceMtimeMs = sourceMtimeMs ?? stat.mtimeMs;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const candidates = parseDreamPromotionCandidates(text);
|
|
299
|
+
return await rpc.call<DreamPromotionResult>("promote_dream_entries", {
|
|
300
|
+
userId,
|
|
301
|
+
sourceDoc: diaryPath,
|
|
302
|
+
sourceRoot: path.dirname(diaryPath),
|
|
303
|
+
sourcePath: path.basename(diaryPath),
|
|
304
|
+
sourceKind: DREAM_SOURCE_KIND,
|
|
305
|
+
fileHash: fileHash ?? "",
|
|
306
|
+
sourceSize: sourceSize ?? 0,
|
|
307
|
+
sourceMtimeMs: sourceMtimeMs ?? 0,
|
|
308
|
+
ingestVersion: DREAM_PROMOTION_VERSION,
|
|
309
|
+
hashBackend: getHashBackendName(),
|
|
310
|
+
entries: candidates.map((candidate, index) => ({
|
|
311
|
+
...candidate,
|
|
312
|
+
sourceLine: candidate.line,
|
|
313
|
+
line: index + 1,
|
|
314
|
+
})),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
export function parseDreamPromotionCandidates(text: string): DreamPromotionCandidate[] {
|
|
319
|
+
const candidates: DreamPromotionCandidate[] = [];
|
|
320
|
+
const lines = text.split("\n");
|
|
321
|
+
let inFence = false;
|
|
322
|
+
let activeSection = "";
|
|
323
|
+
|
|
324
|
+
for (let index = 0; index < lines.length; index++) {
|
|
325
|
+
const line = lines[index] ?? "";
|
|
326
|
+
const trimmed = line.trimStart();
|
|
327
|
+
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
|
|
328
|
+
inFence = !inFence;
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
if (inFence) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const heading = parseHeading(trimmed);
|
|
336
|
+
if (heading) {
|
|
337
|
+
activeSection = heading;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (!isPromotionSection(activeSection)) {
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const bullet = parseBulletCandidate(line);
|
|
346
|
+
if (!bullet) {
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const metadata = parseTrailingMetadata(bullet.body);
|
|
350
|
+
if (!metadata) {
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const textValue = bullet.body.slice(0, metadata.bodyStart).trim();
|
|
355
|
+
if (!textValue) {
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
candidates.push({
|
|
360
|
+
text: textValue,
|
|
361
|
+
score: metadata.score,
|
|
362
|
+
recallCount: metadata.recallCount,
|
|
363
|
+
uniqueQueries: metadata.uniqueQueries,
|
|
364
|
+
section: activeSection,
|
|
365
|
+
line: index + 1,
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return candidates;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
function parseHeading(value: string): string | null {
|
|
373
|
+
const match = /^(#{2,6})\s+(.+)$/.exec(value);
|
|
374
|
+
if (!match) {
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
return normalizeSectionName(match[2] ?? "");
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function isPromotionSection(section: string): boolean {
|
|
381
|
+
return section.includes("deep sleep") || section.includes("promot") || section.includes("dream");
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function parseBulletCandidate(line: string): { body: string } | null {
|
|
385
|
+
const match = /^\s*[-*+]\s+(.+)$/.exec(line);
|
|
386
|
+
if (!match) {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
return { body: match[1] ?? "" };
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function parseTrailingMetadata(body: string): { bodyStart: number; score: number; recallCount: number; uniqueQueries: number } | null {
|
|
393
|
+
const trimmed = body.trimEnd();
|
|
394
|
+
if (!trimmed.endsWith("}")) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const open = trimmed.lastIndexOf("{");
|
|
399
|
+
if (open < 0) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const metadataText = trimmed.slice(open + 1, -1).trim();
|
|
404
|
+
const text = trimmed.slice(0, open).trimEnd();
|
|
405
|
+
if (!metadataText || !text) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const fields = new Map<string, string>();
|
|
410
|
+
for (const token of metadataText.split(/[,\s]+/)) {
|
|
411
|
+
if (!token) {
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
const equals = token.indexOf("=");
|
|
415
|
+
if (equals <= 0) {
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const key = token.slice(0, equals).trim().toLowerCase();
|
|
419
|
+
const value = token.slice(equals + 1).trim();
|
|
420
|
+
if (key && value) {
|
|
421
|
+
fields.set(key, value);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const score = parseNumber(fields.get("score"));
|
|
426
|
+
const recallCount = parseInteger(fields.get("recall") ?? fields.get("recallcount"));
|
|
427
|
+
const uniqueQueries = parseInteger(fields.get("unique") ?? fields.get("uniquequeries"));
|
|
428
|
+
if (score == null || recallCount == null || uniqueQueries == null) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return {
|
|
433
|
+
bodyStart: text.length,
|
|
434
|
+
score,
|
|
435
|
+
recallCount,
|
|
436
|
+
uniqueQueries,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function parseNumber(value: string | undefined): number | null {
|
|
441
|
+
if (!value) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
const parsed = Number.parseFloat(value);
|
|
445
|
+
if (!Number.isFinite(parsed)) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
return parsed;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function parseInteger(value: string | undefined): number | null {
|
|
452
|
+
if (!value) {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
const parsed = Number.parseInt(value, 10);
|
|
456
|
+
if (!Number.isFinite(parsed)) {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
return parsed;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
function normalizeSectionName(value: string): string {
|
|
463
|
+
return value.trim().toLowerCase().replace(/\s+/g, " ");
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function normalizeDiaryPath(value?: string): string {
|
|
467
|
+
const trimmed = value?.trim();
|
|
468
|
+
if (!trimmed) {
|
|
469
|
+
return "";
|
|
470
|
+
}
|
|
471
|
+
return path.resolve(trimmed);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function createRealFsApi(): FsApi {
|
|
475
|
+
return {
|
|
476
|
+
readFile: async (file: string) => fsp.readFile(file),
|
|
477
|
+
stat: async (file: string) => {
|
|
478
|
+
const stat = await fsp.stat(file);
|
|
479
|
+
return { size: stat.size, mtimeMs: stat.mtimeMs };
|
|
480
|
+
},
|
|
481
|
+
watch: (dir: string, onChange: (event: string, filename: string | Buffer | null) => void) => fs.watch(dir, onChange),
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
function formatError(error: unknown): string {
|
|
486
|
+
if (error instanceof Error) {
|
|
487
|
+
return error.message;
|
|
488
|
+
}
|
|
489
|
+
return String(error);
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const textDecoder = new TextDecoder();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const DREAM_COLLECTION_PREFIX = "dream:";
|
|
2
|
+
|
|
3
|
+
const DREAM_PATTERN_RULES: Array<{ label: string; patterns: RegExp[] }> = [
|
|
4
|
+
{
|
|
5
|
+
label: "dream",
|
|
6
|
+
patterns: [
|
|
7
|
+
/\bdream(?:s|ed|ing)?\b/i,
|
|
8
|
+
/\btell\s+me\s+about\s+(?:your\s+)?dreams?\b/i,
|
|
9
|
+
/\bwhat\s+did\s+i\s+dream\s+about\b/i,
|
|
10
|
+
/\bwhat\s+was\s+i\s+dreaming\s+about\b/i,
|
|
11
|
+
],
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const DREAM_MATCHED_PATTERNS: string[] = ["dream"];
|
|
16
|
+
const EMPTY_MATCHED_PATTERNS: string[] = [];
|
|
17
|
+
|
|
18
|
+
export interface DreamQuerySignal {
|
|
19
|
+
active: boolean;
|
|
20
|
+
matchedPatterns: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function detectDreamQuerySignal(queryText: string): DreamQuerySignal {
|
|
24
|
+
for (const rule of DREAM_PATTERN_RULES) {
|
|
25
|
+
if (rule.patterns.some((pattern) => pattern.test(queryText))) {
|
|
26
|
+
return {
|
|
27
|
+
active: true,
|
|
28
|
+
matchedPatterns: DREAM_MATCHED_PATTERNS,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
active: false,
|
|
34
|
+
matchedPatterns: EMPTY_MATCHED_PATTERNS,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function resolveDreamCollection(userId: string): string {
|
|
39
|
+
return `${DREAM_COLLECTION_PREFIX}${userId.trim()}`;
|
|
40
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { definePluginEntry, type OpenClawPluginApi } from "openclaw/plugin-sdk/p
|
|
|
2
2
|
import { registerMemoryCli } from "./cli.js";
|
|
3
3
|
import { buildContextEngineFactory } from "./context-engine.js";
|
|
4
4
|
import { createBeforeResetHook, createSessionEndHook } from "./lifecycle-hooks.js";
|
|
5
|
+
import { createDreamPromotionHandle } from "./dream-promotion.js";
|
|
6
|
+
import { createMarkdownIngestionHandle } from "./markdown-ingest.js";
|
|
5
7
|
import { buildMemoryPromptSection } from "./memory-provider.js";
|
|
6
8
|
import { buildMemoryRuntimeBridge } from "./memory-runtime.js";
|
|
7
9
|
import { createRecallCache } from "./recall-cache.js";
|
|
@@ -18,6 +20,15 @@ export default definePluginEntry({
|
|
|
18
20
|
const cfg = api.pluginConfig as PluginConfig;
|
|
19
21
|
const recallCache = createRecallCache<SearchResult>();
|
|
20
22
|
const runtime = createPluginRuntime(cfg, api.logger ?? console);
|
|
23
|
+
const markdownIngestion = createMarkdownIngestionHandle(cfg, runtime.getRpc, api.logger ?? console);
|
|
24
|
+
const dreamPromotion = createDreamPromotionHandle(cfg, runtime.getRpc, api.logger ?? console);
|
|
25
|
+
|
|
26
|
+
void markdownIngestion.start().catch((error) => {
|
|
27
|
+
api.logger?.warn?.(`LibraVDB markdown ingestion failed to start: ${error instanceof Error ? error.message : String(error)}`);
|
|
28
|
+
});
|
|
29
|
+
void dreamPromotion.start().catch((error) => {
|
|
30
|
+
api.logger?.warn?.(`LibraVDB dream promotion failed to start: ${error instanceof Error ? error.message : String(error)}`);
|
|
31
|
+
});
|
|
21
32
|
|
|
22
33
|
registerMemoryCli(api, runtime, cfg, api.logger ?? console);
|
|
23
34
|
api.registerContextEngine("libravdb-memory", () =>
|
|
@@ -27,6 +38,10 @@ export default definePluginEntry({
|
|
|
27
38
|
api.registerMemoryRuntime?.(buildMemoryRuntimeBridge(runtime.getRpc, cfg));
|
|
28
39
|
api.on("before_reset", createBeforeResetHook(runtime, api.logger ?? console));
|
|
29
40
|
api.on("session_end", createSessionEndHook(runtime, api.logger ?? console));
|
|
30
|
-
api.on("gateway_stop", () =>
|
|
41
|
+
api.on("gateway_stop", async () => {
|
|
42
|
+
await dreamPromotion.stop();
|
|
43
|
+
await markdownIngestion.stop();
|
|
44
|
+
await runtime.shutdown();
|
|
45
|
+
});
|
|
31
46
|
},
|
|
32
47
|
});
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
const WASM_BYTES = new Uint8Array([
|
|
2
|
+
0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, 0x01, 0x07, 0x01, 0x60,
|
|
3
|
+
0x02, 0x7f, 0x7f, 0x01, 0x7e, 0x03, 0x02, 0x01, 0x00, 0x04, 0x05, 0x01,
|
|
4
|
+
0x70, 0x01, 0x01, 0x01, 0x05, 0x03, 0x01, 0x00, 0x10, 0x06, 0x09, 0x01,
|
|
5
|
+
0x7f, 0x01, 0x41, 0x80, 0x80, 0xc0, 0x00, 0x0b, 0x07, 0x19, 0x02, 0x06,
|
|
6
|
+
0x6d, 0x65, 0x6d, 0x6f, 0x72, 0x79, 0x02, 0x00, 0x0c, 0x68, 0x61, 0x73,
|
|
7
|
+
0x68, 0x5f, 0x66, 0x6e, 0x76, 0x31, 0x61, 0x36, 0x34, 0x00, 0x00, 0x0a,
|
|
8
|
+
0x41, 0x01, 0x3f, 0x01, 0x01, 0x7e, 0x42, 0x83, 0x87, 0xf4, 0x9c, 0x87,
|
|
9
|
+
0xf6, 0xc3, 0xb2, 0x14, 0x21, 0x02, 0x02, 0x40, 0x20, 0x01, 0x45, 0x0d,
|
|
10
|
+
0x00, 0x03, 0x40, 0x20, 0x02, 0x20, 0x00, 0x31, 0x00, 0x00, 0x85, 0x42,
|
|
11
|
+
0xb3, 0x83, 0x80, 0x80, 0x80, 0x20, 0x7e, 0x21, 0x02, 0x20, 0x00, 0x41,
|
|
12
|
+
0x01, 0x6a, 0x21, 0x00, 0x20, 0x01, 0x41, 0x7f, 0x6a, 0x22, 0x01, 0x0d,
|
|
13
|
+
0x00, 0x0b, 0x0b, 0x20, 0x02, 0x0b,
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
const textEncoder = new TextEncoder();
|
|
17
|
+
const FNV_OFFSET_BASIS = 0xcbf29ce484222325n;
|
|
18
|
+
const FNV_PRIME = 0x100000001b3n;
|
|
19
|
+
|
|
20
|
+
interface HashBackend {
|
|
21
|
+
kind: string;
|
|
22
|
+
hash(bytes: Uint8Array): string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface WasmExports {
|
|
26
|
+
memory: WebAssembly.Memory;
|
|
27
|
+
hash_fnv1a64(ptr: number, len: number): bigint;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class Fnv64Fallback implements HashBackend {
|
|
31
|
+
kind = "js-fnv1a64";
|
|
32
|
+
|
|
33
|
+
hash(bytes: Uint8Array): string {
|
|
34
|
+
let hash = FNV_OFFSET_BASIS;
|
|
35
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
36
|
+
hash ^= BigInt(bytes[i] ?? 0);
|
|
37
|
+
hash = BigInt.asUintN(64, hash * FNV_PRIME);
|
|
38
|
+
}
|
|
39
|
+
return hash.toString(16).padStart(16, "0");
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class WasmFnv64 implements HashBackend {
|
|
44
|
+
kind = "wasm-fnv1a64";
|
|
45
|
+
private readonly memory: WebAssembly.Memory;
|
|
46
|
+
private readonly hashFn: (ptr: number, len: number) => bigint;
|
|
47
|
+
private view: Uint8Array;
|
|
48
|
+
|
|
49
|
+
constructor() {
|
|
50
|
+
const module = new WebAssembly.Module(WASM_BYTES);
|
|
51
|
+
const instance = new WebAssembly.Instance(module, {});
|
|
52
|
+
const exports = instance.exports as unknown as WasmExports;
|
|
53
|
+
this.memory = exports.memory;
|
|
54
|
+
this.hashFn = exports.hash_fnv1a64;
|
|
55
|
+
this.view = new Uint8Array(this.memory.buffer);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
hash(bytes: Uint8Array): string {
|
|
59
|
+
this.ensureCapacity(bytes.length);
|
|
60
|
+
this.view.set(bytes, 0);
|
|
61
|
+
const raw = this.hashFn(0, bytes.length);
|
|
62
|
+
return BigInt.asUintN(64, raw).toString(16).padStart(16, "0");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
private ensureCapacity(size: number): void {
|
|
66
|
+
if (this.view.byteLength >= size) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const pageSize = 65536;
|
|
71
|
+
const requiredPages = Math.ceil(size / pageSize);
|
|
72
|
+
const currentPages = this.memory.buffer.byteLength / pageSize;
|
|
73
|
+
const deltaPages = requiredPages - currentPages;
|
|
74
|
+
if (deltaPages > 0) {
|
|
75
|
+
this.memory.grow(deltaPages);
|
|
76
|
+
this.view = new Uint8Array(this.memory.buffer);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let backend: HashBackend | null = null;
|
|
82
|
+
|
|
83
|
+
function getBackend(): HashBackend {
|
|
84
|
+
if (!backend) {
|
|
85
|
+
try {
|
|
86
|
+
backend = new WasmFnv64();
|
|
87
|
+
} catch {
|
|
88
|
+
backend = new Fnv64Fallback();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return backend;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function hashBytes(bytes: Uint8Array): string {
|
|
95
|
+
return getBackend().hash(bytes);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function hashText(text: string): string {
|
|
99
|
+
return hashBytes(textEncoder.encode(text));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function getHashBackendName(): string {
|
|
103
|
+
return getBackend().kind;
|
|
104
|
+
}
|