morpheus-cli 0.9.0 → 0.9.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 (63) hide show
  1. package/README.md +18 -4
  2. package/dist/channels/discord.js +133 -6
  3. package/dist/channels/telegram.js +23 -17
  4. package/dist/config/manager.js +11 -0
  5. package/dist/config/schemas.js +5 -0
  6. package/dist/http/api.js +5 -3
  7. package/dist/http/routers/danger.js +137 -0
  8. package/dist/runtime/apoc.js +1 -1
  9. package/dist/runtime/audit/repository.js +2 -0
  10. package/dist/runtime/keymaker.js +1 -1
  11. package/dist/runtime/memory/sati/index.js +1 -1
  12. package/dist/runtime/memory/sati/service.js +28 -1
  13. package/dist/runtime/memory/session-embedding-worker.js +43 -36
  14. package/dist/runtime/memory/sqlite.js +31 -124
  15. package/dist/runtime/neo.js +1 -1
  16. package/dist/runtime/oracle.js +55 -54
  17. package/dist/runtime/setup/__tests__/repository.test.js +115 -0
  18. package/dist/runtime/setup/repository.js +87 -0
  19. package/dist/runtime/smiths/delegator.js +1 -1
  20. package/dist/runtime/tools/setup-tool.js +57 -0
  21. package/dist/runtime/trinity.js +1 -1
  22. package/dist/ui/assets/AuditDashboard-C1f6Hbdw.js +1 -0
  23. package/dist/ui/assets/Chat-5AeRYuRj.js +41 -0
  24. package/dist/ui/assets/{Chronos-BAjeLobF.js → Chronos-BrKldYVw.js} +1 -1
  25. package/dist/ui/assets/{ConfirmationModal-fvgnOWTY.js → ConfirmationModal-DsbS3XkJ.js} +1 -1
  26. package/dist/ui/assets/{Dashboard-Ca5mSefz.js → Dashboard-DvrTXLdo.js} +1 -1
  27. package/dist/ui/assets/{DeleteConfirmationModal-A8EmnHoa.js → DeleteConfirmationModal-BfSjv04R.js} +1 -1
  28. package/dist/ui/assets/{Logs-CYu7se7R.js → Logs-B0ZYWs5x.js} +1 -1
  29. package/dist/ui/assets/MCPManager-BwHGTeNs.js +1 -0
  30. package/dist/ui/assets/{ModelPricing-DnSm_Nh-.js → ModelPricing-CYhGRQr8.js} +1 -1
  31. package/dist/ui/assets/{Notifications-CiljQzvM.js → Notifications-BYMAtVMq.js} +1 -1
  32. package/dist/ui/assets/{Pagination-JsiwxVNQ.js → Pagination-oTGieBLM.js} +1 -1
  33. package/dist/ui/assets/SatiMemories-I1vsYtP2.js +1 -0
  34. package/dist/ui/assets/SessionAudit-BCecQWde.js +9 -0
  35. package/dist/ui/assets/Settings-Cu4D-7tb.js +47 -0
  36. package/dist/ui/assets/Skills-lGU3I5DO.js +7 -0
  37. package/dist/ui/assets/Smiths-DnEH3nID.js +1 -0
  38. package/dist/ui/assets/Tasks-Bz92GPWK.js +1 -0
  39. package/dist/ui/assets/{TrinityDatabases-BzYfecKI.js → TrinityDatabases-BUY-3j7Q.js} +1 -1
  40. package/dist/ui/assets/{UsageStats-CBo2vW2n.js → UsageStats-Dr5eSgJc.js} +1 -1
  41. package/dist/ui/assets/{WebhookManager-0tDFkfHd.js → WebhookManager-DIASAC-1.js} +1 -1
  42. package/dist/ui/assets/{audit-B-F8XPLi.js → audit-CcAEDbZh.js} +1 -1
  43. package/dist/ui/assets/{chronos-BvMxfBQH.js → chronos-2Z9E96_1.js} +1 -1
  44. package/dist/ui/assets/{config-DteVgNGR.js → config-DdfK4DX6.js} +1 -1
  45. package/dist/ui/assets/index-D4fzIKy1.css +1 -0
  46. package/dist/ui/assets/{index-Cwqr-n0Y.js → index-Dpd1Mkgp.js} +5 -5
  47. package/dist/ui/assets/{mcp-DxzodOdH.js → mcp-BWMt8aY7.js} +1 -1
  48. package/dist/ui/assets/{skills--hAyQnmG.js → skills-D7JjK7JH.js} +1 -1
  49. package/dist/ui/assets/{stats-Cibaisqd.js → stats-DoIhtLot.js} +1 -1
  50. package/dist/ui/assets/{vendor-icons-BVuQI-6R.js → vendor-icons-DMd9RGvJ.js} +1 -1
  51. package/dist/ui/index.html +3 -3
  52. package/dist/ui/sw.js +1 -1
  53. package/package.json +5 -4
  54. package/dist/ui/assets/AuditDashboard-5sA8Sd8S.js +0 -1
  55. package/dist/ui/assets/Chat-CjxeAQmd.js +0 -41
  56. package/dist/ui/assets/MCPManager-DsDA_ZVT.js +0 -1
  57. package/dist/ui/assets/SatiMemories-rnO2b0LG.js +0 -1
  58. package/dist/ui/assets/SessionAudit-Dfvhge3Z.js +0 -9
  59. package/dist/ui/assets/Settings-OQlHAJoy.js +0 -41
  60. package/dist/ui/assets/Skills-Crsybug0.js +0 -7
  61. package/dist/ui/assets/Smiths-wm90jRDT.js +0 -1
  62. package/dist/ui/assets/Tasks-C5FMu_Yu.js +0 -1
  63. package/dist/ui/assets/index-DcfyUdLI.css +0 -1
package/README.md CHANGED
@@ -266,6 +266,13 @@ Telegram responses use rich HTML formatting conversion with:
266
266
 
267
267
  Task results are delivered proactively with metadata (task id, agent, status) and output/error body.
268
268
 
269
+ **Session commands:**
270
+ - `/session` — Show current session info
271
+ - `/session list` — List recent sessions
272
+ - `/session new` — Create new session
273
+ - `/session switch <id>` — Switch to existing session
274
+ - `/session rename <name>` — Rename current session
275
+
269
276
  **Voice messages:** Telegram voice messages are automatically transcribed (Gemini / Whisper / OpenRouter) and processed as text through the Oracle.
270
277
 
271
278
  ## Discord Experience
@@ -279,7 +286,11 @@ Discord bot responds to **DMs only** from authorized user IDs (`allowedUsers`).
279
286
  | `/help` | Show available commands |
280
287
  | `/status` | Check Morpheus status |
281
288
  | `/stats` | Token usage statistics |
282
- | `/newsession` | Start a new session |
289
+ | `/session` | Show current session info |
290
+ | `/session_list` | List recent sessions |
291
+ | `/session_new` | Start a new session |
292
+ | `/session_switch id:` | Switch to existing session |
293
+ | `/session_rename name:` | Rename current session |
283
294
  | `/mcps` | List MCP servers with tool counts |
284
295
  | `/mcpreload` | Reload MCP connections and tools |
285
296
  | `/mcp_enable name:` | Enable an MCP server |
@@ -314,16 +325,19 @@ Adding a new channel requires only implementing `IChannelAdapter` (`channel`, `s
314
325
  ## Web UI
315
326
 
316
327
  The dashboard includes:
317
- - Chat with session management
318
- - Tasks page (stats, filters, details, retry)
328
+ - Chat with session management and browser notifications
329
+ - Tasks page (stats, filters, details, retry, pagination)
319
330
  - Agent settings (Oracle/Sati/Neo/Apoc/Trinity/Smiths)
320
331
  - MCP manager (add/edit/delete/toggle/reload)
321
- - Sati memories (search, bulk delete)
332
+ - Sati memories (search, bulk delete, pagination)
322
333
  - Usage stats and model pricing
323
334
  - Trinity databases (register/test/refresh schema)
324
335
  - Chronos scheduler (create/edit/delete jobs, execution history)
336
+ - Smiths management (add/edit/delete, real-time status, ping)
337
+ - Audit dashboard (session audit, tool call tracking, cost breakdowns)
325
338
  - Webhooks and notification inbox
326
339
  - Logs viewer
340
+ - Danger Zone (Settings → reset sessions, tasks, jobs, audit, or factory reset)
327
341
 
328
342
  Chat-specific rendering:
329
343
  - AI messages rendered as markdown
@@ -1,4 +1,4 @@
1
- import { Client, GatewayIntentBits, Partials, Events, ChannelType, REST, Routes, SlashCommandBuilder, } from 'discord.js';
1
+ import { Client, GatewayIntentBits, Partials, Events, ChannelType, REST, Routes, SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, ComponentType, } from 'discord.js';
2
2
  import chalk from 'chalk';
3
3
  import fs from 'fs-extra';
4
4
  import path from 'path';
@@ -26,6 +26,15 @@ const SLASH_COMMANDS = [
26
26
  .setName('newsession')
27
27
  .setDescription('Archive current session and start a new one')
28
28
  .setDMPermission(true),
29
+ new SlashCommandBuilder()
30
+ .setName('sessions')
31
+ .setDescription('List all sessions and switch between them')
32
+ .setDMPermission(true),
33
+ new SlashCommandBuilder()
34
+ .setName('session_switch')
35
+ .setDescription('Switch to a specific session')
36
+ .addStringOption(opt => opt.setName('id').setDescription('Session ID to switch to').setRequired(true))
37
+ .setDMPermission(true),
29
38
  new SlashCommandBuilder()
30
39
  .setName('chronos')
31
40
  .setDescription('Schedule a prompt for the Oracle')
@@ -103,6 +112,8 @@ export class DiscordAdapter {
103
112
  display = DisplayManager.getInstance();
104
113
  config = ConfigManager.getInstance();
105
114
  history = new SQLiteChatMessageHistory({ sessionId: '' });
115
+ /** Per-channel session tracking — which session this Discord adapter is currently using */
116
+ currentSessionId = null;
106
117
  telephonist = null;
107
118
  telephonistProvider = null;
108
119
  telephonistModel = null;
@@ -178,8 +189,8 @@ export class DiscordAdapter {
178
189
  return;
179
190
  this.display.log(`${message.author.tag}: ${text}`, { source: 'Discord' });
180
191
  try {
181
- const sessionId = await this.history.getCurrentSessionOrCreate();
182
- await this.oracle.setSessionId(sessionId);
192
+ const sessionId = this.currentSessionId ?? await this.history.getCurrentSessionOrCreate();
193
+ this.currentSessionId = sessionId;
183
194
  const response = await this.oracle.chat(text, undefined, false, {
184
195
  origin_channel: 'discord',
185
196
  session_id: sessionId,
@@ -292,8 +303,8 @@ export class DiscordAdapter {
292
303
  // Show transcription
293
304
  await channel.send(`🎤 "${text}"`);
294
305
  // Process with Oracle
295
- const sessionId = await this.history.getCurrentSessionOrCreate();
296
- await this.oracle.setSessionId(sessionId);
306
+ const sessionId = this.currentSessionId ?? await this.history.getCurrentSessionOrCreate();
307
+ this.currentSessionId = sessionId;
297
308
  const response = await this.oracle.chat(text, usage, true, {
298
309
  origin_channel: 'discord',
299
310
  session_id: sessionId,
@@ -355,6 +366,12 @@ export class DiscordAdapter {
355
366
  case 'newsession':
356
367
  await this.cmdNewSession(interaction);
357
368
  break;
369
+ case 'sessions':
370
+ await this.cmdSessions(interaction);
371
+ break;
372
+ case 'session_switch':
373
+ await this.cmdSessionSwitch(interaction);
374
+ break;
358
375
  case 'chronos':
359
376
  await this.cmdChronos(interaction);
360
377
  break;
@@ -407,6 +424,8 @@ export class DiscordAdapter {
407
424
  '`/status` — Check Morpheus status',
408
425
  '`/stats` — Token usage statistics',
409
426
  '`/newsession` — Start a new session',
427
+ '`/sessions` — List all sessions (switch, archive, delete)',
428
+ '`/session_switch id:` — Switch to a specific session',
410
429
  '',
411
430
  '**Chronos (Scheduler)**',
412
431
  '`/chronos prompt: time:` — Schedule a job for the Oracle',
@@ -474,14 +493,122 @@ export class DiscordAdapter {
474
493
  async cmdNewSession(interaction) {
475
494
  try {
476
495
  const history = new SQLiteChatMessageHistory({ sessionId: '' });
477
- await history.createNewSession();
496
+ const newSessionId = await history.createNewSession();
478
497
  history.close();
498
+ // Track the new session as the current one for this Discord channel
499
+ this.currentSessionId = newSessionId;
479
500
  await interaction.reply({ content: '✅ New session started.' });
480
501
  }
481
502
  catch (err) {
482
503
  await interaction.reply({ content: `Error: ${err.message}` });
483
504
  }
484
505
  }
506
+ async cmdSessions(interaction) {
507
+ try {
508
+ const history = new SQLiteChatMessageHistory({ sessionId: '' });
509
+ const sessions = (await history.listSessions()).filter((s) => !s.id.startsWith('chronos-job-') && !s.id.startsWith('sati-evaluation'));
510
+ history.close();
511
+ if (sessions.length === 0) {
512
+ await interaction.reply({ content: 'No sessions found.' });
513
+ return;
514
+ }
515
+ const lines = ['**Sessions:**\n'];
516
+ const rows = [];
517
+ for (const session of sessions) {
518
+ const title = session.title || 'Untitled Session';
519
+ const isCurrent = session.id === this.currentSessionId;
520
+ const icon = isCurrent ? '🟢' : '⚪';
521
+ const started = new Date(session.started_at).toLocaleString();
522
+ lines.push(`${icon} **${title}**`);
523
+ lines.push(` ID: \`${session.id}\``);
524
+ lines.push(` Started: ${started}\n`);
525
+ // Discord allows max 5 buttons per row, max 5 rows per message
526
+ if (rows.length < 5) {
527
+ const btns = [];
528
+ if (!isCurrent) {
529
+ btns.push(new ButtonBuilder()
530
+ .setCustomId(`session_switch_${session.id}`)
531
+ .setLabel('Switch')
532
+ .setStyle(ButtonStyle.Primary)
533
+ .setEmoji('➡️'));
534
+ }
535
+ btns.push(new ButtonBuilder()
536
+ .setCustomId(`session_archive_${session.id}`)
537
+ .setLabel('Archive')
538
+ .setStyle(ButtonStyle.Secondary)
539
+ .setEmoji('📂'));
540
+ btns.push(new ButtonBuilder()
541
+ .setCustomId(`session_delete_${session.id}`)
542
+ .setLabel('Delete')
543
+ .setStyle(ButtonStyle.Danger)
544
+ .setEmoji('🗑️'));
545
+ rows.push(new ActionRowBuilder().addComponents(...btns));
546
+ }
547
+ }
548
+ const content = lines.join('\n').slice(0, 2000);
549
+ const reply = await interaction.reply({ content, components: rows, fetchReply: true });
550
+ // Collect button interactions for 60 seconds
551
+ const collector = reply.createMessageComponentCollector({
552
+ componentType: ComponentType.Button,
553
+ time: 60_000,
554
+ });
555
+ collector.on('collect', async (btn) => {
556
+ try {
557
+ if (btn.customId.startsWith('session_switch_')) {
558
+ const sid = btn.customId.replace('session_switch_', '');
559
+ const h = new SQLiteChatMessageHistory({ sessionId: '' });
560
+ await h.switchSession(sid);
561
+ h.close();
562
+ this.currentSessionId = sid;
563
+ await btn.reply({ content: `✅ Switched to session \`${sid}\`.`, ephemeral: true });
564
+ }
565
+ else if (btn.customId.startsWith('session_archive_')) {
566
+ const sid = btn.customId.replace('session_archive_', '');
567
+ const h = new SQLiteChatMessageHistory({ sessionId: '' });
568
+ await h.archiveSession(sid);
569
+ h.close();
570
+ if (this.currentSessionId === sid)
571
+ this.currentSessionId = null;
572
+ await btn.reply({ content: `📂 Session \`${sid}\` archived.`, ephemeral: true });
573
+ }
574
+ else if (btn.customId.startsWith('session_delete_')) {
575
+ const sid = btn.customId.replace('session_delete_', '');
576
+ const h = new SQLiteChatMessageHistory({ sessionId: '' });
577
+ await h.deleteSession(sid);
578
+ h.close();
579
+ if (this.currentSessionId === sid)
580
+ this.currentSessionId = null;
581
+ await btn.reply({ content: `🗑️ Session \`${sid}\` deleted.`, ephemeral: true });
582
+ }
583
+ }
584
+ catch (err) {
585
+ await btn.reply({ content: `Error: ${err.message}`, ephemeral: true }).catch(() => { });
586
+ }
587
+ });
588
+ collector.on('end', async () => {
589
+ try {
590
+ await interaction.editReply({ components: [] });
591
+ }
592
+ catch { /* message may have been deleted */ }
593
+ });
594
+ }
595
+ catch (err) {
596
+ await interaction.reply({ content: `Error: ${err.message}` });
597
+ }
598
+ }
599
+ async cmdSessionSwitch(interaction) {
600
+ const sessionId = interaction.options.getString('id', true);
601
+ try {
602
+ const history = new SQLiteChatMessageHistory({ sessionId: '' });
603
+ await history.switchSession(sessionId);
604
+ history.close();
605
+ this.currentSessionId = sessionId;
606
+ await interaction.reply({ content: `✅ Switched to session \`${sessionId}\`.` });
607
+ }
608
+ catch (err) {
609
+ await interaction.reply({ content: `Error: ${err.message}` });
610
+ }
611
+ }
485
612
  async cmdChronos(interaction) {
486
613
  const prompt = interaction.options.getString('prompt', true);
487
614
  const timeExpr = interaction.options.getString('time', true);
@@ -144,6 +144,8 @@ export class TelegramAdapter {
144
144
  telephonistProvider = null;
145
145
  telephonistModel = null;
146
146
  history = new SQLiteChatMessageHistory({ sessionId: '' });
147
+ /** Per-channel session tracking — which session this Telegram adapter is currently using */
148
+ currentSessionId = null;
147
149
  RATE_LIMIT_MS = 3000; // minimum ms between requests per user
148
150
  rateLimiter = new Map(); // userId -> last request timestamp
149
151
  // Pending Chronos create confirmations (userId -> job data + expiry)
@@ -230,8 +232,8 @@ export class TelegramAdapter {
230
232
  try {
231
233
  // Send "typing" status
232
234
  await ctx.sendChatAction('typing');
233
- const sessionId = await this.history.getCurrentSessionOrCreate();
234
- await this.oracle.setSessionId(sessionId);
235
+ const sessionId = this.currentSessionId ?? await this.history.getCurrentSessionOrCreate();
236
+ this.currentSessionId = sessionId;
235
237
  // Process with Agent
236
238
  const response = await this.oracle.chat(text, undefined, false, {
237
239
  origin_channel: 'telegram',
@@ -317,9 +319,9 @@ export class TelegramAdapter {
317
319
  this.display.log(`Transcription success for @${user}: "${text}"`, { source: 'Telephonist', level: 'success' });
318
320
  // Audit: record telephonist execution
319
321
  try {
320
- const sessionId = await this.history.getCurrentSessionOrCreate();
322
+ const auditSessionId = this.currentSessionId ?? await this.history.getCurrentSessionOrCreate();
321
323
  AuditRepository.getInstance().insert({
322
- session_id: sessionId,
324
+ session_id: auditSessionId,
323
325
  event_type: 'telephonist',
324
326
  agent: 'telephonist',
325
327
  provider: config.audio.provider,
@@ -345,8 +347,8 @@ export class TelegramAdapter {
345
347
  // So I should treat 'text' as if it was a text message.
346
348
  await ctx.reply(`🎤 Transcription: "${text}"`);
347
349
  await ctx.sendChatAction('typing');
348
- const sessionId = await this.history.getCurrentSessionOrCreate();
349
- await this.oracle.setSessionId(sessionId);
350
+ const sessionId = this.currentSessionId ?? await this.history.getCurrentSessionOrCreate();
351
+ this.currentSessionId = sessionId;
350
352
  // Process with Agent
351
353
  const response = await this.oracle.chat(text, usage, true, {
352
354
  origin_channel: 'telegram',
@@ -380,9 +382,9 @@ export class TelegramAdapter {
380
382
  const detail = error?.cause?.message || error?.response?.data?.error?.message || error.message;
381
383
  this.display.log(`Audio processing error for @${user}: ${detail}`, { source: 'Telephonist', level: 'error' });
382
384
  try {
383
- const sessionId = await this.history.getCurrentSessionOrCreate();
385
+ const auditSessionId = this.currentSessionId ?? 'default';
384
386
  AuditRepository.getInstance().insert({
385
- session_id: sessionId,
387
+ session_id: auditSessionId,
386
388
  event_type: 'telephonist',
387
389
  agent: 'telephonist',
388
390
  provider: this.config.get().audio.provider,
@@ -428,12 +430,14 @@ export class TelegramAdapter {
428
430
  return;
429
431
  }
430
432
  try {
431
- // Obter a sessão atual antes de alternar
433
+ // Validate session exists and is usable
432
434
  const history = new SQLiteChatMessageHistory({ sessionId: "" });
433
- // Alternar para a nova sessão
434
435
  await history.switchSession(sessionId);
436
+ history.close();
437
+ // Track this session as the current one for this Telegram channel
438
+ this.currentSessionId = sessionId;
435
439
  await ctx.answerCbQuery();
436
- // Remover a mensagem anterior e enviar confirmação
440
+ // Remove the previous message and send confirmation
437
441
  if (ctx.updateType === 'callback_query') {
438
442
  ctx.deleteMessage().catch(() => { });
439
443
  }
@@ -1154,7 +1158,10 @@ export class TelegramAdapter {
1154
1158
  async handleApproveNewSessionCommand(ctx, user) {
1155
1159
  try {
1156
1160
  const history = new SQLiteChatMessageHistory({ sessionId: "" });
1157
- await history.createNewSession();
1161
+ const newSessionId = await history.createNewSession();
1162
+ history.close();
1163
+ // Track the new session as the current one for this Telegram channel
1164
+ this.currentSessionId = newSessionId;
1158
1165
  }
1159
1166
  catch (e) {
1160
1167
  await ctx.reply(`Error creating new session: ${e.message}`);
@@ -1167,21 +1174,20 @@ export class TelegramAdapter {
1167
1174
  // callback_data limit and they are not user-managed sessions.
1168
1175
  const sessions = (await history.listSessions()).filter((s) => !s.id.startsWith('chronos-job-') && !s.id.startsWith('sati-evaluation'));
1169
1176
  if (sessions.length === 0) {
1170
- await ctx.reply('No active or paused sessions found\\.', { parse_mode: 'MarkdownV2' });
1177
+ await ctx.reply('No sessions found\\.', { parse_mode: 'MarkdownV2' });
1171
1178
  return;
1172
1179
  }
1173
1180
  let response = '*Sessions:*\n\n';
1174
1181
  const keyboard = [];
1175
1182
  for (const session of sessions) {
1176
1183
  const title = session.title || 'Untitled Session';
1177
- const statusEmoji = session.status === 'active' ? '🟢' : '🟡';
1184
+ const isCurrent = session.id === this.currentSessionId;
1185
+ const statusEmoji = isCurrent ? '🟢' : '⚪';
1178
1186
  response += `${statusEmoji} *${escMdRaw(title)}*\n`;
1179
1187
  response += `\\- ID: \`${escMdRaw(session.id)}\`\n`;
1180
- response += `\\- Status: ${escMdRaw(session.status)}\n`;
1181
1188
  response += `\\- Started: ${escMdRaw(new Date(session.started_at).toLocaleString())}\n\n`;
1182
- // Adicionar botão inline para alternar para esta sessão
1183
1189
  const sessionButtons = [];
1184
- if (session.status !== 'active') {
1190
+ if (!isCurrent) {
1185
1191
  sessionButtons.push({
1186
1192
  text: `➡️ Switch`,
1187
1193
  callback_data: `switch_session_${session.id}`
@@ -328,6 +328,10 @@ export class ConfigManager {
328
328
  entries: config.smiths?.entries ?? [],
329
329
  },
330
330
  verbose_mode: resolveBoolean('MORPHEUS_VERBOSE_MODE', config.verbose_mode, true),
331
+ setup: {
332
+ enabled: resolveBoolean('MORPHEUS_SETUP_ENABLED', config.setup?.enabled, true),
333
+ fields: config.setup?.fields ?? ['name', 'timezone', 'preferred_language'],
334
+ },
331
335
  };
332
336
  }
333
337
  get() {
@@ -411,6 +415,13 @@ export class ConfigManager {
411
415
  }
412
416
  return defaults;
413
417
  }
418
+ getSetupConfig() {
419
+ const defaults = { enabled: true, fields: ['name', 'timezone', 'preferred_language'] };
420
+ if (this.config.setup) {
421
+ return { ...defaults, ...this.config.setup };
422
+ }
423
+ return defaults;
424
+ }
414
425
  getSmithsConfig() {
415
426
  const defaults = {
416
427
  enabled: false,
@@ -63,6 +63,10 @@ export const SmithEntrySchema = z.object({
63
63
  auth_token: z.string().min(1),
64
64
  tls: z.boolean().default(false),
65
65
  });
66
+ export const SetupConfigSchema = z.object({
67
+ enabled: z.boolean().default(true),
68
+ fields: z.array(z.string()).default(['name', 'timezone', 'preferred_language']),
69
+ });
66
70
  export const SmithsConfigSchema = z.object({
67
71
  enabled: z.boolean().default(false),
68
72
  execution_mode: z.enum(['sync', 'async']).default('async'),
@@ -96,6 +100,7 @@ export const ConfigSchema = z.object({
96
100
  chronos: ChronosConfigSchema.optional(),
97
101
  devkit: DevKitConfigSchema.optional(),
98
102
  smiths: SmithsConfigSchema.optional(),
103
+ setup: SetupConfigSchema.optional(),
99
104
  verbose_mode: z.boolean().default(true),
100
105
  channels: z.object({
101
106
  telegram: z.object({
package/dist/http/api.js CHANGED
@@ -20,6 +20,7 @@ import { ChronosWorker } from '../runtime/chronos/worker.js';
20
20
  import { createChronosJobRouter, createChronosConfigRouter } from './routers/chronos.js';
21
21
  import { createSkillsRouter } from './routers/skills.js';
22
22
  import { createSmithsRouter } from './routers/smiths.js';
23
+ import { createDangerRouter } from './routers/danger.js';
23
24
  import { getActiveEnvOverrides } from '../config/precedence.js';
24
25
  import { hotReloadConfig, getRestartRequiredChanges } from '../runtime/hot-reload.js';
25
26
  import { AuditRepository } from '../runtime/audit/repository.js';
@@ -49,6 +50,8 @@ export function createApiRouter(oracle, chronosWorker) {
49
50
  router.use('/skills', createSkillsRouter());
50
51
  // Mount Smiths router
51
52
  router.use('/smiths', createSmithsRouter());
53
+ // Mount Danger Zone router
54
+ router.use('/danger', createDangerRouter());
52
55
  // --- Session Management ---
53
56
  router.get('/sessions', async (req, res) => {
54
57
  try {
@@ -61,8 +64,7 @@ export function createApiRouter(oracle, chronosWorker) {
61
64
  });
62
65
  router.post('/sessions', async (req, res) => {
63
66
  try {
64
- await history.createNewSession();
65
- const newSessionId = await history.getCurrentSessionOrCreate(); // Should be the new one
67
+ const newSessionId = await history.createNewSession();
66
68
  res.json({ success: true, id: newSessionId, message: 'New session started' });
67
69
  }
68
70
  catch (err) {
@@ -248,7 +250,7 @@ export function createApiRouter(oracle, chronosWorker) {
248
250
  }
249
251
  try {
250
252
  const { message, sessionId } = parsed.data;
251
- await oracle.setSessionId(sessionId);
253
+ // Session is passed via taskContext — no need to mutate global Oracle state.
252
254
  const response = await oracle.chat(message, undefined, false, {
253
255
  origin_channel: 'ui',
254
256
  session_id: sessionId,
@@ -0,0 +1,137 @@
1
+ import { Router } from 'express';
2
+ import Database from 'better-sqlite3';
3
+ import path from 'path';
4
+ import { homedir } from 'os';
5
+ import fs from 'fs-extra';
6
+ import { z } from 'zod';
7
+ import { SatiRepository } from '../../runtime/memory/sati/repository.js';
8
+ import { DisplayManager } from '../../runtime/display.js';
9
+ /**
10
+ * Valid categories the user can choose to delete.
11
+ */
12
+ const VALID_CATEGORIES = [
13
+ 'sessions', // sessions + messages
14
+ 'memories', // sati-memory.db (long-term memory, embeddings, session chunks)
15
+ 'tasks', // background tasks
16
+ 'audit', // audit_events
17
+ 'chronos', // chronos_jobs + chronos_executions
18
+ 'webhooks', // webhooks + webhook_notifications
19
+ ];
20
+ const ResetBodySchema = z.object({
21
+ categories: z.array(z.enum(VALID_CATEGORIES)).min(1, 'At least one category must be selected'),
22
+ });
23
+ /**
24
+ * Creates the Danger Zone API router.
25
+ * Provides destructive operations for resetting user data.
26
+ */
27
+ export function createDangerRouter() {
28
+ const router = Router();
29
+ const display = DisplayManager.getInstance();
30
+ /**
31
+ * GET /api/danger/categories — List available reset categories
32
+ */
33
+ router.get('/categories', (_req, res) => {
34
+ res.json({
35
+ categories: VALID_CATEGORIES.map((id) => ({ id })),
36
+ });
37
+ });
38
+ /**
39
+ * DELETE /api/danger/reset — Purge selected user data
40
+ *
41
+ * Body: { categories: ['sessions', 'memories', 'tasks', 'audit', 'chronos', 'webhooks'] }
42
+ */
43
+ router.delete('/reset', async (req, res) => {
44
+ // Validate body
45
+ const parsed = ResetBodySchema.safeParse(req.body);
46
+ if (!parsed.success) {
47
+ return res.status(400).json({
48
+ error: 'Invalid request',
49
+ details: parsed.error.issues.map((i) => i.message),
50
+ });
51
+ }
52
+ const { categories } = parsed.data;
53
+ try {
54
+ const memoryDir = path.join(homedir(), '.morpheus', 'memory');
55
+ const shortMemoryPath = path.join(memoryDir, 'short-memory.db');
56
+ const satiMemoryPath = path.join(memoryDir, 'sati-memory.db');
57
+ const counts = {};
58
+ // ─── 1. Purge short-memory.db tables based on selected categories ───
59
+ const needsShortDb = categories.some((c) => ['sessions', 'tasks', 'audit', 'chronos', 'webhooks'].includes(c));
60
+ if (needsShortDb && fs.existsSync(shortMemoryPath)) {
61
+ const db = new Database(shortMemoryPath, { timeout: 5000 });
62
+ db.pragma('journal_mode = WAL');
63
+ const transaction = db.transaction(() => {
64
+ if (categories.includes('sessions')) {
65
+ const msgResult = db.prepare('DELETE FROM messages').run();
66
+ counts.messages = msgResult.changes;
67
+ const sessResult = db.prepare('DELETE FROM sessions').run();
68
+ counts.sessions = sessResult.changes;
69
+ // Also clear first-time setup state so onboarding runs again after reset
70
+ try {
71
+ db.prepare('DELETE FROM setup_state').run();
72
+ }
73
+ catch {
74
+ // Table may not exist on older installations — safe to ignore
75
+ }
76
+ }
77
+ if (categories.includes('tasks')) {
78
+ const taskResult = db.prepare('DELETE FROM tasks').run();
79
+ counts.tasks = taskResult.changes;
80
+ }
81
+ if (categories.includes('audit')) {
82
+ const auditResult = db.prepare('DELETE FROM audit_events').run();
83
+ counts.audit_events = auditResult.changes;
84
+ }
85
+ if (categories.includes('chronos')) {
86
+ const jobsResult = db.prepare('DELETE FROM chronos_jobs').run();
87
+ counts.chronos_jobs = jobsResult.changes;
88
+ const execResult = db.prepare('DELETE FROM chronos_executions').run();
89
+ counts.chronos_executions = execResult.changes;
90
+ }
91
+ if (categories.includes('webhooks')) {
92
+ const notifResult = db.prepare('DELETE FROM webhook_notifications').run();
93
+ counts.webhook_notifications = notifResult.changes;
94
+ const whResult = db.prepare('DELETE FROM webhooks').run();
95
+ counts.webhooks = whResult.changes;
96
+ }
97
+ });
98
+ transaction();
99
+ db.close();
100
+ }
101
+ // ─── 2. Purge sati-memory.db (close, delete, recreate) ───
102
+ if (categories.includes('memories')) {
103
+ const satiRepo = SatiRepository.getInstance();
104
+ satiRepo.close();
105
+ if (fs.existsSync(satiMemoryPath)) {
106
+ fs.removeSync(satiMemoryPath);
107
+ fs.removeSync(satiMemoryPath + '-wal');
108
+ fs.removeSync(satiMemoryPath + '-shm');
109
+ counts.sati_memory = 1;
110
+ }
111
+ // Reinitialize so schema is recreated cleanly
112
+ satiRepo.initialize();
113
+ }
114
+ display.log(`🗑️ Data reset via Danger Zone: [${categories.join(', ')}]`, {
115
+ source: 'DangerZone',
116
+ level: 'warning',
117
+ });
118
+ res.json({
119
+ success: true,
120
+ message: 'Selected data has been reset successfully.',
121
+ categories,
122
+ deleted: counts,
123
+ });
124
+ }
125
+ catch (error) {
126
+ display.log(`❌ Danger Zone reset failed: ${error}`, {
127
+ source: 'DangerZone',
128
+ level: 'error',
129
+ });
130
+ res.status(500).json({
131
+ error: 'Failed to reset data',
132
+ details: error instanceof Error ? error.message : String(error),
133
+ });
134
+ }
135
+ });
136
+ return router;
137
+ }
@@ -259,7 +259,7 @@ ${context ? `CONTEXT FROM ORACLE:\n${context}` : ""}
259
259
  try {
260
260
  const inputCount = messages.length;
261
261
  const startMs = Date.now();
262
- const response = await this.agent.invoke({ messages }, { recursionLimit: 100 });
262
+ const response = await this.agent.invoke({ messages }, { recursionLimit: 50 });
263
263
  const durationMs = Date.now() - startMs;
264
264
  const apocConfig = this.config.apoc || this.config.llm;
265
265
  const lastMessage = response.messages[response.messages.length - 1];
@@ -190,6 +190,7 @@ export class AuditRepository {
190
190
  SUM(CASE WHEN ae.event_type = 'mcp_tool' THEN 1 ELSE 0 END) as mcpToolCount,
191
191
  SUM(CASE WHEN ae.event_type = 'skill_executed' THEN 1 ELSE 0 END) as skillCount,
192
192
  SUM(CASE WHEN ae.event_type = 'memory_recovery' THEN 1 ELSE 0 END) as memoryRecoveryCount,
193
+ SUM(CASE WHEN ae.event_type = 'memory_persist' THEN 1 ELSE 0 END) as memoryPersistCount,
193
194
  SUM(CASE WHEN ae.event_type = 'chronos_job' THEN 1 ELSE 0 END) as chronosJobCount,
194
195
  SUM(CASE WHEN ae.event_type = 'task_created' THEN 1 ELSE 0 END) as taskCreatedCount,
195
196
  SUM(CASE WHEN ae.event_type = 'task_completed' THEN 1 ELSE 0 END) as taskCompletedCount,
@@ -304,6 +305,7 @@ export class AuditRepository {
304
305
  mcpToolCount: totalsRow?.mcpToolCount ?? 0,
305
306
  skillCount: totalsRow?.skillCount ?? 0,
306
307
  memoryRecoveryCount: totalsRow?.memoryRecoveryCount ?? 0,
308
+ memoryPersistCount: totalsRow?.memoryPersistCount ?? 0,
307
309
  chronosJobCount: totalsRow?.chronosJobCount ?? 0,
308
310
  taskCreatedCount: totalsRow?.taskCreatedCount ?? 0,
309
311
  taskCompletedCount: totalsRow?.taskCompletedCount ?? 0,
@@ -117,7 +117,7 @@ CRITICAL — NEVER FABRICATE DATA:
117
117
  origin_user_id: taskContext?.origin_user_id,
118
118
  };
119
119
  const startMs = Date.now();
120
- const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }, { recursionLimit: 100 }));
120
+ const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }, { recursionLimit: 50 }));
121
121
  const durationMs = Date.now() - startMs;
122
122
  const lastMessage = response.messages[response.messages.length - 1];
123
123
  const content = typeof lastMessage.content === "string"
@@ -65,7 +65,7 @@ export class SatiMemoryMiddleware {
65
65
  async afterAgent(generatedResponse, history, userSessionId) {
66
66
  try {
67
67
  await this.service.evaluateAndPersist([
68
- ...history.slice(-5).map(m => ({
68
+ ...history.map(m => ({
69
69
  role: m._getType() === 'human' ? 'user' : 'assistant',
70
70
  content: m.content.toString()
71
71
  })),