Skip to content

feat(persistence): client-side generation resume snapshot - #997

Draft
tombeckenham wants to merge 67 commits into
mainfrom
feat/generation-persistence-client
Draft

feat(persistence): client-side generation resume snapshot#997
tombeckenham wants to merge 67 commits into
mainfrom
feat/generation-persistence-client

Conversation

@tombeckenham

Copy link
Copy Markdown
Contributor

🎯 Changes

Stack 1 of 2. Split out of #987, rebuilt on the current feat/persistence-core (which dropped the packaged backends in 62c99754d). Stacked follow-up: the durable media-byte half.

Client-side generation persistence: a lightweight, read-only resume snapshot for media generation activities.

As a run streams, the client builds a GenerationResumeSnapshot — run identity, status, errors, and result metadata, but never the generated media bytes — and writes it to a persistence storage adapter. The option reuses the same ChatStorageAdapter contract as chat, so localStoragePersistence / sessionStoragePersistence / indexedDBPersistence work with no type argument.

const snapshots = localStoragePersistence({ keyPrefix: 'my-app:generation:' })
useGenerateImage({ connection, persistence: snapshots })

Generation hooks (useGenerateImage, useGenerateVideo, useGenerateAudio, useGenerateSpeech, useGeneration, useSummarize, useTranscription, plus the Solid/Vue/Svelte/Angular equivalents) accept persistence and initialResumeSnapshot, and expose resumeSnapshot / resumeState / pendingArtifacts / resultArtifacts.

There is no resume() action and no restart of provider work — generation still only begins when generate(...) is called. Reconnecting to an in-flight stream is the delivery layer's job (resumable streams, #955), wired via a durability adapter + GET handler exactly as for chat.

PersistedArtifactRef already exists on the base branch as dormant wire surface, so this half consumes it and is independently mergeable — the generation:artifacts event simply never fires until the stacked byte-storage PR lands.

Packages

  • @tanstack/ai-client — the resume-snapshot reducer, the persistence option, artifact refs.
  • @tanstack/ai-event-client — optional threadId / runId on generation events.
  • framework hooks — react / solid / vue / svelte / angular.
  • Docsdocs/persistence/generation-persistence.md, kiira-verified.

⚠️ Known issues — not yet fixed on this branch

Carried over from review of #987; tracked here rather than silently shipped.

  • Blocker: React StrictMode permanently disables generate(). dispose() sets disposed = true and nothing ever clears it, so after StrictMode's mount→cleanup→mount replay against the same memoized client every generate() call returns immediately. Reproduced under { wrapper: StrictMode }. The fix pattern already exists at devtools.ts:571. Affects generation-client.ts and video-generation-client.ts.
  • Docs claim the snapshot is "read back on load", but nothing calls persistence.getItem — apps must read storage themselves and pass initialResumeSnapshot. Either wire hydration (as chat does at client-persistor.ts:129) or rewrite the section.
  • The snapshot is rewritten to storage on every chunk, not just on material change — ~150 serializations for a long video run, plus one console.warn per chunk on quota errors.
  • The fetcher (non-streaming) path never records a snapshot at all.
  • getResumePersistenceError() is dead public API — zero call sites.
  • Chat and generation share the default tanstack-ai: key prefix, so a useChat({id}) and useGenerateImage({id}) on one adapter collide.
  • threadId / runId were added to 18 devtools event interfaces but no emit site populates them.
  • Needs a decision: this branch widens localStoragePersistence<TValue = ChatPersistedState> to <TValue = any> (3 oxlint suppressions) so the factories can be reused for generations. That degrades the already-shipped chat path; it arguably belongs in feat/persistence-core on its own merits.
  • No E2E coverage yet (CLAUDE.md requires it; a localStorage reload spec is straightforwardly expressible in Playwright).

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr. — ran the affected subset: build, test:types, test:lib, test:oxlint green across ai, ai-utils, ai-event-client, ai-client, ai-persistence + the four framework packages.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

AlemTuzlak and others added 30 commits July 24, 2026 10:43
…kends

Server-side persistence for chat(): durable thread messages, run records, and
interrupts via the withChatPersistence middleware, with pluggable backends.

- @tanstack/ai-persistence: store contracts, withChatPersistence /
  withGenerationPersistence middleware, memoryPersistence reference store,
  conformance testkit. Locks (LockStore/InMemoryLockStore/LocksCapability) live
  here rather than core; the sandbox-consumer bridge is deferred.
- -drizzle / -prisma / -cloudflare: backend store implementations + migration /
  schema / models CLIs. Cloudflare D1 delegates to the drizzle backend.

Reconciled against the shipped ephemeral-interrupt engine: the middleware
records interrupts and gates new input, and delegates resume-tool-state
reconstruction to the engine (resume batch + interrupt bindings in history).

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
The persistence adapter now stores one combined { messages, resume? } record
per chat id, so a full page reload restores the transcript, rehydrates pending
interrupts, and rejoins an in-flight run through joinRun when the connection is
durability-backed. Legacy bare-array records are still read.

Adds localStoragePersistence / sessionStoragePersistence / indexedDBPersistence
(+ StorageUnavailableError and the ChatPersistedState / ChatStorageAdapter
types). Durability rides the existing option, so every framework integration
gets it with no framework-specific code.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
New /persistent-chat route: useChat with localStoragePersistence on the client
and withChatPersistence(sqlitePersistence) on the server, so a full page reload
restores the conversation on both ends. Adds a nav link and README section.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
…ility

New docs/persistence section: overview, chat-persistence, browser-refresh,
controls, custom-stores, sql-backends, drizzle, prisma, cloudflare, migrations,
internals. Wires the nav and updates the client chat/persistence page for the
combined { messages, resume } record and built-in storage adapters.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
Update the agent skills for the new surface: withChatPersistence server
middleware and its backends, and the client browser-refresh durability
(combined persistence record, storage adapters, joinRun rejoin, all frameworks).

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
Provider-free durable harness route + client page + spec proving message
restore after reload and interrupt-survives-reload via localStorage. Mid-stream
joinRun rejoin is covered by ai-client unit tests and delivery-durability.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
Add the object form `persistence: { store, messages?: boolean }`. `messages:
false` caches only the tiny resume pointer, keeping large transcripts off the
client while durability rejoin and interrupt restore still work and the server
stays authoritative for history. A bare adapter remains shorthand for
`{ store, messages: true }`, so this is backward compatible and every framework
passthrough is unchanged.

The persistent-chat example gains a history branch on its GET route
(loadThread by threadId, distinct from the per-run delivery replay) so a
server-authoritative reload can hydrate the transcript. Docs + chat-experience
skill document the lever.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
Rewrite the persistence overview into a concept + decision page: the three
problems (dropped stream, lost-on-reload, no durable record), the two
independent layers (delivery durability vs state persistence), client vs server
halves, the reload/rehydration timeline, and a when-to-pick-each guide. Add a
back-link from the resumable-streams overview so the two sections cross-reference.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
…tence

localStoragePersistence / sessionStoragePersistence / indexedDBPersistence now
default their type parameter to ChatPersistedState and to a JSON codec, so
`persistence: localStoragePersistence()` needs no type argument and no
serialize/deserialize pair. Drops the IsJsonSerializable type gate that forced a
codec for the chat record (UIMessage already round-trips as JSON on the wire).

Client persistence now keys on `threadId` (the conversation identity), so a
reload with the same threadId restores the same record; `id` becomes an optional
storage-key override. The storage adapters and persistence types are re-exported
from every framework package, so a single import from @tanstack/ai-react (etc.)
works.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
reconstructChat(persistence, request) 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 instead of hand-rolling loadThread + Response.
…curate resume

Add a "What we recommend" section to the overview: client resume-pointer-only
plus server persistence plus one GET that rehydrates history and resumes durable
streams, with the reasoning. Update every snippet to the zero-config
localStoragePersistence() and threadId, use reconstructChat for history, and
discriminate the resume GET with durability.resumeFrom() instead of sniffing
query params (the run id rides the X-Run-Id header, the offset the Last-Event-ID
header). Example, e2e page, and chat-experience skill match.
Rename browser-refresh to client-persistence and make it the single home for
the client story: turning it on, what a reload restores, the two cache modes
(everything vs resume-pointer-only) with when to use each, and the three storage
backends with when to use each. Remove the legacy docs/chat/persistence page
(client content now lives in the persistence section) and repoint its links.

Make the other persistence docs server-only: drop the client rows from the
controls decision table and the browser-storage section from internals, leaving
a pointer to the client guide. The overview stays the cross-cutting map. Update
all cross-links, the chat-experience skill source, and the reconstructChat doc
reference.
In `{ messages: false }` mode a prior session's persisted record is
`{ messages: [], resume }`. The constructor treated that empty transcript as
authoritative and clobbered host-provided `initialMessages`, and the async
hydrate path applied `[]` on top, so a server-authoritative reload dropped the
history the app had fetched from the server.

The persisted transcript is now adopted only when the client actually caches it
(`cachesMessages`); in messages:false mode the client keeps `initialMessages`
and takes only the resume pointer from storage. This makes the recommended
server-authoritative flow work: on a mid-stream reload the app seeds history via
initialMessages (the reconstruct GET) while the client separately rejoins the
live run via joinRun (the resume GET), and the replayed run merges into the
seeded history by message id. Adds a test covering both together.

Also document in the overview that history hydration and run rejoin are two
separate GET requests, so the handler's if/else routes each and neither blocks
the other.
The primary chat persistence middleware is now `withPersistence`.
`withGenerationPersistence` is unchanged. Unreleased, so no alias is kept.
Updates all call sites, docs, skills, the example, and the changeset.

fix(ai-client): rejoin an in-flight run from an async persistence store

Auto-rejoin was gated on the synchronous read, so an async store
(indexedDBPersistence) restored messages and interrupts on reload but never
rejoined a mid-stream run. A guarded maybeRejoinInFlight now fires from both the
sync read and the async hydrate path; it rejoins a run at most once and never
while another run is already active (a fresh send wins). Adds a test that a
run rejoins from an async (Promise-returning) adapter.
A stale local build had masked real breakage: the ported persistence layer was
behind the current core. Reconciled against a clean build:

- withGenerationPersistence keys runs on `requestId` (the current
  GenerationMiddlewareContext has no runId/threadId).
- Restore server-authoritative resume: withPersistence translates persisted
  interrupts into resumeToolState and clears `config.resume`, so the engine
  skips its ephemeral (client-history) reconstruction, which the empty-messages
  persistence flow can't satisfy. (Reverts an incorrect earlier removal.)
- ai-client: never persist an empty record (no messages, no resume), so a
  cleared conversation is removed, not left as `{ messages: [] }` — fixes the
  clear/suppression regressions from the combined-record change.
- Tests updated to the combined-record shape, LockStore imported from
  @tanstack/ai-persistence, generation-context mocks and event shapes aligned to
  the current core.

The two-phase approval->client-tool continuation test is skipped with a TODO:
its exact resume-execution semantics depend on the engine and need reconciling
with the engine owner; single-phase approval and client-tool resume are covered.
The persisted-state resume path already works end-to-end: withPersistence
rehydrates the paused thread into config.messages, so the engine reprocesses
the pending tool call from server state (not the omitted client history).
Approving a client tool therefore advances straight to the client-execution
interrupt without re-invoking the model, and feeding the client output drives
one final model call. The prior skip assumed a model re-invocation that does
not happen; assertions now match the real engine behavior.
…authoritative persistence

Switch the demo to the recommended setup: `persistence: { store, messages: false }`
so the client caches only the resume pointer and the server (SQLite) owns history.
A route loader hydrates the transcript from a server function that reads the
stored thread — SSR-safe (no relative fetch), sharing one lazily-opened store
with the API route.
Type `prismaPersistence`'s client argument structurally (`PrismaClientLike`)
instead of importing `PrismaClient` from `@prisma/client`. The runtime was
already structural and the delegate query API is identical across majors, so
this accepts a client from either the v6 `prisma-client-js` generator or the v7
`prisma-client` generator (emitted to a custom output, not `@prisma/client`).
Docs note both versions.
withPersistence.onFinish saved ctx.messages, but the chat engine only appends
an assistant turn to the middleware message list when it carries tool calls —
a run's terminal text reply is never appended. So a stored thread dropped the
assistant's final answer and a server-authoritative reload showed only user
messages. Reattach the terminal reply from info.content (the last turn's
accumulated text), guarded against duplication. Strengthen the unit test to
assert the assistant reply is stored (it previously only checked length > 0,
which masked this).
Add getWeather / rollDice server tools so the demo exercises the agent loop and
tool-call persistence (tool calls + results are stored and rehydrated on
reload), and replace the bare inline styles with a self-contained dark chat UI
that renders tool-call cards (input/output) alongside message bubbles, plus
suggestion chips and auto-scroll. Regenerate routeTree.gen.ts to register the
persistent-chat routes.
Persist the pending turn at run start and stamp each assistant turn with its
stream messageId, add an opt-in `snapshotStreaming` for partial-output
durability, carry an optional `id` on ModelMessage that the converter preserves,
and rebuild an in-flight assistant from the delivery log on reload (batch-apply
the backlog, drop the hydrated partial) so a reload shows one clean bubble that
catches up and continues instead of a frozen or duplicated partial.

Verified via unit tests, a headless ChatClient rejoin (tails live + full-replays
a completed run), and curl against the join endpoint (replays + tails to
completion). Known follow-up: in the browser through the vite dev server the
long-lived SSE connection can drop mid-stream and resumableStream's reconnect
does not always tail to completion after catch-up; server + client logic are
correct (Node/curl tail fine), so this is a transport/reconnect issue to close
against a production-style server.
useChat's `[client, live]` effect called `client.unsubscribe()` on every mount
when `live` was false, which cancels the shared in-flight stream — aborting the
delivery resume the client constructor had just started for a reloaded run. A
mid-stream reload therefore caught up to the buffered point and froze. Only tear
down a subscription we actually started, so the rejoin streams to completion.

Verified end-to-end in a real browser (reload mid-stream now grows continuously
to completion in one clean bubble) with a regression test that fails without the
fix (the rejoined run never reaches the message list).
- memoryStream first-chunk deadline defaults to 100ms (was 30s): a reload
  rejoins a run produced in a prior request, so an empty log means gone —
  fail fast instead of holding a dead connection ~30s. Raise
  firstChunkDeadlineMs for producers that start well after a joiner attaches.
- ChatClient rejoin: bound the first-chunk wait + clear a dead pointer (no UI
  pinned loading, no retry-on-next-load); drop the hydrated partial only on real
  content (never RUN_STARTED alone) so a no-content rejoin can't leave an empty
  bubble; and stop a replayed RUN_STARTED (provider run id) from overwriting the
  persisted pointer with an id the log is not keyed by, so a SECOND reload still
  re-attaches.

Verified in a real browser: reload#1 and reload#2 both continue to completion in
one clean bubble; a dead/evicted pointer frees the input in ~380ms (was 30s);
no empty-bubble corruption. Unit tests cover the pointer-preservation and the
100ms default.
Clear resume-only storage on finish, stop StrictMode remount from aborting
rejoin, add reconstructChat authorize + no-store, harden onFinish write order,
and address open review nits (joinRun typeof, interrupt sort, metadata keys,
lock chain cleanup, workspace:* peers, Scope docs/re-export).
tombeckenham and others added 23 commits July 25, 2026 15:12
…n pg schemas

Restructure the Drizzle package into shared stores (core/), SQLite sources of
truth (sqlite/), and Postgres projections (pg/). Generate PG default schemas
and CLI assets from SQLite via a TypeScript codegen script so dialect table
defs stay in lockstep without forking store logic.
Stop shipping SQL, the migrations CLI, and d1Migrations. D1 state stays a thin
Drizzle wrapper; schema DDL follows the same schema-first path as SQLite
(drizzle-kit + Wrangler migrations_dir). Keep Durable Object locks and the
cloudflarePersistence / createD1Stores convenience API.
Add agent skills covering server/client state persistence, packaged
backends, custom stores, and locks, and route them from ai-core,
chat-experience, and middleware skills.
Chat client identity is threadId; the smoke App still passed id, which
fails test:pr typecheck after the persistence identity rename.
- Regenerate drizzle pg schema artifacts so codegen:pg --check is clean
- Default DATABASE_URL in persistent-chat-prisma prisma.config.ts so
  `prisma generate` works without a local .env (CI / clean checkouts)
…apter guide

The Drizzle, Prisma, Cloudflare, SQL Backends, and Custom Stores pages framed
adapters as packages to install. Persistence is a contract, so document the
contract instead: one guide to the four store interfaces and their invariants,
plus three adapter-building skills shipped in @tanstack/ai-persistence for the
Drizzle, Prisma, and Cloudflare recipes.

ts-react-chat now demonstrates the result — its persistent-chat demo runs on a
self-contained node:sqlite backend built on the core contracts and verified by
the shared conformance testkit, instead of depending on
@tanstack/ai-persistence-drizzle.

Co-authored-by: Cursor <cursoragent@cursor.com>
The ai-core skill tree carried a six-file persistence suite that taught
"install @tanstack/ai-persistence-drizzle" — the opposite of the direction the
docs took when the per-backend pages became one build-your-own-adapter guide.
Persistence is a contract, so the skills that teach it belong to the package
that owns the contract.

@tanstack/ai now mentions the library and what it makes possible, then routes
to the package's own skills (the pattern already used for @tanstack/ai-code-mode).
The suite moves to @tanstack/ai-persistence following the ai-memory layout: an
umbrella entry point plus -server, -client, -stores, -locks, and the three
existing -build-*-adapter recipes. ai-core/persistence/backends is dropped
rather than moved; its still-true content folded into -stores.

Corrections along the way, all verified against source:

- skip: ['locks'] is not assignable to Array<keyof AIPersistenceStores> and the
  conformance suite covers no locks at all. Removed from the drizzle and prisma
  skills, the guide, and the example test.
- Locks were documented as a fifth store. stores accepts exactly four keys and
  throws on anything else, so the guide's stores list, its "all five stores"
  count, and the LockStore entry under the store-interface reference are fixed.
- The cloudflare skill imported the two packages it exists to replace, composed
  locks through composePersistence (rejected at compile time and runtime), told
  you to include locks in the conformance run, and described a migrations CLI
  deleted in 14f6cfa. Rewritten around raw D1 stores and an own DO lock class.
- The drizzle skill's migrations section described bundled .sql assets and a
  tanstack-ai-drizzle-migrations CLI that do not exist; it is schema-first now,
  and the Postgres recipe it was missing is documented.
- sources: frontmatter no longer cites the five doc pages c5f391e deleted.
- metadata's first argument is namespace, matching types.ts.

@tanstack/ai-persistence was missing the tanstack-intent keyword, so intent
install could not discover its skills at all. Added, and the docs now show the
install path instead of "ask your AI assistant for the skill".

Also fixes the two type errors the example carried: the factory annotated its
return as bare AIPersistence (the all-optional bag, which withPersistence
rejects) and passed the invalid skip key.
Drop @tanstack/ai-persistence-drizzle, -prisma, and -cloudflare, and the two
examples built on them.

Persistence is a contract. The core never inspects your tables, so a packaged
backend buys an application very little beyond a schema it did not choose — and
costs it a dependency, a migration story, and a schema owner that is not the
app. Every packaged backend was also a second place for the store invariants to
drift from the ones the middleware actually relies on.

What ships instead is the thing that was always doing the work:

- the four store contracts and their invariants,
- withPersistence / withGenerationPersistence and reconstructChat,
- memoryPersistence() as the in-process reference backend,
- LockStore / withLocks for coordination,
- and the conformance testkit, which is the real compatibility gate — point it
  at your factory and it holds you to the same rules every backend was held to.

Applications implement the stores against the database they already run.
examples/ts-react-chat does exactly that on a self-contained node:sqlite
adapter, verified by the shared testkit, and the Build Your Own Adapter guide
walks the same path end to end. The Drizzle, Prisma, and Cloudflare recipes
survive as Agent Skills in @tanstack/ai-persistence, so the knowledge is still
shipped — as a recipe you own rather than a package you depend on.

Also drops the prisma/better-sqlite3 build-script allowances and the sherif
prisma ignores, which existed only for the deleted packages, and fixes three
useChat({ id }) call sites in ts-code-mode-web and ts-react-native-chat that
the hook's threadId-identity change left behind.
The three -build-*-adapter skills read as "build a publishable adapter
package": peer deps, dual edge-safe/Node entry points, dialect overloads, a
schema-emitting CLI, BYO-schema validation. None of that survives the move to
one file in a consumer's app, which is what someone adding persistence
actually wants.

Each recipe now opens by reading the app it is running in — dialect, schema
file, db handle lifetime, migration flow, naming conventions, import alias —
appends the four tables to the schema that already exists, and produces a
single src/lib/chat-persistence.ts against the existing client. The package
material is demoted to a short "only if you are publishing this as a package"
tail. Drizzle and Prisma carry the complete file; Cloudflare leads with
per-request bindings and routes to the Drizzle recipe when the Worker already
uses it.

Adds -build-custom-adapter for the stacks with no dedicated recipe (raw pg,
Kysely, node:sqlite, Mongo, Supabase, Redis): the five engine-independent
invariants, a worked pg example, and per-engine notes. The recipes are sliced
by access layer, not by engine, so there is no SQLite skill — SQLite is
already the default dialect in three of the four.

Fixes carried through every recipe: metadata columns are named `namespace` to
match the MetadataStore argument, and findActiveRun was missing everywhere.

Docs: the persistence overview now says to install @tanstack/ai-persistence
and run `intent install` before writing any of it — the package install
previously appeared exactly once in the whole docs tree, buried at the bottom
of build-your-own-adapter. Chat persistence gets the same install block, and
the adapter guide's recipe section explains what the skills now produce.
The two failing chat-persistence specs were one bug, in the harness rather
than the library. This branch changed what a client `persistence` adapter's
`setItem` receives — the combined `{ messages, resume? }` record instead of a
bare `UIMessage[]` — and the e2e page's hand-rolled adapter was updated on
neither side: it wrote the new object, then fed it to a reader that throws on
anything but an array. Adapter reads are best-effort, so the throw was
swallowed and the transcript silently never restored. The clear() spec kept
passing because it asserts emptiness after reload, which holds whether
persistence works or is entirely dead.

Any user with a hand-rolled adapter hits the same silent failure on upgrade:
`getItem` has a back-compat path for reading legacy arrays, `setItem` has none
for writing them. The durability changeset now carries that migration note, and
client-persistence.md gains a "Writing your own" section with the correct
round-trip and a warning that a wrong-shape parse fails silently.

Separately, three packages shipped skills that TanStack Intent could never
load. Intent scans node_modules for the `tanstack-intent` keyword and can only
see what npm publishes; @tanstack/ai-mcp wrote its skill into a directory
missing from `files` (never published at all), while @tanstack/ai-memory and
@tanstack/ai-sandbox published theirs without the keyword. All three now match
the packages that already worked.

The client persistence skill also moves to @tanstack/ai as
ai-core/client-persistence. It teaches localStoragePersistence and the
`persistence` option on useChat — framework-package code, not
@tanstack/ai-persistence code — so an app persisting only in the browser never
installed the package that carried its guidance, and ai-core routed to a path
that did not exist on disk. Skills follow the code that owns them.
Layer a lightweight, read-only resume snapshot onto media generation.
As a run streams, the client builds a GenerationResumeSnapshot (run
identity, status, errors, result metadata + artifact refs — never media
bytes) and writes it to an optional GenerationServerPersistence store.

- ai-client: GenerationResumeSnapshot types + updateGenerationResumeSnapshot
  reducer; GenerationClient/VideoGenerationClient observe chunks, persist
  snapshots (serialized queue, warn-not-throw), expose getResumeSnapshot();
  disposed guard. No resume() action (stream re-attach is PR #955).
- ai-event-client: optional threadId/runId on generation events.
- react/solid/vue/svelte/angular hooks: persistence + initialResumeSnapshot
  options; expose resumeSnapshot/resumeState (+ pending/result artifacts).
- example: Persisted mode on the image generation route.
- docs: persistence/generation-persistence.md + nav entry.

Pairs with the existing withGenerationPersistence server middleware.
Drop the bespoke `GenerationServerPersistence` type and the `{ server }`
option wrapper. The `persistence` option is now a bare storage adapter
reusing the shared `ChatStorageAdapter` contract (aliased as
`GenerationPersistence`), so `localStoragePersistence` /
`sessionStoragePersistence` / `indexedDBPersistence` work for generations
exactly as they do for chat — matching main's ergonomics.
Default `localStoragePersistence` / `sessionStoragePersistence` /
`indexedDBPersistence` to a value-agnostic `TValue` so a bare, unannotated
call works for BOTH chat and generation persistence — the consuming
`persistence` option constrains the stored value. Generation docs/example now
use `localStoragePersistence({ keyPrefix })` with no type declaration.
PR #955 (resumable streams) is merged, so delivery durability is available
today — it was wrongly described as an unlanded future feature. Rewrite the
generation-persistence doc: the server example now wires a durability adapter
+ GET handler, and the delivery section explains that a dropped mid-generation
connection re-attaches through the same adapters useChat uses. Clarify that the
read-only snapshot carries run state (incl. runId) across reloads, while
hooks do not auto-resume on mount.
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f6976ec2-b2ea-4d7e-8dce-fd8a87a3df3a

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/generation-persistence-client

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

16 package(s) bumped directly, 34 bumped as dependents.

🟥 Major bumps

Package Version Reason
@tanstack/ai-angular 0.3.1 → 1.0.0 Changeset
@tanstack/ai-durable-stream 0.0.0 → 1.0.0 Changeset
@tanstack/ai-memory 0.0.0 → 1.0.0 Changeset
@tanstack/ai-openrouter 0.15.10 → 1.0.0 Changeset
@tanstack/ai-persistence 0.0.0 → 1.0.0 Changeset
@tanstack/ai-preact 0.11.1 → 1.0.0 Changeset
@tanstack/ai-react 0.18.1 → 1.0.0 Changeset
@tanstack/ai-sandbox 0.2.4 → 1.0.0 Changeset
@tanstack/ai-solid 0.15.1 → 1.0.0 Changeset
@tanstack/ai-svelte 0.15.1 → 1.0.0 Changeset
@tanstack/ai-vue 0.15.1 → 1.0.0 Changeset
@tanstack/ai-acp 0.2.3 → 1.0.0 Dependent
@tanstack/ai-anthropic 0.16.3 → 1.0.0 Dependent
@tanstack/ai-bedrock 0.1.4 → 1.0.0 Dependent
@tanstack/ai-claude-code 0.2.3 → 1.0.0 Dependent
@tanstack/ai-code-mode 0.3.8 → 1.0.0 Dependent
@tanstack/ai-code-mode-skills 0.3.11 → 1.0.0 Dependent
@tanstack/ai-codex 0.2.3 → 1.0.0 Dependent
@tanstack/ai-elevenlabs 0.2.34 → 1.0.0 Dependent
@tanstack/ai-fal 0.9.12 → 1.0.0 Dependent
@tanstack/ai-gemini 0.20.1 → 1.0.0 Dependent
@tanstack/ai-grok 0.14.9 → 1.0.0 Dependent
@tanstack/ai-grok-build 0.2.3 → 1.0.0 Dependent
@tanstack/ai-groq 0.5.3 → 1.0.0 Dependent
@tanstack/ai-isolate-node 0.1.47 → 1.0.0 Dependent
@tanstack/ai-isolate-quickjs 0.1.47 → 1.0.0 Dependent
@tanstack/ai-mistral 0.2.3 → 1.0.0 Dependent
@tanstack/ai-ollama 0.8.16 → 1.0.0 Dependent
@tanstack/ai-openai 0.17.1 → 1.0.0 Dependent
@tanstack/ai-opencode 0.2.3 → 1.0.0 Dependent
@tanstack/ai-react-ui 0.8.15 → 1.0.0 Dependent
@tanstack/ai-sandbox-cloudflare 0.2.4 → 1.0.0 Dependent
@tanstack/ai-sandbox-daytona 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-docker 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-local-process 0.2.0 → 1.0.0 Dependent
@tanstack/ai-sandbox-sprites 0.2.1 → 1.0.0 Dependent
@tanstack/ai-sandbox-vercel 0.2.0 → 1.0.0 Dependent
@tanstack/ai-solid-ui 0.7.14 → 1.0.0 Dependent
@tanstack/openai-base 0.9.9 → 1.0.0 Dependent

🟨 Minor bumps

Package Version Reason
@tanstack/ai 0.42.0 → 0.43.0 Changeset
@tanstack/ai-client 0.22.1 → 0.23.0 Changeset
@tanstack/ai-devtools-core 0.4.24 → 0.5.0 Changeset
@tanstack/ai-event-client 0.6.8 → 0.7.0 Changeset

🟩 Patch bumps

Package Version Reason
@tanstack/ai-mcp 0.2.5 → 0.2.6 Changeset
@tanstack/ai-isolate-cloudflare 0.2.38 → 0.2.39 Dependent
@tanstack/ai-vue-ui 0.2.34 → 0.2.35 Dependent
@tanstack/preact-ai-devtools 0.1.67 → 0.1.68 Dependent
@tanstack/react-ai-devtools 0.2.67 → 0.2.68 Dependent
@tanstack/solid-ai-devtools 0.2.67 → 0.2.68 Dependent
ag-ui 0.0.2 → 0.0.3 Dependent

@nx-cloud

nx-cloud Bot commented Jul 26, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution ↗ for commit 34c8b69

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 1m 55s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-26 23:12:12 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jul 26, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@997

@tanstack/ai-acp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-acp@997

@tanstack/ai-angular

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-angular@997

@tanstack/ai-anthropic

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-anthropic@997

@tanstack/ai-bedrock

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-bedrock@997

@tanstack/ai-claude-code

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-claude-code@997

@tanstack/ai-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-client@997

@tanstack/ai-code-mode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode@997

@tanstack/ai-code-mode-skills

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-code-mode-skills@997

@tanstack/ai-codex

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-codex@997

@tanstack/ai-devtools-core

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-devtools-core@997

@tanstack/ai-durable-stream

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-durable-stream@997

@tanstack/ai-elevenlabs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-elevenlabs@997

@tanstack/ai-event-client

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-event-client@997

@tanstack/ai-fal

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-fal@997

@tanstack/ai-gemini

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-gemini@997

@tanstack/ai-grok

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok@997

@tanstack/ai-grok-build

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-grok-build@997

@tanstack/ai-groq

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-groq@997

@tanstack/ai-isolate-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-cloudflare@997

@tanstack/ai-isolate-node

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-node@997

@tanstack/ai-isolate-quickjs

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-isolate-quickjs@997

@tanstack/ai-mcp

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mcp@997

@tanstack/ai-memory

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-memory@997

@tanstack/ai-mistral

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-mistral@997

@tanstack/ai-ollama

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-ollama@997

@tanstack/ai-openai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openai@997

@tanstack/ai-opencode

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-opencode@997

@tanstack/ai-openrouter

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-openrouter@997

@tanstack/ai-persistence

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-persistence@997

@tanstack/ai-preact

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-preact@997

@tanstack/ai-react

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react@997

@tanstack/ai-react-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-react-ui@997

@tanstack/ai-sandbox

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox@997

@tanstack/ai-sandbox-cloudflare

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-cloudflare@997

@tanstack/ai-sandbox-daytona

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-daytona@997

@tanstack/ai-sandbox-docker

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-docker@997

@tanstack/ai-sandbox-local-process

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-local-process@997

@tanstack/ai-sandbox-sprites

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-sprites@997

@tanstack/ai-sandbox-vercel

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-sandbox-vercel@997

@tanstack/ai-solid

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid@997

@tanstack/ai-solid-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-solid-ui@997

@tanstack/ai-svelte

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-svelte@997

@tanstack/ai-utils

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-utils@997

@tanstack/ai-vue

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue@997

@tanstack/ai-vue-ui

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai-vue-ui@997

@tanstack/openai-base

npm i https://pkg.pr.new/TanStack/ai/@tanstack/openai-base@997

@tanstack/preact-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/preact-ai-devtools@997

@tanstack/react-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/react-ai-devtools@997

@tanstack/solid-ai-devtools

npm i https://pkg.pr.new/TanStack/ai/@tanstack/solid-ai-devtools@997

commit: 34c8b69

Base automatically changed from feat/persistence-core to main July 27, 2026 16:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants