@mnemoai/core 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. package/index.ts +3395 -0
  2. package/openclaw.plugin.json +815 -0
  3. package/package.json +59 -0
  4. package/src/access-tracker.ts +341 -0
  5. package/src/adapters/README.md +78 -0
  6. package/src/adapters/chroma.ts +206 -0
  7. package/src/adapters/lancedb.ts +237 -0
  8. package/src/adapters/pgvector.ts +218 -0
  9. package/src/adapters/qdrant.ts +191 -0
  10. package/src/adaptive-retrieval.ts +90 -0
  11. package/src/audit-log.ts +238 -0
  12. package/src/chunker.ts +254 -0
  13. package/src/config.ts +271 -0
  14. package/src/decay-engine.ts +238 -0
  15. package/src/embedder.ts +735 -0
  16. package/src/extraction-prompts.ts +339 -0
  17. package/src/license.ts +258 -0
  18. package/src/llm-client.ts +125 -0
  19. package/src/mcp-server.ts +415 -0
  20. package/src/memory-categories.ts +71 -0
  21. package/src/memory-upgrader.ts +388 -0
  22. package/src/migrate.ts +364 -0
  23. package/src/mnemo.ts +142 -0
  24. package/src/noise-filter.ts +97 -0
  25. package/src/noise-prototypes.ts +164 -0
  26. package/src/observability.ts +81 -0
  27. package/src/query-tracker.ts +57 -0
  28. package/src/reflection-event-store.ts +98 -0
  29. package/src/reflection-item-store.ts +112 -0
  30. package/src/reflection-mapped-metadata.ts +84 -0
  31. package/src/reflection-metadata.ts +23 -0
  32. package/src/reflection-ranking.ts +33 -0
  33. package/src/reflection-retry.ts +181 -0
  34. package/src/reflection-slices.ts +265 -0
  35. package/src/reflection-store.ts +602 -0
  36. package/src/resonance-state.ts +85 -0
  37. package/src/retriever.ts +1510 -0
  38. package/src/scopes.ts +375 -0
  39. package/src/self-improvement-files.ts +143 -0
  40. package/src/semantic-gate.ts +121 -0
  41. package/src/session-recovery.ts +138 -0
  42. package/src/smart-extractor.ts +923 -0
  43. package/src/smart-metadata.ts +561 -0
  44. package/src/storage-adapter.ts +153 -0
  45. package/src/store.ts +1330 -0
  46. package/src/tier-manager.ts +189 -0
  47. package/src/tools.ts +1292 -0
  48. package/src/wal-recovery.ts +172 -0
  49. package/test/core.test.mjs +301 -0
package/src/migrate.ts ADDED
@@ -0,0 +1,364 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * Migration Utilities
4
+ * Migrates data from old memory-lancedb plugin to memory-lancedb-pro
5
+ */
6
+
7
+ import { homedir } from "node:os";
8
+ import { join } from "node:path";
9
+ import fs from "node:fs/promises";
10
+ import type { MemoryStore, MemoryEntry } from "./store.js";
11
+ import { loadLanceDB } from "./store.js";
12
+
13
+ // ============================================================================
14
+ // Types
15
+ // ============================================================================
16
+
17
+ interface LegacyMemoryEntry {
18
+ id: string;
19
+ text: string;
20
+ vector: number[];
21
+ importance: number;
22
+ category: "preference" | "fact" | "decision" | "entity" | "other";
23
+ createdAt: number;
24
+ scope?: string;
25
+ }
26
+
27
+ interface MigrationResult {
28
+ success: boolean;
29
+ migratedCount: number;
30
+ skippedCount: number;
31
+ errors: string[];
32
+ summary: string;
33
+ }
34
+
35
+ interface MigrationOptions {
36
+ sourceDbPath?: string;
37
+ dryRun?: boolean;
38
+ defaultScope?: string;
39
+ skipExisting?: boolean;
40
+ }
41
+
42
+ function normalizeLegacyVector(value: unknown): number[] {
43
+ if (Array.isArray(value)) {
44
+ return value.map((n) => Number(n));
45
+ }
46
+
47
+ if (
48
+ value &&
49
+ typeof value === "object" &&
50
+ Symbol.iterator in (value as Record<PropertyKey, unknown>)
51
+ ) {
52
+ return Array.from(value as Iterable<unknown>, (n) => Number(n));
53
+ }
54
+
55
+ return [];
56
+ }
57
+
58
+ // ============================================================================
59
+ // Default Paths
60
+ // ============================================================================
61
+
62
+ function getDefaultLegacyPaths(): string[] {
63
+ const home = homedir();
64
+ return [
65
+ join(home, ".openclaw", "memory", "lancedb"),
66
+ join(home, ".claude", "memory", "lancedb"),
67
+ // Add more legacy paths as needed
68
+ ];
69
+ }
70
+
71
+ // ============================================================================
72
+ // Migration Functions
73
+ // ============================================================================
74
+
75
+ export class MemoryMigrator {
76
+ constructor(private targetStore: MemoryStore) {}
77
+
78
+ async migrate(options: MigrationOptions = {}): Promise<MigrationResult> {
79
+ const result: MigrationResult = {
80
+ success: false,
81
+ migratedCount: 0,
82
+ skippedCount: 0,
83
+ errors: [],
84
+ summary: "",
85
+ };
86
+
87
+ try {
88
+ // Find source database
89
+ const sourceDbPath = await this.findSourceDatabase(options.sourceDbPath);
90
+ if (!sourceDbPath) {
91
+ result.errors.push("No legacy database found to migrate from");
92
+ result.summary = "Migration failed: No source database found";
93
+ return result;
94
+ }
95
+
96
+ console.log(`Migrating from: ${sourceDbPath}`);
97
+
98
+ // Load legacy data
99
+ const legacyEntries = await this.loadLegacyData(sourceDbPath);
100
+ if (legacyEntries.length === 0) {
101
+ result.summary = "Migration completed: No data to migrate";
102
+ result.success = true;
103
+ return result;
104
+ }
105
+
106
+ console.log(`Found ${legacyEntries.length} entries to migrate`);
107
+
108
+ // Migrate entries
109
+ if (!options.dryRun) {
110
+ const migrationStats = await this.migrateEntries(legacyEntries, options);
111
+ result.migratedCount = migrationStats.migrated;
112
+ result.skippedCount = migrationStats.skipped;
113
+ result.errors.push(...migrationStats.errors);
114
+ } else {
115
+ result.summary = `Dry run: Would migrate ${legacyEntries.length} entries`;
116
+ result.success = true;
117
+ return result;
118
+ }
119
+
120
+ result.success = result.errors.length === 0;
121
+ result.summary = `Migration ${result.success ? 'completed' : 'completed with errors'}: ` +
122
+ `${result.migratedCount} migrated, ${result.skippedCount} skipped`;
123
+
124
+ } catch (error) {
125
+ result.errors.push(`Migration failed: ${error instanceof Error ? error.message : String(error)}`);
126
+ result.summary = "Migration failed due to unexpected error";
127
+ }
128
+
129
+ return result;
130
+ }
131
+
132
+ private async findSourceDatabase(explicitPath?: string): Promise<string | null> {
133
+ if (explicitPath) {
134
+ try {
135
+ await fs.access(explicitPath);
136
+ return explicitPath;
137
+ } catch {
138
+ return null;
139
+ }
140
+ }
141
+
142
+ // Check default legacy paths
143
+ for (const path of getDefaultLegacyPaths()) {
144
+ try {
145
+ await fs.access(path);
146
+ const files = await fs.readdir(path);
147
+ // Check for LanceDB files
148
+ if (files.some(f => f.endsWith('.lance') || f === 'memories.lance')) {
149
+ return path;
150
+ }
151
+ } catch {
152
+ continue;
153
+ }
154
+ }
155
+
156
+ return null;
157
+ }
158
+
159
+ private async loadLegacyData(sourceDbPath: string, limit?: number): Promise<LegacyMemoryEntry[]> {
160
+ const lancedb = await loadLanceDB();
161
+ const db = await lancedb.connect(sourceDbPath);
162
+
163
+ try {
164
+ const table = await db.openTable("memories");
165
+ let query = table.query();
166
+ if (limit) query = query.limit(limit);
167
+ const entries = await query.toArray();
168
+
169
+ return entries.map((row): LegacyMemoryEntry => ({
170
+ id: row.id as string,
171
+ text: row.text as string,
172
+ vector: normalizeLegacyVector(row.vector),
173
+ importance: Number(row.importance),
174
+ category: (row.category as LegacyMemoryEntry["category"]) || "other",
175
+ createdAt: Number(row.createdAt),
176
+ scope: row.scope as string | undefined,
177
+ }));
178
+ } catch (error) {
179
+ console.warn(`Failed to load legacy data: ${error}`);
180
+ return [];
181
+ }
182
+ }
183
+
184
+ private async migrateEntries(
185
+ legacyEntries: LegacyMemoryEntry[],
186
+ options: MigrationOptions
187
+ ): Promise<{ migrated: number; skipped: number; errors: string[] }> {
188
+ let migrated = 0;
189
+ let skipped = 0;
190
+ const errors: string[] = [];
191
+
192
+ const defaultScope = options.defaultScope || "global";
193
+
194
+ for (const legacy of legacyEntries) {
195
+ try {
196
+ // Check if entry already exists (if skipExisting is enabled)
197
+ if (options.skipExisting) {
198
+ if (legacy.id && (await this.targetStore.hasId(legacy.id))) {
199
+ skipped++;
200
+ continue;
201
+ }
202
+
203
+ const existing = await this.targetStore.vectorSearch(
204
+ legacy.vector, 1, 0.9, [legacy.scope || defaultScope]
205
+ );
206
+ if (existing.length > 0 && existing[0].score > 0.95) {
207
+ skipped++;
208
+ continue;
209
+ }
210
+ }
211
+
212
+ // Convert legacy entry to new format while preserving legacy identity.
213
+ const newEntry: MemoryEntry = {
214
+ id: legacy.id,
215
+ text: legacy.text,
216
+ vector: legacy.vector,
217
+ category: legacy.category,
218
+ scope: legacy.scope || defaultScope,
219
+ importance: legacy.importance,
220
+ timestamp: Number.isFinite(legacy.createdAt) ? legacy.createdAt : Date.now(),
221
+ metadata: JSON.stringify({
222
+ migratedFrom: "memory-lancedb",
223
+ originalId: legacy.id,
224
+ originalCreatedAt: legacy.createdAt,
225
+ }),
226
+ };
227
+
228
+ await this.targetStore.importEntry(newEntry);
229
+ migrated++;
230
+
231
+ if (migrated % 100 === 0) {
232
+ console.log(`Migrated ${migrated}/${legacyEntries.length} entries...`);
233
+ }
234
+
235
+ } catch (error) {
236
+ errors.push(`Failed to migrate entry ${legacy.id}: ${error}`);
237
+ skipped++;
238
+ }
239
+ }
240
+
241
+ return { migrated, skipped, errors };
242
+ }
243
+
244
+ async checkMigrationNeeded(sourceDbPath?: string): Promise<{
245
+ needed: boolean;
246
+ sourceFound: boolean;
247
+ sourceDbPath?: string;
248
+ entryCount?: number;
249
+ }> {
250
+ const sourcePath = await this.findSourceDatabase(sourceDbPath);
251
+
252
+ if (!sourcePath) {
253
+ return {
254
+ needed: false,
255
+ sourceFound: false,
256
+ };
257
+ }
258
+
259
+ try {
260
+ const entries = await this.loadLegacyData(sourcePath, 1);
261
+ return {
262
+ needed: entries.length > 0,
263
+ sourceFound: true,
264
+ sourceDbPath: sourcePath,
265
+ entryCount: entries.length > 0 ? undefined : 0,
266
+ };
267
+ } catch (error) {
268
+ return {
269
+ needed: false,
270
+ sourceFound: true,
271
+ sourceDbPath: sourcePath,
272
+ };
273
+ }
274
+ }
275
+
276
+ async verifyMigration(sourceDbPath?: string): Promise<{
277
+ valid: boolean;
278
+ sourceCount: number;
279
+ targetCount: number;
280
+ issues: string[];
281
+ }> {
282
+ const issues: string[] = [];
283
+
284
+ try {
285
+ const sourcePath = await this.findSourceDatabase(sourceDbPath);
286
+ if (!sourcePath) {
287
+ return {
288
+ valid: false,
289
+ sourceCount: 0,
290
+ targetCount: 0,
291
+ issues: ["Source database not found"],
292
+ };
293
+ }
294
+
295
+ const sourceEntries = await this.loadLegacyData(sourcePath);
296
+ const targetStats = await this.targetStore.stats();
297
+
298
+ const sourceCount = sourceEntries.length;
299
+ const targetCount = targetStats.totalCount;
300
+
301
+ if (targetCount < sourceCount) {
302
+ issues.push(`Target has fewer entries (${targetCount}) than source (${sourceCount})`);
303
+ }
304
+
305
+ return {
306
+ valid: issues.length === 0,
307
+ sourceCount,
308
+ targetCount,
309
+ issues,
310
+ };
311
+
312
+ } catch (error) {
313
+ return {
314
+ valid: false,
315
+ sourceCount: 0,
316
+ targetCount: 0,
317
+ issues: [`Verification failed: ${error}`],
318
+ };
319
+ }
320
+ }
321
+ }
322
+
323
+ export function createMigrator(targetStore: MemoryStore): MemoryMigrator {
324
+ return new MemoryMigrator(targetStore);
325
+ }
326
+
327
+ export async function migrateFromLegacy(
328
+ targetStore: MemoryStore,
329
+ options: MigrationOptions = {}
330
+ ): Promise<MigrationResult> {
331
+ const migrator = createMigrator(targetStore);
332
+ return migrator.migrate(options);
333
+ }
334
+
335
+ export async function checkForLegacyData(): Promise<{
336
+ found: boolean;
337
+ paths: string[];
338
+ totalEntries: number;
339
+ }> {
340
+ const paths: string[] = [];
341
+ let totalEntries = 0;
342
+
343
+ for (const path of getDefaultLegacyPaths()) {
344
+ try {
345
+ const lancedb = await loadLanceDB();
346
+ const db = await lancedb.connect(path);
347
+ const table = await db.openTable("memories");
348
+ const entries = await table.query().select(["id"]).toArray();
349
+
350
+ if (entries.length > 0) {
351
+ paths.push(path);
352
+ totalEntries += entries.length;
353
+ }
354
+ } catch {
355
+ continue;
356
+ }
357
+ }
358
+
359
+ return {
360
+ found: paths.length > 0,
361
+ paths,
362
+ totalEntries,
363
+ };
364
+ }
package/src/mnemo.ts ADDED
@@ -0,0 +1,142 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * Mnemo Core — simplified entry point
4
+ * Usage: const mnemo = await createMnemo(config)
5
+ */
6
+
7
+ import { MemoryStore } from "./store.js";
8
+ import { createRetriever, DEFAULT_RETRIEVAL_CONFIG } from "./retriever.js";
9
+ import { Embedder } from "./embedder.js";
10
+ import { createDecayEngine, DEFAULT_DECAY_CONFIG } from "./decay-engine.js";
11
+ import { SmartExtractor } from "./smart-extractor.js";
12
+ import { createLlmClient } from "./llm-client.js";
13
+
14
+ export interface MnemoConfig {
15
+ embedding: {
16
+ provider: "openai-compatible";
17
+ apiKey: string;
18
+ baseURL?: string;
19
+ model?: string;
20
+ dimensions?: number;
21
+ taskQuery?: string;
22
+ taskPassage?: string;
23
+ };
24
+ dbPath: string;
25
+ decay?: {
26
+ recencyHalfLifeDays?: number;
27
+ recencyWeight?: number;
28
+ frequencyWeight?: number;
29
+ intrinsicWeight?: number;
30
+ };
31
+ tier?: {
32
+ coreAccessThreshold?: number;
33
+ coreImportanceThreshold?: number;
34
+ peripheralAgeDays?: number;
35
+ };
36
+ llm?: {
37
+ model?: string;
38
+ baseURL?: string;
39
+ apiKey?: string;
40
+ };
41
+ retrieval?: {
42
+ candidatePoolSize?: number;
43
+ rerank?: "cross-encoder" | "lightweight" | "none";
44
+ rerankApiKey?: string;
45
+ rerankModel?: string;
46
+ rerankEndpoint?: string;
47
+ rerankProvider?: string;
48
+ };
49
+ }
50
+
51
+ export interface MnemoInstance {
52
+ store(entry: {
53
+ text: string;
54
+ category?: string;
55
+ importance?: number;
56
+ scope?: string;
57
+ }): Promise<{ id: string }>;
58
+
59
+ recall(query: string, options?: {
60
+ limit?: number;
61
+ scopeFilter?: string[];
62
+ category?: string;
63
+ }): Promise<Array<{
64
+ text: string;
65
+ score: number;
66
+ category: string;
67
+ importance: number;
68
+ timestamp: number;
69
+ }>>;
70
+
71
+ stats(): Promise<{ totalEntries: number }>;
72
+
73
+ close(): Promise<void>;
74
+ }
75
+
76
+ export async function createMnemo(config: MnemoConfig): Promise<MnemoInstance> {
77
+ const embedder = new Embedder({
78
+ apiKey: config.embedding.apiKey,
79
+ baseURL: config.embedding.baseURL,
80
+ model: config.embedding.model || "voyage-3-large",
81
+ dimensions: config.embedding.dimensions || 1024,
82
+ taskQuery: config.embedding.taskQuery,
83
+ taskPassage: config.embedding.taskPassage,
84
+ });
85
+
86
+ const store = new MemoryStore({
87
+ dbPath: config.dbPath,
88
+ embedder,
89
+ });
90
+
91
+ await store.initialize();
92
+
93
+ const decayEngine = createDecayEngine({
94
+ ...DEFAULT_DECAY_CONFIG,
95
+ ...(config.decay || {}),
96
+ });
97
+
98
+ const retriever = createRetriever(store, embedder, {
99
+ ...DEFAULT_RETRIEVAL_CONFIG,
100
+ ...(config.retrieval || {}),
101
+ }, { decayEngine });
102
+
103
+ return {
104
+ async store(entry) {
105
+ const vector = await embedder.embed(entry.text);
106
+ const result = await store.store({
107
+ text: entry.text,
108
+ vector,
109
+ category: entry.category || "fact",
110
+ importance: entry.importance ?? 0.7,
111
+ scope: entry.scope || "global",
112
+ });
113
+ return { id: result.id };
114
+ },
115
+
116
+ async recall(query, options = {}) {
117
+ const results = await retriever.retrieve({
118
+ query,
119
+ limit: options.limit ?? 5,
120
+ scopeFilter: options.scopeFilter,
121
+ category: options.category,
122
+ source: "manual",
123
+ });
124
+ return results.map(r => ({
125
+ text: r.entry.text,
126
+ score: r.score,
127
+ category: r.entry.category || "fact",
128
+ importance: r.entry.importance ?? 0.7,
129
+ timestamp: r.entry.timestamp ?? Date.now(),
130
+ }));
131
+ },
132
+
133
+ async stats() {
134
+ const count = await store.count();
135
+ return { totalEntries: count };
136
+ },
137
+
138
+ async close() {
139
+ // LanceDB handles cleanup automatically
140
+ },
141
+ };
142
+ }
@@ -0,0 +1,97 @@
1
+ // SPDX-License-Identifier: MIT
2
+ /**
3
+ * Noise Filter
4
+ * Filters out low-quality memories (meta-questions, agent denials, session boilerplate)
5
+ * Inspired by openclaw-plugin-continuity's noise filtering approach.
6
+ */
7
+
8
+ // Agent-side denial patterns
9
+ const DENIAL_PATTERNS = [
10
+ /i don'?t have (any )?(information|data|memory|record)/i,
11
+ /i'?m not sure about/i,
12
+ /i don'?t recall/i,
13
+ /i don'?t remember/i,
14
+ /it looks like i don'?t/i,
15
+ /i wasn'?t able to find/i,
16
+ /no (relevant )?memories found/i,
17
+ /i don'?t have access to/i,
18
+ ];
19
+
20
+ // User-side meta-question patterns (about memory itself, not content)
21
+ const META_QUESTION_PATTERNS = [
22
+ /\bdo you (remember|recall|know about)\b/i,
23
+ /\bcan you (remember|recall)\b/i,
24
+ /\bdid i (tell|mention|say|share)\b/i,
25
+ /\bhave i (told|mentioned|said)\b/i,
26
+ /\bwhat did i (tell|say|mention)\b/i,
27
+ /如果你知道.+只回复/i,
28
+ /如果不知道.+只回复\s*none/i,
29
+ /只回复精确代号/i,
30
+ /只回复\s*none/i,
31
+ // Chinese recall / meta-question patterns
32
+ /你还?记得/,
33
+ /记不记得/,
34
+ /还记得.*吗/,
35
+ /你[知晓]道.+吗/,
36
+ /我(?:之前|上次|以前)(?:说|提|讲).*(?:吗|呢|?|\?)/,
37
+ ];
38
+
39
+ // Session boilerplate
40
+ const BOILERPLATE_PATTERNS = [
41
+ /^(hi|hello|hey|good morning|good evening|greetings)/i,
42
+ /^fresh session/i,
43
+ /^new session/i,
44
+ /^HEARTBEAT/i,
45
+ ];
46
+
47
+ // Extractor artifacts from validation prompts / synthetic summaries
48
+ const DIAGNOSTIC_ARTIFACT_PATTERNS = [
49
+ /\bquery\s*->\s*(none|no explicit solution|unknown|not found)\b/i,
50
+ /\buser asked for\b.*\b(none|no explicit solution|unknown|not found)\b/i,
51
+ /\bno explicit solution\b/i,
52
+ ];
53
+
54
+ export interface NoiseFilterOptions {
55
+ /** Filter agent denial responses (default: true) */
56
+ filterDenials?: boolean;
57
+ /** Filter meta-questions about memory (default: true) */
58
+ filterMetaQuestions?: boolean;
59
+ /** Filter session boilerplate (default: true) */
60
+ filterBoilerplate?: boolean;
61
+ }
62
+
63
+ const DEFAULT_OPTIONS: Required<NoiseFilterOptions> = {
64
+ filterDenials: true,
65
+ filterMetaQuestions: true,
66
+ filterBoilerplate: true,
67
+ };
68
+
69
+ /**
70
+ * Check if a memory text is noise that should be filtered out.
71
+ * Returns true if the text is noise.
72
+ */
73
+ export function isNoise(text: string, options: NoiseFilterOptions = {}): boolean {
74
+ const opts = { ...DEFAULT_OPTIONS, ...options };
75
+ const trimmed = text.trim();
76
+
77
+ if (trimmed.length < 5) return true;
78
+
79
+ if (opts.filterDenials && DENIAL_PATTERNS.some(p => p.test(trimmed))) return true;
80
+ if (opts.filterMetaQuestions && META_QUESTION_PATTERNS.some(p => p.test(trimmed))) return true;
81
+ if (opts.filterBoilerplate && BOILERPLATE_PATTERNS.some(p => p.test(trimmed))) return true;
82
+ if (DIAGNOSTIC_ARTIFACT_PATTERNS.some(p => p.test(trimmed))) return true;
83
+
84
+ return false;
85
+ }
86
+
87
+ /**
88
+ * Filter an array of items, removing noise entries.
89
+ */
90
+ export function filterNoise<T>(
91
+ items: T[],
92
+ getText: (item: T) => string,
93
+ options?: NoiseFilterOptions
94
+ ): T[] {
95
+ const opts = { ...DEFAULT_OPTIONS, ...options };
96
+ return items.filter(item => !isNoise(getText(item), opts));
97
+ }