@smyslenny/agent-memory 4.0.0-alpha.1 → 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,126 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  // AgentMemory v2 — Sleep-cycle memory for AI agents
3
-
4
- // src/bin/agent-memory.ts
5
- import { existsSync as existsSync2, readFileSync as readFileSync2, readdirSync } from "fs";
6
- import { basename, resolve } from "path";
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
+ };
7
12
 
8
13
  // src/core/db.ts
9
14
  import Database from "better-sqlite3";
10
15
  import { randomUUID } from "crypto";
11
- var SCHEMA_VERSION = 5;
12
- var SCHEMA_SQL = `
13
- -- Memory entries
14
- CREATE TABLE IF NOT EXISTS memories (
15
- id TEXT PRIMARY KEY,
16
- content TEXT NOT NULL,
17
- type TEXT NOT NULL CHECK(type IN ('identity','emotion','knowledge','event')),
18
- priority INTEGER NOT NULL DEFAULT 2 CHECK(priority BETWEEN 0 AND 3),
19
- emotion_val REAL NOT NULL DEFAULT 0.0,
20
- vitality REAL NOT NULL DEFAULT 1.0,
21
- stability REAL NOT NULL DEFAULT 1.0,
22
- access_count INTEGER NOT NULL DEFAULT 0,
23
- last_accessed TEXT,
24
- created_at TEXT NOT NULL,
25
- updated_at TEXT NOT NULL,
26
- source TEXT,
27
- agent_id TEXT NOT NULL DEFAULT 'default',
28
- hash TEXT,
29
- UNIQUE(hash, agent_id)
30
- );
31
-
32
- -- URI paths (Content-Path separation, from nocturne)
33
- CREATE TABLE IF NOT EXISTS paths (
34
- id TEXT PRIMARY KEY,
35
- memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
36
- agent_id TEXT NOT NULL DEFAULT 'default',
37
- uri TEXT NOT NULL,
38
- alias TEXT,
39
- domain TEXT NOT NULL,
40
- created_at TEXT NOT NULL,
41
- UNIQUE(agent_id, uri)
42
- );
43
-
44
- -- Association network (knowledge graph)
45
- CREATE TABLE IF NOT EXISTS links (
46
- agent_id TEXT NOT NULL DEFAULT 'default',
47
- source_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
48
- target_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
49
- relation TEXT NOT NULL,
50
- weight REAL NOT NULL DEFAULT 1.0,
51
- created_at TEXT NOT NULL,
52
- PRIMARY KEY (agent_id, source_id, target_id)
53
- );
54
-
55
- -- Snapshots (version control, from nocturne + Memory Palace)
56
- CREATE TABLE IF NOT EXISTS snapshots (
57
- id TEXT PRIMARY KEY,
58
- memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
59
- content TEXT NOT NULL,
60
- changed_by TEXT,
61
- action TEXT NOT NULL CHECK(action IN ('create','update','delete','merge')),
62
- created_at TEXT NOT NULL
63
- );
64
-
65
- -- Full-text search index (BM25)
66
- CREATE VIRTUAL TABLE IF NOT EXISTS memories_fts USING fts5(
67
- id UNINDEXED,
68
- content,
69
- tokenize='unicode61'
70
- );
71
-
72
- -- Embeddings (optional semantic layer)
73
- CREATE TABLE IF NOT EXISTS embeddings (
74
- id TEXT PRIMARY KEY,
75
- memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
76
- provider_id TEXT NOT NULL,
77
- vector BLOB,
78
- content_hash TEXT NOT NULL,
79
- status TEXT NOT NULL CHECK(status IN ('pending','ready','failed')),
80
- created_at TEXT NOT NULL,
81
- UNIQUE(memory_id, provider_id)
82
- );
83
-
84
- -- Maintenance jobs (reflect / reindex checkpoints)
85
- CREATE TABLE IF NOT EXISTS maintenance_jobs (
86
- job_id TEXT PRIMARY KEY,
87
- phase TEXT NOT NULL CHECK(phase IN ('decay','tidy','govern','all')),
88
- status TEXT NOT NULL CHECK(status IN ('running','completed','failed')),
89
- checkpoint TEXT,
90
- error TEXT,
91
- started_at TEXT NOT NULL,
92
- finished_at TEXT
93
- );
94
-
95
- -- Feedback signals (recall/surface usefulness + governance priors)
96
- CREATE TABLE IF NOT EXISTS feedback_events (
97
- id TEXT PRIMARY KEY,
98
- memory_id TEXT NOT NULL REFERENCES memories(id) ON DELETE CASCADE,
99
- source TEXT NOT NULL DEFAULT 'surface',
100
- useful INTEGER NOT NULL DEFAULT 1,
101
- agent_id TEXT NOT NULL DEFAULT 'default',
102
- event_type TEXT NOT NULL DEFAULT 'surface:useful',
103
- value REAL NOT NULL DEFAULT 1.0,
104
- created_at TEXT NOT NULL
105
- );
106
-
107
- -- Schema version tracking
108
- CREATE TABLE IF NOT EXISTS schema_meta (
109
- key TEXT PRIMARY KEY,
110
- value TEXT NOT NULL
111
- );
112
-
113
- -- Indexes for common queries
114
- CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
115
- CREATE INDEX IF NOT EXISTS idx_memories_priority ON memories(priority);
116
- CREATE INDEX IF NOT EXISTS idx_memories_agent ON memories(agent_id);
117
- CREATE INDEX IF NOT EXISTS idx_memories_vitality ON memories(vitality);
118
- CREATE INDEX IF NOT EXISTS idx_memories_hash ON memories(hash);
119
- CREATE INDEX IF NOT EXISTS idx_paths_memory ON paths(memory_id);
120
- CREATE INDEX IF NOT EXISTS idx_paths_domain ON paths(domain);
121
- CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);
122
- CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);
123
- `;
124
16
  function openDatabase(opts) {
125
17
  const db = new Database(opts.path);
126
18
  if (opts.walMode !== false) {
@@ -198,6 +90,11 @@ function migrateDatabase(db, from, to) {
198
90
  v = 5;
199
91
  continue;
200
92
  }
93
+ if (v === 5) {
94
+ migrateV5ToV6(db);
95
+ v = 6;
96
+ continue;
97
+ }
201
98
  throw new Error(`Unsupported schema migration path: v${from} \u2192 v${to} (stuck at v${v})`);
202
99
  }
203
100
  }
@@ -283,6 +180,8 @@ function inferSchemaVersion(db) {
283
180
  const hasV4Embeddings = hasEmbeddings && tableHasColumn(db, "embeddings", "provider_id") && tableHasColumn(db, "embeddings", "status") && tableHasColumn(db, "embeddings", "content_hash") && tableHasColumn(db, "embeddings", "id");
284
181
  const hasMaintenanceJobs = tableExists(db, "maintenance_jobs");
285
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;
286
185
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings && hasMaintenanceJobs && hasFeedbackEvents) return 5;
287
186
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasV4Embeddings) return 4;
288
187
  if (hasAgentScopedPaths && hasAgentScopedLinks && hasEmbeddings) return 3;
@@ -309,6 +208,9 @@ function ensureIndexes(db) {
309
208
  if (tableExists(db, "maintenance_jobs")) {
310
209
  db.exec("CREATE INDEX IF NOT EXISTS idx_maintenance_jobs_phase_status ON maintenance_jobs(phase, status, started_at DESC);");
311
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
+ }
312
214
  if (tableExists(db, "feedback_events")) {
313
215
  db.exec("CREATE INDEX IF NOT EXISTS idx_feedback_events_memory ON feedback_events(memory_id, created_at DESC);");
314
216
  if (tableHasColumn(db, "feedback_events", "agent_id") && tableHasColumn(db, "feedback_events", "source")) {
@@ -442,14 +344,149 @@ function migrateV4ToV5(db) {
442
344
  throw e;
443
345
  }
444
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
+ );
445
391
 
446
- // src/core/memory.ts
447
- import { createHash as createHash2 } 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
+ });
448
486
 
449
487
  // src/search/tokenizer.ts
450
488
  import { readFileSync } from "fs";
451
489
  import { createRequire } from "module";
452
- var _jieba;
453
490
  function getJieba() {
454
491
  if (_jieba !== void 0) return _jieba;
455
492
  try {
@@ -463,38 +500,6 @@ function getJieba() {
463
500
  }
464
501
  return _jieba;
465
502
  }
466
- var STOPWORDS = /* @__PURE__ */ new Set([
467
- "\u7684",
468
- "\u4E86",
469
- "\u5728",
470
- "\u662F",
471
- "\u6211",
472
- "\u6709",
473
- "\u548C",
474
- "\u5C31",
475
- "\u4E0D",
476
- "\u4EBA",
477
- "\u90FD",
478
- "\u4E00",
479
- "\u4E2A",
480
- "\u4E0A",
481
- "\u4E5F",
482
- "\u5230",
483
- "\u4ED6",
484
- "\u6CA1",
485
- "\u8FD9",
486
- "\u8981",
487
- "\u4F1A",
488
- "\u5BF9",
489
- "\u8BF4",
490
- "\u800C",
491
- "\u53BB",
492
- "\u4E4B",
493
- "\u88AB",
494
- "\u5979",
495
- "\u628A",
496
- "\u90A3"
497
- ]);
498
503
  function tokenize(text) {
499
504
  const cleaned = text.replace(/[^\w\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af\s]/g, " ");
500
505
  const tokens = [];
@@ -524,6 +529,44 @@ function tokenizeForIndex(text) {
524
529
  const tokens = tokenize(text);
525
530
  return tokens.join(" ");
526
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
+ });
527
570
 
528
571
  // src/search/embedding.ts
529
572
  import { createHash } from "crypto";
@@ -651,6 +694,11 @@ function createLocalHttpEmbeddingProvider(opts) {
651
694
  function normalizeEmbeddingBaseUrl(baseUrl) {
652
695
  return trimTrailingSlashes(baseUrl);
653
696
  }
697
+ var init_embedding = __esm({
698
+ "src/search/embedding.ts"() {
699
+ "use strict";
700
+ }
701
+ });
654
702
 
655
703
  // src/search/providers.ts
656
704
  function parseDimension(raw) {
@@ -750,6 +798,12 @@ function getConfiguredEmbeddingProviderId(opts) {
750
798
  return null;
751
799
  }
752
800
  }
801
+ var init_providers = __esm({
802
+ "src/search/providers.ts"() {
803
+ "use strict";
804
+ init_embedding();
805
+ }
806
+ });
753
807
 
754
808
  // src/search/vector.ts
755
809
  function encodeVector(vector) {
@@ -862,27 +916,18 @@ function searchByVector(db, queryVector, opts) {
862
916
  provider_id: row.provider_id
863
917
  }));
864
918
  }
919
+ var init_vector = __esm({
920
+ "src/search/vector.ts"() {
921
+ "use strict";
922
+ init_db();
923
+ }
924
+ });
865
925
 
866
926
  // src/core/memory.ts
927
+ import { createHash as createHash2 } from "crypto";
867
928
  function contentHash(content) {
868
929
  return createHash2("sha256").update(content.trim()).digest("hex").slice(0, 16);
869
930
  }
870
- var TYPE_PRIORITY = {
871
- identity: 0,
872
- emotion: 1,
873
- knowledge: 2,
874
- event: 3
875
- };
876
- var PRIORITY_STABILITY = {
877
- 0: Infinity,
878
- // P0: never decays
879
- 1: 365,
880
- // P1: 365-day half-life
881
- 2: 90,
882
- // P2: 90-day half-life
883
- 3: 14
884
- // P3: 14-day half-life
885
- };
886
931
  function resolveEmbeddingProviderId(explicitProviderId) {
887
932
  if (explicitProviderId !== void 0) {
888
933
  return explicitProviderId;
@@ -909,8 +954,8 @@ function createMemory(db, input) {
909
954
  const timestamp = now();
910
955
  db.prepare(
911
956
  `INSERT INTO memories (id, content, type, priority, emotion_val, vitality, stability,
912
- access_count, created_at, updated_at, source, agent_id, hash)
913
- VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?)`
957
+ access_count, created_at, updated_at, source, agent_id, hash, emotion_tag)
958
+ VALUES (?, ?, ?, ?, ?, 1.0, ?, 0, ?, ?, ?, ?, ?, ?)`
914
959
  ).run(
915
960
  id,
916
961
  input.content,
@@ -922,7 +967,8 @@ function createMemory(db, input) {
922
967
  timestamp,
923
968
  input.source ?? null,
924
969
  agentId,
925
- hash
970
+ hash,
971
+ input.emotion_tag ?? null
926
972
  );
927
973
  db.prepare("INSERT INTO memories_fts (id, content) VALUES (?, ?)").run(id, tokenizeForIndex(input.content));
928
974
  markEmbeddingDirtyIfNeeded(db, id, hash, resolveEmbeddingProviderId(input.embedding_provider_id));
@@ -966,6 +1012,10 @@ function updateMemory(db, id, input) {
966
1012
  fields.push("source = ?");
967
1013
  values.push(input.source);
968
1014
  }
1015
+ if (input.emotion_tag !== void 0) {
1016
+ fields.push("emotion_tag = ?");
1017
+ values.push(input.emotion_tag);
1018
+ }
969
1019
  fields.push("updated_at = ?");
970
1020
  values.push(now());
971
1021
  values.push(id);
@@ -1007,6 +1057,10 @@ function listMemories(db, opts) {
1007
1057
  conditions.push("vitality >= ?");
1008
1058
  params.push(opts.min_vitality);
1009
1059
  }
1060
+ if (opts?.emotion_tag) {
1061
+ conditions.push("emotion_tag = ?");
1062
+ params.push(opts.emotion_tag);
1063
+ }
1010
1064
  const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
1011
1065
  const limit = opts?.limit ?? 100;
1012
1066
  const offset = opts?.offset ?? 0;
@@ -1031,72 +1085,34 @@ function countMemories(db, agent_id = "default") {
1031
1085
  by_priority: Object.fromEntries(byPriority.map((r) => [`P${r.priority}`, r.c]))
1032
1086
  };
1033
1087
  }
1034
-
1035
- // src/core/export.ts
1036
- import { writeFileSync, mkdirSync, existsSync } from "fs";
1037
- import { join } from "path";
1038
- function exportMemories(db, dirPath, opts) {
1039
- const agentId = opts?.agent_id ?? "default";
1040
- if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true });
1041
- let exported = 0;
1042
- const files = [];
1043
- const identities = listMemories(db, { agent_id: agentId, type: "identity" });
1044
- const knowledge = listMemories(db, { agent_id: agentId, type: "knowledge" });
1045
- const emotions = listMemories(db, { agent_id: agentId, type: "emotion" });
1046
- if (identities.length || knowledge.length || emotions.length) {
1047
- const sections = ["# Agent Memory Export\n"];
1048
- if (identities.length) {
1049
- sections.push("## Identity\n");
1050
- for (const m of identities) {
1051
- sections.push(`- ${m.content}
1052
- `);
1053
- exported++;
1054
- }
1055
- }
1056
- if (emotions.length) {
1057
- sections.push("\n## Emotions\n");
1058
- for (const m of emotions) {
1059
- sections.push(`- ${m.content}
1060
- `);
1061
- exported++;
1062
- }
1063
- }
1064
- if (knowledge.length) {
1065
- sections.push("\n## Knowledge\n");
1066
- for (const m of knowledge) {
1067
- sections.push(`- ${m.content}
1068
- `);
1069
- exported++;
1070
- }
1071
- }
1072
- const memoryPath = join(dirPath, "MEMORY.md");
1073
- writeFileSync(memoryPath, sections.join("\n"));
1074
- files.push(memoryPath);
1075
- }
1076
- const events = listMemories(db, { agent_id: agentId, type: "event", limit: 1e4 });
1077
- const byDate = /* @__PURE__ */ new Map();
1078
- for (const ev of events) {
1079
- const date = ev.created_at.slice(0, 10);
1080
- if (!byDate.has(date)) byDate.set(date, []);
1081
- byDate.get(date).push(ev);
1082
- }
1083
- for (const [date, mems] of byDate) {
1084
- const lines = [`# ${date}
1085
- `];
1086
- for (const m of mems) {
1087
- lines.push(`- ${m.content}
1088
- `);
1089
- exported++;
1090
- }
1091
- const filePath = join(dirPath, `${date}.md`);
1092
- writeFileSync(filePath, lines.join("\n"));
1093
- files.push(filePath);
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
+ };
1094
1112
  }
1095
- return { exported, files };
1096
- }
1113
+ });
1097
1114
 
1098
1115
  // src/core/path.ts
1099
- var DEFAULT_DOMAINS = /* @__PURE__ */ new Set(["core", "emotion", "knowledge", "event", "system"]);
1100
1116
  function parseUri(uri) {
1101
1117
  const match = uri.match(/^([a-z]+):\/\/(.+)$/);
1102
1118
  if (!match) throw new Error(`Invalid URI: ${uri}. Expected format: domain://path`);
@@ -1130,10 +1146,85 @@ function getPath(db, id) {
1130
1146
  function getPathByUri(db, uri, agent_id = "default") {
1131
1147
  return db.prepare("SELECT * FROM paths WHERE agent_id = ? AND uri = ?").get(agent_id, uri) ?? null;
1132
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
+ });
1133
1157
 
1134
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
+ }
1135
1224
  function boot(db, opts) {
1136
1225
  const agentId = opts?.agent_id ?? "default";
1226
+ const format = opts?.format ?? "json";
1227
+ const agentName = opts?.agent_name ?? "Agent";
1137
1228
  const corePaths = opts?.corePaths ?? [
1138
1229
  "core://agent",
1139
1230
  "core://user",
@@ -1177,17 +1268,105 @@ function boot(db, opts) {
1177
1268
  }
1178
1269
  }
1179
1270
  }
1180
- return {
1271
+ const result = {
1181
1272
  identityMemories: [...memories.values()],
1182
1273
  bootPaths
1183
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;
1184
1281
  }
1282
+ var init_boot = __esm({
1283
+ "src/sleep/boot.ts"() {
1284
+ "use strict";
1285
+ init_path();
1286
+ init_memory();
1287
+ }
1288
+ });
1185
1289
 
1186
- // src/transports/http.ts
1187
- import { randomUUID as randomUUID2 } from "crypto";
1188
- import http from "http";
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";
1294
+
1295
+ // src/core/export.ts
1296
+ init_memory();
1297
+ import { writeFileSync, mkdirSync, existsSync } from "fs";
1298
+ import { join } from "path";
1299
+ function exportMemories(db, dirPath, opts) {
1300
+ const agentId = opts?.agent_id ?? "default";
1301
+ if (!existsSync(dirPath)) mkdirSync(dirPath, { recursive: true });
1302
+ let exported = 0;
1303
+ const files = [];
1304
+ const identities = listMemories(db, { agent_id: agentId, type: "identity" });
1305
+ const knowledge = listMemories(db, { agent_id: agentId, type: "knowledge" });
1306
+ const emotions = listMemories(db, { agent_id: agentId, type: "emotion" });
1307
+ if (identities.length || knowledge.length || emotions.length) {
1308
+ const sections = ["# Agent Memory Export\n"];
1309
+ if (identities.length) {
1310
+ sections.push("## Identity\n");
1311
+ for (const m of identities) {
1312
+ sections.push(`- ${m.content}
1313
+ `);
1314
+ exported++;
1315
+ }
1316
+ }
1317
+ if (emotions.length) {
1318
+ sections.push("\n## Emotions\n");
1319
+ for (const m of emotions) {
1320
+ sections.push(`- ${m.content}
1321
+ `);
1322
+ exported++;
1323
+ }
1324
+ }
1325
+ if (knowledge.length) {
1326
+ sections.push("\n## Knowledge\n");
1327
+ for (const m of knowledge) {
1328
+ sections.push(`- ${m.content}
1329
+ `);
1330
+ exported++;
1331
+ }
1332
+ }
1333
+ const memoryPath = join(dirPath, "MEMORY.md");
1334
+ writeFileSync(memoryPath, sections.join("\n"));
1335
+ files.push(memoryPath);
1336
+ }
1337
+ const events = listMemories(db, { agent_id: agentId, type: "event", limit: 1e4 });
1338
+ const byDate = /* @__PURE__ */ new Map();
1339
+ for (const ev of events) {
1340
+ const date = ev.created_at.slice(0, 10);
1341
+ if (!byDate.has(date)) byDate.set(date, []);
1342
+ byDate.get(date).push(ev);
1343
+ }
1344
+ for (const [date, mems] of byDate) {
1345
+ const lines = [`# ${date}
1346
+ `];
1347
+ for (const m of mems) {
1348
+ lines.push(`- ${m.content}
1349
+ `);
1350
+ exported++;
1351
+ }
1352
+ const filePath = join(dirPath, `${date}.md`);
1353
+ writeFileSync(filePath, lines.join("\n"));
1354
+ files.push(filePath);
1355
+ }
1356
+ return { exported, files };
1357
+ }
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();
1189
1367
 
1190
1368
  // src/search/bm25.ts
1369
+ init_tokenizer();
1191
1370
  function searchBM25(db, query, opts) {
1192
1371
  const limit = opts?.limit ?? 20;
1193
1372
  const agentId = opts?.agent_id ?? "default";
@@ -1258,6 +1437,8 @@ function rebuildBm25Index(db, opts) {
1258
1437
  }
1259
1438
 
1260
1439
  // src/search/hybrid.ts
1440
+ init_providers();
1441
+ init_vector();
1261
1442
  var PRIORITY_WEIGHT = {
1262
1443
  0: 4,
1263
1444
  1: 3,
@@ -1451,714 +1632,742 @@ async function reindexEmbeddings(db, opts) {
1451
1632
  };
1452
1633
  }
1453
1634
 
1454
- // src/core/merge.ts
1455
- function uniqueNonEmpty(values) {
1456
- return [...new Set(values.map((value) => value?.trim()).filter((value) => Boolean(value)))];
1457
- }
1458
- function splitClauses(content) {
1459
- return content.split(/[\n;;。.!?!?]+/).map((part) => part.trim()).filter(Boolean);
1460
- }
1461
- function mergeAliases(existing, incoming, content) {
1462
- const aliases = uniqueNonEmpty([
1463
- existing !== content ? existing : void 0,
1464
- incoming !== content ? incoming : void 0
1465
- ]);
1466
- return aliases.length > 0 ? aliases : void 0;
1467
- }
1468
- function replaceIdentity(context) {
1469
- const content = context.incoming.content.trim();
1470
- return {
1471
- strategy: "replace",
1472
- content,
1473
- aliases: mergeAliases(context.existing.content, context.incoming.content, content),
1474
- notes: ["identity canonicalized to the newest authoritative phrasing"]
1475
- };
1476
- }
1477
- function appendEmotionEvidence(context) {
1478
- const lines = uniqueNonEmpty([
1479
- ...context.existing.content.split(/\n+/),
1480
- context.incoming.content
1481
- ]);
1482
- const content = lines.length <= 1 ? lines[0] ?? context.incoming.content.trim() : [lines[0], "", ...lines.slice(1).map((line) => `- ${line.replace(/^-\s*/, "")}`)].join("\n");
1483
- return {
1484
- strategy: "append_evidence",
1485
- content,
1486
- aliases: mergeAliases(context.existing.content, context.incoming.content, content),
1487
- notes: ["emotion evidence appended to preserve timeline without duplicating identical lines"]
1488
- };
1489
- }
1490
- function synthesizeKnowledge(context) {
1491
- const clauses = uniqueNonEmpty([
1492
- ...splitClauses(context.existing.content),
1493
- ...splitClauses(context.incoming.content)
1494
- ]);
1495
- const content = clauses.length <= 1 ? clauses[0] ?? context.incoming.content.trim() : clauses.join("\uFF1B");
1496
- return {
1497
- strategy: "synthesize",
1498
- content,
1499
- aliases: mergeAliases(context.existing.content, context.incoming.content, content),
1500
- notes: ["knowledge statements synthesized into a canonical summary"]
1501
- };
1502
- }
1503
- function compactEventTimeline(context) {
1504
- const points = uniqueNonEmpty([
1505
- ...context.existing.content.split(/\n+/),
1506
- context.incoming.content
1507
- ]).map((line) => line.replace(/^-\s*/, ""));
1508
- const content = points.length <= 1 ? points[0] ?? context.incoming.content.trim() : ["Timeline:", ...points.map((line) => `- ${line}`)].join("\n");
1509
- return {
1510
- strategy: "compact_timeline",
1511
- content,
1512
- aliases: mergeAliases(context.existing.content, context.incoming.content, content),
1513
- notes: ["event observations compacted into a single timeline window"]
1514
- };
1515
- }
1516
- function buildMergePlan(context) {
1517
- const type = context.incoming.type ?? context.existing.type;
1518
- switch (type) {
1519
- case "identity":
1520
- return replaceIdentity(context);
1521
- case "emotion":
1522
- return appendEmotionEvidence(context);
1523
- case "knowledge":
1524
- return synthesizeKnowledge(context);
1525
- case "event":
1526
- return compactEventTimeline(context);
1527
- }
1528
- }
1635
+ // src/app/surface.ts
1636
+ init_providers();
1637
+ init_tokenizer();
1638
+ init_vector();
1529
1639
 
1530
- // src/core/guard.ts
1531
- var NEAR_EXACT_THRESHOLD = 0.93;
1532
- var MERGE_THRESHOLD = 0.82;
1640
+ // src/app/feedback.ts
1641
+ init_db();
1533
1642
  function clamp01(value) {
1534
1643
  if (!Number.isFinite(value)) return 0;
1535
1644
  return Math.max(0, Math.min(1, value));
1536
1645
  }
1537
- function uniqueTokenSet(text) {
1538
- return new Set(tokenize(text));
1539
- }
1540
- function overlapScore(left, right) {
1541
- const a = new Set(left);
1542
- const b = new Set(right);
1543
- if (a.size === 0 || b.size === 0) return 0;
1544
- let shared = 0;
1545
- for (const token of a) {
1546
- if (b.has(token)) shared += 1;
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}`);
1547
1656
  }
1548
- return shared / Math.max(a.size, b.size);
1549
- }
1550
- function extractEntities(text) {
1551
- const matches = text.match(/[A-Z][A-Za-z0-9_-]+|\d+(?:[-/:]\d+)*|[#@][\w-]+|[\u4e00-\u9fff]{2,}|\w+:\/\/[^\s]+/g) ?? [];
1552
- return new Set(matches.map((value) => value.trim()).filter(Boolean));
1553
- }
1554
- function safeDomain(uri) {
1555
- if (!uri) return null;
1556
1657
  try {
1557
- return parseUri(uri).domain;
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);
1558
1662
  } catch {
1559
- return null;
1560
- }
1561
- }
1562
- function getPrimaryUri(db, memoryId, agentId) {
1563
- const row = db.prepare("SELECT uri FROM paths WHERE memory_id = ? AND agent_id = ? ORDER BY created_at DESC LIMIT 1").get(memoryId, agentId);
1564
- return row?.uri ?? null;
1565
- }
1566
- function uriScopeMatch(inputUri, candidateUri) {
1567
- if (inputUri && candidateUri) {
1568
- if (inputUri === candidateUri) return 1;
1569
- const inputDomain2 = safeDomain(inputUri);
1570
- const candidateDomain2 = safeDomain(candidateUri);
1571
- if (inputDomain2 && candidateDomain2 && inputDomain2 === candidateDomain2) return 0.85;
1572
- return 0;
1573
- }
1574
- if (!inputUri && !candidateUri) {
1575
- return 0.65;
1576
- }
1577
- const inputDomain = safeDomain(inputUri ?? null);
1578
- const candidateDomain = safeDomain(candidateUri ?? null);
1579
- if (inputDomain && candidateDomain && inputDomain === candidateDomain) {
1580
- return 0.75;
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);
1581
1667
  }
1582
- return 0.2;
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
+ };
1583
1677
  }
1584
- function extractObservedAt(parts, fallback) {
1585
- for (const part of parts) {
1586
- if (!part) continue;
1587
- const match = part.match(/(20\d{2}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2}(?::\d{2})?))?/);
1588
- if (!match) continue;
1589
- const iso = match[2] ? `${match[1]}T${match[2]}Z` : `${match[1]}T00:00:00Z`;
1590
- const parsed = new Date(iso);
1591
- if (!Number.isNaN(parsed.getTime())) {
1592
- return parsed;
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 };
1593
1690
  }
1594
- }
1595
- if (fallback) {
1596
- const parsed = new Date(fallback);
1597
- if (!Number.isNaN(parsed.getTime())) {
1598
- return parsed;
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 };
1599
1708
  }
1709
+ return {
1710
+ total: row.total,
1711
+ useful: row.useful,
1712
+ not_useful: row.not_useful,
1713
+ score: clamp01(row.avg_value)
1714
+ };
1600
1715
  }
1601
- return null;
1602
1716
  }
1603
- function timeProximity(input, memory, candidateUri) {
1604
- if (input.type !== "event") {
1605
- return 1;
1606
- }
1607
- const inputTime = extractObservedAt([input.uri, input.source, input.content], input.now ?? null);
1608
- const existingTime = extractObservedAt([candidateUri, memory.source, memory.content], memory.created_at);
1609
- if (!inputTime || !existingTime) {
1610
- return 0.5;
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
1611
1749
  }
1612
- const diffDays = Math.abs(inputTime.getTime() - existingTime.getTime()) / (1e3 * 60 * 60 * 24);
1613
- return clamp01(1 - diffDays / 7);
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));
1614
1759
  }
1615
- function scoreCandidate(input, candidate, candidateUri) {
1616
- const lexicalOverlap = overlapScore(uniqueTokenSet(input.content), uniqueTokenSet(candidate.memory.content));
1617
- const entityOverlap = Math.max(
1618
- overlapScore(extractEntities(input.content), extractEntities(candidate.memory.content)),
1619
- lexicalOverlap * 0.75
1620
- );
1621
- const uriMatch = uriScopeMatch(input.uri, candidateUri);
1622
- const temporal = timeProximity(input, candidate.memory, candidateUri);
1623
- const semantic = clamp01(candidate.vector_score ?? lexicalOverlap);
1624
- const dedupScore = clamp01(
1625
- 0.5 * semantic + 0.2 * lexicalOverlap + 0.15 * uriMatch + 0.1 * entityOverlap + 0.05 * temporal
1760
+ function uniqueTokenSet(values) {
1761
+ return new Set(
1762
+ values.flatMap((value) => tokenize(value ?? "")).map((token) => token.trim()).filter(Boolean)
1626
1763
  );
1627
- return {
1628
- semantic_similarity: semantic,
1629
- lexical_overlap: lexicalOverlap,
1630
- uri_scope_match: uriMatch,
1631
- entity_overlap: entityOverlap,
1632
- time_proximity: temporal,
1633
- dedup_score: dedupScore
1634
- };
1635
1764
  }
1636
- async function recallCandidates(db, input, agentId) {
1637
- const provider = input.provider === void 0 ? getEmbeddingProviderFromEnv() : input.provider;
1638
- const response = await recallMemories(db, input.content, {
1639
- agent_id: agentId,
1640
- limit: Math.max(6, input.candidateLimit ?? 8),
1641
- lexicalLimit: Math.max(8, input.candidateLimit ?? 8),
1642
- vectorLimit: Math.max(8, input.candidateLimit ?? 8),
1643
- provider,
1644
- recordAccess: false
1645
- });
1646
- return response.results.filter((row) => row.memory.type === input.type).map((row) => {
1647
- const uri = getPrimaryUri(db, row.memory.id, agentId);
1648
- return {
1649
- result: row,
1650
- uri,
1651
- domain: safeDomain(uri),
1652
- score: scoreCandidate(input, row, uri)
1653
- };
1654
- }).sort((left, right) => right.score.dedup_score - left.score.dedup_score);
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));
1655
1772
  }
1656
- function fourCriterionGate(input) {
1657
- const content = input.content.trim();
1658
- const failed = [];
1659
- const priority = input.priority ?? (input.type === "identity" ? 0 : input.type === "emotion" ? 1 : input.type === "knowledge" ? 2 : 3);
1660
- const minLength = priority <= 1 ? 4 : 8;
1661
- const specificity = content.length >= minLength ? Math.min(1, content.length / 50) : 0;
1662
- if (specificity === 0) failed.push(`specificity (too short: ${content.length} < ${minLength} chars)`);
1663
- const tokens = tokenize(content);
1664
- const novelty = tokens.length >= 1 ? Math.min(1, tokens.length / 5) : 0;
1665
- if (novelty === 0) failed.push("novelty (no meaningful tokens after filtering)");
1666
- const hasCJK = /[\u4e00-\u9fff]/.test(content);
1667
- const hasCapitalized = /[A-Z][a-z]+/.test(content);
1668
- const hasNumbers = /\d+/.test(content);
1669
- const hasURI = /\w+:\/\//.test(content);
1670
- const hasEntityMarkers = /[@#]/.test(content);
1671
- const hasMeaningfulLength = content.length >= 15;
1672
- const topicSignals = [hasCJK, hasCapitalized, hasNumbers, hasURI, hasEntityMarkers, hasMeaningfulLength].filter(Boolean).length;
1673
- const relevance = topicSignals >= 1 ? Math.min(1, topicSignals / 3) : 0;
1674
- if (relevance === 0) failed.push("relevance (no identifiable topics/entities)");
1675
- const allCaps = content === content.toUpperCase() && content.length > 20 && /^[A-Z\s]+$/.test(content);
1676
- const hasWhitespaceOrPunctuation = /[\s,。!?,.!?;;::]/.test(content) || content.length < 30;
1677
- const excessiveRepetition = /(.)\1{9,}/.test(content);
1678
- let coherence = 1;
1679
- if (allCaps) coherence -= 0.5;
1680
- if (!hasWhitespaceOrPunctuation) coherence -= 0.3;
1681
- if (excessiveRepetition) coherence -= 0.5;
1682
- coherence = Math.max(0, coherence);
1683
- if (coherence < 0.3) failed.push("coherence (garbled or malformed content)");
1684
- return {
1685
- pass: failed.length === 0,
1686
- scores: { specificity, novelty, relevance, coherence },
1687
- failedCriteria: failed
1688
- };
1773
+ function rankScore(rank, window) {
1774
+ if (!rank) return 0;
1775
+ return clamp012(1 - (rank - 1) / Math.max(window, 1));
1689
1776
  }
1690
- async function guard(db, input) {
1691
- const hash = contentHash(input.content);
1692
- const agentId = input.agent_id ?? "default";
1693
- const exactMatch = db.prepare("SELECT id FROM memories WHERE hash = ? AND agent_id = ?").get(hash, agentId);
1694
- if (exactMatch) {
1695
- return { action: "skip", reason: "Exact duplicate (hash match)", existingId: exactMatch.id };
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;
1696
1795
  }
1697
- if (input.uri) {
1698
- const existingPath = getPathByUri(db, input.uri, agentId);
1699
- if (existingPath) {
1700
- return {
1701
- action: "update",
1702
- reason: `URI ${input.uri} already exists, updating canonical content`,
1703
- existingId: existingPath.memory_id,
1704
- updatedContent: input.content
1705
- };
1706
- }
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)}`);
1707
1807
  }
1708
- const gateResult = fourCriterionGate(input);
1709
- if (!gateResult.pass) {
1710
- return { action: "skip", reason: `Gate rejected: ${gateResult.failedCriteria.join(", ")}` };
1808
+ if (input.lexicalScore > 0.2 && input.query) {
1809
+ reasons.add(`lexical:${topicLabel(input.query)}`);
1711
1810
  }
1712
- if (input.conservative) {
1713
- return { action: "add", reason: "Conservative mode enabled; semantic dedup disabled" };
1811
+ if (input.taskMatch > 0.2) {
1812
+ reasons.add(`task:${topicLabel(input.task, input.intent)}`);
1714
1813
  }
1715
- const candidates = await recallCandidates(db, input, agentId);
1716
- const best = candidates[0];
1717
- if (!best) {
1718
- return { action: "add", reason: "No relevant semantic candidates found" };
1814
+ if (input.intent) {
1815
+ reasons.add(`intent:${input.intent}`);
1719
1816
  }
1720
- const score = best.score;
1721
- if (score.dedup_score >= NEAR_EXACT_THRESHOLD) {
1722
- const shouldUpdateMetadata = Boolean(input.uri && !getPathByUri(db, input.uri, agentId));
1723
- return {
1724
- action: shouldUpdateMetadata ? "update" : "skip",
1725
- reason: shouldUpdateMetadata ? `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)}), updating metadata` : `Near-exact duplicate detected (score=${score.dedup_score.toFixed(3)})`,
1726
- existingId: best.result.memory.id,
1727
- score
1728
- };
1817
+ if (input.feedbackScore >= 0.67) {
1818
+ reasons.add("feedback:reinforced");
1819
+ } else if (input.feedbackScore <= 0.33) {
1820
+ reasons.add("feedback:negative");
1729
1821
  }
1730
- if (score.dedup_score >= MERGE_THRESHOLD) {
1731
- const mergePlan = buildMergePlan({
1732
- existing: best.result.memory,
1733
- incoming: {
1734
- content: input.content,
1735
- type: input.type,
1736
- source: input.source
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);
1737
1898
  }
1738
- });
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 });
1910
+ }
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
+ );
1739
1929
  return {
1740
- action: "merge",
1741
- reason: `Semantic near-duplicate detected (score=${score.dedup_score.toFixed(3)}), applying ${mergePlan.strategy}`,
1742
- existingId: best.result.memory.id,
1743
- mergedContent: mergePlan.content,
1744
- mergePlan,
1745
- score
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
1746
1952
  };
1747
- }
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);
1748
1960
  return {
1749
- action: "add",
1750
- reason: `Semantic score below merge threshold (score=${score.dedup_score.toFixed(3)})`,
1751
- score
1961
+ count: results.length,
1962
+ query: trimmedQuery,
1963
+ task: trimmedTask,
1964
+ intent: input.intent,
1965
+ results
1752
1966
  };
1753
1967
  }
1754
1968
 
1969
+ // src/transports/http.ts
1970
+ init_db();
1971
+ import { randomUUID as randomUUID2 } from "crypto";
1972
+ import http from "http";
1973
+
1755
1974
  // src/sleep/sync.ts
1756
- function ensureUriPath(db, memoryId, uri, agentId) {
1757
- if (!uri) return;
1758
- if (getPathByUri(db, uri, agentId ?? "default")) return;
1759
- try {
1760
- createPath(db, memoryId, uri, void 0, void 0, agentId);
1761
- } catch {
1762
- }
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)))];
1763
1986
  }
1764
- async function syncOne(db, input) {
1765
- const memInput = {
1766
- content: input.content,
1767
- type: input.type ?? "event",
1768
- priority: input.priority,
1769
- emotion_val: input.emotion_val,
1770
- source: input.source,
1771
- agent_id: input.agent_id,
1772
- uri: input.uri,
1773
- provider: input.provider,
1774
- conservative: input.conservative
1775
- };
1776
- const guardResult = await guard(db, memInput);
1777
- switch (guardResult.action) {
1778
- case "skip":
1779
- return { action: "skipped", reason: guardResult.reason, memoryId: guardResult.existingId };
1780
- case "add": {
1781
- const mem = createMemory(db, memInput);
1782
- if (!mem) return { action: "skipped", reason: "createMemory returned null" };
1783
- ensureUriPath(db, mem.id, input.uri, input.agent_id);
1784
- return { action: "added", memoryId: mem.id, reason: guardResult.reason };
1785
- }
1786
- case "update": {
1787
- if (!guardResult.existingId) return { action: "skipped", reason: "No existing ID for update" };
1788
- if (guardResult.updatedContent !== void 0) {
1789
- updateMemory(db, guardResult.existingId, { content: guardResult.updatedContent });
1790
- }
1791
- ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
1792
- return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
1793
- }
1794
- case "merge": {
1795
- if (!guardResult.existingId || !guardResult.mergedContent) {
1796
- return { action: "skipped", reason: "Missing merge data" };
1797
- }
1798
- updateMemory(db, guardResult.existingId, { content: guardResult.mergedContent });
1799
- ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
1800
- return { action: "merged", memoryId: guardResult.existingId, reason: guardResult.reason };
1801
- }
1802
- }
1987
+ function splitClauses(content) {
1988
+ return content.split(/[\n;;。.!?!?]+/).map((part) => part.trim()).filter(Boolean);
1803
1989
  }
1804
-
1805
- // src/app/remember.ts
1806
- async function rememberMemory(db, input) {
1807
- return syncOne(db, {
1808
- content: input.content,
1809
- type: input.type,
1810
- priority: input.priority,
1811
- emotion_val: input.emotion_val,
1812
- uri: input.uri,
1813
- source: input.source,
1814
- agent_id: input.agent_id,
1815
- provider: input.provider,
1816
- conservative: input.conservative
1817
- });
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;
1818
1996
  }
1819
-
1820
- // src/app/recall.ts
1821
- async function recallMemory(db, input) {
1822
- return recallMemories(db, input.query, {
1823
- agent_id: input.agent_id,
1824
- limit: input.limit,
1825
- min_vitality: input.min_vitality,
1826
- lexicalLimit: input.lexicalLimit,
1827
- vectorLimit: input.vectorLimit,
1828
- provider: input.provider,
1829
- recordAccess: input.recordAccess
1830
- });
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
+ };
1831
2005
  }
1832
-
1833
- // src/app/feedback.ts
1834
- function clamp012(value) {
1835
- if (!Number.isFinite(value)) return 0;
1836
- return Math.max(0, Math.min(1, value));
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
+ };
1837
2018
  }
1838
- function recordFeedbackEvent(db, input) {
1839
- const id = newId();
1840
- const created_at = now();
1841
- const agentId = input.agent_id ?? "default";
1842
- const useful = input.useful ? 1 : 0;
1843
- const value = input.useful ? 1 : 0;
1844
- const eventType = `${input.source}:${input.useful ? "useful" : "not_useful"}`;
1845
- const exists = db.prepare("SELECT id FROM memories WHERE id = ?").get(input.memory_id);
1846
- if (!exists) {
1847
- throw new Error(`Memory not found: ${input.memory_id}`);
1848
- }
1849
- try {
1850
- db.prepare(
1851
- `INSERT INTO feedback_events (id, memory_id, source, useful, agent_id, event_type, value, created_at)
1852
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
1853
- ).run(id, input.memory_id, input.source, useful, agentId, eventType, value, created_at);
1854
- } catch {
1855
- db.prepare(
1856
- `INSERT INTO feedback_events (id, memory_id, event_type, value, created_at)
1857
- VALUES (?, ?, ?, ?, ?)`
1858
- ).run(id, input.memory_id, eventType, value, created_at);
1859
- }
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");
1860
2025
  return {
1861
- id,
1862
- memory_id: input.memory_id,
1863
- source: input.source,
1864
- useful: input.useful,
1865
- agent_id: agentId,
1866
- created_at,
1867
- value
2026
+ strategy: "synthesize",
2027
+ content,
2028
+ aliases: mergeAliases(context.existing.content, context.incoming.content, content),
2029
+ notes: ["knowledge statements synthesized into a canonical summary"]
1868
2030
  };
1869
2031
  }
1870
- function getFeedbackSummary(db, memoryId, agentId) {
1871
- try {
1872
- const row = db.prepare(
1873
- `SELECT COUNT(*) as total,
1874
- COALESCE(SUM(CASE WHEN useful = 1 THEN 1 ELSE 0 END), 0) as useful,
1875
- COALESCE(SUM(CASE WHEN useful = 0 THEN 1 ELSE 0 END), 0) as not_useful
1876
- FROM feedback_events
1877
- WHERE memory_id = ?
1878
- AND (? IS NULL OR agent_id = ?)`
1879
- ).get(memoryId, agentId ?? null, agentId ?? null);
1880
- if (!row || row.total === 0) {
1881
- return { total: 0, useful: 0, not_useful: 0, score: 0.5 };
1882
- }
1883
- return {
1884
- total: row.total,
1885
- useful: row.useful,
1886
- not_useful: row.not_useful,
1887
- score: clamp012(row.useful / row.total)
1888
- };
1889
- } catch {
1890
- const row = db.prepare(
1891
- `SELECT COUNT(*) as total,
1892
- COALESCE(SUM(CASE WHEN value >= 0.5 THEN 1 ELSE 0 END), 0) as useful,
1893
- COALESCE(SUM(CASE WHEN value < 0.5 THEN 1 ELSE 0 END), 0) as not_useful,
1894
- COALESCE(AVG(value), 0.5) as avg_value
1895
- FROM feedback_events
1896
- WHERE memory_id = ?`
1897
- ).get(memoryId);
1898
- if (!row || row.total === 0) {
1899
- return { total: 0, useful: 0, not_useful: 0, score: 0.5 };
1900
- }
1901
- return {
1902
- total: row.total,
1903
- useful: row.useful,
1904
- not_useful: row.not_useful,
1905
- score: clamp012(row.avg_value)
1906
- };
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);
1907
2056
  }
1908
2057
  }
1909
2058
 
1910
- // src/app/surface.ts
1911
- var INTENT_PRIORS = {
1912
- factual: {
1913
- identity: 0.25,
1914
- emotion: 0.15,
1915
- knowledge: 1,
1916
- event: 0.8
1917
- },
1918
- preference: {
1919
- identity: 1,
1920
- emotion: 0.85,
1921
- knowledge: 0.55,
1922
- event: 0.25
1923
- },
1924
- temporal: {
1925
- identity: 0.15,
1926
- emotion: 0.35,
1927
- knowledge: 0.5,
1928
- event: 1
1929
- },
1930
- planning: {
1931
- identity: 0.65,
1932
- emotion: 0.2,
1933
- knowledge: 1,
1934
- event: 0.6
1935
- },
1936
- design: {
1937
- identity: 0.8,
1938
- emotion: 0.35,
1939
- knowledge: 1,
1940
- event: 0.25
1941
- }
1942
- };
1943
- var DESIGN_HINT_RE = /\b(ui|ux|design|style|component|layout|brand|palette|theme)\b|风格|界面|设计|配色|低饱和|玻璃拟态|渐变/i;
1944
- var PLANNING_HINT_RE = /\b(plan|planning|todo|next|ship|build|implement|roadmap|task|milestone)\b|计划|下一步|待办|实现|重构/i;
1945
- var FACTUAL_HINT_RE = /\b(what|fact|constraint|rule|docs|document|api|status)\b|规则|约束|文档|接口|事实/i;
1946
- var TEMPORAL_HINT_RE = /\b(today|yesterday|tomorrow|recent|before|after|when|timeline)\b|今天|昨天|明天|最近|时间线|何时/i;
1947
- var PREFERENCE_HINT_RE = /\b(prefer|preference|like|dislike|avoid|favorite)\b|喜欢|偏好|不喜欢|避免|讨厌/i;
2059
+ // src/core/guard.ts
2060
+ init_memory();
2061
+ var NEAR_EXACT_THRESHOLD = 0.93;
2062
+ var MERGE_THRESHOLD = 0.82;
1948
2063
  function clamp013(value) {
1949
2064
  if (!Number.isFinite(value)) return 0;
1950
2065
  return Math.max(0, Math.min(1, value));
1951
2066
  }
1952
- function uniqueTokenSet2(values) {
1953
- return new Set(
1954
- values.flatMap((value) => tokenize(value ?? "")).map((token) => token.trim()).filter(Boolean)
1955
- );
2067
+ function uniqueTokenSet2(text) {
2068
+ return new Set(tokenize(text));
1956
2069
  }
1957
2070
  function overlapScore2(left, right) {
1958
- if (left.size === 0 || right.size === 0) return 0;
2071
+ const a = new Set(left);
2072
+ const b = new Set(right);
2073
+ if (a.size === 0 || b.size === 0) return 0;
1959
2074
  let shared = 0;
1960
- for (const token of left) {
1961
- if (right.has(token)) shared += 1;
2075
+ for (const token of a) {
2076
+ if (b.has(token)) shared += 1;
1962
2077
  }
1963
- return clamp013(shared / Math.max(left.size, right.size));
2078
+ return shared / Math.max(a.size, b.size);
1964
2079
  }
1965
- function rankScore(rank, window) {
1966
- if (!rank) return 0;
1967
- return clamp013(1 - (rank - 1) / Math.max(window, 1));
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));
1968
2083
  }
1969
- function topicLabel(...parts) {
1970
- const token = parts.flatMap((part) => tokenize(part ?? "")).find((value) => value.trim().length > 1);
1971
- const label = (token ?? "context").replace(/[^\p{L}\p{N}_-]+/gu, "-").replace(/^-+|-+$/g, "").slice(0, 32);
1972
- return label || "context";
2084
+ function safeDomain(uri) {
2085
+ if (!uri) return null;
2086
+ try {
2087
+ return parseUri(uri).domain;
2088
+ } catch {
2089
+ return null;
2090
+ }
1973
2091
  }
1974
- function intentKeywordBoost(memory, intent) {
1975
- const content = memory.content;
1976
- switch (intent) {
1977
- case "design":
1978
- return DESIGN_HINT_RE.test(content) ? 1 : 0.65;
1979
- case "planning":
1980
- return PLANNING_HINT_RE.test(content) ? 1 : 0.7;
1981
- case "factual":
1982
- return FACTUAL_HINT_RE.test(content) ? 1 : 0.75;
1983
- case "temporal":
1984
- return TEMPORAL_HINT_RE.test(content) ? 1 : 0.75;
1985
- case "preference":
1986
- return PREFERENCE_HINT_RE.test(content) ? 1 : 0.8;
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;
1987
2111
  }
2112
+ return 0.2;
1988
2113
  }
1989
- function intentMatch(memory, intent) {
1990
- if (!intent) return 0;
1991
- const prior = INTENT_PRIORS[intent][memory.type] ?? 0;
1992
- return clamp013(prior * intentKeywordBoost(memory, intent));
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;
1993
2132
  }
1994
- function buildReasonCodes(input) {
1995
- const reasons = /* @__PURE__ */ new Set();
1996
- reasons.add(`type:${input.memory.type}`);
1997
- if (input.semanticScore > 0.2) {
1998
- reasons.add(`semantic:${topicLabel(input.query, input.task)}`);
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" };
1999
2244
  }
2000
- if (input.lexicalScore > 0.2 && input.query) {
2001
- reasons.add(`lexical:${topicLabel(input.query)}`);
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" };
2002
2249
  }
2003
- if (input.taskMatch > 0.2) {
2004
- reasons.add(`task:${topicLabel(input.task, input.intent)}`);
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
+ };
2005
2259
  }
2006
- if (input.intent) {
2007
- reasons.add(`intent:${input.intent}`);
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
+ };
2008
2277
  }
2009
- if (input.feedbackScore >= 0.67) {
2010
- reasons.add("feedback:reinforced");
2011
- } else if (input.feedbackScore <= 0.33) {
2012
- reasons.add("feedback:negative");
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 {
2013
2292
  }
2014
- return [...reasons];
2015
2293
  }
2016
- function collectBranch(signals, rows, key, similarity) {
2017
- for (const row of rows) {
2018
- const existing = signals.get(row.memory.id) ?? { memory: row.memory };
2019
- const currentRank = existing[key];
2020
- if (currentRank === void 0 || row.rank < currentRank) {
2021
- existing[key] = row.rank;
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 };
2022
2316
  }
2023
- if (similarity) {
2024
- const currentSimilarity = similarity.get(row.memory.id);
2025
- if (currentSimilarity !== void 0) {
2026
- existing.semanticSimilarity = Math.max(existing.semanticSimilarity ?? 0, currentSimilarity);
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 });
2027
2321
  }
2322
+ ensureUriPath(db, guardResult.existingId, input.uri, input.agent_id);
2323
+ return { action: "updated", memoryId: guardResult.existingId, reason: guardResult.reason };
2028
2324
  }
2029
- signals.set(row.memory.id, existing);
2030
- }
2031
- }
2032
- async function surfaceMemories(db, input) {
2033
- const agentId = input.agent_id ?? "default";
2034
- const limit = Math.max(1, Math.min(input.limit ?? 5, 20));
2035
- const lexicalWindow = Math.max(24, limit * 6);
2036
- const minVitality = input.min_vitality ?? 0.05;
2037
- const provider = input.provider === void 0 ? getEmbeddingProviderFromEnv() : input.provider;
2038
- const signals = /* @__PURE__ */ new Map();
2039
- const trimmedQuery = input.query?.trim();
2040
- const trimmedTask = input.task?.trim();
2041
- const recentTurns = (input.recent_turns ?? []).map((turn) => turn.trim()).filter(Boolean).slice(-4);
2042
- const queryTokens = uniqueTokenSet2([trimmedQuery, ...recentTurns]);
2043
- const taskTokens = uniqueTokenSet2([trimmedTask]);
2044
- if (trimmedQuery) {
2045
- collectBranch(
2046
- signals,
2047
- searchBM25(db, trimmedQuery, {
2048
- agent_id: agentId,
2049
- limit: lexicalWindow,
2050
- min_vitality: minVitality
2051
- }),
2052
- "queryRank"
2053
- );
2054
- }
2055
- if (trimmedTask) {
2056
- collectBranch(
2057
- signals,
2058
- searchBM25(db, trimmedTask, {
2059
- agent_id: agentId,
2060
- limit: lexicalWindow,
2061
- min_vitality: minVitality
2062
- }),
2063
- "taskRank"
2064
- );
2065
- }
2066
- if (recentTurns.length > 0) {
2067
- collectBranch(
2068
- signals,
2069
- searchBM25(db, recentTurns.join(" "), {
2070
- agent_id: agentId,
2071
- limit: lexicalWindow,
2072
- min_vitality: minVitality
2073
- }),
2074
- "recentRank"
2075
- );
2076
- }
2077
- const semanticQuery = [trimmedQuery, trimmedTask, ...recentTurns].filter(Boolean).join("\n").trim();
2078
- if (provider && semanticQuery) {
2079
- try {
2080
- const [queryVector] = await provider.embed([semanticQuery]);
2081
- if (queryVector) {
2082
- const vectorRows = searchByVector(db, queryVector, {
2083
- providerId: provider.id,
2084
- agent_id: agentId,
2085
- limit: lexicalWindow,
2086
- min_vitality: minVitality
2087
- });
2088
- const similarity = new Map(vectorRows.map((row) => [row.memory.id, row.similarity]));
2089
- collectBranch(signals, vectorRows, "semanticRank", similarity);
2325
+ case "merge": {
2326
+ if (!guardResult.existingId || !guardResult.mergedContent) {
2327
+ return { action: "skipped", reason: "Missing merge data" };
2090
2328
  }
2091
- } catch {
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 };
2092
2332
  }
2093
2333
  }
2094
- const fallbackMemories = listMemories(db, {
2095
- agent_id: agentId,
2096
- min_vitality: minVitality,
2097
- limit: Math.max(48, lexicalWindow)
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
2098
2349
  });
2099
- for (const memory of fallbackMemories) {
2100
- if (!signals.has(memory.id)) {
2101
- signals.set(memory.id, { memory });
2102
- }
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);
2103
2365
  }
2104
- const results = [...signals.values()].map((signal) => signal.memory).filter((memory) => memory.vitality >= minVitality).filter((memory) => input.types?.length ? input.types.includes(memory.type) : true).map((memory) => {
2105
- const signal = signals.get(memory.id) ?? { memory };
2106
- const memoryTokens = new Set(tokenize(memory.content));
2107
- const lexicalOverlap = overlapScore2(memoryTokens, queryTokens);
2108
- const taskOverlap = overlapScore2(memoryTokens, taskTokens);
2109
- const lexicalScore = clamp013(
2110
- 0.45 * rankScore(signal.queryRank, lexicalWindow) + 0.15 * rankScore(signal.recentRank, lexicalWindow) + 0.15 * rankScore(signal.taskRank, lexicalWindow) + 0.25 * lexicalOverlap
2111
- );
2112
- const semanticScore = signal.semanticSimilarity !== void 0 ? clamp013(Math.max(signal.semanticSimilarity, lexicalOverlap * 0.7)) : trimmedQuery || recentTurns.length > 0 ? clamp013(lexicalOverlap * 0.7) : 0;
2113
- const intentScore = intentMatch(memory, input.intent);
2114
- const taskMatch = trimmedTask ? clamp013(0.7 * taskOverlap + 0.3 * intentScore) : intentScore;
2115
- const priorityScore = priorityPrior(memory.priority);
2116
- const feedbackSummary = getFeedbackSummary(db, memory.id, agentId);
2117
- const feedbackScore = feedbackSummary.score;
2118
- const score = clamp013(
2119
- 0.35 * semanticScore + 0.2 * lexicalScore + 0.15 * taskMatch + 0.1 * memory.vitality + 0.1 * priorityScore + 0.1 * feedbackScore
2120
- );
2121
- return {
2122
- memory,
2123
- score,
2124
- semantic_score: semanticScore,
2125
- lexical_score: lexicalScore,
2126
- task_match: taskMatch,
2127
- vitality: memory.vitality,
2128
- priority_prior: priorityScore,
2129
- feedback_score: feedbackScore,
2130
- feedback_summary: feedbackSummary,
2131
- reason_codes: buildReasonCodes({
2132
- memory,
2133
- query: semanticQuery || trimmedQuery,
2134
- task: trimmedTask,
2135
- intent: input.intent,
2136
- semanticScore,
2137
- lexicalScore,
2138
- taskMatch,
2139
- feedbackScore
2140
- }),
2141
- lexical_rank: signal.queryRank ?? signal.recentRank ?? signal.taskRank,
2142
- semantic_rank: signal.semanticRank,
2143
- semantic_similarity: signal.semanticSimilarity
2144
- };
2145
- }).sort((left, right) => {
2146
- if (right.score !== left.score) return right.score - left.score;
2147
- if (right.semantic_score !== left.semantic_score) return right.semantic_score - left.semantic_score;
2148
- if (right.lexical_score !== left.lexical_score) return right.lexical_score - left.lexical_score;
2149
- if (left.memory.priority !== right.memory.priority) return left.memory.priority - right.memory.priority;
2150
- return right.memory.updated_at.localeCompare(left.memory.updated_at);
2151
- }).slice(0, limit);
2152
- return {
2153
- count: results.length,
2154
- query: trimmedQuery,
2155
- task: trimmedTask,
2156
- intent: input.intent,
2157
- results
2158
- };
2366
+ return result;
2159
2367
  }
2160
2368
 
2161
2369
  // src/sleep/decay.ts
2370
+ init_db();
2162
2371
  var MIN_VITALITY = {
2163
2372
  0: 1,
2164
2373
  // P0: identity — never decays
@@ -2219,6 +2428,7 @@ function getDecayedMemories(db, threshold = 0.05, opts) {
2219
2428
  }
2220
2429
 
2221
2430
  // src/sleep/tidy.ts
2431
+ init_memory();
2222
2432
  function runTidy(db, opts) {
2223
2433
  const threshold = opts?.vitalityThreshold ?? 0.05;
2224
2434
  const agentId = opts?.agent_id;
@@ -2235,6 +2445,8 @@ function runTidy(db, opts) {
2235
2445
  }
2236
2446
 
2237
2447
  // src/sleep/govern.ts
2448
+ init_memory();
2449
+ init_tokenizer();
2238
2450
  function clamp014(value) {
2239
2451
  if (!Number.isFinite(value)) return 0;
2240
2452
  return Math.max(0, Math.min(1, value));
@@ -2335,6 +2547,7 @@ function runGovern(db, opts) {
2335
2547
  }
2336
2548
 
2337
2549
  // src/sleep/jobs.ts
2550
+ init_db();
2338
2551
  function parseCheckpoint(raw) {
2339
2552
  if (!raw) return null;
2340
2553
  try {
@@ -2560,6 +2773,7 @@ async function reflectMemories(db, input) {
2560
2773
  }
2561
2774
 
2562
2775
  // src/app/status.ts
2776
+ init_memory();
2563
2777
  function getMemoryStatus(db, input) {
2564
2778
  const agentId = input?.agent_id ?? "default";
2565
2779
  const stats = countMemories(db, agentId);
@@ -3062,6 +3276,7 @@ async function startHttpServer(options) {
3062
3276
  }
3063
3277
 
3064
3278
  // src/bin/agent-memory.ts
3279
+ import { writeFileSync as writeFileSync2 } from "fs";
3065
3280
  var args = process.argv.slice(2);
3066
3281
  var command = args[0];
3067
3282
  function getDbPath() {
@@ -3079,9 +3294,10 @@ Usage: agent-memory <command> [options]
3079
3294
  Commands:
3080
3295
  init Create database
3081
3296
  db:migrate Run schema migrations (no-op if up-to-date)
3082
- remember <content> [--uri X] [--type T] Store a memory
3083
- recall <query> [--limit N] Search memories (hybrid retrieval, auto-fallback to BM25)
3084
- 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
3085
3301
  status Show statistics
3086
3302
  reflect [decay|tidy|govern|all] Run sleep cycle
3087
3303
  reindex [--full] [--batch-size N] Rebuild FTS index and embeddings (if configured)
@@ -3144,12 +3360,14 @@ async function main() {
3144
3360
  const db = openDatabase({ path: getDbPath() });
3145
3361
  const uri = getFlag("--uri");
3146
3362
  const type = getFlag("--type") ?? "knowledge";
3363
+ const emotionTag = getFlag("--emotion-tag");
3147
3364
  const result = await rememberMemory(db, {
3148
3365
  content,
3149
3366
  type,
3150
3367
  uri,
3151
3368
  source: "manual",
3152
- agent_id: getAgentId()
3369
+ agent_id: getAgentId(),
3370
+ emotion_tag: emotionTag
3153
3371
  });
3154
3372
  console.log(`${result.action}: ${result.reason}${result.memoryId ? ` (${result.memoryId.slice(0, 8)})` : ""}`);
3155
3373
  db.close();
@@ -3162,10 +3380,12 @@ async function main() {
3162
3380
  process.exit(1);
3163
3381
  }
3164
3382
  const db = openDatabase({ path: getDbPath() });
3383
+ const emotionTag = getFlag("--emotion-tag");
3165
3384
  const result = await recallMemory(db, {
3166
3385
  query,
3167
3386
  agent_id: getAgentId(),
3168
- limit: Number.parseInt(getFlag("--limit") ?? "10", 10)
3387
+ limit: Number.parseInt(getFlag("--limit") ?? "10", 10),
3388
+ emotion_tag: emotionTag
3169
3389
  });
3170
3390
  console.log(`\u{1F50D} Results: ${result.results.length} (${result.mode})
3171
3391
  `);
@@ -3183,15 +3403,77 @@ async function main() {
3183
3403
  }
3184
3404
  case "boot": {
3185
3405
  const db = openDatabase({ path: getDbPath() });
3186
- const result = boot(db, { agent_id: getAgentId() });
3187
- 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
3188
3413
  `);
3189
- for (const memory of result.identityMemories) {
3190
- console.log(` \u{1F534} ${memory.content.slice(0, 100)}`);
3191
- }
3192
- if (result.bootPaths.length) {
3193
- 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(`
3194
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);
3195
3477
  }
3196
3478
  db.close();
3197
3479
  break;