fix(wecom): treat errcode 846609 as subscription-level failure — force reconnect + retry - #92804
huanglu00-lang wants to merge 2 commits into
Conversation
…e reconnect + retry
Problem
-------
When the WeCom server invalidates a bot's subscription while the WebSocket
remains locally OPEN, every outbound send fails with errcode 846609
('aibot websocket not subscribed'). The adapter treated this as a plain
send error: the reply was silently dropped and no reconnect was triggered.
The stale connection persisted until the server eventually closed the TCP
socket, creating a dead window where all outbound messages were lost.
Additionally, REQUEST_TIMEOUT_SECONDS only wrapped the response wait, not
the frame write itself — a wedged WS write could hang a send coroutine
forever. And heartbeat failures were logged at debug level without any
consequences, so functionally-dead connections survived indefinitely.
Fixes
-----
1. send(): on 846609 (both the response path and the raised-exception
path from _raise_for_wecom_error), invalidate the connection, let
_listen_loop reconnect + resubscribe, then retry once on the fresh
connection.
2. _invalidate_connection(): fail all pending futures, close the WS with
code 1012, wait (bounded) until the socket is usable again.
3. _send_json(): wrap the frame write in SEND_FRAME_TIMEOUT_SECONDS (3s)
so wedged writes can no longer hang callers forever.
4. _heartbeat_loop(): count consecutive failures; after 3, force-close
the socket (code 1011) to trigger reconnect instead of logging at
debug level.
Verified in production: before the fix, ~10-minute disconnect cycles with
every reply sent during the disconnect window silently lost (24 x errcode
846609 in one day); after the fix, 2+ hours with zero disconnects, zero
846609 errors.
Refs NousResearch#29667, NousResearch#47564
… retry Covers the four fix surfaces in the 846609 reconnect PR: - response-path 846609 forces reconnect + retry once - raised-exception path (reply markdown) forces reconnect + retry once - retry failure returns the original subscription error - non-subscription errors (40001) do not trigger reconnect - frame write is bounded by SEND_FRAME_TIMEOUT_SECONDS (no hung sends) - heartbeat force-closes wedged WS after 3 consecutive failures (code 1011)
|
Update: regression tests added + why this PR covers more than the competing fixes Thanks to triage for flagging #24790 / #51801. Since they target the same 846609 reconnect/retry family, here is what this PR adds on top: 1. Bounded frame writes ( 2. Heartbeat-driven dead-connection detection 3. Production verification
Tests (
Happy to coordinate with maintainers on consolidating with #51801 — the reconnect/retry core here is the complete fix for the message-loss bug class. |
|
Consolidation proposal — how to unblock this PR cluster Three PRs (#24790→#61984, #51801, #92804) have been sitting open since May on the same 846609 bug class. Nobody has merged any of them, and I think the blocker isn't the code — it's that each PR approaches the problem from a different layer, and reviewing all three means figuring out overlap by hand. Let me make that decision cheaper. What each PR actually adds (no overlap):
Proposed path:
I'm happy to rebase #92804 onto current main and drop/adapt anything you'd rather take from the other two — one clean PR beats three stale ones. ping @teknium1 |
Summary
Fixes the WeCom outbound message loss described in #29667 and #47564 (both still open; earlier fix PRs #24790 / #31393 / #37257 were not merged).
Problem
When the WeCom server invalidates a bot's subscription while the WebSocket remains locally OPEN, every outbound send fails with
errcode 846609("aibot websocket not subscribed"). The adapter treated this as a plain send error:REQUEST_TIMEOUT_SECONDSonly wrapped the response wait, not the frame write — a wedged WS write could hang a send coroutine forever (observed in production: a send with no return and no timeout)Root cause
ws.closed == Falsedoes not imply the subscription is still valid. The adapter's readiness check (if not self._ws or self._ws.closed) only covers the transport layer, not the WeCom subscription lifecycle.Fixes
_raise_for_wecom_error()— invalidate the connection, let_listen_loopreconnect + resubscribe, then retry once on the fresh connectionSEND_FRAME_TIMEOUT_SECONDS(3s) so wedged writes can no longer hang callers foreverProduction verification
Before the fix: ~10-minute disconnect cycles, every reply sent during the disconnect window silently lost (24 × errcode 846609 in a single day, one send hung forever).
After the fix: 2+ hours with zero disconnects, zero 846609 errors, and one forced-reconnect path exercised without message loss.
Test plan