morpheus-cli 0.2.0 → 0.2.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.
Files changed (34) hide show
  1. package/README.md +346 -273
  2. package/dist/cli/commands/doctor.js +36 -1
  3. package/dist/cli/commands/init.js +92 -0
  4. package/dist/cli/commands/start.js +2 -1
  5. package/dist/cli/index.js +1 -17
  6. package/dist/cli/utils/render.js +2 -1
  7. package/dist/cli/utils/version.js +16 -0
  8. package/dist/config/manager.js +16 -0
  9. package/dist/config/schemas.js +15 -8
  10. package/dist/http/api.js +111 -0
  11. package/dist/runtime/__tests__/manual_santi_verify.js +55 -0
  12. package/dist/runtime/display.js +3 -0
  13. package/dist/runtime/memory/sati/__tests__/repository.test.js +71 -0
  14. package/dist/runtime/memory/sati/__tests__/service.test.js +99 -0
  15. package/dist/runtime/memory/sati/index.js +58 -0
  16. package/dist/runtime/memory/sati/repository.js +226 -0
  17. package/dist/runtime/memory/sati/service.js +142 -0
  18. package/dist/runtime/memory/sati/system-prompts.js +42 -0
  19. package/dist/runtime/memory/sati/types.js +1 -0
  20. package/dist/runtime/memory/sqlite.js +5 -1
  21. package/dist/runtime/migration.js +53 -1
  22. package/dist/runtime/oracle.js +32 -7
  23. package/dist/runtime/santi/contracts.js +1 -0
  24. package/dist/runtime/santi/middleware.js +61 -0
  25. package/dist/runtime/santi/santi.js +109 -0
  26. package/dist/runtime/santi/store.js +158 -0
  27. package/dist/runtime/tools/factory.js +31 -25
  28. package/dist/types/config.js +1 -0
  29. package/dist/ui/assets/index-BLLLlr0w.css +1 -0
  30. package/dist/ui/assets/index-Ccml5qIL.js +50 -0
  31. package/dist/ui/index.html +2 -2
  32. package/package.json +2 -2
  33. package/dist/ui/assets/index-AEbYNHuy.css +0 -1
  34. package/dist/ui/assets/index-BjnI8c1U.js +0 -50
@@ -0,0 +1,58 @@
1
+ import { AIMessage } from "@langchain/core/messages";
2
+ import { SatiService } from "./service.js";
3
+ import { DisplayManager } from "../../display.js";
4
+ const display = DisplayManager.getInstance();
5
+ export class SatiMemoryMiddleware {
6
+ service;
7
+ static instance;
8
+ constructor() {
9
+ this.service = SatiService.getInstance();
10
+ }
11
+ static getInstance() {
12
+ if (!SatiMemoryMiddleware.instance) {
13
+ SatiMemoryMiddleware.instance = new SatiMemoryMiddleware();
14
+ }
15
+ return SatiMemoryMiddleware.instance;
16
+ }
17
+ async beforeAgent(currentMessage, history) {
18
+ try {
19
+ // Extract recent messages content strings for context
20
+ const recentText = history.slice(-10).map(m => m.content.toString());
21
+ display.log(`[Sati] Searching memories for: "${currentMessage.substring(0, 50)}${currentMessage.length > 50 ? '...' : ''}"`, { source: 'Sati' });
22
+ const result = await this.service.recover(currentMessage, recentText);
23
+ if (result.relevant_memories.length === 0) {
24
+ display.log('[Sati] No relevant memories found', { source: 'Sati' });
25
+ return null;
26
+ }
27
+ const memoryContext = result.relevant_memories
28
+ .map(m => `- [${m.category.toUpperCase()}] ${m.summary}`)
29
+ .join('\n');
30
+ display.log(`[Sati] Retrieved ${result.relevant_memories.length} memories.`, { source: 'Sati' });
31
+ return new AIMessage(`
32
+ ### LONG-TERM MEMORY (SATI)
33
+ The following information was retrieved from previous sessions. Use it if relevant:
34
+
35
+ ${memoryContext}
36
+ `);
37
+ }
38
+ catch (error) {
39
+ display.log(`[SatiMiddleware] Error in beforeAgent: ${error}`, { source: 'Sati' });
40
+ // Fail open: return null so execution continues without memory
41
+ return null;
42
+ }
43
+ }
44
+ async afterAgent(generatedResponse, history) {
45
+ try {
46
+ await this.service.evaluateAndPersist([
47
+ ...history.slice(-5).map(m => ({
48
+ role: m._getType() === 'human' ? 'user' : 'assistant',
49
+ content: m.content.toString()
50
+ })),
51
+ { role: 'assistant', content: generatedResponse }
52
+ ]);
53
+ }
54
+ catch (error) {
55
+ console.error('[SatiMiddleware] Error in afterAgent:', error);
56
+ }
57
+ }
58
+ }
@@ -0,0 +1,226 @@
1
+ import Database from 'better-sqlite3';
2
+ import path from 'path';
3
+ import { homedir } from 'os';
4
+ import fs from 'fs-extra';
5
+ import { randomUUID } from 'crypto'; // Available in recent Node versions
6
+ export class SatiRepository {
7
+ db = null;
8
+ dbPath;
9
+ static instance;
10
+ constructor(dbPath) {
11
+ this.dbPath = dbPath || path.join(homedir(), '.morpheus', 'memory', 'santi-memory.db');
12
+ }
13
+ static getInstance(dbPath) {
14
+ if (!SatiRepository.instance) {
15
+ SatiRepository.instance = new SatiRepository(dbPath);
16
+ }
17
+ return SatiRepository.instance;
18
+ }
19
+ 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
+ }
33
+ }
34
+ createSchema() {
35
+ if (!this.db)
36
+ throw new Error("DB not initialized");
37
+ 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
+ `);
73
+ }
74
+ async save(record) {
75
+ if (!this.db)
76
+ this.initialize();
77
+ const now = new Date().toISOString();
78
+ const fullRecord = {
79
+ id: randomUUID(),
80
+ ...record,
81
+ created_at: new Date(now),
82
+ updated_at: new Date(now),
83
+ access_count: 0,
84
+ version: 1,
85
+ archived: false
86
+ };
87
+ const stmt = this.db.prepare(`
88
+ INSERT INTO long_term_memory (
89
+ id, category, importance, summary, details, hash, source,
90
+ created_at, updated_at, last_accessed_at, access_count, version, archived
91
+ ) VALUES (
92
+ @id, @category, @importance, @summary, @details, @hash, @source,
93
+ @created_at, @updated_at, @last_accessed_at, @access_count, @version, @archived
94
+ )
95
+ ON CONFLICT(hash) DO UPDATE SET
96
+ importance = excluded.importance,
97
+ access_count = long_term_memory.access_count + 1,
98
+ last_accessed_at = excluded.updated_at,
99
+ updated_at = excluded.updated_at,
100
+ details = excluded.details
101
+ `);
102
+ // SQLite expects 0/1 for boolean and NULL for undefined
103
+ const params = {
104
+ ...fullRecord,
105
+ details: fullRecord.details || null,
106
+ source: fullRecord.source || null,
107
+ created_at: now,
108
+ updated_at: now,
109
+ last_accessed_at: now, // Set accessing time on save/update
110
+ archived: 0
111
+ };
112
+ stmt.run(params);
113
+ return fullRecord;
114
+ }
115
+ findByHash(hash) {
116
+ if (!this.db)
117
+ this.initialize();
118
+ const row = this.db.prepare('SELECT * FROM long_term_memory WHERE hash = ?').get(hash);
119
+ return row ? this.mapRowToRecord(row) : null;
120
+ }
121
+ search(query, limit = 5) {
122
+ if (!this.db)
123
+ 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);
169
+ }
170
+ getFallbackMemories(limit) {
171
+ if (!this.db)
172
+ return [];
173
+ const stmt = this.db.prepare(`
174
+ SELECT * FROM long_term_memory
175
+ 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
185
+ LIMIT ?
186
+ `);
187
+ const rows = stmt.all(limit);
188
+ return rows.map(this.mapRowToRecord);
189
+ }
190
+ getAllMemories() {
191
+ if (!this.db)
192
+ this.initialize();
193
+ const rows = this.db.prepare('SELECT * FROM long_term_memory WHERE archived = 0 ORDER BY created_at DESC').all();
194
+ return rows.map(this.mapRowToRecord);
195
+ }
196
+ mapRowToRecord(row) {
197
+ return {
198
+ id: row.id,
199
+ category: row.category,
200
+ importance: row.importance,
201
+ summary: row.summary,
202
+ details: row.details,
203
+ hash: row.hash,
204
+ source: row.source,
205
+ created_at: new Date(row.created_at),
206
+ updated_at: new Date(row.updated_at),
207
+ last_accessed_at: row.last_accessed_at ? new Date(row.last_accessed_at) : undefined,
208
+ access_count: row.access_count,
209
+ version: row.version,
210
+ archived: Boolean(row.archived)
211
+ };
212
+ }
213
+ close() {
214
+ if (this.db) {
215
+ this.db.close();
216
+ this.db = null;
217
+ }
218
+ }
219
+ archiveMemory(id) {
220
+ if (!this.db)
221
+ this.initialize();
222
+ const stmt = this.db.prepare('UPDATE long_term_memory SET archived = 1 WHERE id = ?');
223
+ const result = stmt.run(id);
224
+ return result.changes > 0;
225
+ }
226
+ }
@@ -0,0 +1,142 @@
1
+ import { SatiRepository } from './repository.js';
2
+ import { ConfigManager } from '../../../config/manager.js';
3
+ import { ProviderFactory } from '../../providers/factory.js';
4
+ import { SystemMessage, HumanMessage, ToolMessage } from "@langchain/core/messages";
5
+ import { SATI_EVALUATION_PROMPT } from './system-prompts.js';
6
+ import { createHash } from 'crypto';
7
+ import { DisplayManager } from '../../display.js';
8
+ import { SQLiteChatMessageHistory } from '../sqlite.js';
9
+ const display = DisplayManager.getInstance();
10
+ export class SatiService {
11
+ repository;
12
+ static instance;
13
+ constructor() {
14
+ this.repository = SatiRepository.getInstance();
15
+ }
16
+ static getInstance() {
17
+ if (!SatiService.instance) {
18
+ SatiService.instance = new SatiService();
19
+ }
20
+ return SatiService.instance;
21
+ }
22
+ async initialize() {
23
+ this.repository.initialize();
24
+ }
25
+ 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);
32
+ return {
33
+ relevant_memories: memories.map(m => ({
34
+ summary: m.summary,
35
+ category: m.category,
36
+ importance: m.importance
37
+ }))
38
+ };
39
+ }
40
+ async evaluateAndPersist(conversation) {
41
+ try {
42
+ const santiConfig = ConfigManager.getInstance().getSatiConfig();
43
+ if (!santiConfig)
44
+ return;
45
+ // Use the main provider factory to get an agent (Reusing Zion configuration)
46
+ // We pass empty tools as Sati is a pure reasoning agent here
47
+ const agent = await ProviderFactory.create(santiConfig, []);
48
+ // Get existing memories for context (Simulated "Working Memory" or full list if small)
49
+ const allMemories = this.repository.getAllMemories();
50
+ const existingSummaries = allMemories.slice(0, 50).map(m => m.summary);
51
+ // Map conversation to strict types and sanitize
52
+ const recentConversation = conversation.map(c => ({
53
+ role: (c.role === 'human' ? 'user' : c.role),
54
+ content: c.content
55
+ }));
56
+ const inputPayload = {
57
+ recent_conversation: recentConversation,
58
+ existing_memory_summaries: existingSummaries
59
+ };
60
+ const messages = [
61
+ new SystemMessage(SATI_EVALUATION_PROMPT),
62
+ new HumanMessage(JSON.stringify(inputPayload, null, 2))
63
+ ];
64
+ const history = new SQLiteChatMessageHistory({ sessionId: 'sati-evaluation' });
65
+ try {
66
+ const inputMsg = new ToolMessage({
67
+ content: JSON.stringify(inputPayload, null, 2),
68
+ tool_call_id: `sati-input-${Date.now()}`,
69
+ name: 'sati_evaluation_input'
70
+ });
71
+ inputMsg.provider_metadata = {
72
+ provider: santiConfig.provider,
73
+ model: santiConfig.model
74
+ };
75
+ await history.addMessage(inputMsg);
76
+ }
77
+ catch (e) {
78
+ console.warn('[SatiService] Failed to persist input log:', e);
79
+ }
80
+ const response = await agent.invoke({ messages });
81
+ const lastMessage = response.messages[response.messages.length - 1];
82
+ let content = lastMessage.content.toString();
83
+ try {
84
+ const outputToolMsg = new ToolMessage({
85
+ content: content,
86
+ tool_call_id: `sati-output-${Date.now()}`,
87
+ name: 'sati_evaluation_output'
88
+ });
89
+ if (lastMessage.usage_metadata) {
90
+ outputToolMsg.usage_metadata = lastMessage.usage_metadata;
91
+ }
92
+ outputToolMsg.provider_metadata = {
93
+ provider: santiConfig.provider,
94
+ model: santiConfig.model
95
+ };
96
+ await history.addMessage(outputToolMsg);
97
+ }
98
+ catch (e) {
99
+ console.warn('[SatiService] Failed to persist output log:', e);
100
+ }
101
+ // Safe JSON parsing (handle markdown blocks if LLM wraps output)
102
+ content = content.replace(/```json/g, '').replace(/```/g, '').trim();
103
+ let result;
104
+ try {
105
+ result = JSON.parse(content);
106
+ }
107
+ catch (e) {
108
+ console.warn('[SatiService] Failed to parse JSON response:', content);
109
+ return;
110
+ }
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
128
+ }
129
+ else {
130
+ throw saveError;
131
+ }
132
+ }
133
+ }
134
+ }
135
+ catch (error) {
136
+ console.error('[SatiService] Evaluation failed:', error);
137
+ }
138
+ }
139
+ generateHash(content) {
140
+ return createHash('sha256').update(content.trim().toLowerCase()).digest('hex');
141
+ }
142
+ }
@@ -0,0 +1,42 @@
1
+ export const SATI_EVALUATION_PROMPT = `You are **Sati**, an autonomous background memory manager for an AI assistant.
2
+ Your goal is to analyze the conversation interaction and decide if any **Persistent Long-Term Memory** should be stored.
3
+
4
+ ### INPUT DATA
5
+ You will receive:
6
+ 1. A list of recent messages (USER and ASSISTANT).
7
+ 2. A list of ALREADY EXISTING memory summaries (to avoid duplicates).
8
+
9
+ ### MEMORY CATEGORIES
10
+ Classify any new memory into one of these types:
11
+ - **preference**: User preferences (e.g., "I like dark mode", "Use TypeScript").
12
+ - **project**: Details about the user's projects, architecture, or tech stack.
13
+ - **identity**: Facts about the user's identity, role, or background.
14
+ - **constraint**: Hard rules the user wants you to follow (e.g., "Never use single quotes").
15
+ - **context**: General context that is useful for the long term.
16
+ - **personal_data**: Non-sensitive personal info (e.g., birthday, location).
17
+ - **languages**: User's spoken or programming languages.
18
+ - **favorite_things**: Favorites (movies, books, etc.).
19
+ - **relationships**: Mention of colleagues, family, or friends.
20
+ - **pets**: Info about user's pets.
21
+ - **naming**: Naming conventions the user prefers.
22
+ - **professional_profile**: Job title, industry, skills.
23
+
24
+ ### CRITICAL RULES
25
+ 1. **NO SECRETS**: NEVER store API keys, passwords, credit cards, or private tokens. If found, ignore them explicitly.
26
+ 2. **NO DUPLICATES**: If the information is already covered by the \`existing_memory_summaries\`, DO NOT store it again.
27
+ 3. **NO CHIT-CHAT**: Do not store trivial conversation like "Hello", "Thanks", "How are you?".
28
+ 4. **IMPORTANCE**: Assign 'low', 'medium', or 'high' importance. Store only 'medium' or 'high' unless it's a specific user preference (which is always important).
29
+
30
+ ### TOP IMPORTANT GUIDELINES
31
+ 5. **OBEY THE USER**: If the user explicitly states something should be remembered, it must be stored with at least 'medium' importance.
32
+
33
+ ### OUTPUT FORMAT
34
+ You MUST respond with a valid JSON object matching the \`ISatiEvaluationOutput\` interface:
35
+ {
36
+ "should_store": boolean,
37
+ "category": "category_name" | null,
38
+ "importance": "low" | "medium" | "high" | null,
39
+ "summary": "Concise factual statement" | null,
40
+ "reason": "Why you decided to store or not store"
41
+ }
42
+ `;
@@ -0,0 +1 @@
1
+ export {};
@@ -140,7 +140,11 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
140
140
  async getMessages() {
141
141
  try {
142
142
  // Fetch new columns
143
- const stmt = this.db.prepare("SELECT type, content, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model FROM messages WHERE session_id = ? ORDER BY id ASC LIMIT ?");
143
+ const stmt = this.db.prepare(`SELECT type, content, input_tokens, output_tokens, total_tokens, cache_read_tokens, provider, model
144
+ FROM messages
145
+ WHERE session_id = ?
146
+ ORDER BY id DESC
147
+ LIMIT ?`);
144
148
  const rows = stmt.all(this.sessionId, this.limit);
145
149
  return rows.map((row) => {
146
150
  let msg;
@@ -1,5 +1,6 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
+ import yaml from 'js-yaml';
3
4
  import { PATHS } from '../config/paths.js';
4
5
  import { DisplayManager } from './display.js';
5
6
  export async function migrateConfigFile() {
@@ -17,7 +18,6 @@ export async function migrateConfigFile() {
17
18
  catch (err) {
18
19
  display.log(`Failed to migrate config.yaml to zaion.yaml: ${err.message}`, { source: 'Zaion', level: 'warning' });
19
20
  }
20
- return;
21
21
  }
22
22
  if (legacyExists && newExists) {
23
23
  display.log('Both config.yaml and zaion.yaml exist. Using zaion.yaml and leaving config.yaml in place.', {
@@ -25,4 +25,56 @@ export async function migrateConfigFile() {
25
25
  level: 'warning'
26
26
  });
27
27
  }
28
+ // Migrate memory.limit to llm.context_window
29
+ await migrateContextWindow();
30
+ }
31
+ /**
32
+ * Migrates memory.limit to llm.context_window
33
+ * Creates backup before modifying config
34
+ */
35
+ async function migrateContextWindow() {
36
+ const display = DisplayManager.getInstance();
37
+ const configPath = PATHS.config;
38
+ try {
39
+ // Check if config file exists
40
+ if (!await fs.pathExists(configPath)) {
41
+ return; // No config to migrate
42
+ }
43
+ // Read current config
44
+ const configContent = await fs.readFile(configPath, 'utf8');
45
+ const config = yaml.load(configContent);
46
+ // Check if migration is needed
47
+ const hasOldField = config?.memory?.limit !== undefined;
48
+ const hasNewField = config?.llm?.context_window !== undefined;
49
+ // Already migrated or nothing to migrate
50
+ if (!hasOldField || hasNewField) {
51
+ return;
52
+ }
53
+ // Create backup before migration
54
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
55
+ const backupPath = `${configPath}.backup-${timestamp}`;
56
+ await fs.copy(configPath, backupPath);
57
+ display.log(`Created config backup: ${backupPath}`, { source: 'Migration', level: 'info' });
58
+ // Perform migration
59
+ if (!config.llm) {
60
+ config.llm = {};
61
+ }
62
+ config.llm.context_window = config.memory.limit;
63
+ delete config.memory.limit;
64
+ // If memory object is now empty, keep it but with undefined limit for backward compat
65
+ if (Object.keys(config.memory || {}).length === 0) {
66
+ config.memory = {};
67
+ }
68
+ // Write migrated config
69
+ const migratedYaml = yaml.dump(config);
70
+ await fs.writeFile(configPath, migratedYaml, 'utf8');
71
+ display.log('Migrated memory.limit → llm.context_window', { source: 'Migration', level: 'info' });
72
+ }
73
+ catch (error) {
74
+ // Fail open: log error but don't crash
75
+ display.log(`Config migration failed: ${error.message}. System will use defaults.`, {
76
+ source: 'Migration',
77
+ level: 'warning'
78
+ });
79
+ }
28
80
  }
@@ -5,12 +5,14 @@ import { ConfigManager } from "../config/manager.js";
5
5
  import { ProviderError } from "./errors.js";
6
6
  import { DisplayManager } from "./display.js";
7
7
  import { SQLiteChatMessageHistory } from "./memory/sqlite.js";
8
+ import { SatiMemoryMiddleware } from "./memory/sati/index.js";
8
9
  export class Oracle {
9
10
  provider;
10
11
  config;
11
12
  history;
12
13
  display = DisplayManager.getInstance();
13
14
  databasePath;
15
+ satiMiddleware = SatiMemoryMiddleware.getInstance();
14
16
  constructor(config, overrides) {
15
17
  this.config = config || ConfigManager.getInstance().get();
16
18
  this.databasePath = overrides?.databasePath;
@@ -32,10 +34,12 @@ export class Oracle {
32
34
  throw new Error("Provider factory returned undefined");
33
35
  }
34
36
  // Initialize persistent memory with SQLite
37
+ const contextWindow = this.config.llm?.context_window ?? this.config.memory?.limit ?? 100;
38
+ this.display.log(`Using context window: ${contextWindow} messages`, { source: 'Oracle' });
35
39
  this.history = new SQLiteChatMessageHistory({
36
40
  sessionId: "default",
37
41
  databasePath: this.databasePath,
38
- limit: this.config.memory?.limit || 100, // Fallback purely defensive if config type allows optional
42
+ limit: contextWindow,
39
43
  });
40
44
  }
41
45
  catch (err) {
@@ -181,13 +185,29 @@ You do not speculate when verification is possible.
181
185
  You maintain intent until resolution.
182
186
 
183
187
  `);
184
- // Load existing history from database
185
- const previousMessages = await this.history.getMessages();
188
+ // Load existing history from database in reverse order (most recent first)
189
+ let previousMessages = await this.history.getMessages();
190
+ previousMessages = previousMessages.reverse();
191
+ // Sati Middleware: Retrieval
192
+ let memoryMessage = null;
193
+ try {
194
+ memoryMessage = await this.satiMiddleware.beforeAgent(message, previousMessages);
195
+ if (memoryMessage) {
196
+ this.display.log('Sati memory retrieved.', { source: 'Sati' });
197
+ }
198
+ }
199
+ catch (e) {
200
+ // Fail open - do not disrupt main flow
201
+ this.display.log(`Sati memory retrieval failed: ${e.message}`, { source: 'Sati' });
202
+ }
186
203
  const messages = [
187
- systemMessage,
188
- ...previousMessages,
189
- userMessage
204
+ systemMessage
190
205
  ];
206
+ if (memoryMessage) {
207
+ messages.push(memoryMessage);
208
+ }
209
+ messages.push(...previousMessages);
210
+ messages.push(userMessage);
191
211
  const response = await this.provider.invoke({ messages });
192
212
  // Identify new messages generated during the interaction
193
213
  // The `messages` array passed to invoke had length `messages.length`
@@ -195,6 +215,7 @@ You maintain intent until resolution.
195
215
  // New messages start after the inputs.
196
216
  const startNewMessagesIndex = messages.length;
197
217
  const newGeneratedMessages = response.messages.slice(startNewMessagesIndex);
218
+ // console.log('New generated messages', newGeneratedMessages);
198
219
  // Persist User Message first
199
220
  await this.history.addMessage(userMessage);
200
221
  // Persist all new intermediate tool calls and responses
@@ -208,7 +229,11 @@ You maintain intent until resolution.
208
229
  }
209
230
  this.display.log('Response generated.', { source: 'Oracle' });
210
231
  const lastMessage = response.messages[response.messages.length - 1];
211
- return (typeof lastMessage.content === 'string') ? lastMessage.content : JSON.stringify(lastMessage.content);
232
+ const responseContent = (typeof lastMessage.content === 'string') ? lastMessage.content : JSON.stringify(lastMessage.content);
233
+ // Sati Middleware: Evaluation (Fire and forget)
234
+ this.satiMiddleware.afterAgent(responseContent, [...previousMessages, userMessage])
235
+ .catch((e) => this.display.log(`Sati memory evaluation failed: ${e.message}`, { source: 'Sati' }));
236
+ return responseContent;
212
237
  }
213
238
  catch (err) {
214
239
  throw new ProviderError(this.config.llm.provider, err, "Chat request failed");
@@ -0,0 +1 @@
1
+ export {};