morpheus-cli 0.9.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/dist/channels/discord.js +133 -6
  2. package/dist/channels/telegram.js +23 -17
  3. package/dist/http/api.js +2 -3
  4. package/dist/runtime/apoc.js +1 -1
  5. package/dist/runtime/keymaker.js +1 -1
  6. package/dist/runtime/memory/sati/service.js +1 -1
  7. package/dist/runtime/memory/sqlite.js +31 -124
  8. package/dist/runtime/neo.js +1 -1
  9. package/dist/runtime/oracle.js +30 -52
  10. package/dist/runtime/smiths/delegator.js +1 -1
  11. package/dist/runtime/trinity.js +1 -1
  12. package/dist/ui/assets/{AuditDashboard-5sA8Sd8S.js → AuditDashboard-nVV9KKFp.js} +1 -1
  13. package/dist/ui/assets/Chat-ChsmnZzq.js +41 -0
  14. package/dist/ui/assets/{Chronos-BAjeLobF.js → Chronos-kgO7IkEj.js} +1 -1
  15. package/dist/ui/assets/{ConfirmationModal-fvgnOWTY.js → ConfirmationModal-D1BYPXJ4.js} +1 -1
  16. package/dist/ui/assets/{Dashboard-Ca5mSefz.js → Dashboard-DWB5NwQn.js} +1 -1
  17. package/dist/ui/assets/{DeleteConfirmationModal-A8EmnHoa.js → DeleteConfirmationModal-CgIMbyB7.js} +1 -1
  18. package/dist/ui/assets/{Logs-CYu7se7R.js → Logs-DGdRnEFi.js} +1 -1
  19. package/dist/ui/assets/{MCPManager-DsDA_ZVT.js → MCPManager-BDjWMRRX.js} +1 -1
  20. package/dist/ui/assets/{ModelPricing-DnSm_Nh-.js → ModelPricing-DAk1sS7D.js} +1 -1
  21. package/dist/ui/assets/{Notifications-CiljQzvM.js → Notifications-DMEq6EZR.js} +1 -1
  22. package/dist/ui/assets/{SatiMemories-rnO2b0LG.js → SatiMemories-BxicQE35.js} +1 -1
  23. package/dist/ui/assets/{SessionAudit-Dfvhge3Z.js → SessionAudit-CKJQf9LU.js} +1 -1
  24. package/dist/ui/assets/{Settings-OQlHAJoy.js → Settings-CulMd4Qr.js} +1 -1
  25. package/dist/ui/assets/{Skills-Crsybug0.js → Skills-DPoqYa8Y.js} +1 -1
  26. package/dist/ui/assets/{Smiths-wm90jRDT.js → Smiths-Clamjlph.js} +1 -1
  27. package/dist/ui/assets/{Tasks-C5FMu_Yu.js → Tasks-BfTkhB1J.js} +1 -1
  28. package/dist/ui/assets/{TrinityDatabases-BzYfecKI.js → TrinityDatabases-BmM1S9aQ.js} +1 -1
  29. package/dist/ui/assets/{UsageStats-CBo2vW2n.js → UsageStats-aAu2DFlb.js} +1 -1
  30. package/dist/ui/assets/{WebhookManager-0tDFkfHd.js → WebhookManager-DdnRSWl9.js} +1 -1
  31. package/dist/ui/assets/{audit-B-F8XPLi.js → audit-CqszEkOd.js} +1 -1
  32. package/dist/ui/assets/{chronos-BvMxfBQH.js → chronos-CPwFWid9.js} +1 -1
  33. package/dist/ui/assets/{config-DteVgNGR.js → config-D0DePxKu.js} +1 -1
  34. package/dist/ui/assets/{index-Cwqr-n0Y.js → index-BxVeRyTh.js} +2 -2
  35. package/dist/ui/assets/index-OLhpm8I7.css +1 -0
  36. package/dist/ui/assets/{mcp-DxzodOdH.js → mcp-Gjc3IZpO.js} +1 -1
  37. package/dist/ui/assets/{skills--hAyQnmG.js → skills-B5DnmnHW.js} +1 -1
  38. package/dist/ui/assets/{stats-Cibaisqd.js → stats-BAse7jj0.js} +1 -1
  39. package/dist/ui/index.html +2 -2
  40. package/dist/ui/sw.js +1 -1
  41. package/package.json +5 -4
  42. package/dist/ui/assets/Chat-CjxeAQmd.js +0 -41
  43. package/dist/ui/assets/index-DcfyUdLI.css +0 -1
@@ -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}`
package/dist/http/api.js CHANGED
@@ -61,8 +61,7 @@ export function createApiRouter(oracle, chronosWorker) {
61
61
  });
62
62
  router.post('/sessions', async (req, res) => {
63
63
  try {
64
- await history.createNewSession();
65
- const newSessionId = await history.getCurrentSessionOrCreate(); // Should be the new one
64
+ const newSessionId = await history.createNewSession();
66
65
  res.json({ success: true, id: newSessionId, message: 'New session started' });
67
66
  }
68
67
  catch (err) {
@@ -248,7 +247,7 @@ export function createApiRouter(oracle, chronosWorker) {
248
247
  }
249
248
  try {
250
249
  const { message, sessionId } = parsed.data;
251
- await oracle.setSessionId(sessionId);
250
+ // Session is passed via taskContext — no need to mutate global Oracle state.
252
251
  const response = await oracle.chat(message, undefined, false, {
253
252
  origin_channel: 'ui',
254
253
  session_id: sessionId,
@@ -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];
@@ -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"
@@ -97,7 +97,7 @@ export class SatiService {
97
97
  console.warn('[SatiService] Failed to persist input log:', e);
98
98
  }
99
99
  const satiStartMs = Date.now();
100
- const response = await agent.invoke({ messages }, { recursionLimit: 100 });
100
+ const response = await agent.invoke({ messages }, { recursionLimit: 50 });
101
101
  const satiDurationMs = Date.now() - satiStartMs;
102
102
  const lastMessage = response.messages[response.messages.length - 1];
103
103
  let content = lastMessage.content.toString();
@@ -716,36 +716,19 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
716
716
  }
717
717
  async createNewSession() {
718
718
  const now = Date.now();
719
- // Transação para garantir consistência
720
- const tx = this.db.transaction(() => {
721
- // Pegar a sessão atualmente ativa
722
- const activeSession = this.db.prepare(`
723
- SELECT id FROM sessions
724
- WHERE status = 'active'
725
- `).get();
726
- // Se houver uma sessão ativa, mudar seu status para 'paused'
727
- if (activeSession) {
728
- this.db.prepare(`
729
- UPDATE sessions
730
- SET status = 'paused'
731
- WHERE id = ?
732
- `).run(activeSession.id);
733
- }
734
- // Criar uma nova sessão ativa
735
- const newId = randomUUID();
736
- this.db.prepare(`
737
- INSERT INTO sessions (
738
- id,
739
- started_at,
740
- status
741
- ) VALUES (?, ?, 'active')
742
- `).run(newId, now);
743
- // Atualizar o ID da sessão atual desta instância
744
- this.sessionId = newId;
745
- this.titleSet = false; // reset cache for new session
746
- });
747
- tx(); // Executar a transação
748
- this.display.log('✅ Nova sessão iniciada e sessão anterior pausada', { source: 'Sati' });
719
+ const newId = randomUUID();
720
+ this.db.prepare(`
721
+ INSERT INTO sessions (
722
+ id,
723
+ started_at,
724
+ status
725
+ ) VALUES (?, ?, 'active')
726
+ `).run(newId, now);
727
+ // Update this instance to point to the new session
728
+ this.sessionId = newId;
729
+ this.titleSet = false;
730
+ this.display.log('✅ New session created', { source: 'Sati' });
731
+ return newId;
749
732
  }
750
733
  chunkText(text, chunkSize = 500, overlap = 50) {
751
734
  if (!text || text.length === 0)
@@ -890,27 +873,6 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
890
873
  `).run(now, sessionId);
891
874
  });
892
875
  tx(); // Executar a transação
893
- // Se a sessão era active, verificar se há outra para ativar
894
- if (session.status === 'active') {
895
- const nextSession = this.db.prepare(`
896
- SELECT id FROM sessions
897
- WHERE status = 'paused'
898
- ORDER BY started_at DESC
899
- LIMIT 1
900
- `).get();
901
- if (nextSession) {
902
- // Promover a próxima sessão a ativa
903
- this.db.prepare(`
904
- UPDATE sessions
905
- SET status = 'active'
906
- WHERE id = ?
907
- `).run(nextSession.id);
908
- }
909
- else {
910
- // Nenhuma outra sessão, criar nova
911
- this.createFreshSession();
912
- }
913
- }
914
876
  }
915
877
  /**
916
878
  * Renomear uma sessão ativa ou pausada.
@@ -941,101 +903,46 @@ export class SQLiteChatMessageHistory extends BaseListChatMessageHistory {
941
903
  tx(); // Executar a transação
942
904
  }
943
905
  /**
944
- * Trocar o contexto ativo entre sessões não finalizadas.
945
- * Validar sessão alvo: existe e status ∈ (paused, active).
946
- * Se já for active, não faz nada.
947
- * Transação: sessão atual active → paused, sessão alvo → active.
948
- */
949
- /**
950
- * Creates a session row with status 'paused' if it doesn't already exist.
906
+ * Creates a session row with status 'active' if it doesn't already exist.
951
907
  * Safe to call multiple times — idempotent.
952
908
  */
953
909
  ensureSession(sessionId) {
954
910
  const existing = this.db.prepare('SELECT id FROM sessions WHERE id = ?').get(sessionId);
955
911
  if (!existing) {
956
- this.db.prepare("INSERT INTO sessions (id, started_at, status) VALUES (?, ?, 'paused')").run(sessionId, Date.now());
912
+ this.db.prepare("INSERT INTO sessions (id, started_at, status) VALUES (?, ?, 'active')").run(sessionId, Date.now());
957
913
  }
958
914
  }
915
+ /**
916
+ * Validates that the target session exists and is usable (not archived/deleted).
917
+ * No longer swaps active↔paused — sessions are independently usable from any channel.
918
+ */
959
919
  async switchSession(targetSessionId) {
960
- // Validar sessão alvo: existe e status ∈ (paused, active)
961
920
  const targetSession = this.db.prepare(`
962
921
  SELECT id, status FROM sessions
963
922
  WHERE id = ?
964
923
  `).get(targetSessionId);
965
924
  if (!targetSession) {
966
- throw new Error(`Sessão alvo com ID ${targetSessionId} não encontrada.`);
925
+ throw new Error(`Session with ID ${targetSessionId} not found.`);
967
926
  }
968
- if (targetSession.status !== 'active' && targetSession.status !== 'paused') {
969
- throw new Error(`Sessão alvo com ID ${targetSessionId} não está em estado ativo ou pausado. Status atual: ${targetSession.status}`);
927
+ if (targetSession.status === 'archived' || targetSession.status === 'deleted') {
928
+ throw new Error(`Session ${targetSessionId} is ${targetSession.status} and cannot be used.`);
970
929
  }
971
- // Se já for active, não faz nada
972
- if (targetSession.status === 'active') {
973
- return; // A sessão alvo já está ativa, não precisa fazer nada
974
- }
975
- // Transação: sessão atual active → paused, sessão alvo → active
976
- const tx = this.db.transaction(() => {
977
- // Pegar a sessão atualmente ativa
978
- const currentActiveSession = this.db.prepare(`
979
- SELECT id FROM sessions
980
- WHERE status = 'active'
981
- `).get();
982
- // Se houver uma sessão ativa, mudar seu status para 'paused'
983
- if (currentActiveSession) {
984
- this.db.prepare(`
985
- UPDATE sessions
986
- SET status = 'paused'
987
- WHERE id = ?
988
- `).run(currentActiveSession.id);
989
- }
990
- // Mudar o status da sessão alvo para 'active'
991
- this.db.prepare(`
992
- UPDATE sessions
993
- SET status = 'active'
994
- WHERE id = ?
995
- `).run(targetSessionId);
996
- });
997
- tx(); // Executar a transação
998
930
  }
999
931
  /**
1000
- * Garantir que sempre exista uma sessão ativa válida.
1001
- * Buscar sessão com status = 'active', retornar seu id se existir,
1002
- * ou criar nova sessão (createFreshSession) e retornar o novo id.
932
+ * Returns the most recently created usable session, or creates one if none exist.
933
+ * A session is usable if its status is 'active' or 'paused' (both are equivalent post-refactor).
1003
934
  */
1004
935
  async getCurrentSessionOrCreate() {
1005
- // Buscar sessão com status = 'active'
1006
- const activeSession = this.db.prepare(`
1007
- SELECT id FROM sessions
1008
- WHERE status = 'active'
1009
- `).get();
1010
- if (activeSession) {
1011
- // Se existir, retornar seu id
1012
- return activeSession.id;
1013
- }
1014
- else {
1015
- // Se não existir, criar nova sessão (createFreshSession) e retornar o novo id
1016
- const newId = await this.createFreshSession();
1017
- return newId;
1018
- }
1019
- }
1020
- async createFreshSession() {
1021
- // Validar que não existe sessão 'active'
1022
- const activeSession = this.db.prepare(`
936
+ const session = this.db.prepare(`
1023
937
  SELECT id FROM sessions
1024
- WHERE status = 'active'
938
+ WHERE status IN ('active', 'paused')
939
+ ORDER BY started_at DESC
940
+ LIMIT 1
1025
941
  `).get();
1026
- if (activeSession) {
1027
- throw new Error('Já existe uma sessão ativa. Não é possível criar uma nova sessão ativa.');
942
+ if (session) {
943
+ return session.id;
1028
944
  }
1029
- const now = Date.now();
1030
- const newId = randomUUID();
1031
- this.db.prepare(`
1032
- INSERT INTO sessions (
1033
- id,
1034
- started_at,
1035
- status
1036
- ) VALUES (?, ?, 'active')
1037
- `).run(newId, now);
1038
- return newId;
945
+ return this.createNewSession();
1039
946
  }
1040
947
  /**
1041
948
  * Lists all active and paused sessions with their basic information.
@@ -137,7 +137,7 @@ ${context ? `Context:\n${context}` : ""}
137
137
  };
138
138
  const inputCount = messages.length;
139
139
  const startMs = Date.now();
140
- const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }, { recursionLimit: 100 }));
140
+ const response = await TaskRequestContext.run(invokeContext, () => this.agent.invoke({ messages }, { recursionLimit: 50 }));
141
141
  const durationMs = Date.now() - startMs;
142
142
  const lastMessage = response.messages[response.messages.length - 1];
143
143
  const content = typeof lastMessage.content === "string"