Description
The README says underlying HTTP connections are closed when the client is garbage collected, but the async HTTP wrapper can only schedule aclose() when garbage collection happens inside a running event loop.
If an AsyncStagehand client becomes unreachable after asyncio.run() returns (or in other code with no active loop), AsyncHttpxClientWrapper.__del__ catches RuntimeError from asyncio.get_running_loop() and silently does nothing. The connection pool is left unclosed.
There is already a TODO in the implementation noting that only asyncio runtimes are supported, but the no-running-loop case is also unhandled.
Reproduction
import gc
import asyncio
from stagehand import AsyncStagehand
async def create_client():
return AsyncStagehand()
client = asyncio.run(create_client())
del client
gc.collect() # runs after asyncio.run() closed its loop
At src/stagehand/_base_client.py:1416-1424, the finalizer attempts:
asyncio.get_running_loop().create_task(self.aclose())
There is no running loop at gc.collect(), and the broad except Exception: pass suppresses the failure without closing or warning.
Expected behavior
Either async clients should have a reliable cleanup strategy for this case, or the finalizer should emit a ResourceWarning and the documentation should clearly state that async clients are not automatically closed by garbage collection and must use async with / await close().
Actual behavior
Cleanup is silently skipped, contradicting the unconditional garbage-collection claim in README.md:678-680.
Why this matters
Unclosed async clients retain connection pools and sockets. Silence makes leaks hard to diagnose, particularly in test runners, notebooks, and applications that construct a client in one event-loop lifetime and release it later.
Prior-art check
I searched open/closed issues and PRs for unclosed AsyncStagehand, garbage collection, AsyncHttpxClientWrapper, and missing running loops and found no existing report.
Description
The README says underlying HTTP connections are closed when the client is garbage collected, but the async HTTP wrapper can only schedule
aclose()when garbage collection happens inside a running event loop.If an
AsyncStagehandclient becomes unreachable afterasyncio.run()returns (or in other code with no active loop),AsyncHttpxClientWrapper.__del__catchesRuntimeErrorfromasyncio.get_running_loop()and silently does nothing. The connection pool is left unclosed.There is already a TODO in the implementation noting that only asyncio runtimes are supported, but the no-running-loop case is also unhandled.
Reproduction
At
src/stagehand/_base_client.py:1416-1424, the finalizer attempts:There is no running loop at
gc.collect(), and the broadexcept Exception: passsuppresses the failure without closing or warning.Expected behavior
Either async clients should have a reliable cleanup strategy for this case, or the finalizer should emit a
ResourceWarningand the documentation should clearly state that async clients are not automatically closed by garbage collection and must useasync with/await close().Actual behavior
Cleanup is silently skipped, contradicting the unconditional garbage-collection claim in
README.md:678-680.Why this matters
Unclosed async clients retain connection pools and sockets. Silence makes leaks hard to diagnose, particularly in test runners, notebooks, and applications that construct a client in one event-loop lifetime and release it later.
Prior-art check
I searched open/closed issues and PRs for unclosed
AsyncStagehand, garbage collection,AsyncHttpxClientWrapper, and missing running loops and found no existing report.