neoagent 1.4.1 → 1.4.3

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.
@@ -11,6 +11,38 @@ const {
11
11
  } = require('./embeddings');
12
12
  const { AGENT_DATA_DIR } = require('../../../runtime/paths');
13
13
 
14
+ /**
15
+ * Derive the active AI provider name from user settings so the right
16
+ * embedding model is selected automatically (e.g. Gemini when using Google).
17
+ */
18
+ function getActiveProvider(userId) {
19
+ try {
20
+ const { SUPPORTED_MODELS } = require('../ai/models');
21
+ const rows = db.prepare('SELECT key, value FROM user_settings WHERE user_id = ? AND key IN (?, ?)')
22
+ .all(userId || 1, 'default_chat_model', 'enabled_models');
23
+
24
+ let defaultChatModel = null;
25
+ let enabledIds = null;
26
+ for (const row of rows) {
27
+ try {
28
+ const v = JSON.parse(row.value);
29
+ if (row.key === 'default_chat_model') defaultChatModel = v;
30
+ if (row.key === 'enabled_models') enabledIds = v;
31
+ } catch { }
32
+ }
33
+
34
+ const modelId = defaultChatModel && defaultChatModel !== 'auto'
35
+ ? defaultChatModel
36
+ : (Array.isArray(enabledIds) && enabledIds.length > 0 ? enabledIds[0] : null);
37
+
38
+ if (modelId) {
39
+ const def = SUPPORTED_MODELS.find(m => m.id === modelId);
40
+ if (def) return def.provider;
41
+ }
42
+ } catch { }
43
+ return null;
44
+ }
45
+
14
46
  const DATA_DIR = AGENT_DATA_DIR;
15
47
  const SOUL_FILE = path.join(DATA_DIR, 'SOUL.md');
16
48
  const API_KEYS_FILE = path.join(DATA_DIR, 'API_KEYS.json');
@@ -33,6 +65,33 @@ const CATEGORIES = ['user_fact', 'preference', 'personality', 'episodic'];
33
65
  // Core memory keys (always injected into every prompt)
34
66
  const CORE_KEYS = ['user_profile', 'preferences', 'ai_personality', 'active_context'];
35
67
 
68
+ function buildFtsQuery(query) {
69
+ const tokens = String(query || '')
70
+ .match(/[\p{L}\p{N}_-]{2,}/gu) || [];
71
+ if (!tokens.length) return null;
72
+ return tokens.map((token) => `${token.replace(/"/g, '')}*`).join(' AND ');
73
+ }
74
+
75
+ function stripHighlight(text) {
76
+ return String(text || '').replace(/<\/?mark>/g, '');
77
+ }
78
+
79
+ function buildExcerpt(text, query) {
80
+ const raw = stripHighlight(text);
81
+ const needle = String(query || '').trim().toLowerCase();
82
+ if (!raw) return '';
83
+ if (!needle) return raw.slice(0, 220);
84
+
85
+ const pos = raw.toLowerCase().indexOf(needle);
86
+ if (pos === -1) return raw.slice(0, 220);
87
+
88
+ const start = Math.max(0, pos - 80);
89
+ const end = Math.min(raw.length, pos + needle.length + 140);
90
+ const prefix = start > 0 ? '...' : '';
91
+ const suffix = end < raw.length ? '...' : '';
92
+ return `${prefix}${raw.slice(start, end)}${suffix}`;
93
+ }
94
+
36
95
  class MemoryManager {
37
96
  constructor() {
38
97
  this._ensureDirs();
@@ -42,7 +101,7 @@ class MemoryManager {
42
101
  for (const dir of [DATA_DIR, DAILY_DIR, MEMORY_DIR, SKILLS_DIR]) {
43
102
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
44
103
  }
45
- if (!fs.existsSync(SOUL_FILE)) fs.writeFileSync(SOUL_FILE, DEFAULT_SOUL, 'utf-8');
104
+ if (!fs.existsSync(SOUL_FILE)) fs.writeFileSync(SOUL_FILE, DEFAULT_SOUL, 'utf-8');
46
105
  if (!fs.existsSync(API_KEYS_FILE)) fs.writeFileSync(API_KEYS_FILE, '{}', 'utf-8');
47
106
  }
48
107
 
@@ -59,7 +118,7 @@ class MemoryManager {
59
118
  category = CATEGORIES.includes(category) ? category : 'episodic';
60
119
  importance = Math.max(1, Math.min(10, Number(importance) || 5));
61
120
 
62
- const embedding = await getEmbedding(content);
121
+ const embedding = await getEmbedding(content, getActiveProvider(userId));
63
122
 
64
123
  // Dedup check: compare against existing non-archived memories for this user
65
124
  const existing = db.prepare(
@@ -112,7 +171,7 @@ class MemoryManager {
112
171
 
113
172
  if (!all.length) return [];
114
173
 
115
- const queryVec = await getEmbedding(query);
174
+ const queryVec = await getEmbedding(query, getActiveProvider(userId));
116
175
 
117
176
  const scored = all.map(mem => {
118
177
  let score = 0;
@@ -170,13 +229,13 @@ class MemoryManager {
170
229
  const mem = db.prepare(`SELECT * FROM memories WHERE id = ?`).get(id);
171
230
  if (!mem) return null;
172
231
 
173
- const newContent = content ?? mem.content;
232
+ const newContent = content ?? mem.content;
174
233
  const newImportance = importance != null ? Math.max(1, Math.min(10, Number(importance))) : mem.importance;
175
- const newCategory = (category && CATEGORIES.includes(category)) ? category : mem.category;
234
+ const newCategory = (category && CATEGORIES.includes(category)) ? category : mem.category;
176
235
 
177
236
  let newEmbed = mem.embedding;
178
237
  if (content && content !== mem.content) {
179
- const vec = await getEmbedding(newContent);
238
+ const vec = await getEmbedding(newContent, getActiveProvider(null));
180
239
  newEmbed = vec ? serializeEmbedding(vec) : mem.embedding;
181
240
  }
182
241
 
@@ -322,19 +381,153 @@ class MemoryManager {
322
381
  }
323
382
 
324
383
  getRecentConversations(userId, limit = 20) {
325
- return db.prepare(`
326
- SELECT ch.*, ar.task FROM conversation_history ch
327
- JOIN agent_runs ar ON ch.agent_run_id = ar.id
328
- WHERE ch.user_id = ? ORDER BY ch.created_at DESC LIMIT ?
384
+ const rows = db.prepare(`
385
+ SELECT
386
+ ar.id AS run_id,
387
+ ar.title,
388
+ ar.created_at,
389
+ ar.completed_at,
390
+ ar.status,
391
+ (
392
+ SELECT content
393
+ FROM conversation_history ch
394
+ WHERE ch.agent_run_id = ar.id
395
+ ORDER BY ch.created_at DESC
396
+ LIMIT 1
397
+ ) AS latest_content
398
+ FROM agent_runs ar
399
+ WHERE ar.user_id = ?
400
+ ORDER BY COALESCE(ar.completed_at, ar.created_at) DESC
401
+ LIMIT ?
329
402
  `).all(userId, limit);
403
+
404
+ return rows.map((row) => ({
405
+ runId: row.run_id,
406
+ title: row.title || 'Untitled run',
407
+ createdAt: row.created_at,
408
+ completedAt: row.completed_at,
409
+ status: row.status,
410
+ excerpt: buildExcerpt(row.latest_content, '')
411
+ }));
330
412
  }
331
413
 
332
- searchConversations(userId, query) {
333
- return db.prepare(`
334
- SELECT ch.*, ar.task FROM conversation_history ch
335
- JOIN agent_runs ar ON ch.agent_run_id = ar.id
336
- WHERE ch.user_id = ? AND ch.content LIKE ? ORDER BY ch.created_at DESC LIMIT 50
337
- `).all(userId, `%${query}%`);
414
+ searchConversations(userId, query, options = {}) {
415
+ const ftsQuery = buildFtsQuery(query);
416
+ const maxHits = Math.max(6, Math.min(Number(options.limit) || 24, 60));
417
+ if (!ftsQuery) return [];
418
+
419
+ let webHits = [];
420
+ let messageHits = [];
421
+ try {
422
+ webHits = db.prepare(`
423
+ SELECT
424
+ 'web' AS source,
425
+ ch.id AS message_id,
426
+ COALESCE(ch.agent_run_id, 'web:' || ch.id) AS session_id,
427
+ COALESCE(ar.title, 'Web chat') AS title,
428
+ ch.role,
429
+ ch.created_at,
430
+ snippet(conversation_history_fts, 0, '<mark>', '</mark>', ' ... ', 16) AS snippet,
431
+ bm25(conversation_history_fts) AS score
432
+ FROM conversation_history_fts
433
+ JOIN conversation_history ch ON ch.id = conversation_history_fts.rowid
434
+ LEFT JOIN agent_runs ar ON ar.id = ch.agent_run_id
435
+ WHERE conversation_history_fts MATCH ? AND ch.user_id = ?
436
+ ORDER BY score
437
+ LIMIT ?
438
+ `).all(ftsQuery, userId, maxHits);
439
+
440
+ messageHits = db.prepare(`
441
+ SELECT
442
+ 'message' AS source,
443
+ m.id AS message_id,
444
+ COALESCE(m.run_id, m.platform || ':' || COALESCE(m.platform_chat_id, m.id)) AS session_id,
445
+ COALESCE(ar.title, json_extract(m.metadata, '$.senderName'), m.platform_chat_id, m.platform, 'Message thread') AS title,
446
+ m.role,
447
+ m.created_at,
448
+ m.platform,
449
+ snippet(messages_fts, 0, '<mark>', '</mark>', ' ... ', 16) AS snippet,
450
+ bm25(messages_fts) AS score
451
+ FROM messages_fts
452
+ JOIN messages m ON m.id = messages_fts.rowid
453
+ LEFT JOIN agent_runs ar ON ar.id = m.run_id
454
+ WHERE messages_fts MATCH ? AND m.user_id = ?
455
+ ORDER BY score
456
+ LIMIT ?
457
+ `).all(ftsQuery, userId, maxHits);
458
+ } catch {
459
+ const likeQuery = `%${String(query || '').trim()}%`;
460
+ webHits = db.prepare(`
461
+ SELECT
462
+ 'web' AS source,
463
+ ch.id AS message_id,
464
+ COALESCE(ch.agent_run_id, 'web:' || ch.id) AS session_id,
465
+ COALESCE(ar.title, 'Web chat') AS title,
466
+ ch.role,
467
+ ch.created_at,
468
+ ch.content AS snippet,
469
+ 0 AS score
470
+ FROM conversation_history ch
471
+ LEFT JOIN agent_runs ar ON ar.id = ch.agent_run_id
472
+ WHERE ch.user_id = ? AND ch.content LIKE ?
473
+ ORDER BY ch.created_at DESC
474
+ LIMIT ?
475
+ `).all(userId, likeQuery, maxHits);
476
+
477
+ messageHits = db.prepare(`
478
+ SELECT
479
+ 'message' AS source,
480
+ m.id AS message_id,
481
+ COALESCE(m.run_id, m.platform || ':' || COALESCE(m.platform_chat_id, m.id)) AS session_id,
482
+ COALESCE(ar.title, json_extract(m.metadata, '$.senderName'), m.platform_chat_id, m.platform, 'Message thread') AS title,
483
+ m.role,
484
+ m.created_at,
485
+ m.platform,
486
+ m.content AS snippet,
487
+ 0 AS score
488
+ FROM messages m
489
+ LEFT JOIN agent_runs ar ON ar.id = m.run_id
490
+ WHERE m.user_id = ? AND m.content LIKE ?
491
+ ORDER BY m.created_at DESC
492
+ LIMIT ?
493
+ `).all(userId, likeQuery, maxHits);
494
+ }
495
+
496
+ const grouped = new Map();
497
+ for (const hit of [...webHits, ...messageHits]) {
498
+ const key = `${hit.source}:${hit.session_id}`;
499
+ if (!grouped.has(key)) {
500
+ grouped.set(key, {
501
+ sessionId: hit.session_id,
502
+ source: hit.source,
503
+ title: hit.title || 'Untitled session',
504
+ platform: hit.platform || 'web',
505
+ createdAt: hit.created_at,
506
+ score: Number(hit.score || 0),
507
+ matches: []
508
+ });
509
+ }
510
+
511
+ const group = grouped.get(key);
512
+ group.score = Math.min(group.score, Number(hit.score || 0));
513
+ group.createdAt = hit.created_at > group.createdAt ? hit.created_at : group.createdAt;
514
+ if (group.matches.length < 3) {
515
+ group.matches.push({
516
+ role: hit.role,
517
+ createdAt: hit.created_at,
518
+ excerpt: buildExcerpt(hit.snippet, query)
519
+ });
520
+ }
521
+ }
522
+
523
+ return Array.from(grouped.values())
524
+ .sort((a, b) => a.score - b.score || String(b.createdAt).localeCompare(String(a.createdAt)))
525
+ .slice(0, Math.max(1, Math.min(Number(options.sessions) || 8, 12)))
526
+ .map((session) => ({
527
+ ...session,
528
+ matchCount: session.matches.length,
529
+ summary: session.matches.map((match) => `${match.role}: ${match.excerpt}`).join('\n')
530
+ }));
338
531
  }
339
532
 
340
533
  // ─────────────────────────────────────────────────────────────────────────
@@ -1,5 +1,8 @@
1
1
  const db = require('../db/database');
2
2
  const { sanitizeError } = require('../utils/security');
3
+ const { getProviderForUser } = require('./ai/engine');
4
+ const { ensureDefaultAiSettings, getAiSettings } = require('./ai/settings');
5
+ const { getWebChatContext, refreshWebChatSummary, clearWebChatSummary } = require('./ai/history');
3
6
 
4
7
  function setupWebSocket(io, services) {
5
8
  const { agentEngine, messagingManager, mcpClient, scheduler, memoryManager } = services;
@@ -31,6 +34,7 @@ function setupWebSocket(io, services) {
31
34
  case 'new':
32
35
  case 'clear':
33
36
  db.prepare('DELETE FROM conversation_history WHERE user_id = ?').run(userId);
37
+ clearWebChatSummary(userId);
34
38
  socket.emit('chat:cleared');
35
39
  {
36
40
  const resetResult = await agentEngine.run(userId, 'context was just cleared. say something very brief (1-2 sentences max) acknowledging the fresh start, in your own style. no tools needed.', {});
@@ -61,17 +65,26 @@ function setupWebSocket(io, services) {
61
65
  db.prepare('INSERT INTO conversation_history (user_id, role, content, metadata) VALUES (?, ?, ?, ?)')
62
66
  .run(userId, 'user', task, JSON.stringify({ platform: 'web' }));
63
67
 
64
- const priorMessages = db.prepare(
65
- 'SELECT role, content FROM conversation_history WHERE user_id = ? ORDER BY created_at DESC LIMIT 30'
66
- ).all(userId).reverse();
67
- const prior = priorMessages.filter(m => !(m.role === 'user' && m.content === task)).slice(-29);
68
+ ensureDefaultAiSettings(userId);
69
+ const aiSettings = getAiSettings(userId);
70
+ const webContext = getWebChatContext(userId, aiSettings.chat_history_window);
71
+ const prior = webContext.recentMessages.filter((m) => !(m.role === 'user' && m.content === task)).slice(-aiSettings.chat_history_window);
68
72
 
69
- const result = await agentEngine.run(userId, task, { ...options, priorMessages: prior });
73
+ const result = await agentEngine.run(userId, task, {
74
+ ...options,
75
+ priorMessages: prior,
76
+ priorSummary: webContext.summary
77
+ });
70
78
 
71
79
  if (result?.content) {
72
80
  db.prepare('INSERT INTO conversation_history (user_id, agent_run_id, role, content, metadata) VALUES (?, ?, ?, ?, ?)')
73
81
  .run(userId, result.runId, 'assistant', result.content, JSON.stringify({ tokens: result.totalTokens }));
74
82
  }
83
+
84
+ const { provider, model } = getProviderForUser(userId, task, false, options?.model || null);
85
+ refreshWebChatSummary(userId, provider, model, aiSettings.chat_history_window).catch((summaryErr) => {
86
+ console.error('[WS] Web summary refresh failed:', summaryErr.message);
87
+ });
75
88
  } catch (err) {
76
89
  socket.emit('run:error', { error: sanitizeError(err) });
77
90
  }
@@ -102,7 +115,7 @@ function setupWebSocket(io, services) {
102
115
  socket.on('agent:run_detail', (data) => {
103
116
  try {
104
117
  const run = db.prepare('SELECT * FROM agent_runs WHERE id = ? AND user_id = ?').get(data.runId, userId);
105
- const steps = db.prepare('SELECT * FROM agent_steps WHERE run_id = ? ORDER BY step_number ASC').all(data.runId);
118
+ const steps = db.prepare('SELECT * FROM agent_steps WHERE run_id = ? ORDER BY step_index ASC').all(data.runId);
106
119
  const history = db.prepare('SELECT * FROM conversation_history WHERE agent_run_id = ? ORDER BY created_at ASC').all(data.runId);
107
120
  socket.emit('agent:run_detail', { run, steps, history });
108
121
  } catch (err) {