sipeed/picoclaw · error · channels.ErrSendFailed
weixin send: %w: missing context token for chat %s
Error message
weixin send: %w: missing context token for chat %s
What it means
The Weixin channel cannot send a text reply because it has no stored context_token for the target user. The iLink send API requires a context token captured from that user's most recent inbound message (loaded into c.contextTokens on receive); without it any request would be invalid, so the channel refuses locally. The error wraps channels.ErrSendFailed and is explicitly documented in-code as non-temporary so the manager stops retrying.
Source
Thrown at pkg/channels/weixin/weixin.go:424
}
// We need a context_token to send a reply. It should be stored in the conversation metadata.
// The chat_id is the weixin user_id (from_user_id).
toUserID := msg.ChatID
// Retrieve context_token from our per-user map (stored on last inbound)
contextToken := ""
if ct, ok := c.contextTokens.Load(toUserID); ok {
contextToken, _ = ct.(string)
}
// If we don't have a context token for this user, we cannot send a valid reply.
// Treat this as a non-temporary error so the manager doesn't keep retrying.
if contextToken == "" {
logger.ErrorCF("weixin", "Missing context token, cannot send message", map[string]any{
"to_user_id": toUserID,
})
return nil, fmt.Errorf("weixin send: %w: missing context token for chat %s", channels.ErrSendFailed, toUserID)
}
if err := c.sendTextMessage(ctx, toUserID, contextToken, msg.Content); err != nil {
logger.ErrorCF("weixin", "Failed to send message", map[string]any{
"to_user_id": toUserID,
"error": err.Error(),
})
if c.remainingPause() > 0 {
return nil, fmt.Errorf("weixin send: %w", channels.ErrSendFailed)
}
return nil, fmt.Errorf("weixin send: %w", channels.ErrTemporary)
}
return nil, nil
}
// VoiceCapabilities returns the voice capabilities of the channel.
func (c *WeixinChannel) VoiceCapabilities() channels.VoiceCapabilities {View on GitHub (pinned to 49183d7e8d)
Solutions
- Only reply to users who have messaged first — treat an inbound message as the prerequisite for outbound sends
- After a channel restart, accept that old conversations are cold: prompt users to send one message to re-arm the token
- Verify the toUserID passed to Send exactly matches the user id captured on inbound (no renaming/suffixing in between)
- If proactive messaging is required, use a different channel/API that supports it; this Weixin path fundamentally cannot
Example fix
// before
ch.Send(ctx, bus.OutboundMessage{ChatID: userID, Content: "push!"}) // never messaged -> error 674
// after
if _, ok := ch.ContextToken(userID); !ok {
return errors.New("cannot message user before they message us")
}
ch.Send(ctx, bus.OutboundMessage{ChatID: userID, Content: "reply"}) Defensive patterns
Strategy: validation
Validate before calling
// send only to chats we hold a token for (export ContextToken or check inbound history)
if _, ok := ch.ContextToken(toUserID); !ok {
return nil // skip: cannot reply to a chat that never messaged us
}
return ch.Send(ctx, msg) Type guard
func isWeixinMissingContextToken(err error) bool {
return err != nil && strings.Contains(err.Error(), "missing context token for chat")
} Try / catch
if _, err := ch.Send(ctx, msg); err != nil {
if isWeixinMissingContextToken(err) {
// non-retryable by design: wait for the user's next inbound, then send
parkUntilInbound(msg)
}
} Prevention
- Enforce reply-only semantics at the application layer
- Do not attempt proactive pushes over this channel — the protocol forbids them
- Key tokens by the exact inbound user id; never transform ids between receive and send
- After channel restarts, prompt users to send one message before media/text replies
When it happens
Trigger: Sending to a toUserID for whom no inbound message has arrived since channel start (proactive/unsolicited message); the token map was reset (channel restart) and the user has not spoken since; a different ID form is used as the destination than the one the token was stored under (raw vs suffixed user id).
Common situations: Bot tries to push a notification before the user ever messaged it; restart cleared in-memory tokens so historical chats can no longer be replied to until the user speaks again; upstream code synthesizes a Send with an internally-mapped ID that mismatches the stored key.
Related errors
- weixin send media: missing context token for chat %s: %w
- no session_webhook found for chat %s, cannot send message
- weixin send: %w
- login failed: %w
- failed to load config: %w
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/e317dc32e3a21a28.
Report an issue: GitHub.