SIGKILL, or a laptop that slept before teardown can leave one behind — burning your Docker daemon, your E2B credits, or your Modal quota. Two commands find and reclaim them.
Quick Start
1
See what is still running
2
Reclaim it
3
Confirm it is gone
How It Works
Docker containers are found cross-process by thepraisonai=managed label and the praisonai_<id> name, so containers started by an earlier script are visible from a fresh CLI.
Behaviour change: the silent leak on shutdown
Two collaborating fixes close the leak:SyncComputeProvider._offload()falls back to inline execution when the executor is gone — it catches aRuntimeErrorwhose message contains"interpreter shutdown"or"no running event loop"and calls the sync function directly (blocking is fine: there is no event loop left to starve). UnrelatedRuntimeErrors still propagate._shutdown_orphan()intools_placement.pynow logs at warning level (was debug), so a genuine teardown failure is visible instead of buried.SharedCompute.shutdown()re-raises the underlying exception after clearinginstance_id, so the caller can log it.
praisonai managed ps / managed stop (and docker container prune for stopped-but-not-removed containers).
…and the run_on= finalizer that was never registered (PR #4109)
The PR #4107 fix above only fires when a finalizer actually runs. On ComputeManagedAgent — the backend behind run_on= — no finalizer had ever been registered, so the release path was never invoked and the instance survived the whole process.
Your run_on= instance is now reclaimed when your script exits — no code change:
run_on= script exits went from 1 → 0; an ordinary run is still 42 → 42.
The mechanism is a weakref.finalize registered inside _ensure() right after provision() succeeds:
- It runs on garbage collection and at interpreter exit — not one or the other.
- It deliberately does not close over the backend
self— closing overselfwould keep the backend alive and prevent the very collection it waits for.
_release(provider, instance_id, place) is a module-level function that shuts the instance down safely on any thread. As of PR #5348 it routes through the wrapper’s shared AsyncBridge instead of spinning a fresh loop per teardown:
- Shared bridge, never a fresh loop. It submits the shutdown coroutine to
praisonai._async_bridge.current_bridge().submit(_shutdown())— one long-lived background loop, never anasyncio.runloop per teardown and never a per-teardownThreadPoolExecutor. - Fire-and-forget during live GC. The future is not
.result()-ed on the caller’s thread, so a GC-triggered release inside async workflow code never pins the caller’s event loop for the cloud round-trip. Completion is logged viaadd_done_callback. - Blocking (bounded) at interpreter exit. When
sys.is_finalizing()isTrue,_releaseblocks onfut.result(timeout=_RELEASE_EXIT_TIMEOUT)so the cloud instance is actually reclaimed before the bridge’s ownatexitteardown cancels the in-flight shutdown or its daemon loop thread is killed.sys.is_finalizing()is only true during shutdown, so live GC never takes the blocking path. - Bridge unavailable → logged as leaked. If the bridge is poisoned or already gone at interpreter exit, the instance is logged as leaked at warning level instead of raising.
Environment Variables
Explicit
ashutdown() detaches the finalizer. An intentional teardown calls self._finalizer.detach() before shutting down, so the finalizer that fires shortly after never double-reclaims the same instance.run_on=; PR #5348 routed that release through the shared bridge and added the bounded exit-time wait. Together they close the container-leak story end to end for both sandbox= and run_on=.
When a provider can’t be queried
From a script
0 when the query is clean, 1 if any provider errored.
Best Practices
Run managed ps after any crash
Run managed ps after any crash
Any time a script that used
run_on= (or per-agent compute=) crashed or you killed it, run managed ps before starting a new run. Costs stack quietly.Wire stop --all into your exit hooks
Wire stop --all into your exit hooks
For long-running dev sessions, wire
praisonai managed stop --all into your shell’s exit trap or your IDE’s on-close hook so nothing is left behind when you close the laptop.local is not scanned — on purpose
local is not scanned — on purpose
managed ps scans docker, e2b, modal, daytona, flyio, tenki. local runs on this machine and has nothing to reclaim.Since PR #4071, this list is derived from the compute registry rather than hardcoded, so a compute backend contributed by a plugin is listed and stopped automatically — no extra wiring. local, subprocess, sandlock, ssh, native, and novita are excluded on purpose. See Bringing your own compute place.Related
Shared Sandbox
What
run_on= does and how to choose a providerManaged CLI
Full
praisonai managed command referenceAsync Bridge
The shared background loop
_release now uses for teardown
