Install in Claude Code
Copygit clone --depth 1 https://github.com/TanStack/ai /tmp/ai-persistence-stores && cp -r /tmp/ai-persistence-stores/packages/ai-persistence/skills/ai-persistence/stores ~/.claude/skills/ai-persistence-storesThen start a new Claude Code session; the skill loads automatically.
Definition
SKILL.md
# Persistence Stores
> Builds on **ai-persistence** and **ai-persistence/server**.
`@tanstack/ai-persistence` ships **contracts**, not a backend for your
database. An adapter is an object with a `stores` map; implement the stores you
need against whatever you already run and hand the result to
`withPersistence`. The core never inspects your tables, so the schema is yours.
Use `memoryPersistence()` for dev and tests. Everything durable is an adapter
you write. This skill is the contract reference; the per-stack recipes that
write a `chat-persistence.ts` into an app are
`ai-persistence/build-{drizzle,prisma,cloudflare,custom}-adapter`, and
a complete `node:sqlite` implementation lives in
`examples/ts-react-chat/src/lib/sqlite-persistence.ts`.
## Choose a shape
```ts
import { defineAIPersistence } from '@tanstack/ai-persistence'
import type { ChatWithInterruptsPersistence } from '@tanstack/ai-persistence'
// Sparse is fine — only implement what you need.
export const persistence: ChatWithInterruptsPersistence = defineAIPersistence({
stores: {
messages, // required for withPersistence / reconstructChat
runs, // required if you have interrupts
interrupts,
// metadata optional
},
})
```
| Shape | Contents |
| ------------------------------- | ------------------------------------------------ |
| `ChatTranscriptPersistence` | `messages` (+ optional runs/interrupts/metadata) |
| `ChatWithInterruptsPersistence` | `messages` + `runs` + `interrupts` |
| `ChatPersistence` | all four chat stores |
`defineAIPersistence` preserves exact keys and rejects unknown keys at runtime.
**Annotate your factory with a named shape.** Bare `AIPersistence` is the
all-optional sparse bag, so `withPersistence` and `reconstructChat` reject it
(`stores.messages` is possibly `undefined`). This is the single most common
mistake when writing an adapter.
**`stores` accepts exactly four keys** — `messages`, `runs`, `interrupts`,
`metadata`. Anything else (notably `locks` or sandbox instance maps) throws
`Unknown AIPersistence store key` at runtime and fails to type-check. Locks:
**ai-core/locks** / `@tanstack/ai/locks`. Sandbox instance resume:
`@tanstack/ai-sandbox`.
## Contracts and invariants
### `MessageStore`
```ts
interface MessageStore {
loadThread(threadId: string): Promise<Array<ModelMessage>>
saveThread(threadId: string, messages: Array<ModelMessage>): Promise<void>
}
```
- `loadThread` → `[]` for unknown threads (never `null`).
- `saveThread` is a **full overwrite**, not append. A one-message payload wipes history.
### `RunStore`
`RunStatus`, `TerminalRunStatus`, `RunRecord`, `RunStore`, `defineRunStore`, and
`isTerminalRunStatus` are defined in `@tanstack/ai` and re-exported from
`@tanstack/ai-persistence`. Import those from either; the recipes in this skill
import from `@tanstack/ai-persistence` so an adapter author needs only one
package name.
**`RunError` is the exception — it is NOT re-exported.** Import it from
`@tanstack/ai` directly (`import type { RunError } from '@tanstack/ai'`); the
`@tanstack/ai-persistence` barrel has no such export and the import fails to
resolve.
Four methods are required (`createOrResume` / `update` / `get` /
`findActiveRun`). Two are optional: implement only the ones your backend needs,
and leave the rest off the object entirely (not `undefined`, just absent). A
four-method `RunStore` is a fully valid backend.
`withPersistence` itself calls **none** of the three non-`createOrResume`/`update`
query methods, so leaving both optional ones off costs nothing in the middleware.
Their consumers are elsewhere, and each absence disables exactly one feature:
| method | consumer | absent ⇒ |
| ----------------- | --------------------------------------------------------- | ------------------------------------------------------------------------ |
| `findActiveRun` | `reconstruct.ts` (`stores.runs?.findActiveRun(threadId)`) | required — cannot be absent; stubbing it to `null` silently kills rejoin |
| `listReclaimable` | `reapDetachedRuns` in `@tanstack/ai-sandbox` | the store cannot be reaped at all |
| `listByThread` | application code — nothing in the framework calls it | nothing framework-side breaks |
Consumers of the two OPTIONAL methods feature-detect with `store.method?.(...)`
and degrade rather than throwing. `findActiveRun` is required, so nothing
feature-detects it.
The conformance testkit does not feature-detect. An optional method that is
missing and not declared in `skipMethods` fails the suite, so an omission is
always a choice you made on purpose rather than a check that quietly did not
run. Declare yours and the suite reports them as skipped with a reason:
```ts
// The shipped sqlite example implements findActiveRun and listReclaimable and
// declares only the one it omits.
runPersistenceConformance('sqlite', () => persistence, {
skipMethods: ['runs.listByThread'],
})
```
```ts
interface RunStore {
// Required
createOrResume(
input: Pick<RunRecord, 'runId' | 'threadId' | 'startedAt'> & {
status?: RunStatus
},
): Promise<RunRecord>
update(
runId: string,
patch: Partial<
Pick<
RunRecord,
| 'status'
| 'finishedAt'
| 'error'
| 'usage'
| 'sandboxKey'
| 'detachedSince'
| 'cancelRequested'
| 'driverEpoch'
>
>,
): Promise<void>
get(runId: string): Promise<RunRecord | null>
findActiveRun(threadId: string): Promise<RunRecord | null>
// Optional
listByThread?(threadId: string): Promise<Array<RunRecord>>
listReclaimable?(opts: {
now: number
ttlMs: numberMore 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
>