Skip to content

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

Closed
tombeckenham wants to merge 22 commits into
feat/generation-persistence-clientfrom
feat/generation-persistence-bytes
Closed

feat(persistence): durable generation media-byte storage#998
tombeckenham wants to merge 22 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).

@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: f3707e36-553b-48e3-bdd9-2cca5f5a601d

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 a1f39a2

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ✅ Succeeded 8m 17s View ↗
nx run-many --targets=build --exclude=examples/... ✅ Succeeded 1m 53s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-28 07:58:54 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: a1f39a2

Layer a lightweight, read-only resume snapshot onto media generation.
As a run streams, the client builds a GenerationResumeSnapshot (run
identity, status, errors, result metadata + artifact refs — never media
bytes) and writes it to an optional GenerationServerPersistence store.

- ai-client: GenerationResumeSnapshot types + updateGenerationResumeSnapshot
  reducer; GenerationClient/VideoGenerationClient observe chunks, persist
  snapshots (serialized queue, warn-not-throw), expose getResumeSnapshot();
  disposed guard. No resume() action (stream re-attach is PR #955).
- ai-event-client: optional threadId/runId on generation events.
- react/solid/vue/svelte/angular hooks: persistence + initialResumeSnapshot
  options; expose resumeSnapshot/resumeState (+ pending/result artifacts).
- example: Persisted mode on the image generation route.
- docs: persistence/generation-persistence.md + nav entry.

Pairs with the existing withGenerationPersistence server middleware.
Drop the bespoke `GenerationServerPersistence` type and the `{ server }`
option wrapper. The `persistence` option is now a bare storage adapter
reusing the shared `ChatStorageAdapter` contract (aliased as
`GenerationPersistence`), so `localStoragePersistence` /
`sessionStoragePersistence` / `indexedDBPersistence` work for generations
exactly as they do for chat — matching main's ergonomics.
Default `localStoragePersistence` / `sessionStoragePersistence` /
`indexedDBPersistence` to a value-agnostic `TValue` so a bare, unannotated
call works for BOTH chat and generation persistence — the consuming
`persistence` option constrains the stored value. Generation docs/example now
use `localStoragePersistence({ keyPrefix })` with no type declaration.
PR #955 (resumable streams) is merged, so delivery durability is available
today — it was wrongly described as an unlanded future feature. Rewrite the
generation-persistence doc: the server example now wires a durability adapter
+ GET handler, and the delivery section explains that a dropped mid-generation
connection re-attaches through the same adapters useChat uses. Clarify that the
read-only snapshot carries run state (incl. runId) across reloads, while
hooks do not auto-resume on mount.
@tombeckenham
tombeckenham force-pushed the feat/generation-persistence-client branch from 34c8b69 to 765bcfd Compare July 28, 2026 02:32
@tombeckenham
tombeckenham force-pushed the feat/generation-persistence-bytes branch from f795de7 to 21d283a Compare July 28, 2026 02:32
tombeckenham and others added 14 commits July 28, 2026 14:39
…e revival

- kiira: replace phantom @tanstack/ai-persistence-drizzle import with the
  hand-rolled adapter from build-your-own-adapter (CI was red on this)
- hydrate the resume snapshot from persistence.getItem on construction,
  validated via new parseGenerationResumeSnapshot(unknown) export;
  initialResumeSnapshot seed takes precedence
- namespace storage keys as generation:<id> so chat and generation clients
  sharing an id and adapter no longer collide
- write terminal snapshots on stop() (idle) and transport-level errors
  (error); reset() clears memory + removeItem; RUN_STARTED drops stale
  result/error/pendingArtifacts from the previous run; plain-fetcher runs
  now record a complete snapshot built from the fetcher result
- capture video jobId into the snapshot from video:job:created
- add schemaVersion: 1 to persisted snapshots
- gate persistence writes on material change (ignore lastEvent-only churn),
  warn once per failure transition, clear resumePersistenceError on success
- mountDevtools() revives a disposed client (React StrictMode replay);
  generate() checks disposed before mounting devtools
- onResumeSnapshotChange now receives undefined when reset() clears
- fix mojibake em dashes in 12 hook files
…t coverage

- rewrite docs/persistence/generation-persistence.md around the implemented
  behavior: hydration on mount, generation:<id> keys, resumeState vs
  resumeSnapshot semantics, honest reconnect story, no media-URL claim;
  drop the inert threadId/runId spreads from the server sample
- fix the example's Persisted panel: distinguish in-flight run from last-run
  outcome; reload now actually shows the persisted record
- revert ai-event-client: BaseEventContext already carries threadId/runId,
  the 36 added lines were redundant redeclarations; changeset no longer
  bumps that package and now describes hydration + lifecycle accurately
- normalize wrong hook JSDoc (Server-side → client-side storage; read-only
  seed claims; run/cursor wording) and mark artifact fields dormant
- React hooks: post-dispose guards on callbacks/setters, StrictMode revive
  via mount effect, stable empty artifact arrays, re-export persistence
  types (+ PersistedArtifactRef)
- tests: replace the two vacuous reducer tests with real externalUrl
  positive/negative and stop coverage; add reducer seed-merge, RUN_STARTED
  stale-field-drop, video jobId capture, parseGenerationResumeSnapshot
  suite; add client lifecycle suite (hydration, seed precedence, corrupt
  storage, stop/reset/transport-error, write gating, StrictMode revive);
  add React hydration/StrictMode/artifact-exposure hook tests
…hots

Provider-free harness (api.generation-persistence streams a fixed AG-UI
sequence; aimock-exempt) + page using useGenerateImage with
localStoragePersistence. Proves: snapshot written under
tanstack-ai:generation:<id> with no media bytes, hydrated after reload with
no auto-run, and removed by reset().
…ration ordering

- solid: build the client outside reactive tracking (untrack) — the old
  createMemo second-arg was a seed, not deps, so option reads were tracked
  and a change orphaned an undisposed client; stable empty artifact arrays
- svelte: explicit generate() now revives a disposed client (mountDevtools)
  since Svelte has no remount effect; reactive bindings revive with it
- vue: stable empty artifact array constants (shallowRef identity)
- angular: JSDoc for persistence/initialResumeSnapshot on inject-generate-video
- all four: re-export GenerationPersistence/GenerationResumeSnapshot/
  GenerationResumeState/GenerationResumeStatus/GenerationPendingArtifact +
  PersistedArtifactRef from package index; hydration + reset()/removeItem
  tests against Map-backed adapters
- ai-client: kick off snapshot hydration only after callbacksRef is
  assigned (removes a sync-adapter ordering hazard)
…enerate casts

UseGenerationReturn gains a defaulted second generic
(TInput extends Record<string, any> = Record<string, any>) so generate is
typed (input: TInput) => Promise<void>. useGeneration returns the type it
actually builds — the unsound internal narrow-to-wide cast and the five
wrapper-level casts back down to the concrete input type all disappear,
and direct useGeneration consumers get a precisely typed generate.
Existing UseGenerationReturn<MyOutput> references keep compiling via the
default.
…svelte/angular

Same fix as fbc3dc3 for the remaining four frameworks: the base return
interface (UseGenerationReturn / CreateGenerationReturn /
InjectGenerationResult) gains a defaulted second generic
(TInput extends Record<string, any> = Record<string, any>) so generate is
typed (input: TInput) => Promise<void>. The base hooks return the type
they actually build and the internal narrow-to-wide casts plus every
wrapper-level 'generate as' cast are deleted. Video hooks were already
cast-free (they build their own client). Defaults keep existing
single-generic references compiling.
…Value = any)

Revert the web-storage factory defaults to TValue = ChatPersistedState, as
shipped in #984. The any default erased type safety on every direct
adapter use (getItem returned any; a store built for one domain assigned
silently to the other's hook) and carried three oxlint suppressions —
while buying nothing for inline usage, where contextual typing infers the
value type from the persistence option regardless of the default. The one
affected pattern, a standalone store for generations, now states its type:
localStoragePersistence<GenerationResumeSnapshot>(). Doc, example, and e2e
call sites updated; runtime behavior unchanged.
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.
@tombeckenham
tombeckenham force-pushed the feat/generation-persistence-bytes branch from 21d283a to a1f39a2 Compare July 28, 2026 07:48
@tombeckenham
tombeckenham force-pushed the feat/generation-persistence-client branch from 1e99cad to 61b8d07 Compare July 28, 2026 20:47
@tombeckenham
tombeckenham deleted the branch feat/generation-persistence-client July 29, 2026 04:22
@tombeckenham

Copy link
Copy Markdown
Contributor Author

Note: the durable media-byte storage from this stacked PR has been folded into #1011, which now ships the whole generation-persistence feature (client + server) in one PR.

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