morpheus-cli 0.2.8 → 0.3.1

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.
@@ -12,8 +12,10 @@ const __dirname = path.dirname(__filename);
12
12
  export class HttpServer {
13
13
  app;
14
14
  server;
15
- constructor() {
15
+ oracle;
16
+ constructor(oracle) {
16
17
  this.app = express();
18
+ this.oracle = oracle;
17
19
  this.setupMiddleware();
18
20
  this.setupRoutes();
19
21
  }
@@ -43,7 +45,7 @@ export class HttpServer {
43
45
  uptime: process.uptime()
44
46
  });
45
47
  });
46
- this.app.use('/api', authMiddleware, createApiRouter());
48
+ this.app.use('/api', authMiddleware, createApiRouter(this.oracle));
47
49
  // Serve static frontend from compiled output
48
50
  const uiPath = path.resolve(__dirname, '../ui');
49
51
  this.app.use(express.static(uiPath));
@@ -318,14 +318,17 @@ export class SatiRepository {
318
318
  try {
319
319
  this.display.log(`🔍 Iniciando busca de memória | Query: "${query}"`, { source: 'Sati', level: 'debug' });
320
320
  // 1️⃣ Vetorial
321
- if (embedding) {
322
- this.display.log('🧠 Tentando busca vetorial...', { source: 'Sati', level: 'debug' });
321
+ if (embedding && embedding.length > 0) {
322
+ this.display.log('🧠 Tentando busca vetorial...', { source: 'Sati', level: 'debug' });
323
323
  const vectorResults = this.searchUnifiedVector(embedding, limit);
324
324
  if (vectorResults.length > 0) {
325
- this.display.log(`✅ Vetorial retornou ${vectorResults.length} resultado(s)`, { source: 'Sati', level: 'success' });
325
+ this.display.log(`✅ Vetorial retornou ${vectorResults.length} resultado(s)`, { source: 'Sati', level: 'success' });
326
326
  return vectorResults.slice(0, limit);
327
327
  }
328
- this.display.log('⚠️ Vetorial não encontrou resultados relevantes', { source: 'Sati', level: 'debug' });
328
+ this.display.log('⚠️ Vetorial não encontrou resultados relevantes', { source: 'Sati', level: 'debug' });
329
+ }
330
+ else {
331
+ this.display.log('🛡️ Disabled Archived Sessions in Memory Retrieval', { source: 'Sati', level: 'info' });
329
332
  }
330
333
  // 2️⃣ BM25 (FTS)
331
334
  // Sanitize query: remove characters that could break FTS5 syntax (like ?, *, OR, etc)
@@ -335,7 +338,7 @@ export class SatiRepository {
335
338
  .replace(/\s+/g, ' ')
336
339
  .trim();
337
340
  if (safeQuery) {
338
- this.display.log('📚 Tentando busca BM25 (FTS5)...', { source: 'Sati', level: 'debug' });
341
+ this.display.log('📚 Tentando busca BM25 (FTS5)...', { source: 'Sati', level: 'debug' });
339
342
  const stmt = this.db.prepare(`
340
343
  SELECT m.*, bm25(memory_fts) as rank
341
344
  FROM long_term_memory m
@@ -347,13 +350,13 @@ export class SatiRepository {
347
350
  `);
348
351
  const rows = stmt.all(safeQuery, limit);
349
352
  if (rows.length > 0) {
350
- this.display.log(`✅ BM25 retornou ${rows.length} resultado(s)`, { source: 'Sati', level: 'success' });
353
+ this.display.log(`✅ BM25 retornou ${rows.length} resultado(s)`, { source: 'Sati', level: 'success' });
351
354
  return rows.map(this.mapRowToRecord);
352
355
  }
353
- this.display.log('⚠️ BM25 não encontrou resultados', { source: 'Sati', level: 'debug' });
356
+ this.display.log('⚠️ BM25 não encontrou resultados', { source: 'Sati', level: 'debug' });
354
357
  }
355
358
  // 3️⃣ LIKE fallback
356
- this.display.log('🧵 Tentando fallback LIKE...', { source: 'Sati', level: 'debug' });
359
+ this.display.log('🧵 Tentando fallback LIKE...', { source: 'Sati', level: 'debug' });
357
360
  const likeStmt = this.db.prepare(`
358
361
  SELECT * FROM long_term_memory
359
362
  WHERE (summary LIKE ? OR details LIKE ?)
@@ -364,15 +367,15 @@ export class SatiRepository {
364
367
  const pattern = `%${query}%`;
365
368
  const likeRows = likeStmt.all(pattern, pattern, limit);
366
369
  if (likeRows.length > 0) {
367
- this.display.log(`✅ LIKE retornou ${likeRows.length} resultado(s)`, { source: 'Sati', level: 'success' });
370
+ this.display.log(`✅ LIKE retornou ${likeRows.length} resultado(s)`, { source: 'Sati', level: 'success' });
368
371
  return likeRows.map(this.mapRowToRecord);
369
372
  }
370
373
  // 4️⃣ Final fallback
371
- this.display.log('🛟 Nenhum mecanismo encontrou resultados. Usando fallback estratégico.', { source: 'Sati', level: 'warning' });
374
+ this.display.log('🛟 Nenhum mecanismo encontrou resultados. Usando fallback estratégico.', { source: 'Sati', level: 'warning' });
372
375
  return this.getFallbackMemories(limit);
373
376
  }
374
377
  catch (e) {
375
- this.display.log(`❌ Erro durante busca: ${e}`, { source: 'Sati', level: 'error' });
378
+ this.display.log(`❌ Erro durante busca: ${e}`, { source: 'Sati', level: 'error' });
376
379
  return this.getFallbackMemories(limit);
377
380
  }
378
381
  }
@@ -24,8 +24,9 @@ export class SatiService {
24
24
  this.repository.initialize();
25
25
  }
26
26
  async recover(currentMessage, recentMessages) {
27
- const santiConfig = ConfigManager.getInstance().getSatiConfig();
28
- const memoryLimit = santiConfig.memory_limit || 10;
27
+ const satiConfig = ConfigManager.getInstance().getSatiConfig();
28
+ const memoryLimit = satiConfig.memory_limit || 10;
29
+ const enabled_vector_search = satiConfig.enabled_archived_sessions ?? true;
29
30
  let queryEmbedding;
30
31
  try {
31
32
  const embeddingService = await EmbeddingService.getInstance();
@@ -33,13 +34,14 @@ export class SatiService {
33
34
  ...recentMessages.slice(-3),
34
35
  currentMessage
35
36
  ].join(' ');
36
- queryEmbedding = await embeddingService.generate(queryText);
37
+ if (enabled_vector_search) {
38
+ queryEmbedding = await embeddingService.generate(queryText);
39
+ }
37
40
  }
38
41
  catch (err) {
39
42
  console.warn('[Sati] Failed to generate embedding:', err);
40
43
  }
41
- const memories = this.repository.search(currentMessage, memoryLimit, queryEmbedding // 🔥 agora vai usar vector search
42
- );
44
+ const memories = this.repository.search(currentMessage, memoryLimit, queryEmbedding);
43
45
  return {
44
46
  relevant_memories: memories.map(m => ({
45
47
  summary: m.summary,
@@ -50,12 +52,12 @@ export class SatiService {
50
52
  }
51
53
  async evaluateAndPersist(conversation) {
52
54
  try {
53
- const santiConfig = ConfigManager.getInstance().getSatiConfig();
54
- if (!santiConfig)
55
+ const satiConfig = ConfigManager.getInstance().getSatiConfig();
56
+ if (!satiConfig)
55
57
  return;
56
58
  // Use the main provider factory to get an agent (Reusing Zion configuration)
57
59
  // We pass empty tools as Sati is a pure reasoning agent here
58
- const agent = await ProviderFactory.create(santiConfig, []);
60
+ const agent = await ProviderFactory.create(satiConfig, []);
59
61
  // Get existing memories for context (Simulated "Working Memory" or full list if small)
60
62
  const allMemories = this.repository.getAllMemories();
61
63
  const existingSummaries = allMemories.slice(0, 50).map(m => m.summary);
@@ -80,8 +82,8 @@ export class SatiService {
80
82
  name: 'sati_evaluation_input'
81
83
  });
82
84
  inputMsg.provider_metadata = {
83
- provider: santiConfig.provider,
84
- model: santiConfig.model
85
+ provider: satiConfig.provider,
86
+ model: satiConfig.model
85
87
  };
86
88
  await history.addMessage(inputMsg);
87
89
  }
@@ -101,8 +103,8 @@ export class SatiService {
101
103
  outputToolMsg.usage_metadata = lastMessage.usage_metadata;
102
104
  }
103
105
  outputToolMsg.provider_metadata = {
104
- provider: santiConfig.provider,
105
- model: santiConfig.model
106
+ provider: satiConfig.provider,
107
+ model: satiConfig.model
106
108
  };
107
109
  await history.addMessage(outputToolMsg);
108
110
  }
@@ -24,7 +24,6 @@ export async function runSessionEmbeddingWorker() {
24
24
  FROM sessions
25
25
  WHERE ended_at IS NOT NULL
26
26
  AND embedding_status = 'pending'
27
- OR embedding_status = 'failed'
28
27
  LIMIT ?
29
28
  `).all(BATCH_LIMIT);
30
29
  if (sessions.length === 0) {
@@ -35,12 +34,8 @@ export async function runSessionEmbeddingWorker() {
35
34
  const sessionId = session.id;
36
35
  display.log(`🧠 Processando sessão ${sessionId}...`, { source: 'SessionEmbeddingWorker' });
37
36
  try {
38
- // 🔒 marcar como processing
39
- shortDb.prepare(`
40
- UPDATE sessions
41
- SET embedding_status = 'processing'
42
- WHERE id = ?
43
- `).run(sessionId);
37
+ // Skip setting 'processing' as it violates CHECK constraint
38
+ // active_processing.add(sessionId); // If we needed concurrency control
44
39
  const chunks = satiDb.prepare(`
45
40
  SELECT id, content
46
41
  FROM session_chunks
@@ -51,7 +46,7 @@ export async function runSessionEmbeddingWorker() {
51
46
  display.log(`⚠️ Sessão ${sessionId} não possui chunks.`, { source: 'SessionEmbeddingWorker' });
52
47
  shortDb.prepare(`
53
48
  UPDATE sessions
54
- SET embedding_status = 'completed',
49
+ SET embedding_status = 'embedded',
55
50
  embedded = 1
56
51
  WHERE id = ?
57
52
  `).run(sessionId);
@@ -79,7 +74,7 @@ export async function runSessionEmbeddingWorker() {
79
74
  // ✅ finalizar sessão
80
75
  shortDb.prepare(`
81
76
  UPDATE sessions
82
- SET embedding_status = 'completed',
77
+ SET embedding_status = 'embedded',
83
78
  embedded = 1
84
79
  WHERE id = ?
85
80
  `).run(sessionId);
@@ -615,9 +615,26 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
615
615
  `).run(now, sessionId);
616
616
  });
617
617
  tx(); // Executar a transação
618
- // Se a sessão era active, criar nova sessão ativa
618
+ // Se a sessão era active, verificar se outra para ativar
619
619
  if (session.status === 'active') {
620
- this.createFreshSession();
620
+ const nextSession = this.db.prepare(`
621
+ SELECT id FROM sessions
622
+ WHERE status = 'paused'
623
+ ORDER BY started_at DESC
624
+ LIMIT 1
625
+ `).get();
626
+ if (nextSession) {
627
+ // Promover a próxima sessão a ativa
628
+ this.db.prepare(`
629
+ UPDATE sessions
630
+ SET status = 'active'
631
+ WHERE id = ?
632
+ `).run(nextSession.id);
633
+ }
634
+ else {
635
+ // Nenhuma outra sessão, criar nova
636
+ this.createFreshSession();
637
+ }
621
638
  }
622
639
  }
623
640
  /**
@@ -27,6 +27,8 @@ export async function migrateConfigFile() {
27
27
  }
28
28
  // Migrate memory.limit to llm.context_window
29
29
  await migrateContextWindow();
30
+ // Migrate santi -> sati
31
+ await migrateSantiToSati();
30
32
  }
31
33
  /**
32
34
  * Migrates memory.limit to llm.context_window
@@ -78,3 +80,41 @@ async function migrateContextWindow() {
78
80
  });
79
81
  }
80
82
  }
83
+ /**
84
+ * Migrates santi config section to sati
85
+ * Fixes typo in previous versions
86
+ */
87
+ async function migrateSantiToSati() {
88
+ const display = DisplayManager.getInstance();
89
+ const configPath = PATHS.config;
90
+ try {
91
+ // Check if config file exists
92
+ if (!await fs.pathExists(configPath)) {
93
+ return;
94
+ }
95
+ // Read current config
96
+ const configContent = await fs.readFile(configPath, 'utf8');
97
+ const config = yaml.load(configContent);
98
+ // Check if migration is needed
99
+ if (config?.santi !== undefined && config?.sati === undefined) {
100
+ // Create backup before migration
101
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
102
+ const backupPath = `${configPath}.backup-${timestamp}`;
103
+ await fs.copy(configPath, backupPath);
104
+ display.log(`Created config backup: ${backupPath}`, { source: 'Migration', level: 'info' });
105
+ // Perform migration
106
+ config.sati = config.santi;
107
+ delete config.santi;
108
+ // Write migrated config
109
+ const migratedYaml = yaml.dump(config);
110
+ await fs.writeFile(configPath, migratedYaml, 'utf8');
111
+ display.log('Migrated santi → sati in configuration', { source: 'Migration', level: 'info' });
112
+ }
113
+ }
114
+ catch (error) {
115
+ display.log(`Config migration (santi->sati) failed: ${error.message}`, {
116
+ source: 'Migration',
117
+ level: 'warning'
118
+ });
119
+ }
120
+ }
@@ -257,6 +257,62 @@ You maintain intent until resolution.
257
257
  throw new Error("Current history provider does not support session rollover.");
258
258
  }
259
259
  }
260
+ async setSessionId(sessionId) {
261
+ if (!this.history) {
262
+ throw new Error("Message history not initialized. Call initialize() first.");
263
+ }
264
+ // Check if the history provider supports switching sessions
265
+ // SQLiteChatMessageHistory does support it via constructor (new instance) or maybe we can add a method there too?
266
+ // Actually SQLiteChatMessageHistory has `switchSession(targetSessionId)` but that one logic is "pause current, activate target".
267
+ // For API usage, we might just want to *target* a session without necessarily changing the global "active" state regarding the Daemon?
268
+ //
269
+ // However, the user request implies this is "the" chat.
270
+ // If we use `switchSession` it pauses others. That seems correct for a single-user agent model.
271
+ //
272
+ // But `SQLiteChatMessageHistory` properties are `sessionId`.
273
+ // It seems `switchSession` in `sqlite.ts` updates the DB state.
274
+ // We also need to update the `sessionId` property of the `SQLiteChatMessageHistory` instance held by Oracle.
275
+ //
276
+ // Let's check `SQLiteChatMessageHistory` again.
277
+ // It has `sessionId` property.
278
+ // It does NOT have a method to just update `sessionId` property without DB side effects?
279
+ //
280
+ // Use `switchSession` from `sqlite.ts` is good for "Active/Paused" state management.
281
+ // But we also need the `history` instance to know it is now pointing to `sessionId`.
282
+ if (this.history instanceof SQLiteChatMessageHistory) {
283
+ // Logic:
284
+ // 1. If currently active session is different, switch.
285
+ // 2. Update internal sessionId.
286
+ // Actually `switchSession` in `sqlite.ts` takes `targetSessionId`.
287
+ // It updates the DB status.
288
+ // It DOES NOT seem to update `this.sessionId` of the instance?
289
+ // Wait, let me check `sqlite.ts` content from memory or view it again alongside.
290
+ //
291
+ // In `sqlite.ts`:
292
+ // public async switchSession(targetSessionId: string): Promise<void> { ... }
293
+ // It updates DB.
294
+ // It DOES NOT update `this.sessionId`.
295
+ //
296
+ // So we need to ensure `this.history` points to the new session.
297
+ // Since `SQLiteChatMessageHistory` might not allow changing `sessionId` publicly if it's protected/private...
298
+ // It is `private sessionId: string;`.
299
+ //
300
+ // So simple fix: Re-instantiate `this.history`?
301
+ // `this.history = new SQLiteChatMessageHistory({ sessionId: sessionId, ... })`
302
+ //
303
+ // This is safe and clean.
304
+ await this.history.switchSession(sessionId);
305
+ // Re-instantiate to point to new session
306
+ this.history = new SQLiteChatMessageHistory({
307
+ sessionId: sessionId,
308
+ databasePath: this.databasePath,
309
+ limit: this.config.llm?.context_window ?? 100
310
+ });
311
+ }
312
+ else {
313
+ throw new Error("Current history provider does not support session switching.");
314
+ }
315
+ }
260
316
  async clearMemory() {
261
317
  if (!this.history) {
262
318
  throw new Error("Message history not initialized. Call initialize() first.");
@@ -32,4 +32,12 @@ export const DEFAULT_CONFIG = {
32
32
  enabled: true,
33
33
  port: 3333,
34
34
  },
35
+ sati: {
36
+ provider: 'openai',
37
+ model: 'gpt-4',
38
+ temperature: 0.7,
39
+ context_window: 100,
40
+ memory_limit: 100,
41
+ enabled_archived_sessions: true,
42
+ }
35
43
  };