Install in Claude Code
Copygit clone --depth 1 https://github.com/TanStack/ai /tmp/ai-persistence-server && cp -r /tmp/ai-persistence-server/packages/ai-persistence/skills/ai-persistence/server ~/.claude/skills/ai-persistence-serverThen start a new Claude Code session; the skill loads automatically.
Definition
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
```ts
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`** seed the authoritative history. On finish,
persistence **overwrites** the stored thread with the engine's completed
canonical transcript. Post the complete history, 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` | Canonical 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 → `aborted` — **but only sometimes** (see below) | Resumes stay pending |
The canonical transcript already contains the completed terminal assistant
messages. Native-combined output keeps the structured result on its terminal
assistant message. Separate finalization and event-sourced harness output can
preserve plain-text and structured-output assistant messages separately when
those messages use different ids.
```ts
withPersistence(persistence, {
snapshotStreaming: true,
snapshotIntervalMs: 1000, // default
})
```
### `onAbort` writes conditionally, not always
A user pressing Stop and a user closing the tab produce the **identical**
connection close, so `onAbort` can never infer intent from the abort alone.
It writes:
- **`'aborted'`** (terminal, with `finishedAt`) when the abort is an explicit
cancel — `info.cancelRequested === true`, or a durable cancel request found
via `wasCancelRequested(runs, runId)` (both from `@tanstack/ai`; paired with
`requestRunCancel`/`RUN_CANCEL_REASON`) — **or** when the run is not
detachable at all (no sandbox/journal behind it, so there is nothing to
reattach to).
- **Nothing** when it is a plain disconnect on a **detachable** run (some
other middleware, e.g. `@tanstack/ai-sandbox`, has provided
`DetachableRunCapability` from `@tanstack/ai`). The record deliberately
stays `'running'` — the agent keeps running and a later attach can take it
over. (The detaching middleware, not `withPersistence`, is what stamps
`detachedSince`.)
Chat's `onAbort` and generation's `onAbort` (`withGenerationPersistence`) are
**asymmetric on purpose**: a generation job has no journal and no agent loop
to reattach to, so its `onAbort` always writes `'aborted'` unconditionally.
Do not "fix" that asymmetry by making generation conditional, or chat
unconditional — both are correct for what they wrap.
Never build a client, or a persistence backend, that assumes a disconnect
always finalizes the run — for a detachable run it usually does not, and
inventing a `finishedAt` for a still-`'running'` record breaks takeover.
Use `isTerminalRunStatus(status)` (from `@tanstack/ai-persistence`) to test
whether a status is finished, rather than re-listing
`'completed' | 'failed' | 'aborted'` by hand.
Streaming snapshots default **off** (finish is authoritative). Enable only when
partial-output durability is worth extra writes.
ResumeMore from this repository
gap-analysisSkill
>
triage-githubSkill
Triage all open GitHub issues, PRs, and discussions in the current repository by fanning out up to 100 parallel subagents (one per item), then produce a single prioritized report ranking which PRs to review first, which issues to address first, and which discussions need maintainer attention. Use when the user asks to "triage open issues/PRs", "triage discussions", "prioritize the backlog", "what should I review first", "sweep the repo", or any request to bulk-evaluate open GitHub work and recommend an order.
ai-code-modeSkill
>
ai-mcpSkill
>
ai-coreSkill
>
ai-core/adapter-configurationSkill
>
ai-core/ag-ui-protocolSkill
>
ai-core/chat-experienceSkill
>