@grinev/opencode-telegram-bot 0.14.1 → 0.16.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 +25 -0
- package/README.md +50 -26
- package/dist/agent/manager.js +34 -6
- package/dist/agent/types.js +7 -1
- package/dist/app/start-bot-app.js +6 -1
- package/dist/bot/assistant-run-state.js +60 -0
- package/dist/bot/commands/abort.js +2 -0
- package/dist/bot/commands/commands.js +12 -2
- package/dist/bot/commands/definitions.js +2 -0
- package/dist/bot/commands/new.js +3 -2
- package/dist/bot/commands/open.js +314 -0
- package/dist/bot/commands/projects.js +5 -37
- package/dist/bot/commands/sessions.js +3 -0
- package/dist/bot/commands/start.js +2 -0
- package/dist/bot/commands/status.js +5 -2
- package/dist/bot/commands/tts.js +13 -0
- package/dist/bot/handlers/agent.js +3 -4
- package/dist/bot/handlers/inline-menu.js +9 -1
- package/dist/bot/handlers/model.js +3 -2
- package/dist/bot/handlers/prompt.js +33 -5
- package/dist/bot/handlers/variant.js +3 -2
- package/dist/bot/index.js +179 -94
- package/dist/bot/message-patterns.js +2 -2
- package/dist/bot/streaming/response-streamer.js +26 -17
- package/dist/bot/streaming/tool-call-streamer.js +31 -24
- package/dist/bot/utils/assistant-rendering.js +117 -0
- package/dist/bot/utils/assistant-run-footer.js +9 -0
- package/dist/bot/utils/browser-roots.js +140 -0
- package/dist/bot/utils/file-tree.js +92 -0
- package/dist/bot/utils/finalize-assistant-response.js +18 -24
- package/dist/bot/utils/keyboard.js +6 -6
- package/dist/bot/utils/send-tts-response.js +37 -0
- package/dist/bot/utils/send-with-markdown-fallback.js +3 -3
- package/dist/bot/utils/switch-project.js +48 -0
- package/dist/bot/utils/telegram-text.js +95 -1
- package/dist/cli.js +4 -0
- package/dist/config.js +10 -0
- package/dist/i18n/de.js +32 -9
- package/dist/i18n/en.js +32 -9
- package/dist/i18n/es.js +32 -9
- package/dist/i18n/fr.js +32 -9
- package/dist/i18n/ru.js +32 -9
- package/dist/i18n/zh.js +32 -9
- package/dist/index.js +2 -0
- package/dist/pinned/manager.js +66 -4
- package/dist/project/manager.js +2 -1
- package/dist/settings/manager.js +7 -0
- package/dist/summary/aggregator.js +17 -1
- package/dist/summary/formatter.js +12 -89
- package/dist/telegram/render/block-fallback.js +28 -0
- package/dist/telegram/render/block-parser.js +295 -0
- package/dist/telegram/render/block-renderer.js +457 -0
- package/dist/telegram/render/chunker.js +281 -0
- package/dist/telegram/render/inline-renderer.js +128 -0
- package/dist/telegram/render/markdown-normalizer.js +94 -0
- package/dist/telegram/render/pipeline.js +9 -0
- package/dist/telegram/render/types.js +1 -0
- package/dist/telegram/render/validator.js +160 -0
- package/dist/tts/client.js +59 -0
- package/dist/utils/logger.js +200 -73
- package/package.json +6 -2
package/dist/bot/index.js
CHANGED
|
@@ -16,6 +16,7 @@ import { AGENT_MODE_BUTTON_TEXT_PATTERN, MODEL_BUTTON_TEXT_PATTERN, VARIANT_BUTT
|
|
|
16
16
|
import { sessionsCommand, handleSessionSelect } from "./commands/sessions.js";
|
|
17
17
|
import { newCommand } from "./commands/new.js";
|
|
18
18
|
import { projectsCommand, handleProjectSelect } from "./commands/projects.js";
|
|
19
|
+
import { openCommand, handleOpenCallback, clearOpenPathIndex } from "./commands/open.js";
|
|
19
20
|
import { abortCommand } from "./commands/abort.js";
|
|
20
21
|
import { opencodeStartCommand } from "./commands/opencode-start.js";
|
|
21
22
|
import { opencodeStopCommand } from "./commands/opencode-stop.js";
|
|
@@ -23,6 +24,7 @@ import { renameCommand, handleRenameCancel, handleRenameTextAnswer } from "./com
|
|
|
23
24
|
import { handleTaskCallback, handleTaskTextInput, taskCommand } from "./commands/task.js";
|
|
24
25
|
import { handleTaskListCallback, taskListCommand } from "./commands/tasklist.js";
|
|
25
26
|
import { commandsCommand, handleCommandsCallback, handleCommandTextArguments, } from "./commands/commands.js";
|
|
27
|
+
import { ttsCommand } from "./commands/tts.js";
|
|
26
28
|
import { handleQuestionCallback, showCurrentQuestion, handleQuestionTextAnswer, } from "./handlers/question.js";
|
|
27
29
|
import { handlePermissionCallback, showPermissionRequest } from "./handlers/permission.js";
|
|
28
30
|
import { handleAgentSelect, showAgentSelectionMenu } from "./handlers/agent.js";
|
|
@@ -36,7 +38,7 @@ import { clearAllInteractionState } from "../interaction/cleanup.js";
|
|
|
36
38
|
import { keyboardManager } from "../keyboard/manager.js";
|
|
37
39
|
import { subscribeToEvents } from "../opencode/events.js";
|
|
38
40
|
import { summaryAggregator } from "../summary/aggregator.js";
|
|
39
|
-
import {
|
|
41
|
+
import { formatToolInfo } from "../summary/formatter.js";
|
|
40
42
|
import { renderSubagentCards } from "../summary/subagent-formatter.js";
|
|
41
43
|
import { ToolMessageBatcher } from "../summary/tool-message-batcher.js";
|
|
42
44
|
import { getCurrentSession } from "../session/manager.js";
|
|
@@ -46,20 +48,23 @@ import { safeBackgroundTask } from "../utils/safe-background-task.js";
|
|
|
46
48
|
import { withTelegramRateLimitRetry } from "../utils/telegram-rate-limit-retry.js";
|
|
47
49
|
import { pinnedMessageManager } from "../pinned/manager.js";
|
|
48
50
|
import { t } from "../i18n/index.js";
|
|
49
|
-
import { processUserPrompt } from "./handlers/prompt.js";
|
|
51
|
+
import { clearPromptResponseMode, processUserPrompt } from "./handlers/prompt.js";
|
|
50
52
|
import { handleVoiceMessage } from "./handlers/voice.js";
|
|
51
53
|
import { handleDocumentMessage } from "./handlers/document.js";
|
|
52
54
|
import { downloadTelegramFile, toDataUri } from "./utils/file-download.js";
|
|
53
55
|
import { finalizeAssistantResponse } from "./utils/finalize-assistant-response.js";
|
|
56
|
+
import { sendTtsResponseForSession } from "./utils/send-tts-response.js";
|
|
54
57
|
import { deliverThinkingMessage } from "./utils/thinking-message.js";
|
|
55
|
-
import {
|
|
58
|
+
import { editRenderedBotPart, getTelegramRenderedPartSignature, sendRenderedBotPart, } from "./utils/telegram-text.js";
|
|
59
|
+
import { formatAssistantRunFooter } from "./utils/assistant-run-footer.js";
|
|
56
60
|
import { getModelCapabilities, supportsInput } from "../model/capabilities.js";
|
|
57
61
|
import { getStoredModel } from "../model/manager.js";
|
|
58
62
|
import { foregroundSessionState } from "../scheduled-task/foreground-state.js";
|
|
59
63
|
import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
|
|
64
|
+
import { assistantRunState } from "./assistant-run-state.js";
|
|
60
65
|
import { ResponseStreamer } from "./streaming/response-streamer.js";
|
|
61
66
|
import { ToolCallStreamer } from "./streaming/tool-call-streamer.js";
|
|
62
|
-
import {
|
|
67
|
+
import { prepareAssistantFinalStreamingPayload, prepareAssistantStreamingPayload, renderAssistantFinalPartsSafe, } from "./utils/assistant-rendering.js";
|
|
63
68
|
let botInstance = null;
|
|
64
69
|
let chatIdInstance = null;
|
|
65
70
|
let commandsInitialized = false;
|
|
@@ -71,6 +76,7 @@ const SUBAGENT_STREAM_PREFIX = "🧩";
|
|
|
71
76
|
const __filename = fileURLToPath(import.meta.url);
|
|
72
77
|
const __dirname = path.dirname(__filename);
|
|
73
78
|
const TEMP_DIR = path.join(__dirname, "..", ".tmp");
|
|
79
|
+
const sessionCompletionTasks = new Map();
|
|
74
80
|
function getCurrentReplyKeyboard() {
|
|
75
81
|
if (!keyboardManager.isInitialized()) {
|
|
76
82
|
return undefined;
|
|
@@ -88,14 +94,23 @@ function prepareDocumentCaption(caption) {
|
|
|
88
94
|
return `${normalizedCaption.slice(0, TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH - 3)}...`;
|
|
89
95
|
}
|
|
90
96
|
function prepareStreamingPayload(messageText) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
return prepareAssistantStreamingPayload(messageText, RESPONSE_STREAM_TEXT_LIMIT);
|
|
98
|
+
}
|
|
99
|
+
function prepareFinalStreamingPayload(messageText) {
|
|
100
|
+
return prepareAssistantFinalStreamingPayload(messageText, RESPONSE_STREAM_TEXT_LIMIT);
|
|
101
|
+
}
|
|
102
|
+
function enqueueSessionCompletionTask(sessionId, task) {
|
|
103
|
+
const previousTask = sessionCompletionTasks.get(sessionId) ?? Promise.resolve();
|
|
104
|
+
const nextTask = previousTask
|
|
105
|
+
.catch(() => undefined)
|
|
106
|
+
.then(task)
|
|
107
|
+
.finally(() => {
|
|
108
|
+
if (sessionCompletionTasks.get(sessionId) === nextTask) {
|
|
109
|
+
sessionCompletionTasks.delete(sessionId);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
sessionCompletionTasks.set(sessionId, nextTask);
|
|
113
|
+
return nextTask;
|
|
99
114
|
}
|
|
100
115
|
const toolMessageBatcher = new ToolMessageBatcher({
|
|
101
116
|
sendText: async (sessionId, text) => {
|
|
@@ -139,39 +154,36 @@ const toolMessageBatcher = new ToolMessageBatcher({
|
|
|
139
154
|
});
|
|
140
155
|
const responseStreamer = new ResponseStreamer({
|
|
141
156
|
throttleMs: RESPONSE_STREAM_THROTTLE_MS,
|
|
142
|
-
|
|
157
|
+
sendPart: async (part, options) => {
|
|
143
158
|
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
144
159
|
throw new Error("Bot context missing for streamed send");
|
|
145
160
|
}
|
|
146
|
-
|
|
147
|
-
const sentMessage = await sendMessageWithMarkdownFallback({
|
|
161
|
+
return sendRenderedBotPart({
|
|
148
162
|
api: botInstance.api,
|
|
149
163
|
chatId: chatIdInstance,
|
|
150
|
-
|
|
164
|
+
part,
|
|
151
165
|
options,
|
|
152
|
-
parseMode,
|
|
153
166
|
});
|
|
154
|
-
return sentMessage.message_id;
|
|
155
167
|
},
|
|
156
|
-
|
|
168
|
+
editPart: async (messageId, part, options) => {
|
|
157
169
|
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
158
170
|
throw new Error("Bot context missing for streamed edit");
|
|
159
171
|
}
|
|
160
|
-
const parseMode = format === "markdown_v2" ? "MarkdownV2" : undefined;
|
|
161
172
|
try {
|
|
162
|
-
await
|
|
173
|
+
return await editRenderedBotPart({
|
|
163
174
|
api: botInstance.api,
|
|
164
175
|
chatId: chatIdInstance,
|
|
165
176
|
messageId,
|
|
166
|
-
|
|
177
|
+
part,
|
|
167
178
|
options,
|
|
168
|
-
parseMode,
|
|
169
179
|
});
|
|
170
180
|
}
|
|
171
181
|
catch (error) {
|
|
172
182
|
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
173
183
|
if (errorMessage.includes("message is not modified")) {
|
|
174
|
-
return
|
|
184
|
+
return {
|
|
185
|
+
deliveredSignature: getTelegramRenderedPartSignature(part),
|
|
186
|
+
};
|
|
175
187
|
}
|
|
176
188
|
throw error;
|
|
177
189
|
}
|
|
@@ -242,6 +254,12 @@ const toolCallStreamer = new ToolCallStreamer({
|
|
|
242
254
|
});
|
|
243
255
|
},
|
|
244
256
|
});
|
|
257
|
+
function getToolStreamKey(tool) {
|
|
258
|
+
if (tool === "todowrite") {
|
|
259
|
+
return "todo";
|
|
260
|
+
}
|
|
261
|
+
return "default";
|
|
262
|
+
}
|
|
245
263
|
async function ensureCommandsInitialized(ctx, next) {
|
|
246
264
|
if (commandsInitialized || !ctx.from || ctx.from.id !== config.telegram.allowedUserId) {
|
|
247
265
|
await next();
|
|
@@ -290,75 +308,82 @@ async function ensureEventSubscription(directory) {
|
|
|
290
308
|
if (!preparedStreamPayload) {
|
|
291
309
|
return;
|
|
292
310
|
}
|
|
311
|
+
// Reply keyboards make the first streamed message non-editable in Telegram,
|
|
312
|
+
// so partial chunks must be sent without reply_markup and finalized later.
|
|
293
313
|
preparedStreamPayload.sendOptions = { disable_notification: true };
|
|
294
314
|
preparedStreamPayload.editOptions = undefined;
|
|
295
315
|
responseStreamer.enqueue(sessionId, messageId, preparedStreamPayload);
|
|
296
316
|
});
|
|
297
|
-
summaryAggregator.setOnComplete(
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
317
|
+
summaryAggregator.setOnComplete((sessionId, messageId, messageText, completionInfo) => {
|
|
318
|
+
void enqueueSessionCompletionTask(sessionId, async () => {
|
|
319
|
+
if (!botInstance || !chatIdInstance) {
|
|
320
|
+
logger.error("Bot or chat ID not available for sending message");
|
|
321
|
+
clearPromptResponseMode(sessionId);
|
|
322
|
+
responseStreamer.clearMessage(sessionId, messageId, "bot_context_missing");
|
|
323
|
+
toolCallStreamer.clearSession(sessionId, "bot_context_missing");
|
|
324
|
+
assistantRunState.clearRun(sessionId, "bot_context_missing");
|
|
325
|
+
foregroundSessionState.markIdle(sessionId);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
const currentSession = getCurrentSession();
|
|
329
|
+
if (currentSession?.id !== sessionId) {
|
|
330
|
+
clearPromptResponseMode(sessionId);
|
|
331
|
+
responseStreamer.clearMessage(sessionId, messageId, "session_mismatch");
|
|
332
|
+
toolCallStreamer.clearSession(sessionId, "session_mismatch");
|
|
333
|
+
assistantRunState.clearRun(sessionId, "session_mismatch");
|
|
334
|
+
foregroundSessionState.markIdle(sessionId);
|
|
335
|
+
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
const botApi = botInstance.api;
|
|
339
|
+
const chatId = chatIdInstance;
|
|
340
|
+
try {
|
|
341
|
+
assistantRunState.markResponseCompleted(sessionId, {
|
|
342
|
+
agent: completionInfo.agent,
|
|
343
|
+
providerID: completionInfo.providerID,
|
|
344
|
+
modelID: completionInfo.modelID,
|
|
345
|
+
});
|
|
346
|
+
await finalizeAssistantResponse({
|
|
347
|
+
sessionId,
|
|
348
|
+
messageId,
|
|
349
|
+
messageText,
|
|
350
|
+
responseStreamer,
|
|
351
|
+
flushPendingServiceMessages: () => Promise.all([
|
|
352
|
+
toolMessageBatcher.flushSession(sessionId, "assistant_message_completed"),
|
|
353
|
+
toolCallStreamer.breakSession(sessionId, "assistant_message_completed"),
|
|
354
|
+
]).then(() => undefined),
|
|
355
|
+
prepareStreamingPayload: prepareFinalStreamingPayload,
|
|
356
|
+
renderFinalParts: (text) => renderAssistantFinalPartsSafe(text),
|
|
357
|
+
getReplyKeyboard: getCurrentReplyKeyboard,
|
|
358
|
+
sendRenderedPart: async (part, options) => {
|
|
359
|
+
await sendRenderedBotPart({
|
|
360
|
+
api: botApi,
|
|
361
|
+
chatId,
|
|
362
|
+
part,
|
|
363
|
+
options: options,
|
|
364
|
+
});
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
await sendTtsResponseForSession({
|
|
368
|
+
api: botApi,
|
|
369
|
+
sessionId,
|
|
370
|
+
chatId,
|
|
371
|
+
text: messageText,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
catch (err) {
|
|
375
|
+
clearPromptResponseMode(sessionId);
|
|
376
|
+
assistantRunState.clearRun(sessionId, "assistant_finalize_failed");
|
|
377
|
+
logger.error("Failed to send message to Telegram:", err);
|
|
378
|
+
// Stop processing events after critical error to prevent infinite loop
|
|
379
|
+
logger.error("[Bot] CRITICAL: Stopping event processing due to error");
|
|
380
|
+
summaryAggregator.clear();
|
|
381
|
+
foregroundSessionState.markIdle(sessionId);
|
|
382
|
+
}
|
|
383
|
+
finally {
|
|
384
|
+
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
385
|
+
}
|
|
386
|
+
});
|
|
362
387
|
});
|
|
363
388
|
summaryAggregator.setOnTool(async (toolInfo) => {
|
|
364
389
|
if (!botInstance || !chatIdInstance) {
|
|
@@ -379,7 +404,7 @@ async function ensureEventSubscription(directory) {
|
|
|
379
404
|
try {
|
|
380
405
|
const message = formatToolInfo(toolInfo);
|
|
381
406
|
if (message) {
|
|
382
|
-
toolCallStreamer.append(toolInfo.sessionId, message);
|
|
407
|
+
toolCallStreamer.append(toolInfo.sessionId, message, getToolStreamKey(toolInfo.tool));
|
|
383
408
|
}
|
|
384
409
|
}
|
|
385
410
|
catch (err) {
|
|
@@ -402,7 +427,7 @@ async function ensureEventSubscription(directory) {
|
|
|
402
427
|
if (!renderedCards) {
|
|
403
428
|
return;
|
|
404
429
|
}
|
|
405
|
-
toolCallStreamer.replaceByPrefix(sessionId, SUBAGENT_STREAM_PREFIX, renderedCards);
|
|
430
|
+
toolCallStreamer.replaceByPrefix(sessionId, SUBAGENT_STREAM_PREFIX, renderedCards, "subagent");
|
|
406
431
|
}
|
|
407
432
|
catch (err) {
|
|
408
433
|
logger.error("Failed to render subagent activity for Telegram:", err);
|
|
@@ -551,20 +576,70 @@ async function ensureEventSubscription(directory) {
|
|
|
551
576
|
logger.error("[Bot] Error reloading context after compaction:", err);
|
|
552
577
|
}
|
|
553
578
|
});
|
|
579
|
+
summaryAggregator.setOnSessionIdle(async (sessionId) => {
|
|
580
|
+
await sessionCompletionTasks.get(sessionId)?.catch(() => undefined);
|
|
581
|
+
const completedRun = assistantRunState.finishRun(sessionId, "session_idle");
|
|
582
|
+
clearPromptResponseMode(sessionId);
|
|
583
|
+
if (!botInstance || !chatIdInstance) {
|
|
584
|
+
foregroundSessionState.markIdle(sessionId);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const currentSession = getCurrentSession();
|
|
588
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
589
|
+
foregroundSessionState.markIdle(sessionId);
|
|
590
|
+
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
try {
|
|
594
|
+
await Promise.all([
|
|
595
|
+
toolMessageBatcher.flushSession(sessionId, "session_idle"),
|
|
596
|
+
toolCallStreamer.flushSession(sessionId, "session_idle"),
|
|
597
|
+
]);
|
|
598
|
+
if (completedRun?.hasCompletedResponse) {
|
|
599
|
+
const agent = completedRun.actualAgent || completedRun.configuredAgent;
|
|
600
|
+
const providerID = completedRun.actualProviderID || completedRun.configuredProviderID;
|
|
601
|
+
const modelID = completedRun.actualModelID || completedRun.configuredModelID;
|
|
602
|
+
if (agent && providerID && modelID) {
|
|
603
|
+
const keyboard = getCurrentReplyKeyboard();
|
|
604
|
+
await botInstance.api.sendMessage(chatIdInstance, formatAssistantRunFooter({
|
|
605
|
+
agent,
|
|
606
|
+
providerID,
|
|
607
|
+
modelID,
|
|
608
|
+
elapsedMs: Date.now() - completedRun.startedAt,
|
|
609
|
+
}), {
|
|
610
|
+
...(keyboard ? { reply_markup: keyboard } : {}),
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
catch (err) {
|
|
616
|
+
logger.error("[Bot] Failed to send session idle footer:", err);
|
|
617
|
+
}
|
|
618
|
+
finally {
|
|
619
|
+
foregroundSessionState.markIdle(sessionId);
|
|
620
|
+
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
621
|
+
}
|
|
622
|
+
});
|
|
554
623
|
summaryAggregator.setOnSessionError(async (sessionId, message) => {
|
|
555
624
|
if (!botInstance || !chatIdInstance) {
|
|
625
|
+
clearPromptResponseMode(sessionId);
|
|
626
|
+
assistantRunState.clearRun(sessionId, "session_error_no_bot_context");
|
|
556
627
|
foregroundSessionState.markIdle(sessionId);
|
|
557
628
|
return;
|
|
558
629
|
}
|
|
559
630
|
const currentSession = getCurrentSession();
|
|
560
631
|
if (!currentSession || currentSession.id !== sessionId) {
|
|
632
|
+
clearPromptResponseMode(sessionId);
|
|
561
633
|
responseStreamer.clearSession(sessionId, "session_error_not_current");
|
|
562
634
|
toolCallStreamer.clearSession(sessionId, "session_error_not_current");
|
|
635
|
+
assistantRunState.clearRun(sessionId, "session_error_not_current");
|
|
563
636
|
foregroundSessionState.markIdle(sessionId);
|
|
564
637
|
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
565
638
|
return;
|
|
566
639
|
}
|
|
567
640
|
responseStreamer.clearSession(sessionId, "session_error");
|
|
641
|
+
clearPromptResponseMode(sessionId);
|
|
642
|
+
assistantRunState.clearRun(sessionId, "session_error");
|
|
568
643
|
await Promise.all([
|
|
569
644
|
toolMessageBatcher.flushSession(sessionId, "session_error"),
|
|
570
645
|
toolCallStreamer.flushSession(sessionId, "session_error"),
|
|
@@ -641,6 +716,8 @@ async function ensureEventSubscription(directory) {
|
|
|
641
716
|
}
|
|
642
717
|
export function createBot() {
|
|
643
718
|
clearAllInteractionState("bot_startup");
|
|
719
|
+
sessionCompletionTasks.clear();
|
|
720
|
+
assistantRunState.clearAll("bot_startup");
|
|
644
721
|
const botOptions = {};
|
|
645
722
|
if (config.telegram.proxyUrl) {
|
|
646
723
|
const proxyUrl = config.telegram.proxyUrl;
|
|
@@ -712,9 +789,11 @@ export function createBot() {
|
|
|
712
789
|
bot.command("start", startCommand);
|
|
713
790
|
bot.command("help", helpCommand);
|
|
714
791
|
bot.command("status", statusCommand);
|
|
792
|
+
bot.command("tts", ttsCommand);
|
|
715
793
|
bot.command("opencode_start", opencodeStartCommand);
|
|
716
794
|
bot.command("opencode_stop", opencodeStopCommand);
|
|
717
795
|
bot.command("projects", projectsCommand);
|
|
796
|
+
bot.command("open", openCommand);
|
|
718
797
|
bot.command("sessions", sessionsCommand);
|
|
719
798
|
bot.command("new", newCommand);
|
|
720
799
|
bot.command("abort", abortCommand);
|
|
@@ -732,8 +811,13 @@ export function createBot() {
|
|
|
732
811
|
}
|
|
733
812
|
try {
|
|
734
813
|
const handledInlineCancel = await handleInlineMenuCancel(ctx);
|
|
814
|
+
if (handledInlineCancel) {
|
|
815
|
+
// Clean up path index when the open-directory menu is cancelled
|
|
816
|
+
clearOpenPathIndex();
|
|
817
|
+
}
|
|
735
818
|
const handledSession = await handleSessionSelect(ctx);
|
|
736
819
|
const handledProject = await handleProjectSelect(ctx);
|
|
820
|
+
const handledOpen = await handleOpenCallback(ctx);
|
|
737
821
|
const handledQuestion = await handleQuestionCallback(ctx);
|
|
738
822
|
const handledPermission = await handlePermissionCallback(ctx);
|
|
739
823
|
const handledAgent = await handleAgentSelect(ctx);
|
|
@@ -744,10 +828,11 @@ export function createBot() {
|
|
|
744
828
|
const handledTaskList = await handleTaskListCallback(ctx);
|
|
745
829
|
const handledRenameCancel = await handleRenameCancel(ctx);
|
|
746
830
|
const handledCommands = await handleCommandsCallback(ctx, { bot, ensureEventSubscription });
|
|
747
|
-
logger.debug(`[Bot] Callback handled: inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, task=${handledTask}, taskList=${handledTaskList}, rename=${handledRenameCancel}, commands=${handledCommands}`);
|
|
831
|
+
logger.debug(`[Bot] Callback handled: inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, open=${handledOpen}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, task=${handledTask}, taskList=${handledTaskList}, rename=${handledRenameCancel}, commands=${handledCommands}`);
|
|
748
832
|
if (!handledInlineCancel &&
|
|
749
833
|
!handledSession &&
|
|
750
834
|
!handledProject &&
|
|
835
|
+
!handledOpen &&
|
|
751
836
|
!handledQuestion &&
|
|
752
837
|
!handledPermission &&
|
|
753
838
|
!handledAgent &&
|
|
@@ -768,9 +853,9 @@ export function createBot() {
|
|
|
768
853
|
await ctx.answerCallbackQuery({ text: t("callback.processing_error") }).catch(() => { });
|
|
769
854
|
}
|
|
770
855
|
});
|
|
771
|
-
// Handle Reply Keyboard button press (agent
|
|
856
|
+
// Handle Reply Keyboard button press (agent indicator)
|
|
772
857
|
bot.hears(AGENT_MODE_BUTTON_TEXT_PATTERN, async (ctx) => {
|
|
773
|
-
logger.debug(`[Bot] Agent
|
|
858
|
+
logger.debug(`[Bot] Agent button pressed: ${ctx.message?.text}`);
|
|
774
859
|
try {
|
|
775
860
|
if (await blockMenuWhileInteractionActive(ctx)) {
|
|
776
861
|
return;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const AGENT_MODE_BUTTON_TEXT_PATTERN = /^(📋|🛠️|💬|🔍|📝|📄|📦|🤖)\s.+\
|
|
2
|
-
export const MODEL_BUTTON_TEXT_PATTERN = /^🤖\s(?!.*\
|
|
1
|
+
export const AGENT_MODE_BUTTON_TEXT_PATTERN = /^(📋|🛠️|💬|🔍|📝|📄|📦|🤖)\s.+\s(?:Mode|Agent)$/;
|
|
2
|
+
export const MODEL_BUTTON_TEXT_PATTERN = /^🤖\s(?!.*\s(?:Mode|Agent)$)[\s\S]+$/;
|
|
3
3
|
// Keep support for both legacy "💭" and current "💡" prefix.
|
|
4
4
|
export const VARIANT_BUTTON_TEXT_PATTERN = /^(💡|💭)\s.+$/;
|
|
@@ -2,16 +2,22 @@ import { logger } from "../../utils/logger.js";
|
|
|
2
2
|
function buildStateKey(sessionId, messageId) {
|
|
3
3
|
return `${sessionId}:${messageId}`;
|
|
4
4
|
}
|
|
5
|
+
function clonePart(part) {
|
|
6
|
+
return {
|
|
7
|
+
text: part.text,
|
|
8
|
+
entities: part.entities ? [...part.entities] : undefined,
|
|
9
|
+
fallbackText: part.fallbackText,
|
|
10
|
+
source: part.source,
|
|
11
|
+
};
|
|
12
|
+
}
|
|
5
13
|
function normalizePayload(payload) {
|
|
6
|
-
const normalizedParts = payload.parts
|
|
7
|
-
.map((part) => part.trim())
|
|
8
|
-
.filter((part) => part.length > 0);
|
|
14
|
+
const normalizedParts = payload.parts.map(clonePart).filter((part) => part.text.length > 0);
|
|
9
15
|
if (normalizedParts.length === 0) {
|
|
16
|
+
logger.debug("[ResponseStreamer] Dropped empty streaming payload after normalization");
|
|
10
17
|
return null;
|
|
11
18
|
}
|
|
12
19
|
return {
|
|
13
20
|
parts: normalizedParts,
|
|
14
|
-
format: payload.format,
|
|
15
21
|
sendOptions: payload.sendOptions,
|
|
16
22
|
editOptions: payload.editOptions,
|
|
17
23
|
};
|
|
@@ -37,8 +43,8 @@ function getRetryAfterMs(error) {
|
|
|
37
43
|
}
|
|
38
44
|
return seconds * 1000;
|
|
39
45
|
}
|
|
40
|
-
function createSignature(
|
|
41
|
-
return `${
|
|
46
|
+
function createSignature(part) {
|
|
47
|
+
return `${part.text}\n${JSON.stringify(part.entities ?? null)}`;
|
|
42
48
|
}
|
|
43
49
|
function delay(ms) {
|
|
44
50
|
return new Promise((resolve) => {
|
|
@@ -47,14 +53,14 @@ function delay(ms) {
|
|
|
47
53
|
}
|
|
48
54
|
export class ResponseStreamer {
|
|
49
55
|
throttleMs;
|
|
50
|
-
|
|
51
|
-
|
|
56
|
+
sendPart;
|
|
57
|
+
editPart;
|
|
52
58
|
deleteText;
|
|
53
59
|
states = new Map();
|
|
54
60
|
constructor(options) {
|
|
55
61
|
this.throttleMs = Math.max(0, Math.floor(options.throttleMs));
|
|
56
|
-
this.
|
|
57
|
-
this.
|
|
62
|
+
this.sendPart = options.sendPart;
|
|
63
|
+
this.editPart = options.editPart;
|
|
58
64
|
this.deleteText = options.deleteText;
|
|
59
65
|
}
|
|
60
66
|
enqueue(sessionId, messageId, payload) {
|
|
@@ -73,6 +79,7 @@ export class ResponseStreamer {
|
|
|
73
79
|
const notStreamed = { streamed: false, telegramMessageIds: [] };
|
|
74
80
|
const state = this.states.get(buildStateKey(sessionId, messageId));
|
|
75
81
|
if (!state) {
|
|
82
|
+
logger.debug(`[ResponseStreamer] Complete skipped, no active stream state: session=${sessionId}, message=${messageId}`);
|
|
76
83
|
return notStreamed;
|
|
77
84
|
}
|
|
78
85
|
if (payload) {
|
|
@@ -90,6 +97,7 @@ export class ResponseStreamer {
|
|
|
90
97
|
return notStreamed;
|
|
91
98
|
}
|
|
92
99
|
if (state.telegramMessageIds.length === 0) {
|
|
100
|
+
logger.debug(`[ResponseStreamer] Complete returned not streamed: session=${sessionId}, message=${messageId}, reason=no_visible_partials`);
|
|
93
101
|
this.cancelState(state);
|
|
94
102
|
this.states.delete(state.key);
|
|
95
103
|
return notStreamed;
|
|
@@ -204,10 +212,11 @@ export class ResponseStreamer {
|
|
|
204
212
|
if (!payload) {
|
|
205
213
|
return state.telegramMessageIds.length > 0;
|
|
206
214
|
}
|
|
207
|
-
const targetSignatures = payload.parts.map((part) => createSignature(part
|
|
215
|
+
const targetSignatures = payload.parts.map((part) => createSignature(part));
|
|
208
216
|
const unchanged = targetSignatures.length === state.lastSentSignatures.length &&
|
|
209
217
|
targetSignatures.every((signature, index) => signature === state.lastSentSignatures[index]);
|
|
210
218
|
if (unchanged) {
|
|
219
|
+
logger.debug(`[ResponseStreamer] Skipped unchanged payload: session=${state.sessionId}, message=${state.messageId}, parts=${payload.parts.length}`);
|
|
211
220
|
return state.telegramMessageIds.length > 0;
|
|
212
221
|
}
|
|
213
222
|
try {
|
|
@@ -259,20 +268,20 @@ export class ResponseStreamer {
|
|
|
259
268
|
}
|
|
260
269
|
async syncMessages(state, payload, targetSignatures) {
|
|
261
270
|
for (let index = 0; index < payload.parts.length; index++) {
|
|
262
|
-
const
|
|
271
|
+
const part = payload.parts[index];
|
|
263
272
|
const nextSignature = targetSignatures[index];
|
|
264
273
|
const currentMessageId = state.telegramMessageIds[index];
|
|
265
274
|
if (currentMessageId) {
|
|
266
275
|
if (state.lastSentSignatures[index] === nextSignature) {
|
|
267
276
|
continue;
|
|
268
277
|
}
|
|
269
|
-
await this.
|
|
270
|
-
state.lastSentSignatures[index] =
|
|
278
|
+
const result = await this.editPart(currentMessageId, part, payload.editOptions);
|
|
279
|
+
state.lastSentSignatures[index] = result.deliveredSignature;
|
|
271
280
|
continue;
|
|
272
281
|
}
|
|
273
|
-
const
|
|
274
|
-
state.telegramMessageIds[index] = messageId;
|
|
275
|
-
state.lastSentSignatures[index] =
|
|
282
|
+
const result = await this.sendPart(part, payload.sendOptions);
|
|
283
|
+
state.telegramMessageIds[index] = result.messageId;
|
|
284
|
+
state.lastSentSignatures[index] = result.deliveredSignature;
|
|
276
285
|
}
|
|
277
286
|
for (let index = state.telegramMessageIds.length - 1; index >= payload.parts.length; index--) {
|
|
278
287
|
const messageId = state.telegramMessageIds[index];
|