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