Loading workspace insights... Statistics interval
7 days30 daysLatest CI Pipeline Executions
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. d63043af fix(persistence): don't fetch caller-supplied prompt URLs + review fixes
Byte storage had one fetch path serving two purposes: `descriptorBody`
branched on `descriptor.url` alone and never looked at `descriptor.role`,
so a prompt part with `source: { type: 'url' }` was fetched server-side and
stored, readable back through the artifact GET route. Fetching an expiring
provider result URL is the point of the feature; mirroring a caller-supplied
URL is not, and the bytes are redundant since the client already had them.
Input URLs are no longer fetched. Opting back in is `allowInputUrl`, a
predicate rather than a boolean so the check can't be skipped. Every artifact
fetch is now http/https-only, timed out (`artifactFetchTimeoutMs`) and
size-capped during the drain (`maxArtifactBytes`); input fetches also block
loopback/private/link-local hosts and refuse redirects. Output fetches skip
the host block on purpose — a self-hosted provider legitimately returns a
localhost URL. `artifactFetch` injects the fetch for egress-proxy routing.
Also from review:
- gate `emitResumeState` on a signature, so a per-chunk snapshot rebuild no
longer re-renders every framework hook on every stream event
- guard an invalid Date before `toISOString()` in the resume snapshot reducer
- fall back to the literal payload when a data URL has a bad percent escape
- treat a non-object hydration body as a miss instead of reading `.activeRun`
off null
- docs: authorize artifact reads by `ArtifactRecord.threadId` (404, not 403),
drop auto-resume language for snapshot hydration, add the Mode B server
snippet, honour `limit: 0` in the R2 sample