sipeed/picoclaw · error · channels.ErrSendFailed

weixin send: %w

Error message

weixin send: %w

What it means

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).

Source

Thrown at pkg/channels/weixin/weixin.go:433

		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 {
	return channels.VoiceCapabilities{ASR: true, TTS: true}
}

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Wait for the pause window to expire, then resend the message manually if it was dropped
  2. Reduce concurrent sends per user/chat so throttling never arms the pause
  3. Check upstream error handling: if the message matters, capture failed sends and re-queue them after remainingPause() reaches zero
  4. If sends keep failing even when unpaused, debug the underlying sendTextMessage error (logged via ErrorCF with to_user_id)

Example fix

// before
if err := ch.Send(ctx, msg); err != nil {
    retryForever(msg) // wrong: ErrSendFailed means do NOT retry
}

// after
if err := ch.Send(ctx, msg); err != nil {
    if errors.Is(err, channels.ErrTemporary) {
        retryLater(msg)
    } else {
        parkForManualRetry(msg) // permanent (paused or missing token)
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// avoid the permanent classification entirely: don't send while paused
if ch.RemainingPause() > 0 {
    return scheduleLater(msg) // send after pause -> likely ErrTemporary path, retryable
}
return ch.Send(ctx, msg)

Type guard

func classifyWeixinSendErr(err error) (temporary bool) {
    return errors.Is(err, channels.ErrTemporary)
}

func isWeixinSendPermanent(err error) bool {
    return err != nil && strings.HasPrefix(err.Error(), "weixin send:") && errors.Is(err, channels.ErrSendFailed)
}

Try / catch

if _, err := ch.Send(ctx, msg); err != nil {
    if isWeixinSendPermanent(err) {
        parkForManualRetry(msg) // paused: manager will NOT requeue
    } else if classifyWeixinSendErr(err) {
        requeueWithBackoff(msg)
    }
}

Prevention

When it happens

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

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

Related errors


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