morpheus-cli 0.9.41 → 0.9.51

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 (58) hide show
  1. package/dist/channels/discord.js +1 -1
  2. package/dist/channels/telegram.js +1 -1
  3. package/dist/config/manager.js +8 -5
  4. package/dist/config/schemas.js +6 -3
  5. package/dist/runtime/__tests__/manual_start_verify.js +1 -1
  6. package/dist/runtime/__tests__/telephonist-tts.test.js +2 -2
  7. package/dist/runtime/hot-reload.js +5 -0
  8. package/dist/runtime/memory/sati/index.js +14 -2
  9. package/dist/runtime/memory/sati/repository.js +26 -28
  10. package/dist/runtime/memory/session-embedding-worker.js +16 -8
  11. package/dist/runtime/memory/sqlite.js +22 -6
  12. package/dist/runtime/migration.js +45 -0
  13. package/dist/runtime/oracle.js +3 -0
  14. package/dist/runtime/subagents/apoc.js +2 -2
  15. package/dist/runtime/subagents/link/link.js +2 -2
  16. package/dist/runtime/subagents/link/worker.js +2 -1
  17. package/dist/runtime/subagents/neo.js +5 -3
  18. package/dist/runtime/subagents/registry.js +8 -8
  19. package/dist/runtime/subagents/trinity/trinity.js +2 -2
  20. package/dist/runtime/subagents/utils.js +2 -0
  21. package/dist/runtime/telephonist.js +6 -6
  22. package/dist/runtime/tools/factory.js +50 -34
  23. package/dist/types/config.js +4 -3
  24. package/dist/ui/assets/{AuditDashboard-EvtKjy5H.js → AuditDashboard-DYsjEZ2h.js} +1 -1
  25. package/dist/ui/assets/{Chat-yptierPt.js → Chat-u6n5Fcmk.js} +4 -4
  26. package/dist/ui/assets/{Chronos-BA77MYbp.js → Chronos-Dgz6pUbe.js} +1 -1
  27. package/dist/ui/assets/{ConfirmationModal-NOZr-ipQ.js → ConfirmationModal-BCWFgigv.js} +1 -1
  28. package/dist/ui/assets/{Dashboard-ly1MJiB4.js → Dashboard-5ERDbl6k.js} +1 -1
  29. package/dist/ui/assets/{DeleteConfirmationModal-2HMraacH.js → DeleteConfirmationModal-O7K_CzNy.js} +1 -1
  30. package/dist/ui/assets/{Documents-C31fAm0Z.js → Documents-C6vFeWQC.js} +1 -1
  31. package/dist/ui/assets/{Logs-BiajoLAB.js → Logs-DZ820gmZ.js} +1 -1
  32. package/dist/ui/assets/{MCPManager-DS9jfiZT.js → MCPManager-BpscMusl.js} +1 -1
  33. package/dist/ui/assets/{ModelPresets-CxhKcalw.js → ModelPresets-B0OtJ3SG.js} +1 -1
  34. package/dist/ui/assets/{ModelPricing-CN8flHnP.js → ModelPricing-DSe7ibox.js} +1 -1
  35. package/dist/ui/assets/{Notifications-BfP1_CM3.js → Notifications-C4LoP0a6.js} +1 -1
  36. package/dist/ui/assets/{SatiMemories-Bk4_ubo7.js → SatiMemories-WJaz9vS2.js} +1 -1
  37. package/dist/ui/assets/{SessionAudit-D3E6QSQw.js → SessionAudit-BUFsqB5Q.js} +1 -1
  38. package/dist/ui/assets/{Settings-3VBK8muv.js → Settings-lHi1Jo5c.js} +5 -5
  39. package/dist/ui/assets/{Skills-Dp0_GwiW.js → Skills-jwgKeiMX.js} +1 -1
  40. package/dist/ui/assets/{Smiths-COTgI2R4.js → Smiths-DhytcrJX.js} +1 -1
  41. package/dist/ui/assets/{Tasks-COe4lIJ7.js → Tasks-Mn99HoyU.js} +1 -1
  42. package/dist/ui/assets/{TrinityDatabases-BEU4mmyW.js → TrinityDatabases-DDHblq8V.js} +1 -1
  43. package/dist/ui/assets/{UsageStats-BTmDeG2V.js → UsageStats-Czu8MFsZ.js} +1 -1
  44. package/dist/ui/assets/{WebhookManager-FQVyKyW-.js → WebhookManager-CKZOjGG3.js} +1 -1
  45. package/dist/ui/assets/{agents-B6e9N0QI.js → agents-fpKaFvxS.js} +1 -1
  46. package/dist/ui/assets/{audit-giQG2WRk.js → audit-BCFf71Ic.js} +1 -1
  47. package/dist/ui/assets/{chronos-sweaRcNj.js → chronos-_FyFfKFn.js} +1 -1
  48. package/dist/ui/assets/{config-CbUdj76n.js → config-sb-fswxE.js} +1 -1
  49. package/dist/ui/assets/{index-CRPT77Yo.css → index-Bn2nGQ5z.css} +1 -1
  50. package/dist/ui/assets/{index-yu2c4ry1.js → index-DZCpLD9L.js} +2 -2
  51. package/dist/ui/assets/{mcp-v64BBpUk.js → mcp-Dmcsh35T.js} +1 -1
  52. package/dist/ui/assets/{modelPresets-BaNh-gxn.js → modelPresets-CShvjJGA.js} +1 -1
  53. package/dist/ui/assets/{skills-ClRXBlVt.js → skills-CxeLa4Rc.js} +1 -1
  54. package/dist/ui/assets/{stats-nI-89hEX.js → stats-C7Z2ippX.js} +1 -1
  55. package/dist/ui/assets/{useCurrency-D5An8I2f.js → useCurrency-LdM_SjsH.js} +1 -1
  56. package/dist/ui/index.html +2 -2
  57. package/dist/ui/sw.js +1 -1
  58. package/package.json +1 -1
@@ -331,7 +331,7 @@ export class DiscordAdapter {
331
331
  this.display.startActivity('telephonist', 'Synthesizing TTS...');
332
332
  const ttsApiKey = getUsableApiKey(ttsConfig.apiKey) ||
333
333
  getUsableApiKey(config.audio.apiKey) ||
334
- (config.llm.provider === (ttsConfig.provider === 'google' ? 'gemini' : ttsConfig.provider)
334
+ (config.llm.provider === ttsConfig.provider
335
335
  ? getUsableApiKey(config.llm.api_key) : undefined);
336
336
  const ttsResult = await this.ttsTelephonist.synthesize(response, ttsApiKey || '', ttsConfig.voice, ttsConfig.style_prompt);
337
337
  ttsFilePath = ttsResult.filePath;
@@ -1921,7 +1921,7 @@ How can I assist you today?`;
1921
1921
  this.display.startActivity('telephonist', 'Synthesizing TTS...');
1922
1922
  const ttsApiKey = getUsableApiKey(ttsConfig.apiKey) ||
1923
1923
  getUsableApiKey(config.audio.apiKey) ||
1924
- (config.llm.provider === (ttsConfig.provider === 'google' ? 'gemini' : ttsConfig.provider)
1924
+ (config.llm.provider === ttsConfig.provider
1925
1925
  ? getUsableApiKey(config.llm.api_key) : undefined);
1926
1926
  const ttsResult = await this.ttsTelephonist.synthesize(response, ttsApiKey || '', ttsConfig.voice, ttsConfig.style_prompt);
1927
1927
  ttsFilePath = ttsResult.filePath;
@@ -162,6 +162,7 @@ export class ConfigManager {
162
162
  enabled_archived_sessions: resolveBoolean('MORPHEUS_SATI_ENABLED_ARCHIVED_SESSIONS', config.sati.enabled_archived_sessions, true),
163
163
  similarity_threshold: resolveNumeric('MORPHEUS_SATI_SIMILARITY_THRESHOLD', config.sati.similarity_threshold, 0.9),
164
164
  evaluation_interval: resolveNumeric('MORPHEUS_SATI_EVALUATION_INTERVAL', config.sati.evaluation_interval, 1),
165
+ chunk_size: resolveNumeric('MORPHEUS_SATI_CHUNK_SIZE', config.sati.chunk_size, 500),
165
166
  };
166
167
  }
167
168
  // Apply precedence to Apoc config
@@ -282,13 +283,13 @@ export class ConfigManager {
282
283
  }
283
284
  // Apply precedence to audio config
284
285
  const audioProvider = resolveString('MORPHEUS_AUDIO_PROVIDER', config.audio.provider, DEFAULT_CONFIG.audio.provider);
285
- // AudioProvider uses 'google' but resolveApiKey expects LLMProvider which uses 'gemini'
286
- const audioProviderForKey = (audioProvider === 'google' ? 'gemini' : audioProvider);
286
+ // AudioProvider uses 'gemini' which maps to LLMProvider 'gemini' (already compatible)
287
+ const audioProviderForKey = audioProvider;
287
288
  // TTS config
288
289
  const ttsDefaults = DEFAULT_CONFIG.audio.tts;
289
290
  const ttsCfg = config.audio.tts;
290
291
  const ttsProvider = resolveString('MORPHEUS_AUDIO_TTS_PROVIDER', ttsCfg?.provider, ttsDefaults.provider);
291
- const ttsProviderForKey = (ttsProvider === 'google' ? 'gemini' : ttsProvider);
292
+ const ttsProviderForKey = ttsProvider;
292
293
  const ttsConfig = {
293
294
  enabled: resolveBoolean('MORPHEUS_AUDIO_TTS_ENABLED', ttsCfg?.enabled, ttsDefaults.enabled),
294
295
  provider: ttsProvider,
@@ -449,14 +450,16 @@ export class ConfigManager {
449
450
  getSatiConfig() {
450
451
  if (this.config.sati) {
451
452
  return {
452
- memory_limit: 10, // Default if undefined
453
+ memory_limit: 10,
454
+ chunk_size: 500,
453
455
  ...this.config.sati
454
456
  };
455
457
  }
456
458
  // Fallback to main LLM config
457
459
  return {
458
460
  ...this.config.llm,
459
- memory_limit: 10 // Default fallback
461
+ memory_limit: 10,
462
+ chunk_size: 500,
460
463
  };
461
464
  }
462
465
  getApocConfig() {
@@ -2,14 +2,14 @@ import { z } from 'zod';
2
2
  import { DEFAULT_CONFIG } from '../types/config.js';
3
3
  export const TtsConfigSchema = z.object({
4
4
  enabled: z.boolean().default(false),
5
- provider: z.enum(['openai', 'google']).default('google'),
5
+ provider: z.enum(['openai', 'gemini']).default('gemini'),
6
6
  model: z.string().min(1).default('gemini-2.5-flash-preview-tts'),
7
7
  voice: z.string().min(1).default('Kore'),
8
8
  apiKey: z.string().optional(),
9
9
  style_prompt: z.string().optional(),
10
10
  });
11
11
  export const AudioConfigSchema = z.object({
12
- provider: z.enum(['google', 'openai', 'openrouter', 'ollama']).default(DEFAULT_CONFIG.audio.provider),
12
+ provider: z.enum(['gemini', 'openai', 'openrouter', 'ollama']).default(DEFAULT_CONFIG.audio.provider),
13
13
  model: z.string().min(1).default(DEFAULT_CONFIG.audio.model),
14
14
  enabled: z.boolean().default(DEFAULT_CONFIG.audio.enabled),
15
15
  apiKey: z.string().optional(),
@@ -30,8 +30,11 @@ export const LLMConfigSchema = z.object({
30
30
  export const SatiConfigSchema = LLMConfigSchema.extend({
31
31
  memory_limit: z.number().int().positive().optional(),
32
32
  enabled_archived_sessions: z.boolean().default(true),
33
- similarity_threshold: z.number().min(0).max(1).default(0.9),
33
+ similarity_threshold: z.number().min(0).max(1).default(0.7),
34
34
  evaluation_interval: z.number().int().min(1).default(1),
35
+ chunk_size: z.number().int().positive().default(500),
36
+ session_chunk_limit: z.number().int().positive().optional(),
37
+ max_memory_tokens: z.number().int().positive().optional(),
35
38
  });
36
39
  export const ApocConfigSchema = LLMConfigSchema.extend({
37
40
  working_dir: z.string().optional(),
@@ -12,7 +12,7 @@ const mockConfig = {
12
12
  ui: { enabled: false, port: 3333 },
13
13
  logging: { enabled: false, level: 'info', retention: '1d' },
14
14
  audio: {
15
- provider: 'google',
15
+ provider: 'gemini',
16
16
  model: 'gemini-2.5-flash-lite',
17
17
  enabled: false,
18
18
  maxDurationSeconds: 60,
@@ -12,10 +12,10 @@ describe('createTtsTelephonist', () => {
12
12
  expect(telephonist).toBeDefined();
13
13
  expect(typeof telephonist.synthesize).toBe('function');
14
14
  });
15
- it('returns an instance with synthesize() for google provider', () => {
15
+ it('returns an instance with synthesize() for gemini provider', () => {
16
16
  const telephonist = createTtsTelephonist({
17
17
  enabled: true,
18
- provider: 'google',
18
+ provider: 'gemini',
19
19
  model: 'gemini-2.5-flash',
20
20
  voice: 'Kore',
21
21
  });
@@ -9,6 +9,7 @@ import { ConfigManager } from '../config/manager.js';
9
9
  import { DisplayManager } from './display.js';
10
10
  import { SubagentRegistry } from './subagents/registry.js';
11
11
  import { SkillRegistry } from './skills/index.js';
12
+ import { SQLiteChatMessageHistory } from './memory/sqlite.js';
12
13
  let currentOracle = null;
13
14
  /**
14
15
  * Register the current Oracle instance for hot-reload.
@@ -51,6 +52,10 @@ export async function hotReloadConfig() {
51
52
  const agentNames = SubagentRegistry.getAll().map(r => r.label);
52
53
  reinitialized.push(...agentNames);
53
54
  display.log(`Subagents reloaded: ${agentNames.join(', ')}`, { source: 'HotReload', level: 'info' });
55
+ // 4. Clear message cache to prevent stale tool_calls causing Gemini 400 errors
56
+ // This ensures the next chat() call re-fetches from DB with proper sanitization
57
+ SQLiteChatMessageHistory.clearCache();
58
+ display.log('Message cache cleared', { source: 'HotReload', level: 'info' });
54
59
  return {
55
60
  success: true,
56
61
  reinitialized,
@@ -2,6 +2,7 @@ import { AIMessage } from "@langchain/core/messages";
2
2
  import { SatiService } from "./service.js";
3
3
  import { DisplayManager } from "../../display.js";
4
4
  import { AuditRepository } from "../../audit/repository.js";
5
+ import { ConfigManager } from "../../../config/manager.js";
5
6
  const display = DisplayManager.getInstance();
6
7
  export class SatiMemoryMiddleware {
7
8
  service;
@@ -38,10 +39,21 @@ export class SatiMemoryMiddleware {
38
39
  display.log('No relevant memories found', { source: 'Sati' });
39
40
  return null;
40
41
  }
41
- const memoryContext = result.relevant_memories
42
+ // Apply token budget: stop adding memories once we exceed the char limit.
43
+ const satiConfig = ConfigManager.getInstance().getSatiConfig();
44
+ const maxMemoryChars = (satiConfig.max_memory_tokens ?? 3000) * 4;
45
+ let usedChars = 0;
46
+ const budgetedMemories = result.relevant_memories.filter(m => {
47
+ const cost = m.summary.length + m.category.length + 20;
48
+ if (usedChars + cost > maxMemoryChars)
49
+ return false;
50
+ usedChars += cost;
51
+ return true;
52
+ });
53
+ const memoryContext = budgetedMemories
42
54
  .map(m => `- [${m.category.toUpperCase()}] ${m.summary}`)
43
55
  .join('\n');
44
- display.log(`Retrieved ${result.relevant_memories.length} memories.`, { source: 'Sati' });
56
+ display.log(`Retrieved ${budgetedMemories.length}/${result.relevant_memories.length} memories (budget: ~${Math.round(usedChars / 4)} tokens).`, { source: 'Sati' });
45
57
  return new AIMessage(`
46
58
  ### LONG-TERM MEMORY (SATI)
47
59
  The following information was retrieved from previous sessions. Use it if relevant:
@@ -209,19 +209,24 @@ export class SatiRepository {
209
209
  searchUnifiedVector(embedding, limit) {
210
210
  if (!this.db)
211
211
  return [];
212
- const SIMILARITY_THRESHOLD = ConfigManager.getInstance().getSatiConfig().similarity_threshold ?? 0.9;
212
+ const satiConfig = ConfigManager.getInstance().getSatiConfig();
213
+ const SIMILARITY_THRESHOLD = satiConfig.similarity_threshold ?? 0.7;
214
+ // Fetch a larger candidate pool so post-filtering still has enough results.
215
+ // weighted_score is used for ranking; cosine_similarity is used for threshold filtering.
216
+ const candidateLimit = limit * 10;
213
217
  const stmt = this.db.prepare(`
214
218
  SELECT *
215
219
  FROM (
216
220
  -- LONG TERM MEMORY
217
- SELECT
221
+ SELECT
218
222
  m.id as id,
219
223
  m.summary as summary,
220
224
  m.details as details,
221
225
  m.category as category,
222
226
  m.importance as importance,
223
227
  'long_term' as source_type,
224
- (1 - vec_distance_cosine(v.embedding, ?)) * 0.8 as distance
228
+ (1 - vec_distance_cosine(v.embedding, ?)) as cosine_similarity,
229
+ (1 - vec_distance_cosine(v.embedding, ?)) * 0.8 as weighted_score
225
230
  FROM memory_vec v
226
231
  JOIN memory_embedding_map map ON map.vec_rowid = v.rowid
227
232
  JOIN long_term_memory m ON m.id = map.memory_id
@@ -230,42 +235,35 @@ export class SatiRepository {
230
235
  UNION ALL
231
236
 
232
237
  -- SESSION CHUNKS
233
- SELECT
238
+ SELECT
234
239
  sc.id as id,
235
240
  sc.content as summary,
236
241
  sc.content as details,
237
242
  'session' as category,
238
243
  'medium' as importance,
239
244
  'session_chunk' as source_type,
240
- (1 - vec_distance_cosine(v.embedding, ?)) * 0.2 as distance
245
+ (1 - vec_distance_cosine(v.embedding, ?)) as cosine_similarity,
246
+ (1 - vec_distance_cosine(v.embedding, ?)) * 0.2 as weighted_score
241
247
  FROM session_vec v
242
248
  JOIN session_embedding_map map ON map.vec_rowid = v.rowid
243
249
  JOIN session_chunks sc ON sc.id = map.session_chunk_id
244
250
  )
245
- ORDER BY distance ASC
251
+ ORDER BY weighted_score DESC
246
252
  LIMIT ?
247
253
  `);
248
- const rows = stmt.all(new Float32Array(embedding), new Float32Array(embedding), limit);
249
- // console.log(
250
- // `[SatiRepository] Unified vector search returned ${rows.length} raw results`
251
- // );
252
- // console each row
253
- // rows.forEach((row, index) => {
254
- // console.log(`[SatiRepository] Row ${index + 1}:`, row);
255
- // });
256
- // Note: the SQL query already computes distance as (1 - cosine_distance) * weight,
257
- // so higher values mean higher similarity. Use distance directly as similarity score.
258
- const ranked = rows
259
- .map(r => ({
260
- ...r,
261
- similarity: 1 - r.distance
262
- }))
263
- .sort((a, b) => b.similarity - a.similarity);
264
- const filtered = ranked
265
- .filter(r => r.similarity >= SIMILARITY_THRESHOLD)
266
- .sort((a, b) => b.similarity - a.similarity);
267
- this.display.log(`🧠 Unified vector search retornou ${filtered.length} resultados`, { source: 'Sati', level: 'debug' });
268
- return filtered.map(r => ({
254
+ const rows = stmt.all(new Float32Array(embedding), new Float32Array(embedding), new Float32Array(embedding), new Float32Array(embedding), candidateLimit);
255
+ // Filter by raw cosine similarity (not the weighted score) so the threshold
256
+ // is independent of the long-term vs. session-chunk weighting.
257
+ const filtered = rows.filter(r => r.cosine_similarity >= SIMILARITY_THRESHOLD);
258
+ // Cap session chunks to avoid flooding from large archived sessions.
259
+ const chunkCap = satiConfig.session_chunk_limit ?? Math.ceil(limit * 0.3);
260
+ const longTerm = filtered.filter(r => r.source_type === 'long_term');
261
+ const chunks = filtered.filter(r => r.source_type === 'session_chunk');
262
+ const combined = [...longTerm, ...chunks.slice(0, chunkCap)]
263
+ .sort((a, b) => b.weighted_score - a.weighted_score)
264
+ .slice(0, limit);
265
+ this.display.log(`🧠 Unified vector search: ${filtered.length} acima do threshold (${longTerm.length} long-term, ${Math.min(chunks.length, chunkCap)} chunks) → ${combined.length} retornados`, { source: 'Sati', level: 'debug' });
266
+ return combined.map(r => ({
269
267
  id: r.id,
270
268
  summary: r.summary,
271
269
  details: r.details,
@@ -374,7 +372,7 @@ export class SatiRepository {
374
372
  this.display.log('🧵 Tentando fallback LIKE...', { source: 'Sati', level: 'debug' });
375
373
  const likeStmt = this.db.prepare(`
376
374
  SELECT * FROM long_term_memory
377
- WHERE (summary LIKE ? OR details LIKE ?)
375
+ WHERE (summary LIKE ? OR details LIKE ?)
378
376
  AND archived = 0
379
377
  ORDER BY importance DESC, access_count DESC
380
378
  LIMIT ?
@@ -8,6 +8,7 @@ const SHORT_DB_PATH = path.join(homedir(), '.morpheus', 'memory', 'short-memory.
8
8
  const SATI_DB_PATH = path.join(homedir(), '.morpheus', 'memory', 'sati-memory.db');
9
9
  const EMBEDDING_DIM = 384;
10
10
  const BATCH_LIMIT = 5;
11
+ const PARALLEL_CHUNKS = 50;
11
12
  export async function runSessionEmbeddingWorker() {
12
13
  const display = DisplayManager.getInstance();
13
14
  // display.log('🚀 Iniciando worker de embeddings de sessões...', { source: 'SessionEmbeddingWorker' });
@@ -62,15 +63,22 @@ export async function runSessionEmbeddingWorker() {
62
63
  (session_chunk_id, vec_rowid)
63
64
  VALUES (?, ?)
64
65
  `);
65
- for (const chunk of chunks) {
66
- display.log(` ↳ Embedding chunk ${chunk.id}`, { source: 'SessionEmbeddingWorker' });
67
- const embedding = await embeddingService.generate(chunk.content);
68
- if (!embedding || embedding.length !== EMBEDDING_DIM) {
69
- throw new Error(`Embedding inválido. Esperado ${EMBEDDING_DIM}, recebido ${embedding?.length}`);
66
+ display.log(` ↳ Processando ${chunks.length} chunks em paralelo (${PARALLEL_CHUNKS})...`, { source: 'SessionEmbeddingWorker' });
67
+ for (let i = 0; i < chunks.length; i += PARALLEL_CHUNKS) {
68
+ const batch = chunks.slice(i, i + PARALLEL_CHUNKS);
69
+ const batchEmbeddings = await Promise.all(batch.map(async (chunk) => {
70
+ const embedding = await embeddingService.generate(chunk.content);
71
+ return { chunkId: chunk.id, embedding };
72
+ }));
73
+ for (const { chunkId, embedding } of batchEmbeddings) {
74
+ if (!embedding || embedding.length !== EMBEDDING_DIM) {
75
+ throw new Error(`Embedding inválido. Esperado ${EMBEDDING_DIM}, recebido ${embedding?.length}`);
76
+ }
77
+ const result = insertVec.run(new Float32Array(embedding));
78
+ const vecRowId = result.lastInsertRowid;
79
+ insertMap.run(chunkId, vecRowId);
70
80
  }
71
- const result = insertVec.run(new Float32Array(embedding));
72
- const vecRowId = result.lastInsertRowid;
73
- insertMap.run(chunk.id, vecRowId);
81
+ display.log(` ↳ Batch ${Math.floor(i / PARALLEL_CHUNKS) + 1}/${Math.ceil(chunks.length / PARALLEL_CHUNKS)} concluído`, { source: 'SessionEmbeddingWorker' });
74
82
  }
75
83
  // ✅ finalizar sessão
76
84
  shortDb.prepare(`
@@ -3,6 +3,7 @@ import { HumanMessage, AIMessage, SystemMessage, ToolMessage } from "@langchain/
3
3
  import Database from "better-sqlite3";
4
4
  import fs from "fs-extra";
5
5
  import * as path from "path";
6
+ import { ConfigManager } from "../../config/manager.js";
6
7
  import { PATHS } from "../../config/paths.js";
7
8
  import { randomUUID } from 'crypto';
8
9
  import { DisplayManager } from "../display.js";
@@ -39,6 +40,10 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
39
40
  static invalidateCacheForSession(sessionId) {
40
41
  SQLiteChatMessageHistory._cache.delete(sessionId);
41
42
  }
43
+ /** Clear the entire message cache. Called during hot-reload to prevent stale tool_calls. */
44
+ static clearCache() {
45
+ SQLiteChatMessageHistory._cache.clear();
46
+ }
42
47
  /** Prepend new messages (newest-first order) to an existing cache entry. */
43
48
  static _appendToCache(sessionId, newMessages) {
44
49
  if (!sessionId || newMessages.length === 0)
@@ -201,11 +206,11 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
201
206
  ('openai', 'gpt-3.5-turbo', 0.5, 1.5),
202
207
  ('openai', 'o1', 15.0, 60.0),
203
208
  ('openai', 'o1-mini', 3.0, 12.0),
204
- ('google', 'gemini-2.5-flash', 0.15, 0.6),
205
- ('google', 'gemini-2.5-flash-lite', 0.075, 0.3),
206
- ('google', 'gemini-2.0-flash', 0.1, 0.4),
207
- ('google', 'gemini-1.5-pro', 1.25, 5.0),
208
- ('google', 'gemini-1.5-flash', 0.075, 0.3);
209
+ ('gemini', 'gemini-2.5-flash', 0.15, 0.6),
210
+ ('gemini', 'gemini-2.5-flash-lite', 0.075, 0.3),
211
+ ('gemini', 'gemini-2.0-flash', 0.1, 0.4),
212
+ ('gemini', 'gemini-1.5-pro', 1.25, 5.0),
213
+ ('gemini', 'gemini-1.5-flash', 0.075, 0.3);
209
214
 
210
215
  CREATE TABLE IF NOT EXISTS model_presets (
211
216
  id TEXT PRIMARY KEY,
@@ -281,6 +286,17 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
281
286
  catch (error) {
282
287
  console.warn(`[SQLite] model_pricing migration failed: ${error}`);
283
288
  }
289
+ // Migration: rename 'google' provider to 'gemini' for consistency
290
+ // This ensures existing model_pricing records work with the unified 'gemini' provider
291
+ try {
292
+ const result = this.db.prepare("UPDATE model_pricing SET provider = 'gemini' WHERE provider = 'google'").run();
293
+ if (result.changes > 0) {
294
+ console.log(`[SQLite] Migrated ${result.changes} model_pricing records from 'google' to 'gemini'`);
295
+ }
296
+ }
297
+ catch (error) {
298
+ console.warn(`[SQLite] google->gemini migration failed: ${error}`);
299
+ }
284
300
  // Ensure model_presets table exists for databases created before this feature
285
301
  try {
286
302
  this.db.exec(`
@@ -1031,7 +1047,7 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
1031
1047
  );
1032
1048
  CREATE INDEX IF NOT EXISTS idx_session_chunks_session_id ON session_chunks(session_id);
1033
1049
  `);
1034
- const chunks = this.chunkText(sessionText);
1050
+ const chunks = this.chunkText(sessionText, ConfigManager.getInstance().getSatiConfig().chunk_size ?? 500);
1035
1051
  const now = Date.now();
1036
1052
  const insert = dbSati.prepare(`
1037
1053
  INSERT INTO session_chunks (id, session_id, chunk_index, content, created_at)
@@ -29,6 +29,8 @@ export async function migrateConfigFile() {
29
29
  await migrateContextWindow();
30
30
  // Migrate santi -> sati
31
31
  await migrateSantiToSati();
32
+ // Migrate google -> gemini in audio and tts providers
33
+ await migrateGoogleToGemini();
32
34
  }
33
35
  /**
34
36
  * Migrates memory.limit to llm.context_window
@@ -118,3 +120,46 @@ async function migrateSantiToSati() {
118
120
  });
119
121
  }
120
122
  }
123
+ /**
124
+ * Migrates audio.provider and audio.tts.provider from 'google' to 'gemini'
125
+ * Ensures consistency across the system (LLMs and audio use 'gemini' as provider)
126
+ */
127
+ async function migrateGoogleToGemini() {
128
+ const display = DisplayManager.getInstance();
129
+ const configPath = PATHS.config;
130
+ try {
131
+ if (!await fs.pathExists(configPath)) {
132
+ return;
133
+ }
134
+ const configContent = await fs.readFile(configPath, 'utf8');
135
+ const config = yaml.load(configContent);
136
+ // Check if migration is needed
137
+ const needsMigration = config?.audio?.provider === 'google' ||
138
+ config?.audio?.tts?.provider === 'google';
139
+ if (!needsMigration) {
140
+ return;
141
+ }
142
+ // Create backup before migration
143
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
144
+ const backupPath = `${configPath}.backup-${timestamp}`;
145
+ await fs.copy(configPath, backupPath);
146
+ display.log(`Created config backup: ${backupPath}`, { source: 'Migration', level: 'info' });
147
+ // Perform migration
148
+ if (config?.audio?.provider === 'google') {
149
+ config.audio.provider = 'gemini';
150
+ }
151
+ if (config?.audio?.tts?.provider === 'google') {
152
+ config.audio.tts.provider = 'gemini';
153
+ }
154
+ // Write migrated config
155
+ const migratedYaml = yaml.dump(config);
156
+ await fs.writeFile(configPath, migratedYaml, 'utf8');
157
+ display.log('Migrated audio providers: google → gemini', { source: 'Migration', level: 'info' });
158
+ }
159
+ catch (error) {
160
+ display.log(`Config migration (google->gemini) failed: ${error.message}`, {
161
+ source: 'Migration',
162
+ level: 'warning'
163
+ });
164
+ }
165
+ }
@@ -765,5 +765,8 @@ Use it to inform your response and tool selection (if needed), but do not assume
765
765
  ]);
766
766
  await SubagentRegistry.reloadAll();
767
767
  this.display.log(`Oracle and subagent tools reloaded`, { source: 'Oracle' });
768
+ // Clear message cache to prevent stale tool_calls from causing Gemini 400 errors
769
+ SQLiteChatMessageHistory.clearCache();
770
+ this.display.log(`Message cache cleared after tool reload`, { source: 'Oracle' });
768
771
  }
769
772
  }
@@ -73,8 +73,8 @@ Use this tool when the user asks for ANY of the following:
73
73
  delegateToolName: 'apoc_delegate', emoji: '🧑‍🔬', color: 'amber',
74
74
  description: 'Filesystem, shell & browser',
75
75
  colorClass: 'text-amber-600 dark:text-amber-400',
76
- bgClass: 'bg-amber-50 dark:bg-amber-900/10',
77
- badgeClass: 'bg-amber-100 text-amber-700 dark:bg-amber-900/40 dark:text-amber-300',
76
+ bgClass: 'bg-amber-50 dark:bg-amber-900/30',
77
+ badgeClass: 'bg-amber-100 text-amber-700 dark:bg-amber-800 dark:text-amber-200',
78
78
  instance: Apoc.instance,
79
79
  hasDynamicDescription: true,
80
80
  isMultiInstance: false,
@@ -64,8 +64,8 @@ export class Link {
64
64
  delegateToolName: 'link_delegate', emoji: '🕵️‍♂️', color: 'indigo',
65
65
  description: 'Document search & RAG',
66
66
  colorClass: 'text-indigo-600 dark:text-indigo-400',
67
- bgClass: 'bg-indigo-50 dark:bg-indigo-900/10',
68
- badgeClass: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-900/40 dark:text-indigo-300',
67
+ bgClass: 'bg-indigo-50 dark:bg-indigo-900/30',
68
+ badgeClass: 'bg-indigo-100 text-indigo-700 dark:bg-indigo-800 dark:text-indigo-200',
69
69
  instance: Link.instance,
70
70
  hasDynamicDescription: true,
71
71
  isMultiInstance: false,
@@ -294,13 +294,13 @@ export class LinkWorker {
294
294
  }
295
295
  /**
296
296
  * Generate embeddings for chunks using Sati's EmbeddingService.
297
+ * Processes chunks in parallel batches for better performance.
297
298
  */
298
299
  async generateEmbeddings(chunks) {
299
300
  if (!this.embeddingService) {
300
301
  this.embeddingService = await EmbeddingService.getInstance();
301
302
  }
302
303
  const embeddings = [];
303
- // Process in batches to avoid memory issues
304
304
  const batchSize = 50;
305
305
  for (let i = 0; i < chunks.length; i += batchSize) {
306
306
  const batch = chunks.slice(i, i + batchSize);
@@ -309,6 +309,7 @@ export class LinkWorker {
309
309
  return { chunk_id: chunk.id, embedding };
310
310
  }));
311
311
  embeddings.push(...batchEmbeddings);
312
+ this.display.log(`Generated embeddings: batch ${Math.floor(i / batchSize) + 1}/${Math.ceil(chunks.length / batchSize)} (${embeddings.length}/${chunks.length})`, { source: 'Link', level: 'debug' });
312
313
  }
313
314
  // Store embeddings in database
314
315
  this.repository.createEmbeddings(embeddings);
@@ -67,8 +67,8 @@ export class Neo {
67
67
  delegateToolName: 'neo_delegate', emoji: '🥷', color: 'violet',
68
68
  description: 'MCP tool orchestration',
69
69
  colorClass: 'text-violet-600 dark:text-violet-400',
70
- bgClass: 'bg-violet-50 dark:bg-violet-900/10',
71
- badgeClass: 'bg-purple-100 text-purple-700 dark:bg-purple-900/40 dark:text-purple-300',
70
+ bgClass: 'bg-violet-50 dark:bg-violet-900/30',
71
+ badgeClass: 'bg-purple-100 text-purple-700 dark:bg-purple-800 dark:text-purple-200',
72
72
  instance: Neo.instance,
73
73
  hasDynamicDescription: true,
74
74
  isMultiInstance: false,
@@ -161,8 +161,10 @@ ${context ? `Context:\n${context}` : ""}
161
161
  const stepCount = response.messages.filter((m) => m instanceof AIMessage).length;
162
162
  const targetSession = sessionId ?? Neo.currentSessionId ?? "neo";
163
163
  await persistAgentMessage('neo', content, neoConfig, targetSession, rawUsage, durationMs);
164
+ // MCP tools are already audited inline by instrumentMcpTool (with timing + args/result).
165
+ // Only emit audit events here for internal Morpheus tools (not MCP tools).
164
166
  emitToolAuditEvents(response.messages.slice(inputCount), targetSession, 'neo', {
165
- defaultEventType: 'mcp_tool',
167
+ defaultEventType: undefined, // skip MCP tools — audited by instrumentMcpTool
166
168
  internalToolNames: MORPHEUS_TOOL_NAMES,
167
169
  });
168
170
  this.display.log("Neo task completed.", { source: "Neo" });
@@ -8,32 +8,32 @@ export const SYSTEM_AGENTS = [
8
8
  delegateToolName: '', emoji: '🔮', color: 'blue',
9
9
  description: 'Root orchestrator',
10
10
  colorClass: 'text-blue-600 dark:text-blue-400',
11
- bgClass: 'bg-blue-50 dark:bg-blue-900/10',
12
- badgeClass: 'bg-blue-100 text-blue-700 dark:bg-blue-900/40 dark:text-blue-300',
11
+ bgClass: 'bg-blue-50 dark:bg-blue-900/30',
12
+ badgeClass: 'bg-blue-100 text-blue-700 dark:bg-blue-800 dark:text-blue-200',
13
13
  },
14
14
  {
15
15
  agentKey: 'chronos', auditAgent: 'chronos', label: 'Chronos',
16
16
  delegateToolName: '', emoji: '⏰', color: 'orange',
17
17
  description: 'Temporal scheduler',
18
18
  colorClass: 'text-orange-600 dark:text-orange-400',
19
- bgClass: 'bg-orange-50 dark:bg-orange-900/10',
20
- badgeClass: 'bg-orange-100 text-orange-700 dark:bg-orange-900/40 dark:text-orange-300',
19
+ bgClass: 'bg-orange-50 dark:bg-orange-900/30',
20
+ badgeClass: 'bg-orange-100 text-orange-700 dark:bg-orange-800 dark:text-orange-200',
21
21
  },
22
22
  {
23
23
  agentKey: 'sati', auditAgent: 'sati', label: 'Sati',
24
24
  delegateToolName: '', emoji: '🧠', color: 'emerald',
25
25
  description: 'Long-term memory',
26
26
  colorClass: 'text-emerald-600 dark:text-emerald-400',
27
- bgClass: 'bg-emerald-50 dark:bg-emerald-900/10',
28
- badgeClass: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-300',
27
+ bgClass: 'bg-emerald-50 dark:bg-emerald-900/30',
28
+ badgeClass: 'bg-emerald-100 text-emerald-700 dark:bg-emerald-800 dark:text-emerald-200',
29
29
  },
30
30
  {
31
31
  agentKey: 'telephonist', auditAgent: 'telephonist', label: 'Telephonist',
32
32
  delegateToolName: '', emoji: '📞', color: 'rose',
33
33
  description: 'Audio transcription',
34
34
  colorClass: 'text-rose-600 dark:text-rose-400',
35
- bgClass: 'bg-rose-50 dark:bg-rose-900/10',
36
- badgeClass: 'bg-rose-100 text-rose-700 dark:bg-rose-900/40 dark:text-rose-300',
35
+ bgClass: 'bg-rose-50 dark:bg-rose-900/30',
36
+ badgeClass: 'bg-rose-100 text-rose-700 dark:bg-rose-800 dark:text-rose-200',
37
37
  },
38
38
  ];
39
39
  /**
@@ -58,8 +58,8 @@ export class Trinity {
58
58
  delegateToolName: 'trinity_delegate', emoji: '👩‍💻', color: 'teal',
59
59
  description: 'Database specialist',
60
60
  colorClass: 'text-teal-600 dark:text-teal-400',
61
- bgClass: 'bg-teal-50 dark:bg-teal-900/10',
62
- badgeClass: 'bg-teal-100 text-teal-700 dark:bg-teal-900/40 dark:text-teal-300',
61
+ bgClass: 'bg-teal-50 dark:bg-teal-900/30',
62
+ badgeClass: 'bg-teal-100 text-teal-700 dark:bg-teal-800 dark:text-teal-200',
63
63
  instance: Trinity.instance,
64
64
  hasDynamicDescription: true,
65
65
  isMultiInstance: false,
@@ -55,6 +55,8 @@ export function emitToolAuditEvents(messages, sessionId, agent, opts) {
55
55
  const result = tc.id ? toolResults.get(tc.id) : undefined;
56
56
  const isError = typeof result === 'string' && /^error:/i.test(result.trim());
57
57
  const eventType = internalToolNames?.has(tc.name) ? 'tool_call' : defaultEventType;
58
+ if (!eventType)
59
+ continue; // no event type assigned — caller opted out for this tool
58
60
  const meta = {};
59
61
  if (tc.args && Object.keys(tc.args).length > 0)
60
62
  meta.args = tc.args;
@@ -170,14 +170,14 @@ class OpenRouterTelephonist {
170
170
  * based on the audio provider configuration.
171
171
  *
172
172
  * Supported providers:
173
- * - google: Google Gemini (native audio file upload)
173
+ * - gemini: Google Gemini (native audio file upload)
174
174
  * - openai: OpenAI Whisper API (/audio/transcriptions)
175
175
  * - openrouter: OpenRouter SDK with input_audio (multimodal models)
176
176
  * - ollama: Ollama local Whisper via OpenAI-compatible endpoint
177
177
  */
178
178
  export function createTelephonist(config) {
179
179
  switch (config.provider) {
180
- case 'google':
180
+ case 'gemini':
181
181
  return new GeminiTelephonist(config.model);
182
182
  case 'openai':
183
183
  return new WhisperTelephonist(config.model);
@@ -188,7 +188,7 @@ export function createTelephonist(config) {
188
188
  // Requires a Whisper model loaded: `ollama pull whisper`
189
189
  return new WhisperTelephonist(config.model, (config.base_url || 'http://localhost:11434') + '/v1');
190
190
  default:
191
- throw new Error(`Unsupported audio provider: '${config.provider}'. Supported: google, openai, openrouter, ollama.`);
191
+ throw new Error(`Unsupported audio provider: '${config.provider}'. Supported: gemini, openai, openrouter, ollama.`);
192
192
  }
193
193
  }
194
194
  // ─── TTS Implementations ─────────────────────────────────────────────────────
@@ -334,16 +334,16 @@ class GeminiTtsTelephonist {
334
334
  }
335
335
  /**
336
336
  * Factory that creates an ITelephonist with TTS (synthesize) support.
337
- * Supports providers: openai, google.
337
+ * Supports providers: openai, gemini.
338
338
  */
339
339
  export function createTtsTelephonist(config) {
340
340
  switch (config.provider) {
341
341
  case 'openai':
342
342
  return new OpenAITtsTelephonist(config.model, config.voice);
343
- case 'google':
343
+ case 'gemini':
344
344
  return new GeminiTtsTelephonist(config.model, config.voice);
345
345
  default:
346
- throw new Error(`Unsupported TTS provider: '${config.provider}'. Supported: openai, google.`);
346
+ throw new Error(`Unsupported TTS provider: '${config.provider}'. Supported: openai, gemini.`);
347
347
  }
348
348
  }
349
349
  // ─── Legacy export for backward compatibility ─────────────────────────────────