Skip to content

feat(persistence): durable generation media-byte storage#998

Draft
tombeckenham wants to merge 6 commits into
feat/generation-persistence-clientfrom
feat/generation-persistence-bytes
Draft

feat(persistence): durable generation media-byte storage#998
tombeckenham wants to merge 6 commits into
feat/generation-persistence-clientfrom
feat/generation-persistence-bytes

Conversation

@tombeckenham

Copy link
Copy Markdown
Contributor

🎯 Changes

Stack 2 of 2. Stacked on #997 — please review/merge that first; this PR's base is its branch, so the diff here is only the server half. Split out of #987 and rebuilt on the current feat/persistence-core.

Durable media-byte storage for generations (server, opt-in).

When the persistence backend provides both an artifacts (ArtifactStore) and a blobs (BlobStore) store, withGenerationPersistence writes each generated file's bytes to the blob store (key artifacts/<runId>/<artifactId>), records an ArtifactRecord, attaches PersistedArtifactRefs to the result, and emits generation:artifacts so the client records them. Extraction is customizable via extractArtifacts / nameArtifact.

memoryPersistence() now ships both stores; any backend implementing the two contracts works. To serve a stored artifact, @tanstack/ai-persistence exports retrieveArtifact(persistence, id) and retrieveBlob(persistence, idOrRecord) (plus artifactBlobKey).

Packages

  • @tanstack/ai — result-transform machinery (resultTransforms / artifactInputs on GenerationMiddlewareContext, applyGenerationResultTransforms), threadId / runId options on the image/audio/speech/transcription activities, and generation:artifacts emission from streamGenerationResult.
  • @tanstack/ai-utilsbase64ToUint8Array.
  • @tanstack/ai-persistenceArtifactStore + BlobStore contracts, in-memory impls in memoryPersistence(), byte persistence in withGenerationPersistence, retrieveArtifact / retrieveBlob.
  • Docs — the byte-storage + serve-route half of docs/persistence/generation-persistence.md.

Reconciling with the contract-only core

The original commits predate 62c99754d refactor(persistence)!: ship the contract, not the backends, so the rebase onto the reworked ai-persistence needed real decisions. These are isolated in the final fix(persistence): reconcile … commit, separate from the cherry-picks:

  • memoryPersistence() keeps the base's no-locks decision and gains only artifacts + blobs; locks stay separate via InMemoryLockStore + withLocks. Same for AIPersistenceStores / storeKeys.
  • validateGenerationPersistenceStores and InvalidGenerationPersistence now carry both invariants: runs is required (base), and artifacts / blobs come as a pair (this PR).
  • The base's "⚠️ TEMPORARY / WRONG SHAPE" warning on withGenerationPersistence is preserved. The original patch deleted it without fixing the underlying problem, so the note now also records the requestId-vs-runId mismatch below.
  • Two tests adjusted: the type test no longer pins memoryPersistence() to exact ChatPersistence equality, and the artifact-pairing test gains a runs store so it exercises the pairing rule rather than tripping the required-runs rule first.

⚠️ Known issues — not yet fixed on this branch

  • Blocker: the new store contracts skip the conformance testkit. types.ts states the testkit is "the authoritative compatibility gate" and that new invariants must be promoted into it. ArtifactStore / BlobStore add documented invariants (notably BlobStore.list cursor/prefix semantics) with zero suites. Now that backends are app-owned, this is the only thing holding an implementation to the rules.
  • RunRecord.runId and ArtifactRecord.runId disagree. The run record is keyed on ctx.requestId; artifacts use ctx.runId ?? ctx.requestId. With a caller-supplied runId, runs.get(runId) is empty while artifacts.list(runId) returns rows — the stores can't be joined. This is exactly what the preserved TEMPORARY/WRONG-SHAPE note warns about.
  • Video artifacts are never persisted. generateVideo is the only media activity with no artifactInputs / applyGenerationResultTransforms, yet middleware.ts carries an unreachable activity === 'video' branch and the docs lead with video as the motivating case. Either wire it or drop the dead branch and scope it out explicitly.
  • An artifact-persistence failure destroys a successful generation. The fetch(descriptor.url) throw isn't isolated by applyGenerationResultTransforms, so a blip fetching an expiring provider URL discards the paid-for result, fails the run, and orphans already-written blobs.
  • Streamed artifacts record size: 0, and the documented serve route sends content-length: 0 alongside a real body.
  • The documented serve route has no authorization — GET ?id=<artifactId> returns any artifact's bytes, and artifactId is Date.now() + 5 chars of Math.random().
  • Minor: sourcePartDescriptors defaults mime to ${type}/mpeg (yielding invalid image/mpeg); parseDataUrl doesn't match parameterized data URLs and its fallback base64-decodes the whole URL.
  • No E2E coverage yet.

✅ 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 (ai-persistence at 99 tests), test:oxlint all green.

🚀 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 5 commits July 27, 2026 08:51
Server-side artifact + blob storage for generated media, layered on
withGenerationPersistence.

- @tanstack/ai: result-transform machinery (resultTransforms/artifactInputs
  on GenerationMiddlewareContext, applyGenerationResultTransforms), threadId/
  runId on the image/audio/speech/transcription activities, and
  generation:artifacts emission from streamGenerationResult.
- @tanstack/ai-utils: base64ToUint8Array.
- @tanstack/ai-persistence: ArtifactStore + BlobStore contracts, in-memory
  impls in memoryPersistence(), and withGenerationPersistence byte-persistence
  (writes bytes to blobs, records ArtifactRecord, attaches PersistedArtifactRef,
  emits generation:artifacts) with extractArtifacts/nameArtifact options.
…+ blobs, serve route, extractArtifacts/nameArtifact)
Add retrieveArtifact(persistence, id) and retrieveBlob(persistence, idOrRecord)
so a serve handler fetches a persisted generation artifact's metadata and bytes
without hand-rolling the blob key. artifactBlobKey is the shared key builder used
by both withGenerationPersistence (write) and retrieveBlob (read).
… core

Resolve the rebase of the artifact/blob path onto the reworked ai-persistence:

- memoryPersistence() keeps the base's no-locks decision and gains only
  artifacts + blobs; locks stay separate via InMemoryLockStore + withLocks.
- AIPersistenceStores / storeKeys gain artifacts + blobs but not locks.
- validateGenerationPersistenceStores and InvalidGenerationPersistence now
  carry both invariants: runs is required, and artifacts/blobs come as a pair.
- withGenerationPersistence keeps the base's TEMPORARY/WRONG-SHAPE warning; the
  byte path does not fix the requestId-vs-runId keying, so the note now says so.
- Type test widens memoryPersistence() past exact ChatPersistence equality;
  artifact pairing test gains a runs store so it exercises the pairing rule.
@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: 1247efa3-880c-4f14-a9fe-bef46c5b253c

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-bytes

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

17 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
@tanstack/ai-utils 0.3.1 → 0.4.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 f795de7

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

☁️ Nx Cloud last updated this comment at 2026-07-26 23:14:26 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@998

@tanstack/ai-acp

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

@tanstack/ai-angular

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

@tanstack/ai-anthropic

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

@tanstack/ai-bedrock

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

@tanstack/ai-claude-code

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

@tanstack/ai-client

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

@tanstack/ai-code-mode

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

@tanstack/ai-code-mode-skills

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

@tanstack/ai-codex

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

@tanstack/ai-devtools-core

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

@tanstack/ai-durable-stream

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

@tanstack/ai-elevenlabs

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

@tanstack/ai-event-client

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

@tanstack/ai-fal

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

@tanstack/ai-gemini

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

@tanstack/ai-grok

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

@tanstack/ai-grok-build

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

@tanstack/ai-groq

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

@tanstack/ai-isolate-cloudflare

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

@tanstack/ai-isolate-node

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

@tanstack/ai-isolate-quickjs

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

@tanstack/ai-mcp

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

@tanstack/ai-memory

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

@tanstack/ai-mistral

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

@tanstack/ai-ollama

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

@tanstack/ai-openai

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

@tanstack/ai-opencode

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

@tanstack/ai-openrouter

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

@tanstack/ai-persistence

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

@tanstack/ai-preact

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

@tanstack/ai-react

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

@tanstack/ai-react-ui

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

@tanstack/ai-sandbox

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

@tanstack/ai-sandbox-cloudflare

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

@tanstack/ai-sandbox-daytona

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

@tanstack/ai-sandbox-docker

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

@tanstack/ai-sandbox-local-process

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

@tanstack/ai-sandbox-sprites

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

@tanstack/ai-sandbox-vercel

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

@tanstack/ai-solid

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

@tanstack/ai-solid-ui

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

@tanstack/ai-svelte

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

@tanstack/ai-utils

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

@tanstack/ai-vue

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

@tanstack/ai-vue-ui

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

@tanstack/openai-base

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

@tanstack/preact-ai-devtools

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

@tanstack/react-ai-devtools

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

@tanstack/solid-ai-devtools

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

commit: f795de7

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.

2 participants