@smyslenny/agent-memory 2.0.0 → 2.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.
@@ -4,7 +4,7 @@
4
4
  // src/core/db.ts
5
5
  import Database from "better-sqlite3";
6
6
  import { randomUUID } from "crypto";
7
- var SCHEMA_VERSION = 1;
7
+ var SCHEMA_VERSION = 3;
8
8
  var SCHEMA_SQL = `
9
9
  -- Memory entries
10
10
  CREATE TABLE IF NOT EXISTS memories (
@@ -29,20 +29,23 @@ CREATE TABLE IF NOT EXISTS memories (
29
29
  CREATE TABLE IF NOT EXISTS paths (
30
30
  id TEXT PRIMARY KEY,
31
31
  memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
32
- uri TEXT NOT NULL UNIQUE,
32
+ agent_id TEXT NOT NULL DEFAULT 'default',
33
+ uri TEXT NOT NULL,
33
34
  alias TEXT,
34
35
  domain TEXT NOT NULL,
35
- created_at TEXT NOT NULL
36
+ created_at TEXT NOT NULL,
37
+ UNIQUE(agent_id, uri)
36
38
  );
37
39
 
38
40
  -- Association network (knowledge graph)
39
41
  CREATE TABLE IF NOT EXISTS links (
42
+ agent_id TEXT NOT NULL DEFAULT 'default',
40
43
  source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
41
44
  target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
42
45
  relation TEXT NOT NULL,
43
46
  weight REAL NOT NULL DEFAULT 1.0,
44
47
  created_at TEXT NOT NULL,
45
- PRIMARY KEY (source_id, target_id)
48
+ PRIMARY KEY (agent_id, source_id, target_id)
46
49
  );
47
50
 
48
51
  -- Snapshots (version control, from nocturne + Memory Palace)
@@ -62,6 +65,18 @@ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
62
65
  tokenize='unicode61'
63
66
  );
64
67
 
68
+ -- Embeddings (optional semantic layer)
69
+ CREATE TABLE IF NOT EXISTS embeddings (
70
+ agent_id TEXT NOT NULL DEFAULT 'default',
71
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
72
+ model TEXT NOT NULL,
73
+ dim INTEGER NOT NULL,
74
+ vector BLOB NOT NULL,
75
+ created_at TEXT NOT NULL,
76
+ updated_at TEXT NOT NULL,
77
+ PRIMARY KEY (agent_id, memory_id, model)
78
+ );
79
+
65
80
  -- Schema version tracking
66
81
  CREATE TABLE IF NOT EXISTS schema_meta (
67
82
  key TEXT PRIMARY KEY,
@@ -76,8 +91,6 @@ CREATE INDEX IF NOT EXISTS idx_memories_vitality ON memories(vitality);
76
91
  CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
77
92
  CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
78
93
  CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
79
- CREATE INDEX IF NOT EXISTS idx_links_source ON links(source_id);
80
- CREATE INDEX IF NOT EXISTS idx_links_target ON links(target_id);
81
94
  `;
82
95
  function openDatabase(opts) {
83
96
  const db = new Database(opts.path);
@@ -87,13 +100,17 @@ function openDatabase(opts) {
87
100
  db.pragma("foreign_keys = ON");
88
101
  db.pragma("busy_timeout = 5000");
89
102
  db.exec(SCHEMA_SQL);
90
- const getVersion = db.prepare("SELECT value FROM schema_meta WHERE key = 'version'");
91
- const row = getVersion.get();
92
- if (!row) {
93
- db.prepare("INSERT INTO schema_meta (key, value) VALUES ('version', ?)").run(
94
- String(SCHEMA_VERSION)
95
- );
103
+ const currentVersion = getSchemaVersion(db);
104
+ if (currentVersion === null) {
105
+ const inferred = inferSchemaVersion(db);
106
+ if (inferred < SCHEMA_VERSION) {
107
+ migrateDatabase(db, inferred, SCHEMA_VERSION);
108
+ }
109
+ db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(SCHEMA_VERSION));
110
+ } else if (currentVersion < SCHEMA_VERSION) {
111
+ migrateDatabase(db, currentVersion, SCHEMA_VERSION);
96
112
  }
113
+ ensureIndexes(db);
97
114
  return db;
98
115
  }
99
116
  function now() {
@@ -102,9 +119,256 @@ function now() {
102
119
  function newId() {
103
120
  return randomUUID();
104
121
  }
122
+ function getSchemaVersion(db) {
123
+ try {
124
+ const row = db.prepare("SELECT value FROM schema_meta WHERE key = 'version'").get();
125
+ if (!row) return null;
126
+ const n = Number.parseInt(row.value, 10);
127
+ return Number.isFinite(n) ? n : null;
128
+ } catch {
129
+ return null;
130
+ }
131
+ }
132
+ function tableHasColumn(db, table, column) {
133
+ try {
134
+ const cols = db.prepare(`PRAGMA table_info(${table})`).all();
135
+ return cols.some((c) => c.name === column);
136
+ } catch {
137
+ return false;
138
+ }
139
+ }
140
+ function migrateDatabase(db, from, to) {
141
+ let v = from;
142
+ while (v < to) {
143
+ if (v === 1) {
144
+ migrateV1ToV2(db);
145
+ v = 2;
146
+ continue;
147
+ }
148
+ if (v === 2) {
149
+ migrateV2ToV3(db);
150
+ v = 3;
151
+ continue;
152
+ }
153
+ throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
154
+ }
155
+ }
156
+ function migrateV1ToV2(db) {
157
+ const pathsMigrated = tableHasColumn(db, "paths", "agent_id");
158
+ const linksMigrated = tableHasColumn(db, "links", "agent_id");
159
+ const alreadyMigrated = pathsMigrated && linksMigrated;
160
+ if (alreadyMigrated) {
161
+ db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(2));
162
+ return;
163
+ }
164
+ db.pragma("foreign_keys = OFF");
165
+ try {
166
+ db.exec("BEGIN");
167
+ if (!pathsMigrated) {
168
+ db.exec(`
169
+ CREATE TABLE IF NOT EXISTS paths_v2 (
170
+ id TEXT PRIMARY KEY,
171
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
172
+ agent_id TEXT NOT NULL DEFAULT 'default',
173
+ uri TEXT NOT NULL,
174
+ alias TEXT,
175
+ domain TEXT NOT NULL,
176
+ created_at TEXT NOT NULL,
177
+ UNIQUE(agent_id, uri)
178
+ );
179
+ `);
180
+ db.exec(`
181
+ INSERT INTO paths_v2 (id, memory_id, agent_id, uri, alias, domain, created_at)
182
+ SELECT p.id, p.memory_id, COALESCE(m.agent_id, 'default'), p.uri, p.alias, p.domain, p.created_at
183
+ FROM paths p
184
+ LEFT JOIN memories m ON m.id = p.memory_id;
185
+ `);
186
+ db.exec("DROP TABLE paths;");
187
+ db.exec("ALTER TABLE paths_v2 RENAME TO paths;");
188
+ }
189
+ if (!linksMigrated) {
190
+ db.exec(`
191
+ CREATE TABLE IF NOT EXISTS links_v2 (
192
+ agent_id TEXT NOT NULL DEFAULT 'default',
193
+ source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
194
+ target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
195
+ relation TEXT NOT NULL,
196
+ weight REAL NOT NULL DEFAULT 1.0,
197
+ created_at TEXT NOT NULL,
198
+ PRIMARY KEY (agent_id, source_id, target_id)
199
+ );
200
+ `);
201
+ db.exec(`
202
+ INSERT INTO links_v2 (agent_id, source_id, target_id, relation, weight, created_at)
203
+ SELECT COALESCE(ms.agent_id, 'default'), l.source_id, l.target_id, l.relation, l.weight, l.created_at
204
+ FROM links l
205
+ LEFT JOIN memories ms ON ms.id = l.source_id;
206
+ `);
207
+ db.exec(`
208
+ DELETE FROM links_v2
209
+ WHERE EXISTS (SELECT 1 FROM memories s WHERE s.id = links_v2.source_id AND s.agent_id != links_v2.agent_id)
210
+ OR EXISTS (SELECT 1 FROM memories t WHERE t.id = links_v2.target_id AND t.agent_id != links_v2.agent_id);
211
+ `);
212
+ db.exec("DROP TABLE links;");
213
+ db.exec("ALTER TABLE links_v2 RENAME TO links;");
214
+ }
215
+ db.exec(`
216
+ CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
217
+ CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
218
+ `);
219
+ db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(2));
220
+ db.exec("COMMIT");
221
+ } catch (e) {
222
+ try {
223
+ db.exec("ROLLBACK");
224
+ } catch {
225
+ }
226
+ throw e;
227
+ } finally {
228
+ db.pragma("foreign_keys = ON");
229
+ }
230
+ }
231
+ function inferSchemaVersion(db) {
232
+ const hasAgentScopedPaths = tableHasColumn(db, "paths", "agent_id");
233
+ const hasAgentScopedLinks = tableHasColumn(db, "links", "agent_id");
234
+ const hasEmbeddings = (() => {
235
+ try {
236
+ const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'").get();
237
+ return Boolean(row);
238
+ } catch {
239
+ return false;
240
+ }
241
+ })();
242
+ if (hasAgentScopedPaths && hasAgentScopedLinks && hasEmbeddings) return 3;
243
+ if (hasAgentScopedPaths && hasAgentScopedLinks) return 2;
244
+ return 1;
245
+ }
246
+ function ensureIndexes(db) {
247
+ if (tableHasColumn(db, "paths", "agent_id")) {
248
+ db.exec("CREATE INDEX IF NOT EXISTS idx_paths_agent_uri ON paths(agent_id, uri);");
249
+ }
250
+ if (tableHasColumn(db, "links", "agent_id")) {
251
+ db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_source ON links(agent_id, source_id);");
252
+ db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_target ON links(agent_id, target_id);");
253
+ }
254
+ try {
255
+ const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'").get();
256
+ if (row) {
257
+ db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_agent_model ON embeddings(agent_id, model);");
258
+ db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_memory ON embeddings(memory_id);");
259
+ }
260
+ } catch {
261
+ }
262
+ }
263
+ function migrateV2ToV3(db) {
264
+ try {
265
+ db.exec("BEGIN");
266
+ db.exec(`
267
+ CREATE TABLE IF NOT EXISTS embeddings (
268
+ agent_id TEXT NOT NULL DEFAULT 'default',
269
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
270
+ model TEXT NOT NULL,
271
+ dim INTEGER NOT NULL,
272
+ vector BLOB NOT NULL,
273
+ created_at TEXT NOT NULL,
274
+ updated_at TEXT NOT NULL,
275
+ PRIMARY KEY (agent_id, memory_id, model)
276
+ );
277
+ `);
278
+ db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(3));
279
+ db.exec("COMMIT");
280
+ } catch (e) {
281
+ try {
282
+ db.exec("ROLLBACK");
283
+ } catch {
284
+ }
285
+ throw e;
286
+ }
287
+ }
105
288
 
106
289
  // src/core/memory.ts
107
290
  import { createHash } from "crypto";
291
+
292
+ // src/search/tokenizer.ts
293
+ import { readFileSync } from "fs";
294
+ import { createRequire } from "module";
295
+ var _jieba;
296
+ function getJieba() {
297
+ if (_jieba !== void 0) return _jieba;
298
+ try {
299
+ const req = createRequire(import.meta.url);
300
+ const { Jieba } = req("@node-rs/jieba");
301
+ const dictPath = req.resolve("@node-rs/jieba/dict.txt");
302
+ const dictBuf = readFileSync(dictPath);
303
+ _jieba = Jieba.withDict(dictBuf);
304
+ } catch {
305
+ _jieba = null;
306
+ }
307
+ return _jieba;
308
+ }
309
+ var STOPWORDS = /* @__PURE__ */ new Set([
310
+ "\u7684",
311
+ "\u4E86",
312
+ "\u5728",
313
+ "\u662F",
314
+ "\u6211",
315
+ "\u6709",
316
+ "\u548C",
317
+ "\u5C31",
318
+ "\u4E0D",
319
+ "\u4EBA",
320
+ "\u90FD",
321
+ "\u4E00",
322
+ "\u4E2A",
323
+ "\u4E0A",
324
+ "\u4E5F",
325
+ "\u5230",
326
+ "\u4ED6",
327
+ "\u6CA1",
328
+ "\u8FD9",
329
+ "\u8981",
330
+ "\u4F1A",
331
+ "\u5BF9",
332
+ "\u8BF4",
333
+ "\u800C",
334
+ "\u53BB",
335
+ "\u4E4B",
336
+ "\u88AB",
337
+ "\u5979",
338
+ "\u628A",
339
+ "\u90A3"
340
+ ]);
341
+ function tokenize(text) {
342
+ const cleaned = text.replace(/[^\w\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af\s]/g, " ");
343
+ const tokens = [];
344
+ const latinWords = cleaned.replace(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]/g, " ").split(/\s+/).filter((w) => w.length > 1);
345
+ tokens.push(...latinWords);
346
+ const cjkChunks = cleaned.match(/[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]+/g);
347
+ if (cjkChunks && cjkChunks.length > 0) {
348
+ const jieba = getJieba();
349
+ for (const chunk of cjkChunks) {
350
+ if (jieba) {
351
+ const words = jieba.cutForSearch(chunk).filter((w) => w.length >= 1);
352
+ tokens.push(...words);
353
+ } else {
354
+ for (const ch of chunk) {
355
+ tokens.push(ch);
356
+ }
357
+ for (let i = 0; i < chunk.length - 1; i++) {
358
+ tokens.push(chunk[i] + chunk[i + 1]);
359
+ }
360
+ }
361
+ }
362
+ }
363
+ const unique = [...new Set(tokens)].filter((t) => t.length > 0 && !STOPWORDS.has(t)).slice(0, 30);
364
+ return unique;
365
+ }
366
+ function tokenizeForIndex(text) {
367
+ const tokens = tokenize(text);
368
+ return tokens.join(" ");
369
+ }
370
+
371
+ // src/core/memory.ts
108
372
  function contentHash(content) {
109
373
  return createHash("sha256").update(content.trim()).digest("hex").slice(0, 16);
110
374
  }
@@ -152,7 +416,7 @@ function createMemory(db, input) {
152
416
  agentId,
153
417
  hash
154
418
  );
155
- db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, input.content);
419
+ db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
156
420
  return getMemory(db, id);
157
421
  }
158
422
  function getMemory(db, id) {
@@ -197,7 +461,7 @@ function updateMemory(db, id, input) {
197
461
  db.prepare(`UPDATE memories SET ${fields.join(", ")} WHERE id = ?`).run(...values);
198
462
  if (input.content !== void 0) {
199
463
  db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
200
- db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, input.content);
464
+ db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
201
465
  }
202
466
  return getMemory(db, id);
203
467
  }
@@ -250,95 +514,134 @@ function countMemories(db, agent_id = "default") {
250
514
  };
251
515
  }
252
516
 
253
- // src/search/bm25.ts
254
- function searchBM25(db, query, opts) {
255
- const limit = opts?.limit ?? 20;
517
+ // src/core/export.ts
518
+ import { writeFileSync, mkdirSync, existsSync } from "fs";
519
+ import { join } from "path";
520
+ function exportMemories(db, dirPath, opts) {
256
521
  const agentId = opts?.agent_id ?? "default";
257
- const minVitality = opts?.min_vitality ?? 0;
258
- const ftsQuery = buildFtsQuery(query);
259
- if (!ftsQuery) return [];
260
- try {
261
- const rows = db.prepare(
262
- `SELECT m.*, rank AS score
263
- FROM memories_fts f
264
- JOIN memories m ON m.id = f.id
265
- WHERE memories_fts MATCH ?
266
- AND m.agent_id = ?
267
- AND m.vitality >= ?
268
- ORDER BY rank
269
- LIMIT ?`
270
- ).all(ftsQuery, agentId, minVitality, limit);
271
- return rows.map((row) => ({
272
- memory: { ...row, score: void 0 },
273
- score: Math.abs(row.score),
274
- // FTS5 rank is negative (lower = better)
275
- matchReason: "bm25"
276
- }));
277
- } catch {
278
- return searchSimple(db, query, agentId, minVitality, limit);
522
+ if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true });
523
+ let exported = 0;
524
+ const files = [];
525
+ const identities = listMemories(db, { agent_id: agentId, type: "identity" });
526
+ const knowledge = listMemories(db, { agent_id: agentId, type: "knowledge" });
527
+ const emotions = listMemories(db, { agent_id: agentId, type: "emotion" });
528
+ if (identities.length || knowledge.length || emotions.length) {
529
+ const sections = ["# Agent Memory Export\n"];
530
+ if (identities.length) {
531
+ sections.push("## Identity\n");
532
+ for (const m of identities) {
533
+ sections.push(`- ${m.content}
534
+ `);
535
+ exported++;
536
+ }
537
+ }
538
+ if (emotions.length) {
539
+ sections.push("\n## Emotions\n");
540
+ for (const m of emotions) {
541
+ sections.push(`- ${m.content}
542
+ `);
543
+ exported++;
544
+ }
545
+ }
546
+ if (knowledge.length) {
547
+ sections.push("\n## Knowledge\n");
548
+ for (const m of knowledge) {
549
+ sections.push(`- ${m.content}
550
+ `);
551
+ exported++;
552
+ }
553
+ }
554
+ const memoryPath = join(dirPath, "MEMORY.md");
555
+ writeFileSync(memoryPath, sections.join("\n"));
556
+ files.push(memoryPath);
279
557
  }
280
- }
281
- function searchSimple(db, query, agentId, minVitality, limit) {
282
- const rows = db.prepare(
283
- `SELECT * FROM memories
284
- WHERE agent_id = ? AND vitality >= ? AND content LIKE ?
285
- ORDER BY priority ASC, updated_at DESC
286
- LIMIT ?`
287
- ).all(agentId, minVitality, `%${query}%`, limit);
288
- return rows.map((m, i) => ({
289
- memory: m,
290
- score: 1 / (i + 1),
291
- // Simple rank by position
292
- matchReason: "like"
293
- }));
294
- }
295
- function buildFtsQuery(text) {
296
- const words = text.replace(/[^\w\u4e00-\u9fff\u3040-\u30ff\s]/g, " ").split(/\s+/).filter((w) => w.length > 1).slice(0, 10);
297
- if (words.length === 0) return null;
298
- return words.map((w) => `"${w}"`).join(" OR ");
558
+ const events = listMemories(db, { agent_id: agentId, type: "event", limit: 1e4 });
559
+ const byDate = /* @__PURE__ */ new Map();
560
+ for (const ev of events) {
561
+ const date = ev.created_at.slice(0, 10);
562
+ if (!byDate.has(date)) byDate.set(date, []);
563
+ byDate.get(date).push(ev);
564
+ }
565
+ for (const [date, mems] of byDate) {
566
+ const lines = [`# ${date}
567
+ `];
568
+ for (const m of mems) {
569
+ lines.push(`- ${m.content}
570
+ `);
571
+ exported++;
572
+ }
573
+ const filePath = join(dirPath, `${date}.md`);
574
+ writeFileSync(filePath, lines.join("\n"));
575
+ files.push(filePath);
576
+ }
577
+ return { exported, files };
299
578
  }
300
579
 
301
580
  // src/search/intent.ts
302
581
  var INTENT_PATTERNS = {
303
582
  factual: [
304
- /^(what|who|where|which|how much|how many)/i,
305
- /是(什么|谁|哪)/,
306
- /叫什么/,
307
- /名字/,
308
- /地址/,
309
- /号码/,
310
- /密码/,
311
- /配置/,
312
- /设置/
583
+ // English
584
+ /^(what|who|where|which|how much|how many)\b/i,
585
+ /\b(name|address|number|password|config|setting)\b/i,
586
+ // Chinese - questions about facts
587
+ /是(什么|谁|哪|啥)/,
588
+ /叫(什么|啥)/,
589
+ /(名字|地址|号码|密码|配置|设置|账号|邮箱|链接|版本)/,
590
+ /(多少|几个|哪个|哪些|哪里)/,
591
+ // Chinese - lookup patterns
592
+ /(查一下|找一下|看看|搜一下)/,
593
+ /(.+)是什么$/
313
594
  ],
314
595
  temporal: [
315
- /^(when|what time|how long)/i,
316
- /(yesterday|today|last week|recently|ago|before|after)/i,
596
+ // English
597
+ /^(when|what time|how long)\b/i,
598
+ /\b(yesterday|today|tomorrow|last week|recently|ago|before|after)\b/i,
599
+ /\b(first|latest|newest|oldest|previous|next)\b/i,
600
+ // Chinese - time expressions
317
601
  /什么时候/,
318
- /(昨天|今天|上周|最近|以前|之前|之后)/,
319
- /\d{4}[-/]\d{1,2}/,
320
- /(几月|几号|几点)/
602
+ /(昨天|今天|明天|上周|下周|最近|以前|之前|之后|刚才|刚刚)/,
603
+ /(几月|几号|几点|多久|多长时间)/,
604
+ /(上次|下次|第一次|最后一次|那天|那时)/,
605
+ // Date patterns
606
+ /\d{4}[-/.]\d{1,2}/,
607
+ /\d{1,2}月\d{1,2}[日号]/,
608
+ // Chinese - temporal context
609
+ /(历史|记录|日志|以来|至今|期间)/
321
610
  ],
322
611
  causal: [
323
- /^(why|how come|what caused)/i,
324
- /^(because|due to|reason)/i,
325
- /为什么/,
326
- /原因/,
327
- /导致/,
328
- /怎么回事/,
329
- /为啥/
612
+ // English
613
+ /^(why|how come|what caused)\b/i,
614
+ /\b(because|due to|reason|cause|result)\b/i,
615
+ // Chinese - causal questions
616
+ /为(什么|啥|何)/,
617
+ /(原因|导致|造成|引起|因为|所以|结果)/,
618
+ /(怎么回事|怎么了|咋回事|咋了)/,
619
+ /(为啥|凭啥|凭什么)/,
620
+ // Chinese - problem/diagnosis
621
+ /(出(了|了什么)?问题|报错|失败|出错|bug)/
330
622
  ],
331
623
  exploratory: [
332
- /^(how|tell me about|explain|describe)/i,
333
- /^(what do you think|what about)/i,
334
- /怎么样/,
335
- /介绍/,
336
- /说说/,
337
- /讲讲/,
338
- /有哪些/,
339
- /关于/
624
+ // English
625
+ /^(how|tell me about|explain|describe|show me)\b/i,
626
+ /^(what do you think|what about|any)\b/i,
627
+ /\b(overview|summary|list|compare)\b/i,
628
+ // Chinese - exploratory
629
+ /(怎么样|怎样|如何)/,
630
+ /(介绍|说说|讲讲|聊聊|谈谈)/,
631
+ /(有哪些|有什么|有没有)/,
632
+ /(关于|对于|至于|关联)/,
633
+ /(总结|概括|梳理|回顾|盘点)/,
634
+ // Chinese - opinion/analysis
635
+ /(看法|想法|意见|建议|评价|感觉|觉得)/,
636
+ /(对比|比较|区别|差异|优缺点)/
340
637
  ]
341
638
  };
639
+ var CN_STRUCTURE_BOOSTS = {
640
+ factual: [/^.{1,6}(是什么|叫什么|在哪)/, /^(谁|哪)/],
641
+ temporal: [/^(什么时候|上次|最近)/, /(时间|日期)$/],
642
+ causal: [/^(为什么|为啥)/, /(为什么|怎么回事)$/],
643
+ exploratory: [/^(怎么|如何|说说)/, /(哪些|什么样)$/]
644
+ };
342
645
  function classifyIntent(query) {
343
646
  const scores = {
344
647
  factual: 0,
@@ -353,6 +656,18 @@ function classifyIntent(query) {
353
656
  }
354
657
  }
355
658
  }
659
+ for (const [intent, patterns] of Object.entries(CN_STRUCTURE_BOOSTS)) {
660
+ for (const pattern of patterns) {
661
+ if (pattern.test(query)) {
662
+ scores[intent] += 0.5;
663
+ }
664
+ }
665
+ }
666
+ const tokens = tokenize(query);
667
+ const totalPatternScore = Object.values(scores).reduce((a, b) => a + b, 0);
668
+ if (totalPatternScore === 0 && tokens.length <= 3) {
669
+ scores.factual += 1;
670
+ }
356
671
  let maxIntent = "factual";
357
672
  let maxScore = 0;
358
673
  for (const [intent, score] of Object.entries(scores)) {
@@ -362,7 +677,7 @@ function classifyIntent(query) {
362
677
  }
363
678
  }
364
679
  const totalScore = Object.values(scores).reduce((a, b) => a + b, 0);
365
- const confidence = totalScore > 0 ? maxScore / totalScore : 0.5;
680
+ const confidence = totalScore > 0 ? Math.min(0.95, maxScore / totalScore) : 0.5;
366
681
  return { intent: maxIntent, confidence };
367
682
  }
368
683
  function getStrategy(intent) {
@@ -400,6 +715,297 @@ function rerank(results, opts) {
400
715
  return scored.slice(0, opts.limit);
401
716
  }
402
717
 
718
+ // src/search/bm25.ts
719
+ function searchBM25(db, query, opts) {
720
+ const limit = opts?.limit ?? 20;
721
+ const agentId = opts?.agent_id ?? "default";
722
+ const minVitality = opts?.min_vitality ?? 0;
723
+ const ftsQuery = buildFtsQuery(query);
724
+ if (!ftsQuery) return [];
725
+ try {
726
+ const rows = db.prepare(
727
+ `SELECT m.*, rank AS score
728
+ FROM memories_fts f
729
+ JOIN memories m ON m.id = f.id
730
+ WHERE memories_fts MATCH ?
731
+ AND m.agent_id = ?
732
+ AND m.vitality >= ?
733
+ ORDER BY rank
734
+ LIMIT ?`
735
+ ).all(ftsQuery, agentId, minVitality, limit);
736
+ return rows.map((row) => {
737
+ const { score: _score, ...memoryFields } = row;
738
+ return {
739
+ memory: memoryFields,
740
+ score: Math.abs(row.score),
741
+ // FTS5 rank is negative (lower = better)
742
+ matchReason: "bm25"
743
+ };
744
+ });
745
+ } catch {
746
+ return searchSimple(db, query, agentId, minVitality, limit);
747
+ }
748
+ }
749
+ function searchSimple(db, query, agentId, minVitality, limit) {
750
+ const rows = db.prepare(
751
+ `SELECT * FROM memories
752
+ WHERE agent_id = ? AND vitality >= ? AND content LIKE ?
753
+ ORDER BY priority ASC, updated_at DESC
754
+ LIMIT ?`
755
+ ).all(agentId, minVitality, `%${query}%`, limit);
756
+ return rows.map((m, i) => ({
757
+ memory: m,
758
+ score: 1 / (i + 1),
759
+ // Simple rank by position
760
+ matchReason: "like"
761
+ }));
762
+ }
763
+ function buildFtsQuery(text) {
764
+ const tokens = tokenize(text);
765
+ if (tokens.length === 0) return null;
766
+ return tokens.map((w) => `"${w}"`).join(" OR ");
767
+ }
768
+
769
+ // src/search/embeddings.ts
770
+ function encodeEmbedding(vector) {
771
+ const arr = vector instanceof Float32Array ? vector : Float32Array.from(vector);
772
+ return Buffer.from(arr.buffer, arr.byteOffset, arr.byteLength);
773
+ }
774
+ function decodeEmbedding(buf) {
775
+ const copy = Buffer.from(buf);
776
+ return new Float32Array(copy.buffer, copy.byteOffset, Math.floor(copy.byteLength / 4));
777
+ }
778
+ function upsertEmbedding(db, input) {
779
+ const ts = now();
780
+ const vec = input.vector instanceof Float32Array ? input.vector : Float32Array.from(input.vector);
781
+ const blob = encodeEmbedding(vec);
782
+ db.prepare(
783
+ `INSERT INTO embeddings (agent_id, memory_id, model, dim, vector, created_at, updated_at)
784
+ VALUES (?, ?, ?, ?, ?, ?, ?)
785
+ ON CONFLICT(agent_id, memory_id, model) DO UPDATE SET
786
+ dim = excluded.dim,
787
+ vector = excluded.vector,
788
+ updated_at = excluded.updated_at`
789
+ ).run(input.agent_id, input.memory_id, input.model, vec.length, blob, ts, ts);
790
+ }
791
+ function listEmbeddings(db, agent_id, model) {
792
+ const rows = db.prepare(
793
+ "SELECT memory_id, vector FROM embeddings WHERE agent_id = ? AND model = ?"
794
+ ).all(agent_id, model);
795
+ return rows.map((r) => ({ memory_id: r.memory_id, vector: decodeEmbedding(r.vector) }));
796
+ }
797
+
798
+ // src/search/hybrid.ts
799
+ function cosine(a, b) {
800
+ const n = Math.min(a.length, b.length);
801
+ let dot = 0;
802
+ let na = 0;
803
+ let nb = 0;
804
+ for (let i = 0; i < n; i++) {
805
+ const x = a[i];
806
+ const y = b[i];
807
+ dot += x * y;
808
+ na += x * x;
809
+ nb += y * y;
810
+ }
811
+ if (na === 0 || nb === 0) return 0;
812
+ return dot / (Math.sqrt(na) * Math.sqrt(nb));
813
+ }
814
+ function rrfScore(rank, k) {
815
+ return 1 / (k + rank);
816
+ }
817
+ function fuseRrf(lists, k) {
818
+ const out = /* @__PURE__ */ new Map();
819
+ for (const list of lists) {
820
+ for (let i = 0; i < list.items.length; i++) {
821
+ const it = list.items[i];
822
+ const rank = i + 1;
823
+ const add = rrfScore(rank, k);
824
+ const prev = out.get(it.id);
825
+ if (!prev) out.set(it.id, { score: add, sources: [list.name] });
826
+ else {
827
+ prev.score += add;
828
+ if (!prev.sources.includes(list.name)) prev.sources.push(list.name);
829
+ }
830
+ }
831
+ }
832
+ return out;
833
+ }
834
+ function fetchMemories(db, ids, agentId) {
835
+ if (ids.length === 0) return [];
836
+ const placeholders = ids.map(() => "?").join(", ");
837
+ const sql = agentId ? `SELECT * FROM memories WHERE id IN (${placeholders}) AND agent_id = ?` : `SELECT * FROM memories WHERE id IN (${placeholders})`;
838
+ const rows = db.prepare(sql).all(...agentId ? [...ids, agentId] : ids);
839
+ return rows;
840
+ }
841
+ async function searchHybrid(db, query, opts) {
842
+ const agentId = opts?.agent_id ?? "default";
843
+ const limit = opts?.limit ?? 10;
844
+ const bm25Mult = opts?.bm25CandidateMultiplier ?? 3;
845
+ const semanticCandidates = opts?.semanticCandidates ?? 50;
846
+ const rrfK = opts?.rrfK ?? 60;
847
+ const bm25 = searchBM25(db, query, {
848
+ agent_id: agentId,
849
+ limit: limit * bm25Mult
850
+ });
851
+ const provider = opts?.embeddingProvider ?? null;
852
+ const model = opts?.embeddingModel ?? provider?.model;
853
+ if (!provider || !model) {
854
+ return bm25.slice(0, limit);
855
+ }
856
+ const qVec = Float32Array.from(await provider.embed(query));
857
+ const embeddings = listEmbeddings(db, agentId, model);
858
+ const scored = [];
859
+ for (const e of embeddings) {
860
+ scored.push({ id: e.memory_id, score: cosine(qVec, e.vector) });
861
+ }
862
+ scored.sort((a, b) => b.score - a.score);
863
+ const semanticTop = scored.slice(0, semanticCandidates);
864
+ const fused = fuseRrf(
865
+ [
866
+ { name: "bm25", items: bm25.map((r) => ({ id: r.memory.id, score: r.score })) },
867
+ { name: "semantic", items: semanticTop }
868
+ ],
869
+ rrfK
870
+ );
871
+ const ids = [...fused.keys()];
872
+ const memories = fetchMemories(db, ids, agentId);
873
+ const byId = new Map(memories.map((m) => [m.id, m]));
874
+ const out = [];
875
+ for (const [id, meta] of fused) {
876
+ const mem = byId.get(id);
877
+ if (!mem) continue;
878
+ out.push({
879
+ memory: mem,
880
+ score: meta.score,
881
+ matchReason: meta.sources.sort().join("+")
882
+ });
883
+ }
884
+ out.sort((a, b) => b.score - a.score);
885
+ return out.slice(0, limit);
886
+ }
887
+
888
+ // src/search/providers.ts
889
+ function getEmbeddingProviderFromEnv() {
890
+ const provider = (process.env.AGENT_MEMORY_EMBEDDINGS_PROVIDER ?? "none").toLowerCase();
891
+ if (provider === "none" || provider === "off" || provider === "false") return null;
892
+ if (provider === "openai") {
893
+ const apiKey = process.env.OPENAI_API_KEY;
894
+ const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-3-small";
895
+ const baseUrl = process.env.OPENAI_BASE_URL ?? "https://api.openai.com/v1";
896
+ if (!apiKey) return null;
897
+ return createOpenAIProvider({ apiKey, model, baseUrl });
898
+ }
899
+ if (provider === "qwen" || provider === "dashscope" || provider === "tongyi") {
900
+ const apiKey = process.env.DASHSCOPE_API_KEY;
901
+ const model = process.env.AGENT_MEMORY_EMBEDDINGS_MODEL ?? "text-embedding-v3";
902
+ const baseUrl = process.env.DASHSCOPE_BASE_URL ?? "https://dashscope.aliyuncs.com";
903
+ if (!apiKey) return null;
904
+ return createDashScopeProvider({ apiKey, model, baseUrl });
905
+ }
906
+ return null;
907
+ }
908
+ function authHeader(apiKey) {
909
+ return apiKey.startsWith("Bearer ") ? apiKey : `Bearer ${apiKey}`;
910
+ }
911
+ function normalizeEmbedding(e) {
912
+ if (!Array.isArray(e)) throw new Error("Invalid embedding: not an array");
913
+ if (e.length === 0) throw new Error("Invalid embedding: empty");
914
+ return e.map((x) => {
915
+ if (typeof x !== "number" || !Number.isFinite(x)) throw new Error("Invalid embedding: non-numeric value");
916
+ return x;
917
+ });
918
+ }
919
+ function createOpenAIProvider(opts) {
920
+ const baseUrl = opts.baseUrl ?? "https://api.openai.com/v1";
921
+ return {
922
+ id: "openai",
923
+ model: opts.model,
924
+ async embed(text) {
925
+ const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/embeddings`, {
926
+ method: "POST",
927
+ headers: {
928
+ "content-type": "application/json",
929
+ authorization: authHeader(opts.apiKey)
930
+ },
931
+ body: JSON.stringify({ model: opts.model, input: text })
932
+ });
933
+ if (!resp.ok) {
934
+ const body = await resp.text().catch(() => "");
935
+ throw new Error(`OpenAI embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
936
+ }
937
+ const data = await resp.json();
938
+ const emb = data.data?.[0]?.embedding;
939
+ return normalizeEmbedding(emb);
940
+ }
941
+ };
942
+ }
943
+ function createDashScopeProvider(opts) {
944
+ const baseUrl = opts.baseUrl ?? "https://dashscope.aliyuncs.com";
945
+ return {
946
+ id: "dashscope",
947
+ model: opts.model,
948
+ async embed(text) {
949
+ const resp = await fetch(`${baseUrl.replace(/\/$/, "")}/api/v1/services/embeddings/text-embedding/text-embedding`, {
950
+ method: "POST",
951
+ headers: {
952
+ "content-type": "application/json",
953
+ authorization: authHeader(opts.apiKey)
954
+ },
955
+ body: JSON.stringify({
956
+ model: opts.model,
957
+ input: { texts: [text] }
958
+ })
959
+ });
960
+ if (!resp.ok) {
961
+ const body = await resp.text().catch(() => "");
962
+ throw new Error(`DashScope embeddings failed: ${resp.status} ${resp.statusText} ${body}`.trim());
963
+ }
964
+ const data = await resp.json();
965
+ const emb = data?.output?.embeddings?.[0]?.embedding ?? data?.output?.embeddings?.[0]?.vector ?? data?.output?.embedding ?? data?.data?.[0]?.embedding;
966
+ return normalizeEmbedding(emb);
967
+ }
968
+ };
969
+ }
970
+
971
+ // src/search/embed.ts
972
+ async function embedMemory(db, memoryId, provider, opts) {
973
+ const row = db.prepare("SELECT id, agent_id, content FROM memories WHERE id = ?").get(memoryId);
974
+ if (!row) return false;
975
+ if (opts?.agent_id && row.agent_id !== opts.agent_id) return false;
976
+ const model = opts?.model ?? provider.model;
977
+ const maxChars = opts?.maxChars ?? 2e3;
978
+ const text = row.content.length > maxChars ? row.content.slice(0, maxChars) : row.content;
979
+ const vector = await provider.embed(text);
980
+ upsertEmbedding(db, {
981
+ agent_id: row.agent_id,
982
+ memory_id: row.id,
983
+ model,
984
+ vector
985
+ });
986
+ return true;
987
+ }
988
+ async function embedMissingForAgent(db, provider, opts) {
989
+ const agentId = opts?.agent_id ?? "default";
990
+ const model = opts?.model ?? provider.model;
991
+ const limit = opts?.limit ?? 1e3;
992
+ const rows = db.prepare(
993
+ `SELECT m.id
994
+ FROM memories m
995
+ LEFT JOIN embeddings e
996
+ ON e.memory_id = m.id AND e.agent_id = m.agent_id AND e.model = ?
997
+ WHERE m.agent_id = ? AND e.memory_id IS NULL
998
+ ORDER BY m.updated_at DESC
999
+ LIMIT ?`
1000
+ ).all(model, agentId, limit);
1001
+ let embedded = 0;
1002
+ for (const r of rows) {
1003
+ const ok = await embedMemory(db, r.id, provider, { agent_id: agentId, model, maxChars: opts?.maxChars });
1004
+ if (ok) embedded++;
1005
+ }
1006
+ return { embedded, scanned: rows.length };
1007
+ }
1008
+
403
1009
  // src/core/path.ts
404
1010
  var DEFAULT_DOMAINS = /* @__PURE__ */ new Set(["core", "emotion", "knowledge", "event", "system"]);
405
1011
  function parseUri(uri) {
@@ -407,27 +1013,33 @@ function parseUri(uri) {
407
1013
  if (!match) throw new Error(`Invalid URI: ${uri}. Expected format: domain://path`);
408
1014
  return { domain: match[1], path: match[2] };
409
1015
  }
410
- function createPath(db, memoryId, uri, alias, validDomains) {
1016
+ function createPath(db, memoryId, uri, alias, validDomains, agent_id) {
411
1017
  const { domain } = parseUri(uri);
412
1018
  const domains = validDomains ?? DEFAULT_DOMAINS;
413
1019
  if (!domains.has(domain)) {
414
1020
  throw new Error(`Invalid domain "${domain}". Valid: ${[...domains].join(", ")}`);
415
1021
  }
416
- const existing = db.prepare("SELECT id FROM paths WHERE uri = ?").get(uri);
1022
+ const memoryAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(memoryId)?.agent_id;
1023
+ if (!memoryAgent) throw new Error(`Memory not found: ${memoryId}`);
1024
+ if (agent_id && agent_id !== memoryAgent) {
1025
+ throw new Error(`Agent mismatch for path: memory agent_id=${memoryAgent}, requested agent_id=${agent_id}`);
1026
+ }
1027
+ const agentId = agent_id ?? memoryAgent;
1028
+ const existing = db.prepare("SELECT id FROM paths WHERE agent_id = ? AND uri = ?").get(agentId, uri);
417
1029
  if (existing) {
418
1030
  throw new Error(`URI already exists: ${uri}`);
419
1031
  }
420
1032
  const id = newId();
421
1033
  db.prepare(
422
- "INSERT INTO paths (id, memory_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?)"
423
- ).run(id, memoryId, uri, alias ?? null, domain, now());
1034
+ "INSERT INTO paths (id, memory_id, agent_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
1035
+ ).run(id, memoryId, agentId, uri, alias ?? null, domain, now());
424
1036
  return getPath(db, id);
425
1037
  }
426
1038
  function getPath(db, id) {
427
1039
  return db.prepare("SELECT * FROM paths WHERE id = ?").get(id) ?? null;
428
1040
  }
429
- function getPathByUri(db, uri) {
430
- return db.prepare("SELECT * FROM paths WHERE uri = ?").get(uri) ?? null;
1041
+ function getPathByUri(db, uri, agent_id = "default") {
1042
+ return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri = ?").get(agent_id, uri) ?? null;
431
1043
  }
432
1044
 
433
1045
  // src/sleep/boot.ts
@@ -447,7 +1059,7 @@ function boot(db, opts) {
447
1059
  }
448
1060
  const bootPaths = [];
449
1061
  for (const uri of corePaths) {
450
- const path = getPathByUri(db, uri);
1062
+ const path = getPathByUri(db, uri, agentId);
451
1063
  if (path) {
452
1064
  bootPaths.push(uri);
453
1065
  if (!memories.has(path.memory_id)) {
@@ -459,13 +1071,13 @@ function boot(db, opts) {
459
1071
  }
460
1072
  }
461
1073
  }
462
- const bootEntry = getPathByUri(db, "system://boot");
1074
+ const bootEntry = getPathByUri(db, "system://boot", agentId);
463
1075
  if (bootEntry) {
464
1076
  const bootMem = getMemory(db, bootEntry.memory_id);
465
1077
  if (bootMem) {
466
1078
  const additionalUris = bootMem.content.split("\n").map((l) => l.trim()).filter((l) => l.match(/^[a-z]+:\/\//));
467
1079
  for (const uri of additionalUris) {
468
- const path = getPathByUri(db, uri);
1080
+ const path = getPathByUri(db, uri, agentId);
469
1081
  if (path && !memories.has(path.memory_id)) {
470
1082
  const mem = getMemory(db, path.memory_id);
471
1083
  if (mem) {
@@ -493,25 +1105,28 @@ var MIN_VITALITY = {
493
1105
  3: 0
494
1106
  // P3: event — full decay
495
1107
  };
496
- function calculateVitality(stability, daysSinceCreation, priority) {
1108
+ function calculateVitality(stability, daysSinceLastAccess, priority) {
497
1109
  if (priority === 0) return 1;
498
1110
  const S = Math.max(0.01, stability);
499
- const retention = Math.exp(-daysSinceCreation / S);
1111
+ const retention = Math.exp(-daysSinceLastAccess / S);
500
1112
  const minVit = MIN_VITALITY[priority] ?? 0;
501
1113
  return Math.max(minVit, retention);
502
1114
  }
503
- function runDecay(db) {
1115
+ function runDecay(db, opts) {
504
1116
  const currentTime = now();
505
1117
  const currentMs = new Date(currentTime).getTime();
506
- const memories = db.prepare("SELECT id, priority, stability, created_at, vitality FROM memories WHERE priority > 0").all();
1118
+ const agentId = opts?.agent_id;
1119
+ const query = agentId ? "SELECT id, priority, stability, created_at, last_accessed, vitality FROM memories WHERE priority > 0 AND agent_id = ?" : "SELECT id, priority, stability, created_at, last_accessed, vitality FROM memories WHERE priority > 0";
1120
+ const memories = db.prepare(query).all(...agentId ? [agentId] : []);
507
1121
  let updated = 0;
508
1122
  let decayed = 0;
509
1123
  let belowThreshold = 0;
510
1124
  const updateStmt = db.prepare("UPDATE memories SET vitality = ?, updated_at = ? WHERE id = ?");
511
1125
  const transaction = db.transaction(() => {
512
1126
  for (const mem of memories) {
513
- const createdMs = new Date(mem.created_at).getTime();
514
- const daysSince = (currentMs - createdMs) / (1e3 * 60 * 60 * 24);
1127
+ const referenceTime = mem.last_accessed ?? mem.created_at;
1128
+ const referenceMs = new Date(referenceTime).getTime();
1129
+ const daysSince = (currentMs - referenceMs) / (1e3 * 60 * 60 * 24);
515
1130
  const newVitality = calculateVitality(mem.stability, daysSince, mem.priority);
516
1131
  if (Math.abs(newVitality - mem.vitality) > 1e-3) {
517
1132
  updateStmt.run(newVitality, currentTime, mem.id);
@@ -528,12 +1143,15 @@ function runDecay(db) {
528
1143
  transaction();
529
1144
  return { updated, decayed, belowThreshold };
530
1145
  }
531
- function getDecayedMemories(db, threshold = 0.05) {
1146
+ function getDecayedMemories(db, threshold = 0.05, opts) {
1147
+ const agentId = opts?.agent_id;
532
1148
  return db.prepare(
533
- `SELECT id, content, vitality, priority FROM memories
534
- WHERE vitality < ? AND priority >= 3
535
- ORDER BY vitality ASC`
536
- ).all(threshold);
1149
+ agentId ? `SELECT id, content, vitality, priority FROM memories
1150
+ WHERE vitality < ? AND priority >= 3 AND agent_id = ?
1151
+ ORDER BY vitality ASC` : `SELECT id, content, vitality, priority FROM memories
1152
+ WHERE vitality < ? AND priority >= 3
1153
+ ORDER BY vitality ASC`
1154
+ ).all(...agentId ? [threshold, agentId] : [threshold]);
537
1155
  }
538
1156
 
539
1157
  // src/core/snapshot.ts
@@ -552,11 +1170,12 @@ function createSnapshot(db, memoryId, action, changedBy) {
552
1170
  function runTidy(db, opts) {
553
1171
  const threshold = opts?.vitalityThreshold ?? 0.05;
554
1172
  const maxSnapshots = opts?.maxSnapshotsPerMemory ?? 10;
1173
+ const agentId = opts?.agent_id;
555
1174
  let archived = 0;
556
1175
  let orphansCleaned = 0;
557
1176
  let snapshotsPruned = 0;
558
1177
  const transaction = db.transaction(() => {
559
- const decayed = getDecayedMemories(db, threshold);
1178
+ const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
560
1179
  for (const mem of decayed) {
561
1180
  try {
562
1181
  createSnapshot(db, mem.id, "delete", "tidy");
@@ -565,13 +1184,23 @@ function runTidy(db, opts) {
565
1184
  deleteMemory(db, mem.id);
566
1185
  archived++;
567
1186
  }
568
- const orphans = db.prepare(
569
- `DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)`
1187
+ const orphans = agentId ? db.prepare(
1188
+ `DELETE FROM paths
1189
+ WHERE agent_id = ?
1190
+ AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
1191
+ ).run(agentId, agentId) : db.prepare(
1192
+ "DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)"
570
1193
  ).run();
571
1194
  orphansCleaned = orphans.changes;
572
- const memoriesWithSnapshots = db.prepare(
1195
+ const memoriesWithSnapshots = agentId ? db.prepare(
1196
+ `SELECT s.memory_id, COUNT(*) as cnt
1197
+ FROM snapshots s
1198
+ JOIN memories m ON m.id = s.memory_id
1199
+ WHERE m.agent_id = ?
1200
+ GROUP BY s.memory_id HAVING cnt > ?`
1201
+ ).all(agentId, maxSnapshots) : db.prepare(
573
1202
  `SELECT memory_id, COUNT(*) as cnt FROM snapshots
574
- GROUP BY memory_id HAVING cnt > ?`
1203
+ GROUP BY memory_id HAVING cnt > ?`
575
1204
  ).all(maxSnapshots);
576
1205
  for (const { memory_id } of memoriesWithSnapshots) {
577
1206
  const pruned = db.prepare(
@@ -588,20 +1217,31 @@ function runTidy(db, opts) {
588
1217
  }
589
1218
 
590
1219
  // src/sleep/govern.ts
591
- function runGovern(db) {
1220
+ function runGovern(db, opts) {
1221
+ const agentId = opts?.agent_id;
592
1222
  let orphanPaths = 0;
593
1223
  let orphanLinks = 0;
594
1224
  let emptyMemories = 0;
595
1225
  const transaction = db.transaction(() => {
596
- const pathResult = db.prepare("DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)").run();
1226
+ const pathResult = agentId ? db.prepare(
1227
+ `DELETE FROM paths
1228
+ WHERE agent_id = ?
1229
+ AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
1230
+ ).run(agentId, agentId) : db.prepare("DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)").run();
597
1231
  orphanPaths = pathResult.changes;
598
- const linkResult = db.prepare(
1232
+ const linkResult = agentId ? db.prepare(
1233
+ `DELETE FROM links WHERE
1234
+ agent_id = ? AND (
1235
+ source_id NOT IN (SELECT id FROM memories WHERE agent_id = ?) OR
1236
+ target_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)
1237
+ )`
1238
+ ).run(agentId, agentId, agentId) : db.prepare(
599
1239
  `DELETE FROM links WHERE
600
1240
  source_id NOT IN (SELECT id FROM memories) OR
601
1241
  target_id NOT IN (SELECT id FROM memories)`
602
1242
  ).run();
603
1243
  orphanLinks = linkResult.changes;
604
- const emptyResult = db.prepare("DELETE FROM memories WHERE TRIM(content) = ''").run();
1244
+ const emptyResult = agentId ? db.prepare("DELETE FROM memories WHERE agent_id = ? AND TRIM(content) = ''").run(agentId) : db.prepare("DELETE FROM memories WHERE TRIM(content) = ''").run();
605
1245
  emptyMemories = emptyResult.changes;
606
1246
  });
607
1247
  transaction();
@@ -617,7 +1257,7 @@ function guard(db, input) {
617
1257
  return { action: "skip", reason: "Exact duplicate (hash match)", existingId: exactMatch.id };
618
1258
  }
619
1259
  if (input.uri) {
620
- const existingPath = getPathByUri(db, input.uri);
1260
+ const existingPath = getPathByUri(db, input.uri, agentId);
621
1261
  if (existingPath) {
622
1262
  return {
623
1263
  action: "update",
@@ -626,40 +1266,85 @@ function guard(db, input) {
626
1266
  };
627
1267
  }
628
1268
  }
629
- const similar = db.prepare(
630
- `SELECT m.id, m.content, m.type, rank
631
- FROM memories_fts f
632
- JOIN memories m ON m.id = f.id
633
- WHERE memories_fts MATCH ? AND m.agent_id = ?
634
- ORDER BY rank
635
- LIMIT 3`
636
- ).all(escapeFts(input.content), agentId);
637
- if (similar.length > 0 && similar[0].rank < -10) {
638
- const existing = similar[0];
639
- if (existing.type === input.type) {
640
- const merged = `${existing.content}
1269
+ const ftsTokens = tokenize(input.content.slice(0, 200));
1270
+ const ftsQuery = ftsTokens.length > 0 ? ftsTokens.slice(0, 8).map((w) => `"${w}"`).join(" OR ") : null;
1271
+ if (ftsQuery) {
1272
+ try {
1273
+ const similar = db.prepare(
1274
+ `SELECT m.id, m.content, m.type, rank
1275
+ FROM memories_fts f
1276
+ JOIN memories m ON m.id = f.id
1277
+ WHERE memories_fts MATCH ? AND m.agent_id = ?
1278
+ ORDER BY rank
1279
+ LIMIT 3`
1280
+ ).all(ftsQuery, agentId);
1281
+ if (similar.length > 0) {
1282
+ const topRank = Math.abs(similar[0].rank);
1283
+ const tokenCount = ftsTokens.length;
1284
+ const dynamicThreshold = tokenCount * 1.5;
1285
+ if (topRank > dynamicThreshold) {
1286
+ const existing = similar[0];
1287
+ if (existing.type === input.type) {
1288
+ const merged = `${existing.content}
641
1289
 
642
1290
  [Updated] ${input.content}`;
643
- return {
644
- action: "merge",
645
- reason: "Similar content found, merging",
646
- existingId: existing.id,
647
- mergedContent: merged
648
- };
1291
+ return {
1292
+ action: "merge",
1293
+ reason: `Similar content found (score=${topRank.toFixed(1)}, threshold=${dynamicThreshold.toFixed(1)}), merging`,
1294
+ existingId: existing.id,
1295
+ mergedContent: merged
1296
+ };
1297
+ }
1298
+ }
1299
+ }
1300
+ } catch {
649
1301
  }
650
1302
  }
651
- const priority = input.priority ?? (input.type === "identity" ? 0 : input.type === "emotion" ? 1 : 2);
652
- if (priority <= 1) {
653
- if (!input.content.trim()) {
654
- return { action: "skip", reason: "Empty content rejected by gate" };
655
- }
1303
+ const gateResult = fourCriterionGate(input);
1304
+ if (!gateResult.pass) {
1305
+ return { action: "skip", reason: `Gate rejected: ${gateResult.failedCriteria.join(", ")}` };
656
1306
  }
657
1307
  return { action: "add", reason: "Passed all guard checks" };
658
1308
  }
659
- function escapeFts(text) {
660
- const words = text.slice(0, 100).replace(/[^\w\u4e00-\u9fff\s]/g, " ").split(/\s+/).filter((w) => w.length > 1).slice(0, 5);
661
- if (words.length === 0) return '""';
662
- return words.map((w) => `"${w}"`).join(" OR ");
1309
+ function fourCriterionGate(input) {
1310
+ const content = input.content.trim();
1311
+ const failed = [];
1312
+ const priority = input.priority ?? (input.type === "identity" ? 0 : input.type === "emotion" ? 1 : input.type === "knowledge" ? 2 : 3);
1313
+ const minLength = priority <= 1 ? 4 : 8;
1314
+ const specificity = content.length >= minLength ? Math.min(1, content.length / 50) : 0;
1315
+ if (specificity === 0) failed.push(`specificity (too short: ${content.length} < ${minLength} chars)`);
1316
+ const tokens = tokenize(content);
1317
+ const novelty = tokens.length >= 1 ? Math.min(1, tokens.length / 5) : 0;
1318
+ if (novelty === 0) failed.push("novelty (no meaningful tokens after filtering)");
1319
+ const hasCJK = /[\u4e00-\u9fff]/.test(content);
1320
+ const hasCapitalized = /[A-Z][a-z]+/.test(content);
1321
+ const hasNumbers = /\d+/.test(content);
1322
+ const hasURI = /\w+:\/\//.test(content);
1323
+ const hasEntityMarkers = /[@#]/.test(content);
1324
+ const hasMeaningfulLength = content.length >= 15;
1325
+ const topicSignals = [hasCJK, hasCapitalized, hasNumbers, hasURI, hasEntityMarkers, hasMeaningfulLength].filter(Boolean).length;
1326
+ const relevance = topicSignals >= 1 ? Math.min(1, topicSignals / 3) : 0;
1327
+ if (relevance === 0) failed.push("relevance (no identifiable topics/entities)");
1328
+ const allCaps = content === content.toUpperCase() && content.length > 20 && /^[A-Z\s]+$/.test(content);
1329
+ const hasWhitespaceOrPunctuation = /[\s,。!?,.!?;;::]/.test(content) || content.length < 30;
1330
+ const excessiveRepetition = /(.)\1{9,}/.test(content);
1331
+ let coherence = 1;
1332
+ if (allCaps) {
1333
+ coherence -= 0.5;
1334
+ }
1335
+ if (!hasWhitespaceOrPunctuation) {
1336
+ coherence -= 0.3;
1337
+ }
1338
+ if (excessiveRepetition) {
1339
+ coherence -= 0.5;
1340
+ }
1341
+ coherence = Math.max(0, coherence);
1342
+ if (coherence < 0.3) failed.push("coherence (garbled or malformed content)");
1343
+ return {
1344
+ pass: failed.length === 0,
1345
+ scores: { specificity, novelty, relevance, coherence },
1346
+ failedCriteria: failed
1347
+ };
663
1348
  }
664
1349
 
665
1350
  // src/sleep/sync.ts
@@ -706,13 +1391,16 @@ function syncOne(db, input) {
706
1391
  }
707
1392
 
708
1393
  // src/bin/agent-memory.ts
709
- import { existsSync, readFileSync, readdirSync } from "fs";
1394
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
710
1395
  import { resolve, basename } from "path";
711
1396
  var args = process.argv.slice(2);
712
1397
  var command = args[0];
713
1398
  function getDbPath() {
714
1399
  return process.env.AGENT_MEMORY_DB ?? "./agent-memory.db";
715
1400
  }
1401
+ function getAgentId() {
1402
+ return process.env.AGENT_MEMORY_AGENT_ID ?? "default";
1403
+ }
716
1404
  function printHelp() {
717
1405
  console.log(`
718
1406
  \u{1F9E0} AgentMemory v2 \u2014 Sleep-cycle memory for AI agents
@@ -721,12 +1409,16 @@ Usage: agent-memory <command> [options]
721
1409
 
722
1410
  Commands:
723
1411
  init Create database
1412
+ db:migrate Run schema migrations (no-op if up-to-date)
1413
+ embed [--limit N] Embed missing memories (requires provider)
724
1414
  remember <content> [--uri X] [--type T] Store a memory
725
1415
  recall <query> [--limit N] Search memories
726
1416
  boot Load identity memories
727
1417
  status Show statistics
728
1418
  reflect [decay|tidy|govern|all] Run sleep cycle
1419
+ reindex Rebuild FTS index with jieba tokenizer
729
1420
  migrate <dir> Import from Markdown files
1421
+ export <dir> Export memories to Markdown files
730
1422
  help Show this help
731
1423
 
732
1424
  Environment:
@@ -739,178 +1431,254 @@ function getFlag(flag) {
739
1431
  if (idx >= 0 && idx + 1 < args.length) return args[idx + 1];
740
1432
  return void 0;
741
1433
  }
742
- try {
743
- switch (command) {
744
- case "init": {
745
- const dbPath = getDbPath();
746
- openDatabase({ path: dbPath });
747
- console.log(`\u2705 Database created at ${dbPath}`);
748
- break;
749
- }
750
- case "remember": {
751
- const content = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
752
- if (!content) {
753
- console.error("Usage: agent-memory remember <content>");
754
- process.exit(1);
755
- }
756
- const db = openDatabase({ path: getDbPath() });
757
- const uri = getFlag("--uri");
758
- const type = getFlag("--type") ?? "knowledge";
759
- const result = syncOne(db, { content, type, uri });
760
- console.log(`${result.action}: ${result.reason}${result.memoryId ? ` (${result.memoryId.slice(0, 8)})` : ""}`);
761
- db.close();
762
- break;
763
- }
764
- case "recall": {
765
- const query = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
766
- if (!query) {
767
- console.error("Usage: agent-memory recall <query>");
768
- process.exit(1);
1434
+ async function main() {
1435
+ try {
1436
+ switch (command) {
1437
+ case "init": {
1438
+ const dbPath = getDbPath();
1439
+ openDatabase({ path: dbPath });
1440
+ console.log(`\u2705 Database created at ${dbPath}`);
1441
+ break;
769
1442
  }
770
- const db = openDatabase({ path: getDbPath() });
771
- const limit = parseInt(getFlag("--limit") ?? "10");
772
- const { intent } = classifyIntent(query);
773
- const strategy = getStrategy(intent);
774
- const raw = searchBM25(db, query, { limit: limit * 2 });
775
- const results = rerank(raw, { ...strategy, limit });
776
- console.log(`\u{1F50D} Intent: ${intent} | Results: ${results.length}
777
- `);
778
- for (const r of results) {
779
- const p = ["\u{1F534}", "\u{1F7E0}", "\u{1F7E1}", "\u26AA"][r.memory.priority];
780
- const v = (r.memory.vitality * 100).toFixed(0);
781
- console.log(`${p} P${r.memory.priority} [${v}%] ${r.memory.content.slice(0, 80)}`);
1443
+ case "db:migrate": {
1444
+ const dbPath = getDbPath();
1445
+ const db = openDatabase({ path: dbPath });
1446
+ const version = db.prepare("SELECT value FROM schema_meta WHERE key = 'version'").get()?.value;
1447
+ console.log(`\u2705 Schema version: ${version ?? "unknown"} (${dbPath})`);
1448
+ db.close();
1449
+ break;
782
1450
  }
783
- db.close();
784
- break;
785
- }
786
- case "boot": {
787
- const db = openDatabase({ path: getDbPath() });
788
- const result = boot(db);
789
- console.log(`\u{1F9E0} Boot: ${result.identityMemories.length} identity memories loaded
790
- `);
791
- for (const m of result.identityMemories) {
792
- console.log(` \u{1F534} ${m.content.slice(0, 100)}`);
1451
+ case "embed": {
1452
+ const provider = getEmbeddingProviderFromEnv();
1453
+ if (!provider) {
1454
+ console.error("Embedding provider not configured. Set AGENT_MEMORY_EMBEDDINGS_PROVIDER=openai|qwen and the corresponding API key.");
1455
+ process.exit(1);
1456
+ }
1457
+ const db = openDatabase({ path: getDbPath() });
1458
+ const agentId = getAgentId();
1459
+ const limit = parseInt(getFlag("--limit") ?? "200");
1460
+ const r = await embedMissingForAgent(db, provider, { agent_id: agentId, limit });
1461
+ console.log(`\u2705 Embedded: ${r.embedded}/${r.scanned} (agent_id=${agentId}, model=${provider.model})`);
1462
+ db.close();
1463
+ break;
793
1464
  }
794
- if (result.bootPaths.length) {
795
- console.log(`
796
- \u{1F4CD} Boot paths: ${result.bootPaths.join(", ")}`);
1465
+ case "remember": {
1466
+ const content = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
1467
+ if (!content) {
1468
+ console.error("Usage: agent-memory remember <content>");
1469
+ process.exit(1);
1470
+ }
1471
+ const db = openDatabase({ path: getDbPath() });
1472
+ const uri = getFlag("--uri");
1473
+ const type = getFlag("--type") ?? "knowledge";
1474
+ const agentId = getAgentId();
1475
+ const provider = getEmbeddingProviderFromEnv();
1476
+ const result = syncOne(db, { content, type, uri, agent_id: agentId });
1477
+ if (provider && result.memoryId && (result.action === "added" || result.action === "updated" || result.action === "merged")) {
1478
+ try {
1479
+ await embedMemory(db, result.memoryId, provider, { agent_id: agentId });
1480
+ } catch {
1481
+ }
1482
+ }
1483
+ console.log(`${result.action}: ${result.reason}${result.memoryId ? ` (${result.memoryId.slice(0, 8)})` : ""}`);
1484
+ db.close();
1485
+ break;
797
1486
  }
798
- db.close();
799
- break;
800
- }
801
- case "status": {
802
- const db = openDatabase({ path: getDbPath() });
803
- const stats = countMemories(db);
804
- const lowVit = db.prepare("SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1").get().c;
805
- const paths = db.prepare("SELECT COUNT(*) as c FROM paths").get().c;
806
- const links = db.prepare("SELECT COUNT(*) as c FROM links").get().c;
807
- const snaps = db.prepare("SELECT COUNT(*) as c FROM snapshots").get().c;
808
- console.log("\u{1F9E0} AgentMemory Status\n");
809
- console.log(` Total memories: ${stats.total}`);
810
- console.log(` By type: ${Object.entries(stats.by_type).map(([k, v]) => `${k}=${v}`).join(", ")}`);
811
- console.log(` By priority: ${Object.entries(stats.by_priority).map(([k, v]) => `${k}=${v}`).join(", ")}`);
812
- console.log(` Paths: ${paths} | Links: ${links} | Snapshots: ${snaps}`);
813
- console.log(` Low vitality (<10%): ${lowVit}`);
814
- db.close();
815
- break;
816
- }
817
- case "reflect": {
818
- const phase = args[1] ?? "all";
819
- const db = openDatabase({ path: getDbPath() });
820
- console.log(`\u{1F319} Running ${phase} phase...
1487
+ case "recall": {
1488
+ const query = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
1489
+ if (!query) {
1490
+ console.error("Usage: agent-memory recall <query>");
1491
+ process.exit(1);
1492
+ }
1493
+ const db = openDatabase({ path: getDbPath() });
1494
+ const limit = parseInt(getFlag("--limit") ?? "10");
1495
+ const { intent } = classifyIntent(query);
1496
+ const strategy = getStrategy(intent);
1497
+ const agentId = getAgentId();
1498
+ const provider = getEmbeddingProviderFromEnv();
1499
+ const raw = await searchHybrid(db, query, { agent_id: agentId, embeddingProvider: provider, limit: limit * 2 });
1500
+ const results = rerank(raw, { ...strategy, limit });
1501
+ console.log(`\u{1F50D} Intent: ${intent} | Results: ${results.length}
821
1502
  `);
822
- if (phase === "decay" || phase === "all") {
823
- const r = runDecay(db);
824
- console.log(` Decay: ${r.updated} updated, ${r.decayed} decayed, ${r.belowThreshold} below threshold`);
825
- }
826
- if (phase === "tidy" || phase === "all") {
827
- const r = runTidy(db);
828
- console.log(` Tidy: ${r.archived} archived, ${r.orphansCleaned} orphans, ${r.snapshotsPruned} snapshots pruned`);
829
- }
830
- if (phase === "govern" || phase === "all") {
831
- const r = runGovern(db);
832
- console.log(` Govern: ${r.orphanPaths} paths, ${r.orphanLinks} links, ${r.emptyMemories} empty cleaned`);
1503
+ for (const r of results) {
1504
+ const p = ["\u{1F534}", "\u{1F7E0}", "\u{1F7E1}", "\u26AA"][r.memory.priority];
1505
+ const v = (r.memory.vitality * 100).toFixed(0);
1506
+ console.log(`${p} P${r.memory.priority} [${v}%] ${r.memory.content.slice(0, 80)}`);
1507
+ }
1508
+ db.close();
1509
+ break;
833
1510
  }
834
- db.close();
835
- break;
836
- }
837
- case "migrate": {
838
- const dir = args[1];
839
- if (!dir) {
840
- console.error("Usage: agent-memory migrate <directory>");
841
- process.exit(1);
1511
+ case "boot": {
1512
+ const db = openDatabase({ path: getDbPath() });
1513
+ const result = boot(db, { agent_id: getAgentId() });
1514
+ console.log(`\u{1F9E0} Boot: ${result.identityMemories.length} identity memories loaded
1515
+ `);
1516
+ for (const m of result.identityMemories) {
1517
+ console.log(` \u{1F534} ${m.content.slice(0, 100)}`);
1518
+ }
1519
+ if (result.bootPaths.length) {
1520
+ console.log(`
1521
+ \u{1F4CD} Boot paths: ${result.bootPaths.join(", ")}`);
1522
+ }
1523
+ db.close();
1524
+ break;
842
1525
  }
843
- const dirPath = resolve(dir);
844
- if (!existsSync(dirPath)) {
845
- console.error(`Directory not found: ${dirPath}`);
846
- process.exit(1);
1526
+ case "status": {
1527
+ const db = openDatabase({ path: getDbPath() });
1528
+ const agentId = getAgentId();
1529
+ const stats = countMemories(db, agentId);
1530
+ const lowVit = db.prepare("SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?").get(agentId).c;
1531
+ const paths = db.prepare("SELECT COUNT(*) as c FROM paths WHERE agent_id = ?").get(agentId).c;
1532
+ const links = db.prepare("SELECT COUNT(*) as c FROM links WHERE agent_id = ?").get(agentId).c;
1533
+ const snaps = db.prepare(
1534
+ `SELECT COUNT(*) as c FROM snapshots s
1535
+ JOIN memories m ON m.id = s.memory_id
1536
+ WHERE m.agent_id = ?`
1537
+ ).get(agentId).c;
1538
+ console.log("\u{1F9E0} AgentMemory Status\n");
1539
+ console.log(` Total memories: ${stats.total}`);
1540
+ console.log(` By type: ${Object.entries(stats.by_type).map(([k, v]) => `${k}=${v}`).join(", ")}`);
1541
+ console.log(` By priority: ${Object.entries(stats.by_priority).map(([k, v]) => `${k}=${v}`).join(", ")}`);
1542
+ console.log(` Paths: ${paths} | Links: ${links} | Snapshots: ${snaps}`);
1543
+ console.log(` Low vitality (<10%): ${lowVit}`);
1544
+ db.close();
1545
+ break;
847
1546
  }
848
- const db = openDatabase({ path: getDbPath() });
849
- let imported = 0;
850
- const memoryMd = resolve(dirPath, "MEMORY.md");
851
- if (existsSync(memoryMd)) {
852
- const content = readFileSync(memoryMd, "utf-8");
853
- const sections = content.split(/^## /m).filter((s) => s.trim());
854
- for (const section of sections) {
855
- const lines = section.split("\n");
856
- const title = lines[0]?.trim();
857
- const body = lines.slice(1).join("\n").trim();
858
- if (!body) continue;
859
- const type = title?.toLowerCase().includes("\u5173\u4E8E") || title?.toLowerCase().includes("about") ? "identity" : "knowledge";
860
- const uri = `knowledge://memory-md/${title?.replace(/[^a-z0-9\u4e00-\u9fff]/gi, "-").toLowerCase()}`;
861
- syncOne(db, { content: `## ${title}
862
- ${body}`, type, uri, source: "migrate:MEMORY.md" });
863
- imported++;
1547
+ case "reflect": {
1548
+ const phase = args[1] ?? "all";
1549
+ const db = openDatabase({ path: getDbPath() });
1550
+ const agentId = getAgentId();
1551
+ console.log(`\u{1F319} Running ${phase} phase...
1552
+ `);
1553
+ if (phase === "decay" || phase === "all") {
1554
+ const r = runDecay(db, { agent_id: agentId });
1555
+ console.log(` Decay: ${r.updated} updated, ${r.decayed} decayed, ${r.belowThreshold} below threshold`);
1556
+ }
1557
+ if (phase === "tidy" || phase === "all") {
1558
+ const r = runTidy(db, { agent_id: agentId });
1559
+ console.log(` Tidy: ${r.archived} archived, ${r.orphansCleaned} orphans, ${r.snapshotsPruned} snapshots pruned`);
1560
+ }
1561
+ if (phase === "govern" || phase === "all") {
1562
+ const r = runGovern(db, { agent_id: agentId });
1563
+ console.log(` Govern: ${r.orphanPaths} paths, ${r.orphanLinks} links, ${r.emptyMemories} empty cleaned`);
864
1564
  }
865
- console.log(`\u{1F4C4} MEMORY.md: ${sections.length} sections imported`);
1565
+ db.close();
1566
+ break;
866
1567
  }
867
- const mdFiles = readdirSync(dirPath).filter((f) => /^\d{4}-\d{2}-\d{2}\.md$/.test(f)).sort();
868
- for (const file of mdFiles) {
869
- const content = readFileSync(resolve(dirPath, file), "utf-8");
870
- const date = basename(file, ".md");
871
- syncOne(db, {
872
- content,
873
- type: "event",
874
- uri: `event://journal/${date}`,
875
- source: `migrate:${file}`
1568
+ case "reindex": {
1569
+ const db = openDatabase({ path: getDbPath() });
1570
+ const memories = db.prepare("SELECT id, content FROM memories").all();
1571
+ db.exec("DELETE FROM memories_fts");
1572
+ const insert = db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)");
1573
+ let count = 0;
1574
+ const txn = db.transaction(() => {
1575
+ for (const mem of memories) {
1576
+ insert.run(mem.id, tokenizeForIndex(mem.content));
1577
+ count++;
1578
+ }
876
1579
  });
877
- imported++;
1580
+ txn();
1581
+ console.log(`\u{1F504} Reindexed ${count} memories with jieba tokenizer`);
1582
+ db.close();
1583
+ break;
1584
+ }
1585
+ case "export": {
1586
+ const dir = args[1];
1587
+ if (!dir) {
1588
+ console.error("Usage: agent-memory export <directory>");
1589
+ process.exit(1);
1590
+ }
1591
+ const dirPath = resolve(dir);
1592
+ const db = openDatabase({ path: getDbPath() });
1593
+ const agentId = getAgentId();
1594
+ const result = exportMemories(db, dirPath, { agent_id: agentId });
1595
+ console.log(`\u2705 Export complete: ${result.exported} items to ${dirPath} (${result.files.length} files)`);
1596
+ db.close();
1597
+ break;
878
1598
  }
879
- if (mdFiles.length) console.log(`\u{1F4DD} Journals: ${mdFiles.length} files imported`);
880
- const weeklyDir = resolve(dirPath, "weekly");
881
- if (existsSync(weeklyDir)) {
882
- const weeklyFiles = readdirSync(weeklyDir).filter((f) => f.endsWith(".md"));
883
- for (const file of weeklyFiles) {
884
- const content = readFileSync(resolve(weeklyDir, file), "utf-8");
885
- const week = basename(file, ".md");
1599
+ case "migrate": {
1600
+ const dir = args[1];
1601
+ if (!dir) {
1602
+ console.error("Usage: agent-memory migrate <directory>");
1603
+ process.exit(1);
1604
+ }
1605
+ const dirPath = resolve(dir);
1606
+ if (!existsSync2(dirPath)) {
1607
+ console.error(`Directory not found: ${dirPath}`);
1608
+ process.exit(1);
1609
+ }
1610
+ const db = openDatabase({ path: getDbPath() });
1611
+ const agentId = getAgentId();
1612
+ let imported = 0;
1613
+ const memoryFile = ["MEMORY.md", "MEMORY.qmd"].map((f) => resolve(dirPath, f)).find((p) => existsSync2(p));
1614
+ if (memoryFile) {
1615
+ const content = readFileSync2(memoryFile, "utf-8");
1616
+ const sections = content.split(/^## /m).filter((s) => s.trim());
1617
+ for (const section of sections) {
1618
+ const lines = section.split("\n");
1619
+ const title = lines[0]?.trim();
1620
+ const body = lines.slice(1).join("\n").trim();
1621
+ if (!body) continue;
1622
+ const type = title?.toLowerCase().includes("\u5173\u4E8E") || title?.toLowerCase().includes("about") ? "identity" : "knowledge";
1623
+ const uri = `knowledge://memory-md/${title?.replace(/[^a-z0-9\u4e00-\u9fff]/gi, "-").toLowerCase()}`;
1624
+ syncOne(db, { content: `## ${title}
1625
+ ${body}`, type, uri, source: `migrate:${basename(memoryFile)}`, agent_id: agentId });
1626
+ imported++;
1627
+ }
1628
+ console.log(`\u{1F4C4} ${basename(memoryFile)}: ${sections.length} sections imported`);
1629
+ }
1630
+ const mdFiles = readdirSync(dirPath).filter((f) => /^\d{4}-\d{2}-\d{2}\.(md|qmd)$/.test(f)).sort();
1631
+ for (const file of mdFiles) {
1632
+ const content = readFileSync2(resolve(dirPath, file), "utf-8");
1633
+ const date = file.replace(/\.(md|qmd)$/i, "");
886
1634
  syncOne(db, {
887
1635
  content,
888
- type: "knowledge",
889
- uri: `knowledge://weekly/${week}`,
890
- source: `migrate:weekly/${file}`
1636
+ type: "event",
1637
+ uri: `event://journal/${date}`,
1638
+ source: `migrate:${file}`,
1639
+ agent_id: agentId
891
1640
  });
892
1641
  imported++;
893
1642
  }
894
- if (weeklyFiles.length) console.log(`\u{1F4E6} Weekly: ${weeklyFiles.length} files imported`);
895
- }
896
- console.log(`
1643
+ if (mdFiles.length) console.log(`\u{1F4DD} Journals: ${mdFiles.length} files imported`);
1644
+ const weeklyDir = resolve(dirPath, "weekly");
1645
+ if (existsSync2(weeklyDir)) {
1646
+ const weeklyFiles = readdirSync(weeklyDir).filter((f) => f.endsWith(".md") || f.endsWith(".qmd"));
1647
+ for (const file of weeklyFiles) {
1648
+ const content = readFileSync2(resolve(weeklyDir, file), "utf-8");
1649
+ const week = file.replace(/\.(md|qmd)$/i, "");
1650
+ syncOne(db, {
1651
+ content,
1652
+ type: "knowledge",
1653
+ uri: `knowledge://weekly/${week}`,
1654
+ source: `migrate:weekly/${file}`,
1655
+ agent_id: agentId
1656
+ });
1657
+ imported++;
1658
+ }
1659
+ if (weeklyFiles.length) console.log(`\u{1F4E6} Weekly: ${weeklyFiles.length} files imported`);
1660
+ }
1661
+ console.log(`
897
1662
  \u2705 Migration complete: ${imported} items imported`);
898
- db.close();
899
- break;
900
- }
901
- case "help":
902
- case "--help":
903
- case "-h":
904
- case void 0:
905
- printHelp();
906
- break;
907
- default:
908
- console.error(`Unknown command: ${command}`);
909
- printHelp();
910
- process.exit(1);
911
- }
912
- } catch (err) {
913
- console.error("Error:", err.message);
914
- process.exit(1);
1663
+ db.close();
1664
+ break;
1665
+ }
1666
+ case "help":
1667
+ case "--help":
1668
+ case "-h":
1669
+ case void 0:
1670
+ printHelp();
1671
+ break;
1672
+ default:
1673
+ console.error(`Unknown command: ${command}`);
1674
+ printHelp();
1675
+ process.exit(1);
1676
+ }
1677
+ } catch (err) {
1678
+ const message = err instanceof Error ? err.message : String(err);
1679
+ console.error("Error:", message);
1680
+ process.exit(1);
1681
+ }
915
1682
  }
1683
+ main();
916
1684
  //# sourceMappingURL=agent-memory.js.map