{"record":{"id":"e317dc32e3a21a28","repo":"sipeed/picoclaw","slug":"weixin-send-w-missing-context-token-for-chat-s","errorCode":null,"errorMessage":"weixin send: %w: missing context token for chat %s","messagePattern":"weixin send: %w: missing context token for chat (.+?)","errorType":"exception","errorClass":"channels.ErrSendFailed","httpStatus":null,"severity":"error","filePath":"pkg/channels/weixin/weixin.go","lineNumber":424,"sourceCode":"\t}\n\n\t// We need a context_token to send a reply. It should be stored in the conversation metadata.\n\t// The chat_id is the weixin user_id (from_user_id).\n\ttoUserID := msg.ChatID\n\n\t// Retrieve context_token from our per-user map (stored on last inbound)\n\tcontextToken := \"\"\n\tif ct, ok := c.contextTokens.Load(toUserID); ok {\n\t\tcontextToken, _ = ct.(string)\n\t}\n\n\t// If we don't have a context token for this user, we cannot send a valid reply.\n\t// Treat this as a non-temporary error so the manager doesn't keep retrying.\n\tif contextToken == \"\" {\n\t\tlogger.ErrorCF(\"weixin\", \"Missing context token, cannot send message\", map[string]any{\n\t\t\t\"to_user_id\": toUserID,\n\t\t})\n\t\treturn nil, fmt.Errorf(\"weixin send: %w: missing context token for chat %s\", channels.ErrSendFailed, toUserID)\n\t}\n\n\tif err := c.sendTextMessage(ctx, toUserID, contextToken, msg.Content); err != nil {\n\t\tlogger.ErrorCF(\"weixin\", \"Failed to send message\", map[string]any{\n\t\t\t\"to_user_id\": toUserID,\n\t\t\t\"error\":      err.Error(),\n\t\t})\n\t\tif c.remainingPause() > 0 {\n\t\t\treturn nil, fmt.Errorf(\"weixin send: %w\", channels.ErrSendFailed)\n\t\t}\n\t\treturn nil, fmt.Errorf(\"weixin send: %w\", channels.ErrTemporary)\n\t}\n\n\treturn nil, nil\n}\n\n// VoiceCapabilities returns the voice capabilities of the channel.\nfunc (c *WeixinChannel) VoiceCapabilities() channels.VoiceCapabilities {","sourceCodeStart":406,"sourceCodeEnd":442,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/weixin/weixin.go#L406-L442","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\nch.Send(ctx, bus.OutboundMessage{ChatID: userID, Content: \"push!\"}) // never messaged -> error 674\n\n// after\nif _, ok := ch.ContextToken(userID); !ok {\n    return errors.New(\"cannot message user before they message us\")\n}\nch.Send(ctx, bus.OutboundMessage{ChatID: userID, Content: \"reply\"})","handlingStrategy":"validation","validationCode":"// send only to chats we hold a token for (export ContextToken or check inbound history)\nif _, ok := ch.ContextToken(toUserID); !ok {\n    return nil // skip: cannot reply to a chat that never messaged us\n}\nreturn ch.Send(ctx, msg)","typeGuard":"func isWeixinMissingContextToken(err error) bool {\n    return err != nil && strings.Contains(err.Error(), \"missing context token for chat\")\n}","tryCatchPattern":"if _, err := ch.Send(ctx, msg); err != nil {\n    if isWeixinMissingContextToken(err) {\n        // non-retryable by design: wait for the user's next inbound, then send\n        parkUntilInbound(msg)\n    }\n}","preventionTips":["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"],"tags":["weixin","send","context-token","state","proactive-messaging"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}