Lifecycle

Open, suspend, resume, fork, reset and delete Shardflux workspaces: states, idle suspend and parking, waiting, and what survives each transition.

States and operations

A workspace's state (the API's observed_state) is one of:

State Meaning
creating, starting The first open or a restart is booting it.
running It runs, and tool calls are served.
suspending Its memory, processes and disk are being checkpointed. Tool calls wait.
suspended The checkpoint is sealed and the VM is gone. No compute; the disk still counts as storage. The operation's result.durable says when the checkpoint is also in durable storage (Durable storage).
resuming The checkpoint is being restored.
forking A fork's copy is being created (the new workspace's state).
failed A start failed. The workspace's failure says why: the error of the operation that failed it (code, message, retryable, details). Opening the key again starts it again.
deleting, deleted It is being or has been deleted.

A running workspace that nobody uses is parked by its host. That is not a state: it stays running. A file-first workspace is running from its first open until it is deleted.

Every change of state is an operation with a kind (open, suspend, resume, fork, snapshot, reset, delete) and a state of its own: queued, capacity_pending, running, then succeeded, failed or canceled. A workspace runs one lifecycle operation at a time; a second one is refused with 409 conflict (details.reason: operation_in_progress), except that opening or resuming joins a start already in progress.

Shardflux also keeps every workspace well placed on its own: it moves a running workspace to another host while it keeps running (an operation of kind move), and applies upgrades in the background. This work leaves the workspace as it is. Its state, pending_reason and startup stay unchanged, and the work shows only as the workspace's active_operation, with that operation's own state. Operations you request are accepted while it is active and go first, unless a move is already finishing. A workspace that placement restarts instead of moving shows an upgrade operation with input.reason: host_drain_cold_start: it restarts only when no agent turn, command or tool call is in progress, its files and home stay on its disk, and memory and running processes start fresh, as after a reboot.

Requested or finished

Lifecycle calls return as soon as the change is requested, or, with wait, once it has finished:

Call Returns when Returns
await workspace.suspend() The suspend is requested (usually queued; the workspace still runs) The operation
await workspace.suspend({ wait: true }) The suspend has finished; workspace.state is suspended The succeeded operation
ws.suspend() (Python) The suspend is requested The operation
ws.suspend(wait=True) (Python) The suspend has finished; ws.state is suspended The succeeded operation
shard ws suspend <key> The suspend is requested Prints the operation
shard ws suspend <key> --wait The suspend has finished Prints the operation
await workspace.suspend({ durable: true }) (0.12.0+) The suspend has finished and its checkpoint is in durable storage The succeeded operation, result.durable: true
ws.suspend(durable=True) (Python 0.8.0+) The same The succeeded operation
shard ws suspend <key> --durable (CLI 0.7.0+) The same Prints the operation

The same applies to resume, snapshot, delete, reset and close. open() waits by default. fork waits by default in Python and takes { wait: true } in TypeScript.

Open, create or reconnect

open is the one call you need for most of the lifecycle. It creates the workspace for a new key, returns a running one, resumes a suspended one, and joins a start already in progress. It never resets anything. See What opening a key does.

Suspend a workspace

TypeScript
await workspace.suspend({ wait: true });   // memory and processes are checkpointed; compute stops
Python
ws.suspend(wait=True)

A suspend pauses the VM, seals its memory and disk on its host, and releases the CPU and memory. It returns as soon as the workspace is sealed, typically in a few hundred milliseconds, and the copy to durable storage follows right away, typically within a few seconds. See Durable storage.

Durable storage

The finished suspend operation says where its checkpoint is:

JSON
{
  "durable": false,
  "suspend_path": "local_commit",
  "durability": {
    "state": "pending",
    "checkpoint_id": "01a0f11c-3f30-72d5-9311-02d770904b92",
    "generation_id": "g-0001-4dfc6088",
    "local_commit_at": "2026-09-30T06:59:15.384696Z",
    "durable_by": "2026-09-30T07:00:15.384696Z"
  }
}

When the copy commits, the same operation's result becomes durable: true with durability.state: "durable", durable_at and local_commit_to_durable_ms. Measured from the suspend's return to the durable commit (p50): 435 ms for a 1 GiB workspace, 864 ms for 8 GiB.

durability field Meaning
state pending (the copy is being written), durable (it committed) or lost (see below).
checkpoint_id, generation_id The checkpoint being copied.
local_commit_at When the suspend completed on its host.
durable_by When the copy is expected by.
durable_at, local_commit_to_durable_ms When the copy committed, and how long after the suspend.
overdue_at Set once durable_by passed with the copy still pending. The copy keeps going; nothing changes for your code.

While the copy is pending, the workspace behaves as suspended in every way. A resume, a tool call that wakes it, or an open of its key restores it from its host's copy (resume_path: "local_cache", with durability.state: "pending" in the resume's result), and reads of its files are answered from that copy. A resume in the first moments after the suspend, while its memory is still being written, continues the same VM in place (resume_path: "thaw"). Other lifecycle operations on it, such as a snapshot, a fork or the next suspend, start once the copy commits.

A suspend whose result says durable: true, or has no durable field, was stored durably before it completed. Most code needs nothing more. When yours must know the checkpoint is durable, for example before a job reports that its state is saved, wait for it: suspend({ durable: true }) or cloud.workspaces.waitForDurable(operation) (TypeScript 0.12.0+), suspend(durable=True) or wait_for_durable(operation) (Python 0.8.0+), shard ws suspend <key> --durable (CLI 0.7.0+) or workspace_suspend with durable: true (MCP server 0.6.0+).

A fork of a running workspace carries durable and durability the same way: the new workspace starts from the source's sealed capture on the same host, and the capture's durable copy follows.

If the host is lost before the copy commits. The pending checkpoint's durability.state becomes lost (with reason, for example host_gone, and recovery_point_checkpoint_id), and the workspace's recovery point stays its previous durable checkpoint. The next resume restores that checkpoint, and its result says so in lost_suspend:

lost_suspend field Meaning
checkpoint_id, generation_id The suspend that was not kept.
reason Why, for example host_gone.
suspended_at When that suspend captured the workspace.
restored_checkpoint_id The earlier checkpoint this resume restored.
state_as_of The restored state is as of this time.

With no earlier durable checkpoint, the resume fails with checkpoint_not_found. A wait for the durable copy ends with DurabilityLostError in both SDKs (Errors).

Session workspaces cannot be suspended (409, details.reason: session_lifetime).

Resume a workspace

You rarely need to resume explicitly:

  • Opening the key resumes a suspended workspace.
  • A tool call wakes it. An exec, file or other tool call on a suspended workspace resumes it (or joins the resume already running), then runs. A call made during a suspend or resume waits for it to finish. The call never runs twice. The SDKs bound the wait per call (default 120 seconds, transitionTimeoutMs in TypeScript, transition_timeout in Python) and follow a resume queued for capacity until its deadline (TypeScript 0.22.0+, Python 0.18.0+); the CLI uses --wake-timeout (0.16.0+: without it, the same) and the MCP server SHARDFLUX_WAKE_TIMEOUT_MS.
  • The wake is one request (@shardflux/sdk 0.9.0+, shardflux 0.5.0+ for Python, CLI 0.5.0+, MCP server 0.4.0+). The client sends one resume that the API holds until the workspace runs, and the answer carries a tool token for that client, so the refused call is retried at once. An explicit resume({ wait: true }), resume(wait=True) or shard ws resume --wait is the same single request. Earlier versions wait for the resume operation and then fetch a token.
  • Reads do not wake it. Reading, listing, stating and searching files of a suspended workspace are answered from its disk while a host still holds it, without a resume (@shardflux/sdk 0.9.0+, shardflux 0.5.0+, CLI 0.5.0+, MCP server 0.4.0+). See Search and edit files.
  • Following an exec's output never wakes a workspace, so a suspend you asked for is respected.
TypeScript
await workspace.resume({ wait: true });           // explicit
await workspace.wake();                           // resume if needed; resolves once it runs
const cell = workspace.cell({ wake: null });      // opt out: calls fail with workspace_not_running instead
Python
ws.resume(wait=True)
ws.wake()                   # True if it resumed or waited, False if it was already running
cell = ws.cell(wake=None)   # opt out

A resume is admitted like a new start, so plan limits apply (see Pricing and limits).

Fork a workspace

A fork creates a new workspace under a new key from the current state of a running or suspended one: a copy of its disk and memory, restored into an independent VM with its own network identity. The source keeps running, or stays suspended.

TypeScript
const { workspace: copy } = await workspace.fork({ key: 'customer-42/experiment' }, { wait: true });
Python
copy = ws.fork("customer-42/experiment")   # waits until the copy runs
Shell
shard ws fork customer-42/main customer-42/experiment --wait
  • The new key must be unused, including by deleted workspaces (409, details.reason: key_in_use).
  • The fork keeps the source's template version, secret bindings, text inputs and egress policy override, so it starts under the same outbound rules as its source. It gets the source's caps unless you pass caps.
  • A fork is persistent unless you pass lifetime: 'session'. Forking is how you keep a session workspace's state.
  • A fork is a new start: plan limits apply to it as to an open.
  • Software activation can depend on the VM's context. A Unity license accepted on the source is not proof that the fork's Editor is activated: complete the activation in the fork before Editor, rendering or gameplay checks.

Snapshot a workspace

workspace.snapshot({ label }) (TypeScript), ws.snapshot(label=...) (Python) records a checkpoint of a running or suspended workspace without stopping it. While a running workspace is captured, reads work and writes wait. The API has no call to list or restore snapshots yet; to branch from a state, fork.

Reset a workspace

reset wipes every change a workspace made and restarts it on its template version. It keeps the key, id, template version, caps, secret bindings and volume attachments. Running processes end. It works only on layered workspaces (workspace.diskLayout === 'layered'; others get 409, details.reason: legacy_disk_layout). The previous state is kept as a recovery point for 7 days.

TypeScript
await workspace.reset({ wait: true });
Shell
shard ws reset customer-42/main --yes --wait

A suspended workspace stays suspended and starts blank on its next resume.

Delete a workspace

TypeScript
await workspace.delete({ wait: true });
Shell
shard ws delete customer-42/main --yes

Tool access ends at once and the workspace's storage is cleaned up. Deletion cannot be undone, and a persistent workspace's key becomes reusable once deletion finishes, creating a new workspace ID. The MCP server does not offer deletion to agents; use the SDK, the CLI or the console.

A session workspace ends with close() (shard ws close <key>), which deletes it; its key then opens a new workspace.

Keep only what you use

Keep a workspace per customer, chat or task and let retention remove the idle ones automatically. On open, pass retention: { delete_after_idle_days: 14 } for a persistent workspace. Set a new policy with ws.setRetention({ delete_after_idle_days: 30 }), or ws.setRetention(null) to follow the project default. cloud.workspace(key, { template, retention }) accepts the same policy. In Python, use retention={"delete_after_idle_days": 14} and ws.set_retention(...). The CLI has shard ws open KEY --template TEMPLATE --delete-after-idle 14d and shard ws retention KEY 14d|off.

A project default can target workspaces by their labels: cloud.projects.setRetention(projectId, { delete_after_idle_days: 14, labels: { app: 'chat' } }). All selector labels match exactly, and a workspace's own policy wins. cloud.projects.getRetention(projectId) reads the default; setting it to null clears it. The workspace's retention shows its effective days and projected delete_at, or null with no policy. Calls, resumes, file operations and port requests refresh activity; ongoing work postpones deletion. Retention accepts 1 through 3650 days. Omit it to keep your workspaces until you delete them. Deletion frees their storage through normal cleanup, and a deleted key creates a new workspace ID.

Automatic suspend when idle

A persistent workspace suspends itself when it has been idle for its idle timeout. Nothing is killed: the suspend keeps memory and processes like any other. Once its host parks an idle workspace, it no longer counts toward workspaces working at once and its RAM GiB-hours drop to 256 MiB and then to none, but a workspace that its processes keep busy keeps working, and uses the memory it holds, until it suspends. The shorter the idle tail after its last work, the less a busy workspace costs.

What keeps a workspace awake:

  • a tool call (exec, files, terminal input, processes, git, browser);
  • an attached exec output stream or terminal (a terminal while someone types or it prints);
  • a command started through exec, until it ends or its timeout passes (1 hour when it has none);
  • a keepalive (POST /v1/workspaces/{id}/keepalive on the workspace's cell endpoint, for up to 12 hours);
  • a request to a port URL, until its response ends, and an open WebSocket on one. The idle timeout counts from the end of the last request. A request postpones a suspend when idle request and never cancels it.

Start a long job with a timeout_ms (timeoutMs in TypeScript) that covers its whole run, and it keeps the workspace awake until it finishes.

A detached process, a dev server nobody is attached to, or CPU use alone does not keep it awake. Such processes continue after the resume.

Idle policy. The default policy is adaptive: each workspace learns its own timeout, from 10 seconds to 4 hours.

  • It learns from the workspace's idle periods, every pause of 5 seconds or more between one activity and the next. Pauses inside its usual active hours and outside them are learned separately.
  • Short pauses, such as an agent thinking between tool calls, are not worth a suspend and a resume, so the timeout outlasts them. When a workspace is woken soon after an automatic suspend, its next timeouts get longer (a suspend you asked for never has this effect).
  • Until the workspace has 8 idle periods of its own, its history is blended with that of its template, else its organization, else all workspaces. With no history at all, the timeout is 5 minutes.
  • The work signals above always win: the policy only decides how long an idle workspace waits.
  • When its host needs room to wake another workspace, an idle workspace under the adaptive policy may be suspended sooner (its suspend operation has input.reason: host_capacity); this does not shorten its learned timeout. A workspace with never, or a fixed timeout not reached yet, may be moved to another host instead, and keeps running.

The other policies are never and fixed:<seconds> (60 to 604800). The workspace view reports under idle the policy, the current timeout (timeout_seconds), what it is based on (basis, for example learned from 37 idle periods (active-hours), template prior or default), when it would suspend (next_eligible_at) and a pending suspend when idle request (suspend_request).

Set it with the HTTP API; the SDKs have no dedicated method yet, so use their request() passthrough:

TypeScript
await cloud.request('PUT', `/v1/workspaces/${workspace.id}/idle-policy`, { json: { idle_policy: 'fixed:600' } });
Python
sf.request("PUT", f"/v1/workspaces/{ws.id}/idle-policy", json={"idle_policy": "never"})

null clears the workspace's own policy, so the template default (else adaptive) applies. Session workspaces have no idle policy; they end after their idle timeout (10 minutes unless the template sets one).

Suspend when idle

Send hint() at turn start without awaiting it, while the model thinks; request suspendWhenIdle({ afterSeconds: 0 }) at turn end. 0 frees RAM about 2 seconds after the turn; the next turn resumes, hidden while the model thinks. 60 keeps the workspace warm for a minute and bills that minute. TypeScript 0.16.0+ wraps both calls in ref.turn(async (ref) => { ... }) (also workspace.turn()); Python 0.12.0+ uses with ref.turn(): ... or async with ref.turn(): .... Both schedule suspension when the body settles, including on error.

Your code knows when an agent's turn ends. Instead of waiting for the idle timeout, ask for a suspend once the workspace has been idle for a short time. The request is stored with the workspace, so your process does not have to stay around for the suspend.

TypeScript
await workspace.suspendWhenIdle({ afterSeconds: 0 });   // 0 to 3600
workspace.suspendRequest;                                // { requested_at, after_seconds, not_before }, or null
await workspace.cancelSuspendWhenIdle();                 // idempotent
Python
ws.suspend_when_idle(after_seconds=0)  # 0 to 3600
ws.suspend_request                      # SuspendRequest(requested_at, after_seconds, not_before), or None
ws.cancel_suspend_when_idle()           # idempotent
Shell
shard ws suspend customer-42/main --when-idle 1m      # 0 to 1h
shard ws suspend customer-42/main --cancel-when-idle
  • The workspace is suspended once it has been idle for after_seconds, counted from the later of its last work and the request. not_before is the earliest suspend.
  • after_seconds: 0 suspends it as soon as it is idle, a few seconds after its last work (--when-idle 0 needs @shardflux/cli 0.6.1+, after_seconds: 0 in workspace_suspend the MCP server 0.5.1+).
  • A command still running, an attached exec output stream or terminal (a terminal while someone types or it prints), or a keepalive postpones the suspend until after_seconds after it ends. The request stays.
  • The next tool call on the workspace (the next turn) or a resume cancels the request. Asking again replaces it.
  • It applies under every idle policy, never included, and never delays a suspend the policy would do sooner.
  • If a suspend is already in progress, the call returns that operation and records nothing.
  • When the suspend is due, the host checks atomically that the workspace is still idle. If a tool call arrived after the request or a stream is attached, the suspend operation ends canceled with error.code: "workspace_active": nothing changed and the workspace keeps running. It is not a failure; the SDKs' waitUntilReady() (TypeScript 0.12.0+) and wait_until_ready() (Python 0.8.0+) and tool-call wakes treat it as running.
  • A session workspace is refused (409, details.reason: session_lifetime), and so is a workspace that is not running (not_running).

It is in @shardflux/sdk 0.10.0+ (cloud.workspaces.suspendWhenIdle(id, { afterSeconds }) by id), shardflux 0.6.0+ on PyPI (sf.workspaces.suspend_when_idle(workspace_id, after_seconds=0)), @shardflux/cli 0.5.1+ and the MCP server 0.4.1+ (workspace_suspend with after_seconds). Over HTTP it is POST /v1/workspaces/{id}/suspend-when-idle with {"after_seconds": 0}, and DELETE on the same path cancels it. The workspace view shows a pending request as idle.suspend_request. Give your agent workspace tools shows the call at the end of an agent loop.

Idle running workspaces are parked

Long before its idle timeout, a running workspace that nobody is using is parked by its host: after a few seconds without activity its VM is paused and most of its memory is compressed in place, in the host's memory. Parking is not a lifecycle state and needs nothing from you. The workspace stays running, keeps its files, memory and processes, and the API answers as before. The next tool call wakes it first and then runs, a few milliseconds later.

  • What keeps it resident: a tool call in progress, an attached exec output stream or terminal (a terminal while someone types or it prints), a command started through exec until it ends, a keepalive, a request to a port URL in progress or an open WebSocket on one, and processes that keep using CPU or the network.
  • Background processes that wait (an idle dev server, a file watcher, a shell) are paused with the workspace and continue at the next wake. Timers inside the workspace fire late, never early, and the clock is right after the wake.
  • Requests to a port URL wake it. A request to a port URL wakes a parked workspace inside the request, and resumes a suspended one, then reaches your server. Other network traffic does not: a process that waits for data from a connection it opened is paused too, and a remote peer that expects a prompt answer may time out. A keepalive (POST /v1/workspaces/{id}/keepalive on the cell endpoint) keeps the workspace resident, and wakes it if it is parked.
  • Billing follows parking. A parked workspace is running, and it does not count toward workspaces working at once: only working workspaces do. Its RAM GiB-hours follow what the host holds: the memory it holds while it works (its floor or what its processes use, whichever is more), and while it is parked at most 256 MiB plus the compressed size of the rest (what each allowance measures). CPU hours count the CPU time its processes use, which is next to none while it is parked. A workspace whose processes keep using CPU or the network stays resident and uses the memory it holds until it suspends. Parking neither delays nor replaces the automatic suspend.
  • Reads may not wake it. Reading, listing, stating and searching files of a parked workspace can be answered from its disk (X-Served-From: disk) without waking it.
  • A wake can wait for room. When the host needs room to restore the workspace, it makes it: idle workspaces on that host are suspended or moved to another host, and more capacity is added when that is not enough. The tool call waits meanwhile, in line with other wakes, for 25 to 50 seconds depending on the call (limits). One still waiting then is answered 503 service_unavailable (details.reason: host_capacity) with Retry-After; when a restore fails, the answer is wake_failed (the saved state is intact). Both are retryable, and nothing was executed. The SDKs retry host_capacity for any call, commands and terminals included, for up to 2 minutes (admissionTimeoutMs, admission_timeout; TypeScript 0.22.0+, Python 0.18.0+), and wake_failed for reads, searches and calls with an Idempotency-Key. When your organization already has its plan's workspaces working at once, the call waits for one to stop working and may be refused with 429 quota_exceeded, which the SDKs retry the same way.

Wake hint

A wake hint tells the host that a tool call is coming, so a parked workspace starts waking before the call arrives. The agent tools of both SDKs (TypeScript 0.9.0+, Python 0.6.0+) and the MCP server (0.4.0+) send one when each tool call starts, so an agent loop built on them needs no extra code. To start the wake even earlier, send it yourself as soon as your model starts writing a tool call:

TypeScript
void workspace.hint().catch(() => {});   // @shardflux/sdk 0.9.0+: returns at once
Python
ws.hint()  # shardflux 0.5.0+: returns at once
  • A hint is cheap and never waits. For a suspended workspace it starts the resume in the background (TypeScript result.wake, a promise; Python WakeHint.wake, a Future); hint({ wake: null }) / hint(wake=False) only reports.
  • The agent tools send no hint for read_file, list_files and search_files, which a sleeping workspace answers from its disk. workspaceTools(ws, { hint: false }) / workspace_tools(ws, hint=False) turns their hint off, for example when you send your own.
  • Over HTTP, POST /v1/workspaces/{id}/wake-hint on the cell endpoint answers 202 with the residency the host found: resident, frozen (paused), hibernated (saved to the host's disk) or restoring. A suspended workspace answers 409 workspace_not_running: resume it through the API. A hint that no tool call follows within 60 seconds is dropped. A hint is not tool activity and does not keep the workspace from its idle suspend.

What survives each transition

Files and disk, and memory and running processes, are listed separately.

Transition Files and disk Memory and running processes Also
Open the same key again Kept Kept Nothing is reset.
Parked while idle, then woken by a tool call Kept Kept: processes are paused and continue The workspace stays running; while it is parked it does not count toward workspaces working at once. RAM GiB-hours: at most 256 MiB plus the compressed rest while parked. Timers fire late.
Suspend (by you, when idle, or at a compute allowance or spend cap), then resume Kept: every file write acknowledged before the suspend began, and installed packages. Immutable paths show the template's newest version Kept: every process with its PID, memory, open files, working directory and environment, including detached processes, dev servers and exec or terminal sessions; listening sockets and loopback connections Your connections to exec output and terminals end; reattach by offset (the SDK does). Remote peers may close connections while the workspace sleeps. GPU apps keep their Vulkan state; CUDA and NVENC objects made before the suspend are released: calls on them return CUDA_ERROR_CONTEXT_IS_DESTROYED or NV_ENC_ERR_DEVICE_NOT_EXIST, and the app creates new ones.
Resume after a platform runtime change (rare, see below) Kept: as of the suspend Ended: the workspace boots from its saved disk, as after a reboot The resume result says resume_path: "cold_boot", memory_restored: false, cold_boot_reason: "runtime_changed". Start dev servers and background jobs again.
The workspace's host fails, then its next use Kept: as on its disk when the host failed Ended: the workspace boots from its disk, as after a reboot It is suspended until its next tool call, open or resume. The result says cold_boot_reason: "host_lost" (see below).
Fork Copied into the new workspace Copied into the new workspace, as an independent VM with a new network identity The source is unchanged. Shared volumes are attached, not copied: both see the same data. CUDA, NVDEC and NVENC objects are released in both, as at a suspend.
Snapshot Unchanged Unchanged Writes wait during the capture.
Reset (layered workspaces) Wiped back to the template Ended Key, id, template version, caps, secrets and volumes are kept; the old state is a recovery point for 7 days.
Session ends (close() or idle timeout) Deleted Ended The key opens a new, empty workspace.
Delete Deleted Ended The key can open a new workspace ID after deletion finishes.
A start fails with capacity_unavailable Unchanged Unchanged Nothing was started; a suspended workspace stays suspended.

A resume restores memory and disk from one checkpoint. Shared volumes are outside checkpoints: a suspend keeps the attachment, and the resume mounts the volume's current contents.

Immutable paths

A template's immutable paths show its newest published version, and they move to a newer one only at a start or a resume:

Transition Immutable paths show
A boot: the first open, a reset, a resume that boots The template's newest published version
Resume from suspend The template's newest published version. A resume never waits for an update: while the newest version is still being prepared for that workspace, it resumes on the version it shows and moves up at a later resume.
Running, parked and woken by a tool call Unchanged, for as long as the workspace runs
Fork The source's version; the fork moves up at its own next resume
Draft and test instance The draft's or version's own files, writable

A process that has a file open when the version changes keeps reading the version it opened; files it opens afterwards, and new processes, see the new version. Everything else on the disk is kept as the table above says. workspace.template.immutable_version is the version a workspace shows.

When a resume boots instead

A memory checkpoint restores on the VM runtime that saved it (kernel, CPU family and Firecracker snapshot format), and we keep that runtime stable so suspended workspaces resume with their memory. After a runtime change, such as a security update, a workspace's next resume boots it from its saved disk instead:

  • its files are exactly as they were at the suspend;
  • its memory and processes are not restored: every process starts fresh, as after a reboot, and the template's startup runs its boot steps;
  • the resume operation's result says so: resume_path: "cold_boot", memory_restored: false and cold_boot_reason: "runtime_changed". Every resume result has memory_restored (true when memory came back).

A workspace whose host failed boots from its disk the same way, with cold_boot_reason: "host_lost" (If a workspace's host fails).

The clients surface it: workspace.lastTiming.server.memoryRestored and coldBootReason in TypeScript (@shardflux/sdk 0.11.0+), memory_restored and cold_boot_reason in Python (shardflux 0.7.0+), a note on stderr from the CLI (0.6.0+) and a notice the MCP server (0.5.0+) hands the agent, so it can restart what it was running.

If a workspace's host fails

If the host a workspace runs on fails or is replaced, the workspace becomes suspended. Its next tool call, an open of its key or a resume brings it back, exactly as for any suspended workspace: there is nothing to call.

  • It boots from its disk. Its files are kept as they were on its disk; running processes and memory are not, so every process starts fresh and the template's boot start commands and services run, as after a reboot. The result says resume_path: "cold_boot", memory_restored: false, cold_boot_reason: "host_lost" and host_lost: {detected_at, restored_from: "disk"}.
  • If its disk cannot be recovered, it is restored from its newest checkpoint instead: host_lost says restored_from: "checkpoint" with restored_checkpoint_id and state_as_of. Changes made after state_as_of are not in it. With neither a disk nor a checkpoint, the resume fails with workspace_storage_unavailable (Errors).
  • A suspend completes. A suspend that meets the failed host succeeds with the workspace suspended, and its result has host_lost: {detected_at}.
  • Fork and snapshot follow the resume. Until the workspace has run again, a fork of it or a snapshot is refused with resume_required. Resume it (or open its key) first, so the copy has its newest files.

What to do: nothing. Start again what you ran by hand, such as dev servers and background jobs; the template's start commands and services run by themselves. The clients surface host_lost: hostLostOf(op) and workspace.lastTiming.server.hostLost in TypeScript (@shardflux/sdk 0.13.1+), host_lost_of(op) and ws.last_timing.server.host_lost in Python (shardflux 0.9.1+), a note on stderr from the CLI (0.8.1+) and a notice from the MCP server (0.7.1+).

Wait for an operation to finish

wait takes options: { timeoutMs, signal, onProgress } in TypeScript (default 5 minutes), timeout= in seconds in Python (default 300). When the time runs out, the SDK throws OperationTimeoutError, but the operation continues on the server. A start that is queued for capacity (capacity_pending) when the time runs out is followed until its deadline instead, so it is seen through to the end (TypeScript 0.22.0+, Python 0.18.0+; untilCapacityDeadline: false or until_capacity_deadline=False stops at the timeout). OperationTimeoutError then carries the start's queuePosition / queue_position. Wait for it again:

TypeScript
import { OperationFailedError, OperationTimeoutError } from '@shardflux/sdk';

try {
  await workspace.resume({ wait: { timeoutMs: 60_000 } });
} catch (err) {
  if (err instanceof OperationTimeoutError) {
    await cloud.workspaces.waitForOperation(err.operationId);          // keep waiting
  } else if (err instanceof OperationFailedError && err.retryable) {
    // err.errorCode === 'capacity_unavailable': nothing changed; retry.
  } else {
    throw err;
  }
}
Python
from shardflux import OperationFailedError, OperationTimeoutError

try:
    ws.resume(wait=True, timeout=60)
except OperationTimeoutError as err:
    sf.workspaces.wait_for_operation(err.operation_id)
except OperationFailedError as err:
    if not err.retryable:
        raise
    # err.error_code == "capacity_unavailable": nothing changed; retry.

From the command line: shard operations wait <operation id>.

A queued open, resume or fork is capacity_pending, in line (error.details.queue_position, served in the order the starts were requested) with a deadline (deadline_at) 15 minutes after it was created. While new capacity is starting for it, the deadline moves to 5 minutes after that capacity is expected to be ready (pool.expected_ready_by), at most an hour after creation. While the workspace's files are being moved to new storage (state_reason: preserving_files), the deadline is an hour after creation. Past the deadline, or after 2 minutes when no more capacity can be added right now, the operation fails with capacity_unavailable and retryable: true (with details.retry_after_seconds in the second case, and at once when the start queue is full): nothing was started, and a suspended workspace stays suspended with its state, so the same request can simply be sent again.

A failed operation throws OperationFailedError (Python: raises) with errorCode / error_code and retryable. Treat unknown error codes as generic errors: show the message and use retryable.

Requests during transitions

Request While suspending While suspended While resuming
Tool call Waits (the SDKs retry workspace_busy) Wakes the workspace (SDKs, CLI, MCP); reads of files may be answered from its disk instead Waits for the resume
Resume or open 409 conflict (operation_in_progress) Resumes Joins the resume
Suspend Joins the suspend 409 conflict (not_running) 409 conflict (operation_in_progress)

A refused call was never executed, so retrying it is safe. A command that is running when a suspend begins is frozen with the VM and continues after the resume; its output stays readable by offset.

Timings

Every open, wake and waited lifecycle call is timed. formatTiming(workspace.lastTiming!) (TypeScript), format_timing(ws.last_timing) (Python) and --timing (CLI) print where the time went:

Text
resume 413 ms, succeeded (workspace 01a0ead6-92e5-7d19-ab7a-f565bf4676cb, operation 01a0ead6-bd61-737a-ae88-66e2d01a6e25)
  client: request 218 ms → queued 195 ms
  server: queued 51 ms, ran 290 ms, total 341 ms; resume from local_cache, boot to ready 211 ms, host disk 0 ms, load 6 ms, ready 62 ms, total 211 ms, after_restore 36 ms
  outside the server: 72 ms

This resume brought the workspace back from its host's local cache, memory and running processes included, in 341 ms on the server and 413 ms end to end. outside the server is network and polling time between you and the API. Watch the phases live with onProgress (TypeScript) or on_progress (Python).

Recoverable failures and conditional suspend

failed describes a failed lifecycle attempt, not deletion. Resume/auto-wake can retry the existing ID through the same start path as reopening its key. A failed recovery still raises its operation error; retries are bounded.

Use suspendWhenIdle({ afterSeconds: 2 }) (or 0 for as soon as idle) instead of reading /idle and then suspending. At capture time a host fence checks and blocks tool admission atomically; running exec sessions and accepted keepalives are rechecked under that fence. A request that loses the race to work is canceled before capture and leaves the workspace running. An immediate suspend() retains its explicit full-state behavior.

Upgrade while keeping your files

An upgrade brings a long-lived workspace to the current supported layout and template base through one cold start. Files, installed package files and home stay on the workspace disk. Memory and running processes restart. Available in TypeScript 0.16.0+, Python 0.12.0+, the CLI 0.11.0+ (shard ws upgrade) and MCP 0.10.0+ (workspace_upgrade).

TypeScript
await ws.upgrade({ at: 'next_resume' });
// The next open, or the next resume once the upgrade is prepared, applies it before your first tool call.

Use at: 'now' with wait: true to apply it immediately. ws.upgradeAvailable shows the available target, and ws.upgradePending shows a scheduled one. A scheduled operation reports applied: next_resume; the applying start reports cold_boot_reason: upgraded and memory_restored: false. Shardflux prepares a scheduled upgrade in the background, next to the workspace's disk, so resuming never waits for it: a resume applies the upgrade once it is prepared and otherwise continues the workspace from memory, reports upgrade_deferred, and keeps the upgrade pending for a following resume. When a suspended workspace is quiet, Shardflux also applies its prepared upgrade ahead of time: an upgrade operation boots it on the new base and a suspend operation captures it within seconds, so the next resume restores memory on the new base. Start commands and services of an organization template run at that next resume, not in between. A scheduled upgrade never ends running work: a resume that finds the workspace still in memory on its host continues it in place (resume_path: thaw), and the upgrade stays pending for the next resume. Layered workspaces refresh their base and guest with your changes taking precedence. Legacy disks upgrade memory while retaining their base and guest. A workspace whose upgrade start did not complete keeps its disk and its target; upgrade({ at: 'now' }) applies it, and targets the newer version instead when one was published since.

Tunnel reconnection

A persistent workspace retains tunnel replay through explicit suspend/resume and live move while its client remains open. Each output stream retains up to 1 MiB in guest RAM, with 64 MiB shared by the workspace's tunnel streams. When that budget is in use, additional tunnels open without replay; exec/PTY status labels report shardflux.tunnel_replay=disabled:memory_budget. Reconnect these connections after a lifecycle interruption.

If the tunnel replay owner restarts or its cursor has left the retained window, existing TCP peers reset. TypeScript SDK 0.27.3+, Python 0.24.0+ and CLI 0.24.1+ replace reverse listeners automatically for fresh connections. A forward connection, including SSH, closes so the application can establish a new connection. The cell refusal carries details.reason=tunnel_replay_unavailable; replaying the old byte cursor cannot recover the connection.

View this page as Markdown