kernelbot 1.0.24 → 1.0.26

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/src/agent.js CHANGED
@@ -1,18 +1,105 @@
1
- import Anthropic from '@anthropic-ai/sdk';
1
+ import { createProvider, PROVIDERS } from './providers/index.js';
2
2
  import { toolDefinitions, executeTool, checkConfirmation } from './tools/index.js';
3
+ import { selectToolsForMessage, expandToolsForUsed } from './tools/categories.js';
3
4
  import { getSystemPrompt } from './prompts/system.js';
4
5
  import { getLogger } from './utils/logger.js';
5
- import { getMissingCredential, saveCredential } from './utils/config.js';
6
+ import { getMissingCredential, saveCredential, saveProviderToYaml } from './utils/config.js';
7
+
8
+ const MAX_RESULT_LENGTH = 3000;
9
+ const LARGE_FIELDS = ['stdout', 'stderr', 'content', 'diff', 'output', 'body', 'html', 'text', 'log', 'logs'];
6
10
 
7
11
  export class Agent {
8
12
  constructor({ config, conversationManager }) {
9
13
  this.config = config;
10
14
  this.conversationManager = conversationManager;
11
- this.client = new Anthropic({ apiKey: config.anthropic.api_key });
15
+ this.provider = createProvider(config);
12
16
  this.systemPrompt = getSystemPrompt(config);
13
17
  this._pending = new Map(); // chatId -> pending state
14
18
  }
15
19
 
20
+ /** Return current brain info for display. */
21
+ getBrainInfo() {
22
+ const { provider, model } = this.config.brain;
23
+ const providerDef = PROVIDERS[provider];
24
+ const providerName = providerDef ? providerDef.name : provider;
25
+ const modelEntry = providerDef?.models.find((m) => m.id === model);
26
+ const modelLabel = modelEntry ? modelEntry.label : model;
27
+ return { provider, providerName, model, modelLabel };
28
+ }
29
+
30
+ /**
31
+ * Switch to a different provider/model at runtime.
32
+ * Resolves the API key from process.env automatically.
33
+ * Returns null on success, or an error string if the key is missing.
34
+ */
35
+ switchBrain(providerKey, modelId) {
36
+ const logger = getLogger();
37
+ const providerDef = PROVIDERS[providerKey];
38
+ if (!providerDef) return `Unknown provider: ${providerKey}`;
39
+
40
+ const envKey = providerDef.envKey;
41
+ const apiKey = process.env[envKey];
42
+ if (!apiKey) {
43
+ return envKey; // caller handles prompting
44
+ }
45
+
46
+ this.config.brain.provider = providerKey;
47
+ this.config.brain.model = modelId;
48
+ this.config.brain.api_key = apiKey;
49
+
50
+ // Recreate the provider instance
51
+ this.provider = createProvider(this.config);
52
+
53
+ // Persist to config.yaml
54
+ saveProviderToYaml(providerKey, modelId);
55
+
56
+ logger.info(`Brain switched to ${providerDef.name} / ${modelId}`);
57
+ return null;
58
+ }
59
+
60
+ /**
61
+ * Finalize brain switch after API key was provided via chat.
62
+ */
63
+ switchBrainWithKey(providerKey, modelId, apiKey) {
64
+ const logger = getLogger();
65
+ const providerDef = PROVIDERS[providerKey];
66
+
67
+ // Save the key
68
+ saveCredential(this.config, providerDef.envKey, apiKey);
69
+
70
+ this.config.brain.provider = providerKey;
71
+ this.config.brain.model = modelId;
72
+ this.config.brain.api_key = apiKey;
73
+
74
+ this.provider = createProvider(this.config);
75
+ saveProviderToYaml(providerKey, modelId);
76
+
77
+ logger.info(`Brain switched to ${providerDef.name} / ${modelId} (new key saved)`);
78
+ }
79
+
80
+ /**
81
+ * Truncate a tool result to stay within token budget.
82
+ */
83
+ _truncateResult(name, result) {
84
+ let str = JSON.stringify(result);
85
+ if (str.length <= MAX_RESULT_LENGTH) return str;
86
+
87
+ // Try truncating known large fields first
88
+ if (result && typeof result === 'object') {
89
+ const truncated = { ...result };
90
+ for (const field of LARGE_FIELDS) {
91
+ if (typeof truncated[field] === 'string' && truncated[field].length > 500) {
92
+ truncated[field] = truncated[field].slice(0, 500) + `\n... [truncated ${truncated[field].length - 500} chars]`;
93
+ }
94
+ }
95
+ str = JSON.stringify(truncated);
96
+ if (str.length <= MAX_RESULT_LENGTH) return str;
97
+ }
98
+
99
+ // Hard truncate
100
+ return str.slice(0, MAX_RESULT_LENGTH) + `\n... [truncated, total ${str.length} chars]`;
101
+ }
102
+
16
103
  async processMessage(chatId, userMessage, user, onUpdate, sendPhoto) {
17
104
  const logger = getLogger();
18
105
 
@@ -33,15 +120,19 @@ export class Agent {
33
120
  }
34
121
  }
35
122
 
36
- const { max_tool_depth } = this.config.anthropic;
123
+ const { max_tool_depth } = this.config.brain;
37
124
 
38
125
  // Add user message to persistent history
39
126
  this.conversationManager.addMessage(chatId, 'user', userMessage);
40
127
 
41
- // Build working messages from history
42
- const messages = [...this.conversationManager.getHistory(chatId)];
128
+ // Build working messages from compressed history
129
+ const messages = [...this.conversationManager.getSummarizedHistory(chatId)];
43
130
 
44
- return await this._runLoop(chatId, messages, user, 0, max_tool_depth);
131
+ // Select relevant tools based on user message
132
+ const tools = selectToolsForMessage(userMessage, toolDefinitions);
133
+ logger.debug(`Selected ${tools.length}/${toolDefinitions.length} tools for message`);
134
+
135
+ return await this._runLoop(chatId, messages, user, 0, max_tool_depth, tools);
45
136
  }
46
137
 
47
138
  _formatToolSummary(name, input) {
@@ -92,7 +183,7 @@ export class Agent {
92
183
  pending.toolResults.push({
93
184
  type: 'tool_result',
94
185
  tool_use_id: pending.block.id,
95
- content: JSON.stringify({ error: `${pending.credential.label} not provided. Operation skipped.` }),
186
+ content: this._truncateResult(pending.block.name, { error: `${pending.credential.label} not provided. Operation skipped.` }),
96
187
  });
97
188
  return await this._resumeAfterPause(chatId, user, pending);
98
189
  }
@@ -112,7 +203,7 @@ export class Agent {
112
203
  pending.toolResults.push({
113
204
  type: 'tool_result',
114
205
  tool_use_id: pending.block.id,
115
- content: JSON.stringify(result),
206
+ content: this._truncateResult(pending.block.name, result),
116
207
  });
117
208
 
118
209
  return await this._resumeAfterPause(chatId, user, pending);
@@ -129,14 +220,14 @@ export class Agent {
129
220
  pending.toolResults.push({
130
221
  type: 'tool_result',
131
222
  tool_use_id: pending.block.id,
132
- content: JSON.stringify(result),
223
+ content: this._truncateResult(pending.block.name, result),
133
224
  });
134
225
  } else {
135
226
  logger.info(`User denied dangerous tool: ${pending.block.name}`);
136
227
  pending.toolResults.push({
137
228
  type: 'tool_result',
138
229
  tool_use_id: pending.block.id,
139
- content: JSON.stringify({ error: 'User denied this operation.' }),
230
+ content: this._truncateResult(pending.block.name, { error: 'User denied this operation.' }),
140
231
  });
141
232
  }
142
233
 
@@ -155,13 +246,13 @@ export class Agent {
155
246
  pending.toolResults.push({
156
247
  type: 'tool_result',
157
248
  tool_use_id: block.id,
158
- content: JSON.stringify(r),
249
+ content: this._truncateResult(block.name, r),
159
250
  });
160
251
  }
161
252
 
162
253
  pending.messages.push({ role: 'user', content: pending.toolResults });
163
- const { max_tool_depth } = this.config.anthropic;
164
- return await this._runLoop(chatId, pending.messages, user, 0, max_tool_depth);
254
+ const { max_tool_depth } = this.config.brain;
255
+ return await this._runLoop(chatId, pending.messages, user, 0, max_tool_depth, pending.tools || toolDefinitions);
165
256
  }
166
257
 
167
258
  _checkPause(chatId, block, user, toolResults, remainingBlocks, messages) {
@@ -201,58 +292,48 @@ export class Agent {
201
292
  return null;
202
293
  }
203
294
 
204
- async _runLoop(chatId, messages, user, startDepth, maxDepth) {
295
+ async _runLoop(chatId, messages, user, startDepth, maxDepth, tools) {
205
296
  const logger = getLogger();
206
- const { model, max_tokens, temperature } = this.config.anthropic;
297
+ let currentTools = tools || toolDefinitions;
207
298
 
208
299
  for (let depth = startDepth; depth < maxDepth; depth++) {
209
300
  logger.debug(`Agent loop iteration ${depth + 1}/${maxDepth}`);
210
301
 
211
- const response = await this.client.messages.create({
212
- model,
213
- max_tokens,
214
- temperature,
302
+ const response = await this.provider.chat({
215
303
  system: this.systemPrompt,
216
- tools: toolDefinitions,
217
304
  messages,
305
+ tools: currentTools,
218
306
  });
219
307
 
220
- if (response.stop_reason === 'end_turn') {
221
- const textBlocks = response.content
222
- .filter((b) => b.type === 'text')
223
- .map((b) => b.text);
224
- const reply = textBlocks.join('\n');
225
-
308
+ if (response.stopReason === 'end_turn') {
309
+ const reply = response.text || '';
226
310
  this.conversationManager.addMessage(chatId, 'assistant', reply);
227
311
  return reply;
228
312
  }
229
313
 
230
- if (response.stop_reason === 'tool_use') {
231
- messages.push({ role: 'assistant', content: response.content });
314
+ if (response.stopReason === 'tool_use') {
315
+ messages.push({ role: 'assistant', content: response.rawContent });
232
316
 
233
- // Send Claude's thinking text to the user
234
- const thinkingBlocks = response.content.filter((b) => b.type === 'text' && b.text.trim());
235
- if (thinkingBlocks.length > 0) {
236
- const thinking = thinkingBlocks.map((b) => b.text).join('\n');
237
- logger.info(`Agent thinking: ${thinking.slice(0, 200)}`);
238
- await this._sendUpdate(`💭 ${thinking}`);
317
+ // Send thinking text to the user
318
+ if (response.text && response.text.trim()) {
319
+ logger.info(`Agent thinking: ${response.text.slice(0, 200)}`);
320
+ await this._sendUpdate(`💭 ${response.text}`);
239
321
  }
240
322
 
241
- const toolUseBlocks = response.content.filter((b) => b.type === 'tool_use');
242
323
  const toolResults = [];
324
+ const usedToolNames = [];
243
325
 
244
- for (let i = 0; i < toolUseBlocks.length; i++) {
245
- const block = toolUseBlocks[i];
326
+ for (let i = 0; i < response.toolCalls.length; i++) {
327
+ const block = response.toolCalls[i];
328
+
329
+ // Build a block-like object for _checkPause (needs .type for remainingBlocks filter)
330
+ const blockObj = { type: 'tool_use', id: block.id, name: block.name, input: block.input };
246
331
 
247
332
  // Check if we need to pause (missing cred or dangerous action)
248
- const pauseMsg = this._checkPause(
249
- chatId,
250
- block,
251
- user,
252
- toolResults,
253
- toolUseBlocks.slice(i + 1),
254
- messages,
255
- );
333
+ const remaining = response.toolCalls.slice(i + 1).map((tc) => ({
334
+ type: 'tool_use', id: tc.id, name: tc.name, input: tc.input,
335
+ }));
336
+ const pauseMsg = this._checkPause(chatId, blockObj, user, toolResults, remaining, messages);
256
337
  if (pauseMsg) return pauseMsg;
257
338
 
258
339
  const summary = this._formatToolSummary(block.name, block.input);
@@ -266,26 +347,27 @@ export class Agent {
266
347
  sendPhoto: this._sendPhoto,
267
348
  });
268
349
 
350
+ usedToolNames.push(block.name);
351
+
269
352
  toolResults.push({
270
353
  type: 'tool_result',
271
354
  tool_use_id: block.id,
272
- content: JSON.stringify(result),
355
+ content: this._truncateResult(block.name, result),
273
356
  });
274
357
  }
275
358
 
359
+ // Expand tools based on what was actually used
360
+ currentTools = expandToolsForUsed(usedToolNames, currentTools, toolDefinitions);
361
+
276
362
  messages.push({ role: 'user', content: toolResults });
277
363
  continue;
278
364
  }
279
365
 
280
366
  // Unexpected stop reason
281
- logger.warn(`Unexpected stop_reason: ${response.stop_reason}`);
282
- const fallbackText = response.content
283
- .filter((b) => b.type === 'text')
284
- .map((b) => b.text)
285
- .join('\n');
286
- if (fallbackText) {
287
- this.conversationManager.addMessage(chatId, 'assistant', fallbackText);
288
- return fallbackText;
367
+ logger.warn(`Unexpected stopReason: ${response.stopReason}`);
368
+ if (response.text) {
369
+ this.conversationManager.addMessage(chatId, 'assistant', response.text);
370
+ return response.text;
289
371
  }
290
372
  return 'Something went wrong — unexpected response from the model.';
291
373
  }
package/src/bot.js CHANGED
@@ -2,6 +2,7 @@ import TelegramBot from 'node-telegram-bot-api';
2
2
  import { createReadStream } from 'fs';
3
3
  import { isAllowedUser, getUnauthorizedMessage } from './security/auth.js';
4
4
  import { getLogger } from './utils/logger.js';
5
+ import { PROVIDERS } from './providers/models.js';
5
6
 
6
7
  function splitMessage(text, maxLength = 4096) {
7
8
  if (text.length <= maxLength) return [text];
@@ -22,9 +23,32 @@ function splitMessage(text, maxLength = 4096) {
22
23
  return chunks;
23
24
  }
24
25
 
26
+ /**
27
+ * Simple per-chat queue to serialize agent processing.
28
+ * Each chat gets its own promise chain so messages are processed in order.
29
+ */
30
+ class ChatQueue {
31
+ constructor() {
32
+ this.queues = new Map();
33
+ }
34
+
35
+ enqueue(chatId, fn) {
36
+ const key = String(chatId);
37
+ const prev = this.queues.get(key) || Promise.resolve();
38
+ const next = prev.then(() => fn()).catch(() => {});
39
+ this.queues.set(key, next);
40
+ return next;
41
+ }
42
+ }
43
+
25
44
  export function startBot(config, agent, conversationManager) {
26
45
  const logger = getLogger();
27
46
  const bot = new TelegramBot(config.telegram.bot_token, { polling: true });
47
+ const chatQueue = new ChatQueue();
48
+ const batchWindowMs = config.telegram.batch_window_ms || 3000;
49
+
50
+ // Per-chat message batching: chatId -> { messages[], timer, resolve }
51
+ const chatBatches = new Map();
28
52
 
29
53
  // Load previous conversations from disk
30
54
  const loaded = conversationManager.load();
@@ -34,6 +58,124 @@ export function startBot(config, agent, conversationManager) {
34
58
 
35
59
  logger.info('Telegram bot started with polling');
36
60
 
61
+ // Track pending brain API key input: chatId -> { providerKey, modelId }
62
+ const pendingBrainKey = new Map();
63
+
64
+ // Handle inline keyboard callbacks for /brain
65
+ bot.on('callback_query', async (query) => {
66
+ const chatId = query.message.chat.id;
67
+ const data = query.data;
68
+
69
+ if (!isAllowedUser(query.from.id, config)) {
70
+ await bot.answerCallbackQuery(query.id, { text: 'Unauthorized' });
71
+ return;
72
+ }
73
+
74
+ try {
75
+ if (data.startsWith('brain_provider:')) {
76
+ // User picked a provider — show model list
77
+ const providerKey = data.split(':')[1];
78
+ const providerDef = PROVIDERS[providerKey];
79
+ if (!providerDef) {
80
+ await bot.answerCallbackQuery(query.id, { text: 'Unknown provider' });
81
+ return;
82
+ }
83
+
84
+ const modelButtons = providerDef.models.map((m) => ([{
85
+ text: m.label,
86
+ callback_data: `brain_model:${providerKey}:${m.id}`,
87
+ }]));
88
+ modelButtons.push([{ text: 'Cancel', callback_data: 'brain_cancel' }]);
89
+
90
+ await bot.editMessageText(`Select a *${providerDef.name}* model:`, {
91
+ chat_id: chatId,
92
+ message_id: query.message.message_id,
93
+ parse_mode: 'Markdown',
94
+ reply_markup: { inline_keyboard: modelButtons },
95
+ });
96
+ await bot.answerCallbackQuery(query.id);
97
+
98
+ } else if (data.startsWith('brain_model:')) {
99
+ // User picked a model — attempt switch
100
+ const [, providerKey, modelId] = data.split(':');
101
+ const providerDef = PROVIDERS[providerKey];
102
+ const modelEntry = providerDef?.models.find((m) => m.id === modelId);
103
+ const modelLabel = modelEntry ? modelEntry.label : modelId;
104
+
105
+ const missing = agent.switchBrain(providerKey, modelId);
106
+ if (missing) {
107
+ // API key missing — ask for it
108
+ pendingBrainKey.set(chatId, { providerKey, modelId });
109
+ await bot.editMessageText(
110
+ `🔑 *${providerDef.name}* API key is required.\n\nPlease send your \`${missing}\` now.\n\nOr send *cancel* to abort.`,
111
+ {
112
+ chat_id: chatId,
113
+ message_id: query.message.message_id,
114
+ parse_mode: 'Markdown',
115
+ },
116
+ );
117
+ } else {
118
+ const info = agent.getBrainInfo();
119
+ await bot.editMessageText(
120
+ `🧠 Brain switched to *${info.providerName}* / *${info.modelLabel}*`,
121
+ {
122
+ chat_id: chatId,
123
+ message_id: query.message.message_id,
124
+ parse_mode: 'Markdown',
125
+ },
126
+ );
127
+ }
128
+ await bot.answerCallbackQuery(query.id);
129
+
130
+ } else if (data === 'brain_cancel') {
131
+ pendingBrainKey.delete(chatId);
132
+ await bot.editMessageText('Brain change cancelled.', {
133
+ chat_id: chatId,
134
+ message_id: query.message.message_id,
135
+ });
136
+ await bot.answerCallbackQuery(query.id);
137
+ }
138
+ } catch (err) {
139
+ logger.error(`Callback query error: ${err.message}`);
140
+ await bot.answerCallbackQuery(query.id, { text: 'Error' });
141
+ }
142
+ });
143
+
144
+ /**
145
+ * Batch messages for a chat. Returns the merged text for the first message,
146
+ * or null for subsequent messages (they get merged into the first).
147
+ */
148
+ function batchMessage(chatId, text) {
149
+ return new Promise((resolve) => {
150
+ const key = String(chatId);
151
+ let batch = chatBatches.get(key);
152
+
153
+ if (!batch) {
154
+ batch = { messages: [], timer: null, resolvers: [] };
155
+ chatBatches.set(key, batch);
156
+ }
157
+
158
+ batch.messages.push(text);
159
+ batch.resolvers.push(resolve);
160
+
161
+ // Reset timer on each new message
162
+ if (batch.timer) clearTimeout(batch.timer);
163
+
164
+ batch.timer = setTimeout(() => {
165
+ chatBatches.delete(key);
166
+ const merged = batch.messages.length === 1
167
+ ? batch.messages[0]
168
+ : batch.messages.map((m, i) => `[${i + 1}]: ${m}`).join('\n\n');
169
+
170
+ // First resolver gets the merged text, rest get null (skip)
171
+ batch.resolvers[0](merged);
172
+ for (let i = 1; i < batch.resolvers.length; i++) {
173
+ batch.resolvers[i](null);
174
+ }
175
+ }, batchWindowMs);
176
+ });
177
+ }
178
+
37
179
  bot.on('message', async (msg) => {
38
180
  if (!msg.text) return; // ignore non-text
39
181
 
@@ -50,7 +192,47 @@ export function startBot(config, agent, conversationManager) {
50
192
 
51
193
  let text = msg.text.trim();
52
194
 
53
- // Handle commands
195
+ // Handle pending brain API key input
196
+ if (pendingBrainKey.has(chatId)) {
197
+ const pending = pendingBrainKey.get(chatId);
198
+ pendingBrainKey.delete(chatId);
199
+
200
+ if (text.toLowerCase() === 'cancel') {
201
+ await bot.sendMessage(chatId, 'Brain change cancelled.');
202
+ return;
203
+ }
204
+
205
+ agent.switchBrainWithKey(pending.providerKey, pending.modelId, text);
206
+ const info = agent.getBrainInfo();
207
+ await bot.sendMessage(
208
+ chatId,
209
+ `🧠 Brain switched to *${info.providerName}* / *${info.modelLabel}*\n\nAPI key saved.`,
210
+ { parse_mode: 'Markdown' },
211
+ );
212
+ return;
213
+ }
214
+
215
+ // Handle commands — these bypass batching entirely
216
+ if (text === '/brain') {
217
+ const info = agent.getBrainInfo();
218
+ const providerKeys = Object.keys(PROVIDERS);
219
+ const buttons = providerKeys.map((key) => ([{
220
+ text: `${PROVIDERS[key].name}${key === info.provider ? ' ✓' : ''}`,
221
+ callback_data: `brain_provider:${key}`,
222
+ }]));
223
+ buttons.push([{ text: 'Cancel', callback_data: 'brain_cancel' }]);
224
+
225
+ await bot.sendMessage(
226
+ chatId,
227
+ `🧠 *Current brain:* ${info.providerName} / ${info.modelLabel}\n\nSelect a provider to switch:`,
228
+ {
229
+ parse_mode: 'Markdown',
230
+ reply_markup: { inline_keyboard: buttons },
231
+ },
232
+ );
233
+ return;
234
+ }
235
+
54
236
  if (text === '/clean' || text === '/clear' || text === '/reset') {
55
237
  conversationManager.clear(chatId);
56
238
  logger.info(`Conversation cleared for chat ${chatId} by ${username}`);
@@ -68,6 +250,7 @@ export function startBot(config, agent, conversationManager) {
68
250
  await bot.sendMessage(chatId, [
69
251
  '*KernelBot Commands*',
70
252
  '',
253
+ '/brain — Show current AI model and switch provider/model',
71
254
  '/clean — Clear conversation and start fresh',
72
255
  '/history — Show message count in memory',
73
256
  '/browse <url> — Browse a website and get a summary',
@@ -106,91 +289,101 @@ export function startBot(config, agent, conversationManager) {
106
289
  text = `Extract content from ${extractUrl} using the CSS selector: ${extractSelector}`;
107
290
  }
108
291
 
109
- logger.info(`Message from ${username} (${userId}): ${text.slice(0, 100)}`);
292
+ // Batch messages wait for the batch window to close
293
+ const mergedText = await batchMessage(chatId, text);
294
+ if (mergedText === null) {
295
+ // This message was merged into another batch — skip
296
+ return;
297
+ }
298
+
299
+ logger.info(`Message from ${username} (${userId}): ${mergedText.slice(0, 100)}`);
110
300
 
111
- // Show typing and keep refreshing it
112
- const typingInterval = setInterval(() => {
301
+ // Enqueue into per-chat queue for serialized processing
302
+ chatQueue.enqueue(chatId, async () => {
303
+ // Show typing and keep refreshing it
304
+ const typingInterval = setInterval(() => {
305
+ bot.sendChatAction(chatId, 'typing').catch(() => {});
306
+ }, 4000);
113
307
  bot.sendChatAction(chatId, 'typing').catch(() => {});
114
- }, 4000);
115
- bot.sendChatAction(chatId, 'typing').catch(() => {});
116
308
 
117
- try {
118
- const onUpdate = async (update, opts = {}) => {
119
- // Edit an existing message instead of sending a new one
120
- if (opts.editMessageId) {
121
- try {
122
- const edited = await bot.editMessageText(update, {
123
- chat_id: chatId,
124
- message_id: opts.editMessageId,
125
- parse_mode: 'Markdown',
126
- });
127
- return edited.message_id;
128
- } catch {
309
+ try {
310
+ const onUpdate = async (update, opts = {}) => {
311
+ // Edit an existing message instead of sending a new one
312
+ if (opts.editMessageId) {
129
313
  try {
130
314
  const edited = await bot.editMessageText(update, {
131
315
  chat_id: chatId,
132
316
  message_id: opts.editMessageId,
317
+ parse_mode: 'Markdown',
133
318
  });
134
319
  return edited.message_id;
135
320
  } catch {
136
- return opts.editMessageId;
321
+ try {
322
+ const edited = await bot.editMessageText(update, {
323
+ chat_id: chatId,
324
+ message_id: opts.editMessageId,
325
+ });
326
+ return edited.message_id;
327
+ } catch {
328
+ return opts.editMessageId;
329
+ }
137
330
  }
138
331
  }
139
- }
140
332
 
141
- // Send new message(s)
142
- const parts = splitMessage(update);
143
- let lastMsgId = null;
144
- for (const part of parts) {
145
- try {
146
- const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
147
- lastMsgId = sent.message_id;
148
- } catch {
149
- const sent = await bot.sendMessage(chatId, part);
150
- lastMsgId = sent.message_id;
333
+ // Send new message(s)
334
+ const parts = splitMessage(update);
335
+ let lastMsgId = null;
336
+ for (const part of parts) {
337
+ try {
338
+ const sent = await bot.sendMessage(chatId, part, { parse_mode: 'Markdown' });
339
+ lastMsgId = sent.message_id;
340
+ } catch {
341
+ const sent = await bot.sendMessage(chatId, part);
342
+ lastMsgId = sent.message_id;
343
+ }
151
344
  }
152
- }
153
- return lastMsgId;
154
- };
155
-
156
- const sendPhoto = async (filePath, caption) => {
157
- try {
158
- await bot.sendPhoto(chatId, createReadStream(filePath), {
159
- caption: caption || '',
160
- parse_mode: 'Markdown',
161
- });
162
- } catch {
345
+ return lastMsgId;
346
+ };
347
+
348
+ const sendPhoto = async (filePath, caption) => {
163
349
  try {
164
350
  await bot.sendPhoto(chatId, createReadStream(filePath), {
165
351
  caption: caption || '',
352
+ parse_mode: 'Markdown',
166
353
  });
167
- } catch (err) {
168
- logger.error(`Failed to send photo: ${err.message}`);
354
+ } catch {
355
+ try {
356
+ await bot.sendPhoto(chatId, createReadStream(filePath), {
357
+ caption: caption || '',
358
+ });
359
+ } catch (err) {
360
+ logger.error(`Failed to send photo: ${err.message}`);
361
+ }
362
+ }
363
+ };
364
+
365
+ const reply = await agent.processMessage(chatId, mergedText, {
366
+ id: userId,
367
+ username,
368
+ }, onUpdate, sendPhoto);
369
+
370
+ clearInterval(typingInterval);
371
+
372
+ const chunks = splitMessage(reply || 'Done.');
373
+ for (const chunk of chunks) {
374
+ try {
375
+ await bot.sendMessage(chatId, chunk, { parse_mode: 'Markdown' });
376
+ } catch {
377
+ // Fallback to plain text if Markdown fails
378
+ await bot.sendMessage(chatId, chunk);
169
379
  }
170
380
  }
171
- };
172
-
173
- const reply = await agent.processMessage(chatId, text, {
174
- id: userId,
175
- username,
176
- }, onUpdate, sendPhoto);
177
-
178
- clearInterval(typingInterval);
179
-
180
- const chunks = splitMessage(reply || 'Done.');
181
- for (const chunk of chunks) {
182
- try {
183
- await bot.sendMessage(chatId, chunk, { parse_mode: 'Markdown' });
184
- } catch {
185
- // Fallback to plain text if Markdown fails
186
- await bot.sendMessage(chatId, chunk);
187
- }
381
+ } catch (err) {
382
+ clearInterval(typingInterval);
383
+ logger.error(`Error processing message: ${err.message}`);
384
+ await bot.sendMessage(chatId, `Error: ${err.message}`);
188
385
  }
189
- } catch (err) {
190
- clearInterval(typingInterval);
191
- logger.error(`Error processing message: ${err.message}`);
192
- await bot.sendMessage(chatId, `Error: ${err.message}`);
193
- }
386
+ });
194
387
  });
195
388
 
196
389
  bot.on('polling_error', (err) => {