{"record":{"id":"6dc7fd76b057fa75","repo":"CherryHQ/cherry-studio","slug":"bot-is-not-connected-6dc7fd","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/wechat/WeChatAdapter.ts","lineNumber":91,"sourceCode":"      }\n    })\n\n    this.log.info('WeChat bot started')\n  }\n\n  protected override async performDisconnect(): Promise<void> {\n    if (this.bot) {\n      this.bot.stop()\n      this.bot = null\n      this.sendQrToRenderer('', 'disconnected')\n      this.log.info('WeChat bot stopped')\n    }\n  }\n\n  // oxlint-disable-next-line no-unused-vars -- abstract method signature\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 chunks = splitMessage(text, WECHAT_MAX_LENGTH)\n\n    for (let i = 0; i < chunks.length; i++) {\n      await this.bot.send(chatId, chunks[i])\n\n      if (i < chunks.length - 1) {\n        await new Promise((resolve) => setTimeout(resolve, 100))\n      }\n    }\n  }\n\n  override async sendFile(chatId: string, file: FileAttachment): Promise<void> {\n    if (!this.bot) {\n      throw new Error('Bot is not connected')\n    }\n    // The reverse-engineered WeChat protocol only supports outbound images today","sourceCodeStart":73,"sourceCodeEnd":109,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/adapters/wechat/WeChatAdapter.ts#L73-L109","documentation":"Thrown by WeChatAdapter.sendMessage() when this.bot (a WeixinBot instance) is null. The bot is set in performConnect() (WeChatAdapter.ts:46) and nulled in performDisconnect() (line 82). WeChat login is a multi-step QR flow that can take many seconds, so the window where sendMessage is called before connect completes is larger than for the other adapters. After disconnect, this.bot is null and sends fail with this message.","triggerScenarios":"sendMessage() called before performConnect() finished the QR login flow (bot is assigned at line 46 before login completes, but a connect failure leaves it null); or after performDisconnect() nulled it. Because WeChat requires an interactive QR scan, the adapter can sit in a 'pending QR' state for a long time during which this.bot may be set but not yet logged in — sending in that window fails downstream rather than here.","commonSituations":"A scheduled notification fires while the WeChat channel is waiting for QR scan (user has not scanned yet); the WeChat session expired and runLoop() is re-authenticating (login flow blocks the loop); the user disconnected the channel and a queued send drained after teardown.","solutions":["Gate outbound sends on the adapter connected state, and specifically on WeChat login completion (markConnected is called at WeChatAdapter.ts:64 only after credentials are obtained).","Queue messages while the channel is in QR-pending or reconnecting state and flush on markConnected.","Have performDisconnect await/drain pending sends before nulling this.bot.","Subscribe callers to channel-state events so they stop sending on disconnect."],"exampleFix":"// before — races with the long WeChat QR login\nawait wechatAdapter.sendMessage(chatId, text)\n\n// after — check the connected flag set only after login\nif (!wechatAdapter.isConnected()) {\n  await pendingWeChatSends.enqueue({ chatId, text })\n  return\n}\nawait wechatAdapter.sendMessage(chatId, text)","handlingStrategy":"validation","validationCode":"// WeChat login is slow and interactive; check the connected state (set only after\n// markConnected at WeChatAdapter.ts:64) and queue while in QR-pending or reconnecting.\nif (!wechatAdapter.isConnected()) {\n  await pendingWeChatSends.enqueue({ chatId, text })\n  return\n}\nawait wechatAdapter.sendMessage(chatId, text)","typeGuard":"// WeChat login completion is signaled by markConnected — use the base class flag\nfunction isWeChatReadyToSend(adapter: ChannelAdapter): boolean {\n  return adapter.isConnected()\n}","tryCatchPattern":"try {\n  await wechatAdapter.sendMessage(chatId, text)\n} catch (e) {\n  if (e instanceof Error && e.message === 'Bot is not connected') {\n    // WeChat disconnects are often session-expiry re-logins; queue and retry\n    await pendingWeChatSends.enqueue({ chatId, text })\n    return\n  }\n  throw e\n}","preventionTips":["Treat the WeChat channel as 'not connected' until the QR scan completes — markConnected fires only after credentials are obtained (WeChatAdapter.ts:64).","Queue outbound messages during QR-pending and session-expiry re-login; flush on reconnect.","Drain pending sends in performDisconnect before nulling this.bot.","For WeChat, also handle the case where this.bot is set but session-expired (runLoop re-login) — gate on a finer 'logged in' flag."],"tags":["wechat","lifecycle","race-condition","state","login"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}