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.
- package/README.md +18 -4
- package/dist/channels/discord.js +133 -6
- package/dist/channels/telegram.js +23 -17
- package/dist/config/manager.js +11 -0
- package/dist/config/schemas.js +5 -0
- package/dist/http/api.js +5 -3
- package/dist/http/routers/danger.js +137 -0
- package/dist/runtime/apoc.js +1 -1
- package/dist/runtime/audit/repository.js +2 -0
- package/dist/runtime/keymaker.js +1 -1
- package/dist/runtime/memory/sati/index.js +1 -1
- package/dist/runtime/memory/sati/service.js +28 -1
- package/dist/runtime/memory/session-embedding-worker.js +43 -36
- package/dist/runtime/memory/sqlite.js +31 -124
- package/dist/runtime/neo.js +1 -1
- package/dist/runtime/oracle.js +55 -54
- package/dist/runtime/setup/__tests__/repository.test.js +115 -0
- package/dist/runtime/setup/repository.js +87 -0
- package/dist/runtime/smiths/delegator.js +1 -1
- package/dist/runtime/tools/setup-tool.js +57 -0
- package/dist/runtime/trinity.js +1 -1
- package/dist/ui/assets/AuditDashboard-C1f6Hbdw.js +1 -0
- package/dist/ui/assets/Chat-5AeRYuRj.js +41 -0
- package/dist/ui/assets/{Chronos-BAjeLobF.js → Chronos-BrKldYVw.js} +1 -1
- package/dist/ui/assets/{ConfirmationModal-fvgnOWTY.js → ConfirmationModal-DsbS3XkJ.js} +1 -1
- package/dist/ui/assets/{Dashboard-Ca5mSefz.js → Dashboard-DvrTXLdo.js} +1 -1
- package/dist/ui/assets/{DeleteConfirmationModal-A8EmnHoa.js → DeleteConfirmationModal-BfSjv04R.js} +1 -1
- package/dist/ui/assets/{Logs-CYu7se7R.js → Logs-B0ZYWs5x.js} +1 -1
- package/dist/ui/assets/MCPManager-BwHGTeNs.js +1 -0
- package/dist/ui/assets/{ModelPricing-DnSm_Nh-.js → ModelPricing-CYhGRQr8.js} +1 -1
- package/dist/ui/assets/{Notifications-CiljQzvM.js → Notifications-BYMAtVMq.js} +1 -1
- package/dist/ui/assets/{Pagination-JsiwxVNQ.js → Pagination-oTGieBLM.js} +1 -1
- package/dist/ui/assets/SatiMemories-I1vsYtP2.js +1 -0
- package/dist/ui/assets/SessionAudit-BCecQWde.js +9 -0
- package/dist/ui/assets/Settings-Cu4D-7tb.js +47 -0
- package/dist/ui/assets/Skills-lGU3I5DO.js +7 -0
- package/dist/ui/assets/Smiths-DnEH3nID.js +1 -0
- package/dist/ui/assets/Tasks-Bz92GPWK.js +1 -0
- package/dist/ui/assets/{TrinityDatabases-BzYfecKI.js → TrinityDatabases-BUY-3j7Q.js} +1 -1
- package/dist/ui/assets/{UsageStats-CBo2vW2n.js → UsageStats-Dr5eSgJc.js} +1 -1
- package/dist/ui/assets/{WebhookManager-0tDFkfHd.js → WebhookManager-DIASAC-1.js} +1 -1
- package/dist/ui/assets/{audit-B-F8XPLi.js → audit-CcAEDbZh.js} +1 -1
- package/dist/ui/assets/{chronos-BvMxfBQH.js → chronos-2Z9E96_1.js} +1 -1
- package/dist/ui/assets/{config-DteVgNGR.js → config-DdfK4DX6.js} +1 -1
- package/dist/ui/assets/index-D4fzIKy1.css +1 -0
- package/dist/ui/assets/{index-Cwqr-n0Y.js → index-Dpd1Mkgp.js} +5 -5
- package/dist/ui/assets/{mcp-DxzodOdH.js → mcp-BWMt8aY7.js} +1 -1
- package/dist/ui/assets/{skills--hAyQnmG.js → skills-D7JjK7JH.js} +1 -1
- package/dist/ui/assets/{stats-Cibaisqd.js → stats-DoIhtLot.js} +1 -1
- package/dist/ui/assets/{vendor-icons-BVuQI-6R.js → vendor-icons-DMd9RGvJ.js} +1 -1
- package/dist/ui/index.html +3 -3
- package/dist/ui/sw.js +1 -1
- package/package.json +5 -4
- package/dist/ui/assets/AuditDashboard-5sA8Sd8S.js +0 -1
- package/dist/ui/assets/Chat-CjxeAQmd.js +0 -41
- package/dist/ui/assets/MCPManager-DsDA_ZVT.js +0 -1
- package/dist/ui/assets/SatiMemories-rnO2b0LG.js +0 -1
- package/dist/ui/assets/SessionAudit-Dfvhge3Z.js +0 -9
- package/dist/ui/assets/Settings-OQlHAJoy.js +0 -41
- package/dist/ui/assets/Skills-Crsybug0.js +0 -7
- package/dist/ui/assets/Smiths-wm90jRDT.js +0 -1
- package/dist/ui/assets/Tasks-C5FMu_Yu.js +0 -1
- 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
|
-
| `/
|
|
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
|
package/dist/channels/discord.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
322
|
+
const auditSessionId = this.currentSessionId ?? await this.history.getCurrentSessionOrCreate();
|
|
321
323
|
AuditRepository.getInstance().insert({
|
|
322
|
-
session_id:
|
|
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
|
-
|
|
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
|
|
385
|
+
const auditSessionId = this.currentSessionId ?? 'default';
|
|
384
386
|
AuditRepository.getInstance().insert({
|
|
385
|
-
session_id:
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
|
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
|
|
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 (
|
|
1190
|
+
if (!isCurrent) {
|
|
1185
1191
|
sessionButtons.push({
|
|
1186
1192
|
text: `➡️ Switch`,
|
|
1187
1193
|
callback_data: `switch_session_${session.id}`
|
package/dist/config/manager.js
CHANGED
|
@@ -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,
|
package/dist/config/schemas.js
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|
package/dist/runtime/apoc.js
CHANGED
|
@@ -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:
|
|
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,
|
package/dist/runtime/keymaker.js
CHANGED
|
@@ -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:
|
|
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.
|
|
68
|
+
...history.map(m => ({
|
|
69
69
|
role: m._getType() === 'human' ? 'user' : 'assistant',
|
|
70
70
|
content: m.content.toString()
|
|
71
71
|
})),
|