{"record":{"id":"c2d36ee0c3dbc151","repo":"sipeed/picoclaw","slug":"weixin-send-w","errorCode":null,"errorMessage":"weixin send: %w","messagePattern":"weixin send: %w","errorType":"exception","errorClass":"channels.ErrSendFailed","httpStatus":null,"severity":"error","filePath":"pkg/channels/weixin/weixin.go","lineNumber":433,"sourceCode":"\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 {\n\treturn channels.VoiceCapabilities{ASR: true, TTS: true}\n}\n","sourceCodeStart":415,"sourceCodeEnd":445,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/weixin/weixin.go#L415-L445","documentation":"A Weixin text send failed at the HTTP layer and, additionally, the channel is currently in a rate-limit pause (remainingPause() > 0). Because the failure coincides with an active pause, the channel classifies it as permanent: the error wraps channels.ErrSendFailed so the send manager will not requeue the message. Distinguish it from the temporary variant (error 676) with errors.Is(err, channels.ErrTemporary).","triggerScenarios":"sendTextMessage returning an error (network failure, 4xx/5xx from iLink — see error 667) while c.pauseUntil is in the future. Typical ordering: an earlier send got throttled and armed the pause; the retry of that same message fails again while paused, hitting this branch.","commonSituations":"Reply storm triggering throttle: first failure arms the pause, queued duplicates fail permanently here; long pauses armed by severe throttling converting what would be retryable errors into dropped messages.","solutions":["Wait for the pause window to expire, then resend the message manually if it was dropped","Reduce concurrent sends per user/chat so throttling never arms the pause","Check upstream error handling: if the message matters, capture failed sends and re-queue them after remainingPause() reaches zero","If sends keep failing even when unpaused, debug the underlying sendTextMessage error (logged via ErrorCF with to_user_id)"],"exampleFix":"// before\nif err := ch.Send(ctx, msg); err != nil {\n    retryForever(msg) // wrong: ErrSendFailed means do NOT retry\n}\n\n// after\nif err := ch.Send(ctx, msg); err != nil {\n    if errors.Is(err, channels.ErrTemporary) {\n        retryLater(msg)\n    } else {\n        parkForManualRetry(msg) // permanent (paused or missing token)\n    }\n}","handlingStrategy":"try-catch","validationCode":"// avoid the permanent classification entirely: don't send while paused\nif ch.RemainingPause() > 0 {\n    return scheduleLater(msg) // send after pause -> likely ErrTemporary path, retryable\n}\nreturn ch.Send(ctx, msg)","typeGuard":"func classifyWeixinSendErr(err error) (temporary bool) {\n    return errors.Is(err, channels.ErrTemporary)\n}\n\nfunc isWeixinSendPermanent(err error) bool {\n    return err != nil && strings.HasPrefix(err.Error(), \"weixin send:\") && errors.Is(err, channels.ErrSendFailed)\n}","tryCatchPattern":"if _, err := ch.Send(ctx, msg); err != nil {\n    if isWeixinSendPermanent(err) {\n        parkForManualRetry(msg) // paused: manager will NOT requeue\n    } else if classifyWeixinSendErr(err) {\n        requeueWithBackoff(msg)\n    }\n}","preventionTips":["Never blind-retry sends wrapped in ErrSendFailed — the manager semantics are permanent","Defer sends while the session pause is active so they classify as temporary instead","Throttle fan-out replies; every throttle-adjacent failure during a pause becomes permanent","Log to_user_id with the failure to reconstruct which user lost the message"],"tags":["weixin","send","rate-limit","send-failed","error-classification"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}