Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
83c026a
feat(persistence): add ai-persistence + drizzle/prisma/cloudflare bac…
AlemTuzlak Jul 22, 2026
6165a30
feat(ai-client): browser-refresh durability via the persistence option
AlemTuzlak Jul 22, 2026
fd4742d
example(ts-react-chat): persistent chat with SQLite + reload durability
AlemTuzlak Jul 22, 2026
022efd1
docs(persistence): add persistence section for server + browser durab…
AlemTuzlak Jul 22, 2026
205e803
docs(skills): cover persistence in middleware + chat-experience skills
AlemTuzlak Jul 22, 2026
1a44001
test(e2e): persistence browser-refresh durability spec
AlemTuzlak Jul 22, 2026
5a187d2
ci: apply automated fixes
autofix-ci[bot] Jul 22, 2026
4937235
feat(ai-client): message-storage lever for the persistence option
AlemTuzlak Jul 23, 2026
2367ad2
docs(persistence): explain durability + persistence as one story
AlemTuzlak Jul 23, 2026
d18072b
feat(ai-client): zero-config storage adapters + threadId-keyed persis…
AlemTuzlak Jul 23, 2026
bdb112b
feat(ai-persistence): reconstructChat server helper
AlemTuzlak Jul 23, 2026
98e6878
docs(persistence): recommended setup, zero-config adapters, header-ac…
AlemTuzlak Jul 23, 2026
ac34676
ci: apply automated fixes
autofix-ci[bot] Jul 23, 2026
ad02850
docs(persistence): consolidate client persistence into one page
AlemTuzlak Jul 23, 2026
c11994b
docs(persistence): lead client-persistence with both use-cases
AlemTuzlak Jul 23, 2026
f004d46
fix(ai-client): keep host initialMessages in messages:false mode
AlemTuzlak Jul 23, 2026
7bc79fd
ci: apply automated fixes
autofix-ci[bot] Jul 23, 2026
428b4c3
feat(ai-persistence)!: rename withChatPersistence to withPersistence
AlemTuzlak Jul 23, 2026
1700669
ci: apply automated fixes
autofix-ci[bot] Jul 23, 2026
9eac120
fix(persistence): reconcile with current @tanstack/ai core
AlemTuzlak Jul 23, 2026
93f3a75
test(persistence): unskip two-phase approval->client-tool resume
AlemTuzlak Jul 23, 2026
26d6a36
docs(example): ts-react-chat persistent-chat uses recommended server-…
AlemTuzlak Jul 23, 2026
38d518f
feat(persistence-prisma): support Prisma 7 alongside Prisma 6
AlemTuzlak Jul 23, 2026
d55b1be
fix(persistence): persist the terminal assistant reply on finish
AlemTuzlak Jul 23, 2026
7bcf775
docs(example): add server tools and a styled UI to persistent-chat
AlemTuzlak Jul 23, 2026
9b66b1e
feat(persistence): seamless resume of a mid-stream reload
AlemTuzlak Jul 23, 2026
64c7bdd
fix(ai-react): don't abort an in-flight rejoin on mount
AlemTuzlak Jul 23, 2026
6f12ce3
fix(persistence): fast, robust, repeatable reload rejoin
AlemTuzlak Jul 23, 2026
d2d08e8
fix(persistence): review blockers and CodeRabbit follow-ups
tombeckenham Jul 24, 2026
f8594e3
ci: apply automated fixes
autofix-ci[bot] Jul 24, 2026
d64dfff
docs(persistence): drop PR number references from Scope docs
tombeckenham Jul 24, 2026
bd243e1
feat(ai-client): tail an in-flight run on a fresh client via initialR…
AlemTuzlak Jul 24, 2026
9426796
feat(ai-persistence-drizzle): schema-first ownership, Postgres provid…
tombeckenham Jul 24, 2026
6470807
feat(persistence): thread-keyed automatic reconnect (no client run po…
AlemTuzlak Jul 24, 2026
470c37f
example(ts-react-chat): multi-thread persistent chat + full-screen; d…
AlemTuzlak Jul 24, 2026
922eb02
fix(ai): memoryStream delivers the full agent-loop run, not just the …
AlemTuzlak Jul 24, 2026
c2c96b9
example(ts-react-chat): show reasoning in persistent chat (effort low)
AlemTuzlak Jul 24, 2026
e87998d
test(e2e): guard full agent-loop delivery through durable streams
AlemTuzlak Jul 24, 2026
83f3c5c
example(ts-react-chat): durable tool-approval interrupt in persistent…
AlemTuzlak Jul 24, 2026
0a3be05
feat(persistence): server-authoritative interrupt reconstruction; thr…
AlemTuzlak Jul 24, 2026
3f2c9fb
refactor(examples,docs): migrate approvals off deprecated addToolAppr…
AlemTuzlak Jul 24, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .changeset/client-browser-refresh-durability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@tanstack/ai-client': minor
'@tanstack/ai-react': minor
'@tanstack/ai-solid': minor
'@tanstack/ai-vue': minor
'@tanstack/ai-svelte': minor
'@tanstack/ai-angular': minor
'@tanstack/ai-preact': minor
---

Add browser-refresh durability to the `persistence` option.

The client `persistence` adapter now stores one combined record per chat id, the message transcript plus a resume snapshot, so a full page reload restores the conversation, rehydrates any pending interrupt, and rejoins a run that was still streaming (via `joinRun`, when the connection is durability-backed). A bare `UIMessage[]` from an older store is still read for backward compatibility.

The `persistence` option also accepts an object form, `{ store, messages?: boolean }`. `messages: false` caches only the tiny resume pointer (which run to rejoin, which interrupts are pending), keeping large transcripts off the client, durability rejoin and interrupt restore still work, and the server stays authoritative for history. A bare adapter is shorthand for `{ store, messages: true }`.

New web storage adapters are exported for this: `localStoragePersistence`, `sessionStoragePersistence`, and `indexedDBPersistence` (plus `StorageUnavailableError` and the `ChatPersistedState` / `ChatStorageAdapter` / `ChatPersistenceConfig` / `ChatPersistenceOption` types). Because durability rides the existing `persistence` option, every framework integration (`react`, `solid`, `vue`, `svelte`, `angular`, `preact`) gets it with no framework-specific code.
23 changes: 23 additions & 0 deletions .changeset/fast-fail-rejoin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@tanstack/ai': minor
'@tanstack/ai-client': patch
---

Make a reload rejoin fast, robust, and repeatable.

- **`memoryStream` first-chunk deadline now defaults to 100ms** (was 30s). The
common from-start join is a reload rejoining a run whose producer ran in a
prior request: an in-flight run's log already holds chunks (it streams
immediately, the deadline never applies), and an empty log means the run is
gone — so failing fast lets the client re-enable input near-instantly instead
of holding a dead connection open. Raise `firstChunkDeadlineMs` for a backend
whose producer can legitimately start well after a joiner attaches.
- **`ChatClient` reload rejoin hardened:** it bounds the wait for the first
chunk and clears a dead resume pointer (so a stale pointer can't pin the UI in
a loading state and can't be retried on the next load); it drops the hydrated
in-flight partial only when real content arrives (never on `RUN_STARTED`
alone), so a rejoin that connects but delivers nothing can't leave an empty
assistant bubble; and it no longer lets a replayed `RUN_STARTED` (which
carries the provider run id) overwrite the persisted resume pointer with an id
the durability log isn't keyed by — so a SECOND consecutive reload still
re-attaches and continues.
41 changes: 41 additions & 0 deletions .changeset/fresh-client-tail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
'@tanstack/ai-persistence': minor
'@tanstack/ai-persistence-drizzle': minor
'@tanstack/ai-persistence-prisma': minor
'@tanstack/ai-client': minor
---

Server-authoritative reconnect is now automatic and keyed on the thread, not the run.

A chat's durable identity is its **thread**; run ids are ephemeral (a single turn
can span several runs via interrupts or tool continuations), so basing reconnect
on a client-cached run id goes stale the moment a turn rolls to a new run. This
moves the whole reconnect story onto the stable thread id, resolved by the server.

- **`RunStore.findActiveRun(threadId)`** — new optional, feature-detected store
method returning the most recent `'running'` run for a thread (implemented for
memory, drizzle/SQLite — which also covers Cloudflare D1 — and prisma).
- **`reconstructChat` now returns `{ messages, activeRun, interrupts }`** (was a
bare message array): the stored transcript as UI messages, a cursor to an
in-flight run if one exists, and any pending human-in-the-loop interrupts (tool
approvals / waits) plus the run they paused. It reads the active run before the
transcript so observing "no active run" guarantees the transcript is final
(closing a finish-window race).
- **`@tanstack/ai-client` hydrates itself on mount.** In `messages: false`
(server-authoritative) mode the client now caches no transcript and no run
pointer: on mount `useChat`/`ChatClient` calls the connection's new
`hydrate(threadId)` (a JSON GET against the same endpoint), paints the returned
transcript, and — if a run is in flight — tails it via the existing `joinRun`
durability replay. A reload and the same thread opened on another device are the
identical, server-resolved path. No loader, no `initialMessages`, no
`initialResumeSnapshot`, no app-side fetching required.
- **Interrupts reconstruct from the server too.** A paused approval (a tool with
`needsApproval`) is restored from `reconstructChat`'s `interrupts` exactly as a
persisted resume snapshot would be, so a reload — or another device — re-prompts
the same approve/reject decision and resumes the run it paused. Previously the
pending interrupt was only recoverable from client storage, so a fresh client
showed the paused tool call with no way to resolve it.

Apps keep the single GET endpoint they already have (durability replay when a
resume cursor is present, else `reconstructChat`); everything else is handled by
the hook.
22 changes: 22 additions & 0 deletions .changeset/memorystream-agent-loop-delivery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@tanstack/ai': patch
---

Fix `memoryStream` truncating a tool-calling (agent-loop) run at its first tool
call.

An agent-loop run emits one `RUN_STARTED`/`RUN_FINISHED` pair per iteration
(`finishReason: "tool_calls"` for a turn that calls a tool, then `"stop"` for the
final answer). `memoryStream` treated the _first_ terminal chunk as the end of
the log — both marking the log complete on append and ending the reader on read —
so a run that called a tool was delivered only up to that first `RUN_FINISHED`:
the tool result and everything after (the model's actual answer) never reached
the client, leaving the tool call stuck "running" and the reply missing, on the
initial stream and on any reconnect/reload.

Completion is now driven solely by the producer calling `close()` (which it does
on every exit — the documented `StreamDurability.close` contract, honored by
`toServerSentEventsResponse`/`resumeServerSentEventsResponse` and detached
producers). The reader tails across per-iteration terminals and ends when the
producer closes, so a tool-calling run is delivered in full — live, on rejoin,
and on a server-authoritative reload.
19 changes: 19 additions & 0 deletions .changeset/persistence-packages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@tanstack/ai-persistence': minor
'@tanstack/ai-persistence-drizzle': minor
'@tanstack/ai-persistence-prisma': minor
'@tanstack/ai-persistence-cloudflare': minor
---

Add server-side persistence for `chat()`: durable thread messages, run records, and interrupts.

`withPersistence(persistence)` is a chat middleware that stores the conversation transcript, tracks each run's status, and records interrupt state so a paused run (tool approval, client-tool execution, generic interrupt) survives a server restart. Point it at any backend that implements the store contract:

- `@tanstack/ai-persistence` — the store contracts, the `withPersistence` / `withGenerationPersistence` middleware, and an in-memory reference store (`memoryPersistence`) plus a conformance testkit.
- `@tanstack/ai-persistence-drizzle` — Drizzle-backed stores for SQLite and Postgres behind one `drizzlePersistence(db, { provider, schema })` entry (plus a `node:sqlite` convenience factory), sharing one bring-your-own-schema contract. Get the tables into your drizzle-kit journal by re-exporting the `/sqlite-schema` or `/pg-schema` subpath (stock tables, tracks upgrades) or by emitting an owned starter with the dialect-aware schema CLI (renames, extra columns, index tuning).
- `@tanstack/ai-persistence-prisma` — Prisma-backed stores with a models CLI that emits a provider-neutral schema fragment. Works with both Prisma 6 (`prisma-client-js`) and Prisma 7 (`prisma-client`): the client argument is typed structurally, so it accepts a client generated to any output path.
- `@tanstack/ai-persistence-cloudflare` — D1-backed stores (delegating to the Drizzle backend) plus a Durable-Object lock store for cross-instance locking.

Resume reconstruction is delegated to the chat engine: persistence records interrupts and gates new input on a thread with pending interrupts, while the engine rebuilds the resume tool state from the resume batch and the interrupt bindings carried in the (server-loaded) message history.

`reconstructChat(persistence, request)` is a server helper that returns a thread's stored messages as a JSON `Response`, so a server-authoritative client can hydrate its transcript on load from a one-line `GET` handler.
11 changes: 11 additions & 0 deletions .changeset/rejoin-not-aborted-on-mount.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@tanstack/ai-react': patch
---

Fix `useChat` aborting an in-flight delivery resume on mount. When `live` was
not enabled, the mount effect called `client.unsubscribe()` unconditionally,
which cancelled the shared in-flight stream — including the `joinRun` rejoin the
client had just started for a reloaded run. The result was a mid-stream reload
that caught up to the buffered point and then froze instead of continuing.
`useChat` now only tears down a subscription it actually started, so a reload
rejoins and streams the run through to completion.
21 changes: 21 additions & 0 deletions .changeset/seamless-reload-resume.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@tanstack/ai': minor
'@tanstack/ai-client': minor
'@tanstack/ai-persistence': minor
---

Make a mid-stream reload resume the same conversation cleanly.

- `withPersistence` now persists the pending turn at the start of a run (so a
reload during generation still shows the user's message), stamps each
assistant turn with its stream `messageId`, and accepts
`withPersistence(persistence, { snapshotStreaming: true })` to also persist the
in-progress reply on a throttled interval (`snapshotIntervalMs`, default
`1000`) for partial-output durability.
- `ModelMessage` gains an optional `id`; `modelMessagesToUIMessages` preserves
it, so a hydrated message keeps the same identity as its live stream.
- On reload, the chat client rebuilds an in-flight assistant turn from the
delivery log (replaying from the start and applying the buffered backlog in one
batch) instead of reconciling against the persisted partial, so the reload
shows one clean bubble that catches up and continues rather than a frozen or
duplicated partial.
32 changes: 32 additions & 0 deletions .changeset/usechat-threadid-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
'@tanstack/ai-react': minor
'@tanstack/ai-preact': minor
'@tanstack/ai-solid': minor
'@tanstack/ai-vue': minor
'@tanstack/ai-svelte': minor
'@tanstack/ai-client': minor
---

The chat hooks no longer take an `id` option — a hook's identity is its `threadId`.

`useChat` / `createChat` previously accepted a separate `id` that keyed client
persistence and named the devtools instance, defaulting to a framework
`useId()` when omitted. That meant persistence keyed on an ephemeral render-tree
id even when you passed a stable `threadId`, so a reload found nothing under the
thread's key.

Now the `threadId` is the single identity:

- The hooks drop the `id` option. Pass `threadId` to persist a conversation and
restore it on reload; omit it for an ephemeral chat.
- Persistence keys on `threadId` (unchanged in `ChatClient`, which already
resolved `id ?? threadId` — the hooks simply stop overriding it).
- `ChatClient.uniqueId` (the devtools instance id) now falls back to `threadId`
instead of a generated id, so a thread shows up in devtools under its own id.
- Changing `threadId` on a mounted `useChat` (react/preact/solid) now recreates
the client so the new thread takes effect; previously the change was ignored.

`ChatClient` still accepts `id` directly as a lower-level escape hatch for
keying storage separately from the wire thread; only the framework hooks drop it.

Migration: replace `useChat({ id })` with `useChat({ threadId })`.
153 changes: 0 additions & 153 deletions docs/chat/persistence.md

This file was deleted.

Loading
Loading