neoagent 2.4.1-beta.8 → 2.4.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.
Files changed (248) hide show
  1. package/.env.example +33 -3
  2. package/LICENSE +111 -56
  3. package/README.md +8 -3
  4. package/docs/configuration.md +8 -0
  5. package/docs/getting-started.md +9 -3
  6. package/docs/index.md +4 -0
  7. package/extensions/chrome-browser/background.mjs +45 -7
  8. package/extensions/chrome-browser/icons/icon128.png +0 -0
  9. package/extensions/chrome-browser/icons/icon16.png +0 -0
  10. package/extensions/chrome-browser/icons/icon48.png +0 -0
  11. package/extensions/chrome-browser/icons/logo.svg +39 -8
  12. package/extensions/chrome-browser/manifest.json +3 -2
  13. package/extensions/chrome-browser/popup.html +5 -1
  14. package/extensions/chrome-browser/popup.js +15 -2
  15. package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
  16. package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  17. package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  18. package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  19. package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  20. package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  21. package/flutter_app/assets/branding/app_icon_1024.png +0 -0
  22. package/flutter_app/assets/branding/app_icon_128.png +0 -0
  23. package/flutter_app/assets/branding/app_icon_192.png +0 -0
  24. package/flutter_app/assets/branding/app_icon_256.png +0 -0
  25. package/flutter_app/assets/branding/app_icon_32.png +0 -0
  26. package/flutter_app/assets/branding/app_icon_512.png +0 -0
  27. package/flutter_app/assets/branding/app_icon_64.png +0 -0
  28. package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
  29. package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
  30. package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
  31. package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
  32. package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
  33. package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
  34. package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
  35. package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
  36. package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
  37. package/flutter_app/assets/branding/tray_icon_template.png +0 -0
  38. package/flutter_app/lib/features/location/location_service.dart +3 -0
  39. package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
  40. package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
  41. package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
  42. package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
  43. package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
  44. package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
  45. package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
  46. package/flutter_app/lib/main.dart +3 -0
  47. package/flutter_app/lib/main_account_settings.dart +10 -34
  48. package/flutter_app/lib/main_admin.dart +14 -1
  49. package/flutter_app/lib/main_app_shell.dart +377 -340
  50. package/flutter_app/lib/main_chat.dart +707 -227
  51. package/flutter_app/lib/main_controller.dart +338 -46
  52. package/flutter_app/lib/main_devices.dart +851 -122
  53. package/flutter_app/lib/main_integrations.dart +255 -6
  54. package/flutter_app/lib/main_launcher.dart +1 -1
  55. package/flutter_app/lib/main_model_picker.dart +685 -0
  56. package/flutter_app/lib/main_models.dart +212 -0
  57. package/flutter_app/lib/main_navigation.dart +1 -9
  58. package/flutter_app/lib/main_operations.dart +1417 -614
  59. package/flutter_app/lib/main_settings.dart +764 -887
  60. package/flutter_app/lib/main_shared.dart +971 -443
  61. package/flutter_app/lib/main_spacing.dart +4 -4
  62. package/flutter_app/lib/main_theme.dart +22 -14
  63. package/flutter_app/lib/main_unified.dart +5 -72
  64. package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
  65. package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
  66. package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
  67. package/flutter_app/lib/src/backend_client.dart +79 -0
  68. package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
  69. package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
  70. package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
  71. package/flutter_app/lib/src/security/password_strength.dart +84 -0
  72. package/flutter_app/lib/src/stream_renderer.dart +205 -35
  73. package/flutter_app/lib/src/theme/palette.dart +76 -34
  74. package/flutter_app/linux/runner/resources/app_icon.png +0 -0
  75. package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
  76. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
  77. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
  78. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
  79. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
  80. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
  81. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
  82. package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
  83. package/flutter_app/pubspec.lock +2 -2
  84. package/flutter_app/pubspec.yaml +7 -1
  85. package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
  86. package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
  87. package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
  88. package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
  89. package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
  90. package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
  91. package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
  92. package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
  93. package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
  94. package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
  95. package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
  96. package/flutter_app/tool/generate_desktop_branding.py +154 -152
  97. package/flutter_app/web/favicon.png +0 -0
  98. package/flutter_app/web/favicon.svg +40 -9
  99. package/flutter_app/web/favicon_light.svg +43 -0
  100. package/flutter_app/web/icons/Icon-192-light.png +0 -0
  101. package/flutter_app/web/icons/Icon-192.png +0 -0
  102. package/flutter_app/web/icons/Icon-512-light.png +0 -0
  103. package/flutter_app/web/icons/Icon-512.png +0 -0
  104. package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
  105. package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
  106. package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
  107. package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
  108. package/flutter_app/windows/runner/main.cpp +7 -1
  109. package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
  110. package/lib/manager.js +445 -83
  111. package/package.json +17 -3
  112. package/runtime/paths.js +3 -1
  113. package/server/admin/access.js +198 -0
  114. package/server/admin/admin.css +268 -0
  115. package/server/admin/admin.js +348 -0
  116. package/server/admin/analytics.js +128 -0
  117. package/server/admin/index.html +1015 -0
  118. package/server/admin/login.html +290 -0
  119. package/server/admin/logo.svg +43 -0
  120. package/server/admin/sql.js +134 -0
  121. package/server/admin/users.js +147 -0
  122. package/server/config/origins.js +3 -1
  123. package/server/db/database.js +92 -0
  124. package/server/db/ftsQuery.js +27 -0
  125. package/server/http/routes.js +1 -0
  126. package/server/http/static.js +23 -6
  127. package/server/index.js +1 -1
  128. package/server/middleware/adminAuth.js +48 -0
  129. package/server/middleware/auth.js +1 -40
  130. package/server/public/.last_build_id +1 -1
  131. package/server/public/app_icon.png +0 -0
  132. package/server/public/assets/AssetManifest.bin +1 -1
  133. package/server/public/assets/AssetManifest.bin.json +1 -1
  134. package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
  135. package/server/public/assets/assets/branding/app_icon_256.png +0 -0
  136. package/server/public/assets/assets/branding/app_icon_512.png +0 -0
  137. package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
  138. package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
  139. package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
  140. package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
  141. package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
  142. package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
  143. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  144. package/server/public/assets/web/icons/Icon-192.png +0 -0
  145. package/server/public/favicon.png +0 -0
  146. package/server/public/favicon.svg +40 -9
  147. package/server/public/favicon_light.svg +43 -0
  148. package/server/public/flutter_bootstrap.js +2 -2
  149. package/server/public/icons/Icon-192-light.png +0 -0
  150. package/server/public/icons/Icon-192.png +0 -0
  151. package/server/public/icons/Icon-512-light.png +0 -0
  152. package/server/public/icons/Icon-512.png +0 -0
  153. package/server/public/icons/Icon-maskable-192-light.png +0 -0
  154. package/server/public/icons/Icon-maskable-192.png +0 -0
  155. package/server/public/icons/Icon-maskable-512-light.png +0 -0
  156. package/server/public/icons/Icon-maskable-512.png +0 -0
  157. package/server/public/main.dart.js +88944 -85720
  158. package/server/routes/admin.js +632 -0
  159. package/server/routes/agent_profiles.js +3 -0
  160. package/server/routes/agents.js +1 -1
  161. package/server/routes/auth.js +3 -1
  162. package/server/routes/browser.js +14 -0
  163. package/server/routes/browser_extension.js +21 -4
  164. package/server/routes/desktop.js +10 -0
  165. package/server/routes/mcp.js +29 -13
  166. package/server/routes/memory.js +23 -1
  167. package/server/routes/messaging.js +2 -0
  168. package/server/routes/screenHistory.js +14 -8
  169. package/server/routes/settings.js +14 -2
  170. package/server/routes/stream.js +12 -3
  171. package/server/routes/triggers.js +4 -4
  172. package/server/routes/voice_assistant.js +36 -1
  173. package/server/routes/workspace.js +86 -0
  174. package/server/services/account/admin_two_factor.js +132 -0
  175. package/server/services/account/password_policy.js +6 -1
  176. package/server/services/ai/completion.js +44 -0
  177. package/server/services/ai/engine.js +321 -500
  178. package/server/services/ai/imageAnalysis.js +9 -5
  179. package/server/services/ai/logFormat.js +46 -0
  180. package/server/services/ai/loopPolicy.js +11 -0
  181. package/server/services/ai/messagingFallback.js +228 -0
  182. package/server/services/ai/models.js +194 -55
  183. package/server/services/ai/providerRetry.js +169 -0
  184. package/server/services/ai/providers/anthropic.js +15 -4
  185. package/server/services/ai/providers/google.js +38 -11
  186. package/server/services/ai/providers/grok.js +19 -68
  187. package/server/services/ai/providers/grokOauth.js +141 -0
  188. package/server/services/ai/providers/nvidia.js +154 -0
  189. package/server/services/ai/providers/ollama.js +76 -39
  190. package/server/services/ai/providers/openai.js +20 -31
  191. package/server/services/ai/providers/openaiCodex.js +6 -2
  192. package/server/services/ai/providers/openaiCompatible.js +70 -0
  193. package/server/services/ai/providers/openrouter.js +162 -0
  194. package/server/services/ai/settings.js +30 -0
  195. package/server/services/ai/systemPrompt.js +51 -29
  196. package/server/services/ai/taskAnalysis.js +60 -1
  197. package/server/services/ai/toolEvidence.js +207 -0
  198. package/server/services/ai/toolSelector.js +8 -1
  199. package/server/services/ai/tools.js +80 -15
  200. package/server/services/android/controller.js +36 -1
  201. package/server/services/browser/controller.js +18 -0
  202. package/server/services/browser/extension/manifest.js +33 -0
  203. package/server/services/browser/extension/provider.js +12 -6
  204. package/server/services/browser/extension/registry.js +229 -18
  205. package/server/services/desktop/protocol.js +1 -0
  206. package/server/services/desktop/provider.js +4 -0
  207. package/server/services/desktop/registry.js +104 -1
  208. package/server/services/desktop/screenRecorder.js +208 -98
  209. package/server/services/desktop/screen_recorder_support.js +46 -0
  210. package/server/services/integrations/google/provider.js +13 -0
  211. package/server/services/integrations/home_assistant/constants.js +88 -0
  212. package/server/services/integrations/home_assistant/network.js +207 -0
  213. package/server/services/integrations/home_assistant/provider.js +249 -0
  214. package/server/services/integrations/home_assistant/snapshot.js +98 -0
  215. package/server/services/integrations/home_assistant/tools.js +101 -0
  216. package/server/services/integrations/manager.js +5 -0
  217. package/server/services/integrations/registry.js +2 -0
  218. package/server/services/integrations/trello/provider.js +8 -2
  219. package/server/services/manager.js +51 -47
  220. package/server/services/mcp/client.js +208 -268
  221. package/server/services/mcp/client_support.js +172 -0
  222. package/server/services/mcp/recovery.js +116 -0
  223. package/server/services/mcp/tool_operations.js +123 -0
  224. package/server/services/memory/ingestion.js +185 -370
  225. package/server/services/memory/ingestion_coverage.js +129 -0
  226. package/server/services/memory/ingestion_documents.js +123 -0
  227. package/server/services/memory/ingestion_support.js +171 -0
  228. package/server/services/memory/intelligence.js +181 -0
  229. package/server/services/memory/manager.js +476 -32
  230. package/server/services/messaging/automation.js +71 -134
  231. package/server/services/messaging/formatting_guides.js +26 -1
  232. package/server/services/messaging/inbound_queue.js +111 -0
  233. package/server/services/messaging/manager.js +25 -8
  234. package/server/services/messaging/typing_keepalive.js +110 -0
  235. package/server/services/runtime/manager.js +7 -3
  236. package/server/services/runtime/settings.js +17 -0
  237. package/server/services/streaming/android-stream.js +296 -32
  238. package/server/services/streaming/stream-hub.js +1 -1
  239. package/server/services/tasks/integration_runtime.js +4 -1
  240. package/server/services/tasks/runtime.js +415 -56
  241. package/server/services/tasks/task_repository.js +48 -6
  242. package/server/services/websocket.js +8 -2
  243. package/server/services/widgets/service.js +17 -1
  244. package/server/services/workspace/manager.js +116 -0
  245. package/server/utils/logger.js +44 -6
  246. package/server/utils/security.js +3 -0
  247. package/server/utils/version.js +29 -19
  248. package/server/services/memory/openhuman_uplift.test.js +0 -98
@@ -10,8 +10,18 @@ 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');
24
+ const { buildFtsQuery } = require('../../db/ftsQuery');
15
25
  const {
16
26
  decryptLocalValue,
17
27
  encryptLocalValue,
@@ -139,11 +149,16 @@ function computeFreshnessMultiplier(row) {
139
149
 
140
150
  function serializeMemoryRow(row) {
141
151
  const metadata = parseJsonObject(row?.metadata_json, {});
152
+ const entities = Array.isArray(row?.entities)
153
+ ? row.entities
154
+ : parseJsonArray(row?.entities_json, []);
142
155
  return {
143
156
  id: row.id,
144
157
  category: normalizeMemoryCategory(row.category),
145
158
  content: row.content,
159
+ summary: row.summary || '',
146
160
  importance: Number(row.importance || 0),
161
+ confidence: row.confidence == null ? 0.7 : Number(row.confidence),
147
162
  access_count: Number(row.access_count || 0),
148
163
  archived: Number(row.archived || 0),
149
164
  created_at: row.created_at,
@@ -159,6 +174,7 @@ function serializeMemoryRow(row) {
159
174
  },
160
175
  staleAfterDays: row.stale_after_days == null ? null : Number(row.stale_after_days),
161
176
  metadata,
177
+ entities,
162
178
  };
163
179
  }
164
180
 
@@ -172,13 +188,6 @@ function parseStringSetting(value) {
172
188
  }
173
189
  }
174
190
 
175
- function buildFtsQuery(query) {
176
- const tokens = String(query || '')
177
- .match(/[\p{L}\p{N}_-]{2,}/gu) || [];
178
- if (!tokens.length) return null;
179
- return tokens.map((token) => `${token.replace(/"/g, '')}*`).join(' AND ');
180
- }
181
-
182
191
  function stripHighlight(text) {
183
192
  return String(text || '').replace(/<\/?mark>/g, '');
184
193
  }
@@ -225,6 +234,7 @@ function scoreSchedulerRunMatch(queryTokens, title, finalResponse) {
225
234
  class MemoryManager {
226
235
  constructor() {
227
236
  this._ensureDirs();
237
+ this._backfillMemoryIntelligence();
228
238
  }
229
239
 
230
240
  _ensureDirs() {
@@ -266,6 +276,50 @@ class MemoryManager {
266
276
  return resolveAgentId(userId, options?.agentId || options?.agent_id || null);
267
277
  }
268
278
 
279
+ _backfillMemoryIntelligence(limit = 1000) {
280
+ try {
281
+ const rows = db.prepare(
282
+ `SELECT m.*
283
+ FROM memories m
284
+ LEFT JOIN memory_facts f ON f.memory_id = m.id
285
+ WHERE m.archived = 0 AND f.id IS NULL
286
+ ORDER BY m.updated_at DESC
287
+ LIMIT ?`
288
+ ).all(Math.max(1, Math.min(Number(limit) || 1000, 5000)));
289
+ if (!rows.length) return;
290
+
291
+ const backfill = db.transaction(() => {
292
+ for (const row of rows) {
293
+ const category = normalizeMemoryCategory(row.category);
294
+ const content = String(row.content || '').trim();
295
+ if (!content) continue;
296
+ const summary = row.summary || summarizeForPrompt({ content, entities: extractEntities(content) });
297
+ const memoryHash = row.memory_hash || stableHash(`${category}:${content}`);
298
+ db.prepare(
299
+ `UPDATE memories
300
+ SET summary = COALESCE(summary, ?),
301
+ confidence = COALESCE(confidence, 0.7),
302
+ memory_hash = COALESCE(memory_hash, ?)
303
+ WHERE id = ?`
304
+ ).run(summary, memoryHash, row.id);
305
+ this._upsertMemoryIntelligence(row.user_id, row.agent_id, row.id, {
306
+ content,
307
+ category,
308
+ sourceRef: normalizeSourceRef({
309
+ sourceType: row.source_type,
310
+ sourceId: row.source_id,
311
+ sourceLabel: row.source_label,
312
+ }),
313
+ metadata: parseJsonObject(row.metadata_json, {}),
314
+ });
315
+ }
316
+ });
317
+ backfill();
318
+ } catch (err) {
319
+ console.error('[Memory] Backfill intelligence failed:', err.message);
320
+ }
321
+ }
322
+
269
323
  getAssistantBehaviorNotes(userId, options = {}) {
270
324
  if (userId == null) return '';
271
325
  const agentId = this._agentId(userId, options);
@@ -540,10 +594,74 @@ class MemoryManager {
540
594
  }));
541
595
  }
542
596
 
597
+ getMemoryStats(userId, { agentId = null } = {}) {
598
+ const scopedAgentId = this._agentId(userId, { agentId });
599
+ const row = db.prepare(
600
+ `SELECT
601
+ COUNT(*) AS total,
602
+ SUM(CASE WHEN archived = 0 THEN 1 ELSE 0 END) AS active,
603
+ SUM(CASE WHEN archived = 1 THEN 1 ELSE 0 END) AS archived,
604
+ AVG(COALESCE(importance, 0)) AS avg_importance,
605
+ AVG(COALESCE(confidence, 0.7)) AS avg_confidence
606
+ FROM memories
607
+ WHERE user_id = ? AND agent_id = ?`
608
+ ).get(userId, scopedAgentId) || {};
609
+ const entityCount = db.prepare(
610
+ `SELECT COUNT(*) AS count FROM memory_entities WHERE user_id = ? AND agent_id = ?`
611
+ ).get(userId, scopedAgentId)?.count || 0;
612
+ const factCount = db.prepare(
613
+ `SELECT COUNT(*) AS count FROM memory_facts WHERE user_id = ? AND agent_id = ?`
614
+ ).get(userId, scopedAgentId)?.count || 0;
615
+ const viewCount = db.prepare(
616
+ `SELECT COUNT(*) AS count FROM materialized_knowledge_views WHERE user_id = ? AND agent_id = ?`
617
+ ).get(userId, scopedAgentId)?.count || 0;
618
+ const documentCount = db.prepare(
619
+ `SELECT COUNT(*) AS count FROM memory_ingestion_documents WHERE user_id = ? AND agent_id = ?`
620
+ ).get(userId, scopedAgentId)?.count || 0;
621
+ return {
622
+ total: Number(row.total || 0),
623
+ active: Number(row.active || 0),
624
+ archived: Number(row.archived || 0),
625
+ facts: Number(factCount || 0),
626
+ entities: Number(entityCount || 0),
627
+ knowledgeViews: Number(viewCount || 0),
628
+ ingestionDocuments: Number(documentCount || 0),
629
+ averageImportance: Number(row.avg_importance || 0),
630
+ averageConfidence: Number(row.avg_confidence || 0),
631
+ };
632
+ }
633
+
634
+ listEntities(userId, { agentId = null, limit = 24, query = null } = {}) {
635
+ const scopedAgentId = this._agentId(userId, { agentId });
636
+ let sql = `SELECT * FROM memory_entities WHERE user_id = ? AND agent_id = ?`;
637
+ const params = [userId, scopedAgentId];
638
+ const normalizedQuery = String(query || '').trim();
639
+ if (normalizedQuery) {
640
+ sql += ` AND (entity_key LIKE ? OR name LIKE ?)`;
641
+ const like = `%${canonicalEntityKey(normalizedQuery)}%`;
642
+ params.push(like, `%${normalizedQuery}%`);
643
+ }
644
+ sql += ` ORDER BY mention_count DESC, last_seen_at DESC LIMIT ?`;
645
+ params.push(Math.max(1, Math.min(Number(limit) || 24, 100)));
646
+ return db.prepare(sql).all(...params).map((row) => ({
647
+ id: row.id,
648
+ key: row.entity_key,
649
+ name: row.name,
650
+ kind: row.kind || 'concept',
651
+ aliases: normalizeStringArray(parseJsonArray(row.aliases_json), 16, 160),
652
+ summary: row.summary || '',
653
+ mentionCount: Number(row.mention_count || 0),
654
+ firstSeenAt: row.first_seen_at || null,
655
+ lastSeenAt: row.last_seen_at || null,
656
+ metadata: parseJsonObject(row.metadata_json, {}),
657
+ }));
658
+ }
659
+
543
660
  materializeKnowledgeViews(userId, { agentId = null } = {}) {
544
661
  const scopedAgentId = this._agentId(userId, { agentId });
545
662
  const memories = this.listMemories(userId, { limit: 200, agentId: scopedAgentId });
546
663
  const documents = this.listIngestionDocuments(userId, { limit: 200, agentId: scopedAgentId });
664
+ const entities = this.listEntities(userId, { limit: 48, agentId: scopedAgentId });
547
665
  const views = [];
548
666
 
549
667
  const topicGroups = new Map();
@@ -553,7 +671,7 @@ class MemoryManager {
553
671
  topicGroups.get(key).push(memory);
554
672
  }
555
673
  for (const [topic, items] of topicGroups.entries()) {
556
- const summary = items.slice(0, 4).map((item) => `- ${item.content}`).join('\n');
674
+ const summary = items.slice(0, 6).map((item) => `- ${summarizeForPrompt(item)}`).join('\n');
557
675
  views.push({
558
676
  viewType: 'topic',
559
677
  subjectKey: topic,
@@ -569,6 +687,57 @@ class MemoryManager {
569
687
  });
570
688
  }
571
689
 
690
+ for (const entity of entities.slice(0, 24)) {
691
+ const rows = db.prepare(
692
+ `SELECT m.id, m.content, m.category, m.importance, m.summary, m.created_at, m.updated_at
693
+ FROM memory_entity_mentions mem
694
+ JOIN memories m ON m.id = mem.memory_id
695
+ WHERE mem.entity_id = ? AND m.archived = 0
696
+ ORDER BY m.importance DESC, m.updated_at DESC
697
+ LIMIT 8`
698
+ ).all(entity.id);
699
+ if (!rows.length) continue;
700
+ const lines = rows.map((item) => `- [${normalizeMemoryCategory(item.category)}] ${item.summary || item.content}`);
701
+ views.push({
702
+ viewType: 'entity',
703
+ subjectKey: entity.key,
704
+ title: entity.name,
705
+ summary: lines.join('\n').slice(0, 520),
706
+ markdownText: `# ${entity.name}\n\nKind: ${entity.kind}\nMentions: ${entity.mentionCount}\n\n${lines.join('\n')}`,
707
+ sourceMemoryIds: rows.map((item) => item.id),
708
+ sourceDocumentIds: [],
709
+ metadata: {
710
+ entityId: entity.id,
711
+ entityKey: entity.key,
712
+ kind: entity.kind,
713
+ mentionCount: entity.mentionCount,
714
+ },
715
+ });
716
+ }
717
+
718
+ const factRows = db.prepare(
719
+ `SELECT id, memory_id, subject, predicate, object, category, confidence, updated_at
720
+ FROM memory_facts
721
+ WHERE user_id = ? AND agent_id = ?
722
+ ORDER BY updated_at DESC
723
+ LIMIT 20`
724
+ ).all(userId, scopedAgentId);
725
+ if (factRows.length > 0) {
726
+ const lines = factRows.slice(0, 10).map((fact) => `- ${fact.subject}: ${fact.object}`);
727
+ views.push({
728
+ viewType: 'facts',
729
+ subjectKey: 'recent',
730
+ title: 'Recent extracted facts',
731
+ summary: lines.join('\n').slice(0, 520),
732
+ markdownText: `# Recent extracted facts\n\n${lines.join('\n')}`,
733
+ sourceMemoryIds: [...new Set(factRows.map((fact) => fact.memory_id))],
734
+ sourceDocumentIds: [],
735
+ metadata: {
736
+ itemCount: factRows.length,
737
+ },
738
+ });
739
+ }
740
+
572
741
  const accountGroups = new Map();
573
742
  for (const doc of documents) {
574
743
  const accountKey = `${doc.providerKey || 'local'}:${doc.sourceAccount || 'default'}`;
@@ -739,6 +908,189 @@ class MemoryManager {
739
908
  // Semantic Memories (SQLite + embeddings)
740
909
  // ─────────────────────────────────────────────────────────────────────────
741
910
 
911
+ _deleteMemoryIndex(memoryId) {
912
+ try {
913
+ db.prepare('DELETE FROM memories_fts WHERE memory_id = ?').run(memoryId);
914
+ } catch {
915
+ // FTS is optional.
916
+ }
917
+ db.prepare('DELETE FROM memory_entity_mentions WHERE memory_id = ?').run(memoryId);
918
+ db.prepare('DELETE FROM memory_facts WHERE memory_id = ?').run(memoryId);
919
+ }
920
+
921
+ _upsertMemoryIntelligence(userId, agentId, memoryId, {
922
+ content,
923
+ category,
924
+ sourceRef,
925
+ metadata,
926
+ } = {}) {
927
+ const entities = extractEntities(content);
928
+ const facts = buildFacts({ content, category, sourceRef, metadata });
929
+ const now = new Date().toISOString();
930
+
931
+ const upsert = db.transaction(() => {
932
+ this._deleteMemoryIndex(memoryId);
933
+
934
+ for (const fact of facts) {
935
+ db.prepare(
936
+ `INSERT INTO memory_facts (
937
+ id, memory_id, user_id, agent_id, subject, predicate, object, category,
938
+ confidence, event_time, metadata_json, created_at, updated_at
939
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`
940
+ ).run(
941
+ uuidv4(),
942
+ memoryId,
943
+ userId,
944
+ agentId,
945
+ fact.subject,
946
+ fact.predicate,
947
+ fact.object,
948
+ normalizeMemoryCategory(fact.category),
949
+ Math.max(0, Math.min(1, Number(fact.confidence) || 0.7)),
950
+ fact.eventTime || null,
951
+ JSON.stringify(parseJsonObject(fact.metadata, {})),
952
+ );
953
+ }
954
+
955
+ for (const entity of entities) {
956
+ const existing = db.prepare(
957
+ `SELECT id, aliases_json, mention_count
958
+ FROM memory_entities
959
+ WHERE user_id = ? AND agent_id = ? AND entity_key = ?`
960
+ ).get(userId, agentId, entity.key);
961
+ const aliases = normalizeStringArray(parseJsonArray(existing?.aliases_json), 16, 160);
962
+ if (!aliases.includes(entity.name)) aliases.push(entity.name);
963
+ const entityId = existing?.id || uuidv4();
964
+ db.prepare(
965
+ `INSERT INTO memory_entities (
966
+ id, user_id, agent_id, entity_key, name, kind, aliases_json,
967
+ mention_count, first_seen_at, last_seen_at, metadata_json
968
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 1, datetime('now'), ?, '{}')
969
+ ON CONFLICT(user_id, agent_id, entity_key) DO UPDATE SET
970
+ name = CASE WHEN length(excluded.name) > length(memory_entities.name) THEN excluded.name ELSE memory_entities.name END,
971
+ kind = CASE WHEN memory_entities.kind = 'concept' THEN excluded.kind ELSE memory_entities.kind END,
972
+ aliases_json = excluded.aliases_json,
973
+ mention_count = memory_entities.mention_count + 1,
974
+ last_seen_at = excluded.last_seen_at`
975
+ ).run(
976
+ entityId,
977
+ userId,
978
+ agentId,
979
+ entity.key,
980
+ entity.name,
981
+ entity.kind,
982
+ JSON.stringify(aliases.slice(0, 16)),
983
+ now,
984
+ );
985
+ db.prepare(
986
+ `INSERT OR IGNORE INTO memory_entity_mentions (entity_id, memory_id, user_id, agent_id)
987
+ VALUES (?, ?, ?, ?)`
988
+ ).run(entityId, memoryId, userId, agentId);
989
+ }
990
+
991
+ try {
992
+ db.prepare(
993
+ `INSERT INTO memories_fts (memory_id, user_id, agent_id, content, category, entities)
994
+ VALUES (?, ?, ?, ?, ?, ?)`
995
+ ).run(
996
+ memoryId,
997
+ userId,
998
+ agentId || '',
999
+ String(content || ''),
1000
+ String(category || ''),
1001
+ entities.map((entity) => `${entity.name} ${entity.kind}`).join(' '),
1002
+ );
1003
+ } catch {
1004
+ // FTS is optional.
1005
+ }
1006
+ });
1007
+ upsert();
1008
+
1009
+ return entities;
1010
+ }
1011
+
1012
+ _entitiesForMemoryIds(memoryIds) {
1013
+ const ids = normalizeStringArray(memoryIds, 200, 80);
1014
+ if (!ids.length) return new Map();
1015
+ const placeholders = ids.map(() => '?').join(', ');
1016
+ const rows = db.prepare(
1017
+ `SELECT mem.memory_id, ent.id, ent.entity_key, ent.name, ent.kind, ent.mention_count
1018
+ FROM memory_entity_mentions mem
1019
+ JOIN memory_entities ent ON ent.id = mem.entity_id
1020
+ WHERE mem.memory_id IN (${placeholders})
1021
+ ORDER BY ent.mention_count DESC, ent.name ASC`
1022
+ ).all(...ids);
1023
+ const byMemory = new Map();
1024
+ for (const row of rows) {
1025
+ if (!byMemory.has(row.memory_id)) byMemory.set(row.memory_id, []);
1026
+ const list = byMemory.get(row.memory_id);
1027
+ if (list.length < 8) {
1028
+ list.push({
1029
+ id: row.id,
1030
+ key: row.entity_key,
1031
+ name: row.name,
1032
+ kind: row.kind || 'concept',
1033
+ mentionCount: Number(row.mention_count || 0),
1034
+ });
1035
+ }
1036
+ }
1037
+ return byMemory;
1038
+ }
1039
+
1040
+ _attachEntities(memories) {
1041
+ const byMemory = this._entitiesForMemoryIds(memories.map((memory) => memory.id));
1042
+ return memories.map((memory) => ({
1043
+ ...memory,
1044
+ entities: byMemory.get(memory.id) || [],
1045
+ }));
1046
+ }
1047
+
1048
+ _searchMemoryFts(userId, agentId, query, limit) {
1049
+ const ftsQuery = buildFtsQuery(query);
1050
+ if (!ftsQuery) return [];
1051
+ try {
1052
+ return db.prepare(
1053
+ `SELECT memory_id, bm25(memories_fts) AS rank
1054
+ FROM memories_fts
1055
+ WHERE memories_fts MATCH ? AND user_id = ? AND agent_id = ?
1056
+ ORDER BY rank
1057
+ LIMIT ?`
1058
+ ).all(ftsQuery, userId, agentId || '', Math.max(1, Math.min(Number(limit) || 40, 120)));
1059
+ } catch {
1060
+ return [];
1061
+ }
1062
+ }
1063
+
1064
+ _searchEntityMemoryIds(userId, agentId, query, limit) {
1065
+ const entities = extractEntities(query);
1066
+ const keywords = extractKeywords(query, { maxKeywords: 8 });
1067
+ const keys = [
1068
+ ...entities.map((entity) => entity.key),
1069
+ ...keywords.map(canonicalEntityKey),
1070
+ ].filter(Boolean);
1071
+ if (!keys.length) return [];
1072
+
1073
+ const results = [];
1074
+ const seen = new Set();
1075
+ for (const key of keys) {
1076
+ const rows = db.prepare(
1077
+ `SELECT mem.memory_id, ent.mention_count
1078
+ FROM memory_entities ent
1079
+ JOIN memory_entity_mentions mem ON mem.entity_id = ent.id
1080
+ WHERE ent.user_id = ? AND ent.agent_id = ?
1081
+ AND (ent.entity_key = ? OR ent.entity_key LIKE ?)
1082
+ ORDER BY ent.mention_count DESC, ent.last_seen_at DESC
1083
+ LIMIT ?`
1084
+ ).all(userId, agentId, key, `%${key}%`, Math.max(1, Math.min(Number(limit) || 40, 80)));
1085
+ for (const row of rows) {
1086
+ if (seen.has(row.memory_id)) continue;
1087
+ seen.add(row.memory_id);
1088
+ results.push(row);
1089
+ }
1090
+ }
1091
+ return results.slice(0, Math.max(1, Math.min(Number(limit) || 40, 120)));
1092
+ }
1093
+
742
1094
  /**
743
1095
  * Save a new memory. Deduplicates if an existing memory is very similar.
744
1096
  * Returns the memory id (new or existing).
@@ -756,9 +1108,22 @@ class MemoryManager {
756
1108
  ? Math.max(1, Number(options.staleAfterDays))
757
1109
  : null;
758
1110
  const metadata = parseJsonObject(options.metadata, {});
1111
+ const confidence = Math.max(0, Math.min(1, Number(options.confidence) || 0.7));
1112
+ const memoryHash = stableHash(`${category}:${content}`);
1113
+ const summary = summarizeForPrompt({ content, entities: extractEntities(content) });
759
1114
 
760
1115
  const embedding = await getEmbedding(content, await getActiveProvider(userId, agentId));
761
1116
 
1117
+ const exact = db.prepare(
1118
+ `SELECT id FROM memories
1119
+ WHERE user_id = ? AND agent_id = ? AND archived = 0
1120
+ AND memory_hash = ?
1121
+ AND COALESCE(scope_type, 'agent') = ?
1122
+ AND COALESCE(scope_id, '') = COALESCE(?, '')
1123
+ LIMIT 1`
1124
+ ).get(userId, agentId, memoryHash, scope.scopeType, scope.scopeId);
1125
+ if (exact?.id) return exact.id;
1126
+
762
1127
  // Dedup check: compare against existing non-archived memories in the same scope
763
1128
  const existing = db.prepare(
764
1129
  `SELECT id, content, embedding, metadata_json
@@ -785,22 +1150,31 @@ class MemoryManager {
785
1150
  ...metadata,
786
1151
  };
787
1152
  db.prepare(
788
- `UPDATE memories SET content = ?, importance = MAX(importance, ?), embedding = ?,
1153
+ `UPDATE memories SET content = ?, summary = ?, importance = MAX(importance, ?), confidence = MAX(confidence, ?), embedding = ?,
789
1154
  source_type = COALESCE(?, source_type), source_id = COALESCE(?, source_id),
790
1155
  source_label = COALESCE(?, source_label), stale_after_days = COALESCE(?, stale_after_days),
791
- metadata_json = ?,
1156
+ metadata_json = ?, memory_hash = ?,
792
1157
  updated_at = datetime('now') WHERE id = ?`
793
1158
  ).run(
794
1159
  content,
1160
+ summary,
795
1161
  importance,
1162
+ confidence,
796
1163
  embedding ? serializeEmbedding(embedding) : mem.embedding,
797
1164
  sourceRef.sourceType,
798
1165
  sourceRef.sourceId,
799
1166
  sourceRef.sourceLabel,
800
1167
  staleAfterDays,
801
1168
  JSON.stringify(mergedMetadata),
1169
+ memoryHash,
802
1170
  mem.id,
803
1171
  );
1172
+ this._upsertMemoryIntelligence(userId, agentId, mem.id, {
1173
+ content,
1174
+ category,
1175
+ sourceRef,
1176
+ metadata: mergedMetadata,
1177
+ });
804
1178
  return mem.id;
805
1179
  }
806
1180
  return mem.id; // already covered, skip
@@ -812,9 +1186,9 @@ class MemoryManager {
812
1186
  db.prepare(
813
1187
  `INSERT INTO memories (
814
1188
  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
1189
+ stale_after_days, metadata_json, content, summary, importance, confidence, memory_hash, embedding
816
1190
  )
817
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
1191
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
818
1192
  ).run(
819
1193
  id,
820
1194
  userId,
@@ -828,10 +1202,20 @@ class MemoryManager {
828
1202
  staleAfterDays,
829
1203
  JSON.stringify(metadata),
830
1204
  content,
1205
+ summary,
831
1206
  importance,
1207
+ confidence,
1208
+ memoryHash,
832
1209
  embedding ? serializeEmbedding(embedding) : null,
833
1210
  );
834
1211
 
1212
+ this._upsertMemoryIntelligence(userId, agentId, id, {
1213
+ content,
1214
+ category,
1215
+ sourceRef,
1216
+ metadata,
1217
+ });
1218
+
835
1219
  return id;
836
1220
  }
837
1221
 
@@ -843,9 +1227,10 @@ class MemoryManager {
843
1227
  if (!query || !query.trim()) return [];
844
1228
  const agentId = this._agentId(userId, options);
845
1229
  const scope = normalizeScope(options.scope, agentId);
1230
+ const limit = Math.max(1, Math.min(Number(topK) || 6, 50));
846
1231
 
847
1232
  const all = db.prepare(
848
- `SELECT id, category, content, importance, embedding, access_count, created_at, updated_at,
1233
+ `SELECT id, category, content, summary, importance, confidence, embedding, access_count, created_at, updated_at,
849
1234
  scope_type, scope_id, source_type, source_id, source_label, stale_after_days, metadata_json
850
1235
  FROM memories
851
1236
  WHERE user_id = ? AND agent_id = ? AND archived = 0
@@ -853,33 +1238,69 @@ class MemoryManager {
853
1238
  (COALESCE(scope_type, 'agent') = ? AND COALESCE(scope_id, '') = COALESCE(?, ''))
854
1239
  OR COALESCE(scope_type, 'agent') = 'shared'
855
1240
  )
856
- ORDER BY updated_at DESC`
1241
+ ORDER BY updated_at DESC
1242
+ LIMIT 800`
857
1243
  ).all(userId, agentId, scope.scopeType, scope.scopeId);
858
1244
 
859
1245
  if (!all.length) return [];
860
1246
 
861
1247
  const queryVec = await getEmbedding(query, await getActiveProvider(userId, agentId));
1248
+ const lexicalHits = this._searchMemoryFts(userId, agentId, query, Math.max(40, limit * 8));
1249
+ const entityHits = this._searchEntityMemoryIds(userId, agentId, query, Math.max(40, limit * 8));
1250
+ const lexicalRanks = new Map(lexicalHits.map((hit, index) => [hit.memory_id, index]));
1251
+ const entityRanks = new Map(entityHits.map((hit, index) => [hit.memory_id, index]));
862
1252
 
863
- const scored = all.map(mem => {
864
- let score = 0;
1253
+ const semanticScored = all.map(mem => {
1254
+ let semanticScore = 0;
865
1255
  if (queryVec && mem.embedding) {
866
1256
  const memVec = deserializeEmbedding(mem.embedding);
867
1257
  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);
1258
+ semanticScore = cosineSimilarity(queryVec, memVec);
871
1259
  }
872
1260
  }
873
- const lexicalScore = keywordSimilarity(query, mem.content) * 0.7;
874
- score = Math.max(score, lexicalScore);
875
- score *= computeFreshnessMultiplier(mem);
876
- return { ...mem, score };
1261
+ return { ...mem, semanticScore };
1262
+ }).sort((left, right) => right.semanticScore - left.semanticScore);
1263
+
1264
+ const semanticRanks = new Map();
1265
+ semanticScored.forEach((mem, index) => {
1266
+ if (mem.semanticScore > 0) semanticRanks.set(mem.id, index);
1267
+ });
1268
+
1269
+ const scored = semanticScored.map((mem) => {
1270
+ const lexicalScore = keywordSimilarity(query, `${mem.content} ${mem.summary || ''}`) * 0.75;
1271
+ const ftsScore = lexicalRanks.has(mem.id) ? 0.42 : 0;
1272
+ const entityScore = entityRanks.has(mem.id) ? 0.48 : 0;
1273
+ const baseScore = Math.max(
1274
+ mem.semanticScore * (0.65 + Number(mem.importance || 5) / 25),
1275
+ lexicalScore,
1276
+ ftsScore,
1277
+ entityScore,
1278
+ );
1279
+ const score = scoreMemoryCandidate({
1280
+ semanticRank: semanticRanks.get(mem.id) ?? -1,
1281
+ lexicalRank: lexicalRanks.get(mem.id) ?? -1,
1282
+ entityRank: entityRanks.get(mem.id) ?? -1,
1283
+ baseScore,
1284
+ importance: mem.importance,
1285
+ accessCount: mem.access_count,
1286
+ freshness: computeFreshnessMultiplier(mem),
1287
+ });
1288
+ return {
1289
+ ...mem,
1290
+ score,
1291
+ scoreBreakdown: {
1292
+ semantic: Number(mem.semanticScore || 0),
1293
+ lexical: Number(lexicalScore || 0),
1294
+ fullText: ftsScore,
1295
+ entity: entityScore,
1296
+ },
1297
+ };
877
1298
  });
878
1299
 
879
1300
  const results = scored
880
- .filter(m => m.score > 0.2)
1301
+ .filter(m => m.score > 0.12)
881
1302
  .sort((a, b) => b.score - a.score)
882
- .slice(0, topK);
1303
+ .slice(0, limit);
883
1304
 
884
1305
  // Update access counts
885
1306
  if (results.length) {
@@ -888,9 +1309,10 @@ class MemoryManager {
888
1309
  db.prepare(`UPDATE memories SET access_count = access_count + 1 WHERE id IN (${placeholders})`).run(...ids);
889
1310
  }
890
1311
 
891
- return results.map((row) => ({
1312
+ return this._attachEntities(results).map((row) => ({
892
1313
  ...serializeMemoryRow(row),
893
1314
  score: row.score,
1315
+ scoreBreakdown: row.scoreBreakdown,
894
1316
  }));
895
1317
  }
896
1318
 
@@ -899,7 +1321,7 @@ class MemoryManager {
899
1321
  */
900
1322
  listMemories(userId, { category, limit = 50, offset = 0, includeArchived = false, agentId = null } = {}) {
901
1323
  const scopedAgentId = this._agentId(userId, { agentId });
902
- let sql = `SELECT id, category, content, importance, access_count, archived, created_at, updated_at,
1324
+ let sql = `SELECT id, category, content, summary, importance, confidence, access_count, archived, created_at, updated_at,
903
1325
  scope_type, scope_id, source_type, source_id, source_label, stale_after_days, metadata_json
904
1326
  FROM memories WHERE user_id = ? AND agent_id = ? AND archived = ?`;
905
1327
  const params = [userId, scopedAgentId, includeArchived ? 1 : 0];
@@ -912,7 +1334,7 @@ class MemoryManager {
912
1334
  }
913
1335
  sql += ` ORDER BY importance DESC, updated_at DESC LIMIT ? OFFSET ?`;
914
1336
  params.push(limit, offset);
915
- return db.prepare(sql).all(...params).map(serializeMemoryRow);
1337
+ return this._attachEntities(db.prepare(sql).all(...params)).map(serializeMemoryRow);
916
1338
  }
917
1339
 
918
1340
  /**
@@ -925,17 +1347,30 @@ class MemoryManager {
925
1347
  const newContent = content ?? mem.content;
926
1348
  const newImportance = importance != null ? Math.max(1, Math.min(10, Number(importance))) : mem.importance;
927
1349
  const newCategory = category ? normalizeMemoryCategory(category) : mem.category;
1350
+ const newSummary = summarizeForPrompt({ content: newContent, entities: extractEntities(newContent) });
1351
+ const newHash = stableHash(`${newCategory}:${newContent}`);
928
1352
 
929
1353
  let newEmbed = mem.embedding;
930
1354
  if (content && content !== mem.content) {
931
- const vec = await getEmbedding(newContent, await getActiveProvider(null));
1355
+ const vec = await getEmbedding(newContent, await getActiveProvider(mem.user_id, mem.agent_id));
932
1356
  newEmbed = vec ? serializeEmbedding(vec) : mem.embedding;
933
1357
  }
934
1358
 
935
1359
  db.prepare(
936
- `UPDATE memories SET content = ?, importance = ?, category = ?, embedding = ?,
1360
+ `UPDATE memories SET content = ?, summary = ?, importance = ?, category = ?, memory_hash = ?, embedding = ?,
937
1361
  updated_at = datetime('now') WHERE id = ?`
938
- ).run(newContent, newImportance, newCategory, newEmbed, id);
1362
+ ).run(newContent, newSummary, newImportance, newCategory, newHash, newEmbed, id);
1363
+
1364
+ this._upsertMemoryIntelligence(mem.user_id, mem.agent_id, id, {
1365
+ content: newContent,
1366
+ category: newCategory,
1367
+ sourceRef: normalizeSourceRef({
1368
+ sourceType: mem.source_type,
1369
+ sourceId: mem.source_id,
1370
+ sourceLabel: mem.source_label,
1371
+ }),
1372
+ metadata: parseJsonObject(mem.metadata_json, {}),
1373
+ });
939
1374
 
940
1375
  return serializeMemoryRow(db.prepare(
941
1376
  `SELECT * FROM memories WHERE id = ?`
@@ -957,6 +1392,7 @@ class MemoryManager {
957
1392
  )];
958
1393
  if (!uniqueIds.length) return 0;
959
1394
  const placeholders = uniqueIds.map(() => '?').join(', ');
1395
+ for (const id of uniqueIds) this._deleteMemoryIndex(id);
960
1396
  const result = userId != null
961
1397
  ? db.prepare(`DELETE FROM memories WHERE id IN (${placeholders}) AND user_id = ?`).run(...uniqueIds, userId)
962
1398
  : db.prepare(`DELETE FROM memories WHERE id IN (${placeholders})`).run(...uniqueIds);
@@ -978,6 +1414,11 @@ class MemoryManager {
978
1414
  )];
979
1415
  if (!uniqueIds.length) return 0;
980
1416
  const placeholders = uniqueIds.map(() => '?').join(', ');
1417
+ if (archived) {
1418
+ for (const id of uniqueIds) {
1419
+ try { db.prepare('DELETE FROM memories_fts WHERE memory_id = ?').run(id); } catch {}
1420
+ }
1421
+ }
981
1422
  const result = userId != null
982
1423
  ? db.prepare(
983
1424
  `UPDATE memories SET archived = ? WHERE id IN (${placeholders}) AND user_id = ?`
@@ -1576,7 +2017,10 @@ class MemoryManager {
1576
2017
  if (recalled.length) {
1577
2018
  const memoryLines = recalled.map(m => {
1578
2019
  const badge = m.category !== 'episodic' ? ` [${m.category}]` : '';
1579
- return `- ${m.content}${badge}`;
2020
+ const entities = Array.isArray(m.entities) && m.entities.length
2021
+ ? ` (entities: ${m.entities.slice(0, 4).map((entity) => entity.name).join(', ')})`
2022
+ : '';
2023
+ return `- ${summarizeForPrompt(m)}${badge}${entities}`;
1580
2024
  });
1581
2025
  sections.push(`Relevant memory:\n${memoryLines.join('\n')}`);
1582
2026
  }