{"record":{"id":"b8c9e0cc16a7e1fd","repo":"sipeed/picoclaw","slug":"no-session-webhook-found-for-chat-s-cannot-send","errorCode":null,"errorMessage":"no session_webhook found for chat %s, cannot send message","messagePattern":"no session_webhook found for chat (.+?), cannot send message","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"pkg/channels/dingtalk/dingtalk.go","lineNumber":119,"sourceCode":"\tif c.streamClient != nil {\n\t\tc.streamClient.Close()\n\t}\n\n\tc.SetRunning(false)\n\tlogger.InfoC(\"dingtalk\", \"DingTalk channel stopped\")\n\treturn nil\n}\n\n// Send sends a message to DingTalk via the chatbot reply API\nfunc (c *DingTalkChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {\n\tif !c.IsRunning() {\n\t\treturn nil, channels.ErrNotRunning\n\t}\n\n\t// Get session webhook from storage\n\tsessionWebhookRaw, ok := c.sessionWebhooks.Load(msg.ChatID)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"no session_webhook found for chat %s, cannot send message\", msg.ChatID)\n\t}\n\n\tsessionWebhook, ok := sessionWebhookRaw.(string)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"invalid session_webhook type for chat %s\", msg.ChatID)\n\t}\n\n\tlogger.DebugCF(\"dingtalk\", \"Sending message\", map[string]any{\n\t\t\"chat_id\": msg.ChatID,\n\t\t\"preview\": utils.Truncate(msg.Content, 100),\n\t})\n\n\t// Use the session webhook to send the reply\n\treturn nil, c.SendDirectReply(ctx, sessionWebhook, msg.Content)\n}\n\n// onChatBotMessageReceived implements the IChatBotMessageHandler function signature\n// This is called by the Stream SDK when a new message arrives","sourceCodeStart":101,"sourceCodeEnd":137,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/dingtalk/dingtalk.go#L101-L137","documentation":"DingTalk replies are delivered through a per-conversation sessionWebhook captured from each inbound bot callback (stored in the in-memory sync.Map in onChatBotMessageReceived). Send fails with this error when no webhook was ever stored for msg.ChatID, i.e. the bot has no way to address that chat because it never saw an inbound message for it in this process lifetime.","triggerScenarios":"Calling Send with a ChatID that never messaged the bot (proactive push), after a process restart (sync.Map wiped), with a mismatched ID (senderId vs conversationId), or after the stored webhook entry was never written because the inbound callback had an empty SessionWebhook.","commonSituations":"Trying to proactively broadcast to DingTalk chats (unsupported for stream-mode bots), replying after the bot process restarted, session webhooks being short-lived so stale entries do not help replies hours later, using the wrong ID kind as ChatID.","solutions":["Have the user send a fresh inbound message first — each callback refreshes the webhook for that conversation","Use the exact ChatID from the InboundContext (ConversationId, or the senderId fallback for direct chats with ConversationType == \"1\")","For proactive/late replies, capture the webhook from the inbound message's ReplyHandles[\"session_webhook\"] and persist it yourself","Do not design DingTalk flows that require pushing to unseen chats; the stream-mode bot is reply-only"],"exampleFix":"// before\n_ = ch.Send(ctx, bus.OutboundMessage{ChatID: guessedChatID, Content: \"hi\"}) // no webhook -> error\n\n// after: reply using the handle captured from the inbound message\nif hook, ok := inbound.ReplyHandles[\"session_webhook\"]; ok {\n    _ = ch.SendDirectReply(ctx, hook, \"hi\")\n}","handlingStrategy":"validation","validationCode":"// Verify a webhook exists for the chat before attempting Send\nif _, ok := ch.HasSessionWebhook(chatID); !ok {\n    // ask the user to send a message first; proactive send is not possible\n    return fmt.Errorf(\"no webhook for %s; user must message the bot first\", chatID)\n}","typeGuard":null,"tryCatchPattern":"if _, err := ch.Send(ctx, msg); err != nil {\n    if strings.Contains(err.Error(), \"no session_webhook found\") {\n        // permanent for this chat until a new inbound message arrives: do not retry blindly\n        notifyUserToSendMessage(chatID)\n        return nil\n    }\n    return err\n}","preventionTips":["Design DingTalk flows as reply-only: always answer from a fresh inbound message","Capture ReplyHandles[\"session_webhook\"] from the inbound context and persist it if you need later replies","Expect webhooks to be lost on restart (in-memory sync.Map) and short-lived over time","Use the exact InboundContext.ChatID when constructing outbound messages"],"tags":["dingtalk","session-webhook","send","state"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}