@grinev/opencode-telegram-bot 0.15.0 → 0.16.1
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 +16 -0
- package/README.md +22 -1
- package/dist/app/start-bot-app.js +82 -7
- package/dist/bot/assistant-run-state.js +60 -0
- package/dist/bot/commands/abort.js +2 -0
- package/dist/bot/commands/commands.js +10 -0
- package/dist/bot/commands/definitions.js +1 -0
- package/dist/bot/commands/open.js +314 -0
- package/dist/bot/commands/projects.js +5 -38
- package/dist/bot/commands/start.js +2 -0
- package/dist/bot/handlers/inline-menu.js +9 -1
- package/dist/bot/handlers/prompt.js +11 -0
- package/dist/bot/index.js +187 -100
- package/dist/bot/streaming/response-streamer.js +26 -17
- 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/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/args.js +36 -7
- package/dist/cli.js +133 -15
- package/dist/config.js +4 -0
- package/dist/i18n/de.js +15 -10
- package/dist/i18n/en.js +15 -10
- package/dist/i18n/es.js +15 -10
- package/dist/i18n/fr.js +15 -10
- package/dist/i18n/ru.js +15 -10
- package/dist/i18n/zh.js +15 -10
- package/dist/index.js +2 -0
- package/dist/project/manager.js +2 -1
- package/dist/scheduled-task/runtime.js +8 -0
- package/dist/service/manager.js +244 -0
- package/dist/service/runtime.js +19 -0
- package/dist/service/types.js +1 -0
- package/dist/summary/aggregator.js +17 -1
- package/dist/summary/formatter.js +2 -88
- 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/utils/logger.js +200 -73
- package/package.json +6 -2
|
@@ -1,17 +1,10 @@
|
|
|
1
1
|
import { InlineKeyboard } from "grammy";
|
|
2
|
-
import {
|
|
2
|
+
import { getCurrentProject } from "../../settings/manager.js";
|
|
3
3
|
import { getProjects } from "../../project/manager.js";
|
|
4
4
|
import { syncSessionDirectoryCache } from "../../session/cache-manager.js";
|
|
5
|
-
import { clearSession } from "../../session/manager.js";
|
|
6
|
-
import { summaryAggregator } from "../../summary/aggregator.js";
|
|
7
|
-
import { pinnedMessageManager } from "../../pinned/manager.js";
|
|
8
|
-
import { keyboardManager } from "../../keyboard/manager.js";
|
|
9
|
-
import { getStoredAgent, resolveProjectAgent } from "../../agent/manager.js";
|
|
10
|
-
import { getStoredModel } from "../../model/manager.js";
|
|
11
|
-
import { formatVariantForButton } from "../../variant/manager.js";
|
|
12
|
-
import { clearAllInteractionState } from "../../interaction/cleanup.js";
|
|
13
|
-
import { createMainKeyboard } from "../utils/keyboard.js";
|
|
14
5
|
import { appendInlineMenuCancelButton, ensureActiveInlineMenu, replyWithInlineMenu, } from "../handlers/inline-menu.js";
|
|
6
|
+
import { switchToProject } from "../utils/switch-project.js";
|
|
7
|
+
import { clearAllInteractionState } from "../../interaction/cleanup.js";
|
|
15
8
|
import { isForegroundBusy, replyBusyBlocked } from "../utils/busy-guard.js";
|
|
16
9
|
import { logger } from "../../utils/logger.js";
|
|
17
10
|
import { t } from "../../i18n/index.js";
|
|
@@ -179,35 +172,9 @@ export async function handleProjectSelect(ctx) {
|
|
|
179
172
|
if (!selectedProject) {
|
|
180
173
|
throw new Error(`Project with id ${projectId} not found`);
|
|
181
174
|
}
|
|
182
|
-
logger.info(`[Bot] Project selected: ${selectedProject.name || selectedProject.worktree} (id: ${projectId})`);
|
|
183
|
-
setCurrentProject(selectedProject);
|
|
184
|
-
clearSession();
|
|
185
|
-
summaryAggregator.clear();
|
|
186
|
-
clearAllInteractionState("project_switched");
|
|
187
|
-
// Clear pinned message when switching projects
|
|
188
|
-
try {
|
|
189
|
-
await pinnedMessageManager.clear();
|
|
190
|
-
}
|
|
191
|
-
catch (err) {
|
|
192
|
-
logger.error("[Bot] Error clearing pinned message:", err);
|
|
193
|
-
}
|
|
194
|
-
// Initialize keyboard manager if not already
|
|
195
|
-
if (ctx.chat) {
|
|
196
|
-
keyboardManager.initialize(ctx.api, ctx.chat.id);
|
|
197
|
-
}
|
|
198
|
-
// Refresh context limit for current model
|
|
199
|
-
await pinnedMessageManager.refreshContextLimit();
|
|
200
|
-
const contextLimit = pinnedMessageManager.getContextLimit();
|
|
201
|
-
// Reset context to 0 (no session selected) with current model's limit
|
|
202
|
-
keyboardManager.updateContext(0, contextLimit);
|
|
203
|
-
// Get current state for keyboard (with context = 0)
|
|
204
|
-
const currentAgent = await resolveProjectAgent(getStoredAgent());
|
|
205
|
-
const currentModel = getStoredModel();
|
|
206
|
-
const contextInfo = { tokensUsed: 0, tokensLimit: contextLimit };
|
|
207
|
-
const variantName = formatVariantForButton(currentModel.variant || "default");
|
|
208
|
-
keyboardManager.updateAgent(currentAgent);
|
|
209
|
-
const keyboard = createMainKeyboard(currentAgent, currentModel, contextInfo, variantName);
|
|
210
175
|
const projectName = selectedProject.name || selectedProject.worktree;
|
|
176
|
+
logger.info(`[Bot] Project selected: ${projectName} (id: ${projectId})`);
|
|
177
|
+
const keyboard = await switchToProject(ctx, selectedProject, "project_switched");
|
|
211
178
|
await ctx.answerCallbackQuery();
|
|
212
179
|
await ctx.reply(t("projects.selected", { project: projectName }), {
|
|
213
180
|
reply_markup: keyboard,
|
|
@@ -9,6 +9,7 @@ import { clearProject } from "../../settings/manager.js";
|
|
|
9
9
|
import { foregroundSessionState } from "../../scheduled-task/foreground-state.js";
|
|
10
10
|
import { abortCurrentOperation } from "./abort.js";
|
|
11
11
|
import { t } from "../../i18n/index.js";
|
|
12
|
+
import { assistantRunState } from "../assistant-run-state.js";
|
|
12
13
|
export async function startCommand(ctx) {
|
|
13
14
|
if (ctx.chat) {
|
|
14
15
|
if (!pinnedMessageManager.isInitialized()) {
|
|
@@ -18,6 +19,7 @@ export async function startCommand(ctx) {
|
|
|
18
19
|
}
|
|
19
20
|
await abortCurrentOperation(ctx, { notifyUser: false });
|
|
20
21
|
foregroundSessionState.clearAll("start_command_reset");
|
|
22
|
+
assistantRunState.clearAll("start_command_reset");
|
|
21
23
|
clearSession();
|
|
22
24
|
clearProject();
|
|
23
25
|
keyboardManager.clearContext();
|
|
@@ -3,7 +3,15 @@ import { logger } from "../../utils/logger.js";
|
|
|
3
3
|
import { t } from "../../i18n/index.js";
|
|
4
4
|
const INLINE_MENU_CANCEL_PREFIX = "inline:cancel:";
|
|
5
5
|
const LEGACY_CONTEXT_CANCEL_CALLBACK = "compact:cancel";
|
|
6
|
-
const INLINE_MENU_KINDS = [
|
|
6
|
+
const INLINE_MENU_KINDS = [
|
|
7
|
+
"project",
|
|
8
|
+
"session",
|
|
9
|
+
"model",
|
|
10
|
+
"agent",
|
|
11
|
+
"variant",
|
|
12
|
+
"context",
|
|
13
|
+
"open",
|
|
14
|
+
];
|
|
7
15
|
function isInlineMenuKind(value) {
|
|
8
16
|
return INLINE_MENU_KINDS.includes(value);
|
|
9
17
|
}
|
|
@@ -17,6 +17,7 @@ import { formatErrorDetails } from "../../utils/error-format.js";
|
|
|
17
17
|
import { logger } from "../../utils/logger.js";
|
|
18
18
|
import { t } from "../../i18n/index.js";
|
|
19
19
|
import { foregroundSessionState } from "../../scheduled-task/foreground-state.js";
|
|
20
|
+
import { assistantRunState } from "../assistant-run-state.js";
|
|
20
21
|
/** Module-level references for async callbacks that don't have ctx. */
|
|
21
22
|
let botInstance = null;
|
|
22
23
|
let chatIdInstance = null;
|
|
@@ -61,6 +62,7 @@ async function resetMismatchedSessionContext() {
|
|
|
61
62
|
stopEventListening();
|
|
62
63
|
summaryAggregator.clear();
|
|
63
64
|
foregroundSessionState.clearAll("session_mismatch_reset");
|
|
65
|
+
assistantRunState.clearAll("session_mismatch_reset");
|
|
64
66
|
clearAllInteractionState("session_mismatch_reset");
|
|
65
67
|
clearSession();
|
|
66
68
|
keyboardManager.clearContext();
|
|
@@ -209,6 +211,12 @@ export async function processUserPrompt(ctx, text, deps, fileParts = [], options
|
|
|
209
211
|
};
|
|
210
212
|
logger.info(`[Bot] Calling session.prompt (fire-and-forget) with agent=${currentAgent}, fileCount=${fileParts.length}...`);
|
|
211
213
|
foregroundSessionState.markBusy(currentSession.id);
|
|
214
|
+
assistantRunState.startRun(currentSession.id, {
|
|
215
|
+
startedAt: Date.now(),
|
|
216
|
+
configuredAgent: currentAgent,
|
|
217
|
+
configuredProviderID: storedModel.providerID,
|
|
218
|
+
configuredModelID: storedModel.modelID,
|
|
219
|
+
});
|
|
212
220
|
setPromptResponseMode(currentSession.id, responseMode);
|
|
213
221
|
// CRITICAL: DO NOT wait for session.prompt to complete.
|
|
214
222
|
// If we wait, the handler will not finish and grammY will not call getUpdates,
|
|
@@ -220,6 +228,7 @@ export async function processUserPrompt(ctx, text, deps, fileParts = [], options
|
|
|
220
228
|
onSuccess: ({ error }) => {
|
|
221
229
|
if (error) {
|
|
222
230
|
foregroundSessionState.markIdle(currentSession.id);
|
|
231
|
+
assistantRunState.clearRun(currentSession.id, "session_prompt_api_error");
|
|
223
232
|
clearPromptResponseMode(currentSession.id);
|
|
224
233
|
const details = formatErrorDetails(error, 6000);
|
|
225
234
|
logger.error("[Bot] OpenCode API returned an error for session.prompt", promptErrorLogContext);
|
|
@@ -233,6 +242,7 @@ export async function processUserPrompt(ctx, text, deps, fileParts = [], options
|
|
|
233
242
|
},
|
|
234
243
|
onError: (error) => {
|
|
235
244
|
foregroundSessionState.markIdle(currentSession.id);
|
|
245
|
+
assistantRunState.clearRun(currentSession.id, "session_prompt_background_error");
|
|
236
246
|
clearPromptResponseMode(currentSession.id);
|
|
237
247
|
const details = formatErrorDetails(error, 6000);
|
|
238
248
|
logger.error("[Bot] session.prompt background task failed", promptErrorLogContext);
|
|
@@ -246,6 +256,7 @@ export async function processUserPrompt(ctx, text, deps, fileParts = [], options
|
|
|
246
256
|
catch (err) {
|
|
247
257
|
if (currentSession) {
|
|
248
258
|
foregroundSessionState.markIdle(currentSession.id);
|
|
259
|
+
assistantRunState.clearRun(currentSession.id, "session_prompt_handler_error");
|
|
249
260
|
}
|
|
250
261
|
logger.error("Error in prompt handler:", err);
|
|
251
262
|
if (interactionManager.getSnapshot()) {
|
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";
|
|
@@ -35,9 +36,9 @@ import { questionManager } from "../question/manager.js";
|
|
|
35
36
|
import { interactionManager } from "../interaction/manager.js";
|
|
36
37
|
import { clearAllInteractionState } from "../interaction/cleanup.js";
|
|
37
38
|
import { keyboardManager } from "../keyboard/manager.js";
|
|
38
|
-
import { subscribeToEvents } from "../opencode/events.js";
|
|
39
|
+
import { stopEventListening, subscribeToEvents } from "../opencode/events.js";
|
|
39
40
|
import { summaryAggregator } from "../summary/aggregator.js";
|
|
40
|
-
import {
|
|
41
|
+
import { formatToolInfo } from "../summary/formatter.js";
|
|
41
42
|
import { renderSubagentCards } from "../summary/subagent-formatter.js";
|
|
42
43
|
import { ToolMessageBatcher } from "../summary/tool-message-batcher.js";
|
|
43
44
|
import { getCurrentSession } from "../session/manager.js";
|
|
@@ -54,17 +55,20 @@ import { downloadTelegramFile, toDataUri } from "./utils/file-download.js";
|
|
|
54
55
|
import { finalizeAssistantResponse } from "./utils/finalize-assistant-response.js";
|
|
55
56
|
import { sendTtsResponseForSession } from "./utils/send-tts-response.js";
|
|
56
57
|
import { deliverThinkingMessage } from "./utils/thinking-message.js";
|
|
57
|
-
import {
|
|
58
|
+
import { editRenderedBotPart, getTelegramRenderedPartSignature, sendRenderedBotPart, } from "./utils/telegram-text.js";
|
|
59
|
+
import { formatAssistantRunFooter } from "./utils/assistant-run-footer.js";
|
|
58
60
|
import { getModelCapabilities, supportsInput } from "../model/capabilities.js";
|
|
59
61
|
import { getStoredModel } from "../model/manager.js";
|
|
60
62
|
import { foregroundSessionState } from "../scheduled-task/foreground-state.js";
|
|
61
63
|
import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
|
|
64
|
+
import { assistantRunState } from "./assistant-run-state.js";
|
|
62
65
|
import { ResponseStreamer } from "./streaming/response-streamer.js";
|
|
63
66
|
import { ToolCallStreamer } from "./streaming/tool-call-streamer.js";
|
|
64
|
-
import {
|
|
67
|
+
import { prepareAssistantFinalStreamingPayload, prepareAssistantStreamingPayload, renderAssistantFinalPartsSafe, } from "./utils/assistant-rendering.js";
|
|
65
68
|
let botInstance = null;
|
|
66
69
|
let chatIdInstance = null;
|
|
67
70
|
let commandsInitialized = false;
|
|
71
|
+
let heartbeatTimer = null;
|
|
68
72
|
const TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH = 1024;
|
|
69
73
|
const RESPONSE_STREAM_THROTTLE_MS = config.bot.responseStreamThrottleMs;
|
|
70
74
|
const RESPONSE_STREAM_TEXT_LIMIT = 3800;
|
|
@@ -73,6 +77,7 @@ const SUBAGENT_STREAM_PREFIX = "🧩";
|
|
|
73
77
|
const __filename = fileURLToPath(import.meta.url);
|
|
74
78
|
const __dirname = path.dirname(__filename);
|
|
75
79
|
const TEMP_DIR = path.join(__dirname, "..", ".tmp");
|
|
80
|
+
const sessionCompletionTasks = new Map();
|
|
76
81
|
function getCurrentReplyKeyboard() {
|
|
77
82
|
if (!keyboardManager.isInitialized()) {
|
|
78
83
|
return undefined;
|
|
@@ -90,14 +95,23 @@ function prepareDocumentCaption(caption) {
|
|
|
90
95
|
return `${normalizedCaption.slice(0, TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH - 3)}...`;
|
|
91
96
|
}
|
|
92
97
|
function prepareStreamingPayload(messageText) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
98
|
+
return prepareAssistantStreamingPayload(messageText, RESPONSE_STREAM_TEXT_LIMIT);
|
|
99
|
+
}
|
|
100
|
+
function prepareFinalStreamingPayload(messageText) {
|
|
101
|
+
return prepareAssistantFinalStreamingPayload(messageText, RESPONSE_STREAM_TEXT_LIMIT);
|
|
102
|
+
}
|
|
103
|
+
function enqueueSessionCompletionTask(sessionId, task) {
|
|
104
|
+
const previousTask = sessionCompletionTasks.get(sessionId) ?? Promise.resolve();
|
|
105
|
+
const nextTask = previousTask
|
|
106
|
+
.catch(() => undefined)
|
|
107
|
+
.then(task)
|
|
108
|
+
.finally(() => {
|
|
109
|
+
if (sessionCompletionTasks.get(sessionId) === nextTask) {
|
|
110
|
+
sessionCompletionTasks.delete(sessionId);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
sessionCompletionTasks.set(sessionId, nextTask);
|
|
114
|
+
return nextTask;
|
|
101
115
|
}
|
|
102
116
|
const toolMessageBatcher = new ToolMessageBatcher({
|
|
103
117
|
sendText: async (sessionId, text) => {
|
|
@@ -141,39 +155,36 @@ const toolMessageBatcher = new ToolMessageBatcher({
|
|
|
141
155
|
});
|
|
142
156
|
const responseStreamer = new ResponseStreamer({
|
|
143
157
|
throttleMs: RESPONSE_STREAM_THROTTLE_MS,
|
|
144
|
-
|
|
158
|
+
sendPart: async (part, options) => {
|
|
145
159
|
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
146
160
|
throw new Error("Bot context missing for streamed send");
|
|
147
161
|
}
|
|
148
|
-
|
|
149
|
-
const sentMessage = await sendMessageWithMarkdownFallback({
|
|
162
|
+
return sendRenderedBotPart({
|
|
150
163
|
api: botInstance.api,
|
|
151
164
|
chatId: chatIdInstance,
|
|
152
|
-
|
|
165
|
+
part,
|
|
153
166
|
options,
|
|
154
|
-
parseMode,
|
|
155
167
|
});
|
|
156
|
-
return sentMessage.message_id;
|
|
157
168
|
},
|
|
158
|
-
|
|
169
|
+
editPart: async (messageId, part, options) => {
|
|
159
170
|
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
160
171
|
throw new Error("Bot context missing for streamed edit");
|
|
161
172
|
}
|
|
162
|
-
const parseMode = format === "markdown_v2" ? "MarkdownV2" : undefined;
|
|
163
173
|
try {
|
|
164
|
-
await
|
|
174
|
+
return await editRenderedBotPart({
|
|
165
175
|
api: botInstance.api,
|
|
166
176
|
chatId: chatIdInstance,
|
|
167
177
|
messageId,
|
|
168
|
-
|
|
178
|
+
part,
|
|
169
179
|
options,
|
|
170
|
-
parseMode,
|
|
171
180
|
});
|
|
172
181
|
}
|
|
173
182
|
catch (error) {
|
|
174
183
|
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
175
184
|
if (errorMessage.includes("message is not modified")) {
|
|
176
|
-
return
|
|
185
|
+
return {
|
|
186
|
+
deliveredSignature: getTelegramRenderedPartSignature(part),
|
|
187
|
+
};
|
|
177
188
|
}
|
|
178
189
|
throw error;
|
|
179
190
|
}
|
|
@@ -298,84 +309,82 @@ async function ensureEventSubscription(directory) {
|
|
|
298
309
|
if (!preparedStreamPayload) {
|
|
299
310
|
return;
|
|
300
311
|
}
|
|
312
|
+
// Reply keyboards make the first streamed message non-editable in Telegram,
|
|
313
|
+
// so partial chunks must be sent without reply_markup and finalized later.
|
|
301
314
|
preparedStreamPayload.sendOptions = { disable_notification: true };
|
|
302
315
|
preparedStreamPayload.editOptions = undefined;
|
|
303
316
|
responseStreamer.enqueue(sessionId, messageId, preparedStreamPayload);
|
|
304
317
|
});
|
|
305
|
-
summaryAggregator.setOnComplete(
|
|
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
|
-
|
|
362
|
-
|
|
363
|
-
sessionId
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
}
|
|
375
|
-
finally {
|
|
376
|
-
foregroundSessionState.markIdle(sessionId);
|
|
377
|
-
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
378
|
-
}
|
|
318
|
+
summaryAggregator.setOnComplete((sessionId, messageId, messageText, completionInfo) => {
|
|
319
|
+
void enqueueSessionCompletionTask(sessionId, async () => {
|
|
320
|
+
if (!botInstance || !chatIdInstance) {
|
|
321
|
+
logger.error("Bot or chat ID not available for sending message");
|
|
322
|
+
clearPromptResponseMode(sessionId);
|
|
323
|
+
responseStreamer.clearMessage(sessionId, messageId, "bot_context_missing");
|
|
324
|
+
toolCallStreamer.clearSession(sessionId, "bot_context_missing");
|
|
325
|
+
assistantRunState.clearRun(sessionId, "bot_context_missing");
|
|
326
|
+
foregroundSessionState.markIdle(sessionId);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const currentSession = getCurrentSession();
|
|
330
|
+
if (currentSession?.id !== sessionId) {
|
|
331
|
+
clearPromptResponseMode(sessionId);
|
|
332
|
+
responseStreamer.clearMessage(sessionId, messageId, "session_mismatch");
|
|
333
|
+
toolCallStreamer.clearSession(sessionId, "session_mismatch");
|
|
334
|
+
assistantRunState.clearRun(sessionId, "session_mismatch");
|
|
335
|
+
foregroundSessionState.markIdle(sessionId);
|
|
336
|
+
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const botApi = botInstance.api;
|
|
340
|
+
const chatId = chatIdInstance;
|
|
341
|
+
try {
|
|
342
|
+
assistantRunState.markResponseCompleted(sessionId, {
|
|
343
|
+
agent: completionInfo.agent,
|
|
344
|
+
providerID: completionInfo.providerID,
|
|
345
|
+
modelID: completionInfo.modelID,
|
|
346
|
+
});
|
|
347
|
+
await finalizeAssistantResponse({
|
|
348
|
+
sessionId,
|
|
349
|
+
messageId,
|
|
350
|
+
messageText,
|
|
351
|
+
responseStreamer,
|
|
352
|
+
flushPendingServiceMessages: () => Promise.all([
|
|
353
|
+
toolMessageBatcher.flushSession(sessionId, "assistant_message_completed"),
|
|
354
|
+
toolCallStreamer.breakSession(sessionId, "assistant_message_completed"),
|
|
355
|
+
]).then(() => undefined),
|
|
356
|
+
prepareStreamingPayload: prepareFinalStreamingPayload,
|
|
357
|
+
renderFinalParts: (text) => renderAssistantFinalPartsSafe(text),
|
|
358
|
+
getReplyKeyboard: getCurrentReplyKeyboard,
|
|
359
|
+
sendRenderedPart: async (part, options) => {
|
|
360
|
+
await sendRenderedBotPart({
|
|
361
|
+
api: botApi,
|
|
362
|
+
chatId,
|
|
363
|
+
part,
|
|
364
|
+
options: options,
|
|
365
|
+
});
|
|
366
|
+
},
|
|
367
|
+
});
|
|
368
|
+
await sendTtsResponseForSession({
|
|
369
|
+
api: botApi,
|
|
370
|
+
sessionId,
|
|
371
|
+
chatId,
|
|
372
|
+
text: messageText,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
clearPromptResponseMode(sessionId);
|
|
377
|
+
assistantRunState.clearRun(sessionId, "assistant_finalize_failed");
|
|
378
|
+
logger.error("Failed to send message to Telegram:", err);
|
|
379
|
+
// Stop processing events after critical error to prevent infinite loop
|
|
380
|
+
logger.error("[Bot] CRITICAL: Stopping event processing due to error");
|
|
381
|
+
summaryAggregator.clear();
|
|
382
|
+
foregroundSessionState.markIdle(sessionId);
|
|
383
|
+
}
|
|
384
|
+
finally {
|
|
385
|
+
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
386
|
+
}
|
|
387
|
+
});
|
|
379
388
|
});
|
|
380
389
|
summaryAggregator.setOnTool(async (toolInfo) => {
|
|
381
390
|
if (!botInstance || !chatIdInstance) {
|
|
@@ -434,6 +443,9 @@ async function ensureEventSubscription(directory) {
|
|
|
434
443
|
if (!currentSession || currentSession.id !== fileInfo.sessionId) {
|
|
435
444
|
return;
|
|
436
445
|
}
|
|
446
|
+
if (config.bot.hideToolFileMessages) {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
437
449
|
try {
|
|
438
450
|
await toolCallStreamer.breakSession(fileInfo.sessionId, "tool_file_boundary");
|
|
439
451
|
const toolMessage = formatToolInfo(fileInfo);
|
|
@@ -568,9 +580,54 @@ async function ensureEventSubscription(directory) {
|
|
|
568
580
|
logger.error("[Bot] Error reloading context after compaction:", err);
|
|
569
581
|
}
|
|
570
582
|
});
|
|
583
|
+
summaryAggregator.setOnSessionIdle(async (sessionId) => {
|
|
584
|
+
await sessionCompletionTasks.get(sessionId)?.catch(() => undefined);
|
|
585
|
+
const completedRun = assistantRunState.finishRun(sessionId, "session_idle");
|
|
586
|
+
clearPromptResponseMode(sessionId);
|
|
587
|
+
if (!botInstance || !chatIdInstance) {
|
|
588
|
+
foregroundSessionState.markIdle(sessionId);
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const currentSession = getCurrentSession();
|
|
592
|
+
if (!currentSession || currentSession.id !== sessionId) {
|
|
593
|
+
foregroundSessionState.markIdle(sessionId);
|
|
594
|
+
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
try {
|
|
598
|
+
await Promise.all([
|
|
599
|
+
toolMessageBatcher.flushSession(sessionId, "session_idle"),
|
|
600
|
+
toolCallStreamer.flushSession(sessionId, "session_idle"),
|
|
601
|
+
]);
|
|
602
|
+
if (completedRun?.hasCompletedResponse) {
|
|
603
|
+
const agent = completedRun.actualAgent || completedRun.configuredAgent;
|
|
604
|
+
const providerID = completedRun.actualProviderID || completedRun.configuredProviderID;
|
|
605
|
+
const modelID = completedRun.actualModelID || completedRun.configuredModelID;
|
|
606
|
+
if (agent && providerID && modelID) {
|
|
607
|
+
const keyboard = getCurrentReplyKeyboard();
|
|
608
|
+
await botInstance.api.sendMessage(chatIdInstance, formatAssistantRunFooter({
|
|
609
|
+
agent,
|
|
610
|
+
providerID,
|
|
611
|
+
modelID,
|
|
612
|
+
elapsedMs: Date.now() - completedRun.startedAt,
|
|
613
|
+
}), {
|
|
614
|
+
...(keyboard ? { reply_markup: keyboard } : {}),
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
}
|
|
619
|
+
catch (err) {
|
|
620
|
+
logger.error("[Bot] Failed to send session idle footer:", err);
|
|
621
|
+
}
|
|
622
|
+
finally {
|
|
623
|
+
foregroundSessionState.markIdle(sessionId);
|
|
624
|
+
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
625
|
+
}
|
|
626
|
+
});
|
|
571
627
|
summaryAggregator.setOnSessionError(async (sessionId, message) => {
|
|
572
628
|
if (!botInstance || !chatIdInstance) {
|
|
573
629
|
clearPromptResponseMode(sessionId);
|
|
630
|
+
assistantRunState.clearRun(sessionId, "session_error_no_bot_context");
|
|
574
631
|
foregroundSessionState.markIdle(sessionId);
|
|
575
632
|
return;
|
|
576
633
|
}
|
|
@@ -579,12 +636,14 @@ async function ensureEventSubscription(directory) {
|
|
|
579
636
|
clearPromptResponseMode(sessionId);
|
|
580
637
|
responseStreamer.clearSession(sessionId, "session_error_not_current");
|
|
581
638
|
toolCallStreamer.clearSession(sessionId, "session_error_not_current");
|
|
639
|
+
assistantRunState.clearRun(sessionId, "session_error_not_current");
|
|
582
640
|
foregroundSessionState.markIdle(sessionId);
|
|
583
641
|
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
584
642
|
return;
|
|
585
643
|
}
|
|
586
644
|
responseStreamer.clearSession(sessionId, "session_error");
|
|
587
645
|
clearPromptResponseMode(sessionId);
|
|
646
|
+
assistantRunState.clearRun(sessionId, "session_error");
|
|
588
647
|
await Promise.all([
|
|
589
648
|
toolMessageBatcher.flushSession(sessionId, "session_error"),
|
|
590
649
|
toolCallStreamer.flushSession(sessionId, "session_error"),
|
|
@@ -661,6 +720,12 @@ async function ensureEventSubscription(directory) {
|
|
|
661
720
|
}
|
|
662
721
|
export function createBot() {
|
|
663
722
|
clearAllInteractionState("bot_startup");
|
|
723
|
+
sessionCompletionTasks.clear();
|
|
724
|
+
assistantRunState.clearAll("bot_startup");
|
|
725
|
+
if (heartbeatTimer) {
|
|
726
|
+
clearInterval(heartbeatTimer);
|
|
727
|
+
heartbeatTimer = null;
|
|
728
|
+
}
|
|
664
729
|
const botOptions = {};
|
|
665
730
|
if (config.telegram.proxyUrl) {
|
|
666
731
|
const proxyUrl = config.telegram.proxyUrl;
|
|
@@ -683,7 +748,7 @@ export function createBot() {
|
|
|
683
748
|
const bot = new Bot(config.telegram.token, botOptions);
|
|
684
749
|
// Heartbeat for diagnostics: verify the event loop is not blocked
|
|
685
750
|
let heartbeatCounter = 0;
|
|
686
|
-
setInterval(() => {
|
|
751
|
+
heartbeatTimer = setInterval(() => {
|
|
687
752
|
heartbeatCounter++;
|
|
688
753
|
if (heartbeatCounter % 6 === 0) {
|
|
689
754
|
// Log every 30 seconds (5 sec * 6)
|
|
@@ -736,6 +801,7 @@ export function createBot() {
|
|
|
736
801
|
bot.command("opencode_start", opencodeStartCommand);
|
|
737
802
|
bot.command("opencode_stop", opencodeStopCommand);
|
|
738
803
|
bot.command("projects", projectsCommand);
|
|
804
|
+
bot.command("open", openCommand);
|
|
739
805
|
bot.command("sessions", sessionsCommand);
|
|
740
806
|
bot.command("new", newCommand);
|
|
741
807
|
bot.command("abort", abortCommand);
|
|
@@ -753,8 +819,13 @@ export function createBot() {
|
|
|
753
819
|
}
|
|
754
820
|
try {
|
|
755
821
|
const handledInlineCancel = await handleInlineMenuCancel(ctx);
|
|
822
|
+
if (handledInlineCancel) {
|
|
823
|
+
// Clean up path index when the open-directory menu is cancelled
|
|
824
|
+
clearOpenPathIndex();
|
|
825
|
+
}
|
|
756
826
|
const handledSession = await handleSessionSelect(ctx);
|
|
757
827
|
const handledProject = await handleProjectSelect(ctx);
|
|
828
|
+
const handledOpen = await handleOpenCallback(ctx);
|
|
758
829
|
const handledQuestion = await handleQuestionCallback(ctx);
|
|
759
830
|
const handledPermission = await handlePermissionCallback(ctx);
|
|
760
831
|
const handledAgent = await handleAgentSelect(ctx);
|
|
@@ -765,10 +836,11 @@ export function createBot() {
|
|
|
765
836
|
const handledTaskList = await handleTaskListCallback(ctx);
|
|
766
837
|
const handledRenameCancel = await handleRenameCancel(ctx);
|
|
767
838
|
const handledCommands = await handleCommandsCallback(ctx, { bot, ensureEventSubscription });
|
|
768
|
-
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}`);
|
|
839
|
+
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}`);
|
|
769
840
|
if (!handledInlineCancel &&
|
|
770
841
|
!handledSession &&
|
|
771
842
|
!handledProject &&
|
|
843
|
+
!handledOpen &&
|
|
772
844
|
!handledQuestion &&
|
|
773
845
|
!handledPermission &&
|
|
774
846
|
!handledAgent &&
|
|
@@ -989,3 +1061,18 @@ export function createBot() {
|
|
|
989
1061
|
});
|
|
990
1062
|
return bot;
|
|
991
1063
|
}
|
|
1064
|
+
export function cleanupBotRuntime(reason) {
|
|
1065
|
+
stopEventListening();
|
|
1066
|
+
summaryAggregator.clear();
|
|
1067
|
+
responseStreamer.clearAll(reason);
|
|
1068
|
+
toolCallStreamer.clearAll(reason);
|
|
1069
|
+
toolMessageBatcher.clearAll(reason);
|
|
1070
|
+
sessionCompletionTasks.clear();
|
|
1071
|
+
assistantRunState.clearAll(reason);
|
|
1072
|
+
if (heartbeatTimer) {
|
|
1073
|
+
clearInterval(heartbeatTimer);
|
|
1074
|
+
heartbeatTimer = null;
|
|
1075
|
+
}
|
|
1076
|
+
botInstance = null;
|
|
1077
|
+
chatIdInstance = null;
|
|
1078
|
+
}
|