@yhc3577/memory-new 0.1.1 → 0.1.3

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/dist/index.js CHANGED
@@ -1,54 +1,1660 @@
1
1
  // index.ts
2
2
  import { definePluginEntry } from "openclaw/plugin-sdk/plugin-entry";
3
+
4
+ // src/store/storage.ts
5
+ import { mkdirSync, writeFileSync, readFileSync, existsSync, appendFileSync } from "fs";
6
+ import { join, dirname } from "path";
7
+ var STORAGE_PATHS = {
8
+ l0: "memory/l0/",
9
+ l1: "memory/l1/",
10
+ l2: "memory/scenes/",
11
+ l3: "persona.md",
12
+ index: {
13
+ l2: "memory/scenes/index.json",
14
+ l3: "persona.md.meta"
15
+ }
16
+ };
17
+ var StorageAdapter = class {
18
+ baseDir;
19
+ constructor(baseDir = "~/.openclaw/memory-new") {
20
+ this.baseDir = baseDir.replace("~", process.env.HOME || "/root");
21
+ mkdirSync(this.baseDir, { recursive: true });
22
+ }
23
+ resolve(path) {
24
+ return join(this.baseDir, path);
25
+ }
26
+ // ========== File Operations ==========
27
+ async readFile(key) {
28
+ const filePath = this.resolve(key);
29
+ if (!existsSync(filePath)) return null;
30
+ return readFileSync(filePath, "utf-8");
31
+ }
32
+ async writeFile(key, content) {
33
+ const filePath = this.resolve(key);
34
+ mkdirSync(dirname(filePath), { recursive: true });
35
+ writeFileSync(filePath, content, "utf-8");
36
+ }
37
+ async appendFile(key, content) {
38
+ const filePath = this.resolve(key);
39
+ mkdirSync(dirname(filePath), { recursive: true });
40
+ appendFileSync(filePath, content, "utf-8");
41
+ }
42
+ async exists(key) {
43
+ return existsSync(this.resolve(key));
44
+ }
45
+ // ========== L0 Operations (JSONL per session) ==========
46
+ /**
47
+ * L0: Append message to session's JSONL file (TDB pattern: append-only)
48
+ */
49
+ async appendL0(record) {
50
+ const sessionFile = `${STORAGE_PATHS.l0}${record.sessionKey}.jsonl`;
51
+ const line = JSON.stringify(record) + "\n";
52
+ await this.appendFile(sessionFile, line);
53
+ }
54
+ /**
55
+ * L0: Batch read messages from session
56
+ */
57
+ async readL0(sessionKey, limit = 100) {
58
+ const sessionFile = `${STORAGE_PATHS.l0}${sessionKey}.jsonl`;
59
+ const content = await this.readFile(sessionFile);
60
+ if (!content) return [];
61
+ const lines = content.split("\n").filter((l) => l.trim());
62
+ const messages = lines.slice(-limit).map((line) => JSON.parse(line));
63
+ return messages;
64
+ }
65
+ // ========== L1 Operations (JSONL) ==========
66
+ /**
67
+ * L1: Append atomic memory to session's JSONL file
68
+ */
69
+ async appendL1(record) {
70
+ const sessionFile = `${STORAGE_PATHS.l1}${record.sessionKey}.jsonl`;
71
+ const line = JSON.stringify(record) + "\n";
72
+ await this.appendFile(sessionFile, line);
73
+ }
74
+ /**
75
+ * L1: Search memories by content (simple full-text scan)
76
+ */
77
+ async searchL1(query, limit = 10) {
78
+ const content = await this.readFile(`${STORAGE_PATHS.l1}*.jsonl`);
79
+ if (!content) return [];
80
+ const lines = content.split("\n").filter((l) => l.trim());
81
+ const allRecords = lines.map((line) => JSON.parse(line));
82
+ const queryLower = query.toLowerCase();
83
+ return allRecords.filter((r) => r.content.toLowerCase().includes(queryLower)).slice(0, limit);
84
+ }
85
+ // ========== L2 Operations (Markdown files) ==========
86
+ /**
87
+ * L2: Write scene block as Markdown file
88
+ */
89
+ async writeScene(scene2) {
90
+ const sceneFile = `${STORAGE_PATHS.l2}${scene2.id}.md`;
91
+ const frontmatter = `---
92
+ id: ${scene2.id}
93
+ title: ${scene2.title}
94
+ summary: ${scene2.summary}
95
+ tags: ${JSON.stringify(scene2.tags)}
96
+ created_at: ${scene2.createdAt}
97
+ updated_at: ${scene2.updatedAt}
98
+ ---
99
+
100
+ `;
101
+ await this.writeFile(sceneFile, frontmatter + scene2.content);
102
+ await this.updateSceneIndex(scene2);
103
+ }
104
+ /**
105
+ * L2: Read scene block
106
+ */
107
+ async readScene(sceneId) {
108
+ const content = await this.readFile(`${STORAGE_PATHS.l2}${sceneId}.md`);
109
+ if (!content) return null;
110
+ const lines = content.split("\n");
111
+ const frontmatterEnd = lines.findIndex((l) => l === "---", 1);
112
+ if (frontmatterEnd <= 1) return null;
113
+ const frontmatter = {};
114
+ for (let i = 1; i < frontmatterEnd; i++) {
115
+ const [key, ...valueParts] = lines[i].split(":");
116
+ if (key && valueParts.length > 0) {
117
+ frontmatter[key.trim()] = valueParts.join(":").trim();
118
+ }
119
+ }
120
+ return {
121
+ id: scene.id,
122
+ title: frontmatter.title || "",
123
+ content: lines.slice(frontmatterEnd + 1).join("\n"),
124
+ summary: frontmatter.summary || "",
125
+ tags: JSON.parse(frontmatter.tags || "[]"),
126
+ metadata: { layer: "L2", heat: 0, sourceRecords: [] },
127
+ createdAt: frontmatter.created_at || "",
128
+ updatedAt: frontmatter.updated_at || ""
129
+ };
130
+ }
131
+ /**
132
+ * L2: Maintain scene index for navigation
133
+ */
134
+ async updateSceneIndex(scene2) {
135
+ const indexFile = STORAGE_PATHS.index.l2;
136
+ let index = {};
137
+ const existing = await this.readFile(indexFile);
138
+ if (existing) {
139
+ try {
140
+ index = JSON.parse(existing);
141
+ } catch {
142
+ }
143
+ }
144
+ index[scene2.id] = {
145
+ id: scene2.id,
146
+ title: scene2.title,
147
+ summary: scene2.summary,
148
+ tags: scene2.tags
149
+ };
150
+ await this.writeFile(indexFile, JSON.stringify(index, null, 2));
151
+ }
152
+ /**
153
+ * L2: Read scene index for navigation
154
+ */
155
+ async readSceneIndex() {
156
+ const indexFile = STORAGE_PATHS.index.l2;
157
+ const content = await this.readFile(indexFile);
158
+ if (!content) return [];
159
+ try {
160
+ const index = JSON.parse(content);
161
+ return Object.values(index);
162
+ } catch {
163
+ return [];
164
+ }
165
+ }
166
+ // ========== L3 Operations (persona.md) ==========
167
+ /**
168
+ * L3: Write persona file
169
+ */
170
+ async writePersona(persona) {
171
+ const frontmatter = `---
172
+ id: ${persona.id}
173
+ created_at: ${persona.createdAt}
174
+ updated_at: ${persona.updatedAt}
175
+ ---
176
+
177
+ `;
178
+ await this.writeFile(STORAGE_PATHS.l3, frontmatter + persona.content);
179
+ }
180
+ /**
181
+ * L3: Read persona file
182
+ */
183
+ async readPersona() {
184
+ const content = await this.readFile(STORAGE_PATHS.l3);
185
+ if (!content) return null;
186
+ const lines = content.split("\n");
187
+ const frontmatterEnd = lines.findIndex((l) => l === "---", 1);
188
+ if (frontmatterEnd <= 1) return null;
189
+ const frontmatter = {};
190
+ for (let i = 1; i < frontmatterEnd; i++) {
191
+ const [key, ...valueParts] = lines[i].split(":");
192
+ if (key && valueParts.length > 0) {
193
+ frontmatter[key.trim()] = valueParts.join(":").trim();
194
+ }
195
+ }
196
+ return {
197
+ id: frontmatter.id || "",
198
+ content: lines.slice(frontmatterEnd + 1).join("\n"),
199
+ summary: "",
200
+ metadata: { layer: "L3", sourceScenes: [] },
201
+ createdAt: frontmatter.created_at || "",
202
+ updatedAt: frontmatter.updated_at || ""
203
+ };
204
+ }
205
+ };
206
+ var RECALL_LINE_SEPARATOR = "\n";
207
+ var MEMORY_TOOLS_GUIDE = `<memory-tools-guide>
208
+ ## \u8BB0\u5FC6\u5DE5\u5177\u8C03\u7528\u6307\u5357
209
+
210
+ \u5F53\u4E0A\u65B9\u6CE8\u5165\u7684\u8BB0\u5FC6\u7247\u6BB5\u4E0D\u8DB3\u4EE5\u56DE\u7B54\u7528\u6237\u95EE\u9898\u65F6\uFF0C\u53EF\u4E3B\u52A8\u8C03\u7528\u4EE5\u4E0B\u5DE5\u5177\u83B7\u53D6\u66F4\u591A\u4FE1\u606F\uFF1A
211
+
212
+ - **memory_search**\uFF1A\u641C\u7D22\u7ED3\u6784\u5316\u8BB0\u5FC6\uFF08L1\uFF09\uFF0C\u9002\u7528\u4E8E\u56DE\u5FC6\u7528\u6237\u504F\u597D\u3001\u5386\u53F2\u4E8B\u4EF6\u8282\u70B9\u3001\u89C4\u5219\u7B49\u5173\u952E\u4FE1\u606F\u3002
213
+ - **memory_get**\uFF1A\u83B7\u53D6\u7279\u5B9A\u8BB0\u5FC6\u8BE6\u60C5\u3002
214
+
215
+ ### \u8C03\u7528\u6B21\u6570\u9650\u5236
216
+ \u6BCF\u8F6E\u5BF9\u8BDD\u4E2D\uFF0C\u8BB0\u5FC6\u641C\u7D22\u5DE5\u5177**\u5408\u8BA1\u6700\u591A\u8C03\u7528 3 \u6B21**\u3002
217
+ </memory-tools-guide>`;
218
+ function generateSceneNavigation(scenes) {
219
+ if (scenes.length === 0) return "";
220
+ const lines = scenes.map(
221
+ (s) => `- [${s.title}](memory://scene/${s.id}): ${s.summary}`
222
+ );
223
+ return `## \u60C5\u5883\u5BFC\u822A
224
+ ${lines.join(RECALL_LINE_SEPARATOR)}`;
225
+ }
226
+ var RecallEngine = class {
227
+ storage;
228
+ options;
229
+ constructor(storage, options = {}) {
230
+ this.storage = storage;
231
+ this.options = {
232
+ hybridSearch: options.hybridSearch ?? true,
233
+ semanticWeight: options.semanticWeight ?? 0.5,
234
+ bm25Weight: options.bm25Weight ?? 0.25,
235
+ entityBoostWeight: options.entityBoostWeight ?? 0.25
236
+ };
237
+ }
238
+ /**
239
+ * Perform recall: search L1 + read L2 + read L3
240
+ * (Reference: TDB's performAutoRecallCore)
241
+ *
242
+ * Uses hybrid search when vector store is available via hybridSearch option.
243
+ */
244
+ async recall(params) {
245
+ const { query, sessionKey, topK = 10, vectorStore } = params;
246
+ let memories = [];
247
+ let recallStrategy = "text";
248
+ if (vectorStore && this.options.hybridSearch) {
249
+ const searchResults = await vectorStore.hybridSearch({
250
+ query,
251
+ topK,
252
+ semanticWeight: this.options.semanticWeight,
253
+ bm25Weight: this.options.bm25Weight,
254
+ entityBoostWeight: this.options.entityBoostWeight
255
+ });
256
+ const matchedRecords = [];
257
+ for (const result of searchResults) {
258
+ const records = await this.storage.searchL1("", 100);
259
+ const record = records.find((r) => r.id === result.id);
260
+ if (record) {
261
+ matchedRecords.push(record);
262
+ }
263
+ }
264
+ memories = matchedRecords;
265
+ recallStrategy = "hybrid";
266
+ } else {
267
+ memories = await this.storage.searchL1(query, topK);
268
+ recallStrategy = "text";
269
+ }
270
+ const sceneIndex = await this.storage.readSceneIndex();
271
+ const persona = await this.storage.readPersona();
272
+ let prependContext;
273
+ if (memories.length > 0) {
274
+ const memoryLines = memories.map(
275
+ (m) => `- [${m.type}] ${m.content}`
276
+ );
277
+ prependContext = `<relevant-memories>
278
+ \u4EE5\u4E0B\u662F\u4E0E\u5F53\u524D\u5BF9\u8BDD\u76F8\u5173\u7684\u8BB0\u5FC6\uFF1A
279
+
280
+ ${memoryLines.join(RECALL_LINE_SEPARATOR)}
281
+ </relevant-memories>`;
282
+ }
283
+ const stableParts = [];
284
+ if (persona) {
285
+ stableParts.push(`<user-persona>
286
+ ${persona.content}
287
+ </user-persona>`);
288
+ }
289
+ if (sceneIndex.length > 0) {
290
+ stableParts.push(`<scene-navigation>
291
+ ${generateSceneNavigation(sceneIndex)}
292
+ </scene-navigation>`);
293
+ }
294
+ if (stableParts.length > 0 || prependContext) {
295
+ stableParts.push(MEMORY_TOOLS_GUIDE);
296
+ }
297
+ const appendSystemContext = stableParts.length > 0 ? stableParts.join("\n\n") : void 0;
298
+ return {
299
+ prependContext,
300
+ appendSystemContext,
301
+ recalledL1Memories: memories.map((m) => ({
302
+ content: m.content,
303
+ score: 0.5,
304
+ // TODO: calculate real score
305
+ type: m.type
306
+ })),
307
+ recalledL3Persona: persona?.content ?? null,
308
+ recallStrategy
309
+ };
310
+ }
311
+ };
312
+ var MemoryStore = class {
313
+ storage;
314
+ recall;
315
+ _vectorStore;
316
+ constructor(baseDir = "~/.openclaw/memory-new") {
317
+ this.storage = new StorageAdapter(baseDir);
318
+ this.recall = new RecallEngine(this.storage);
319
+ }
320
+ // ========== Vector Store (for semantic search) ==========
321
+ get vectorStore() {
322
+ return this._vectorStore;
323
+ }
324
+ setVectorStore(store) {
325
+ this._vectorStore = store;
326
+ }
327
+ // ========== L0 Operations ==========
328
+ async ingestMessage(message) {
329
+ const record = {
330
+ ...message,
331
+ id: `l0_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
332
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString()
333
+ };
334
+ await this.storage.appendL0(record);
335
+ return record;
336
+ }
337
+ async getMessages(sessionKey, limit = 100) {
338
+ return this.storage.readL0(sessionKey, limit);
339
+ }
340
+ // ========== L1 Operations ==========
341
+ async storeL1(record) {
342
+ const now = (/* @__PURE__ */ new Date()).toISOString();
343
+ const fullRecord = {
344
+ ...record,
345
+ id: `l1_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
346
+ createdAt: now,
347
+ updatedAt: now,
348
+ version: 1
349
+ };
350
+ await this.storage.appendL1(fullRecord);
351
+ return fullRecord;
352
+ }
353
+ async searchL1(query, limit = 10) {
354
+ return this.storage.searchL1(query, limit);
355
+ }
356
+ // ========== L2 Operations ==========
357
+ async storeL2(scene2) {
358
+ const now = (/* @__PURE__ */ new Date()).toISOString();
359
+ const fullScene = {
360
+ ...scene2,
361
+ id: `scene_${Date.now()}_${Math.random().toString(36).slice(2, 9)}`,
362
+ createdAt: now,
363
+ updatedAt: now
364
+ };
365
+ await this.storage.writeScene(fullScene);
366
+ return fullScene;
367
+ }
368
+ async getScene(sceneId) {
369
+ return this.storage.readScene(sceneId);
370
+ }
371
+ async getSceneIndex() {
372
+ return this.storage.readSceneIndex();
373
+ }
374
+ // ========== L3 Operations ==========
375
+ async storeL3(persona) {
376
+ const now = (/* @__PURE__ */ new Date()).toISOString();
377
+ const fullPersona = {
378
+ ...persona,
379
+ id: `persona_${Date.now()}`,
380
+ createdAt: now,
381
+ updatedAt: now
382
+ };
383
+ await this.storage.writePersona(fullPersona);
384
+ return fullPersona;
385
+ }
386
+ async getPersona() {
387
+ return this.storage.readPersona();
388
+ }
389
+ // ========== Recall ==========
390
+ async recallMemories(query, sessionKey, userId, agentId, topK) {
391
+ return this.recall.recall({ query, sessionKey, userId, agentId, topK });
392
+ }
393
+ };
394
+
395
+ // src/vector/vector-store.ts
396
+ var VectorStore = class {
397
+ records = /* @__PURE__ */ new Map();
398
+ embeddingProvider = null;
399
+ dimension = 384;
400
+ constructor(dimension = 384) {
401
+ this.dimension = dimension;
402
+ }
403
+ setEmbeddingProvider(provider) {
404
+ this.embeddingProvider = provider;
405
+ }
406
+ // ========== Record Operations ==========
407
+ async add(record) {
408
+ const fullRecord = {
409
+ ...record,
410
+ createdAt: Date.now()
411
+ };
412
+ this.records.set(record.id, fullRecord);
413
+ return fullRecord;
414
+ }
415
+ async get(id) {
416
+ return this.records.get(id) ?? null;
417
+ }
418
+ async delete(id) {
419
+ return this.records.delete(id);
420
+ }
421
+ async update(id, updates) {
422
+ const existing = this.records.get(id);
423
+ if (!existing) return null;
424
+ const updated = { ...existing, ...updates };
425
+ this.records.set(id, updated);
426
+ return updated;
427
+ }
428
+ // ========== Embedding Operations ==========
429
+ async embedContent(content) {
430
+ if (!this.embeddingProvider) {
431
+ return this.fallbackEmbed(content);
432
+ }
433
+ try {
434
+ const embedding = await this.embeddingProvider.embed(content, { inputType: "query" });
435
+ return embedding;
436
+ } catch (error) {
437
+ console.error("Embedding failed, using fallback:", error);
438
+ return this.fallbackEmbed(content);
439
+ }
440
+ }
441
+ async embedBatch(contents) {
442
+ if (!this.embeddingProvider) {
443
+ return contents.map((c) => this.fallbackEmbed(c));
444
+ }
445
+ try {
446
+ return await this.embeddingProvider.embedBatch(contents, { inputType: "document" });
447
+ } catch (error) {
448
+ console.error("Batch embedding failed, using fallback:", error);
449
+ return contents.map((c) => this.fallbackEmbed(c));
450
+ }
451
+ }
452
+ // Fallback pseudo-embedding using hash (for testing without API)
453
+ fallbackEmbed(content) {
454
+ return this.generatePseudoEmbedding(content);
455
+ }
456
+ /**
457
+ * Generate pseudo-embedding from text content.
458
+ * Used as fallback when no embedding provider is available.
459
+ */
460
+ generatePseudoEmbedding(content) {
461
+ const embedding = new Array(this.dimension).fill(0);
462
+ let hash = 0;
463
+ for (let i = 0; i < content.length; i++) {
464
+ hash = (hash << 5) - hash + content.charCodeAt(i);
465
+ hash = hash & hash;
466
+ }
467
+ const seed = Math.abs(hash);
468
+ for (let i = 0; i < this.dimension; i++) {
469
+ const charCode = content.charCodeAt(i % content.length) || 1;
470
+ embedding[i] = Math.sin(seed * (i + 1) * charCode) * 0.5 + 0.5;
471
+ }
472
+ const magnitude = Math.sqrt(embedding.reduce((sum, v) => sum + v * v, 0));
473
+ if (magnitude > 0) {
474
+ for (let i = 0; i < embedding.length; i++) {
475
+ embedding[i] /= magnitude;
476
+ }
477
+ }
478
+ return embedding;
479
+ }
480
+ // ========== Vector Search ==========
481
+ async searchByVector(queryEmbedding, topK, minScore = 0) {
482
+ const results = [];
483
+ for (const record of this.records.values()) {
484
+ if (record.embedding.length !== queryEmbedding.length) continue;
485
+ const score = this.cosineSimilarity(queryEmbedding, record.embedding);
486
+ if (score >= minScore) {
487
+ results.push({
488
+ id: record.id,
489
+ content: record.content,
490
+ score,
491
+ metadata: record.metadata
492
+ });
493
+ }
494
+ }
495
+ results.sort((a, b) => b.score - a.score);
496
+ return results.slice(0, topK);
497
+ }
498
+ // ========== Hybrid Search ==========
499
+ async hybridSearch(options, getTextScore = bm25Score) {
500
+ const { query, topK, semanticWeight, bm25Weight, entityBoostWeight, minScore = 0.1 } = options;
501
+ let queryEmbedding = options.queryEmbedding;
502
+ if (!queryEmbedding) {
503
+ queryEmbedding = await this.embedContent(query);
504
+ }
505
+ const semanticResults = await this.searchByVector(queryEmbedding, topK * 2, 0);
506
+ const bm25Results = this.bm25Search(query, topK * 2);
507
+ const entities = this.extractEntities(query);
508
+ const entityBoostResults = this.entityBoostSearch(entities, topK * 2);
509
+ const scoreMap = /* @__PURE__ */ new Map();
510
+ for (const result of semanticResults) {
511
+ const semanticScore = result.score * semanticWeight;
512
+ scoreMap.set(result.id, {
513
+ id: result.id,
514
+ content: result.content,
515
+ score: semanticScore,
516
+ metadata: result.metadata
517
+ });
518
+ }
519
+ for (const result of bm25Results) {
520
+ const existing = scoreMap.get(result.id);
521
+ const bm25Contribution = result.score * bm25Weight;
522
+ if (existing) {
523
+ existing.score += bm25Contribution;
524
+ } else {
525
+ scoreMap.set(result.id, {
526
+ id: result.id,
527
+ content: result.content,
528
+ score: bm25Contribution,
529
+ metadata: result.metadata
530
+ });
531
+ }
532
+ }
533
+ for (const result of entityBoostResults) {
534
+ const existing = scoreMap.get(result.id);
535
+ const boostContribution = result.score * entityBoostWeight;
536
+ if (existing) {
537
+ existing.score += boostContribution;
538
+ } else {
539
+ scoreMap.set(result.id, {
540
+ id: result.id,
541
+ content: result.content,
542
+ score: boostContribution,
543
+ metadata: result.metadata
544
+ });
545
+ }
546
+ }
547
+ const finalResults = Array.from(scoreMap.values()).filter((r) => r.score >= minScore).sort((a, b) => b.score - a.score).slice(0, topK);
548
+ return finalResults;
549
+ }
550
+ // ========== BM25 (in-memory simplified) ==========
551
+ bm25Search(query, topK) {
552
+ const queryTerms = query.toLowerCase().split(/\s+/);
553
+ const results = [];
554
+ const avgDocLen = this.getAverageDocLength();
555
+ const k1 = 1.5;
556
+ const b = 0.75;
557
+ for (const record of this.records.values()) {
558
+ const terms = record.content.toLowerCase().split(/\s+/);
559
+ let score = 0;
560
+ for (const term of queryTerms) {
561
+ const tf = terms.filter((t) => t === term).length;
562
+ if (tf > 0) {
563
+ const idf = Math.log((this.records.size + 1) / 2);
564
+ const docLen = terms.length;
565
+ const numerator = tf * (k1 + 1);
566
+ const denominator = tf + k1 * (1 - b + b * (docLen / avgDocLen));
567
+ score += idf * (numerator / denominator);
568
+ }
569
+ }
570
+ if (score > 0) {
571
+ results.push({
572
+ id: record.id,
573
+ content: record.content,
574
+ score,
575
+ metadata: record.metadata
576
+ });
577
+ }
578
+ }
579
+ results.sort((a, b2) => b2.score - a.score);
580
+ return results.slice(0, topK);
581
+ }
582
+ getAverageDocLength() {
583
+ if (this.records.size === 0) return 1;
584
+ let total = 0;
585
+ for (const record of this.records.values()) {
586
+ total += record.content.split(/\s+/).length;
587
+ }
588
+ return total / this.records.size;
589
+ }
590
+ // ========== Entity Extraction & Boost ==========
591
+ extractEntities(query) {
592
+ const entities = [];
593
+ const capitalizedPattern = /[A-Z][a-z]+/g;
594
+ let match;
595
+ while ((match = capitalizedPattern.exec(query)) !== null) {
596
+ entities.push(match[0].toLowerCase());
597
+ }
598
+ const quotedPattern = /"([^"]+)"|'([^']+)'/g;
599
+ while ((match = quotedPattern.exec(query)) !== null) {
600
+ const entity = match[1] || match[2];
601
+ entities.push(entity.toLowerCase());
602
+ }
603
+ return [...new Set(entities)];
604
+ }
605
+ entityBoostSearch(entities, topK) {
606
+ if (entities.length === 0) return [];
607
+ const results = [];
608
+ for (const record of this.records.values()) {
609
+ const content = record.content.toLowerCase();
610
+ let matchCount = 0;
611
+ for (const entity of entities) {
612
+ if (content.includes(entity)) {
613
+ matchCount++;
614
+ }
615
+ }
616
+ if (matchCount > 0) {
617
+ const score = matchCount / entities.length;
618
+ results.push({
619
+ id: record.id,
620
+ content: record.content,
621
+ score,
622
+ metadata: record.metadata
623
+ });
624
+ }
625
+ }
626
+ results.sort((a, b) => b.score - a.score);
627
+ return results.slice(0, topK);
628
+ }
629
+ // ========== Utilities ==========
630
+ cosineSimilarity(a, b) {
631
+ if (a.length !== b.length) return 0;
632
+ let dotProduct = 0;
633
+ let normA = 0;
634
+ let normB = 0;
635
+ for (let i = 0; i < a.length; i++) {
636
+ dotProduct += a[i] * b[i];
637
+ normA += a[i] * a[i];
638
+ normB += b[i] * b[i];
639
+ }
640
+ const denominator = Math.sqrt(normA) * Math.sqrt(normB);
641
+ if (denominator === 0) return 0;
642
+ return dotProduct / denominator;
643
+ }
644
+ async getAll() {
645
+ return Array.from(this.records.values());
646
+ }
647
+ async count() {
648
+ return this.records.size;
649
+ }
650
+ async clear() {
651
+ this.records.clear();
652
+ }
653
+ };
654
+ function bm25Score(content, query) {
655
+ const queryTerms = query.toLowerCase().split(/\s+/);
656
+ const terms = content.toLowerCase().split(/\s+/);
657
+ let score = 0;
658
+ for (const term of queryTerms) {
659
+ const tf = terms.filter((t) => t === term).length;
660
+ if (tf > 0) {
661
+ score += 1 + Math.log(1 + tf);
662
+ }
663
+ }
664
+ return score;
665
+ }
666
+
667
+ // src/pipeline/distillation.ts
668
+ var DEFAULT_DISTILLATION_CONFIG = {
669
+ l1: {
670
+ messageThreshold: 5,
671
+ idleSeconds: 60,
672
+ batchSize: 10,
673
+ enableDedup: true,
674
+ maxMemoriesPerSession: 50
675
+ },
676
+ l2: {
677
+ minIntervalMs: 15 * 60 * 1e3,
678
+ maxIntervalMs: 60 * 60 * 1e3,
679
+ topicThreshold: 3,
680
+ delayAfterL1Seconds: 90
681
+ },
682
+ l3: {
683
+ conditions: ["explicit_request", "cold_start", "restore", "first_scene", "threshold"],
684
+ importanceThreshold: 0.6
685
+ }
686
+ };
687
+ var L1_EXTRACTION_SYSTEM_PROMPT = `\u4F60\u662F\u4E13\u4E1A\u7684"\u5DE5\u4F5C\u60C5\u5883\u5207\u5206\u4E0E\u56E2\u961F\u5171\u4EAB\u8BB0\u5FC6\u63D0\u53D6\u4E13\u5BB6"\u3002
688
+ \u4F60\u7684\u4EFB\u52A1\u662F\u5206\u6790\u5DE5\u4F5C\u6D88\u606F\uFF0C\u5224\u65AD\u5DE5\u4F5C\u60C5\u5883\u5207\u6362\uFF0C\u5E76\u4ECE\u4E2D\u63D0\u53D6\u53EF\u5728\u56E2\u961F\u5185\u5171\u4EAB\u7684\u7ED3\u6784\u5316\u5DE5\u4F5C\u8BB0\u5FC6\u3002
689
+
690
+ ## \u8F93\u51FA\u8981\u6C42
691
+
692
+ \u4E25\u683C\u6309\u4EE5\u4E0BJSON\u6570\u7EC4\u683C\u5F0F\u8F93\u51FA\uFF0C\u4E0D\u8981\u8F93\u51FA\u4EFB\u4F55\u989D\u5916\u7684 Markdown \u4EE3\u7801\u5757\u4FEE\u9970\u7B26\uFF08\u5982 \`\`\`json\uFF09\u6216\u89E3\u91CA\u6587\u672C\uFF1A
693
+
694
+ [
695
+ {
696
+ "scene_name": "\u60C5\u5883\u540D\u79F0\uFF08\u7B80\u6D01\uFF0C1-10\u4E2A\u5B57\uFF09",
697
+ "memories": [
698
+ {
699
+ "content": "\u8BB0\u5FC6\u5185\u5BB9\uFF08\u5B8C\u6574\u53E5\u5B50\uFF0C20-200\u5B57\uFF09",
700
+ "type": "persona | episodic | instruction",
701
+ "priority": \u4F18\u5148\u7EA7(0-100, \u8D8A\u9AD8\u8D8A\u91CD\u8981),
702
+ "source_message_ids": ["\u76F8\u5173\u6D88\u606FID"],
703
+ "metadata": {}
704
+ }
705
+ ]
706
+ }
707
+ ]
708
+
709
+ ## Memory Type \u5B9A\u4E49
710
+
711
+ - **persona**: \u5173\u4E8E\u7528\u6237\u504F\u597D\u3001\u4E60\u60EF\u3001\u5DE5\u4F5C\u65B9\u5F0F\u7684\u8BB0\u5FC6\uFF08\u5982"\u7528\u6237\u559C\u6B22\u5728\u4E0A\u5348\u5904\u7406\u590D\u6742\u4EFB\u52A1"\uFF09
712
+ - **episodic**: \u5177\u4F53\u7684\u9879\u76EE\u4E8B\u4EF6\u3001\u51B3\u7B56\u3001\u8BA8\u8BBA\u8981\u70B9\uFF08\u5982"\u9879\u76EEX\u51B3\u5B9A\u4F7F\u7528\u5FAE\u670D\u52A1\u67B6\u6784"\uFF09
713
+ - **instruction**: \u7528\u6237\u7684\u660E\u786E\u6307\u4EE4\u6216\u9700\u6C42\uFF08\u5982"\u7528\u6237\u8981\u6C42\u6BCF\u5468\u4E94\u540C\u6B65\u8FDB\u5EA6"\uFF09
714
+
715
+ ## \u573A\u666F\u5207\u6362\u5224\u65AD
716
+
717
+ \u5F53\u51FA\u73B0\u4EE5\u4E0B\u60C5\u51B5\u65F6\uFF0C\u5E94\u8BE5\u5207\u6362\u5230\u65B0\u573A\u666F\uFF1A
718
+ 1. \u8BDD\u9898\u53D1\u751F\u5B9E\u8D28\u6027\u53D8\u5316
719
+ 2. \u53C2\u4E0E\u4EBA\u5458\u53D1\u751F\u660E\u663E\u53D8\u5316
720
+ 3. \u4EFB\u52A1\u76EE\u6807\u53D1\u751F\u5207\u6362
721
+ 4. \u65F6\u95F4\u95F4\u9694\u8D85\u8FC730\u5206\u949F`;
722
+ function formatExtractionPrompt(newMessages, backgroundMessages, previousSceneName) {
723
+ const bgText = backgroundMessages.length > 0 ? backgroundMessages.map((m) => `[${m.id}] [${m.role}] [${new Date(m.timestamp).toISOString()}]: ${m.content}`).join("\n\n") : "\u65E0";
724
+ const newText = newMessages.map((m) => `[${m.id}] [${m.role}] [${new Date(m.timestamp).toISOString()}]: ${m.content}`).join("\n\n");
725
+ const userPrompt = `**\u8F93\u51FA\u8BED\u8A00**\uFF1A\u6839\u636E\u4E0B\u65B9"\u5F85\u63D0\u53D6\u7684\u65B0\u6D88\u606F"\u4E2D user \u53D1\u8A00\u7684\u4E3B\u5BFC\u8BED\u8A00\u4E66\u5199 \`scene_name\` \u548C memory \`content\`\u3002
726
+
727
+ \u3010\u4E0A\u4E00\u4E2A\u60C5\u5883\u3011\uFF1A${previousSceneName || "\u65E0"}
728
+
729
+ \u3010\u80CC\u666F\u5BF9\u8BDD\u3011\uFF08\u4EC5\u4F9B\u7406\u89E3\u4E0A\u4E0B\u6587\u63A8\u65AD\u5173\u7CFB/\u65F6\u95F4\uFF0C\u4E25\u7981\u4ECE\u4E2D\u63D0\u53D6\u8BB0\u5FC6\uFF09\uFF1A
730
+ ${bgText}
731
+
732
+ \u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501
733
+
734
+ \u3010\u5F85\u63D0\u53D6\u7684\u65B0\u6D88\u606F\u3011\uFF08\u52A1\u5FC5\u7ED3\u5408 timestamp \u63A8\u7B97\u65F6\u95F4\uFF0C\u53EA\u4ECE\u8FD9\u91CC\u63D0\u53D6\u8BB0\u5FC6\uFF01\uFF09\uFF1A
735
+ ${newText}`;
736
+ return {
737
+ systemPrompt: L1_EXTRACTION_SYSTEM_PROMPT,
738
+ userPrompt
739
+ };
740
+ }
741
+ var DistillationPipeline = class {
742
+ config;
743
+ store;
744
+ vectorStore;
745
+ llmRunner;
746
+ subagentRunner;
747
+ lastL1At = null;
748
+ lastL2At = null;
749
+ lastL3At = null;
750
+ pendingL1Extraction = null;
751
+ // In-memory buffers (TDB uses VectorStore + JSONL)
752
+ messageBuffer = [];
753
+ constructor(config = DEFAULT_DISTILLATION_CONFIG, store, vectorStore, llmRunner) {
754
+ this.config = config;
755
+ this.store = store || new MemoryStore();
756
+ this.vectorStore = vectorStore || new VectorStore();
757
+ this.llmRunner = llmRunner;
758
+ }
759
+ /**
760
+ * Set LLM runner for simple prompt-completion style extraction
761
+ */
762
+ setLLMRunner(runner) {
763
+ this.llmRunner = runner;
764
+ }
765
+ /**
766
+ * Set subagent runner for L1 extraction via OpenClaw subagent runtime
767
+ */
768
+ setSubagentRunner(runner) {
769
+ this.subagentRunner = runner;
770
+ }
771
+ /**
772
+ * Set vector store for embedding-based search
773
+ */
774
+ setVectorStore(store) {
775
+ this.vectorStore = store;
776
+ }
777
+ // ============================================================================
778
+ // Ingestion (L0)
779
+ // ============================================================================
780
+ /**
781
+ * Ingest messages into L0 layer (TDB's captureAtomic pattern)
782
+ */
783
+ async ingest(messages) {
784
+ const records = [];
785
+ for (const msg of messages) {
786
+ if (!shouldExtractL1(msg.content)) continue;
787
+ const record = await this.store.ingestMessage(msg);
788
+ records.push(record);
789
+ this.messageBuffer.push(record);
790
+ }
791
+ if (this.messageBuffer.length >= this.config.l1.messageThreshold) {
792
+ await this.triggerL1Extraction();
793
+ } else {
794
+ this.scheduleL1Extraction();
795
+ }
796
+ return records;
797
+ }
798
+ scheduleL1Extraction() {
799
+ if (this.pendingL1Extraction) return;
800
+ this.pendingL1Extraction = setTimeout(async () => {
801
+ this.pendingL1Extraction = null;
802
+ if (this.messageBuffer.length > 0) {
803
+ await this.distill("L1");
804
+ }
805
+ }, this.config.l1.idleSeconds * 1e3);
806
+ }
807
+ async triggerL1Extraction() {
808
+ if (this.pendingL1Extraction) {
809
+ clearTimeout(this.pendingL1Extraction);
810
+ this.pendingL1Extraction = null;
811
+ }
812
+ await this.distill("L1");
813
+ }
814
+ // ============================================================================
815
+ // Distillation
816
+ // ============================================================================
817
+ async distill(stage) {
818
+ switch (stage) {
819
+ case "L1":
820
+ return this.runL1Distillation();
821
+ case "L2":
822
+ return this.runL2Distillation();
823
+ case "L3":
824
+ return this.runL3Distillation();
825
+ default:
826
+ return { stage, produced: 0, errors: ["Unknown stage"] };
827
+ }
828
+ }
829
+ /**
830
+ * L1 Distillation: Extract atomic memories from L0 messages
831
+ * (Reference: TDB's extractL1Memories)
832
+ */
833
+ async runL1Distillation() {
834
+ const errors = [];
835
+ let produced = 0;
836
+ try {
837
+ const messages = this.messageBuffer.slice(-this.config.l1.batchSize);
838
+ if (messages.length === 0) {
839
+ return { stage: "L1", produced: 0, errors: [] };
840
+ }
841
+ const lastScene = await this.getLastSceneName();
842
+ const maxNew = 5;
843
+ const newMessages = messages.slice(-maxNew);
844
+ const backgroundMessages = messages.slice(0, -maxNew);
845
+ const { systemPrompt, userPrompt } = formatExtractionPrompt(
846
+ newMessages,
847
+ backgroundMessages,
848
+ lastScene
849
+ );
850
+ let extractionOutput = "";
851
+ if (this.subagentRunner) {
852
+ const fullPrompt = `${systemPrompt}
853
+
854
+ ${userPrompt}`;
855
+ extractionOutput = await this.subagentRunner(fullPrompt);
856
+ } else if (this.llmRunner) {
857
+ extractionOutput = await this.llmRunner(systemPrompt, userPrompt);
858
+ } else {
859
+ extractionOutput = this.fallbackExtract(messages, lastScene);
860
+ }
861
+ const extractedMemories = parseExtractionOutput(extractionOutput);
862
+ for (const mem of extractedMemories) {
863
+ await this.store.storeL1({
864
+ content: mem.content,
865
+ type: mem.type,
866
+ priority: mem.priority,
867
+ sceneName: mem.scene_name,
868
+ sourceMessageIds: mem.source_message_ids,
869
+ metadata: mem.metadata,
870
+ timestamps: messages.map((m) => new Date(m.timestamp).toISOString()),
871
+ sessionKey: messages[0]?.sessionKey || "default",
872
+ sessionId: messages[0]?.sessionId || "",
873
+ teamId: messages[0]?.teamId,
874
+ userId: messages[0]?.userId || "",
875
+ agentId: messages[0]?.agentId || ""
876
+ });
877
+ produced++;
878
+ }
879
+ this.messageBuffer = this.messageBuffer.slice(0, Math.max(0, this.messageBuffer.length - messages.length));
880
+ this.lastL1At = Date.now();
881
+ setTimeout(() => this.distill("L2"), this.config.l2.delayAfterL1Seconds * 1e3);
882
+ } catch (e) {
883
+ errors.push(String(e));
884
+ }
885
+ return { stage: "L1", produced, errors };
886
+ }
887
+ async getLastSceneName() {
888
+ const recent = await this.store.searchL1("", 1);
889
+ return recent[0]?.sceneName || "\u65E0";
890
+ }
891
+ /**
892
+ * L2 Distillation: Cluster L1 memories into scene blocks
893
+ * (Reference: TDB's SceneExtractor)
894
+ */
895
+ async runL2Distillation() {
896
+ const errors = [];
897
+ let produced = 0;
898
+ try {
899
+ const now = Date.now();
900
+ if (this.lastL2At && now - this.lastL2At < this.config.l2.minIntervalMs) {
901
+ return { stage: "L2", produced: 0, errors: ["Too soon since last L2"] };
902
+ }
903
+ const l1Records = await this.store.searchL1("", 100);
904
+ if (l1Records.length < this.config.l2.topicThreshold) {
905
+ return { stage: "L2", produced: 0, errors: ["Not enough L1 records"] };
906
+ }
907
+ const sceneGroups = /* @__PURE__ */ new Map();
908
+ for (const record of l1Records) {
909
+ if (!sceneGroups.has(record.sceneName)) {
910
+ sceneGroups.set(record.sceneName, []);
911
+ }
912
+ sceneGroups.get(record.sceneName).push(record);
913
+ }
914
+ for (const [sceneName, records] of sceneGroups) {
915
+ if (records.length < this.config.l2.topicThreshold) continue;
916
+ const avgPriority = records.reduce((sum, r) => sum + r.priority, 0) / records.length;
917
+ const content = this.buildSceneContent(sceneName, records);
918
+ await this.store.storeL2({
919
+ title: sceneName,
920
+ content,
921
+ summary: `\u5E73\u5747\u4F18\u5148\u7EA7: ${avgPriority.toFixed(0)}`,
922
+ tags: [sceneName],
923
+ metadata: {
924
+ layer: "L2",
925
+ heat: records.length,
926
+ sourceRecords: records.map((r) => r.id)
927
+ }
928
+ });
929
+ produced++;
930
+ }
931
+ this.lastL2At = now;
932
+ setTimeout(() => this.distill("L3"), 5e3);
933
+ } catch (e) {
934
+ errors.push(String(e));
935
+ }
936
+ return { stage: "L2", produced, errors };
937
+ }
938
+ buildSceneContent(sceneName, records) {
939
+ const points = records.map((r) => `- [${r.type}] ${r.content}`).join("\n");
940
+ const avgPriority = records.reduce((sum, r) => sum + r.priority, 0) / records.length;
941
+ return `# ${sceneName}
942
+
943
+ ## Key Points
944
+ ${points}
945
+
946
+ ## Summary
947
+ \u5E73\u5747\u4F18\u5148\u7EA7: ${avgPriority.toFixed(0)}
948
+ \u5171 ${records.length} \u6761\u76F8\u5173\u8BB0\u5FC6
949
+
950
+ ---
951
+ Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
952
+ `;
953
+ }
954
+ /**
955
+ * L3 Distillation: Build persona from high-value scenes
956
+ * (Reference: TDB's PersonaExtractor)
957
+ */
958
+ async runL3Distillation() {
959
+ const errors = [];
960
+ let produced = 0;
961
+ try {
962
+ const l1Records = await this.store.searchL1("", 100);
963
+ if (l1Records.length === 0) {
964
+ return { stage: "L3", produced: 0, errors: ["No L1 records"] };
965
+ }
966
+ const avgImportance = l1Records.reduce((sum, r) => sum + r.priority, 0) / l1Records.length / 100;
967
+ if (avgImportance < this.config.l3.importanceThreshold) {
968
+ return { stage: "L3", produced: 0, errors: ["Avg importance below threshold"] };
969
+ }
970
+ const sceneIndex = await this.store.getSceneIndex();
971
+ const highValueScenes = sceneIndex.filter((s) => {
972
+ const avg = l1Records.filter((r) => r.sceneName === s.title).reduce((sum, r) => sum + r.priority, 0) / Math.max(1, l1Records.filter((r) => r.sceneName === s.title).length);
973
+ return avg >= this.config.l3.importanceThreshold * 100;
974
+ });
975
+ if (highValueScenes.length === 0) {
976
+ return { stage: "L3", produced: 0, errors: ["No high-value scenes"] };
977
+ }
978
+ const personaContent = this.buildPersona(highValueScenes, l1Records);
979
+ await this.store.storeL3({
980
+ content: personaContent,
981
+ summary: "Agent Self-Model",
982
+ metadata: {
983
+ layer: "L3",
984
+ sourceScenes: highValueScenes.map((s) => s.id)
985
+ }
986
+ });
987
+ produced = 1;
988
+ this.lastL3At = Date.now();
989
+ } catch (e) {
990
+ errors.push(String(e));
991
+ }
992
+ return { stage: "L3", produced, errors };
993
+ }
994
+ buildPersona(scenes, l1Records) {
995
+ const sceneContents = scenes.map((scene2) => {
996
+ const relatedMemories = l1Records.filter((r) => r.sceneName === scene2.title);
997
+ return `### ${scene2.title}
998
+ ${relatedMemories.map((m) => `- ${m.content}`).join("\n")}`;
999
+ }).join("\n\n");
1000
+ const avgImportance = l1Records.reduce((sum, r) => sum + r.priority, 0) / l1Records.length / 100;
1001
+ return `# Agent Self-Model
1002
+
1003
+ ## Core Knowledge
1004
+ ${sceneContents}
1005
+
1006
+ ## Behavioral Patterns
1007
+ - \u5171 ${scenes.length} \u4E2A\u9AD8\u4EF7\u503C\u573A\u666F
1008
+ - \u5E73\u5747\u91CD\u8981\u6027: ${avgImportance.toFixed(2)}
1009
+
1010
+ ## Preferences
1011
+ (\u4ECE persona \u7C7B\u578B\u8BB0\u5FC6\u4E2D\u63D0\u53D6)
1012
+
1013
+ ## Communication Style
1014
+ (\u4ECE\u4EA4\u4E92\u6A21\u5F0F\u4E2D\u5B66\u4E60)
1015
+
1016
+ ---
1017
+ Generated: ${(/* @__PURE__ */ new Date()).toISOString()}
1018
+ `;
1019
+ }
1020
+ // ============================================================================
1021
+ // Utilities
1022
+ // ============================================================================
1023
+ fallbackExtract(messages, previousSceneName) {
1024
+ const contents = messages.map((m) => m.content).join("\n");
1025
+ const facts = [];
1026
+ const patterns = [
1027
+ /(?:decided|决定)(.+)/gi,
1028
+ /(?:learned|学习)(.+)/gi,
1029
+ /(?:remember|记住)(.+)/gi
1030
+ ];
1031
+ for (const pattern of patterns) {
1032
+ let match;
1033
+ while ((match = pattern.exec(contents)) !== null) {
1034
+ facts.push(match[1].trim());
1035
+ }
1036
+ }
1037
+ if (facts.length === 0 && messages[0]) {
1038
+ const content = messages[0].content;
1039
+ if (content.length > 20) {
1040
+ facts.push(content.slice(0, 100));
1041
+ }
1042
+ }
1043
+ const sceneName = previousSceneName || "General";
1044
+ return JSON.stringify([{
1045
+ scene_name: sceneName,
1046
+ memories: facts.slice(0, 3).map((content, i) => ({
1047
+ content,
1048
+ type: "episodic",
1049
+ priority: 50 + i * 10,
1050
+ source_message_ids: [messages[0]?.id || "unknown"],
1051
+ metadata: {}
1052
+ }))
1053
+ }]);
1054
+ }
1055
+ checkTriggers() {
1056
+ const now = Date.now();
1057
+ return {
1058
+ l1: this.messageBuffer.length >= this.config.l1.messageThreshold,
1059
+ l2: this.lastL2At ? now - this.lastL2At >= this.config.l2.minIntervalMs : this.messageBuffer.length >= this.config.l2.topicThreshold,
1060
+ l3: this.lastL3At === null
1061
+ };
1062
+ }
1063
+ async getStats() {
1064
+ const l1Records = await this.store.searchL1("", 1e3);
1065
+ const l2Scenes = await this.store.getSceneIndex();
1066
+ const persona = await this.store.getPersona();
1067
+ return {
1068
+ l0Count: this.messageBuffer.length,
1069
+ l1Count: l1Records.length,
1070
+ l2Count: l2Scenes.length,
1071
+ l3Count: persona ? 1 : 0
1072
+ };
1073
+ }
1074
+ };
1075
+ function shouldExtractL1(content) {
1076
+ if (content.length < 10) return false;
1077
+ if (content.length > 1e4) return false;
1078
+ return true;
1079
+ }
1080
+ function parseExtractionOutput(output) {
1081
+ try {
1082
+ const jsonMatch = output.match(/\[[\s\S]*\]/);
1083
+ if (!jsonMatch) {
1084
+ const objMatch = output.match(/\{[\s\S]*\}/);
1085
+ if (objMatch) {
1086
+ return JSON.parse(objMatch[0]);
1087
+ }
1088
+ return [];
1089
+ }
1090
+ const parsed = JSON.parse(jsonMatch[0]);
1091
+ if (parsed.scene_name) {
1092
+ return (parsed.memories || []).map((m) => ({
1093
+ content: m.content,
1094
+ type: normalizeMemoryType(m.type),
1095
+ priority: Math.min(100, Math.max(0, Number(m.priority) || 50)),
1096
+ source_message_ids: Array.isArray(m.source_message_ids) ? m.source_message_ids : [],
1097
+ metadata: m.metadata || {},
1098
+ scene_name: parsed.scene_name
1099
+ }));
1100
+ }
1101
+ const memories = [];
1102
+ for (const scene2 of parsed) {
1103
+ for (const m of scene2.memories || []) {
1104
+ memories.push({
1105
+ content: m.content,
1106
+ type: normalizeMemoryType(m.type),
1107
+ priority: Math.min(100, Math.max(0, Number(m.priority) || 50)),
1108
+ source_message_ids: Array.isArray(m.source_message_ids) ? m.source_message_ids : [],
1109
+ metadata: m.metadata || {},
1110
+ scene_name: scene2.scene_name
1111
+ });
1112
+ }
1113
+ }
1114
+ return memories;
1115
+ } catch (e) {
1116
+ console.error("Failed to parse LLM output:", e);
1117
+ return [];
1118
+ }
1119
+ }
1120
+ function normalizeMemoryType(type) {
1121
+ const t = type?.toLowerCase();
1122
+ if (t === "persona" || t === "episodic" || t === "instruction") {
1123
+ return t;
1124
+ }
1125
+ return "episodic";
1126
+ }
1127
+
1128
+ // index.ts
1129
+ var DEFAULT_CONFIG = {
1130
+ enabled: true,
1131
+ layersEnabled: { L0: true, L1: true, L2: true, L3: false },
1132
+ decay: {
1133
+ ttl: { enabled: true, retentionDays: 30, safetyThreshold: 0.8, minRetainL0: 50, minRetainL1: 20 },
1134
+ importance: { enabled: true, baseHalflifeDays: 50 },
1135
+ accessFrequency: { enabled: true },
1136
+ stateMachine: { enabled: true }
1137
+ },
1138
+ teamMemory: {
1139
+ enabled: true,
1140
+ maxImportedAgents: 2,
1141
+ visibilityGate: true
1142
+ },
1143
+ retrieval: {
1144
+ hybridSearch: true,
1145
+ semanticWeight: 0.5,
1146
+ bm25Weight: 0.25,
1147
+ entityBoostWeight: 0.25,
1148
+ topK: 10,
1149
+ overFetch: 4
1150
+ },
1151
+ storage: {
1152
+ backend: "memory",
1153
+ dataDir: "~/.openclaw/memory-new"
1154
+ },
1155
+ llm: {
1156
+ enabled: false,
1157
+ // Disabled by default, use fallback extraction
1158
+ model: "default"
1159
+ }
1160
+ };
1161
+ function deriveFreshness(engram, config) {
1162
+ if (!config.decay.importance.enabled) return "fresh";
1163
+ const ageDays = (Date.now() - engram.lastEffectiveAt) / (1e3 * 60 * 60 * 24);
1164
+ const halflife = config.decay.importance.baseHalflifeDays * Math.pow(engram.importance + 0.1, 1.5);
1165
+ if (ageDays <= halflife) return "fresh";
1166
+ if (ageDays <= halflife * 2) return "aging";
1167
+ if (ageDays <= halflife * 4) return "stale";
1168
+ return "forgotten";
1169
+ }
1170
+ function deriveHotness(retrievalCount, ageDays) {
1171
+ return 1 / (1 + Math.exp(-Math.log(1 + retrievalCount))) * Math.exp(-Math.LN2 * ageDays / 7);
1172
+ }
1173
+ function calculateScore(relevance, recency, importance, strength, hotness, weights = { relevance: 0.5, recency: 0.15, importance: 0.25, strength: 0.05, hotness: 0.05 }) {
1174
+ return weights.relevance * relevance + weights.recency * recency + weights.importance * importance + weights.strength * strength + weights.hotness * hotness;
1175
+ }
1176
+ function validateVisibilityTransition(from, to, config) {
1177
+ if (!config.teamMemory.visibilityGate) return true;
1178
+ if (from === to) return true;
1179
+ if (to === "private" && from !== "private") return false;
1180
+ if (from === "private") return true;
1181
+ return true;
1182
+ }
3
1183
  var index_default = definePluginEntry({
4
1184
  id: "memory_new",
5
1185
  name: "Memory New",
6
- description: "A new memory extension for OpenClaw.",
1186
+ description: "Multi-layered memory system with L0\u2192L1\u2192L2\u2192L3 distillation, persistent storage, and recall",
7
1187
  register(api) {
8
- const config = api.pluginConfig ?? { enabled: true };
1188
+ const config = api.pluginConfig ?? DEFAULT_CONFIG;
1189
+ const storage = new StorageAdapter(config.storage.dataDir);
1190
+ const store = new MemoryStore(config.storage.dataDir);
1191
+ const recall = new RecallEngine(storage);
1192
+ const vectorStore = new VectorStore();
1193
+ const pipeline = new DistillationPipeline(DEFAULT_DISTILLATION_CONFIG, store, vectorStore);
1194
+ store.setVectorStore(vectorStore);
1195
+ if (config.llm?.enabled) {
1196
+ api.registerEmbeddingProvider({
1197
+ id: "memory-new-embedder",
1198
+ defaultModel: config.llm.model ?? "default",
1199
+ transport: "local",
1200
+ create: async (options) => {
1201
+ const provider = {
1202
+ id: "memory-new-embedder",
1203
+ model: options.model,
1204
+ dimensions: 384,
1205
+ maxInputTokens: 8192,
1206
+ embed: async (input) => {
1207
+ const text = typeof input === "string" ? input : input.text;
1208
+ return vectorStore.generatePseudoEmbedding(text);
1209
+ },
1210
+ embedBatch: async (inputs) => {
1211
+ return inputs.map((input) => {
1212
+ const text = typeof input === "string" ? input : input.text;
1213
+ return vectorStore.generatePseudoEmbedding(text);
1214
+ });
1215
+ }
1216
+ };
1217
+ return { provider, runtime: { id: "memory-new-embedder" } };
1218
+ }
1219
+ });
1220
+ pipeline.setSubagentRunner(async (prompt) => {
1221
+ try {
1222
+ const result = await api.runtime.subagent.run({
1223
+ sessionKey: `memory-${Date.now()}`,
1224
+ message: prompt,
1225
+ model: config.llm?.model,
1226
+ disableTools: true
1227
+ });
1228
+ const waitResult = await api.runtime.subagent.waitForRun({ runId: result.runId, timeoutMs: 3e4 });
1229
+ const messages = await api.runtime.subagent.getSessionMessages({ sessionKey: result.sessionKey });
1230
+ const assistantMsg = messages.messages.find((m) => m.role === "assistant");
1231
+ return assistantMsg?.content?.[0]?.text ?? "";
1232
+ } catch (error) {
1233
+ api.logger.debug?.(`Subagent extraction failed: ${error}`);
1234
+ throw error;
1235
+ }
1236
+ });
1237
+ }
1238
+ const sessionMessages = /* @__PURE__ */ new Map();
9
1239
  api.registerCommand({
10
- name: "memory_new",
11
- description: "Interact with Memory New extension.",
1240
+ name: "memory",
1241
+ description: "Interact with memory system",
12
1242
  acceptsArgs: true,
13
1243
  exposeSenderIsOwner: true,
14
1244
  handler: async (ctx) => {
15
- const tokens = ctx.args?.trim().split(/\s+/).filter(Boolean) ?? [];
16
- const action = (tokens[0] ?? "status").toLowerCase();
17
- if (action === "status") {
18
- return {
19
- text: `Memory New: ${config.enabled ? "enabled" : "disabled"}`
20
- };
1245
+ const args = ctx.args ?? "";
1246
+ const [action, ...rest] = args.trim().split(/\s+/);
1247
+ switch (action) {
1248
+ case "add": {
1249
+ const content = rest.join(" ");
1250
+ if (!content) return { text: "Usage: memory add <content>" };
1251
+ const now = (/* @__PURE__ */ new Date()).toISOString();
1252
+ await store.storeL1({
1253
+ content,
1254
+ type: "episodic",
1255
+ priority: 50,
1256
+ sceneName: "User Added",
1257
+ sourceMessageIds: [],
1258
+ metadata: { source: "command" },
1259
+ timestamps: [now],
1260
+ sessionKey: ctx.sessionKey ?? "default",
1261
+ sessionId: ctx.sessionKey ?? "default",
1262
+ userId: ctx.senderIsOwner ? "owner" : "user",
1263
+ agentId: "self"
1264
+ });
1265
+ return { text: `Added: ${content.slice(0, 50)}...` };
1266
+ }
1267
+ case "search": {
1268
+ const query = rest.join(" ");
1269
+ if (!query) return { text: "Usage: memory search <query>" };
1270
+ const result = await recall.recall({
1271
+ query,
1272
+ sessionKey: ctx.sessionKey ?? "default",
1273
+ userId: ctx.senderIsOwner ? "owner" : "user",
1274
+ agentId: "self",
1275
+ topK: config.retrieval.topK
1276
+ });
1277
+ if (!result.prependContext && !result.appendSystemContext) {
1278
+ return { text: "No relevant memories found." };
1279
+ }
1280
+ return {
1281
+ text: `Found memories:
1282
+
1283
+ ${result.prependContext ?? ""}
1284
+
1285
+ ${result.appendSystemContext ?? ""}`
1286
+ };
1287
+ }
1288
+ case "list": {
1289
+ const records = await store.searchL1("", 20);
1290
+ if (records.length === 0) return { text: "No memories stored." };
1291
+ const lines = records.map(
1292
+ (r) => `[${r.type}] ${r.content.slice(0, 60)}${r.content.length > 60 ? "..." : ""}`
1293
+ );
1294
+ return { text: `Recent ${records.length} memories:
1295
+
1296
+ ${lines.join("\n")}` };
1297
+ }
1298
+ case "reinforce": {
1299
+ return { text: "Reinforce is not yet supported with persistent storage." };
1300
+ }
1301
+ case "decay": {
1302
+ return { text: "Decay is not yet fully implemented." };
1303
+ }
1304
+ case "stats": {
1305
+ const stats = await pipeline.getStats();
1306
+ return {
1307
+ text: `Memory Stats:
1308
+ L0 (buffered): ${stats.l0Count}
1309
+ L1 (stored): ${stats.l1Count}
1310
+ L2 (scenes): ${stats.l2Count}
1311
+ L3 (persona): ${stats.l3Count}`
1312
+ };
1313
+ }
1314
+ case "config": {
1315
+ return { text: JSON.stringify(config, null, 2) };
1316
+ }
1317
+ default:
1318
+ return {
1319
+ text: `Memory New commands:
1320
+ memory add <content> - Add a memory
1321
+ memory search <query> - Search memories
1322
+ memory list - List recent memories
1323
+ memory stats - Show memory statistics
1324
+ memory config - Show configuration
1325
+
1326
+ Memory layers: L0 (raw) \u2192 L1 (atomic) \u2192 L2 (scene) \u2192 L3 (persona)`
1327
+ };
21
1328
  }
22
- if (action === "help") {
23
- return {
24
- text: `Memory New Commands:
25
- status - Show current status
26
- help - Show this help`
27
- };
1329
+ }
1330
+ });
1331
+ api.registerCommand({
1332
+ name: "team-memory",
1333
+ description: "Team memory operations",
1334
+ acceptsArgs: true,
1335
+ exposeSenderIsOwner: true,
1336
+ handler: async (ctx) => {
1337
+ if (!config.teamMemory.enabled) {
1338
+ return { text: "Team memory is disabled." };
1339
+ }
1340
+ const args = ctx.args ?? "";
1341
+ const [action, ...rest] = args.trim().split(/\s+/);
1342
+ switch (action) {
1343
+ case "share": {
1344
+ return { text: "Share is not yet implemented." };
1345
+ }
1346
+ case "import": {
1347
+ return { text: "Import is not yet implemented." };
1348
+ }
1349
+ case "status": {
1350
+ const stats = await pipeline.getStats();
1351
+ return {
1352
+ text: `Team memory status:
1353
+ Enabled: ${config.teamMemory.enabled}
1354
+ Max imported agents: ${config.teamMemory.maxImportedAgents}
1355
+ Visibility gate: ${config.teamMemory.visibilityGate}
1356
+ Total L1 memories: ${stats.l1Count}
1357
+ Total L2 scenes: ${stats.l2Count}`
1358
+ };
1359
+ }
1360
+ default:
1361
+ return {
1362
+ text: `Team memory commands:
1363
+ team-memory status - Show team memory status`
1364
+ };
28
1365
  }
29
- return {
30
- text: `Unknown action: ${action}
31
-
32
- Type "memory_new help" for available commands.`
33
- };
34
1366
  }
35
1367
  });
36
1368
  api.on("before_prompt_build", async (event, ctx) => {
37
- if (!config.enabled) {
1369
+ if (!config.enabled) return void 0;
1370
+ const sessionKey = ctx.sessionKey ?? "default";
1371
+ const userId = "user";
1372
+ try {
1373
+ const query = event.prompt?.slice(0, 200) ?? "";
1374
+ const recallResult = await recall.recall({
1375
+ query,
1376
+ sessionKey,
1377
+ userId,
1378
+ agentId: "self",
1379
+ topK: config.retrieval.topK,
1380
+ vectorStore: config.retrieval.hybridSearch ? vectorStore : void 0
1381
+ });
1382
+ if (!recallResult.prependContext && !recallResult.appendSystemContext) {
1383
+ return void 0;
1384
+ }
1385
+ return {
1386
+ prependContext: recallResult.prependContext,
1387
+ appendContext: recallResult.appendSystemContext
1388
+ };
1389
+ } catch (error) {
1390
+ api.logger.debug?.(`Memory recall failed: ${error}`);
38
1391
  return void 0;
39
1392
  }
40
- api.logger.info?.("memory_new: before_prompt_build triggered");
41
- return {
42
- prependContext: "[Memory New Plugin] Memory New plugin is active."
43
- };
1393
+ });
1394
+ api.on("after_prompt_build", async (event, ctx) => {
1395
+ if (!config.enabled || !config.layersEnabled.L0) return;
1396
+ const sessionKey = ctx.sessionKey ?? "default";
1397
+ try {
1398
+ const messages = event.messages ?? [];
1399
+ if (messages.length > 0) {
1400
+ for (const msg of messages) {
1401
+ const l0Msg = {
1402
+ role: msg.role === "user" ? "user" : "assistant",
1403
+ content: msg.content?.slice(0, 1e4) ?? "",
1404
+ // Limit length
1405
+ timestamp: msg.timestamp ?? Date.now(),
1406
+ sessionKey,
1407
+ sessionId: sessionKey,
1408
+ userId: "user",
1409
+ agentId: "self"
1410
+ };
1411
+ await store.ingestMessage(l0Msg);
1412
+ }
1413
+ if (config.layersEnabled.L1 && config.llm?.enabled) {
1414
+ await pipeline.distill("L1");
1415
+ }
1416
+ }
1417
+ } catch (error) {
1418
+ api.logger.debug?.(`Memory capture failed: ${error}`);
1419
+ }
44
1420
  });
45
1421
  api.on("agent_end", (event, ctx) => {
1422
+ if (!config.enabled) return;
46
1423
  const runId = event.runId ?? ctx.runId;
47
- api.logger.debug?.(`memory_new: agent_end for run ${runId}`);
1424
+ api.logger.debug?.(`Memory: agent ended for run ${runId}`);
1425
+ });
1426
+ api.on("session_end", async (event, ctx) => {
1427
+ if (!config.enabled) return;
1428
+ const sessionKey = ctx.sessionKey ?? "default";
1429
+ try {
1430
+ if (config.layersEnabled.L2) {
1431
+ await pipeline.distill("L2");
1432
+ }
1433
+ if (config.layersEnabled.L3) {
1434
+ await pipeline.distill("L3");
1435
+ }
1436
+ sessionMessages.delete(sessionKey);
1437
+ api.logger.debug?.(`Memory: session ended for ${sessionKey}`);
1438
+ } catch (error) {
1439
+ api.logger.debug?.(`Memory session cleanup failed: ${error}`);
1440
+ }
1441
+ });
1442
+ api.registerTool({
1443
+ name: "memory_search",
1444
+ description: "Search memory store using hybrid retrieval",
1445
+ parameters: {
1446
+ type: "object",
1447
+ properties: {
1448
+ query: { type: "string", description: "Search query" },
1449
+ limit: { type: "number", description: "Max results", default: 10 },
1450
+ visibility: {
1451
+ type: "string",
1452
+ enum: ["all", "public", "team", "private"],
1453
+ default: "all"
1454
+ }
1455
+ },
1456
+ required: ["query"]
1457
+ },
1458
+ execute: async (params, ctx) => {
1459
+ const result = await recall.recall({
1460
+ query: params.query,
1461
+ sessionKey: ctx.sessionKey ?? "default",
1462
+ userId: "user",
1463
+ agentId: "self",
1464
+ topK: params.limit ?? 10
1465
+ });
1466
+ return {
1467
+ memories: result.recalledL1Memories ?? [],
1468
+ prependContext: result.prependContext,
1469
+ appendContext: result.appendSystemContext
1470
+ };
1471
+ }
1472
+ });
1473
+ api.registerTool({
1474
+ name: "memory_store",
1475
+ description: "Store a new memory engram",
1476
+ parameters: {
1477
+ type: "object",
1478
+ properties: {
1479
+ content: { type: "string", description: "Memory content" },
1480
+ type: {
1481
+ type: "string",
1482
+ enum: ["persona", "episodic", "instruction"],
1483
+ default: "episodic",
1484
+ description: "Memory type"
1485
+ },
1486
+ priority: {
1487
+ type: "number",
1488
+ default: 50,
1489
+ description: "Priority 0-100"
1490
+ },
1491
+ sceneName: {
1492
+ type: "string",
1493
+ default: "General",
1494
+ description: "Scene name"
1495
+ }
1496
+ },
1497
+ required: ["content"]
1498
+ },
1499
+ execute: async (params, ctx) => {
1500
+ const record = await store.storeL1({
1501
+ content: params.content,
1502
+ type: params.type ?? "episodic",
1503
+ priority: params.priority ?? 50,
1504
+ sceneName: params.sceneName ?? "General",
1505
+ sourceMessageIds: [],
1506
+ metadata: { source: "tool" },
1507
+ timestamps: [(/* @__PURE__ */ new Date()).toISOString()],
1508
+ sessionKey: ctx.sessionKey ?? "default",
1509
+ sessionId: ctx.sessionKey ?? "default",
1510
+ userId: "user",
1511
+ agentId: "self"
1512
+ });
1513
+ return { engramId: record.id, stored: true };
1514
+ }
1515
+ });
1516
+ api.registerTool({
1517
+ name: "memory_get",
1518
+ description: "Get a specific memory by ID",
1519
+ parameters: {
1520
+ type: "object",
1521
+ properties: {
1522
+ layer: {
1523
+ type: "string",
1524
+ enum: ["L0", "L1", "L2", "L3"],
1525
+ default: "L1",
1526
+ description: "Memory layer"
1527
+ },
1528
+ id: { type: "string", description: "Memory ID (for L2/L3)" }
1529
+ },
1530
+ required: []
1531
+ },
1532
+ execute: async (params, ctx) => {
1533
+ if (params.layer === "L2" && params.id) {
1534
+ const scene2 = await store.getScene(params.id);
1535
+ return scene2 ?? { error: "Scene not found" };
1536
+ }
1537
+ if (params.layer === "L3") {
1538
+ const persona = await store.getPersona();
1539
+ return persona ?? { error: "Persona not found" };
1540
+ }
1541
+ const records = await store.searchL1("", 10);
1542
+ return { records };
1543
+ }
1544
+ });
1545
+ api.registerTool({
1546
+ name: "memory_distill",
1547
+ description: "Trigger memory distillation manually",
1548
+ parameters: {
1549
+ type: "object",
1550
+ properties: {
1551
+ layer: {
1552
+ type: "string",
1553
+ enum: ["L1", "L2", "L3"],
1554
+ default: "L1",
1555
+ description: "Layer to distill"
1556
+ }
1557
+ },
1558
+ required: ["layer"]
1559
+ },
1560
+ execute: async (params, ctx) => {
1561
+ const layer = params.layer;
1562
+ const result = await pipeline.distill(layer);
1563
+ return {
1564
+ layer: result.stage,
1565
+ produced: result.produced,
1566
+ errors: result.errors
1567
+ };
1568
+ }
1569
+ });
1570
+ api.registerHttpRoute({
1571
+ method: "GET",
1572
+ path: "/memory/team/status",
1573
+ handler: async (req, ctx) => {
1574
+ if (!config.teamMemory.enabled) {
1575
+ return { status: 403, body: { error: "Team memory disabled" } };
1576
+ }
1577
+ const stats = await pipeline.getStats();
1578
+ return {
1579
+ status: 200,
1580
+ body: {
1581
+ enabled: true,
1582
+ maxImportedAgents: config.teamMemory.maxImportedAgents,
1583
+ l1Count: stats.l1Count,
1584
+ l2Count: stats.l2Count
1585
+ }
1586
+ };
1587
+ }
1588
+ });
1589
+ api.registerHttpRoute({
1590
+ method: "POST",
1591
+ path: "/memory/team/share",
1592
+ handler: async (req, ctx) => {
1593
+ if (!config.teamMemory.enabled) {
1594
+ return { status: 403, body: { error: "Team memory disabled" } };
1595
+ }
1596
+ return { status: 501, body: { error: "Not implemented" } };
1597
+ }
1598
+ });
1599
+ api.registerHttpRoute({
1600
+ method: "POST",
1601
+ path: "/memory/team/import",
1602
+ handler: async (req, ctx) => {
1603
+ if (!config.teamMemory.enabled) {
1604
+ return { status: 403, body: { error: "Team memory disabled" } };
1605
+ }
1606
+ return { status: 501, body: { error: "Not implemented" } };
1607
+ }
1608
+ });
1609
+ api.logger.info?.("Memory New plugin registered with storage and pipeline");
1610
+ api.registerConfigMigration?.({
1611
+ id: "memory-new-default-config",
1612
+ migrate: (existingConfig) => {
1613
+ const current = existingConfig?.memory_new;
1614
+ if (!current) {
1615
+ return {
1616
+ memory_new: {
1617
+ enabled: true,
1618
+ layersEnabled: { L0: true, L1: true, L2: true, L3: false },
1619
+ llm: { enabled: false, model: "default" },
1620
+ retrieval: {
1621
+ hybridSearch: true,
1622
+ semanticWeight: 0.5,
1623
+ bm25Weight: 0.25,
1624
+ entityBoostWeight: 0.25,
1625
+ topK: 10,
1626
+ overFetch: 4
1627
+ },
1628
+ storage: { backend: "memory", dataDir: "~/.openclaw/memory-new" },
1629
+ decay: {
1630
+ ttl: { enabled: true, retentionDays: 30, safetyThreshold: 0.8, minRetainL0: 50, minRetainL1: 20 },
1631
+ importance: { enabled: true, baseHalflifeDays: 50 },
1632
+ accessFrequency: { enabled: true },
1633
+ stateMachine: { enabled: true }
1634
+ },
1635
+ teamMemory: { enabled: true, maxImportedAgents: 2, visibilityGate: true }
1636
+ }
1637
+ };
1638
+ }
1639
+ return {};
1640
+ }
1641
+ });
1642
+ api.registerAutoEnableProbe?.({
1643
+ id: "memory-new",
1644
+ check: async (config2) => {
1645
+ const hasConfig = config2?.plugins?.memory_new?.enabled === true;
1646
+ const hasStorageDir = false;
1647
+ return hasConfig || hasStorageDir;
1648
+ }
48
1649
  });
49
1650
  }
50
1651
  });
51
- var testing = {};
1652
+ var testing = {
1653
+ deriveFreshness,
1654
+ deriveHotness,
1655
+ calculateScore,
1656
+ validateVisibilityTransition
1657
+ };
52
1658
  export {
53
1659
  index_default as default,
54
1660
  testing