# Sandboxes and environments Compute in WAMP Cloud is not a resource you manage — it is a three-state projection on the session, and this page explains how to read it and what survives when it expires. Source: https://docs.cloud.vampikez.fun/concepts/sandboxes/ 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 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: ```json { "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 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: ```json { "canContinue": true, "state": "fresh", "conversation": { "fidelity": "none" }, "workspace": { "fidelity": "none" }, "reason": "no_prior_work" } ``` ## 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: ```json { "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: ```js 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. ## 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 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](/concepts/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. ## 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: ```json { "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](/concepts/environments/) 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 One call answers both questions — "can I send another turn" and "will the agent remember" — and they are two different fields: ```bash 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. :::caution[Do not poll for a warm sandbox] There is no way to pre-warm compute from this API, and polling the session until `workspace.state` becomes `attached` before submitting a turn inverts the design: the turn is what causes provisioning. Submit, then follow the event log. ::: ## Related - [Sessions, turns, runs](/concepts/sessions-turns-runs/) — the objects that survive a sandbox. - [Artifacts](/concepts/artifacts/) — what stays fetchable after compute is gone. - [Follow a run live](/guides/follow-a-run/) — watching work that may span leases. - [Sessions operations](/api/operations/tags/sessions/) — the generated reference for the resource carrying `workspace` and `continuation`, and [the API reference](/api/) for both objects field by field.