Skip to main content
Sandboxes normally self-destruct when your run ends. A crashed script, a 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

If nothing is up:
If something is up:
2

Reclaim it

3

Confirm it is gone


How It Works

Docker containers are found cross-process by the praisonai=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

Before PR #4107, every tools_run_on= run silently leaked its sandbox on shutdown. The weakref.finalize release ran while concurrent.futures was already in atexit, so run_in_executor raised “cannot schedule new futures after interpreter shutdown” — and the error was swallowed and logged as a successful release. Docker and Fly.io have no idle reaper, so leaked containers/machines lived indefinitely.
Two collaborating fixes close the leak:
  1. SyncComputeProvider._offload() falls back to inline execution when the executor is gone — it catches a RuntimeError whose 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). Unrelated RuntimeErrors still propagate.
  2. _shutdown_orphan() in tools_placement.py now logs at warning level (was debug), so a genuine teardown failure is visible instead of buried. SharedCompute.shutdown() re-raises the underlying exception after clearing instance_id, so the caller can log it.
If you ran an older version, reclaim any accumulated leaks with 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.
ComputeManagedAgent defaults to keep_alive=True — the instance is meant to survive between calls, not between processes. Without a finalizer, it survived the whole script:
  • For docker, that was one container per script.
  • For e2b, modal, daytona, tenki, flyio, that was a billed cloud instance whose only other reaper is the provider’s idle timer.
  • Docker and Fly.io have no idle timer, so those instances lived indefinitely.
The specific “cannot schedule new futures after interpreter shutdown” failure mode is now doubly defended by PR #5348: the release routes through the shared bridge (no per-teardown executor) and blocks on a bounded wait while sys.is_finalizing().
Your run_on= instance is now reclaimed when your script exits — no code change:
Verified before/after: containers left after a 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 over self would 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 an asyncio.run loop per teardown and never a per-teardown ThreadPoolExecutor.
  • 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 via add_done_callback.
  • Blocking (bounded) at interpreter exit. When sys.is_finalizing() is True, _release blocks on fut.result(timeout=_RELEASE_EXIT_TIMEOUT) so the cloud instance is actually reclaimed before the bridge’s own atexit teardown 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.
PR #4107 made the release path survive interpreter shutdown; PR #4109 made sure the release path is actually invoked on 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

Exit code is 0 when the query is clean, 1 if any provider errored.

Best Practices

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.
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.
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.

Shared Sandbox

What run_on= does and how to choose a provider

Managed CLI

Full praisonai managed command reference

Async Bridge

The shared background loop _release now uses for teardown