CherryHQ/cherry-studio · error · Error
Bot is not connected
Error message
Bot is not connected
What it means
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.
Source
Thrown at src/main/ai/channels/adapters/telegram/TelegramAdapter.ts:305
const url = `https://api.telegram.org/file/bot${this.botToken}/${file.file_path}`
const attachment = await downloadFileAsBase64(url, filename)
if (!attachment) return []
// Override media_type with Telegram's reported mime_type if available
if (mimeType) attachment.media_type = mimeType
return [attachment]
} catch (error) {
this.log.warn('Failed to download Telegram document', {
fileId,
filename,
error: error instanceof Error ? error.message : String(error)
})
return []
}
}
async sendMessage(chatId: string, text: string, opts?: SendMessageOptions): Promise<void> {
if (!this.bot) {
throw new Error('Bot is not connected')
}
const parseMode = opts?.parseMode ?? 'MarkdownV2'
const isMarkdown = parseMode === 'MarkdownV2'
// Split the PLAIN text first and escape each chunk, so the MarkdownV2 send and
// its plain-text fallback share one chunk boundary. (Splitting the *formatted*
// text and then re-splitting the *raw* text by the same index misaligns — escaping
// changes lengths/boundaries — dropping, duplicating, or passing `undefined` chunks.)
const plainChunks = splitMessage(text, isMarkdown ? TELEGRAM_MARKDOWN_CHUNK_BUDGET : TELEGRAM_MAX_LENGTH)
for (let i = 0; i < plainChunks.length; i++) {
const plain = plainChunks[i]
const formatted = isMarkdown ? toMarkdownV2(plain).trimEnd() : plain
// Telegram message ids are numeric; a string replyToMessageId (QQ's msg_id) isn't ours.
const replyParams =
typeof opts?.replyToMessageId === 'number' && i === 0
? { reply_parameters: { message_id: opts.replyToMessageId } }
: {}View on GitHub (pinned to 726446b54c)
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.
Example fix
// before — caller throws when adapter is mid-reconnect
await telegramAdapter.sendMessage(chatId, text)
// after — guard on the adapter's connected state, queue otherwise
if (!telegramAdapter.isConnected()) {
await messageQueue.enqueue({ chatId, text, channel: 'telegram' })
return
}
await telegramAdapter.sendMessage(chatId, text) Defensive patterns
Strategy: validation
Validate before calling
// Check the adapter's connected state before sending. The adapter calls markConnected()
// in startBot() (TelegramAdapter.ts:200) and markDisconnected() on polling failure.
// Expose isConnected() from the ChannelAdapter base and use it:
if (!telegramAdapter.isConnected()) {
// Queue or drop — do not call sendMessage
await queueOrDrop({ channel: 'telegram', chatId, text })
return
}
await telegramAdapter.sendMessage(chatId, text) Type guard
// Narrow the adapter state via the base class's connection flag
function isAdapterConnected(adapter: ChannelAdapter): boolean {
return adapter.isConnected() // exposed by ChannelAdapter base after markConnected()
}
// Guard before the call site
function assertConnected(adapter: ChannelAdapter, action: string): void {
if (!isAdapterConnected(adapter)) {
throw new ChannelDisconnectedError(`Cannot ${action}: adapter disconnected`)
}
} Try / catch
// Distinguish 'bot null' from genuine send failures so transient disconnects
// do not crash the caller (e.g. an agent tool running mid-stream)
try {
await telegramAdapter.sendMessage(chatId, text)
} catch (e) {
if (e instanceof Error && e.message === 'Bot is not connected') {
logger.warn('Telegram adapter not connected, queuing message', { chatId })
await pendingTelegram.enqueue({ chatId, text })
return
}
throw e
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Bot is not connected
- Client is not connected
- MCP runtime is stopping
- Agent not found: ${agentId}
- Agent session ${session.id} became invalid while starting ta
AI-assisted analysis of CherryHQ/cherry-studio@726446b54c (2026-08-12).
Data as JSON: /api/errors/68d54d1f446f6b16.
Report an issue: GitHub.