Restore points
A restore point is a saved copy of a whole running VM: its files, its memory and the programs running in it. Restoring one puts the VM back the way it was when you saved it, shells and servers included. Use restore points to let users undo a risky step, to resume a session where they left it after a reload, and to move a session into a file and back.
A workspaceId on its own already keeps workspace files between visits (see Workspace files); restore points add memory and running processes. Userspace and System VMs offer the same API: vm.snapshots, vm.start and vm.suspend. Restore points belong to their workspace, so give the VM a stable workspaceId whenever they should still be there after a reload.
Try it
Section titled “Try it”This demo starts a Linux shell in this tab, makes three changes and saves a restore point after each. Pick any point in the timeline to put the VM back there: the file comes back, and so does the shell’s MOOD variable, which lives only in the running shell’s memory. Type something in the shell, save your own point, and jump between them.
oxidevm:~$ echo "Buy milk" > notes.txt; MOOD=calmoxidevm:~$ echo "Water the plants" >> notes.txt; MOOD=busyoxidevm:~$ rm notes.txt; MOOD=oopsoxidevm:~$
- First note
- Another line
- Notes deleted
Written by the shell once the demo runs
Each point is vm.snapshots.capture({ label }), and each jump is vm.snapshots.restore(point.ref, { saveCurrentFirst: false }) followed by attach() on the restored shell from start.terminals, as the sections below explain.
Check that restore points are available
Section titled “Check that restore points are available”Check vm.capabilities.snapshots before you show restore controls:
import { createVM } from "@oxidevm/sdk";
const vm = await createVM({ mode: "userspace", workspaceId: "example-1", runtime: { hosting: "frame" },});const support = vm.capabilities.snapshots;if (!support.available) { console.log(`Restore points are unavailable: ${support.reason}`);}When they aren’t available, the vm.snapshots methods fail with FEATURE_NOT_AVAILABLE and the same detail.reason.
Capture and restore
Section titled “Capture and restore”import { createVM } from "@oxidevm/sdk";
const vm = await createVM({ mode: "userspace", workspaceId: "restore-demo", runtime: { hosting: "frame" },});try { await vm.workspace.writeText("note.txt", "Before the change\n"); const point = await vm.snapshots.capture({ label: "Before editing" });
await vm.workspace.writeText("note.txt", "After the change\n"); const { undo } = await vm.snapshots.restore(point.ref); console.log(await vm.workspace.readText("note.txt")); // Before the change
// Take the restore back: return to the safety point restore() saved first. if (undo !== undefined) await vm.snapshots.restore(undo.ref, { saveCurrentFirst: false }); console.log(await vm.workspace.readText("note.txt")); // After the change} finally { await vm.dispose();}capture({ label, kind?, signal? }) saves the running VM and resolves with its SnapshotInfo. The label is required: it can’t be blank and is at most 256 UTF-8 bytes. kind is "named" by default; pass "safety" for a save right before a destructive action, or "automatic" for an automatic save made now.
restore(ref, options?) takes these options:
| Option | Meaning |
|---|---|
saveCurrentFirst |
Default true: first save the current state as a safety point labelled "Before restore". The result’s undo is that point. |
onProgress |
Called with each step of the restore, such as step: "open" (with byte counts), "wake" and "reconnect". |
signal |
An AbortSignal that cancels the restore. |
It resolves with { start, undo }, where undo is there only when it saved a safety point first. After a restore:
- The files, memory and processes are the restore point’s. Workspace files roll back with them.
- Process and terminal handles from before the restore stop working. Reattach the restored ones through
start.terminals; see Reattach terminals. - Guest internet access through
vm.networkis off. Restore points never keep it, so connect again if the user needs it.
List, rename and delete
Section titled “List, rename and delete”list() returns every restore point of the workspace, newest first, as SnapshotInfos:
| Field | Meaning |
|---|---|
ref |
{ id, createdAt }, which you pass to restore, rename, delete and export |
label |
Its name, when it has one |
kind |
"named", "automatic", "safety", "imported" or "shipped" |
capturedAtUnixMs |
When it was saved |
mode |
"userspace" or "system" |
bytes |
{ ram, disk, unique }; unique is about what deleting it frees |
resumable |
true, or why it can’t restore into this VM |
import { createVM } from "@oxidevm/sdk";
const vm = await createVM({ mode: "userspace", workspaceId: "restore-demo", runtime: { hosting: "frame" },});const points = await vm.snapshots.list();for (const point of points) { const name = point.label ?? point.kind; const when = new Date(point.capturedAtUnixMs).toLocaleString(); console.log(name, when, point.resumable === true ? "ready" : `can't restore: ${point.resumable}`);}
const newest = points[0];if (newest !== undefined) await vm.snapshots.rename(newest.ref, "Working version");delete(ref) removes a point for good. Named and imported points stay until you delete them. Of the others, the SDK keeps the newest three safety points, the newest automatic save and a few older automatic saves spread over today and yesterday, and removes the rest. usage() reports the space: referencedBytes (what restore points need), reclaimableBytes (what a cleanup would free now), and the browser’s estimate and persisted state for your origin.
Resume after a reload
Section titled “Resume after a reload”Pass restorePoint: "latest" to createVM to continue where the VM left off, and turn on automatic saves so there is something to continue from:
import { createVM } from "@oxidevm/sdk";
const vm = await createVM({ mode: "userspace", workspaceId: "example-1", restorePoint: "latest", runtime: { hosting: "frame" },});
if (vm.start.kind === "resumed") { console.log("Welcome back:", vm.start.snapshot.label ?? "where you left off");} else { console.log("Fresh start:", vm.start.reason); // for example "first-start" or "no-save"}
if (vm.capabilities.snapshots.available) { await vm.snapshots.autoSave.configure({ enabled: true }); document.addEventListener("visibilitychange", () => { vm.snapshots.autoSave.hint(document.visibilityState === "hidden" ? "hidden" : "visible"); });}vm.start says how the VM started:
kind: "resumed"bringssnapshot(the restore point it continued from),terminals,appData,workspaceandnewerFilesDropped.workspaceis"in-sync","reconciled"or"conflict-preserved", depending on how files changed since the save.newerFilesDroppedistruewhen the workspace held files newer than the restore point;restorePoint: "pre-resume"boots with those files instead.kind: "booted"brings areason:"first-start","restart","no-save","incompatible","damaged","no-space","restore-failed","agent-unavailable","fresh-image"or"update-image".- Either kind can carry
skipped, the restore point that couldn’t resume.
restorePoint |
How the VM starts |
|---|---|
"none" (default) |
Boots cold with the workspace’s current files. |
"latest" |
Resumes the save the VM last matched. If that one is damaged, it tries the automatic saves before it, newest first. Boots cold when none can resume. |
a SnapshotRef |
Resumes that restore point, or boots cold and reports why. |
"pre-resume" |
Boots cold with the files from before the last resume. |
"fresh-image" |
Boots a fresh copy of the image. This is a reset: it erases the workspace’s files too. |
"update-image" |
Moves the workspace to the image your app now configures, which vm.start.update offers when it differs. |
Automatic saves start off for every VM. Once on, they save while the VM changes. hint() tells them what the page sees:
| Hint | Meaning |
|---|---|
"hidden" |
The page is hidden and may be closing: save now. |
"visible" |
The page is visible again. |
"activity" |
The user is typing, for example in a terminal: save once input pauses. |
"files-changed" |
Your app changed files: save soon. |
Reattach terminals
Section titled “Reattach terminals”A resumed or restored VM brings back the processes your app had started with vm.spawn, shells included, as start.terminals. They run in the VM and wait for a page to attach to them:
import { createVM, type ProcessHandle, type TerminalHandle } from "@oxidevm/sdk";
const vm = await createVM({ mode: "userspace", workspaceId: "example-1", restorePoint: "latest", runtime: { hosting: "frame" },});const handles: (ProcessHandle | TerminalHandle)[] = [];
if (vm.start.kind === "resumed") { for (const saved of vm.start.terminals) { if (saved.source === "console") continue; // a System console: only its last screen, screenTail const handle = await saved.attach(); // replays the saved screen, then streams live output console.log(`Reattached ${saved.command} as a ${handle.kind}`); handles.push(handle); }}Each handle is a TerminalHandle or a ProcessHandle; check kind and connect it as in Terminals. Don’t write screenTail to the screen for an attached process, because attach() already replays it.
To restore your own UI state with the VM, such as tab names or the open file, save it with vm.snapshots.setAppData(bytes). Every later restore point carries it (at most 1 MiB; null clears it), and a resume returns it as vm.start.appData. A process handle’s snapshotKey, when it has one, matches the key of its entry in start.terminals, so you can store tab names by that key.
Suspend
Section titled “Suspend”vm.suspend({ label }) saves the running VM and then disposes it, which disconnects guest internet access and releases the workspace. Use it before handing the workspace to another tab, or when the user pauses a session on purpose. If saving fails, the VM stays usable when it can.
import { createVM } from "@oxidevm/sdk";
const vm = await createVM({ mode: "userspace", workspaceId: "example-1", restorePoint: "latest", runtime: { hosting: "frame" },});const saved = await vm.suspend({ label: "Paused example" }); // vm is disposed nowconsole.log("Saved", saved.ref.id);
// Later, in this tab or another one:const again = await createVM({ mode: "userspace", workspaceId: "example-1", restorePoint: "latest", runtime: { hosting: "frame" },});console.log(again.start.kind); // "resumed", unless the save can't be restoredBack up to a file
Section titled “Back up to a file”export(ref) writes a restore point as an .oxvm backup file. Without a destination, the SDK stages the file and returns { kind: "file", file, suggestedName } for you to offer as a download. Pass destination, a file handle the user picked (for example with the browser’s save file picker), and it writes there and returns { kind: "written", bytes }.
import { createVM } from "@oxidevm/sdk";
const vm = await createVM({ mode: "userspace", workspaceId: "example-1", runtime: { hosting: "frame" },});const point = await vm.snapshots.capture({ label: "Before export" });
const backup = await vm.snapshots.export(point.ref, { onProgress: ({ completedBytes, totalBytes }) => console.log(`${completedBytes} of ${totalBytes} bytes`),});if (backup.kind === "file") { const url = URL.createObjectURL(backup.file); const link = document.createElement("a"); link.href = url; link.download = backup.suggestedName; link.click(); setTimeout(() => URL.revokeObjectURL(url), 60_000);}
// A file the user picked, for example from an <input type="file">:export async function openBackup(file: File): Promise<void> { const info = await vm.snapshots.inspect(file); if (info.compatible !== true) throw new Error(`This backup can't open here: ${info.compatible}`); const imported = await vm.snapshots.import(file); await vm.snapshots.restore(imported.ref);}inspect(file) reads a backup without storing anything and tells you its mode, kind, label, capturedAtUnixMs, machine (memoryMiB and vcpus), fileBytes, and whether it is compatible with this VM. import(file) stores it as a new restore point of kind "imported"; restore that point to open it. A refused file fails with a detail.reason such as "backup-wrong-file" or "backup-damaged" (INVALID_ARGUMENT), "backup-newer" or "backup-older" (SNAPSHOT_VERSION), or "backup-no-space" (QUOTA_EXCEEDED).
A backup holds everything in the VM as the user left it, including anything they typed or stored there, so treat it like the user’s own data. Backups never include guest internet access.
Follow saves as they happen
Section titled “Follow saves as they happen”observe(listener, { signal }) reports what restore points do until you abort the signal:
import { createVM } from "@oxidevm/sdk";
const vm = await createVM({ mode: "userspace", workspaceId: "example-1", restorePoint: "latest", runtime: { hosting: "frame" },});const watching = new AbortController();vm.snapshots.observe( (event) => { if (event.type === "save-finished") console.log("Saved", event.snapshot.id); else if (event.type === "save-failed") console.error("Save failed:", event.error.code); else if (event.type === "auto-save-paused") console.warn(`Out of space, retrying in ${event.retryInMilliseconds} ms`); }, { signal: watching.signal },);event.type |
Fields |
|---|---|
"save-started" |
kind |
"save-finished" |
kind, snapshot, pauseMilliseconds (how long the VM paused), durationMilliseconds |
"save-failed" |
kind, error (a VMError) |
"auto-save-paused" |
reason: "quota", retryInMilliseconds: automatic saves ran out of storage and will try again |
"restore-progress" |
completedBytes, totalBytes |
"image-preparation-progress", "image-prepared", "image-preparation-failed" |
Automatic saves preparing the complete image in the background. This is not a save. |
Limits
Section titled “Limits”- A restore point belongs to the mode that made it. Restoring a Userspace point into System, or the other way round, fails with
FEATURE_NOT_AVAILABLEanddetail.reason"runtime-mismatch". - A point fits only a VM of the same shape: the same
memoryMiBand, in System, the samevcpus. Otherwiseresumableis"machine-mismatch". Points saved on another image report"profile-mismatch", and"base-missing"means the image they need is no longer stored in this browser. - Restoring, suspending and disposing a VM disconnect
vm.network, and restore points and backups never keep guest internet access. - In Userspace, capture and
suspendfail while a JavaScript process runs in a browser worker. The defaultcommandInterceptors: "auto"runs qualified JavaScript launches that way;commandInterceptors: "off"turns it off. - Restore points live in the browser’s storage for your origin. Watch
usage()and theauto-save-pausedevent, and offer backups for sessions users can’t afford to lose.
Every method and type is listed in the VM reference, and the restorePoint option in createVM.