{"record":{"id":"68d54d1f446f6b16","repo":"CherryHQ/cherry-studio","slug":"bot-is-not-connected","errorCode":null,"errorMessage":"Bot is not connected","messagePattern":"Bot is not connected","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/channels/adapters/telegram/TelegramAdapter.ts","lineNumber":305,"sourceCode":"      const url = `https://api.telegram.org/file/bot${this.botToken}/${file.file_path}`\n      const attachment = await downloadFileAsBase64(url, filename)\n      if (!attachment) return []\n      // Override media_type with Telegram's reported mime_type if available\n      if (mimeType) attachment.media_type = mimeType\n      return [attachment]\n    } catch (error) {\n      this.log.warn('Failed to download Telegram document', {\n        fileId,\n        filename,\n        error: error instanceof Error ? error.message : String(error)\n      })\n      return []\n    }\n  }\n\n  async sendMessage(chatId: string, text: string, opts?: SendMessageOptions): Promise<void> {\n    if (!this.bot) {\n      throw new Error('Bot is not connected')\n    }\n\n    const parseMode = opts?.parseMode ?? 'MarkdownV2'\n    const isMarkdown = parseMode === 'MarkdownV2'\n    // Split the PLAIN text first and escape each chunk, so the MarkdownV2 send and\n    // its plain-text fallback share one chunk boundary. (Splitting the *formatted*\n    // text and then re-splitting the *raw* text by the same index misaligns — escaping\n    // changes lengths/boundaries — dropping, duplicating, or passing `undefined` chunks.)\n    const plainChunks = splitMessage(text, isMarkdown ? TELEGRAM_MARKDOWN_CHUNK_BUDGET : TELEGRAM_MAX_LENGTH)\n\n    for (let i = 0; i < plainChunks.length; i++) {\n      const plain = plainChunks[i]\n      const formatted = isMarkdown ? toMarkdownV2(plain).trimEnd() : plain\n      // Telegram message ids are numeric; a string replyToMessageId (QQ's msg_id) isn't ours.\n      const replyParams =\n        typeof opts?.replyToMessageId === 'number' && i === 0\n          ? { reply_parameters: { message_id: opts.replyToMessageId } }\n          : {}","sourceCodeStart":287,"sourceCodeEnd":323,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/adapters/telegram/TelegramAdapter.ts#L287-L323","documentation":"Thrown by TelegramAdapter.sendMessage() when this.bot is null. The bot instance is set in startBot() (TelegramAdapter.ts:70) and cleared to null in performDisconnect() (TelegramAdapter.ts:256). So this throw means sendMessage was called before connect completed or after disconnect ran. This is a lifecycle violation: the caller sent a message while the adapter was not in the connected state.","triggerScenarios":"sendMessage() is invoked after performDisconnect() set this.bot=null (e.g. a notify/scheduled-send racing with a disconnect, or a reconnect failure left the bot down); or sendMessage() is called before performConnect() finished starting the bot (e.g. immediately after connect() returned but markConnected() not yet reached). The polling bot.start() is fire-and-forget (line 192), so a failed poll can null the bot via disconnect while queued sends are in flight.","commonSituations":"A scheduled task fires while the channel is disconnected or reconnecting; the bot hit a fatal 409/401 and reconnect backoff is in progress (the adapter marks itself disconnected but downstream callers were not notified); a notify tool call races with the user disabling the channel.","solutions":["Gate outbound sends on the adapter's connected state (isConnected()/markConnected lifecycle) before calling sendMessage, and queue or drop messages while disconnected.","Have callers subscribe to disconnect events so they stop sending when the bot is null.","Ensure performDisconnect() awaits in-flight sends or signals them to abort before nulling the bot.","If reconnect is in progress, buffer the message and flush after markConnected."],"exampleFix":"// before — caller throws when adapter is mid-reconnect\nawait telegramAdapter.sendMessage(chatId, text)\n\n// after — guard on the adapter's connected state, queue otherwise\nif (!telegramAdapter.isConnected()) {\n  await messageQueue.enqueue({ chatId, text, channel: 'telegram' })\n  return\n}\nawait telegramAdapter.sendMessage(chatId, text)","handlingStrategy":"validation","validationCode":"// Check the adapter's connected state before sending. The adapter calls markConnected()\n// in startBot() (TelegramAdapter.ts:200) and markDisconnected() on polling failure.\n// Expose isConnected() from the ChannelAdapter base and use it:\nif (!telegramAdapter.isConnected()) {\n  // Queue or drop — do not call sendMessage\n  await queueOrDrop({ channel: 'telegram', chatId, text })\n  return\n}\nawait telegramAdapter.sendMessage(chatId, text)","typeGuard":"// Narrow the adapter state via the base class's connection flag\nfunction isAdapterConnected(adapter: ChannelAdapter): boolean {\n  return adapter.isConnected() // exposed by ChannelAdapter base after markConnected()\n}\n\n// Guard before the call site\nfunction assertConnected(adapter: ChannelAdapter, action: string): void {\n  if (!isAdapterConnected(adapter)) {\n    throw new ChannelDisconnectedError(`Cannot ${action}: adapter disconnected`)\n  }\n}","tryCatchPattern":"// Distinguish 'bot null' from genuine send failures so transient disconnects\n// do not crash the caller (e.g. an agent tool running mid-stream)\ntry {\n  await telegramAdapter.sendMessage(chatId, text)\n} catch (e) {\n  if (e instanceof Error && e.message === 'Bot is not connected') {\n    logger.warn('Telegram adapter not connected, queuing message', { chatId })\n    await pendingTelegram.enqueue({ chatId, text })\n    return\n  }\n  throw e\n}","preventionTips":["Subscribe outbound-send paths to channel-state events so they stop sending on disconnect.","Have performDisconnect await/drain in-flight sends before setting this.bot = null.","Buffer messages during reconnect backoff and flush them after markConnected.","For scheduled tasks, query adapter.isConnected() immediately before sending, not at schedule time."],"tags":["telegram","lifecycle","race-condition","state"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}