Skip to content

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.

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, or none. exact means the agent resumes with the same history. result_only means the conversation could not be captured — the run’s outcome and artifacts survived, the history did not — and it is never restorable: a result_only conversation is why state is unavailable with reason: "conversation_not_restorable". It comes from a capture that failed or exceeded its size cap, not from which runtime ran the session. Every runtime, including claude-code and codex, checkpoints its conversation and normally reports exact.
  • workspace.fidelity — live, portable_tree, or none. portable_tree means a captured copy exists; it comes back only when state is restorable. Restored sessions run on the current managed environment. Cloud does not pin a historical image. A stored wamp.workspace.v1 checkpoint remains readable but cannot be restored; if no other boundary can be restored, continuation reports workspace_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.

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 is wamp.run.crashed.
  • Any open interaction expires with wamp.interaction.expired and reason: "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.
  • continuation is 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.

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.

One call answers both questions — “can I send another turn” and “will the agent remember” — and they are two different fields:

Terminal window
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.
  • reason absent — 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 — canContinue is 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.