sipeed/picoclaw · warning · channels.ErrSendFailed

weixin session paused (%d min remaining): %w

Error message

weixin session paused (%d min remaining): %w

What it means

A Weixin channel send was rejected because the channel is in a self-imposed rate-limit pause: remainingPause() reports time left on pauseUntil (set when the channel earlier detected throttling), so ensureSessionActive() fails. The message text includes whole minutes remaining (rounded up) and the error wraps basechannels.ErrSendFailed, marking it non-temporary for the send manager. No network request is made; the send is refused locally.

Source

Thrown at pkg/channels/weixin/state.go:181

	}

	timer := time.NewTimer(remaining)
	defer timer.Stop()

	select {
	case <-ctx.Done():
		return ctx.Err()
	case <-timer.C:
		return nil
	}
}

func (c *WeixinChannel) ensureSessionActive() error {
	remaining := c.remainingPause()
	if remaining <= 0 {
		return nil
	}
	return fmt.Errorf(
		"weixin session paused (%d min remaining): %w",
		int((remaining+time.Minute-1)/time.Minute),
		basechannels.ErrSendFailed,
	)
}

func (c *WeixinChannel) getTypingTicket(ctx context.Context, userID string) (string, error) {
	now := time.Now()

	c.typingMu.Lock()
	entry, ok := c.typingCache[userID]
	if ok && now.Before(entry.nextFetchAt) {
		ticket := entry.ticket
		c.typingMu.Unlock()
		return ticket, nil
	}
	cachedTicket := entry.ticket
	retryDelay := entry.retryDelay

View on GitHub (pinned to 49183d7e8d)

Solutions

  1. Wait out the reported remaining minutes — the pause clears automatically once pauseUntil passes (remainingPause zeroes it)
  2. Reduce outbound send rate (batch replies, throttle per-user sends) so the pause is never armed
  3. Drain or defer the send queue while paused instead of letting each item error If the pause seems stuck, restart the channel (pause state is in-memory) — but first confirm the upstream rate limit actually lifted

Example fix

// before
err := ch.Send(ctx, msg) // fails fast with 'weixin session paused (N min remaining)'

// after
if r := ch.RemainingPause(); r > 0 {
    time.Sleep(r) // or schedule the send for later
}
err := ch.Send(ctx, msg)
Defensive patterns

Strategy: validation

Validate before calling

// ask the channel before sending (export RemainingPause or equivalent)
if r := ch.RemainingPause(); r > 0 {
    scheduleSend(msg, time.Now().Add(r))
} else {
    _ = ch.Send(ctx, msg)
}

Type guard

func isWeixinSessionPaused(err error) bool {
    return err != nil && strings.HasPrefix(err.Error(), "weixin session paused")
}

Try / catch

if err := ch.Send(ctx, msg); err != nil {
    if isWeixinSessionPaused(err) && errors.Is(err, channels.ErrSendFailed) {
        // locally refused, no request was made: defer, don't drop
        deferSendUntilUnpaused(msg)
    }
}

Prevention

When it happens

Trigger: Any Weixin send (text or media) while c.pauseUntil is in the future: the pause is armed after a previous send hit Weixin rate limiting (e.g. a 4xx/limit response that called the state.go pause setter at line ~128). The first send attempt after the pause window starts fails immediately with minutes remaining.

Common situations: Bot replying to a burst of user messages (fan-out replies) triggering Weixin throttling; a previous send storm armed a multi-minute pause and subsequent queued sends all fail fast; pause left armed after a restart because remainingPause was checked before pauseUntil lapsed.

Related errors


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