- MCP Server ToolsUpdated
- Managed MCP with chat()Updated
- Manual MCPUpdated
- MCP Type GenerationUpdated
- MCP Client InputUpdated
- MCP Server Tools: AdvancedNew
- MCP EventsNew
- MCP AppsUpdated
- Serve Tools over HTTPUpdated
- Serve Resources and PromptsUpdated
- Ask for InputUpdated
- Draft Text from a ToolUpdated
- MCP Server TasksUpdated
- MCP Server AuthUpdated
- MCP Server SessionsUpdated
- MCP Server on stdioUpdated
Persistence (Advanced)
- GenerationsUpdated
- Realtime Voice ChatUpdated
- Text-to-SpeechUpdated
- TranscriptionUpdated
- Transcription: AdvancedNew
- Voice CreationUpdated
- Audio RecordingUpdated
- Audio GenerationUpdated
- Image GenerationUpdated
- Image Generation: AdvancedNew
- Video GenerationUpdated
- Video Generation: AdvancedNew
- Live GenerationUpdated
- World GenerationUpdated
- Generation HooksUpdated
- Build your first harnessUpdated
- Host optionsNew
- Send and show mediaUpdated
- Connect clientsUpdated
- Write a client in any languageNew
- Access and tenantsNew
- Run in the terminalUpdated
- Deploy a harnessUpdated
- Send inputs safelyUpdated
- Run a tool without a model
- Durable sessionsUpdated
- Run background work on workersNew
- List, rename, and fork sessionsUpdated
- Lifecycle and retentionNew
- Run side effects onceUpdated
- Wait for a personNew
- Build on the session logUpdated
- Control how a turn endsUpdated
- Store settings per threadUpdated
- Compact a long sessionUpdated
- Fork and reset a threadUpdated
- One log for a session treeUpdated
- Write a pluginUpdated
- Track usage and costUpdated
- Ask before risky tool callsUpdated
- Give your agent coding toolsUpdated
- Sandbox and extend the coding toolsUpdated
- Run the coding tools for many threadsNew
- Undo a turnUpdated
- Build a coding agentUpdated
- Delegate to coding agentsUpdated
- Switch between agent profilesUpdated
- Auth and connectorsUpdated
- Share a thread between peopleUpdated
- Connect model providersUpdated
- Build your own UIUpdated
- Code mode in a harnessUpdated
- Work until a goal is metUpdated
- Run agents from a harnessUpdated
- Hand off a conversationNew
- Add skillsUpdated
- Use MCP serversUpdated
- Give your agent a computerNew
- Use from any MCP clientUpdated
- Use a harness from an editor (ACP)Updated
- OverviewUpdated
- Quick StartUpdated
- ProvidersUpdated
- HarnessesUpdated
- Harness AuthUpdated
- WorkspaceUpdated
- ToolsUpdated
- PolicyUpdated
- Lifecycle & SnapshotsUpdated
- Portable SnapshotsUpdated
- Keep Files After ReloadUpdated
- Save a Named VersionUpdated
- Branch From a VersionUpdated
- Send a Frozen FileUpdated
- Let the Agent Save and ForkUpdated
- Pick Which Files to KeepUpdated
- What a Snapshot StoresUpdated
- Instance DurabilityUpdated
- Durable RunsUpdated
- EventsUpdated
Sandboxes (Advanced)
- Runtime ContextUpdated
- Bring Your Own KeyUpdated
- Keep Keys on the ServerNew
- Debug LoggingUpdated
- Multimodal ContentUpdated
- Files APIUpdated
- Per-Model Type SafetyUpdated
- Runtime Adapter SwitchingUpdated
- Tree-ShakingUpdated
- Extend AdapterUpdated
- Extend Adapter: AdvancedNew
- Typed Pre-Configured OptionsUpdated
- Test with a Fake ModelUpdated
- Prompt CachingUpdated
- Mid-Conversation ChangesUpdated
- Approval Flow ProcessingUpdated
Chat Persistence
withPersistence writes the transcript and a waiting approval outside the request. A process restart then keeps both.
withPersistence is a chat middleware. You choose the store, and the server keeps the true copy of every conversation.
Quick setup#
1. Install the package#
npm i @tanstack/ai-persistencepnpm add @tanstack/ai-persistenceyarn add @tanstack/ai-persistencebun add @tanstack/ai-persistence2. Create the persistence object#
Start with the in-memory backend. Replace it with your database adapter later:
// persistence.ts
import { memoryPersistence } from '@tanstack/ai-persistence'
export const persistence = memoryPersistence()3. Add the middleware to the server route#
Add withPersistence to chat(). Then add a GET that returns the stored thread:
// api/chat.ts
import {
chat,
chatParamsFromRequest,
toServerSentEventsResponse,
} from '@tanstack/ai'
import { openaiText } from '@tanstack/ai-openai'
import { reconstructChat, withPersistence } from '@tanstack/ai-persistence'
import { persistence } from './persistence'
export async function POST(request: Request) {
const params = await chatParamsFromRequest(request)
const stream = chat({
adapter: openaiText('gpt-6.1-sol'),
messages: params.messages,
threadId: params.threadId,
runId: params.runId,
// Forward the resume batch so a thread with pending interrupts continues.
...(params.resume ? { resume: params.resume } : {}),
middleware: [withPersistence(persistence)],
})
return toServerSentEventsResponse(stream)
}
export function GET(request: Request): Promise<Response> {
// Add `authorize` in a multi-user app. See Client persistence.
return reconstructChat(persistence, request)
}4. Load the thread in the client#
import { fetchServerSentEvents, useChat } from '@tanstack/ai-react'
export function Chat({ threadId }: { threadId: string }) {
const { messages, sendMessage } = useChat({
threadId,
connection: fetchServerSentEvents('/api/chat'),
persistence: true,
})
return (
<div>
{messages.map((message) => (
<div key={message.id}>{message.role}</div>
))}
<button type="button" onClick={() => void sendMessage('hi')}>
Send
</button>
</div>
)
}Send a message, then reload the page. The thread comes back from the server store. With a database adapter, it also comes back after a server restart.
Client persistence covers the client side in full, including a reply that is still streaming.
Tip
Run npx @tanstack/intent@latest install to give your coding assistant this package's Agent Skills. The skills read your database setup and write the adapter to match. They also know the rules that are easy to get wrong, such as a full overwrite in saveThread and insert-if-absent creates for runs and interrupts.
How it works#
The middleware uses the stores that your backend has. There are no feature flags.
- messages (required): loads and saves the full model-message thread.
- runs: records the status of each run: running, interrupted, completed, failed, or aborted.
- interrupts: records pending tool approvals, client tools, and generic waits. Needs runs.
withPersistence writes at these moments:
- Start of a run. It saves the new user message, so a reload during generation still shows the question.
- Interrupt. It saves the new interrupt records and marks the run interrupted.
- Finish. It saves the full transcript, then marks the run completed.
On an error, the run is marked failed. On an abort, the run is marked aborted.
Before you go to production:
- Replace memoryPersistence() with an adapter for your database. Build your own adapter walks through a complete SQLite version.
- Apply the schema through your deploy workflow, not when the adapter opens. See Migrations.
Threads, runs, and turns#
The transcript is stored per threadId. Each run gets a runs record with its status, timings, and the usage that the provider reported.
So a client that reconnects never has to send a run id. The store finds the live run of the thread with findActiveRun(threadId), and the client follows that run.
Id map explains how to choose a thread id. How persistence works has the full model.
Interrupts survive a restart#
A run can pause on an interrupt: a tool approval, a client-side tool, or a generic middleware request. The middleware records it in the interrupts store.
The next request on that thread must carry a resume batch that answers the pending interrupts. If it does not, the middleware rejects the request. That is why the route forwards params.resume.
The answers are committed only when the run reaches the next interrupt or finishes. If the run fails first, the interrupts stay pending, and the user can answer them again.
Choose which stores to run#
Each store is independent, so give the backend only the stores you need. Controls shows how to mix backends per store.
Advanced#
These topics are on Chat persistence: advanced:
- Subagent cards and AG-UI activity rows after a reload.
- How incoming messages merge with the stored thread.
- Compaction, streaming snapshots, and structured output.
- The full run and interrupt lifecycles, including detached runs.