neoagent 2.4.1-beta.19 → 2.4.1-beta.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +4 -1
  2. package/docs/getting-started.md +9 -3
  3. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  4. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  5. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  6. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  7. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  8. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  9. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  10. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  11. package/flutter_app/lib/features/location/location_service.dart +3 -0
  12. package/flutter_app/lib/main.dart +1 -0
  13. package/flutter_app/lib/main_account_settings.dart +9 -33
  14. package/flutter_app/lib/main_app_shell.dart +237 -197
  15. package/flutter_app/lib/main_controller.dart +0 -25
  16. package/flutter_app/lib/main_devices.dart +2 -0
  17. package/flutter_app/lib/main_models.dart +144 -0
  18. package/flutter_app/lib/main_operations.dart +150 -19
  19. package/flutter_app/lib/main_shared.dart +642 -195
  20. package/flutter_app/lib/main_theme.dart +2 -0
  21. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +3 -1
  22. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  23. package/flutter_app/lib/src/theme/palette.dart +15 -15
  24. package/flutter_app/pubspec.yaml +3 -0
  25. package/flutter_app/web/favicon_light.svg +3 -0
  26. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  27. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  28. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  29. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  30. package/lib/manager.js +282 -81
  31. package/package.json +17 -3
  32. package/server/config/origins.js +3 -1
  33. package/server/db/database.js +73 -0
  34. package/server/public/.last_build_id +1 -1
  35. package/server/public/assets/AssetManifest.bin +1 -1
  36. package/server/public/assets/AssetManifest.bin.json +1 -1
  37. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  38. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  39. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  40. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  41. package/server/public/favicon_light.svg +3 -0
  42. package/server/public/flutter_bootstrap.js +1 -1
  43. package/server/public/icons/Icon-192-light.png +0 -0
  44. package/server/public/icons/Icon-512-light.png +0 -0
  45. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  46. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  47. package/server/public/main.dart.js +68769 -68268
  48. package/server/routes/agent_profiles.js +3 -0
  49. package/server/routes/memory.js +22 -1
  50. package/server/services/account/password_policy.js +6 -1
  51. package/server/services/memory/intelligence.js +181 -0
  52. package/server/services/memory/manager.js +475 -25
  53. package/server/utils/security.js +3 -0
  54. package/server/services/memory/openhuman_uplift.test.js +0 -98
  55. package/server/utils/version.test.js +0 -39
@@ -10,6 +10,15 @@ const {
10
10
  keywordSimilarity
11
11
  } = require('./embeddings');
12
12
  const { getMemoryStorageDecision } = require('./policy');
13
+ const {
14
+ buildFacts,
15
+ canonicalEntityKey,
16
+ extractEntities,
17
+ extractKeywords,
18
+ scoreMemoryCandidate,
19
+ stableHash,
20
+ summarizeForPrompt,
21
+ } = require('./intelligence');
13
22
  const { AGENT_DATA_DIR } = require('../../../runtime/paths');
14
23
  const { isMainAgent, resolveAgentId } = require('../agents/manager');
15
24
  const {
@@ -139,11 +148,16 @@ function computeFreshnessMultiplier(row) {
139
148
 
140
149
  function serializeMemoryRow(row) {
141
150
  const metadata = parseJsonObject(row?.metadata_json, {});
151
+ const entities = Array.isArray(row?.entities)
152
+ ? row.entities
153
+ : parseJsonArray(row?.entities_json, []);
142
154
  return {
143
155
  id: row.id,
144
156
  category: normalizeMemoryCategory(row.category),
145
157
  content: row.content,
158
+ summary: row.summary || '',
146
159
  importance: Number(row.importance || 0),
160
+ confidence: row.confidence == null ? 0.7 : Number(row.confidence),
147
161
  access_count: Number(row.access_count || 0),
148
162
  archived: Number(row.archived || 0),
149
163
  created_at: row.created_at,
@@ -159,6 +173,7 @@ function serializeMemoryRow(row) {
159
173
  },
160
174
  staleAfterDays: row.stale_after_days == null ? null : Number(row.stale_after_days),
161
175
  metadata,
176
+ entities,
162
177
  };
163
178
  }
164
179
 
@@ -225,6 +240,7 @@ function scoreSchedulerRunMatch(queryTokens, title, finalResponse) {
225
240
  class MemoryManager {
226
241
  constructor() {
227
242
  this._ensureDirs();
243
+ this._backfillMemoryIntelligence();
228
244
  }
229
245
 
230
246
  _ensureDirs() {
@@ -266,6 +282,50 @@ class MemoryManager {
266
282
  return resolveAgentId(userId, options?.agentId || options?.agent_id || null);
267
283
  }
268
284
 
285
+ _backfillMemoryIntelligence(limit = 1000) {
286
+ try {
287
+ const rows = db.prepare(
288
+ `SELECT m.*
289
+ FROM memories m
290
+ LEFT JOIN memory_facts f ON f.memory_id = m.id
291
+ WHERE m.archived = 0 AND f.id IS NULL
292
+ ORDER BY m.updated_at DESC
293
+ LIMIT ?`
294
+ ).all(Math.max(1, Math.min(Number(limit) || 1000, 5000)));
295
+ if (!rows.length) return;
296
+
297
+ const backfill = db.transaction(() => {
298
+ for (const row of rows) {
299
+ const category = normalizeMemoryCategory(row.category);
300
+ const content = String(row.content || '').trim();
301
+ if (!content) continue;
302
+ const summary = row.summary || summarizeForPrompt({ content, entities: extractEntities(content) });
303
+ const memoryHash = row.memory_hash || stableHash(`${category}:${content}`);
304
+ db.prepare(
305
+ `UPDATE memories
306
+ SET summary = COALESCE(summary, ?),
307
+ confidence = COALESCE(confidence, 0.7),
308
+ memory_hash = COALESCE(memory_hash, ?)
309
+ WHERE id = ?`
310
+ ).run(summary, memoryHash, row.id);
311
+ this._upsertMemoryIntelligence(row.user_id, row.agent_id, row.id, {
312
+ content,
313
+ category,
314
+ sourceRef: normalizeSourceRef({
315
+ sourceType: row.source_type,
316
+ sourceId: row.source_id,
317
+ sourceLabel: row.source_label,
318
+ }),
319
+ metadata: parseJsonObject(row.metadata_json, {}),
320
+ });
321
+ }
322
+ });
323
+ backfill();
324
+ } catch (err) {
325
+ console.error('[Memory] Backfill intelligence failed:', err.message);
326
+ }
327
+ }
328
+
269
329
  getAssistantBehaviorNotes(userId, options = {}) {
270
330
  if (userId == null) return '';
271
331
  const agentId = this._agentId(userId, options);
@@ -540,10 +600,74 @@ class MemoryManager {
540
600
  }));
541
601
  }
542
602
 
603
+ getMemoryStats(userId, { agentId = null } = {}) {
604
+ const scopedAgentId = this._agentId(userId, { agentId });
605
+ const row = db.prepare(
606
+ `SELECT
607
+ COUNT(*) AS total,
608
+ SUM(CASE WHEN archived = 0 THEN 1 ELSE 0 END) AS active,
609
+ SUM(CASE WHEN archived = 1 THEN 1 ELSE 0 END) AS archived,
610
+ AVG(COALESCE(importance, 0)) AS avg_importance,
611
+ AVG(COALESCE(confidence, 0.7)) AS avg_confidence
612
+ FROM memories
613
+ WHERE user_id = ? AND agent_id = ?`
614
+ ).get(userId, scopedAgentId) || {};
615
+ const entityCount = db.prepare(
616
+ `SELECT COUNT(*) AS count FROM memory_entities WHERE user_id = ? AND agent_id = ?`
617
+ ).get(userId, scopedAgentId)?.count || 0;
618
+ const factCount = db.prepare(
619
+ `SELECT COUNT(*) AS count FROM memory_facts WHERE user_id = ? AND agent_id = ?`
620
+ ).get(userId, scopedAgentId)?.count || 0;
621
+ const viewCount = db.prepare(
622
+ `SELECT COUNT(*) AS count FROM materialized_knowledge_views WHERE user_id = ? AND agent_id = ?`
623
+ ).get(userId, scopedAgentId)?.count || 0;
624
+ const documentCount = db.prepare(
625
+ `SELECT COUNT(*) AS count FROM memory_ingestion_documents WHERE user_id = ? AND agent_id = ?`
626
+ ).get(userId, scopedAgentId)?.count || 0;
627
+ return {
628
+ total: Number(row.total || 0),
629
+ active: Number(row.active || 0),
630
+ archived: Number(row.archived || 0),
631
+ facts: Number(factCount || 0),
632
+ entities: Number(entityCount || 0),
633
+ knowledgeViews: Number(viewCount || 0),
634
+ ingestionDocuments: Number(documentCount || 0),
635
+ averageImportance: Number(row.avg_importance || 0),
636
+ averageConfidence: Number(row.avg_confidence || 0),
637
+ };
638
+ }
639
+
640
+ listEntities(userId, { agentId = null, limit = 24, query = null } = {}) {
641
+ const scopedAgentId = this._agentId(userId, { agentId });
642
+ let sql = `SELECT * FROM memory_entities WHERE user_id = ? AND agent_id = ?`;
643
+ const params = [userId, scopedAgentId];
644
+ const normalizedQuery = String(query || '').trim();
645
+ if (normalizedQuery) {
646
+ sql += ` AND (entity_key LIKE ? OR name LIKE ?)`;
647
+ const like = `%${canonicalEntityKey(normalizedQuery)}%`;
648
+ params.push(like, `%${normalizedQuery}%`);
649
+ }
650
+ sql += ` ORDER BY mention_count DESC, last_seen_at DESC LIMIT ?`;
651
+ params.push(Math.max(1, Math.min(Number(limit) || 24, 100)));
652
+ return db.prepare(sql).all(...params).map((row) => ({
653
+ id: row.id,
654
+ key: row.entity_key,
655
+ name: row.name,
656
+ kind: row.kind || 'concept',
657
+ aliases: normalizeStringArray(parseJsonArray(row.aliases_json), 16, 160),
658
+ summary: row.summary || '',
659
+ mentionCount: Number(row.mention_count || 0),
660
+ firstSeenAt: row.first_seen_at || null,
661
+ lastSeenAt: row.last_seen_at || null,
662
+ metadata: parseJsonObject(row.metadata_json, {}),
663
+ }));
664
+ }
665
+
543
666
  materializeKnowledgeViews(userId, { agentId = null } = {}) {
544
667
  const scopedAgentId = this._agentId(userId, { agentId });
545
668
  const memories = this.listMemories(userId, { limit: 200, agentId: scopedAgentId });
546
669
  const documents = this.listIngestionDocuments(userId, { limit: 200, agentId: scopedAgentId });
670
+ const entities = this.listEntities(userId, { limit: 48, agentId: scopedAgentId });
547
671
  const views = [];
548
672
 
549
673
  const topicGroups = new Map();
@@ -553,7 +677,7 @@ class MemoryManager {
553
677
  topicGroups.get(key).push(memory);
554
678
  }
555
679
  for (const [topic, items] of topicGroups.entries()) {
556
- const summary = items.slice(0, 4).map((item) => `- ${item.content}`).join('\n');
680
+ const summary = items.slice(0, 6).map((item) => `- ${summarizeForPrompt(item)}`).join('\n');
557
681
  views.push({
558
682
  viewType: 'topic',
559
683
  subjectKey: topic,
@@ -569,6 +693,57 @@ class MemoryManager {
569
693
  });
570
694
  }
571
695
 
696
+ for (const entity of entities.slice(0, 24)) {
697
+ const rows = db.prepare(
698
+ `SELECT m.id, m.content, m.category, m.importance, m.summary, m.created_at, m.updated_at
699
+ FROM memory_entity_mentions mem
700
+ JOIN memories m ON m.id = mem.memory_id
701
+ WHERE mem.entity_id = ? AND m.archived = 0
702
+ ORDER BY m.importance DESC, m.updated_at DESC
703
+ LIMIT 8`
704
+ ).all(entity.id);
705
+ if (!rows.length) continue;
706
+ const lines = rows.map((item) => `- [${normalizeMemoryCategory(item.category)}] ${item.summary || item.content}`);
707
+ views.push({
708
+ viewType: 'entity',
709
+ subjectKey: entity.key,
710
+ title: entity.name,
711
+ summary: lines.join('\n').slice(0, 520),
712
+ markdownText: `# ${entity.name}\n\nKind: ${entity.kind}\nMentions: ${entity.mentionCount}\n\n${lines.join('\n')}`,
713
+ sourceMemoryIds: rows.map((item) => item.id),
714
+ sourceDocumentIds: [],
715
+ metadata: {
716
+ entityId: entity.id,
717
+ entityKey: entity.key,
718
+ kind: entity.kind,
719
+ mentionCount: entity.mentionCount,
720
+ },
721
+ });
722
+ }
723
+
724
+ const factRows = db.prepare(
725
+ `SELECT id, memory_id, subject, predicate, object, category, confidence, updated_at
726
+ FROM memory_facts
727
+ WHERE user_id = ? AND agent_id = ?
728
+ ORDER BY updated_at DESC
729
+ LIMIT 20`
730
+ ).all(userId, scopedAgentId);
731
+ if (factRows.length > 0) {
732
+ const lines = factRows.slice(0, 10).map((fact) => `- ${fact.subject}: ${fact.object}`);
733
+ views.push({
734
+ viewType: 'facts',
735
+ subjectKey: 'recent',
736
+ title: 'Recent extracted facts',
737
+ summary: lines.join('\n').slice(0, 520),
738
+ markdownText: `# Recent extracted facts\n\n${lines.join('\n')}`,
739
+ sourceMemoryIds: [...new Set(factRows.map((fact) => fact.memory_id))],
740
+ sourceDocumentIds: [],
741
+ metadata: {
742
+ itemCount: factRows.length,
743
+ },
744
+ });
745
+ }
746
+
572
747
  const accountGroups = new Map();
573
748
  for (const doc of documents) {
574
749
  const accountKey = `${doc.providerKey || 'local'}:${doc.sourceAccount || 'default'}`;
@@ -739,6 +914,189 @@ class MemoryManager {
739
914
  // Semantic Memories (SQLite + embeddings)
740
915
  // ─────────────────────────────────────────────────────────────────────────
741
916
 
917
+ _deleteMemoryIndex(memoryId) {
918
+ try {
919
+ db.prepare('DELETE FROM memories_fts WHERE memory_id = ?').run(memoryId);
920
+ } catch {
921
+ // FTS is optional.
922
+ }
923
+ db.prepare('DELETE FROM memory_entity_mentions WHERE memory_id = ?').run(memoryId);
924
+ db.prepare('DELETE FROM memory_facts WHERE memory_id = ?').run(memoryId);
925
+ }
926
+
927
+ _upsertMemoryIntelligence(userId, agentId, memoryId, {
928
+ content,
929
+ category,
930
+ sourceRef,
931
+ metadata,
932
+ } = {}) {
933
+ const entities = extractEntities(content);
934
+ const facts = buildFacts({ content, category, sourceRef, metadata });
935
+ const now = new Date().toISOString();
936
+
937
+ const upsert = db.transaction(() => {
938
+ this._deleteMemoryIndex(memoryId);
939
+
940
+ for (const fact of facts) {
941
+ db.prepare(
942
+ `INSERT INTO memory_facts (
943
+ id, memory_id, user_id, agent_id, subject, predicate, object, category,
944
+ confidence, event_time, metadata_json, created_at, updated_at
945
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`
946
+ ).run(
947
+ uuidv4(),
948
+ memoryId,
949
+ userId,
950
+ agentId,
951
+ fact.subject,
952
+ fact.predicate,
953
+ fact.object,
954
+ normalizeMemoryCategory(fact.category),
955
+ Math.max(0, Math.min(1, Number(fact.confidence) || 0.7)),
956
+ fact.eventTime || null,
957
+ JSON.stringify(parseJsonObject(fact.metadata, {})),
958
+ );
959
+ }
960
+
961
+ for (const entity of entities) {
962
+ const existing = db.prepare(
963
+ `SELECT id, aliases_json, mention_count
964
+ FROM memory_entities
965
+ WHERE user_id = ? AND agent_id = ? AND entity_key = ?`
966
+ ).get(userId, agentId, entity.key);
967
+ const aliases = normalizeStringArray(parseJsonArray(existing?.aliases_json), 16, 160);
968
+ if (!aliases.includes(entity.name)) aliases.push(entity.name);
969
+ const entityId = existing?.id || uuidv4();
970
+ db.prepare(
971
+ `INSERT INTO memory_entities (
972
+ id, user_id, agent_id, entity_key, name, kind, aliases_json,
973
+ mention_count, first_seen_at, last_seen_at, metadata_json
974
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 1, datetime('now'), ?, '{}')
975
+ ON CONFLICT(user_id, agent_id, entity_key) DO UPDATE SET
976
+ name = CASE WHEN length(excluded.name) > length(memory_entities.name) THEN excluded.name ELSE memory_entities.name END,
977
+ kind = CASE WHEN memory_entities.kind = 'concept' THEN excluded.kind ELSE memory_entities.kind END,
978
+ aliases_json = excluded.aliases_json,
979
+ mention_count = memory_entities.mention_count + 1,
980
+ last_seen_at = excluded.last_seen_at`
981
+ ).run(
982
+ entityId,
983
+ userId,
984
+ agentId,
985
+ entity.key,
986
+ entity.name,
987
+ entity.kind,
988
+ JSON.stringify(aliases.slice(0, 16)),
989
+ now,
990
+ );
991
+ db.prepare(
992
+ `INSERT OR IGNORE INTO memory_entity_mentions (entity_id, memory_id, user_id, agent_id)
993
+ VALUES (?, ?, ?, ?)`
994
+ ).run(entityId, memoryId, userId, agentId);
995
+ }
996
+
997
+ try {
998
+ db.prepare(
999
+ `INSERT INTO memories_fts (memory_id, user_id, agent_id, content, category, entities)
1000
+ VALUES (?, ?, ?, ?, ?, ?)`
1001
+ ).run(
1002
+ memoryId,
1003
+ userId,
1004
+ agentId || '',
1005
+ String(content || ''),
1006
+ String(category || ''),
1007
+ entities.map((entity) => `${entity.name} ${entity.kind}`).join(' '),
1008
+ );
1009
+ } catch {
1010
+ // FTS is optional.
1011
+ }
1012
+ });
1013
+ upsert();
1014
+
1015
+ return entities;
1016
+ }
1017
+
1018
+ _entitiesForMemoryIds(memoryIds) {
1019
+ const ids = normalizeStringArray(memoryIds, 200, 80);
1020
+ if (!ids.length) return new Map();
1021
+ const placeholders = ids.map(() => '?').join(', ');
1022
+ const rows = db.prepare(
1023
+ `SELECT mem.memory_id, ent.id, ent.entity_key, ent.name, ent.kind, ent.mention_count
1024
+ FROM memory_entity_mentions mem
1025
+ JOIN memory_entities ent ON ent.id = mem.entity_id
1026
+ WHERE mem.memory_id IN (${placeholders})
1027
+ ORDER BY ent.mention_count DESC, ent.name ASC`
1028
+ ).all(...ids);
1029
+ const byMemory = new Map();
1030
+ for (const row of rows) {
1031
+ if (!byMemory.has(row.memory_id)) byMemory.set(row.memory_id, []);
1032
+ const list = byMemory.get(row.memory_id);
1033
+ if (list.length < 8) {
1034
+ list.push({
1035
+ id: row.id,
1036
+ key: row.entity_key,
1037
+ name: row.name,
1038
+ kind: row.kind || 'concept',
1039
+ mentionCount: Number(row.mention_count || 0),
1040
+ });
1041
+ }
1042
+ }
1043
+ return byMemory;
1044
+ }
1045
+
1046
+ _attachEntities(memories) {
1047
+ const byMemory = this._entitiesForMemoryIds(memories.map((memory) => memory.id));
1048
+ return memories.map((memory) => ({
1049
+ ...memory,
1050
+ entities: byMemory.get(memory.id) || [],
1051
+ }));
1052
+ }
1053
+
1054
+ _searchMemoryFts(userId, agentId, query, limit) {
1055
+ const ftsQuery = buildFtsQuery(query);
1056
+ if (!ftsQuery) return [];
1057
+ try {
1058
+ return db.prepare(
1059
+ `SELECT memory_id, bm25(memories_fts) AS rank
1060
+ FROM memories_fts
1061
+ WHERE memories_fts MATCH ? AND user_id = ? AND agent_id = ?
1062
+ ORDER BY rank
1063
+ LIMIT ?`
1064
+ ).all(ftsQuery, userId, agentId || '', Math.max(1, Math.min(Number(limit) || 40, 120)));
1065
+ } catch {
1066
+ return [];
1067
+ }
1068
+ }
1069
+
1070
+ _searchEntityMemoryIds(userId, agentId, query, limit) {
1071
+ const entities = extractEntities(query);
1072
+ const keywords = extractKeywords(query, { maxKeywords: 8 });
1073
+ const keys = [
1074
+ ...entities.map((entity) => entity.key),
1075
+ ...keywords.map(canonicalEntityKey),
1076
+ ].filter(Boolean);
1077
+ if (!keys.length) return [];
1078
+
1079
+ const results = [];
1080
+ const seen = new Set();
1081
+ for (const key of keys) {
1082
+ const rows = db.prepare(
1083
+ `SELECT mem.memory_id, ent.mention_count
1084
+ FROM memory_entities ent
1085
+ JOIN memory_entity_mentions mem ON mem.entity_id = ent.id
1086
+ WHERE ent.user_id = ? AND ent.agent_id = ?
1087
+ AND (ent.entity_key = ? OR ent.entity_key LIKE ?)
1088
+ ORDER BY ent.mention_count DESC, ent.last_seen_at DESC
1089
+ LIMIT ?`
1090
+ ).all(userId, agentId, key, `%${key}%`, Math.max(1, Math.min(Number(limit) || 40, 80)));
1091
+ for (const row of rows) {
1092
+ if (seen.has(row.memory_id)) continue;
1093
+ seen.add(row.memory_id);
1094
+ results.push(row);
1095
+ }
1096
+ }
1097
+ return results.slice(0, Math.max(1, Math.min(Number(limit) || 40, 120)));
1098
+ }
1099
+
742
1100
  /**
743
1101
  * Save a new memory. Deduplicates if an existing memory is very similar.
744
1102
  * Returns the memory id (new or existing).
@@ -756,9 +1114,22 @@ class MemoryManager {
756
1114
  ? Math.max(1, Number(options.staleAfterDays))
757
1115
  : null;
758
1116
  const metadata = parseJsonObject(options.metadata, {});
1117
+ const confidence = Math.max(0, Math.min(1, Number(options.confidence) || 0.7));
1118
+ const memoryHash = stableHash(`${category}:${content}`);
1119
+ const summary = summarizeForPrompt({ content, entities: extractEntities(content) });
759
1120
 
760
1121
  const embedding = await getEmbedding(content, await getActiveProvider(userId, agentId));
761
1122
 
1123
+ const exact = db.prepare(
1124
+ `SELECT id FROM memories
1125
+ WHERE user_id = ? AND agent_id = ? AND archived = 0
1126
+ AND memory_hash = ?
1127
+ AND COALESCE(scope_type, 'agent') = ?
1128
+ AND COALESCE(scope_id, '') = COALESCE(?, '')
1129
+ LIMIT 1`
1130
+ ).get(userId, agentId, memoryHash, scope.scopeType, scope.scopeId);
1131
+ if (exact?.id) return exact.id;
1132
+
762
1133
  // Dedup check: compare against existing non-archived memories in the same scope
763
1134
  const existing = db.prepare(
764
1135
  `SELECT id, content, embedding, metadata_json
@@ -785,22 +1156,31 @@ class MemoryManager {
785
1156
  ...metadata,
786
1157
  };
787
1158
  db.prepare(
788
- `UPDATE memories SET content = ?, importance = MAX(importance, ?), embedding = ?,
1159
+ `UPDATE memories SET content = ?, summary = ?, importance = MAX(importance, ?), confidence = MAX(confidence, ?), embedding = ?,
789
1160
  source_type = COALESCE(?, source_type), source_id = COALESCE(?, source_id),
790
1161
  source_label = COALESCE(?, source_label), stale_after_days = COALESCE(?, stale_after_days),
791
- metadata_json = ?,
1162
+ metadata_json = ?, memory_hash = ?,
792
1163
  updated_at = datetime('now') WHERE id = ?`
793
1164
  ).run(
794
1165
  content,
1166
+ summary,
795
1167
  importance,
1168
+ confidence,
796
1169
  embedding ? serializeEmbedding(embedding) : mem.embedding,
797
1170
  sourceRef.sourceType,
798
1171
  sourceRef.sourceId,
799
1172
  sourceRef.sourceLabel,
800
1173
  staleAfterDays,
801
1174
  JSON.stringify(mergedMetadata),
1175
+ memoryHash,
802
1176
  mem.id,
803
1177
  );
1178
+ this._upsertMemoryIntelligence(userId, agentId, mem.id, {
1179
+ content,
1180
+ category,
1181
+ sourceRef,
1182
+ metadata: mergedMetadata,
1183
+ });
804
1184
  return mem.id;
805
1185
  }
806
1186
  return mem.id; // already covered, skip
@@ -812,9 +1192,9 @@ class MemoryManager {
812
1192
  db.prepare(
813
1193
  `INSERT INTO memories (
814
1194
  id, user_id, agent_id, category, scope_type, scope_id, source_type, source_id, source_label,
815
- stale_after_days, metadata_json, content, importance, embedding
1195
+ stale_after_days, metadata_json, content, summary, importance, confidence, memory_hash, embedding
816
1196
  )
817
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1197
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
818
1198
  ).run(
819
1199
  id,
820
1200
  userId,
@@ -828,10 +1208,20 @@ class MemoryManager {
828
1208
  staleAfterDays,
829
1209
  JSON.stringify(metadata),
830
1210
  content,
1211
+ summary,
831
1212
  importance,
1213
+ confidence,
1214
+ memoryHash,
832
1215
  embedding ? serializeEmbedding(embedding) : null,
833
1216
  );
834
1217
 
1218
+ this._upsertMemoryIntelligence(userId, agentId, id, {
1219
+ content,
1220
+ category,
1221
+ sourceRef,
1222
+ metadata,
1223
+ });
1224
+
835
1225
  return id;
836
1226
  }
837
1227
 
@@ -843,9 +1233,10 @@ class MemoryManager {
843
1233
  if (!query || !query.trim()) return [];
844
1234
  const agentId = this._agentId(userId, options);
845
1235
  const scope = normalizeScope(options.scope, agentId);
1236
+ const limit = Math.max(1, Math.min(Number(topK) || 6, 50));
846
1237
 
847
1238
  const all = db.prepare(
848
- `SELECT id, category, content, importance, embedding, access_count, created_at, updated_at,
1239
+ `SELECT id, category, content, summary, importance, confidence, embedding, access_count, created_at, updated_at,
849
1240
  scope_type, scope_id, source_type, source_id, source_label, stale_after_days, metadata_json
850
1241
  FROM memories
851
1242
  WHERE user_id = ? AND agent_id = ? AND archived = 0
@@ -853,33 +1244,69 @@ class MemoryManager {
853
1244
  (COALESCE(scope_type, 'agent') = ? AND COALESCE(scope_id, '') = COALESCE(?, ''))
854
1245
  OR COALESCE(scope_type, 'agent') = 'shared'
855
1246
  )
856
- ORDER BY updated_at DESC`
1247
+ ORDER BY updated_at DESC
1248
+ LIMIT 800`
857
1249
  ).all(userId, agentId, scope.scopeType, scope.scopeId);
858
1250
 
859
1251
  if (!all.length) return [];
860
1252
 
861
1253
  const queryVec = await getEmbedding(query, await getActiveProvider(userId, agentId));
1254
+ const lexicalHits = this._searchMemoryFts(userId, agentId, query, Math.max(40, limit * 8));
1255
+ const entityHits = this._searchEntityMemoryIds(userId, agentId, query, Math.max(40, limit * 8));
1256
+ const lexicalRanks = new Map(lexicalHits.map((hit, index) => [hit.memory_id, index]));
1257
+ const entityRanks = new Map(entityHits.map((hit, index) => [hit.memory_id, index]));
862
1258
 
863
- const scored = all.map(mem => {
864
- let score = 0;
1259
+ const semanticScored = all.map(mem => {
1260
+ let semanticScore = 0;
865
1261
  if (queryVec && mem.embedding) {
866
1262
  const memVec = deserializeEmbedding(mem.embedding);
867
1263
  if (memVec) {
868
- score = cosineSimilarity(queryVec, memVec);
869
- // Boost by importance (1–10 → up to +50% weight)
870
- score = score * (0.5 + mem.importance / 20);
1264
+ semanticScore = cosineSimilarity(queryVec, memVec);
871
1265
  }
872
1266
  }
873
- const lexicalScore = keywordSimilarity(query, mem.content) * 0.7;
874
- score = Math.max(score, lexicalScore);
875
- score *= computeFreshnessMultiplier(mem);
876
- return { ...mem, score };
1267
+ return { ...mem, semanticScore };
1268
+ }).sort((left, right) => right.semanticScore - left.semanticScore);
1269
+
1270
+ const semanticRanks = new Map();
1271
+ semanticScored.forEach((mem, index) => {
1272
+ if (mem.semanticScore > 0) semanticRanks.set(mem.id, index);
1273
+ });
1274
+
1275
+ const scored = semanticScored.map((mem) => {
1276
+ const lexicalScore = keywordSimilarity(query, `${mem.content} ${mem.summary || ''}`) * 0.75;
1277
+ const ftsScore = lexicalRanks.has(mem.id) ? 0.42 : 0;
1278
+ const entityScore = entityRanks.has(mem.id) ? 0.48 : 0;
1279
+ const baseScore = Math.max(
1280
+ mem.semanticScore * (0.65 + Number(mem.importance || 5) / 25),
1281
+ lexicalScore,
1282
+ ftsScore,
1283
+ entityScore,
1284
+ );
1285
+ const score = scoreMemoryCandidate({
1286
+ semanticRank: semanticRanks.get(mem.id) ?? -1,
1287
+ lexicalRank: lexicalRanks.get(mem.id) ?? -1,
1288
+ entityRank: entityRanks.get(mem.id) ?? -1,
1289
+ baseScore,
1290
+ importance: mem.importance,
1291
+ accessCount: mem.access_count,
1292
+ freshness: computeFreshnessMultiplier(mem),
1293
+ });
1294
+ return {
1295
+ ...mem,
1296
+ score,
1297
+ scoreBreakdown: {
1298
+ semantic: Number(mem.semanticScore || 0),
1299
+ lexical: Number(lexicalScore || 0),
1300
+ fullText: ftsScore,
1301
+ entity: entityScore,
1302
+ },
1303
+ };
877
1304
  });
878
1305
 
879
1306
  const results = scored
880
- .filter(m => m.score > 0.2)
1307
+ .filter(m => m.score > 0.12)
881
1308
  .sort((a, b) => b.score - a.score)
882
- .slice(0, topK);
1309
+ .slice(0, limit);
883
1310
 
884
1311
  // Update access counts
885
1312
  if (results.length) {
@@ -888,9 +1315,10 @@ class MemoryManager {
888
1315
  db.prepare(`UPDATE memories SET access_count = access_count + 1 WHERE id IN (${placeholders})`).run(...ids);
889
1316
  }
890
1317
 
891
- return results.map((row) => ({
1318
+ return this._attachEntities(results).map((row) => ({
892
1319
  ...serializeMemoryRow(row),
893
1320
  score: row.score,
1321
+ scoreBreakdown: row.scoreBreakdown,
894
1322
  }));
895
1323
  }
896
1324
 
@@ -899,7 +1327,7 @@ class MemoryManager {
899
1327
  */
900
1328
  listMemories(userId, { category, limit = 50, offset = 0, includeArchived = false, agentId = null } = {}) {
901
1329
  const scopedAgentId = this._agentId(userId, { agentId });
902
- let sql = `SELECT id, category, content, importance, access_count, archived, created_at, updated_at,
1330
+ let sql = `SELECT id, category, content, summary, importance, confidence, access_count, archived, created_at, updated_at,
903
1331
  scope_type, scope_id, source_type, source_id, source_label, stale_after_days, metadata_json
904
1332
  FROM memories WHERE user_id = ? AND agent_id = ? AND archived = ?`;
905
1333
  const params = [userId, scopedAgentId, includeArchived ? 1 : 0];
@@ -912,7 +1340,7 @@ class MemoryManager {
912
1340
  }
913
1341
  sql += ` ORDER BY importance DESC, updated_at DESC LIMIT ? OFFSET ?`;
914
1342
  params.push(limit, offset);
915
- return db.prepare(sql).all(...params).map(serializeMemoryRow);
1343
+ return this._attachEntities(db.prepare(sql).all(...params)).map(serializeMemoryRow);
916
1344
  }
917
1345
 
918
1346
  /**
@@ -925,17 +1353,30 @@ class MemoryManager {
925
1353
  const newContent = content ?? mem.content;
926
1354
  const newImportance = importance != null ? Math.max(1, Math.min(10, Number(importance))) : mem.importance;
927
1355
  const newCategory = category ? normalizeMemoryCategory(category) : mem.category;
1356
+ const newSummary = summarizeForPrompt({ content: newContent, entities: extractEntities(newContent) });
1357
+ const newHash = stableHash(`${newCategory}:${newContent}`);
928
1358
 
929
1359
  let newEmbed = mem.embedding;
930
1360
  if (content && content !== mem.content) {
931
- const vec = await getEmbedding(newContent, await getActiveProvider(null));
1361
+ const vec = await getEmbedding(newContent, await getActiveProvider(mem.user_id, mem.agent_id));
932
1362
  newEmbed = vec ? serializeEmbedding(vec) : mem.embedding;
933
1363
  }
934
1364
 
935
1365
  db.prepare(
936
- `UPDATE memories SET content = ?, importance = ?, category = ?, embedding = ?,
1366
+ `UPDATE memories SET content = ?, summary = ?, importance = ?, category = ?, memory_hash = ?, embedding = ?,
937
1367
  updated_at = datetime('now') WHERE id = ?`
938
- ).run(newContent, newImportance, newCategory, newEmbed, id);
1368
+ ).run(newContent, newSummary, newImportance, newCategory, newHash, newEmbed, id);
1369
+
1370
+ this._upsertMemoryIntelligence(mem.user_id, mem.agent_id, id, {
1371
+ content: newContent,
1372
+ category: newCategory,
1373
+ sourceRef: normalizeSourceRef({
1374
+ sourceType: mem.source_type,
1375
+ sourceId: mem.source_id,
1376
+ sourceLabel: mem.source_label,
1377
+ }),
1378
+ metadata: parseJsonObject(mem.metadata_json, {}),
1379
+ });
939
1380
 
940
1381
  return serializeMemoryRow(db.prepare(
941
1382
  `SELECT * FROM memories WHERE id = ?`
@@ -957,6 +1398,7 @@ class MemoryManager {
957
1398
  )];
958
1399
  if (!uniqueIds.length) return 0;
959
1400
  const placeholders = uniqueIds.map(() => '?').join(', ');
1401
+ for (const id of uniqueIds) this._deleteMemoryIndex(id);
960
1402
  const result = userId != null
961
1403
  ? db.prepare(`DELETE FROM memories WHERE id IN (${placeholders}) AND user_id = ?`).run(...uniqueIds, userId)
962
1404
  : db.prepare(`DELETE FROM memories WHERE id IN (${placeholders})`).run(...uniqueIds);
@@ -978,6 +1420,11 @@ class MemoryManager {
978
1420
  )];
979
1421
  if (!uniqueIds.length) return 0;
980
1422
  const placeholders = uniqueIds.map(() => '?').join(', ');
1423
+ if (archived) {
1424
+ for (const id of uniqueIds) {
1425
+ try { db.prepare('DELETE FROM memories_fts WHERE memory_id = ?').run(id); } catch {}
1426
+ }
1427
+ }
981
1428
  const result = userId != null
982
1429
  ? db.prepare(
983
1430
  `UPDATE memories SET archived = ? WHERE id IN (${placeholders}) AND user_id = ?`
@@ -1576,7 +2023,10 @@ class MemoryManager {
1576
2023
  if (recalled.length) {
1577
2024
  const memoryLines = recalled.map(m => {
1578
2025
  const badge = m.category !== 'episodic' ? ` [${m.category}]` : '';
1579
- return `- ${m.content}${badge}`;
2026
+ const entities = Array.isArray(m.entities) && m.entities.length
2027
+ ? ` (entities: ${m.entities.slice(0, 4).map((entity) => entity.name).join(', ')})`
2028
+ : '';
2029
+ return `- ${summarizeForPrompt(m)}${badge}${entities}`;
1580
2030
  });
1581
2031
  sections.push(`Relevant memory:\n${memoryLines.join('\n')}`);
1582
2032
  }