Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/bright-cloudflare-snapshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-sandbox-cloudflare': patch
---

Support filesystem metadata needed by portable sandbox snapshots.
5 changes: 5 additions & 0 deletions .changeset/calm-daytona-snapshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-sandbox-daytona': patch
---

Support filesystem metadata needed by portable sandbox snapshots.
7 changes: 7 additions & 0 deletions .changeset/chunked-sandbox-fs-write.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/ai-sandbox-docker': patch
'@tanstack/ai-sandbox-vercel': patch
'@tanstack/ai-sandbox-cloudflare': patch
---

Write files in 32KB base64 chunks so snapshot restore no longer hits Linux `MAX_ARG_STRLEN`.
5 changes: 5 additions & 0 deletions .changeset/direct-docker-snapshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-sandbox-docker': patch
---

Support filesystem metadata needed by portable sandbox snapshots.
5 changes: 5 additions & 0 deletions .changeset/eager-local-process-snapshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-sandbox-local-process': patch
---

Support filesystem metadata needed by portable sandbox snapshots.
5 changes: 5 additions & 0 deletions .changeset/fresh-sprites-snapshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-sandbox-sprites': patch
---

Support filesystem metadata needed by portable sandbox snapshots.
5 changes: 5 additions & 0 deletions .changeset/fuzzy-snapshots-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-sandbox': minor
---

Add portable sandbox checkpoints. `createSandboxSnapshots` and `memorySandboxSnapshots` return one object with `save`, `fork`, and `readArtifact`. `createSnapshotTools` turns those methods into host tools bound to the route `threadId`.
5 changes: 5 additions & 0 deletions .changeset/gentle-vercel-snapshots.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-sandbox-vercel': patch
---

Support filesystem metadata needed by portable sandbox snapshots.
5 changes: 5 additions & 0 deletions .changeset/tidy-artifact-history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/ai-persistence': minor
---

Add complete thread artifact history for portable sandbox snapshots.
66 changes: 56 additions & 10 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -240,7 +240,8 @@
{
"label": "Overview",
"to": "persistence/overview",
"addedAt": "2026-08-04"
"addedAt": "2026-08-04",
"updatedAt": "2026-08-14"
},
{
"label": "Chat Persistence",
Expand All @@ -260,12 +261,14 @@
{
"label": "Controls",
"to": "persistence/controls",
"addedAt": "2026-08-04"
"addedAt": "2026-08-04",
"updatedAt": "2026-08-14"
},
{
"label": "Build Your Own Adapter",
"to": "persistence/build-your-own-adapter",
"addedAt": "2026-08-04"
"addedAt": "2026-08-04",
"updatedAt": "2026-08-14"
},
{
"label": "Migrations",
Expand Down Expand Up @@ -298,17 +301,20 @@
{
"label": "Build a Generation Adapter",
"to": "persistence/build-your-own-generation-adapter",
"addedAt": "2026-08-04"
"addedAt": "2026-08-04",
"updatedAt": "2026-08-14"
},
{
"label": "Build a Sandbox Adapter",
"to": "persistence/build-a-sandbox-adapter",
"addedAt": "2026-08-04"
"addedAt": "2026-08-04",
"updatedAt": "2026-08-14"
},
{
"label": "Store Reference",
"to": "persistence/store-reference",
"addedAt": "2026-08-04"
"addedAt": "2026-08-04",
"updatedAt": "2026-08-14"
},
{
"label": "How Persistence Works",
Expand Down Expand Up @@ -509,7 +515,7 @@
"label": "Overview",
"to": "sandbox/overview",
"addedAt": "2026-06-16",
"updatedAt": "2026-08-12"
"updatedAt": "2026-08-14"
},
{
"label": "Quick Start",
Expand All @@ -521,7 +527,7 @@
"label": "Providers",
"to": "sandbox/providers",
"addedAt": "2026-06-29",
"updatedAt": "2026-08-12"
"updatedAt": "2026-08-14"
},
{
"label": "Harnesses",
Expand Down Expand Up @@ -551,12 +557,52 @@
"label": "Lifecycle & Snapshots",
"to": "sandbox/lifecycle",
"addedAt": "2026-06-29",
"updatedAt": "2026-08-12"
"updatedAt": "2026-08-14"
},
{
"label": "Portable Snapshots",
"to": "sandbox/portable-snapshots",
"addedAt": "2026-08-14",
"updatedAt": "2026-08-14"
},
{
"label": "Keep Files After Reload",
"to": "sandbox/portable-snapshots-configure",
"addedAt": "2026-08-14",
"updatedAt": "2026-08-14"
},
{
"label": "Save a Named Version",
"to": "sandbox/portable-snapshots-save",
"addedAt": "2026-08-14",
"updatedAt": "2026-08-14"
},
{
"label": "Branch From a Version",
"to": "sandbox/portable-snapshots-fork",
"addedAt": "2026-08-14",
"updatedAt": "2026-08-14"
},
{
"label": "Send a Frozen File",
"to": "sandbox/portable-snapshots-artifacts",
"addedAt": "2026-08-14"
},
{
"label": "Let the Agent Save and Fork",
"to": "sandbox/portable-snapshots-tools",
"addedAt": "2026-08-14"
},
{
"label": "What a Snapshot Stores",
"to": "sandbox/portable-snapshots-safety",
"addedAt": "2026-08-14"
},
{
"label": "Instance Durability",
"to": "sandbox/durability",
"addedAt": "2026-08-04"
"addedAt": "2026-08-04",
"updatedAt": "2026-08-14"
},
{
"label": "Durable Runs",
Expand Down
4 changes: 4 additions & 0 deletions docs/persistence/build-a-sandbox-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ place the two meet. It is the third of the adapter walkthroughs, next to
[generation](./build-your-own-generation-adapter), and it needs neither of their store
contracts.

To rebuild completed workspace files after the provider sandbox is gone, use
[Keep Files After Reload](../sandbox/portable-snapshots-configure). Pass the same
persistence object that `withPersistence` uses.

## Decide what you store

| You keep | Wire | You get | You give up |
Expand Down
42 changes: 25 additions & 17 deletions docs/persistence/build-your-own-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,29 @@ object inline so you never annotate it by hand.
Each store switches on one capability. Find your column and implement the rows marked
with a tick:

| Store | Save the transcript | Rejoin a run after reload | Durable approvals | App key/value | Persist generation runs | Keep generated files |
| --- | :-: | :-: | :-: | :-: | :-: | :-: |
| `messages` | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
| `runs` | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ |
| `interrupts` | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| `metadata` | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ |
| `generationRuns` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `artifacts` | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| `blobs` | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |

- **Columns stack.** Durable approvals and generated files means the union of both.
- **Two pairs cannot be split.** `interrupts` needs `runs`, and `artifacts` needs
`blobs`.
- **The generation stores feed `withGenerationPersistence`** instead, and need none of
the chat stores. See [Generation persistence](./generation-persistence).

The common production shape is `messages` + `runs` + `interrupts`.
| Store | Save the transcript | Rejoin a run after reload | Durable approvals | App key/value | Persist generation runs | Keep generated files | Rebuild sandbox files |
| --- | :-: | :-: | :-: | :-: | :-: | :-: | :-: |
| `messages` | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
| `runs` | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| `interrupts` | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| `metadata` | ❌ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| `generationRuns` | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ |
| `artifacts` | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |
| `blobs` | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ |

- **Columns stack.** Chat plus sandbox files means `messages` + `artifacts` +
`blobs`. Durable approvals plus generated files means the union of both
columns.
- **Two pairs cannot be split.** `interrupts` needs `runs`, and `artifacts`
needs `blobs`.
- **Generation runs need none of the chat stores.** See
[Generation persistence](./generation-persistence).
- **Sandbox files need a checkpoint store too.** That store lives on
`@tanstack/ai-sandbox`, not in this table. See
[Keep Files After Reload](../sandbox/portable-snapshots-configure).

The common production shape is `messages` + `runs` + `interrupts`. When you
keep generated files or rebuild sandbox files, add `artifacts` and `blobs`.

You can also own only part of it. Put `messages` and `runs` in your database and fill
the rest from somewhere else with `composePersistence`:
Expand Down Expand Up @@ -158,6 +164,8 @@ for `withPersistence`, and with the generation stores for
artifacts and blobs.
- [Build a sandbox adapter](./build-a-sandbox-adapter): the sandbox instance store, and
what a durable sandboxed run adds to `runs`. Only if you run sandboxes.
- [Keep Files After Reload](../sandbox/portable-snapshots-configure): reuse this
adapter for portable snapshots. You need `messages`, `artifacts`, and `blobs`.
- [Store reference](./store-reference): every signature and invariant, and how the
records relate.
- [Controls](./controls): compose stores from different systems.
Expand Down
15 changes: 14 additions & 1 deletion docs/persistence/build-your-own-generation-adapter.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,10 @@ CREATE TABLE IF NOT EXISTS artifacts (
source_url text,
created_at integer NOT NULL
);
CREATE INDEX IF NOT EXISTS artifacts_run_order
ON artifacts (run_id, created_at, artifact_id);
CREATE INDEX IF NOT EXISTS artifacts_thread_order
ON artifacts (thread_id, created_at, artifact_id);
CREATE TABLE IF NOT EXISTS blobs (
key text PRIMARY KEY NOT NULL,
bytes blob NOT NULL,
Expand Down Expand Up @@ -218,6 +222,9 @@ function createGenerationRunStore(db: DatabaseSync) {

- `save` is an upsert.
- `list(runId)` returns every artifact for a run, `[]` when there are none.
- `listForThread(threadId)` returns every artifact for the thread in exact
`(createdAt, artifactId)` ascending order. It must return the complete thread
history, not a page or only the latest run. Snapshot capture uses this cut.
- `delete` / `deleteForRun` are required. Retention and erasure are the point of
storing media durably, and they mirror `BlobStore.delete`.

Expand Down Expand Up @@ -261,7 +268,10 @@ function createArtifactStore(db: DatabaseSync) {
)
const selectOne = db.prepare('SELECT * FROM artifacts WHERE artifact_id = ?')
const byRun = db.prepare(
'SELECT * FROM artifacts WHERE run_id = ? ORDER BY created_at ASC',
'SELECT * FROM artifacts WHERE run_id = ? ORDER BY created_at ASC, artifact_id ASC',
)
const byThread = db.prepare(
'SELECT * FROM artifacts WHERE thread_id = ? ORDER BY created_at ASC, artifact_id ASC',
)
return defineArtifactStore({
async save(record) {
Expand All @@ -284,6 +294,9 @@ function createArtifactStore(db: DatabaseSync) {
async list(runId) {
return byRun.all(runId).map(mapArtifact)
},
async listForThread(threadId) {
return byThread.all(threadId).map(mapArtifact)
},
async delete(artifactId) {
db.prepare('DELETE FROM artifacts WHERE artifact_id = ?').run(artifactId)
},
Expand Down
3 changes: 3 additions & 0 deletions docs/persistence/controls.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ values arrive from untyped JavaScript.
- `withPersistence` requires `messages`.
- `interrupts` requires `runs`: an interrupt record is scoped to a run.
- `withGenerationPersistence` requires `generationRuns`.
- Portable sandbox snapshots require `messages`, `artifacts`, and `blobs` on
the same persistence object. See
[Keep Files After Reload](../sandbox/portable-snapshots-configure).

To define a partial backend directly rather than by composing, use
`defineAIPersistence({ stores: { ... } })` and pass only the stores you have.
Expand Down
5 changes: 5 additions & 0 deletions docs/persistence/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ streaming. That is [Resumable Streams](../resumable-streams/overview), a differe
layer you can add on its own. Step 3 below combines them, which is what most apps
end up wanting.

When the provider sandbox is gone, the workspace files can also disappear. Use
the same persistence object with
[Keep Files After Reload](../sandbox/portable-snapshots-configure).

## Install

```bash
Expand Down Expand Up @@ -164,6 +168,7 @@ To make the `POST` resumable too, hand the same adapter to the response:
| A reload mid-answer to pick the answer back up | Steps 1, 2 and 3 |
| A dropped socket to resume with the page still open | [Resumable Streams](../resumable-streams/overview) alone |
| To pause for a human approval and resume it days later | Step 1 with an `interrupts` store |
| Sandbox files to come back after the provider sandbox is gone | [Keep Files After Reload](../sandbox/portable-snapshots-configure) |

## Where to go next

Expand Down
20 changes: 13 additions & 7 deletions docs/persistence/store-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ there is no separate enable list.
| `interrupts` | Pending, resolved or cancelled human waits. Needs `runs`. | `withPersistence` |
| `metadata` | App and integration key/value state. | `withPersistence` |
| `generationRuns` | Generation run status and result metadata, keyed by its own `runId`. | `withGenerationPersistence`, required |
| `artifacts` | Generated-file metadata. Needs `blobs`. | `withGenerationPersistence` |
| `blobs` | The generated bytes. Needs `artifacts`. | `withGenerationPersistence` |
| `artifacts` | File metadata. Needs `blobs`. | `withGenerationPersistence`, portable snapshots |
| `blobs` | File bytes. Needs `artifacts`. | `withGenerationPersistence`, portable snapshots |

Named groupings of the chat stores (`ChatTranscriptStores`, `ChatPersistenceStores`,
`ChatWithInterruptsStores`) are covered in [Controls](./controls).
Expand Down Expand Up @@ -337,14 +337,20 @@ interface ArtifactRecord {
}

interface ArtifactStore {
save(record: ArtifactRecord): Promise<void>
get(artifactId: string): Promise<ArtifactRecord | null>
list(runId: string): Promise<Array<ArtifactRecord>> // [] when the run has none
delete(artifactId: string): Promise<void>
deleteForRun(runId: string): Promise<void>
save: (record: ArtifactRecord) => Promise<void>
get: (artifactId: string) => Promise<ArtifactRecord | null>
list: (runId: string) => Promise<Array<ArtifactRecord>> // [] when the run has none
// Complete thread history, ordered by (createdAt, artifactId) ascending.
listForThread: (threadId: string) => Promise<Array<ArtifactRecord>>
delete: (artifactId: string) => Promise<void>
deleteForRun: (runId: string) => Promise<void>
}
```

`list` and `listForThread` use `createdAt` first, then ordinal bytewise
`artifactId` order. Compare UTF-8 bytes from left to right. Do not use locale
collation.

## BlobStore

A durable object/blob store for the bytes. `withGenerationPersistence` writes
Expand Down
4 changes: 4 additions & 0 deletions docs/sandbox/durability.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@ owned by `@tanstack/ai-sandbox`, independent of `@tanstack/ai-persistence`
(transcript / runs / interrupts). You may share a database with chat stores, but
you compose a separate middleware.

Instance durability does not copy the workspace into your application storage.
Use [Keep Files After Reload](./portable-snapshots-configure) when a new
sandbox must rebuild completed files, artifacts, and the saved conversation.

It is also not the agent's *output*. This page keeps a sandbox findable across
processes; keeping the run's event stream readable across processes is
[The Run Journal](./journal). The two compose: a resumed sandbox still holds the
Expand Down
9 changes: 9 additions & 0 deletions docs/sandbox/lifecycle.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,11 @@ Bootstrapping a sandbox (cloning the repo, installing dependencies, running
cost once and reuse the result: keep one sandbox per thread, snapshot it after
setup, and resume instead of re-bootstrapping on the next run.

When you must also recover files after a sandbox is gone, configure
[Keep Files After Reload](./portable-snapshots-configure). Provider-native
snapshots make bootstrap faster. Portable snapshots save the completed
workspace as durable application data.

```ts
import { defineSandbox, defineWorkspace, githubRepo } from '@tanstack/ai-sandbox'
import { dockerSandbox } from '@tanstack/ai-sandbox-docker'
Expand Down Expand Up @@ -124,6 +129,10 @@ Each step falls through to the next only when the prior one is unavailable. This
is what turns a warm thread into a near-instant start, and a cold one into a
full bootstrap.

Portable sandbox snapshots run after this lifecycle work. They restore a saved
workspace only into a newly created private sandbox. They never overwrite a
live resumed sandbox. See [Portable Snapshots](./portable-snapshots).

> Which providers support durable disk, snapshots, and resume-by-id is listed on
> [Providers](./providers).

Expand Down
2 changes: 2 additions & 0 deletions docs/sandbox/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ After that, pick the piece you need:
- [Tools](./tools): bridge your app's own tools into the in-sandbox agent.
- [Policy](./policy): allow, ask or deny guardrails on what the agent may run.
- [Lifecycle & Snapshots](./lifecycle): reuse a sandbox, snapshot after setup, resume.
- [Portable Snapshots](./portable-snapshots): keep completed files after the
sandbox is gone. Start with [Keep Files After Reload](./portable-snapshots-configure).
- [Instance Durability](./durability): reuse it across replicas too.
- [Durable Runs](./durable-runs): let a run outlive the tab, and turn it on.
- [Events](./events): stream the agent's edits and tool calls to a UI, and choose what
Expand Down
Loading
Loading