morpheus-cli 0.2.7 → 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.
@@ -2,13 +2,18 @@ import Database from 'better-sqlite3';
2
2
  import path from 'path';
3
3
  import { homedir } from 'os';
4
4
  import fs from 'fs-extra';
5
- import { randomUUID } from 'crypto'; // Available in recent Node versions
5
+ import { randomUUID } from 'crypto';
6
+ import loadVecExtension from '../sqlite-vec.js';
7
+ import { DisplayManager } from '../../display.js';
8
+ const EMBEDDING_DIM = 384;
6
9
  export class SatiRepository {
7
10
  db = null;
8
11
  dbPath;
9
12
  static instance;
13
+ display = DisplayManager.getInstance();
10
14
  constructor(dbPath) {
11
- this.dbPath = dbPath || path.join(homedir(), '.morpheus', 'memory', 'santi-memory.db');
15
+ this.dbPath =
16
+ dbPath || path.join(homedir(), '.morpheus', 'memory', 'sati-memory.db');
12
17
  }
13
18
  static getInstance(dbPath) {
14
19
  if (!SatiRepository.instance) {
@@ -17,59 +22,248 @@ export class SatiRepository {
17
22
  return SatiRepository.instance;
18
23
  }
19
24
  initialize() {
20
- try {
21
- // Ensure directory exists
22
- fs.ensureDirSync(path.dirname(this.dbPath));
23
- // Connect to database
24
- this.db = new Database(this.dbPath, { timeout: 5000 });
25
- this.db.pragma('journal_mode = WAL');
26
- // Create schema
27
- this.createSchema();
28
- }
29
- catch (error) {
30
- console.error(`[SatiRepository] Failed to initialize database: ${error}`);
31
- throw error;
32
- }
25
+ fs.ensureDirSync(path.dirname(this.dbPath));
26
+ this.db = new Database(this.dbPath, { timeout: 5000 });
27
+ this.db.pragma('journal_mode = WAL');
28
+ loadVecExtension(this.db);
29
+ this.createSchema();
33
30
  }
34
31
  createSchema() {
35
32
  if (!this.db)
36
- throw new Error("DB not initialized");
33
+ throw new Error('DB not initialized');
37
34
  this.db.exec(`
38
- CREATE TABLE IF NOT EXISTS long_term_memory (
39
- id TEXT PRIMARY KEY,
40
- category TEXT NOT NULL,
41
- importance TEXT NOT NULL,
42
- summary TEXT NOT NULL,
43
- details TEXT,
44
- hash TEXT NOT NULL UNIQUE,
45
- source TEXT,
46
- created_at TEXT NOT NULL,
47
- updated_at TEXT NOT NULL,
48
- last_accessed_at TEXT,
49
- access_count INTEGER DEFAULT 0,
50
- version INTEGER DEFAULT 1,
51
- archived INTEGER DEFAULT 0
52
- );
53
-
54
- CREATE INDEX IF NOT EXISTS idx_memory_category ON long_term_memory(category);
55
- CREATE INDEX IF NOT EXISTS idx_memory_importance ON long_term_memory(importance);
56
- CREATE INDEX IF NOT EXISTS idx_memory_archived ON long_term_memory(archived);
57
-
58
- -- FTS5 Virtual Table for semantic-like keyword search
59
- CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(summary, content='long_term_memory', content_rowid='rowid');
60
-
61
- -- Triggers to sync FTS
62
- CREATE TRIGGER IF NOT EXISTS memory_ai AFTER INSERT ON long_term_memory BEGIN
63
- INSERT INTO memory_fts(rowid, summary) VALUES (new.rowid, new.summary);
64
- END;
65
- CREATE TRIGGER IF NOT EXISTS memory_ad AFTER DELETE ON long_term_memory BEGIN
66
- INSERT INTO memory_fts(memory_fts, rowid, summary) VALUES('delete', old.rowid, old.summary);
67
- END;
68
- CREATE TRIGGER IF NOT EXISTS memory_au AFTER UPDATE ON long_term_memory BEGIN
69
- INSERT INTO memory_fts(memory_fts, rowid, summary) VALUES('delete', old.rowid, old.summary);
70
- INSERT INTO memory_fts(rowid, summary) VALUES (new.rowid, new.summary);
71
- END;
72
- `);
35
+ -- ===============================
36
+ -- 1️⃣ TABELA PRINCIPAL
37
+ -- ===============================
38
+ CREATE TABLE IF NOT EXISTS long_term_memory (
39
+ id TEXT PRIMARY KEY,
40
+ category TEXT NOT NULL,
41
+ importance TEXT NOT NULL,
42
+ summary TEXT NOT NULL,
43
+ details TEXT,
44
+ hash TEXT NOT NULL UNIQUE,
45
+ source TEXT,
46
+ created_at TEXT NOT NULL,
47
+ updated_at TEXT NOT NULL,
48
+ last_accessed_at TEXT,
49
+ access_count INTEGER DEFAULT 0,
50
+ version INTEGER DEFAULT 1,
51
+ archived INTEGER DEFAULT 0
52
+ );
53
+
54
+ CREATE INDEX IF NOT EXISTS idx_memory_category
55
+ ON long_term_memory(category);
56
+
57
+ CREATE INDEX IF NOT EXISTS idx_memory_importance
58
+ ON long_term_memory(importance);
59
+
60
+ CREATE INDEX IF NOT EXISTS idx_memory_archived
61
+ ON long_term_memory(archived);
62
+
63
+ -- ===============================
64
+ -- 2️⃣ FTS5
65
+ -- ===============================
66
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_fts USING fts5(
67
+ summary,
68
+ details,
69
+ content='long_term_memory',
70
+ content_rowid='rowid',
71
+ tokenize = 'unicode61 remove_diacritics 2'
72
+ );
73
+
74
+ -- ===============================
75
+ -- 3️⃣ VECTOR TABLE (vec0)
76
+ -- ===============================
77
+ CREATE VIRTUAL TABLE IF NOT EXISTS memory_vec USING vec0(
78
+ embedding float[${EMBEDDING_DIM}]
79
+ );
80
+
81
+ CREATE TABLE IF NOT EXISTS memory_embedding_map (
82
+ memory_id TEXT PRIMARY KEY,
83
+ vec_rowid INTEGER NOT NULL
84
+ );
85
+
86
+ -- ===============================
87
+ -- 4️⃣ TRIGGERS FTS
88
+ -- ===============================
89
+ CREATE TRIGGER IF NOT EXISTS memory_ai
90
+ AFTER INSERT ON long_term_memory BEGIN
91
+ INSERT INTO memory_fts(rowid, summary, details)
92
+ VALUES (new.rowid, new.summary, new.details);
93
+ END;
94
+
95
+ CREATE TRIGGER IF NOT EXISTS memory_ad
96
+ AFTER DELETE ON long_term_memory BEGIN
97
+ INSERT INTO memory_fts(memory_fts, rowid, summary, details)
98
+ VALUES('delete', old.rowid, old.summary, old.details);
99
+ END;
100
+
101
+ CREATE TRIGGER IF NOT EXISTS memory_au
102
+ AFTER UPDATE ON long_term_memory BEGIN
103
+ INSERT INTO memory_fts(memory_fts, rowid, summary, details)
104
+ VALUES('delete', old.rowid, old.summary, old.details);
105
+
106
+ INSERT INTO memory_fts(rowid, summary, details)
107
+ VALUES (new.rowid, new.summary, new.details);
108
+ END;
109
+
110
+ -- ===============================
111
+ -- 3️⃣ VECTOR TABLE SESSIONS (vec0)
112
+ -- ===============================
113
+
114
+ CREATE TABLE IF NOT EXISTS session_chunks (
115
+ id TEXT PRIMARY KEY,
116
+ session_id TEXT NOT NULL,
117
+ chunk_index INTEGER NOT NULL,
118
+ content TEXT NOT NULL,
119
+ created_at TEXT NOT NULL
120
+ );
121
+
122
+
123
+ CREATE VIRTUAL TABLE IF NOT EXISTS session_vec USING vec0(
124
+ embedding float[384]
125
+ );
126
+
127
+ CREATE TABLE IF NOT EXISTS session_embedding_map (
128
+ session_chunk_id TEXT PRIMARY KEY,
129
+ vec_rowid INTEGER NOT NULL
130
+ );
131
+
132
+ `);
133
+ }
134
+ // 🔥 NOVO — Salvar embedding
135
+ upsertEmbedding(memoryId, embedding) {
136
+ if (!this.db)
137
+ this.initialize();
138
+ const getExisting = this.db.prepare(`
139
+ SELECT vec_rowid FROM memory_embedding_map
140
+ WHERE memory_id = ?
141
+ `);
142
+ const insertVec = this.db.prepare(`
143
+ INSERT INTO memory_vec (embedding)
144
+ VALUES (?)
145
+ `);
146
+ const deleteVec = this.db.prepare(`
147
+ DELETE FROM memory_vec WHERE rowid = ?
148
+ `);
149
+ const upsertMap = this.db.prepare(`
150
+ INSERT OR REPLACE INTO memory_embedding_map (memory_id, vec_rowid)
151
+ VALUES (?, ?)
152
+ `);
153
+ const transaction = this.db.transaction(() => {
154
+ const existing = getExisting.get(memoryId);
155
+ if (existing?.vec_rowid) {
156
+ deleteVec.run(existing.vec_rowid);
157
+ }
158
+ const result = insertVec.run(new Float32Array(embedding));
159
+ const newVecRowId = result.lastInsertRowid;
160
+ upsertMap.run(memoryId, newVecRowId);
161
+ });
162
+ transaction();
163
+ }
164
+ // 🔥 NOVO — Busca vetorial
165
+ searchByVector(embedding, limit) {
166
+ if (!this.db)
167
+ return [];
168
+ const SIMILARITY_THRESHOLD = 0.5; // ajuste fino depois
169
+ const stmt = this.db.prepare(`
170
+ SELECT
171
+ m.*,
172
+ vec_distance_cosine(v.embedding, ?) as distance
173
+ FROM memory_vec v
174
+ JOIN memory_embedding_map map ON map.vec_rowid = v.rowid
175
+ JOIN long_term_memory m ON m.id = map.memory_id
176
+ WHERE m.archived = 0
177
+ ORDER BY distance ASC
178
+ LIMIT ?
179
+ `);
180
+ const rows = stmt.all(new Float32Array(embedding), limit);
181
+ // 🔥 Filtrar por similaridade real
182
+ const ranked = rows
183
+ .map(r => ({
184
+ ...r,
185
+ similarity: 1 - r.distance
186
+ }))
187
+ .sort((a, b) => b.distance - a.distance);
188
+ const filtered = ranked
189
+ .filter(r => r.distance >= SIMILARITY_THRESHOLD)
190
+ .sort((a, b) => b.distance - a.distance);
191
+ if (filtered.length > 0) {
192
+ console.log(`[SatiRepository] Vector hit (${filtered.length})`);
193
+ }
194
+ return filtered.map(this.mapRowToRecord);
195
+ }
196
+ searchUnifiedVector(embedding, limit) {
197
+ if (!this.db)
198
+ return [];
199
+ const SIMILARITY_THRESHOLD = 0.75;
200
+ const stmt = this.db.prepare(`
201
+ SELECT *
202
+ FROM (
203
+ -- LONG TERM MEMORY
204
+ SELECT
205
+ m.id as id,
206
+ m.summary as summary,
207
+ m.details as details,
208
+ m.category as category,
209
+ m.importance as importance,
210
+ 'long_term' as source_type,
211
+ (1 - vec_distance_cosine(v.embedding, ?)) * 0.7 as distance
212
+ FROM memory_vec v
213
+ JOIN memory_embedding_map map ON map.vec_rowid = v.rowid
214
+ JOIN long_term_memory m ON m.id = map.memory_id
215
+ WHERE m.archived = 0
216
+
217
+ UNION ALL
218
+
219
+ -- SESSION CHUNKS
220
+ SELECT
221
+ sc.id as id,
222
+ sc.content as summary,
223
+ sc.content as details,
224
+ 'session' as category,
225
+ 'medium' as importance,
226
+ 'session_chunk' as source_type,
227
+ (1 - vec_distance_cosine(v.embedding, ?)) * 0.3 as distance
228
+ FROM session_vec v
229
+ JOIN session_embedding_map map ON map.vec_rowid = v.rowid
230
+ JOIN session_chunks sc ON sc.id = map.session_chunk_id
231
+ )
232
+ ORDER BY distance ASC
233
+ LIMIT ?
234
+ `);
235
+ const rows = stmt.all(new Float32Array(embedding), new Float32Array(embedding), limit);
236
+ // console.log(
237
+ // `[SatiRepository] Unified vector search returned ${rows.length} raw results`
238
+ // );
239
+ // console each row
240
+ // rows.forEach((row, index) => {
241
+ // console.log(`[SatiRepository] Row ${index + 1}:`, row);
242
+ // });
243
+ const ranked = rows
244
+ .map(r => ({
245
+ ...r,
246
+ similarity: 1 - r.distance
247
+ }))
248
+ .sort((a, b) => b.similarity - a.similarity);
249
+ const filtered = ranked
250
+ .filter(r => r.similarity >= SIMILARITY_THRESHOLD)
251
+ .sort((a, b) => b.similarity - a.similarity);
252
+ this.display.log(`🧠 Unified vector search retornou ${filtered.length} resultados`, { source: 'Sati', level: 'debug' });
253
+ return filtered.map(r => ({
254
+ id: r.id,
255
+ summary: r.summary,
256
+ details: r.details,
257
+ category: r.category,
258
+ importance: r.importance,
259
+ hash: '',
260
+ source: r.source_type,
261
+ created_at: new Date(),
262
+ updated_at: new Date(),
263
+ access_count: 0,
264
+ version: 1,
265
+ archived: false
266
+ }));
73
267
  }
74
268
  async save(record) {
75
269
  if (!this.db)
@@ -118,73 +312,84 @@ export class SatiRepository {
118
312
  const row = this.db.prepare('SELECT * FROM long_term_memory WHERE hash = ?').get(hash);
119
313
  return row ? this.mapRowToRecord(row) : null;
120
314
  }
121
- search(query, limit = 5) {
315
+ search(query, limit = 5, embedding) {
122
316
  if (!this.db)
123
317
  this.initialize();
124
- // Sanitize query for FTS5: remove characters that break FTS5 syntax
125
- // Keep only alphanumeric, spaces, and safe punctuation (comma, period, hyphen)
126
- // const safeQuery = query.replace(/[^a-zA-Z0-9\s,.\-]/g, "").trim();
127
- // if (!safeQuery) {
128
- // console.warn('[SatiRepository] Empty query after sanitization');
129
- // return this.getFallbackMemories(limit);
130
- // }
131
- // try {
132
- // // Try FTS5 search first
133
- // const stmt = this.db!.prepare(`
134
- // SELECT m.*
135
- // FROM long_term_memory m
136
- // JOIN memory_fts f ON m.rowid = f.rowid
137
- // WHERE memory_fts MATCH ? AND m.archived = 0
138
- // ORDER BY rank
139
- // LIMIT ?
140
- // `);
141
- // const rows = stmt.all(safeQuery, limit) as any[];
142
- // if (rows.length > 0) {
143
- // console.log(`[SatiRepository] FTS5 found ${rows.length} memories for: "${safeQuery}"`);
144
- // return rows.map(this.mapRowToRecord);
145
- // }
146
- // // Fallback: try LIKE search
147
- // console.log(`[SatiRepository] FTS5 returned no results, trying LIKE search for: "${safeQuery}"`);
148
- // const likeStmt = this.db!.prepare(`
149
- // SELECT * FROM long_term_memory
150
- // WHERE (summary LIKE ? OR details LIKE ?)
151
- // AND archived = 0
152
- // ORDER BY importance DESC, access_count DESC
153
- // LIMIT ?
154
- // `);
155
- // const likePattern = `%${safeQuery}%`;
156
- // const likeRows = likeStmt.all(likePattern, likePattern, limit) as any[];
157
- // if (likeRows.length > 0) {
158
- // console.log(`[SatiRepository] LIKE search found ${likeRows.length} memories`);
159
- // return likeRows.map(this.mapRowToRecord);
160
- // }
161
- // // Final fallback: return most important/accessed memories
162
- // console.log('[SatiRepository] No search results, returning most important memories');
163
- // return this.getFallbackMemories(limit);
164
- // } catch (e) {
165
- // console.warn(`[SatiRepository] Search failed for query "${query}": ${e}`);
166
- // return this.getFallbackMemories(limit);
167
- // }
168
- return this.getFallbackMemories(limit);
318
+ try {
319
+ this.display.log(`🔍 Iniciando busca de memória | Query: "${query}"`, { source: 'Sati', level: 'debug' });
320
+ // 1️⃣ Vetorial
321
+ if (embedding && embedding.length > 0) {
322
+ this.display.log('🧠 Tentando busca vetorial...', { source: 'Sati', level: 'debug' });
323
+ const vectorResults = this.searchUnifiedVector(embedding, limit);
324
+ if (vectorResults.length > 0) {
325
+ this.display.log(`✅ Vetorial retornou ${vectorResults.length} resultado(s)`, { source: 'Sati', level: 'success' });
326
+ return vectorResults.slice(0, limit);
327
+ }
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' });
332
+ }
333
+ // 2️⃣ BM25 (FTS)
334
+ // Sanitize query: remove characters that could break FTS5 syntax (like ?, *, OR, etc)
335
+ // keeping only letters, numbers and spaces.
336
+ const safeQuery = query
337
+ .replace(/[^\p{L}\p{N}\s]/gu, ' ')
338
+ .replace(/\s+/g, ' ')
339
+ .trim();
340
+ if (safeQuery) {
341
+ this.display.log('📚 Tentando busca BM25 (FTS5)...', { source: 'Sati', level: 'debug' });
342
+ const stmt = this.db.prepare(`
343
+ SELECT m.*, bm25(memory_fts) as rank
344
+ FROM long_term_memory m
345
+ JOIN memory_fts ON m.rowid = memory_fts.rowid
346
+ WHERE memory_fts MATCH ?
347
+ AND m.archived = 0
348
+ ORDER BY rank
349
+ LIMIT ?
350
+ `);
351
+ const rows = stmt.all(safeQuery, limit);
352
+ if (rows.length > 0) {
353
+ this.display.log(`✅ BM25 retornou ${rows.length} resultado(s)`, { source: 'Sati', level: 'success' });
354
+ return rows.map(this.mapRowToRecord);
355
+ }
356
+ this.display.log('⚠️ BM25 não encontrou resultados', { source: 'Sati', level: 'debug' });
357
+ }
358
+ // 3️⃣ LIKE fallback
359
+ this.display.log('🧵 Tentando fallback LIKE...', { source: 'Sati', level: 'debug' });
360
+ const likeStmt = this.db.prepare(`
361
+ SELECT * FROM long_term_memory
362
+ WHERE (summary LIKE ? OR details LIKE ?)
363
+ AND archived = 0
364
+ ORDER BY importance DESC, access_count DESC
365
+ LIMIT ?
366
+ `);
367
+ const pattern = `%${query}%`;
368
+ const likeRows = likeStmt.all(pattern, pattern, limit);
369
+ if (likeRows.length > 0) {
370
+ this.display.log(`✅ LIKE retornou ${likeRows.length} resultado(s)`, { source: 'Sati', level: 'success' });
371
+ return likeRows.map(this.mapRowToRecord);
372
+ }
373
+ // 4️⃣ Final fallback
374
+ this.display.log('🛟 Nenhum mecanismo encontrou resultados. Usando fallback estratégico.', { source: 'Sati', level: 'warning' });
375
+ return this.getFallbackMemories(limit);
376
+ }
377
+ catch (e) {
378
+ this.display.log(`❌ Erro durante busca: ${e}`, { source: 'Sati', level: 'error' });
379
+ return this.getFallbackMemories(limit);
380
+ }
169
381
  }
170
382
  getFallbackMemories(limit) {
171
383
  if (!this.db)
172
384
  return [];
173
- const stmt = this.db.prepare(`
385
+ const rows = this.db
386
+ .prepare(`
174
387
  SELECT * FROM long_term_memory
175
388
  WHERE archived = 0
176
- ORDER BY
177
- CASE importance
178
- WHEN 'critical' THEN 1
179
- WHEN 'high' THEN 2
180
- WHEN 'medium' THEN 3
181
- WHEN 'low' THEN 4
182
- END,
183
- access_count DESC,
184
- created_at DESC
389
+ ORDER BY access_count DESC, created_at DESC
185
390
  LIMIT ?
186
- `);
187
- const rows = stmt.all(limit);
391
+ `)
392
+ .all(limit);
188
393
  return rows.map(this.mapRowToRecord);
189
394
  }
190
395
  getAllMemories() {
@@ -204,18 +409,14 @@ export class SatiRepository {
204
409
  source: row.source,
205
410
  created_at: new Date(row.created_at),
206
411
  updated_at: new Date(row.updated_at),
207
- last_accessed_at: row.last_accessed_at ? new Date(row.last_accessed_at) : undefined,
412
+ last_accessed_at: row.last_accessed_at
413
+ ? new Date(row.last_accessed_at)
414
+ : undefined,
208
415
  access_count: row.access_count,
209
416
  version: row.version,
210
417
  archived: Boolean(row.archived)
211
418
  };
212
419
  }
213
- close() {
214
- if (this.db) {
215
- this.db.close();
216
- this.db = null;
217
- }
218
- }
219
420
  archiveMemory(id) {
220
421
  if (!this.db)
221
422
  this.initialize();
@@ -223,4 +424,10 @@ export class SatiRepository {
223
424
  const result = stmt.run(id);
224
425
  return result.changes > 0;
225
426
  }
427
+ close() {
428
+ if (this.db) {
429
+ this.db.close();
430
+ this.db = null;
431
+ }
432
+ }
226
433
  }
@@ -6,6 +6,7 @@ import { SATI_EVALUATION_PROMPT } from './system-prompts.js';
6
6
  import { createHash } from 'crypto';
7
7
  import { DisplayManager } from '../../display.js';
8
8
  import { SQLiteChatMessageHistory } from '../sqlite.js';
9
+ import { EmbeddingService } from '../embedding.service.js';
9
10
  const display = DisplayManager.getInstance();
10
11
  export class SatiService {
11
12
  repository;
@@ -23,12 +24,24 @@ export class SatiService {
23
24
  this.repository.initialize();
24
25
  }
25
26
  async recover(currentMessage, recentMessages) {
26
- const santiConfig = ConfigManager.getInstance().getSatiConfig();
27
- const memoryLimit = santiConfig.memory_limit || 1000;
28
- // Use the current message as the primary search query
29
- // We could enhance this by extracting keywords from the last few messages
30
- // but for FR-004 we start with user input.
31
- const memories = this.repository.search(currentMessage, memoryLimit);
27
+ const satiConfig = ConfigManager.getInstance().getSatiConfig();
28
+ const memoryLimit = satiConfig.memory_limit || 10;
29
+ const enabled_vector_search = satiConfig.enabled_archived_sessions ?? true;
30
+ let queryEmbedding;
31
+ try {
32
+ const embeddingService = await EmbeddingService.getInstance();
33
+ const queryText = [
34
+ ...recentMessages.slice(-3),
35
+ currentMessage
36
+ ].join(' ');
37
+ if (enabled_vector_search) {
38
+ queryEmbedding = await embeddingService.generate(queryText);
39
+ }
40
+ }
41
+ catch (err) {
42
+ console.warn('[Sati] Failed to generate embedding:', err);
43
+ }
44
+ const memories = this.repository.search(currentMessage, memoryLimit, queryEmbedding);
32
45
  return {
33
46
  relevant_memories: memories.map(m => ({
34
47
  summary: m.summary,
@@ -39,12 +52,12 @@ export class SatiService {
39
52
  }
40
53
  async evaluateAndPersist(conversation) {
41
54
  try {
42
- const santiConfig = ConfigManager.getInstance().getSatiConfig();
43
- if (!santiConfig)
55
+ const satiConfig = ConfigManager.getInstance().getSatiConfig();
56
+ if (!satiConfig)
44
57
  return;
45
58
  // Use the main provider factory to get an agent (Reusing Zion configuration)
46
59
  // We pass empty tools as Sati is a pure reasoning agent here
47
- const agent = await ProviderFactory.create(santiConfig, []);
60
+ const agent = await ProviderFactory.create(satiConfig, []);
48
61
  // Get existing memories for context (Simulated "Working Memory" or full list if small)
49
62
  const allMemories = this.repository.getAllMemories();
50
63
  const existingSummaries = allMemories.slice(0, 50).map(m => m.summary);
@@ -69,8 +82,8 @@ export class SatiService {
69
82
  name: 'sati_evaluation_input'
70
83
  });
71
84
  inputMsg.provider_metadata = {
72
- provider: santiConfig.provider,
73
- model: santiConfig.model
85
+ provider: satiConfig.provider,
86
+ model: satiConfig.model
74
87
  };
75
88
  await history.addMessage(inputMsg);
76
89
  }
@@ -90,8 +103,8 @@ export class SatiService {
90
103
  outputToolMsg.usage_metadata = lastMessage.usage_metadata;
91
104
  }
92
105
  outputToolMsg.provider_metadata = {
93
- provider: santiConfig.provider,
94
- model: santiConfig.model
106
+ provider: satiConfig.provider,
107
+ model: satiConfig.model
95
108
  };
96
109
  await history.addMessage(outputToolMsg);
97
110
  }
@@ -100,7 +113,7 @@ export class SatiService {
100
113
  }
101
114
  // Safe JSON parsing (handle markdown blocks if LLM wraps output)
102
115
  content = content.replace(/```json/g, '').replace(/```/g, '').trim();
103
- let result;
116
+ let result = [];
104
117
  try {
105
118
  result = JSON.parse(content);
106
119
  }
@@ -108,26 +121,38 @@ export class SatiService {
108
121
  console.warn('[SatiService] Failed to parse JSON response:', content);
109
122
  return;
110
123
  }
111
- if (result.should_store && result.summary && result.category && result.importance) {
112
- display.log(`Persisting new memory: [${result.category.toUpperCase()}] ${result.summary}`, { source: 'Sati' });
113
- try {
114
- await this.repository.save({
115
- summary: result.summary,
116
- category: result.category,
117
- importance: result.importance,
118
- details: result.reason,
119
- hash: this.generateHash(result.summary),
120
- source: 'conversation' // Could track actual session ID here if available
121
- });
122
- // Quiet success - logging handled by repository/middleware if needed, or verbose debug
123
- }
124
- catch (saveError) {
125
- if (saveError.message && saveError.message.includes('UNIQUE constraint failed')) {
126
- // Duplicate detected by DB (Hash collision)
127
- // This is expected given T012 logic
124
+ for (const item of result) {
125
+ if (item.should_store && item.summary && item.category && item.importance) {
126
+ display.log(`Persisting new memory: [${item.category.toUpperCase()}] ${item.summary}`, { source: 'Sati' });
127
+ try {
128
+ const savedMemory = await this.repository.save({
129
+ summary: item.summary,
130
+ category: item.category,
131
+ importance: item.importance,
132
+ details: item.reason,
133
+ hash: this.generateHash(item.summary),
134
+ source: 'conversation'
135
+ });
136
+ // 🔥 GERAR EMBEDDING
137
+ const embeddingService = await EmbeddingService.getInstance();
138
+ const textForEmbedding = [
139
+ savedMemory.summary,
140
+ savedMemory.details ?? ''
141
+ ].join(' ');
142
+ const embedding = await embeddingService.generate(textForEmbedding);
143
+ display.log(`Generated embedding for memory ID ${savedMemory.id}`, { source: 'Sati', level: 'debug' });
144
+ // 🔥 SALVAR EMBEDDING NO SQLITE_VEC
145
+ this.repository.upsertEmbedding(savedMemory.id, embedding);
146
+ // Quiet success - logging handled by repository/middleware if needed, or verbose debug
128
147
  }
129
- else {
130
- throw saveError;
148
+ catch (saveError) {
149
+ if (saveError.message && saveError.message.includes('UNIQUE constraint failed')) {
150
+ // Duplicate detected by DB (Hash collision)
151
+ // This is expected given T012 logic
152
+ }
153
+ else {
154
+ throw saveError;
155
+ }
131
156
  }
132
157
  }
133
158
  }