hermes - ✅(Solved) Fix [WeCom] Timeout triggers plain-text fallback, causing duplicate messages [1 pull requests, 1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
NousResearch/hermes-agent#14061Fetched 2026-04-23 07:47:02
View on GitHub
Comments
1
Participants
2
Timeline
6
Reactions
0
Author
Participants
Timeline (top)
labeled ×4commented ×1cross-referenced ×1

Error Message

When sending a message to WeCom times out, the error string "Timeout sending message to WeCom" is not recognized by _is_timeout_error(), causing it to fall through to the plain-text fallback. This results in the user receiving two identical messages — one prefixed with "(Response formatting failed, plain text:)".

Root Cause

_is_timeout_error in gateway/platforms/base.py only checks for "timed out", "readtimeout", "writetimeout", but not strings that start with "timeout" — which is what WeCom's adapter raises (asyncio.TimeoutError caught as "Timeout sending message to WeCom").

Additionally, the timeout check was placed after the is_network classification, so timeouts could still be treated as retryable network errors. After retries exhausted, they fell through to the plain-text fallback regardless.

Fix Action

Fix

  1. Extend _is_timeout_error to also match lowered.startswith("timeout")
  2. Move the timeout guard to execute before the is_network classification
# _is_timeout_error
return (
    "timed out" in lowered
    or "readtimeout" in lowered
    or "writetimeout" in lowered
    or lowered.startswith("timeout")
)

# _send_with_retry — check timeout FIRST
if self._is_timeout_error(error_str):
    logger.warning("[%s] Send timed out — skipping retry and fallback to avoid duplicate delivery", self.name)
    return result

PR fix notes

PR #14071: fix(gateway): skip plain-text fallback on send timeouts

Description (problem / solution / changelog)

Summary

  • treat Timeout ... send errors as delivery-ambiguous timeouts
  • stop _send_with_retry() from sending a second plain-text copy after a timeout-prefixed failure
  • add regression coverage for both timeout classification and the no-fallback behavior

Problem

On current main, WeCom returns Timeout sending message to WeCom from its adapter. _is_timeout_error() does not recognize that prefix, so _send_with_retry() falls through to the plain-text fallback and attempts a second send.

Closes #14061.

Testing

  • pytest -o addopts= tests/gateway/test_send_retry.py
  • manual repro: _send_with_retry() with Timeout sending message to WeCom now performs 1 send attempt instead of 2

Changed files

  • gateway/platforms/base.py (modified, +6/-1)
  • tests/gateway/test_send_retry.py (modified, +17/-0)

Code Example

# _is_timeout_error
return (
    "timed out" in lowered
    or "readtimeout" in lowered
    or "writetimeout" in lowered
    or lowered.startswith("timeout")
)

# _send_with_retry — check timeout FIRST
if self._is_timeout_error(error_str):
    logger.warning("[%s] Send timed out — skipping retry and fallback to avoid duplicate delivery", self.name)
    return result
RAW_BUFFERClick to expand / collapse

Bug Description

When sending a message to WeCom times out, the error string "Timeout sending message to WeCom" is not recognized by _is_timeout_error(), causing it to fall through to the plain-text fallback. This results in the user receiving two identical messages — one prefixed with "(Response formatting failed, plain text:)".

Root Cause

_is_timeout_error in gateway/platforms/base.py only checks for "timed out", "readtimeout", "writetimeout", but not strings that start with "timeout" — which is what WeCom's adapter raises (asyncio.TimeoutError caught as "Timeout sending message to WeCom").

Additionally, the timeout check was placed after the is_network classification, so timeouts could still be treated as retryable network errors. After retries exhausted, they fell through to the plain-text fallback regardless.

Steps to Reproduce

  1. Configure WeCom AI Bot adapter (WebSocket mode)
  2. Trigger a condition where WeCom's WebSocket is slow/unresponsive
  3. Observe the bot sends two messages: one with (Response formatting failed, plain text:) prefix, and one without

Fix

  1. Extend _is_timeout_error to also match lowered.startswith("timeout")
  2. Move the timeout guard to execute before the is_network classification
# _is_timeout_error
return (
    "timed out" in lowered
    or "readtimeout" in lowered
    or "writetimeout" in lowered
    or lowered.startswith("timeout")
)

# _send_with_retry — check timeout FIRST
if self._is_timeout_error(error_str):
    logger.warning("[%s] Send timed out — skipping retry and fallback to avoid duplicate delivery", self.name)
    return result

Environment

  • Platform: WeCom (企业微信) AI Bot — WebSocket mode (wecom.py)

extent analysis

TL;DR

Update the _is_timeout_error function to match error strings starting with "timeout" and reorder the error classification to check for timeouts before network errors.

Guidance

  • Extend the _is_timeout_error function to include a check for error strings that start with "timeout" using the lowered.startswith("timeout") condition.
  • Move the timeout check to execute before the is_network classification in the _send_with_retry method to prevent timeouts from being treated as retryable network errors.
  • Verify the fix by triggering a timeout condition with the WeCom AI Bot adapter in WebSocket mode and observing that only one message is sent without the "(Response formatting failed, plain text:)" prefix.
  • Review the gateway/platforms/base.py file to ensure the updated _is_timeout_error function and reordered error classification are correctly implemented.

Example

# _is_timeout_error
return (
    "timed out" in lowered
    or "readtimeout" in lowered
    or "writetimeout" in lowered
    or lowered.startswith("timeout")
)

# _send_with_retry — check timeout FIRST
if self._is_timeout_error(error_str):
    logger.warning("[%s] Send timed out — skipping retry and fallback to avoid duplicate delivery", self.name)
    return result

Notes

This fix assumes that the asyncio.TimeoutError caught by the WeCom adapter always raises an error string starting with "Timeout sending message to WeCom". If this is not the case, additional error handling may be necessary.

Recommendation

Apply the workaround by updating the _is_timeout_error function and reordering the error classification, as this directly addresses the identified root cause of the issue.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING

hermes - ✅(Solved) Fix [WeCom] Timeout triggers plain-text fallback, causing duplicate messages [1 pull requests, 1 comments, 2 participants]