Bug: send_message tool fails with Weixin - Timeout context manager should be used inside a task
Repository: https://github.com/NousResearch/hermes-agent
Description
Calling the send_message tool with the weixin platform target fails with:
Weixin send failed: Timeout context manager should be used inside a task
This happens on both text and media messages (including MEDIA:/path/to/file uploads). The error originates from aiohttp/helpers.py:
# aiohttp/helpers.py, line 678
def __enter__(self) -> BaseTimerContext:
task = asyncio.current_task(loop=self._loop)
if task is None:
raise RuntimeError("Timeout context manager should be used inside a task")
The same underlying issue causes text_to_speech to fail with:
TTS generation failed (edge): Connection timeout to host wss://speech.platform.bing.com/...
Environment
- Hermes Agent: NousResearch/hermes-agent (latest)
- Python: 3.11
- aiohttp: 3.13.5
- Platform: Weixin (WeChat) via send_message tool
- Session: Running inside the CLI/gateway with an existing event loop
Root Cause Analysis
Trigger path:
- send_message_tool._handle_send() -> model_tools._run_async(_send_to_platform(...))
- _run_async detects asyncio.get_running_loop().is_running() == True (gateway/CLI loop is already running)
- Falls into the thread pool path: ThreadPoolExecutor(max_workers=1).submit(asyncio.run, coro)
- asyncio.run() creates a fresh event loop in the worker thread
- Inside _send_weixin() -> send_weixin_direct() -> async with aiohttp.ClientSession(...) as session (line 2010 of gateway/platforms/weixin.py)
- ClientSession.aenter creates asyncio.timeout(self._timeout) (aiohttp >= 3.9)
- asyncio.timeout.enter calls asyncio.current_task(loop=self._loop) which returns None because the Timeout object was initialized with a loop reference that does not match the actual running loop in the worker thread.
Known aiohttp issues:
Reproduction Steps
# In a Hermes session with Weixin configured:
send_message(target="weixin", message="test")
# OR
send_message(target="weixin:o9cq80zzgXQ1kJhIVQTWD36hui1k@im.wechat", message="MEDIA:/tmp/test.png")
Expected Behavior
Message is sent successfully to Weixin/WeChat.
Actual Behavior
Weixin send failed: Timeout context manager should be used inside a task
Suggested Fix
The issue is that asyncio.run() in _run_async's thread pool path creates a new loop, but the ClientSession initialization in send_weixin_direct may be receiving a stale loop reference.
-
For send_weixin_direct: Ensure the ClientSession is created without relying on implicit loop binding, or use explicit loop handling with aiohttp.TCPConnector.
-
For _run_async: Consider switching the thread pool path to use loop.run_until_complete(coro) on a persistent per-thread loop (matching the non-worker-thread path) instead of asyncio.run(), to avoid the loop reference mismatch.
-
Alternative: The _run_async docstring mentions When called from a worker thread... use a per-thread persistent loop. The worker thread path already has this (_get_worker_loop().run_until_complete(coro)). Consider whether the main thread path (tool_loop.run_until_complete(coro)) should be used more broadly when the running loop is in the main thread but we are calling from a sync tool handler.
Bug: send_message tool fails with Weixin - Timeout context manager should be used inside a task
Repository: https://github.com/NousResearch/hermes-agent
Description
Calling the
send_messagetool with theweixinplatform target fails with:This happens on both text and media messages (including MEDIA:/path/to/file uploads). The error originates from aiohttp/helpers.py:
The same underlying issue causes
text_to_speechto fail with:Environment
Root Cause Analysis
Trigger path:
Known aiohttp issues:
Reproduction Steps
Expected Behavior
Message is sent successfully to Weixin/WeChat.
Actual Behavior
Suggested Fix
The issue is that asyncio.run() in _run_async's thread pool path creates a new loop, but the ClientSession initialization in send_weixin_direct may be receiving a stale loop reference.
For send_weixin_direct: Ensure the ClientSession is created without relying on implicit loop binding, or use explicit loop handling with aiohttp.TCPConnector.
For _run_async: Consider switching the thread pool path to use loop.run_until_complete(coro) on a persistent per-thread loop (matching the non-worker-thread path) instead of asyncio.run(), to avoid the loop reference mismatch.
Alternative: The _run_async docstring mentions When called from a worker thread... use a per-thread persistent loop. The worker thread path already has this (_get_worker_loop().run_until_complete(coro)). Consider whether the main thread path (tool_loop.run_until_complete(coro)) should be used more broadly when the running loop is in the main thread but we are calling from a sync tool handler.