@ducci/jarvis 1.0.77 → 1.0.78

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.78",
4
4
  "description": "A fully automated agent system that lives on a server.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -98,15 +98,73 @@ 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' },
167
+ { command: 'slot', description: 'Switch or delete a slot: /slot 2 or /slot del 2' },
110
168
  ]);
111
169
 
112
170
  bot.command('usage', async (ctx) => {
@@ -114,7 +172,8 @@ export async function startTelegramChannel(config) {
114
172
  if (!allowedUserIds.includes(userId)) return;
115
173
 
116
174
  const chatId = ctx.chat.id;
117
- const sessionId = sessions[chatId];
175
+ const slot = getActiveSlot(chatId);
176
+ const sessionId = getSessionId(chatId, slot);
118
177
  if (!sessionId) {
119
178
  await ctx.reply('No active session. Send a message to start one.');
120
179
  return;
@@ -134,7 +193,7 @@ export async function startTelegramChannel(config) {
134
193
  ? `\nCache read: ${cacheRead.toLocaleString()}\nCache written: ${cacheCreation.toLocaleString()}`
135
194
  : '';
136
195
  await ctx.reply(
137
- `Token usage for current session:\nIn: ${u.prompt.toLocaleString()}\nOut: ${u.completion.toLocaleString()}\nTotal: ${total.toLocaleString()}${cacheLines}`
196
+ `Token usage for slot ${slot}:\nIn: ${u.prompt.toLocaleString()}\nOut: ${u.completion.toLocaleString()}\nTotal: ${total.toLocaleString()}${cacheLines}`
138
197
  );
139
198
  });
140
199
 
@@ -143,9 +202,11 @@ export async function startTelegramChannel(config) {
143
202
  if (!allowedUserIds.includes(userId)) return;
144
203
 
145
204
  const chatId = ctx.chat.id;
146
- const sessionId = sessions[chatId];
205
+ const slot = getActiveSlot(chatId);
206
+ const sessionId = getSessionId(chatId, slot);
207
+ const key = slotKey(chatId, slot);
147
208
 
148
- if (!isRunning.has(chatId) || !sessionId) {
209
+ if (!isRunning.has(key) || !sessionId) {
149
210
  await ctx.reply('Nothing is currently running.');
150
211
  return;
151
212
  }
@@ -160,24 +221,127 @@ export async function startTelegramChannel(config) {
160
221
  if (!allowedUserIds.includes(userId)) return;
161
222
 
162
223
  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}`);
224
+ const slot = getActiveSlot(chatId);
225
+ const key = slotKey(chatId, slot);
226
+ pendingMessages.delete(key);
227
+ const oldSessionId = getSessionId(chatId, slot);
228
+ if (oldSessionId) {
229
+ await appendTelegramChatLog(chatId, oldSessionId, 'SYSTEM', '--- /new: session reset ---');
230
+ setSessionId(chatId, slot, null);
231
+ console.log(`[telegram] session unlinked chat_id=${chatId} slot=${slot}`);
232
+ }
233
+
234
+ await ctx.reply(`New session started on slot ${slot}.`);
235
+ });
236
+
237
+ bot.command('slots', async (ctx) => {
238
+ const userId = ctx.from?.id;
239
+ if (!allowedUserIds.includes(userId)) return;
240
+
241
+ const chatId = ctx.chat.id;
242
+ const d = sessions[chatId];
243
+ const activeSlot = getActiveSlot(chatId);
244
+
245
+ const slotsMap = {};
246
+ if (typeof d === 'string') {
247
+ slotsMap['1'] = d;
248
+ } else if (d && d.slots) {
249
+ Object.assign(slotsMap, d.slots);
169
250
  }
170
251
 
171
- await ctx.reply('New session started.');
252
+ const slotNums = [...new Set(['1', ...Object.keys(slotsMap)])].sort((a, b) => Number(a) - Number(b));
253
+
254
+ const lines = ['<b>Slots:</b>'];
255
+ for (const sn of slotNums) {
256
+ const n = Number(sn);
257
+ const sid = slotsMap[sn] ?? null;
258
+ const key = slotKey(chatId, n);
259
+ const activeMarker = n === activeSlot ? ' ← aktiv' : '';
260
+ let statusIcon;
261
+ if (isRunning.has(key)) {
262
+ const startTime = runStartTimes.get(key);
263
+ let elapsed = '';
264
+ if (startTime) {
265
+ const secs = Math.floor((Date.now() - startTime.getTime()) / 1000);
266
+ const m = Math.floor(secs / 60);
267
+ const s = secs % 60;
268
+ elapsed = m > 0 ? ` (seit ${m}m ${s}s)` : ` (seit ${s}s)`;
269
+ }
270
+ statusIcon = `🟢 läuft${elapsed}`;
271
+ } else if (sid) {
272
+ statusIcon = '💬 bereit';
273
+ } else {
274
+ statusIcon = '➕ leer';
275
+ }
276
+ lines.push(`Slot ${n}: ${statusIcon}${activeMarker}`);
277
+ }
278
+
279
+ // Always show one empty slot beyond the highest existing one
280
+ const maxSlot = Math.max(...slotNums.map(Number));
281
+ const nextSlot = maxSlot + 1;
282
+ if (!isRunning.has(slotKey(chatId, nextSlot)) && !slotsMap[String(nextSlot)]) {
283
+ lines.push(`Slot ${nextSlot}: ➕ leer`);
284
+ }
285
+
286
+ await ctx.reply(lines.join('\n'), { parse_mode: 'HTML' });
287
+ });
288
+
289
+ bot.command('slot', async (ctx) => {
290
+ const userId = ctx.from?.id;
291
+ if (!allowedUserIds.includes(userId)) return;
292
+
293
+ const chatId = ctx.chat.id;
294
+ const args = (ctx.match || '').trim().split(/\s+/).filter(Boolean);
295
+
296
+ // /slot del N
297
+ if (args[0] === 'del') {
298
+ const n = parseInt(args[1], 10);
299
+ if (!n || n < 1) { await ctx.reply('Usage: /slot del <number>'); return; }
300
+ const key = slotKey(chatId, n);
301
+ if (isRunning.has(key) || pendingMessages.has(key)) {
302
+ await ctx.reply(`Slot ${n} ist gerade aktiv. Erst /stop, dann löschen.`);
303
+ return;
304
+ }
305
+ const oldSid = getSessionId(chatId, n);
306
+ if (oldSid) {
307
+ await appendTelegramChatLog(chatId, oldSid, 'SYSTEM', `--- /slot del ${n} ---`);
308
+ }
309
+ setSessionId(chatId, n, null);
310
+ pendingMessages.delete(key);
311
+ runStartTimes.delete(key);
312
+ if (getActiveSlot(chatId) === n) {
313
+ setActiveSlot(chatId, 1);
314
+ await ctx.reply(`Slot ${n} gelöscht. Zu Slot 1 gewechselt.`);
315
+ } else {
316
+ await ctx.reply(`Slot ${n} gelöscht.`);
317
+ }
318
+ return;
319
+ }
320
+
321
+ // /slot N — switch active slot
322
+ const n = parseInt(args[0], 10);
323
+ if (!n || n < 1) { await ctx.reply('Usage: /slot <number> oder /slot del <number>'); return; }
324
+ setActiveSlot(chatId, n);
325
+ const sid = getSessionId(chatId, n);
326
+ const key = slotKey(chatId, n);
327
+ let status;
328
+ if (isRunning.has(key)) {
329
+ status = '🟢 läuft';
330
+ } else if (sid) {
331
+ status = '💬 bereit (vorhandene Session)';
332
+ } else {
333
+ status = '➕ leer (neue Session beim nächsten Message)';
334
+ }
335
+ await ctx.reply(`Slot ${n} ist jetzt aktiv. Status: ${status}`);
172
336
  });
173
337
 
174
338
  // Runs one or more batches until the pending queue is drained.
175
339
  // Each iteration takes all currently pending messages, merges them into a
176
340
  // single user turn, calls handleChat once, and sends one response.
177
- async function processQueue(api, chatId, firstBatch) {
341
+ async function processQueue(api, chatId, slot, firstBatch) {
178
342
  let batch = firstBatch;
179
343
  while (batch.length > 0) {
180
- const sessionId = sessions[chatId] || null;
344
+ const sessionId = getSessionId(chatId, slot) || null;
181
345
  const combinedText = batch.length === 1
182
346
  ? batch[0].text
183
347
  : batch.map(m => m.text).join('\n\n');
@@ -213,28 +377,30 @@ export async function startTelegramChannel(config) {
213
377
 
214
378
  let lastCheckpointSent = null;
215
379
  let result;
380
+ const key = slotKey(chatId, slot);
216
381
  try {
217
382
  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);
383
+ const rawText = typeof checkpointResponse === 'string' ? checkpointResponse : JSON.stringify(checkpointResponse);
384
+ const currentActive = getActiveSlot(chatId);
385
+ const prefixed = slot !== currentActive ? `[Slot ${slot}] ${rawText}` : rawText;
386
+ lastCheckpointSent = prefixed;
387
+ await appendTelegramChatLog(chatId, getSessionId(chatId, slot) || null, 'JARVIS', prefixed);
388
+ await sendMessage(api, chatId, prefixed, getSessionId(chatId, slot) || null);
222
389
  });
223
390
  } catch (e) {
224
- console.error(`[telegram] agent error chat_id=${chatId}: ${e.message}`);
391
+ console.error(`[telegram] agent error chat_id=${chatId} slot=${slot}: ${e.message}`);
225
392
  const errText = e.message
226
393
  ? `Sorry, something went wrong: ${e.message}`
227
394
  : 'Sorry, something went wrong. Please try again.';
228
395
  await api.sendMessage(chatId, errText).catch(() => {});
229
- batch = pendingMessages.get(chatId) || [];
230
- pendingMessages.delete(chatId);
396
+ batch = pendingMessages.get(key) || [];
397
+ pendingMessages.delete(key);
231
398
  continue;
232
399
  }
233
400
 
234
- if (!sessions[chatId]) {
235
- sessions[chatId] = result.sessionId;
236
- save(sessions);
237
- console.log(`[telegram] session created sessionId=${result.sessionId.slice(0, 8)}`);
401
+ if (!getSessionId(chatId, slot)) {
402
+ setSessionId(chatId, slot, result.sessionId);
403
+ console.log(`[telegram] session created slot=${slot} sessionId=${result.sessionId.slice(0, 8)}`);
238
404
  }
239
405
 
240
406
  // Log each original message individually with its own timestamp
@@ -248,24 +414,27 @@ export async function startTelegramChannel(config) {
248
414
  : result.response != null ? JSON.stringify(result.response, null, 2) : '';
249
415
  const text = rawResponse.trim()
250
416
  || 'The agent encountered an error and could not produce a response. Please try again.';
417
+ // Prefix response with slot number if the user has switched away from this slot
418
+ const currentActive = getActiveSlot(chatId);
419
+ const displayText = slot !== currentActive ? `[Slot ${slot}] ${text}` : text;
251
420
  // Skip sending if this response was already sent as a checkpoint update —
252
421
  // intervention_required and zero-progress reuse the last checkpoint response
253
422
  // 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}`);
423
+ if (displayText !== lastCheckpointSent) {
424
+ await appendTelegramChatLog(chatId, result.sessionId, 'JARVIS', displayText);
425
+ await sendMessage(api, chatId, displayText, result.sessionId);
426
+ console.log(`[telegram] response sent chat_id=${chatId} slot=${slot} length=${displayText.length}`);
258
427
  } else {
259
- console.log(`[telegram] skipped duplicate final response chat_id=${chatId}`);
428
+ console.log(`[telegram] skipped duplicate final response chat_id=${chatId} slot=${slot}`);
260
429
  }
261
430
  } catch (e) {
262
- console.error(`[telegram] delivery error chat_id=${chatId}: ${e.message}`);
431
+ console.error(`[telegram] delivery error chat_id=${chatId} slot=${slot}: ${e.message}`);
263
432
  await api.sendMessage(chatId, 'Sorry, something went wrong sending the response. Please try again.').catch(() => {});
264
433
  }
265
434
 
266
435
  // Drain any messages that arrived while we were running
267
- batch = pendingMessages.get(chatId) || [];
268
- pendingMessages.delete(chatId);
436
+ batch = pendingMessages.get(key) || [];
437
+ pendingMessages.delete(key);
269
438
  }
270
439
  }
271
440
 
@@ -296,25 +465,29 @@ export async function startTelegramChannel(config) {
296
465
  }
297
466
 
298
467
  const entry = { text: ctx.message.caption || '', attachments: [attachment], ts };
468
+ const slot = getActiveSlot(chatId);
469
+ const key = slotKey(chatId, slot);
299
470
 
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}`);
471
+ if (isRunning.has(key)) {
472
+ if (!pendingMessages.has(key)) pendingMessages.set(key, []);
473
+ pendingMessages.get(key).push(entry);
474
+ console.log(`[telegram] buffered photo chat_id=${chatId} slot=${slot} pending=${pendingMessages.get(key).length}`);
304
475
  return;
305
476
  }
306
477
 
307
- isRunning.add(chatId);
478
+ isRunning.add(key);
479
+ runStartTimes.set(key, new Date());
308
480
  await ctx.api.sendChatAction(chatId, 'typing');
309
481
  const typingInterval = setInterval(() => {
310
482
  ctx.api.sendChatAction(chatId, 'typing').catch(() => {});
311
483
  }, 4000);
312
484
 
313
485
  try {
314
- await processQueue(ctx.api, chatId, [entry]);
486
+ await processQueue(ctx.api, chatId, slot, [entry]);
315
487
  } finally {
316
488
  clearInterval(typingInterval);
317
- isRunning.delete(chatId);
489
+ isRunning.delete(key);
490
+ runStartTimes.delete(key);
318
491
  }
319
492
  });
320
493
 
@@ -327,26 +500,30 @@ export async function startTelegramChannel(config) {
327
500
  const chatId = ctx.chat.id;
328
501
  const ts = new Date().toISOString();
329
502
  const entry = { text: ctx.message.text, attachments: [], ts };
503
+ const slot = getActiveSlot(chatId);
504
+ const key = slotKey(chatId, slot);
330
505
 
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}`);
506
+ if (isRunning.has(key)) {
507
+ if (!pendingMessages.has(key)) pendingMessages.set(key, []);
508
+ pendingMessages.get(key).push(entry);
509
+ console.log(`[telegram] buffered message chat_id=${chatId} slot=${slot} pending=${pendingMessages.get(key).length}`);
335
510
  return;
336
511
  }
337
512
 
338
- isRunning.add(chatId);
339
- console.log(`[telegram] incoming chat_id=${chatId}`);
513
+ isRunning.add(key);
514
+ runStartTimes.set(key, new Date());
515
+ console.log(`[telegram] incoming chat_id=${chatId} slot=${slot}`);
340
516
  await ctx.api.sendChatAction(chatId, 'typing');
341
517
  const typingInterval = setInterval(() => {
342
518
  ctx.api.sendChatAction(chatId, 'typing').catch(() => {});
343
519
  }, 4000);
344
520
 
345
521
  try {
346
- await processQueue(ctx.api, chatId, [entry]);
522
+ await processQueue(ctx.api, chatId, slot, [entry]);
347
523
  } finally {
348
524
  clearInterval(typingInterval);
349
- isRunning.delete(chatId);
525
+ isRunning.delete(key);
526
+ runStartTimes.delete(key);
350
527
  }
351
528
  });
352
529
 
@@ -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');