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.retryDelayView on GitHub (pinned to 49183d7e8d)
Solutions
- Wait out the reported remaining minutes — the pause clears automatically once pauseUntil passes (remainingPause zeroes it)
- Reduce outbound send rate (batch replies, throttle per-user sends) so the pause is never armed
- 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
- Throttle outbound sends per user to keep the pause from ever arming
- Check the pause state before batch sends instead of letting each item fail
- Treat the minutes-remaining text as scheduling input, not just a log line
- After restarts, remember the pause is in-memory: rate-limit conservatively until steady state
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
- weixin send: %w
- %s failed: %d %s
- weixin send media: missing context token for chat %s: %w
- weixin send media: %w
- API error %d: %s
AI-assisted analysis of sipeed/picoclaw@49183d7e8d (2026-08-15).
Data as JSON: /api/errors/2591fc92754e49a8.
Report an issue: GitHub.