Skip to content

fix(weixin): skip live adapter reuse in send_weixin_direct when on a different event loop - #12810

Closed
yueyefengs wants to merge 1 commit into
NousResearch:mainfrom
yueyefengs:fix/weixin-cross-loop-timeout
Closed

fix(weixin): skip live adapter reuse in send_weixin_direct when on a different event loop#12810
yueyefengs wants to merge 1 commit into
NousResearch:mainfrom
yueyefengs:fix/weixin-cross-loop-timeout

Conversation

@yueyefengs

Copy link
Copy Markdown

Problem

When the cron scheduler's live adapter delivery fails (e.g. ret=-2 network error), it falls back to a standalone path that runs _send_to_platform inside asyncio.run() in a thread-pool executor — a fresh event loop.

send_weixin_direct then finds the live adapter in _LIVE_ADAPTERS and unconditionally reuses its aiohttp.ClientSession, which was created on the gateway event loop. Using that session from a different loop causes aiohttp's internal asyncio.timeout() to raise:

Timeout context manager should be used inside a task

because asyncio.timeout() (Python 3.11+) checks asyncio.current_task(), and while a Task does exist on the new loop, the connector is bound to the old one — the mismatch triggers the assertion.

The result is three retried failures followed by a delivery error log, and the cron message never reaches the user.

Fix

Before reusing the live adapter session in send_weixin_direct, compare connector._loop against asyncio.get_running_loop(). If they differ, skip the live adapter branch and fall through to the existing fresh-session path (async with aiohttp.ClientSession(...)).

The live adapter fast-path is preserved for all callers that are already on the correct loop (e.g. the /send_message tool running inside the gateway).

Test Plan

  • pytest tests/gateway/test_weixin.py — 42 passed, 0 failures
  • Reproduces via: cron job delivery where first live-adapter attempt returns ret=-2, triggering the standalone fallback path

…reuse

The previous attempt used connector._loop to detect cross-loop reuse, but
aiohttp 3.9+ no longer stores _loop on the connector, so the guard always
evaluated to True and the bug persisted.

Instead, store asyncio.get_running_loop() as self._event_loop in connect(),
then compare it against the caller's running loop in send_weixin_direct()
before deciding whether to reuse the live adapter's session.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@yueyefengs
yueyefengs force-pushed the fix/weixin-cross-loop-timeout branch from 2b533a4 to bf020c2 Compare April 20, 2026 04:56
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists platform/wecom WeCom / WeChat Work adapter comp/gateway Gateway runner, session dispatch, delivery comp/cron Cron scheduler and job management labels Apr 22, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Likely duplicate of #13350 and #13520 — all three fix cross-event-loop aiohttp session reuse in Weixin direct send.

1 similar comment
@alt-glitch

Copy link
Copy Markdown
Collaborator

Likely duplicate of #13350 and #13520 — all three fix cross-event-loop aiohttp session reuse in Weixin direct send.

@u9wangcl

u9wangcl commented May 1, 2026

Copy link
Copy Markdown

Confirming reproduction and verifying this fix shape on recent main:

  • Version: v0.12.0 (commit 0159f25)
  • Python: 3.11.15, aiohttp 3.13.5
  • Platform: weixin (DM)
  • Trigger: agent calls send_message tool with MEDIA:/tmp/foo.pdf while the gateway has the live adapter registered. 100% reproducible.

I applied a functionally-equivalent local patch to send_weixin_direct (loop-affinity check on the live adapter's session before reusing it, fall through to the existing fresh-ClientSession branch otherwise) and the PDF was delivered successfully via the agent tool path on the first try. Text sends were never affected; the existing fresh-session fallback path worked as-is once the cross-loop branch was bypassed.

Minor naming nit on this PR: caching self._event_loop in connect() avoids reaching into aiohttp's private session._loop, which is the more forward-compatible choice — happy to see it land.

For the maintainers triaging the cluster of similar PRs (#13520, #14530, #14873, #15911, #16790, #17267, #17557): all that I've read converge on the same one-liner — gate the live-adapter reuse on loop affinity. This PR is the earliest and one of the simpler diffs.

@teknium1

Copy link
Copy Markdown
Contributor

Thanks for the focused cross-loop fix. Current main already provides the requested behavioral guarantee.

  • Automated hermes-sweeper review verified gateway/platforms/weixin.py:2307-2336: the live adapter is used only when send_session._loop is asyncio.get_running_loop(); a mismatch falls through to the fresh aiohttp.ClientSession path.
  • This landed in a22465e07ab4b71019f711e7a6463f6590c50742 (fix(weixin): send_weixin_direct cross-loop session check) and is contained in v2026.5.7.
  • This also covers the cron standalone fallback, which invokes _send_to_platform through asyncio.run() at cron/scheduler.py:1901-1932.

Closing as implemented on main.

@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/cron Cron scheduler and job management 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.

5 participants