@smyslenny/agent-memory 2.0.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.
@@ -0,0 +1,916 @@
1
+ #!/usr/bin/env node
2
+ // AgentMemory v2 — Sleep-cycle memory for AI agents
3
+
4
+ // src/core/db.ts
5
+ import Database from "better-sqlite3";
6
+ import { randomUUID } from "crypto";
7
+ var SCHEMA_VERSION = 1;
8
+ var SCHEMA_SQL = `
9
+ -- Memory entries
10
+ CREATE TABLE IF NOT EXISTS memories (
11
+ id TEXT PRIMARY KEY,
12
+ content TEXT NOT NULL,
13
+ type TEXT NOT NULL CHECK(type IN ('identity','emotion','knowledge','event')),
14
+ priority INTEGER NOT NULL DEFAULT 2 CHECK(priority BETWEEN 0 AND 3),
15
+ emotion_val REAL NOT NULL DEFAULT 0.0,
16
+ vitality REAL NOT NULL DEFAULT 1.0,
17
+ stability REAL NOT NULL DEFAULT 1.0,
18
+ access_count INTEGER NOT NULL DEFAULT 0,
19
+ last_accessed TEXT,
20
+ created_at TEXT NOT NULL,
21
+ updated_at TEXT NOT NULL,
22
+ source TEXT,
23
+ agent_id TEXT NOT NULL DEFAULT 'default',
24
+ hash TEXT,
25
+ UNIQUE(hash, agent_id)
26
+ );
27
+
28
+ -- URI paths (Content-Path separation, from nocturne)
29
+ CREATE TABLE IF NOT EXISTS paths (
30
+ id TEXT PRIMARY KEY,
31
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
32
+ uri TEXT NOT NULL UNIQUE,
33
+ alias TEXT,
34
+ domain TEXT NOT NULL,
35
+ created_at TEXT NOT NULL
36
+ );
37
+
38
+ -- Association network (knowledge graph)
39
+ CREATE TABLE IF NOT EXISTS links (
40
+ source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
41
+ target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
42
+ relation TEXT NOT NULL,
43
+ weight REAL NOT NULL DEFAULT 1.0,
44
+ created_at TEXT NOT NULL,
45
+ PRIMARY KEY (source_id, target_id)
46
+ );
47
+
48
+ -- Snapshots (version control, from nocturne + Memory Palace)
49
+ CREATE TABLE IF NOT EXISTS snapshots (
50
+ id TEXT PRIMARY KEY,
51
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
52
+ content TEXT NOT NULL,
53
+ changed_by TEXT,
54
+ action TEXT NOT NULL CHECK(action IN ('create','update','delete','merge')),
55
+ created_at TEXT NOT NULL
56
+ );
57
+
58
+ -- Full-text search index (BM25)
59
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
60
+ id UNINDEXED,
61
+ content,
62
+ tokenize='unicode61'
63
+ );
64
+
65
+ -- Schema version tracking
66
+ CREATE TABLE IF NOT EXISTS schema_meta (
67
+ key TEXT PRIMARY KEY,
68
+ value TEXT NOT NULL
69
+ );
70
+
71
+ -- Indexes for common queries
72
+ CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
73
+ CREATE INDEX IF NOT EXISTS idx_memories_priority ON memories(priority);
74
+ CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
75
+ CREATE INDEX IF NOT EXISTS idx_memories_vitality ON memories(vitality);
76
+ CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
77
+ CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
78
+ 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
+ `;
82
+ function openDatabase(opts) {
83
+ const db = new Database(opts.path);
84
+ if (opts.walMode !== false) {
85
+ db.pragma("journal_mode = WAL");
86
+ }
87
+ db.pragma("foreign_keys = ON");
88
+ db.pragma("busy_timeout = 5000");
89
+ 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
+ );
96
+ }
97
+ return db;
98
+ }
99
+ function now() {
100
+ return (/* @__PURE__ */ new Date()).toISOString();
101
+ }
102
+ function newId() {
103
+ return randomUUID();
104
+ }
105
+
106
+ // src/core/memory.ts
107
+ import { createHash } from "crypto";
108
+ function contentHash(content) {
109
+ return createHash("sha256").update(content.trim()).digest("hex").slice(0, 16);
110
+ }
111
+ var TYPE_PRIORITY = {
112
+ identity: 0,
113
+ emotion: 1,
114
+ knowledge: 2,
115
+ event: 3
116
+ };
117
+ var PRIORITY_STABILITY = {
118
+ 0: Infinity,
119
+ // P0: never decays
120
+ 1: 365,
121
+ // P1: 365-day half-life
122
+ 2: 90,
123
+ // P2: 90-day half-life
124
+ 3: 14
125
+ // P3: 14-day half-life
126
+ };
127
+ function createMemory(db, input) {
128
+ const hash = contentHash(input.content);
129
+ const agentId = input.agent_id ?? "default";
130
+ const priority = input.priority ?? TYPE_PRIORITY[input.type];
131
+ const stability = PRIORITY_STABILITY[priority];
132
+ const existing = db.prepare("SELECT id FROM memories WHERE hash = ? AND agent_id = ?").get(hash, agentId);
133
+ if (existing) {
134
+ return null;
135
+ }
136
+ const id = newId();
137
+ const timestamp = now();
138
+ db.prepare(
139
+ `INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
140
+ access_count, created_at, updated_at, source, agent_id, hash)
141
+ VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?)`
142
+ ).run(
143
+ id,
144
+ input.content,
145
+ input.type,
146
+ priority,
147
+ input.emotion_val ?? 0,
148
+ stability === Infinity ? 999999 : stability,
149
+ timestamp,
150
+ timestamp,
151
+ input.source ?? null,
152
+ agentId,
153
+ hash
154
+ );
155
+ db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, input.content);
156
+ return getMemory(db, id);
157
+ }
158
+ function getMemory(db, id) {
159
+ return db.prepare("SELECT * FROM memories WHERE id = ?").get(id) ?? null;
160
+ }
161
+ function updateMemory(db, id, input) {
162
+ const existing = getMemory(db, id);
163
+ if (!existing) return null;
164
+ const fields = [];
165
+ const values = [];
166
+ if (input.content !== void 0) {
167
+ fields.push("content = ?", "hash = ?");
168
+ values.push(input.content, contentHash(input.content));
169
+ }
170
+ if (input.type !== void 0) {
171
+ fields.push("type = ?");
172
+ values.push(input.type);
173
+ }
174
+ if (input.priority !== void 0) {
175
+ fields.push("priority = ?");
176
+ values.push(input.priority);
177
+ }
178
+ if (input.emotion_val !== void 0) {
179
+ fields.push("emotion_val = ?");
180
+ values.push(input.emotion_val);
181
+ }
182
+ if (input.vitality !== void 0) {
183
+ fields.push("vitality = ?");
184
+ values.push(input.vitality);
185
+ }
186
+ if (input.stability !== void 0) {
187
+ fields.push("stability = ?");
188
+ values.push(input.stability);
189
+ }
190
+ if (input.source !== void 0) {
191
+ fields.push("source = ?");
192
+ values.push(input.source);
193
+ }
194
+ fields.push("updated_at = ?");
195
+ values.push(now());
196
+ values.push(id);
197
+ db.prepare(`UPDATE memories SET ${fields.join(", ")} WHERE id = ?`).run(...values);
198
+ if (input.content !== void 0) {
199
+ db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
200
+ db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, input.content);
201
+ }
202
+ return getMemory(db, id);
203
+ }
204
+ function deleteMemory(db, id) {
205
+ db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
206
+ const result = db.prepare("DELETE FROM memories WHERE id = ?").run(id);
207
+ return result.changes > 0;
208
+ }
209
+ function listMemories(db, opts) {
210
+ const conditions = [];
211
+ const params = [];
212
+ if (opts?.agent_id) {
213
+ conditions.push("agent_id = ?");
214
+ params.push(opts.agent_id);
215
+ }
216
+ if (opts?.type) {
217
+ conditions.push("type = ?");
218
+ params.push(opts.type);
219
+ }
220
+ if (opts?.priority !== void 0) {
221
+ conditions.push("priority = ?");
222
+ params.push(opts.priority);
223
+ }
224
+ if (opts?.min_vitality !== void 0) {
225
+ conditions.push("vitality >= ?");
226
+ params.push(opts.min_vitality);
227
+ }
228
+ const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
229
+ const limit = opts?.limit ?? 100;
230
+ const offset = opts?.offset ?? 0;
231
+ return db.prepare(`SELECT * FROM memories ${where} ORDER BY priority ASC, updated_at DESC LIMIT ? OFFSET ?`).all(...params, limit, offset);
232
+ }
233
+ function recordAccess(db, id, growthFactor = 1.5) {
234
+ const mem = getMemory(db, id);
235
+ if (!mem) return;
236
+ const newStability = Math.min(999999, mem.stability * growthFactor);
237
+ db.prepare(
238
+ `UPDATE memories SET access_count = access_count + 1, last_accessed = ?, stability = ?,
239
+ vitality = MIN(1.0, vitality * 1.2) WHERE id = ?`
240
+ ).run(now(), newStability, id);
241
+ }
242
+ function countMemories(db, agent_id = "default") {
243
+ const total = db.prepare("SELECT COUNT(*) as c FROM memories WHERE agent_id = ?").get(agent_id).c;
244
+ const byType = db.prepare("SELECT type, COUNT(*) as c FROM memories WHERE agent_id = ? GROUP BY type").all(agent_id);
245
+ const byPriority = db.prepare("SELECT priority, COUNT(*) as c FROM memories WHERE agent_id = ? GROUP BY priority").all(agent_id);
246
+ return {
247
+ total,
248
+ by_type: Object.fromEntries(byType.map((r) => [r.type, r.c])),
249
+ by_priority: Object.fromEntries(byPriority.map((r) => [`P${r.priority}`, r.c]))
250
+ };
251
+ }
252
+
253
+ // src/search/bm25.ts
254
+ function searchBM25(db, query, opts) {
255
+ const limit = opts?.limit ?? 20;
256
+ 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);
279
+ }
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 ");
299
+ }
300
+
301
+ // src/search/intent.ts
302
+ var INTENT_PATTERNS = {
303
+ factual: [
304
+ /^(what|who|where|which|how much|how many)/i,
305
+ /是(什么|谁|哪)/,
306
+ /叫什么/,
307
+ /名字/,
308
+ /地址/,
309
+ /号码/,
310
+ /密码/,
311
+ /配置/,
312
+ /设置/
313
+ ],
314
+ temporal: [
315
+ /^(when|what time|how long)/i,
316
+ /(yesterday|today|last week|recently|ago|before|after)/i,
317
+ /什么时候/,
318
+ /(昨天|今天|上周|最近|以前|之前|之后)/,
319
+ /\d{4}[-/]\d{1,2}/,
320
+ /(几月|几号|几点)/
321
+ ],
322
+ causal: [
323
+ /^(why|how come|what caused)/i,
324
+ /^(because|due to|reason)/i,
325
+ /为什么/,
326
+ /原因/,
327
+ /导致/,
328
+ /怎么回事/,
329
+ /为啥/
330
+ ],
331
+ exploratory: [
332
+ /^(how|tell me about|explain|describe)/i,
333
+ /^(what do you think|what about)/i,
334
+ /怎么样/,
335
+ /介绍/,
336
+ /说说/,
337
+ /讲讲/,
338
+ /有哪些/,
339
+ /关于/
340
+ ]
341
+ };
342
+ function classifyIntent(query) {
343
+ const scores = {
344
+ factual: 0,
345
+ exploratory: 0,
346
+ temporal: 0,
347
+ causal: 0
348
+ };
349
+ for (const [intent, patterns] of Object.entries(INTENT_PATTERNS)) {
350
+ for (const pattern of patterns) {
351
+ if (pattern.test(query)) {
352
+ scores[intent] += 1;
353
+ }
354
+ }
355
+ }
356
+ let maxIntent = "factual";
357
+ let maxScore = 0;
358
+ for (const [intent, score] of Object.entries(scores)) {
359
+ if (score > maxScore) {
360
+ maxScore = score;
361
+ maxIntent = intent;
362
+ }
363
+ }
364
+ const totalScore = Object.values(scores).reduce((a, b) => a + b, 0);
365
+ const confidence = totalScore > 0 ? maxScore / totalScore : 0.5;
366
+ return { intent: maxIntent, confidence };
367
+ }
368
+ function getStrategy(intent) {
369
+ switch (intent) {
370
+ case "factual":
371
+ return { boostRecent: false, boostPriority: true, limit: 5 };
372
+ case "temporal":
373
+ return { boostRecent: true, boostPriority: false, limit: 10 };
374
+ case "causal":
375
+ return { boostRecent: false, boostPriority: false, limit: 10 };
376
+ case "exploratory":
377
+ return { boostRecent: false, boostPriority: false, limit: 15 };
378
+ }
379
+ }
380
+
381
+ // src/search/rerank.ts
382
+ function rerank(results, opts) {
383
+ const now2 = Date.now();
384
+ const scored = results.map((r) => {
385
+ let finalScore = r.score;
386
+ if (opts.boostPriority) {
387
+ const priorityMultiplier = [4, 3, 2, 1][r.memory.priority] ?? 1;
388
+ finalScore *= priorityMultiplier;
389
+ }
390
+ if (opts.boostRecent && r.memory.updated_at) {
391
+ const age = now2 - new Date(r.memory.updated_at).getTime();
392
+ const daysSinceUpdate = age / (1e3 * 60 * 60 * 24);
393
+ const recencyBoost = Math.max(0.1, 1 / (1 + daysSinceUpdate * 0.1));
394
+ finalScore *= recencyBoost;
395
+ }
396
+ finalScore *= Math.max(0.1, r.memory.vitality);
397
+ return { ...r, score: finalScore };
398
+ });
399
+ scored.sort((a, b) => b.score - a.score);
400
+ return scored.slice(0, opts.limit);
401
+ }
402
+
403
+ // src/core/path.ts
404
+ var DEFAULT_DOMAINS = /* @__PURE__ */ new Set(["core", "emotion", "knowledge", "event", "system"]);
405
+ function parseUri(uri) {
406
+ const match = uri.match(/^([a-z]+):\/\/(.+)$/);
407
+ if (!match) throw new Error(`Invalid URI: ${uri}. Expected format: domain://path`);
408
+ return { domain: match[1], path: match[2] };
409
+ }
410
+ function createPath(db, memoryId, uri, alias, validDomains) {
411
+ const { domain } = parseUri(uri);
412
+ const domains = validDomains ?? DEFAULT_DOMAINS;
413
+ if (!domains.has(domain)) {
414
+ throw new Error(`Invalid domain "${domain}". Valid: ${[...domains].join(", ")}`);
415
+ }
416
+ const existing = db.prepare("SELECT id FROM paths WHERE uri = ?").get(uri);
417
+ if (existing) {
418
+ throw new Error(`URI already exists: ${uri}`);
419
+ }
420
+ const id = newId();
421
+ db.prepare(
422
+ "INSERT INTO paths (id, memory_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?)"
423
+ ).run(id, memoryId, uri, alias ?? null, domain, now());
424
+ return getPath(db, id);
425
+ }
426
+ function getPath(db, id) {
427
+ return db.prepare("SELECT * FROM paths WHERE id = ?").get(id) ?? null;
428
+ }
429
+ function getPathByUri(db, uri) {
430
+ return db.prepare("SELECT * FROM paths WHERE uri = ?").get(uri) ?? null;
431
+ }
432
+
433
+ // src/sleep/boot.ts
434
+ function boot(db, opts) {
435
+ const agentId = opts?.agent_id ?? "default";
436
+ const corePaths = opts?.corePaths ?? [
437
+ "core://agent",
438
+ "core://user",
439
+ "core://agent/identity",
440
+ "core://user/identity"
441
+ ];
442
+ const memories = /* @__PURE__ */ new Map();
443
+ const identities = listMemories(db, { agent_id: agentId, priority: 0 });
444
+ for (const mem of identities) {
445
+ memories.set(mem.id, mem);
446
+ recordAccess(db, mem.id, 1.1);
447
+ }
448
+ const bootPaths = [];
449
+ for (const uri of corePaths) {
450
+ const path = getPathByUri(db, uri);
451
+ if (path) {
452
+ bootPaths.push(uri);
453
+ if (!memories.has(path.memory_id)) {
454
+ const mem = getMemory(db, path.memory_id);
455
+ if (mem) {
456
+ memories.set(mem.id, mem);
457
+ recordAccess(db, mem.id, 1.1);
458
+ }
459
+ }
460
+ }
461
+ }
462
+ const bootEntry = getPathByUri(db, "system://boot");
463
+ if (bootEntry) {
464
+ const bootMem = getMemory(db, bootEntry.memory_id);
465
+ if (bootMem) {
466
+ const additionalUris = bootMem.content.split("\n").map((l) => l.trim()).filter((l) => l.match(/^[a-z]+:\/\//));
467
+ for (const uri of additionalUris) {
468
+ const path = getPathByUri(db, uri);
469
+ if (path && !memories.has(path.memory_id)) {
470
+ const mem = getMemory(db, path.memory_id);
471
+ if (mem) {
472
+ memories.set(mem.id, mem);
473
+ bootPaths.push(uri);
474
+ }
475
+ }
476
+ }
477
+ }
478
+ }
479
+ return {
480
+ identityMemories: [...memories.values()],
481
+ bootPaths
482
+ };
483
+ }
484
+
485
+ // src/sleep/decay.ts
486
+ var MIN_VITALITY = {
487
+ 0: 1,
488
+ // P0: identity — never decays
489
+ 1: 0.3,
490
+ // P1: emotion — slow decay
491
+ 2: 0.1,
492
+ // P2: knowledge — normal decay
493
+ 3: 0
494
+ // P3: event — full decay
495
+ };
496
+ function calculateVitality(stability, daysSinceCreation, priority) {
497
+ if (priority === 0) return 1;
498
+ const S = Math.max(0.01, stability);
499
+ const retention = Math.exp(-daysSinceCreation / S);
500
+ const minVit = MIN_VITALITY[priority] ?? 0;
501
+ return Math.max(minVit, retention);
502
+ }
503
+ function runDecay(db) {
504
+ const currentTime = now();
505
+ const currentMs = new Date(currentTime).getTime();
506
+ const memories = db.prepare("SELECT id, priority, stability, created_at, vitality FROM memories WHERE priority > 0").all();
507
+ let updated = 0;
508
+ let decayed = 0;
509
+ let belowThreshold = 0;
510
+ const updateStmt = db.prepare("UPDATE memories SET vitality = ?, updated_at = ? WHERE id = ?");
511
+ const transaction = db.transaction(() => {
512
+ for (const mem of memories) {
513
+ const createdMs = new Date(mem.created_at).getTime();
514
+ const daysSince = (currentMs - createdMs) / (1e3 * 60 * 60 * 24);
515
+ const newVitality = calculateVitality(mem.stability, daysSince, mem.priority);
516
+ if (Math.abs(newVitality - mem.vitality) > 1e-3) {
517
+ updateStmt.run(newVitality, currentTime, mem.id);
518
+ updated++;
519
+ if (newVitality < mem.vitality) {
520
+ decayed++;
521
+ }
522
+ if (newVitality < 0.05) {
523
+ belowThreshold++;
524
+ }
525
+ }
526
+ }
527
+ });
528
+ transaction();
529
+ return { updated, decayed, belowThreshold };
530
+ }
531
+ function getDecayedMemories(db, threshold = 0.05) {
532
+ 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);
537
+ }
538
+
539
+ // src/core/snapshot.ts
540
+ function createSnapshot(db, memoryId, action, changedBy) {
541
+ const memory = db.prepare("SELECT content FROM memories WHERE id = ?").get(memoryId);
542
+ if (!memory) throw new Error(`Memory not found: ${memoryId}`);
543
+ const id = newId();
544
+ db.prepare(
545
+ `INSERT INTO snapshots (id, memory_id, content, changed_by, action, created_at)
546
+ VALUES (?, ?, ?, ?, ?, ?)`
547
+ ).run(id, memoryId, memory.content, changedBy ?? null, action, now());
548
+ return { id, memory_id: memoryId, content: memory.content, changed_by: changedBy ?? null, action, created_at: now() };
549
+ }
550
+
551
+ // src/sleep/tidy.ts
552
+ function runTidy(db, opts) {
553
+ const threshold = opts?.vitalityThreshold ?? 0.05;
554
+ const maxSnapshots = opts?.maxSnapshotsPerMemory ?? 10;
555
+ let archived = 0;
556
+ let orphansCleaned = 0;
557
+ let snapshotsPruned = 0;
558
+ const transaction = db.transaction(() => {
559
+ const decayed = getDecayedMemories(db, threshold);
560
+ for (const mem of decayed) {
561
+ try {
562
+ createSnapshot(db, mem.id, "delete", "tidy");
563
+ } catch {
564
+ }
565
+ deleteMemory(db, mem.id);
566
+ archived++;
567
+ }
568
+ const orphans = db.prepare(
569
+ `DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)`
570
+ ).run();
571
+ orphansCleaned = orphans.changes;
572
+ const memoriesWithSnapshots = db.prepare(
573
+ `SELECT memory_id, COUNT(*) as cnt FROM snapshots
574
+ GROUP BY memory_id HAVING cnt > ?`
575
+ ).all(maxSnapshots);
576
+ for (const { memory_id } of memoriesWithSnapshots) {
577
+ const pruned = db.prepare(
578
+ `DELETE FROM snapshots WHERE id NOT IN (
579
+ SELECT id FROM snapshots WHERE memory_id = ?
580
+ ORDER BY created_at DESC LIMIT ?
581
+ ) AND memory_id = ?`
582
+ ).run(memory_id, maxSnapshots, memory_id);
583
+ snapshotsPruned += pruned.changes;
584
+ }
585
+ });
586
+ transaction();
587
+ return { archived, orphansCleaned, snapshotsPruned };
588
+ }
589
+
590
+ // src/sleep/govern.ts
591
+ function runGovern(db) {
592
+ let orphanPaths = 0;
593
+ let orphanLinks = 0;
594
+ let emptyMemories = 0;
595
+ const transaction = db.transaction(() => {
596
+ const pathResult = db.prepare("DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)").run();
597
+ orphanPaths = pathResult.changes;
598
+ const linkResult = db.prepare(
599
+ `DELETE FROM links WHERE
600
+ source_id NOT IN (SELECT id FROM memories) OR
601
+ target_id NOT IN (SELECT id FROM memories)`
602
+ ).run();
603
+ orphanLinks = linkResult.changes;
604
+ const emptyResult = db.prepare("DELETE FROM memories WHERE TRIM(content) = ''").run();
605
+ emptyMemories = emptyResult.changes;
606
+ });
607
+ transaction();
608
+ return { orphanPaths, orphanLinks, emptyMemories };
609
+ }
610
+
611
+ // src/core/guard.ts
612
+ function guard(db, input) {
613
+ const hash = contentHash(input.content);
614
+ const agentId = input.agent_id ?? "default";
615
+ const exactMatch = db.prepare("SELECT id FROM memories WHERE hash = ? AND agent_id = ?").get(hash, agentId);
616
+ if (exactMatch) {
617
+ return { action: "skip", reason: "Exact duplicate (hash match)", existingId: exactMatch.id };
618
+ }
619
+ if (input.uri) {
620
+ const existingPath = getPathByUri(db, input.uri);
621
+ if (existingPath) {
622
+ return {
623
+ action: "update",
624
+ reason: `URI ${input.uri} already exists, updating`,
625
+ existingId: existingPath.memory_id
626
+ };
627
+ }
628
+ }
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}
641
+
642
+ [Updated] ${input.content}`;
643
+ return {
644
+ action: "merge",
645
+ reason: "Similar content found, merging",
646
+ existingId: existing.id,
647
+ mergedContent: merged
648
+ };
649
+ }
650
+ }
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
+ }
656
+ }
657
+ return { action: "add", reason: "Passed all guard checks" };
658
+ }
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 ");
663
+ }
664
+
665
+ // src/sleep/sync.ts
666
+ function syncOne(db, input) {
667
+ const memInput = {
668
+ content: input.content,
669
+ type: input.type ?? "event",
670
+ priority: input.priority,
671
+ emotion_val: input.emotion_val,
672
+ source: input.source,
673
+ agent_id: input.agent_id,
674
+ uri: input.uri
675
+ };
676
+ const guardResult = guard(db, memInput);
677
+ switch (guardResult.action) {
678
+ case "skip":
679
+ return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
680
+ case "add": {
681
+ const mem = createMemory(db, memInput);
682
+ if (!mem) return { action: "skipped", reason: "createMemory returned null" };
683
+ if (input.uri) {
684
+ try {
685
+ createPath(db, mem.id, input.uri);
686
+ } catch {
687
+ }
688
+ }
689
+ return { action: "added", memoryId: mem.id, reason: guardResult.reason };
690
+ }
691
+ case "update": {
692
+ if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
693
+ createSnapshot(db, guardResult.existingId, "update", "sync");
694
+ updateMemory(db, guardResult.existingId, { content: input.content });
695
+ return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
696
+ }
697
+ case "merge": {
698
+ if (!guardResult.existingId || !guardResult.mergedContent) {
699
+ return { action: "skipped", reason: "Missing merge data" };
700
+ }
701
+ createSnapshot(db, guardResult.existingId, "merge", "sync");
702
+ updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
703
+ return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
704
+ }
705
+ }
706
+ }
707
+
708
+ // src/bin/agent-memory.ts
709
+ import { existsSync, readFileSync, readdirSync } from "fs";
710
+ import { resolve, basename } from "path";
711
+ var args = process.argv.slice(2);
712
+ var command = args[0];
713
+ function getDbPath() {
714
+ return process.env.AGENT_MEMORY_DB ?? "./agent-memory.db";
715
+ }
716
+ function printHelp() {
717
+ console.log(`
718
+ \u{1F9E0} AgentMemory v2 \u2014 Sleep-cycle memory for AI agents
719
+
720
+ Usage: agent-memory <command> [options]
721
+
722
+ Commands:
723
+ init Create database
724
+ remember <content> [--uri X] [--type T] Store a memory
725
+ recall <query> [--limit N] Search memories
726
+ boot Load identity memories
727
+ status Show statistics
728
+ reflect [decay|tidy|govern|all] Run sleep cycle
729
+ migrate <dir> Import from Markdown files
730
+ help Show this help
731
+
732
+ Environment:
733
+ AGENT_MEMORY_DB Database path (default: ./agent-memory.db)
734
+ AGENT_MEMORY_AGENT_ID Agent ID (default: "default")
735
+ `);
736
+ }
737
+ function getFlag(flag) {
738
+ const idx = args.indexOf(flag);
739
+ if (idx >= 0 && idx + 1 < args.length) return args[idx + 1];
740
+ return void 0;
741
+ }
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);
769
+ }
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)}`);
782
+ }
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)}`);
793
+ }
794
+ if (result.bootPaths.length) {
795
+ console.log(`
796
+ \u{1F4CD} Boot paths: ${result.bootPaths.join(", ")}`);
797
+ }
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...
821
+ `);
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`);
833
+ }
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);
842
+ }
843
+ const dirPath = resolve(dir);
844
+ if (!existsSync(dirPath)) {
845
+ console.error(`Directory not found: ${dirPath}`);
846
+ process.exit(1);
847
+ }
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++;
864
+ }
865
+ console.log(`\u{1F4C4} MEMORY.md: ${sections.length} sections imported`);
866
+ }
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}`
876
+ });
877
+ imported++;
878
+ }
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");
886
+ syncOne(db, {
887
+ content,
888
+ type: "knowledge",
889
+ uri: `knowledge://weekly/${week}`,
890
+ source: `migrate:weekly/${file}`
891
+ });
892
+ imported++;
893
+ }
894
+ if (weeklyFiles.length) console.log(`\u{1F4E6} Weekly: ${weeklyFiles.length} files imported`);
895
+ }
896
+ console.log(`
897
+ \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);
915
+ }
916
+ //# sourceMappingURL=agent-memory.js.map