@smyslenny/agent-memory 3.1.0 → 4.1.0-alpha.1

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.
@@ -1,97 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  // AgentMemory v2 — Sleep-cycle memory for AI agents
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __esm = (fn, res) => function __init() {
6
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
7
+ };
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
3
12
 
4
13
  // src/core/db.ts
5
14
  import Database from "better-sqlite3";
6
15
  import { randomUUID } from "crypto";
7
- var SCHEMA_VERSION = 3;
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
- agent_id TEXT NOT NULL DEFAULT 'default',
33
- uri TEXT NOT NULL,
34
- alias TEXT,
35
- domain TEXT NOT NULL,
36
- created_at TEXT NOT NULL,
37
- UNIQUE(agent_id, uri)
38
- );
39
-
40
- -- Association network (knowledge graph)
41
- CREATE TABLE IF NOT EXISTS links (
42
- agent_id TEXT NOT NULL DEFAULT 'default',
43
- source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
44
- target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
45
- relation TEXT NOT NULL,
46
- weight REAL NOT NULL DEFAULT 1.0,
47
- created_at TEXT NOT NULL,
48
- PRIMARY KEY (agent_id, source_id, target_id)
49
- );
50
-
51
- -- Snapshots (version control, from nocturne + Memory Palace)
52
- CREATE TABLE IF NOT EXISTS snapshots (
53
- id TEXT PRIMARY KEY,
54
- memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
55
- content TEXT NOT NULL,
56
- changed_by TEXT,
57
- action TEXT NOT NULL CHECK(action IN ('create','update','delete','merge')),
58
- created_at TEXT NOT NULL
59
- );
60
-
61
- -- Full-text search index (BM25)
62
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
63
- id UNINDEXED,
64
- content,
65
- tokenize='unicode61'
66
- );
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
-
80
- -- Schema version tracking
81
- CREATE TABLE IF NOT EXISTS schema_meta (
82
- key TEXT PRIMARY KEY,
83
- value TEXT NOT NULL
84
- );
85
-
86
- -- Indexes for common queries
87
- CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
88
- CREATE INDEX IF NOT EXISTS idx_memories_priority ON memories(priority);
89
- CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
90
- CREATE INDEX IF NOT EXISTS idx_memories_vitality ON memories(vitality);
91
- CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
92
- CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
93
- CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
94
- `;
95
16
  function openDatabase(opts) {
96
17
  const db = new Database(opts.path);
97
18
  if (opts.walMode !== false) {
@@ -111,6 +32,7 @@ function openDatabase(opts) {
111
32
  migrateDatabase(db, currentVersion, SCHEMA_VERSION);
112
33
  }
113
34
  ensureIndexes(db);
35
+ ensureFeedbackEventSchema(db);
114
36
  return db;
115
37
  }
116
38
  function now() {
@@ -129,6 +51,14 @@ function getSchemaVersion(db) {
129
51
  return null;
130
52
  }
131
53
  }
54
+ function tableExists(db, table) {
55
+ try {
56
+ const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name = ?").get(table);
57
+ return Boolean(row?.name);
58
+ } catch {
59
+ return false;
60
+ }
61
+ }
132
62
  function tableHasColumn(db, table, column) {
133
63
  try {
134
64
  const cols = db.prepare(`PRAGMA table_info(${table})`).all();
@@ -150,6 +80,21 @@ function migrateDatabase(db, from, to) {
150
80
  v = 3;
151
81
  continue;
152
82
  }
83
+ if (v === 3) {
84
+ migrateV3ToV4(db);
85
+ v = 4;
86
+ continue;
87
+ }
88
+ if (v === 4) {
89
+ migrateV4ToV5(db);
90
+ v = 5;
91
+ continue;
92
+ }
93
+ if (v === 5) {
94
+ migrateV5ToV6(db);
95
+ v = 6;
96
+ continue;
97
+ }
153
98
  throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
154
99
  }
155
100
  }
@@ -231,14 +176,14 @@ function migrateV1ToV2(db) {
231
176
  function inferSchemaVersion(db) {
232
177
  const hasAgentScopedPaths = tableHasColumn(db, "paths", "agent_id");
233
178
  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
- })();
179
+ const hasEmbeddings = tableExists(db, "embeddings");
180
+ const hasV4Embeddings = hasEmbeddings && tableHasColumn(db, "embeddings", "provider_id") && tableHasColumn(db, "embeddings", "status") && tableHasColumn(db, "embeddings", "content_hash") && tableHasColumn(db, "embeddings", "id");
181
+ const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
182
+ const hasFeedbackEvents = tableExists(db, "feedback_events");
183
+ const hasEmotionTag = tableHasColumn(db, "memories", "emotion_tag");
184
+ if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents && hasEmotionTag) return 6;
185
+ if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents) return 5;
186
+ if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings) return 4;
242
187
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasEmbeddings) return 3;
243
188
  if (hasAgentScopedPaths && hasAgentScopedLinks) return 2;
244
189
  return 1;
@@ -251,14 +196,40 @@ function ensureIndexes(db) {
251
196
  db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_source ON links(agent_id, source_id);");
252
197
  db.exec("CREATE INDEX IF NOT EXISTS idx_links_agent_target ON links(agent_id, target_id);");
253
198
  }
254
- try {
255
- const row = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='embeddings'").get();
256
- if (row) {
199
+ if (tableExists(db, "embeddings")) {
200
+ if (tableHasColumn(db, "embeddings", "provider_id")) {
201
+ db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_provider_status ON embeddings(provider_id, status);");
202
+ db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_memory_provider ON embeddings(memory_id, provider_id);");
203
+ } else {
257
204
  db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_agent_model ON embeddings(agent_id, model);");
258
205
  db.exec("CREATE INDEX IF NOT EXISTS idx_embeddings_memory ON embeddings(memory_id);");
259
206
  }
260
- } catch {
261
207
  }
208
+ if (tableExists(db, "maintenance_jobs")) {
209
+ db.exec("CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);");
210
+ }
211
+ if (tableHasColumn(db, "memories", "emotion_tag")) {
212
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_emotion_tag ON memories(emotion_tag) WHERE emotion_tag IS NOT NULL;");
213
+ }
214
+ if (tableExists(db, "feedback_events")) {
215
+ db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);");
216
+ if (tableHasColumn(db, "feedback_events", "agent_id") && tableHasColumn(db, "feedback_events", "source")) {
217
+ db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_agent_source ON feedback_events(agent_id, source, created_at DESC);");
218
+ }
219
+ }
220
+ }
221
+ function ensureFeedbackEventSchema(db) {
222
+ if (!tableExists(db, "feedback_events")) return;
223
+ if (!tableHasColumn(db, "feedback_events", "source")) {
224
+ db.exec("ALTER TABLE feedback_events ADD COLUMN source TEXT NOT NULL DEFAULT 'surface';");
225
+ }
226
+ if (!tableHasColumn(db, "feedback_events", "useful")) {
227
+ db.exec("ALTER TABLE feedback_events ADD COLUMN useful INTEGER NOT NULL DEFAULT 1;");
228
+ }
229
+ if (!tableHasColumn(db, "feedback_events", "agent_id")) {
230
+ db.exec("ALTER TABLE feedback_events ADD COLUMN agent_id TEXT NOT NULL DEFAULT 'default';");
231
+ }
232
+ db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_agent_source ON feedback_events(agent_id, source, created_at DESC);");
262
233
  }
263
234
  function migrateV2ToV3(db) {
264
235
  try {
@@ -285,14 +256,237 @@ function migrateV2ToV3(db) {
285
256
  throw e;
286
257
  }
287
258
  }
259
+ function migrateV3ToV4(db) {
260
+ const alreadyMigrated = tableHasColumn(db, "embeddings", "provider_id") && tableHasColumn(db, "embeddings", "status") && tableHasColumn(db, "embeddings", "content_hash") && tableHasColumn(db, "embeddings", "id");
261
+ if (alreadyMigrated) {
262
+ db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(4));
263
+ return;
264
+ }
265
+ try {
266
+ db.exec("BEGIN");
267
+ const legacyRows = tableExists(db, "embeddings") ? db.prepare(
268
+ `SELECT e.agent_id, e.memory_id, e.model, e.vector, e.created_at, m.hash
269
+ FROM embeddings e
270
+ LEFT JOIN memories m ON m.id = e.memory_id`
271
+ ).all() : [];
272
+ db.exec(`
273
+ CREATE TABLE IF NOT EXISTS embeddings_v4 (
274
+ id TEXT PRIMARY KEY,
275
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
276
+ provider_id TEXT NOT NULL,
277
+ vector BLOB,
278
+ content_hash TEXT NOT NULL,
279
+ status TEXT NOT NULL CHECK(status IN ('pending','ready','failed')),
280
+ created_at TEXT NOT NULL,
281
+ UNIQUE(memory_id, provider_id)
282
+ );
283
+ `);
284
+ const insert = db.prepare(
285
+ `INSERT INTO embeddings_v4 (id, memory_id, provider_id, vector, content_hash, status, created_at)
286
+ VALUES (?, ?, ?, ?, ?, 'ready', ?)`
287
+ );
288
+ for (const row of legacyRows) {
289
+ insert.run(newId(), row.memory_id, `legacy:${row.agent_id}:${row.model}`, row.vector, row.hash ?? "", row.created_at);
290
+ }
291
+ if (tableExists(db, "embeddings")) {
292
+ db.exec("DROP TABLE embeddings;");
293
+ }
294
+ db.exec("ALTER TABLE embeddings_v4 RENAME TO embeddings;");
295
+ db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(4));
296
+ db.exec("COMMIT");
297
+ } catch (e) {
298
+ try {
299
+ db.exec("ROLLBACK");
300
+ } catch {
301
+ }
302
+ throw e;
303
+ }
304
+ }
305
+ function migrateV4ToV5(db) {
306
+ const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
307
+ const hasFeedbackEvents = tableExists(db, "feedback_events");
308
+ if (hasMaintenanceJobs && hasFeedbackEvents) {
309
+ db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(5));
310
+ return;
311
+ }
312
+ try {
313
+ db.exec("BEGIN");
314
+ db.exec(`
315
+ CREATE TABLE IF NOT EXISTS maintenance_jobs (
316
+ job_id TEXT PRIMARY KEY,
317
+ phase TEXT NOT NULL CHECK(phase IN ('decay','tidy','govern','all')),
318
+ status TEXT NOT NULL CHECK(status IN ('running','completed','failed')),
319
+ checkpoint TEXT,
320
+ error TEXT,
321
+ started_at TEXT NOT NULL,
322
+ finished_at TEXT
323
+ );
324
+ `);
325
+ db.exec(`
326
+ CREATE TABLE IF NOT EXISTS feedback_events (
327
+ id TEXT PRIMARY KEY,
328
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
329
+ source TEXT NOT NULL DEFAULT 'surface',
330
+ useful INTEGER NOT NULL DEFAULT 1,
331
+ agent_id TEXT NOT NULL DEFAULT 'default',
332
+ event_type TEXT NOT NULL DEFAULT 'surface:useful',
333
+ value REAL NOT NULL DEFAULT 1.0,
334
+ created_at TEXT NOT NULL
335
+ );
336
+ `);
337
+ db.prepare("UPDATE schema_meta SET value = ? WHERE key = 'version'").run(String(5));
338
+ db.exec("COMMIT");
339
+ } catch (e) {
340
+ try {
341
+ db.exec("ROLLBACK");
342
+ } catch {
343
+ }
344
+ throw e;
345
+ }
346
+ }
347
+ function migrateV5ToV6(db) {
348
+ if (tableHasColumn(db, "memories", "emotion_tag")) {
349
+ db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(6));
350
+ return;
351
+ }
352
+ try {
353
+ db.exec("BEGIN");
354
+ db.exec("ALTER TABLE memories ADD COLUMN emotion_tag TEXT;");
355
+ db.exec("CREATE INDEX IF NOT EXISTS idx_memories_emotion_tag ON memories(emotion_tag) WHERE emotion_tag IS NOT NULL;");
356
+ db.prepare("INSERT OR REPLACE INTO schema_meta (key, value) VALUES ('version', ?)").run(String(6));
357
+ db.exec("COMMIT");
358
+ } catch (e) {
359
+ try {
360
+ db.exec("ROLLBACK");
361
+ } catch {
362
+ }
363
+ throw e;
364
+ }
365
+ }
366
+ var SCHEMA_VERSION, SCHEMA_SQL;
367
+ var init_db = __esm({
368
+ "src/core/db.ts"() {
369
+ "use strict";
370
+ SCHEMA_VERSION = 6;
371
+ SCHEMA_SQL = `
372
+ -- Memory entries
373
+ CREATE TABLE IF NOT EXISTS memories (
374
+ id TEXT PRIMARY KEY,
375
+ content TEXT NOT NULL,
376
+ type TEXT NOT NULL CHECK(type IN ('identity','emotion','knowledge','event')),
377
+ priority INTEGER NOT NULL DEFAULT 2 CHECK(priority BETWEEN 0 AND 3),
378
+ emotion_val REAL NOT NULL DEFAULT 0.0,
379
+ vitality REAL NOT NULL DEFAULT 1.0,
380
+ stability REAL NOT NULL DEFAULT 1.0,
381
+ access_count INTEGER NOT NULL DEFAULT 0,
382
+ last_accessed TEXT,
383
+ created_at TEXT NOT NULL,
384
+ updated_at TEXT NOT NULL,
385
+ source TEXT,
386
+ agent_id TEXT NOT NULL DEFAULT 'default',
387
+ hash TEXT,
388
+ emotion_tag TEXT,
389
+ UNIQUE(hash, agent_id)
390
+ );
288
391
 
289
- // src/core/memory.ts
290
- import { createHash } from "crypto";
392
+ -- URI paths (Content-Path separation, from nocturne)
393
+ CREATE TABLE IF NOT EXISTS paths (
394
+ id TEXT PRIMARY KEY,
395
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
396
+ agent_id TEXT NOT NULL DEFAULT 'default',
397
+ uri TEXT NOT NULL,
398
+ alias TEXT,
399
+ domain TEXT NOT NULL,
400
+ created_at TEXT NOT NULL,
401
+ UNIQUE(agent_id, uri)
402
+ );
403
+
404
+ -- Association network (knowledge graph)
405
+ CREATE TABLE IF NOT EXISTS links (
406
+ agent_id TEXT NOT NULL DEFAULT 'default',
407
+ source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
408
+ target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
409
+ relation TEXT NOT NULL,
410
+ weight REAL NOT NULL DEFAULT 1.0,
411
+ created_at TEXT NOT NULL,
412
+ PRIMARY KEY (agent_id, source_id, target_id)
413
+ );
414
+
415
+ -- Snapshots (version control, from nocturne + Memory Palace)
416
+ CREATE TABLE IF NOT EXISTS snapshots (
417
+ id TEXT PRIMARY KEY,
418
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
419
+ content TEXT NOT NULL,
420
+ changed_by TEXT,
421
+ action TEXT NOT NULL CHECK(action IN ('create','update','delete','merge')),
422
+ created_at TEXT NOT NULL
423
+ );
424
+
425
+ -- Full-text search index (BM25)
426
+ CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
427
+ id UNINDEXED,
428
+ content,
429
+ tokenize='unicode61'
430
+ );
431
+
432
+ -- Embeddings (optional semantic layer)
433
+ CREATE TABLE IF NOT EXISTS embeddings (
434
+ id TEXT PRIMARY KEY,
435
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
436
+ provider_id TEXT NOT NULL,
437
+ vector BLOB,
438
+ content_hash TEXT NOT NULL,
439
+ status TEXT NOT NULL CHECK(status IN ('pending','ready','failed')),
440
+ created_at TEXT NOT NULL,
441
+ UNIQUE(memory_id, provider_id)
442
+ );
443
+
444
+ -- Maintenance jobs (reflect / reindex checkpoints)
445
+ CREATE TABLE IF NOT EXISTS maintenance_jobs (
446
+ job_id TEXT PRIMARY KEY,
447
+ phase TEXT NOT NULL CHECK(phase IN ('decay','tidy','govern','all')),
448
+ status TEXT NOT NULL CHECK(status IN ('running','completed','failed')),
449
+ checkpoint TEXT,
450
+ error TEXT,
451
+ started_at TEXT NOT NULL,
452
+ finished_at TEXT
453
+ );
454
+
455
+ -- Feedback signals (recall/surface usefulness + governance priors)
456
+ CREATE TABLE IF NOT EXISTS feedback_events (
457
+ id TEXT PRIMARY KEY,
458
+ memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
459
+ source TEXT NOT NULL DEFAULT 'surface',
460
+ useful INTEGER NOT NULL DEFAULT 1,
461
+ agent_id TEXT NOT NULL DEFAULT 'default',
462
+ event_type TEXT NOT NULL DEFAULT 'surface:useful',
463
+ value REAL NOT NULL DEFAULT 1.0,
464
+ created_at TEXT NOT NULL
465
+ );
466
+
467
+ -- Schema version tracking
468
+ CREATE TABLE IF NOT EXISTS schema_meta (
469
+ key TEXT PRIMARY KEY,
470
+ value TEXT NOT NULL
471
+ );
472
+
473
+ -- Indexes for common queries
474
+ CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
475
+ CREATE INDEX IF NOT EXISTS idx_memories_priority ON memories(priority);
476
+ CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
477
+ CREATE INDEX IF NOT EXISTS idx_memories_vitality ON memories(vitality);
478
+ CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
479
+ CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
480
+ CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
481
+ CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);
482
+ CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);
483
+ `;
484
+ }
485
+ });
291
486
 
292
487
  // src/search/tokenizer.ts
293
488
  import { readFileSync } from "fs";
294
489
  import { createRequire } from "module";
295
- var _jieba;
296
490
  function getJieba() {
297
491
  if (_jieba !== void 0) return _jieba;
298
492
  try {
@@ -306,38 +500,6 @@ function getJieba() {
306
500
  }
307
501
  return _jieba;
308
502
  }
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
503
  function tokenize(text) {
342
504
  const cleaned = text.replace(/[^\w\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af\s]/g, " ");
343
505
  const tokens = [];
@@ -367,42 +529,433 @@ function tokenizeForIndex(text) {
367
529
  const tokens = tokenize(text);
368
530
  return tokens.join(" ");
369
531
  }
532
+ var _jieba, STOPWORDS;
533
+ var init_tokenizer = __esm({
534
+ "src/search/tokenizer.ts"() {
535
+ "use strict";
536
+ STOPWORDS = /* @__PURE__ */ new Set([
537
+ "\u7684",
538
+ "\u4E86",
539
+ "\u5728",
540
+ "\u662F",
541
+ "\u6211",
542
+ "\u6709",
543
+ "\u548C",
544
+ "\u5C31",
545
+ "\u4E0D",
546
+ "\u4EBA",
547
+ "\u90FD",
548
+ "\u4E00",
549
+ "\u4E2A",
550
+ "\u4E0A",
551
+ "\u4E5F",
552
+ "\u5230",
553
+ "\u4ED6",
554
+ "\u6CA1",
555
+ "\u8FD9",
556
+ "\u8981",
557
+ "\u4F1A",
558
+ "\u5BF9",
559
+ "\u8BF4",
560
+ "\u800C",
561
+ "\u53BB",
562
+ "\u4E4B",
563
+ "\u88AB",
564
+ "\u5979",
565
+ "\u628A",
566
+ "\u90A3"
567
+ ]);
568
+ }
569
+ });
370
570
 
371
- // src/core/memory.ts
372
- function contentHash(content) {
373
- return createHash("sha256").update(content.trim()).digest("hex").slice(0, 16);
571
+ // src/search/embedding.ts
572
+ import { createHash } from "crypto";
573
+ function trimTrailingSlashes(value) {
574
+ return value.replace(/\/+$/, "");
374
575
  }
375
- var TYPE_PRIORITY = {
376
- identity: 0,
377
- emotion: 1,
378
- knowledge: 2,
379
- event: 3
380
- };
381
- var PRIORITY_STABILITY = {
382
- 0: Infinity,
383
- // P0: never decays
384
- 1: 365,
385
- // P1: 365-day half-life
386
- 2: 90,
387
- // P2: 90-day half-life
388
- 3: 14
389
- // P3: 14-day half-life
390
- };
391
- function createMemory(db, input) {
392
- const hash = contentHash(input.content);
393
- const agentId = input.agent_id ?? "default";
394
- const priority = input.priority ?? TYPE_PRIORITY[input.type];
395
- const stability = PRIORITY_STABILITY[priority];
396
- const existing = db.prepare("SELECT id FROM memories WHERE hash = ? AND agent_id = ?").get(hash, agentId);
397
- if (existing) {
398
- return null;
576
+ function resolveEndpoint(baseUrl, endpoint = "/embeddings") {
577
+ const trimmed = trimTrailingSlashes(baseUrl);
578
+ if (trimmed.endsWith("/embeddings")) {
579
+ return trimmed;
580
+ }
581
+ const normalizedEndpoint = endpoint.startsWith("/") ? endpoint : `/${endpoint}`;
582
+ return `${trimmed}${normalizedEndpoint}`;
583
+ }
584
+ function stableProviderId(prefix, input) {
585
+ const digest = createHash("sha256").update(input).digest("hex").slice(0, 12);
586
+ return `${prefix}:${digest}`;
587
+ }
588
+ function getFetch(fetchImpl) {
589
+ const candidate = fetchImpl ?? globalThis.fetch;
590
+ if (!candidate) {
591
+ throw new Error("Global fetch is not available in this runtime");
592
+ }
593
+ return candidate;
594
+ }
595
+ function assertEmbeddingVector(vector, dimension, context) {
596
+ if (!Array.isArray(vector) || !vector.every((value) => typeof value === "number" && Number.isFinite(value))) {
597
+ throw new Error(`${context} returned an invalid embedding vector`);
598
+ }
599
+ if (vector.length !== dimension) {
600
+ throw new Error(`${context} returned dimension ${vector.length}, expected ${dimension}`);
601
+ }
602
+ return vector;
603
+ }
604
+ function parseOpenAIResponse(json2, dimension, context) {
605
+ const rows = json2?.data;
606
+ if (!Array.isArray(rows)) {
607
+ throw new Error(`${context} returned an invalid embeddings payload`);
608
+ }
609
+ return rows.map((row, index) => assertEmbeddingVector(row?.embedding, dimension, `${context} item ${index}`));
610
+ }
611
+ function parseLocalHttpResponse(json2, dimension, context) {
612
+ if (Array.isArray(json2.embeddings)) {
613
+ const embeddings = json2.embeddings;
614
+ return embeddings.map((row, index) => assertEmbeddingVector(row, dimension, `${context} item ${index}`));
615
+ }
616
+ return parseOpenAIResponse(json2, dimension, context);
617
+ }
618
+ async function runEmbeddingRequest(input) {
619
+ const fetchFn = getFetch(input.fetchImpl);
620
+ const response = await fetchFn(input.url, {
621
+ method: "POST",
622
+ headers: {
623
+ "content-type": "application/json",
624
+ ...input.headers
625
+ },
626
+ body: JSON.stringify(input.body)
627
+ });
628
+ if (!response.ok) {
629
+ const detail = await response.text().catch(() => "");
630
+ throw new Error(`${input.context} request failed: ${response.status} ${response.statusText}${detail ? ` \u2014 ${detail}` : ""}`);
631
+ }
632
+ const json2 = await response.json();
633
+ return input.parser(json2, input.dimension, input.context);
634
+ }
635
+ function createOpenAICompatibleEmbeddingProvider(opts) {
636
+ const url = resolveEndpoint(opts.baseUrl, opts.endpoint);
637
+ const providerDescriptor = `${trimTrailingSlashes(opts.baseUrl)}|${opts.model}|${opts.dimension}`;
638
+ const id = stableProviderId(`openai-compatible:${opts.model}`, providerDescriptor);
639
+ return {
640
+ id,
641
+ model: opts.model,
642
+ dimension: opts.dimension,
643
+ async embed(texts) {
644
+ if (texts.length === 0) return [];
645
+ return runEmbeddingRequest({
646
+ context: "openai-compatible embedding provider",
647
+ url,
648
+ dimension: opts.dimension,
649
+ fetchImpl: opts.fetchImpl,
650
+ headers: {
651
+ ...opts.apiKey ? { authorization: `Bearer ${opts.apiKey}` } : {},
652
+ ...opts.headers
653
+ },
654
+ body: {
655
+ model: opts.model,
656
+ input: texts
657
+ },
658
+ parser: parseOpenAIResponse
659
+ });
660
+ },
661
+ async healthcheck() {
662
+ await this.embed(["healthcheck"]);
663
+ }
664
+ };
665
+ }
666
+ function createLocalHttpEmbeddingProvider(opts) {
667
+ const url = resolveEndpoint(opts.baseUrl, opts.endpoint);
668
+ const providerDescriptor = `${trimTrailingSlashes(opts.baseUrl)}|${opts.model}|${opts.dimension}`;
669
+ const id = stableProviderId(`local-http:${opts.model}`, providerDescriptor);
670
+ return {
671
+ id,
672
+ model: opts.model,
673
+ dimension: opts.dimension,
674
+ async embed(texts) {
675
+ if (texts.length === 0) return [];
676
+ return runEmbeddingRequest({
677
+ context: "local-http embedding provider",
678
+ url,
679
+ dimension: opts.dimension,
680
+ fetchImpl: opts.fetchImpl,
681
+ headers: opts.headers,
682
+ body: {
683
+ model: opts.model,
684
+ input: texts
685
+ },
686
+ parser: parseLocalHttpResponse
687
+ });
688
+ },
689
+ async healthcheck() {
690
+ await this.embed(["healthcheck"]);
691
+ }
692
+ };
693
+ }
694
+ function normalizeEmbeddingBaseUrl(baseUrl) {
695
+ return trimTrailingSlashes(baseUrl);
696
+ }
697
+ var init_embedding = __esm({
698
+ "src/search/embedding.ts"() {
699
+ "use strict";
700
+ }
701
+ });
702
+
703
+ // src/search/providers.ts
704
+ function parseDimension(raw) {
705
+ if (!raw) return void 0;
706
+ const value = Number.parseInt(raw, 10);
707
+ return Number.isFinite(value) && value > 0 ? value : void 0;
708
+ }
709
+ function parseProvider(raw) {
710
+ if (!raw) return null;
711
+ if (raw === "openai-compatible" || raw === "local-http") {
712
+ return raw;
713
+ }
714
+ throw new Error(`Unsupported embedding provider: ${raw}`);
715
+ }
716
+ function getEmbeddingProviderConfigFromEnv(env = process.env) {
717
+ const provider = parseProvider(env.AGENT_MEMORY_EMBEDDING_PROVIDER);
718
+ if (!provider) return null;
719
+ const baseUrl = env.AGENT_MEMORY_EMBEDDING_BASE_URL;
720
+ const model = env.AGENT_MEMORY_EMBEDDING_MODEL;
721
+ const dimension = parseDimension(env.AGENT_MEMORY_EMBEDDING_DIMENSION);
722
+ if (!baseUrl) {
723
+ throw new Error("AGENT_MEMORY_EMBEDDING_BASE_URL is required when embeddings are enabled");
724
+ }
725
+ if (!model) {
726
+ throw new Error("AGENT_MEMORY_EMBEDDING_MODEL is required when embeddings are enabled");
727
+ }
728
+ if (!dimension) {
729
+ throw new Error("AGENT_MEMORY_EMBEDDING_DIMENSION is required when embeddings are enabled");
730
+ }
731
+ if (provider === "openai-compatible" && !env.AGENT_MEMORY_EMBEDDING_API_KEY) {
732
+ throw new Error("AGENT_MEMORY_EMBEDDING_API_KEY is required for openai-compatible providers");
733
+ }
734
+ return {
735
+ provider,
736
+ baseUrl,
737
+ model,
738
+ dimension,
739
+ apiKey: env.AGENT_MEMORY_EMBEDDING_API_KEY
740
+ };
741
+ }
742
+ function createEmbeddingProvider(input, opts) {
743
+ const normalized = {
744
+ ...input,
745
+ baseUrl: normalizeEmbeddingBaseUrl(input.baseUrl)
746
+ };
747
+ if (normalized.provider === "openai-compatible") {
748
+ return createOpenAICompatibleEmbeddingProvider({
749
+ baseUrl: normalized.baseUrl,
750
+ model: normalized.model,
751
+ dimension: normalized.dimension,
752
+ apiKey: normalized.apiKey,
753
+ fetchImpl: opts?.fetchImpl
754
+ });
755
+ }
756
+ return createLocalHttpEmbeddingProvider({
757
+ baseUrl: normalized.baseUrl,
758
+ model: normalized.model,
759
+ dimension: normalized.dimension,
760
+ fetchImpl: opts?.fetchImpl
761
+ });
762
+ }
763
+ function resolveEmbeddingProviderConfig(opts) {
764
+ const envConfig = getEmbeddingProviderConfigFromEnv(opts?.env);
765
+ if (!envConfig && !opts?.config?.provider) {
766
+ return null;
767
+ }
768
+ const provider = opts?.config?.provider ?? envConfig?.provider;
769
+ const baseUrl = opts?.config?.baseUrl ?? envConfig?.baseUrl;
770
+ const model = opts?.config?.model ?? envConfig?.model;
771
+ const dimension = opts?.config?.dimension ?? envConfig?.dimension;
772
+ const apiKey = opts?.config?.apiKey ?? envConfig?.apiKey;
773
+ if (!provider || !baseUrl || !model || !dimension) {
774
+ throw new Error("Incomplete embedding provider configuration");
775
+ }
776
+ if (provider === "openai-compatible" && !apiKey) {
777
+ throw new Error("OpenAI-compatible embedding providers require an API key");
778
+ }
779
+ return { provider, baseUrl, model, dimension, apiKey };
780
+ }
781
+ function getEmbeddingProvider(opts) {
782
+ const config = resolveEmbeddingProviderConfig({ config: opts?.config, env: opts?.env });
783
+ if (!config) return null;
784
+ return createEmbeddingProvider(config, { fetchImpl: opts?.fetchImpl });
785
+ }
786
+ function getEmbeddingProviderFromEnv(env = process.env) {
787
+ try {
788
+ return getEmbeddingProvider({ env });
789
+ } catch {
790
+ return null;
791
+ }
792
+ }
793
+ function getConfiguredEmbeddingProviderId(opts) {
794
+ try {
795
+ const provider = getEmbeddingProvider({ config: opts?.config, env: opts?.env });
796
+ return provider?.id ?? null;
797
+ } catch {
798
+ return null;
799
+ }
800
+ }
801
+ var init_providers = __esm({
802
+ "src/search/providers.ts"() {
803
+ "use strict";
804
+ init_embedding();
805
+ }
806
+ });
807
+
808
+ // src/search/vector.ts
809
+ function encodeVector(vector) {
810
+ const float32 = vector instanceof Float32Array ? vector : Float32Array.from(vector);
811
+ return Buffer.from(float32.buffer.slice(float32.byteOffset, float32.byteOffset + float32.byteLength));
812
+ }
813
+ function decodeVector(blob) {
814
+ const buffer = blob instanceof Uint8Array ? blob : new Uint8Array(blob);
815
+ const aligned = buffer.byteOffset === 0 && buffer.byteLength === buffer.buffer.byteLength ? buffer.buffer : buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength);
816
+ return Array.from(new Float32Array(aligned));
817
+ }
818
+ function cosineSimilarity(a, b) {
819
+ const length = Math.min(a.length, b.length);
820
+ if (length === 0 || a.length !== b.length) return 0;
821
+ let dot = 0;
822
+ let normA = 0;
823
+ let normB = 0;
824
+ for (let index = 0; index < length; index++) {
825
+ const left = Number(a[index] ?? 0);
826
+ const right = Number(b[index] ?? 0);
827
+ dot += left * right;
828
+ normA += left * left;
829
+ normB += right * right;
830
+ }
831
+ if (normA === 0 || normB === 0) return 0;
832
+ return dot / (Math.sqrt(normA) * Math.sqrt(normB));
833
+ }
834
+ function markMemoryEmbeddingPending(db, memoryId, providerId, contentHash2) {
835
+ db.prepare(
836
+ `INSERT INTO embeddings (id, memory_id, provider_id, vector, content_hash, status, created_at)
837
+ VALUES (?, ?, ?, NULL, ?, 'pending', ?)
838
+ ON CONFLICT(memory_id, provider_id) DO UPDATE SET
839
+ vector = NULL,
840
+ content_hash = excluded.content_hash,
841
+ status = 'pending'`
842
+ ).run(newId(), memoryId, providerId, contentHash2, now());
843
+ }
844
+ function markAllEmbeddingsPending(db, memoryId, contentHash2) {
845
+ const result = db.prepare(
846
+ `UPDATE embeddings
847
+ SET vector = NULL,
848
+ content_hash = ?,
849
+ status = 'pending'
850
+ WHERE memory_id = ?`
851
+ ).run(contentHash2, memoryId);
852
+ return result.changes;
853
+ }
854
+ function upsertReadyEmbedding(input) {
855
+ input.db.prepare(
856
+ `INSERT INTO embeddings (id, memory_id, provider_id, vector, content_hash, status, created_at)
857
+ VALUES (?, ?, ?, ?, ?, 'ready', ?)
858
+ ON CONFLICT(memory_id, provider_id) DO UPDATE SET
859
+ vector = excluded.vector,
860
+ content_hash = excluded.content_hash,
861
+ status = 'ready'`
862
+ ).run(newId(), input.memoryId, input.providerId, encodeVector(input.vector), input.contentHash, now());
863
+ }
864
+ function markEmbeddingFailed(db, memoryId, providerId, contentHash2) {
865
+ db.prepare(
866
+ `INSERT INTO embeddings (id, memory_id, provider_id, vector, content_hash, status, created_at)
867
+ VALUES (?, ?, ?, NULL, ?, 'failed', ?)
868
+ ON CONFLICT(memory_id, provider_id) DO UPDATE SET
869
+ vector = NULL,
870
+ content_hash = excluded.content_hash,
871
+ status = 'failed'`
872
+ ).run(newId(), memoryId, providerId, contentHash2, now());
873
+ }
874
+ function searchByVector(db, queryVector, opts) {
875
+ const limit = opts.limit ?? 20;
876
+ const agentId = opts.agent_id ?? "default";
877
+ const minVitality = opts.min_vitality ?? 0;
878
+ const rows = db.prepare(
879
+ `SELECT e.provider_id, e.vector, e.content_hash,
880
+ m.id, m.content, m.type, m.priority, m.emotion_val, m.vitality,
881
+ m.stability, m.access_count, m.last_accessed, m.created_at,
882
+ m.updated_at, m.source, m.agent_id, m.hash
883
+ FROM embeddings e
884
+ JOIN memories m ON m.id = e.memory_id
885
+ WHERE e.provider_id = ?
886
+ AND e.status = 'ready'
887
+ AND e.vector IS NOT NULL
888
+ AND e.content_hash = m.hash
889
+ AND m.agent_id = ?
890
+ AND m.vitality >= ?`
891
+ ).all(opts.providerId, agentId, minVitality);
892
+ const scored = rows.map((row) => ({
893
+ provider_id: row.provider_id,
894
+ memory: {
895
+ id: row.id,
896
+ content: row.content,
897
+ type: row.type,
898
+ priority: row.priority,
899
+ emotion_val: row.emotion_val,
900
+ vitality: row.vitality,
901
+ stability: row.stability,
902
+ access_count: row.access_count,
903
+ last_accessed: row.last_accessed,
904
+ created_at: row.created_at,
905
+ updated_at: row.updated_at,
906
+ source: row.source,
907
+ agent_id: row.agent_id,
908
+ hash: row.hash
909
+ },
910
+ similarity: cosineSimilarity(queryVector, decodeVector(row.vector))
911
+ })).filter((row) => Number.isFinite(row.similarity) && row.similarity > 0).sort((left, right) => right.similarity - left.similarity).slice(0, limit);
912
+ return scored.map((row, index) => ({
913
+ memory: row.memory,
914
+ similarity: row.similarity,
915
+ rank: index + 1,
916
+ provider_id: row.provider_id
917
+ }));
918
+ }
919
+ var init_vector = __esm({
920
+ "src/search/vector.ts"() {
921
+ "use strict";
922
+ init_db();
923
+ }
924
+ });
925
+
926
+ // src/core/memory.ts
927
+ import { createHash as createHash2 } from "crypto";
928
+ function contentHash(content) {
929
+ return createHash2("sha256").update(content.trim()).digest("hex").slice(0, 16);
930
+ }
931
+ function resolveEmbeddingProviderId(explicitProviderId) {
932
+ if (explicitProviderId !== void 0) {
933
+ return explicitProviderId;
934
+ }
935
+ return getConfiguredEmbeddingProviderId();
936
+ }
937
+ function markEmbeddingDirtyIfNeeded(db, memoryId, hash, providerId) {
938
+ if (!providerId) return;
939
+ try {
940
+ markMemoryEmbeddingPending(db, memoryId, providerId, hash);
941
+ } catch {
942
+ }
943
+ }
944
+ function createMemory(db, input) {
945
+ const hash = contentHash(input.content);
946
+ const agentId = input.agent_id ?? "default";
947
+ const priority = input.priority ?? TYPE_PRIORITY[input.type];
948
+ const stability = PRIORITY_STABILITY[priority];
949
+ const existing = db.prepare("SELECT id FROM memories WHERE hash = ? AND agent_id = ?").get(hash, agentId);
950
+ if (existing) {
951
+ return null;
399
952
  }
400
953
  const id = newId();
401
954
  const timestamp = now();
402
955
  db.prepare(
403
956
  `INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
404
- access_count, created_at, updated_at, source, agent_id, hash)
405
- VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?)`
957
+ access_count, created_at, updated_at, source, agent_id, hash, emotion_tag)
958
+ VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?, ?)`
406
959
  ).run(
407
960
  id,
408
961
  input.content,
@@ -414,9 +967,11 @@ function createMemory(db, input) {
414
967
  timestamp,
415
968
  input.source ?? null,
416
969
  agentId,
417
- hash
970
+ hash,
971
+ input.emotion_tag ?? null
418
972
  );
419
973
  db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
974
+ markEmbeddingDirtyIfNeeded(db, id, hash, resolveEmbeddingProviderId(input.embedding_provider_id));
420
975
  return getMemory(db, id);
421
976
  }
422
977
  function getMemory(db, id) {
@@ -427,9 +982,11 @@ function updateMemory(db, id, input) {
427
982
  if (!existing) return null;
428
983
  const fields = [];
429
984
  const values = [];
985
+ let nextHash = null;
430
986
  if (input.content !== void 0) {
987
+ nextHash = contentHash(input.content);
431
988
  fields.push("content = ?", "hash = ?");
432
- values.push(input.content, contentHash(input.content));
989
+ values.push(input.content, nextHash);
433
990
  }
434
991
  if (input.type !== void 0) {
435
992
  fields.push("type = ?");
@@ -455,6 +1012,10 @@ function updateMemory(db, id, input) {
455
1012
  fields.push("source = ?");
456
1013
  values.push(input.source);
457
1014
  }
1015
+ if (input.emotion_tag !== void 0) {
1016
+ fields.push("emotion_tag = ?");
1017
+ values.push(input.emotion_tag);
1018
+ }
458
1019
  fields.push("updated_at = ?");
459
1020
  values.push(now());
460
1021
  values.push(id);
@@ -462,6 +1023,13 @@ function updateMemory(db, id, input) {
462
1023
  if (input.content !== void 0) {
463
1024
  db.prepare("DELETE FROM memories_fts WHERE id = ?").run(id);
464
1025
  db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
1026
+ if (nextHash) {
1027
+ try {
1028
+ markAllEmbeddingsPending(db, id, nextHash);
1029
+ } catch {
1030
+ }
1031
+ markEmbeddingDirtyIfNeeded(db, id, nextHash, resolveEmbeddingProviderId(input.embedding_provider_id));
1032
+ }
465
1033
  }
466
1034
  return getMemory(db, id);
467
1035
  }
@@ -489,6 +1057,10 @@ function listMemories(db, opts) {
489
1057
  conditions.push("vitality >= ?");
490
1058
  params.push(opts.min_vitality);
491
1059
  }
1060
+ if (opts?.emotion_tag) {
1061
+ conditions.push("emotion_tag = ?");
1062
+ params.push(opts.emotion_tag);
1063
+ }
492
1064
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
493
1065
  const limit = opts?.limit ?? 100;
494
1066
  const offset = opts?.offset ?? 0;
@@ -513,8 +1085,215 @@ function countMemories(db, agent_id = "default") {
513
1085
  by_priority: Object.fromEntries(byPriority.map((r) => [`P${r.priority}`, r.c]))
514
1086
  };
515
1087
  }
1088
+ var TYPE_PRIORITY, PRIORITY_STABILITY;
1089
+ var init_memory = __esm({
1090
+ "src/core/memory.ts"() {
1091
+ "use strict";
1092
+ init_db();
1093
+ init_tokenizer();
1094
+ init_providers();
1095
+ init_vector();
1096
+ TYPE_PRIORITY = {
1097
+ identity: 0,
1098
+ emotion: 1,
1099
+ knowledge: 2,
1100
+ event: 3
1101
+ };
1102
+ PRIORITY_STABILITY = {
1103
+ 0: Infinity,
1104
+ // P0: never decays
1105
+ 1: 365,
1106
+ // P1: 365-day half-life
1107
+ 2: 90,
1108
+ // P2: 90-day half-life
1109
+ 3: 14
1110
+ // P3: 14-day half-life
1111
+ };
1112
+ }
1113
+ });
1114
+
1115
+ // src/core/path.ts
1116
+ function parseUri(uri) {
1117
+ const match = uri.match(/^([a-z]+):\/\/(.+)$/);
1118
+ if (!match) throw new Error(`Invalid URI: ${uri}. Expected format: domain://path`);
1119
+ return { domain: match[1], path: match[2] };
1120
+ }
1121
+ function createPath(db, memoryId, uri, alias, validDomains, agent_id) {
1122
+ const { domain } = parseUri(uri);
1123
+ const domains = validDomains ?? DEFAULT_DOMAINS;
1124
+ if (!domains.has(domain)) {
1125
+ throw new Error(`Invalid domain "${domain}". Valid: ${[...domains].join(", ")}`);
1126
+ }
1127
+ const memoryAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(memoryId)?.agent_id;
1128
+ if (!memoryAgent) throw new Error(`Memory not found: ${memoryId}`);
1129
+ if (agent_id && agent_id !== memoryAgent) {
1130
+ throw new Error(`Agent mismatch for path: memory agent_id=${memoryAgent}, requested agent_id=${agent_id}`);
1131
+ }
1132
+ const agentId = agent_id ?? memoryAgent;
1133
+ const existing = db.prepare("SELECT id FROM paths WHERE agent_id = ? AND uri = ?").get(agentId, uri);
1134
+ if (existing) {
1135
+ throw new Error(`URI already exists: ${uri}`);
1136
+ }
1137
+ const id = newId();
1138
+ db.prepare(
1139
+ "INSERT INTO paths (id, memory_id, agent_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
1140
+ ).run(id, memoryId, agentId, uri, alias ?? null, domain, now());
1141
+ return getPath(db, id);
1142
+ }
1143
+ function getPath(db, id) {
1144
+ return db.prepare("SELECT * FROM paths WHERE id = ?").get(id) ?? null;
1145
+ }
1146
+ function getPathByUri(db, uri, agent_id = "default") {
1147
+ return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri = ?").get(agent_id, uri) ?? null;
1148
+ }
1149
+ var DEFAULT_DOMAINS;
1150
+ var init_path = __esm({
1151
+ "src/core/path.ts"() {
1152
+ "use strict";
1153
+ init_db();
1154
+ DEFAULT_DOMAINS = /* @__PURE__ */ new Set(["core", "emotion", "knowledge", "event", "system"]);
1155
+ }
1156
+ });
1157
+
1158
+ // src/sleep/boot.ts
1159
+ var boot_exports = {};
1160
+ __export(boot_exports, {
1161
+ boot: () => boot,
1162
+ formatNarrativeBoot: () => formatNarrativeBoot,
1163
+ formatRelativeDate: () => formatRelativeDate,
1164
+ loadWarmBootLayers: () => loadWarmBootLayers
1165
+ });
1166
+ function formatRelativeDate(isoDate) {
1167
+ const diffMs = Date.now() - new Date(isoDate).getTime();
1168
+ const diffDays = Math.floor(diffMs / 864e5);
1169
+ if (diffDays <= 0) return "\u4ECA\u5929";
1170
+ if (diffDays === 1) return "\u6628\u5929";
1171
+ if (diffDays <= 7) return `${diffDays}\u5929\u524D`;
1172
+ return isoDate.slice(0, 10);
1173
+ }
1174
+ function loadWarmBootLayers(db, agentId) {
1175
+ const identity = listMemories(db, { agent_id: agentId, type: "identity", limit: 50 });
1176
+ const emotion = listMemories(db, { agent_id: agentId, type: "emotion", limit: 5 });
1177
+ const event = listMemories(db, { agent_id: agentId, type: "event", limit: 7 });
1178
+ const knowledge = listMemories(db, {
1179
+ agent_id: agentId,
1180
+ type: "knowledge",
1181
+ min_vitality: 0.5,
1182
+ limit: 10
1183
+ });
1184
+ return { identity, emotion, event, knowledge };
1185
+ }
1186
+ function formatNarrativeBoot(layers, agentName) {
1187
+ const lines = [];
1188
+ lines.push(`# ${agentName}\u7684\u56DE\u5FC6`);
1189
+ lines.push("");
1190
+ if (layers.identity.length > 0) {
1191
+ lines.push("## \u6211\u662F\u8C01");
1192
+ for (const mem of layers.identity) {
1193
+ lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)}`);
1194
+ }
1195
+ lines.push("");
1196
+ }
1197
+ if (layers.emotion.length > 0) {
1198
+ lines.push("## \u6700\u8FD1\u7684\u5FC3\u60C5");
1199
+ for (const mem of layers.emotion) {
1200
+ const tag = mem.emotion_tag;
1201
+ const time = formatRelativeDate(mem.updated_at);
1202
+ const tagStr = tag ? `${tag}, ${time}` : time;
1203
+ lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)} (${tagStr})`);
1204
+ }
1205
+ lines.push("");
1206
+ }
1207
+ if (layers.event.length > 0) {
1208
+ lines.push("## \u6700\u8FD1\u53D1\u751F\u7684\u4E8B");
1209
+ for (const mem of layers.event) {
1210
+ const time = formatRelativeDate(mem.updated_at);
1211
+ lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)} (${time})`);
1212
+ }
1213
+ lines.push("");
1214
+ }
1215
+ if (layers.knowledge.length > 0) {
1216
+ lines.push("## \u8FD8\u8BB0\u5F97\u7684\u77E5\u8BC6");
1217
+ for (const mem of layers.knowledge) {
1218
+ lines.push(`- ${mem.content.split("\n")[0].slice(0, 200)}`);
1219
+ }
1220
+ lines.push("");
1221
+ }
1222
+ return lines.join("\n");
1223
+ }
1224
+ function boot(db, opts) {
1225
+ const agentId = opts?.agent_id ?? "default";
1226
+ const format = opts?.format ?? "json";
1227
+ const agentName = opts?.agent_name ?? "Agent";
1228
+ const corePaths = opts?.corePaths ?? [
1229
+ "core://agent",
1230
+ "core://user",
1231
+ "core://agent/identity",
1232
+ "core://user/identity"
1233
+ ];
1234
+ const memories = /* @__PURE__ */ new Map();
1235
+ const identities = listMemories(db, { agent_id: agentId, priority: 0 });
1236
+ for (const mem of identities) {
1237
+ memories.set(mem.id, mem);
1238
+ recordAccess(db, mem.id, 1.1);
1239
+ }
1240
+ const bootPaths = [];
1241
+ for (const uri of corePaths) {
1242
+ const path = getPathByUri(db, uri, agentId);
1243
+ if (path) {
1244
+ bootPaths.push(uri);
1245
+ if (!memories.has(path.memory_id)) {
1246
+ const mem = getMemory(db, path.memory_id);
1247
+ if (mem) {
1248
+ memories.set(mem.id, mem);
1249
+ recordAccess(db, mem.id, 1.1);
1250
+ }
1251
+ }
1252
+ }
1253
+ }
1254
+ const bootEntry = getPathByUri(db, "system://boot", agentId);
1255
+ if (bootEntry) {
1256
+ const bootMem = getMemory(db, bootEntry.memory_id);
1257
+ if (bootMem) {
1258
+ const additionalUris = bootMem.content.split("\n").map((l) => l.trim()).filter((l) => l.match(/^[a-z]+:\/\//));
1259
+ for (const uri of additionalUris) {
1260
+ const path = getPathByUri(db, uri, agentId);
1261
+ if (path && !memories.has(path.memory_id)) {
1262
+ const mem = getMemory(db, path.memory_id);
1263
+ if (mem) {
1264
+ memories.set(mem.id, mem);
1265
+ bootPaths.push(uri);
1266
+ }
1267
+ }
1268
+ }
1269
+ }
1270
+ }
1271
+ const result = {
1272
+ identityMemories: [...memories.values()],
1273
+ bootPaths
1274
+ };
1275
+ if (format === "narrative") {
1276
+ const layers = loadWarmBootLayers(db, agentId);
1277
+ result.layers = layers;
1278
+ result.narrative = formatNarrativeBoot(layers, agentName);
1279
+ }
1280
+ return result;
1281
+ }
1282
+ var init_boot = __esm({
1283
+ "src/sleep/boot.ts"() {
1284
+ "use strict";
1285
+ init_path();
1286
+ init_memory();
1287
+ }
1288
+ });
1289
+
1290
+ // src/bin/agent-memory.ts
1291
+ init_db();
1292
+ import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
1293
+ import { basename, resolve } from "path";
516
1294
 
517
1295
  // src/core/export.ts
1296
+ init_memory();
518
1297
  import { writeFileSync, mkdirSync, existsSync } from "fs";
519
1298
  import { join } from "path";
520
1299
  function exportMemories(db, dirPath, opts) {
@@ -577,7 +1356,17 @@ function exportMemories(db, dirPath, opts) {
577
1356
  return { exported, files };
578
1357
  }
579
1358
 
1359
+ // src/bin/agent-memory.ts
1360
+ init_boot();
1361
+
1362
+ // src/app/surface.ts
1363
+ init_memory();
1364
+
1365
+ // src/search/hybrid.ts
1366
+ init_memory();
1367
+
580
1368
  // src/search/bm25.ts
1369
+ init_tokenizer();
581
1370
  function searchBM25(db, query, opts) {
582
1371
  const limit = opts?.limit ?? 20;
583
1372
  const agentId = opts?.agent_id ?? "default";
@@ -595,12 +1384,13 @@ function searchBM25(db, query, opts) {
595
1384
  ORDER BY rank
596
1385
  LIMIT ?`
597
1386
  ).all(ftsQuery, agentId, minVitality, limit);
598
- return rows.map((row) => {
1387
+ return rows.map((row, index) => {
599
1388
  const { score: _score, ...memoryFields } = row;
600
1389
  return {
601
1390
  memory: memoryFields,
602
1391
  score: Math.abs(row.score),
603
1392
  // FTS5 rank is negative (lower = better)
1393
+ rank: index + 1,
604
1394
  matchReason: "bm25"
605
1395
  };
606
1396
  });
@@ -615,10 +1405,10 @@ function searchSimple(db, query, agentId, minVitality, limit) {
615
1405
  ORDER BY priority ASC, updated_at DESC
616
1406
  LIMIT ?`
617
1407
  ).all(agentId, minVitality, `%${query}%`, limit);
618
- return rows.map((m, i) => ({
619
- memory: m,
620
- score: 1 / (i + 1),
621
- // Simple rank by position
1408
+ return rows.map((memory, index) => ({
1409
+ memory,
1410
+ score: 1 / (index + 1),
1411
+ rank: index + 1,
622
1412
  matchReason: "like"
623
1413
  }));
624
1414
  }
@@ -627,96 +1417,957 @@ function buildFtsQuery(text) {
627
1417
  if (tokens.length === 0) return null;
628
1418
  return tokens.map((w) => `"${w}"`).join(" OR ");
629
1419
  }
1420
+ function rebuildBm25Index(db, opts) {
1421
+ const memories = opts?.agent_id ? db.prepare("SELECT id, content FROM memories WHERE agent_id = ?").all(opts.agent_id) : db.prepare("SELECT id, content FROM memories").all();
1422
+ const insert = db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)");
1423
+ const deleteOne = db.prepare("DELETE FROM memories_fts WHERE id = ?");
1424
+ const transaction = db.transaction(() => {
1425
+ if (!opts?.agent_id) {
1426
+ db.exec("DELETE FROM memories_fts");
1427
+ }
1428
+ for (const memory of memories) {
1429
+ if (opts?.agent_id) {
1430
+ deleteOne.run(memory.id);
1431
+ }
1432
+ insert.run(memory.id, tokenizeForIndex(memory.content));
1433
+ }
1434
+ });
1435
+ transaction();
1436
+ return { reindexed: memories.length };
1437
+ }
630
1438
 
631
- // src/core/path.ts
632
- var DEFAULT_DOMAINS = /* @__PURE__ */ new Set(["core", "emotion", "knowledge", "event", "system"]);
633
- function parseUri(uri) {
634
- const match = uri.match(/^([a-z]+):\/\/(.+)$/);
635
- if (!match) throw new Error(`Invalid URI: ${uri}. Expected format: domain://path`);
636
- return { domain: match[1], path: match[2] };
1439
+ // src/search/hybrid.ts
1440
+ init_providers();
1441
+ init_vector();
1442
+ var PRIORITY_WEIGHT = {
1443
+ 0: 4,
1444
+ 1: 3,
1445
+ 2: 2,
1446
+ 3: 1
1447
+ };
1448
+ var PRIORITY_PRIOR = {
1449
+ 0: 1,
1450
+ 1: 0.75,
1451
+ 2: 0.5,
1452
+ 3: 0.25
1453
+ };
1454
+ function scoreBm25Only(results, limit) {
1455
+ return results.map((row) => {
1456
+ const weight = PRIORITY_WEIGHT[row.memory.priority] ?? 1;
1457
+ const vitality = Math.max(0.1, row.memory.vitality);
1458
+ return {
1459
+ memory: row.memory,
1460
+ score: row.score * weight * vitality,
1461
+ bm25_rank: row.rank,
1462
+ bm25_score: row.score
1463
+ };
1464
+ }).sort((left, right) => right.score - left.score).slice(0, limit);
637
1465
  }
638
- function createPath(db, memoryId, uri, alias, validDomains, agent_id) {
639
- const { domain } = parseUri(uri);
640
- const domains = validDomains ?? DEFAULT_DOMAINS;
641
- if (!domains.has(domain)) {
642
- throw new Error(`Invalid domain "${domain}". Valid: ${[...domains].join(", ")}`);
643
- }
644
- const memoryAgent = db.prepare("SELECT agent_id FROM memories WHERE id = ?").get(memoryId)?.agent_id;
645
- if (!memoryAgent) throw new Error(`Memory not found: ${memoryId}`);
646
- if (agent_id && agent_id !== memoryAgent) {
647
- throw new Error(`Agent mismatch for path: memory agent_id=${memoryAgent}, requested agent_id=${agent_id}`);
1466
+ function priorityPrior(priority) {
1467
+ return PRIORITY_PRIOR[priority] ?? 0.25;
1468
+ }
1469
+ function fusionScore(input) {
1470
+ const lexical = input.bm25Rank ? 0.45 / (60 + input.bm25Rank) : 0;
1471
+ const semantic = input.vectorRank ? 0.45 / (60 + input.vectorRank) : 0;
1472
+ return lexical + semantic + 0.05 * priorityPrior(input.memory.priority) + 0.05 * input.memory.vitality;
1473
+ }
1474
+ function fuseHybridResults(lexical, vector, limit) {
1475
+ const candidates = /* @__PURE__ */ new Map();
1476
+ for (const row of lexical) {
1477
+ candidates.set(row.memory.id, {
1478
+ memory: row.memory,
1479
+ score: 0,
1480
+ bm25_rank: row.rank,
1481
+ bm25_score: row.score
1482
+ });
648
1483
  }
649
- const agentId = agent_id ?? memoryAgent;
650
- const existing = db.prepare("SELECT id FROM paths WHERE agent_id = ? AND uri = ?").get(agentId, uri);
651
- if (existing) {
652
- throw new Error(`URI already exists: ${uri}`);
1484
+ for (const row of vector) {
1485
+ const existing = candidates.get(row.memory.id);
1486
+ if (existing) {
1487
+ existing.vector_rank = row.rank;
1488
+ existing.vector_score = row.similarity;
1489
+ } else {
1490
+ candidates.set(row.memory.id, {
1491
+ memory: row.memory,
1492
+ score: 0,
1493
+ vector_rank: row.rank,
1494
+ vector_score: row.similarity
1495
+ });
1496
+ }
653
1497
  }
654
- const id = newId();
655
- db.prepare(
656
- "INSERT INTO paths (id, memory_id, agent_id, uri, alias, domain, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)"
657
- ).run(id, memoryId, agentId, uri, alias ?? null, domain, now());
658
- return getPath(db, id);
1498
+ return [...candidates.values()].map((row) => ({
1499
+ ...row,
1500
+ score: fusionScore({ memory: row.memory, bm25Rank: row.bm25_rank, vectorRank: row.vector_rank })
1501
+ })).sort((left, right) => {
1502
+ if (right.score !== left.score) return right.score - left.score;
1503
+ return right.memory.updated_at.localeCompare(left.memory.updated_at);
1504
+ }).slice(0, limit);
659
1505
  }
660
- function getPath(db, id) {
661
- return db.prepare("SELECT * FROM paths WHERE id = ?").get(id) ?? null;
662
- }
663
- function getPathByUri(db, uri, agent_id = "default") {
664
- return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri = ?").get(agent_id, uri) ?? null;
1506
+ async function searchVectorBranch(db, query, opts) {
1507
+ const [queryVector] = await opts.provider.embed([query]);
1508
+ if (!queryVector) return [];
1509
+ return searchByVector(db, queryVector, {
1510
+ providerId: opts.provider.id,
1511
+ agent_id: opts.agent_id,
1512
+ limit: opts.limit,
1513
+ min_vitality: opts.min_vitality
1514
+ });
665
1515
  }
666
-
667
- // src/sleep/boot.ts
668
- function boot(db, opts) {
1516
+ async function recallMemories(db, query, opts) {
1517
+ const limit = opts?.limit ?? 10;
669
1518
  const agentId = opts?.agent_id ?? "default";
670
- const corePaths = opts?.corePaths ?? [
671
- "core://agent",
672
- "core://user",
673
- "core://agent/identity",
674
- "core://user/identity"
675
- ];
676
- const memories = /* @__PURE__ */ new Map();
677
- const identities = listMemories(db, { agent_id: agentId, priority: 0 });
678
- for (const mem of identities) {
679
- memories.set(mem.id, mem);
680
- recordAccess(db, mem.id, 1.1);
1519
+ const minVitality = opts?.min_vitality ?? 0;
1520
+ const lexicalLimit = opts?.lexicalLimit ?? Math.max(limit * 2, limit);
1521
+ const vectorLimit = opts?.vectorLimit ?? Math.max(limit * 2, limit);
1522
+ const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
1523
+ const lexical = searchBM25(db, query, {
1524
+ agent_id: agentId,
1525
+ limit: lexicalLimit,
1526
+ min_vitality: minVitality
1527
+ });
1528
+ let vector = [];
1529
+ if (provider) {
1530
+ try {
1531
+ vector = await searchVectorBranch(db, query, {
1532
+ provider,
1533
+ agent_id: agentId,
1534
+ limit: vectorLimit,
1535
+ min_vitality: minVitality
1536
+ });
1537
+ } catch {
1538
+ vector = [];
1539
+ }
681
1540
  }
682
- const bootPaths = [];
683
- for (const uri of corePaths) {
684
- const path = getPathByUri(db, uri, agentId);
685
- if (path) {
686
- bootPaths.push(uri);
687
- if (!memories.has(path.memory_id)) {
688
- const mem = getMemory(db, path.memory_id);
689
- if (mem) {
690
- memories.set(mem.id, mem);
691
- recordAccess(db, mem.id, 1.1);
692
- }
1541
+ const mode = vector.length > 0 && lexical.length > 0 ? "dual-path" : vector.length > 0 ? "vector-only" : "bm25-only";
1542
+ const results = mode === "bm25-only" ? scoreBm25Only(lexical, limit) : fuseHybridResults(lexical, vector, limit);
1543
+ if (opts?.recordAccess !== false) {
1544
+ for (const row of results) {
1545
+ recordAccess(db, row.memory.id);
1546
+ }
1547
+ }
1548
+ return {
1549
+ mode,
1550
+ providerId: provider?.id ?? null,
1551
+ usedVectorSearch: vector.length > 0,
1552
+ results
1553
+ };
1554
+ }
1555
+ function listReindexCandidates(db, providerId, agentId, force) {
1556
+ const rows = db.prepare(
1557
+ `SELECT m.id as memoryId,
1558
+ m.content as content,
1559
+ m.hash as contentHash,
1560
+ e.status as embeddingStatus,
1561
+ e.content_hash as embeddingHash
1562
+ FROM memories m
1563
+ LEFT JOIN embeddings e
1564
+ ON e.memory_id = m.id
1565
+ AND e.provider_id = ?
1566
+ WHERE m.agent_id = ?
1567
+ AND m.hash IS NOT NULL`
1568
+ ).all(providerId, agentId);
1569
+ return rows.filter((row) => {
1570
+ if (force) return true;
1571
+ if (!row.embeddingStatus) return true;
1572
+ if (row.embeddingStatus !== "ready") return true;
1573
+ return row.embeddingHash !== row.contentHash;
1574
+ }).map((row) => ({
1575
+ memoryId: row.memoryId,
1576
+ content: row.content,
1577
+ contentHash: row.contentHash
1578
+ }));
1579
+ }
1580
+ async function reindexEmbeddings(db, opts) {
1581
+ const provider = opts?.provider === void 0 ? getEmbeddingProviderFromEnv() : opts.provider;
1582
+ if (!provider) {
1583
+ return {
1584
+ enabled: false,
1585
+ providerId: null,
1586
+ scanned: 0,
1587
+ pending: 0,
1588
+ embedded: 0,
1589
+ failed: 0
1590
+ };
1591
+ }
1592
+ const agentId = opts?.agent_id ?? "default";
1593
+ const force = opts?.force ?? false;
1594
+ const batchSize = Math.max(1, opts?.batchSize ?? 16);
1595
+ const candidates = listReindexCandidates(db, provider.id, agentId, force);
1596
+ for (const candidate of candidates) {
1597
+ markMemoryEmbeddingPending(db, candidate.memoryId, provider.id, candidate.contentHash);
1598
+ }
1599
+ let embedded = 0;
1600
+ let failed = 0;
1601
+ for (let index = 0; index < candidates.length; index += batchSize) {
1602
+ const batch = candidates.slice(index, index + batchSize);
1603
+ try {
1604
+ const vectors = await provider.embed(batch.map((row) => row.content));
1605
+ if (vectors.length !== batch.length) {
1606
+ throw new Error(`Expected ${batch.length} embeddings, received ${vectors.length}`);
1607
+ }
1608
+ for (let offset = 0; offset < batch.length; offset++) {
1609
+ upsertReadyEmbedding({
1610
+ db,
1611
+ memoryId: batch[offset].memoryId,
1612
+ providerId: provider.id,
1613
+ vector: vectors[offset],
1614
+ contentHash: batch[offset].contentHash
1615
+ });
1616
+ embedded += 1;
1617
+ }
1618
+ } catch {
1619
+ for (const candidate of batch) {
1620
+ markEmbeddingFailed(db, candidate.memoryId, provider.id, candidate.contentHash);
1621
+ failed += 1;
693
1622
  }
694
1623
  }
695
1624
  }
696
- const bootEntry = getPathByUri(db, "system://boot", agentId);
697
- if (bootEntry) {
698
- const bootMem = getMemory(db, bootEntry.memory_id);
699
- if (bootMem) {
700
- const additionalUris = bootMem.content.split("\n").map((l) => l.trim()).filter((l) => l.match(/^[a-z]+:\/\//));
701
- for (const uri of additionalUris) {
702
- const path = getPathByUri(db, uri, agentId);
703
- if (path && !memories.has(path.memory_id)) {
704
- const mem = getMemory(db, path.memory_id);
705
- if (mem) {
706
- memories.set(mem.id, mem);
707
- bootPaths.push(uri);
708
- }
709
- }
1625
+ return {
1626
+ enabled: true,
1627
+ providerId: provider.id,
1628
+ scanned: candidates.length,
1629
+ pending: candidates.length,
1630
+ embedded,
1631
+ failed
1632
+ };
1633
+ }
1634
+
1635
+ // src/app/surface.ts
1636
+ init_providers();
1637
+ init_tokenizer();
1638
+ init_vector();
1639
+
1640
+ // src/app/feedback.ts
1641
+ init_db();
1642
+ function clamp01(value) {
1643
+ if (!Number.isFinite(value)) return 0;
1644
+ return Math.max(0, Math.min(1, value));
1645
+ }
1646
+ function recordFeedbackEvent(db, input) {
1647
+ const id = newId();
1648
+ const created_at = now();
1649
+ const agentId = input.agent_id ?? "default";
1650
+ const useful = input.useful ? 1 : 0;
1651
+ const value = input.useful ? 1 : 0;
1652
+ const eventType = `${input.source}:${input.useful ? "useful" : "not_useful"}`;
1653
+ const exists = db.prepare("SELECT id FROM memories WHERE id = ?").get(input.memory_id);
1654
+ if (!exists) {
1655
+ throw new Error(`Memory not found: ${input.memory_id}`);
1656
+ }
1657
+ try {
1658
+ db.prepare(
1659
+ `INSERT INTO feedback_events (id, memory_id, source, useful, agent_id, event_type, value, created_at)
1660
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
1661
+ ).run(id, input.memory_id, input.source, useful, agentId, eventType, value, created_at);
1662
+ } catch {
1663
+ db.prepare(
1664
+ `INSERT INTO feedback_events (id, memory_id, event_type, value, created_at)
1665
+ VALUES (?, ?, ?, ?, ?)`
1666
+ ).run(id, input.memory_id, eventType, value, created_at);
1667
+ }
1668
+ return {
1669
+ id,
1670
+ memory_id: input.memory_id,
1671
+ source: input.source,
1672
+ useful: input.useful,
1673
+ agent_id: agentId,
1674
+ created_at,
1675
+ value
1676
+ };
1677
+ }
1678
+ function getFeedbackSummary(db, memoryId, agentId) {
1679
+ try {
1680
+ const row = db.prepare(
1681
+ `SELECT COUNT(*) as total,
1682
+ COALESCE(SUM(CASE WHEN useful = 1 THEN 1 ELSE 0 END), 0) as useful,
1683
+ COALESCE(SUM(CASE WHEN useful = 0 THEN 1 ELSE 0 END), 0) as not_useful
1684
+ FROM feedback_events
1685
+ WHERE memory_id = ?
1686
+ AND (? IS NULL OR agent_id = ?)`
1687
+ ).get(memoryId, agentId ?? null, agentId ?? null);
1688
+ if (!row || row.total === 0) {
1689
+ return { total: 0, useful: 0, not_useful: 0, score: 0.5 };
1690
+ }
1691
+ return {
1692
+ total: row.total,
1693
+ useful: row.useful,
1694
+ not_useful: row.not_useful,
1695
+ score: clamp01(row.useful / row.total)
1696
+ };
1697
+ } catch {
1698
+ const row = db.prepare(
1699
+ `SELECT COUNT(*) as total,
1700
+ COALESCE(SUM(CASE WHEN value >= 0.5 THEN 1 ELSE 0 END), 0) as useful,
1701
+ COALESCE(SUM(CASE WHEN value < 0.5 THEN 1 ELSE 0 END), 0) as not_useful,
1702
+ COALESCE(AVG(value), 0.5) as avg_value
1703
+ FROM feedback_events
1704
+ WHERE memory_id = ?`
1705
+ ).get(memoryId);
1706
+ if (!row || row.total === 0) {
1707
+ return { total: 0, useful: 0, not_useful: 0, score: 0.5 };
1708
+ }
1709
+ return {
1710
+ total: row.total,
1711
+ useful: row.useful,
1712
+ not_useful: row.not_useful,
1713
+ score: clamp01(row.avg_value)
1714
+ };
1715
+ }
1716
+ }
1717
+
1718
+ // src/app/surface.ts
1719
+ var INTENT_PRIORS = {
1720
+ factual: {
1721
+ identity: 0.25,
1722
+ emotion: 0.15,
1723
+ knowledge: 1,
1724
+ event: 0.8
1725
+ },
1726
+ preference: {
1727
+ identity: 1,
1728
+ emotion: 0.85,
1729
+ knowledge: 0.55,
1730
+ event: 0.25
1731
+ },
1732
+ temporal: {
1733
+ identity: 0.15,
1734
+ emotion: 0.35,
1735
+ knowledge: 0.5,
1736
+ event: 1
1737
+ },
1738
+ planning: {
1739
+ identity: 0.65,
1740
+ emotion: 0.2,
1741
+ knowledge: 1,
1742
+ event: 0.6
1743
+ },
1744
+ design: {
1745
+ identity: 0.8,
1746
+ emotion: 0.35,
1747
+ knowledge: 1,
1748
+ event: 0.25
1749
+ }
1750
+ };
1751
+ var DESIGN_HINT_RE = /\b(ui|ux|design|style|component|layout|brand|palette|theme)\b|风格|界面|设计|配色|低饱和|玻璃拟态|渐变/i;
1752
+ var PLANNING_HINT_RE = /\b(plan|planning|todo|next|ship|build|implement|roadmap|task|milestone)\b|计划|下一步|待办|实现|重构/i;
1753
+ var FACTUAL_HINT_RE = /\b(what|fact|constraint|rule|docs|document|api|status)\b|规则|约束|文档|接口|事实/i;
1754
+ var TEMPORAL_HINT_RE = /\b(today|yesterday|tomorrow|recent|before|after|when|timeline)\b|今天|昨天|明天|最近|时间线|何时/i;
1755
+ var PREFERENCE_HINT_RE = /\b(prefer|preference|like|dislike|avoid|favorite)\b|喜欢|偏好|不喜欢|避免|讨厌/i;
1756
+ function clamp012(value) {
1757
+ if (!Number.isFinite(value)) return 0;
1758
+ return Math.max(0, Math.min(1, value));
1759
+ }
1760
+ function uniqueTokenSet(values) {
1761
+ return new Set(
1762
+ values.flatMap((value) => tokenize(value ?? "")).map((token) => token.trim()).filter(Boolean)
1763
+ );
1764
+ }
1765
+ function overlapScore(left, right) {
1766
+ if (left.size === 0 || right.size === 0) return 0;
1767
+ let shared = 0;
1768
+ for (const token of left) {
1769
+ if (right.has(token)) shared += 1;
1770
+ }
1771
+ return clamp012(shared / Math.max(left.size, right.size));
1772
+ }
1773
+ function rankScore(rank, window) {
1774
+ if (!rank) return 0;
1775
+ return clamp012(1 - (rank - 1) / Math.max(window, 1));
1776
+ }
1777
+ function topicLabel(...parts) {
1778
+ const token = parts.flatMap((part) => tokenize(part ?? "")).find((value) => value.trim().length > 1);
1779
+ const label = (token ?? "context").replace(/[^\p{L}\p{N}_-]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 32);
1780
+ return label || "context";
1781
+ }
1782
+ function intentKeywordBoost(memory, intent) {
1783
+ const content = memory.content;
1784
+ switch (intent) {
1785
+ case "design":
1786
+ return DESIGN_HINT_RE.test(content) ? 1 : 0.65;
1787
+ case "planning":
1788
+ return PLANNING_HINT_RE.test(content) ? 1 : 0.7;
1789
+ case "factual":
1790
+ return FACTUAL_HINT_RE.test(content) ? 1 : 0.75;
1791
+ case "temporal":
1792
+ return TEMPORAL_HINT_RE.test(content) ? 1 : 0.75;
1793
+ case "preference":
1794
+ return PREFERENCE_HINT_RE.test(content) ? 1 : 0.8;
1795
+ }
1796
+ }
1797
+ function intentMatch(memory, intent) {
1798
+ if (!intent) return 0;
1799
+ const prior = INTENT_PRIORS[intent][memory.type] ?? 0;
1800
+ return clamp012(prior * intentKeywordBoost(memory, intent));
1801
+ }
1802
+ function buildReasonCodes(input) {
1803
+ const reasons = /* @__PURE__ */ new Set();
1804
+ reasons.add(`type:${input.memory.type}`);
1805
+ if (input.semanticScore > 0.2) {
1806
+ reasons.add(`semantic:${topicLabel(input.query, input.task)}`);
1807
+ }
1808
+ if (input.lexicalScore > 0.2 && input.query) {
1809
+ reasons.add(`lexical:${topicLabel(input.query)}`);
1810
+ }
1811
+ if (input.taskMatch > 0.2) {
1812
+ reasons.add(`task:${topicLabel(input.task, input.intent)}`);
1813
+ }
1814
+ if (input.intent) {
1815
+ reasons.add(`intent:${input.intent}`);
1816
+ }
1817
+ if (input.feedbackScore >= 0.67) {
1818
+ reasons.add("feedback:reinforced");
1819
+ } else if (input.feedbackScore <= 0.33) {
1820
+ reasons.add("feedback:negative");
1821
+ }
1822
+ return [...reasons];
1823
+ }
1824
+ function collectBranch(signals, rows, key, similarity) {
1825
+ for (const row of rows) {
1826
+ const existing = signals.get(row.memory.id) ?? { memory: row.memory };
1827
+ const currentRank = existing[key];
1828
+ if (currentRank === void 0 || row.rank < currentRank) {
1829
+ existing[key] = row.rank;
1830
+ }
1831
+ if (similarity) {
1832
+ const currentSimilarity = similarity.get(row.memory.id);
1833
+ if (currentSimilarity !== void 0) {
1834
+ existing.semanticSimilarity = Math.max(existing.semanticSimilarity ?? 0, currentSimilarity);
1835
+ }
1836
+ }
1837
+ signals.set(row.memory.id, existing);
1838
+ }
1839
+ }
1840
+ async function surfaceMemories(db, input) {
1841
+ const agentId = input.agent_id ?? "default";
1842
+ const limit = Math.max(1, Math.min(input.limit ?? 5, 20));
1843
+ const lexicalWindow = Math.max(24, limit * 6);
1844
+ const minVitality = input.min_vitality ?? 0.05;
1845
+ const provider = input.provider === void 0 ? getEmbeddingProviderFromEnv() : input.provider;
1846
+ const signals = /* @__PURE__ */ new Map();
1847
+ const trimmedQuery = input.query?.trim();
1848
+ const trimmedTask = input.task?.trim();
1849
+ const recentTurns = (input.recent_turns ?? []).map((turn) => turn.trim()).filter(Boolean).slice(-4);
1850
+ const queryTokens = uniqueTokenSet([trimmedQuery, ...recentTurns]);
1851
+ const taskTokens = uniqueTokenSet([trimmedTask]);
1852
+ if (trimmedQuery) {
1853
+ collectBranch(
1854
+ signals,
1855
+ searchBM25(db, trimmedQuery, {
1856
+ agent_id: agentId,
1857
+ limit: lexicalWindow,
1858
+ min_vitality: minVitality
1859
+ }),
1860
+ "queryRank"
1861
+ );
1862
+ }
1863
+ if (trimmedTask) {
1864
+ collectBranch(
1865
+ signals,
1866
+ searchBM25(db, trimmedTask, {
1867
+ agent_id: agentId,
1868
+ limit: lexicalWindow,
1869
+ min_vitality: minVitality
1870
+ }),
1871
+ "taskRank"
1872
+ );
1873
+ }
1874
+ if (recentTurns.length > 0) {
1875
+ collectBranch(
1876
+ signals,
1877
+ searchBM25(db, recentTurns.join(" "), {
1878
+ agent_id: agentId,
1879
+ limit: lexicalWindow,
1880
+ min_vitality: minVitality
1881
+ }),
1882
+ "recentRank"
1883
+ );
1884
+ }
1885
+ const semanticQuery = [trimmedQuery, trimmedTask, ...recentTurns].filter(Boolean).join("\n").trim();
1886
+ if (provider && semanticQuery) {
1887
+ try {
1888
+ const [queryVector] = await provider.embed([semanticQuery]);
1889
+ if (queryVector) {
1890
+ const vectorRows = searchByVector(db, queryVector, {
1891
+ providerId: provider.id,
1892
+ agent_id: agentId,
1893
+ limit: lexicalWindow,
1894
+ min_vitality: minVitality
1895
+ });
1896
+ const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
1897
+ collectBranch(signals, vectorRows, "semanticRank", similarity);
710
1898
  }
1899
+ } catch {
1900
+ }
1901
+ }
1902
+ const fallbackMemories = listMemories(db, {
1903
+ agent_id: agentId,
1904
+ min_vitality: minVitality,
1905
+ limit: Math.max(48, lexicalWindow)
1906
+ });
1907
+ for (const memory of fallbackMemories) {
1908
+ if (!signals.has(memory.id)) {
1909
+ signals.set(memory.id, { memory });
711
1910
  }
712
1911
  }
1912
+ const results = [...signals.values()].map((signal) => signal.memory).filter((memory) => memory.vitality >= minVitality).filter((memory) => input.types?.length ? input.types.includes(memory.type) : true).filter((memory) => input.emotion_tag ? memory.emotion_tag === input.emotion_tag : true).map((memory) => {
1913
+ const signal = signals.get(memory.id) ?? { memory };
1914
+ const memoryTokens = new Set(tokenize(memory.content));
1915
+ const lexicalOverlap = overlapScore(memoryTokens, queryTokens);
1916
+ const taskOverlap = overlapScore(memoryTokens, taskTokens);
1917
+ const lexicalScore = clamp012(
1918
+ 0.45 * rankScore(signal.queryRank, lexicalWindow) + 0.15 * rankScore(signal.recentRank, lexicalWindow) + 0.15 * rankScore(signal.taskRank, lexicalWindow) + 0.25 * lexicalOverlap
1919
+ );
1920
+ const semanticScore = signal.semanticSimilarity !== void 0 ? clamp012(Math.max(signal.semanticSimilarity, lexicalOverlap * 0.7)) : trimmedQuery || recentTurns.length > 0 ? clamp012(lexicalOverlap * 0.7) : 0;
1921
+ const intentScore = intentMatch(memory, input.intent);
1922
+ const taskMatch = trimmedTask ? clamp012(0.7 * taskOverlap + 0.3 * intentScore) : intentScore;
1923
+ const priorityScore = priorityPrior(memory.priority);
1924
+ const feedbackSummary = getFeedbackSummary(db, memory.id, agentId);
1925
+ const feedbackScore = feedbackSummary.score;
1926
+ const score = clamp012(
1927
+ 0.35 * semanticScore + 0.2 * lexicalScore + 0.15 * taskMatch + 0.1 * memory.vitality + 0.1 * priorityScore + 0.1 * feedbackScore
1928
+ );
1929
+ return {
1930
+ memory,
1931
+ score,
1932
+ semantic_score: semanticScore,
1933
+ lexical_score: lexicalScore,
1934
+ task_match: taskMatch,
1935
+ vitality: memory.vitality,
1936
+ priority_prior: priorityScore,
1937
+ feedback_score: feedbackScore,
1938
+ feedback_summary: feedbackSummary,
1939
+ reason_codes: buildReasonCodes({
1940
+ memory,
1941
+ query: semanticQuery || trimmedQuery,
1942
+ task: trimmedTask,
1943
+ intent: input.intent,
1944
+ semanticScore,
1945
+ lexicalScore,
1946
+ taskMatch,
1947
+ feedbackScore
1948
+ }),
1949
+ lexical_rank: signal.queryRank ?? signal.recentRank ?? signal.taskRank,
1950
+ semantic_rank: signal.semanticRank,
1951
+ semantic_similarity: signal.semanticSimilarity
1952
+ };
1953
+ }).sort((left, right) => {
1954
+ if (right.score !== left.score) return right.score - left.score;
1955
+ if (right.semantic_score !== left.semantic_score) return right.semantic_score - left.semantic_score;
1956
+ if (right.lexical_score !== left.lexical_score) return right.lexical_score - left.lexical_score;
1957
+ if (left.memory.priority !== right.memory.priority) return left.memory.priority - right.memory.priority;
1958
+ return right.memory.updated_at.localeCompare(left.memory.updated_at);
1959
+ }).slice(0, limit);
713
1960
  return {
714
- identityMemories: [...memories.values()],
715
- bootPaths
1961
+ count: results.length,
1962
+ query: trimmedQuery,
1963
+ task: trimmedTask,
1964
+ intent: input.intent,
1965
+ results
716
1966
  };
717
1967
  }
718
1968
 
1969
+ // src/transports/http.ts
1970
+ init_db();
1971
+ import { randomUUID as randomUUID2 } from "crypto";
1972
+ import http from "http";
1973
+
1974
+ // src/sleep/sync.ts
1975
+ init_memory();
1976
+ init_path();
1977
+
1978
+ // src/core/guard.ts
1979
+ init_providers();
1980
+ init_tokenizer();
1981
+ init_path();
1982
+
1983
+ // src/core/merge.ts
1984
+ function uniqueNonEmpty(values) {
1985
+ return [...new Set(values.map((value) => value?.trim()).filter((value) => Boolean(value)))];
1986
+ }
1987
+ function splitClauses(content) {
1988
+ return content.split(/[\n;;。.!?!?]+/).map((part) => part.trim()).filter(Boolean);
1989
+ }
1990
+ function mergeAliases(existing, incoming, content) {
1991
+ const aliases = uniqueNonEmpty([
1992
+ existing !== content ? existing : void 0,
1993
+ incoming !== content ? incoming : void 0
1994
+ ]);
1995
+ return aliases.length > 0 ? aliases : void 0;
1996
+ }
1997
+ function replaceIdentity(context) {
1998
+ const content = context.incoming.content.trim();
1999
+ return {
2000
+ strategy: "replace",
2001
+ content,
2002
+ aliases: mergeAliases(context.existing.content, context.incoming.content, content),
2003
+ notes: ["identity canonicalized to the newest authoritative phrasing"]
2004
+ };
2005
+ }
2006
+ function appendEmotionEvidence(context) {
2007
+ const lines = uniqueNonEmpty([
2008
+ ...context.existing.content.split(/\n+/),
2009
+ context.incoming.content
2010
+ ]);
2011
+ const content = lines.length <= 1 ? lines[0] ?? context.incoming.content.trim() : [lines[0], "", ...lines.slice(1).map((line) => `- ${line.replace(/^-\s*/, "")}`)].join("\n");
2012
+ return {
2013
+ strategy: "append_evidence",
2014
+ content,
2015
+ aliases: mergeAliases(context.existing.content, context.incoming.content, content),
2016
+ notes: ["emotion evidence appended to preserve timeline without duplicating identical lines"]
2017
+ };
2018
+ }
2019
+ function synthesizeKnowledge(context) {
2020
+ const clauses = uniqueNonEmpty([
2021
+ ...splitClauses(context.existing.content),
2022
+ ...splitClauses(context.incoming.content)
2023
+ ]);
2024
+ const content = clauses.length <= 1 ? clauses[0] ?? context.incoming.content.trim() : clauses.join("\uFF1B");
2025
+ return {
2026
+ strategy: "synthesize",
2027
+ content,
2028
+ aliases: mergeAliases(context.existing.content, context.incoming.content, content),
2029
+ notes: ["knowledge statements synthesized into a canonical summary"]
2030
+ };
2031
+ }
2032
+ function compactEventTimeline(context) {
2033
+ const points = uniqueNonEmpty([
2034
+ ...context.existing.content.split(/\n+/),
2035
+ context.incoming.content
2036
+ ]).map((line) => line.replace(/^-\s*/, ""));
2037
+ const content = points.length <= 1 ? points[0] ?? context.incoming.content.trim() : ["Timeline:", ...points.map((line) => `- ${line}`)].join("\n");
2038
+ return {
2039
+ strategy: "compact_timeline",
2040
+ content,
2041
+ aliases: mergeAliases(context.existing.content, context.incoming.content, content),
2042
+ notes: ["event observations compacted into a single timeline window"]
2043
+ };
2044
+ }
2045
+ function buildMergePlan(context) {
2046
+ const type = context.incoming.type ?? context.existing.type;
2047
+ switch (type) {
2048
+ case "identity":
2049
+ return replaceIdentity(context);
2050
+ case "emotion":
2051
+ return appendEmotionEvidence(context);
2052
+ case "knowledge":
2053
+ return synthesizeKnowledge(context);
2054
+ case "event":
2055
+ return compactEventTimeline(context);
2056
+ }
2057
+ }
2058
+
2059
+ // src/core/guard.ts
2060
+ init_memory();
2061
+ var NEAR_EXACT_THRESHOLD = 0.93;
2062
+ var MERGE_THRESHOLD = 0.82;
2063
+ function clamp013(value) {
2064
+ if (!Number.isFinite(value)) return 0;
2065
+ return Math.max(0, Math.min(1, value));
2066
+ }
2067
+ function uniqueTokenSet2(text) {
2068
+ return new Set(tokenize(text));
2069
+ }
2070
+ function overlapScore2(left, right) {
2071
+ const a = new Set(left);
2072
+ const b = new Set(right);
2073
+ if (a.size === 0 || b.size === 0) return 0;
2074
+ let shared = 0;
2075
+ for (const token of a) {
2076
+ if (b.has(token)) shared += 1;
2077
+ }
2078
+ return shared / Math.max(a.size, b.size);
2079
+ }
2080
+ function extractEntities(text) {
2081
+ const matches = text.match(/[A-Z][A-Za-z0-9_-]+|\d+(?:[-/:]\d+)*|[#@][\w-]+|[\u4e00-\u9fff]{2,}|\w+:\/\/[^\s]+/g) ?? [];
2082
+ return new Set(matches.map((value) => value.trim()).filter(Boolean));
2083
+ }
2084
+ function safeDomain(uri) {
2085
+ if (!uri) return null;
2086
+ try {
2087
+ return parseUri(uri).domain;
2088
+ } catch {
2089
+ return null;
2090
+ }
2091
+ }
2092
+ function getPrimaryUri(db, memoryId, agentId) {
2093
+ const row = db.prepare("SELECT uri FROM paths WHERE memory_id = ? AND agent_id = ? ORDER BY created_at DESC LIMIT 1").get(memoryId, agentId);
2094
+ return row?.uri ?? null;
2095
+ }
2096
+ function uriScopeMatch(inputUri, candidateUri) {
2097
+ if (inputUri && candidateUri) {
2098
+ if (inputUri === candidateUri) return 1;
2099
+ const inputDomain2 = safeDomain(inputUri);
2100
+ const candidateDomain2 = safeDomain(candidateUri);
2101
+ if (inputDomain2 && candidateDomain2 && inputDomain2 === candidateDomain2) return 0.85;
2102
+ return 0;
2103
+ }
2104
+ if (!inputUri && !candidateUri) {
2105
+ return 0.65;
2106
+ }
2107
+ const inputDomain = safeDomain(inputUri ?? null);
2108
+ const candidateDomain = safeDomain(candidateUri ?? null);
2109
+ if (inputDomain && candidateDomain && inputDomain === candidateDomain) {
2110
+ return 0.75;
2111
+ }
2112
+ return 0.2;
2113
+ }
2114
+ function extractObservedAt(parts, fallback) {
2115
+ for (const part of parts) {
2116
+ if (!part) continue;
2117
+ const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
2118
+ if (!match) continue;
2119
+ const iso = match[2] ? `${match[1]}T${match[2]}Z` : `${match[1]}T00:00:00Z`;
2120
+ const parsed = new Date(iso);
2121
+ if (!Number.isNaN(parsed.getTime())) {
2122
+ return parsed;
2123
+ }
2124
+ }
2125
+ if (fallback) {
2126
+ const parsed = new Date(fallback);
2127
+ if (!Number.isNaN(parsed.getTime())) {
2128
+ return parsed;
2129
+ }
2130
+ }
2131
+ return null;
2132
+ }
2133
+ function timeProximity(input, memory, candidateUri) {
2134
+ if (input.type !== "event") {
2135
+ return 1;
2136
+ }
2137
+ const inputTime = extractObservedAt([input.uri, input.source, input.content], input.now ?? null);
2138
+ const existingTime = extractObservedAt([candidateUri, memory.source, memory.content], memory.created_at);
2139
+ if (!inputTime || !existingTime) {
2140
+ return 0.5;
2141
+ }
2142
+ const diffDays = Math.abs(inputTime.getTime() - existingTime.getTime()) / (1e3 * 60 * 60 * 24);
2143
+ return clamp013(1 - diffDays / 7);
2144
+ }
2145
+ function scoreCandidate(input, candidate, candidateUri) {
2146
+ const lexicalOverlap = overlapScore2(uniqueTokenSet2(input.content), uniqueTokenSet2(candidate.memory.content));
2147
+ const entityOverlap = Math.max(
2148
+ overlapScore2(extractEntities(input.content), extractEntities(candidate.memory.content)),
2149
+ lexicalOverlap * 0.75
2150
+ );
2151
+ const uriMatch = uriScopeMatch(input.uri, candidateUri);
2152
+ const temporal = timeProximity(input, candidate.memory, candidateUri);
2153
+ const semantic = clamp013(candidate.vector_score ?? lexicalOverlap);
2154
+ const dedupScore = clamp013(
2155
+ 0.5 * semantic + 0.2 * lexicalOverlap + 0.15 * uriMatch + 0.1 * entityOverlap + 0.05 * temporal
2156
+ );
2157
+ return {
2158
+ semantic_similarity: semantic,
2159
+ lexical_overlap: lexicalOverlap,
2160
+ uri_scope_match: uriMatch,
2161
+ entity_overlap: entityOverlap,
2162
+ time_proximity: temporal,
2163
+ dedup_score: dedupScore
2164
+ };
2165
+ }
2166
+ async function recallCandidates(db, input, agentId) {
2167
+ const provider = input.provider === void 0 ? getEmbeddingProviderFromEnv() : input.provider;
2168
+ const response = await recallMemories(db, input.content, {
2169
+ agent_id: agentId,
2170
+ limit: Math.max(6, input.candidateLimit ?? 8),
2171
+ lexicalLimit: Math.max(8, input.candidateLimit ?? 8),
2172
+ vectorLimit: Math.max(8, input.candidateLimit ?? 8),
2173
+ provider,
2174
+ recordAccess: false
2175
+ });
2176
+ return response.results.filter((row) => row.memory.type === input.type).map((row) => {
2177
+ const uri = getPrimaryUri(db, row.memory.id, agentId);
2178
+ return {
2179
+ result: row,
2180
+ uri,
2181
+ domain: safeDomain(uri),
2182
+ score: scoreCandidate(input, row, uri)
2183
+ };
2184
+ }).sort((left, right) => right.score.dedup_score - left.score.dedup_score);
2185
+ }
2186
+ function fourCriterionGate(input) {
2187
+ const content = input.content.trim();
2188
+ const failed = [];
2189
+ const priority = input.priority ?? (input.type === "identity" ? 0 : input.type === "emotion" ? 1 : input.type === "knowledge" ? 2 : 3);
2190
+ const minLength = priority <= 1 ? 4 : 8;
2191
+ const specificity = content.length >= minLength ? Math.min(1, content.length / 50) : 0;
2192
+ if (specificity === 0) failed.push(`specificity (too short: ${content.length} < ${minLength} chars)`);
2193
+ const tokens = tokenize(content);
2194
+ const novelty = tokens.length >= 1 ? Math.min(1, tokens.length / 5) : 0;
2195
+ if (novelty === 0) failed.push("novelty (no meaningful tokens after filtering)");
2196
+ const hasCJK = /[\u4e00-\u9fff]/.test(content);
2197
+ const hasCapitalized = /[A-Z][a-z]+/.test(content);
2198
+ const hasNumbers = /\d+/.test(content);
2199
+ const hasURI = /\w+:\/\//.test(content);
2200
+ const hasEntityMarkers = /[@#]/.test(content);
2201
+ const hasMeaningfulLength = content.length >= 15;
2202
+ const topicSignals = [hasCJK, hasCapitalized, hasNumbers, hasURI, hasEntityMarkers, hasMeaningfulLength].filter(Boolean).length;
2203
+ const relevance = topicSignals >= 1 ? Math.min(1, topicSignals / 3) : 0;
2204
+ if (relevance === 0) failed.push("relevance (no identifiable topics/entities)");
2205
+ const allCaps = content === content.toUpperCase() && content.length > 20 && /^[A-Z\s]+$/.test(content);
2206
+ const hasWhitespaceOrPunctuation = /[\s,。!?,.!?;;::]/.test(content) || content.length < 30;
2207
+ const excessiveRepetition = /(.)\1{9,}/.test(content);
2208
+ let coherence = 1;
2209
+ if (allCaps) coherence -= 0.5;
2210
+ if (!hasWhitespaceOrPunctuation) coherence -= 0.3;
2211
+ if (excessiveRepetition) coherence -= 0.5;
2212
+ coherence = Math.max(0, coherence);
2213
+ if (coherence < 0.3) failed.push("coherence (garbled or malformed content)");
2214
+ return {
2215
+ pass: failed.length === 0,
2216
+ scores: { specificity, novelty, relevance, coherence },
2217
+ failedCriteria: failed
2218
+ };
2219
+ }
2220
+ async function guard(db, input) {
2221
+ const hash = contentHash(input.content);
2222
+ const agentId = input.agent_id ?? "default";
2223
+ const exactMatch = db.prepare("SELECT id FROM memories WHERE hash = ? AND agent_id = ?").get(hash, agentId);
2224
+ if (exactMatch) {
2225
+ return { action: "skip", reason: "Exact duplicate (hash match)", existingId: exactMatch.id };
2226
+ }
2227
+ if (input.uri) {
2228
+ const existingPath = getPathByUri(db, input.uri, agentId);
2229
+ if (existingPath) {
2230
+ return {
2231
+ action: "update",
2232
+ reason: `URI ${input.uri} already exists, updating canonical content`,
2233
+ existingId: existingPath.memory_id,
2234
+ updatedContent: input.content
2235
+ };
2236
+ }
2237
+ }
2238
+ const gateResult = fourCriterionGate(input);
2239
+ if (!gateResult.pass) {
2240
+ return { action: "skip", reason: `Gate rejected: ${gateResult.failedCriteria.join(", ")}` };
2241
+ }
2242
+ if (input.conservative) {
2243
+ return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
2244
+ }
2245
+ const candidates = await recallCandidates(db, input, agentId);
2246
+ const best = candidates[0];
2247
+ if (!best) {
2248
+ return { action: "add", reason: "No relevant semantic candidates found" };
2249
+ }
2250
+ const score = best.score;
2251
+ if (score.dedup_score >= NEAR_EXACT_THRESHOLD) {
2252
+ const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
2253
+ return {
2254
+ action: shouldUpdateMetadata ? "update" : "skip",
2255
+ reason: shouldUpdateMetadata ? `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)}), updating metadata` : `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)})`,
2256
+ existingId: best.result.memory.id,
2257
+ score
2258
+ };
2259
+ }
2260
+ if (score.dedup_score >= MERGE_THRESHOLD) {
2261
+ const mergePlan = buildMergePlan({
2262
+ existing: best.result.memory,
2263
+ incoming: {
2264
+ content: input.content,
2265
+ type: input.type,
2266
+ source: input.source
2267
+ }
2268
+ });
2269
+ return {
2270
+ action: "merge",
2271
+ reason: `Semantic near-duplicate detected (score=${score.dedup_score.toFixed(3)}), applying ${mergePlan.strategy}`,
2272
+ existingId: best.result.memory.id,
2273
+ mergedContent: mergePlan.content,
2274
+ mergePlan,
2275
+ score
2276
+ };
2277
+ }
2278
+ return {
2279
+ action: "add",
2280
+ reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
2281
+ score
2282
+ };
2283
+ }
2284
+
2285
+ // src/sleep/sync.ts
2286
+ function ensureUriPath(db, memoryId, uri, agentId) {
2287
+ if (!uri) return;
2288
+ if (getPathByUri(db, uri, agentId ?? "default")) return;
2289
+ try {
2290
+ createPath(db, memoryId, uri, void 0, void 0, agentId);
2291
+ } catch {
2292
+ }
2293
+ }
2294
+ async function syncOne(db, input) {
2295
+ const memInput = {
2296
+ content: input.content,
2297
+ type: input.type ?? "event",
2298
+ priority: input.priority,
2299
+ emotion_val: input.emotion_val,
2300
+ source: input.source,
2301
+ agent_id: input.agent_id,
2302
+ uri: input.uri,
2303
+ provider: input.provider,
2304
+ conservative: input.conservative,
2305
+ emotion_tag: input.emotion_tag
2306
+ };
2307
+ const guardResult = await guard(db, memInput);
2308
+ switch (guardResult.action) {
2309
+ case "skip":
2310
+ return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
2311
+ case "add": {
2312
+ const mem = createMemory(db, memInput);
2313
+ if (!mem) return { action: "skipped", reason: "createMemory returned null" };
2314
+ ensureUriPath(db, mem.id, input.uri, input.agent_id);
2315
+ return { action: "added", memoryId: mem.id, reason: guardResult.reason };
2316
+ }
2317
+ case "update": {
2318
+ if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
2319
+ if (guardResult.updatedContent !== void 0) {
2320
+ updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
2321
+ }
2322
+ ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
2323
+ return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
2324
+ }
2325
+ case "merge": {
2326
+ if (!guardResult.existingId || !guardResult.mergedContent) {
2327
+ return { action: "skipped", reason: "Missing merge data" };
2328
+ }
2329
+ updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
2330
+ ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
2331
+ return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
2332
+ }
2333
+ }
2334
+ }
2335
+
2336
+ // src/app/remember.ts
2337
+ async function rememberMemory(db, input) {
2338
+ return syncOne(db, {
2339
+ content: input.content,
2340
+ type: input.type,
2341
+ priority: input.priority,
2342
+ emotion_val: input.emotion_val,
2343
+ uri: input.uri,
2344
+ source: input.source,
2345
+ agent_id: input.agent_id,
2346
+ provider: input.provider,
2347
+ conservative: input.conservative,
2348
+ emotion_tag: input.emotion_tag
2349
+ });
2350
+ }
2351
+
2352
+ // src/app/recall.ts
2353
+ async function recallMemory(db, input) {
2354
+ const result = await recallMemories(db, input.query, {
2355
+ agent_id: input.agent_id,
2356
+ limit: input.emotion_tag ? (input.limit ?? 10) * 3 : input.limit,
2357
+ min_vitality: input.min_vitality,
2358
+ lexicalLimit: input.lexicalLimit,
2359
+ vectorLimit: input.vectorLimit,
2360
+ provider: input.provider,
2361
+ recordAccess: input.recordAccess
2362
+ });
2363
+ if (input.emotion_tag) {
2364
+ result.results = result.results.filter((r) => r.memory.emotion_tag === input.emotion_tag).slice(0, input.limit ?? 10);
2365
+ }
2366
+ return result;
2367
+ }
2368
+
719
2369
  // src/sleep/decay.ts
2370
+ init_db();
720
2371
  var MIN_VITALITY = {
721
2372
  0: 1,
722
2373
  // P0: identity — never decays
@@ -777,35 +2428,102 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
777
2428
  }
778
2429
 
779
2430
  // src/sleep/tidy.ts
2431
+ init_memory();
780
2432
  function runTidy(db, opts) {
781
2433
  const threshold = opts?.vitalityThreshold ?? 0.05;
782
2434
  const agentId = opts?.agent_id;
783
2435
  let archived = 0;
784
- let orphansCleaned = 0;
785
2436
  const transaction = db.transaction(() => {
786
2437
  const decayed = getDecayedMemories(db, threshold, agentId ? { agent_id: agentId } : void 0);
787
2438
  for (const mem of decayed) {
788
2439
  deleteMemory(db, mem.id);
789
- archived++;
2440
+ archived += 1;
790
2441
  }
791
- const orphans = agentId ? db.prepare(
792
- `DELETE FROM paths
793
- WHERE agent_id = ?
794
- AND memory_id NOT IN (SELECT id FROM memories WHERE agent_id = ?)`
795
- ).run(agentId, agentId) : db.prepare(
796
- "DELETE FROM paths WHERE memory_id NOT IN (SELECT id FROM memories)"
797
- ).run();
798
- orphansCleaned = orphans.changes;
799
2442
  });
800
2443
  transaction();
801
- return { archived, orphansCleaned };
2444
+ return { archived, orphansCleaned: 0 };
802
2445
  }
803
2446
 
804
2447
  // src/sleep/govern.ts
2448
+ init_memory();
2449
+ init_tokenizer();
2450
+ function clamp014(value) {
2451
+ if (!Number.isFinite(value)) return 0;
2452
+ return Math.max(0, Math.min(1, value));
2453
+ }
2454
+ function overlapScore3(left, right) {
2455
+ if (left.size === 0 || right.size === 0) return 0;
2456
+ let shared = 0;
2457
+ for (const token of left) {
2458
+ if (right.has(token)) shared += 1;
2459
+ }
2460
+ return shared / Math.max(left.size, right.size);
2461
+ }
2462
+ function feedbackPenalty(db, memoryId) {
2463
+ try {
2464
+ const row = db.prepare(
2465
+ `SELECT COUNT(*) as count, COALESCE(AVG(value), 0) as avgValue
2466
+ FROM feedback_events
2467
+ WHERE memory_id = ?`
2468
+ ).get(memoryId);
2469
+ if (!row || row.count === 0) return 1;
2470
+ return clamp014(1 - row.avgValue);
2471
+ } catch {
2472
+ return 1;
2473
+ }
2474
+ }
2475
+ function ageScore(memory, referenceMs = Date.now()) {
2476
+ const createdAt = new Date(memory.created_at).getTime();
2477
+ if (Number.isNaN(createdAt)) return 0;
2478
+ const ageDays = Math.max(0, (referenceMs - createdAt) / (1e3 * 60 * 60 * 24));
2479
+ return clamp014(ageDays / 180);
2480
+ }
2481
+ function computeEvictionScore(input) {
2482
+ return clamp014(
2483
+ 0.4 * (1 - clamp014(input.vitality)) + 0.2 * clamp014(input.redundancy_score) + 0.2 * clamp014(input.age_score) + 0.1 * clamp014(input.low_feedback_penalty) + 0.1 * clamp014(input.low_priority_penalty)
2484
+ );
2485
+ }
2486
+ function rankEvictionCandidates(db, opts) {
2487
+ const agentId = opts?.agent_id;
2488
+ const rows = db.prepare(
2489
+ agentId ? `SELECT * FROM memories WHERE agent_id = ? AND priority > 0 AND TRIM(content) != ''` : `SELECT * FROM memories WHERE priority > 0 AND TRIM(content) != ''`
2490
+ ).all(...agentId ? [agentId] : []);
2491
+ const tokenSets = new Map(rows.map((memory) => [memory.id, new Set(tokenize(memory.content))]));
2492
+ return rows.map((memory) => {
2493
+ const ownTokens = tokenSets.get(memory.id) ?? /* @__PURE__ */ new Set();
2494
+ const redundancy = rows.filter((candidate2) => candidate2.id !== memory.id && candidate2.type === memory.type).reduce((maxOverlap, candidate2) => {
2495
+ const candidateTokens = tokenSets.get(candidate2.id) ?? /* @__PURE__ */ new Set();
2496
+ return Math.max(maxOverlap, overlapScore3(ownTokens, candidateTokens));
2497
+ }, 0);
2498
+ const candidate = {
2499
+ memory,
2500
+ redundancy_score: redundancy,
2501
+ age_score: ageScore(memory),
2502
+ low_feedback_penalty: feedbackPenalty(db, memory.id),
2503
+ low_priority_penalty: clamp014(memory.priority / 3),
2504
+ eviction_score: 0
2505
+ };
2506
+ candidate.eviction_score = computeEvictionScore({
2507
+ vitality: memory.vitality,
2508
+ redundancy_score: candidate.redundancy_score,
2509
+ age_score: candidate.age_score,
2510
+ low_feedback_penalty: candidate.low_feedback_penalty,
2511
+ low_priority_penalty: candidate.low_priority_penalty
2512
+ });
2513
+ return candidate;
2514
+ }).sort((left, right) => {
2515
+ if (right.eviction_score !== left.eviction_score) {
2516
+ return right.eviction_score - left.eviction_score;
2517
+ }
2518
+ return left.memory.priority - right.memory.priority;
2519
+ });
2520
+ }
805
2521
  function runGovern(db, opts) {
806
2522
  const agentId = opts?.agent_id;
2523
+ const maxMemories = opts?.maxMemories ?? 200;
807
2524
  let orphanPaths = 0;
808
2525
  let emptyMemories = 0;
2526
+ let evicted = 0;
809
2527
  const transaction = db.transaction(() => {
810
2528
  const pathResult = agentId ? db.prepare(
811
2529
  `DELETE FROM paths
@@ -815,154 +2533,750 @@ function runGovern(db, opts) {
815
2533
  orphanPaths = pathResult.changes;
816
2534
  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();
817
2535
  emptyMemories = emptyResult.changes;
2536
+ const total = db.prepare(agentId ? "SELECT COUNT(*) as c FROM memories WHERE agent_id = ?" : "SELECT COUNT(*) as c FROM memories").get(...agentId ? [agentId] : []).c;
2537
+ const excess = Math.max(0, total - maxMemories);
2538
+ if (excess <= 0) return;
2539
+ const candidates = rankEvictionCandidates(db, { agent_id: agentId }).slice(0, excess);
2540
+ for (const candidate of candidates) {
2541
+ deleteMemory(db, candidate.memory.id);
2542
+ evicted += 1;
2543
+ }
818
2544
  });
819
2545
  transaction();
820
- return { orphanPaths, emptyMemories };
2546
+ return { orphanPaths, emptyMemories, evicted };
821
2547
  }
822
2548
 
823
- // src/core/guard.ts
824
- function guard(db, input) {
825
- const hash = contentHash(input.content);
826
- const agentId = input.agent_id ?? "default";
827
- const exactMatch = db.prepare("SELECT id FROM memories WHERE hash = ? AND agent_id = ?").get(hash, agentId);
828
- if (exactMatch) {
829
- return { action: "skip", reason: "Exact duplicate (hash match)", existingId: exactMatch.id };
2549
+ // src/sleep/jobs.ts
2550
+ init_db();
2551
+ function parseCheckpoint(raw) {
2552
+ if (!raw) return null;
2553
+ try {
2554
+ return JSON.parse(raw);
2555
+ } catch {
2556
+ return null;
830
2557
  }
831
- if (input.uri) {
832
- const existingPath = getPathByUri(db, input.uri, agentId);
833
- if (existingPath) {
834
- return {
835
- action: "update",
836
- reason: `URI ${input.uri} already exists, updating`,
837
- existingId: existingPath.memory_id
838
- };
2558
+ }
2559
+ function serializeCheckpoint(checkpoint) {
2560
+ if (!checkpoint) return null;
2561
+ return JSON.stringify(checkpoint);
2562
+ }
2563
+ function toJob(row) {
2564
+ if (!row) return null;
2565
+ return {
2566
+ ...row,
2567
+ checkpoint: parseCheckpoint(row.checkpoint)
2568
+ };
2569
+ }
2570
+ function createInitialCheckpoint(phase) {
2571
+ return {
2572
+ requestedPhase: phase,
2573
+ nextPhase: phase === "all" ? "decay" : phase,
2574
+ completedPhases: [],
2575
+ phaseResults: {}
2576
+ };
2577
+ }
2578
+ function createMaintenanceJob(db, phase, checkpoint = createInitialCheckpoint(phase)) {
2579
+ const jobId = newId();
2580
+ const startedAt = now();
2581
+ db.prepare(
2582
+ `INSERT INTO maintenance_jobs (job_id, phase, status, checkpoint, error, started_at, finished_at)
2583
+ VALUES (?, ?, 'running', ?, NULL, ?, NULL)`
2584
+ ).run(jobId, phase, serializeCheckpoint(checkpoint), startedAt);
2585
+ return getMaintenanceJob(db, jobId);
2586
+ }
2587
+ function getMaintenanceJob(db, jobId) {
2588
+ const row = db.prepare("SELECT * FROM maintenance_jobs WHERE job_id = ?").get(jobId);
2589
+ return toJob(row);
2590
+ }
2591
+ function findResumableMaintenanceJob(db, phase) {
2592
+ const row = db.prepare(
2593
+ `SELECT *
2594
+ FROM maintenance_jobs
2595
+ WHERE phase = ?
2596
+ AND status IN ('running', 'failed')
2597
+ ORDER BY started_at DESC
2598
+ LIMIT 1`
2599
+ ).get(phase);
2600
+ return toJob(row);
2601
+ }
2602
+ function updateMaintenanceCheckpoint(db, jobId, checkpoint) {
2603
+ db.prepare(
2604
+ `UPDATE maintenance_jobs
2605
+ SET checkpoint = ?,
2606
+ error = NULL,
2607
+ finished_at = NULL,
2608
+ status = 'running'
2609
+ WHERE job_id = ?`
2610
+ ).run(serializeCheckpoint(checkpoint), jobId);
2611
+ return getMaintenanceJob(db, jobId);
2612
+ }
2613
+ function failMaintenanceJob(db, jobId, error, checkpoint) {
2614
+ db.prepare(
2615
+ `UPDATE maintenance_jobs
2616
+ SET status = 'failed',
2617
+ checkpoint = COALESCE(?, checkpoint),
2618
+ error = ?,
2619
+ finished_at = ?
2620
+ WHERE job_id = ?`
2621
+ ).run(serializeCheckpoint(checkpoint), error, now(), jobId);
2622
+ return getMaintenanceJob(db, jobId);
2623
+ }
2624
+ function completeMaintenanceJob(db, jobId, checkpoint) {
2625
+ db.prepare(
2626
+ `UPDATE maintenance_jobs
2627
+ SET status = 'completed',
2628
+ checkpoint = COALESCE(?, checkpoint),
2629
+ error = NULL,
2630
+ finished_at = ?
2631
+ WHERE job_id = ?`
2632
+ ).run(serializeCheckpoint(checkpoint), now(), jobId);
2633
+ return getMaintenanceJob(db, jobId);
2634
+ }
2635
+
2636
+ // src/sleep/orchestrator.ts
2637
+ var DEFAULT_RUNNERS = {
2638
+ decay: (db, opts) => runDecay(db, opts),
2639
+ tidy: (db, opts) => runTidy(db, opts),
2640
+ govern: (db, opts) => runGovern(db, opts)
2641
+ };
2642
+ var PHASE_SEQUENCE = ["decay", "tidy", "govern"];
2643
+ function getSummaryStats(db, agentId) {
2644
+ const row = agentId ? db.prepare("SELECT COUNT(*) as total, COALESCE(AVG(vitality), 0) as avg FROM memories WHERE agent_id = ?").get(agentId) : db.prepare("SELECT COUNT(*) as total, COALESCE(AVG(vitality), 0) as avg FROM memories").get();
2645
+ return {
2646
+ total: row.total,
2647
+ avgVitality: row.avg
2648
+ };
2649
+ }
2650
+ function getPhaseSequence(phase) {
2651
+ return phase === "all" ? [...PHASE_SEQUENCE] : [phase];
2652
+ }
2653
+ function resolveJob(db, opts) {
2654
+ if (opts.jobId) {
2655
+ const job = getMaintenanceJob(db, opts.jobId);
2656
+ if (!job) {
2657
+ throw new Error(`Maintenance job not found: ${opts.jobId}`);
2658
+ }
2659
+ if (job.phase !== opts.phase) {
2660
+ throw new Error(`Maintenance job ${opts.jobId} phase mismatch: expected ${opts.phase}, got ${job.phase}`);
839
2661
  }
2662
+ return { job, resumed: true };
840
2663
  }
841
- const ftsTokens = tokenize(input.content.slice(0, 200));
842
- const ftsQuery = ftsTokens.length > 0 ? ftsTokens.slice(0, 8).map((w) => `"${w}"`).join(" OR ") : null;
843
- if (ftsQuery) {
844
- try {
845
- const similar = db.prepare(
846
- `SELECT m.id, m.content, m.type, rank
847
- FROM memories_fts f
848
- JOIN memories m ON m.id = f.id
849
- WHERE memories_fts MATCH ? AND m.agent_id = ?
850
- ORDER BY rank
851
- LIMIT 3`
852
- ).all(ftsQuery, agentId);
853
- if (similar.length > 0) {
854
- const topRank = Math.abs(similar[0].rank);
855
- const tokenCount = ftsTokens.length;
856
- const dynamicThreshold = tokenCount * 1.5;
857
- if (topRank > dynamicThreshold) {
858
- const existing = similar[0];
859
- if (existing.type === input.type) {
860
- const merged = `${existing.content}
861
-
862
- [Updated] ${input.content}`;
863
- return {
864
- action: "merge",
865
- reason: `Similar content found (score=${topRank.toFixed(1)}, threshold=${dynamicThreshold.toFixed(1)}), merging`,
866
- existingId: existing.id,
867
- mergedContent: merged
868
- };
869
- }
870
- }
871
- }
872
- } catch {
2664
+ if (opts.resume !== false) {
2665
+ const resumable = findResumableMaintenanceJob(db, opts.phase);
2666
+ if (resumable) {
2667
+ return { job: resumable, resumed: true };
873
2668
  }
874
2669
  }
875
- const gateResult = fourCriterionGate(input);
876
- if (!gateResult.pass) {
877
- return { action: "skip", reason: `Gate rejected: ${gateResult.failedCriteria.join(", ")}` };
2670
+ return {
2671
+ job: createMaintenanceJob(db, opts.phase),
2672
+ resumed: false
2673
+ };
2674
+ }
2675
+ function nextPhase(current, requested) {
2676
+ if (requested !== "all") return null;
2677
+ const index = PHASE_SEQUENCE.indexOf(current);
2678
+ return PHASE_SEQUENCE[index + 1] ?? null;
2679
+ }
2680
+ async function runReflectOrchestrator(db, opts) {
2681
+ const runners = {
2682
+ ...DEFAULT_RUNNERS,
2683
+ ...opts.runners
2684
+ };
2685
+ const before = getSummaryStats(db, opts.agent_id);
2686
+ const { job: baseJob, resumed } = resolveJob(db, opts);
2687
+ let checkpoint = baseJob.checkpoint ?? createInitialCheckpoint(opts.phase);
2688
+ const jobId = baseJob.job_id;
2689
+ const orderedPhases = getPhaseSequence(opts.phase);
2690
+ const startPhase = checkpoint.nextPhase ?? orderedPhases[orderedPhases.length - 1] ?? "decay";
2691
+ const startIndex = Math.max(0, orderedPhases.indexOf(startPhase));
2692
+ const phasesToRun = checkpoint.nextPhase === null ? [] : orderedPhases.slice(startIndex);
2693
+ const totalPhases = Math.max(orderedPhases.length, 1);
2694
+ opts.onProgress?.({
2695
+ status: "started",
2696
+ phase: opts.phase,
2697
+ progress: checkpoint.completedPhases.length / totalPhases,
2698
+ jobId,
2699
+ detail: {
2700
+ resumed,
2701
+ nextPhase: checkpoint.nextPhase
2702
+ }
2703
+ });
2704
+ try {
2705
+ for (const phase of phasesToRun) {
2706
+ const result = await Promise.resolve(runners[phase](db, { agent_id: opts.agent_id }));
2707
+ checkpoint = {
2708
+ ...checkpoint,
2709
+ completedPhases: [.../* @__PURE__ */ new Set([...checkpoint.completedPhases, phase])],
2710
+ phaseResults: {
2711
+ ...checkpoint.phaseResults,
2712
+ [phase]: result
2713
+ },
2714
+ nextPhase: nextPhase(phase, opts.phase)
2715
+ };
2716
+ updateMaintenanceCheckpoint(db, jobId, checkpoint);
2717
+ opts.onProgress?.({
2718
+ status: "phase-completed",
2719
+ phase,
2720
+ progress: checkpoint.completedPhases.length / totalPhases,
2721
+ jobId,
2722
+ detail: result
2723
+ });
2724
+ }
2725
+ } catch (error) {
2726
+ const message = error instanceof Error ? error.message : String(error);
2727
+ const failed = failMaintenanceJob(db, jobId, message, checkpoint) ?? baseJob;
2728
+ opts.onProgress?.({
2729
+ status: "failed",
2730
+ phase: checkpoint.nextPhase ?? opts.phase,
2731
+ progress: checkpoint.completedPhases.length / totalPhases,
2732
+ jobId,
2733
+ detail: { error: message }
2734
+ });
2735
+ throw Object.assign(new Error(message), { job: failed, checkpoint });
878
2736
  }
879
- return { action: "add", reason: "Passed all guard checks" };
2737
+ const completedCheckpoint = {
2738
+ ...checkpoint,
2739
+ nextPhase: null
2740
+ };
2741
+ const job = completeMaintenanceJob(db, jobId, completedCheckpoint) ?? baseJob;
2742
+ const after = getSummaryStats(db, opts.agent_id);
2743
+ opts.onProgress?.({
2744
+ status: "completed",
2745
+ phase: opts.phase,
2746
+ progress: 1,
2747
+ jobId,
2748
+ detail: completedCheckpoint.phaseResults
2749
+ });
2750
+ return {
2751
+ job,
2752
+ jobId,
2753
+ phase: opts.phase,
2754
+ resumed,
2755
+ checkpoint: completedCheckpoint,
2756
+ results: completedCheckpoint.phaseResults,
2757
+ before,
2758
+ after
2759
+ };
880
2760
  }
881
- function fourCriterionGate(input) {
882
- const content = input.content.trim();
883
- const failed = [];
884
- const priority = input.priority ?? (input.type === "identity" ? 0 : input.type === "emotion" ? 1 : input.type === "knowledge" ? 2 : 3);
885
- const minLength = priority <= 1 ? 4 : 8;
886
- const specificity = content.length >= minLength ? Math.min(1, content.length / 50) : 0;
887
- if (specificity === 0) failed.push(`specificity (too short: ${content.length} < ${minLength} chars)`);
888
- const tokens = tokenize(content);
889
- const novelty = tokens.length >= 1 ? Math.min(1, tokens.length / 5) : 0;
890
- if (novelty === 0) failed.push("novelty (no meaningful tokens after filtering)");
891
- const hasCJK = /[\u4e00-\u9fff]/.test(content);
892
- const hasCapitalized = /[A-Z][a-z]+/.test(content);
893
- const hasNumbers = /\d+/.test(content);
894
- const hasURI = /\w+:\/\//.test(content);
895
- const hasEntityMarkers = /[@#]/.test(content);
896
- const hasMeaningfulLength = content.length >= 15;
897
- const topicSignals = [hasCJK, hasCapitalized, hasNumbers, hasURI, hasEntityMarkers, hasMeaningfulLength].filter(Boolean).length;
898
- const relevance = topicSignals >= 1 ? Math.min(1, topicSignals / 3) : 0;
899
- if (relevance === 0) failed.push("relevance (no identifiable topics/entities)");
900
- const allCaps = content === content.toUpperCase() && content.length > 20 && /^[A-Z\s]+$/.test(content);
901
- const hasWhitespaceOrPunctuation = /[\s,。!?,.!?;;::]/.test(content) || content.length < 30;
902
- const excessiveRepetition = /(.)\1{9,}/.test(content);
903
- let coherence = 1;
904
- if (allCaps) {
905
- coherence -= 0.5;
2761
+
2762
+ // src/app/reflect.ts
2763
+ async function reflectMemories(db, input) {
2764
+ const options = {
2765
+ phase: input.phase,
2766
+ agent_id: input.agent_id,
2767
+ jobId: input.jobId,
2768
+ resume: input.resume,
2769
+ runners: input.runners,
2770
+ onProgress: input.onProgress
2771
+ };
2772
+ return runReflectOrchestrator(db, options);
2773
+ }
2774
+
2775
+ // src/app/status.ts
2776
+ init_memory();
2777
+ function getMemoryStatus(db, input) {
2778
+ const agentId = input?.agent_id ?? "default";
2779
+ const stats = countMemories(db, agentId);
2780
+ const lowVitality = db.prepare(
2781
+ "SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?"
2782
+ ).get(agentId);
2783
+ const totalPaths = db.prepare(
2784
+ "SELECT COUNT(*) as c FROM paths WHERE agent_id = ?"
2785
+ ).get(agentId);
2786
+ const feedbackEvents = db.prepare(
2787
+ "SELECT COUNT(*) as c FROM feedback_events WHERE agent_id = ?"
2788
+ ).get(agentId);
2789
+ return {
2790
+ ...stats,
2791
+ paths: totalPaths.c,
2792
+ low_vitality: lowVitality.c,
2793
+ feedback_events: feedbackEvents.c,
2794
+ agent_id: agentId
2795
+ };
2796
+ }
2797
+
2798
+ // src/app/reindex.ts
2799
+ async function reindexMemories(db, input) {
2800
+ input?.onProgress?.({ status: "started", stage: "fts", progress: 0 });
2801
+ try {
2802
+ const fts = rebuildBm25Index(db, { agent_id: input?.agent_id });
2803
+ input?.onProgress?.({
2804
+ status: "stage-completed",
2805
+ stage: "fts",
2806
+ progress: 0.5,
2807
+ detail: fts
2808
+ });
2809
+ const embeddings = await reindexEmbeddings(db, {
2810
+ agent_id: input?.agent_id,
2811
+ provider: input?.provider,
2812
+ force: input?.force,
2813
+ batchSize: input?.batchSize
2814
+ });
2815
+ input?.onProgress?.({
2816
+ status: "stage-completed",
2817
+ stage: "embeddings",
2818
+ progress: 0.9,
2819
+ detail: embeddings
2820
+ });
2821
+ const result = { fts, embeddings };
2822
+ input?.onProgress?.({
2823
+ status: "completed",
2824
+ stage: "done",
2825
+ progress: 1,
2826
+ detail: result
2827
+ });
2828
+ return result;
2829
+ } catch (error) {
2830
+ input?.onProgress?.({
2831
+ status: "failed",
2832
+ stage: "done",
2833
+ progress: 1,
2834
+ detail: error instanceof Error ? error.message : String(error)
2835
+ });
2836
+ throw error;
906
2837
  }
907
- if (!hasWhitespaceOrPunctuation) {
908
- coherence -= 0.3;
2838
+ }
2839
+
2840
+ // src/transports/http.ts
2841
+ var VALID_MEMORY_TYPES = /* @__PURE__ */ new Set(["identity", "emotion", "knowledge", "event"]);
2842
+ var VALID_PHASES = /* @__PURE__ */ new Set(["decay", "tidy", "govern", "all"]);
2843
+ var VALID_INTENTS = /* @__PURE__ */ new Set(["factual", "preference", "temporal", "planning", "design"]);
2844
+ function json(value) {
2845
+ return JSON.stringify(value);
2846
+ }
2847
+ function now2() {
2848
+ return (/* @__PURE__ */ new Date()).toISOString();
2849
+ }
2850
+ function sendJson(res, statusCode, payload) {
2851
+ res.writeHead(statusCode, {
2852
+ "content-type": "application/json; charset=utf-8",
2853
+ "cache-control": "no-store"
2854
+ });
2855
+ res.end(json(payload));
2856
+ }
2857
+ function sendError(res, statusCode, error, details) {
2858
+ sendJson(res, statusCode, { error, details });
2859
+ }
2860
+ function openSse(res) {
2861
+ res.writeHead(200, {
2862
+ "content-type": "text/event-stream; charset=utf-8",
2863
+ "cache-control": "no-cache, no-transform",
2864
+ connection: "keep-alive"
2865
+ });
2866
+ res.write(": connected\n\n");
2867
+ }
2868
+ function sendSse(res, event, payload) {
2869
+ if (res.writableEnded) return;
2870
+ res.write(`event: ${event}
2871
+ `);
2872
+ res.write(`data: ${json(payload)}
2873
+
2874
+ `);
2875
+ }
2876
+ async function readJsonBody(req) {
2877
+ const chunks = [];
2878
+ for await (const chunk of req) {
2879
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
909
2880
  }
910
- if (excessiveRepetition) {
911
- coherence -= 0.5;
2881
+ const raw = Buffer.concat(chunks).toString("utf8").trim();
2882
+ if (!raw) return {};
2883
+ try {
2884
+ const parsed = JSON.parse(raw);
2885
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
2886
+ throw new Error("Request body must be a JSON object");
2887
+ }
2888
+ return parsed;
2889
+ } catch (error) {
2890
+ throw new Error(error instanceof Error ? error.message : "Invalid JSON body");
912
2891
  }
913
- coherence = Math.max(0, coherence);
914
- if (coherence < 0.3) failed.push("coherence (garbled or malformed content)");
2892
+ }
2893
+ function asString(value) {
2894
+ return typeof value === "string" ? value : void 0;
2895
+ }
2896
+ function asBoolean(value) {
2897
+ return typeof value === "boolean" ? value : void 0;
2898
+ }
2899
+ function asNumber(value) {
2900
+ return typeof value === "number" && Number.isFinite(value) ? value : void 0;
2901
+ }
2902
+ function asStringArray(value) {
2903
+ if (!Array.isArray(value)) return void 0;
2904
+ const result = value.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean);
2905
+ return result.length > 0 ? result : [];
2906
+ }
2907
+ function wantsSse(req, body) {
2908
+ const accept = req.headers.accept ?? "";
2909
+ return accept.includes("text/event-stream") || body.stream === true;
2910
+ }
2911
+ function formatRecallResponse(result) {
915
2912
  return {
916
- pass: failed.length === 0,
917
- scores: { specificity, novelty, relevance, coherence },
918
- failedCriteria: failed
2913
+ mode: result.mode,
2914
+ provider_id: result.providerId,
2915
+ used_vector_search: result.usedVectorSearch,
2916
+ count: result.results.length,
2917
+ memories: result.results.map((row) => ({
2918
+ id: row.memory.id,
2919
+ content: row.memory.content,
2920
+ type: row.memory.type,
2921
+ priority: row.memory.priority,
2922
+ vitality: row.memory.vitality,
2923
+ score: row.score,
2924
+ bm25_rank: row.bm25_rank,
2925
+ vector_rank: row.vector_rank,
2926
+ bm25_score: row.bm25_score,
2927
+ vector_score: row.vector_score,
2928
+ updated_at: row.memory.updated_at
2929
+ }))
919
2930
  };
920
2931
  }
921
-
922
- // src/sleep/sync.ts
923
- function syncOne(db, input) {
924
- const memInput = {
925
- content: input.content,
926
- type: input.type ?? "event",
927
- priority: input.priority,
928
- emotion_val: input.emotion_val,
929
- source: input.source,
930
- agent_id: input.agent_id,
931
- uri: input.uri
2932
+ function formatSurfaceResponse(result) {
2933
+ return {
2934
+ count: result.count,
2935
+ query: result.query,
2936
+ task: result.task,
2937
+ intent: result.intent,
2938
+ results: result.results.map((row) => ({
2939
+ id: row.memory.id,
2940
+ content: row.memory.content,
2941
+ type: row.memory.type,
2942
+ priority: row.memory.priority,
2943
+ vitality: row.memory.vitality,
2944
+ score: row.score,
2945
+ semantic_score: row.semantic_score,
2946
+ lexical_score: row.lexical_score,
2947
+ task_match: row.task_match,
2948
+ priority_prior: row.priority_prior,
2949
+ feedback_score: row.feedback_score,
2950
+ feedback_summary: row.feedback_summary,
2951
+ reason_codes: row.reason_codes,
2952
+ updated_at: row.memory.updated_at
2953
+ }))
932
2954
  };
933
- const guardResult = guard(db, memInput);
934
- switch (guardResult.action) {
935
- case "skip":
936
- return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
937
- case "add": {
938
- const mem = createMemory(db, memInput);
939
- if (!mem) return { action: "skipped", reason: "createMemory returned null" };
940
- if (input.uri) {
941
- try {
942
- createPath(db, mem.id, input.uri);
943
- } catch {
2955
+ }
2956
+ function createJob(jobs, kind, agentId) {
2957
+ const job = {
2958
+ id: randomUUID2(),
2959
+ kind,
2960
+ status: "running",
2961
+ stage: "queued",
2962
+ progress: 0,
2963
+ agent_id: agentId,
2964
+ started_at: now2(),
2965
+ finished_at: null
2966
+ };
2967
+ jobs.set(job.id, job);
2968
+ return job;
2969
+ }
2970
+ function updateJob(job, patch) {
2971
+ Object.assign(job, patch);
2972
+ return job;
2973
+ }
2974
+ function createHttpServer(options) {
2975
+ const ownsDb = !options?.db;
2976
+ const db = options?.db ?? openDatabase({ path: options?.dbPath ?? process.env.AGENT_MEMORY_DB ?? "./agent-memory.db" });
2977
+ const defaultAgentId = options?.agentId ?? process.env.AGENT_MEMORY_AGENT_ID ?? "default";
2978
+ const jobs = /* @__PURE__ */ new Map();
2979
+ const executeReflectJob = async (job, body, stream) => {
2980
+ const phase = asString(body.phase) ?? "all";
2981
+ if (!VALID_PHASES.has(phase)) {
2982
+ throw new Error(`Invalid phase: ${String(body.phase)}`);
2983
+ }
2984
+ updateJob(job, { stage: phase, progress: 0.01 });
2985
+ try {
2986
+ const result = await reflectMemories(db, {
2987
+ phase,
2988
+ agent_id: asString(body.agent_id) ?? defaultAgentId,
2989
+ runners: options?.reflectRunners,
2990
+ onProgress: (event) => {
2991
+ updateJob(job, {
2992
+ stage: String(event.phase),
2993
+ progress: event.progress,
2994
+ backend_job_id: event.jobId ?? job.backend_job_id
2995
+ });
2996
+ if (stream) {
2997
+ sendSse(stream, "progress", {
2998
+ job,
2999
+ event
3000
+ });
3001
+ }
944
3002
  }
945
- }
946
- return { action: "added", memoryId: mem.id, reason: guardResult.reason };
3003
+ });
3004
+ updateJob(job, {
3005
+ status: "completed",
3006
+ stage: "done",
3007
+ progress: 1,
3008
+ backend_job_id: result.jobId,
3009
+ finished_at: now2(),
3010
+ result
3011
+ });
3012
+ return { job, result };
3013
+ } catch (error) {
3014
+ updateJob(job, {
3015
+ status: "failed",
3016
+ stage: "failed",
3017
+ progress: 1,
3018
+ finished_at: now2(),
3019
+ error: error instanceof Error ? error.message : String(error)
3020
+ });
3021
+ throw error;
947
3022
  }
948
- case "update": {
949
- if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
950
- updateMemory(db, guardResult.existingId, { content: input.content });
951
- return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
3023
+ };
3024
+ const executeReindexJob = async (job, body, stream) => {
3025
+ updateJob(job, { stage: "fts", progress: 0.01 });
3026
+ try {
3027
+ const result = await reindexMemories(db, {
3028
+ agent_id: asString(body.agent_id) ?? defaultAgentId,
3029
+ provider: options?.provider,
3030
+ force: asBoolean(body.full) ?? false,
3031
+ batchSize: asNumber(body.batch_size) ?? 16,
3032
+ onProgress: (event) => {
3033
+ updateJob(job, {
3034
+ stage: event.stage,
3035
+ progress: event.progress
3036
+ });
3037
+ if (stream) {
3038
+ sendSse(stream, "progress", {
3039
+ job,
3040
+ event
3041
+ });
3042
+ }
3043
+ }
3044
+ });
3045
+ updateJob(job, {
3046
+ status: "completed",
3047
+ stage: "done",
3048
+ progress: 1,
3049
+ finished_at: now2(),
3050
+ result
3051
+ });
3052
+ return { job, result };
3053
+ } catch (error) {
3054
+ updateJob(job, {
3055
+ status: "failed",
3056
+ stage: "failed",
3057
+ progress: 1,
3058
+ finished_at: now2(),
3059
+ error: error instanceof Error ? error.message : String(error)
3060
+ });
3061
+ throw error;
952
3062
  }
953
- case "merge": {
954
- if (!guardResult.existingId || !guardResult.mergedContent) {
955
- return { action: "skipped", reason: "Missing merge data" };
3063
+ };
3064
+ const server = http.createServer(async (req, res) => {
3065
+ try {
3066
+ const method = req.method ?? "GET";
3067
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
3068
+ const pathname = url.pathname;
3069
+ if (method === "GET" && pathname === "/health") {
3070
+ sendJson(res, 200, {
3071
+ ok: true,
3072
+ service: "agent-memory",
3073
+ time: now2()
3074
+ });
3075
+ return;
956
3076
  }
957
- updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
958
- return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
3077
+ if (method === "GET" && pathname === "/v1/status") {
3078
+ const agentId = url.searchParams.get("agent_id") ?? defaultAgentId;
3079
+ sendJson(res, 200, getMemoryStatus(db, { agent_id: agentId }));
3080
+ return;
3081
+ }
3082
+ if (method === "GET" && pathname.startsWith("/v1/jobs/")) {
3083
+ const id = decodeURIComponent(pathname.slice("/v1/jobs/".length));
3084
+ const job = jobs.get(id);
3085
+ if (job) {
3086
+ sendJson(res, 200, job);
3087
+ return;
3088
+ }
3089
+ const maintenanceJob = getMaintenanceJob(db, id);
3090
+ if (maintenanceJob) {
3091
+ sendJson(res, 200, maintenanceJob);
3092
+ return;
3093
+ }
3094
+ sendError(res, 404, `Job not found: ${id}`);
3095
+ return;
3096
+ }
3097
+ if (method !== "POST") {
3098
+ sendError(res, 404, `Route not found: ${method} ${pathname}`);
3099
+ return;
3100
+ }
3101
+ const body = await readJsonBody(req);
3102
+ if (pathname === "/v1/memories") {
3103
+ const content = asString(body.content)?.trim();
3104
+ if (!content) {
3105
+ sendError(res, 400, "content is required");
3106
+ return;
3107
+ }
3108
+ const type = asString(body.type) ?? "knowledge";
3109
+ if (!VALID_MEMORY_TYPES.has(type)) {
3110
+ sendError(res, 400, `Invalid memory type: ${String(body.type)}`);
3111
+ return;
3112
+ }
3113
+ const result = await rememberMemory(db, {
3114
+ content,
3115
+ type,
3116
+ uri: asString(body.uri),
3117
+ source: asString(body.source),
3118
+ emotion_val: asNumber(body.emotion_val),
3119
+ agent_id: asString(body.agent_id) ?? defaultAgentId,
3120
+ conservative: asBoolean(body.conservative),
3121
+ provider: options?.provider
3122
+ });
3123
+ sendJson(res, 200, result);
3124
+ return;
3125
+ }
3126
+ if (pathname === "/v1/recall") {
3127
+ const query = asString(body.query)?.trim();
3128
+ if (!query) {
3129
+ sendError(res, 400, "query is required");
3130
+ return;
3131
+ }
3132
+ const result = await recallMemory(db, {
3133
+ query,
3134
+ limit: asNumber(body.limit),
3135
+ agent_id: asString(body.agent_id) ?? defaultAgentId,
3136
+ provider: options?.provider
3137
+ });
3138
+ sendJson(res, 200, formatRecallResponse(result));
3139
+ return;
3140
+ }
3141
+ if (pathname === "/v1/surface") {
3142
+ const types = asStringArray(body.types)?.filter((type) => VALID_MEMORY_TYPES.has(type));
3143
+ const intent = asString(body.intent);
3144
+ if (intent !== void 0 && !VALID_INTENTS.has(intent)) {
3145
+ sendError(res, 400, `Invalid intent: ${intent}`);
3146
+ return;
3147
+ }
3148
+ const result = await surfaceMemories(db, {
3149
+ query: asString(body.query),
3150
+ task: asString(body.task),
3151
+ recent_turns: asStringArray(body.recent_turns),
3152
+ intent,
3153
+ types,
3154
+ limit: asNumber(body.limit),
3155
+ agent_id: asString(body.agent_id) ?? defaultAgentId,
3156
+ provider: options?.provider
3157
+ });
3158
+ sendJson(res, 200, formatSurfaceResponse(result));
3159
+ return;
3160
+ }
3161
+ if (pathname === "/v1/feedback") {
3162
+ const memoryId = asString(body.memory_id)?.trim();
3163
+ const source = asString(body.source);
3164
+ const useful = asBoolean(body.useful);
3165
+ if (!memoryId) {
3166
+ sendError(res, 400, "memory_id is required");
3167
+ return;
3168
+ }
3169
+ if (source !== "recall" && source !== "surface") {
3170
+ sendError(res, 400, "source must be 'recall' or 'surface'");
3171
+ return;
3172
+ }
3173
+ if (useful === void 0) {
3174
+ sendError(res, 400, "useful must be boolean");
3175
+ return;
3176
+ }
3177
+ const result = recordFeedbackEvent(db, {
3178
+ memory_id: memoryId,
3179
+ source,
3180
+ useful,
3181
+ agent_id: asString(body.agent_id) ?? defaultAgentId
3182
+ });
3183
+ sendJson(res, 200, result);
3184
+ return;
3185
+ }
3186
+ if (pathname === "/v1/reflect") {
3187
+ const agentId = asString(body.agent_id) ?? defaultAgentId;
3188
+ const job = createJob(jobs, "reflect", agentId);
3189
+ if (wantsSse(req, body)) {
3190
+ openSse(res);
3191
+ sendSse(res, "job", job);
3192
+ void executeReflectJob(job, body, res).then(({ job: currentJob, result: result2 }) => {
3193
+ sendSse(res, "done", { job: currentJob, result: result2 });
3194
+ }).catch((error) => {
3195
+ sendSse(res, "error", {
3196
+ job,
3197
+ error: error instanceof Error ? error.message : String(error)
3198
+ });
3199
+ }).finally(() => {
3200
+ if (!res.writableEnded) res.end();
3201
+ });
3202
+ return;
3203
+ }
3204
+ const result = await executeReflectJob(job, body);
3205
+ sendJson(res, 200, result);
3206
+ return;
3207
+ }
3208
+ if (pathname === "/v1/reindex") {
3209
+ const agentId = asString(body.agent_id) ?? defaultAgentId;
3210
+ const job = createJob(jobs, "reindex", agentId);
3211
+ if (wantsSse(req, body)) {
3212
+ openSse(res);
3213
+ sendSse(res, "job", job);
3214
+ void executeReindexJob(job, body, res).then(({ job: currentJob, result: result2 }) => {
3215
+ sendSse(res, "done", { job: currentJob, result: result2 });
3216
+ }).catch((error) => {
3217
+ sendSse(res, "error", {
3218
+ job,
3219
+ error: error instanceof Error ? error.message : String(error)
3220
+ });
3221
+ }).finally(() => {
3222
+ if (!res.writableEnded) res.end();
3223
+ });
3224
+ return;
3225
+ }
3226
+ const result = await executeReindexJob(job, body);
3227
+ sendJson(res, 200, result);
3228
+ return;
3229
+ }
3230
+ sendError(res, 404, `Route not found: ${method} ${pathname}`);
3231
+ } catch (error) {
3232
+ sendError(res, 500, error instanceof Error ? error.message : String(error));
959
3233
  }
960
- }
3234
+ });
3235
+ return {
3236
+ server,
3237
+ db,
3238
+ jobs,
3239
+ listen(port = 3e3, host = "127.0.0.1") {
3240
+ return new Promise((resolve2, reject) => {
3241
+ server.once("error", reject);
3242
+ server.listen(port, host, () => {
3243
+ server.off("error", reject);
3244
+ const address = server.address();
3245
+ if (!address || typeof address === "string") {
3246
+ resolve2({ port, host });
3247
+ return;
3248
+ }
3249
+ resolve2({ port: address.port, host: address.address });
3250
+ });
3251
+ });
3252
+ },
3253
+ close() {
3254
+ return new Promise((resolve2, reject) => {
3255
+ server.close((error) => {
3256
+ if (ownsDb) {
3257
+ try {
3258
+ db.close();
3259
+ } catch {
3260
+ }
3261
+ }
3262
+ if (error) {
3263
+ reject(error);
3264
+ return;
3265
+ }
3266
+ resolve2();
3267
+ });
3268
+ });
3269
+ }
3270
+ };
3271
+ }
3272
+ async function startHttpServer(options) {
3273
+ const service = createHttpServer(options);
3274
+ await service.listen(options?.port, options?.host);
3275
+ return service;
961
3276
  }
962
3277
 
963
3278
  // src/bin/agent-memory.ts
964
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
965
- import { resolve, basename } from "path";
3279
+ import { writeFileSync as writeFileSync2 } from "fs";
966
3280
  var args = process.argv.slice(2);
967
3281
  var command = args[0];
968
3282
  function getDbPath() {
@@ -973,19 +3287,21 @@ function getAgentId() {
973
3287
  }
974
3288
  function printHelp() {
975
3289
  console.log(`
976
- \u{1F9E0} AgentMemory v3 \u2014 Sleep-cycle memory for AI agents
3290
+ \u{1F9E0} AgentMemory v4 \u2014 Sleep-cycle memory for AI agents
977
3291
 
978
3292
  Usage: agent-memory <command> [options]
979
3293
 
980
3294
  Commands:
981
3295
  init Create database
982
3296
  db:migrate Run schema migrations (no-op if up-to-date)
983
- remember <content> [--uri X] [--type T] Store a memory
984
- recall <query> [--limit N] Search memories (BM25 + priority\xD7vitality)
985
- boot Load identity memories
3297
+ remember <content> [--uri X] [--type T] [--emotion-tag TAG] Store a memory
3298
+ recall <query> [--limit N] [--emotion-tag TAG] Search memories (hybrid retrieval)
3299
+ boot [--format json|narrative] [--agent-name NAME] Load identity memories
3300
+ surface [--out FILE] [--days N] [--limit N] Export recent memories as Markdown
986
3301
  status Show statistics
987
3302
  reflect [decay|tidy|govern|all] Run sleep cycle
988
- reindex Rebuild FTS index with jieba tokenizer
3303
+ reindex [--full] [--batch-size N] Rebuild FTS index and embeddings (if configured)
3304
+ serve [--host H] [--port N] Start the HTTP/SSE API server
989
3305
  migrate <dir> Import from Markdown files
990
3306
  export <dir> Export memories to Markdown files
991
3307
  help Show this help
@@ -996,10 +3312,28 @@ Environment:
996
3312
  `);
997
3313
  }
998
3314
  function getFlag(flag) {
999
- const idx = args.indexOf(flag);
1000
- if (idx >= 0 && idx + 1 < args.length) return args[idx + 1];
3315
+ const index = args.indexOf(flag);
3316
+ if (index >= 0 && index + 1 < args.length) return args[index + 1];
1001
3317
  return void 0;
1002
3318
  }
3319
+ function hasFlag(flag) {
3320
+ return args.includes(flag);
3321
+ }
3322
+ function getPositionalArgs(startIndex = 1) {
3323
+ const values = [];
3324
+ for (let index = startIndex; index < args.length; index++) {
3325
+ const token = args[index];
3326
+ if (token.startsWith("--")) {
3327
+ const next = args[index + 1];
3328
+ if (next !== void 0 && !next.startsWith("--")) {
3329
+ index += 1;
3330
+ }
3331
+ continue;
3332
+ }
3333
+ values.push(token);
3334
+ }
3335
+ return values;
3336
+ }
1003
3337
  async function main() {
1004
3338
  try {
1005
3339
  switch (command) {
@@ -1018,7 +3352,7 @@ async function main() {
1018
3352
  break;
1019
3353
  }
1020
3354
  case "remember": {
1021
- const content = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
3355
+ const content = getPositionalArgs(1).join(" ");
1022
3356
  if (!content) {
1023
3357
  console.error("Usage: agent-memory remember <content>");
1024
3358
  process.exit(1);
@@ -1026,116 +3360,203 @@ async function main() {
1026
3360
  const db = openDatabase({ path: getDbPath() });
1027
3361
  const uri = getFlag("--uri");
1028
3362
  const type = getFlag("--type") ?? "knowledge";
1029
- const agentId = getAgentId();
1030
- const result = syncOne(db, { content, type, uri, source: "manual", agent_id: agentId });
3363
+ const emotionTag = getFlag("--emotion-tag");
3364
+ const result = await rememberMemory(db, {
3365
+ content,
3366
+ type,
3367
+ uri,
3368
+ source: "manual",
3369
+ agent_id: getAgentId(),
3370
+ emotion_tag: emotionTag
3371
+ });
1031
3372
  console.log(`${result.action}: ${result.reason}${result.memoryId ? ` (${result.memoryId.slice(0, 8)})` : ""}`);
1032
3373
  db.close();
1033
3374
  break;
1034
3375
  }
1035
3376
  case "recall": {
1036
- const query = args.slice(1).filter((a) => !a.startsWith("--")).join(" ");
3377
+ const query = getPositionalArgs(1).join(" ");
1037
3378
  if (!query) {
1038
3379
  console.error("Usage: agent-memory recall <query>");
1039
3380
  process.exit(1);
1040
3381
  }
1041
3382
  const db = openDatabase({ path: getDbPath() });
1042
- const limit = parseInt(getFlag("--limit") ?? "10", 10);
1043
- const agentId = getAgentId();
1044
- const raw = searchBM25(db, query, { agent_id: agentId, limit: Math.max(limit * 2, limit) });
1045
- const weighted = raw.map((r) => {
1046
- const weight = [4, 3, 2, 1][r.memory.priority] ?? 1;
1047
- const vitality = Math.max(0.1, r.memory.vitality);
1048
- return { ...r, score: r.score * weight * vitality };
1049
- }).sort((a, b) => b.score - a.score).slice(0, limit);
1050
- console.log(`\u{1F50D} Results: ${weighted.length}
3383
+ const emotionTag = getFlag("--emotion-tag");
3384
+ const result = await recallMemory(db, {
3385
+ query,
3386
+ agent_id: getAgentId(),
3387
+ limit: Number.parseInt(getFlag("--limit") ?? "10", 10),
3388
+ emotion_tag: emotionTag
3389
+ });
3390
+ console.log(`\u{1F50D} Results: ${result.results.length} (${result.mode})
1051
3391
  `);
1052
- for (const r of weighted) {
1053
- const p = ["\u{1F534}", "\u{1F7E0}", "\u{1F7E1}", "\u26AA"][r.memory.priority];
1054
- const v = (r.memory.vitality * 100).toFixed(0);
1055
- console.log(`${p} P${r.memory.priority} [${v}%] ${r.memory.content.slice(0, 80)}`);
3392
+ for (const row of result.results) {
3393
+ const priorityLabel = ["\u{1F534}", "\u{1F7E0}", "\u{1F7E1}", "\u26AA"][row.memory.priority];
3394
+ const vitality = (row.memory.vitality * 100).toFixed(0);
3395
+ const branches = [
3396
+ row.bm25_rank ? `bm25#${row.bm25_rank}` : null,
3397
+ row.vector_rank ? `vec#${row.vector_rank}` : null
3398
+ ].filter(Boolean).join(" + ");
3399
+ console.log(`${priorityLabel} P${row.memory.priority} [${vitality}%] ${row.memory.content.slice(0, 80)}${branches ? ` (${branches})` : ""}`);
1056
3400
  }
1057
3401
  db.close();
1058
3402
  break;
1059
3403
  }
1060
3404
  case "boot": {
1061
3405
  const db = openDatabase({ path: getDbPath() });
1062
- const result = boot(db, { agent_id: getAgentId() });
1063
- console.log(`\u{1F9E0} Boot: ${result.identityMemories.length} identity memories loaded
3406
+ const format = getFlag("--format") ?? "narrative";
3407
+ const agentName = getFlag("--agent-name") ?? "Agent";
3408
+ const result = boot(db, { agent_id: getAgentId(), format, agent_name: agentName });
3409
+ if (format === "narrative" && result.narrative) {
3410
+ console.log(result.narrative);
3411
+ } else {
3412
+ console.log(`\u{1F9E0} Boot: ${result.identityMemories.length} identity memories loaded
1064
3413
  `);
1065
- for (const m of result.identityMemories) {
1066
- console.log(` \u{1F534} ${m.content.slice(0, 100)}`);
1067
- }
1068
- if (result.bootPaths.length) {
1069
- console.log(`
3414
+ for (const memory of result.identityMemories) {
3415
+ console.log(` \u{1F534} ${memory.content.slice(0, 100)}`);
3416
+ }
3417
+ if (result.bootPaths.length) {
3418
+ console.log(`
1070
3419
  \u{1F4CD} Boot paths: ${result.bootPaths.join(", ")}`);
3420
+ }
3421
+ }
3422
+ db.close();
3423
+ break;
3424
+ }
3425
+ case "surface": {
3426
+ const db = openDatabase({ path: getDbPath() });
3427
+ const days = Number.parseInt(getFlag("--days") ?? "7", 10);
3428
+ const limit = Number.parseInt(getFlag("--limit") ?? "50", 10);
3429
+ const minVitality = Number.parseFloat(getFlag("--min-vitality") ?? "0.1");
3430
+ const outFile = getFlag("--out");
3431
+ const typesRaw = getFlag("--types");
3432
+ const types = typesRaw ? typesRaw.split(",").map((t) => t.trim()) : void 0;
3433
+ const cutoff = new Date(Date.now() - days * 864e5).toISOString();
3434
+ const surfaceResult = await surfaceMemories(db, {
3435
+ task: "context loading",
3436
+ intent: "temporal",
3437
+ agent_id: getAgentId(),
3438
+ types,
3439
+ limit: Math.max(limit, 100),
3440
+ // fetch more, filter by date after
3441
+ min_vitality: minVitality
3442
+ });
3443
+ const filtered = surfaceResult.results.filter((r) => r.memory.updated_at >= cutoff).slice(0, limit);
3444
+ const grouped = {};
3445
+ for (const r of filtered) {
3446
+ const t = r.memory.type;
3447
+ if (!grouped[t]) grouped[t] = [];
3448
+ grouped[t].push(r);
3449
+ }
3450
+ const { formatRelativeDate: formatRelativeDate2 } = await Promise.resolve().then(() => (init_boot(), boot_exports));
3451
+ const lines = [];
3452
+ lines.push("# Recent Memories");
3453
+ lines.push("");
3454
+ lines.push(`> Auto-generated by AgentMemory surface. Last updated: ${(/* @__PURE__ */ new Date()).toISOString()}`);
3455
+ lines.push("");
3456
+ const typeOrder = ["identity", "emotion", "knowledge", "event"];
3457
+ const typeLabels = { identity: "Identity", emotion: "Emotion", knowledge: "Knowledge", event: "Events" };
3458
+ for (const t of typeOrder) {
3459
+ const items = grouped[t];
3460
+ if (!items?.length) continue;
3461
+ lines.push(`## ${typeLabels[t]}`);
3462
+ for (const item of items) {
3463
+ const content = item.memory.content.split("\n")[0].slice(0, 200);
3464
+ const time = formatRelativeDate2(item.memory.updated_at);
3465
+ const tag = item.memory.emotion_tag;
3466
+ const meta = t === "emotion" && tag ? `(${tag}, ${time})` : `(${time})`;
3467
+ lines.push(`- ${content} ${meta}`);
3468
+ }
3469
+ lines.push("");
3470
+ }
3471
+ const markdown = lines.join("\n");
3472
+ if (outFile) {
3473
+ writeFileSync2(outFile, markdown, "utf-8");
3474
+ console.log(`\u2705 Surface: ${filtered.length} memories written to ${outFile}`);
3475
+ } else {
3476
+ console.log(markdown);
1071
3477
  }
1072
3478
  db.close();
1073
3479
  break;
1074
3480
  }
1075
3481
  case "status": {
1076
3482
  const db = openDatabase({ path: getDbPath() });
1077
- const agentId = getAgentId();
1078
- const stats = countMemories(db, agentId);
1079
- const lowVit = db.prepare("SELECT COUNT(*) as c FROM memories WHERE vitality < 0.1 AND agent_id = ?").get(agentId).c;
1080
- const paths = db.prepare("SELECT COUNT(*) as c FROM paths WHERE agent_id = ?").get(agentId).c;
3483
+ const status = getMemoryStatus(db, { agent_id: getAgentId() });
1081
3484
  console.log("\u{1F9E0} AgentMemory Status\n");
1082
- console.log(` Total memories: ${stats.total}`);
1083
- console.log(` By type: ${Object.entries(stats.by_type).map(([k, v]) => `${k}=${v}`).join(", ")}`);
1084
- console.log(` By priority: ${Object.entries(stats.by_priority).map(([k, v]) => `${k}=${v}`).join(", ")}`);
1085
- console.log(` Paths: ${paths}`);
1086
- console.log(` Low vitality (<10%): ${lowVit}`);
3485
+ console.log(` Total memories: ${status.total}`);
3486
+ console.log(` By type: ${Object.entries(status.by_type).map(([key, value]) => `${key}=${value}`).join(", ")}`);
3487
+ console.log(` By priority: ${Object.entries(status.by_priority).map(([key, value]) => `${key}=${value}`).join(", ")}`);
3488
+ console.log(` Paths: ${status.paths}`);
3489
+ console.log(` Low vitality (<10%): ${status.low_vitality}`);
3490
+ console.log(` Feedback events: ${status.feedback_events}`);
1087
3491
  db.close();
1088
3492
  break;
1089
3493
  }
1090
3494
  case "reflect": {
1091
3495
  const phase = args[1] ?? "all";
1092
3496
  const db = openDatabase({ path: getDbPath() });
1093
- const agentId = getAgentId();
1094
- console.log(`\u{1F319} Running ${phase} phase...
1095
- `);
1096
- if (phase === "decay" || phase === "all") {
1097
- const r = runDecay(db, { agent_id: agentId });
1098
- console.log(` Decay: ${r.updated} updated, ${r.decayed} decayed, ${r.belowThreshold} below threshold`);
1099
- }
1100
- if (phase === "tidy" || phase === "all") {
1101
- const r = runTidy(db, { agent_id: agentId });
1102
- console.log(` Tidy: ${r.archived} archived, ${r.orphansCleaned} orphans`);
1103
- }
1104
- if (phase === "govern" || phase === "all") {
1105
- const r = runGovern(db, { agent_id: agentId });
1106
- console.log(` Govern: ${r.orphanPaths} paths, ${r.emptyMemories} empty cleaned`);
3497
+ const result = await reflectMemories(db, { phase, agent_id: getAgentId() });
3498
+ console.log(`\u{1F319} Reflect job ${result.jobId}${result.resumed ? " (resume)" : ""}`);
3499
+ for (const [name, summary] of Object.entries(result.results)) {
3500
+ console.log(` ${name}: ${JSON.stringify(summary)}`);
1107
3501
  }
1108
3502
  db.close();
1109
3503
  break;
1110
3504
  }
1111
3505
  case "reindex": {
1112
3506
  const db = openDatabase({ path: getDbPath() });
1113
- const memories = db.prepare("SELECT id, content FROM memories").all();
1114
- db.exec("DELETE FROM memories_fts");
1115
- const insert = db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)");
1116
- let count = 0;
1117
- const txn = db.transaction(() => {
1118
- for (const mem of memories) {
1119
- insert.run(mem.id, tokenizeForIndex(mem.content));
1120
- count++;
1121
- }
3507
+ const result = await reindexMemories(db, {
3508
+ agent_id: getAgentId(),
3509
+ force: hasFlag("--full"),
3510
+ batchSize: Number.parseInt(getFlag("--batch-size") ?? "16", 10)
1122
3511
  });
1123
- txn();
1124
- console.log(`\u{1F504} Reindexed ${count} memories with jieba tokenizer`);
3512
+ console.log(`\u{1F504} Reindexed ${result.fts.reindexed} memories in BM25 index`);
3513
+ if (result.embeddings.enabled) {
3514
+ console.log(`\u{1F9EC} Embeddings: provider=${result.embeddings.providerId} scanned=${result.embeddings.scanned} embedded=${result.embeddings.embedded} failed=${result.embeddings.failed}`);
3515
+ } else {
3516
+ console.log("\u{1F9EC} Embeddings: disabled (no provider configured)");
3517
+ }
1125
3518
  db.close();
1126
3519
  break;
1127
3520
  }
3521
+ case "serve": {
3522
+ const port = Number.parseInt(getFlag("--port") ?? process.env.AGENT_MEMORY_HTTP_PORT ?? "3000", 10);
3523
+ const host = getFlag("--host") ?? process.env.AGENT_MEMORY_HTTP_HOST ?? "127.0.0.1";
3524
+ const service = await startHttpServer({
3525
+ dbPath: getDbPath(),
3526
+ agentId: getAgentId(),
3527
+ port,
3528
+ host
3529
+ });
3530
+ const address = service.server.address();
3531
+ if (address && typeof address !== "string") {
3532
+ console.log(`\u{1F310} AgentMemory HTTP server listening on http://${address.address}:${address.port}`);
3533
+ } else {
3534
+ console.log(`\u{1F310} AgentMemory HTTP server listening on http://${host}:${port}`);
3535
+ }
3536
+ const shutdown = async () => {
3537
+ try {
3538
+ await service.close();
3539
+ } finally {
3540
+ process.exit(0);
3541
+ }
3542
+ };
3543
+ process.once("SIGINT", () => {
3544
+ void shutdown();
3545
+ });
3546
+ process.once("SIGTERM", () => {
3547
+ void shutdown();
3548
+ });
3549
+ break;
3550
+ }
1128
3551
  case "export": {
1129
3552
  const dir = args[1];
1130
3553
  if (!dir) {
1131
3554
  console.error("Usage: agent-memory export <directory>");
1132
3555
  process.exit(1);
1133
3556
  }
1134
- const dirPath = resolve(dir);
1135
3557
  const db = openDatabase({ path: getDbPath() });
1136
- const agentId = getAgentId();
1137
- const result = exportMemories(db, dirPath, { agent_id: agentId });
1138
- console.log(`\u2705 Export complete: ${result.exported} items to ${dirPath} (${result.files.length} files)`);
3558
+ const result = exportMemories(db, resolve(dir), { agent_id: getAgentId() });
3559
+ console.log(`\u2705 Export complete: ${result.exported} items to ${resolve(dir)} (${result.files.length} files)`);
1139
3560
  db.close();
1140
3561
  break;
1141
3562
  }
@@ -1153,10 +3574,10 @@ async function main() {
1153
3574
  const db = openDatabase({ path: getDbPath() });
1154
3575
  const agentId = getAgentId();
1155
3576
  let imported = 0;
1156
- const memoryFile = ["MEMORY.md", "MEMORY.qmd"].map((f) => resolve(dirPath, f)).find((p) => existsSync2(p));
3577
+ const memoryFile = ["MEMORY.md", "MEMORY.qmd"].map((file) => resolve(dirPath, file)).find((file) => existsSync2(file));
1157
3578
  if (memoryFile) {
1158
3579
  const content = readFileSync2(memoryFile, "utf-8");
1159
- const sections = content.split(/^## /m).filter((s) => s.trim());
3580
+ const sections = content.split(/^## /m).filter((section) => section.trim());
1160
3581
  for (const section of sections) {
1161
3582
  const lines = section.split("\n");
1162
3583
  const title = lines[0]?.trim();
@@ -1164,40 +3585,46 @@ async function main() {
1164
3585
  if (!body) continue;
1165
3586
  const type = title?.toLowerCase().includes("\u5173\u4E8E") || title?.toLowerCase().includes("about") ? "identity" : "knowledge";
1166
3587
  const uri = `knowledge://memory-md/${title?.replace(/[^a-z0-9\u4e00-\u9fff]/gi, "-").toLowerCase()}`;
1167
- syncOne(db, { content: `## ${title}
1168
- ${body}`, type, uri, source: `migrate:${basename(memoryFile)}`, agent_id: agentId });
1169
- imported++;
3588
+ await rememberMemory(db, {
3589
+ content: `## ${title}
3590
+ ${body}`,
3591
+ type,
3592
+ uri,
3593
+ source: `migrate:${basename(memoryFile)}`,
3594
+ agent_id: agentId
3595
+ });
3596
+ imported += 1;
1170
3597
  }
1171
3598
  console.log(`\u{1F4C4} ${basename(memoryFile)}: ${sections.length} sections imported`);
1172
3599
  }
1173
- const mdFiles = readdirSync(dirPath).filter((f) => /^\d{4}-\d{2}-\d{2}\.(md|qmd)$/.test(f)).sort();
3600
+ const mdFiles = readdirSync(dirPath).filter((file) => /^\d{4}-\d{2}-\d{2}\.(md|qmd)$/.test(file)).sort();
1174
3601
  for (const file of mdFiles) {
1175
3602
  const content = readFileSync2(resolve(dirPath, file), "utf-8");
1176
3603
  const date = file.replace(/\.(md|qmd)$/i, "");
1177
- syncOne(db, {
3604
+ await rememberMemory(db, {
1178
3605
  content,
1179
3606
  type: "event",
1180
3607
  uri: `event://journal/${date}`,
1181
3608
  source: `migrate:${file}`,
1182
3609
  agent_id: agentId
1183
3610
  });
1184
- imported++;
3611
+ imported += 1;
1185
3612
  }
1186
3613
  if (mdFiles.length) console.log(`\u{1F4DD} Journals: ${mdFiles.length} files imported`);
1187
3614
  const weeklyDir = resolve(dirPath, "weekly");
1188
3615
  if (existsSync2(weeklyDir)) {
1189
- const weeklyFiles = readdirSync(weeklyDir).filter((f) => f.endsWith(".md") || f.endsWith(".qmd"));
3616
+ const weeklyFiles = readdirSync(weeklyDir).filter((file) => file.endsWith(".md") || file.endsWith(".qmd"));
1190
3617
  for (const file of weeklyFiles) {
1191
3618
  const content = readFileSync2(resolve(weeklyDir, file), "utf-8");
1192
3619
  const week = file.replace(/\.(md|qmd)$/i, "");
1193
- syncOne(db, {
3620
+ await rememberMemory(db, {
1194
3621
  content,
1195
3622
  type: "knowledge",
1196
3623
  uri: `knowledge://weekly/${week}`,
1197
3624
  source: `migrate:weekly/${file}`,
1198
3625
  agent_id: agentId
1199
3626
  });
1200
- imported++;
3627
+ imported += 1;
1201
3628
  }
1202
3629
  if (weeklyFiles.length) console.log(`\u{1F4E6} Weekly: ${weeklyFiles.length} files imported`);
1203
3630
  }
@@ -1217,8 +3644,8 @@ ${body}`, type, uri, source: `migrate:${basename(memoryFile)}`, agent_id: agentI
1217
3644
  printHelp();
1218
3645
  process.exit(1);
1219
3646
  }
1220
- } catch (err) {
1221
- const message = err instanceof Error ? err.message : String(err);
3647
+ } catch (error) {
3648
+ const message = error instanceof Error ? error.message : String(error);
1222
3649
  console.error("Error:", message);
1223
3650
  process.exit(1);
1224
3651
  }