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