@grinev/opencode-telegram-bot 0.21.0 → 0.21.2

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.
Files changed (182) hide show
  1. package/.env.example +18 -2
  2. package/README.md +21 -8
  3. package/dist/app/{start-bot-app.js → bootstrap/start-bot-app.js} +16 -15
  4. package/dist/{bot/utils/assistant-run-footer.js → app/formatters/assistant-run-footer-formatter.js} +1 -1
  5. package/dist/{summary → app/formatters}/subagent-formatter.js +8 -3
  6. package/dist/{summary/formatter.js → app/formatters/summary-formatter.js} +4 -132
  7. package/dist/{summary → app/formatters}/tool-message-batcher.js +1 -1
  8. package/dist/{bot/assistant-run-state.js → app/managers/assistant-run-state-manager.js} +1 -1
  9. package/dist/{attach/manager.js → app/managers/attach-manager.js} +1 -1
  10. package/dist/{background-session/tracker.js → app/managers/background-session-manager.js} +2 -2
  11. package/dist/{scheduled-task/foreground-state.js → app/managers/foreground-session-state-manager.js} +1 -1
  12. package/dist/{interaction/manager.js → app/managers/interaction-manager.js} +31 -1
  13. package/dist/{permission/manager.js → app/managers/permission-manager.js} +1 -1
  14. package/dist/{question/manager.js → app/managers/question-manager.js} +1 -1
  15. package/dist/{rename/manager.js → app/managers/rename-manager.js} +1 -1
  16. package/dist/{scheduled-task/creation-manager.js → app/managers/scheduled-task-creation-manager.js} +2 -2
  17. package/dist/{summary/aggregator.js → app/managers/summary-aggregation-manager.js} +3 -3
  18. package/dist/{agent/manager.js → app/services/agent-selection-service.js} +4 -5
  19. package/dist/{attach/service.js → app/services/attach-service.js} +23 -44
  20. package/dist/{bot/utils/busy-reconciliation.js → app/services/busy-reconciliation-service.js} +12 -7
  21. package/dist/app/services/command-catalog-service.js +21 -0
  22. package/dist/{bot/utils/external-user-input.js → app/services/external-user-input-service.js} +4 -19
  23. package/dist/app/services/file-browser-service.js +265 -0
  24. package/dist/{bot/utils/file-download.js → app/services/file-download-service.js} +14 -28
  25. package/dist/app/services/mcp-catalog-service.js +79 -0
  26. package/dist/app/services/message-history-service.js +81 -0
  27. package/dist/{model/capabilities.js → app/services/model-capabilities-service.js} +2 -2
  28. package/dist/{model/context-limit.js → app/services/model-context-limit-service.js} +4 -4
  29. package/dist/{model/manager.js → app/services/model-selection-service.js} +4 -4
  30. package/dist/{project/manager.js → app/services/project-service.js} +3 -3
  31. package/dist/app/services/project-switch-service.js +39 -0
  32. package/dist/{bot/utils/busy-guard.js → app/services/run-control-service.js} +3 -14
  33. package/dist/{scheduled-task/executor.js → app/services/scheduled-task-executor-service.js} +5 -5
  34. package/dist/{scheduled-task/runtime.js → app/services/scheduled-task-runtime-service.js} +17 -56
  35. package/dist/{scheduled-task/schedule-parser.js → app/services/scheduled-task-schedule-parser-service.js} +3 -3
  36. package/dist/{scheduled-task/session-ignore.js → app/services/scheduled-task-session-ignore-service.js} +1 -1
  37. package/dist/{session/cache-manager.js → app/services/session-cache-service.js} +3 -3
  38. package/dist/{session/manager.js → app/services/session-service.js} +1 -1
  39. package/dist/app/services/skills-catalog-service.js +21 -0
  40. package/dist/{stt/client.js → app/services/stt-service.js} +2 -2
  41. package/dist/{tts/client.js → app/services/tts-service.js} +68 -5
  42. package/dist/{variant/manager.js → app/services/variant-selection-service.js} +3 -3
  43. package/dist/{scheduled-task/store.js → app/stores/scheduled-task-store.js} +3 -3
  44. package/dist/{settings/manager.js → app/stores/settings-store.js} +3 -3
  45. package/dist/app/types/worktree.js +1 -0
  46. package/dist/bot/{handlers/agent.js → callbacks/agent-selection-callback-handler.js} +8 -57
  47. package/dist/bot/callbacks/callback-router.js +110 -0
  48. package/dist/bot/{commands/commands.js → callbacks/command-catalog-callback-handler.js} +17 -197
  49. package/dist/bot/{handlers/context.js → callbacks/context-control-callback-handler.js} +3 -32
  50. package/dist/bot/callbacks/file-browser-callback-handler.js +219 -0
  51. package/dist/bot/callbacks/inline-menu-cancel-callback-handler.js +32 -0
  52. package/dist/bot/{commands/mcps.js → callbacks/mcp-catalog-callback-handler.js} +7 -172
  53. package/dist/bot/{commands/messages.js → callbacks/message-history-callback-handler.js} +17 -248
  54. package/dist/bot/{handlers/model.js → callbacks/model-selection-callback-handler.js} +10 -71
  55. package/dist/bot/callbacks/permission-callback-handler.js +114 -0
  56. package/dist/bot/callbacks/project-callback-handler.js +79 -0
  57. package/dist/bot/callbacks/question-callback-handler.js +159 -0
  58. package/dist/bot/{commands/rename.js → callbacks/rename-callback-handler.js} +6 -34
  59. package/dist/bot/{commands/tasklist.js → callbacks/scheduled-task-callback-handler.js} +83 -68
  60. package/dist/bot/{commands/sessions.js → callbacks/session-callback-handler.js} +19 -158
  61. package/dist/bot/{commands/skills.js → callbacks/skills-catalog-callback-handler.js} +8 -188
  62. package/dist/bot/{handlers/variant.js → callbacks/variant-selection-callback-handler.js} +7 -69
  63. package/dist/bot/callbacks/worktree-callback-handler.js +97 -0
  64. package/dist/bot/commands/{abort.js → abort-command.js} +6 -6
  65. package/dist/bot/commands/command-catalog-command.js +42 -0
  66. package/dist/bot/commands/{detach.js → detach-command.js} +8 -8
  67. package/dist/bot/commands/ls-command.js +48 -0
  68. package/dist/bot/commands/mcp-catalog-command.js +39 -0
  69. package/dist/bot/commands/messages-command.js +59 -0
  70. package/dist/bot/commands/{new.js → new-command.js} +12 -11
  71. package/dist/bot/commands/open-command.js +45 -0
  72. package/dist/bot/commands/{opencode-start.js → opencode-start-command.js} +1 -1
  73. package/dist/bot/commands/{opencode-stop.js → opencode-stop-command.js} +1 -1
  74. package/dist/bot/commands/projects-command.js +32 -0
  75. package/dist/bot/commands/rename-command.js +33 -0
  76. package/dist/bot/commands/sessions-command.js +42 -0
  77. package/dist/bot/commands/skills-catalog-command.js +42 -0
  78. package/dist/bot/commands/{start.js → start-command.js} +12 -12
  79. package/dist/bot/commands/{status.js → status-command.js} +9 -9
  80. package/dist/bot/commands/{task.js → task-command.js} +9 -81
  81. package/dist/bot/commands/tasklist-command.js +40 -0
  82. package/dist/bot/commands/{tts.js → tts-command.js} +2 -2
  83. package/dist/bot/commands/worktree-command.js +47 -0
  84. package/dist/bot/handlers/{document.js → document-handler.js} +3 -3
  85. package/dist/bot/handlers/{media-group.js → media-group-handler.js} +3 -3
  86. package/dist/bot/handlers/photo-handler.js +44 -0
  87. package/dist/bot/handlers/prompt.js +16 -16
  88. package/dist/bot/handlers/text-message-handler.js +61 -0
  89. package/dist/bot/handlers/tts-response-handler.js +29 -0
  90. package/dist/bot/handlers/{voice.js → voice-handler.js} +2 -2
  91. package/dist/bot/index.js +27 -1130
  92. package/dist/{keyboard/manager.js → bot/keyboards/keyboard-manager.js} +6 -6
  93. package/dist/bot/keyboards/keyboard-types.js +1 -0
  94. package/dist/bot/{utils/keyboard.js → keyboards/main-reply-keyboard.js} +2 -2
  95. package/dist/bot/menus/agent-selection-menu.js +54 -0
  96. package/dist/bot/menus/command-catalog-menu.js +101 -0
  97. package/dist/bot/menus/context-control-menu.js +33 -0
  98. package/dist/bot/menus/file-browser-menu.js +265 -0
  99. package/dist/bot/{handlers → menus}/inline-menu.js +4 -33
  100. package/dist/bot/menus/mcp-catalog-menu.js +97 -0
  101. package/dist/bot/menus/message-history-menu.js +107 -0
  102. package/dist/bot/menus/model-selection-menu.js +66 -0
  103. package/dist/bot/menus/permission-menu.js +116 -0
  104. package/dist/bot/{commands/projects.js → menus/project-selection-menu.js} +5 -105
  105. package/dist/bot/{handlers/question.js → menus/question-menu.js} +10 -166
  106. package/dist/bot/menus/rename-menu.js +6 -0
  107. package/dist/bot/menus/scheduled-task-menu.js +42 -0
  108. package/dist/bot/menus/session-selection-menu.js +115 -0
  109. package/dist/bot/menus/skills-catalog-menu.js +99 -0
  110. package/dist/bot/menus/variant-selection-menu.js +67 -0
  111. package/dist/bot/menus/worktree-selection-menu.js +76 -0
  112. package/dist/bot/{utils → messages}/assistant-rendering.js +2 -2
  113. package/dist/bot/messages/busy-blocked-renderer.js +11 -0
  114. package/dist/bot/messages/external-user-input-notification.js +22 -0
  115. package/dist/bot/messages/scheduled-task-delivery.js +63 -0
  116. package/dist/bot/{utils → messages}/send-downloaded-file.js +1 -1
  117. package/dist/bot/messages/summary-message-formatter.js +130 -0
  118. package/dist/bot/{utils → messages}/telegram-text.js +60 -0
  119. package/dist/{interaction/guard.js → bot/middleware/interaction-guard-decision.js} +11 -4
  120. package/dist/bot/middleware/interaction-guard.js +2 -2
  121. package/dist/bot/middleware/unknown-command.js +1 -1
  122. package/dist/{pinned/format.js → bot/pinned/pinned-message-format.js} +1 -2
  123. package/dist/{pinned/manager.js → bot/pinned/pinned-message-manager.js} +10 -10
  124. package/dist/bot/pinned/pinned-message-types.js +1 -0
  125. package/dist/{summary → bot/render}/markdown-to-telegram-v2.js +2 -2
  126. package/dist/bot/render/types.js +1 -0
  127. package/dist/bot/routers/command-router.js +73 -0
  128. package/dist/bot/routers/message-router.js +150 -0
  129. package/dist/bot/services/attach-presentation.js +40 -0
  130. package/dist/bot/services/event-subscription-service.js +842 -0
  131. package/dist/bot/services/project-switch-presentation.js +26 -0
  132. package/dist/bot/streaming/response-streamer.js +18 -2
  133. package/dist/cli.js +4 -4
  134. package/dist/config.js +21 -3
  135. package/dist/i18n/ar.js +491 -0
  136. package/dist/i18n/index.js +7 -0
  137. package/dist/index.js +1 -1
  138. package/dist/opencode/ready-refresh.js +2 -2
  139. package/dist/{service → runtime/service}/manager.js +2 -2
  140. package/dist/runtime/service/types.js +1 -0
  141. package/package.json +1 -1
  142. package/dist/bot/commands/ls.js +0 -445
  143. package/dist/bot/commands/open.js +0 -319
  144. package/dist/bot/commands/worktree.js +0 -201
  145. package/dist/bot/handlers/permission.js +0 -235
  146. package/dist/bot/utils/browser-roots.js +0 -140
  147. package/dist/bot/utils/file-tree.js +0 -92
  148. package/dist/bot/utils/send-tts-response.js +0 -37
  149. package/dist/bot/utils/switch-project.js +0 -45
  150. package/dist/bot/utils/telegram-file-url.js +0 -11
  151. package/dist/interaction/busy.js +0 -8
  152. package/dist/interaction/cleanup.js +0 -32
  153. /package/dist/{scheduled-task/display.js → app/formatters/scheduled-task-display-formatter.js} +0 -0
  154. /package/dist/{bot/utils/abort-error-suppression.js → app/managers/abort-suppression-manager.js} +0 -0
  155. /package/dist/{external-input/suppression.js → app/managers/external-input-suppression-manager.js} +0 -0
  156. /package/dist/{scheduled-task/next-run.js → app/services/scheduled-task-next-run-service.js} +0 -0
  157. /package/dist/{git/worktree.js → app/services/worktree-service.js} +0 -0
  158. /package/dist/{agent/types.js → app/types/agent.js} +0 -0
  159. /package/dist/{interaction/types.js → app/types/interaction.js} +0 -0
  160. /package/dist/{model/types.js → app/types/model.js} +0 -0
  161. /package/dist/{keyboard/types.js → app/types/permission.js} +0 -0
  162. /package/dist/{permission/types.js → app/types/project.js} +0 -0
  163. /package/dist/{pinned/types.js → app/types/question.js} +0 -0
  164. /package/dist/{scheduled-task/types.js → app/types/scheduled-task.js} +0 -0
  165. /package/dist/{question/types.js → app/types/session.js} +0 -0
  166. /package/dist/{service/types.js → app/types/settings.js} +0 -0
  167. /package/dist/{telegram/render/types.js → app/types/summary.js} +0 -0
  168. /package/dist/{variant/types.js → app/types/variant.js} +0 -0
  169. /package/dist/bot/commands/{help.js → help-command.js} +0 -0
  170. /package/dist/bot/{utils → messages}/send-with-markdown-fallback.js +0 -0
  171. /package/dist/bot/{utils → messages}/thinking-message.js +0 -0
  172. /package/dist/{telegram → bot}/render/block-fallback.js +0 -0
  173. /package/dist/{telegram → bot}/render/block-parser.js +0 -0
  174. /package/dist/{telegram → bot}/render/block-renderer.js +0 -0
  175. /package/dist/{telegram → bot}/render/chunker.js +0 -0
  176. /package/dist/{telegram → bot}/render/inline-renderer.js +0 -0
  177. /package/dist/{telegram → bot}/render/markdown-normalizer.js +0 -0
  178. /package/dist/{telegram → bot}/render/pipeline.js +0 -0
  179. /package/dist/{telegram → bot}/render/validator.js +0 -0
  180. /package/dist/bot/{utils/commands.js → routers/command-utils.js} +0 -0
  181. /package/dist/bot/{utils → streaming}/finalize-assistant-response.js +0 -0
  182. /package/dist/{service/runtime.js → runtime/service/env.js} +0 -0
@@ -0,0 +1,842 @@
1
+ import { promises as fs } from "fs";
2
+ import * as path from "path";
3
+ import { fileURLToPath } from "url";
4
+ import { InputFile } from "grammy";
5
+ import { config } from "../../config.js";
6
+ import { t } from "../../i18n/index.js";
7
+ import { summaryAggregator } from "../../app/managers/summary-aggregation-manager.js";
8
+ import { formatToolInfo } from "../../app/formatters/summary-formatter.js";
9
+ import { renderSubagentCards } from "../../app/formatters/subagent-formatter.js";
10
+ import { ToolMessageBatcher } from "../../app/formatters/tool-message-batcher.js";
11
+ import { getCurrentSession } from "../../app/services/session-service.js";
12
+ import { ingestSessionInfoForCache } from "../../app/services/session-cache-service.js";
13
+ import { logger } from "../../utils/logger.js";
14
+ import { safeBackgroundTask } from "../../utils/safe-background-task.js";
15
+ import { pinnedMessageManager } from "../pinned/pinned-message-manager.js";
16
+ import { keyboardManager } from "../keyboards/keyboard-manager.js";
17
+ import { clearPromptResponseMode } from "../handlers/prompt.js";
18
+ import { reconcileBusyState, setPromptResponseModeClearerForReconciliation, setResponseStreamerForReconciliation, } from "../../app/services/busy-reconciliation-service.js";
19
+ import { finalizeAssistantResponse } from "../streaming/finalize-assistant-response.js";
20
+ import { sendTtsResponseForSession } from "../handlers/tts-response-handler.js";
21
+ import { deliverThinkingMessage } from "../messages/thinking-message.js";
22
+ import { shouldSuppressUserAbortSessionError } from "../../app/managers/abort-suppression-manager.js";
23
+ import { completeDraftPart, editRenderedBotPart, getTelegramRenderedPartSignature, sendDraftBotPart, sendRenderedBotPart, } from "../messages/telegram-text.js";
24
+ import { formatAssistantRunFooter } from "../../app/formatters/assistant-run-footer-formatter.js";
25
+ import { foregroundSessionState } from "../../app/managers/foreground-session-state-manager.js";
26
+ import { scheduledTaskRuntime } from "../../app/services/scheduled-task-runtime-service.js";
27
+ import { assistantRunState } from "../../app/managers/assistant-run-state-manager.js";
28
+ import { ResponseStreamer } from "../streaming/response-streamer.js";
29
+ import { ToolCallStreamer } from "../streaming/tool-call-streamer.js";
30
+ import { attachManager } from "../../app/managers/attach-manager.js";
31
+ import { markAttachedSessionBusy, markAttachedSessionIdle, } from "../../app/services/attach-service.js";
32
+ import { externalUserInputSuppressionManager } from "../../app/managers/external-input-suppression-manager.js";
33
+ import { prepareAssistantFinalStreamingPayload, prepareAssistantStreamingPayload, renderAssistantFinalPartsSafe, } from "../messages/assistant-rendering.js";
34
+ import { deliverExternalUserInputNotification } from "../messages/external-user-input-notification.js";
35
+ import { backgroundSessionTracker, } from "../../app/managers/background-session-manager.js";
36
+ import { buildBackgroundSessionOpenKeyboard } from "../menus/session-selection-menu.js";
37
+ import { questionManager } from "../../app/managers/question-manager.js";
38
+ import { showCurrentQuestion } from "../menus/question-menu.js";
39
+ import { showPermissionRequest } from "../menus/permission-menu.js";
40
+ import { clearAllInteractionState } from "../../app/managers/interaction-manager.js";
41
+ import { stopEventListening, subscribeToEvents } from "../../opencode/events.js";
42
+ const TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH = 1024;
43
+ const RESPONSE_STREAM_THROTTLE_MS = config.bot.responseStreamThrottleMs;
44
+ const RESPONSE_STREAMING_MODE = config.bot.responseStreamingMode;
45
+ const RESPONSE_STREAM_TEXT_LIMIT = 3800;
46
+ const SESSION_RETRY_PREFIX = "🔁";
47
+ const SUBAGENT_STREAM_PREFIX = "🧩";
48
+ const __filename = fileURLToPath(import.meta.url);
49
+ const __dirname = path.dirname(__filename);
50
+ const TEMP_DIR = path.join(__dirname, "..", "..", ".tmp");
51
+ export function createEventSubscriptionService() {
52
+ return new EventSubscriptionService();
53
+ }
54
+ class EventSubscriptionService {
55
+ botInstance = null;
56
+ chatIdInstance = null;
57
+ nextDraftId = 1;
58
+ sessionCompletionTasks = new Map();
59
+ responseStreamer;
60
+ toolCallStreamer;
61
+ toolMessageBatcher;
62
+ constructor() {
63
+ this.toolMessageBatcher = new ToolMessageBatcher({
64
+ sendText: async (sessionId, text) => {
65
+ if (!this.botInstance || !this.chatIdInstance) {
66
+ return;
67
+ }
68
+ const currentSession = getCurrentSession();
69
+ if (!currentSession || currentSession.id !== sessionId) {
70
+ return;
71
+ }
72
+ const keyboard = this.getCurrentReplyKeyboard();
73
+ await this.botInstance.api.sendMessage(this.chatIdInstance, text, {
74
+ disable_notification: true,
75
+ ...(keyboard ? { reply_markup: keyboard } : {}),
76
+ });
77
+ },
78
+ sendFile: async (sessionId, fileData) => {
79
+ if (!this.botInstance || !this.chatIdInstance) {
80
+ return;
81
+ }
82
+ const currentSession = getCurrentSession();
83
+ if (!currentSession || currentSession.id !== sessionId) {
84
+ return;
85
+ }
86
+ const tempFilePath = path.join(TEMP_DIR, fileData.filename);
87
+ try {
88
+ logger.debug(`[Bot] Sending code file: ${fileData.filename} (${fileData.buffer.length} bytes, session=${sessionId})`);
89
+ await fs.mkdir(TEMP_DIR, { recursive: true });
90
+ await fs.writeFile(tempFilePath, fileData.buffer);
91
+ const keyboard = this.getCurrentReplyKeyboard();
92
+ await this.botInstance.api.sendDocument(this.chatIdInstance, new InputFile(tempFilePath), {
93
+ caption: fileData.caption,
94
+ disable_notification: true,
95
+ ...(keyboard ? { reply_markup: keyboard } : {}),
96
+ });
97
+ }
98
+ finally {
99
+ await fs.unlink(tempFilePath).catch(() => { });
100
+ }
101
+ },
102
+ });
103
+ this.responseStreamer = this.createResponseStreamer();
104
+ setResponseStreamerForReconciliation(this.responseStreamer);
105
+ setPromptResponseModeClearerForReconciliation(clearPromptResponseMode);
106
+ this.toolCallStreamer = new ToolCallStreamer({
107
+ throttleMs: RESPONSE_STREAM_THROTTLE_MS,
108
+ sendText: async (sessionId, text) => {
109
+ if (!this.botInstance || !this.chatIdInstance || this.chatIdInstance <= 0) {
110
+ throw new Error("Bot context missing for tool stream send");
111
+ }
112
+ const currentSession = getCurrentSession();
113
+ if (!currentSession || currentSession.id !== sessionId) {
114
+ throw new Error(`Tool stream session mismatch for send: ${sessionId}`);
115
+ }
116
+ const sentMessage = await this.botInstance.api.sendMessage(this.chatIdInstance, text, {
117
+ disable_notification: true,
118
+ });
119
+ return sentMessage.message_id;
120
+ },
121
+ editText: async (sessionId, messageId, text) => {
122
+ if (!this.botInstance || !this.chatIdInstance || this.chatIdInstance <= 0) {
123
+ throw new Error("Bot context missing for tool stream edit");
124
+ }
125
+ const currentSession = getCurrentSession();
126
+ if (!currentSession || currentSession.id !== sessionId) {
127
+ throw new Error(`Tool stream session mismatch for edit: ${sessionId}`);
128
+ }
129
+ try {
130
+ await this.botInstance.api.editMessageText(this.chatIdInstance, messageId, text);
131
+ }
132
+ catch (error) {
133
+ const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
134
+ if (errorMessage.includes("message is not modified")) {
135
+ return;
136
+ }
137
+ throw error;
138
+ }
139
+ },
140
+ deleteText: async (sessionId, messageId) => {
141
+ if (!this.botInstance || !this.chatIdInstance || this.chatIdInstance <= 0) {
142
+ throw new Error("Bot context missing for tool stream delete");
143
+ }
144
+ const currentSession = getCurrentSession();
145
+ if (!currentSession || currentSession.id !== sessionId) {
146
+ throw new Error(`Tool stream session mismatch for delete: ${sessionId}`);
147
+ }
148
+ await this.botInstance.api.deleteMessage(this.chatIdInstance, messageId).catch((error) => {
149
+ const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
150
+ if (errorMessage.includes("message to delete not found") ||
151
+ errorMessage.includes("message identifier is not specified")) {
152
+ return;
153
+ }
154
+ throw error;
155
+ });
156
+ },
157
+ });
158
+ }
159
+ setTelegramContext(bot, chatId) {
160
+ this.botInstance = bot;
161
+ this.chatIdInstance = chatId;
162
+ }
163
+ clearRuntimeState(reason) {
164
+ backgroundSessionTracker.clear();
165
+ this.nextDraftId = 1;
166
+ this.responseStreamer.clearAll(reason);
167
+ this.toolCallStreamer.clearAll(reason);
168
+ this.toolMessageBatcher.clearAll(reason);
169
+ this.sessionCompletionTasks.clear();
170
+ assistantRunState.clearAll(reason);
171
+ }
172
+ cleanup(reason) {
173
+ stopEventListening();
174
+ summaryAggregator.clear();
175
+ this.clearRuntimeState(reason);
176
+ this.setTelegramContext(null, null);
177
+ }
178
+ ensureEventSubscription = async (directory) => {
179
+ if (!directory) {
180
+ logger.error("No directory found for event subscription");
181
+ return;
182
+ }
183
+ summaryAggregator.setTypingIndicatorEnabled(true);
184
+ backgroundSessionTracker.setDirectory(directory);
185
+ backgroundSessionTracker.setOnNotification(this.deliverBackgroundSessionNotification);
186
+ if (!config.bot.trackBackgroundSessions) {
187
+ backgroundSessionTracker.clear();
188
+ }
189
+ summaryAggregator.setOnCleared(() => {
190
+ this.toolMessageBatcher.clearAll("summary_aggregator_clear");
191
+ this.toolCallStreamer.clearAll("summary_aggregator_clear");
192
+ this.responseStreamer.clearAll("summary_aggregator_clear");
193
+ });
194
+ summaryAggregator.setOnPartial((sessionId, messageId, messageText) => {
195
+ if (!this.botInstance || !this.chatIdInstance) {
196
+ return;
197
+ }
198
+ const currentSession = getCurrentSession();
199
+ if (!currentSession || currentSession.id !== sessionId) {
200
+ return;
201
+ }
202
+ const preparedStreamPayload = this.prepareStreamingPayload(messageText);
203
+ if (!preparedStreamPayload) {
204
+ return;
205
+ }
206
+ preparedStreamPayload.sendOptions = { disable_notification: true };
207
+ preparedStreamPayload.editOptions = undefined;
208
+ this.responseStreamer.enqueue(sessionId, messageId, preparedStreamPayload);
209
+ });
210
+ summaryAggregator.setOnComplete((sessionId, messageId, messageText, completionInfo) => {
211
+ void this.enqueueSessionCompletionTask(sessionId, async () => {
212
+ if (!this.botInstance || !this.chatIdInstance) {
213
+ logger.error("Bot or chat ID not available for sending message");
214
+ clearPromptResponseMode(sessionId);
215
+ this.responseStreamer.clearMessage(sessionId, messageId, "bot_context_missing");
216
+ this.toolCallStreamer.clearSession(sessionId, "bot_context_missing");
217
+ assistantRunState.clearRun(sessionId, "bot_context_missing");
218
+ foregroundSessionState.markIdle(sessionId);
219
+ return;
220
+ }
221
+ const currentSession = getCurrentSession();
222
+ if (currentSession?.id !== sessionId) {
223
+ clearPromptResponseMode(sessionId);
224
+ this.responseStreamer.clearMessage(sessionId, messageId, "session_mismatch");
225
+ this.toolCallStreamer.clearSession(sessionId, "session_mismatch");
226
+ assistantRunState.clearRun(sessionId, "session_mismatch");
227
+ foregroundSessionState.markIdle(sessionId);
228
+ await scheduledTaskRuntime.flushDeferredDeliveries();
229
+ return;
230
+ }
231
+ const botApi = this.botInstance.api;
232
+ const chatId = this.chatIdInstance;
233
+ try {
234
+ assistantRunState.markResponseCompleted(sessionId, {
235
+ agent: completionInfo.agent,
236
+ providerID: completionInfo.providerID,
237
+ modelID: completionInfo.modelID,
238
+ });
239
+ await finalizeAssistantResponse({
240
+ sessionId,
241
+ messageId,
242
+ messageText,
243
+ responseStreamer: this.responseStreamer,
244
+ flushPendingServiceMessages: () => Promise.all([
245
+ this.toolMessageBatcher.flushSession(sessionId, "assistant_message_completed"),
246
+ this.toolCallStreamer.breakSession(sessionId, "assistant_message_completed"),
247
+ ]).then(() => undefined),
248
+ prepareStreamingPayload: this.prepareFinalStreamingPayload,
249
+ renderFinalParts: (text) => renderAssistantFinalPartsSafe(text),
250
+ getReplyKeyboard: this.getCurrentReplyKeyboard,
251
+ sendRenderedPart: async (part, options) => {
252
+ await sendRenderedBotPart({
253
+ api: botApi,
254
+ chatId,
255
+ part,
256
+ options: options,
257
+ });
258
+ },
259
+ });
260
+ await sendTtsResponseForSession({
261
+ api: botApi,
262
+ sessionId,
263
+ chatId,
264
+ text: messageText,
265
+ });
266
+ }
267
+ catch (err) {
268
+ clearPromptResponseMode(sessionId);
269
+ assistantRunState.clearRun(sessionId, "assistant_finalize_failed");
270
+ logger.error("Failed to send message to Telegram:", err);
271
+ logger.error("[Bot] CRITICAL: Stopping event processing due to error");
272
+ summaryAggregator.clear();
273
+ foregroundSessionState.markIdle(sessionId);
274
+ }
275
+ finally {
276
+ await scheduledTaskRuntime.flushDeferredDeliveries();
277
+ }
278
+ });
279
+ });
280
+ summaryAggregator.setOnExternalUserInput(async (sessionId, _messageId, messageText) => {
281
+ void this.enqueueSessionCompletionTask(sessionId, async () => {
282
+ if (!this.botInstance || !this.chatIdInstance) {
283
+ return;
284
+ }
285
+ try {
286
+ await deliverExternalUserInputNotification({
287
+ api: this.botInstance.api,
288
+ chatId: this.chatIdInstance,
289
+ currentSessionId: getCurrentSession()?.id ?? null,
290
+ sessionId,
291
+ text: messageText,
292
+ consumeSuppressedInput: (incomingSessionId, incomingText) => externalUserInputSuppressionManager.consume(incomingSessionId, incomingText),
293
+ });
294
+ }
295
+ catch (err) {
296
+ logger.error("[Bot] Failed to deliver external user input to Telegram:", err);
297
+ }
298
+ });
299
+ });
300
+ summaryAggregator.setOnTool(async (toolInfo) => {
301
+ if (!this.botInstance || !this.chatIdInstance) {
302
+ logger.error("Bot or chat ID not available for sending tool notification");
303
+ return;
304
+ }
305
+ const currentSession = getCurrentSession();
306
+ if (!currentSession || currentSession.id !== toolInfo.sessionId) {
307
+ return;
308
+ }
309
+ const shouldIncludeToolInfoInFileCaption = toolInfo.hasFileAttachment &&
310
+ (toolInfo.tool === "write" || toolInfo.tool === "edit" || toolInfo.tool === "apply_patch");
311
+ if (config.bot.hideToolCallMessages ||
312
+ shouldIncludeToolInfoInFileCaption ||
313
+ toolInfo.tool === "task") {
314
+ return;
315
+ }
316
+ try {
317
+ const message = formatToolInfo(toolInfo);
318
+ if (message) {
319
+ this.toolCallStreamer.append(toolInfo.sessionId, message, this.getToolStreamKey(toolInfo.tool));
320
+ }
321
+ }
322
+ catch (err) {
323
+ logger.error("Failed to send tool notification to Telegram:", err);
324
+ }
325
+ });
326
+ summaryAggregator.setOnSubagent(async (sessionId, subagents) => {
327
+ if (!this.botInstance || !this.chatIdInstance) {
328
+ return;
329
+ }
330
+ if (config.bot.hideToolCallMessages) {
331
+ return;
332
+ }
333
+ const currentSession = getCurrentSession();
334
+ if (!currentSession || currentSession.id !== sessionId) {
335
+ return;
336
+ }
337
+ try {
338
+ const renderedCards = await renderSubagentCards(subagents);
339
+ if (!renderedCards) {
340
+ return;
341
+ }
342
+ this.toolCallStreamer.replaceByPrefix(sessionId, SUBAGENT_STREAM_PREFIX, renderedCards, "subagent");
343
+ }
344
+ catch (err) {
345
+ logger.error("Failed to render subagent activity for Telegram:", err);
346
+ }
347
+ });
348
+ summaryAggregator.setOnToolFile(async (fileInfo) => {
349
+ if (!this.botInstance || !this.chatIdInstance) {
350
+ logger.error("Bot or chat ID not available for sending file");
351
+ return;
352
+ }
353
+ const currentSession = getCurrentSession();
354
+ if (!currentSession || currentSession.id !== fileInfo.sessionId) {
355
+ return;
356
+ }
357
+ if (config.bot.hideToolFileMessages) {
358
+ return;
359
+ }
360
+ try {
361
+ await this.toolCallStreamer.breakSession(fileInfo.sessionId, "tool_file_boundary");
362
+ const toolMessage = formatToolInfo(fileInfo);
363
+ const caption = this.prepareDocumentCaption(toolMessage || fileInfo.fileData.caption);
364
+ this.toolMessageBatcher.enqueueFile(fileInfo.sessionId, {
365
+ ...fileInfo.fileData,
366
+ caption,
367
+ });
368
+ }
369
+ catch (err) {
370
+ logger.error("Failed to send file to Telegram:", err);
371
+ }
372
+ });
373
+ summaryAggregator.setOnQuestion(async (questions, requestID, sessionId) => {
374
+ if (!this.botInstance || !this.chatIdInstance) {
375
+ logger.error("Bot or chat ID not available for showing questions");
376
+ return;
377
+ }
378
+ const currentSession = getCurrentSession();
379
+ if (!currentSession || currentSession.id !== sessionId) {
380
+ return;
381
+ }
382
+ await Promise.all([
383
+ this.toolMessageBatcher.flushSession(currentSession.id, "question_asked"),
384
+ this.toolCallStreamer.flushSession(currentSession.id, "question_asked"),
385
+ ]);
386
+ if (questionManager.isActive()) {
387
+ logger.warn("[Bot] Replacing active poll with a new one");
388
+ const previousMessageIds = questionManager.getMessageIds();
389
+ for (const messageId of previousMessageIds) {
390
+ await this.botInstance.api.deleteMessage(this.chatIdInstance, messageId).catch(() => { });
391
+ }
392
+ clearAllInteractionState("question_replaced_by_new_poll");
393
+ }
394
+ logger.info(`[Bot] Received ${questions.length} questions from agent, requestID=${requestID}`);
395
+ questionManager.startQuestions(questions, requestID);
396
+ await showCurrentQuestion(this.botInstance.api, this.chatIdInstance);
397
+ });
398
+ summaryAggregator.setOnQuestionError(async () => {
399
+ logger.info("[Bot] Question tool failed, clearing active poll and deleting messages");
400
+ const messageIds = questionManager.getMessageIds();
401
+ for (const messageId of messageIds) {
402
+ if (this.chatIdInstance) {
403
+ await this.botInstance?.api.deleteMessage(this.chatIdInstance, messageId).catch((err) => {
404
+ logger.error(`[Bot] Failed to delete question message ${messageId}:`, err);
405
+ });
406
+ }
407
+ }
408
+ clearAllInteractionState("question_error");
409
+ });
410
+ summaryAggregator.setOnPermission(async (request) => {
411
+ if (!this.botInstance || !this.chatIdInstance) {
412
+ logger.error("Bot or chat ID not available for showing permission request");
413
+ return;
414
+ }
415
+ const currentSession = getCurrentSession();
416
+ const isCurrent = currentSession?.id === request.sessionID;
417
+ const isSubagent = summaryAggregator.isSubagentSession(request.sessionID);
418
+ if (!currentSession || (!isCurrent && !isSubagent)) {
419
+ return;
420
+ }
421
+ await Promise.all([
422
+ this.toolMessageBatcher.flushSession(request.sessionID, "permission_asked"),
423
+ this.toolCallStreamer.flushSession(request.sessionID, "permission_asked"),
424
+ ]);
425
+ logger.info(`[Bot] Received permission request from agent: type=${request.permission}, requestID=${request.id}, subagent=${isSubagent}`);
426
+ await showPermissionRequest(this.botInstance.api, this.chatIdInstance, request);
427
+ });
428
+ summaryAggregator.setOnThinking(async (sessionId) => {
429
+ if (!this.botInstance || !this.chatIdInstance) {
430
+ return;
431
+ }
432
+ const currentSession = getCurrentSession();
433
+ if (!currentSession || currentSession.id !== sessionId) {
434
+ return;
435
+ }
436
+ logger.debug("[Bot] Agent started thinking");
437
+ await this.toolCallStreamer.breakSession(sessionId, "thinking_started");
438
+ deliverThinkingMessage(sessionId, this.toolMessageBatcher, {
439
+ hideThinkingMessages: config.bot.hideThinkingMessages,
440
+ });
441
+ if (pinnedMessageManager.isInitialized()) {
442
+ await pinnedMessageManager.refresh();
443
+ }
444
+ });
445
+ summaryAggregator.setOnTokens(async (tokens, isCompleted) => {
446
+ if (!pinnedMessageManager.isInitialized()) {
447
+ return;
448
+ }
449
+ try {
450
+ logger.debug(`[Bot] Received tokens: input=${tokens.input}, output=${tokens.output}, completed=${isCompleted}`);
451
+ const contextSize = tokens.input + tokens.cacheRead;
452
+ const contextLimit = pinnedMessageManager.getContextLimit();
453
+ if (!isCompleted && contextSize === 0) {
454
+ logger.debug("[Bot] Skipping zero-token intermediate update");
455
+ return;
456
+ }
457
+ if (contextLimit > 0) {
458
+ keyboardManager.updateContext(contextSize, contextLimit);
459
+ }
460
+ pinnedMessageManager.updateTokensSilent(tokens);
461
+ if (isCompleted) {
462
+ await pinnedMessageManager.onMessageComplete(tokens);
463
+ }
464
+ }
465
+ catch (err) {
466
+ logger.error("[Bot] Error updating pinned message with tokens:", err);
467
+ }
468
+ });
469
+ summaryAggregator.setOnCost(async (cost) => {
470
+ if (!pinnedMessageManager.isInitialized()) {
471
+ return;
472
+ }
473
+ try {
474
+ logger.debug(`[Bot] Cost update: $${cost.toFixed(2)}`);
475
+ await pinnedMessageManager.onCostUpdate(cost);
476
+ }
477
+ catch (err) {
478
+ logger.error("[Bot] Error updating cost:", err);
479
+ }
480
+ });
481
+ summaryAggregator.setOnSessionCompacted(async (sessionId, directory) => {
482
+ if (!pinnedMessageManager.isInitialized()) {
483
+ return;
484
+ }
485
+ try {
486
+ logger.info(`[Bot] Session compacted, reloading context: ${sessionId}`);
487
+ await pinnedMessageManager.onSessionCompacted(sessionId, directory);
488
+ }
489
+ catch (err) {
490
+ logger.error("[Bot] Error reloading context after compaction:", err);
491
+ }
492
+ });
493
+ summaryAggregator.setOnSessionIdle(async (sessionId) => {
494
+ await markAttachedSessionIdle(sessionId);
495
+ await this.sessionCompletionTasks.get(sessionId)?.catch(() => undefined);
496
+ const completedRun = assistantRunState.finishRun(sessionId, "session_idle");
497
+ clearPromptResponseMode(sessionId);
498
+ if (!this.botInstance || !this.chatIdInstance) {
499
+ foregroundSessionState.markIdle(sessionId);
500
+ return;
501
+ }
502
+ const currentSession = getCurrentSession();
503
+ if (!currentSession || currentSession.id !== sessionId) {
504
+ foregroundSessionState.markIdle(sessionId);
505
+ await scheduledTaskRuntime.flushDeferredDeliveries();
506
+ return;
507
+ }
508
+ try {
509
+ await Promise.all([
510
+ this.toolMessageBatcher.flushSession(sessionId, "session_idle"),
511
+ this.toolCallStreamer.flushSession(sessionId, "session_idle"),
512
+ ]);
513
+ if (completedRun?.hasCompletedResponse) {
514
+ const agent = completedRun.actualAgent || completedRun.configuredAgent;
515
+ const providerID = completedRun.actualProviderID || completedRun.configuredProviderID;
516
+ const modelID = completedRun.actualModelID || completedRun.configuredModelID;
517
+ if (agent && providerID && modelID) {
518
+ const keyboard = this.getCurrentReplyKeyboard();
519
+ await this.botInstance.api.sendMessage(this.chatIdInstance, formatAssistantRunFooter({
520
+ agent,
521
+ providerID,
522
+ modelID,
523
+ elapsedMs: Date.now() - completedRun.startedAt,
524
+ }), {
525
+ ...(keyboard ? { reply_markup: keyboard } : {}),
526
+ });
527
+ }
528
+ }
529
+ }
530
+ catch (err) {
531
+ logger.error("[Bot] Failed to send session idle footer:", err);
532
+ }
533
+ finally {
534
+ foregroundSessionState.markIdle(sessionId);
535
+ await scheduledTaskRuntime.flushDeferredDeliveries();
536
+ }
537
+ });
538
+ summaryAggregator.setOnSessionError(async (sessionId, message) => {
539
+ await markAttachedSessionIdle(sessionId);
540
+ if (!this.botInstance || !this.chatIdInstance) {
541
+ clearPromptResponseMode(sessionId);
542
+ assistantRunState.clearRun(sessionId, "session_error_no_bot_context");
543
+ foregroundSessionState.markIdle(sessionId);
544
+ return;
545
+ }
546
+ const currentSession = getCurrentSession();
547
+ if (!currentSession || currentSession.id !== sessionId) {
548
+ clearPromptResponseMode(sessionId);
549
+ this.responseStreamer.clearSession(sessionId, "session_error_not_current");
550
+ this.toolCallStreamer.clearSession(sessionId, "session_error_not_current");
551
+ assistantRunState.clearRun(sessionId, "session_error_not_current");
552
+ foregroundSessionState.markIdle(sessionId);
553
+ await scheduledTaskRuntime.flushDeferredDeliveries();
554
+ return;
555
+ }
556
+ this.responseStreamer.clearSession(sessionId, "session_error");
557
+ clearPromptResponseMode(sessionId);
558
+ assistantRunState.clearRun(sessionId, "session_error");
559
+ await Promise.all([
560
+ this.toolMessageBatcher.flushSession(sessionId, "session_error"),
561
+ this.toolCallStreamer.flushSession(sessionId, "session_error"),
562
+ ]);
563
+ const normalizedMessage = message.trim() || t("common.unknown_error");
564
+ if (shouldSuppressUserAbortSessionError(sessionId, normalizedMessage)) {
565
+ logger.debug(`[Bot] Suppressed user-initiated abort error: session=${sessionId}`);
566
+ foregroundSessionState.markIdle(sessionId);
567
+ await scheduledTaskRuntime.flushDeferredDeliveries();
568
+ return;
569
+ }
570
+ const truncatedMessage = normalizedMessage.length > 3500
571
+ ? `${normalizedMessage.slice(0, 3497)}...`
572
+ : normalizedMessage;
573
+ await this.botInstance.api
574
+ .sendMessage(this.chatIdInstance, t("bot.session_error", { message: truncatedMessage }))
575
+ .catch((err) => {
576
+ logger.error("[Bot] Failed to send session.error message:", err);
577
+ });
578
+ foregroundSessionState.markIdle(sessionId);
579
+ await scheduledTaskRuntime.flushDeferredDeliveries();
580
+ });
581
+ summaryAggregator.setOnSessionRetry(async ({ sessionId, message }) => {
582
+ if (!this.botInstance || !this.chatIdInstance) {
583
+ return;
584
+ }
585
+ const currentSession = getCurrentSession();
586
+ if (!currentSession || currentSession.id !== sessionId) {
587
+ return;
588
+ }
589
+ const normalizedMessage = message.trim() || t("common.unknown_error");
590
+ const truncatedMessage = normalizedMessage.length > 3500
591
+ ? `${normalizedMessage.slice(0, 3497)}...`
592
+ : normalizedMessage;
593
+ const retryMessage = t("bot.session_retry", { message: truncatedMessage });
594
+ this.toolCallStreamer.replaceByPrefix(sessionId, SESSION_RETRY_PREFIX, retryMessage);
595
+ });
596
+ summaryAggregator.setOnSessionDiff(async (_sessionId, diffs) => {
597
+ if (!pinnedMessageManager.isInitialized()) {
598
+ return;
599
+ }
600
+ try {
601
+ await pinnedMessageManager.onSessionDiff(diffs);
602
+ }
603
+ catch (err) {
604
+ logger.error("[Bot] Error updating session diff:", err);
605
+ }
606
+ });
607
+ summaryAggregator.setOnFileChange((change) => {
608
+ if (!pinnedMessageManager.isInitialized()) {
609
+ return;
610
+ }
611
+ pinnedMessageManager.addFileChange(change);
612
+ });
613
+ pinnedMessageManager.setOnKeyboardUpdate(async (tokensUsed, tokensLimit) => {
614
+ try {
615
+ logger.debug(`[Bot] Updating keyboard with context: ${tokensUsed}/${tokensLimit}`);
616
+ keyboardManager.updateContext(tokensUsed, tokensLimit);
617
+ }
618
+ catch (err) {
619
+ logger.error("[Bot] Error updating keyboard context:", err);
620
+ }
621
+ });
622
+ logger.info(`[Bot] Subscribing to OpenCode events for project: ${directory}`);
623
+ subscribeToEvents(directory, (event) => {
624
+ if (event.type === "server.heartbeat") {
625
+ void reconcileBusyState(directory);
626
+ }
627
+ const attached = attachManager.getSnapshot();
628
+ const eventSessionId = this.getEventSessionId(event);
629
+ if (attached &&
630
+ eventSessionId === attached.sessionId &&
631
+ this.shouldMarkAttachedBusyFromEvent(event)) {
632
+ void markAttachedSessionBusy(attached.sessionId);
633
+ }
634
+ if (event.type === "session.created" || event.type === "session.updated") {
635
+ const info = event.properties.info;
636
+ if (info?.directory) {
637
+ safeBackgroundTask({
638
+ taskName: `session.cache.${event.type}`,
639
+ task: () => ingestSessionInfoForCache(info),
640
+ });
641
+ }
642
+ }
643
+ if (config.bot.trackBackgroundSessions) {
644
+ backgroundSessionTracker.processEvent(event, getCurrentSession()?.id ?? null);
645
+ }
646
+ summaryAggregator.processEvent(event);
647
+ }).catch((err) => {
648
+ logger.error("Failed to subscribe to events:", err);
649
+ });
650
+ };
651
+ createResponseStreamer() {
652
+ if (RESPONSE_STREAMING_MODE === "draft") {
653
+ return new ResponseStreamer({
654
+ throttleMs: RESPONSE_STREAM_THROTTLE_MS,
655
+ sendPart: async (part) => {
656
+ if (!this.botInstance || !this.chatIdInstance || this.chatIdInstance <= 0) {
657
+ throw new Error("Bot context missing for draft send");
658
+ }
659
+ const draftId = this.getNextDraftId();
660
+ const result = await sendDraftBotPart({
661
+ api: this.botInstance.api,
662
+ chatId: this.chatIdInstance,
663
+ draftId,
664
+ part,
665
+ });
666
+ return { messageId: draftId, deliveredSignature: result.deliveredSignature };
667
+ },
668
+ editPart: async (messageId, part) => {
669
+ if (!this.botInstance || !this.chatIdInstance || this.chatIdInstance <= 0) {
670
+ throw new Error("Bot context missing for draft edit");
671
+ }
672
+ return sendDraftBotPart({
673
+ api: this.botInstance.api,
674
+ chatId: this.chatIdInstance,
675
+ draftId: messageId,
676
+ part,
677
+ });
678
+ },
679
+ deleteText: async () => { },
680
+ completePart: async (part, options) => {
681
+ if (!this.botInstance || !this.chatIdInstance || this.chatIdInstance <= 0) {
682
+ throw new Error("Bot context missing for draft complete");
683
+ }
684
+ return completeDraftPart({
685
+ api: this.botInstance.api,
686
+ chatId: this.chatIdInstance,
687
+ part,
688
+ options,
689
+ });
690
+ },
691
+ });
692
+ }
693
+ return new ResponseStreamer({
694
+ throttleMs: RESPONSE_STREAM_THROTTLE_MS,
695
+ sendPart: async (part, options) => {
696
+ if (!this.botInstance || !this.chatIdInstance || this.chatIdInstance <= 0) {
697
+ throw new Error("Bot context missing for streamed send");
698
+ }
699
+ return sendRenderedBotPart({
700
+ api: this.botInstance.api,
701
+ chatId: this.chatIdInstance,
702
+ part,
703
+ options,
704
+ });
705
+ },
706
+ editPart: async (messageId, part, options) => {
707
+ if (!this.botInstance || !this.chatIdInstance || this.chatIdInstance <= 0) {
708
+ throw new Error("Bot context missing for streamed edit");
709
+ }
710
+ try {
711
+ return await editRenderedBotPart({
712
+ api: this.botInstance.api,
713
+ chatId: this.chatIdInstance,
714
+ messageId,
715
+ part,
716
+ options,
717
+ });
718
+ }
719
+ catch (error) {
720
+ const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
721
+ if (errorMessage.includes("message is not modified")) {
722
+ return {
723
+ deliveredSignature: getTelegramRenderedPartSignature(part),
724
+ };
725
+ }
726
+ throw error;
727
+ }
728
+ },
729
+ deleteText: async (messageId) => {
730
+ if (!this.botInstance || !this.chatIdInstance || this.chatIdInstance <= 0) {
731
+ throw new Error("Bot context missing for streamed delete");
732
+ }
733
+ await this.botInstance.api.deleteMessage(this.chatIdInstance, messageId).catch((error) => {
734
+ const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
735
+ if (errorMessage.includes("message to delete not found") ||
736
+ errorMessage.includes("message identifier is not specified")) {
737
+ return;
738
+ }
739
+ throw error;
740
+ });
741
+ },
742
+ });
743
+ }
744
+ getCurrentReplyKeyboard = () => {
745
+ if (!keyboardManager.isInitialized()) {
746
+ return undefined;
747
+ }
748
+ return keyboardManager.getKeyboard();
749
+ };
750
+ prepareDocumentCaption(caption) {
751
+ const normalizedCaption = caption.trim();
752
+ if (!normalizedCaption) {
753
+ return "";
754
+ }
755
+ if (normalizedCaption.length <= TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH) {
756
+ return normalizedCaption;
757
+ }
758
+ return `${normalizedCaption.slice(0, TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH - 3)}...`;
759
+ }
760
+ prepareStreamingPayload(messageText) {
761
+ return prepareAssistantStreamingPayload(messageText, RESPONSE_STREAM_TEXT_LIMIT);
762
+ }
763
+ prepareFinalStreamingPayload(messageText) {
764
+ return prepareAssistantFinalStreamingPayload(messageText, RESPONSE_STREAM_TEXT_LIMIT);
765
+ }
766
+ enqueueSessionCompletionTask(sessionId, task) {
767
+ const previousTask = this.sessionCompletionTasks.get(sessionId) ?? Promise.resolve();
768
+ const nextTask = previousTask
769
+ .catch(() => undefined)
770
+ .then(task)
771
+ .finally(() => {
772
+ if (this.sessionCompletionTasks.get(sessionId) === nextTask) {
773
+ this.sessionCompletionTasks.delete(sessionId);
774
+ }
775
+ });
776
+ this.sessionCompletionTasks.set(sessionId, nextTask);
777
+ return nextTask;
778
+ }
779
+ getNextDraftId() {
780
+ const id = this.nextDraftId;
781
+ this.nextDraftId += 1;
782
+ return id;
783
+ }
784
+ getToolStreamKey(tool) {
785
+ if (tool === "todowrite") {
786
+ return "todo";
787
+ }
788
+ return "default";
789
+ }
790
+ formatShortSessionId(sessionId) {
791
+ return sessionId.length <= 8 ? sessionId : sessionId.slice(0, 8);
792
+ }
793
+ getBackgroundSessionLabel(notification) {
794
+ const title = notification.sessionTitle?.trim();
795
+ if (title) {
796
+ return title;
797
+ }
798
+ return t("background.session_fallback", {
799
+ id: this.formatShortSessionId(notification.sessionId),
800
+ });
801
+ }
802
+ formatBackgroundSessionNotification(notification) {
803
+ const session = this.getBackgroundSessionLabel(notification);
804
+ switch (notification.kind) {
805
+ case "assistant_response":
806
+ return t("background.assistant_response", { session });
807
+ case "question_asked":
808
+ return t("background.question_asked", { session });
809
+ case "permission_asked":
810
+ return t("background.permission_asked", { session });
811
+ }
812
+ }
813
+ deliverBackgroundSessionNotification = async (notification) => {
814
+ if (!this.botInstance || !this.chatIdInstance) {
815
+ return;
816
+ }
817
+ await this.botInstance.api.sendMessage(this.chatIdInstance, this.formatBackgroundSessionNotification(notification), {
818
+ reply_markup: buildBackgroundSessionOpenKeyboard(notification.sessionId, notification.kind),
819
+ });
820
+ };
821
+ getEventSessionId(event) {
822
+ const properties = event.properties;
823
+ return properties.sessionID || properties.info?.sessionID || properties.part?.sessionID || null;
824
+ }
825
+ shouldMarkAttachedBusyFromEvent(event) {
826
+ switch (event.type) {
827
+ case "session.status":
828
+ return event.properties.status?.type === "busy";
829
+ case "message.updated": {
830
+ const info = event.properties.info;
831
+ return info?.role === "assistant" && !info.time?.completed;
832
+ }
833
+ case "message.part.updated":
834
+ case "message.part.delta":
835
+ case "question.asked":
836
+ case "permission.asked":
837
+ return true;
838
+ default:
839
+ return false;
840
+ }
841
+ }
842
+ }