Persistence

Persistence Controls

Persistence Controls

Persistence has no feature flags. What you persist is decided by which state stores the backend provides, and you compose backends per store. Supply only the stores your workflow needs.

Need a mutex across instances? See Locks below.

Named shapes (prefer these)

TypeRequired storesUse
ChatTranscriptStores / ChatTranscriptPersistencemessages (optional runs/interrupts/metadata)Floor for withPersistence / reconstructChat
ChatPersistenceStores / ChatPersistencemessages + runs + interrupts + metadataPackaged backends (memoryPersistence, Drizzle, Prisma, D1)
ChatWithInterruptsStores / ChatWithInterruptsPersistencemessages + runs + interruptsHITL without requiring metadata

There is no public sparse AIPersistenceStores export — use a named shape or AIPersistence<{ messages: MessageStore, … }> for custom maps. defineAIPersistence / composePersistence still accept sparse maps by inference.

What each state store gives you

RequirementStore
Authoritative server transcriptmessages (required by withPersistence / reconstructChat)
Run status and usageruns (required on ChatPersistence; required when interrupts is set)
Durable approvals or human inputinterrupts (requires runs)
App or integration checkpointsmetadata (always optional)

withPersistence(persistence) inspects the stores that are present. Store presence is the capability selection mechanism for optional chat features.

Entrypoint requirements

EntrypointShapeNotes
withPersistenceChatTranscriptStores floorinterruptsruns
reconstructChatChatTranscriptStoresruns / interrupts enrich the response when present
Packaged *Persistence()ChatPersistencemessages + runs (+ interrupts + metadata)
defineAIPersistence / composePersistencesparse by inferencePrefer a named shape for the result

Compose and override stores

composePersistence takes the base backend first and an overrides object second. Here it starts from the in-memory reference backend and swaps in custom interrupts / runs stores:

ts
import { composePersistence, memoryPersistence } from '@tanstack/ai-persistence'
// Your own store implementations of the InterruptStore / RunStore contracts.
import { interruptStore, runStore } from './stores'

const persistence = composePersistence(memoryPersistence(), {
  overrides: {
    interrupts: interruptStore,
    runs: runStore,
  },
})

Each override is independent:

Override valueResult
key omittedInherit the base store.
undefinedInherit the base store.
a store objectReplace that store only.
falseRemove that store.
ts
import { composePersistence, memoryPersistence } from '@tanstack/ai-persistence'

// Drop metadata; the resulting type has no `metadata` key.
const withoutMetadata = composePersistence(memoryPersistence(), {
  overrides: { metadata: false },
})

Unknown store names fail type checking, and are also rejected at runtime when values arrive from untyped JavaScript.

Valid store combinations

  • withPersistence requires messages.
  • interrupts requires runs: an interrupt record is scoped to a run.
  • withGenerationPersistence requires runs.

To define a partial backend directly rather than by composing, use defineAIPersistence({ stores: { ... } }) and pass only the stores you have. See the store interface reference for the store contracts.

Locks (coordination)

Locks coordinate work across instances (a distributed mutex). They live in @tanstack/ai/locks and apply as their own middleware with withLocks, alongside withPersistence. Full guide: Locks.

ts
import { withLocks, InMemoryLockStore } from '@tanstack/ai/locks'
import { withPersistence, memoryPersistence } from '@tanstack/ai-persistence'

const middleware = [
  withPersistence(memoryPersistence()),
  withLocks(new InMemoryLockStore()), // multi-instance: distributed LockStore
]