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:
- No running loop (
asyncio.get_running_loop()raises) →asyncio.run(_shutdown()). - A loop is already running → it bridges through
concurrent.futures.ThreadPoolExecutor(max_workers=1)so the shutdown coroutine runs on its own loop instead ofRuntimeError-ing. Without this bridge, GC landing inside async workflow code would raise and silently leak — the same class of bug PR #4107 fixed one layer down. This matches the patternSharedComputealready uses. - Errors are logged at warning and swallowed — teardown stays quiet.
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=. 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 reference
