hermes - 💡(How to fix) Fix WeChat adapter: "Timeout context manager should be used inside a task" error when cron job delivers to WeChat [2 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#20229Fetched 2026-05-06 06:38:02
View on GitHub
Comments
2
Participants
2
Timeline
8
Reactions
0
Author
Participants
Timeline (top)
labeled ×6commented ×2

Error Message

Pseudocode for gateway/platforms/weixin.py around line 2031

import asyncio import threading

live_adapter = _LIVE_ADAPTERS.get(resolved_token) send_session = getattr(live_adapter, '_send_session', None)

Only use live adapter if we are in the same event loop context.

Skip the live path if we are in asyncio.run() from a different thread

(cron scheduler uses ThreadPoolExecutor + asyncio.run, creating a new loop).

use_live = ( live_adapter is not None and send_session is not None and not send_session.closed ) if use_live: try: current_loop = asyncio.get_running_loop() # If we got here from a ThreadPoolExecutor thread with its own # asyncio.run() loop, current_loop differs from the gateway loop - # fall through to standalone path in that case. except RuntimeError: use_live = False # No running loop, fall through

if use_live: # ... live adapter path (existing code) else: # ... standalone path (existing async with ClientSession() block)

Root Cause

The issue is a thread/event-loop mismatch between the gateway's main async context and the cron scheduler's async invocation:

  1. WeixinAdapter.__init__ (gateway/platforms/weixin.py:1219) creates self._send_session = aiohttp.ClientSession(...) in the gateway's main async event loop (when the user first connects via WeChat). This session is stored in the global _LIVE_ADAPTERS dict keyed by token.

  2. send_weixin_direct (gateway/platforms/weixin.py:2031-2038) has a "live adapter" fast path that preferentially uses this existing session:

    live_adapter = _LIVE_ADAPTERS.get(resolved_token)
    send_session = getattr(live_adapter, '_send_session', None)
    if live_adapter is not None and send_session is not None and not send_session.closed:
        last_result = await live_adapter.send(chat_id, cleaned)  # <- called from wrong loop
  3. cron/scheduler.py:481 runs the delivery coroutine in a fresh event loop via asyncio.run(coro) - inside a ThreadPoolExecutor on the fallback path (line 488-490). This creates a new thread with a new event loop that has no awareness of the gateway's loop.

  4. When live_adapter.send() executes HTTP requests, aiohttp 3.13.5 detects that the session's internal state (SSL context, socket handles) is tied to the original gateway loop and throws:

    Timeout context manager should be used inside a task

Code Example

Timeout context manager should be used inside a task
[Weixin] send chunk failed ... attempt=1/5

---

live_adapter = _LIVE_ADAPTERS.get(resolved_token)
   send_session = getattr(live_adapter, '_send_session', None)
   if live_adapter is not None and send_session is not None and not send_session.closed:
       last_result = await live_adapter.send(chat_id, cleaned)  # <- called from wrong loop

---

Timeout context manager should be used inside a task

---

# Pseudocode for gateway/platforms/weixin.py around line 2031
import asyncio
import threading

live_adapter = _LIVE_ADAPTERS.get(resolved_token)
send_session = getattr(live_adapter, '_send_session', None)

# Only use live adapter if we are in the same event loop context.
# Skip the live path if we are in asyncio.run() from a different thread
# (cron scheduler uses ThreadPoolExecutor + asyncio.run, creating a new loop).
use_live = (
    live_adapter is not None
    and send_session is not None
    and not send_session.closed
)
if use_live:
    try:
        current_loop = asyncio.get_running_loop()
        # If we got here from a ThreadPoolExecutor thread with its own
        # asyncio.run() loop, current_loop differs from the gateway loop -
        # fall through to standalone path in that case.
    except RuntimeError:
        use_live = False  # No running loop, fall through

if use_live:
    # ... live adapter path (existing code)
else:
    # ... standalone path (existing async with ClientSession() block)
RAW_BUFFERClick to expand / collapse

Environment

  • hermes-agent: 0.12.0
  • Python: 3.13.5
  • aiohttp: 3.13.5
  • Platform: WeChat (iLink)

Problem Description

When a cron job tries to deliver a message to WeChat, the send fails with the following error in gateway.log:

Timeout context manager should be used inside a task
[Weixin] send chunk failed ... attempt=1/5

The same WeChat adapter works perfectly when the user interacts in the active gateway session, but fails when triggered by a cron job or a delegate_task subagent.

Root Cause Analysis

The issue is a thread/event-loop mismatch between the gateway's main async context and the cron scheduler's async invocation:

  1. WeixinAdapter.__init__ (gateway/platforms/weixin.py:1219) creates self._send_session = aiohttp.ClientSession(...) in the gateway's main async event loop (when the user first connects via WeChat). This session is stored in the global _LIVE_ADAPTERS dict keyed by token.

  2. send_weixin_direct (gateway/platforms/weixin.py:2031-2038) has a "live adapter" fast path that preferentially uses this existing session:

    live_adapter = _LIVE_ADAPTERS.get(resolved_token)
    send_session = getattr(live_adapter, '_send_session', None)
    if live_adapter is not None and send_session is not None and not send_session.closed:
        last_result = await live_adapter.send(chat_id, cleaned)  # <- called from wrong loop
  3. cron/scheduler.py:481 runs the delivery coroutine in a fresh event loop via asyncio.run(coro) - inside a ThreadPoolExecutor on the fallback path (line 488-490). This creates a new thread with a new event loop that has no awareness of the gateway's loop.

  4. When live_adapter.send() executes HTTP requests, aiohttp 3.13.5 detects that the session's internal state (SSL context, socket handles) is tied to the original gateway loop and throws:

    Timeout context manager should be used inside a task

Expected Behavior

Cron jobs and subagents should be able to send WeChat messages without errors.

Suggested Fix

In send_weixin_direct, the live adapter fast path should detect whether it is being called from the same event loop as the one the session was created in. If not (i.e., running in a cron/delegate context), it should skip the live adapter path and fall through to the standalone aiohttp.ClientSession() path, which creates a fresh, self-contained session per call.

A possible implementation - check whether we are in an asyncio.run() thread vs. the gateway's main loop, and fall through to the standalone path if not:

# Pseudocode for gateway/platforms/weixin.py around line 2031
import asyncio
import threading

live_adapter = _LIVE_ADAPTERS.get(resolved_token)
send_session = getattr(live_adapter, '_send_session', None)

# Only use live adapter if we are in the same event loop context.
# Skip the live path if we are in asyncio.run() from a different thread
# (cron scheduler uses ThreadPoolExecutor + asyncio.run, creating a new loop).
use_live = (
    live_adapter is not None
    and send_session is not None
    and not send_session.closed
)
if use_live:
    try:
        current_loop = asyncio.get_running_loop()
        # If we got here from a ThreadPoolExecutor thread with its own
        # asyncio.run() loop, current_loop differs from the gateway loop -
        # fall through to standalone path in that case.
    except RuntimeError:
        use_live = False  # No running loop, fall through

if use_live:
    # ... live adapter path (existing code)
else:
    # ... standalone path (existing async with ClientSession() block)

Alternatively, the fix could be in cron/scheduler.py to ensure cron delivery always uses the standalone session path for WeChat.

Additional Context

  • last_run_at: null confirms cron jobs never successfully delivered a WeChat message
  • Manual trigger of cron job updates next_run_at but does not execute the agent
  • The error is 100% reproducible on every WeChat cron delivery attempt

extent analysis

TL;DR

The issue can be fixed by modifying the send_weixin_direct function to detect whether it's being called from the same event loop as the one the session was created in and fall through to the standalone aiohttp.ClientSession() path if not.

Guidance

  • Identify the event loop mismatch between the gateway's main async context and the cron scheduler's async invocation.
  • Modify the send_weixin_direct function to check if it's being called from the same event loop as the one the session was created in.
  • If not, fall through to the standalone aiohttp.ClientSession() path to create a fresh, self-contained session per call.
  • Consider alternative fix in cron/scheduler.py to ensure cron delivery always uses the standalone session path for WeChat.

Example

import asyncio

# ...

use_live = (
    live_adapter is not None
    and send_session is not None
    and not send_session.closed
)
if use_live:
    try:
        current_loop = asyncio.get_running_loop()
    except RuntimeError:
        use_live = False  # No running loop, fall through

if use_live:
    # ... live adapter path (existing code)
else:
    # ... standalone path (existing async with ClientSession() block)

Notes

The provided pseudocode suggests a possible implementation, but the actual fix may require adjustments based on the specific codebase and requirements.

Recommendation

Apply the suggested workaround by modifying the send_weixin_direct function to detect the event loop mismatch and fall through to the standalone session path if necessary. This should resolve the issue with WeChat message delivery in cron jobs and subagents.

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