@grinev/opencode-telegram-bot 0.13.1 → 0.14.0
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/.env.example +0 -8
- package/README.md +25 -26
- package/dist/bot/commands/sessions.js +1 -0
- package/dist/bot/index.js +136 -29
- package/dist/bot/streaming/response-streamer.js +6 -4
- package/dist/bot/streaming/tool-call-streamer.js +285 -0
- package/dist/bot/utils/finalize-assistant-response.js +20 -11
- package/dist/bot/utils/send-with-markdown-fallback.js +13 -2
- package/dist/bot/utils/thinking-message.js +1 -5
- package/dist/config.js +0 -16
- package/dist/i18n/de.js +13 -1
- package/dist/i18n/en.js +13 -1
- package/dist/i18n/es.js +13 -1
- package/dist/i18n/fr.js +13 -1
- package/dist/i18n/ru.js +13 -1
- package/dist/i18n/zh.js +13 -1
- package/dist/model/context-limit.js +57 -0
- package/dist/pinned/format.js +29 -0
- package/dist/pinned/manager.js +30 -55
- package/dist/scheduled-task/runtime.js +27 -21
- package/dist/summary/aggregator.js +435 -16
- package/dist/summary/formatter.js +84 -12
- package/dist/summary/subagent-formatter.js +63 -0
- package/dist/summary/tool-message-batcher.js +14 -209
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -50,20 +50,12 @@ OPENCODE_MODEL_ID=big-pickle
|
|
|
50
50
|
# Supported locales: en, de, es, fr, ru, zh
|
|
51
51
|
# BOT_LOCALE=en
|
|
52
52
|
|
|
53
|
-
# Service message batching interval in seconds (thinking + tool calls, default: 5)
|
|
54
|
-
# Recommended: keep >=2 to reduce risk of hitting Telegram rate limits (about 1 message/sec)
|
|
55
|
-
# 0 = send immediately (can hit rate limits if there are many tool calls)
|
|
56
|
-
# SERVICE_MESSAGES_INTERVAL_SEC=5
|
|
57
|
-
|
|
58
53
|
# Hide thinking indicator messages (default: false)
|
|
59
54
|
# HIDE_THINKING_MESSAGES=false
|
|
60
55
|
|
|
61
56
|
# Hide tool call service messages (default: false)
|
|
62
57
|
# HIDE_TOOL_CALL_MESSAGES=false
|
|
63
58
|
|
|
64
|
-
# Stream assistant responses while they are being generated (default: true)
|
|
65
|
-
# RESPONSE_STREAMING=true
|
|
66
|
-
|
|
67
59
|
# Assistant message formatting mode (default: markdown)
|
|
68
60
|
# markdown = convert assistant replies to Telegram MarkdownV2
|
|
69
61
|
# raw = show assistant replies as plain text
|
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@ Languages: English (`en`), Deutsch (`de`), Español (`es`), Français (`fr`), Р
|
|
|
28
28
|
- **Live status** — pinned message with current project, model, context usage, and changed files list, updated in real time
|
|
29
29
|
- **Model switching** — pick models from OpenCode favorites and recent history directly in the chat (favorites are shown first)
|
|
30
30
|
- **Agent modes** — switch between Plan and Build modes on the fly
|
|
31
|
+
- **Subagent activity** — watch live subagent progress in chat, including the current task, agent, model, and active tool step
|
|
31
32
|
- **Custom Commands** — run OpenCode custom commands (and built-ins like `init`/`review`) from an inline menu with confirmation
|
|
32
33
|
- **Interactive Q&A** — answer agent questions and approve permissions via inline buttons
|
|
33
34
|
- **Voice prompts** — send voice/audio messages, transcribe them via a Whisper-compatible API, then forward recognized text to OpenCode
|
|
@@ -146,32 +147,30 @@ When installed via npm, the configuration wizard handles the initial setup. The
|
|
|
146
147
|
- **Windows:** `%APPDATA%\opencode-telegram-bot\.env`
|
|
147
148
|
- **Linux:** `~/.config/opencode-telegram-bot/.env`
|
|
148
149
|
|
|
149
|
-
| Variable
|
|
150
|
-
|
|
|
151
|
-
| `TELEGRAM_BOT_TOKEN`
|
|
152
|
-
| `TELEGRAM_ALLOWED_USER_ID`
|
|
153
|
-
| `TELEGRAM_PROXY_URL`
|
|
154
|
-
| `OPENCODE_API_URL`
|
|
155
|
-
| `OPENCODE_SERVER_USERNAME`
|
|
156
|
-
| `OPENCODE_SERVER_PASSWORD`
|
|
157
|
-
| `OPENCODE_MODEL_PROVIDER`
|
|
158
|
-
| `OPENCODE_MODEL_ID`
|
|
159
|
-
| `BOT_LOCALE`
|
|
160
|
-
| `SESSIONS_LIST_LIMIT`
|
|
161
|
-
| `PROJECTS_LIST_LIMIT`
|
|
162
|
-
| `COMMANDS_LIST_LIMIT`
|
|
163
|
-
| `TASK_LIMIT`
|
|
164
|
-
| `
|
|
165
|
-
| `
|
|
166
|
-
| `
|
|
167
|
-
| `
|
|
168
|
-
| `
|
|
169
|
-
| `
|
|
170
|
-
| `
|
|
171
|
-
| `
|
|
172
|
-
| `
|
|
173
|
-
| `STT_LANGUAGE` | Optional language hint (empty = provider auto-detect) | No | — |
|
|
174
|
-
| `LOG_LEVEL` | Log level (`debug`, `info`, `warn`, `error`) | No | `info` |
|
|
150
|
+
| Variable | Description | Required | Default |
|
|
151
|
+
| -------------------------- | -------------------------------------------------------------------------------- | :------: | ------------------------ |
|
|
152
|
+
| `TELEGRAM_BOT_TOKEN` | Bot token from @BotFather | Yes | — |
|
|
153
|
+
| `TELEGRAM_ALLOWED_USER_ID` | Your numeric Telegram user ID | Yes | — |
|
|
154
|
+
| `TELEGRAM_PROXY_URL` | Proxy URL for Telegram API (SOCKS5/HTTP) | No | — |
|
|
155
|
+
| `OPENCODE_API_URL` | OpenCode server URL | No | `http://localhost:4096` |
|
|
156
|
+
| `OPENCODE_SERVER_USERNAME` | Server auth username | No | `opencode` |
|
|
157
|
+
| `OPENCODE_SERVER_PASSWORD` | Server auth password | No | — |
|
|
158
|
+
| `OPENCODE_MODEL_PROVIDER` | Default model provider | Yes | `opencode` |
|
|
159
|
+
| `OPENCODE_MODEL_ID` | Default model ID | Yes | `big-pickle` |
|
|
160
|
+
| `BOT_LOCALE` | Bot UI language (supported locale code, e.g. `en`, `de`, `es`, `fr`, `ru`, `zh`) | No | `en` |
|
|
161
|
+
| `SESSIONS_LIST_LIMIT` | Sessions per page in `/sessions` | No | `10` |
|
|
162
|
+
| `PROJECTS_LIST_LIMIT` | Projects per page in `/projects` | No | `10` |
|
|
163
|
+
| `COMMANDS_LIST_LIMIT` | Commands per page in `/commands` | No | `10` |
|
|
164
|
+
| `TASK_LIMIT` | Maximum number of scheduled tasks that can exist at once | No | `10` |
|
|
165
|
+
| `HIDE_THINKING_MESSAGES` | Hide `💭 Thinking...` service messages | No | `false` |
|
|
166
|
+
| `HIDE_TOOL_CALL_MESSAGES` | Hide tool-call service messages (`💻 bash ...`, `📖 read ...`, etc.) | No | `false` |
|
|
167
|
+
| `MESSAGE_FORMAT_MODE` | Assistant reply formatting mode: `markdown` (Telegram MarkdownV2) or `raw` | No | `markdown` |
|
|
168
|
+
| `CODE_FILE_MAX_SIZE_KB` | Max file size (KB) to send as document | No | `100` |
|
|
169
|
+
| `STT_API_URL` | Whisper-compatible API base URL (enables voice/audio transcription) | No | — |
|
|
170
|
+
| `STT_API_KEY` | API key for your STT provider | No | — |
|
|
171
|
+
| `STT_MODEL` | STT model name passed to `/audio/transcriptions` | No | `whisper-large-v3-turbo` |
|
|
172
|
+
| `STT_LANGUAGE` | Optional language hint (empty = provider auto-detect) | No | — |
|
|
173
|
+
| `LOG_LEVEL` | Log level (`debug`, `info`, `warn`, `error`) | No | `info` |
|
|
175
174
|
|
|
176
175
|
> **Keep your `.env` file private.** It contains your bot token. Never commit it to version control.
|
|
177
176
|
|
|
@@ -51,6 +51,7 @@ async function loadSessionPage(directory, page, pageSize) {
|
|
|
51
51
|
const { data: sessions, error } = await opencodeClient.session.list({
|
|
52
52
|
directory,
|
|
53
53
|
limit: endExclusive + SESSION_FETCH_EXTRA_COUNT,
|
|
54
|
+
roots: true,
|
|
54
55
|
});
|
|
55
56
|
if (error || !sessions) {
|
|
56
57
|
throw error || new Error("No data received from server");
|
package/dist/bot/index.js
CHANGED
|
@@ -37,6 +37,7 @@ import { keyboardManager } from "../keyboard/manager.js";
|
|
|
37
37
|
import { subscribeToEvents } from "../opencode/events.js";
|
|
38
38
|
import { summaryAggregator } from "../summary/aggregator.js";
|
|
39
39
|
import { formatSummary, formatSummaryWithMode, formatToolInfo, getAssistantParseMode, } from "../summary/formatter.js";
|
|
40
|
+
import { renderSubagentCards } from "../summary/subagent-formatter.js";
|
|
40
41
|
import { ToolMessageBatcher } from "../summary/tool-message-batcher.js";
|
|
41
42
|
import { getCurrentSession } from "../session/manager.js";
|
|
42
43
|
import { ingestSessionInfoForCache } from "../session/cache-manager.js";
|
|
@@ -56,6 +57,7 @@ import { getStoredModel } from "../model/manager.js";
|
|
|
56
57
|
import { foregroundSessionState } from "../scheduled-task/foreground-state.js";
|
|
57
58
|
import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
|
|
58
59
|
import { ResponseStreamer } from "./streaming/response-streamer.js";
|
|
60
|
+
import { ToolCallStreamer } from "./streaming/tool-call-streamer.js";
|
|
59
61
|
import { editMessageWithMarkdownFallback, sendMessageWithMarkdownFallback, } from "./utils/send-with-markdown-fallback.js";
|
|
60
62
|
let botInstance = null;
|
|
61
63
|
let chatIdInstance = null;
|
|
@@ -64,6 +66,7 @@ const TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH = 1024;
|
|
|
64
66
|
const RESPONSE_STREAM_THROTTLE_MS = 200;
|
|
65
67
|
const RESPONSE_STREAM_TEXT_LIMIT = 3800;
|
|
66
68
|
const SESSION_RETRY_PREFIX = "🔁";
|
|
69
|
+
const SUBAGENT_STREAM_PREFIX = "🧩";
|
|
67
70
|
const __filename = fileURLToPath(import.meta.url);
|
|
68
71
|
const __dirname = path.dirname(__filename);
|
|
69
72
|
const TEMP_DIR = path.join(__dirname, "..", ".tmp");
|
|
@@ -94,7 +97,6 @@ function prepareStreamingPayload(messageText) {
|
|
|
94
97
|
};
|
|
95
98
|
}
|
|
96
99
|
const toolMessageBatcher = new ToolMessageBatcher({
|
|
97
|
-
intervalSeconds: 5,
|
|
98
100
|
sendText: async (sessionId, text) => {
|
|
99
101
|
if (!botInstance || !chatIdInstance) {
|
|
100
102
|
return;
|
|
@@ -187,6 +189,58 @@ const responseStreamer = new ResponseStreamer({
|
|
|
187
189
|
});
|
|
188
190
|
},
|
|
189
191
|
});
|
|
192
|
+
const toolCallStreamer = new ToolCallStreamer({
|
|
193
|
+
throttleMs: RESPONSE_STREAM_THROTTLE_MS,
|
|
194
|
+
sendText: async (sessionId, text) => {
|
|
195
|
+
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
196
|
+
throw new Error("Bot context missing for tool stream send");
|
|
197
|
+
}
|
|
198
|
+
const currentSession = getCurrentSession();
|
|
199
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
200
|
+
throw new Error(`Tool stream session mismatch for send: ${sessionId}`);
|
|
201
|
+
}
|
|
202
|
+
const sentMessage = await botInstance.api.sendMessage(chatIdInstance, text, {
|
|
203
|
+
disable_notification: true,
|
|
204
|
+
});
|
|
205
|
+
return sentMessage.message_id;
|
|
206
|
+
},
|
|
207
|
+
editText: async (sessionId, messageId, text) => {
|
|
208
|
+
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
209
|
+
throw new Error("Bot context missing for tool stream edit");
|
|
210
|
+
}
|
|
211
|
+
const currentSession = getCurrentSession();
|
|
212
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
213
|
+
throw new Error(`Tool stream session mismatch for edit: ${sessionId}`);
|
|
214
|
+
}
|
|
215
|
+
try {
|
|
216
|
+
await botInstance.api.editMessageText(chatIdInstance, messageId, text);
|
|
217
|
+
}
|
|
218
|
+
catch (error) {
|
|
219
|
+
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
220
|
+
if (errorMessage.includes("message is not modified")) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
},
|
|
226
|
+
deleteText: async (sessionId, messageId) => {
|
|
227
|
+
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
228
|
+
throw new Error("Bot context missing for tool stream delete");
|
|
229
|
+
}
|
|
230
|
+
const currentSession = getCurrentSession();
|
|
231
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
232
|
+
throw new Error(`Tool stream session mismatch for delete: ${sessionId}`);
|
|
233
|
+
}
|
|
234
|
+
await botInstance.api.deleteMessage(chatIdInstance, messageId).catch((error) => {
|
|
235
|
+
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
236
|
+
if (errorMessage.includes("message to delete not found") ||
|
|
237
|
+
errorMessage.includes("message identifier is not specified")) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
throw error;
|
|
241
|
+
});
|
|
242
|
+
},
|
|
243
|
+
});
|
|
190
244
|
async function ensureCommandsInitialized(ctx, next) {
|
|
191
245
|
if (commandsInitialized || !ctx.from || ctx.from.id !== config.telegram.allowedUserId) {
|
|
192
246
|
await next();
|
|
@@ -217,16 +271,13 @@ async function ensureEventSubscription(directory) {
|
|
|
217
271
|
logger.error("No directory found for event subscription");
|
|
218
272
|
return;
|
|
219
273
|
}
|
|
220
|
-
toolMessageBatcher.setIntervalSeconds(config.bot.serviceMessagesIntervalSec);
|
|
221
274
|
summaryAggregator.setTypingIndicatorEnabled(true);
|
|
222
275
|
summaryAggregator.setOnCleared(() => {
|
|
223
276
|
toolMessageBatcher.clearAll("summary_aggregator_clear");
|
|
277
|
+
toolCallStreamer.clearAll("summary_aggregator_clear");
|
|
224
278
|
responseStreamer.clearAll("summary_aggregator_clear");
|
|
225
279
|
});
|
|
226
280
|
summaryAggregator.setOnPartial((sessionId, messageId, messageText) => {
|
|
227
|
-
if (!config.bot.responseStreaming) {
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
281
|
if (!botInstance || !chatIdInstance) {
|
|
231
282
|
return;
|
|
232
283
|
}
|
|
@@ -238,7 +289,7 @@ async function ensureEventSubscription(directory) {
|
|
|
238
289
|
if (!preparedStreamPayload) {
|
|
239
290
|
return;
|
|
240
291
|
}
|
|
241
|
-
preparedStreamPayload.sendOptions =
|
|
292
|
+
preparedStreamPayload.sendOptions = { disable_notification: true };
|
|
242
293
|
preparedStreamPayload.editOptions = undefined;
|
|
243
294
|
responseStreamer.enqueue(sessionId, messageId, preparedStreamPayload);
|
|
244
295
|
});
|
|
@@ -246,12 +297,14 @@ async function ensureEventSubscription(directory) {
|
|
|
246
297
|
if (!botInstance || !chatIdInstance) {
|
|
247
298
|
logger.error("Bot or chat ID not available for sending message");
|
|
248
299
|
responseStreamer.clearMessage(sessionId, messageId, "bot_context_missing");
|
|
300
|
+
toolCallStreamer.clearSession(sessionId, "bot_context_missing");
|
|
249
301
|
foregroundSessionState.markIdle(sessionId);
|
|
250
302
|
return;
|
|
251
303
|
}
|
|
252
304
|
const currentSession = getCurrentSession();
|
|
253
305
|
if (currentSession?.id !== sessionId) {
|
|
254
306
|
responseStreamer.clearMessage(sessionId, messageId, "session_mismatch");
|
|
307
|
+
toolCallStreamer.clearSession(sessionId, "session_mismatch");
|
|
255
308
|
foregroundSessionState.markIdle(sessionId);
|
|
256
309
|
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
257
310
|
return;
|
|
@@ -259,13 +312,15 @@ async function ensureEventSubscription(directory) {
|
|
|
259
312
|
const botApi = botInstance.api;
|
|
260
313
|
const chatId = chatIdInstance;
|
|
261
314
|
try {
|
|
262
|
-
|
|
263
|
-
responseStreaming: config.bot.responseStreaming,
|
|
315
|
+
await finalizeAssistantResponse({
|
|
264
316
|
sessionId,
|
|
265
317
|
messageId,
|
|
266
318
|
messageText,
|
|
267
319
|
responseStreamer,
|
|
268
|
-
flushPendingServiceMessages: () =>
|
|
320
|
+
flushPendingServiceMessages: () => Promise.all([
|
|
321
|
+
toolMessageBatcher.flushSession(sessionId, "assistant_message_completed"),
|
|
322
|
+
toolCallStreamer.flushSession(sessionId, "assistant_message_completed"),
|
|
323
|
+
]).then(() => undefined),
|
|
269
324
|
prepareStreamingPayload,
|
|
270
325
|
formatSummary,
|
|
271
326
|
resolveFormat: () => (getAssistantParseMode() === "MarkdownV2" ? "markdown_v2" : "raw"),
|
|
@@ -279,13 +334,17 @@ async function ensureEventSubscription(directory) {
|
|
|
279
334
|
format,
|
|
280
335
|
});
|
|
281
336
|
},
|
|
337
|
+
deleteMessages: async (messageIds) => {
|
|
338
|
+
for (const msgId of messageIds) {
|
|
339
|
+
try {
|
|
340
|
+
await botApi.deleteMessage(chatId, msgId);
|
|
341
|
+
}
|
|
342
|
+
catch (err) {
|
|
343
|
+
logger.warn(`[Bot] Failed to delete streamed message ${msgId}:`, err);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
},
|
|
282
347
|
});
|
|
283
|
-
if (streamedViaMessages) {
|
|
284
|
-
logger.debug(`[Bot] Final assistant message already streamed (session=${sessionId}, message=${messageId})`);
|
|
285
|
-
foregroundSessionState.markIdle(sessionId);
|
|
286
|
-
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
348
|
}
|
|
290
349
|
catch (err) {
|
|
291
350
|
logger.error("Failed to send message to Telegram:", err);
|
|
@@ -309,19 +368,43 @@ async function ensureEventSubscription(directory) {
|
|
|
309
368
|
}
|
|
310
369
|
const shouldIncludeToolInfoInFileCaption = toolInfo.hasFileAttachment &&
|
|
311
370
|
(toolInfo.tool === "write" || toolInfo.tool === "edit" || toolInfo.tool === "apply_patch");
|
|
312
|
-
if (config.bot.hideToolCallMessages ||
|
|
371
|
+
if (config.bot.hideToolCallMessages ||
|
|
372
|
+
shouldIncludeToolInfoInFileCaption ||
|
|
373
|
+
toolInfo.tool === "task") {
|
|
313
374
|
return;
|
|
314
375
|
}
|
|
315
376
|
try {
|
|
316
377
|
const message = formatToolInfo(toolInfo);
|
|
317
378
|
if (message) {
|
|
318
|
-
|
|
379
|
+
toolCallStreamer.append(toolInfo.sessionId, message);
|
|
319
380
|
}
|
|
320
381
|
}
|
|
321
382
|
catch (err) {
|
|
322
383
|
logger.error("Failed to send tool notification to Telegram:", err);
|
|
323
384
|
}
|
|
324
385
|
});
|
|
386
|
+
summaryAggregator.setOnSubagent(async (sessionId, subagents) => {
|
|
387
|
+
if (!botInstance || !chatIdInstance) {
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (config.bot.hideToolCallMessages) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const currentSession = getCurrentSession();
|
|
394
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
const renderedCards = await renderSubagentCards(subagents);
|
|
399
|
+
if (!renderedCards) {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
toolCallStreamer.replaceByPrefix(sessionId, SUBAGENT_STREAM_PREFIX, renderedCards);
|
|
403
|
+
}
|
|
404
|
+
catch (err) {
|
|
405
|
+
logger.error("Failed to render subagent activity for Telegram:", err);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
325
408
|
summaryAggregator.setOnToolFile(async (fileInfo) => {
|
|
326
409
|
if (!botInstance || !chatIdInstance) {
|
|
327
410
|
logger.error("Bot or chat ID not available for sending file");
|
|
@@ -332,6 +415,7 @@ async function ensureEventSubscription(directory) {
|
|
|
332
415
|
return;
|
|
333
416
|
}
|
|
334
417
|
try {
|
|
418
|
+
await toolCallStreamer.breakSession(fileInfo.sessionId, "tool_file_boundary");
|
|
335
419
|
const toolMessage = formatToolInfo(fileInfo);
|
|
336
420
|
const caption = prepareDocumentCaption(toolMessage || fileInfo.fileData.caption);
|
|
337
421
|
toolMessageBatcher.enqueueFile(fileInfo.sessionId, {
|
|
@@ -350,7 +434,10 @@ async function ensureEventSubscription(directory) {
|
|
|
350
434
|
}
|
|
351
435
|
const currentSession = getCurrentSession();
|
|
352
436
|
if (currentSession) {
|
|
353
|
-
await
|
|
437
|
+
await Promise.all([
|
|
438
|
+
toolMessageBatcher.flushSession(currentSession.id, "question_asked"),
|
|
439
|
+
toolCallStreamer.flushSession(currentSession.id, "question_asked"),
|
|
440
|
+
]);
|
|
354
441
|
}
|
|
355
442
|
if (questionManager.isActive()) {
|
|
356
443
|
logger.warn("[Bot] Replacing active poll with a new one");
|
|
@@ -382,7 +469,10 @@ async function ensureEventSubscription(directory) {
|
|
|
382
469
|
logger.error("Bot or chat ID not available for showing permission request");
|
|
383
470
|
return;
|
|
384
471
|
}
|
|
385
|
-
await
|
|
472
|
+
await Promise.all([
|
|
473
|
+
toolMessageBatcher.flushSession(request.sessionID, "permission_asked"),
|
|
474
|
+
toolCallStreamer.flushSession(request.sessionID, "permission_asked"),
|
|
475
|
+
]);
|
|
386
476
|
logger.info(`[Bot] Received permission request from agent: type=${request.permission}, requestID=${request.id}`);
|
|
387
477
|
await showPermissionRequest(botInstance.api, chatIdInstance, request);
|
|
388
478
|
});
|
|
@@ -395,25 +485,40 @@ async function ensureEventSubscription(directory) {
|
|
|
395
485
|
return;
|
|
396
486
|
}
|
|
397
487
|
logger.debug("[Bot] Agent started thinking");
|
|
488
|
+
await toolCallStreamer.breakSession(sessionId, "thinking_started");
|
|
398
489
|
deliverThinkingMessage(sessionId, toolMessageBatcher, {
|
|
399
|
-
responseStreaming: config.bot.responseStreaming,
|
|
400
490
|
hideThinkingMessages: config.bot.hideThinkingMessages,
|
|
401
491
|
});
|
|
492
|
+
// Refresh pinned message so it shows the latest in-memory context
|
|
493
|
+
// (accumulated from silent token updates). 1 API call per thinking event.
|
|
494
|
+
if (pinnedMessageManager.isInitialized()) {
|
|
495
|
+
await pinnedMessageManager.refresh();
|
|
496
|
+
}
|
|
402
497
|
});
|
|
403
|
-
summaryAggregator.setOnTokens(async (tokens) => {
|
|
498
|
+
summaryAggregator.setOnTokens(async (tokens, isCompleted) => {
|
|
404
499
|
if (!pinnedMessageManager.isInitialized()) {
|
|
405
500
|
return;
|
|
406
501
|
}
|
|
407
502
|
try {
|
|
408
|
-
logger.debug(`[Bot] Received tokens: input=${tokens.input}, output=${tokens.output}`);
|
|
409
|
-
// Update keyboardManager SYNCHRONOUSLY before any await
|
|
410
|
-
// This ensures keyboard has correct context when onComplete sends the reply
|
|
503
|
+
logger.debug(`[Bot] Received tokens: input=${tokens.input}, output=${tokens.output}, completed=${isCompleted}`);
|
|
411
504
|
const contextSize = tokens.input + tokens.cacheRead;
|
|
412
505
|
const contextLimit = pinnedMessageManager.getContextLimit();
|
|
506
|
+
// Skip non-completed messages with zero context: a new assistant message
|
|
507
|
+
// starts with tokens={input:0, ...} which would overwrite valid context
|
|
508
|
+
// from the previous step. Only accept zeros from completed messages.
|
|
509
|
+
if (!isCompleted && contextSize === 0) {
|
|
510
|
+
logger.debug("[Bot] Skipping zero-token intermediate update");
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
// Update both keyboard and pinned state in memory (keeps them in sync)
|
|
413
514
|
if (contextLimit > 0) {
|
|
414
515
|
keyboardManager.updateContext(contextSize, contextLimit);
|
|
415
516
|
}
|
|
416
|
-
|
|
517
|
+
pinnedMessageManager.updateTokensSilent(tokens);
|
|
518
|
+
// Full pinned message update (API call) only on completed messages
|
|
519
|
+
if (isCompleted) {
|
|
520
|
+
await pinnedMessageManager.onMessageComplete(tokens);
|
|
521
|
+
}
|
|
417
522
|
}
|
|
418
523
|
catch (err) {
|
|
419
524
|
logger.error("[Bot] Error updating pinned message with tokens:", err);
|
|
@@ -451,12 +556,16 @@ async function ensureEventSubscription(directory) {
|
|
|
451
556
|
const currentSession = getCurrentSession();
|
|
452
557
|
if (!currentSession || currentSession.id !== sessionId) {
|
|
453
558
|
responseStreamer.clearSession(sessionId, "session_error_not_current");
|
|
559
|
+
toolCallStreamer.clearSession(sessionId, "session_error_not_current");
|
|
454
560
|
foregroundSessionState.markIdle(sessionId);
|
|
455
561
|
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
456
562
|
return;
|
|
457
563
|
}
|
|
458
564
|
responseStreamer.clearSession(sessionId, "session_error");
|
|
459
|
-
await
|
|
565
|
+
await Promise.all([
|
|
566
|
+
toolMessageBatcher.flushSession(sessionId, "session_error"),
|
|
567
|
+
toolCallStreamer.flushSession(sessionId, "session_error"),
|
|
568
|
+
]);
|
|
460
569
|
const normalizedMessage = message.trim() || t("common.unknown_error");
|
|
461
570
|
const truncatedMessage = normalizedMessage.length > 3500
|
|
462
571
|
? `${normalizedMessage.slice(0, 3497)}...`
|
|
@@ -482,7 +591,7 @@ async function ensureEventSubscription(directory) {
|
|
|
482
591
|
? `${normalizedMessage.slice(0, 3497)}...`
|
|
483
592
|
: normalizedMessage;
|
|
484
593
|
const retryMessage = t("bot.session_retry", { message: truncatedMessage });
|
|
485
|
-
|
|
594
|
+
toolCallStreamer.replaceByPrefix(sessionId, SESSION_RETRY_PREFIX, retryMessage);
|
|
486
595
|
});
|
|
487
596
|
summaryAggregator.setOnSessionDiff(async (_sessionId, diffs) => {
|
|
488
597
|
if (!pinnedMessageManager.isInitialized()) {
|
|
@@ -529,8 +638,6 @@ async function ensureEventSubscription(directory) {
|
|
|
529
638
|
}
|
|
530
639
|
export function createBot() {
|
|
531
640
|
clearAllInteractionState("bot_startup");
|
|
532
|
-
toolMessageBatcher.setIntervalSeconds(config.bot.serviceMessagesIntervalSec);
|
|
533
|
-
logger.debug(`[ToolBatcher] Service messages interval: ${config.bot.serviceMessagesIntervalSec}s`);
|
|
534
641
|
const botOptions = {};
|
|
535
642
|
if (config.telegram.proxyUrl) {
|
|
536
643
|
const proxyUrl = config.telegram.proxyUrl;
|
|
@@ -70,9 +70,10 @@ export class ResponseStreamer {
|
|
|
70
70
|
this.ensureTimer(state);
|
|
71
71
|
}
|
|
72
72
|
async complete(sessionId, messageId, payload, options) {
|
|
73
|
+
const notStreamed = { streamed: false, telegramMessageIds: [] };
|
|
73
74
|
const state = this.states.get(buildStateKey(sessionId, messageId));
|
|
74
75
|
if (!state) {
|
|
75
|
-
return
|
|
76
|
+
return notStreamed;
|
|
76
77
|
}
|
|
77
78
|
if (payload) {
|
|
78
79
|
const normalizedPayload = normalizePayload(payload);
|
|
@@ -86,12 +87,12 @@ export class ResponseStreamer {
|
|
|
86
87
|
await this.cleanupBrokenStream(state, "complete_broken_stream");
|
|
87
88
|
this.cancelState(state);
|
|
88
89
|
this.states.delete(state.key);
|
|
89
|
-
return
|
|
90
|
+
return notStreamed;
|
|
90
91
|
}
|
|
91
92
|
if (state.telegramMessageIds.length === 0) {
|
|
92
93
|
this.cancelState(state);
|
|
93
94
|
this.states.delete(state.key);
|
|
94
|
-
return
|
|
95
|
+
return notStreamed;
|
|
95
96
|
}
|
|
96
97
|
let synced = true;
|
|
97
98
|
if (options?.flushFinal !== false) {
|
|
@@ -100,9 +101,10 @@ export class ResponseStreamer {
|
|
|
100
101
|
await this.cleanupBrokenStream(state, "final_sync_failed_cleanup");
|
|
101
102
|
}
|
|
102
103
|
}
|
|
104
|
+
const messageIds = [...state.telegramMessageIds];
|
|
103
105
|
this.cancelState(state);
|
|
104
106
|
this.states.delete(state.key);
|
|
105
|
-
return synced;
|
|
107
|
+
return { streamed: synced, telegramMessageIds: messageIds };
|
|
106
108
|
}
|
|
107
109
|
clearMessage(sessionId, messageId, reason) {
|
|
108
110
|
const key = buildStateKey(sessionId, messageId);
|