Skip to content

fix(auxiliary_client): skip async close() during cache eviction - #45337

Open
liuhao1024 wants to merge 1 commit into
NousResearch:mainfrom
liuhao1024:fix/auxiliary-cache-async-close
Open

liuhao1024 wants to merge 1 commit into
NousResearch:mainfrom
liuhao1024:fix/auxiliary-cache-async-close

Conversation

@liuhao1024

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes unawaited coroutine warnings during auxiliary-client cache eviction. When _store_cached_client() evicts an old client whose close() method is a coroutine (e.g., AsyncOpenAI), calling it synchronously creates a RuntimeWarning: coroutine 'AsyncAPIClient.close' was never awaited. The fix adds an inspect.iscoroutinefunction guard matching the existing pattern in shutdown_cached_clients().

Related Issue

Fixes #45328

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • agent/auxiliary_client.py: Added inspect.iscoroutinefunction guard in _store_cached_client() and the manual cache-eviction path (lines ~4372 and ~2612) to skip synchronous close() on async clients — _force_close_async_httpx() already handles transport teardown
  • tests/agent/test_auxiliary_cache_async_close.py: 2 tests verifying async close is skipped (no unawaited coroutine warning) and sync close is still called

How to Test

  1. Run pytest tests/agent/test_auxiliary_cache_async_close.py -v — both tests should pass
  2. The async-close test seeds _client_cache with an AsyncCloseClient, replaces it via _store_cached_client(), and asserts zero RuntimeWarning about unawaited coroutines
  3. The sync-close test verifies that synchronous close() methods are still called during eviction

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/ -q and all tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Code Intelligence

  • Analyzed: agent/auxiliary_client.py::_store_cached_client (callers: cache refresh paths, 2 call sites fixed)
  • Blast radius: LOW — only affects cache eviction, no behavioral change for sync clients
  • Related patterns: mirrors existing shutdown_cached_clients() guard at line 4497

_store_cached_client() and the manual cache-eviction path called
close() synchronously on evicted clients. For AsyncOpenAI clients,
close() is a coroutine — calling it without await creates an unawaited
coroutine RuntimeWarning.

Fix: add `inspect.iscoroutinefunction` guard (matching the existing
pattern in shutdown_cached_clients()) so async clients are only
neutered via _force_close_async_httpx, not via synchronous close().

Fixes NousResearch#45328

@tonydwb tonydwb left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review Summary

Verdict: Approved

Clean, well-scoped fix/feature with comprehensive tests. No issues found.

  • Logic is correct and focused
  • Tests cover the new behavior
  • No security concerns
  • Good error handling

Reviewed by Hermes Agent

@alt-glitch alt-glitch added type/bug Something isn't working comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have labels Jun 13, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the focused fix. The premise remains live on current main: agent/auxiliary_client.py:3221-3223 and agent/auxiliary_client.py:5563-5565 still synchronously invoke cached clients' close() methods, while shutdown_cached_clients() already skips declared coroutine close methods at agent/auxiliary_client.py:5688-5690.

Problems

  • The new if close_fn and ... condition drops the existing callable(close_fn) guard. Retain it so a truthy non-callable close attribute is not invoked.
  • The guard only covers methods declared with async def; a callable that returns a coroutine would still leak that coroutine from this synchronous path. Issue #45328 identifies that broader cleanup concern. The added tests cover _store_cached_client() but not the separate live _evict_cached_clients() path at agent/auxiliary_client.py:3208-3226.

Suggested changes

  • Centralize the cached-client close policy and use it from both eviction paths.
  • Add a regression test for provider-wide eviction, alongside the replacement-path test.

Automated hermes-sweeper review.

Comment thread agent/auxiliary_client.py
_force_close_async_httpx(client)
try:
import inspect
close_fn = getattr(client, "close", None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please retain callable(close_fn) here. close_fn being truthy does not establish that it can be called; the previous guard preserved that distinction before applying the async-close exclusion.

Comment thread agent/auxiliary_client.py
_force_close_async_httpx(old_entry[0])
try:
import inspect
close_fn = getattr(old_entry[0], "close", None)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This declaration-level check still invokes a regular callable that returns a coroutine, then discards that coroutine. Consider routing both eviction sites through one helper that captures the result and closes a returned coroutine, as described in #45328.

@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 14, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/agent Core agent runtime: loop, agent_init, prompt builder, context-compression, responses endpoint P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(auxiliary_client): cache eviction calls async close without awaiting

4 participants