@grinev/opencode-telegram-bot 0.21.1 → 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 (179) hide show
  1. package/.env.example +11 -1
  2. package/README.md +18 -5
  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 -1179
  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/{interaction/guard.js → bot/middleware/interaction-guard-decision.js} +11 -4
  119. package/dist/bot/middleware/interaction-guard.js +2 -2
  120. package/dist/bot/middleware/unknown-command.js +1 -1
  121. package/dist/{pinned/format.js → bot/pinned/pinned-message-format.js} +1 -2
  122. package/dist/{pinned/manager.js → bot/pinned/pinned-message-manager.js} +10 -10
  123. package/dist/bot/pinned/pinned-message-types.js +1 -0
  124. package/dist/{summary → bot/render}/markdown-to-telegram-v2.js +2 -2
  125. package/dist/bot/render/types.js +1 -0
  126. package/dist/bot/routers/command-router.js +73 -0
  127. package/dist/bot/routers/message-router.js +150 -0
  128. package/dist/bot/services/attach-presentation.js +40 -0
  129. package/dist/bot/services/event-subscription-service.js +842 -0
  130. package/dist/bot/services/project-switch-presentation.js +26 -0
  131. package/dist/cli.js +4 -4
  132. package/dist/config.js +9 -3
  133. package/dist/index.js +1 -1
  134. package/dist/opencode/ready-refresh.js +2 -2
  135. package/dist/{service → runtime/service}/manager.js +2 -2
  136. package/dist/runtime/service/types.js +1 -0
  137. package/package.json +1 -1
  138. package/dist/bot/commands/ls.js +0 -445
  139. package/dist/bot/commands/open.js +0 -319
  140. package/dist/bot/commands/worktree.js +0 -201
  141. package/dist/bot/handlers/permission.js +0 -235
  142. package/dist/bot/utils/browser-roots.js +0 -140
  143. package/dist/bot/utils/file-tree.js +0 -92
  144. package/dist/bot/utils/send-tts-response.js +0 -37
  145. package/dist/bot/utils/switch-project.js +0 -45
  146. package/dist/bot/utils/telegram-file-url.js +0 -11
  147. package/dist/interaction/busy.js +0 -8
  148. package/dist/interaction/cleanup.js +0 -32
  149. /package/dist/{scheduled-task/display.js → app/formatters/scheduled-task-display-formatter.js} +0 -0
  150. /package/dist/{bot/utils/abort-error-suppression.js → app/managers/abort-suppression-manager.js} +0 -0
  151. /package/dist/{external-input/suppression.js → app/managers/external-input-suppression-manager.js} +0 -0
  152. /package/dist/{scheduled-task/next-run.js → app/services/scheduled-task-next-run-service.js} +0 -0
  153. /package/dist/{git/worktree.js → app/services/worktree-service.js} +0 -0
  154. /package/dist/{agent/types.js → app/types/agent.js} +0 -0
  155. /package/dist/{interaction/types.js → app/types/interaction.js} +0 -0
  156. /package/dist/{model/types.js → app/types/model.js} +0 -0
  157. /package/dist/{keyboard/types.js → app/types/permission.js} +0 -0
  158. /package/dist/{permission/types.js → app/types/project.js} +0 -0
  159. /package/dist/{pinned/types.js → app/types/question.js} +0 -0
  160. /package/dist/{scheduled-task/types.js → app/types/scheduled-task.js} +0 -0
  161. /package/dist/{question/types.js → app/types/session.js} +0 -0
  162. /package/dist/{service/types.js → app/types/settings.js} +0 -0
  163. /package/dist/{telegram/render/types.js → app/types/summary.js} +0 -0
  164. /package/dist/{variant/types.js → app/types/variant.js} +0 -0
  165. /package/dist/bot/commands/{help.js → help-command.js} +0 -0
  166. /package/dist/bot/{utils → messages}/send-with-markdown-fallback.js +0 -0
  167. /package/dist/bot/{utils → messages}/telegram-text.js +0 -0
  168. /package/dist/bot/{utils → messages}/thinking-message.js +0 -0
  169. /package/dist/{telegram → bot}/render/block-fallback.js +0 -0
  170. /package/dist/{telegram → bot}/render/block-parser.js +0 -0
  171. /package/dist/{telegram → bot}/render/block-renderer.js +0 -0
  172. /package/dist/{telegram → bot}/render/chunker.js +0 -0
  173. /package/dist/{telegram → bot}/render/inline-renderer.js +0 -0
  174. /package/dist/{telegram → bot}/render/markdown-normalizer.js +0 -0
  175. /package/dist/{telegram → bot}/render/pipeline.js +0 -0
  176. /package/dist/{telegram → bot}/render/validator.js +0 -0
  177. /package/dist/bot/{utils/commands.js → routers/command-utils.js} +0 -0
  178. /package/dist/bot/{utils → streaming}/finalize-assistant-response.js +0 -0
  179. /package/dist/{service/runtime.js → runtime/service/env.js} +0 -0
package/dist/bot/index.js CHANGED
@@ -1,913 +1,42 @@
1
- import { Bot, InputFile } from "grammy";
2
- import { promises as fs } from "fs";
3
- import * as path from "path";
4
- import { fileURLToPath } from "url";
1
+ import { Bot } from "grammy";
5
2
  import { config } from "../config.js";
6
- import { authMiddleware } from "./middleware/auth.js";
7
- import { interactionGuardMiddleware } from "./middleware/interaction-guard.js";
8
- import { unknownCommandMiddleware } from "./middleware/unknown-command.js";
9
- import { BOT_COMMANDS } from "./commands/definitions.js";
10
- import { startCommand } from "./commands/start.js";
11
- import { helpCommand } from "./commands/help.js";
12
- import { statusCommand } from "./commands/status.js";
13
- import { AGENT_MODE_BUTTON_TEXT_PATTERN, MODEL_BUTTON_TEXT_PATTERN, VARIANT_BUTTON_TEXT_PATTERN, } from "./message-patterns.js";
14
- import { buildBackgroundSessionOpenKeyboard, handleBackgroundSessionOpen, handleSessionSelect, sessionsCommand, } from "./commands/sessions.js";
15
- import { newCommand } from "./commands/new.js";
16
- import { projectsCommand, handleProjectSelect } from "./commands/projects.js";
17
- import { worktreeCommand, handleWorktreeCallback } from "./commands/worktree.js";
18
- import { openCommand, handleOpenCallback, clearOpenPathIndex } from "./commands/open.js";
19
- import { clearLsPathIndex, handleLsCallback, lsCommand } from "./commands/ls.js";
20
- import { abortCommand } from "./commands/abort.js";
21
- import { detachCommand } from "./commands/detach.js";
22
- import { opencodeStartCommand } from "./commands/opencode-start.js";
23
- import { opencodeStopCommand } from "./commands/opencode-stop.js";
24
- import { renameCommand, handleRenameCancel, handleRenameTextAnswer } from "./commands/rename.js";
25
- import { handleTaskCallback, handleTaskTextInput, taskCommand } from "./commands/task.js";
26
- import { handleTaskListCallback, taskListCommand } from "./commands/tasklist.js";
27
- import { commandsCommand, handleCommandsCallback, handleCommandTextArguments, } from "./commands/commands.js";
28
- import { handleMessagesCallback, messagesCommand } from "./commands/messages.js";
29
- import { skillsCommand, handleSkillsCallback, handleSkillTextArguments, } from "./commands/skills.js";
30
- import { mcpsCommand, handleMcpsCallback } from "./commands/mcps.js";
31
- import { ttsCommand } from "./commands/tts.js";
32
- import { handleQuestionCallback, showCurrentQuestion, handleQuestionTextAnswer, } from "./handlers/question.js";
33
- import { handlePermissionCallback, showPermissionRequest } from "./handlers/permission.js";
34
- import { handleAgentSelect, showAgentSelectionMenu } from "./handlers/agent.js";
35
- import { handleModelSelect, handleModelSearchCallback, handleModelSearchResults, handleModelSearchTextInput, showModelSelectionMenu, } from "./handlers/model.js";
36
- import { handleVariantSelect, showVariantSelectionMenu } from "./handlers/variant.js";
37
- import { handleContextButtonPress, handleCompactConfirm } from "./handlers/context.js";
38
- import { handleInlineMenuCancel } from "./handlers/inline-menu.js";
39
- import { questionManager } from "../question/manager.js";
40
- import { interactionManager } from "../interaction/manager.js";
41
- import { clearAllInteractionState } from "../interaction/cleanup.js";
42
- import { keyboardManager } from "../keyboard/manager.js";
43
- import { stopEventListening, subscribeToEvents } from "../opencode/events.js";
3
+ import { getCurrentProject } from "../app/stores/settings-store.js";
4
+ import { attachManager } from "../app/managers/attach-manager.js";
5
+ import { clearAllInteractionState } from "../app/managers/interaction-manager.js";
6
+ import { configureAttachPresentation, restoreAttachedCurrentSession, } from "../app/services/attach-service.js";
44
7
  import { opencodeReadyLifecycle } from "../opencode/ready-lifecycle.js";
45
- import { summaryAggregator } from "../summary/aggregator.js";
46
- import { formatToolInfo } from "../summary/formatter.js";
47
- import { renderSubagentCards } from "../summary/subagent-formatter.js";
48
- import { ToolMessageBatcher } from "../summary/tool-message-batcher.js";
49
- import { getCurrentSession } from "../session/manager.js";
50
- import { ingestSessionInfoForCache } from "../session/cache-manager.js";
51
8
  import { logger } from "../utils/logger.js";
52
9
  import { safeBackgroundTask } from "../utils/safe-background-task.js";
53
10
  import { withTelegramRateLimitRetry } from "../utils/telegram-rate-limit-retry.js";
54
- import { pinnedMessageManager } from "../pinned/manager.js";
55
- import { t } from "../i18n/index.js";
56
- import { getCurrentProject } from "../settings/manager.js";
11
+ import { registerCallbackRouter } from "./callbacks/callback-router.js";
12
+ import { authMiddleware } from "./middleware/auth.js";
13
+ import { interactionGuardMiddleware } from "./middleware/interaction-guard.js";
14
+ import { ensureCommandsInitialized, registerCommandRouter, } from "./routers/command-router.js";
15
+ import { registerMessageRouter } from "./routers/message-router.js";
16
+ import { createEventSubscriptionService, } from "./services/event-subscription-service.js";
17
+ import { createAttachPresentation } from "./services/attach-presentation.js";
57
18
  import { createTelegramBotOptions } from "./telegram-client-options.js";
58
- import { clearPromptResponseMode, processUserPrompt } from "./handlers/prompt.js";
59
- import { handleVoiceMessage } from "./handlers/voice.js";
60
- import { handleDocumentMessage } from "./handlers/document.js";
61
- import { createMediaGroupAttachmentMiddleware } from "./handlers/media-group.js";
62
- import { downloadTelegramFile, toDataUri } from "./utils/file-download.js";
63
- import { reconcileBusyState, setResponseStreamerForReconciliation } from "./utils/busy-reconciliation.js";
64
- import { finalizeAssistantResponse } from "./utils/finalize-assistant-response.js";
65
- import { sendTtsResponseForSession } from "./utils/send-tts-response.js";
66
- import { deliverThinkingMessage } from "./utils/thinking-message.js";
67
- import { shouldSuppressUserAbortSessionError } from "./utils/abort-error-suppression.js";
68
- import { completeDraftPart, editRenderedBotPart, getTelegramRenderedPartSignature, sendDraftBotPart, sendRenderedBotPart, } from "./utils/telegram-text.js";
69
- import { formatAssistantRunFooter } from "./utils/assistant-run-footer.js";
70
- import { getModelCapabilities, supportsInput } from "../model/capabilities.js";
71
- import { getStoredModel } from "../model/manager.js";
72
- import { foregroundSessionState } from "../scheduled-task/foreground-state.js";
73
- import { scheduledTaskRuntime } from "../scheduled-task/runtime.js";
74
- import { assistantRunState } from "./assistant-run-state.js";
75
- import { ResponseStreamer } from "./streaming/response-streamer.js";
76
- import { ToolCallStreamer } from "./streaming/tool-call-streamer.js";
77
- import { attachManager } from "../attach/manager.js";
78
- import { markAttachedSessionBusy, markAttachedSessionIdle, restoreAttachedCurrentSession, } from "../attach/service.js";
79
- import { externalUserInputSuppressionManager } from "../external-input/suppression.js";
80
- import { prepareAssistantFinalStreamingPayload, prepareAssistantStreamingPayload, renderAssistantFinalPartsSafe, } from "./utils/assistant-rendering.js";
81
- import { deliverExternalUserInputNotification } from "./utils/external-user-input.js";
82
- import { backgroundSessionTracker, } from "../background-session/tracker.js";
83
- let botInstance = null;
84
- let chatIdInstance = null;
85
- let commandsInitialized = false;
86
19
  let heartbeatTimer = null;
87
20
  let unsubscribeReadyRestore = null;
88
- const TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH = 1024;
89
- const RESPONSE_STREAM_THROTTLE_MS = config.bot.responseStreamThrottleMs;
90
- const RESPONSE_STREAMING_MODE = config.bot.responseStreamingMode;
91
- const RESPONSE_STREAM_TEXT_LIMIT = 3800;
92
- const SESSION_RETRY_PREFIX = "🔁";
93
- const SUBAGENT_STREAM_PREFIX = "🧩";
94
- const __filename = fileURLToPath(import.meta.url);
95
- const __dirname = path.dirname(__filename);
96
- const TEMP_DIR = path.join(__dirname, "..", ".tmp");
97
- const sessionCompletionTasks = new Map();
98
- function getCurrentReplyKeyboard() {
99
- if (!keyboardManager.isInitialized()) {
100
- return undefined;
101
- }
102
- return keyboardManager.getKeyboard();
103
- }
104
- function prepareDocumentCaption(caption) {
105
- const normalizedCaption = caption.trim();
106
- if (!normalizedCaption) {
107
- return "";
108
- }
109
- if (normalizedCaption.length <= TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH) {
110
- return normalizedCaption;
111
- }
112
- return `${normalizedCaption.slice(0, TELEGRAM_DOCUMENT_CAPTION_MAX_LENGTH - 3)}...`;
113
- }
114
- function prepareStreamingPayload(messageText) {
115
- return prepareAssistantStreamingPayload(messageText, RESPONSE_STREAM_TEXT_LIMIT);
116
- }
117
- function prepareFinalStreamingPayload(messageText) {
118
- return prepareAssistantFinalStreamingPayload(messageText, RESPONSE_STREAM_TEXT_LIMIT);
119
- }
120
- function enqueueSessionCompletionTask(sessionId, task) {
121
- const previousTask = sessionCompletionTasks.get(sessionId) ?? Promise.resolve();
122
- const nextTask = previousTask
123
- .catch(() => undefined)
124
- .then(task)
125
- .finally(() => {
126
- if (sessionCompletionTasks.get(sessionId) === nextTask) {
127
- sessionCompletionTasks.delete(sessionId);
128
- }
129
- });
130
- sessionCompletionTasks.set(sessionId, nextTask);
131
- return nextTask;
132
- }
133
- const toolMessageBatcher = new ToolMessageBatcher({
134
- sendText: async (sessionId, text) => {
135
- if (!botInstance || !chatIdInstance) {
136
- return;
137
- }
138
- const currentSession = getCurrentSession();
139
- if (!currentSession || currentSession.id !== sessionId) {
140
- return;
141
- }
142
- const keyboard = getCurrentReplyKeyboard();
143
- await botInstance.api.sendMessage(chatIdInstance, text, {
144
- disable_notification: true,
145
- ...(keyboard ? { reply_markup: keyboard } : {}),
146
- });
147
- },
148
- sendFile: async (sessionId, fileData) => {
149
- if (!botInstance || !chatIdInstance) {
150
- return;
151
- }
152
- const currentSession = getCurrentSession();
153
- if (!currentSession || currentSession.id !== sessionId) {
154
- return;
155
- }
156
- const tempFilePath = path.join(TEMP_DIR, fileData.filename);
157
- try {
158
- logger.debug(`[Bot] Sending code file: ${fileData.filename} (${fileData.buffer.length} bytes, session=${sessionId})`);
159
- await fs.mkdir(TEMP_DIR, { recursive: true });
160
- await fs.writeFile(tempFilePath, fileData.buffer);
161
- const keyboard = getCurrentReplyKeyboard();
162
- await botInstance.api.sendDocument(chatIdInstance, new InputFile(tempFilePath), {
163
- caption: fileData.caption,
164
- disable_notification: true,
165
- ...(keyboard ? { reply_markup: keyboard } : {}),
166
- });
167
- }
168
- finally {
169
- await fs.unlink(tempFilePath).catch(() => { });
170
- }
171
- },
172
- });
173
- let nextDraftId = 1;
174
- function getNextDraftId() {
175
- const id = nextDraftId;
176
- nextDraftId += 1;
177
- return id;
178
- }
179
- const responseStreamer = RESPONSE_STREAMING_MODE === "draft"
180
- ? new ResponseStreamer({
181
- throttleMs: RESPONSE_STREAM_THROTTLE_MS,
182
- sendPart: async (part) => {
183
- if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
184
- throw new Error("Bot context missing for draft send");
185
- }
186
- const draftId = getNextDraftId();
187
- const result = await sendDraftBotPart({
188
- api: botInstance.api,
189
- chatId: chatIdInstance,
190
- draftId,
191
- part,
192
- });
193
- return { messageId: draftId, deliveredSignature: result.deliveredSignature };
194
- },
195
- editPart: async (messageId, part) => {
196
- if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
197
- throw new Error("Bot context missing for draft edit");
198
- }
199
- return sendDraftBotPart({
200
- api: botInstance.api,
201
- chatId: chatIdInstance,
202
- draftId: messageId,
203
- part,
204
- });
205
- },
206
- deleteText: async () => {
207
- // Drafts are ephemeral — no need to delete
208
- },
209
- completePart: async (part, options) => {
210
- if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
211
- throw new Error("Bot context missing for draft complete");
212
- }
213
- return completeDraftPart({
214
- api: botInstance.api,
215
- chatId: chatIdInstance,
216
- part,
217
- options,
218
- });
219
- },
220
- })
221
- : new ResponseStreamer({
222
- throttleMs: RESPONSE_STREAM_THROTTLE_MS,
223
- sendPart: async (part, options) => {
224
- if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
225
- throw new Error("Bot context missing for streamed send");
226
- }
227
- return sendRenderedBotPart({
228
- api: botInstance.api,
229
- chatId: chatIdInstance,
230
- part,
231
- options,
232
- });
233
- },
234
- editPart: async (messageId, part, options) => {
235
- if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
236
- throw new Error("Bot context missing for streamed edit");
237
- }
238
- try {
239
- return await editRenderedBotPart({
240
- api: botInstance.api,
241
- chatId: chatIdInstance,
242
- messageId,
243
- part,
244
- options,
245
- });
246
- }
247
- catch (error) {
248
- const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
249
- if (errorMessage.includes("message is not modified")) {
250
- return {
251
- deliveredSignature: getTelegramRenderedPartSignature(part),
252
- };
253
- }
254
- throw error;
255
- }
256
- },
257
- deleteText: async (messageId) => {
258
- if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
259
- throw new Error("Bot context missing for streamed delete");
260
- }
261
- await botInstance.api.deleteMessage(chatIdInstance, messageId).catch((error) => {
262
- const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
263
- if (errorMessage.includes("message to delete not found") ||
264
- errorMessage.includes("message identifier is not specified")) {
265
- return;
266
- }
267
- throw error;
268
- });
269
- },
270
- });
271
- setResponseStreamerForReconciliation(responseStreamer);
272
- const toolCallStreamer = new ToolCallStreamer({
273
- throttleMs: RESPONSE_STREAM_THROTTLE_MS,
274
- sendText: async (sessionId, text) => {
275
- if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
276
- throw new Error("Bot context missing for tool stream send");
277
- }
278
- const currentSession = getCurrentSession();
279
- if (!currentSession || currentSession.id !== sessionId) {
280
- throw new Error(`Tool stream session mismatch for send: ${sessionId}`);
281
- }
282
- const sentMessage = await botInstance.api.sendMessage(chatIdInstance, text, {
283
- disable_notification: true,
284
- });
285
- return sentMessage.message_id;
286
- },
287
- editText: async (sessionId, messageId, text) => {
288
- if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
289
- throw new Error("Bot context missing for tool stream edit");
290
- }
291
- const currentSession = getCurrentSession();
292
- if (!currentSession || currentSession.id !== sessionId) {
293
- throw new Error(`Tool stream session mismatch for edit: ${sessionId}`);
294
- }
295
- try {
296
- await botInstance.api.editMessageText(chatIdInstance, messageId, text);
297
- }
298
- catch (error) {
299
- const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
300
- if (errorMessage.includes("message is not modified")) {
301
- return;
302
- }
303
- throw error;
304
- }
305
- },
306
- deleteText: async (sessionId, messageId) => {
307
- if (!botInstance || !chatIdInstance || chatIdInstance <= 0) {
308
- throw new Error("Bot context missing for tool stream delete");
309
- }
310
- const currentSession = getCurrentSession();
311
- if (!currentSession || currentSession.id !== sessionId) {
312
- throw new Error(`Tool stream session mismatch for delete: ${sessionId}`);
313
- }
314
- await botInstance.api.deleteMessage(chatIdInstance, messageId).catch((error) => {
315
- const errorMessage = error instanceof Error ? error.message.toLowerCase() : String(error).toLowerCase();
316
- if (errorMessage.includes("message to delete not found") ||
317
- errorMessage.includes("message identifier is not specified")) {
318
- return;
319
- }
320
- throw error;
321
- });
322
- },
323
- });
324
- function getToolStreamKey(tool) {
325
- if (tool === "todowrite") {
326
- return "todo";
327
- }
328
- return "default";
329
- }
330
- function formatShortSessionId(sessionId) {
331
- return sessionId.length <= 8 ? sessionId : sessionId.slice(0, 8);
332
- }
333
- function getBackgroundSessionLabel(notification) {
334
- const title = notification.sessionTitle?.trim();
335
- if (title) {
336
- return title;
337
- }
338
- return t("background.session_fallback", { id: formatShortSessionId(notification.sessionId) });
339
- }
340
- function formatBackgroundSessionNotification(notification) {
341
- const session = getBackgroundSessionLabel(notification);
342
- switch (notification.kind) {
343
- case "assistant_response":
344
- return t("background.assistant_response", { session });
345
- case "question_asked":
346
- return t("background.question_asked", { session });
347
- case "permission_asked":
348
- return t("background.permission_asked", { session });
349
- }
350
- }
351
- async function deliverBackgroundSessionNotification(notification) {
352
- if (!botInstance || !chatIdInstance) {
353
- return;
354
- }
355
- await botInstance.api.sendMessage(chatIdInstance, formatBackgroundSessionNotification(notification), {
356
- reply_markup: buildBackgroundSessionOpenKeyboard(notification.sessionId, notification.kind),
357
- });
358
- }
359
- function getEventSessionId(event) {
360
- const properties = event.properties;
361
- return properties.sessionID || properties.info?.sessionID || properties.part?.sessionID || null;
362
- }
363
- function shouldMarkAttachedBusyFromEvent(event) {
364
- switch (event.type) {
365
- case "session.status":
366
- return event.properties.status?.type === "busy";
367
- case "message.updated": {
368
- const info = event.properties
369
- .info;
370
- return info?.role === "assistant" && !info.time?.completed;
371
- }
372
- case "message.part.updated":
373
- case "message.part.delta":
374
- case "question.asked":
375
- case "permission.asked":
376
- return true;
377
- default:
378
- return false;
379
- }
380
- }
381
- async function ensureCommandsInitialized(ctx, next) {
382
- if (commandsInitialized || !ctx.from || ctx.from.id !== config.telegram.allowedUserId) {
383
- await next();
384
- return;
385
- }
386
- if (!ctx.chat) {
387
- logger.warn("[Bot] Cannot initialize commands: chat context is missing");
388
- await next();
389
- return;
390
- }
391
- try {
392
- await ctx.api.setMyCommands(BOT_COMMANDS, {
393
- scope: {
394
- type: "chat",
395
- chat_id: ctx.chat.id,
396
- },
397
- });
398
- commandsInitialized = true;
399
- logger.debug(`[Bot] Commands initialized for authorized user (chat_id=${ctx.chat.id})`);
400
- }
401
- catch (err) {
402
- logger.error("[Bot] Failed to set commands:", err);
403
- }
404
- await next();
405
- }
406
- async function ensureEventSubscription(directory) {
407
- if (!directory) {
408
- logger.error("No directory found for event subscription");
409
- return;
410
- }
411
- summaryAggregator.setTypingIndicatorEnabled(true);
412
- backgroundSessionTracker.setDirectory(directory);
413
- backgroundSessionTracker.setOnNotification(deliverBackgroundSessionNotification);
414
- if (!config.bot.trackBackgroundSessions) {
415
- backgroundSessionTracker.clear();
416
- }
417
- summaryAggregator.setOnCleared(() => {
418
- toolMessageBatcher.clearAll("summary_aggregator_clear");
419
- toolCallStreamer.clearAll("summary_aggregator_clear");
420
- responseStreamer.clearAll("summary_aggregator_clear");
421
- });
422
- summaryAggregator.setOnPartial((sessionId, messageId, messageText) => {
423
- if (!botInstance || !chatIdInstance) {
424
- return;
425
- }
426
- const currentSession = getCurrentSession();
427
- if (!currentSession || currentSession.id !== sessionId) {
428
- return;
429
- }
430
- const preparedStreamPayload = prepareStreamingPayload(messageText);
431
- if (!preparedStreamPayload) {
432
- return;
433
- }
434
- // Reply keyboards make the first streamed message non-editable in Telegram,
435
- // so partial chunks must be sent without reply_markup and finalized later.
436
- preparedStreamPayload.sendOptions = { disable_notification: true };
437
- preparedStreamPayload.editOptions = undefined;
438
- responseStreamer.enqueue(sessionId, messageId, preparedStreamPayload);
439
- });
440
- summaryAggregator.setOnComplete((sessionId, messageId, messageText, completionInfo) => {
441
- void enqueueSessionCompletionTask(sessionId, async () => {
442
- if (!botInstance || !chatIdInstance) {
443
- logger.error("Bot or chat ID not available for sending message");
444
- clearPromptResponseMode(sessionId);
445
- responseStreamer.clearMessage(sessionId, messageId, "bot_context_missing");
446
- toolCallStreamer.clearSession(sessionId, "bot_context_missing");
447
- assistantRunState.clearRun(sessionId, "bot_context_missing");
448
- foregroundSessionState.markIdle(sessionId);
449
- return;
450
- }
451
- const currentSession = getCurrentSession();
452
- if (currentSession?.id !== sessionId) {
453
- clearPromptResponseMode(sessionId);
454
- responseStreamer.clearMessage(sessionId, messageId, "session_mismatch");
455
- toolCallStreamer.clearSession(sessionId, "session_mismatch");
456
- assistantRunState.clearRun(sessionId, "session_mismatch");
457
- foregroundSessionState.markIdle(sessionId);
458
- await scheduledTaskRuntime.flushDeferredDeliveries();
459
- return;
460
- }
461
- const botApi = botInstance.api;
462
- const chatId = chatIdInstance;
463
- try {
464
- assistantRunState.markResponseCompleted(sessionId, {
465
- agent: completionInfo.agent,
466
- providerID: completionInfo.providerID,
467
- modelID: completionInfo.modelID,
468
- });
469
- await finalizeAssistantResponse({
470
- sessionId,
471
- messageId,
472
- messageText,
473
- responseStreamer,
474
- flushPendingServiceMessages: () => Promise.all([
475
- toolMessageBatcher.flushSession(sessionId, "assistant_message_completed"),
476
- toolCallStreamer.breakSession(sessionId, "assistant_message_completed"),
477
- ]).then(() => undefined),
478
- prepareStreamingPayload: prepareFinalStreamingPayload,
479
- renderFinalParts: (text) => renderAssistantFinalPartsSafe(text),
480
- getReplyKeyboard: getCurrentReplyKeyboard,
481
- sendRenderedPart: async (part, options) => {
482
- await sendRenderedBotPart({
483
- api: botApi,
484
- chatId,
485
- part,
486
- options: options,
487
- });
488
- },
489
- });
490
- await sendTtsResponseForSession({
491
- api: botApi,
492
- sessionId,
493
- chatId,
494
- text: messageText,
495
- });
496
- }
497
- catch (err) {
498
- clearPromptResponseMode(sessionId);
499
- assistantRunState.clearRun(sessionId, "assistant_finalize_failed");
500
- logger.error("Failed to send message to Telegram:", err);
501
- // Stop processing events after critical error to prevent infinite loop
502
- logger.error("[Bot] CRITICAL: Stopping event processing due to error");
503
- summaryAggregator.clear();
504
- foregroundSessionState.markIdle(sessionId);
505
- }
506
- finally {
507
- await scheduledTaskRuntime.flushDeferredDeliveries();
508
- }
509
- });
510
- });
511
- summaryAggregator.setOnExternalUserInput(async (sessionId, _messageId, messageText) => {
512
- void enqueueSessionCompletionTask(sessionId, async () => {
513
- if (!botInstance || !chatIdInstance) {
514
- return;
515
- }
516
- try {
517
- await deliverExternalUserInputNotification({
518
- api: botInstance.api,
519
- chatId: chatIdInstance,
520
- currentSessionId: getCurrentSession()?.id ?? null,
521
- sessionId,
522
- text: messageText,
523
- consumeSuppressedInput: (incomingSessionId, incomingText) => externalUserInputSuppressionManager.consume(incomingSessionId, incomingText),
524
- });
525
- }
526
- catch (err) {
527
- logger.error("[Bot] Failed to deliver external user input to Telegram:", err);
528
- }
529
- });
530
- });
531
- summaryAggregator.setOnTool(async (toolInfo) => {
532
- if (!botInstance || !chatIdInstance) {
533
- logger.error("Bot or chat ID not available for sending tool notification");
534
- return;
535
- }
536
- const currentSession = getCurrentSession();
537
- if (!currentSession || currentSession.id !== toolInfo.sessionId) {
538
- return;
539
- }
540
- const shouldIncludeToolInfoInFileCaption = toolInfo.hasFileAttachment &&
541
- (toolInfo.tool === "write" || toolInfo.tool === "edit" || toolInfo.tool === "apply_patch");
542
- if (config.bot.hideToolCallMessages ||
543
- shouldIncludeToolInfoInFileCaption ||
544
- toolInfo.tool === "task") {
545
- return;
546
- }
547
- try {
548
- const message = formatToolInfo(toolInfo);
549
- if (message) {
550
- toolCallStreamer.append(toolInfo.sessionId, message, getToolStreamKey(toolInfo.tool));
551
- }
552
- }
553
- catch (err) {
554
- logger.error("Failed to send tool notification to Telegram:", err);
555
- }
556
- });
557
- summaryAggregator.setOnSubagent(async (sessionId, subagents) => {
558
- if (!botInstance || !chatIdInstance) {
559
- return;
560
- }
561
- if (config.bot.hideToolCallMessages) {
562
- return;
563
- }
564
- const currentSession = getCurrentSession();
565
- if (!currentSession || currentSession.id !== sessionId) {
566
- return;
567
- }
568
- try {
569
- const renderedCards = await renderSubagentCards(subagents);
570
- if (!renderedCards) {
571
- return;
572
- }
573
- toolCallStreamer.replaceByPrefix(sessionId, SUBAGENT_STREAM_PREFIX, renderedCards, "subagent");
574
- }
575
- catch (err) {
576
- logger.error("Failed to render subagent activity for Telegram:", err);
577
- }
578
- });
579
- summaryAggregator.setOnToolFile(async (fileInfo) => {
580
- if (!botInstance || !chatIdInstance) {
581
- logger.error("Bot or chat ID not available for sending file");
582
- return;
583
- }
584
- const currentSession = getCurrentSession();
585
- if (!currentSession || currentSession.id !== fileInfo.sessionId) {
586
- return;
587
- }
588
- if (config.bot.hideToolFileMessages) {
589
- return;
590
- }
591
- try {
592
- await toolCallStreamer.breakSession(fileInfo.sessionId, "tool_file_boundary");
593
- const toolMessage = formatToolInfo(fileInfo);
594
- const caption = prepareDocumentCaption(toolMessage || fileInfo.fileData.caption);
595
- toolMessageBatcher.enqueueFile(fileInfo.sessionId, {
596
- ...fileInfo.fileData,
597
- caption,
598
- });
599
- }
600
- catch (err) {
601
- logger.error("Failed to send file to Telegram:", err);
602
- }
603
- });
604
- summaryAggregator.setOnQuestion(async (questions, requestID, sessionId) => {
605
- if (!botInstance || !chatIdInstance) {
606
- logger.error("Bot or chat ID not available for showing questions");
607
- return;
608
- }
609
- const currentSession = getCurrentSession();
610
- if (!currentSession || currentSession.id !== sessionId) {
611
- return;
612
- }
613
- await Promise.all([
614
- toolMessageBatcher.flushSession(currentSession.id, "question_asked"),
615
- toolCallStreamer.flushSession(currentSession.id, "question_asked"),
616
- ]);
617
- if (questionManager.isActive()) {
618
- logger.warn("[Bot] Replacing active poll with a new one");
619
- const previousMessageIds = questionManager.getMessageIds();
620
- for (const messageId of previousMessageIds) {
621
- await botInstance.api.deleteMessage(chatIdInstance, messageId).catch(() => { });
622
- }
623
- clearAllInteractionState("question_replaced_by_new_poll");
624
- }
625
- logger.info(`[Bot] Received ${questions.length} questions from agent, requestID=${requestID}`);
626
- questionManager.startQuestions(questions, requestID);
627
- await showCurrentQuestion(botInstance.api, chatIdInstance);
628
- });
629
- summaryAggregator.setOnQuestionError(async () => {
630
- logger.info(`[Bot] Question tool failed, clearing active poll and deleting messages`);
631
- // Delete all messages from the invalid poll
632
- const messageIds = questionManager.getMessageIds();
633
- for (const messageId of messageIds) {
634
- if (chatIdInstance) {
635
- await botInstance?.api.deleteMessage(chatIdInstance, messageId).catch((err) => {
636
- logger.error(`[Bot] Failed to delete question message ${messageId}:`, err);
637
- });
638
- }
639
- }
640
- clearAllInteractionState("question_error");
641
- });
642
- summaryAggregator.setOnPermission(async (request) => {
643
- if (!botInstance || !chatIdInstance) {
644
- logger.error("Bot or chat ID not available for showing permission request");
645
- return;
646
- }
647
- const currentSession = getCurrentSession();
648
- const isCurrent = currentSession?.id === request.sessionID;
649
- const isSubagent = summaryAggregator.isSubagentSession(request.sessionID);
650
- if (!currentSession || (!isCurrent && !isSubagent)) {
651
- return;
652
- }
653
- await Promise.all([
654
- toolMessageBatcher.flushSession(request.sessionID, "permission_asked"),
655
- toolCallStreamer.flushSession(request.sessionID, "permission_asked"),
656
- ]);
657
- logger.info(`[Bot] Received permission request from agent: type=${request.permission}, requestID=${request.id}, subagent=${isSubagent}`);
658
- await showPermissionRequest(botInstance.api, chatIdInstance, request);
659
- });
660
- summaryAggregator.setOnThinking(async (sessionId) => {
661
- if (!botInstance || !chatIdInstance) {
662
- return;
663
- }
664
- const currentSession = getCurrentSession();
665
- if (!currentSession || currentSession.id !== sessionId) {
666
- return;
667
- }
668
- logger.debug("[Bot] Agent started thinking");
669
- await toolCallStreamer.breakSession(sessionId, "thinking_started");
670
- deliverThinkingMessage(sessionId, toolMessageBatcher, {
671
- hideThinkingMessages: config.bot.hideThinkingMessages,
672
- });
673
- // Refresh pinned message so it shows the latest in-memory context
674
- // (accumulated from silent token updates). 1 API call per thinking event.
675
- if (pinnedMessageManager.isInitialized()) {
676
- await pinnedMessageManager.refresh();
677
- }
678
- });
679
- summaryAggregator.setOnTokens(async (tokens, isCompleted) => {
680
- if (!pinnedMessageManager.isInitialized()) {
681
- return;
682
- }
683
- try {
684
- logger.debug(`[Bot] Received tokens: input=${tokens.input}, output=${tokens.output}, completed=${isCompleted}`);
685
- const contextSize = tokens.input + tokens.cacheRead;
686
- const contextLimit = pinnedMessageManager.getContextLimit();
687
- // Skip non-completed messages with zero context: a new assistant message
688
- // starts with tokens={input:0, ...} which would overwrite valid context
689
- // from the previous step. Only accept zeros from completed messages.
690
- if (!isCompleted && contextSize === 0) {
691
- logger.debug("[Bot] Skipping zero-token intermediate update");
692
- return;
693
- }
694
- // Update both keyboard and pinned state in memory (keeps them in sync)
695
- if (contextLimit > 0) {
696
- keyboardManager.updateContext(contextSize, contextLimit);
697
- }
698
- pinnedMessageManager.updateTokensSilent(tokens);
699
- // Full pinned message update (API call) only on completed messages
700
- if (isCompleted) {
701
- await pinnedMessageManager.onMessageComplete(tokens);
702
- }
703
- }
704
- catch (err) {
705
- logger.error("[Bot] Error updating pinned message with tokens:", err);
706
- }
707
- });
708
- summaryAggregator.setOnCost(async (cost) => {
709
- if (!pinnedMessageManager.isInitialized()) {
710
- return;
711
- }
712
- try {
713
- logger.debug(`[Bot] Cost update: $${cost.toFixed(2)}`);
714
- await pinnedMessageManager.onCostUpdate(cost);
715
- }
716
- catch (err) {
717
- logger.error("[Bot] Error updating cost:", err);
718
- }
719
- });
720
- summaryAggregator.setOnSessionCompacted(async (sessionId, directory) => {
721
- if (!pinnedMessageManager.isInitialized()) {
722
- return;
723
- }
724
- try {
725
- logger.info(`[Bot] Session compacted, reloading context: ${sessionId}`);
726
- await pinnedMessageManager.onSessionCompacted(sessionId, directory);
727
- }
728
- catch (err) {
729
- logger.error("[Bot] Error reloading context after compaction:", err);
730
- }
731
- });
732
- summaryAggregator.setOnSessionIdle(async (sessionId) => {
733
- await markAttachedSessionIdle(sessionId);
734
- await sessionCompletionTasks.get(sessionId)?.catch(() => undefined);
735
- const completedRun = assistantRunState.finishRun(sessionId, "session_idle");
736
- clearPromptResponseMode(sessionId);
737
- if (!botInstance || !chatIdInstance) {
738
- foregroundSessionState.markIdle(sessionId);
739
- return;
740
- }
741
- const currentSession = getCurrentSession();
742
- if (!currentSession || currentSession.id !== sessionId) {
743
- foregroundSessionState.markIdle(sessionId);
744
- await scheduledTaskRuntime.flushDeferredDeliveries();
745
- return;
746
- }
747
- try {
748
- await Promise.all([
749
- toolMessageBatcher.flushSession(sessionId, "session_idle"),
750
- toolCallStreamer.flushSession(sessionId, "session_idle"),
751
- ]);
752
- if (completedRun?.hasCompletedResponse) {
753
- const agent = completedRun.actualAgent || completedRun.configuredAgent;
754
- const providerID = completedRun.actualProviderID || completedRun.configuredProviderID;
755
- const modelID = completedRun.actualModelID || completedRun.configuredModelID;
756
- if (agent && providerID && modelID) {
757
- const keyboard = getCurrentReplyKeyboard();
758
- await botInstance.api.sendMessage(chatIdInstance, formatAssistantRunFooter({
759
- agent,
760
- providerID,
761
- modelID,
762
- elapsedMs: Date.now() - completedRun.startedAt,
763
- }), {
764
- ...(keyboard ? { reply_markup: keyboard } : {}),
765
- });
766
- }
767
- }
768
- }
769
- catch (err) {
770
- logger.error("[Bot] Failed to send session idle footer:", err);
771
- }
772
- finally {
773
- foregroundSessionState.markIdle(sessionId);
774
- await scheduledTaskRuntime.flushDeferredDeliveries();
775
- }
776
- });
777
- summaryAggregator.setOnSessionError(async (sessionId, message) => {
778
- await markAttachedSessionIdle(sessionId);
779
- if (!botInstance || !chatIdInstance) {
780
- clearPromptResponseMode(sessionId);
781
- assistantRunState.clearRun(sessionId, "session_error_no_bot_context");
782
- foregroundSessionState.markIdle(sessionId);
783
- return;
784
- }
785
- const currentSession = getCurrentSession();
786
- if (!currentSession || currentSession.id !== sessionId) {
787
- clearPromptResponseMode(sessionId);
788
- responseStreamer.clearSession(sessionId, "session_error_not_current");
789
- toolCallStreamer.clearSession(sessionId, "session_error_not_current");
790
- assistantRunState.clearRun(sessionId, "session_error_not_current");
791
- foregroundSessionState.markIdle(sessionId);
792
- await scheduledTaskRuntime.flushDeferredDeliveries();
793
- return;
794
- }
795
- responseStreamer.clearSession(sessionId, "session_error");
796
- clearPromptResponseMode(sessionId);
797
- assistantRunState.clearRun(sessionId, "session_error");
798
- await Promise.all([
799
- toolMessageBatcher.flushSession(sessionId, "session_error"),
800
- toolCallStreamer.flushSession(sessionId, "session_error"),
801
- ]);
802
- const normalizedMessage = message.trim() || t("common.unknown_error");
803
- if (shouldSuppressUserAbortSessionError(sessionId, normalizedMessage)) {
804
- logger.debug(`[Bot] Suppressed user-initiated abort error: session=${sessionId}`);
805
- foregroundSessionState.markIdle(sessionId);
806
- await scheduledTaskRuntime.flushDeferredDeliveries();
807
- return;
808
- }
809
- const truncatedMessage = normalizedMessage.length > 3500
810
- ? `${normalizedMessage.slice(0, 3497)}...`
811
- : normalizedMessage;
812
- await botInstance.api
813
- .sendMessage(chatIdInstance, t("bot.session_error", { message: truncatedMessage }))
814
- .catch((err) => {
815
- logger.error("[Bot] Failed to send session.error message:", err);
816
- });
817
- foregroundSessionState.markIdle(sessionId);
818
- await scheduledTaskRuntime.flushDeferredDeliveries();
819
- });
820
- summaryAggregator.setOnSessionRetry(async ({ sessionId, message }) => {
821
- if (!botInstance || !chatIdInstance) {
822
- return;
823
- }
824
- const currentSession = getCurrentSession();
825
- if (!currentSession || currentSession.id !== sessionId) {
826
- return;
827
- }
828
- const normalizedMessage = message.trim() || t("common.unknown_error");
829
- const truncatedMessage = normalizedMessage.length > 3500
830
- ? `${normalizedMessage.slice(0, 3497)}...`
831
- : normalizedMessage;
832
- const retryMessage = t("bot.session_retry", { message: truncatedMessage });
833
- toolCallStreamer.replaceByPrefix(sessionId, SESSION_RETRY_PREFIX, retryMessage);
834
- });
835
- summaryAggregator.setOnSessionDiff(async (_sessionId, diffs) => {
836
- if (!pinnedMessageManager.isInitialized()) {
837
- return;
838
- }
839
- try {
840
- await pinnedMessageManager.onSessionDiff(diffs);
841
- }
842
- catch (err) {
843
- logger.error("[Bot] Error updating session diff:", err);
844
- }
845
- });
846
- summaryAggregator.setOnFileChange((change) => {
847
- if (!pinnedMessageManager.isInitialized()) {
848
- return;
849
- }
850
- pinnedMessageManager.addFileChange(change);
851
- });
852
- pinnedMessageManager.setOnKeyboardUpdate(async (tokensUsed, tokensLimit) => {
853
- try {
854
- logger.debug(`[Bot] Updating keyboard with context: ${tokensUsed}/${tokensLimit}`);
855
- keyboardManager.updateContext(tokensUsed, tokensLimit);
856
- // Don't send automatic keyboard updates - keyboard will update naturally with user messages
857
- }
858
- catch (err) {
859
- logger.error("[Bot] Error updating keyboard context:", err);
860
- }
861
- });
862
- logger.info(`[Bot] Subscribing to OpenCode events for project: ${directory}`);
863
- subscribeToEvents(directory, (event) => {
864
- if (event.type === "server.heartbeat") {
865
- void reconcileBusyState(directory);
866
- }
867
- const attached = attachManager.getSnapshot();
868
- const eventSessionId = getEventSessionId(event);
869
- if (attached &&
870
- eventSessionId === attached.sessionId &&
871
- shouldMarkAttachedBusyFromEvent(event)) {
872
- void markAttachedSessionBusy(attached.sessionId);
873
- }
874
- if (event.type === "session.created" || event.type === "session.updated") {
875
- const info = event.properties.info;
876
- if (info?.directory) {
877
- safeBackgroundTask({
878
- taskName: `session.cache.${event.type}`,
879
- task: () => ingestSessionInfoForCache(info),
880
- });
881
- }
882
- }
883
- if (config.bot.trackBackgroundSessions) {
884
- backgroundSessionTracker.processEvent(event, getCurrentSession()?.id ?? null);
885
- }
886
- summaryAggregator.processEvent(event);
887
- }).catch((err) => {
888
- logger.error("Failed to subscribe to events:", err);
889
- });
890
- }
21
+ const eventSubscriptionService = createEventSubscriptionService();
891
22
  export function createBot() {
892
23
  clearAllInteractionState("bot_startup");
893
- sessionCompletionTasks.clear();
894
24
  attachManager.clear("bot_startup");
895
- assistantRunState.clearAll("bot_startup");
896
- backgroundSessionTracker.clear();
25
+ eventSubscriptionService.clearRuntimeState("bot_startup");
897
26
  if (heartbeatTimer) {
898
27
  clearInterval(heartbeatTimer);
899
28
  heartbeatTimer = null;
900
29
  }
901
30
  const botOptions = createTelegramBotOptions(config.telegram);
902
31
  const bot = new Bot(config.telegram.token, botOptions);
903
- botInstance = bot;
904
- chatIdInstance = config.telegram.allowedUserId;
32
+ configureAttachPresentation(createAttachPresentation());
33
+ eventSubscriptionService.setTelegramContext(bot, config.telegram.allowedUserId);
905
34
  unsubscribeReadyRestore?.();
906
35
  unsubscribeReadyRestore = opencodeReadyLifecycle.onReady(async (reason) => {
907
36
  const restored = await restoreAttachedCurrentSession({
908
37
  bot,
909
38
  chatId: config.telegram.allowedUserId,
910
- ensureEventSubscription,
39
+ ensureEventSubscription: eventSubscriptionService.ensureEventSubscription,
911
40
  forceFullRestore: true,
912
41
  });
913
42
  if (restored) {
@@ -916,20 +45,17 @@ export function createBot() {
916
45
  }
917
46
  const currentProject = getCurrentProject();
918
47
  if (config.bot.trackBackgroundSessions && currentProject?.worktree) {
919
- await ensureEventSubscription(currentProject.worktree);
48
+ await eventSubscriptionService.ensureEventSubscription(currentProject.worktree);
920
49
  logger.info(`[Bot] Started background session tracking after OpenCode ready: reason=${reason}, directory=${currentProject.worktree}`);
921
50
  }
922
51
  });
923
- // Heartbeat for diagnostics: verify the event loop is not blocked
924
52
  let heartbeatCounter = 0;
925
53
  heartbeatTimer = setInterval(() => {
926
54
  heartbeatCounter++;
927
55
  if (heartbeatCounter % 6 === 0) {
928
- // Log every 30 seconds (5 sec * 6)
929
56
  logger.debug(`[Bot] Heartbeat #${heartbeatCounter} - event loop alive`);
930
57
  }
931
58
  }, 5000);
932
- // Log all API calls for diagnostics
933
59
  let lastGetUpdatesTime = Date.now();
934
60
  bot.api.config.use(async (prev, method, payload, signal) => {
935
61
  if (method === "getUpdates") {
@@ -959,175 +85,17 @@ export function createBot() {
959
85
  bot.use(authMiddleware);
960
86
  bot.use(ensureCommandsInitialized);
961
87
  bot.use(interactionGuardMiddleware);
962
- const blockMenuWhileInteractionActive = async (ctx) => {
963
- const activeInteraction = interactionManager.getSnapshot();
964
- if (!activeInteraction) {
965
- return false;
966
- }
967
- logger.debug(`[Bot] Blocking menu open while interaction active: kind=${activeInteraction.kind}, expectedInput=${activeInteraction.expectedInput}`);
968
- await ctx.reply(t("interaction.blocked.finish_current"));
969
- return true;
970
- };
971
- bot.command("start", startCommand);
972
- bot.command("help", helpCommand);
973
- bot.command("status", statusCommand);
974
- bot.command("tts", ttsCommand);
975
- bot.command("opencode_start", opencodeStartCommand);
976
- bot.command("opencode_stop", opencodeStopCommand);
977
- bot.command("projects", projectsCommand);
978
- bot.command("worktree", worktreeCommand);
979
- bot.command("open", openCommand);
980
- bot.command("ls", lsCommand);
981
- bot.command("sessions", sessionsCommand);
982
- bot.command("messages", messagesCommand);
983
- bot.command("new", (ctx) => newCommand(ctx, { bot, ensureEventSubscription }));
984
- bot.command("abort", abortCommand);
985
- bot.command("detach", detachCommand);
986
- bot.command("task", taskCommand);
987
- bot.command("tasklist", taskListCommand);
988
- bot.command("rename", renameCommand);
989
- bot.command("commands", commandsCommand);
990
- bot.command("skills", skillsCommand);
991
- bot.command("mcps", mcpsCommand);
992
- bot.on("message:text", unknownCommandMiddleware);
993
- bot.on("callback_query:data", async (ctx) => {
994
- logger.debug(`[Bot] Received callback_query:data: ${ctx.callbackQuery?.data}`);
995
- logger.debug(`[Bot] Callback context: from=${ctx.from?.id}, chat=${ctx.chat?.id}`);
996
- if (ctx.chat) {
997
- botInstance = bot;
998
- chatIdInstance = ctx.chat.id;
999
- }
1000
- try {
1001
- const handledBackgroundSession = await handleBackgroundSessionOpen(ctx, {
1002
- bot,
1003
- ensureEventSubscription,
1004
- });
1005
- const handledInlineCancel = await handleInlineMenuCancel(ctx);
1006
- if (handledInlineCancel) {
1007
- // Clean up path index when the open-directory menu is cancelled
1008
- clearOpenPathIndex();
1009
- clearLsPathIndex();
1010
- }
1011
- const handledSession = await handleSessionSelect(ctx, { bot, ensureEventSubscription });
1012
- const handledProject = await handleProjectSelect(ctx, { ensureEventSubscription });
1013
- const handledWorktree = await handleWorktreeCallback(ctx, { ensureEventSubscription });
1014
- const handledOpen = await handleOpenCallback(ctx, { ensureEventSubscription });
1015
- const handledLs = await handleLsCallback(ctx);
1016
- const handledQuestion = await handleQuestionCallback(ctx);
1017
- const handledPermission = await handlePermissionCallback(ctx);
1018
- const handledAgent = await handleAgentSelect(ctx);
1019
- const handledModelSearch = await handleModelSearchCallback(ctx);
1020
- const handledModelSearchResults = await handleModelSearchResults(ctx);
1021
- const handledModel = await handleModelSelect(ctx);
1022
- const handledVariant = await handleVariantSelect(ctx);
1023
- const handledCompactConfirm = await handleCompactConfirm(ctx);
1024
- const handledTask = await handleTaskCallback(ctx);
1025
- const handledTaskList = await handleTaskListCallback(ctx);
1026
- const handledRenameCancel = await handleRenameCancel(ctx);
1027
- const handledCommands = await handleCommandsCallback(ctx, { bot, ensureEventSubscription });
1028
- const handledMessages = await handleMessagesCallback(ctx, { bot, ensureEventSubscription });
1029
- const handledSkills = await handleSkillsCallback(ctx, { bot, ensureEventSubscription });
1030
- const handledMcps = await handleMcpsCallback(ctx);
1031
- logger.debug(`[Bot] Callback handled: backgroundSession=${handledBackgroundSession}, inlineCancel=${handledInlineCancel}, session=${handledSession}, project=${handledProject}, worktree=${handledWorktree}, open=${handledOpen}, ls=${handledLs}, question=${handledQuestion}, permission=${handledPermission}, agent=${handledAgent}, modelSearch=${handledModelSearch}, modelSearchResults=${handledModelSearchResults}, model=${handledModel}, variant=${handledVariant}, compactConfirm=${handledCompactConfirm}, task=${handledTask}, taskList=${handledTaskList}, rename=${handledRenameCancel}, commands=${handledCommands}, messages=${handledMessages}, skills=${handledSkills}, mcps=${handledMcps}`);
1032
- if (!handledBackgroundSession &&
1033
- !handledInlineCancel &&
1034
- !handledSession &&
1035
- !handledProject &&
1036
- !handledWorktree &&
1037
- !handledOpen &&
1038
- !handledLs &&
1039
- !handledQuestion &&
1040
- !handledPermission &&
1041
- !handledAgent &&
1042
- !handledModelSearch &&
1043
- !handledModelSearchResults &&
1044
- !handledModel &&
1045
- !handledVariant &&
1046
- !handledCompactConfirm &&
1047
- !handledTask &&
1048
- !handledTaskList &&
1049
- !handledRenameCancel &&
1050
- !handledCommands &&
1051
- !handledMessages &&
1052
- !handledSkills &&
1053
- !handledMcps) {
1054
- logger.debug("Unknown callback query:", ctx.callbackQuery?.data);
1055
- await ctx.answerCallbackQuery({ text: t("callback.unknown_command") });
1056
- }
1057
- }
1058
- catch (err) {
1059
- logger.error("[Bot] Error handling callback:", err);
1060
- clearAllInteractionState("callback_handler_error");
1061
- await ctx.answerCallbackQuery({ text: t("callback.processing_error") }).catch(() => { });
1062
- }
88
+ registerCommandRouter(bot, {
89
+ ensureEventSubscription: eventSubscriptionService.ensureEventSubscription,
1063
90
  });
1064
- // Handle Reply Keyboard button press (agent indicator)
1065
- bot.hears(AGENT_MODE_BUTTON_TEXT_PATTERN, async (ctx) => {
1066
- logger.debug(`[Bot] Agent button pressed: ${ctx.message?.text}`);
1067
- try {
1068
- if (await blockMenuWhileInteractionActive(ctx)) {
1069
- return;
1070
- }
1071
- await showAgentSelectionMenu(ctx);
1072
- }
1073
- catch (err) {
1074
- logger.error("[Bot] Error showing agent menu:", err);
1075
- await ctx.reply(t("error.load_agents"));
1076
- }
91
+ registerCallbackRouter(bot, {
92
+ ensureEventSubscription: eventSubscriptionService.ensureEventSubscription,
93
+ setTelegramContext: eventSubscriptionService.setTelegramContext,
1077
94
  });
1078
- // Handle Reply Keyboard button press (model selector)
1079
- // Model button text is produced by formatModelForButton() and always starts with "🤖 ".
1080
- bot.hears(MODEL_BUTTON_TEXT_PATTERN, async (ctx) => {
1081
- logger.debug(`[Bot] Model button pressed: ${ctx.message?.text}`);
1082
- try {
1083
- if (await blockMenuWhileInteractionActive(ctx)) {
1084
- return;
1085
- }
1086
- await showModelSelectionMenu(ctx);
1087
- }
1088
- catch (err) {
1089
- logger.error("[Bot] Error showing model menu:", err);
1090
- await ctx.reply(t("error.load_models"));
1091
- }
1092
- });
1093
- // Handle Reply Keyboard button press (context button)
1094
- bot.hears(/^📊(?:\s|$)/, async (ctx) => {
1095
- logger.debug(`[Bot] Context button pressed: ${ctx.message?.text}`);
1096
- try {
1097
- if (await blockMenuWhileInteractionActive(ctx)) {
1098
- return;
1099
- }
1100
- await handleContextButtonPress(ctx);
1101
- }
1102
- catch (err) {
1103
- logger.error("[Bot] Error handling context button:", err);
1104
- await ctx.reply(t("error.context_button"));
1105
- }
95
+ registerMessageRouter(bot, {
96
+ ensureEventSubscription: eventSubscriptionService.ensureEventSubscription,
97
+ setTelegramContext: eventSubscriptionService.setTelegramContext,
1106
98
  });
1107
- // Handle Reply Keyboard button press (variant selector)
1108
- // Keep support for both legacy "💭" and current "💡" prefix.
1109
- bot.hears(VARIANT_BUTTON_TEXT_PATTERN, async (ctx) => {
1110
- logger.debug(`[Bot] Variant button pressed: ${ctx.message?.text}`);
1111
- try {
1112
- if (await blockMenuWhileInteractionActive(ctx)) {
1113
- return;
1114
- }
1115
- await showVariantSelectionMenu(ctx);
1116
- }
1117
- catch (err) {
1118
- logger.error("[Bot] Error showing variant menu:", err);
1119
- await ctx.reply(t("error.load_variants"));
1120
- }
1121
- });
1122
- bot.on("message:text", async (ctx, next) => {
1123
- const text = ctx.message?.text;
1124
- if (text) {
1125
- const isCommand = text.startsWith("/");
1126
- logger.debug(`[Bot] Received text message: ${isCommand ? `command="${text}"` : `prompt (length=${text.length})`}, chatId=${ctx.chat.id}`);
1127
- }
1128
- await next();
1129
- });
1130
- // Remove any previously set global commands to prevent unauthorized users from seeing them
1131
99
  safeBackgroundTask({
1132
100
  taskName: "bot.clearGlobalCommands",
1133
101
  task: async () => {
@@ -1150,117 +118,6 @@ export function createBot() {
1150
118
  logger.warn("[Bot] Could not clear global commands:", result.error);
1151
119
  },
1152
120
  });
1153
- // Voice and audio message handlers (STT transcription -> prompt)
1154
- const voicePromptDeps = { bot, ensureEventSubscription };
1155
- bot.on("message:voice", async (ctx) => {
1156
- logger.debug(`[Bot] Received voice message, chatId=${ctx.chat.id}`);
1157
- botInstance = bot;
1158
- chatIdInstance = ctx.chat.id;
1159
- await handleVoiceMessage(ctx, voicePromptDeps);
1160
- });
1161
- bot.on("message:audio", async (ctx) => {
1162
- logger.debug(`[Bot] Received audio message, chatId=${ctx.chat.id}`);
1163
- botInstance = bot;
1164
- chatIdInstance = ctx.chat.id;
1165
- await handleVoiceMessage(ctx, voicePromptDeps);
1166
- });
1167
- bot.on("message", createMediaGroupAttachmentMiddleware({ bot, ensureEventSubscription }));
1168
- // Photo message handler
1169
- bot.on("message:photo", async (ctx) => {
1170
- logger.debug(`[Bot] Received photo message, chatId=${ctx.chat.id}`);
1171
- const photos = ctx.message?.photo;
1172
- if (!photos || photos.length === 0) {
1173
- return;
1174
- }
1175
- const caption = ctx.message.caption || "";
1176
- try {
1177
- // Get the largest photo (last element in array)
1178
- const largestPhoto = photos[photos.length - 1];
1179
- // Check model capabilities
1180
- const storedModel = getStoredModel();
1181
- const capabilities = await getModelCapabilities(storedModel.providerID, storedModel.modelID);
1182
- if (!supportsInput(capabilities, "image")) {
1183
- logger.warn(`[Bot] Model ${storedModel.providerID}/${storedModel.modelID} doesn't support image input`);
1184
- await ctx.reply(t("bot.photo_model_no_image"));
1185
- // Fall back to caption-only if present
1186
- if (caption.trim().length > 0) {
1187
- botInstance = bot;
1188
- chatIdInstance = ctx.chat.id;
1189
- const promptDeps = { bot, ensureEventSubscription };
1190
- await processUserPrompt(ctx, caption, promptDeps);
1191
- }
1192
- return;
1193
- }
1194
- // Download photo
1195
- await ctx.reply(t("bot.photo_downloading"));
1196
- const downloadedFile = await downloadTelegramFile(ctx.api, largestPhoto.file_id);
1197
- // Convert to data URI (Telegram always converts photos to JPEG)
1198
- const dataUri = toDataUri(downloadedFile.buffer, "image/jpeg");
1199
- // Create file part
1200
- const filePart = {
1201
- type: "file",
1202
- mime: "image/jpeg",
1203
- filename: "photo.jpg",
1204
- url: dataUri,
1205
- };
1206
- logger.info(`[Bot] Sending photo (${downloadedFile.buffer.length} bytes) with prompt`);
1207
- botInstance = bot;
1208
- chatIdInstance = ctx.chat.id;
1209
- // Send via processUserPrompt with file part
1210
- const promptDeps = { bot, ensureEventSubscription };
1211
- await processUserPrompt(ctx, caption, promptDeps, [filePart]);
1212
- }
1213
- catch (err) {
1214
- logger.error("[Bot] Error handling photo message:", err);
1215
- await ctx.reply(t("bot.photo_download_error"));
1216
- }
1217
- });
1218
- // Document message handler (PDF and text files)
1219
- bot.on("message:document", async (ctx) => {
1220
- logger.debug(`[Bot] Received document message, chatId=${ctx.chat.id}`);
1221
- botInstance = bot;
1222
- chatIdInstance = ctx.chat.id;
1223
- const deps = { bot, ensureEventSubscription };
1224
- await handleDocumentMessage(ctx, deps);
1225
- });
1226
- bot.on("message:text", async (ctx) => {
1227
- const text = ctx.message?.text;
1228
- if (!text) {
1229
- return;
1230
- }
1231
- botInstance = bot;
1232
- chatIdInstance = ctx.chat.id;
1233
- if (text.startsWith("/")) {
1234
- return;
1235
- }
1236
- if (questionManager.isActive()) {
1237
- await handleQuestionTextAnswer(ctx);
1238
- return;
1239
- }
1240
- const handledTask = await handleTaskTextInput(ctx);
1241
- if (handledTask) {
1242
- return;
1243
- }
1244
- const handledModelSearchText = await handleModelSearchTextInput(ctx);
1245
- if (handledModelSearchText) {
1246
- return;
1247
- }
1248
- const handledRename = await handleRenameTextAnswer(ctx);
1249
- if (handledRename) {
1250
- return;
1251
- }
1252
- const promptDeps = { bot, ensureEventSubscription };
1253
- const handledCommandArgs = await handleCommandTextArguments(ctx, promptDeps);
1254
- if (handledCommandArgs) {
1255
- return;
1256
- }
1257
- const handledSkillArgs = await handleSkillTextArguments(ctx, promptDeps);
1258
- if (handledSkillArgs) {
1259
- return;
1260
- }
1261
- await processUserPrompt(ctx, text, promptDeps);
1262
- logger.debug("[Bot] message:text handler completed (prompt sent in background)");
1263
- });
1264
121
  bot.catch((err) => {
1265
122
  logger.error("[Bot] Unhandled error in bot:", err);
1266
123
  clearAllInteractionState("bot_unhandled_error");
@@ -1273,18 +130,9 @@ export function createBot() {
1273
130
  export function cleanupBotRuntime(reason) {
1274
131
  unsubscribeReadyRestore?.();
1275
132
  unsubscribeReadyRestore = null;
1276
- stopEventListening();
1277
- summaryAggregator.clear();
1278
- backgroundSessionTracker.clear();
1279
- responseStreamer.clearAll(reason);
1280
- toolCallStreamer.clearAll(reason);
1281
- toolMessageBatcher.clearAll(reason);
1282
- sessionCompletionTasks.clear();
1283
- assistantRunState.clearAll(reason);
133
+ eventSubscriptionService.cleanup(reason);
1284
134
  if (heartbeatTimer) {
1285
135
  clearInterval(heartbeatTimer);
1286
136
  heartbeatTimer = null;
1287
137
  }
1288
- botInstance = null;
1289
- chatIdInstance = null;
1290
138
  }