Lifecycle & typed failures

States: creating -> running -> suspended -> terminated | failed. Every terminal state names its cause in a field, not in prose. You never parse an error string to learn how a sandbox died.

Typed causes

user_exitthe workload exited on its own; exit code attached
user_destroyyou called destroy
user_oomthe workload exceeded its memory request
user_timeoutan exec exceeded its timeout
user_image_build_failedyour image/template could not build; build log attached
policy_killed_ttlexpires_at passed; enforced server-side with teardown proof
provider_capacitywe lacked capacity. Typed 409, reservable, never billed
provider_infraour fault. Never billed, by contract
from numinous import NuminousError

try:
    sb = nc.sandboxes.create(...)
except NuminousError as e:
    e.cause              # "provider_capacity"
    e.is_provider_fault  # True: our problem, your credit
    e.retryable          # True (or reserve ahead: nc.capacity.reserve(...))

Hard TTLs, no idle heuristics

ttl_seconds at create becomes an absolute expires_at. The platform enforces it within seconds. There is no idle timer. A sandbox mid-upload or mid-build never gets reaped early, and an orphan never outlives its deadline. Enforcement produces policy_killed_ttl plus a teardown proof.

Idempotent creation

# a retried worker re-sends the same launch_token:
a = nc.sandboxes.create(..., launch_token="job-41-attempt-2")
b = nc.sandboxes.create(..., launch_token="job-41-attempt-2")
assert a["id"] == b["id"]          # replay, not a second sandbox
assert b["idempotent_replay"]

Teardown proof

out = nc.sandboxes.destroy(sb["id"])
out["teardown_proof"]
# {"verified_absent": true, "method": "...", "at": "..."}
# issued only after the data plane confirms absence.
# you never write a reaper.

Export survives death

Export runs on the host, outside the sandbox. It works while the sandbox runs, after it exits, after an OOM, after a TTL kill. A GB-scale artifact upload cannot race its own teardown, because the sandbox being gone does not matter.

# sandbox already terminated? doesn't matter:
nc.sandboxes.export(sb["id"], "/logs", to="s3://bucket/run-41/")

Suspend / resume, and sleep between steps

Suspend manually, or opt in to idle auto-suspend. Agents spend most of their wall clock waiting on model output; with auto-suspend the sandbox sleeps between exec calls, bills storage instead of compute, and wakes automatically on the next exec or file operation.

nc.sandboxes.suspend(sb["id"])   # scale to zero: storage cost only
nc.sandboxes.resume(sb["id"])    # same machine, processes intact

# opt-in sleep between agent steps:
sb = nc.sandboxes.create(template_id=tpl, vcpu=4, mem_gib=8,
                         auto_suspend_idle_seconds=60)
nc.sandboxes.exec(sb["id"], "make test")   # wakes it if asleep

Checkpoint and fork

Freeze a running sandbox into a new template while it keeps running, then boot any number of copies from that point. The per-episode reset primitive for RL rollouts.

ck = nc.sandboxes.checkpoint(sb["id"], name="after-setup")
fork = nc.sandboxes.create(template_id=ck["template_id"], vcpu=2, mem_gib=4)

Observed usage

Running sandboxes are sampled about every 30 seconds. The series is yours to query, and it is the measured basis for active-usage pricing.

m = nc.sandboxes.metrics(sb["id"])
m["avg_cpu_pct"], m["samples"][-1]