Persistence (Advanced)
- Build your first harness
- Host options
- Send and show media
- Connect clients
- Write a client in any language
- Access and tenants
- Run in the terminal
- Deploy a harness
- Send inputs safely
- Run a tool without a model
- Durable sessions
- Run background work on workers
- List, rename, and fork sessions
- Lifecycle and retention
- Run side effects once
- Wait for a person
- Build on the session log
- Control how a turn ends
- Store settings per thread
- Compact a long session
- Fork and reset a thread
- One log for a session tree
- Write a plugin
- Track usage and cost
- Ask before risky tool calls
- Give your agent coding tools
- Sandbox and extend the coding tools
- Run the coding tools for many threads
- Undo a turn
- Build a coding agent
- Delegate to coding agents
- Switch between agent profiles
- Auth and connectors
- Share a thread between people
- Connect model providers
- Build your own UI
- Code mode in a harness
- Work until a goal is met
- Run agents from a harness
- Hand off a conversation
- Add skills
- Use MCP servers
- Give your agent a computer
- Use from any MCP client
- Use a harness from an editor (ACP)
- Overview
- Quick Start
- ProvidersUpdated
- Harnesses
- Harness Auth
- Workspace
- Tools
- Policy
- Lifecycle & Snapshots
- Portable Snapshots
- Keep Files After Reload
- Save a Named Version
- Branch From a Version
- Send a Frozen File
- Let the Agent Save and Fork
- Pick Which Files to Keep
- What a Snapshot Stores
- Instance Durability
- Durable Runs
- Events
Sandboxes (Advanced)
- Runtime Context
- Bring Your Own Key
- Keep Keys on the Server
- Debug Logging
- Multimodal Content
- Files API
- Per-Model Type Safety
- Runtime Adapter Switching
- Tree-Shaking
- Extend Adapter
- Extend Adapter: Advanced
- Typed Pre-Configured Options
- Test with a Fake Model
- Prompt Caching
- Mid-Conversation Changes
- Approval Flow Processing
Sandbox Instance Durability
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.
Wire it up#
You need two pieces:
- SandboxInstanceStore: maps the compound sandbox key to the provider sandbox id (and an optional snapshot). It must be shared across servers.
- LockStore from @tanstack/ai/locks: makes sure that only one run at a time does resume-or-create. Across servers you need a distributed lock. See Locks.
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.
How it works#
- Instance durability is about where the sandbox runs. It is not chat history. @tanstack/ai-sandbox owns it, separate from @tanstack/ai-persistence (transcripts, runs, interrupts). You can use the same database for both, but each has its own middleware.
- It does not copy the workspace into your storage. When a new sandbox must rebuild finished files, artifacts, and the conversation, use Keep Files After Reload.
- It does not keep the agent's output. To read a run's events from another process, see The Run Journal.
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.
Other ways to wire it#
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 }),
]Advanced#
Why you need a lock#
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.
Implement SandboxInstanceStore#
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.
Related pages#
- Takeover & Detached Runs: survive a client disconnect. A detached run keeps its sandbox up. A later server finds it again with RunStore.sandboxKey and RunStore.detachedSince.
- Persistence overview: chat state only.