Weixin iLink ret=-2 stale-session detection and retry
NousResearch/hermes-agent · cluster 13305 · cron
Data as of
15 open items. Fix merged in #17432. Check it covers your case.
The short answer
- Known?
- Yes. 29 related items grouped here, 5 issues already closed.
- Fix event
- A fix was merged to main in PR #17432. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
partially resolved, deciding PR #17432. That is separate from the fix event. - Still open
- 8 issues and 7 PRs.
Next step: read #17432, then check whether your report is one of the 8 still open.
Read #17432Pick up a Lab taskThese issues and fixes center on the Weixin/WeChat iLink adapter misidentifying stale context_token errors (ret=-2, various errmsg values) as rate limits or unknown errors, and the related work to add proper session-expiry detection, token refresh, retry logic, and backoff behavior.
Open issues8
- Issue#18100weixin: ret=-2 with empty errmsg also indicates stale context_token (follow-up to #17228)
- Issue#31131[Weixin] Messages silently dropped under iLink rate limiting due to insufficient backoff
- Issue#35062[Weixin] ret=-3 cron push silently fails after tokenless retry (regression from v0.14)
- Issue#35713weixin: ret=-2 errmsg="rate limited" also indicates stale context_token (follow-up to #18100)
- Issue#35949Weixin iLink: stale context_token misidentified as rate limit, causing permanent send failure
- Issue#56814WeChat (iLink) context_token auto-refresh for proactive/cron messages
- Issue#60116hermes send --to weixin permanently fails: iLink sendmessage rate limited; cooldown never expires
- Issue#62383[Bug]: Weixin iLink cron delivery fails with ret=-2 'rate limited' when context_token is stale
Open PRs7
- PR#23529fix(weixin): propagate asyncio.TimeoutError from _get_updates to prevent zombie connections
- PR#31132fix(weixin): exponential backoff for iLink rate limiting
- PR#35065fix(weixin): retry on session/rate-limit errors in send_weixin_direct
- PR#35066fix(weixin): allow tokenless retry when no cached context_token exists
- PR#35714fix(weixin): treat 'rate limited' errmsg as stale-session signal in _is_stale_session_ret
- PR#60459fix(gateway): reset weixin rate-limit cooldown on next attempt instead of permanent block (#60116)
- PR#62386fix(platforms): treat ret=-2 as stale session when using context_token
Fixes on record
-
#17432 fix(weixin): recognize ret=-2 errmsg='unknown error' as stale-session signal (#17228)
files:
gateway/platforms/weixin.py,tests/gateway/test_weixin.pyShow the diff
-
gateway/platforms/weixin.py@@ -92,6 +92,18 @@ RATE_LIMIT_ERRCODE = -2 # iLink frequency limit — backoff and retry MESSAGE_DEDUP_TTL_SECONDS = 300 + +def _is_stale_session_ret( + ret: "Optional[int]", errcode: "Optional[int]", errmsg: "Optional[str]", +) -> bool: + """True when iLink returns ret=-2 / errcode=-2 with 'unknown error', + which is a stale-session signal (same as errcode=-14) rather than + a genuine rate limit.""" + if ret != RATE_LIMIT_ERRCODE and errcode != RATE_LIMIT_ERRCODE: + return False + return (errmsg or "").lower() == "unknown error" + + MEDIA_IMAGE = 1 MEDIA_VIDEO = 2 MEDIA_FILE = 3 @@ -1254,7 +1266,8 @@ async def _poll_loop(self) -> None: ret = response.get("ret", 0) errcode = response.get("errcode", 0) if ret not in (0, None) or errcode not in (0, None): - if ret == SESSION_EXPIRED_ERRCODE or errcode == SESSION_EXPIRED_ERRCODE: + if (ret == SESSION_EXPIRED_ERRCODE or errcode == SESSION_EXPIRED_ERRCODE + or _is_stale_session_ret(ret, errcode, response.get("errmsg"))): logger.error("[%s] Session expired; pausing for 10 minutes", self.name) await asyncio.sleep(600) consecutive_failures = 0 @@ -1519,6 +1532,7 @@ async def _send_text_chunk( is_session_expired = ( ret == SESSION_EXPIRED_ERRCODE or errcode == SESSION_EXPIRED_ERRCODE + or _is_stale_session_ret(ret, errcode, resp.get("errmsg")) ) # Session expired — strip token and retry once if is_session_expired and not retried_without_token and context_token: -
tests/gateway/test_weixin.py@@ -758,3 +758,33 @@ def test_send_file_sets_voice_metadata_for_silk_payload( assert voice_item["encode_type"] == 6 assert voice_item["sample_rate"] == 24000 assert voice_item["bits_per_sample"] == 16 + + +class TestIsStaleSessionRet: + """Regression test for #17228: distinguish stale-session ret=-2 from rate-limit ret=-2.""" + + def test_ret_minus_2_with_unknown_error_is_stale(self): + assert weixin._is_stale_session_ret(-2, None, "unknown error") is True + + def test_errcode_minus_2_with_unknown_error_is_stale(self): + assert weixin._is_stale_session_ret(None, -2, "unknown error") is True + + def test_unknown_error_case_insensitive(self): + assert weixin._is_stale_session_ret(-2, None, "Unknown Error") is True + + def test_ret_minus_2_with_freq_limit_is_not_stale(self): + # Genuine rate limit — must NOT be treated as stale session. + assert weixin._is_stale_session_ret(-2, None, "freq limit") is False + + def test_ret_minus_2_with_no_errmsg_is_not_stale(self): + assert weixin._is_stale_session_ret(-2, None, None) is False + assert weixin._is_stale_session_ret(-2, None, "") is False + + def test_errcode_minus_14_is_not_matched_here(self): + # -14 is handled by the separate SESSION_EXPIRED_ERRCODE path; the + # helper only disambiguates -2 from a genuine rate limit. + assert weixin._is_stale_session_ret(-14, None, "session expired") is False + + def test_success_codes_are_not_stale(self): + assert weixin._is_stale_session_ret(0, 0, "") is False + assert weixin._is_stale_session_ret(None, None, "unknown error") is False
-
8 PRs closed without merging
From the close comments: duplicate 2 · merged via another change 2 · done elsewhere 1 · stale or abandoned 1 · superseded by another PR 1 · other 1
-
#14384 fix: avoid cross-loop session reuse in Weixin direct sends · done elsewhere →
a22465e -
#17287 fix: recognize ret=-2 as stale-session signal in Weixin adapter (#17228) · merged via another change →
e9b96fd,#17432,#17228 - #18105 fix(weixin): treat empty rate-limit message as stale session · stale or abandoned
-
#20797 fix(gateway): treat weixin ret=-2 with empty errmsg as stale context_token · duplicate →
#18105 - #35283 fix(weixin): 修复 iLink 消息投递可靠性 — stale session、退避、zombie poll 和 typing 频率限流 · other
Show 3 more
-
#35983 fix(weixin): detect stale context_token when iLink returns "rate limited" (#35949) · superseded by another PR →
7c04b5c - #66943 fix(cron): wait out live adapter rate-limit cooldown before standalone fallback (#66928) · merged via another change
-
#73908 fix(weixin): recover from ret=-2 with empty stale-context errors · duplicate →
#35714
Closed issues · 5
- Closed#13305[Bug]: weixin send file failed
- Closed#17228weixin: cron-initiated push fails with iLink ret=-2 when context_token is stale (not recognized as session-expired)
- Closed#21011iLink WeChat sendmessage rate limiting (ret=-2) with no built-in retry or backoff
- Closed#48222[bug]:weixin channel
- Closed#66928[Bug] Cron job delivery to Weixin always fails: iLink rate limited, fallback double-fails
How this cluster was grouped
-
Link evidence (
partly structural links): some members are connected by structural evidence (shared changed files, or a closing/superseded reference); the rest are joined by an automated relevance judgement over their text. - Members outside that structural core may belong to a different underlying problem.
-
Pairwise link density
0.08: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- cron
- Merged PRs
- #17432
- Closed issues
- 5
- Attempts
- 8 PRs closed without merging
- Fix link checked
- 29 Apr 2026