OpenHands agent-server · design proposal · 2 Oct 2026

One reclaimer for local and remote runtimes

Every conversation leaves a runtime behind: files on our disk for Docker and local worktrees, or a sandbox at a provider for remote runtimes. This proposal manages both with the same policies, and keeps what the current PR stack (#5263 → #5433) already built.

The architecture

Policies (desired state) IdlePause idle > X min → suspend Retention inactive > N days → delete Budget / Cap disk % or sandbox count Reconciler (one pass) Observe re-read every runtime and its state Plan list of commands · printable as dry run Apply one command at a time, under idle() idempotent: a step the provider already did is a no-op Activity tracker (ours) last_active runtimes() commands role interfaces Local · files on our disk Docker runtime-data/<id> Local worktrees (#5433) Podman, Apptainer (future) Discard → Trash → background delete Remote · provider API Daytona, E2B, Runloop … OpenHands Cloud Suspend · Delete via the provider no file tiers here at creation: translated into provider settings
Each pass re-reads real state, plans commands from the policies, and applies them one at a time while the runtime is held idle. Where a provider has its own lifecycle settings (dashed path), the policies are written into them when the sandbox is created, and the loop only enforces what the provider can't.

The five building blocks

PatternSolvesExists today as
Reconciler loopProviders change state behind our back (Daytona stops sandboxes after 15 min by default). Re-reading every pass makes those races harmless.Reclaimer.run_pass()
BridgePolicies and backends vary independently: a new provider gets every policy, and a new policy works on every backend.Reclaimer + ReclaimableStorage
Role interfacesBackends differ in what they can do. Each implements only Listable, Suspendable, Deletable, FileReclaimable or NativeLifecycle, and the type checker catches a missing method.one protocol; local retire() returns []
Native first, loop as fallbackMost providers already have idle and expiry settings. Configure those; run our loop only for the rest (E2B never auto-deletes).not yet
Commands: plan, then applyEvery step destroys something. A plan doubles as a dry-run report and a test fixture.select outside the lock, move inside it

What each backend can do

From the SDK code and the providers' official docs, checked 2 Oct 2026. ✓ yes · ~ partly · ✗ no · — not applicable.

BackendList + last activitySuspend, files keptDeleteFile tiersNative lifecycle
Docker (ours)✓ from our state file✓ stop container✓ retire✓—
Local worktrees✓ from our state file—✓ on delete✓—
Daytona✓ lastActivityAt✓✓✗✓ stop, pause, archive, delete
E2B~ list, no activity✓ incl. memory✓✗~ auto-pause, never deletes
Modal~ list, no state✗ no pause✓✗~ timeouts terminate
Cloudflare Sandbox✗ no list✗ sleep wipes files✓✗~ sleep after idle
OpenHands Cloud~ list endpoint, no activity field✗ no pause✓✗unverified

Seven of the fifteen providers surveyed expose no last-activity time, so the activity tracker has to be ours.

Where it runs

Trade-offs

Pros

  • One set of policies for every runtime, written and tested once.
  • Gaps are explicit. A backend that can't pause simply doesn't implement Suspendable, and the type checker enforces it.
  • Works with providers' own settings instead of racing them.
  • Safe by construction: it re-reads state every pass, acts only while a runtime is idle, and can print a dry run before deleting anything.
  • Builds on the current stack. Reclaimer, idle(), Trash and the selectors move in unchanged.

Cons

  • More moving parts than today's file-only reclaimer, for backends nobody uses yet.
  • Native settings are written at creation. Changing a policy later means re-syncing every existing sandbox, or the two drift apart.
  • For most providers we must track activity ourselves, so only a host that sees every conversation can run it.
  • Common-denominator risk: archive tiers, memory snapshots and snapshot billing need per-backend escape hatches.
  • Eventual, not instant. Actions land on the next pass, minutes later.
  • Remote backends need provider accounts or mocks to test, and break when provider APIs change.