Skip to content

feat(persistence): durable sandbox persistence (SandboxStore + shared lock)#988

Open
AlemTuzlak wants to merge 20 commits into
feat/persistence-corefrom
feat/persistence-sandbox
Open

feat(persistence): durable sandbox persistence (SandboxStore + shared lock)#988
AlemTuzlak wants to merge 20 commits into
feat/persistence-corefrom
feat/persistence-sandbox

Conversation

@AlemTuzlak

Copy link
Copy Markdown
Contributor

Summary

Stacked on #984. Lands the sandbox persistence piece #984 explicitly deferred: durable, multi-instance-correct resume for @tanstack/ai-sandbox.

withSandbox already consumes SandboxStore (which sandbox to resume) and LockStore (mutual exclusion around ensure) as optional capabilities, defaulting to in-memory (single-process). This adds the durable half — no behavioral change to the sandbox layer.

Core: one shared locks token

The 'locks' capability now lives in core @tanstack/ai (LocksCapability, LockStore, InMemoryLockStore, getLocks, provideLocks); @tanstack/ai-sandbox and @tanstack/ai-persistence re-export it. Because capability identity is by reference, one shared token means a withPersistence({ stores: { locks } }) already in the chain feeds the sandbox lock automatically — this is the "sandbox-consumer bridge" #984 noted as deferred.

@tanstack/ai-sandbox

  • withSandboxPersistence({ store, locks? }) — provides a durable SandboxStore (and optional distributed lock) to withSandbox. Compose before withSandbox.
  • SandboxStore conformance testkit exported at @tanstack/ai-sandbox/testkit (runSandboxStoreConformance).

Durable SandboxStore backends

  • @tanstack/ai-persistence-drizzlecreateDrizzleSandboxStore(db) + a new sandboxes table folded into the initial migration (regenerated; asset synced to the Cloudflare sibling).
  • @tanstack/ai-persistence-prismacreatePrismaSandboxStore(prisma) + a Sandbox model in the fragment.
  • @tanstack/ai-persistence-cloudflarecreateD1SandboxStore(d1) (delegates to Drizzle); the existing Durable-Object lock doubles as the distributed sandbox lock.

The three backends add @tanstack/ai-sandbox as an optional peer dependency. Sandboxes stays out of the chat BYO schema/model contract, so chat-only users are unaffected.

Tests / docs / skill

  • Conformance suites for the in-memory reference store and all three backends; withSandboxPersistence provide + cross-run-resume unit tests.
  • New docs/sandbox/persistence.md (+ nav, overview callout, cross-links from the persistence backend docs).
  • Updated the ai-sandbox agent skill.
  • E2E: a provider-free /api/sandbox-durability harness route + spec asserting a second run resumes the persisted sandbox (create stays 1).

Testing

Green locally (canonical nx targets, --skip-nx-cache): test:sherif, test:knip, test:docs, test:kiira, test:oxlint, test:types, and test:lib for @tanstack/ai, @tanstack/ai-sandbox, and all four persistence packages (conformance + wiring). @tanstack/ai-sandbox builds clean including the testkit + DTS.

Deferred to CI (per #984's precedent — this dev machine's nx output-materialization is unreliable here, which also blocks the Playwright dev server for every route): the full workspace build / test:build and the Playwright test:e2e run. The sandbox-durability spec is authored, registered in the route tree, and typechecks; the identical server-side-resume behavior is covered by the @tanstack/ai-sandbox persistence.test.ts unit test.

Release

Changeset: minor for @tanstack/ai, @tanstack/ai-sandbox, and the four persistence packages.

AlemTuzlak and others added 19 commits July 22, 2026 18:06
…kends

Server-side persistence for chat(): durable thread messages, run records, and
interrupts via the withChatPersistence middleware, with pluggable backends.

- @tanstack/ai-persistence: store contracts, withChatPersistence /
  withGenerationPersistence middleware, memoryPersistence reference store,
  conformance testkit. Locks (LockStore/InMemoryLockStore/LocksCapability) live
  here rather than core; the sandbox-consumer bridge is deferred.
- -drizzle / -prisma / -cloudflare: backend store implementations + migration /
  schema / models CLIs. Cloudflare D1 delegates to the drizzle backend.

Reconciled against the shipped ephemeral-interrupt engine: the middleware
records interrupts and gates new input, and delegates resume-tool-state
reconstruction to the engine (resume batch + interrupt bindings in history).

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
The persistence adapter now stores one combined { messages, resume? } record
per chat id, so a full page reload restores the transcript, rehydrates pending
interrupts, and rejoins an in-flight run through joinRun when the connection is
durability-backed. Legacy bare-array records are still read.

Adds localStoragePersistence / sessionStoragePersistence / indexedDBPersistence
(+ StorageUnavailableError and the ChatPersistedState / ChatStorageAdapter
types). Durability rides the existing option, so every framework integration
gets it with no framework-specific code.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
New /persistent-chat route: useChat with localStoragePersistence on the client
and withChatPersistence(sqlitePersistence) on the server, so a full page reload
restores the conversation on both ends. Adds a nav link and README section.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
…ility

New docs/persistence section: overview, chat-persistence, browser-refresh,
controls, custom-stores, sql-backends, drizzle, prisma, cloudflare, migrations,
internals. Wires the nav and updates the client chat/persistence page for the
combined { messages, resume } record and built-in storage adapters.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
Update the agent skills for the new surface: withChatPersistence server
middleware and its backends, and the client browser-refresh durability
(combined persistence record, storage adapters, joinRun rejoin, all frameworks).

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
Provider-free durable harness route + client page + spec proving message
restore after reload and interrupt-survives-reload via localStorage. Mid-stream
joinRun rejoin is covered by ai-client unit tests and delivery-durability.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
Add the object form `persistence: { store, messages?: boolean }`. `messages:
false` caches only the tiny resume pointer, keeping large transcripts off the
client while durability rejoin and interrupt restore still work and the server
stays authoritative for history. A bare adapter remains shorthand for
`{ store, messages: true }`, so this is backward compatible and every framework
passthrough is unchanged.

The persistent-chat example gains a history branch on its GET route
(loadThread by threadId, distinct from the per-run delivery replay) so a
server-authoritative reload can hydrate the transcript. Docs + chat-experience
skill document the lever.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
Rewrite the persistence overview into a concept + decision page: the three
problems (dropped stream, lost-on-reload, no durable record), the two
independent layers (delivery durability vs state persistence), client vs server
halves, the reload/rehydration timeline, and a when-to-pick-each guide. Add a
back-link from the resumable-streams overview so the two sections cross-reference.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
…tence

localStoragePersistence / sessionStoragePersistence / indexedDBPersistence now
default their type parameter to ChatPersistedState and to a JSON codec, so
`persistence: localStoragePersistence()` needs no type argument and no
serialize/deserialize pair. Drops the IsJsonSerializable type gate that forced a
codec for the chat record (UIMessage already round-trips as JSON on the wire).

Client persistence now keys on `threadId` (the conversation identity), so a
reload with the same threadId restores the same record; `id` becomes an optional
storage-key override. The storage adapters and persistence types are re-exported
from every framework package, so a single import from @tanstack/ai-react (etc.)
works.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
reconstructChat(persistence, request) returns a thread's stored messages as a
JSON Response, so a server-authoritative client can hydrate its transcript on
load from a one-line GET handler instead of hand-rolling loadThread + Response.
…curate resume

Add a "What we recommend" section to the overview: client resume-pointer-only
plus server persistence plus one GET that rehydrates history and resumes durable
streams, with the reasoning. Update every snippet to the zero-config
localStoragePersistence() and threadId, use reconstructChat for history, and
discriminate the resume GET with durability.resumeFrom() instead of sniffing
query params (the run id rides the X-Run-Id header, the offset the Last-Event-ID
header). Example, e2e page, and chat-experience skill match.
Rename browser-refresh to client-persistence and make it the single home for
the client story: turning it on, what a reload restores, the two cache modes
(everything vs resume-pointer-only) with when to use each, and the three storage
backends with when to use each. Remove the legacy docs/chat/persistence page
(client content now lives in the persistence section) and repoint its links.

Make the other persistence docs server-only: drop the client rows from the
controls decision table and the browser-storage section from internals, leaving
a pointer to the client guide. The overview stays the cross-cutting map. Update
all cross-links, the chat-experience skill source, and the reconstructChat doc
reference.
In `{ messages: false }` mode a prior session's persisted record is
`{ messages: [], resume }`. The constructor treated that empty transcript as
authoritative and clobbered host-provided `initialMessages`, and the async
hydrate path applied `[]` on top, so a server-authoritative reload dropped the
history the app had fetched from the server.

The persisted transcript is now adopted only when the client actually caches it
(`cachesMessages`); in messages:false mode the client keeps `initialMessages`
and takes only the resume pointer from storage. This makes the recommended
server-authoritative flow work: on a mid-stream reload the app seeds history via
initialMessages (the reconstruct GET) while the client separately rejoins the
live run via joinRun (the resume GET), and the replayed run merges into the
seeded history by message id. Adds a test covering both together.

Also document in the overview that history hydration and run rejoin are two
separate GET requests, so the handler's if/else routes each and neither blocks
the other.
The primary chat persistence middleware is now `withPersistence`.
`withGenerationPersistence` is unchanged. Unreleased, so no alias is kept.
Updates all call sites, docs, skills, the example, and the changeset.

fix(ai-client): rejoin an in-flight run from an async persistence store

Auto-rejoin was gated on the synchronous read, so an async store
(indexedDBPersistence) restored messages and interrupts on reload but never
rejoined a mid-stream run. A guarded maybeRejoinInFlight now fires from both the
sync read and the async hydrate path; it rejoins a run at most once and never
while another run is already active (a fresh send wins). Adds a test that a
run rejoins from an async (Promise-returning) adapter.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e00d2fec-0031-45a9-81ab-dbfc20a88197

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/persistence-sandbox

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

14 package(s) bumped directly, 37 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-angular 0.3.1 → 1.0.0 Changeset
@tanstack/ai-durable-stream 0.0.0 → 1.0.0 Changeset
@tanstack/ai-persistence 0.0.0 → 1.0.0 Changeset
@tanstack/ai-persistence-cloudflare 0.0.0 → 1.0.0 Changeset
@tanstack/ai-persistence-drizzle 0.0.0 → 1.0.0 Changeset
@tanstack/ai-persistence-prisma 0.0.0 → 1.0.0 Changeset
@tanstack/ai-preact 0.11.1 → 1.0.0 Changeset
@tanstack/ai-react 0.18.1 → 1.0.0 Changeset
@tanstack/ai-sandbox 0.2.4 → 1.0.0 Changeset
@tanstack/ai-solid 0.15.1 → 1.0.0 Changeset
@tanstack/ai-svelte 0.15.1 → 1.0.0 Changeset
@tanstack/ai-vue 0.15.1 → 1.0.0 Changeset
@tanstack/ai-acp 0.2.3 → 1.0.0 Dependent
@tanstack/ai-anthropic 0.16.3 → 1.0.0 Dependent
@tanstack/ai-bedrock 0.1.4 → 1.0.0 Dependent
@tanstack/ai-claude-code 0.2.3 → 1.0.0 Dependent
@tanstack/ai-code-mode 0.3.8 → 1.0.0 Dependent
@tanstack/ai-code-mode-skills 0.3.11 → 1.0.0 Dependent
@tanstack/ai-codex 0.2.3 → 1.0.0 Dependent
@tanstack/ai-elevenlabs 0.2.34 → 1.0.0 Dependent
@tanstack/ai-fal 0.9.12 → 1.0.0 Dependent
@tanstack/ai-gemini 0.20.1 → 1.0.0 Dependent
@tanstack/ai-grok 0.14.9 → 1.0.0 Dependent
@tanstack/ai-grok-build 0.2.3 → 1.0.0 Dependent
@tanstack/ai-groq 0.5.3 → 1.0.0 Dependent
@tanstack/ai-isolate-node 0.1.47 → 1.0.0 Dependent
@tanstack/ai-isolate-quickjs 0.1.47 → 1.0.0 Dependent
@tanstack/ai-mistral 0.2.3 → 1.0.0 Dependent
@tanstack/ai-ollama 0.8.16 → 1.0.0 Dependent
@tanstack/ai-openai 0.17.1 → 1.0.0 Dependent
@tanstack/ai-opencode 0.2.3 → 1.0.0 Dependent
@tanstack/ai-openrouter 0.15.10 → 1.0.0 Dependent
@tanstack/ai-react-ui 0.8.15 → 1.0.0 Dependent
@tanstack/ai-sandbox-cloudflare 0.2.4 → 1.0.0 Dependent
@tanstack/ai-sandbox-daytona 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-docker 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-local-process 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-sprites 0.2.1 → 1.0.0 Dependent
@tanstack/ai-sandbox-vercel 0.2.0 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.7.14 → 1.0.0 Dependent
@tanstack/openai-base 0.9.9 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai 0.42.0 → 0.43.0 Changeset
@tanstack/ai-client 0.22.1 → 0.23.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-devtools-core 0.4.24 → 0.4.25 Dependent
@tanstack/ai-isolate-cloudflare 0.2.38 → 0.2.39 Dependent
@tanstack/ai-mcp 0.2.5 → 0.2.6 Dependent
@tanstack/ai-vue-ui 0.2.34 → 0.2.35 Dependent
@tanstack/preact-ai-devtools 0.1.67 → 0.1.68 Dependent
@tanstack/react-ai-devtools 0.2.67 → 0.2.68 Dependent
@tanstack/solid-ai-devtools 0.2.67 → 0.2.68 Dependent
ag-ui 0.0.2 → 0.0.3 Dependent

@nx-cloud

nx-cloud Bot commented Jul 23, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit e1e51d0

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 30s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-23 12:20:30 UTC

@AlemTuzlak
AlemTuzlak force-pushed the feat/persistence-sandbox branch 2 times, most recently from 4b2e68a to e1e51d0 Compare July 23, 2026 12:14
@pkg-pr-new

pkg-pr-new Bot commented Jul 23, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@988

@tanstack/ai-acp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-acp@988

@tanstack/ai-angular

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-angular@988

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@988

@tanstack/ai-bedrock

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-bedrock@988

@tanstack/ai-claude-code

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-claude-code@988

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@988

@tanstack/ai-code-mode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode@988

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode-skills@988

@tanstack/ai-codex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-codex@988

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@988

@tanstack/ai-durable-stream

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-durable-stream@988

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-elevenlabs@988

@tanstack/ai-event-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-event-client@988

@tanstack/ai-fal

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-fal@988

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@988

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@988

@tanstack/ai-grok-build

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok-build@988

@tanstack/ai-groq

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-groq@988

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-cloudflare@988

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-node@988

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs@988

@tanstack/ai-mcp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mcp@988

@tanstack/ai-mistral

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mistral@988

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@988

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@988

@tanstack/ai-opencode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-opencode@988

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@988

@tanstack/ai-persistence

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-persistence@988

@tanstack/ai-persistence-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-persistence-cloudflare@988

@tanstack/ai-persistence-drizzle

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-persistence-drizzle@988

@tanstack/ai-persistence-prisma

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-persistence-prisma@988

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@988

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@988

@tanstack/ai-react-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react-ui@988

@tanstack/ai-sandbox

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox@988

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-cloudflare@988

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-daytona@988

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-docker@988

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-local-process@988

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-sprites@988

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-vercel@988

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@988

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid-ui@988

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@988

@tanstack/ai-utils

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-utils@988

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@988

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue-ui@988

@tanstack/openai-base

npm i https://pkg.pr.new/TanStack/ai/@tanstack/openai-base@988

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@988

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@988

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@988

commit: e1e51d0

@AlemTuzlak
AlemTuzlak force-pushed the feat/persistence-sandbox branch from e1e51d0 to ef3e5f5 Compare July 23, 2026 12:53
Sandbox resume (SandboxStore + shared lock) is provided by withPersistence and consumed by withSandbox. SandboxStore/SandboxStoreCapability and the lock primitives live in core @tanstack/ai; the persistence backends carry a durable sandbox store; conformance testkit, e2e, docs, and the ai-sandbox skill updated.
@tombeckenham
tombeckenham force-pushed the feat/persistence-core branch from 0b17612 to d2d08e8 Compare July 24, 2026 01:31
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant