{"record":{"id":"2591fc92754e49a8","repo":"sipeed/picoclaw","slug":"weixin-session-paused-d-min-remaining-w","errorCode":null,"errorMessage":"weixin session paused (%d min remaining): %w","messagePattern":"weixin session paused \\((.+?) min remaining\\): %w","errorType":"exception","errorClass":"channels.ErrSendFailed","httpStatus":null,"severity":"warning","filePath":"pkg/channels/weixin/state.go","lineNumber":181,"sourceCode":"\t}\n\n\ttimer := time.NewTimer(remaining)\n\tdefer timer.Stop()\n\n\tselect {\n\tcase <-ctx.Done():\n\t\treturn ctx.Err()\n\tcase <-timer.C:\n\t\treturn nil\n\t}\n}\n\nfunc (c *WeixinChannel) ensureSessionActive() error {\n\tremaining := c.remainingPause()\n\tif remaining <= 0 {\n\t\treturn nil\n\t}\n\treturn fmt.Errorf(\n\t\t\"weixin session paused (%d min remaining): %w\",\n\t\tint((remaining+time.Minute-1)/time.Minute),\n\t\tbasechannels.ErrSendFailed,\n\t)\n}\n\nfunc (c *WeixinChannel) getTypingTicket(ctx context.Context, userID string) (string, error) {\n\tnow := time.Now()\n\n\tc.typingMu.Lock()\n\tentry, ok := c.typingCache[userID]\n\tif ok && now.Before(entry.nextFetchAt) {\n\t\tticket := entry.ticket\n\t\tc.typingMu.Unlock()\n\t\treturn ticket, nil\n\t}\n\tcachedTicket := entry.ticket\n\tretryDelay := entry.retryDelay","sourceCodeStart":163,"sourceCodeEnd":199,"githubUrl":"https://github.com/sipeed/picoclaw/blob/49183d7e8daed0dba89ddbb6fcb60089401d9680/pkg/channels/weixin/state.go#L163-L199","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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\nIf the pause seems stuck, restart the channel (pause state is in-memory) — but first confirm the upstream rate limit actually lifted"],"exampleFix":"// before\nerr := ch.Send(ctx, msg) // fails fast with 'weixin session paused (N min remaining)'\n\n// after\nif r := ch.RemainingPause(); r > 0 {\n    time.Sleep(r) // or schedule the send for later\n}\nerr := ch.Send(ctx, msg)","handlingStrategy":"validation","validationCode":"// ask the channel before sending (export RemainingPause or equivalent)\nif r := ch.RemainingPause(); r > 0 {\n    scheduleSend(msg, time.Now().Add(r))\n} else {\n    _ = ch.Send(ctx, msg)\n}","typeGuard":"func isWeixinSessionPaused(err error) bool {\n    return err != nil && strings.HasPrefix(err.Error(), \"weixin session paused\")\n}","tryCatchPattern":"if err := ch.Send(ctx, msg); err != nil {\n    if isWeixinSessionPaused(err) && errors.Is(err, channels.ErrSendFailed) {\n        // locally refused, no request was made: defer, don't drop\n        deferSendUntilUnpaused(msg)\n    }\n}","preventionTips":["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"],"tags":["weixin","rate-limit","backoff","send-failed","session-state"],"backgroundTag":null,"analyzedSha":"49183d7e8daed0dba89ddbb6fcb60089401d9680","analyzedAt":"2026-08-15T21:55:41.315Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}