Durability stores the sandbox for a thread. The next run on another server reuses it and skips the cold start.
Lifecycle & Snapshots can resume a sandbox, but it keeps its records in memory, in one process. A SandboxInstanceStore keeps those records in shared storage, so every server finds the same sandbox.
You need two pieces:
This sample uses the in-memory versions, which work in one process for local dev:
import { chat } from '@tanstack/ai'
import { InMemoryLockStore, withLocks } from '@tanstack/ai/locks'
import { GROK_CLI_INSTALL_COMMAND, grokBuildText } from '@tanstack/ai-grok-build'
import {
createSecrets,
defineSandbox,
defineWorkspace,
githubRepo,
InMemorySandboxInstanceStore,
withSandbox,
} from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
// Production: your durable store and a distributed LockStore.
const instances = new InMemorySandboxInstanceStore()
const locks = new InMemoryLockStore()
const sandbox = defineSandbox({
id: 'repo-agent',
provider: dockerSandbox({ image: 'node:22' }),
workspace: defineWorkspace({
source: githubRepo({ repo: 'owner/app' }),
setup: [GROK_CLI_INSTALL_COMMAND],
secrets: createSecrets({ XAI_API_KEY: process.env.XAI_API_KEY ?? '' }),
}),
})
const stream = chat({
threadId: 'thread-1',
adapter: grokBuildText('grok-build'),
messages: [{ role: 'user', content: 'hi' }],
middleware: [
// The lock comes BEFORE withSandbox.
withLocks(locks),
withSandbox(sandbox, { instances }),
],
})With reuse: 'thread' (the default), the first run creates the sandbox and records it. A later run for the same threadId resumes it, on any server that shares the store and the lock.
For production, replace both in-memory stores. To write your own SandboxInstanceStore, see Build a Sandbox Adapter.
The two work together. A resumed sandbox still holds the journal of every durable run that ran in it, under /tmp/tanstack-runs. A run is durable when withSandbox gets both runs and durability. A run without that opt-in leaves no journal file.
If nothing else shares the lock, pass both as options and skip withLocks:
import { InMemoryLockStore } from '@tanstack/ai/locks'
import { withSandbox } from '@tanstack/ai-sandbox'
import { instances } from './instance-store'
import { sandbox } from './sandbox'
const middleware = [
withSandbox(sandbox, {
instances,
locks: new InMemoryLockStore(), // multi-server: a distributed LockStore
}),
]Chat persistence is a separate middleware. Add it next to the sandbox:
import { withLocks, InMemoryLockStore } from '@tanstack/ai/locks'
import { withPersistence, memoryPersistence } from '@tanstack/ai-persistence'
import { withSandbox } from '@tanstack/ai-sandbox'
import { instances } from './instance-store'
import { sandbox } from './sandbox'
const middleware = [
withPersistence(memoryPersistence()), // chat state only
withLocks(new InMemoryLockStore()), // multi-server: a distributed LockStore
withSandbox(sandbox, { instances }),
]A shared instance store without a distributed lock is still wrong across servers. Two runs for one thread can start at the same time. Both find no record, and both create a sandbox.
Pair the store with a lock: use withLocks from @tanstack/ai/locks, or the locks option. See Locks.
The store has three methods: get, upsert, and delete. Each has a rule that a conformance suite checks for you. Build a Sandbox Adapter shows the code, the table, and the suite. It also helps you choose how much a sandboxed run leaves behind.