neoagent 2.4.1-beta.34 → 2.4.1-beta.36
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 +10 -3
- package/flutter_app/lib/main_admin.dart +13 -0
- package/flutter_app/lib/main_controller.dart +24 -6
- package/flutter_app/lib/main_models.dart +14 -0
- package/package.json +1 -1
- package/server/public/.last_build_id +1 -1
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +13767 -13696
- package/server/routes/mcp.js +29 -13
- package/server/routes/memory.js +1 -0
- package/server/services/ai/engine.js +35 -6
- package/server/services/ai/systemPrompt.js +28 -22
- package/server/services/ai/taskAnalysis.js +4 -1
- package/server/services/ai/toolSelector.js +8 -1
- package/server/services/ai/tools.js +12 -3
- package/server/services/desktop/screenRecorder.js +208 -98
- package/server/services/desktop/screen_recorder_support.js +46 -0
- package/server/services/manager.js +51 -53
- 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/messaging/automation.js +71 -134
- package/server/services/messaging/inbound_queue.js +111 -0
- package/server/services/messaging/typing_keepalive.js +110 -0
- package/server/services/tasks/runtime.js +133 -21
|
@@ -3,292 +3,136 @@
|
|
|
3
3
|
const { v4: uuidv4 } = require('uuid');
|
|
4
4
|
const db = require('../../db/database');
|
|
5
5
|
const { resolveAgentId } = require('../agents/manager');
|
|
6
|
-
const {
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
const FRESHNESS_POLICIES = Object.freeze({
|
|
22
|
-
email: Object.freeze({ intervalMinutes: 60, staleAfterDays: 14 }),
|
|
23
|
-
calendar: Object.freeze({ intervalMinutes: 180, staleAfterDays: 30 }),
|
|
24
|
-
chat: Object.freeze({ intervalMinutes: 60, staleAfterDays: 14 }),
|
|
25
|
-
docs: Object.freeze({ intervalMinutes: 360, staleAfterDays: 60 }),
|
|
26
|
-
tickets: Object.freeze({ intervalMinutes: 120, staleAfterDays: 30 }),
|
|
27
|
-
repos: Object.freeze({ intervalMinutes: 180, staleAfterDays: 45 }),
|
|
28
|
-
files: Object.freeze({ intervalMinutes: 360, staleAfterDays: 60 }),
|
|
29
|
-
crm: Object.freeze({ intervalMinutes: 240, staleAfterDays: 45 }),
|
|
30
|
-
payments: Object.freeze({ intervalMinutes: 360, staleAfterDays: 90 }),
|
|
31
|
-
notes: Object.freeze({ intervalMinutes: 360, staleAfterDays: 90 }),
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
const SOURCE_MEMORY_CATEGORIES = Object.freeze({
|
|
35
|
-
email: 'episodic',
|
|
36
|
-
calendar: 'events',
|
|
37
|
-
chat: 'episodic',
|
|
38
|
-
docs: 'projects',
|
|
39
|
-
tickets: 'tasks',
|
|
40
|
-
repos: 'projects',
|
|
41
|
-
files: 'episodic',
|
|
42
|
-
crm: 'contacts',
|
|
43
|
-
payments: 'events',
|
|
44
|
-
notes: 'episodic',
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
const INTEGRATION_SOURCE_TYPES = Object.freeze({
|
|
48
|
-
google_workspace: Object.freeze({
|
|
49
|
-
gmail: Object.freeze(['email']),
|
|
50
|
-
calendar: Object.freeze(['calendar']),
|
|
51
|
-
drive: Object.freeze(['files']),
|
|
52
|
-
docs: Object.freeze(['docs']),
|
|
53
|
-
sheets: Object.freeze(['docs', 'files']),
|
|
54
|
-
}),
|
|
55
|
-
microsoft_365: Object.freeze({
|
|
56
|
-
outlook: Object.freeze(['email']),
|
|
57
|
-
calendar: Object.freeze(['calendar']),
|
|
58
|
-
onedrive: Object.freeze(['files']),
|
|
59
|
-
teams: Object.freeze(['chat']),
|
|
60
|
-
}),
|
|
61
|
-
github: Object.freeze({
|
|
62
|
-
repos: Object.freeze(['repos', 'tickets']),
|
|
63
|
-
}),
|
|
64
|
-
slack: Object.freeze({
|
|
65
|
-
slack: Object.freeze(['chat']),
|
|
66
|
-
}),
|
|
67
|
-
whatsapp: Object.freeze({
|
|
68
|
-
personal: Object.freeze(['chat']),
|
|
69
|
-
}),
|
|
70
|
-
notion: Object.freeze({
|
|
71
|
-
notion: Object.freeze(['notes', 'docs']),
|
|
72
|
-
}),
|
|
73
|
-
trello: Object.freeze({
|
|
74
|
-
trello: Object.freeze(['tickets']),
|
|
75
|
-
}),
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
function safeTrim(value, maxLength = 240) {
|
|
79
|
-
return String(value || '').trim().slice(0, maxLength);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function parseJsonObject(value, fallback = {}) {
|
|
83
|
-
if (!value) return { ...fallback };
|
|
84
|
-
if (typeof value === 'object' && !Array.isArray(value)) return { ...value };
|
|
85
|
-
try {
|
|
86
|
-
const parsed = JSON.parse(String(value));
|
|
87
|
-
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
88
|
-
? parsed
|
|
89
|
-
: { ...fallback };
|
|
90
|
-
} catch {
|
|
91
|
-
return { ...fallback };
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function normalizeSourceType(value) {
|
|
96
|
-
const normalized = String(value || '').trim().toLowerCase();
|
|
97
|
-
return SOURCE_TYPES.includes(normalized) ? normalized : 'notes';
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
function getFreshnessPolicy(sourceType) {
|
|
101
|
-
const normalized = normalizeSourceType(sourceType);
|
|
102
|
-
return { ...FRESHNESS_POLICIES[normalized] };
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
function nextSyncFromPolicy(policy, now = Date.now()) {
|
|
106
|
-
const intervalMinutes = Math.max(15, Number(policy?.intervalMinutes) || 360);
|
|
107
|
-
return new Date(now + intervalMinutes * 60 * 1000).toISOString();
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function normalizeDocument(raw = {}, defaults = {}) {
|
|
111
|
-
const sourceType = normalizeSourceType(raw.sourceType || defaults.sourceType);
|
|
112
|
-
const externalObjectId = safeTrim(raw.externalObjectId || raw.id || raw.sourceId, 180);
|
|
113
|
-
const contentSource = raw.content ?? raw.text ?? raw.body ?? '';
|
|
114
|
-
const content = safeTrim(contentSource, 12000);
|
|
115
|
-
if (!externalObjectId || !content) {
|
|
116
|
-
throw new Error('Normalized memory documents require externalObjectId and content.');
|
|
117
|
-
}
|
|
118
|
-
const compacted = compactTextPayload(content, { maxChars: 2400, maxLines: 50 });
|
|
119
|
-
return {
|
|
120
|
-
sourceType,
|
|
121
|
-
normalizedType: normalizeSourceType(raw.normalizedType || raw.memoryType || sourceType),
|
|
122
|
-
providerKey: safeTrim(raw.providerKey || defaults.providerKey, 80),
|
|
123
|
-
connectionId: Number.isInteger(Number(raw.connectionId ?? defaults.connectionId))
|
|
124
|
-
? Number(raw.connectionId ?? defaults.connectionId)
|
|
125
|
-
: 0,
|
|
126
|
-
externalObjectId,
|
|
127
|
-
sourceAccount: safeTrim(raw.sourceAccount || defaults.sourceAccount, 180),
|
|
128
|
-
title: safeTrim(raw.title || raw.subject || raw.name || externalObjectId, 220),
|
|
129
|
-
content,
|
|
130
|
-
summary: safeTrim(raw.summary || compacted.text, 1200),
|
|
131
|
-
salience: Math.max(1, Math.min(10, Number(raw.salience) || 5)),
|
|
132
|
-
sourceTimestamp: safeTrim(raw.sourceTimestamp || raw.updatedAt || raw.createdAt, 80) || null,
|
|
133
|
-
metadata: {
|
|
134
|
-
...parseJsonObject(raw.metadata, {}),
|
|
135
|
-
compaction: compacted.metrics,
|
|
136
|
-
},
|
|
137
|
-
payload: parseJsonObject(raw.payload, {}),
|
|
138
|
-
};
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function sourceTypesForConnection(providerKey, appKey) {
|
|
142
|
-
const providerMap = INTEGRATION_SOURCE_TYPES[String(providerKey || '').trim()] || {};
|
|
143
|
-
return Array.from(providerMap[String(appKey || '').trim()] || []);
|
|
144
|
-
}
|
|
6
|
+
const { getErrorMessage } = require('../bootstrap_helpers');
|
|
7
|
+
const { ingestDocuments } = require('./ingestion_documents');
|
|
8
|
+
const {
|
|
9
|
+
decorateProviderSnapshot,
|
|
10
|
+
listConnectionStatuses,
|
|
11
|
+
} = require('./ingestion_coverage');
|
|
12
|
+
const {
|
|
13
|
+
FRESHNESS_POLICIES,
|
|
14
|
+
SOURCE_TYPES,
|
|
15
|
+
getFreshnessPolicy,
|
|
16
|
+
nextSyncFromPolicy,
|
|
17
|
+
normalizeSourceType,
|
|
18
|
+
sourceTypesForConnection,
|
|
19
|
+
} = require('./ingestion_support');
|
|
145
20
|
|
|
146
|
-
|
|
147
|
-
const dataDomains = sourceTypesForConnection(connection.provider_key, connection.app_key);
|
|
148
|
-
const supported = dataDomains.length > 0;
|
|
149
|
-
return {
|
|
150
|
-
supported,
|
|
151
|
-
contributesToMemory: supported,
|
|
152
|
-
contributesToTaskExecution: String(connection.status || '') === 'connected',
|
|
153
|
-
status: latestJob?.status || (supported ? 'ready' : 'not_supported'),
|
|
154
|
-
dataDomains,
|
|
155
|
-
lastRefreshAt: latestJob?.completedAt || latestJob?.updatedAt || null,
|
|
156
|
-
nextRefreshAt: latestJob?.nextSyncAt || null,
|
|
157
|
-
documentCount: Number(latestJob?.documentCount || 0),
|
|
158
|
-
error: latestJob?.error || null,
|
|
159
|
-
};
|
|
160
|
-
}
|
|
21
|
+
const DEFAULT_REFRESH_INTERVAL_MS = 10 * 60 * 1000;
|
|
161
22
|
|
|
162
23
|
class MemoryIngestionService {
|
|
163
|
-
constructor({
|
|
24
|
+
constructor({
|
|
25
|
+
memoryManager,
|
|
26
|
+
integrationManager,
|
|
27
|
+
intervalMs = DEFAULT_REFRESH_INTERVAL_MS,
|
|
28
|
+
database = db,
|
|
29
|
+
setIntervalFn = setInterval,
|
|
30
|
+
clearIntervalFn = clearInterval,
|
|
31
|
+
} = {}) {
|
|
164
32
|
this.memoryManager = memoryManager;
|
|
165
33
|
this.integrationManager = integrationManager;
|
|
166
|
-
this.intervalMs = intervalMs;
|
|
34
|
+
this.intervalMs = Number(intervalMs);
|
|
35
|
+
if (!Number.isSafeInteger(this.intervalMs) || this.intervalMs < 1000) {
|
|
36
|
+
throw new Error('Memory ingestion intervalMs must be an integer greater than or equal to 1000.');
|
|
37
|
+
}
|
|
38
|
+
this.db = database;
|
|
39
|
+
this.setInterval = setIntervalFn;
|
|
40
|
+
this.clearInterval = clearIntervalFn;
|
|
167
41
|
this.timer = null;
|
|
168
|
-
this.
|
|
42
|
+
this.activeBatches = new Map();
|
|
43
|
+
this.activeConnections = new Map();
|
|
44
|
+
this.stopping = false;
|
|
45
|
+
this.stopPromise = null;
|
|
46
|
+
this.state = 'idle';
|
|
47
|
+
this.lastRunAt = null;
|
|
48
|
+
this.lastCompletedAt = null;
|
|
49
|
+
this.lastError = null;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
getStatus() {
|
|
53
|
+
return {
|
|
54
|
+
state: this.state,
|
|
55
|
+
intervalMs: this.intervalMs,
|
|
56
|
+
activeBatchCount: this.activeBatches.size,
|
|
57
|
+
activeConnectionCount: this.activeConnections.size,
|
|
58
|
+
lastRunAt: this.lastRunAt,
|
|
59
|
+
lastCompletedAt: this.lastCompletedAt,
|
|
60
|
+
lastError: this.lastError,
|
|
61
|
+
};
|
|
169
62
|
}
|
|
170
63
|
|
|
171
64
|
start() {
|
|
172
|
-
if (this.timer) return;
|
|
173
|
-
this.
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
65
|
+
if (this.timer) return this.getStatus();
|
|
66
|
+
if (this.stopPromise) {
|
|
67
|
+
throw new Error('Memory ingestion cannot start while shutdown is in progress.');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
this.stopping = false;
|
|
71
|
+
this.state = 'running';
|
|
72
|
+
this.lastError = null;
|
|
73
|
+
this.timer = this.setInterval(() => {
|
|
74
|
+
void this._runBackgroundRefresh();
|
|
177
75
|
}, this.intervalMs);
|
|
178
76
|
this.timer.unref?.();
|
|
77
|
+
void this._runBackgroundRefresh();
|
|
78
|
+
return this.getStatus();
|
|
179
79
|
}
|
|
180
80
|
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
81
|
+
async _runBackgroundRefresh() {
|
|
82
|
+
try {
|
|
83
|
+
await this.refreshDueConnections();
|
|
84
|
+
} catch (err) {
|
|
85
|
+
this.lastError = getErrorMessage(err);
|
|
86
|
+
console.warn('[MemoryIngestion] Background refresh failed:', this.lastError);
|
|
87
|
+
}
|
|
184
88
|
}
|
|
185
89
|
|
|
186
|
-
|
|
187
|
-
|
|
90
|
+
async stop() {
|
|
91
|
+
if (this.stopPromise) return this.stopPromise;
|
|
92
|
+
this.stopping = true;
|
|
93
|
+
this.state = 'stopping';
|
|
94
|
+
if (this.timer) this.clearInterval(this.timer);
|
|
95
|
+
this.timer = null;
|
|
96
|
+
|
|
97
|
+
this.stopPromise = (async () => {
|
|
98
|
+
while (this.activeBatches.size > 0 || this.activeConnections.size > 0) {
|
|
99
|
+
await Promise.allSettled([
|
|
100
|
+
...this.activeBatches.values(),
|
|
101
|
+
...this.activeConnections.values(),
|
|
102
|
+
]);
|
|
103
|
+
}
|
|
104
|
+
this.stopping = false;
|
|
105
|
+
this.state = 'stopped';
|
|
106
|
+
return this.getStatus();
|
|
107
|
+
})().finally(() => {
|
|
108
|
+
this.stopPromise = null;
|
|
109
|
+
});
|
|
110
|
+
return this.stopPromise;
|
|
188
111
|
}
|
|
189
112
|
|
|
190
113
|
async ingestDocuments(userId, documents = [], options = {}) {
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
.map((document) => normalizeDocument(document, {
|
|
194
|
-
sourceType: options.sourceType,
|
|
195
|
-
providerKey: options.providerKey,
|
|
196
|
-
connectionId: options.connectionId,
|
|
197
|
-
sourceAccount: options.sourceAccount,
|
|
198
|
-
}));
|
|
199
|
-
const sourceType = normalizeSourceType(options.sourceType || normalizedDocs[0]?.sourceType);
|
|
200
|
-
const policy = getFreshnessPolicy(sourceType);
|
|
201
|
-
const jobId = uuidv4();
|
|
114
|
+
return ingestDocuments(this, userId, documents, options);
|
|
115
|
+
}
|
|
202
116
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
metadata: parseJsonObject(options.metadata, {}),
|
|
211
|
-
documentCount: 0,
|
|
212
|
-
nextSyncAt: nextSyncFromPolicy(policy),
|
|
213
|
-
}, { agentId });
|
|
117
|
+
refreshDueConnections(userId = null) {
|
|
118
|
+
const scopeKey = userId == null ? 'all' : `user:${userId}`;
|
|
119
|
+
if (this.stopping) {
|
|
120
|
+
return Promise.resolve({ skipped: true, reason: 'service_stopping' });
|
|
121
|
+
}
|
|
122
|
+
const active = this.activeBatches.get(scopeKey);
|
|
123
|
+
if (active) return active;
|
|
214
124
|
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
for (const document of normalizedDocs) {
|
|
219
|
-
const documentId = this.memoryManager.upsertIngestionDocument(userId, document, { agentId });
|
|
220
|
-
documentIds.push(documentId);
|
|
221
|
-
const memoryId = await this.memoryManager.saveMemory(
|
|
222
|
-
userId,
|
|
223
|
-
`${document.title}: ${document.summary || document.content}`,
|
|
224
|
-
SOURCE_MEMORY_CATEGORIES[document.normalizedType] || SOURCE_MEMORY_CATEGORIES[document.sourceType] || 'episodic',
|
|
225
|
-
document.salience,
|
|
226
|
-
{
|
|
227
|
-
agentId,
|
|
228
|
-
staleAfterDays: getFreshnessPolicy(document.sourceType).staleAfterDays,
|
|
229
|
-
sourceRef: {
|
|
230
|
-
sourceType: 'memory_ingestion',
|
|
231
|
-
sourceId: document.externalObjectId,
|
|
232
|
-
sourceLabel: document.title,
|
|
233
|
-
},
|
|
234
|
-
scope: {
|
|
235
|
-
scopeType: 'agent',
|
|
236
|
-
scopeId: agentId,
|
|
237
|
-
},
|
|
238
|
-
metadata: {
|
|
239
|
-
ingestionJobId: jobId,
|
|
240
|
-
ingestionDocumentId: documentId,
|
|
241
|
-
providerKey: document.providerKey || null,
|
|
242
|
-
connectionId: document.connectionId || null,
|
|
243
|
-
sourceType: document.sourceType,
|
|
244
|
-
},
|
|
245
|
-
},
|
|
246
|
-
);
|
|
247
|
-
if (memoryId) memoryIds.push(memoryId);
|
|
125
|
+
const promise = this._refreshDueConnections(userId).finally(() => {
|
|
126
|
+
if (this.activeBatches.get(scopeKey) === promise) {
|
|
127
|
+
this.activeBatches.delete(scopeKey);
|
|
248
128
|
}
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
sourceType,
|
|
253
|
-
providerKey: safeTrim(options.providerKey, 80),
|
|
254
|
-
connectionId: Number.isInteger(Number(options.connectionId)) ? Number(options.connectionId) : 0,
|
|
255
|
-
status: 'completed',
|
|
256
|
-
freshnessPolicy: policy,
|
|
257
|
-
summary: { documentIds, memoryIds },
|
|
258
|
-
metadata: parseJsonObject(options.metadata, {}),
|
|
259
|
-
documentCount: documentIds.length,
|
|
260
|
-
completedAt: new Date().toISOString(),
|
|
261
|
-
nextSyncAt: nextSyncFromPolicy(policy),
|
|
262
|
-
}, { agentId });
|
|
263
|
-
|
|
264
|
-
const knowledgeViews = this.memoryManager.materializeKnowledgeViews(userId, { agentId });
|
|
265
|
-
return {
|
|
266
|
-
jobId,
|
|
267
|
-
status: 'completed',
|
|
268
|
-
documentIds,
|
|
269
|
-
memoryIds,
|
|
270
|
-
knowledgeViews,
|
|
271
|
-
};
|
|
272
|
-
} catch (err) {
|
|
273
|
-
this.memoryManager.recordIngestionJob(userId, {
|
|
274
|
-
id: jobId,
|
|
275
|
-
sourceType,
|
|
276
|
-
providerKey: safeTrim(options.providerKey, 80),
|
|
277
|
-
connectionId: Number.isInteger(Number(options.connectionId)) ? Number(options.connectionId) : 0,
|
|
278
|
-
status: 'failed',
|
|
279
|
-
freshnessPolicy: policy,
|
|
280
|
-
documentCount: documentIds.length,
|
|
281
|
-
error: err.message,
|
|
282
|
-
completedAt: new Date().toISOString(),
|
|
283
|
-
nextSyncAt: nextSyncFromPolicy(policy),
|
|
284
|
-
}, { agentId });
|
|
285
|
-
throw err;
|
|
286
|
-
}
|
|
129
|
+
});
|
|
130
|
+
this.activeBatches.set(scopeKey, promise);
|
|
131
|
+
return promise;
|
|
287
132
|
}
|
|
288
133
|
|
|
289
|
-
async
|
|
290
|
-
|
|
291
|
-
this.running = true;
|
|
134
|
+
async _refreshDueConnections(userId = null) {
|
|
135
|
+
this.lastRunAt = new Date().toISOString();
|
|
292
136
|
try {
|
|
293
137
|
const params = [];
|
|
294
138
|
let sql = `SELECT *
|
|
@@ -298,14 +142,75 @@ class MemoryIngestionService {
|
|
|
298
142
|
sql += ' AND user_id = ?';
|
|
299
143
|
params.push(userId);
|
|
300
144
|
}
|
|
301
|
-
const connections = db.prepare(sql).all(...params);
|
|
145
|
+
const connections = this.db.prepare(sql).all(...params);
|
|
302
146
|
const results = [];
|
|
303
147
|
for (const connection of connections) {
|
|
304
|
-
|
|
148
|
+
if (this.stopping) break;
|
|
149
|
+
results.push(await this._refreshConnectionSafely(connection));
|
|
150
|
+
}
|
|
151
|
+
if (!results.some((result) => result?.status === 'failed')) {
|
|
152
|
+
this.lastError = null;
|
|
305
153
|
}
|
|
154
|
+
this.lastCompletedAt = new Date().toISOString();
|
|
306
155
|
return { refreshed: results.length, results };
|
|
307
|
-
}
|
|
308
|
-
this.
|
|
156
|
+
} catch (err) {
|
|
157
|
+
this.lastError = getErrorMessage(err);
|
|
158
|
+
throw err;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
_refreshConnectionSafely(connection) {
|
|
163
|
+
const connectionKey = `${connection.user_id}:${connection.id}`;
|
|
164
|
+
const active = this.activeConnections.get(connectionKey);
|
|
165
|
+
if (active) return active;
|
|
166
|
+
|
|
167
|
+
const promise = Promise.resolve()
|
|
168
|
+
.then(() => this.refreshConnection(connection))
|
|
169
|
+
.catch((err) => {
|
|
170
|
+
const error = getErrorMessage(err);
|
|
171
|
+
this.lastError = error;
|
|
172
|
+
console.warn(
|
|
173
|
+
`[MemoryIngestion] Connection ${connection.id} refresh failed:`,
|
|
174
|
+
error,
|
|
175
|
+
);
|
|
176
|
+
return {
|
|
177
|
+
connectionId: connection.id,
|
|
178
|
+
status: 'failed',
|
|
179
|
+
error,
|
|
180
|
+
};
|
|
181
|
+
})
|
|
182
|
+
.finally(() => {
|
|
183
|
+
if (this.activeConnections.get(connectionKey) === promise) {
|
|
184
|
+
this.activeConnections.delete(connectionKey);
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
this.activeConnections.set(connectionKey, promise);
|
|
188
|
+
return promise;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
_recordCollectorFailure(connection, sourceType, policy, agentId, err) {
|
|
192
|
+
try {
|
|
193
|
+
this.memoryManager.recordIngestionJob(connection.user_id, {
|
|
194
|
+
id: uuidv4(),
|
|
195
|
+
sourceType,
|
|
196
|
+
providerKey: connection.provider_key,
|
|
197
|
+
connectionId: connection.id,
|
|
198
|
+
status: 'failed',
|
|
199
|
+
freshnessPolicy: policy,
|
|
200
|
+
documentCount: 0,
|
|
201
|
+
error: getErrorMessage(err),
|
|
202
|
+
completedAt: new Date().toISOString(),
|
|
203
|
+
nextSyncAt: nextSyncFromPolicy(policy),
|
|
204
|
+
metadata: {
|
|
205
|
+
appKey: connection.app_key,
|
|
206
|
+
sourceTypes: sourceTypesForConnection(connection.provider_key, connection.app_key),
|
|
207
|
+
},
|
|
208
|
+
}, { agentId });
|
|
209
|
+
} catch (recordError) {
|
|
210
|
+
console.warn(
|
|
211
|
+
`[MemoryIngestion] Could not persist failure for connection ${connection.id}:`,
|
|
212
|
+
getErrorMessage(recordError),
|
|
213
|
+
);
|
|
309
214
|
}
|
|
310
215
|
}
|
|
311
216
|
|
|
@@ -331,11 +236,17 @@ class MemoryIngestionService {
|
|
|
331
236
|
}
|
|
332
237
|
|
|
333
238
|
if (typeof provider?.collectMemoryDocuments === 'function') {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
239
|
+
let collected;
|
|
240
|
+
try {
|
|
241
|
+
collected = await provider.collectMemoryDocuments({
|
|
242
|
+
connection,
|
|
243
|
+
sourceTypes,
|
|
244
|
+
cursor: latestJob?.cursor || {},
|
|
245
|
+
});
|
|
246
|
+
} catch (err) {
|
|
247
|
+
this._recordCollectorFailure(connection, primarySource, policy, agentId, err);
|
|
248
|
+
throw err;
|
|
249
|
+
}
|
|
339
250
|
return this.ingestDocuments(connection.user_id, collected.documents || [], {
|
|
340
251
|
agentId,
|
|
341
252
|
sourceType: primarySource,
|
|
@@ -373,107 +284,11 @@ class MemoryIngestionService {
|
|
|
373
284
|
}
|
|
374
285
|
|
|
375
286
|
listConnectionStatuses(userId, { agentId = null } = {}) {
|
|
376
|
-
|
|
377
|
-
const connections = db.prepare(
|
|
378
|
-
`SELECT *
|
|
379
|
-
FROM integration_connections
|
|
380
|
-
WHERE user_id = ? AND agent_id = ?
|
|
381
|
-
ORDER BY updated_at DESC, id DESC`,
|
|
382
|
-
).all(userId, scopedAgentId);
|
|
383
|
-
const jobs = this.memoryManager.listIngestionJobs(userId, { agentId: scopedAgentId, limit: 100 });
|
|
384
|
-
return connections.map((connection) => {
|
|
385
|
-
const latestJob = jobs.find((job) => Number(job.connectionId || 0) === Number(connection.id));
|
|
386
|
-
return {
|
|
387
|
-
connectionId: connection.id,
|
|
388
|
-
providerKey: connection.provider_key,
|
|
389
|
-
appKey: connection.app_key,
|
|
390
|
-
accountEmail: connection.account_email || null,
|
|
391
|
-
...buildCoverageForConnection(connection, latestJob),
|
|
392
|
-
};
|
|
393
|
-
});
|
|
287
|
+
return listConnectionStatuses(this, userId, { agentId });
|
|
394
288
|
}
|
|
395
289
|
|
|
396
290
|
decorateProviderSnapshot(snapshot, userId, agentId = null) {
|
|
397
|
-
|
|
398
|
-
const scopedAgentId = resolveAgentId(userId, agentId);
|
|
399
|
-
const connections = db.prepare(
|
|
400
|
-
`SELECT *
|
|
401
|
-
FROM integration_connections
|
|
402
|
-
WHERE user_id = ? AND agent_id = ? AND provider_key = ?`,
|
|
403
|
-
).all(userId, scopedAgentId, snapshot.provider);
|
|
404
|
-
const jobs = this.memoryManager.listIngestionJobs(userId, {
|
|
405
|
-
agentId: scopedAgentId,
|
|
406
|
-
providerKey: snapshot.provider,
|
|
407
|
-
limit: 100,
|
|
408
|
-
});
|
|
409
|
-
const latestJobForConnection = (connectionId) =>
|
|
410
|
-
jobs.find((job) => Number(job.connectionId || 0) === Number(connectionId));
|
|
411
|
-
|
|
412
|
-
const connectionCoverage = connections.map((connection) => ({
|
|
413
|
-
connectionId: connection.id,
|
|
414
|
-
appKey: connection.app_key,
|
|
415
|
-
accountEmail: connection.account_email || null,
|
|
416
|
-
...buildCoverageForConnection(connection, latestJobForConnection(connection.id)),
|
|
417
|
-
}));
|
|
418
|
-
const providerDomains = Array.from(new Set(connectionCoverage.flatMap((item) => item.dataDomains || [])));
|
|
419
|
-
const providerJob = jobs[0] || null;
|
|
420
|
-
const decorated = {
|
|
421
|
-
...snapshot,
|
|
422
|
-
memoryCoverage: {
|
|
423
|
-
supported: providerDomains.length > 0,
|
|
424
|
-
contributesToMemory: providerDomains.length > 0,
|
|
425
|
-
contributesToTaskExecution: Boolean(snapshot.connection?.connected),
|
|
426
|
-
status: providerJob?.status || (providerDomains.length > 0 ? 'ready' : 'not_supported'),
|
|
427
|
-
dataDomains: providerDomains,
|
|
428
|
-
documentCount: connectionCoverage.reduce((sum, item) => sum + Number(item.documentCount || 0), 0),
|
|
429
|
-
lastRefreshAt: providerJob?.completedAt || providerJob?.updatedAt || null,
|
|
430
|
-
nextRefreshAt: providerJob?.nextSyncAt || null,
|
|
431
|
-
error: providerJob?.error || null,
|
|
432
|
-
},
|
|
433
|
-
};
|
|
434
|
-
|
|
435
|
-
decorated.apps = (snapshot.apps || []).map((app) => {
|
|
436
|
-
const appConnections = connectionCoverage.filter((item) => item.appKey === app.id);
|
|
437
|
-
const dataDomains = Array.from(new Set([
|
|
438
|
-
...sourceTypesForConnection(snapshot.provider, app.id),
|
|
439
|
-
...appConnections.flatMap((item) => item.dataDomains || []),
|
|
440
|
-
]));
|
|
441
|
-
const latest = appConnections[0] || null;
|
|
442
|
-
return {
|
|
443
|
-
...app,
|
|
444
|
-
memoryCoverage: {
|
|
445
|
-
supported: dataDomains.length > 0,
|
|
446
|
-
contributesToMemory: dataDomains.length > 0 && app.connection?.connected === true,
|
|
447
|
-
contributesToTaskExecution: app.connection?.connected === true,
|
|
448
|
-
status: latest?.status || (dataDomains.length > 0 ? 'ready' : 'not_supported'),
|
|
449
|
-
dataDomains,
|
|
450
|
-
documentCount: appConnections.reduce((sum, item) => sum + Number(item.documentCount || 0), 0),
|
|
451
|
-
lastRefreshAt: latest?.lastRefreshAt || null,
|
|
452
|
-
nextRefreshAt: latest?.nextRefreshAt || null,
|
|
453
|
-
error: latest?.error || null,
|
|
454
|
-
},
|
|
455
|
-
accounts: (app.accounts || []).map((account) => {
|
|
456
|
-
const accountCoverage = appConnections.find((item) =>
|
|
457
|
-
Number(item.connectionId) === Number(account.connectionId),
|
|
458
|
-
);
|
|
459
|
-
return {
|
|
460
|
-
...account,
|
|
461
|
-
memoryCoverage: accountCoverage || {
|
|
462
|
-
supported: dataDomains.length > 0,
|
|
463
|
-
contributesToMemory: false,
|
|
464
|
-
contributesToTaskExecution: app.connection?.connected === true,
|
|
465
|
-
status: dataDomains.length > 0 ? 'ready' : 'not_supported',
|
|
466
|
-
dataDomains,
|
|
467
|
-
documentCount: 0,
|
|
468
|
-
lastRefreshAt: null,
|
|
469
|
-
nextRefreshAt: null,
|
|
470
|
-
error: null,
|
|
471
|
-
},
|
|
472
|
-
};
|
|
473
|
-
}),
|
|
474
|
-
};
|
|
475
|
-
});
|
|
476
|
-
return decorated;
|
|
291
|
+
return decorateProviderSnapshot(this, snapshot, userId, agentId);
|
|
477
292
|
}
|
|
478
293
|
}
|
|
479
294
|
|