This page describes the server-side contracts between the persistence middleware and the state stores.
Server state persistence is one of three boundaries that intentionally share no code:
State middleware never mutates chunks to add delivery offsets, and it stores server event state, not the client's rendered messages.
withPersistence(persistence) derives a plan from store presence:
Accepted resumes are committed (interrupts marked resolved/cancelled) only once the run reaches a successful boundary, so a provider failure or abort between accepting a resume and reaching that boundary leaves the interrupt pending and a retry with the same resume succeeds. The canonical AG-UI chunk stream remains unchanged; persistence does not create a second event stream.
When a request carries a non-empty messages array it is treated as the full authoritative history and, on finish, overwrites the stored thread. To continue a stored thread without resending history, pass an empty messages array — the stored transcript is loaded and used.
withGenerationPersistence(persistence) records the run: onStart creates or resumes the run record, and onFinish, onError, and onAbort terminalize it. Durable media storage (artifact metadata plus blob bytes) is a follow-up feature.
Do not treat this as the long-term generation model. Today it reuses chat RunStore and dual-keys (runId, threadId) both to requestId. That is a stopgap: generation jobs must not fake threadId = requestId. threadId is the shared conversation key (Scope.threadId); a generation job's primary id is requestId / jobId. The follow-up should introduce a dedicated generation job store (and later artifact store), not chat RunStore / MessageStore.
import {
composePersistence,
memoryPersistence,
} from '@tanstack/ai-persistence'
const base = memoryPersistence()
const replacement = base.stores.messages
const result = composePersistence(base, {
overrides: {
messages: replacement,
metadata: undefined,
interrupts: false,
},
})Composition copies the store map and does not mutate or dispose either input. The return type calculates which keys are required, optional, replaced, or removed. Unknown store keys are rejected statically and by runtime validation.
Middleware adds entrypoint validation:
The runtime checks are required because JavaScript, configuration loading, and explicitly widened types can bypass static guarantees.
An adapter owns its own resources: connection lifecycle, when migrations run, and how each store record maps to rows. The middleware only calls the store methods; it never opens a connection or inspects a table. A backend may provide any subset of the stores (for example, no metadata), and the return type reflects exactly the stores it exposes. Build your own adapter shows this end to end for SQLite.
composePersistence does not add distributed transactions. When related stores use different systems, adapter authors must define retry, idempotency, and consistency behavior.