neoagent 2.4.4-beta.0 → 2.4.4-beta.11

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 (66) hide show
  1. package/.env.example +17 -0
  2. package/README.md +9 -3
  3. package/docs/capabilities.md +16 -7
  4. package/docs/configuration.md +1 -0
  5. package/docs/getting-started.md +6 -0
  6. package/docs/index.md +1 -0
  7. package/docs/security-boundaries.md +122 -0
  8. package/docs/supermemory-memory-review.md +852 -0
  9. package/flutter_app/lib/features/memory/views/retrieval_inspector_view.dart +128 -0
  10. package/flutter_app/lib/main.dart +3 -0
  11. package/flutter_app/lib/main_account_settings.dart +79 -15
  12. package/flutter_app/lib/main_app_shell.dart +22 -0
  13. package/flutter_app/lib/main_chat.dart +155 -8
  14. package/flutter_app/lib/main_controller.dart +74 -5
  15. package/flutter_app/lib/main_devices.dart +9 -3
  16. package/flutter_app/lib/main_models.dart +32 -0
  17. package/flutter_app/lib/main_operations.dart +13 -0
  18. package/flutter_app/lib/main_security.dart +967 -0
  19. package/flutter_app/lib/main_settings.dart +56 -0
  20. package/flutter_app/lib/src/backend_client.dart +60 -3
  21. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +2 -0
  22. package/flutter_app/pubspec.lock +32 -0
  23. package/flutter_app/pubspec.yaml +1 -0
  24. package/lib/install_helpers.js +1 -0
  25. package/lib/manager.js +63 -1
  26. package/lib/schema_migrations.js +262 -0
  27. package/package.json +4 -2
  28. package/server/admin/admin.js +151 -0
  29. package/server/admin/index.html +55 -3
  30. package/server/db/database.js +3 -0
  31. package/server/http/routes.js +2 -1
  32. package/server/public/.last_build_id +1 -1
  33. package/server/public/assets/NOTICES +86 -0
  34. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  35. package/server/public/flutter_bootstrap.js +1 -1
  36. package/server/public/main.dart.js +82243 -80308
  37. package/server/routes/account.js +2 -23
  38. package/server/routes/admin.js +18 -2
  39. package/server/routes/agents.js +5 -1
  40. package/server/routes/memory.js +41 -4
  41. package/server/routes/security.js +112 -0
  42. package/server/services/account/service_email_settings.js +167 -0
  43. package/server/services/ai/engine.js +269 -27
  44. package/server/services/ai/rate_limits.js +150 -0
  45. package/server/services/ai/systemPrompt.js +26 -3
  46. package/server/services/ai/tools.js +11 -8
  47. package/server/services/cli/shell_worker.js +135 -0
  48. package/server/services/cli/shell_worker_pool.js +125 -0
  49. package/server/services/integrations/google/gmail.js +7 -7
  50. package/server/services/manager.js +20 -1
  51. package/server/services/memory/consolidation.js +111 -0
  52. package/server/services/memory/embedding_index.js +175 -0
  53. package/server/services/memory/embeddings.js +22 -2
  54. package/server/services/memory/evaluation.js +187 -0
  55. package/server/services/memory/ingestion_chunking.js +191 -0
  56. package/server/services/memory/ingestion_documents.js +96 -26
  57. package/server/services/memory/intelligence.js +3 -1
  58. package/server/services/memory/manager.js +855 -43
  59. package/server/services/memory/policy.js +0 -40
  60. package/server/services/memory/retrieval_reasoning.js +191 -0
  61. package/server/services/runtime/manager.js +7 -0
  62. package/server/services/security/approval_gate_service.js +93 -0
  63. package/server/services/security/tool_categories.js +105 -0
  64. package/server/services/security/tool_policy_service.js +92 -0
  65. package/server/services/security/tool_security_hook.js +77 -0
  66. package/server/services/websocket.js +5 -1
@@ -141,12 +141,31 @@ async function getOpenAIEmbedding(text) {
141
141
  * @returns {Float32Array|null}
142
142
  */
143
143
  async function getEmbedding(text, provider) {
144
+ const result = await getEmbeddingWithMetadata(text, provider);
145
+ return result?.vector || null;
146
+ }
147
+
148
+ async function getEmbeddingWithMetadata(text, provider) {
144
149
  if (!text || !text.trim()) return null;
145
150
  if (provider === 'google' && process.env.GOOGLE_AI_KEY) {
146
151
  const vec = await getGeminiEmbedding(text);
147
- if (vec) return vec;
152
+ if (vec) {
153
+ return {
154
+ vector: vec,
155
+ provider: 'google',
156
+ model: GOOGLE_MODEL,
157
+ dimensions: vec.length,
158
+ };
159
+ }
148
160
  }
149
- return getOpenAIEmbedding(text);
161
+ const vec = await getOpenAIEmbedding(text);
162
+ if (!vec) return null;
163
+ return {
164
+ vector: vec,
165
+ provider: 'openai',
166
+ model: OPENAI_MODEL,
167
+ dimensions: vec.length,
168
+ };
150
169
  }
151
170
 
152
171
  /**
@@ -203,6 +222,7 @@ function keywordSimilarity(query, text) {
203
222
 
204
223
  module.exports = {
205
224
  getEmbedding,
225
+ getEmbeddingWithMetadata,
206
226
  cosineSimilarity,
207
227
  serializeEmbedding,
208
228
  deserializeEmbedding,
@@ -0,0 +1,187 @@
1
+ 'use strict';
2
+
3
+ const { performance } = require('node:perf_hooks');
4
+
5
+ function mean(values) {
6
+ if (!values.length) return 0;
7
+ return values.reduce((sum, value) => sum + value, 0) / values.length;
8
+ }
9
+
10
+ function percentile(values, fraction) {
11
+ if (!values.length) return 0;
12
+ const sorted = [...values].sort((left, right) => left - right);
13
+ const index = Math.min(sorted.length - 1, Math.ceil(sorted.length * fraction) - 1);
14
+ return sorted[Math.max(0, index)];
15
+ }
16
+
17
+ function calculateRetrievalMetrics(results, relevantKeys, k) {
18
+ const returned = results.slice(0, k);
19
+ const relevance = returned.map((result) => (
20
+ relevantKeys.has(`memory:${result.id}`)
21
+ || relevantKeys.has(`source:${result.sourceRef?.sourceId || ''}`)
22
+ ? 1
23
+ : 0
24
+ ));
25
+ const relevantRetrieved = relevance.reduce((sum, value) => sum + value, 0);
26
+ const totalRelevant = relevantKeys.size;
27
+ const precisionAtK = returned.length ? relevantRetrieved / returned.length : 0;
28
+ const recallAtK = totalRelevant ? relevantRetrieved / totalRelevant : 0;
29
+ const firstRelevant = relevance.indexOf(1);
30
+ const dcg = relevance.reduce(
31
+ (sum, value, index) => sum + value / Math.log2(index + 2),
32
+ 0,
33
+ );
34
+ let idealDcg = 0;
35
+ for (let index = 0; index < Math.min(totalRelevant, returned.length); index += 1) {
36
+ idealDcg += 1 / Math.log2(index + 2);
37
+ }
38
+
39
+ return {
40
+ hitAtK: relevantRetrieved > 0 ? 1 : 0,
41
+ precisionAtK,
42
+ recallAtK,
43
+ f1AtK: precisionAtK + recallAtK
44
+ ? (2 * precisionAtK * recallAtK) / (precisionAtK + recallAtK)
45
+ : 0,
46
+ mrr: firstRelevant >= 0 ? 1 / (firstRelevant + 1) : 0,
47
+ ndcg: idealDcg ? dcg / idealDcg : 0,
48
+ relevantRetrieved,
49
+ totalRelevant,
50
+ };
51
+ }
52
+
53
+ function normalizeQuestions(dataset) {
54
+ const questions = Array.isArray(dataset)
55
+ ? dataset
56
+ : dataset?.questions || dataset?.queries;
57
+ if (!Array.isArray(questions) || !questions.length) {
58
+ throw new Error('Dataset must contain a non-empty questions or queries array.');
59
+ }
60
+
61
+ return questions.map((question, index) => {
62
+ const query = String(question.query || question.question || '').trim();
63
+ const relevantMemoryIds = Array.isArray(question.relevantMemoryIds)
64
+ ? question.relevantMemoryIds.map(String).filter(Boolean)
65
+ : [];
66
+ const relevantSourceIds = Array.isArray(question.relevantSourceIds)
67
+ ? question.relevantSourceIds.map(String).filter(Boolean)
68
+ : Array.isArray(question.haystackSessionIds)
69
+ ? question.haystackSessionIds.map(String).filter(Boolean)
70
+ : [];
71
+ if (!query) throw new Error(`Question ${index + 1} has no query.`);
72
+ if (!relevantMemoryIds.length && !relevantSourceIds.length) {
73
+ throw new Error(`Question ${index + 1} has no relevant memory or source IDs.`);
74
+ }
75
+ return {
76
+ id: String(question.id || question.questionId || index + 1),
77
+ query,
78
+ category: String(question.category || question.questionType || 'uncategorized'),
79
+ relevantMemoryIds,
80
+ relevantSourceIds,
81
+ validAt: question.validAt || null,
82
+ knownAt: question.knownAt || null,
83
+ };
84
+ });
85
+ }
86
+
87
+ function aggregateEvaluations(evaluations, k) {
88
+ const metricNames = ['hitAtK', 'precisionAtK', 'recallAtK', 'f1AtK', 'mrr', 'ndcg'];
89
+ const retrieval = { k };
90
+ for (const metric of metricNames) {
91
+ retrieval[metric] = mean(evaluations.map((evaluation) => evaluation.metrics[metric]));
92
+ }
93
+ const latencies = evaluations.map((evaluation) => evaluation.latencyMs);
94
+ const contextTokens = evaluations.map((evaluation) => evaluation.contextTokensEstimate);
95
+ const candidateCounts = evaluations.map((evaluation) => evaluation.candidateCount);
96
+ return {
97
+ questions: evaluations.length,
98
+ retrieval,
99
+ latencyMs: {
100
+ mean: mean(latencies),
101
+ median: percentile(latencies, 0.5),
102
+ p95: percentile(latencies, 0.95),
103
+ p99: percentile(latencies, 0.99),
104
+ max: latencies.length ? Math.max(...latencies) : 0,
105
+ },
106
+ contextTokensEstimate: {
107
+ mean: mean(contextTokens),
108
+ p95: percentile(contextTokens, 0.95),
109
+ max: contextTokens.length ? Math.max(...contextTokens) : 0,
110
+ },
111
+ candidateCount: {
112
+ mean: mean(candidateCounts),
113
+ p95: percentile(candidateCounts, 0.95),
114
+ max: candidateCounts.length ? Math.max(...candidateCounts) : 0,
115
+ },
116
+ };
117
+ }
118
+
119
+ async function runRetrievalBenchmark({
120
+ memoryManager,
121
+ userId,
122
+ agentId,
123
+ dataset,
124
+ k = 15,
125
+ }) {
126
+ const limit = Math.max(1, Math.min(Number(k) || 15, 50));
127
+ const questions = normalizeQuestions(dataset);
128
+ const evaluations = [];
129
+
130
+ for (const question of questions) {
131
+ const relevantKeys = new Set([
132
+ ...question.relevantMemoryIds.map((id) => `memory:${id}`),
133
+ ...question.relevantSourceIds.map((id) => `source:${id}`),
134
+ ]);
135
+ const startedAt = performance.now();
136
+ const results = await memoryManager.recallMemory(userId, question.query, limit, {
137
+ agentId,
138
+ validAt: question.validAt,
139
+ knownAt: question.knownAt,
140
+ });
141
+ const latencyMs = performance.now() - startedAt;
142
+ const contextText = results
143
+ .map((result) => result.summary || result.content || '')
144
+ .join('\n');
145
+
146
+ evaluations.push({
147
+ id: question.id,
148
+ category: question.category,
149
+ query: question.query,
150
+ latencyMs,
151
+ contextTokensEstimate: Math.ceil(contextText.length / 4),
152
+ candidateCount: Math.max(
153
+ 0,
154
+ ...results.map((result) => Number(result.scoreBreakdown?.candidateCount) || 0),
155
+ ),
156
+ retrievedMemoryIds: results.map((result) => result.id),
157
+ retrievedSourceIds: results
158
+ .map((result) => result.sourceRef?.sourceId)
159
+ .filter(Boolean),
160
+ metrics: calculateRetrievalMetrics(results, relevantKeys, limit),
161
+ });
162
+ }
163
+
164
+ const categories = {};
165
+ for (const category of new Set(evaluations.map((evaluation) => evaluation.category))) {
166
+ categories[category] = aggregateEvaluations(
167
+ evaluations.filter((evaluation) => evaluation.category === category),
168
+ limit,
169
+ );
170
+ }
171
+
172
+ return {
173
+ generatedAt: new Date().toISOString(),
174
+ dataset: String(dataset?.name || 'unnamed'),
175
+ userId,
176
+ agentId,
177
+ ...aggregateEvaluations(evaluations, limit),
178
+ categories,
179
+ evaluations,
180
+ };
181
+ }
182
+
183
+ module.exports = {
184
+ calculateRetrievalMetrics,
185
+ normalizeQuestions,
186
+ runRetrievalBenchmark,
187
+ };
@@ -0,0 +1,191 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+
5
+ const TARGET_CHARS = 1400;
6
+ const MAX_CHARS = 2200;
7
+
8
+ function contentHash(content) {
9
+ return crypto.createHash('sha256').update(content).digest('hex');
10
+ }
11
+
12
+ function messageBlocks(document) {
13
+ const messages = Array.isArray(document?.payload?.messages)
14
+ ? document.payload.messages
15
+ : [];
16
+ return messages
17
+ .map((message) => {
18
+ const speaker = String(message.speaker || message.role || message.author || '').trim();
19
+ const timestamp = String(message.timestamp || message.createdAt || '').trim();
20
+ const content = String(message.content || message.text || message.body || '').trim();
21
+ if (!content) return null;
22
+ const prefix = [timestamp, speaker].filter(Boolean).join(' ');
23
+ return {
24
+ content,
25
+ rendered: prefix ? `${prefix}\n${content}` : content,
26
+ messageId: String(message.id || message.messageId || '').trim() || null,
27
+ };
28
+ })
29
+ .filter(Boolean);
30
+ }
31
+
32
+ function rawStructuralBlocks(text) {
33
+ return String(text || '')
34
+ .replace(/\r\n?/g, '\n')
35
+ .split(/\n{2,}|(?=^#{1,6}\s)/gm)
36
+ .map((block) => block.trim())
37
+ .filter(Boolean);
38
+ }
39
+
40
+ function rawEmailBlocks(text) {
41
+ return String(text || '')
42
+ .replace(/\r\n?/g, '\n')
43
+ .split(/\n(?=On\s.*wrote:|\s*>\s|-----Original Message-----|________________________________)/g)
44
+ .map((block) => block.trim())
45
+ .filter(Boolean);
46
+ }
47
+
48
+ function rawCodeBlocks(text) {
49
+ return String(text || '')
50
+ .replace(/\r\n?/g, '\n')
51
+ .split(/\n(?=(?:export\s+)?(?:class|function|const|let|var|import)\s)/g)
52
+ .map((block) => block.trim())
53
+ .filter(Boolean);
54
+ }
55
+
56
+ function findBlockPositions(originalContent, blocks) {
57
+ let searchFrom = 0;
58
+ return blocks.map((block) => {
59
+ const pos = originalContent.indexOf(block, searchFrom);
60
+ const charStart = pos >= 0 ? pos : searchFrom;
61
+ const charEnd = charStart + block.length;
62
+ searchFrom = charEnd;
63
+ return { content: block, charStart, charEnd };
64
+ });
65
+ }
66
+
67
+ function splitOversizedWithPos({ content, charStart }) {
68
+ if (content.length <= MAX_CHARS) {
69
+ return [{ content, charStart, charEnd: charStart + content.length }];
70
+ }
71
+ const parts = [];
72
+ let offset = 0;
73
+ while (offset < content.length) {
74
+ const slice = content.slice(offset, offset + MAX_CHARS);
75
+ parts.push({
76
+ content: slice,
77
+ charStart: charStart + offset,
78
+ charEnd: charStart + offset + slice.length,
79
+ });
80
+ offset += MAX_CHARS;
81
+ }
82
+ return parts;
83
+ }
84
+
85
+ function packBlocksWithPositions(blocksWithPos) {
86
+ const chunks = [];
87
+ let group = [];
88
+ let groupLen = 0;
89
+
90
+ for (const bwp of blocksWithPos.flatMap(splitOversizedWithPos)) {
91
+ const addLen = (groupLen ? 2 : 0) + bwp.content.length;
92
+ if (group.length && groupLen + addLen > TARGET_CHARS) {
93
+ chunks.push(group);
94
+ group = [bwp];
95
+ groupLen = bwp.content.length;
96
+ } else {
97
+ group.push(bwp);
98
+ groupLen += addLen;
99
+ }
100
+ }
101
+ if (group.length) chunks.push(group);
102
+
103
+ return chunks.map((g) => ({
104
+ content: g.map((b) => b.content).join('\n\n'),
105
+ charStart: g[0].charStart,
106
+ charEnd: g[g.length - 1].charEnd,
107
+ }));
108
+ }
109
+
110
+ function chunkDocument(document) {
111
+ const content = String(document?.content || '').trim();
112
+ if (!content) return [];
113
+
114
+ const messages = messageBlocks(document);
115
+ if (messages.length) {
116
+ let searchOffset = 0;
117
+ return messages.map((message, chunkIndex) => {
118
+ let charStart = content.indexOf(message.content, searchOffset);
119
+ if (charStart < 0) charStart = searchOffset;
120
+ const charEnd = Math.min(content.length, charStart + message.content.length);
121
+ searchOffset = charEnd;
122
+ return {
123
+ chunkIndex,
124
+ charStart,
125
+ charEnd,
126
+ content: message.rendered,
127
+ contentHash: contentHash(message.rendered),
128
+ metadata: {
129
+ boundary: 'message',
130
+ messageId: message.messageId,
131
+ },
132
+ };
133
+ });
134
+ }
135
+
136
+ const sourceType = String(document?.sourceType || document?.normalizedType || '').toLowerCase();
137
+ const title = String(document?.title || '').toLowerCase();
138
+
139
+ let rawBlocks;
140
+ let boundaryType;
141
+
142
+ if (sourceType.includes('email') || title.endsWith('.eml') || title.includes('message')) {
143
+ rawBlocks = rawEmailBlocks(content);
144
+ boundaryType = 'email_thread';
145
+ } else if (
146
+ sourceType.includes('code') ||
147
+ title.endsWith('.js') ||
148
+ title.endsWith('.ts') ||
149
+ title.endsWith('.py') ||
150
+ title.endsWith('.java')
151
+ ) {
152
+ rawBlocks = rawCodeBlocks(content);
153
+ boundaryType = 'code_structure';
154
+ } else {
155
+ rawBlocks = rawStructuralBlocks(content);
156
+ boundaryType = 'structural';
157
+ }
158
+
159
+ const blocksWithPos = findBlockPositions(content, rawBlocks);
160
+ const packed = packBlocksWithPositions(blocksWithPos);
161
+
162
+ return packed.map((chunk, chunkIndex) => ({
163
+ chunkIndex,
164
+ charStart: chunk.charStart,
165
+ charEnd: chunk.charEnd,
166
+ content: chunk.content,
167
+ contentHash: contentHash(chunk.content),
168
+ metadata: { boundary: boundaryType },
169
+ }));
170
+ }
171
+
172
+ function overlapWindowChunks(chunks) {
173
+ if (chunks.length < 2) return [];
174
+ return chunks.slice(0, -1).map((chunk, i) => {
175
+ const next = chunks[i + 1];
176
+ const combined = `${chunk.content}\n\n${next.content}`;
177
+ return {
178
+ chunkIndex: -(i + 1),
179
+ charStart: chunk.charStart,
180
+ charEnd: next.charEnd,
181
+ content: combined.slice(0, MAX_CHARS * 2),
182
+ contentHash: contentHash(combined),
183
+ metadata: { boundary: 'overlap_window', baseChunkIndex: i },
184
+ };
185
+ });
186
+ }
187
+
188
+ module.exports = {
189
+ chunkDocument,
190
+ overlapWindowChunks,
191
+ };
@@ -11,6 +11,7 @@ const {
11
11
  parseJsonObject,
12
12
  safeTrim,
13
13
  } = require('./ingestion_support');
14
+ const { chunkDocument, overlapWindowChunks } = require('./ingestion_chunking');
14
15
 
15
16
  async function ingestDocuments(service, userId, documents = [], options = {}) {
16
17
  const agentId = resolveAgentId(userId, options.agentId || options.agent_id || null);
@@ -50,35 +51,104 @@ async function ingestDocuments(service, userId, documents = [], options = {}) {
50
51
  { agentId },
51
52
  );
52
53
  documentIds.push(documentId);
53
- const memoryId = await service.memoryManager.saveMemory(
54
- userId,
55
- `${document.title}: ${document.summary || document.content}`,
56
- SOURCE_MEMORY_CATEGORIES[document.normalizedType]
57
- || SOURCE_MEMORY_CATEGORIES[document.sourceType]
58
- || 'episodic',
59
- document.salience,
60
- {
61
- agentId,
62
- staleAfterDays: getFreshnessPolicy(document.sourceType).staleAfterDays,
63
- sourceRef: {
64
- sourceType: 'memory_ingestion',
65
- sourceId: document.externalObjectId,
66
- sourceLabel: document.title,
54
+ const retainedChunkIds = [];
55
+ const chunks = chunkDocument(document);
56
+ const savedChunkMemoryIds = [];
57
+ for (const chunk of chunks) {
58
+ const memoryId = await service.memoryManager.saveMemory(
59
+ userId,
60
+ `${document.title}\n${chunk.content}`,
61
+ SOURCE_MEMORY_CATEGORIES[document.normalizedType]
62
+ || SOURCE_MEMORY_CATEGORIES[document.sourceType]
63
+ || 'episodic',
64
+ document.salience,
65
+ {
66
+ agentId,
67
+ staleAfterDays: getFreshnessPolicy(document.sourceType).staleAfterDays,
68
+ sourceRef: {
69
+ sourceType: 'memory_ingestion',
70
+ sourceId: document.externalObjectId,
71
+ sourceLabel: document.title,
72
+ },
73
+ scope: {
74
+ scopeType: 'agent',
75
+ scopeId: agentId,
76
+ },
77
+ metadata: {
78
+ ingestionJobId: jobId,
79
+ ingestionDocumentId: documentId,
80
+ providerKey: document.providerKey || null,
81
+ connectionId: document.connectionId || null,
82
+ sourceType: document.sourceType,
83
+ trustLevel: 'external_source',
84
+ chunkIndex: chunk.chunkIndex,
85
+ sourceSpan: {
86
+ charStart: chunk.charStart,
87
+ charEnd: chunk.charEnd,
88
+ },
89
+ },
67
90
  },
68
- scope: {
69
- scopeType: 'agent',
70
- scopeId: agentId,
91
+ );
92
+ if (!memoryId) continue;
93
+ memoryIds.push(memoryId);
94
+ savedChunkMemoryIds.push(memoryId);
95
+ retainedChunkIds.push(service.memoryManager.replaceSourceChunk(
96
+ documentId,
97
+ chunk,
98
+ memoryId,
99
+ {
100
+ userId,
101
+ agentId,
102
+ sourceTimestamp: document.sourceTimestamp,
103
+ metadata: {
104
+ ingestionJobId: jobId,
105
+ externalObjectId: document.externalObjectId,
106
+ },
71
107
  },
72
- metadata: {
73
- ingestionJobId: jobId,
74
- ingestionDocumentId: documentId,
75
- providerKey: document.providerKey || null,
76
- connectionId: document.connectionId || null,
77
- sourceType: document.sourceType,
108
+ ));
109
+ }
110
+
111
+ const overlapChunks = overlapWindowChunks(chunks);
112
+ for (const window of overlapChunks) {
113
+ const windowMemoryId = await service.memoryManager.saveMemory(
114
+ userId,
115
+ `${document.title}\n${window.content}`,
116
+ SOURCE_MEMORY_CATEGORIES[document.normalizedType]
117
+ || SOURCE_MEMORY_CATEGORIES[document.sourceType]
118
+ || 'episodic',
119
+ document.salience,
120
+ {
121
+ agentId,
122
+ staleAfterDays: getFreshnessPolicy(document.sourceType).staleAfterDays,
123
+ sourceRef: {
124
+ sourceType: 'memory_ingestion',
125
+ sourceId: document.externalObjectId,
126
+ sourceLabel: document.title,
127
+ },
128
+ scope: {
129
+ scopeType: 'agent',
130
+ scopeId: agentId,
131
+ },
132
+ metadata: {
133
+ ingestionJobId: jobId,
134
+ ingestionDocumentId: documentId,
135
+ providerKey: document.providerKey || null,
136
+ connectionId: document.connectionId || null,
137
+ sourceType: document.sourceType,
138
+ trustLevel: 'external_source',
139
+ chunkIndex: window.chunkIndex,
140
+ sourceSpan: {
141
+ charStart: window.charStart,
142
+ charEnd: window.charEnd,
143
+ },
144
+ isOverlapWindow: true,
145
+ },
78
146
  },
79
- },
80
- );
81
- if (memoryId) memoryIds.push(memoryId);
147
+ );
148
+ if (windowMemoryId) savedChunkMemoryIds.push(windowMemoryId);
149
+ }
150
+
151
+ service.memoryManager.pruneSourceChunks(documentId, retainedChunkIds);
82
152
  }
83
153
 
84
154
  service.memoryManager.recordIngestionJob(userId, {
@@ -157,6 +157,7 @@ function scoreMemoryCandidate({
157
157
  entityRank = -1,
158
158
  baseScore = 0,
159
159
  importance = 5,
160
+ confidence = 0.7,
160
161
  accessCount = 0,
161
162
  freshness = 1,
162
163
  } = {}) {
@@ -166,8 +167,9 @@ function scoreMemoryCandidate({
166
167
  rankFuse(entityRank, 0.95)
167
168
  ) * 20;
168
169
  const quality = 0.15 + clamp(importance, 1, 10, 5) / 22;
170
+ const confidenceMultiplier = 0.65 + clamp(confidence, 0, 1, 0.7) * 0.35;
169
171
  const usage = Math.min(0.08, Math.log1p(Math.max(0, Number(accessCount) || 0)) / 50);
170
- return Math.max(baseScore, fused + quality + usage) * freshness;
172
+ return Math.max(baseScore, fused + quality + usage) * freshness * confidenceMultiplier;
171
173
  }
172
174
 
173
175
  module.exports = {