neoagent 2.3.1-beta.62 → 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.
Files changed (45) hide show
  1. package/.env.example +9 -0
  2. package/docs/configuration.md +11 -0
  3. package/flutter_app/lib/main.dart +2 -0
  4. package/flutter_app/lib/main_account_settings.dart +50 -22
  5. package/flutter_app/lib/main_admin.dart +24 -10
  6. package/flutter_app/lib/main_app_shell.dart +10 -9
  7. package/flutter_app/lib/main_chat.dart +433 -309
  8. package/flutter_app/lib/main_controller.dart +164 -6
  9. package/flutter_app/lib/main_integrations.dart +15 -7
  10. package/flutter_app/lib/main_models.dart +116 -7
  11. package/flutter_app/lib/main_navigation.dart +27 -18
  12. package/flutter_app/lib/main_operations.dart +162 -91
  13. package/flutter_app/lib/main_runtime.dart +22 -0
  14. package/flutter_app/lib/main_settings.dart +287 -75
  15. package/flutter_app/lib/main_shared.dart +165 -6
  16. package/flutter_app/lib/main_unified.dart +388 -0
  17. package/flutter_app/lib/src/analytics_service.dart +294 -0
  18. package/flutter_app/lib/src/backend_client.dart +4 -0
  19. package/flutter_app/macos/Flutter/GeneratedPluginRegistrant.swift +2 -0
  20. package/flutter_app/pubspec.lock +8 -0
  21. package/flutter_app/pubspec.yaml +1 -0
  22. package/flutter_app/web/index.html +1 -0
  23. package/package.json +1 -1
  24. package/server/config/analytics.js +30 -0
  25. package/server/db/database.js +52 -0
  26. package/server/http/routes.js +1 -0
  27. package/server/public/.last_build_id +1 -1
  28. package/server/public/assets/AssetManifest.bin +1 -1
  29. package/server/public/assets/AssetManifest.bin.json +1 -1
  30. package/server/public/assets/NOTICES +183 -0
  31. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  32. package/server/public/assets/packages/mixpanel_flutter/assets/mixpanel.js +3 -0
  33. package/server/public/flutter_bootstrap.js +1 -1
  34. package/server/public/index.html +1 -0
  35. package/server/public/main.dart.js +83685 -82046
  36. package/server/routes/agents.js +2 -1
  37. package/server/routes/memory.js +75 -3
  38. package/server/routes/runtime.js +14 -0
  39. package/server/routes/widgets.js +4 -4
  40. package/server/services/ai/engine.js +86 -0
  41. package/server/services/ai/runEvents.js +100 -0
  42. package/server/services/memory/manager.js +242 -26
  43. package/server/services/websocket.js +3 -1
  44. package/server/services/widgets/focus_widget.js +126 -0
  45. 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 = ['user_fact', 'preference', 'personality', 'episodic'];
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 = CATEGORIES.includes(category) ? category : 'episodic';
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 for this user
335
+ // Dedup check: compare against existing non-archived memories in the same scope
187
336
  const existing = db.prepare(
188
- `SELECT id, content, embedding FROM memories WHERE user_id = ? AND agent_id = ? AND archived = 0`
189
- ).all(userId, agentId);
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(content, importance, embedding ? serializeEmbedding(embedding) : mem.embedding, mem.id);
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 (id, user_id, agent_id, category, content, importance, embedding)
217
- VALUES (?, ?, ?, ?, ?, ?, ?)`
218
- ).run(id, userId, agentId, category, content, importance, embedding ? serializeEmbedding(embedding) : null);
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
- FROM memories WHERE user_id = ? AND agent_id = ? AND archived = 0 ORDER BY updated_at DESC`
234
- ).all(userId, agentId);
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
- if (!score) {
251
- // Keyword fallback
252
- score = keywordSimilarity(query, mem.content) * 0.7;
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.45)
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(({ id, category, content, importance, created_at }) => ({
270
- id, category, content, importance, created_at
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 = (category && CATEGORIES.includes(category)) ? category : mem.category;
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(`SELECT id, category, content, importance, created_at, updated_at FROM memories WHERE id = ?`).get(id);
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
- socket.emit('agent:run_detail', { run, steps, history });
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
+ };