Call captureSandboxCheckpoint to save the files of a sandbox at any time. After a crash, call restoreSandboxCheckpoint on a new sandbox. You lose only the work since the last checkpoint.
Both functions take a SandboxHandle. They work with sandboxWorkspaceBackend, with every provider, and without withSandbox. When you use withSandbox, the middleware writes checkpoints for you. See Keep Files After Reload.
// checkpoint-stores.ts
import { memoryPersistence } from '@tanstack/ai-persistence'
import { InMemorySandboxCheckpointStore } from '@tanstack/ai-sandbox'
// The checkpoint list, the head, and the writer lease of each thread.
export const checkpoints = new InMemorySandboxCheckpointStore()
// The file bytes. Any object with `get`, `head`, and `put` works.
export const blobs = memoryPersistence().stores.blobsIn production, use a durable checkpoint store and an R2 bucket for blobs. See Store Checkpoints.
// workspace.ts
import { restoreSandboxCheckpoint } from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
import { blobs, checkpoints } from './checkpoint-stores'
const docker = dockerSandbox({ image: 'node:22' })
export async function startWorkspace(threadId: string) {
const handle = await docker.create({})
const restored = await restoreSandboxCheckpoint({
handle,
checkpoints,
blobs,
threadId,
})
// `null`: the thread has no checkpoint yet, so the sandbox stays as it is.
console.log(restored === null ? 'New thread' : `Restored ${restored.id}`)
return handle
}// checkpoint.ts
import {
captureSandboxCheckpoint,
SandboxCheckpointWriterConflictError,
} from '@tanstack/ai-sandbox'
import type { SandboxHandle } from '@tanstack/ai-sandbox'
import { blobs, checkpoints } from './checkpoint-stores'
export async function checkpoint(handle: SandboxHandle, threadId: string) {
try {
const saved = await captureSandboxCheckpoint({
handle,
checkpoints,
blobs,
threadId,
})
return saved.id
} catch (error) {
if (error instanceof SandboxCheckpointWriterConflictError) return null
throw error
}
}
export function checkpointEveryMinute(handle: SandboxHandle, threadId: string) {
const timer = setInterval(() => {
checkpoint(handle, threadId).catch(console.error)
}, 60_000)
return () => clearInterval(timer)
}Call checkpoint where your loop finishes a step, or start checkpointEveryMinute. A capture reads the files one by one. For a consistent set of files, capture between steps, when no command writes to the workspace.
// tools.ts
import { workspaceTools } from '@tanstack/ai-harness/plugins/coding'
import { sandboxWorkspaceBackend } from '@tanstack/ai-sandbox/harness'
import { checkpointEveryMinute } from './checkpoint'
import { startWorkspace } from './workspace'
export const tools = workspaceTools({
workspace: {
bind: async ({ run }) => {
const handle = await startWorkspace(run.threadId)
checkpointEveryMinute(handle, run.threadId)
return {
...sandboxWorkspaceBackend(handle, { owned: true }),
root: handle.workspaceRoot,
}
},
},
})Let the agent write a file, and wait one minute. Then remove the container and start the thread again. bind makes a new sandbox, and the file is back.
captureSandboxCheckpoint also takes these options:
| Option | What it does |
|---|---|
| label | Names the checkpoint. The checkpoint reason is then named. |
| policy | Picks which files to keep. Pass the same policy to the restore. See Pick Which Files to Keep. |
| secrets | Secret values. The capture replaces them with zero bytes in the stored files. |
| conversation | Messages to keep with the files. The default is []. |
| workspaceRoot | The folder to capture. The default is /workspace. |
restoreSandboxCheckpoint takes policy and workspaceRoot too. Pass checkpointId to restore an older checkpoint of the thread in place of the newest one.
Make the handle with containerSandbox over the container of your Durable Object. Pass your R2 bucket as blobs:
// ignore: needs the Cloudflare Workers runtime types (cloudflare:workers, Env, R2Bucket), which the docs check does not load.
// src/agent-container.ts
import { Files } from '@cloudflare/sandbox'
import { restoreSandboxCheckpoint } from '@tanstack/ai-sandbox'
import { containerSandbox } from '@tanstack/ai-sandbox-cloudflare/container'
import { DurableObject } from 'cloudflare:workers'
import { createCheckpointStore } from './checkpoint-store'
export class AgentContainer extends DurableObject<Env> {
async start(threadId: string) {
const container = this.ctx.container
if (!container) throw new Error('The container binding is not configured')
if (!container.running) container.start({ enableInternet: false })
const provider = containerSandbox({ container, files: new Files(container) })
const handle = await provider.create({ id: threadId })
await restoreSandboxCheckpoint({
handle,
checkpoints: createCheckpointStore(this.env.DB),
blobs: this.env.BLOBS,
threadId,
})
return handle
}
}createCheckpointStore is your durable store. Store Checkpoints shows its rules and the test that proves it.
When the container stops, the next start makes a new container, and the files of the last checkpoint are back.