Skip to content

feat(persistence): generation persistence β€” client snapshot + durable media bytes - #999

Open
AlemTuzlak wants to merge 13 commits into
mainfrom
feat/generation-persistence
Open

feat(persistence): generation persistence β€” client snapshot + durable media bytes#999
AlemTuzlak wants to merge 13 commits into
mainfrom
feat/generation-persistence

Conversation

@AlemTuzlak

@AlemTuzlak AlemTuzlak commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Generation Persistence β€” client snapshot + durable media bytes

Targets main (the persistence base, #984, has merged; this was previously stacked on feat/persistence-core, now rebased onto main).

Generation persistence in two halves.

1. Client resume snapshot (read-only)

As a media generation streams, the client builds a lightweight GenerationResumeSnapshot (run identity, status, errors, result metadata + artifact refs β€” never the bytes) and writes it to a persistence storage adapter. The option reuses the shared ChatStorageAdapter contract, so localStoragePersistence / sessionStoragePersistence / indexedDBPersistence work with no type argument:

const snapshots = localStoragePersistence({ keyPrefix: 'my-app:generation:' })
useGenerateImage({ connection, persistence: snapshots })

Hooks expose resumeSnapshot / resumeState / pendingArtifacts / resultArtifacts across react/solid/vue/svelte/angular. No resume() action; reconnecting to an in-flight stream is the delivery layer's job (resumable streams), wired via a durability adapter + GET handler exactly as for chat.

2. Durable media-byte storage (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. memoryPersistence() ships both stores; any backend implementing the two contracts works. Extraction is customizable via extractArtifacts / nameArtifact. Serve the bytes back with the retrieveArtifact / retrieveBlob helpers (and the shared artifactBlobKey).

Changes

  • @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-utils β€” base64ToUint8Array.
  • @tanstack/ai-persistence β€” ArtifactStore + BlobStore contracts, in-memory impls in memoryPersistence(), byte-persistence in withGenerationPersistence, and retrieveArtifact / retrieveBlob / artifactBlobKey serve helpers.
  • @tanstack/ai-client + framework hooks (react/solid/vue/svelte/angular) β€” the client snapshot + persistence option + artifact refs.
  • @tanstack/ai-event-client β€” optional threadId/runId on generation events.
  • Docs β€” docs/persistence/generation-persistence.md, kiira-verified.
  • Changeset β€” minor across the affected packages.

Verification

  • Builds green across ai-utils, ai, ai-event-client, ai-client, ai-persistence.
  • test:types β€” 0 errors in ai / ai-client / ai-persistence / all 5 frameworks.
  • Tests β€” ai-persistence 96 pass (incl. artifact + chat resume suites), ai-client generation 50 pass, framework generation 192 pass (react 39 / solid 56 / vue 47 / svelte 45 / angular 5).
  • kiira β€” doc passes (3 snippets).

Scope note

Durable SQL/R2 artifact+blob backends (Drizzle/Prisma/Cloudflare R2) are not in this PR β€” memoryPersistence plus any custom ArtifactStore+BlobStore cover the contract today; the durable backends (SQL schema + migrations per package) are a clean follow-up. generateVideo's own job-polling artifact path is likewise deferred (image/audio/speech/transcription persist bytes).

Summary by CodeRabbit

  • New Features

    • Added generation persistence for lightweight resume snapshots across reloads and interrupted streams.
    • Added optional backend storage for generated media artifacts, with artifact references available in results.
    • Added persistence and resume-state support across React, Vue, Solid, Svelte, and Angular generation APIs.
    • Added artifact retrieval helpers and expanded in-memory persistence support.
    • Added documentation and an example for persisted image generation.
  • Bug Fixes

    • Prevented late updates after generation disposal or cancellation.
    • Improved handling of concurrent generations and persistence write ordering.
    • Added safer artifact and URL handling for persisted results.

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.
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).
The base removed the @tanstack/ai-persistence-drizzle/sqlite subpath, so the
state-persistence example now uses memoryPersistence() (with a note to back it
with a durable backend in production), matching the byte-storage example.
@coderabbitai

coderabbitai Bot commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

πŸ“ Walkthrough

Walkthrough

Generation persistence adds artifact/blob storage middleware, resumable generation snapshots, lifecycle-safe client behavior, framework API exposure, activity result transforms, retrieval helpers, documentation, examples, and tests across the AI packages.

Changes

Generation persistence

Layer / File(s) Summary
Persistence contracts and storage
packages/ai-persistence/src/types.ts, packages/ai-persistence/src/memory.ts, packages/ai-persistence/src/retrieve.ts
Adds artifact/blob store contracts, in-memory implementations, artifact retrieval helpers, and paired-store validation.
Artifact middleware and activity integration
packages/ai-persistence/src/middleware.ts, packages/ai/src/activities/*
Extracts generation artifacts, persists bytes and metadata, attaches artifact references, applies result transforms, and emits artifact events before results.
Client resume snapshots
packages/ai-client/src/generation-types.ts, packages/ai-client/src/generation-client.ts, packages/ai-client/src/video-generation-client.ts
Tracks sanitized resume snapshots, queues persistence writes, exposes cloned snapshot state, and guards aborted or disposed generations.
Framework APIs
packages/ai-react/src/*, packages/ai-angular/src/*, packages/ai-solid/src/*, packages/ai-svelte/src/*, packages/ai-vue/src/*
Adds persistence inputs and resume/artifact outputs to generation hooks, composables, factories, and injectors.
Validation and documentation
packages/*/tests/*, docs/persistence/*, examples/ts-react-chat/*
Adds reducer, persistence, lifecycle, framework regression tests and documents persisted runs and artifacts.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GenerationClient
  participant GenerationMiddleware
  participant ArtifactStore
  participant BlobStore
  Client->>GenerationClient: start generation
  GenerationClient->>GenerationMiddleware: process generation stream
  GenerationMiddleware->>BlobStore: store generated bytes
  GenerationMiddleware->>ArtifactStore: store artifact metadata
  GenerationMiddleware-->>GenerationClient: artifact references
  GenerationClient-->>Client: resume snapshot and result
Loading

Suggested reviewers: tombeckenham

πŸš₯ Pre-merge checks | βœ… 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is relevant but does not follow the required template and omits the Checklist and Release Impact sections. Rewrite the PR description to use the required template and add the missing checklist and release-impact sections.
Docstring Coverage ⚠️ Warning Docstring coverage is 40.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
βœ… Passed checks (3 passed)
Check name Status Explanation
Title check βœ… Passed The title clearly matches the PR’s main change: generation persistence with client snapshots and durable media bytes.
Linked Issues check βœ… Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check βœ… Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
πŸ“ Generate docstrings
  • Create stacked PR
  • Commit on current branch
πŸ§ͺ Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/generation-persistence

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 27, 2026

Copy link
Copy Markdown

View your CI Pipeline Execution β†— for commit 5ec5ad9

Command Status Duration Result
nx run-many --targets=build --exclude=examples/... βœ… Succeeded 1m 55s View β†—

☁️ Nx Cloud last updated this comment at 2026-07-27 17:22:09 UTC

@pkg-pr-new

pkg-pr-new Bot commented Jul 27, 2026

Copy link
Copy Markdown

Open in StackBlitz

@tanstack/ai

npm i https://pkg.pr.new/TanStack/ai/@tanstack/ai@999

@tanstack/ai-acp

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

@tanstack/ai-angular

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

@tanstack/ai-anthropic

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

@tanstack/ai-bedrock

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

@tanstack/ai-claude-code

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

@tanstack/ai-client

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

@tanstack/ai-code-mode

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

@tanstack/ai-code-mode-skills

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

@tanstack/ai-codex

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

@tanstack/ai-devtools-core

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

@tanstack/ai-durable-stream

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

@tanstack/ai-elevenlabs

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

@tanstack/ai-event-client

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

@tanstack/ai-fal

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

@tanstack/ai-gemini

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

@tanstack/ai-grok

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

@tanstack/ai-grok-build

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

@tanstack/ai-groq

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

@tanstack/ai-isolate-cloudflare

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

@tanstack/ai-isolate-node

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

@tanstack/ai-isolate-quickjs

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

@tanstack/ai-mcp

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

@tanstack/ai-memory

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

@tanstack/ai-mistral

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

@tanstack/ai-ollama

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

@tanstack/ai-openai

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

@tanstack/ai-opencode

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

@tanstack/ai-openrouter

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

@tanstack/ai-persistence

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

@tanstack/ai-preact

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

@tanstack/ai-react

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

@tanstack/ai-react-ui

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

@tanstack/ai-sandbox

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

@tanstack/ai-sandbox-cloudflare

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

@tanstack/ai-sandbox-daytona

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

@tanstack/ai-sandbox-docker

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

@tanstack/ai-sandbox-local-process

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

@tanstack/ai-sandbox-sprites

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

@tanstack/ai-sandbox-vercel

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

@tanstack/ai-solid

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

@tanstack/ai-solid-ui

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

@tanstack/ai-svelte

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

@tanstack/ai-utils

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

@tanstack/ai-vue

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

@tanstack/ai-vue-ui

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

@tanstack/openai-base

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

@tanstack/preact-ai-devtools

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

@tanstack/react-ai-devtools

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

@tanstack/solid-ai-devtools

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

commit: 5ec5ad9

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

🧹 Nitpick comments (11)
packages/ai-persistence/tests/generation-artifacts.test.ts (1)

28-30: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | πŸ’€ Low value

Drop the void (undefined as unknown as T) statements.

These exist only to mark the type imports as used and emit real (if inert) runtime statements. Either exercise the types in an actual assertion or rely on import type being erased.

♻️ Suggested cleanup
-void (undefined as unknown as GenerationArtifactDescriptor)
-void (undefined as unknown as GenerationArtifactExtractionInput)
-void (undefined as unknown as GenerationArtifactNameInput)
-
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-persistence/tests/generation-artifacts.test.ts` around lines 28 -
30, Remove the inert void-cast statements for GenerationArtifactDescriptor,
GenerationArtifactExtractionInput, and GenerationArtifactNameInput. Use
type-only imports for these symbols, or reference them through a meaningful type
assertion if the test requires type coverage, without emitting runtime
statements.
.changeset/generation-persistence.md (1)

14-22: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | ⚑ Quick win

Redundant duplicate intro sentences in the changeset body.

Line 14 ("Add generation persistence: a lightweight client resume snapshot plus optional durable storage of the generated media bytes.") and line 18 ("Add client-side generation persistence: a lightweight, read-only resume snapshot for media generation activities.") restate the same thing. This reads like two changesets were concatenated without a transition, and will show up verbatim in the CHANGELOG.

Consider merging into a single intro followed by the "Media byte storage" and client-snapshot sections as subheadings.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.changeset/generation-persistence.md around lines 14 - 22, Remove the
redundant introductory sentence from the changeset body and keep one concise
introduction covering client resume snapshots and optional durable media
storage. Preserve the existing β€œMedia byte storage” and client-side generation
persistence sections, using them as the detailed follow-up content.
packages/ai-solid/src/use-generate-audio.ts (1)

59-72: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial

Redundant re-declaration of inherited members.

UseGenerateAudioReturn already inherits result, isLoading, error, and status unchanged from Omit<UseGenerationReturn<TOutput>, 'generate'> β€” only generate needed restating (its input type differs). The extra re-declarations are dead weight that could silently drift from the base type in the future.

♻️ Suggested cleanup
 export interface UseGenerateAudioReturn<
   TOutput = AudioGenerationResult,
 > extends Omit<UseGenerationReturn<TOutput>, 'generate'> {
   /** Trigger audio generation */
   generate: (input: AudioGenerateInput) => Promise<void>
-  /** The generation result containing audio, or null */
-  result: Accessor<TOutput | null>
-  /** Whether generation is in progress */
-  isLoading: Accessor<boolean>
-  /** Current error, if any */
-  error: Accessor<Error | undefined>
-  /** Current state of the generation */
-  status: Accessor<GenerationClientState>
 }
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-solid/src/use-generate-audio.ts` around lines 59 - 72, Remove the
redundant result, isLoading, error, and status members from
UseGenerateAudioReturn, retaining only the generate override whose input type
differs. Continue inheriting the unchanged members through
Omit<UseGenerationReturn<TOutput>, 'generate'>.
packages/ai-client/src/generation-types.ts (1)

273-289: 🎯 Functional Correctness | πŸ”΅ Trivial | πŸ’€ Low value

ARTIFACTS chunks replace, rather than accumulate, pendingArtifacts.

If the server emits artifact refs in more than one generation:artifacts event for a run, only the last batch survives. If batching is intentional (one event per run), a short comment stating that would prevent future confusion; otherwise merge by artifactId.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-client/src/generation-types.ts` around lines 273 - 289, The
CUSTOM chunk handling for GENERATION_EVENTS.ARTIFACTS currently overwrites
pendingArtifacts, losing earlier batches. Update this branch to merge new
artifact references with the existing next.pendingArtifacts by artifactId while
preserving prior entries; if the single-event behavior is intentional instead,
document that contract with a concise comment.
packages/ai-client/tests/generation-resume-state.test.ts (1)

180-214: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | ⚑ Quick win

This test passes vacuously.

createGenerationResultSnapshot never reads a top-level url field, so expect(snapshot.result).not.toHaveProperty('url') holds for any input β€” including a short, durable https: URL. If the intent is to pin the durable-URL policy, exercise durableUrlField directly through an artifact ref's externalUrl (data:, blob:, and >2048 chars) and assert the valid https case is retained.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-client/tests/generation-resume-state.test.ts` around lines 180 -
214, The test around createGenerationResultSnapshot currently passes without
validating URL filtering because it supplies a top-level url that the snapshot
builder ignores. Update it to exercise durableUrlField through an artifact ref’s
externalUrl, covering data:, blob:, and URLs over 2048 characters as omitted
while confirming a valid https URL is retained.
packages/ai-angular/src/inject-generation.ts (1)

113-139: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | ⚑ Quick win

Derive resumeState/pendingArtifacts/resultArtifacts instead of manually syncing 4 signals.

resumeState, pendingArtifacts, and resultArtifacts are separate mutable signals that setResumeSnapshotState must remember to update in lockstep with resumeSnapshot on every change. Solid's equivalent useGeneration avoids this duplication entirely by deriving these three fields as simple closures over a single resumeSnapshot signal (resumeState: () => resumeSnapshot()?.resumeState ?? null, etc.), with no manual sync path to drift. Angular's computed() gives the same guarantee here.

♻️ Proposed refactor using `computed()`
-  const resumeState = signal<GenerationResumeState | null>(
-    options.initialResumeSnapshot?.resumeState ?? null,
-  )
-  const pendingArtifacts = signal<Array<GenerationPendingArtifact>>(
-    options.initialResumeSnapshot?.pendingArtifacts ?? [],
-  )
-  const resultArtifacts = signal<Array<PersistedArtifactRef>>(
-    options.initialResumeSnapshot?.result?.artifacts ?? [],
-  )
+  const resumeState = computed(
+    () => resumeSnapshot()?.resumeState ?? null,
+  )
+  const pendingArtifacts = computed(
+    () => resumeSnapshot()?.pendingArtifacts ?? [],
+  )
+  const resultArtifacts = computed(
+    () => resumeSnapshot()?.result?.artifacts ?? [],
+  )
   let disposed = false

   const setResumeSnapshotState = (
     snapshot: GenerationResumeSnapshot | undefined,
   ) => {
     if (disposed) return
     resumeSnapshot.set(snapshot)
-    resumeState.set(snapshot?.resumeState ?? null)
-    pendingArtifacts.set(snapshot?.pendingArtifacts ?? [])
-    resultArtifacts.set(snapshot?.result?.artifacts ?? [])
   }

and in the return statement (computed() results are already read-only, so drop .asReadonly() on these three):

     resumeSnapshot: resumeSnapshot.asReadonly(),
-    resumeState: resumeState.asReadonly(),
-    pendingArtifacts: pendingArtifacts.asReadonly(),
-    resultArtifacts: resultArtifacts.asReadonly(),
+    resumeState,
+    pendingArtifacts,
+    resultArtifacts,

Please confirm computed is imported from @angular/core in this file (not shown in the unshown import lines) before applying.

Also applies to: 240-243

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-angular/src/inject-generation.ts` around lines 113 - 139, Replace
the mutable resumeState, pendingArtifacts, and resultArtifacts signals in the
generation setup with Angular computed values derived from the single
resumeSnapshot signal, confirming computed is imported from `@angular/core`.
Remove their manual updates from setResumeSnapshotState, and return the computed
values directly without asReadonly() while preserving the existing fallback
values.
packages/ai-solid/src/use-generation.ts (1)

223-224: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | πŸ’€ Low value

Garbled em dash in comment.

Same mojibake (Ò€”) as in use-generate-video.ts; see consolidated comment.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-solid/src/use-generation.ts` around lines 223 - 224, Replace the
garbled em dash in the mount-devtools comment near the generation flow with the
correct dash character, matching the corresponding comment in
use-generate-video.ts.
packages/ai-solid/src/use-generate-video.ts (1)

256-257: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial | πŸ’€ Low value

Garbled em dash in comment.

Ò€” is a mojibake artifact for β€”. Same pattern recurs in use-generation.ts; see consolidated comment.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-solid/src/use-generate-video.ts` around lines 256 - 257, Replace
the garbled β€œΓ’β‚¬β€β€ sequence in the mount/devtools comment with the intended em
dash β€œβ€”β€, and apply the same comment-text correction in the corresponding
occurrence in use-generation.ts.
packages/ai-react/src/use-transcription.ts (1)

33-36: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial

Same duplication concern as the other react generation hooks.

See consolidated comment; this hook duplicates persistence/initialResumeSnapshot/resume-return fields instead of extending shared UseGenerationOptions/UseGenerationReturn like the Solid equivalent.

Also applies to: 73-80

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-react/src/use-transcription.ts` around lines 33 - 36, Update the
transcription hook’s options and return types to extend the shared
UseGenerationOptions and UseGenerationReturn types, removing the duplicated
persistence, initialResumeSnapshot, and resume-related fields. Follow the
existing Solid generation hook pattern while preserving the hook’s
transcription-specific fields and behavior.
packages/ai-react/src/use-generate-speech.ts (1)

33-36: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial

Same duplication concern as use-generate-image.ts: options/return fields aren't inherited from a shared type.

See consolidated comment for details; the Solid equivalent hook uses Pick/Omit on UseGenerationOptions/UseGenerationReturn to avoid this duplication.

Also applies to: 73-80

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-react/src/use-generate-speech.ts` around lines 33 - 36, Update
the options and return types in the speech generation hook to derive shared
fields from UseGenerationOptions and UseGenerationReturn using Pick/Omit,
matching the pattern in the Solid equivalent and use-generate-image.ts. Keep
only speech-specific additions such as persistence and initialResumeSnapshot
locally defined, and remove duplicated inherited field declarations.
packages/ai-react/src/use-generate-image.ts (1)

33-36: πŸ“ Maintainability & Code Quality | πŸ”΅ Trivial

Consider extending shared UseGenerationOptions/UseGenerationReturn like the Solid hooks now do.

The Solid counterpart (packages/ai-solid/src/use-generate-image.ts) was refactored in this same PR to inherit persistence/initialResumeSnapshot and the resume/artifact return fields via Pick/Omit on the shared generation types, instead of duplicating them. This React hook (and its sibling use-generate-speech.ts/use-transcription.ts) still hand-duplicates the same fields and JSDoc, risking drift as the shared contract evolves.

Also applies to: 73-88

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-react/src/use-generate-image.ts` around lines 33 - 36, Refactor
the React generation hooks to derive persistence, initialResumeSnapshot, and
resume/artifact return fields from the shared UseGenerationOptions and
UseGenerationReturn types, following the Pick/Omit pattern used by the Solid
useGenerateImage hook. Remove the duplicated declarations and JSDoc from
useGenerateImage, useGenerateSpeech, and useTranscription while preserving their
existing public behavior.
πŸ€– Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/persistence/generation-persistence.md`:
- Line 237: Update the sentence beginning β€œA full page reload” to hyphenate the
compound adjective as β€œA full-page reload,” leaving the rest of the sentence
unchanged.
- Around line 147-165: The documented artifact GET example reads and serves
bytes without verifying caller authorization. Update the GET handler before
retrieveArtifact to derive the caller identity from trusted server-side session
state and authorize access to artifactId against the owning user or thread; only
perform retrieveArtifact and retrieveBlob after authorization, and clearly mark
this check as required for production routes.

In `@packages/ai-angular/src/inject-generation.ts`:
- Around line 217-218: Replace the mojibake em-dash in the comment near the
devtools-only generation logic in packages/ai-angular/src/inject-generation.ts
(lines 217-218) with a proper em dash; also replace both mojibake occurrences in
the useGenerateAudio transport-mode JSDoc in
packages/ai-react/src/use-generate-audio.ts (lines 83-88), without changing the
surrounding wording.

In `@packages/ai-client/src/generation-client.ts`:
- Around line 176-179: Move the disposed guard in generate ahead of
mountDevtools so calls after disposal return without re-registering devtools or
emitting bridge events. Preserve the existing isLoading guard and normal
devtools mounting for active clients.
- Around line 537-548: Update completePlainFetcherResumeSnapshot() to create a
new completed resume snapshot when this.resumeSnapshot is undefined, using the
client’s established snapshot shape and current generation context. Preserve
existing seeded-snapshot updates, callback notification, and persistence
behavior, and inspect VideoGenerationClient’s analogous fetcher completion hook
for the same missing initialization.

In `@packages/ai-client/src/video-generation-client.ts`:
- Around line 614-621: Update observeResumeSnapshot and the related persistence
flow so frequent stream chunks do not enqueue one storage write each. Coalesce
or debounce pending persistResumeSnapshot work to retain only the latest resume
snapshot, while preserving callback updates; ensure terminal events still
persist the final snapshot unconditionally.
- Around line 651-664: Update writeResumeSnapshot so a successful
serverPersistence.setItem call clears resumePersistenceError before returning.
Keep the existing error assignment and warning behavior in the catch block
unchanged, so getResumePersistenceError reflects only the latest write failure.
- Around line 623-634: Update completePlainFetcherResumeSnapshot so
plain-fetcher mode creates a terminal resume snapshot when this.resumeSnapshot
is undefined, using the configured persistence-compatible snapshot shape and
status "complete" with resumeState null. Preserve existing snapshot fields and
callback/persistence behavior when a snapshot already exists, ensuring fetcher
mode with persistence always persists the terminal state.

In `@packages/ai-event-client/src/index.ts`:
- Around line 615-618: Retain threadId and runId in the event interfaces in
packages/ai-event-client/src/index.ts, and update every lifecycle event payload
in generateAudio, generateImage, generateSpeech, and generateTranscription to
populate both IDs consistently across request, usage, completion, and error
emitters. Ensure each producer-side event includes the correlation values
available for its streamed or persisted run.

In `@packages/ai-persistence/src/middleware.ts`:
- Around line 46-53: Resolve the duplicate WithPersistenceOptions declarations
by keeping chat persistence options separate from generation persistence
options: rename the generation-specific declaration and update
withGenerationPersistence to use it, or consolidate only if both APIs
intentionally share all options. Ensure withPersistence and
withGenerationPersistence no longer accidentally accept each other’s option
fields.
- Around line 332-345: Update parseDataUrl to parse the data URL’s media type
and semicolon-delimited parameters generically, allowing parameters such as
codecs or charset before an optional final base64 marker. Return the MIME type
without parameters, decode the payload according to whether base64 is present,
and preserve undefined for invalid data URLs so callers do not process the
original data: string.
- Around line 617-655: Harden URL handling in the descriptor.url fetch path:
validate and allow only http/https URLs, invoke the existing allowlist or SSRF
guard hook before fetch, and use an AbortSignal timeout. Enforce a maximum
response size for both streamed and buffered bodies, aborting or rejecting
oversized responses before persisting them; preserve data-URL handling and
metadata behavior.

In `@packages/ai-persistence/src/retrieve.ts`:
- Around line 14-45: Update the JSDoc for retrieveArtifact and retrieveBlob to
state that artifact identifiers are opaque lookup keys and callers must derive
identity server-side and verify record.threadId ownership at the route boundary
before serving bytes. Remove or qualify wording that suggests mapping a null
result directly to a 404 without authorization.

In `@packages/ai-vue/src/use-generation.ts`:
- Around line 187-198: Guard the public onResult callbacks against disposed
scopes, matching the existing onError/onProgress/onChunk behavior. In
packages/ai-vue/src/use-generation.ts lines 187-198, return without invoking
options.onResult when disposed; apply the identical change to the baseOptions
onResult callback in packages/ai-vue/src/use-generate-video.ts lines 203-214.

---

Nitpick comments:
In @.changeset/generation-persistence.md:
- Around line 14-22: Remove the redundant introductory sentence from the
changeset body and keep one concise introduction covering client resume
snapshots and optional durable media storage. Preserve the existing β€œMedia byte
storage” and client-side generation persistence sections, using them as the
detailed follow-up content.

In `@packages/ai-angular/src/inject-generation.ts`:
- Around line 113-139: Replace the mutable resumeState, pendingArtifacts, and
resultArtifacts signals in the generation setup with Angular computed values
derived from the single resumeSnapshot signal, confirming computed is imported
from `@angular/core`. Remove their manual updates from setResumeSnapshotState, and
return the computed values directly without asReadonly() while preserving the
existing fallback values.

In `@packages/ai-client/src/generation-types.ts`:
- Around line 273-289: The CUSTOM chunk handling for GENERATION_EVENTS.ARTIFACTS
currently overwrites pendingArtifacts, losing earlier batches. Update this
branch to merge new artifact references with the existing next.pendingArtifacts
by artifactId while preserving prior entries; if the single-event behavior is
intentional instead, document that contract with a concise comment.

In `@packages/ai-client/tests/generation-resume-state.test.ts`:
- Around line 180-214: The test around createGenerationResultSnapshot currently
passes without validating URL filtering because it supplies a top-level url that
the snapshot builder ignores. Update it to exercise durableUrlField through an
artifact ref’s externalUrl, covering data:, blob:, and URLs over 2048 characters
as omitted while confirming a valid https URL is retained.

In `@packages/ai-persistence/tests/generation-artifacts.test.ts`:
- Around line 28-30: Remove the inert void-cast statements for
GenerationArtifactDescriptor, GenerationArtifactExtractionInput, and
GenerationArtifactNameInput. Use type-only imports for these symbols, or
reference them through a meaningful type assertion if the test requires type
coverage, without emitting runtime statements.

In `@packages/ai-react/src/use-generate-image.ts`:
- Around line 33-36: Refactor the React generation hooks to derive persistence,
initialResumeSnapshot, and resume/artifact return fields from the shared
UseGenerationOptions and UseGenerationReturn types, following the Pick/Omit
pattern used by the Solid useGenerateImage hook. Remove the duplicated
declarations and JSDoc from useGenerateImage, useGenerateSpeech, and
useTranscription while preserving their existing public behavior.

In `@packages/ai-react/src/use-generate-speech.ts`:
- Around line 33-36: Update the options and return types in the speech
generation hook to derive shared fields from UseGenerationOptions and
UseGenerationReturn using Pick/Omit, matching the pattern in the Solid
equivalent and use-generate-image.ts. Keep only speech-specific additions such
as persistence and initialResumeSnapshot locally defined, and remove duplicated
inherited field declarations.

In `@packages/ai-react/src/use-transcription.ts`:
- Around line 33-36: Update the transcription hook’s options and return types to
extend the shared UseGenerationOptions and UseGenerationReturn types, removing
the duplicated persistence, initialResumeSnapshot, and resume-related fields.
Follow the existing Solid generation hook pattern while preserving the hook’s
transcription-specific fields and behavior.

In `@packages/ai-solid/src/use-generate-audio.ts`:
- Around line 59-72: Remove the redundant result, isLoading, error, and status
members from UseGenerateAudioReturn, retaining only the generate override whose
input type differs. Continue inheriting the unchanged members through
Omit<UseGenerationReturn<TOutput>, 'generate'>.

In `@packages/ai-solid/src/use-generate-video.ts`:
- Around line 256-257: Replace the garbled β€œΓ’β‚¬β€β€ sequence in the mount/devtools
comment with the intended em dash β€œβ€”β€, and apply the same comment-text
correction in the corresponding occurrence in use-generation.ts.

In `@packages/ai-solid/src/use-generation.ts`:
- Around line 223-224: Replace the garbled em dash in the mount-devtools comment
near the generation flow with the correct dash character, matching the
corresponding comment in use-generate-video.ts.
πŸͺ„ Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
βš™οΈ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c51b7a1b-4fb8-4467-8a7a-defc34ae7003

πŸ“₯ Commits

Reviewing files that changed from the base of the PR and between 4ab149f and 5ec5ad9.

β›” Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
πŸ“’ Files selected for processing (72)
  • .changeset/generation-persistence.md
  • docs/config.json
  • docs/persistence/generation-persistence.md
  • examples/ts-react-chat/src/routes/generations.image.tsx
  • packages/ai-angular/src/inject-generate-audio.ts
  • packages/ai-angular/src/inject-generate-image.ts
  • packages/ai-angular/src/inject-generate-speech.ts
  • packages/ai-angular/src/inject-generate-video.ts
  • packages/ai-angular/src/inject-generation.ts
  • packages/ai-angular/src/inject-summarize.ts
  • packages/ai-angular/src/inject-transcription.ts
  • packages/ai-angular/tests/inject-generation.test.ts
  • packages/ai-client/src/generation-client.ts
  • packages/ai-client/src/generation-types.ts
  • packages/ai-client/src/index.ts
  • packages/ai-client/src/storage-adapters.ts
  • packages/ai-client/src/video-generation-client.ts
  • packages/ai-client/tests/generation-client.test.ts
  • packages/ai-client/tests/generation-resume-state.test.ts
  • packages/ai-event-client/src/index.ts
  • packages/ai-persistence/package.json
  • packages/ai-persistence/src/index.ts
  • packages/ai-persistence/src/memory.ts
  • packages/ai-persistence/src/middleware.ts
  • packages/ai-persistence/src/retrieve.ts
  • packages/ai-persistence/src/types.ts
  • packages/ai-persistence/tests/generation-artifacts.test.ts
  • packages/ai-persistence/tests/memory.test.ts
  • packages/ai-persistence/tests/persistence-types.test-d.ts
  • packages/ai-react/src/use-generate-audio.ts
  • packages/ai-react/src/use-generate-image.ts
  • packages/ai-react/src/use-generate-speech.ts
  • packages/ai-react/src/use-generate-video.ts
  • packages/ai-react/src/use-generation.ts
  • packages/ai-react/src/use-summarize.ts
  • packages/ai-react/src/use-transcription.ts
  • packages/ai-react/tests/use-generation.test.ts
  • packages/ai-solid/src/use-generate-audio.ts
  • packages/ai-solid/src/use-generate-image.ts
  • packages/ai-solid/src/use-generate-speech.ts
  • packages/ai-solid/src/use-generate-video.ts
  • packages/ai-solid/src/use-generation.ts
  • packages/ai-solid/src/use-summarize.ts
  • packages/ai-solid/src/use-transcription.ts
  • packages/ai-solid/tests/use-generation.test.ts
  • packages/ai-svelte/src/create-generate-audio.svelte.ts
  • packages/ai-svelte/src/create-generate-image.svelte.ts
  • packages/ai-svelte/src/create-generate-speech.svelte.ts
  • packages/ai-svelte/src/create-generate-video.svelte.ts
  • packages/ai-svelte/src/create-generation.svelte.ts
  • packages/ai-svelte/src/create-summarize.svelte.ts
  • packages/ai-svelte/src/create-transcription.svelte.ts
  • packages/ai-svelte/tests/create-generation.test.ts
  • packages/ai-utils/src/base64.ts
  • packages/ai-utils/src/index.ts
  • packages/ai-vue/src/use-generate-audio.ts
  • packages/ai-vue/src/use-generate-image.ts
  • packages/ai-vue/src/use-generate-speech.ts
  • packages/ai-vue/src/use-generate-video.ts
  • packages/ai-vue/src/use-generation.ts
  • packages/ai-vue/src/use-summarize.ts
  • packages/ai-vue/src/use-transcription.ts
  • packages/ai-vue/tests/use-generation.test.ts
  • packages/ai/src/activities/generateAudio/index.ts
  • packages/ai/src/activities/generateImage/index.ts
  • packages/ai/src/activities/generateSpeech/index.ts
  • packages/ai/src/activities/generateTranscription/index.ts
  • packages/ai/src/activities/middleware/index.ts
  • packages/ai/src/activities/middleware/run.ts
  • packages/ai/src/activities/middleware/types.ts
  • packages/ai/src/activities/stream-generation-result.ts
  • packages/ai/src/index.ts

Comment on lines +147 to +165
// Serve a stored artifact's bytes by id.
export async function GET(request: Request) {
const artifactId = new URL(request.url).searchParams.get('id')
if (!artifactId) return new Response('missing id', { status: 400 })

const artifact = await retrieveArtifact(persistence, artifactId)
if (!artifact) return new Response('not found', { status: 404 })

const blob = await retrieveBlob(persistence, artifact)
if (!blob) return new Response('not found', { status: 404 })

return new Response(blob.body ?? (await blob.arrayBuffer()), {
headers: {
'content-type': artifact.mimeType,
'content-length': String(artifact.size),
},
})
}
```

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”’ Security & Privacy | 🟠 Major | ⚑ Quick win

Artifact GET route has no authorization check.

The example serves stored bytes for any artifactId supplied in the query string, with no check that the caller is entitled to read that artifact. Since this doc is likely copied verbatim, this teaches an IDOR-prone pattern: anyone who obtains or guesses an artifactId can fetch another user's/thread's generated media.

Add a note (or code) showing that production routes must authorize the artifact against the caller's session/thread ownership before calling retrieveArtifact/retrieveBlob.

πŸ”’ Suggested addition
 export async function GET(request: Request) {
   const artifactId = new URL(request.url).searchParams.get('id')
   if (!artifactId) return new Response('missing id', { status: 400 })
 
   const artifact = await retrieveArtifact(persistence, artifactId)
   if (!artifact) return new Response('not found', { status: 404 })
+  // Authorize: verify the artifact's runId/threadId belongs to the
+  // requesting session before serving its bytes.
+  if (!(await isOwnedByCurrentUser(request, artifact))) {
+    return new Response('forbidden', { status: 403 })
+  }
 
   const blob = await retrieveBlob(persistence, artifact)

Based on learnings, "do not rely on client-provided [opaque identifiers] as an authorization mechanism ... derive the effective user/session identity from server-side session state, then authorize ... before any persistence reads."

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/persistence/generation-persistence.md` around lines 147 - 165, The
documented artifact GET example reads and serves bytes without verifying caller
authorization. Update the GET handler before retrieveArtifact to derive the
caller identity from trusted server-side session state and authorize access to
artifactId against the owning user or thread; only perform retrieveArtifact and
retrieveBlob after authorization, and clearly mark this check as required for
production routes.

Source: Learnings

re-attaches on its own through `fetchServerSentEvents` or `fetchHttpStream`, the
same adapters `useChat` uses.

A full page reload is different. The hooks do not start a run on mount, so they

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ“ Maintainability & Code Quality | 🟑 Minor | ⚑ Quick win

Minor grammar: hyphenate compound adjective.

"A full page reload is different" β†’ "A full-page reload is different."

🧰 Tools
πŸͺ› LanguageTool

[uncategorized] ~237-~237: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ..., the same adapters useChat` uses. A full page reload is different. The hooks do not s...

(EN_COMPOUND_ADJECTIVE_INTERNAL)

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/persistence/generation-persistence.md` at line 237, Update the sentence
beginning β€œA full page reload” to hyphenate the compound adjective as β€œA
full-page reload,” leaving the rest of the sentence unchanged.

Source: Linters/SAST tools

Comment on lines +217 to +218
// Mount devtools only. Generation runs are never auto-started after render Ò€”
// persisted state is read-only for display.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ“ Maintainability & Code Quality | 🟑 Minor | ⚑ Quick win

Mojibake em dash ("Ò€”") in newly-added comments/docs. Both sites introduce the same broken UTF-8-as-Latin1 em-dash artifact in new comment text; replace with a proper em dash (β€”).

  • packages/ai-angular/src/inject-generation.ts#L217-L218: fix "never auto-started after render Ò€”" to use a proper em dash.
  • packages/ai-react/src/use-generate-audio.ts#L83-L88: fix the two Ò€” occurrences in the useGenerateAudio JSDoc transport-mode bullets.
πŸ“ Affects 2 files
  • packages/ai-angular/src/inject-generation.ts#L217-L218 (this comment)
  • packages/ai-react/src/use-generate-audio.ts#L83-L88
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-angular/src/inject-generation.ts` around lines 217 - 218, Replace
the mojibake em-dash in the comment near the devtools-only generation logic in
packages/ai-angular/src/inject-generation.ts (lines 217-218) with a proper em
dash; also replace both mojibake occurrences in the useGenerateAudio
transport-mode JSDoc in packages/ai-react/src/use-generate-audio.ts (lines
83-88), without changing the surrounding wording.

Comment on lines 176 to 179
async generate(input: TInput): Promise<void> {
this.mountDevtools()
if (this.disposed) return
if (this.isLoading) return

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟑 Minor | ⚑ Quick win

mountDevtools() runs before the disposed check.

Calling generate() after dispose() re-registers devtools (devtoolsMounted was reset to false in dispose()) before the disposed guard returns, re-invoking emitRegistered()/emitSnapshot() on an already-disposed bridge.

πŸ”§ Suggested fix
   async generate(input: TInput): Promise<void> {
-    this.mountDevtools()
-    if (this.disposed) return
+    if (this.disposed) return
+    this.mountDevtools()
     if (this.isLoading) return
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async generate(input: TInput): Promise<void> {
this.mountDevtools()
if (this.disposed) return
if (this.isLoading) return
async generate(input: TInput): Promise<void> {
if (this.disposed) return
this.mountDevtools()
if (this.isLoading) return
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-client/src/generation-client.ts` around lines 176 - 179, Move the
disposed guard in generate ahead of mountDevtools so calls after disposal return
without re-registering devtools or emitting bridge events. Preserve the existing
isLoading guard and normal devtools mounting for active clients.

Comment on lines +537 to +548
private completePlainFetcherResumeSnapshot(): void {
if (!this.resumeSnapshot) {
return
}
this.resumeSnapshot = {
...this.resumeSnapshot,
resumeState: null,
status: 'complete',
}
this.callbacksRef.onResumeSnapshotChange?.(this.resumeSnapshot)
void this.persistResumeSnapshot(this.resumeSnapshot)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ—„οΈ Data Integrity & Integration | 🟠 Major | ⚑ Quick win

Fetcher-path completions never create a resume snapshot on the first run.

completePlainFetcherResumeSnapshot() is the only completion hook for the direct fetcher (non-streaming) path (called from generate()), but it no-ops when this.resumeSnapshot is undefined. Since the fetcher path never goes through processStream/observeResumeSnapshot, resumeSnapshot stays undefined unless an initialResumeSnapshot was supplied β€” so a client configured with persistence + fetcher and no seed snapshot never persists anything for a completed run. This silently breaks the persistence option for the entire fetcher use case, and isn't covered by the new "resume snapshot persistence" tests (which all use connection).

πŸ› Suggested fix
   private completePlainFetcherResumeSnapshot(): void {
-    if (!this.resumeSnapshot) {
-      return
-    }
     this.resumeSnapshot = {
-      ...this.resumeSnapshot,
+      ...(this.resumeSnapshot ?? {}),
       resumeState: null,
       status: 'complete',
     }
     this.callbacksRef.onResumeSnapshotChange?.(this.resumeSnapshot)
     void this.persistResumeSnapshot(this.resumeSnapshot)
   }

Worth checking whether VideoGenerationClient's fetcher path has the same gap.

πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private completePlainFetcherResumeSnapshot(): void {
if (!this.resumeSnapshot) {
return
}
this.resumeSnapshot = {
...this.resumeSnapshot,
resumeState: null,
status: 'complete',
}
this.callbacksRef.onResumeSnapshotChange?.(this.resumeSnapshot)
void this.persistResumeSnapshot(this.resumeSnapshot)
}
private completePlainFetcherResumeSnapshot(): void {
this.resumeSnapshot = {
...(this.resumeSnapshot ?? {}),
resumeState: null,
status: 'complete',
}
this.callbacksRef.onResumeSnapshotChange?.(this.resumeSnapshot)
void this.persistResumeSnapshot(this.resumeSnapshot)
}
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-client/src/generation-client.ts` around lines 537 - 548, Update
completePlainFetcherResumeSnapshot() to create a new completed resume snapshot
when this.resumeSnapshot is undefined, using the client’s established snapshot
shape and current generation context. Preserve existing seeded-snapshot updates,
callback notification, and persistence behavior, and inspect
VideoGenerationClient’s analogous fetcher completion hook for the same missing
initialization.

Comment on lines +46 to +53
export interface WithPersistenceOptions {
extractArtifacts?: (
input: GenerationArtifactExtractionInput,
) =>
| Array<GenerationArtifactDescriptor | PersistedArtifactRef>
| Promise<Array<GenerationArtifactDescriptor | PersistedArtifactRef>>
nameArtifact?: (input: GenerationArtifactNameInput) => string
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ“ Maintainability & Code Quality | 🟠 Major | ⚑ Quick win

WithPersistenceOptions is declared twice in this file (interface merging).

Line 916 declares another export interface WithPersistenceOptions with snapshotStreaming / snapshotIntervalMs. TypeScript merges them, so both withPersistence and withGenerationPersistence silently accept the union of chat-snapshot and artifact options. If that is intentional, fold the new members into the existing declaration; if not, give the generation entrypoint its own option type (e.g. WithGenerationPersistenceOptions) so the two surfaces stay distinct.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-persistence/src/middleware.ts` around lines 46 - 53, Resolve the
duplicate WithPersistenceOptions declarations by keeping chat persistence
options separate from generation persistence options: rename the
generation-specific declaration and update withGenerationPersistence to use it,
or consolidate only if both APIs intentionally share all options. Ensure
withPersistence and withGenerationPersistence no longer accidentally accept each
other’s option fields.

Comment on lines +332 to +345
function parseDataUrl(
value: string,
): { mimeType: string; bytes: Uint8Array } | undefined {
const match = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(value)
if (!match) return undefined
const mimeType = match[1] || 'application/octet-stream'
const payload = decodeURIComponent(match[3] ?? '')
return {
mimeType,
bytes: match[2]
? base64ToUint8Array(payload)
: new TextEncoder().encode(payload),
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚑ Quick win

parseDataUrl rejects data URLs carrying extra media-type parameters.

The regex only allows ;base64 immediately after the MIME type, so data:audio/wav;codecs=1;base64,… or data:text/plain;charset=utf-8,… fails to match. The consequences are silent: line 546 then feeds the whole data: string to base64ToUint8Array, persisting garbage bytes, and line 626 tries to fetch() a data: URL.

Parse the parameter list generically instead.

πŸ› Proposed fix
-  const match = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(value)
+  const match = /^data:([^,]*),(.*)$/s.exec(value)
   if (!match) return undefined
-  const mimeType = match[1] || 'application/octet-stream'
-  const payload = decodeURIComponent(match[3] ?? '')
+  const meta = match[1] ?? ''
+  const params = meta.split(';')
+  const isBase64 = params.at(-1)?.trim().toLowerCase() === 'base64'
+  const mimeType = params[0] || 'application/octet-stream'
+  const raw = match[2] ?? ''
+  const payload = isBase64 ? raw.trim() : decodeURIComponent(raw)
   return {
     mimeType,
-    bytes: match[2]
+    bytes: isBase64
       ? base64ToUint8Array(payload)
       : new TextEncoder().encode(payload),
   }
πŸ“ Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function parseDataUrl(
value: string,
): { mimeType: string; bytes: Uint8Array } | undefined {
const match = /^data:([^;,]+)?(;base64)?,(.*)$/s.exec(value)
if (!match) return undefined
const mimeType = match[1] || 'application/octet-stream'
const payload = decodeURIComponent(match[3] ?? '')
return {
mimeType,
bytes: match[2]
? base64ToUint8Array(payload)
: new TextEncoder().encode(payload),
}
}
function parseDataUrl(
value: string,
): { mimeType: string; bytes: Uint8Array } | undefined {
const match = /^data:([^,]*),(.*)$/s.exec(value)
if (!match) return undefined
const meta = match[1] ?? ''
const params = meta.split(';')
const isBase64 = params.at(-1)?.trim().toLowerCase() === 'base64'
const mimeType = params[0] || 'application/octet-stream'
const raw = match[2] ?? ''
const payload = isBase64 ? raw.trim() : decodeURIComponent(raw)
return {
mimeType,
bytes: isBase64
? base64ToUint8Array(payload)
: new TextEncoder().encode(payload),
}
}
🧰 Tools
πŸͺ› OpenGrep (1.25.0)

[ERROR] 335-335: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-persistence/src/middleware.ts` around lines 332 - 345, Update
parseDataUrl to parse the data URL’s media type and semicolon-delimited
parameters generically, allowing parameters such as codecs or charset before an
optional final base64 marker. Return the MIME type without parameters, decode
the payload according to whether base64 is present, and preserve undefined for
invalid data URLs so callers do not process the original data: string.

Comment on lines +617 to +655
if (descriptor.url) {
const data = parseDataUrl(descriptor.url)
if (data) {
return {
body: data.bytes,
size: data.bytes.byteLength,
mimeType: descriptor.mimeType ?? data.mimeType,
}
}
const response = await fetch(descriptor.url)
if (!response.ok) {
throw new Error(
`Failed to persist artifact from ${descriptor.url}: HTTP ${response.status}`,
)
}
const mimeType =
descriptor.mimeType ??
response.headers.get('content-type') ??
'application/octet-stream'
// Stream the body straight into the blob store instead of buffering the
// whole artifact in memory. `size` is left 0 (unknown up front); the store
// records the actual byte length as it drains the stream. Fall back to
// buffering only when the response has no body to stream.
if (response.body) {
return {
body: response.body,
size: 0,
mimeType,
externalUrl: descriptor.url,
}
}
const body = await response.arrayBuffer()
return {
body,
size: body.byteLength,
mimeType,
externalUrl: descriptor.url,
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”’ Security & Privacy | πŸ”΄ Critical | πŸ—οΈ Heavy lift

Unbounded server-side fetch of a model/prompt-supplied URL β€” SSRF and no timeout.

descriptor.url reaches here from built-in extraction of prompt parts (sourcePartDescriptors, url source values) and provider output, i.e. potentially attacker-influenced input. The server then fetches it with no scheme/host validation, no redirect policy, no size cap, and no timeout, and streams the response into durable storage. That allows internal-network probing/exfiltration into the blob store, plus a request thread that can hang indefinitely or store an unbounded payload.

At minimum: restrict to http/https, apply an allowlist or SSRF guard hook, pass an AbortSignal timeout, and cap the persisted byte length.

πŸ”’ Sketch of the guard
-    const response = await fetch(descriptor.url)
+    const target = new URL(descriptor.url)
+    if (target.protocol !== 'https:' && target.protocol !== 'http:') {
+      throw new Error(`Refusing to fetch artifact from ${target.protocol}`)
+    }
+    // Callers should be able to veto hosts (private ranges, metadata IPs, …).
+    await assertFetchableArtifactUrl?.(target)
+    const response = await fetch(target, {
+      redirect: 'error',
+      signal: AbortSignal.timeout(artifactFetchTimeoutMs ?? 30_000),
+    })
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-persistence/src/middleware.ts` around lines 617 - 655, Harden URL
handling in the descriptor.url fetch path: validate and allow only http/https
URLs, invoke the existing allowlist or SSRF guard hook before fetch, and use an
AbortSignal timeout. Enforce a maximum response size for both streamed and
buffered bodies, aborting or rejecting oversized responses before persisting
them; preserve data-URL handling and metadata behavior.

Comment on lines +14 to +45
/**
* Look up a persisted generation artifact's metadata by id. Returns `null` when
* the persistence has no `artifacts` store or no record matches β€” so a serve
* handler can map that straight to a 404.
*/
export async function retrieveArtifact(
persistence: AIPersistence,
artifactId: string,
): Promise<ArtifactRecord | null> {
const record = await persistence.stores.artifacts?.get(artifactId)
return record ?? null
}

/**
* Look up a persisted generation artifact's stored bytes. Pass an `artifactId`
* (resolved to its record first) or an already-loaded {@link ArtifactRecord}
* (no second metadata lookup). Returns `null` when the artifact, its record, or
* its blob is missing, or the stores are not configured.
*/
export async function retrieveBlob(
persistence: AIPersistence,
artifact: string | ArtifactRecord,
): Promise<BlobObject | null> {
const record =
typeof artifact === 'string'
? await retrieveArtifact(persistence, artifact)
: artifact
if (!record) return null

const blob = await persistence.stores.blobs?.get(artifactBlobKey(record))
return blob ?? null
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”’ Security & Privacy | 🟠 Major | ⚑ Quick win

Document that callers must authorize before serving artifact bytes.

retrieveArtifact/retrieveBlob key purely on a client-supplied artifactId and return the record's threadId/runId without any ownership check. The JSDoc suggests wiring the null case straight to a 404 in a serve handler, which invites an IDOR if the handler skips authorization. Add a line telling callers to derive identity server-side and verify record.threadId ownership before returning bytes.

Based on learnings: treat identifiers as opaque lookup keys and authorize thread ownership at the route boundary before any persistence read.

πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-persistence/src/retrieve.ts` around lines 14 - 45, Update the
JSDoc for retrieveArtifact and retrieveBlob to state that artifact identifiers
are opaque lookup keys and callers must derive identity server-side and verify
record.threadId ownership at the route boundary before serving bytes. Remove or
qualify wording that suggests mapping a null result directly to a 404 without
authorization.

Source: Learnings

Comment on lines 187 to +198
onResult: ((r: TResult) => options.onResult?.(r)) as (
result: TResult,
) => TOutput | null | void,
onError: (e: Error) => options.onError?.(e),
onProgress: (p: number, m?: string) => options.onProgress?.(p, m),
onChunk: (c: StreamChunk) => options.onChunk?.(c),
onError: (e: Error) => {
if (!disposed) options.onError?.(e)
},
onProgress: (p: number, m?: string) => {
if (!disposed) options.onProgress?.(p, m)
},
onChunk: (c: StreamChunk) => {
if (!disposed) options.onChunk?.(c)
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚑ Quick win

onResult callback missing the new disposed guard in both useGeneration and useGenerateVideo. This PR wraps onError/onProgress/onChunk (and the internal on*Change state setters) with if (!disposed) checks to stop firing after onScopeDispose, but the sibling public user callback onResult was left unguarded in both places β€” the shared root cause is one missing guard pattern applied to a new callback category.

  • packages/ai-vue/src/use-generation.ts#L187-L198: wrap the onResult callback body with the same if (disposed) return guard used for onError/onProgress/onChunk just below it.
  • packages/ai-vue/src/use-generate-video.ts#L203-L214: apply the identical guard to onResult in baseOptions, matching onError/onProgress/onChunk.
πŸ“ Affects 2 files
  • packages/ai-vue/src/use-generation.ts#L187-L198 (this comment)
  • packages/ai-vue/src/use-generate-video.ts#L203-L214
πŸ€– Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/ai-vue/src/use-generation.ts` around lines 187 - 198, Guard the
public onResult callbacks against disposed scopes, matching the existing
onError/onProgress/onChunk behavior. In packages/ai-vue/src/use-generation.ts
lines 187-198, return without invoking options.onResult when disposed; apply the
identical change to the baseOptions onResult callback in
packages/ai-vue/src/use-generate-video.ts lines 203-214.

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.

1 participant