Weixin/WeChat integration fixes and feature work
NousResearch/hermes-agent · cluster 16411
Data as of
Nothing open on GitHub. Fix merged in #16985. Check it covers your case.
The short answer
- Known?
- Yes. 5 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #16985. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #16985. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These items cover adding native Weixin/WeChat support via iLink Bot API and a series of fixes addressing stale-session detection, permanent error handling, rate-limit retry, and splitting long messages over 2000 characters.
Open issues0
Open PRs0
Fixes on record
-
#16985 fix(weixin): split long messages >2000 chars + harden rate-limit retry
files:
gateway/platforms/weixin.py,scripts/release.pyShow the diff
-
gateway/platforms/weixin.py@@ -89,6 +89,7 @@ RETRY_DELAY_SECONDS = 2 BACKOFF_DELAY_SECONDS = 30 SESSION_EXPIRED_ERRCODE = -14 +RATE_LIMIT_ERRCODE = -2 # iLink frequency limit — backoff and retry MESSAGE_DEDUP_TTL_SECONDS = 300 MEDIA_IMAGE = 1 @@ -1113,7 +1114,7 @@ async def qr_login( class WeixinAdapter(BasePlatformAdapter): """Native Hermes adapter for Weixin personal accounts.""" - MAX_MESSAGE_LENGTH = 4000 + MAX_MESSAGE_LENGTH = 2000 # WeChat does not support editing sent messages — streaming must use the # fallback "send-final-only" path so the cursor (▉) is never left visible. @@ -1138,10 +1139,10 @@ def __init__(self, config: PlatformConfig): extra.get("cdn_base_url") or os.getenv("WEIXIN_CDN_BASE_URL", WEIXIN_CDN_BASE_URL) ).strip().rstrip("/") self._send_chunk_delay_seconds = float( - extra.get("send_chunk_delay_seconds") or os.getenv("WEIXIN_SEND_CHUNK_DELAY_SECONDS", "0.35") + extra.get("send_chunk_delay_seconds") or os.getenv("WEIXIN_SEND_CHUNK_DELAY_SECONDS", "1.5") ) self._send_chunk_retries = int( - extra.get("send_chunk_retries") or os.getenv("WEIXIN_SEND_CHUNK_RETRIES", "2") + extra.get("send_chunk_retries") or os.getenv("WEIXIN_SEND_CHUNK_RETRIES", "4") ) self._send_chunk_retry_delay_seconds = float( extra.get("send_chunk_retry_delay_seconds") @@ -1531,6 +1532,28 @@ async def _send_text_chunk( self.name, _safe_id(chat_id), ) continue + # Rate limit (-2) — backoff and retry + is_rate_limited = ( + ret == RATE_LIMIT_ERRCODE + or errcode == RATE_LIMIT_ERRCODE + ) + if is_rate_limited: + errmsg = resp.get("errmsg") or resp.get("msg") or "rate limited" + # Record the error so we raise a descriptive + # RuntimeError (instead of AssertionError) if the + # loop exhausts with the server still rate-limiting. + last_error = RuntimeError( + f"iLink sendmessage rate limited: ret={ret} errcode={errcode} errmsg={errmsg}" + ) + if attempt >= self._send_chunk_retries: + break + wait = self._send_chunk_retry_delay_seconds * 3 # 3x backoff for rate limit + logger.warning( + "[%s] rate limited for %s; backing off %.1fs before retry", + self.name, _safe_id(chat_id), wait, + ) + await asyncio.sleep(wait) + continue errmsg = resp.get("errmsg") or resp.get("msg") or "unknown error" raise RuntimeError( f"iLink sendmessage error: ret={ret} errcode={errcode} errmsg={errmsg}" -
scripts/release.py@@ -581,6 +581,7 @@ "[redacted:email]": "hekaru-agent", "[redacted:email]": "twozle", "[redacted:email]": "rfilgueiras", + "[redacted:email]": "Leihb", }
-
-
#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
-
2 PRs closed without merging
From the close comments: merged via another change 1 · superseded by another PR 1
Closed issues · 1
- Closed#16411[Weixin] Long text messages (>2000 chars) are truncated without error
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.40: 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 Lab