feat(persistence): durable generation media-byte storage#998
feat(persistence): durable generation media-byte storage#998tombeckenham wants to merge 6 commits into
Conversation
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.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
🚀 Changeset Version Preview17 package(s) bumped directly, 34 bumped as dependents. 🟥 Major bumps
🟨 Minor bumps
🟩 Patch bumps
|
|
View your CI Pipeline Execution ↗ for commit f795de7
☁️ Nx Cloud last updated this comment at |
@tanstack/ai
@tanstack/ai-acp
@tanstack/ai-angular
@tanstack/ai-anthropic
@tanstack/ai-bedrock
@tanstack/ai-claude-code
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-skills
@tanstack/ai-codex
@tanstack/ai-devtools-core
@tanstack/ai-durable-stream
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-grok-build
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-mcp
@tanstack/ai-memory
@tanstack/ai-mistral
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-opencode
@tanstack/ai-openrouter
@tanstack/ai-persistence
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-sandbox
@tanstack/ai-sandbox-cloudflare
@tanstack/ai-sandbox-daytona
@tanstack/ai-sandbox-docker
@tanstack/ai-sandbox-local-process
@tanstack/ai-sandbox-sprites
@tanstack/ai-sandbox-vercel
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-utils
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
🎯 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 ablobs(BlobStore) store,withGenerationPersistencewrites each generated file's bytes to the blob store (keyartifacts/<runId>/<artifactId>), records anArtifactRecord, attachesPersistedArtifactRefs to the result, and emitsgeneration:artifactsso the client records them. Extraction is customizable viaextractArtifacts/nameArtifact.memoryPersistence()now ships both stores; any backend implementing the two contracts works. To serve a stored artifact,@tanstack/ai-persistenceexportsretrieveArtifact(persistence, id)andretrieveBlob(persistence, idOrRecord)(plusartifactBlobKey).Packages
@tanstack/ai— result-transform machinery (resultTransforms/artifactInputsonGenerationMiddlewareContext,applyGenerationResultTransforms),threadId/runIdoptions on the image/audio/speech/transcription activities, andgeneration:artifactsemission fromstreamGenerationResult.@tanstack/ai-utils—base64ToUint8Array.@tanstack/ai-persistence—ArtifactStore+BlobStorecontracts, in-memory impls inmemoryPersistence(), byte persistence inwithGenerationPersistence,retrieveArtifact/retrieveBlob.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 reworkedai-persistenceneeded real decisions. These are isolated in the finalfix(persistence): reconcile …commit, separate from the cherry-picks:memoryPersistence()keeps the base's no-locks decision and gains onlyartifacts+blobs; locks stay separate viaInMemoryLockStore+withLocks. Same forAIPersistenceStores/storeKeys.validateGenerationPersistenceStoresandInvalidGenerationPersistencenow carry both invariants:runsis required (base), andartifacts/blobscome as a pair (this PR).withGenerationPersistenceis preserved. The original patch deleted it without fixing the underlying problem, so the note now also records therequestId-vs-runIdmismatch below.memoryPersistence()to exactChatPersistenceequality, and the artifact-pairing test gains arunsstore so it exercises the pairing rule rather than tripping the required-runs rule first.types.tsstates the testkit is "the authoritative compatibility gate" and that new invariants must be promoted into it.ArtifactStore/BlobStoreadd documented invariants (notablyBlobStore.listcursor/prefix semantics) with zero suites. Now that backends are app-owned, this is the only thing holding an implementation to the rules.RunRecord.runIdandArtifactRecord.runIddisagree. The run record is keyed onctx.requestId; artifacts usectx.runId ?? ctx.requestId. With a caller-suppliedrunId,runs.get(runId)is empty whileartifacts.list(runId)returns rows — the stores can't be joined. This is exactly what the preserved TEMPORARY/WRONG-SHAPE note warns about.generateVideois the only media activity with noartifactInputs/applyGenerationResultTransforms, yetmiddleware.tscarries an unreachableactivity === '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.fetch(descriptor.url)throw isn't isolated byapplyGenerationResultTransforms, so a blip fetching an expiring provider URL discards the paid-for result, fails the run, and orphans already-written blobs.size: 0, and the documented serve route sendscontent-length: 0alongside a real body.GET ?id=<artifactId>returns any artifact's bytes, andartifactIdisDate.now()+ 5 chars ofMath.random().sourcePartDescriptorsdefaults mime to${type}/mpeg(yielding invalidimage/mpeg);parseDataUrldoesn't match parameterized data URLs and its fallback base64-decodes the whole URL.✅ Checklist
pnpm run test:pr. — ran the affected subset:build,test:types,test:lib(ai-persistence at 99 tests),test:oxlintall green.🚀 Release Impact