Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
b3fe626
feat(persistence): client-side generation persistence
AlemTuzlak Jul 23, 2026
5d4c721
docs(persistence): drop generic from generation snapshot store example
AlemTuzlak Jul 23, 2026
5813491
refactor(persistence): align generation persistence API with chat
AlemTuzlak Jul 23, 2026
ad704da
refactor(persistence): infer generation store type via GenerationPers…
AlemTuzlak Jul 23, 2026
19a9fe9
refactor(persistence): value-agnostic web-storage adapter defaults
AlemTuzlak Jul 23, 2026
415627a
docs(persistence): fix stale generation-persistence delivery guidance
AlemTuzlak Jul 23, 2026
4a2b345
docs(persistence): rewrite generation-persistence for clarity + when-…
AlemTuzlak Jul 23, 2026
1398899
docs(persistence): drop redundant storage-adapter comment
AlemTuzlak Jul 23, 2026
946fb16
fix(persistence): generation snapshot lifecycle, hydration, StrictMod…
tombeckenham Jul 28, 2026
81aa215
fix(persistence): docs, example, changeset, React hooks, and real tes…
tombeckenham Jul 28, 2026
eae47bb
test(persistence): E2E reload-and-rehydrate spec for generation snaps…
tombeckenham Jul 28, 2026
72220ea
docs(skills): cover generation resume snapshots in client-persistence…
tombeckenham Jul 28, 2026
d668114
fix(persistence): framework sweeps for Solid/Vue/Svelte/Angular + hyd…
tombeckenham Jul 28, 2026
a92bd99
ci: apply automated fixes
autofix-ci[bot] Jul 28, 2026
65269ea
refactor(ai-react): thread TInput through UseGenerationReturn, drop g…
tombeckenham Jul 28, 2026
bdb2e89
refactor: thread TInput through generation return types in solid/vue/…
tombeckenham Jul 28, 2026
d1a56aa
refactor(persistence): restore typed storage-adapter defaults (drop T…
tombeckenham Jul 28, 2026
a9c4c19
feat(persistence): durable generation media-byte storage
AlemTuzlak Jul 28, 2026
a9421b9
feat(persistence): two-mode generation persistence + GenerationJobStore
AlemTuzlak Jul 28, 2026
ff17c96
ci: apply automated fixes
autofix-ci[bot] Jul 28, 2026
37a615a
docs(persistence): route readers to generation persistence + split by…
AlemTuzlak Jul 28, 2026
336ec96
refactor(generation-persistence): restore transparently into the norm…
AlemTuzlak Jul 28, 2026
11db2e0
ci: apply automated fixes
autofix-ci[bot] Jul 28, 2026
9c7f3dc
docs(persistence): slim the generation page, move advanced material t…
AlemTuzlak Jul 28, 2026
f976c90
feat(generation-persistence): rejoin an in-flight run on mount (useCh…
AlemTuzlak Jul 28, 2026
709032e
ci: apply automated fixes
autofix-ci[bot] Jul 28, 2026
61b8d07
docs(persistence): remove the generation-persistence advanced page
AlemTuzlak Jul 28, 2026
7fe9796
feat(examples): shared generation run history + fix stale generation-…
tombeckenham Jul 29, 2026
3ece807
refactor(persistence): rename generation job to run (GenerationRunSto…
tombeckenham Jul 29, 2026
9e4f1bb
docs: explain threads, runs, and turns across streaming, interrupts, …
tombeckenham Jul 29, 2026
20d5942
docs: narrow streaming guide to threads and runs, cross-link from per…
tombeckenham Jul 29, 2026
b6b9fa6
refactor(examples): drop generation run history, switch image/video t…
tombeckenham Jul 29, 2026
3df8aba
ci: apply automated fixes
autofix-ci[bot] Jul 29, 2026
7d87a30
fix(persistence): don't fetch caller-supplied prompt URLs + review fixes
tombeckenham Jul 29, 2026
951030f
refactor(persistence): rename artifact externalUrl to sourceUrl
tombeckenham Jul 29, 2026
9c73d86
feat(generation-persistence): require threadId when persistence is on
tombeckenham Jul 29, 2026
2358c1d
fix(persistence): fail loudly when a threadId lookup needs findLatest…
tombeckenham Jul 29, 2026
14b8d00
feat(persistence): storageKey for blob paths, and blobKey on the record
tombeckenham Jul 29, 2026
4c0a9b4
feat(persistence): server-side generation persistence in the example;…
tombeckenham Jul 29, 2026
08ab3f7
fix(example): keep generation persistence across HMR re-evaluation
tombeckenham Jul 29, 2026
ec23ae3
feat(persistence): sqlite generation stores + conformance coverage
tombeckenham Jul 29, 2026
a94ef8c
feat(example): server-side generation persistence on every activity
tombeckenham Jul 29, 2026
1ac59f1
fix(example): make generation routes resumable so a refresh can rejoin
tombeckenham Jul 29, 2026
b9e726b
fix(example): let nitro's dev middleware serve /api to subresources
tombeckenham Jul 29, 2026
420a9da
feat(persistence): server-driven generation persistence over server f…
tombeckenham Jul 29, 2026
bb24b70
Merge remote-tracking branch 'origin/main' into feat/generation-persi…
tombeckenham Jul 29, 2026
02d9ad1
fix(ai): apply result transforms and carry identity in streaming gene…
tombeckenham Jul 29, 2026
740dea2
feat(persistence)!: require threadId on withGenerationPersistence
tombeckenham Jul 29, 2026
7061f14
fix(generation): make runs survive client disconnect and resume mid-run
AlemTuzlak Jul 29, 2026
beca80d
feat(example): persistent-generation route; rely on library durability
AlemTuzlak Jul 29, 2026
11f98a2
docs(resumable-streams): durable producer survives a client disconnect
AlemTuzlak Jul 29, 2026
c424b4b
ci: apply automated fixes
autofix-ci[bot] Jul 29, 2026
281e241
fix(docs): generation hydration server fn returns a Response so kiira…
AlemTuzlak Jul 29, 2026
f9001e3
test(e2e): durable generation survives a mid-run reload
AlemTuzlak Jul 29, 2026
69c1d5f
test(e2e): make the mid-run rejoin deterministic and self-proving
AlemTuzlak Jul 29, 2026
8df68ca
feat(hooks)!: expose runId instead of resumeState
AlemTuzlak Jul 30, 2026
7f48d80
docs(persistence): add the id map, and use lists over run-on paragraphs
AlemTuzlak Jul 30, 2026
2a9be2a
docs: drop em dashes from the prose added in this branch
AlemTuzlak Jul 30, 2026
f78ba5a
docs(persistence): open the adapter guide with a store picker table
AlemTuzlak Jul 30, 2026
daa9fef
refactor(ai-persistence)!: one status vocabulary for both run stores
AlemTuzlak Jul 30, 2026
1e5466e
fix(ai)!: take the generation scope from the activity, not the middle…
AlemTuzlak Jul 30, 2026
e68a15f
refactor(generation)!: drop client-side generation persistence
AlemTuzlak Jul 30, 2026
caa3e52
fix(generation): persist image server-fns; deprecate dual id
tombeckenham Jul 30, 2026
4bd1a4d
ci: apply automated fixes
autofix-ci[bot] Jul 30, 2026
bcd465c
refactor(persistence)!: require threadId on a generation run record
tombeckenham Jul 31, 2026
a6e36a4
fix(generation): close the review findings across persistence, client…
tombeckenham Jul 31, 2026
8f2305e
ci: apply automated fixes
autofix-ci[bot] Jul 31, 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
28 changes: 28 additions & 0 deletions .changeset/artifact-persistence-options-rename.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'@tanstack/ai-persistence': minor
---

The artifact options for `withGenerationPersistence` are now named
`ArtifactPersistenceOptions`.

They were declared as a second `export interface WithPersistenceOptions`, which
TypeScript merged with the chat middleware's options of the same name. The merge
was invisible but not harmless: `withPersistence(chat, …)` silently accepted
`extractArtifacts` / `storageKey` / `allowInputUrl` / `artifactFetch`, and
`WithGenerationPersistenceOptions` β€” which extends it β€” advertised
`snapshotStreaming` / `snapshotIntervalMs`. Every one of those is a no-op on the
other middleware, so autocomplete offered options that did nothing.

`WithPersistenceOptions` keeps its meaning: the chat middleware's options.
`WithGenerationPersistenceOptions` is unchanged in shape and is still what you
pass to `withGenerationPersistence`, so only code that named the artifact
interface directly needs an edit:

```diff
-import type { WithPersistenceOptions } from '@tanstack/ai-persistence'
-function artifactOptions(): WithPersistenceOptions {
+import type { ArtifactPersistenceOptions } from '@tanstack/ai-persistence'
+function artifactOptions(): ArtifactPersistenceOptions {
return { storageKey: ({ runId, artifactId }) => `media/${runId}/${artifactId}` }
}
```
20 changes: 20 additions & 0 deletions .changeset/client-declined-generation-restore.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@tanstack/ai-client': minor
---

A restored generation whose result can't be rebuilt now reports an error instead
of repainting as a blank success.

Every `reconstructResult` mapper in `generation-reconstruct.ts` (and the video
client's built-in `reconstructVideoResult`) returns `null` when the persisted
record lacks what it needs β€” most commonly an output artifact stored without a
serve `url`, which is possible because `artifactUrl` is optional server-side.
`repaintFromSnapshot` silently skipped `setResult` in that case, leaving
`status: 'success'` with `result: null`: a state no consumer can render, and one
that hides the real cause.

When a mapper declines a snapshot whose status is `complete`, the restore now
settles on `status: 'error'` with an explanatory message and fires `onError`. A
decline on any other status is still silent β€” a `running` snapshot has no result
yet by definition, and the rejoin delivers it. A client with no
`reconstructResult` mapper at all is unaffected.
25 changes: 25 additions & 0 deletions .changeset/client-generation-hydration-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@tanstack/ai-client': minor
---

Server-driven generation hydration no longer swallows every failure.

`GenerationClient` / `VideoGenerationClient` mount hydration
(`persistence: true`) wrapped the whole `hydrateGeneration` call in a bare
`try { … } catch { return }`, collapsing a transport error, a `403` from the
`reconstructGeneration` authorize gate, an unparseable body, and "no record for
this thread" into one indistinguishable silent no-op β€” so an app could not tell a
broken server from a fresh thread, and had no signal to retry.

- A genuine **miss** (the server reports no record) stays silent, as before.
- A genuine **failure** now surfaces on `status` / `error` and fires `onError`,
with a message naming the cause. A record the client's own validator rejects
(unknown schema version, missing/invalid `status` or `resumeState`) counts as a
failure, not a miss.
- The failure is skipped when a `generate()` took ownership of the client while
the hydrate request was in flight β€” the live run still wins.

Relatedly, `fetchServerSentEvents` / `fetchHttpStream` `hydrateGeneration` now
only treats a `200` carrying `null` as a miss. Any other non-object body (a
string, an array) rejects instead of being reported as an empty thread, so a
misconfigured route no longer masquerades as a fresh one.
27 changes: 27 additions & 0 deletions .changeset/client-truncated-generation-stream.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
'@tanstack/ai-client': minor
---

A generation stream that ends without a terminal chunk now settles to `error`
instead of wedging the client on `generating` forever.

`GenerationClient.processStream` / `VideoGenerationClient.processStream` only
settled the status on `RUN_FINISHED` or `RUN_ERROR`. A `for await` loop over a
stream that simply _ends_ β€” a proxy/load-balancer idle timeout, a server restart
mid-run, or a durable log whose terminal append never landed β€” returns normally,
so no catch fired and the client came to rest on
`status: 'generating'`, `isLoading: false`, `result: null`, with `onError` never
called. Worse, the resume snapshot stayed `running`, so every subsequent mount
rejoined the same dead run and repeated the same outcome.

Both clients now throw when the stream ends with no terminal chunk seen (and the
read wasn't aborted by `stop()` / `dispose()`), which routes the failure through
the existing error path: `status: 'error'`, `error` set, `onError` fired, and the
resume snapshot rewritten to a terminal `error` with a null `resumeState` so
nothing chases it again. This applies to both the initial `generate()` path and
the mount-time `rejoinInFlight` path. A rejoin failure now also fires `onError`,
matching `generate()`.

This is the sibling of the earlier "rejoin settles to error" fix, which covered a
missing and a throwing `joinRun` but not a join that returns cleanly with no
terminal chunk.
17 changes: 17 additions & 0 deletions .changeset/client-typed-storage-adapter-defaults.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
---
'@tanstack/ai-client': minor
---

`localStoragePersistence` / `sessionStoragePersistence` / `indexedDBPersistence`
default their `TValue` back to `ChatPersistedState` instead of `any`.

The `any` default was justified by a claim that "a bare call works for both the
chat **and generation** `persistence` options with no type argument". That is no
longer true: generation `persistence` is now `boolean` (server-driven only), so
chat is the sole `persistence` option that takes a storage adapter β€” and the
`any` default erased `getItem` / `setItem` type safety for chat users in exchange
for nothing.

A bare `localStoragePersistence()` still needs no type argument. Only a
standalone store holding something other than a chat transcript needs the
explicit one, e.g. `localStoragePersistence<MyValue>()`.
20 changes: 20 additions & 0 deletions .changeset/conformance-generation-stores.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@tanstack/ai-persistence': minor
---

Extend the shared conformance testkit to the generation stores.

**Migration β€” every existing adapter must update its conformance call.** The suite now fails loudly on a store that is absent without being declared, so a chat-only adapter that used to pass unchanged will start failing on `generationRuns` / `artifacts` / `blobs`. Declare them absent:

```diff
- runPersistenceConformance('my-adapter', () => makePersistence())
+ runPersistenceConformance('my-adapter', () => makePersistence(), {
+ skip: ['generationRuns', 'artifacts', 'blobs'],
+ })
```

Drop an entry from `skip` as you implement that store β€” the suite then holds it to the contract below. Declaring absence is deliberate: a silently skipped store is how an adapter ships a `generationRuns` implementation that was never exercised.

`runPersistenceConformance` now exercises `generationRuns`, `artifacts`, and `blobs` alongside the four chat state stores, so a hand-rolled generation backend is held to the same gate as a chat one: `createOrResume` idempotency and `findLatestForThread` (latest by `startedAt`, thread-scoped, terminal runs included) on the run store; upsert `save`, `list(runId)` ordering, and `delete` / `deleteForRun` scoping on the artifact store; and byte/metadata round-trips, overwrite, silent absent-key `delete`, and `list` prefix + cursor paging on the blob store. Two invariants that were easy to get wrong and are now checked: `list`'s `prefix` matches **literally and case-sensitively** (a SQL backend using `LIKE` fails on both counts, since SQLite's `LIKE` is case-insensitive for ASCII and treats `%` / `_` as wildcards), and cursor paging visits every key exactly once.

`examples/ts-react-chat`'s self-contained `node:sqlite` adapter implements all seven stores and runs the full suite; its server-side generation route is backed by that adapter, so generated images survive a dev-server restart.
7 changes: 7 additions & 0 deletions .changeset/core-jsdoc-server-fn-example.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/ai': patch
---

Fix `@tanstack/ai` breaking non-React TanStack Start builds.

A JSDoc example on `replayRunStream` inlined a server-function builder chain. Comments survive into `dist`, and Start's server-fn Vite plugin decides whether a module needs compiling by regex-matching the source β€” so it treated this package as a server-fn module and tried to resolve the framework's `@tanstack/*-start` package, failing the build of any Solid/Vue/Svelte Start app (`could not resolve "@tanstack/solid-start"`). The example now declares the generator separately and no longer trips the match.
7 changes: 7 additions & 0 deletions .changeset/core-result-transforms-required.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@tanstack/ai': minor
---

`GenerationMiddlewareContext.resultTransforms` is now required.

Middleware registers a result transform by pushing onto the array, so an optional one let a host that builds its own context omit it and silently no-op every registration β€” generation persistence would then mark a run completed with neither its result nor its artifacts written, with nothing to observe but the missing data. Every context the library builds already comes from `createGenerationContext`, which always sets `[]`, so this only affects code that constructs a `GenerationMiddlewareContext` by hand: set `resultTransforms: []`.
9 changes: 9 additions & 0 deletions .changeset/core-summarize-generation-middleware.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@tanstack/ai': minor
---

`summarize()` accepts generation middleware, so summaries can be persisted.

`useSummarize({ persistence: true, threadId })` type-checked exactly like the six media hooks, but `summarize()` took no `middleware`, so no library path could ever write its run record and a reload restored nothing. It now takes `middleware` like the `generate*` activities: one `onStart`, the result transforms applied to the `SummarizationResult`, then `onFinish` / `onError`, in both streaming and non-streaming mode (a consumer that disconnects mid-summary fires `onAbort`). In streaming mode the transformed result is what is yielded, so the client and the persisted record hold the same object.

`GenerationActivity` gained `'summarize'`, and `otelMiddleware` maps it to the `summarize` operation name. Summaries are text, so there are no artifacts: a persistence middleware stores the run record and its result and nothing else.
15 changes: 15 additions & 0 deletions .changeset/core-video-job-run-lifecycle.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@tanstack/ai': minor
---

Fix non-streaming `generateVideo()` losing the generation when persistence is on.

A non-streaming `generateVideo()` call only SUBMITS a job β€” the video does not exist until a later poll β€” but it fired `onFinish` as soon as the job was queued and never applied the result transforms, and it never put the caller's `threadId` on the middleware context. With `withGenerationPersistence` that meant `generateVideo({ threadId, middleware })` threw for want of a scope, and (once given one) would have stamped the run `completed` with no result, no url, and no stored bytes, while the eventual result had nowhere to land.

Submitting a job now OPENS the run and `getVideoJobStatus()` closes it, with the two calls correlated by the provider's **`jobId`** β€” the one id a poller structurally cannot be missing, since it cannot poll without it. Nothing else has to be threaded through:

- `generateVideo()` (non-streaming) passes `threadId` and the prompt inputs to middleware, files the run under an id derived from the provider + `jobId`, applies the result transforms to the submission (so the run record captures the `jobId` and stays resumable from a later request or process), and fires **no terminal hook**.
- `getVideoJobStatus()` accepts `threadId` and `middleware`, and recomputes the same run id from `adapter` + `jobId`. On the poll that first observes a terminal job state it resumes that run, applies the result transforms β€” which is where persistence copies the video into the blob store and rewrites `url` to a durable one, so the returned result and the stored record carry the same urls β€” and fires `onFinish`, or `onError` when the job (or the url fetch) failed. Intermediate polls invoke nothing. Its result gained `jobId`, `expiresAt`, and `artifacts`; `VideoJobResult` gained `artifacts` (refs for persisted prompt INPUTS, e.g. a start frame).
- `runId` on a non-streaming `generateVideo()` call is **ignored** (it remains the wire run id in stream mode). The run id has to be recomputable by the poll from the `jobId` alone; honoring a custom one would reintroduce the failure this avoids β€” a caller who set it on the submit and forgot it on the poll would silently open a second record while the first sat unfinished forever.

Two consequences worth knowing. Because the job id only exists once the provider accepts the job, `onStart` now fires AFTER the submit request, so an `otelMiddleware()` span covers the run from acceptance onward rather than the submit round-trip, and a submission that FAILS (no job to key on) opens and immediately fails a run under the call's `requestId` β€” terminal and unresumable, but filed under the thread so a hydrating client sees the failure. And `threadId` must reach the poll: omitting it makes generation persistence throw loudly rather than file the finished video where nothing can hydrate it.
15 changes: 15 additions & 0 deletions .changeset/deprecate-generation-id.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
'@tanstack/ai-client': minor
'@tanstack/ai-react': minor
'@tanstack/ai-solid': minor
'@tanstack/ai-vue': minor
'@tanstack/ai-svelte': minor
'@tanstack/ai-angular': minor
---

Deprecate generation `id` in favor of `threadId` as the single identity.

`threadId` is the scope for the wire, devtools, and persistence. When it is
supplied, `id` is typed `never` so you cannot pass both. Legacy `id` remains
only for ephemeral runs that have no `threadId` (wire/devtools fallback) and is
marked `@deprecated`.
28 changes: 28 additions & 0 deletions .changeset/durable-runs-survive-disconnect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
'@tanstack/ai': patch
---

Durable streaming runs now survive a client disconnect (page reload) and can be
tailed to completion by a rejoining client β€” no route-side detachment code
required. Two internal fixes to `toServerSentEventsResponse` /
`toHttpResponse`, both additive with no public API change:

- **`RUN_STARTED` is a durability flush boundary.** One-shot generation
activities (image, speech, transcription, summarize) emit `RUN_STARTED`, then
await the provider for seconds, then a terminal. Previously `RUN_STARTED` sat
in the batch buffer, so the durable log was empty for the whole run and a
mount-time `joinRun` fast-failed as "run gone". It now flushes immediately, so
the run is resumable from the instant it starts.
- **The producer is decoupled from the HTTP response when durability is on.**
A client disconnect used to abort the producer and seal the log with
`RUN_ERROR`, even though the run kept running and recorded success. Now, on a
durable (persistence-on) run, a response cancel detaches and the producer
keeps draining into the log to its real terminal, so a rejoining client tails
it to completion. This supersedes the earlier "producers terminalize the log
on cancellation" behavior **for durable runs only**:
- **No durability (persistence off)** β†’ unchanged: a disconnect aborts and
stops the run.
- **Durability present (persistence on)** β†’ the run survives a disconnect.
- A genuine caller stop β€” aborting an `abortController` you pass (e.g. wired to
`request.signal`, as the resumable-streams demo does) β€” still terminalizes
the run, so opt-in die-on-disconnect keeps working.
11 changes: 11 additions & 0 deletions .changeset/generate-video-result-transforms.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@tanstack/ai': patch
---

Fix `generateVideo` dropping result transforms and run identity, which made a persisted video restore as nothing.

Streaming video was the only media activity that never called `applyGenerationResultTransforms`, and never put the caller's `threadId` / `runId` on the middleware context. Because `withGenerationPersistence` registers BOTH its artifact capture and its run-record `result` write as result transforms β€” pushed onto an optional `ctx.resultTransforms` β€” both silently no-opped. A completed video therefore stored a run record with `status: 'complete'` and nothing else: no result metadata, no artifact refs, no stored bytes, and no thread link (the run was filed under the internal `requestId`). On reload the client found no output artifact and restored nothing.

Streaming video now applies the transforms to its terminal result before yielding it, so the `generation:result` chunk and the stored run record carry the same URLs β€” including the durable app-origin URL that `artifactUrl` stamps. It also passes `threadId`, `runId`, and `artifactInputs` into the middleware context, matching `generateImage`.

`threadId` is now a documented option on `generateVideo` (it previously had none β€” callers passing one via an object spread type-checked but were silently ignored). When omitted, an id is still minted for the `RUN_STARTED` / `RUN_FINISHED` wire chunks, but the middleware context gets `undefined` rather than the minted value: a fabricated thread id is a slot no client can hydrate by, which is worse than recording no link at all.
32 changes: 32 additions & 0 deletions .changeset/generation-mount-hydration-and-speech-restore.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
'@tanstack/ai-client': patch
'@tanstack/ai-react': patch
'@tanstack/ai-solid': patch
'@tanstack/ai-vue': patch
'@tanstack/ai-svelte': patch
'@tanstack/ai-angular': patch
---

Fix generation mount hydration to run in the commit phase, and restore TTS
results.

- The `GenerationClient` / `VideoGenerationClient` used to kick off mount
hydration from their constructor. Framework hooks build the client inside
`useMemo`, so that ran in React's render phase, and several clients mounting
together re-fired the hydrate GET on every discarded/speculative render,
flooding the connection pool (`ERR_INSUFFICIENT_RESOURCES`). Hydration now runs once from `mountDevtools`
(the hooks' commit-phase mount effect), guarded by `serverHydrationStarted`.
`initialResumeSnapshot` still seeds SSR/first paint. Note for direct
(non-framework) `GenerationClient`/`VideoGenerationClient` users: mount
hydration and the "missing `hydrateGeneration` handler" warning now fire from
`mountDevtools()` rather than the constructor, so call `mountDevtools()` (as
every framework hook does on mount) to trigger a server/storage restore;
`generate()` still triggers it too.
- New `reconstructSpeechResult` mapper, wired into the speech hook of **every**
framework package β€” `useGenerateSpeech` (React, Solid, Vue),
`createGenerateSpeech` (Svelte) and `injectGenerateSpeech` (Angular). A
restored `TTSResult` carries no base64 bytes (they live in the blob store), so
it surfaces the durable serve URL through `result.artifacts`; the speech clip
now repaints after a reload instead of showing status only. Previously only
React was wired, so a restored TTS run on the other four repainted
`status`/`error` but left `result` null.
Loading
Loading