@ducci/jarvis 1.0.77 → 1.0.79

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ducci/jarvis",
3
- "version": "1.0.77",
3
+ "version": "1.0.79",
4
4
  "description": "A fully automated agent system that lives on a server.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
- import { Bot } from 'grammy';
3
+ import { Bot, InlineKeyboard } from 'grammy';
4
4
  import { run } from '@grammyjs/runner';
5
5
  import { handleChat, requestAbort } from '../../server/agent.js';
6
6
  import { loadSession } from '../../server/sessions.js';
@@ -98,15 +98,72 @@ export async function startTelegramChannel(config) {
98
98
  const bot = new Bot(token);
99
99
  const sessions = load();
100
100
 
101
- // Tracks chats with an active agent run and buffers messages arriving during that run.
102
- // When the run finishes all buffered messages are merged into one combined run.
101
+ // Tracks chats with an active agent run per slot.
102
+ // Keys are "chatId:slot" strings.
103
103
  const isRunning = new Set();
104
- const pendingMessages = new Map(); // chatId -> [{text, attachments, ts}]
104
+ const pendingMessages = new Map(); // "chatId:slot" -> [{text, attachments, ts}]
105
+ const runStartTimes = new Map(); // "chatId:slot" -> Date
106
+
107
+ // --- Slot helpers ---
108
+ // sessions[chatId] is either:
109
+ // - undefined (no session yet)
110
+ // - string (legacy: single session ID, treated as slot 1)
111
+ // - { active: number, slots: { "1": sessionId, "2": sessionId, ... } }
112
+
113
+ function getActiveSlot(chatId) {
114
+ const d = sessions[chatId];
115
+ if (!d || typeof d === 'string') return 1;
116
+ return d.active ?? 1;
117
+ }
118
+
119
+ function getSessionId(chatId, slot) {
120
+ const d = sessions[chatId];
121
+ if (!d) return null;
122
+ if (typeof d === 'string') return slot == 1 ? d : null;
123
+ return d.slots?.[String(slot)] ?? null;
124
+ }
125
+
126
+ function setSessionId(chatId, slot, sessionId) {
127
+ const d = sessions[chatId];
128
+ if (!d || typeof d === 'string') {
129
+ const legacy = typeof d === 'string' ? d : null;
130
+ sessions[chatId] = { active: Number(slot), slots: {} };
131
+ if (legacy) sessions[chatId].slots['1'] = legacy;
132
+ }
133
+ if (sessionId === null) {
134
+ delete sessions[chatId].slots[String(slot)];
135
+ if (Object.keys(sessions[chatId].slots).length === 0) {
136
+ delete sessions[chatId];
137
+ }
138
+ } else {
139
+ sessions[chatId].slots[String(slot)] = sessionId;
140
+ }
141
+ save(sessions);
142
+ }
143
+
144
+ function setActiveSlot(chatId, slot) {
145
+ const d = sessions[chatId];
146
+ if (!d || typeof d === 'string') {
147
+ const legacy = typeof d === 'string' ? d : null;
148
+ sessions[chatId] = { active: Number(slot), slots: {} };
149
+ if (legacy) sessions[chatId].slots['1'] = legacy;
150
+ } else {
151
+ sessions[chatId].active = Number(slot);
152
+ }
153
+ save(sessions);
154
+ }
155
+
156
+ function slotKey(chatId, slot) {
157
+ return `${chatId}:${slot}`;
158
+ }
159
+
160
+ // --- Commands ---
105
161
 
106
162
  await bot.api.setMyCommands([
107
- { command: 'new', description: 'Start a fresh session' },
108
- { command: 'usage', description: 'Show token usage for the current session' },
109
- { command: 'stop', description: 'Stop the current run' },
163
+ { command: 'new', description: 'Reset the active slot (fresh session)' },
164
+ { command: 'usage', description: 'Token usage for the active slot' },
165
+ { command: 'stop', description: 'Stop the running agent on the active slot' },
166
+ { command: 'slots', description: 'Show all slots and their status' },
110
167
  ]);
111
168
 
112
169
  bot.command('usage', async (ctx) => {
@@ -114,7 +171,8 @@ export async function startTelegramChannel(config) {
114
171
  if (!allowedUserIds.includes(userId)) return;
115
172
 
116
173
  const chatId = ctx.chat.id;
117
- const sessionId = sessions[chatId];
174
+ const slot = getActiveSlot(chatId);
175
+ const sessionId = getSessionId(chatId, slot);
118
176
  if (!sessionId) {
119
177
  await ctx.reply('No active session. Send a message to start one.');
120
178
  return;
@@ -134,7 +192,7 @@ export async function startTelegramChannel(config) {
134
192
  ? `\nCache read: ${cacheRead.toLocaleString()}\nCache written: ${cacheCreation.toLocaleString()}`
135
193
  : '';
136
194
  await ctx.reply(
137
- `Token usage for current session:\nIn: ${u.prompt.toLocaleString()}\nOut: ${u.completion.toLocaleString()}\nTotal: ${total.toLocaleString()}${cacheLines}`
195
+ `Token usage for slot ${slot}:\nIn: ${u.prompt.toLocaleString()}\nOut: ${u.completion.toLocaleString()}\nTotal: ${total.toLocaleString()}${cacheLines}`
138
196
  );
139
197
  });
140
198
 
@@ -143,9 +201,11 @@ export async function startTelegramChannel(config) {
143
201
  if (!allowedUserIds.includes(userId)) return;
144
202
 
145
203
  const chatId = ctx.chat.id;
146
- const sessionId = sessions[chatId];
204
+ const slot = getActiveSlot(chatId);
205
+ const sessionId = getSessionId(chatId, slot);
206
+ const key = slotKey(chatId, slot);
147
207
 
148
- if (!isRunning.has(chatId) || !sessionId) {
208
+ if (!isRunning.has(key) || !sessionId) {
149
209
  await ctx.reply('Nothing is currently running.');
150
210
  return;
151
211
  }
@@ -160,24 +220,155 @@ export async function startTelegramChannel(config) {
160
220
  if (!allowedUserIds.includes(userId)) return;
161
221
 
162
222
  const chatId = ctx.chat.id;
163
- pendingMessages.delete(chatId);
164
- if (sessions[chatId]) {
165
- await appendTelegramChatLog(chatId, sessions[chatId], 'SYSTEM', '--- /new: session reset ---');
166
- delete sessions[chatId];
167
- save(sessions);
168
- console.log(`[telegram] session unlinked chat_id=${chatId}`);
223
+ const slot = getActiveSlot(chatId);
224
+ const key = slotKey(chatId, slot);
225
+ pendingMessages.delete(key);
226
+ const oldSessionId = getSessionId(chatId, slot);
227
+ if (oldSessionId) {
228
+ await appendTelegramChatLog(chatId, oldSessionId, 'SYSTEM', '--- /new: session reset ---');
229
+ setSessionId(chatId, slot, null);
230
+ console.log(`[telegram] session unlinked chat_id=${chatId} slot=${slot}`);
231
+ }
232
+
233
+ await ctx.reply(`New session started on slot ${slot}.`);
234
+ });
235
+
236
+ function buildSlotsDisplay(chatId) {
237
+ const d = sessions[chatId];
238
+ const activeSlot = getActiveSlot(chatId);
239
+
240
+ const slotsMap = {};
241
+ if (typeof d === 'string') {
242
+ slotsMap['1'] = d;
243
+ } else if (d && d.slots) {
244
+ Object.assign(slotsMap, d.slots);
245
+ }
246
+
247
+ const slotNums = [...new Set(['1', ...Object.keys(slotsMap)])].sort((a, b) => Number(a) - Number(b));
248
+ const maxSlot = Math.max(...slotNums.map(Number));
249
+ const nextSlot = maxSlot + 1;
250
+
251
+ // Status text
252
+ const lines = ['<b>Slots:</b>'];
253
+ for (const sn of slotNums) {
254
+ const n = Number(sn);
255
+ const sid = slotsMap[sn] ?? null;
256
+ const key = slotKey(chatId, n);
257
+ const activeMarker = n === activeSlot ? ' ← aktiv' : '';
258
+ let statusIcon;
259
+ if (isRunning.has(key)) {
260
+ const startTime = runStartTimes.get(key);
261
+ let elapsed = '';
262
+ if (startTime) {
263
+ const secs = Math.floor((Date.now() - startTime.getTime()) / 1000);
264
+ const m = Math.floor(secs / 60);
265
+ const s = secs % 60;
266
+ elapsed = m > 0 ? ` (seit ${m}m ${s}s)` : ` (seit ${s}s)`;
267
+ }
268
+ statusIcon = `🟢 läuft${elapsed}`;
269
+ } else if (sid) {
270
+ statusIcon = '💬 bereit';
271
+ } else {
272
+ statusIcon = '➕ leer';
273
+ }
274
+ lines.push(`Slot ${n}: ${statusIcon}${activeMarker}`);
275
+ }
276
+ if (!isRunning.has(slotKey(chatId, nextSlot)) && !slotsMap[String(nextSlot)]) {
277
+ lines.push(`Slot ${nextSlot}: ➕ leer`);
278
+ }
279
+
280
+ // Inline keyboard
281
+ const kb = new InlineKeyboard();
282
+ for (const sn of slotNums) {
283
+ const n = Number(sn);
284
+ const sid = slotsMap[sn] ?? null;
285
+ const key = slotKey(chatId, n);
286
+ const running = isRunning.has(key);
287
+ if (n === activeSlot) {
288
+ kb.text(`✓ Slot ${n} (aktiv)`, `slots_noop`);
289
+ } else {
290
+ kb.text(`↩️ Slot ${n}`, `slots_switch_${n}`);
291
+ }
292
+ if (sid && !running) {
293
+ kb.text(`🗑️`, `slots_del_${n}`);
294
+ }
295
+ kb.row();
296
+ }
297
+ // Button for the next empty slot
298
+ kb.text(`➕ Slot ${nextSlot} (neu)`, `slots_switch_${nextSlot}`);
299
+
300
+ return { text: lines.join('\n'), keyboard: kb };
301
+ }
302
+
303
+ bot.command('slots', async (ctx) => {
304
+ const userId = ctx.from?.id;
305
+ if (!allowedUserIds.includes(userId)) return;
306
+
307
+ const chatId = ctx.chat.id;
308
+ const { text, keyboard } = buildSlotsDisplay(chatId);
309
+ await ctx.reply(text, { parse_mode: 'HTML', reply_markup: keyboard });
310
+ });
311
+
312
+ bot.callbackQuery(/^slots_switch_(\d+)$/, async (ctx) => {
313
+ const userId = ctx.from?.id;
314
+ if (!allowedUserIds.includes(userId)) { await ctx.answerCallbackQuery(); return; }
315
+
316
+ const chatId = ctx.chat.id;
317
+ const n = parseInt(ctx.match[1], 10);
318
+ setActiveSlot(chatId, n);
319
+ const key = slotKey(chatId, n);
320
+ const sid = getSessionId(chatId, n);
321
+ let status;
322
+ if (isRunning.has(key)) status = '🟢 läuft';
323
+ else if (sid) status = '💬 bereit';
324
+ else status = '➕ leer (neue Session beim nächsten Message)';
325
+
326
+ const { text, keyboard } = buildSlotsDisplay(chatId);
327
+ await ctx.editMessageText(text, { parse_mode: 'HTML', reply_markup: keyboard });
328
+ await ctx.answerCallbackQuery(`Slot ${n} aktiv — ${status}`);
329
+ });
330
+
331
+ bot.callbackQuery(/^slots_del_(\d+)$/, async (ctx) => {
332
+ const userId = ctx.from?.id;
333
+ if (!allowedUserIds.includes(userId)) { await ctx.answerCallbackQuery(); return; }
334
+
335
+ const chatId = ctx.chat.id;
336
+ const n = parseInt(ctx.match[1], 10);
337
+ const key = slotKey(chatId, n);
338
+
339
+ if (isRunning.has(key) || pendingMessages.has(key)) {
340
+ await ctx.answerCallbackQuery(`Slot ${n} läuft gerade — erst /stop`);
341
+ return;
342
+ }
343
+
344
+ const oldSid = getSessionId(chatId, n);
345
+ if (oldSid) {
346
+ await appendTelegramChatLog(chatId, oldSid, 'SYSTEM', `--- slot del ${n} (via keyboard) ---`);
169
347
  }
348
+ setSessionId(chatId, n, null);
349
+ pendingMessages.delete(key);
350
+ runStartTimes.delete(key);
351
+
352
+ if (getActiveSlot(chatId) === n) {
353
+ setActiveSlot(chatId, 1);
354
+ }
355
+
356
+ const { text, keyboard } = buildSlotsDisplay(chatId);
357
+ await ctx.editMessageText(text, { parse_mode: 'HTML', reply_markup: keyboard });
358
+ await ctx.answerCallbackQuery(`Slot ${n} gelöscht`);
359
+ });
170
360
 
171
- await ctx.reply('New session started.');
361
+ bot.callbackQuery('slots_noop', async (ctx) => {
362
+ await ctx.answerCallbackQuery();
172
363
  });
173
364
 
174
365
  // Runs one or more batches until the pending queue is drained.
175
366
  // Each iteration takes all currently pending messages, merges them into a
176
367
  // single user turn, calls handleChat once, and sends one response.
177
- async function processQueue(api, chatId, firstBatch) {
368
+ async function processQueue(api, chatId, slot, firstBatch) {
178
369
  let batch = firstBatch;
179
370
  while (batch.length > 0) {
180
- const sessionId = sessions[chatId] || null;
371
+ const sessionId = getSessionId(chatId, slot) || null;
181
372
  const combinedText = batch.length === 1
182
373
  ? batch[0].text
183
374
  : batch.map(m => m.text).join('\n\n');
@@ -213,28 +404,30 @@ export async function startTelegramChannel(config) {
213
404
 
214
405
  let lastCheckpointSent = null;
215
406
  let result;
407
+ const key = slotKey(chatId, slot);
216
408
  try {
217
409
  result = await handleChat(config, sessionId, userText, allAttachments, async (checkpointResponse) => {
218
- const text = typeof checkpointResponse === 'string' ? checkpointResponse : JSON.stringify(checkpointResponse);
219
- lastCheckpointSent = text;
220
- await appendTelegramChatLog(chatId, sessions[chatId] || null, 'JARVIS', text);
221
- await sendMessage(api, chatId, text, sessions[chatId] || null);
410
+ const rawText = typeof checkpointResponse === 'string' ? checkpointResponse : JSON.stringify(checkpointResponse);
411
+ const currentActive = getActiveSlot(chatId);
412
+ const prefixed = slot !== currentActive ? `[Slot ${slot}] ${rawText}` : rawText;
413
+ lastCheckpointSent = prefixed;
414
+ await appendTelegramChatLog(chatId, getSessionId(chatId, slot) || null, 'JARVIS', prefixed);
415
+ await sendMessage(api, chatId, prefixed, getSessionId(chatId, slot) || null);
222
416
  });
223
417
  } catch (e) {
224
- console.error(`[telegram] agent error chat_id=${chatId}: ${e.message}`);
418
+ console.error(`[telegram] agent error chat_id=${chatId} slot=${slot}: ${e.message}`);
225
419
  const errText = e.message
226
420
  ? `Sorry, something went wrong: ${e.message}`
227
421
  : 'Sorry, something went wrong. Please try again.';
228
422
  await api.sendMessage(chatId, errText).catch(() => {});
229
- batch = pendingMessages.get(chatId) || [];
230
- pendingMessages.delete(chatId);
423
+ batch = pendingMessages.get(key) || [];
424
+ pendingMessages.delete(key);
231
425
  continue;
232
426
  }
233
427
 
234
- if (!sessions[chatId]) {
235
- sessions[chatId] = result.sessionId;
236
- save(sessions);
237
- console.log(`[telegram] session created sessionId=${result.sessionId.slice(0, 8)}`);
428
+ if (!getSessionId(chatId, slot)) {
429
+ setSessionId(chatId, slot, result.sessionId);
430
+ console.log(`[telegram] session created slot=${slot} sessionId=${result.sessionId.slice(0, 8)}`);
238
431
  }
239
432
 
240
433
  // Log each original message individually with its own timestamp
@@ -248,24 +441,27 @@ export async function startTelegramChannel(config) {
248
441
  : result.response != null ? JSON.stringify(result.response, null, 2) : '';
249
442
  const text = rawResponse.trim()
250
443
  || 'The agent encountered an error and could not produce a response. Please try again.';
444
+ // Prefix response with slot number if the user has switched away from this slot
445
+ const currentActive = getActiveSlot(chatId);
446
+ const displayText = slot !== currentActive ? `[Slot ${slot}] ${text}` : text;
251
447
  // Skip sending if this response was already sent as a checkpoint update —
252
448
  // intervention_required and zero-progress reuse the last checkpoint response
253
449
  // as their finalResponse, which would otherwise cause a duplicate message.
254
- if (text !== lastCheckpointSent) {
255
- await appendTelegramChatLog(chatId, result.sessionId, 'JARVIS', text);
256
- await sendMessage(api, chatId, text, result.sessionId);
257
- console.log(`[telegram] response sent chat_id=${chatId} length=${text.length}`);
450
+ if (displayText !== lastCheckpointSent) {
451
+ await appendTelegramChatLog(chatId, result.sessionId, 'JARVIS', displayText);
452
+ await sendMessage(api, chatId, displayText, result.sessionId);
453
+ console.log(`[telegram] response sent chat_id=${chatId} slot=${slot} length=${displayText.length}`);
258
454
  } else {
259
- console.log(`[telegram] skipped duplicate final response chat_id=${chatId}`);
455
+ console.log(`[telegram] skipped duplicate final response chat_id=${chatId} slot=${slot}`);
260
456
  }
261
457
  } catch (e) {
262
- console.error(`[telegram] delivery error chat_id=${chatId}: ${e.message}`);
458
+ console.error(`[telegram] delivery error chat_id=${chatId} slot=${slot}: ${e.message}`);
263
459
  await api.sendMessage(chatId, 'Sorry, something went wrong sending the response. Please try again.').catch(() => {});
264
460
  }
265
461
 
266
462
  // Drain any messages that arrived while we were running
267
- batch = pendingMessages.get(chatId) || [];
268
- pendingMessages.delete(chatId);
463
+ batch = pendingMessages.get(key) || [];
464
+ pendingMessages.delete(key);
269
465
  }
270
466
  }
271
467
 
@@ -296,25 +492,29 @@ export async function startTelegramChannel(config) {
296
492
  }
297
493
 
298
494
  const entry = { text: ctx.message.caption || '', attachments: [attachment], ts };
495
+ const slot = getActiveSlot(chatId);
496
+ const key = slotKey(chatId, slot);
299
497
 
300
- if (isRunning.has(chatId)) {
301
- if (!pendingMessages.has(chatId)) pendingMessages.set(chatId, []);
302
- pendingMessages.get(chatId).push(entry);
303
- console.log(`[telegram] buffered photo chat_id=${chatId} pending=${pendingMessages.get(chatId).length}`);
498
+ if (isRunning.has(key)) {
499
+ if (!pendingMessages.has(key)) pendingMessages.set(key, []);
500
+ pendingMessages.get(key).push(entry);
501
+ console.log(`[telegram] buffered photo chat_id=${chatId} slot=${slot} pending=${pendingMessages.get(key).length}`);
304
502
  return;
305
503
  }
306
504
 
307
- isRunning.add(chatId);
505
+ isRunning.add(key);
506
+ runStartTimes.set(key, new Date());
308
507
  await ctx.api.sendChatAction(chatId, 'typing');
309
508
  const typingInterval = setInterval(() => {
310
509
  ctx.api.sendChatAction(chatId, 'typing').catch(() => {});
311
510
  }, 4000);
312
511
 
313
512
  try {
314
- await processQueue(ctx.api, chatId, [entry]);
513
+ await processQueue(ctx.api, chatId, slot, [entry]);
315
514
  } finally {
316
515
  clearInterval(typingInterval);
317
- isRunning.delete(chatId);
516
+ isRunning.delete(key);
517
+ runStartTimes.delete(key);
318
518
  }
319
519
  });
320
520
 
@@ -327,26 +527,30 @@ export async function startTelegramChannel(config) {
327
527
  const chatId = ctx.chat.id;
328
528
  const ts = new Date().toISOString();
329
529
  const entry = { text: ctx.message.text, attachments: [], ts };
530
+ const slot = getActiveSlot(chatId);
531
+ const key = slotKey(chatId, slot);
330
532
 
331
- if (isRunning.has(chatId)) {
332
- if (!pendingMessages.has(chatId)) pendingMessages.set(chatId, []);
333
- pendingMessages.get(chatId).push(entry);
334
- console.log(`[telegram] buffered message chat_id=${chatId} pending=${pendingMessages.get(chatId).length}`);
533
+ if (isRunning.has(key)) {
534
+ if (!pendingMessages.has(key)) pendingMessages.set(key, []);
535
+ pendingMessages.get(key).push(entry);
536
+ console.log(`[telegram] buffered message chat_id=${chatId} slot=${slot} pending=${pendingMessages.get(key).length}`);
335
537
  return;
336
538
  }
337
539
 
338
- isRunning.add(chatId);
339
- console.log(`[telegram] incoming chat_id=${chatId}`);
540
+ isRunning.add(key);
541
+ runStartTimes.set(key, new Date());
542
+ console.log(`[telegram] incoming chat_id=${chatId} slot=${slot}`);
340
543
  await ctx.api.sendChatAction(chatId, 'typing');
341
544
  const typingInterval = setInterval(() => {
342
545
  ctx.api.sendChatAction(chatId, 'typing').catch(() => {});
343
546
  }, 4000);
344
547
 
345
548
  try {
346
- await processQueue(ctx.api, chatId, [entry]);
549
+ await processQueue(ctx.api, chatId, slot, [entry]);
347
550
  } finally {
348
551
  clearInterval(typingInterval);
349
- isRunning.delete(chatId);
552
+ isRunning.delete(key);
553
+ runStartTimes.delete(key);
350
554
  }
351
555
  });
352
556
 
@@ -543,7 +543,10 @@ const SEED_TOOLS = {
543
543
  try {
544
544
  const tgSessionsFile = path.join(process.env.HOME, '.jarvis/data/channels/telegram/sessions.json');
545
545
  const tgSessions = JSON.parse(await fs.promises.readFile(tgSessionsFile, 'utf8').catch(() => '{}'));
546
- const sessionId = tgSessions[String(chatId)];
546
+ const chatData = tgSessions[String(chatId)];
547
+ const sessionId = typeof chatData === 'string'
548
+ ? chatData
549
+ : chatData?.slots?.[String(chatData?.active ?? 1)] ?? null;
547
550
  const prefix = sessionId ? String(sessionId).slice(0, 8) : 'unknown';
548
551
  const logDir = path.join(process.env.HOME, '.jarvis/telegram-chats');
549
552
  const logFile = path.join(logDir, String(chatId) + '-' + prefix + '.log');