
Skill
ai-persistence/server
configure server-side chat state for TanStack AI
Description
Server chat state with withPersistence from @tanstack/ai-persistence. Authoritative transcript, run lifecycle, durable interrupts/approvals, chatParamsFromRequest, reconstructChat, snapshotStreaming. Use when the server owns history, multi-device, or durable tool approvals. NOT client localStorage (see ai-core/client-persistence in @tanstack/ai) and NOT stream reconnect alone.
SKILL.md
Server Chat Persistence
Builds on ai-persistence. Package:
@tanstack/ai-persistence.
withPersistence(persistence) is a ChatMiddleware that writes chat state
to a backend: messages, runs, interrupts (optional metadata). It does not
mutate the chunk stream and does not replace delivery durability.
Setup
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { withPersistence } from '@tanstack/ai-persistence'
// Your adapter — see ai-persistence/stores.
import { persistence } from './persistence'
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: openaiText('gpt-5.5'),
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
...(params.resume ? { resume: params.resume } : {}),
middleware: [withPersistence(persistence)],
})
return toServerSentEventsResponse(stream)
}
Always pass threadId and runId from the client (via
chatParamsFromRequest / body helpers). Forward resume when the client
resolves pending interrupts.
For dev and tests, memoryPersistence() from @tanstack/ai-persistence is a
drop-in backend that implements all four stores in process.
What each store does
| Store | Role | Required? |
|---|---|---|
messages | Full model-message transcript load/save | Yes for withPersistence |
runs | Run status, timing, usage, errors | Optional; needed for interrupt durability |
interrupts | Pending/resolved tool approvals & waits | Optional; requires runs |
metadata | App-owned namespaced key/value | Optional |
Named shapes: ChatTranscriptPersistence (floor), ChatPersistence (all four).
Annotate your factory with one of these, not with bare AIPersistence —
the unparameterized type is the all-optional bag, and withPersistence rejects
it because stores.messages is possibly undefined.
Authoritative-history contract
- Non-empty
messages→ finish overwrites the stored thread with that array. Post the complete transcript, never a delta. - Empty
messages→ middleware loads the stored thread and continues.
When state is written
| Moment | Writes | Best-effort? |
|---|---|---|
onStart | Pending turn snapshot (user + history) | Yes — failure does not abort |
| Interrupt boundary | New interrupts, run → interrupted, message snapshot | No |
onFinish | Full transcript first, then run → completed, commit resumes | No |
| Stream (optional) | Throttled partial assistant text | Yes if snapshotStreaming: true |
onError | Run → failed | Resumes stay pending |
onAbort | Run → interrupted | Resumes stay pending |
withPersistence(persistence, {
snapshotStreaming: true,
snapshotIntervalMs: 1000, // default
})
Streaming snapshots default off (finish is authoritative). Enable only when partial-output durability is worth extra writes.
Resumes accepted in onConfig commit only at a success boundary (interrupt or
finish). A failed run leaves interrupts pending so the same resume batch can
retry.
Interrupt / resume flow
- Middleware records pending interrupts and gates new input: if pending
exist, the request must include a matching
resumebatch oronConfigthrows. - On valid resume, middleware builds
resumeToolStateand clearsconfig.resumeso the engine does not double-reconstruct from client history (server owns transcript). - On success boundary, interrupts are marked resolved/cancelled.
Hydrate a thread for the client (reconstructChat)
Server-authoritative clients load history by threadId (often GET):
import { reconstructChat } from '@tanstack/ai-persistence'
export async function GET(request: Request) {
return reconstructChat(persistence, request, {
// Multi-user: required in production
authorize: async (threadId, req) => {
const userId = await sessionUserId(req)
return userOwnsThread(userId, threadId)
},
})
}
Returns { messages, activeRun, interrupts }:
messages— UI messages for paintactiveRun—{ runId }if a run is still generating (runs.findActiveRun)interrupts— pending human-in-the-loop state for re-prompt
Without authorize, anyone who guesses ?threadId= gets the transcript.
Generation activities
withGenerationPersistence(persistence) tracks run records for non-chat
activities (image, audio, TTS, video, transcription). Do not fake
threadId = requestId on chat run stores — use the generation helper.
Common mistakes
CRITICAL: Posting a message delta as messages
Wipes the stored thread down to that delta. Always send full history or [].
HIGH: Omitting threadId / runId
Persistence keys and resume need stable ids. Use chatParamsFromRequest.
HIGH: Interrupts without runs
interrupts requires runs; withPersistence throws otherwise.
HIGH: Typing a factory as bare AIPersistence
AIPersistence defaults to the sparse all-optional bag, so withPersistence
and reconstructChat reject the value. Return ChatPersistence (or
ChatTranscriptPersistence) instead.
MEDIUM: Expecting withPersistence to reconnect a dropped stream
That is delivery durability (resumable streams), not state persistence.
Cross-references
- ai-persistence — layers and recommended stack
- ai-persistence/stores — implement the store interfaces
- ai-core/client-persistence (
@tanstack/ai) — browser half - ai-core/locks — multi-instance coordination
More skills from the ai repository
View all 27 skillsai-code-mode
execute sandboxed TypeScript code with LLMs
Jul 16AI SDKCode ExecutionSandboxingTanStack +1ai-core
configure TanStack AI agent features
Jul 30AgentsAIMiddlewareai-core/adapter-configuration
configure AI provider adapters
Jul 16AI SDKConfigurationLLMTanStackai-core/ag-ui-protocol
implement TanStack AI streaming protocol
Jul 16API DevelopmentLLMTanStackai-core/chat-experience
implement chat experiences with TanStack AI
Jul 30API DevelopmentFrontendLLMTanStackai-core/client-persistence
implement browser chat persistence for TanStack AI
Jul 30AIFrontendPersistenceTanStack
More from TanStack
View publisheraggregation
perform data aggregation in TanStack Table
table
Jul 30Data AnalysisFrontendTanStackapi-not-found
diagnose TanStack Table API errors
table
Jul 30DebuggingFrontendTanStackcell-selection
select rectangular cell ranges in tables
table
Jul 30Data AnalysisTanStackUI Componentsclient-vs-server
manage TanStack Table data pipelines
table
Jul 30Data PipelineFrontendPerformanceTanStackcolumn-faceting
build faceted filter UIs
table
Jul 30Data VisualizationFrontendTanStackcolumn-filtering
implement column filtering in TanStack Table
table
Jul 30Data AnalysisFrontendTanStack