dc4b88714b
Flood-capped / rate-limited sends (Telegram FloodWait, Weixin bare RuntimeError) were not honored as retryable when retry_after was absent, so _send_with_retry fell through to the truncating plain-text fallback, which re-entered the server ban and dropped the message tail — silently burning the turn with no delivery. Three changes to _send_with_retry: 1. Route rate-limit checks through _is_rate_limited_error (single wrapper over classify_send_error) so rate limits are treated as transient even when the platform omits retry_after. 2. Reclassify rate-limit per retry attempt, not just the initial send, so the in-loop break/continue reflects the CURRENT attempt (covers transient->flood and rate-limited->formatting transitions). 3. Never take the truncating plain-text fallback for a rate-limited send — return the typed failure so the delivery ledger owns redelivery after the cooldown. And when retries exhaust on a rate-limited / retry_after-carrying failure, return BEFORE sending the delivery-failure notice: the notice send would land inside the same flood penalty and re-enter the ban ([0, 189, 378, 378] -> 3 sends, no 4th). Ordinary exhausted network errors keep the existing notice. Honors server retry_after (when present) as the backoff delay instead of the default exponential schedule. Rebased onto current main (was 5163 commits behind, CONFLICTING) and folded in reviewer feedback. Regression coverage in tests/gateway/test_send_retry.py: rate-limit classifier, rate-limited-without-retry_after retries & succeeds, rate-limited exhaustion returns a typed failure with no fallback and no notice inside the active flood penalty, and failure-kind transitions between attempts.