Skip to content

fix: resolve 'Timeout context manager should be used inside a task' in Weixin send - #14530

Closed
yyufoyy02 wants to merge 1 commit into
NousResearch:mainfrom
yyufoyy02:fix/weixin-send-timeout-context-manager
Closed

fix: resolve 'Timeout context manager should be used inside a task' in Weixin send#14530
yyufoyy02 wants to merge 1 commit into
NousResearch:mainfrom
yyufoyy02:fix/weixin-send-timeout-context-manager

Conversation

@yyufoyy02

Copy link
Copy Markdown

Problem

When the gateway is running and a Weixin user sends a message, the agent responds via send_weixin_direct() which tries to reuse the live adapter's aiohttp.ClientSession (bound to the gateway's event loop). However, send_message tool calls are dispatched via _run_async() which may run on a worker thread using loop.run_until_complete().

run_until_complete() does not create a task, so asyncio.current_task() returns None. This causes aiohttp.ClientTimeout.__enter__ to raise:

RuntimeError: Timeout context manager should be used inside a task

This breaks all Weixin send operations (text + media) from the send_message tool and cron delivery when the gateway is running.

Introduced by commit 5ca52bae (split poll/send sessions, reuse live adapter).

Fix

  1. gateway/platforms/weixin.pysend_weixin_direct() always creates its own aiohttp.ClientSession instead of reusing the live adapter's session (which is bound to the gateway's event loop and may be on a different thread).

  2. model_tools.py_run_async() uses asyncio.run() instead of run_until_complete() for both worker-thread and main-thread fallback paths. asyncio.run() creates a proper task context, which aiohttp >= 3.9 requires for ClientTimeout.

Trade-off

Using asyncio.run() instead of a persistent loop means each call creates/destroys an event loop. The original persistent loop was introduced to prevent "Event loop is closed" errors with cached httpx/AsyncOpenAI clients (commit 7a427d7b). If this becomes an issue again, an alternative fix would be to wrap the coroutine in loop.create_task() before run_until_complete(), e.g.:

worker_loop = _get_worker_loop()
return worker_loop.run_until_complete(worker_loop.create_task(coro))

This preserves the persistent loop while still providing a task context.

Verification

  • Reproduced: all send_message calls to Weixin fail with timeout error when gateway is running
  • Fixed: send_weixin_direct() creates its own session, asyncio.run() provides task context
  • No impact on gateway's own send path (which runs on the gateway loop directly)

…n Weixin send

When send_weixin_direct() reuses the live adapter's aiohttp session
(from the gateway's event loop) but runs via _run_async on a worker
thread, asyncio.current_task() returns None because
run_until_complete() does not create a task.  This causes aiohttp's
ClientTimeout.__enter__ to raise RuntimeError.

Two changes:

1. gateway/platforms/weixin.py — send_weixin_direct() always creates
   its own aiohttp.ClientSession instead of reusing the live adapter's
   session bound to the gateway's event loop.

2. model_tools.py — _run_async() uses asyncio.run() instead of
   run_until_complete() for both worker-thread and main-thread paths.
   asyncio.run() creates a proper task context, which aiohttp >= 3.9
   requires for ClientTimeout.

Fixes: send_message tool and cron delivery failing for Weixin platform
with 'Timeout context manager should be used inside a task' error.
@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused Weixin investigation. This is now implemented on main with a narrower fix that preserves the async bridge's persistent-loop contract.

  • Automated hermes-sweeper review verified a22465e07ab4b71019f711e7a6463f6590c50742 (fix(weixin): send_weixin_direct cross-loop session check), shipped in v2026.5.7. gateway/platforms/weixin.py:2307-2336 reuses the live session only on its owning event loop and otherwise creates a fresh session.
  • 566669013f3f9c0b52cb1392250b76d510d99dc7 additionally changed the remaining iLink API timeout paths to asyncio.wait_for; see gateway/platforms/weixin.py:370-414.
  • The proposed global asyncio.run() change is not needed on current main and would conflict with the persistent-loop regression contract in tests/test_model_tools_async_bridge.py:50-84.

@teknium1 teknium1 closed this Jul 12, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jul 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/wecom WeCom / WeChat Work adapter sweeper:implemented-on-main Sweeper: behavior already present on current main type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants