{"record":{"id":"7208194551aaaf28","repo":"CherryHQ/cherry-studio","slug":"client-is-not-connected","errorCode":null,"errorMessage":"Client is not connected","messagePattern":"Client is not connected","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"src/main/ai/channels/adapters/feishu/FeishuAdapter.ts","lineNumber":533,"sourceCode":"    }\n    this.client = null\n    this.sendQrToRenderer('', 'disconnected')\n    this.log.info('Feishu bot stopped')\n  }\n\n  async sendMessage(chatId: string, text: string, _opts?: SendMessageOptions): Promise<void> {\n    void _opts\n    // Promote the typing reaction to DONE before delivering the reply,\n    // so the user sees the lifecycle transition. No-op for messages that\n    // weren't preceded by a typing indicator (e.g. /new acks).\n    await this.transitionChatReaction(chatId, REACTION_DONE, [REACTION_THINKING])\n    await this.sendRawMessage(chatId, text)\n  }\n\n  /** Send chunked text via the IM API without touching status reactions. */\n  private async sendRawMessage(chatId: string, text: string): Promise<void> {\n    if (!this.client) {\n      throw new Error('Client is not connected')\n    }\n\n    const chunks = splitMessage(text, FEISHU_MAX_LENGTH)\n\n    for (let i = 0; i < chunks.length; i++) {\n      ensureFeishuSuccess(\n        await this.client.im.message.create({\n          params: { receive_id_type: 'chat_id' },\n          data: {\n            receive_id: chatId,\n            msg_type: 'post',\n            content: buildPostPayload(chunks[i])\n          }\n        }),\n        'Send Feishu message'\n      )\n\n      if (i < chunks.length - 1) {","sourceCodeStart":515,"sourceCodeEnd":551,"githubUrl":"https://github.com/CherryHQ/cherry-studio/blob/726446b54cd69ffe51a276638672f6d95ca0768c/src/main/ai/channels/adapters/feishu/FeishuAdapter.ts#L515-L551","documentation":"Thrown inside sendRawMessage() when this.client is falsy at send time. FeishuAdapter lazily holds the Lark Client; if it was never created (never connected) or was cleared on disconnect, any text send aborts here before the IM API call. sendMessage() calls sendRawMessage() after transitioning the reaction.","triggerScenarios":"sendMessage/sendRawMessage invoked while the adapter is disconnected, during the gap between connect start and client creation, after performDisconnect ran, or after a connection error that left client unset.","commonSituations":"Outbound queue draining after the bot was stopped, a /new ack path firing before initial connect completes, or a race where disconnect wins over an in-flight message.","solutions":["Check adapter isConnected / lifecycle state before enqueueing outbound messages; drop or queue them instead of calling sendMessage.","Ensure performConnect fully resolves (client assigned and markConnected() called) before the message pump is allowed to run.","On disconnect, drain or reject queued sends explicitly rather than letting them hit a null client."],"exampleFix":"// before\nprivate async sendRawMessage(chatId: string, text: string): Promise<void> {\n  if (!this.client) throw new Error('Client is not connected')\n\n// after — fail with lifecycle context the caller can branch on\nprivate async sendRawMessage(chatId: string, text: string): Promise<void> {\n  if (!this.client || !this.isConnected()) {\n    throw new Error(`Cannot send to ${chatId}: Feishu client not connected (state=${this.getState()})`)\n  }","handlingStrategy":"validation","validationCode":"// Only call sendMessage when the adapter reports connected.\nif (!feishu.isConnected()) {\n  queueOrDrop(outbound)\n} else {\n  await feishu.sendMessage(chatId, text)\n}","typeGuard":"function isFeishuNotConnected(e: unknown): e is Error {\n  return e instanceof Error && e.message === 'Client is not connected'\n}","tryCatchPattern":"try {\n  await feishu.sendMessage(chatId, text)\n} catch (e) {\n  if (isFeishuNotConnected(e)) { enqueueForLater(chatId, text); return }\n  throw e\n}","preventionTips":["Gate the outbound pump on isConnected and on connect having fully resolved.","Drain or reject queued sends explicitly on disconnect.","Treat 'Client is not connected' as a lifecycle state, not a transient retry."],"tags":["feishu","lifecycle","null-state","race-condition"],"backgroundTag":null,"analyzedSha":"726446b54cd69ffe51a276638672f6d95ca0768c","analyzedAt":"2026-08-12T17:30:37.448Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}