JuliusBrussee/caveman · error · MiddlewareError
closed
closed
Error message
closed
What it means
The async runtime's _submit refuses to run work after aclose() has been called: it raises MiddlewareError("closed"). This protects against submitting sync functions onto a shut-down executor and leaking tasks after teardown.
Solutions
- Keep all runtime calls inside the `async with runtime:` block
- Re-create a new runtime instance if you need to use it after close
- Track lifecycle in app code: don't close in one component while another still holds the runtime
- Catch MiddlewareError with code "closed" and treat it as a lifecycle bug, not retryable
Example fix
// before
runtime = AsyncRuntime(...)
await runtime.aclose()
await runtime.optimize(...) # MiddlewareError: closed
// after
async with AsyncRuntime(...) as runtime:
await runtime.optimize(...) # all usage inside the context Defensive patterns
Strategy: try-catch
Validate before calling
if runtime_is_closed:
raise RuntimeError('runtime already closed; create a new instance') Type guard
def runtime_open(runtime) -> bool:
return not getattr(runtime, '_closed', True) Try / catch
try:
await runtime.optimize(msg)
except MiddlewareError as e:
if str(e) == 'closed':
runtime = AsyncRuntime(...) # recreate
await runtime.optimize(msg)
else:
raise Prevention
- Scope all runtime usage to `async with runtime:` blocks
- Never store long-lived references to the runtime outside the owning component
- Shut down the runtime only after all producers/consumers are joined
- Expose runtime readiness through your app's health state, not raw attribute access
When it happens
Trigger: Calling any runtime entry point — ready, preflight, optimize, retrieve, observe, delete_session — after the runtime was used as an async context manager and exited, or after an explicit await aclose().
Common situations: Calling runtime methods outside the `async with` block; keeping a reference to the runtime after shutdown; a lifespan/handler that closes the runtime while other code still uses it.
Related errors
- ccr: recovery store is closed
- closed
- Create a separate Caveman Strands bundle for each agent
- Expected an HTTPX2 AsyncBaseTransport
- Match the sync/async Caveman transport to the native client
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/e96d1dbcd2930df6.
Report an issue: GitHub.
Appendix: source
Thrown at packages/sdk/python/caveman_cloud/middleware/async_runtime.py:118
async def delete_session(self, scope):
return await self._submit(self._runtime.delete_session, scope)
async def aclose(self):
self._closed = True
if self._owns_runtime:
self._runtime.close()
await asyncio.to_thread(self._executor.shutdown, wait=True, cancel_futures=True)
async def __aenter__(self):
return self
async def __aexit__(self, *_):
await self.aclose()
async def _submit(self, function, *args, **kwargs):
if self._closed:
raise MiddlewareError("closed")
if not self._slots.acquire(blocking=False):
raise MiddlewareError("capacity")
context = contextvars.copy_context()
try:
future = self._executor.submit(context.run, functools.partial(function, *args, **kwargs))
except BaseException:
self._slots.release()
raise
# Release only when the underlying worker actually terminates. Releasing
# on coroutine cancellation would allow unlimited queued/running work.
future.add_done_callback(lambda _: self._slots.release())
return await asyncio.wrap_future(future)
View on GitHub (pinned to 3ee70a1026)