Loading workspace insights... Statistics interval
7 days30 daysLatest CI Pipeline Executions
55638f64 feat(persistence)!: require threadId on withGenerationPersistence
The client hooks require `threadId` whenever `persistence` is set; the server
middleware did not. That asymmetry hid a class of silent failure: a run filed
under no scope cannot be hydrated by one, so `persistence: true` restored
nothing, forever, with no error to explain why. The example's video route hit
exactly this — its runs recorded `thread_id: NULL`.
`withGenerationPersistence(persistence, { threadId, ... })` now takes a
required `threadId` via the new `WithGenerationPersistenceOptions`, mirroring
the client's discriminated union.
The option is also the AUTHORITY for the run record's and artifacts' scope, in
preference to `ctx.threadId`. An activity mints a throwaway thread id for its
RUN_* wire chunks when the caller passes none, and persisting that fabricated
id filed runs in a slot nothing could look up — worse than recording no link,
because it looks like one. A test that asserted the old fallback (wire id ==
persisted id) now asserts they deliberately diverge.
Call sites updated across the example routes, docs and skills. The example
routes reject a request carrying no `threadId` with a 400 rather than inventing
one, which is the pattern the docs now show.
Note: `docs/persistence/generation-persistence.md` has one remaining kiira
failure in the `getImageHydrationFn` snippet (a `ReconstructedGeneration` /
Start `ServerFn` return-type mismatch). It predates this commit — verified by
stashing these changes — and is left alone. c8283d3a fix(ai): apply result transforms and carry identity in streaming generateVideo
A persisted video restored as nothing on reload. The run record showed
`status: 'complete'` and nothing else — no result metadata, no artifact refs,
no stored bytes, and `thread_id` NULL.
Streaming video was the only media activity that never called
`applyGenerationResultTransforms`, and never put the caller's `threadId` /
`runId` on the middleware context. `withGenerationPersistence` registers BOTH
its artifact capture and its run-record `result` write as result transforms,
pushed onto an OPTIONAL `ctx.resultTransforms` — so both silently no-opped,
and the run was filed under the internal `requestId` with no thread link. The
client rebuilds a restored video from an output artifact carrying a durable
url, found none, and restored nothing.
Video now applies the transforms to its terminal result before yielding it, so
the `generation:result` chunk and the stored record carry the same urls
(including the app-origin one `artifactUrl` stamps), and passes `threadId` /
`runId` / `artifactInputs` into the context like `generateImage`.
`threadId` is now a documented option on `generateVideo`. It previously had
none, so callers passing one through an object spread type-checked and were
silently ignored — which is how the example's route looked correct while
recording NULL. When omitted, an id is still minted for the RUN_* wire chunks,
but the middleware context gets `undefined` instead: a fabricated thread id is
a slot no client can hydrate by, which is worse than no link at all.
Both regression tests fail against the previous behaviour. 516302e5 feat(persistence): server-driven generation persistence over server functions
Server-driven persistence (`persistence: true`) previously required an HTTP
endpoint, because the hydrate/rejoin handlers lived on the connection adapter
and only the fetch/XHR adapters implemented them. A TanStack Start server
function had no way to participate, so `persistence: true` silently restored
nothing there.
Persistence handlers are now supplied independently of the transport:
- `stream()` takes an optional second argument of `{ hydrate,
hydrateGeneration, joinRun }`, spread onto the adapter.
- The generation client accepts `hydrateGeneration` / `joinRun` as options,
used when the connection carries none. The connection's handlers win when
both exist, and `persistence: true` with no handler from either source warns
instead of silently no-opping.
- `memoryStream` accepts an explicit `{ runId, offset }` alongside a `Request`,
and the new `replayRunStream` replays a run's delivery log as a bare chunk
stream — what a server function needs to serve `joinRun` without an HTTP
`Response`.
A restored snapshot that reports a run still in flight is now repainted through
one path: tail it via `joinRun` when a handler exists, otherwise repaint it as
an interrupted error rather than a `generating` status that would never settle.
The generation hooks across React, Solid, Vue, Svelte and Angular forward the
new options. cec15062 feat(persistence): server-driven generation persistence over server functions
Server-driven persistence (`persistence: true`) previously required an HTTP
endpoint, because the hydrate/rejoin handlers lived on the connection adapter
and only the fetch/XHR adapters implemented them. A TanStack Start server
function had no way to participate, so `persistence: true` silently restored
nothing there.
Persistence handlers are now supplied independently of the transport:
- `stream()` takes an optional second argument of `{ hydrate,
hydrateGeneration, joinRun }`, spread onto the adapter.
- The generation client accepts `hydrateGeneration` / `joinRun` as options,
used when the connection carries none. The connection's handlers win when
both exist, and `persistence: true` with no handler from either source warns
instead of silently no-opping.
- `memoryStream` accepts an explicit `{ runId, offset }` alongside a `Request`,
and the new `replayRunStream` replays a run's delivery log as a bare chunk
stream — what a server function needs to serve `joinRun` without an HTTP
`Response`.
A restored snapshot that reports a run still in flight is now repainted through
one path: tail it via `joinRun` when a handler exists, otherwise repaint it as
an interrupted error rather than a `generating` status that would never settle.
The generation hooks across React, Solid, Vue, Svelte and Angular forward the
new options. 2e3b1010 feat(persistence): server-side generation persistence in the example; require store methods
The example demonstrated only the client-driven half of generation persistence.
`withGenerationPersistence` and `reconstructGeneration` had never been run
against each other over HTTP anywhere — each was unit-tested in isolation, and
the e2e harness deliberately hand-builds the hydration JSON rather than pull in
`@tanstack/ai-persistence`. That left the join between them, which this branch
just changed the key of, as the least-covered part of the feature.
`/api/generate/image` now runs the real thing: `withGenerationPersistence` with
byte storage and an `artifactUrl`, plus a GET that serves artifact bytes by id
or answers mount hydration. The Streaming variant switches to `persistence:
true`; Direct and Server Fn keep the client adapter because server functions
have no GET path for server-driven restore to use.
Make three store methods required, per this file's own evolution policy:
- `GenerationRunStore.findLatestForThread` was optional and feature-detected —
the exact anti-pattern the policy documents, and the exact bug it records
`findActiveRun` causing for a release cycle. Server-driven hydration calls it
on every mount, so an adapter without it was indistinguishable from a thread
with no runs: `persistence: true` silently restored nothing, forever. The
runtime guard added earlier on this branch is deleted — the compiler enforces
it now, and the cases those tests covered are unrepresentable.
- `ArtifactStore.delete` / `deleteForRun` were optional while their pair
`BlobStore.delete` is required, so a backend could drop the bytes but keep the
record. An app calling `stores.artifacts.delete?.(id)` for an erasure request
would silently no-op.
Also documents `blobKey` in the adapter guide's reference record and ER diagram,
where `ARTIFACT ||--|| BLOB` was a derived convention and is now a real key, and
deletes `api.interrupts.test.ts` — 19 assertions no runner has ever executed
(the example's vitest config scopes to `src/lib/**`), which also cost a
route-scanner warning on every dev start. 87b58737 feat(persistence): storageKey for blob paths, and blobKey on the record
Generated bytes were written to a hardcoded `artifacts/<runId>/<artifactId>`
with no way to influence it, so "keep my generated files in my own R2 folder
structure" was not expressible. `withGenerationPersistence` now takes a
`storageKey` mapper receiving the artifact's identity, role, activity, mime type
and resolved name.
Server-side only, deliberately: a key supplied by the browser would be a
path-traversal and cross-tenant-write vector, the same class as the two issues
already fixed on this branch.
This forces a companion change. `retrieveBlob` RECOMPUTED the path from runId +
artifactId, which only works while the derivation is a fixed constant — the
moment it is user-supplied the read looks in the wrong place. The resolved key is
therefore recorded on the new `ArtifactRecord.blobKey`, and reads go through
`resolveArtifactBlobKey`, which falls back to the old convention for records
written before the field existed. That fallback is what makes this a
non-breaking addition, and also why the default convention can never be changed
retroactively.
Worth having independently of `storageKey`: with the key recomputed rather than
remembered, the default convention was effectively frozen forever — changing
`artifactBlobKey` would have orphaned every blob already written.
Also threads the required `threadId` through the docs, skills, E2E harness and
example call sites, and documents both new capabilities in the changeset.