morpheus-cli 0.4.15 → 0.5.0

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 (38) hide show
  1. package/README.md +275 -1116
  2. package/dist/channels/telegram.js +206 -74
  3. package/dist/cli/commands/doctor.js +34 -0
  4. package/dist/cli/commands/init.js +128 -0
  5. package/dist/cli/commands/restart.js +17 -0
  6. package/dist/cli/commands/start.js +15 -0
  7. package/dist/config/manager.js +51 -0
  8. package/dist/config/schemas.js +7 -0
  9. package/dist/devkit/tools/network.js +1 -1
  10. package/dist/http/api.js +177 -10
  11. package/dist/runtime/apoc.js +25 -17
  12. package/dist/runtime/memory/sati/repository.js +30 -2
  13. package/dist/runtime/memory/sati/service.js +46 -15
  14. package/dist/runtime/memory/sati/system-prompts.js +71 -29
  15. package/dist/runtime/memory/sqlite.js +24 -0
  16. package/dist/runtime/neo.js +134 -0
  17. package/dist/runtime/oracle.js +244 -205
  18. package/dist/runtime/providers/factory.js +1 -12
  19. package/dist/runtime/tasks/context.js +53 -0
  20. package/dist/runtime/tasks/dispatcher.js +70 -0
  21. package/dist/runtime/tasks/notifier.js +68 -0
  22. package/dist/runtime/tasks/repository.js +370 -0
  23. package/dist/runtime/tasks/types.js +1 -0
  24. package/dist/runtime/tasks/worker.js +96 -0
  25. package/dist/runtime/tools/apoc-tool.js +61 -8
  26. package/dist/runtime/tools/delegation-guard.js +29 -0
  27. package/dist/runtime/tools/index.js +1 -0
  28. package/dist/runtime/tools/neo-tool.js +99 -0
  29. package/dist/runtime/tools/task-query-tool.js +76 -0
  30. package/dist/runtime/webhooks/dispatcher.js +10 -19
  31. package/dist/types/config.js +10 -0
  32. package/dist/ui/assets/index-20lLB1sM.js +112 -0
  33. package/dist/ui/assets/index-BJ56bRfs.css +1 -0
  34. package/dist/ui/index.html +2 -2
  35. package/dist/ui/sw.js +1 -1
  36. package/package.json +1 -1
  37. package/dist/ui/assets/index-LemKVRjC.js +0 -112
  38. package/dist/ui/assets/index-TCQ7VNYO.css +0 -1
@@ -15,6 +15,9 @@ import { ProviderError } from '../../runtime/errors.js';
15
15
  import { HttpServer } from '../../http/server.js';
16
16
  import { getVersion } from '../utils/version.js';
17
17
  import { startSessionEmbeddingScheduler } from '../../runtime/session-embedding-scheduler.js';
18
+ import { TaskWorker } from '../../runtime/tasks/worker.js';
19
+ import { TaskNotifier } from '../../runtime/tasks/notifier.js';
20
+ import { TaskDispatcher } from '../../runtime/tasks/dispatcher.js';
18
21
  export const startCommand = new Command('start')
19
22
  .description('Start the Morpheus agent')
20
23
  .option('--ui', 'Enable web UI', true)
@@ -119,6 +122,9 @@ export const startCommand = new Command('start')
119
122
  }
120
123
  const adapters = [];
121
124
  let httpServer;
125
+ const taskWorker = new TaskWorker();
126
+ const taskNotifier = new TaskNotifier();
127
+ const asyncTasksEnabled = config.runtime?.async_tasks?.enabled !== false;
122
128
  // Initialize Web UI
123
129
  if (options.ui && config.ui.enabled) {
124
130
  try {
@@ -139,6 +145,7 @@ export const startCommand = new Command('start')
139
145
  await telegram.connect(config.channels.telegram.token, config.channels.telegram.allowedUsers || []);
140
146
  // Wire Telegram adapter to webhook dispatcher for proactive notifications
141
147
  WebhookDispatcher.setTelegramAdapter(telegram);
148
+ TaskDispatcher.setTelegramAdapter(telegram);
142
149
  adapters.push(telegram);
143
150
  }
144
151
  catch (e) {
@@ -151,6 +158,10 @@ export const startCommand = new Command('start')
151
158
  }
152
159
  // Start Background Services
153
160
  startSessionEmbeddingScheduler();
161
+ if (asyncTasksEnabled) {
162
+ taskWorker.start();
163
+ taskNotifier.start();
164
+ }
154
165
  // Handle graceful shutdown
155
166
  const shutdown = async (signal) => {
156
167
  display.stopSpinner();
@@ -161,6 +172,10 @@ export const startCommand = new Command('start')
161
172
  for (const adapter of adapters) {
162
173
  await adapter.disconnect();
163
174
  }
175
+ if (asyncTasksEnabled) {
176
+ taskWorker.stop();
177
+ taskNotifier.stop();
178
+ }
164
179
  await clearPid();
165
180
  process.exit(0);
166
181
  };
@@ -85,6 +85,45 @@ export class ConfigManager {
85
85
  timeout_ms: config.apoc.timeout_ms !== undefined ? resolveNumeric('MORPHEUS_APOC_TIMEOUT_MS', config.apoc.timeout_ms, 30_000) : 30_000
86
86
  };
87
87
  }
88
+ // Apply precedence to Neo config
89
+ const neoEnvVars = [
90
+ 'MORPHEUS_NEO_PROVIDER',
91
+ 'MORPHEUS_NEO_MODEL',
92
+ 'MORPHEUS_NEO_TEMPERATURE',
93
+ 'MORPHEUS_NEO_MAX_TOKENS',
94
+ 'MORPHEUS_NEO_API_KEY',
95
+ 'MORPHEUS_NEO_BASE_URL',
96
+ 'MORPHEUS_NEO_CONTEXT_WINDOW',
97
+ ];
98
+ const hasNeoEnvOverrides = neoEnvVars.some((envVar) => process.env[envVar] !== undefined);
99
+ const resolveOptionalNumeric = (envVar, configValue, fallbackValue) => {
100
+ if (fallbackValue !== undefined) {
101
+ return resolveNumeric(envVar, configValue, fallbackValue);
102
+ }
103
+ if (process.env[envVar] !== undefined && process.env[envVar] !== '') {
104
+ const parsed = Number(process.env[envVar]);
105
+ if (!Number.isNaN(parsed)) {
106
+ return parsed;
107
+ }
108
+ }
109
+ return configValue;
110
+ };
111
+ let neoConfig;
112
+ if (config.neo || hasNeoEnvOverrides) {
113
+ const neoProvider = resolveProvider('MORPHEUS_NEO_PROVIDER', config.neo?.provider, llmConfig.provider);
114
+ const neoBaseUrl = resolveString('MORPHEUS_NEO_BASE_URL', config.neo?.base_url, llmConfig.base_url || '');
115
+ const neoMaxTokensFallback = config.neo?.max_tokens ?? llmConfig.max_tokens;
116
+ const neoContextWindowFallback = config.neo?.context_window ?? llmConfig.context_window;
117
+ neoConfig = {
118
+ provider: neoProvider,
119
+ model: resolveModel(neoProvider, 'MORPHEUS_NEO_MODEL', config.neo?.model || llmConfig.model),
120
+ temperature: resolveNumeric('MORPHEUS_NEO_TEMPERATURE', config.neo?.temperature, llmConfig.temperature),
121
+ max_tokens: resolveOptionalNumeric('MORPHEUS_NEO_MAX_TOKENS', config.neo?.max_tokens, neoMaxTokensFallback),
122
+ api_key: resolveApiKey(neoProvider, 'MORPHEUS_NEO_API_KEY', config.neo?.api_key || llmConfig.api_key),
123
+ base_url: neoBaseUrl || undefined,
124
+ context_window: resolveOptionalNumeric('MORPHEUS_NEO_CONTEXT_WINDOW', config.neo?.context_window, neoContextWindowFallback),
125
+ };
126
+ }
88
127
  // Apply precedence to audio config
89
128
  const audioProvider = resolveString('MORPHEUS_AUDIO_PROVIDER', config.audio.provider, DEFAULT_CONFIG.audio.provider);
90
129
  // AudioProvider uses 'google' but resolveApiKey expects LLMProvider which uses 'gemini'
@@ -128,6 +167,7 @@ export class ConfigManager {
128
167
  agent: agentConfig,
129
168
  llm: llmConfig,
130
169
  sati: satiConfig,
170
+ neo: neoConfig,
131
171
  apoc: apocConfig,
132
172
  audio: audioConfig,
133
173
  channels: channelsConfig,
@@ -185,4 +225,15 @@ export class ConfigManager {
185
225
  timeout_ms: 30_000
186
226
  };
187
227
  }
228
+ getNeoConfig() {
229
+ if (this.config.neo) {
230
+ return {
231
+ ...this.config.neo
232
+ };
233
+ }
234
+ // Fallback to main LLM config
235
+ return {
236
+ ...this.config.llm,
237
+ };
238
+ }
188
239
  }
@@ -26,6 +26,7 @@ export const ApocConfigSchema = LLMConfigSchema.extend({
26
26
  working_dir: z.string().optional(),
27
27
  timeout_ms: z.number().int().positive().optional(),
28
28
  });
29
+ export const NeoConfigSchema = LLMConfigSchema;
29
30
  export const WebhookConfigSchema = z.object({
30
31
  telegram_notify_all: z.boolean().optional(),
31
32
  }).optional();
@@ -37,12 +38,18 @@ export const ConfigSchema = z.object({
37
38
  }).default(DEFAULT_CONFIG.agent),
38
39
  llm: LLMConfigSchema.default(DEFAULT_CONFIG.llm),
39
40
  sati: SatiConfigSchema.optional(),
41
+ neo: NeoConfigSchema.optional(),
40
42
  apoc: ApocConfigSchema.optional(),
41
43
  webhooks: WebhookConfigSchema,
42
44
  audio: AudioConfigSchema.default(DEFAULT_CONFIG.audio),
43
45
  memory: z.object({
44
46
  limit: z.number().int().positive().optional(),
45
47
  }).default(DEFAULT_CONFIG.memory),
48
+ runtime: z.object({
49
+ async_tasks: z.object({
50
+ enabled: z.boolean().default(DEFAULT_CONFIG.runtime?.async_tasks.enabled ?? true),
51
+ }).default(DEFAULT_CONFIG.runtime?.async_tasks ?? { enabled: true }),
52
+ }).optional(),
46
53
  channels: z.object({
47
54
  telegram: z.object({
48
55
  enabled: z.boolean().default(false),
@@ -64,7 +64,7 @@ export function createNetworkTools(ctx) {
64
64
  });
65
65
  }, {
66
66
  name: 'ping',
67
- description: 'Check if a host is reachable on a given port (TCP connect check).',
67
+ description: 'Preferred connectivity check tool. Verify if a host is reachable on a given port (TCP connect check). Use this instead of shell ping for routine reachability checks.',
68
68
  schema: z.object({
69
69
  host: z.string().describe('Hostname or IP'),
70
70
  port: z.number().int().optional().describe('Port to check, default 80'),
package/dist/http/api.js CHANGED
@@ -11,6 +11,7 @@ import { z } from 'zod';
11
11
  import { MCPManager } from '../config/mcp-manager.js';
12
12
  import { MCPServerConfigSchema } from '../config/schemas.js';
13
13
  import { Construtor } from '../runtime/tools/factory.js';
14
+ import { TaskRepository } from '../runtime/tasks/repository.js';
14
15
  async function readLastLines(filePath, n) {
15
16
  try {
16
17
  const content = await fs.readFile(filePath, 'utf8');
@@ -25,6 +26,7 @@ export function createApiRouter(oracle) {
25
26
  const router = Router();
26
27
  const configManager = ConfigManager.getInstance();
27
28
  const history = new SQLiteChatMessageHistory({ sessionId: 'api-reader' });
29
+ const taskRepository = TaskRepository.getInstance();
28
30
  // --- Session Management ---
29
31
  router.get('/sessions', async (req, res) => {
30
32
  try {
@@ -83,18 +85,76 @@ export function createApiRouter(oracle) {
83
85
  const { id } = req.params;
84
86
  const sessionHistory = new SQLiteChatMessageHistory({ sessionId: id, limit: 100 });
85
87
  try {
86
- const messages = await sessionHistory.getMessages();
87
- // Normalize messages for UI
88
- const normalizedMessages = messages.map((msg) => {
89
- const type = msg._getType ? msg._getType() : 'unknown';
88
+ const relatedSessionIds = id.startsWith('sati-evaluation-')
89
+ ? [id]
90
+ : [id, `sati-evaluation-${id}`];
91
+ const rows = await sessionHistory.getRawMessagesBySessionIds(relatedSessionIds, 200);
92
+ const normalizedMessages = rows.map((row) => {
93
+ let content = row.content;
94
+ let tool_calls;
95
+ let tool_name;
96
+ let tool_call_id;
97
+ if (row.type === 'ai') {
98
+ try {
99
+ const parsed = JSON.parse(row.content);
100
+ if (parsed && typeof parsed === 'object') {
101
+ if (Array.isArray(parsed.tool_calls)) {
102
+ tool_calls = parsed.tool_calls;
103
+ }
104
+ if (typeof parsed.text === 'string') {
105
+ content = parsed.text;
106
+ }
107
+ }
108
+ }
109
+ catch {
110
+ // Keep raw content for legacy/plain-text messages.
111
+ }
112
+ }
113
+ if (row.type === 'tool') {
114
+ try {
115
+ const parsed = JSON.parse(row.content);
116
+ if (parsed && typeof parsed === 'object') {
117
+ if (parsed.content !== undefined) {
118
+ const parsedContent = parsed.content;
119
+ content =
120
+ typeof parsedContent === 'string'
121
+ ? parsedContent
122
+ : JSON.stringify(parsedContent, null, 2);
123
+ }
124
+ if (typeof parsed.name === 'string') {
125
+ tool_name = parsed.name;
126
+ }
127
+ if (typeof parsed.tool_call_id === 'string') {
128
+ tool_call_id = parsed.tool_call_id;
129
+ }
130
+ }
131
+ }
132
+ catch {
133
+ // Keep raw content for legacy/plain-text tool messages.
134
+ }
135
+ }
136
+ const usage_metadata = row.total_tokens != null
137
+ ? {
138
+ input_tokens: row.input_tokens || 0,
139
+ output_tokens: row.output_tokens || 0,
140
+ total_tokens: row.total_tokens || 0,
141
+ input_token_details: row.cache_read_tokens
142
+ ? { cache_read: row.cache_read_tokens }
143
+ : undefined,
144
+ }
145
+ : undefined;
90
146
  return {
91
- type,
92
- content: msg.content,
93
- tool_calls: msg.tool_calls,
94
- usage_metadata: msg.usage_metadata
147
+ session_id: row.session_id,
148
+ created_at: row.created_at,
149
+ type: row.type,
150
+ content,
151
+ tool_calls,
152
+ tool_name,
153
+ tool_call_id,
154
+ usage_metadata,
95
155
  };
96
156
  });
97
- // Reverse to chronological order for UI
157
+ // Convert DESC to ASC for UI rendering
98
158
  res.json(normalizedMessages.reverse());
99
159
  }
100
160
  catch (err) {
@@ -117,13 +177,74 @@ export function createApiRouter(oracle) {
117
177
  try {
118
178
  const { message, sessionId } = parsed.data;
119
179
  await oracle.setSessionId(sessionId);
120
- const response = await oracle.chat(message);
180
+ const response = await oracle.chat(message, undefined, false, {
181
+ origin_channel: 'ui',
182
+ session_id: sessionId,
183
+ });
121
184
  res.json({ response });
122
185
  }
123
186
  catch (err) {
124
187
  res.status(500).json({ error: err.message });
125
188
  }
126
189
  });
190
+ const TaskStatusSchema = z.enum(['pending', 'running', 'completed', 'failed', 'cancelled']);
191
+ const TaskAgentSchema = z.enum(['apoc', 'neo', 'trinit']);
192
+ const OriginChannelSchema = z.enum(['telegram', 'discord', 'ui', 'api', 'webhook', 'cli']);
193
+ router.get('/tasks', (req, res) => {
194
+ try {
195
+ const status = req.query.status;
196
+ const agent = req.query.agent;
197
+ const originChannel = req.query.origin_channel;
198
+ const sessionId = req.query.session_id;
199
+ const limit = req.query.limit;
200
+ const parsedStatus = typeof status === 'string' ? TaskStatusSchema.safeParse(status) : null;
201
+ const parsedAgent = typeof agent === 'string' ? TaskAgentSchema.safeParse(agent) : null;
202
+ const parsedOrigin = typeof originChannel === 'string' ? OriginChannelSchema.safeParse(originChannel) : null;
203
+ const tasks = taskRepository.listTasks({
204
+ status: parsedStatus?.success ? parsedStatus.data : undefined,
205
+ agent: parsedAgent?.success ? parsedAgent.data : undefined,
206
+ origin_channel: parsedOrigin?.success ? parsedOrigin.data : undefined,
207
+ session_id: typeof sessionId === 'string' ? sessionId : undefined,
208
+ limit: typeof limit === 'string' ? Math.max(1, Math.min(500, Number(limit) || 200)) : 200,
209
+ });
210
+ res.json(tasks);
211
+ }
212
+ catch (err) {
213
+ res.status(500).json({ error: err.message });
214
+ }
215
+ });
216
+ router.get('/tasks/stats', (req, res) => {
217
+ try {
218
+ res.json(taskRepository.getStats());
219
+ }
220
+ catch (err) {
221
+ res.status(500).json({ error: err.message });
222
+ }
223
+ });
224
+ router.get('/tasks/:id', (req, res) => {
225
+ try {
226
+ const task = taskRepository.getTaskById(req.params.id);
227
+ if (!task) {
228
+ return res.status(404).json({ error: 'Task not found' });
229
+ }
230
+ res.json(task);
231
+ }
232
+ catch (err) {
233
+ res.status(500).json({ error: err.message });
234
+ }
235
+ });
236
+ router.post('/tasks/:id/retry', (req, res) => {
237
+ try {
238
+ const ok = taskRepository.retryTask(req.params.id);
239
+ if (!ok) {
240
+ return res.status(404).json({ error: 'Failed task not found for retry' });
241
+ }
242
+ res.json({ success: true });
243
+ }
244
+ catch (err) {
245
+ res.status(500).json({ error: err.message });
246
+ }
247
+ });
127
248
  // Legacy /session/reset (keep for backward compat or redirect to POST /sessions)
128
249
  router.post('/session/reset', async (req, res) => {
129
250
  try {
@@ -396,6 +517,52 @@ export function createApiRouter(oracle) {
396
517
  res.status(500).json({ error: error.message });
397
518
  }
398
519
  });
520
+ // Neo config endpoints
521
+ router.get('/config/neo', (req, res) => {
522
+ try {
523
+ const neoConfig = configManager.getNeoConfig();
524
+ res.json(neoConfig);
525
+ }
526
+ catch (error) {
527
+ res.status(500).json({ error: error.message });
528
+ }
529
+ });
530
+ router.post('/config/neo', async (req, res) => {
531
+ try {
532
+ const config = configManager.get();
533
+ await configManager.save({ ...config, neo: req.body });
534
+ const display = DisplayManager.getInstance();
535
+ display.log('Neo configuration updated via UI', {
536
+ source: 'Zaion',
537
+ level: 'info'
538
+ });
539
+ res.json({ success: true });
540
+ }
541
+ catch (error) {
542
+ if (error.name === 'ZodError') {
543
+ res.status(400).json({ error: 'Validation failed', details: error.errors });
544
+ }
545
+ else {
546
+ res.status(500).json({ error: error.message });
547
+ }
548
+ }
549
+ });
550
+ router.delete('/config/neo', async (req, res) => {
551
+ try {
552
+ const config = configManager.get();
553
+ const { neo: _neo, ...restConfig } = config;
554
+ await configManager.save(restConfig);
555
+ const display = DisplayManager.getInstance();
556
+ display.log('Neo configuration removed via UI (falling back to Oracle config)', {
557
+ source: 'Zaion',
558
+ level: 'info'
559
+ });
560
+ res.json({ success: true });
561
+ }
562
+ catch (error) {
563
+ res.status(500).json({ error: error.message });
564
+ }
565
+ });
399
566
  router.post('/config/apoc', async (req, res) => {
400
567
  try {
401
568
  const config = configManager.get();
@@ -1,4 +1,4 @@
1
- import { HumanMessage, SystemMessage } from "@langchain/core/messages";
1
+ import { HumanMessage, SystemMessage, AIMessage } from "@langchain/core/messages";
2
2
  import { ConfigManager } from "../config/manager.js";
3
3
  import { ProviderFactory } from "./providers/factory.js";
4
4
  import { ProviderError } from "./errors.js";
@@ -95,6 +95,11 @@ OPERATING RULES:
95
95
  3. Report clearly what was done and what the result was.
96
96
  4. If something fails, report the error and what you tried.
97
97
  5. Stay focused on the delegated task only.
98
+ 6. Respond in the language requested by the user. If not explicit, use the dominant language of the task/context.
99
+ 7. For connectivity checks, prefer the dedicated network tool "ping" (TCP reachability) instead of shell "ping".
100
+ 8. Only use shell ping when explicitly required by the user. If shell ping is needed, detect OS first:
101
+ - Windows: use "-n" (never use "-c")
102
+ - Linux/macOS: use "-c"
98
103
 
99
104
 
100
105
  ────────────────────────────────────────
@@ -216,27 +221,30 @@ ${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
216
221
  const messages = [systemMessage, userMessage];
217
222
  try {
218
223
  const response = await this.agent.invoke({ messages });
219
- // Persist Apoc-generated messages so token usage is tracked in short-memory.db.
220
- // Use the caller's session when provided, then the static session set by Oracle,
221
- // otherwise fall back to 'apoc'.
224
+ // Persist one AI message per delegated task so usage can be parameterized later.
225
+ // Use task session id when provided.
222
226
  const apocConfig = this.config.apoc || this.config.llm;
223
- const newMessages = response.messages.slice(messages.length);
224
- if (newMessages.length > 0) {
225
- const targetSession = sessionId ?? Apoc.currentSessionId ?? 'apoc';
226
- const history = new SQLiteChatMessageHistory({ sessionId: targetSession });
227
- for (const msg of newMessages) {
228
- msg.provider_metadata = {
229
- provider: apocConfig.provider,
230
- model: apocConfig.model,
231
- };
232
- }
233
- await history.addMessages(newMessages);
234
- history.close();
235
- }
236
227
  const lastMessage = response.messages[response.messages.length - 1];
237
228
  const content = typeof lastMessage.content === "string"
238
229
  ? lastMessage.content
239
230
  : JSON.stringify(lastMessage.content);
231
+ const targetSession = sessionId ?? Apoc.currentSessionId ?? "apoc";
232
+ const history = new SQLiteChatMessageHistory({ sessionId: targetSession });
233
+ try {
234
+ const persisted = new AIMessage(content);
235
+ persisted.usage_metadata = lastMessage.usage_metadata
236
+ ?? lastMessage.response_metadata?.usage
237
+ ?? lastMessage.response_metadata?.tokenUsage
238
+ ?? lastMessage.usage;
239
+ persisted.provider_metadata = {
240
+ provider: apocConfig.provider,
241
+ model: apocConfig.model,
242
+ };
243
+ await history.addMessage(persisted);
244
+ }
245
+ finally {
246
+ history.close();
247
+ }
240
248
  this.display.log("Apoc task completed.", { source: "Apoc" });
241
249
  return content;
242
250
  }
@@ -220,7 +220,7 @@ export class SatiRepository {
220
220
  m.category as category,
221
221
  m.importance as importance,
222
222
  'long_term' as source_type,
223
- (1 - vec_distance_cosine(v.embedding, ?)) * 1.7 as distance
223
+ (1 - vec_distance_cosine(v.embedding, ?)) * 0.8 as distance
224
224
  FROM memory_vec v
225
225
  JOIN memory_embedding_map map ON map.vec_rowid = v.rowid
226
226
  JOIN long_term_memory m ON m.id = map.memory_id
@@ -236,7 +236,7 @@ export class SatiRepository {
236
236
  'session' as category,
237
237
  'medium' as importance,
238
238
  'session_chunk' as source_type,
239
- (1 - vec_distance_cosine(v.embedding, ?)) * 0.5 as distance
239
+ (1 - vec_distance_cosine(v.embedding, ?)) * 0.2 as distance
240
240
  FROM session_vec v
241
241
  JOIN session_embedding_map map ON map.vec_rowid = v.rowid
242
242
  JOIN session_chunks sc ON sc.id = map.session_chunk_id
@@ -431,6 +431,34 @@ export class SatiRepository {
431
431
  archived: Boolean(row.archived)
432
432
  };
433
433
  }
434
+ update(id, data) {
435
+ if (!this.db)
436
+ this.initialize();
437
+ const existing = this.db.prepare('SELECT * FROM long_term_memory WHERE id = ?').get(id);
438
+ if (!existing)
439
+ return null;
440
+ const setClauses = ['updated_at = @updated_at', 'version = version + 1'];
441
+ const params = { id, updated_at: new Date().toISOString() };
442
+ if (data.importance !== undefined) {
443
+ setClauses.push('importance = @importance');
444
+ params.importance = data.importance;
445
+ }
446
+ if (data.category !== undefined) {
447
+ setClauses.push('category = @category');
448
+ params.category = data.category;
449
+ }
450
+ if (data.summary !== undefined) {
451
+ setClauses.push('summary = @summary');
452
+ params.summary = data.summary;
453
+ }
454
+ if (data.details !== undefined) {
455
+ setClauses.push('details = @details');
456
+ params.details = data.details;
457
+ }
458
+ this.db.prepare(`UPDATE long_term_memory SET ${setClauses.join(', ')} WHERE id = @id`).run(params);
459
+ const updated = this.db.prepare('SELECT * FROM long_term_memory WHERE id = ?').get(id);
460
+ return updated ? this.mapRowToRecord(updated) : null;
461
+ }
434
462
  archiveMemory(id) {
435
463
  if (!this.db)
436
464
  this.initialize();
@@ -60,7 +60,6 @@ export class SatiService {
60
60
  const agent = await ProviderFactory.create(satiConfig, []);
61
61
  // Get existing memories for context (Simulated "Working Memory" or full list if small)
62
62
  const allMemories = this.repository.getAllMemories();
63
- const existingSummaries = allMemories.slice(0, 50).map(m => m.summary);
64
63
  // Map conversation to strict types and sanitize
65
64
  const recentConversation = conversation.map(c => ({
66
65
  role: (c.role === 'human' ? 'user' : c.role),
@@ -68,7 +67,12 @@ export class SatiService {
68
67
  }));
69
68
  const inputPayload = {
70
69
  recent_conversation: recentConversation,
71
- existing_memory_summaries: existingSummaries
70
+ existing_memories: allMemories.map(m => ({
71
+ id: m.id,
72
+ category: m.category,
73
+ importance: m.importance,
74
+ summary: m.summary
75
+ }))
72
76
  };
73
77
  const messages = [
74
78
  new SystemMessage(SATI_EVALUATION_PROMPT),
@@ -114,7 +118,7 @@ export class SatiService {
114
118
  }
115
119
  // Safe JSON parsing (handle markdown blocks if LLM wraps output)
116
120
  content = content.replace(/```json/g, '').replace(/```/g, '').trim();
117
- let result = [];
121
+ let result = { inclusions: [], edits: [], deletions: [] };
118
122
  try {
119
123
  result = JSON.parse(content);
120
124
  }
@@ -122,8 +126,10 @@ export class SatiService {
122
126
  console.warn('[SatiService] Failed to parse JSON response:', content);
123
127
  return;
124
128
  }
125
- for (const item of result) {
126
- if (item.should_store && item.summary && item.category && item.importance) {
129
+ const embeddingService = await EmbeddingService.getInstance();
130
+ // Process inclusions (new memories)
131
+ for (const item of (result.inclusions ?? [])) {
132
+ if (item.summary && item.category && item.importance) {
127
133
  display.log(`Persisting new memory: [${item.category.toUpperCase()}] ${item.summary}`, { source: 'Sati' });
128
134
  try {
129
135
  const savedMemory = await this.repository.save({
@@ -134,22 +140,14 @@ export class SatiService {
134
140
  hash: this.generateHash(item.summary),
135
141
  source: 'conversation'
136
142
  });
137
- // 🔥 GERAR EMBEDDING
138
- const embeddingService = await EmbeddingService.getInstance();
139
- const textForEmbedding = [
140
- savedMemory.summary,
141
- savedMemory.details ?? ''
142
- ].join(' ');
143
+ const textForEmbedding = [savedMemory.summary, savedMemory.details ?? ''].join(' ');
143
144
  const embedding = await embeddingService.generate(textForEmbedding);
144
145
  display.log(`Generated embedding for memory ID ${savedMemory.id}`, { source: 'Sati', level: 'debug' });
145
- // 🔥 SALVAR EMBEDDING NO SQLITE_VEC
146
146
  this.repository.upsertEmbedding(savedMemory.id, embedding);
147
- // Quiet success - logging handled by repository/middleware if needed, or verbose debug
148
147
  }
149
148
  catch (saveError) {
150
149
  if (saveError.message && saveError.message.includes('UNIQUE constraint failed')) {
151
- // Duplicate detected by DB (Hash collision)
152
- // This is expected given T012 logic
150
+ // Duplicate detected by DB (hash collision) — expected
153
151
  }
154
152
  else {
155
153
  throw saveError;
@@ -157,6 +155,39 @@ export class SatiService {
157
155
  }
158
156
  }
159
157
  }
158
+ // Process edits (update existing memories)
159
+ for (const edit of (result.edits ?? [])) {
160
+ if (!edit.id)
161
+ continue;
162
+ const updated = this.repository.update(edit.id, {
163
+ importance: edit.importance,
164
+ summary: edit.summary,
165
+ details: edit.details,
166
+ });
167
+ if (updated) {
168
+ display.log(`Updated memory ${edit.id}: ${edit.reason ?? ''}`, { source: 'Sati' });
169
+ if (edit.summary || edit.details) {
170
+ const text = [updated.summary, updated.details ?? ''].join(' ');
171
+ const embedding = await embeddingService.generate(text);
172
+ this.repository.upsertEmbedding(updated.id, embedding);
173
+ }
174
+ }
175
+ else {
176
+ display.log(`Edit skipped — memory not found: ${edit.id}`, { source: 'Sati', level: 'warning' });
177
+ }
178
+ }
179
+ // Process deletions (archive memories)
180
+ for (const deletion of (result.deletions ?? [])) {
181
+ if (!deletion.id)
182
+ continue;
183
+ const archived = this.repository.archiveMemory(deletion.id);
184
+ if (archived) {
185
+ display.log(`Archived memory ${deletion.id}: ${deletion.reason ?? ''}`, { source: 'Sati' });
186
+ }
187
+ else {
188
+ display.log(`Deletion skipped — memory not found: ${deletion.id}`, { source: 'Sati', level: 'warning' });
189
+ }
190
+ }
160
191
  }
161
192
  catch (error) {
162
193
  console.error('[SatiService] Evaluation failed:', error);