sipeed/picoclaw · error

no session_webhook found for chat %s, cannot send message

Error message

no session_webhook found for chat %s, cannot send message

What it means

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.

Source

Thrown at pkg/channels/dingtalk/dingtalk.go:119

	if c.streamClient != nil {
		c.streamClient.Close()
	}

	c.SetRunning(false)
	logger.InfoC("dingtalk", "DingTalk channel stopped")
	return nil
}

// Send sends a message to DingTalk via the chatbot reply API
func (c *DingTalkChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
	if !c.IsRunning() {
		return nil, channels.ErrNotRunning
	}

	// Get session webhook from storage
	sessionWebhookRaw, ok := c.sessionWebhooks.Load(msg.ChatID)
	if !ok {
		return nil, fmt.Errorf("no session_webhook found for chat %s, cannot send message", msg.ChatID)
	}

	sessionWebhook, ok := sessionWebhookRaw.(string)
	if !ok {
		return nil, fmt.Errorf("invalid session_webhook type for chat %s", msg.ChatID)
	}

	logger.DebugCF("dingtalk", "Sending message", map[string]any{
		"chat_id": msg.ChatID,
		"preview": utils.Truncate(msg.Content, 100),
	})

	// Use the session webhook to send the reply
	return nil, c.SendDirectReply(ctx, sessionWebhook, msg.Content)
}

// onChatBotMessageReceived implements the IChatBotMessageHandler function signature
// This is called by the Stream SDK when a new message arrives

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Have the user send a fresh inbound message first — each callback refreshes the webhook for that conversation
  2. Use the exact ChatID from the InboundContext (ConversationId, or the senderId fallback for direct chats with ConversationType == "1")
  3. For proactive/late replies, capture the webhook from the inbound message's ReplyHandles["session_webhook"] and persist it yourself
  4. Do not design DingTalk flows that require pushing to unseen chats; the stream-mode bot is reply-only

Example fix

// before
_ = ch.Send(ctx, bus.OutboundMessage{ChatID: guessedChatID, Content: "hi"}) // no webhook -> error

// after: reply using the handle captured from the inbound message
if hook, ok := inbound.ReplyHandles["session_webhook"]; ok {
    _ = ch.SendDirectReply(ctx, hook, "hi")
}
Defensive patterns

Strategy: validation

Validate before calling

// Verify a webhook exists for the chat before attempting Send
if _, ok := ch.HasSessionWebhook(chatID); !ok {
    // ask the user to send a message first; proactive send is not possible
    return fmt.Errorf("no webhook for %s; user must message the bot first", chatID)
}

Try / catch

if _, err := ch.Send(ctx, msg); err != nil {
    if strings.Contains(err.Error(), "no session_webhook found") {
        // permanent for this chat until a new inbound message arrives: do not retry blindly
        notifyUserToSendMessage(chatID)
        return nil
    }
    return err
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15). Data as JSON: /api/errors/b8c9e0cc16a7e1fd. Report an issue: GitHub.