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.
- package/.env.example +33 -3
- package/LICENSE +111 -56
- package/README.md +8 -3
- package/docs/configuration.md +8 -0
- package/docs/getting-started.md +9 -3
- package/docs/index.md +4 -0
- package/extensions/chrome-browser/background.mjs +45 -7
- package/extensions/chrome-browser/icons/icon128.png +0 -0
- package/extensions/chrome-browser/icons/icon16.png +0 -0
- package/extensions/chrome-browser/icons/icon48.png +0 -0
- package/extensions/chrome-browser/icons/logo.svg +39 -8
- package/extensions/chrome-browser/manifest.json +3 -2
- package/extensions/chrome-browser/popup.html +5 -1
- package/extensions/chrome-browser/popup.js +15 -2
- package/flutter_app/android/app/src/main/AndroidManifest.xml +2 -1
- package/flutter_app/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/flutter_app/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/flutter_app/assets/branding/app_icon_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_64.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_1024.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_128.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_192.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_256.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_32.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_512.png +0 -0
- package/flutter_app/assets/branding/app_icon_light_64.png +0 -0
- package/flutter_app/assets/branding/onboarding_intro.mp4 +0 -0
- package/flutter_app/assets/branding/tray_icon_light_template.png +0 -0
- package/flutter_app/assets/branding/tray_icon_template.png +0 -0
- package/flutter_app/lib/features/location/location_service.dart +3 -0
- package/flutter_app/lib/features/onboarding/onboarding_chrome.dart +391 -382
- package/flutter_app/lib/features/onboarding/onboarding_companion_step.dart +743 -0
- package/flutter_app/lib/features/onboarding/onboarding_messaging_step.dart +18 -16
- package/flutter_app/lib/features/onboarding/onboarding_model_step.dart +19 -18
- package/flutter_app/lib/features/onboarding/onboarding_shell.dart +8 -1
- package/flutter_app/lib/features/onboarding/onboarding_video_step.dart +16 -13
- package/flutter_app/lib/features/onboarding/onboarding_welcome_step.dart +17 -13
- package/flutter_app/lib/main.dart +3 -0
- package/flutter_app/lib/main_account_settings.dart +10 -34
- package/flutter_app/lib/main_admin.dart +14 -1
- package/flutter_app/lib/main_app_shell.dart +377 -340
- package/flutter_app/lib/main_chat.dart +707 -227
- package/flutter_app/lib/main_controller.dart +338 -46
- package/flutter_app/lib/main_devices.dart +851 -122
- package/flutter_app/lib/main_integrations.dart +255 -6
- package/flutter_app/lib/main_launcher.dart +1 -1
- package/flutter_app/lib/main_model_picker.dart +685 -0
- package/flutter_app/lib/main_models.dart +212 -0
- package/flutter_app/lib/main_navigation.dart +1 -9
- package/flutter_app/lib/main_operations.dart +1417 -614
- package/flutter_app/lib/main_settings.dart +764 -887
- package/flutter_app/lib/main_shared.dart +971 -443
- package/flutter_app/lib/main_spacing.dart +4 -4
- package/flutter_app/lib/main_theme.dart +22 -14
- package/flutter_app/lib/main_unified.dart +5 -72
- package/flutter_app/lib/src/android_apk_drop_zone.dart +24 -0
- package/flutter_app/lib/src/android_apk_drop_zone_stub.dart +13 -0
- package/flutter_app/lib/src/android_apk_drop_zone_web.dart +219 -0
- package/flutter_app/lib/src/backend_client.dart +79 -0
- package/flutter_app/lib/src/desktop_companion_actions.dart +56 -7
- package/flutter_app/lib/src/desktop_companion_io.dart +77 -1
- package/flutter_app/lib/src/desktop_native_bridge.dart +13 -0
- package/flutter_app/lib/src/security/password_strength.dart +84 -0
- package/flutter_app/lib/src/stream_renderer.dart +205 -35
- package/flutter_app/lib/src/theme/palette.dart +76 -34
- package/flutter_app/linux/runner/resources/app_icon.png +0 -0
- package/flutter_app/macos/Runner/AppDelegate.swift +44 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +0 -0
- package/flutter_app/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +0 -0
- package/flutter_app/pubspec.lock +2 -2
- package/flutter_app/pubspec.yaml +7 -1
- package/flutter_app/tool/branding_source/neoagent-icon-1024.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-128.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-16.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-180.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-192.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-256.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-32.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-48.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-512.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon-64.png +0 -0
- package/flutter_app/tool/branding_source/neoagent-icon.svg +43 -0
- package/flutter_app/tool/generate_desktop_branding.py +154 -152
- package/flutter_app/web/favicon.png +0 -0
- package/flutter_app/web/favicon.svg +40 -9
- package/flutter_app/web/favicon_light.svg +43 -0
- package/flutter_app/web/icons/Icon-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-192.png +0 -0
- package/flutter_app/web/icons/Icon-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-512.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-192.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512-light.png +0 -0
- package/flutter_app/web/icons/Icon-maskable-512.png +0 -0
- package/flutter_app/windows/runner/main.cpp +7 -1
- package/flutter_app/windows/runner/resources/app_icon.ico +0 -0
- package/lib/manager.js +445 -83
- package/package.json +17 -3
- package/runtime/paths.js +3 -1
- package/server/admin/access.js +198 -0
- package/server/admin/admin.css +268 -0
- package/server/admin/admin.js +348 -0
- package/server/admin/analytics.js +128 -0
- package/server/admin/index.html +1015 -0
- package/server/admin/login.html +290 -0
- package/server/admin/logo.svg +43 -0
- package/server/admin/sql.js +134 -0
- package/server/admin/users.js +147 -0
- package/server/config/origins.js +3 -1
- package/server/db/database.js +92 -0
- package/server/db/ftsQuery.js +27 -0
- package/server/http/routes.js +1 -0
- package/server/http/static.js +23 -6
- package/server/index.js +1 -1
- package/server/middleware/adminAuth.js +48 -0
- package/server/middleware/auth.js +1 -40
- package/server/public/.last_build_id +1 -1
- package/server/public/app_icon.png +0 -0
- package/server/public/assets/AssetManifest.bin +1 -1
- package/server/public/assets/AssetManifest.bin.json +1 -1
- package/server/public/assets/assets/branding/app_icon_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_512.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_1024.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_256.png +0 -0
- package/server/public/assets/assets/branding/app_icon_light_512.png +0 -0
- package/server/public/assets/assets/branding/onboarding_intro.mp4 +0 -0
- package/server/public/assets/assets/branding/tray_icon_light_template.png +0 -0
- package/server/public/assets/assets/branding/tray_icon_template.png +0 -0
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/assets/web/icons/Icon-192.png +0 -0
- package/server/public/favicon.png +0 -0
- package/server/public/favicon.svg +40 -9
- package/server/public/favicon_light.svg +43 -0
- package/server/public/flutter_bootstrap.js +2 -2
- package/server/public/icons/Icon-192-light.png +0 -0
- package/server/public/icons/Icon-192.png +0 -0
- package/server/public/icons/Icon-512-light.png +0 -0
- package/server/public/icons/Icon-512.png +0 -0
- package/server/public/icons/Icon-maskable-192-light.png +0 -0
- package/server/public/icons/Icon-maskable-192.png +0 -0
- package/server/public/icons/Icon-maskable-512-light.png +0 -0
- package/server/public/icons/Icon-maskable-512.png +0 -0
- package/server/public/main.dart.js +88944 -85720
- package/server/routes/admin.js +632 -0
- package/server/routes/agent_profiles.js +3 -0
- package/server/routes/agents.js +1 -1
- package/server/routes/auth.js +3 -1
- package/server/routes/browser.js +14 -0
- package/server/routes/browser_extension.js +21 -4
- package/server/routes/desktop.js +10 -0
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +23 -1
- package/server/routes/messaging.js +2 -0
- package/server/routes/screenHistory.js +14 -8
- package/server/routes/settings.js +14 -2
- package/server/routes/stream.js +12 -3
- package/server/routes/triggers.js +4 -4
- package/server/routes/voice_assistant.js +36 -1
- package/server/routes/workspace.js +86 -0
- package/server/services/account/admin_two_factor.js +132 -0
- package/server/services/account/password_policy.js +6 -1
- package/server/services/ai/completion.js +44 -0
- package/server/services/ai/engine.js +321 -500
- package/server/services/ai/imageAnalysis.js +9 -5
- package/server/services/ai/logFormat.js +46 -0
- package/server/services/ai/loopPolicy.js +11 -0
- package/server/services/ai/messagingFallback.js +228 -0
- package/server/services/ai/models.js +194 -55
- package/server/services/ai/providerRetry.js +169 -0
- package/server/services/ai/providers/anthropic.js +15 -4
- package/server/services/ai/providers/google.js +38 -11
- package/server/services/ai/providers/grok.js +19 -68
- package/server/services/ai/providers/grokOauth.js +141 -0
- package/server/services/ai/providers/nvidia.js +154 -0
- package/server/services/ai/providers/ollama.js +76 -39
- package/server/services/ai/providers/openai.js +20 -31
- package/server/services/ai/providers/openaiCodex.js +6 -2
- package/server/services/ai/providers/openaiCompatible.js +70 -0
- package/server/services/ai/providers/openrouter.js +162 -0
- package/server/services/ai/settings.js +30 -0
- package/server/services/ai/systemPrompt.js +51 -29
- package/server/services/ai/taskAnalysis.js +60 -1
- package/server/services/ai/toolEvidence.js +207 -0
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +80 -15
- package/server/services/android/controller.js +36 -1
- package/server/services/browser/controller.js +18 -0
- package/server/services/browser/extension/manifest.js +33 -0
- package/server/services/browser/extension/provider.js +12 -6
- package/server/services/browser/extension/registry.js +229 -18
- package/server/services/desktop/protocol.js +1 -0
- package/server/services/desktop/provider.js +4 -0
- package/server/services/desktop/registry.js +104 -1
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/integrations/google/provider.js +13 -0
- package/server/services/integrations/home_assistant/constants.js +88 -0
- package/server/services/integrations/home_assistant/network.js +207 -0
- package/server/services/integrations/home_assistant/provider.js +249 -0
- package/server/services/integrations/home_assistant/snapshot.js +98 -0
- package/server/services/integrations/home_assistant/tools.js +101 -0
- package/server/services/integrations/manager.js +5 -0
- package/server/services/integrations/registry.js +2 -0
- package/server/services/integrations/trello/provider.js +8 -2
- package/server/services/manager.js +51 -47
- package/server/services/mcp/client.js +208 -268
- package/server/services/mcp/client_support.js +172 -0
- package/server/services/mcp/recovery.js +116 -0
- package/server/services/mcp/tool_operations.js +123 -0
- package/server/services/memory/ingestion.js +185 -370
- package/server/services/memory/ingestion_coverage.js +129 -0
- package/server/services/memory/ingestion_documents.js +123 -0
- package/server/services/memory/ingestion_support.js +171 -0
- package/server/services/memory/intelligence.js +181 -0
- package/server/services/memory/manager.js +476 -32
- package/server/services/messaging/automation.js +71 -134
- package/server/services/messaging/formatting_guides.js +26 -1
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/manager.js +25 -8
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/runtime/manager.js +7 -3
- package/server/services/runtime/settings.js +17 -0
- package/server/services/streaming/android-stream.js +296 -32
- package/server/services/streaming/stream-hub.js +1 -1
- package/server/services/tasks/integration_runtime.js +4 -1
- package/server/services/tasks/runtime.js +415 -56
- package/server/services/tasks/task_repository.js +48 -6
- package/server/services/websocket.js +8 -2
- package/server/services/widgets/service.js +17 -1
- package/server/services/workspace/manager.js +116 -0
- package/server/utils/logger.js +44 -6
- package/server/utils/security.js +3 -0
- package/server/utils/version.js +29 -19
- 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,
|
|
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
|
|
864
|
-
let
|
|
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
|
-
|
|
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
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
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.
|
|
1301
|
+
.filter(m => m.score > 0.12)
|
|
881
1302
|
.sort((a, b) => b.score - a.score)
|
|
882
|
-
.slice(0,
|
|
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(
|
|
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
|
-
|
|
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
|
}
|