ai-persistence/server
GitHub提供服务器端聊天状态持久化中间件,支持消息、运行状态及中断恢复。适用于需多设备同步或耐用性工具审批的场景,确保会话历史在服务器端权威存储与重建。
触发场景
安装
npx skills add TanStack/ai --skill ai-persistence/server -g -y
SKILL.md
Frontmatter
{
"name": "ai-persistence\/server",
"type": "sub-skill",
"library": "tanstack-ai",
"sources": [
"TanStack\/ai:docs\/persistence\/chat-persistence.md",
"TanStack\/ai:docs\/persistence\/overview.md",
"TanStack\/ai:docs\/persistence\/controls.md"
],
"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.\n",
"library_version": "0.0.0"
}
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
版本历史
- 05280a5 当前 2026-07-30 23:52


