Skip to content

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

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

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.
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 type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants