fix(weixin): rebuild aiohttp session for cross-loop send_* calls#17267
Closed
hackiey wants to merge 1 commit into
Closed
fix(weixin): rebuild aiohttp session for cross-loop send_* calls#17267hackiey wants to merge 1 commit into
hackiey wants to merge 1 commit into
Conversation
Reusing the gateway-owned aiohttp.ClientSession across event loops triggers aiohttp's TimerContext to raise "Timeout context manager should be used inside a task" because asyncio.current_task() returns None when checked against the session's original loop. This fires whenever the agent invokes the send_message tool with media: model_tools._run_async() spawns a worker thread and calls asyncio.run(), which creates a fresh event loop. send_document/send_image_file/etc. then try to use self._send_session, which was bound to the gateway's main loop, and aiohttp blows up before any bytes go over the wire. Regular text replies are unaffected because the gateway delivers them inline on its own loop. Add WeixinAdapter._loop_safe_session(), an async context manager that detects cross-loop usage and swaps in a per-call ClientSession bound to the running loop. Wrap the body of every send_* method with it (send, send_image, send_image_file, send_document, send_video, send_voice). send_typing is gateway-internal and never crosses loops, so it is left untouched. Refs: NousResearch#13099, NousResearch#12154, NousResearch#13281, NousResearch#16293
Collaborator
This was referenced Apr 29, 2026
Open
2 tasks
19 tasks
Contributor
|
Thanks for the detailed repro and runtime notes. This is now covered on current main, so I’m closing this as implemented by an automated hermes-sweeper review. Evidence:
The maintainer comment noting overlap with the other Weixin cross-loop PRs was useful context; current main has taken the guarded/fresh-session route for this path. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What does this PR do?
Fix the long-standing
RuntimeError: Timeout context manager should be used inside a taskraised by the Weixin adapter whenever the agent uses thesend_messagetool to send media (files, images, voice, video).Root cause (verified with runtime debug logging on macOS, Python 3.11.15, aiohttp 3.13.5):
When the agent invokes the
send_messagetool,model_tools._run_async()detects an active event loop in the gateway's main thread and dispatches the call to a worker thread that callsasyncio.run(). This creates a fresh event loop in the worker thread. Inside that loop,_send_to_platform → WeixinAdapter.send_document → _send_file → _api_postreusesself._send_session, which is anaiohttp.ClientSessionbound to the gateway's main loop. aiohttp'sTimerContext.__enter__then callsasyncio.current_task()against the session's original loop, getsNone, and raises.The bug only fires for media sends, because the gateway delivers normal text replies inline on its own loop — that path never crosses loops.
Debug capture from a failing call:
loopandsession_loopreprs look identical but are different instances (one in MainThread, one inasyncio_1).Fix: add
WeixinAdapter._loop_safe_session(), an async context manager that swaps in a per-callaiohttp.ClientSessionbound to the running loop when it detects the cached session belongs to a different loop. Eachsend_*method wraps its body withasync with self._loop_safe_session():. When the call is on the original loop the helper is a no-op (no new session, no extra connection).Related Issue
Refs #13099, #12154, #13281, #16293
(Partial fix — addresses the immediate Weixin symptom in all four issues. The deeper
model_tools._run_asynccross-loop architecture concern raised in #13281 is left for a separate change.)Type of Change
Changes Made
gateway/platforms/weixin.py:import contextlibWeixinAdapter._loop_safe_session()async context managerasync with self._loop_safe_session():insend,send_image,send_image_file,send_document,send_video,send_voicesend_typingis left untouched (gateway-internal, never crosses loops)tests/gateway/test_weixin.py: addTestWeixinLoopSafeSessionregression class with three cases (foreign-loop session swap, same-loop pass-through, no-session no-op)How to Test
pytest tests/gateway/test_weixin.py -q— 45 passed (42 existing + 3 new)hermes gateway, chat with the bot from WeChat[Weixin] send_document failed to=...: Timeout context manager should be used inside a taskChecklist
Code
fix(weixin): ...)pytest tests/gateway/test_weixin.py -qand all tests passDocumentation & Housekeeping
cli-config.yaml.exampleif I added/changed config keys — N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — N/AScreenshots / Logs
Debug log capturing the cross-loop root cause
```
[WEIXIN-DBG] task=<Task pending name='weixin-poll' coro=<WeixinAdapter._poll_loop()>>
loop=<_UnixSelectorEventLoop running=True closed=False debug=False>
session_loop=<_UnixSelectorEventLoop running=True closed=False debug=False>
thread='MainThread'
[WEIXIN-DBG] task=<Task pending name='Task-25' coro=<WeixinAdapter.send_typing()>>
... thread='MainThread' ← gateway-owned, OK
[WEIXIN-DBG] task=<Task pending name='Task-33'
coro=<_send_to_platform() running at .../send_message_tool.py:507>
cb=[_run_until_complete_cb()]>
loop=<_UnixSelectorEventLoop running=True closed=False debug=False>
session_loop=<_UnixSelectorEventLoop running=True closed=False debug=False>
thread='asyncio_1' ← worker thread, fresh loop
ERROR gateway.platforms.weixin: [Weixin] send_document failed to=o9cq80xK:
Timeout context manager should be used inside a task
Traceback (most recent call last):
File ".../weixin.py", line 1715, in send_document
message_id = await self._send_file(chat_id, file_path, caption or "")
File ".../weixin.py", line 1795, in _send_file
upload_response = await _get_upload_url(...)
File ".../weixin.py", line 508, in _get_upload_url
return await _api_post(...)
File ".../weixin.py", line 366, in _api_post
async with session.post(url, ..., timeout=timeout) as response:
File ".../aiohttp/client.py", line 632, in _request
with timer:
File ".../aiohttp/helpers.py", line 678, in enter
raise RuntimeError("Timeout context manager should be used inside a task")
```
After the fix, the same call path on
thread='asyncio_1'swaps in a freshaiohttp.ClientSessionbound to that thread's loop, and the upload completes normally.