neoagent 2.3.1-beta.63 → 2.3.1-beta.64
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/flutter_app/lib/main.dart +1 -0
- package/flutter_app/lib/main_account_settings.dart +50 -22
- package/flutter_app/lib/main_admin.dart +24 -10
- package/flutter_app/lib/main_app_shell.dart +10 -9
- package/flutter_app/lib/main_chat.dart +433 -309
- package/flutter_app/lib/main_controller.dart +2 -1
- package/flutter_app/lib/main_integrations.dart +15 -7
- package/flutter_app/lib/main_models.dart +116 -7
- package/flutter_app/lib/main_navigation.dart +27 -18
- package/flutter_app/lib/main_operations.dart +162 -91
- package/flutter_app/lib/main_settings.dart +268 -71
- package/flutter_app/lib/main_shared.dart +4 -6
- package/flutter_app/lib/main_unified.dart +388 -0
- package/package.json +1 -1
- package/server/db/database.js +52 -0
- package/server/public/.last_build_id +1 -1
- package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
- package/server/public/flutter_bootstrap.js +1 -1
- package/server/public/main.dart.js +77499 -76627
- package/server/routes/agents.js +2 -1
- package/server/routes/memory.js +75 -3
- package/server/routes/widgets.js +4 -4
- package/server/services/ai/engine.js +86 -0
- package/server/services/ai/runEvents.js +100 -0
- package/server/services/memory/manager.js +242 -26
- package/server/services/websocket.js +3 -1
- package/server/services/widgets/focus_widget.js +126 -0
- package/server/services/widgets/service.js +130 -2
|
@@ -46,12 +46,112 @@ const MEMORY_DIR = path.join(DATA_DIR, 'memory');
|
|
|
46
46
|
const SKILLS_DIR = path.join(DATA_DIR, 'skills');
|
|
47
47
|
const USERS_DIR = path.join(DATA_DIR, 'users');
|
|
48
48
|
|
|
49
|
-
// Memory categories
|
|
50
|
-
const CATEGORIES = [
|
|
49
|
+
// Memory categories / v2 types
|
|
50
|
+
const CATEGORIES = [
|
|
51
|
+
'identity',
|
|
52
|
+
'preferences',
|
|
53
|
+
'projects',
|
|
54
|
+
'contacts',
|
|
55
|
+
'events',
|
|
56
|
+
'tasks',
|
|
57
|
+
'episodic',
|
|
58
|
+
'assistant_self',
|
|
59
|
+
'user_fact',
|
|
60
|
+
'preference',
|
|
61
|
+
'personality',
|
|
62
|
+
];
|
|
51
63
|
|
|
52
64
|
// Core memory keys (always injected into every prompt)
|
|
53
65
|
const CORE_KEYS = ['user_profile', 'preferences', 'ai_personality'];
|
|
54
66
|
|
|
67
|
+
const CATEGORY_ALIASES = {
|
|
68
|
+
user_fact: 'identity',
|
|
69
|
+
preference: 'preferences',
|
|
70
|
+
personality: 'assistant_self',
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
function normalizeMemoryCategory(value) {
|
|
74
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
75
|
+
if (CATEGORY_ALIASES[normalized]) return CATEGORY_ALIASES[normalized];
|
|
76
|
+
return CATEGORIES.includes(normalized) ? normalized : 'episodic';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function normalizeScope(input, fallbackAgentId) {
|
|
80
|
+
const raw = input && typeof input === 'object' && !Array.isArray(input) ? input : {};
|
|
81
|
+
const scopeType = String(raw.scopeType || raw.type || 'agent').trim().toLowerCase();
|
|
82
|
+
const scopeId = String(raw.scopeId || raw.id || '').trim() || null;
|
|
83
|
+
switch (scopeType) {
|
|
84
|
+
case 'conversation':
|
|
85
|
+
case 'task':
|
|
86
|
+
case 'channel':
|
|
87
|
+
case 'shared':
|
|
88
|
+
return { scopeType, scopeId };
|
|
89
|
+
default:
|
|
90
|
+
return { scopeType: 'agent', scopeId: fallbackAgentId || null };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function normalizeSourceRef(input = {}) {
|
|
95
|
+
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
|
96
|
+
return { sourceType: null, sourceId: null, sourceLabel: null };
|
|
97
|
+
}
|
|
98
|
+
return {
|
|
99
|
+
sourceType: String(input.sourceType || input.type || '').trim().slice(0, 48) || null,
|
|
100
|
+
sourceId: String(input.sourceId || input.id || '').trim().slice(0, 128) || null,
|
|
101
|
+
sourceLabel: String(input.sourceLabel || input.label || '').trim().slice(0, 160) || null,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function parseJsonObject(value, fallback = {}) {
|
|
106
|
+
if (!value) return { ...fallback };
|
|
107
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
108
|
+
return { ...value };
|
|
109
|
+
}
|
|
110
|
+
try {
|
|
111
|
+
const parsed = JSON.parse(String(value));
|
|
112
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
113
|
+
? parsed
|
|
114
|
+
: { ...fallback };
|
|
115
|
+
} catch {
|
|
116
|
+
return { ...fallback };
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function computeFreshnessMultiplier(row) {
|
|
121
|
+
const staleAfterDays = Number(row?.stale_after_days);
|
|
122
|
+
if (!Number.isFinite(staleAfterDays) || staleAfterDays <= 0) return 1;
|
|
123
|
+
const updatedAt = Date.parse(row?.updated_at || row?.created_at || '');
|
|
124
|
+
if (!Number.isFinite(updatedAt)) return 1;
|
|
125
|
+
const ageDays = Math.max(0, (Date.now() - updatedAt) / (1000 * 60 * 60 * 24));
|
|
126
|
+
if (ageDays <= staleAfterDays) return 1;
|
|
127
|
+
return Math.max(0.35, 1 - ((ageDays - staleAfterDays) / Math.max(staleAfterDays, 1)) * 0.2);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function serializeMemoryRow(row) {
|
|
131
|
+
const metadata = parseJsonObject(row?.metadata_json, {});
|
|
132
|
+
return {
|
|
133
|
+
id: row.id,
|
|
134
|
+
category: normalizeMemoryCategory(row.category),
|
|
135
|
+
content: row.content,
|
|
136
|
+
importance: Number(row.importance || 0),
|
|
137
|
+
access_count: Number(row.access_count || 0),
|
|
138
|
+
archived: Number(row.archived || 0),
|
|
139
|
+
created_at: row.created_at,
|
|
140
|
+
updated_at: row.updated_at,
|
|
141
|
+
sourceRef: {
|
|
142
|
+
sourceType: row.source_type || null,
|
|
143
|
+
sourceId: row.source_id || null,
|
|
144
|
+
sourceLabel: row.source_label || null,
|
|
145
|
+
},
|
|
146
|
+
scope: {
|
|
147
|
+
scopeType: row.scope_type || 'agent',
|
|
148
|
+
scopeId: row.scope_id || null,
|
|
149
|
+
},
|
|
150
|
+
staleAfterDays: row.stale_after_days == null ? null : Number(row.stale_after_days),
|
|
151
|
+
metadata,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
55
155
|
function parseStringSetting(value) {
|
|
56
156
|
if (typeof value !== 'string') return '';
|
|
57
157
|
try {
|
|
@@ -165,6 +265,49 @@ class MemoryManager {
|
|
|
165
265
|
).run(userId, agentId, 'assistant_behavior_notes', String(content || ''));
|
|
166
266
|
}
|
|
167
267
|
|
|
268
|
+
getAssistantSelfState(userId, options = {}) {
|
|
269
|
+
if (userId == null) {
|
|
270
|
+
return { identity: {}, focus: {} };
|
|
271
|
+
}
|
|
272
|
+
const agentId = this._agentId(userId, options);
|
|
273
|
+
const row = db.prepare(
|
|
274
|
+
'SELECT identity_json, focus_json, updated_at FROM assistant_self_state WHERE user_id = ? AND agent_id = ?'
|
|
275
|
+
).get(userId, agentId);
|
|
276
|
+
return {
|
|
277
|
+
identity: parseJsonObject(row?.identity_json, {}),
|
|
278
|
+
focus: parseJsonObject(row?.focus_json, {}),
|
|
279
|
+
updatedAt: row?.updated_at || null,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
updateAssistantSelfState(userId, patch = {}, options = {}) {
|
|
284
|
+
if (userId == null) return this.getAssistantSelfState(userId, options);
|
|
285
|
+
const agentId = this._agentId(userId, options);
|
|
286
|
+
const current = this.getAssistantSelfState(userId, { agentId });
|
|
287
|
+
const nextIdentity = {
|
|
288
|
+
...current.identity,
|
|
289
|
+
...parseJsonObject(patch.identity, {}),
|
|
290
|
+
};
|
|
291
|
+
const nextFocus = {
|
|
292
|
+
...current.focus,
|
|
293
|
+
...parseJsonObject(patch.focus, {}),
|
|
294
|
+
};
|
|
295
|
+
db.prepare(
|
|
296
|
+
`INSERT INTO assistant_self_state (user_id, agent_id, identity_json, focus_json, updated_at)
|
|
297
|
+
VALUES (?, ?, ?, ?, datetime('now'))
|
|
298
|
+
ON CONFLICT(user_id, agent_id) DO UPDATE SET
|
|
299
|
+
identity_json = excluded.identity_json,
|
|
300
|
+
focus_json = excluded.focus_json,
|
|
301
|
+
updated_at = excluded.updated_at`
|
|
302
|
+
).run(
|
|
303
|
+
userId,
|
|
304
|
+
agentId,
|
|
305
|
+
JSON.stringify(nextIdentity),
|
|
306
|
+
JSON.stringify(nextFocus),
|
|
307
|
+
);
|
|
308
|
+
return this.getAssistantSelfState(userId, { agentId });
|
|
309
|
+
}
|
|
310
|
+
|
|
168
311
|
// ─────────────────────────────────────────────────────────────────────────
|
|
169
312
|
// Semantic Memories (SQLite + embeddings)
|
|
170
313
|
// ─────────────────────────────────────────────────────────────────────────
|
|
@@ -178,15 +321,25 @@ class MemoryManager {
|
|
|
178
321
|
const decision = getMemoryStorageDecision(content);
|
|
179
322
|
if (!decision.allow) return null;
|
|
180
323
|
content = decision.normalized;
|
|
181
|
-
category =
|
|
324
|
+
category = normalizeMemoryCategory(category);
|
|
182
325
|
importance = Math.max(1, Math.min(10, Number(importance) || 5));
|
|
326
|
+
const scope = normalizeScope(options.scope, agentId);
|
|
327
|
+
const sourceRef = normalizeSourceRef(options.sourceRef);
|
|
328
|
+
const staleAfterDays = Number.isFinite(Number(options.staleAfterDays))
|
|
329
|
+
? Math.max(1, Number(options.staleAfterDays))
|
|
330
|
+
: null;
|
|
331
|
+
const metadata = parseJsonObject(options.metadata, {});
|
|
183
332
|
|
|
184
333
|
const embedding = await getEmbedding(content, await getActiveProvider(userId, agentId));
|
|
185
334
|
|
|
186
|
-
// Dedup check: compare against existing non-archived memories
|
|
335
|
+
// Dedup check: compare against existing non-archived memories in the same scope
|
|
187
336
|
const existing = db.prepare(
|
|
188
|
-
`SELECT id, content, embedding
|
|
189
|
-
|
|
337
|
+
`SELECT id, content, embedding, metadata_json
|
|
338
|
+
FROM memories
|
|
339
|
+
WHERE user_id = ? AND agent_id = ? AND archived = 0
|
|
340
|
+
AND COALESCE(scope_type, 'agent') = ?
|
|
341
|
+
AND COALESCE(scope_id, '') = COALESCE(?, '')`
|
|
342
|
+
).all(userId, agentId, scope.scopeType, scope.scopeId);
|
|
190
343
|
|
|
191
344
|
for (const mem of existing) {
|
|
192
345
|
let sim = 0;
|
|
@@ -200,10 +353,27 @@ class MemoryManager {
|
|
|
200
353
|
if (sim > 0.85) {
|
|
201
354
|
// Very similar — update in place if new content is longer, otherwise skip
|
|
202
355
|
if (content.length > mem.content.length) {
|
|
356
|
+
const mergedMetadata = {
|
|
357
|
+
...parseJsonObject(mem.metadata_json, {}),
|
|
358
|
+
...metadata,
|
|
359
|
+
};
|
|
203
360
|
db.prepare(
|
|
204
361
|
`UPDATE memories SET content = ?, importance = MAX(importance, ?), embedding = ?,
|
|
362
|
+
source_type = COALESCE(?, source_type), source_id = COALESCE(?, source_id),
|
|
363
|
+
source_label = COALESCE(?, source_label), stale_after_days = COALESCE(?, stale_after_days),
|
|
364
|
+
metadata_json = ?,
|
|
205
365
|
updated_at = datetime('now') WHERE id = ?`
|
|
206
|
-
).run(
|
|
366
|
+
).run(
|
|
367
|
+
content,
|
|
368
|
+
importance,
|
|
369
|
+
embedding ? serializeEmbedding(embedding) : mem.embedding,
|
|
370
|
+
sourceRef.sourceType,
|
|
371
|
+
sourceRef.sourceId,
|
|
372
|
+
sourceRef.sourceLabel,
|
|
373
|
+
staleAfterDays,
|
|
374
|
+
JSON.stringify(mergedMetadata),
|
|
375
|
+
mem.id,
|
|
376
|
+
);
|
|
207
377
|
return mem.id;
|
|
208
378
|
}
|
|
209
379
|
return mem.id; // already covered, skip
|
|
@@ -213,9 +383,27 @@ class MemoryManager {
|
|
|
213
383
|
// Save new
|
|
214
384
|
const id = uuidv4();
|
|
215
385
|
db.prepare(
|
|
216
|
-
`INSERT INTO memories (
|
|
217
|
-
|
|
218
|
-
|
|
386
|
+
`INSERT INTO memories (
|
|
387
|
+
id, user_id, agent_id, category, scope_type, scope_id, source_type, source_id, source_label,
|
|
388
|
+
stale_after_days, metadata_json, content, importance, embedding
|
|
389
|
+
)
|
|
390
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
391
|
+
).run(
|
|
392
|
+
id,
|
|
393
|
+
userId,
|
|
394
|
+
agentId,
|
|
395
|
+
category,
|
|
396
|
+
scope.scopeType,
|
|
397
|
+
scope.scopeId,
|
|
398
|
+
sourceRef.sourceType,
|
|
399
|
+
sourceRef.sourceId,
|
|
400
|
+
sourceRef.sourceLabel,
|
|
401
|
+
staleAfterDays,
|
|
402
|
+
JSON.stringify(metadata),
|
|
403
|
+
content,
|
|
404
|
+
importance,
|
|
405
|
+
embedding ? serializeEmbedding(embedding) : null,
|
|
406
|
+
);
|
|
219
407
|
|
|
220
408
|
return id;
|
|
221
409
|
}
|
|
@@ -227,11 +415,19 @@ class MemoryManager {
|
|
|
227
415
|
async recallMemory(userId, query, topK = 6, options = {}) {
|
|
228
416
|
if (!query || !query.trim()) return [];
|
|
229
417
|
const agentId = this._agentId(userId, options);
|
|
418
|
+
const scope = normalizeScope(options.scope, agentId);
|
|
230
419
|
|
|
231
420
|
const all = db.prepare(
|
|
232
|
-
`SELECT id, category, content, importance, embedding, access_count, created_at
|
|
233
|
-
|
|
234
|
-
|
|
421
|
+
`SELECT id, category, content, importance, embedding, access_count, created_at, updated_at,
|
|
422
|
+
scope_type, scope_id, source_type, source_id, source_label, stale_after_days, metadata_json
|
|
423
|
+
FROM memories
|
|
424
|
+
WHERE user_id = ? AND agent_id = ? AND archived = 0
|
|
425
|
+
AND (
|
|
426
|
+
(COALESCE(scope_type, 'agent') = ? AND COALESCE(scope_id, '') = COALESCE(?, ''))
|
|
427
|
+
OR COALESCE(scope_type, 'agent') = 'shared'
|
|
428
|
+
)
|
|
429
|
+
ORDER BY updated_at DESC`
|
|
430
|
+
).all(userId, agentId, scope.scopeType, scope.scopeId);
|
|
235
431
|
|
|
236
432
|
if (!all.length) return [];
|
|
237
433
|
|
|
@@ -247,15 +443,14 @@ class MemoryManager {
|
|
|
247
443
|
score = score * (0.5 + mem.importance / 20);
|
|
248
444
|
}
|
|
249
445
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
}
|
|
446
|
+
const lexicalScore = keywordSimilarity(query, mem.content) * 0.7;
|
|
447
|
+
score = Math.max(score, lexicalScore);
|
|
448
|
+
score *= computeFreshnessMultiplier(mem);
|
|
254
449
|
return { ...mem, score };
|
|
255
450
|
});
|
|
256
451
|
|
|
257
452
|
const results = scored
|
|
258
|
-
.filter(m => m.score > 0.
|
|
453
|
+
.filter(m => m.score > 0.2)
|
|
259
454
|
.sort((a, b) => b.score - a.score)
|
|
260
455
|
.slice(0, topK);
|
|
261
456
|
|
|
@@ -266,8 +461,9 @@ class MemoryManager {
|
|
|
266
461
|
db.prepare(`UPDATE memories SET access_count = access_count + 1 WHERE id IN (${placeholders})`).run(...ids);
|
|
267
462
|
}
|
|
268
463
|
|
|
269
|
-
return results.map((
|
|
270
|
-
|
|
464
|
+
return results.map((row) => ({
|
|
465
|
+
...serializeMemoryRow(row),
|
|
466
|
+
score: row.score,
|
|
271
467
|
}));
|
|
272
468
|
}
|
|
273
469
|
|
|
@@ -276,16 +472,20 @@ class MemoryManager {
|
|
|
276
472
|
*/
|
|
277
473
|
listMemories(userId, { category, limit = 50, offset = 0, includeArchived = false, agentId = null } = {}) {
|
|
278
474
|
const scopedAgentId = this._agentId(userId, { agentId });
|
|
279
|
-
let sql = `SELECT id, category, content, importance, access_count, archived, created_at, updated_at
|
|
475
|
+
let sql = `SELECT id, category, content, importance, access_count, archived, created_at, updated_at,
|
|
476
|
+
scope_type, scope_id, source_type, source_id, source_label, stale_after_days, metadata_json
|
|
280
477
|
FROM memories WHERE user_id = ? AND agent_id = ? AND archived = ?`;
|
|
281
478
|
const params = [userId, scopedAgentId, includeArchived ? 1 : 0];
|
|
282
|
-
if (category && CATEGORIES.includes(category)) {
|
|
479
|
+
if (category && !CATEGORIES.includes(category)) {
|
|
480
|
+
category = 'episodic';
|
|
481
|
+
}
|
|
482
|
+
if (category) {
|
|
283
483
|
sql += ` AND category = ?`;
|
|
284
|
-
params.push(category);
|
|
484
|
+
params.push(normalizeMemoryCategory(category));
|
|
285
485
|
}
|
|
286
486
|
sql += ` ORDER BY importance DESC, updated_at DESC LIMIT ? OFFSET ?`;
|
|
287
487
|
params.push(limit, offset);
|
|
288
|
-
return db.prepare(sql).all(...params);
|
|
488
|
+
return db.prepare(sql).all(...params).map(serializeMemoryRow);
|
|
289
489
|
}
|
|
290
490
|
|
|
291
491
|
/**
|
|
@@ -297,7 +497,7 @@ class MemoryManager {
|
|
|
297
497
|
|
|
298
498
|
const newContent = content ?? mem.content;
|
|
299
499
|
const newImportance = importance != null ? Math.max(1, Math.min(10, Number(importance))) : mem.importance;
|
|
300
|
-
const newCategory =
|
|
500
|
+
const newCategory = category ? normalizeMemoryCategory(category) : mem.category;
|
|
301
501
|
|
|
302
502
|
let newEmbed = mem.embedding;
|
|
303
503
|
if (content && content !== mem.content) {
|
|
@@ -310,7 +510,9 @@ class MemoryManager {
|
|
|
310
510
|
updated_at = datetime('now') WHERE id = ?`
|
|
311
511
|
).run(newContent, newImportance, newCategory, newEmbed, id);
|
|
312
512
|
|
|
313
|
-
return db.prepare(
|
|
513
|
+
return serializeMemoryRow(db.prepare(
|
|
514
|
+
`SELECT * FROM memories WHERE id = ?`
|
|
515
|
+
).get(id));
|
|
314
516
|
}
|
|
315
517
|
|
|
316
518
|
/**
|
|
@@ -894,6 +1096,20 @@ class MemoryManager {
|
|
|
894
1096
|
ctx += `${behaviorNotes}\n\n`;
|
|
895
1097
|
}
|
|
896
1098
|
|
|
1099
|
+
if (userId != null) {
|
|
1100
|
+
const selfState = this.getAssistantSelfState(userId, { agentId });
|
|
1101
|
+
if (Object.keys(selfState.identity || {}).length || Object.keys(selfState.focus || {}).length) {
|
|
1102
|
+
ctx += `## Assistant Self State\n`;
|
|
1103
|
+
if (Object.keys(selfState.identity || {}).length) {
|
|
1104
|
+
ctx += `Identity: ${JSON.stringify(selfState.identity)}\n`;
|
|
1105
|
+
}
|
|
1106
|
+
if (Object.keys(selfState.focus || {}).length) {
|
|
1107
|
+
ctx += `Focus: ${JSON.stringify(selfState.focus)}\n`;
|
|
1108
|
+
}
|
|
1109
|
+
ctx += '\n';
|
|
1110
|
+
}
|
|
1111
|
+
}
|
|
1112
|
+
|
|
897
1113
|
// 2. Core memory — always-relevant user facts
|
|
898
1114
|
if (userId != null) {
|
|
899
1115
|
const core = this.getCoreMemory(userId, { agentId });
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const db = require('../db/database');
|
|
2
2
|
const { sanitizeError } = require('../utils/security');
|
|
3
|
+
const { listRunEvents } = require('./ai/runEvents');
|
|
3
4
|
const { resolveAgentId } = require('./agents/manager');
|
|
4
5
|
|
|
5
6
|
const MAX_VOICE_SCREENSHOT_BYTES = 3 * 1024 * 1024;
|
|
@@ -352,7 +353,8 @@ function setupWebSocket(io, services) {
|
|
|
352
353
|
const run = db.prepare('SELECT * FROM agent_runs WHERE id = ? AND user_id = ?').get(runId, userId);
|
|
353
354
|
const steps = db.prepare('SELECT * FROM agent_steps WHERE run_id = ? ORDER BY step_index ASC').all(runId);
|
|
354
355
|
const history = db.prepare('SELECT * FROM conversation_history WHERE agent_run_id = ? ORDER BY created_at ASC').all(runId);
|
|
355
|
-
|
|
356
|
+
const events = listRunEvents(runId);
|
|
357
|
+
socket.emit('agent:run_detail', { run, steps, history, events });
|
|
356
358
|
} catch (err) {
|
|
357
359
|
console.error(`[WS] agent:run_detail failed for user ${userId}:`, err);
|
|
358
360
|
socket.emit('error', { message: sanitizeError(err) });
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const db = require('../../db/database');
|
|
4
|
+
const { resolveAgentId } = require('../agents/manager');
|
|
5
|
+
|
|
6
|
+
function parseJsonObject(value, fallback = {}) {
|
|
7
|
+
if (!value) return { ...fallback };
|
|
8
|
+
if (typeof value === 'object' && !Array.isArray(value)) return { ...value };
|
|
9
|
+
try {
|
|
10
|
+
const parsed = JSON.parse(String(value));
|
|
11
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
12
|
+
? parsed
|
|
13
|
+
: { ...fallback };
|
|
14
|
+
} catch {
|
|
15
|
+
return { ...fallback };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function safeTrim(value, maxLength = 240) {
|
|
20
|
+
return String(value || '').trim().slice(0, maxLength);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function formatRelativeTimestamp(value) {
|
|
24
|
+
if (!value) return 'No recent activity';
|
|
25
|
+
const date = new Date(value);
|
|
26
|
+
if (Number.isNaN(date.getTime())) return safeTrim(value, 80);
|
|
27
|
+
return date.toISOString();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildAssistantFocusSnapshot(memoryManager, userId, agentId) {
|
|
31
|
+
const scopedAgentId = resolveAgentId(userId, agentId);
|
|
32
|
+
const selfState = memoryManager?.getAssistantSelfState?.(userId, { agentId: scopedAgentId }) || {
|
|
33
|
+
identity: {},
|
|
34
|
+
focus: {},
|
|
35
|
+
};
|
|
36
|
+
const tasks = db.prepare(
|
|
37
|
+
`SELECT id, name, trigger_type, enabled, last_run
|
|
38
|
+
FROM scheduled_tasks
|
|
39
|
+
WHERE user_id = ? AND agent_id = ? AND enabled = 1 AND task_type != 'widget_refresh'
|
|
40
|
+
ORDER BY COALESCE(last_run, created_at) DESC
|
|
41
|
+
LIMIT 6`
|
|
42
|
+
).all(userId, scopedAgentId);
|
|
43
|
+
const runs = db.prepare(
|
|
44
|
+
`SELECT id, title, status, trigger_source, created_at, completed_at, error
|
|
45
|
+
FROM agent_runs
|
|
46
|
+
WHERE user_id = ? AND agent_id = ?
|
|
47
|
+
ORDER BY created_at DESC
|
|
48
|
+
LIMIT 6`
|
|
49
|
+
).all(userId, scopedAgentId);
|
|
50
|
+
const conversations = memoryManager?.getRecentConversations?.(userId, 4, { agentId: scopedAgentId }) || [];
|
|
51
|
+
const memories = memoryManager?.listMemories?.(userId, { limit: 6, agentId: scopedAgentId }) || [];
|
|
52
|
+
|
|
53
|
+
const activeThreads = conversations.slice(0, 3).map((conversation) => ({
|
|
54
|
+
title: safeTrim(conversation.title, 80),
|
|
55
|
+
preview: safeTrim(conversation.preview || conversation.summary, 120),
|
|
56
|
+
updatedAt: conversation.updatedAt || null,
|
|
57
|
+
}));
|
|
58
|
+
const nearTermPriorities = tasks.slice(0, 3).map((task) => ({
|
|
59
|
+
label: safeTrim(task.name, 80),
|
|
60
|
+
value: safeTrim(task.trigger_type === 'schedule' ? 'Scheduled' : 'Triggered', 40),
|
|
61
|
+
}));
|
|
62
|
+
const recentSignals = runs.slice(0, 3).map((run) => ({
|
|
63
|
+
label: safeTrim(run.title || 'Run', 80),
|
|
64
|
+
value: safeTrim(run.status || 'unknown', 32),
|
|
65
|
+
}));
|
|
66
|
+
const rememberedContext = memories.slice(0, 2).map((memory) => safeTrim(memory.content, 140));
|
|
67
|
+
|
|
68
|
+
const currentFocus = safeTrim(
|
|
69
|
+
selfState.focus?.currentFocus
|
|
70
|
+
|| activeThreads[0]?.title
|
|
71
|
+
|| nearTermPriorities[0]?.label
|
|
72
|
+
|| 'Monitoring your active work',
|
|
73
|
+
120,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
currentFocus,
|
|
78
|
+
activeThreads,
|
|
79
|
+
nearTermPriorities,
|
|
80
|
+
recentSignals,
|
|
81
|
+
rememberedContext,
|
|
82
|
+
assistantIdentity: {
|
|
83
|
+
name: safeTrim(selfState.identity?.displayName, 80),
|
|
84
|
+
style: safeTrim(selfState.identity?.style, 120),
|
|
85
|
+
},
|
|
86
|
+
generatedAt: new Date().toISOString(),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildAssistantFocusWidgetPayload(snapshot) {
|
|
91
|
+
const rows = [
|
|
92
|
+
...snapshot.nearTermPriorities,
|
|
93
|
+
...snapshot.recentSignals,
|
|
94
|
+
].slice(0, 3);
|
|
95
|
+
const chips = [
|
|
96
|
+
...snapshot.activeThreads.map((item) => item.title),
|
|
97
|
+
...snapshot.rememberedContext,
|
|
98
|
+
].filter(Boolean).slice(0, 3);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
title: 'Today / Focus',
|
|
102
|
+
kicker: 'Assistant state',
|
|
103
|
+
subtitle: snapshot.currentFocus,
|
|
104
|
+
body: snapshot.activeThreads[0]?.preview
|
|
105
|
+
|| snapshot.rememberedContext[0]
|
|
106
|
+
|| 'Watching recent conversations, tasks, and runs.',
|
|
107
|
+
metric: String(snapshot.activeThreads.length),
|
|
108
|
+
metricLabel: snapshot.activeThreads.length === 1 ? 'active thread' : 'active threads',
|
|
109
|
+
secondaryMetric: String(snapshot.nearTermPriorities.length),
|
|
110
|
+
secondaryLabel: 'priorities',
|
|
111
|
+
tertiaryMetric: formatRelativeTimestamp(snapshot.generatedAt),
|
|
112
|
+
tertiaryLabel: 'updated',
|
|
113
|
+
rows,
|
|
114
|
+
chips,
|
|
115
|
+
iconToken: 'focus',
|
|
116
|
+
accentToken: 'focus',
|
|
117
|
+
backgroundToken: 'night',
|
|
118
|
+
updatedAt: snapshot.generatedAt,
|
|
119
|
+
deepLink: 'widget:assistant-focus',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
buildAssistantFocusSnapshot,
|
|
125
|
+
buildAssistantFocusWidgetPayload,
|
|
126
|
+
};
|