Sandboxes and environments
After this page you can reason about the compute your agent runs on without ever addressing it: you will know how to tell whether a session currently has a sandbox, what happens to the session when it goes away, and how to decide whether more work can still be done.
A sandbox is not a resource
Section titled “A sandbox is not a resource”There is no sandbox object on this API. You cannot list one, extend one, keep
one warm, or delete one, and nothing in /v1 takes a sandbox identifier. The
one thing you can ask for is that a session have a workspace again —
PUT /v1/sessions/{sessionId}/workspace, described below — and even that is
addressed by session, never by sandbox.
What you get instead is a projection on the session — the workspace field:
{ "session": { "id": "3f7d4f4c-2b6a-4a2e-9c1a-1f2b3c4d5e6f", "workspace": { "state": "attached", "expiresAt": "2026-08-11T10:04:00.000Z" } }}Three states, and that is the entire vocabulary:
workspace.state |
Meaning | expiresAt |
|---|---|---|
unavailable |
No sandbox is associated with this session. The normal state of a session that has just been created, and of one whose compute was released long ago. | absent |
attached |
A sandbox is leased to this session and the lease has not passed its deadline. | present |
expired |
A sandbox was leased and its deadline has passed. | present, in the past |
expired and unavailable are both “there is no compute right now”. They differ
only in whether one existed: expired is your signal that state may be
recoverable from a checkpoint, which is what continuation reports.
Compute is allocated by work, not by request. Creating a session provisions
nothing; submitting a turn is what causes a sandbox to be leased, and the 202
comes back before that has happened. So unavailable immediately after a submit
is expected, not an error.
Leases are bounded twice, and neither bound is yours to raise
Section titled “Leases are bounded twice, and neither bound is yours to raise”A lease ends at whichever of two deadlines comes first.
- A runaway ceiling of 24 hours, measured from when the lease started. Nothing moves it: no request field, no header, no capability, no keepalive.
- An idle hold of 25 minutes past the last sign that the workspace is in use. Attaching to it, connecting to it, and the platform seeing a run the engine still reports as live each push the lease out again — and the deadline the session reports moves with it.
In practice the idle hold is the bound you meet, not the ceiling — but it
bounds idle time. A run the engine still reports as live is itself a touch,
so an executing run keeps the lease open until it finishes or reaches the
24-hour ceiling. What the idle hold ends is an abandoned session: nothing
running and nothing connected, so its capacity is released 25 minutes after
the last touch instead of being held to the ceiling. A run still executing
when the hold does lapse — the sandbox gone, or no longer reporting — is
terminated with stopReason: "execution_deadline" on wamp.run.crashed, the
same event a ceiling stop produces. An idle release with nothing running just
leaves the workspace expired.
workspace.expiresAt is a live view of the hold the sandbox is actually
under: it does move forward while the workspace is in use, up to the
ceiling. Read it when it matters rather than caching it; at the moment you
read it, it is the true deadline of the current hold.
This is much less restrictive than it sounds, because both bounds bound the sandbox, not the session. When a lease ends the session is untouched — its turns, event log, artifact manifests and publications are all still there — and the next turn runs on a fresh sandbox. A conversation that spans a week is ordinary; it simply spans many leases.
What you must not build is a state machine that assumes the workspace it saw a
minute ago is still there. Read workspace.state when it matters, and treat
expired as routine.
Two calls need a live workspace to answer at all —
GET /v1/sessions/{sessionId}/repository/review and
PUT /v1/sessions/{sessionId}/publications/{publicationId} — and both return
409 cloud_workspace_expired when the lease has lapsed. When that happens,
PUT /v1/sessions/{sessionId}/workspace provisions a fresh sandbox and replays
the session’s checkpoint into it, so a late publish is a retry rather than lost
work. It needs wamp.cloud.turns:submit, is idempotent (a session that still
holds a usable lease comes back unchanged), and answers
409 cloud_resume_unavailable rather than attaching an empty workspace when
continuation.canContinue is false.
A fresh Session can accept work without having prior state to restore. Its continuation is different from a restorable checkpoint:
{ "canContinue": true, "state": "fresh", "conversation": { "fidelity": "none" }, "workspace": { "fidelity": "none" }, "reason": "no_prior_work"}Sessions outlive sandboxes, and continuation says at what cost
Section titled “Sessions outlive sandboxes, and continuation says at what cost”Because compute is disposable and the session is not, every session reports what a further turn would actually get:
{ "continuation": { "canContinue": true, "state": "restorable", "conversation": { "fidelity": "exact" }, "workspace": { "fidelity": "portable_tree" }, "boundaryRunId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d" }}canContinue means “this session will accept another turn”, not “your
previous work survived”. Those are two questions and continuation answers
them with two different fields. A fresh session reports canContinue: true
with both fidelities none: it accepts a turn, and it carries nothing.
reason is the survival answer, and its rule is exact. It is present
whenever a next turn will not carry the previous work, and absent whenever it
will. So one comparison is the whole check — no fidelity arithmetic:
const carriesPreviousWork = session.continuation.reason == null;no_prior_work is the value that arrives alongside canContinue: true, and it
is the case a caller branching on canContinue alone silently gets wrong: the
turn is accepted, the agent starts from nothing, and it looks like a resume.
state is the summary, and there are four values:
state |
canContinue |
reason |
What a next turn gets |
|---|---|---|---|
live |
true |
absent | The lease is still attached. The agent continues in place, conversation and files intact. |
fresh |
true |
no_prior_work |
Nothing has run yet on this session. A next turn starts clean, which is what you wanted anyway. |
restorable |
true |
absent | Compute is gone, but a checkpoint can be restored: the conversation exactly, and the working tree as a portable copy. |
unavailable |
false |
one of the other four | No further turn will be admitted, so work in this session cannot go on. Usually that is because no sandbox and no restorable checkpoint exist — but an archived session reports unavailable too, keeping the fidelities and the boundaryRunId it had. Read reason for which case you are in. |
The two fidelity fields say what “restore” means, and they are worth reading
rather than collapsing into a boolean:
conversation.fidelity—live,exact,result_only, ornone.exactmeans the agent resumes with the same history.result_onlymeans the conversation could not be captured — the run’s outcome and artifacts survived, the history did not — and it is never restorable: aresult_onlyconversation is whystateisunavailablewithreason: "conversation_not_restorable". It comes from a capture that failed or exceeded its size cap, not from which runtime ran the session. Every runtime, includingclaude-codeandcodex, checkpoints its conversation and normally reportsexact.workspace.fidelity—live,portable_tree, ornone.portable_treemeans a captured copy exists; it comes back only whenstateisrestorable. Restored sessions run on the current managed environment. Cloud does not pin a historical image. A storedwamp.workspace.v1checkpoint remains readable but cannot be restored; if no other boundary can be restored, continuation reportsworkspace_not_restorable.
reason has five values, and which canContinue each one arrives with is part
of the contract:
reason |
canContinue |
Why the work will not carry |
|---|---|---|
| absent | true |
It will carry. This is the only shape that means “resuming here keeps what happened”. |
no_prior_work |
true |
Nothing has run yet, so there is nothing to carry. |
checkpoint_unavailable |
false |
Work ran and no sealed checkpoint is available to restore from. |
conversation_not_restorable |
false |
A checkpoint exists, but its conversation was not captured exactly — a capture that failed or exceeded its size cap. |
workspace_not_restorable |
false |
A checkpoint exists, but its working tree cannot be restored into this session — not captured as a portable tree, or captured against a different repository base. |
session_archived |
false |
The session is readable but closed to commands. It keeps whatever fidelities it had; it will never admit another turn. |
boundaryRunId, when present, is the run at which the recoverable history ends;
it is the honest dividing line to draw in a UI between “the agent remembers
this” and “it does not”.
Capture is bounded. GET /v1/capabilities advertises the ceilings —
artifacts.conversationCheckpointMaxBytes (256 MiB) and
artifacts.workspaceCheckpointMaxBytes (256 MiB, also reported as
environments[0].workspace.maxCheckpointBytes). A working tree larger than that
cannot be captured, which is one way a session ends up unavailable with
reason: "workspace_not_restorable". Read the numbers from discovery rather than
hardcoding them.
When compute disappears mid-run
Section titled “When compute disappears mid-run”Losing a sandbox during a run is a designed-for case, not an outage:
- The run reaches a terminal state — typically
crashed, and its event iswamp.run.crashed. - Any open interaction expires with
wamp.interaction.expiredandreason: "workspace_lost". It can no longer be answered; submit a fresh turn instead. - Events already recorded stay recorded. The log is append-only and gap-free across sandbox boundaries — sequence numbers belong to the session.
- No checkpoint is fabricated for the interrupted prompt. Conversation,
workspace and native ACP state resume from
boundaryRunId, the latest completed checkpoint; changes made after that boundary may have died with the sandbox. - Artifacts already registered keep their manifests. Whether their bytes are still fetchable is a separate question, answered in Artifacts.
continuationis recomputed. Read it before deciding whether to retry the same session or open a new one.
The errors that mean “compute, not you” are worth handling explicitly:
| Status | Code | What to do |
|---|---|---|
503 |
cloud_workspace_starting |
Retry, honoring Retry-After: 2. A sandbox is coming up. |
503 |
cloud_workspace_unavailable |
No sandbox could be provided. Retry with backoff; surface it if it persists. |
409 |
cloud_workspace_expired |
The lease you were counting on is gone. Re-read the session; if continuation.canContinue is true, PUT /v1/sessions/{sessionId}/workspace brings a workspace back and the call can be retried. |
409 |
cloud_workspace_expiring |
The lease is too close to its deadline for the requested work. Retry; a fresh lease follows. |
409 |
cloud_resume_unavailable |
The previous state cannot be restored. Start a new session for a clean run. |
409 |
cloud_turn_execution_lost |
The turn’s execution died with its sandbox. Submit a new turn. |
Note the asymmetry in how the retry hint arrives: for these codes
Retry-After is a header only, while a 429 rate-limit response carries
retryAfterSeconds in the body as well. Read the header.
Environments are discovery-only
Section titled “Environments are discovery-only”GET /v1/capabilities reports an environments array of exactly one
element — the contract bounds it to one (minItems: 1, maxItems: 1) and the
SDK types it as a one-element tuple, so environments[0] is not a convenience,
it is the whole array. It carries the machine-readable fact — whether the
organization has anywhere to run — and a label describing whether that capacity
is hosted by WAMP or supplied by the organization:
{ "capabilities": { "environments": [ { "id": "managed", "label": "WAMP managed sandbox", "availability": { "state": "available" }, "default": true, "runtimes": ["wamp", "claude-code"], "workspace": { "checkpoint": "portable_tree", "maxCheckpointBytes": 268435456 } } ] }}availability.state is the field to branch on, in the same shape runtimes[]
uses: available when the organization has compute it may run on, and
not_configured when it has none — no compute of its own, and no shared
capacity on this deployment. not_configured is terminal until an administrator
acts, and it is not enforced at request time: a session and a turn are both
admitted, and the run goes terminal with lastError.code: compute_not_configured. Read it before you create a session.
The managed entry describes the sandbox platform and remains the only entry
in GET /v1/capabilities.environments. An organization can also store
revisioned environment objects with
/v1/environments and select one when creating a Session; every fresh sandbox
of that Session applies its setup, variables and secrets before the first turn. Read
environments[0] for the sandbox platform’s compute availability. Its label
is resolved per request from where your
organization is actually placed, and it is one of:
| Label | availability.state |
What it means |
|---|---|---|
WAMP managed sandbox |
available |
Capacity WAMP owns and operates |
Organization self-hosted runner |
available |
Your organization’s own hardware |
Organization-owned managed sandbox |
available |
Capacity your organization owns at a hosted provider |
No compute configured |
not_configured |
Your organization has nowhere to run yet, and every run fails at placement until an administrator adds compute |
Which label you see is an administrator’s decision, not an API call. In the WAMP Cloud web app it is made under Runners in Settings: that is where an organization sees where it is placed, and where it adds capacity of its own. Adding a machine is one command on a Linux host with Docker, and that host needs outbound HTTPS and nothing inbound — no port to open, no address to hand over, no certificate. Sessions then run there, including the byte stream between the control plane and the agent, and this API does not change shape: the same requests, the same events, a different label.
A machine an organization runs itself holds the sandbox image it was installed
with, and a Cloud release asks for the image that release was built with. A
machine that has not been updated is reachable, healthy, and unable to serve the
new release. A Turn on it is still admitted — the Session and the Turn both
answer normally — and its Run fails immediately instead of retrying, with
lastError.code: "compute_outdated" and a wamp.run.failed event carrying the
same code, because only an administrator updating that machine can clear it.
PUT /v1/sessions/{sessionId}/workspace is the call that refuses synchronously,
with 409 cloud_compute_outdated.
Clearing it is work on the machine, not a button in the web app — Runners
reports that a machine needs updating and can drain or remove it, but the update
runs on the host. It is the same install command re-run with the new release’s
image archive; the machine’s identity and enrollment are generated once and
survive it. A machine that was enrolled with the release-signing key pinned has a
second option: one wamp-runner-update on the host, which fetches and verifies
the release itself and needs no archive at all. Either way your integration does
nothing but wait: compute_outdated is not retried automatically, and
it clears the moment the machine serves the release’s image.
The control URL, pool identity, provider, and image behind any of them are never
exposed. Read environments[0] for its workspace ceilings and for the
runtimes it lists as usable, and do not build a picker for a bounded list of
one.
You also do not choose where in the sandbox your work happens. If the session
has a repository source, the checkout is placed for you and the agent starts
there; if it has none, the agent gets an empty workspace. Paths are not part of
the contract.
Reading it before you act
Section titled “Reading it before you act”One call answers both questions — “can I send another turn” and “will the agent remember” — and they are two different fields:
curl -sS "$WAMP_API/v1/sessions/$SESSION_ID" \ -H "Authorization: Bearer $WAMP_TOKEN" \ | jq '{ workspace: .session.workspace, canContinue: .session.continuation.canContinue, carriesPreviousWork: (.session.continuation.reason == null), reason: .session.continuation.reason, state: .session.continuation.state, conversation: .session.continuation.conversation.fidelity, files: .session.continuation.workspace.fidelity, busy: (.session.activeRun != null) }'A reasonable policy in a backend, branching on reason rather than on
canContinue:
busy— wait. One run at a time per session.reasonabsent — submit the turn. The agent picks up where it left off, and provisioning is the server’s problem.no_prior_work— submit the turn, and put the whole brief in it. This session has run nothing, so nothing is implied by its history.- any other
reason—canContinueis false. Open a new session and, if the previous one produced a repository change, say so in the new initial Turn rather than hoping the agent recalls it.
Related
Section titled “Related”- Sessions, turns, runs — the objects that survive a sandbox.
- Artifacts — what stays fetchable after compute is gone.
- Follow a run live — watching work that may span leases.
- Sessions operations — the generated
reference for the resource carrying
workspaceandcontinuation, and the API reference for both objects field by field.