@ducci/jarvis 1.0.64 → 1.0.65

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.64",
3
+ "version": "1.0.65",
4
4
  "description": "A fully automated agent system that lives on a server.",
5
5
  "main": "./src/index.js",
6
6
  "type": "module",
@@ -58,6 +58,11 @@ export async function startTelegramChannel(config) {
58
58
  const bot = new Bot(token);
59
59
  const sessions = load();
60
60
 
61
+ // Tracks chats with an active agent run and buffers messages arriving during that run.
62
+ // When the run finishes all buffered messages are merged into one combined run.
63
+ const isRunning = new Set();
64
+ const pendingMessages = new Map(); // chatId -> [{text, attachments, ts}]
65
+
61
66
  await bot.api.setMyCommands([
62
67
  { command: 'new', description: 'Start a fresh session' },
63
68
  { command: 'usage', description: 'Show token usage for the current session' },
@@ -97,6 +102,7 @@ export async function startTelegramChannel(config) {
97
102
  if (!allowedUserIds.includes(userId)) return;
98
103
 
99
104
  const chatId = ctx.chat.id;
105
+ pendingMessages.delete(chatId);
100
106
  if (sessions[chatId]) {
101
107
  await appendTelegramChatLog(chatId, sessions[chatId], 'SYSTEM', '--- /new: session reset ---');
102
108
  delete sessions[chatId];
@@ -107,22 +113,74 @@ export async function startTelegramChannel(config) {
107
113
  await ctx.reply('New session started.');
108
114
  });
109
115
 
116
+ // Runs one or more batches until the pending queue is drained.
117
+ // Each iteration takes all currently pending messages, merges them into a
118
+ // single user turn, calls handleChat once, and sends one response.
119
+ async function processQueue(api, chatId, firstBatch) {
120
+ let batch = firstBatch;
121
+ while (batch.length > 0) {
122
+ const sessionId = sessions[chatId] || null;
123
+ const combinedText = batch.length === 1
124
+ ? batch[0].text
125
+ : batch.map(m => m.text).join('\n\n');
126
+ const allAttachments = batch.flatMap(m => m.attachments);
127
+
128
+ let result;
129
+ try {
130
+ result = await handleChat(config, sessionId, combinedText, allAttachments);
131
+ } catch (e) {
132
+ console.error(`[telegram] agent error chat_id=${chatId}: ${e.message}`);
133
+ const errText = e.message
134
+ ? `Sorry, something went wrong: ${e.message}`
135
+ : 'Sorry, something went wrong. Please try again.';
136
+ await api.sendMessage(chatId, errText).catch(() => {});
137
+ batch = pendingMessages.get(chatId) || [];
138
+ pendingMessages.delete(chatId);
139
+ continue;
140
+ }
141
+
142
+ if (!sessions[chatId]) {
143
+ sessions[chatId] = result.sessionId;
144
+ save(sessions);
145
+ console.log(`[telegram] session created sessionId=${result.sessionId.slice(0, 8)}`);
146
+ }
147
+
148
+ // Log each original message individually with its own timestamp
149
+ for (const m of batch) {
150
+ await appendTelegramChatLog(chatId, result.sessionId, 'USER', m.text || '[photo]', m.ts);
151
+ }
152
+
153
+ try {
154
+ const rawResponse = typeof result.response === 'string'
155
+ ? result.response
156
+ : result.response != null ? JSON.stringify(result.response, null, 2) : '';
157
+ const text = rawResponse.trim()
158
+ || 'The agent encountered an error and could not produce a response. Please try again.';
159
+ await appendTelegramChatLog(chatId, result.sessionId, 'JARVIS', text);
160
+ await sendMessage(api, chatId, text, result.sessionId);
161
+ console.log(`[telegram] response sent chat_id=${chatId} length=${text.length}`);
162
+ } catch (e) {
163
+ console.error(`[telegram] delivery error chat_id=${chatId}: ${e.message}`);
164
+ await api.sendMessage(chatId, 'Sorry, something went wrong sending the response. Please try again.').catch(() => {});
165
+ }
166
+
167
+ // Drain any messages that arrived while we were running
168
+ batch = pendingMessages.get(chatId) || [];
169
+ pendingMessages.delete(chatId);
170
+ }
171
+ }
172
+
110
173
  bot.on('message:photo', async (ctx) => {
111
174
  const userId = ctx.from?.id;
112
175
  if (!allowedUserIds.includes(userId)) return;
113
176
 
114
177
  const chatId = ctx.chat.id;
115
- const sessionId = sessions[chatId] || null;
178
+ const ts = new Date().toISOString();
116
179
 
117
180
  console.log(`[telegram] incoming photo chat_id=${chatId}`);
118
181
 
119
- await ctx.api.sendChatAction(chatId, 'typing');
120
- const typingInterval = setInterval(() => {
121
- ctx.api.sendChatAction(chatId, 'typing').catch(() => {});
122
- }, 4000);
123
-
124
- const userTs = new Date().toISOString();
125
- let result;
182
+ // Download the photo first regardless of whether we buffer or run immediately
183
+ let attachment;
126
184
  try {
127
185
  const photo = ctx.message.photo.filter(p => p.width <= 800).at(-1)
128
186
  ?? ctx.message.photo[0];
@@ -131,42 +189,33 @@ export async function startTelegramChannel(config) {
131
189
  const imgResponse = await fetch(fileUrl);
132
190
  const buffer = await imgResponse.arrayBuffer();
133
191
  const base64 = Buffer.from(buffer).toString('base64');
134
- const dataUrl = `data:image/jpeg;base64,${base64}`;
135
- const caption = ctx.message.caption || '';
136
- result = await handleChat(config, sessionId, caption, [{ url: dataUrl }]);
192
+ attachment = { url: `data:image/jpeg;base64,${base64}` };
137
193
  } catch (e) {
138
- console.error(`[telegram] agent error chat_id=${chatId}: ${e.message}`);
139
- const errText = e.message
140
- ? `Sorry, something went wrong: ${e.message}`
141
- : 'Sorry, something went wrong. Please try again.';
142
- await ctx.reply(errText).catch(() => {});
143
- clearInterval(typingInterval);
194
+ console.error(`[telegram] photo download error chat_id=${chatId}: ${e.message}`);
195
+ await ctx.reply('Sorry, could not process the photo.').catch(() => {});
144
196
  return;
145
197
  }
146
198
 
147
- if (!sessions[chatId]) {
148
- sessions[chatId] = result.sessionId;
149
- save(sessions);
150
- console.log(`[telegram] session created sessionId=${result.sessionId.slice(0, 8)}`);
199
+ const entry = { text: ctx.message.caption || '', attachments: [attachment], ts };
200
+
201
+ if (isRunning.has(chatId)) {
202
+ if (!pendingMessages.has(chatId)) pendingMessages.set(chatId, []);
203
+ pendingMessages.get(chatId).push(entry);
204
+ console.log(`[telegram] buffered photo chat_id=${chatId} pending=${pendingMessages.get(chatId).length}`);
205
+ return;
151
206
  }
152
207
 
153
- const captionText = ctx.message.caption || '[photo]';
154
- await appendTelegramChatLog(chatId, result.sessionId, 'USER', `[photo] ${captionText}`, userTs);
208
+ isRunning.add(chatId);
209
+ await ctx.api.sendChatAction(chatId, 'typing');
210
+ const typingInterval = setInterval(() => {
211
+ ctx.api.sendChatAction(chatId, 'typing').catch(() => {});
212
+ }, 4000);
155
213
 
156
214
  try {
157
- const rawResponse = typeof result.response === 'string'
158
- ? result.response
159
- : result.response != null ? JSON.stringify(result.response, null, 2) : '';
160
- const text = rawResponse.trim()
161
- || 'The agent encountered an error and could not produce a response. Please try again.';
162
- await appendTelegramChatLog(chatId, result.sessionId, 'JARVIS', text);
163
- await sendMessage(ctx.api, chatId, text, result.sessionId);
164
- console.log(`[telegram] response sent chat_id=${chatId} length=${text.length}`);
165
- } catch (e) {
166
- console.error(`[telegram] delivery error chat_id=${chatId}: ${e.message}`);
167
- await ctx.api.sendMessage(chatId, 'Sorry, something went wrong sending the response. Please try again.').catch(() => {});
215
+ await processQueue(ctx.api, chatId, [entry]);
168
216
  } finally {
169
217
  clearInterval(typingInterval);
218
+ isRunning.delete(chatId);
170
219
  }
171
220
  });
172
221
 
@@ -177,53 +226,28 @@ export async function startTelegramChannel(config) {
177
226
  if (!allowedUserIds.includes(userId)) return;
178
227
 
179
228
  const chatId = ctx.chat.id;
180
- const sessionId = sessions[chatId] || null;
229
+ const ts = new Date().toISOString();
230
+ const entry = { text: ctx.message.text, attachments: [], ts };
181
231
 
182
- console.log(`[telegram] incoming chat_id=${chatId}`);
232
+ if (isRunning.has(chatId)) {
233
+ if (!pendingMessages.has(chatId)) pendingMessages.set(chatId, []);
234
+ pendingMessages.get(chatId).push(entry);
235
+ console.log(`[telegram] buffered message chat_id=${chatId} pending=${pendingMessages.get(chatId).length}`);
236
+ return;
237
+ }
183
238
 
239
+ isRunning.add(chatId);
240
+ console.log(`[telegram] incoming chat_id=${chatId}`);
184
241
  await ctx.api.sendChatAction(chatId, 'typing');
185
242
  const typingInterval = setInterval(() => {
186
243
  ctx.api.sendChatAction(chatId, 'typing').catch(() => {});
187
244
  }, 4000);
188
245
 
189
- const userTs = new Date().toISOString();
190
- let result;
191
- try {
192
- result = await handleChat(config, sessionId, ctx.message.text);
193
- } catch (e) {
194
- console.error(`[telegram] agent error chat_id=${chatId}: ${e.message}`);
195
- const errText = e.message
196
- ? `Sorry, something went wrong: ${e.message}`
197
- : 'Sorry, something went wrong. Please try again.';
198
- await ctx.reply(errText).catch(() => {});
199
- clearInterval(typingInterval);
200
- return;
201
- }
202
-
203
- // Persist new session mapping on first message
204
- if (!sessions[chatId]) {
205
- sessions[chatId] = result.sessionId;
206
- save(sessions);
207
- console.log(`[telegram] session created sessionId=${result.sessionId.slice(0, 8)}`);
208
- }
209
-
210
- await appendTelegramChatLog(chatId, result.sessionId, 'USER', ctx.message.text, userTs);
211
-
212
246
  try {
213
- // Guard against empty or non-string response (e.g. model returns array instead of string)
214
- const rawResponse = typeof result.response === 'string'
215
- ? result.response
216
- : result.response != null ? JSON.stringify(result.response, null, 2) : '';
217
- const text = rawResponse.trim()
218
- || 'The agent encountered an error and could not produce a response. Please try again.';
219
- await appendTelegramChatLog(chatId, result.sessionId, 'JARVIS', text);
220
- await sendMessage(ctx.api, chatId, text, result.sessionId);
221
- console.log(`[telegram] response sent chat_id=${chatId} length=${text.length}`);
222
- } catch (e) {
223
- console.error(`[telegram] delivery error chat_id=${chatId}: ${e.message}`);
224
- await ctx.api.sendMessage(chatId, 'Sorry, something went wrong sending the response. Please try again.').catch(() => {});
247
+ await processQueue(ctx.api, chatId, [entry]);
225
248
  } finally {
226
249
  clearInterval(typingInterval);
250
+ isRunning.delete(chatId);
227
251
  }
228
252
  });
229
253