@grinev/opencode-telegram-bot 0.15.0 → 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 +13 -0
- package/README.md +6 -0
- 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 +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 +162 -98
- 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.js +4 -0
- package/dist/config.js +3 -0
- package/dist/i18n/de.js +15 -0
- package/dist/i18n/en.js +15 -0
- package/dist/i18n/es.js +15 -0
- package/dist/i18n/fr.js +15 -0
- package/dist/i18n/ru.js +15 -0
- package/dist/i18n/zh.js +15 -0
- package/dist/index.js +2 -0
- package/dist/project/manager.js +2 -1
- 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
|
@@ -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";
|
|
@@ -37,7 +38,7 @@ import { clearAllInteractionState } from "../interaction/cleanup.js";
|
|
|
37
38
|
import { keyboardManager } from "../keyboard/manager.js";
|
|
38
39
|
import { 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,14 +55,16 @@ 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;
|
|
@@ -73,6 +76,7 @@ const SUBAGENT_STREAM_PREFIX = "🧩";
|
|
|
73
76
|
const __filename = fileURLToPath(import.meta.url);
|
|
74
77
|
const __dirname = path.dirname(__filename);
|
|
75
78
|
const TEMP_DIR = path.join(__dirname, "..", ".tmp");
|
|
79
|
+
const sessionCompletionTasks = new Map();
|
|
76
80
|
function getCurrentReplyKeyboard() {
|
|
77
81
|
if (!keyboardManager.isInitialized()) {
|
|
78
82
|
return undefined;
|
|
@@ -90,14 +94,23 @@ function prepareDocumentCaption(caption) {
|
|
|
90
94
|
return `${normalizedCaption.slice(0, TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH - 3)}...`;
|
|
91
95
|
}
|
|
92
96
|
function prepareStreamingPayload(messageText) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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;
|
|
101
114
|
}
|
|
102
115
|
const toolMessageBatcher = new ToolMessageBatcher({
|
|
103
116
|
sendText: async (sessionId, text) => {
|
|
@@ -141,39 +154,36 @@ const toolMessageBatcher = new ToolMessageBatcher({
|
|
|
141
154
|
});
|
|
142
155
|
const responseStreamer = new ResponseStreamer({
|
|
143
156
|
throttleMs: RESPONSE_STREAM_THROTTLE_MS,
|
|
144
|
-
|
|
157
|
+
sendPart: async (part, options) => {
|
|
145
158
|
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
146
159
|
throw new Error("Bot context missing for streamed send");
|
|
147
160
|
}
|
|
148
|
-
|
|
149
|
-
const sentMessage = await sendMessageWithMarkdownFallback({
|
|
161
|
+
return sendRenderedBotPart({
|
|
150
162
|
api: botInstance.api,
|
|
151
163
|
chatId: chatIdInstance,
|
|
152
|
-
|
|
164
|
+
part,
|
|
153
165
|
options,
|
|
154
|
-
parseMode,
|
|
155
166
|
});
|
|
156
|
-
return sentMessage.message_id;
|
|
157
167
|
},
|
|
158
|
-
|
|
168
|
+
editPart: async (messageId, part, options) => {
|
|
159
169
|
if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
|
|
160
170
|
throw new Error("Bot context missing for streamed edit");
|
|
161
171
|
}
|
|
162
|
-
const parseMode = format === "markdown_v2" ? "MarkdownV2" : undefined;
|
|
163
172
|
try {
|
|
164
|
-
await
|
|
173
|
+
return await editRenderedBotPart({
|
|
165
174
|
api: botInstance.api,
|
|
166
175
|
chatId: chatIdInstance,
|
|
167
176
|
messageId,
|
|
168
|
-
|
|
177
|
+
part,
|
|
169
178
|
options,
|
|
170
|
-
parseMode,
|
|
171
179
|
});
|
|
172
180
|
}
|
|
173
181
|
catch (error) {
|
|
174
182
|
const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
|
|
175
183
|
if (errorMessage.includes("message is not modified")) {
|
|
176
|
-
return
|
|
184
|
+
return {
|
|
185
|
+
deliveredSignature: getTelegramRenderedPartSignature(part),
|
|
186
|
+
};
|
|
177
187
|
}
|
|
178
188
|
throw error;
|
|
179
189
|
}
|
|
@@ -298,84 +308,82 @@ async function ensureEventSubscription(directory) {
|
|
|
298
308
|
if (!preparedStreamPayload) {
|
|
299
309
|
return;
|
|
300
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.
|
|
301
313
|
preparedStreamPayload.sendOptions = { disable_notification: true };
|
|
302
314
|
preparedStreamPayload.editOptions = undefined;
|
|
303
315
|
responseStreamer.enqueue(sessionId, messageId, preparedStreamPayload);
|
|
304
316
|
});
|
|
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
|
-
}
|
|
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
|
+
});
|
|
379
387
|
});
|
|
380
388
|
summaryAggregator.setOnTool(async (toolInfo) => {
|
|
381
389
|
if (!botInstance || !chatIdInstance) {
|
|
@@ -568,9 +576,54 @@ async function ensureEventSubscription(directory) {
|
|
|
568
576
|
logger.error("[Bot] Error reloading context after compaction:", err);
|
|
569
577
|
}
|
|
570
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
|
+
});
|
|
571
623
|
summaryAggregator.setOnSessionError(async (sessionId, message) => {
|
|
572
624
|
if (!botInstance || !chatIdInstance) {
|
|
573
625
|
clearPromptResponseMode(sessionId);
|
|
626
|
+
assistantRunState.clearRun(sessionId, "session_error_no_bot_context");
|
|
574
627
|
foregroundSessionState.markIdle(sessionId);
|
|
575
628
|
return;
|
|
576
629
|
}
|
|
@@ -579,12 +632,14 @@ async function ensureEventSubscription(directory) {
|
|
|
579
632
|
clearPromptResponseMode(sessionId);
|
|
580
633
|
responseStreamer.clearSession(sessionId, "session_error_not_current");
|
|
581
634
|
toolCallStreamer.clearSession(sessionId, "session_error_not_current");
|
|
635
|
+
assistantRunState.clearRun(sessionId, "session_error_not_current");
|
|
582
636
|
foregroundSessionState.markIdle(sessionId);
|
|
583
637
|
await scheduledTaskRuntime.flushDeferredDeliveries();
|
|
584
638
|
return;
|
|
585
639
|
}
|
|
586
640
|
responseStreamer.clearSession(sessionId, "session_error");
|
|
587
641
|
clearPromptResponseMode(sessionId);
|
|
642
|
+
assistantRunState.clearRun(sessionId, "session_error");
|
|
588
643
|
await Promise.all([
|
|
589
644
|
toolMessageBatcher.flushSession(sessionId, "session_error"),
|
|
590
645
|
toolCallStreamer.flushSession(sessionId, "session_error"),
|
|
@@ -661,6 +716,8 @@ async function ensureEventSubscription(directory) {
|
|
|
661
716
|
}
|
|
662
717
|
export function createBot() {
|
|
663
718
|
clearAllInteractionState("bot_startup");
|
|
719
|
+
sessionCompletionTasks.clear();
|
|
720
|
+
assistantRunState.clearAll("bot_startup");
|
|
664
721
|
const botOptions = {};
|
|
665
722
|
if (config.telegram.proxyUrl) {
|
|
666
723
|
const proxyUrl = config.telegram.proxyUrl;
|
|
@@ -736,6 +793,7 @@ export function createBot() {
|
|
|
736
793
|
bot.command("opencode_start", opencodeStartCommand);
|
|
737
794
|
bot.command("opencode_stop", opencodeStopCommand);
|
|
738
795
|
bot.command("projects", projectsCommand);
|
|
796
|
+
bot.command("open", openCommand);
|
|
739
797
|
bot.command("sessions", sessionsCommand);
|
|
740
798
|
bot.command("new", newCommand);
|
|
741
799
|
bot.command("abort", abortCommand);
|
|
@@ -753,8 +811,13 @@ export function createBot() {
|
|
|
753
811
|
}
|
|
754
812
|
try {
|
|
755
813
|
const handledInlineCancel = await handleInlineMenuCancel(ctx);
|
|
814
|
+
if (handledInlineCancel) {
|
|
815
|
+
// Clean up path index when the open-directory menu is cancelled
|
|
816
|
+
clearOpenPathIndex();
|
|
817
|
+
}
|
|
756
818
|
const handledSession = await handleSessionSelect(ctx);
|
|
757
819
|
const handledProject = await handleProjectSelect(ctx);
|
|
820
|
+
const handledOpen = await handleOpenCallback(ctx);
|
|
758
821
|
const handledQuestion = await handleQuestionCallback(ctx);
|
|
759
822
|
const handledPermission = await handlePermissionCallback(ctx);
|
|
760
823
|
const handledAgent = await handleAgentSelect(ctx);
|
|
@@ -765,10 +828,11 @@ export function createBot() {
|
|
|
765
828
|
const handledTaskList = await handleTaskListCallback(ctx);
|
|
766
829
|
const handledRenameCancel = await handleRenameCancel(ctx);
|
|
767
830
|
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}`);
|
|
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}`);
|
|
769
832
|
if (!handledInlineCancel &&
|
|
770
833
|
!handledSession &&
|
|
771
834
|
!handledProject &&
|
|
835
|
+
!handledOpen &&
|
|
772
836
|
!handledQuestion &&
|
|
773
837
|
!handledPermission &&
|
|
774
838
|
!handledAgent &&
|
|
@@ -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];
|