Skip to content

feat(persistence): client-side generation resume snapshot - #997

Open
tombeckenham wants to merge 17 commits into
mainfrom
feat/generation-persistence-client
Open

feat(persistence): client-side generation resume snapshot#997
tombeckenham wants to merge 17 commits into
mainfrom
feat/generation-persistence-client

Conversation

@tombeckenham

@tombeckenham tombeckenham commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

🎯 Changes

Stack 1 of 2. Split out of #987, rebuilt on the current feat/persistence-core (which dropped the packaged backends in 62c99754d). Stacked follow-up: the durable media-byte half.

Client-side generation persistence: a lightweight resume snapshot for media generation activities.

As a run streams, the client builds a GenerationResumeSnapshot — run identity, status, errors, and result metadata, but never the generated media bytes — and writes it to a persistence storage adapter under the key generation:<id> (namespaced so a chat client sharing the id and adapter can't collide). On construction the client reads the snapshot back, validated through the new parseGenerationResumeSnapshot(unknown) export, so the last run's outcome survives a full page reload; an explicit initialResumeSnapshot seed skips the read for apps that manage storage themselves. The option reuses the same ChatStorageAdapter contract as chat, so localStoragePersistence / sessionStoragePersistence / indexedDBPersistence work for generations too — the factory defaults stay typed to the chat shape as shipped in #984; inline usage infers the value type contextually, and a standalone generation store states its type explicitly.

const snapshots = localStoragePersistence<GenerationResumeSnapshot>({
  keyPrefix: 'my-app:',
})
useGenerateImage({ id: 'hero-image', connection, persistence: snapshots })
// after reload: resumeSnapshot holds the last run's status/error/result metadata

Generation hooks (useGenerateImage, useGenerateVideo, useGenerateAudio, useGenerateSpeech, useGeneration, useSummarize, useTranscription, plus the Solid/Vue/Svelte/Angular equivalents) accept persistence and initialResumeSnapshot, and expose resumeSnapshot / resumeState / pendingArtifacts / resultArtifacts.

The base generation return types (UseGenerationReturn / CreateGenerationReturn / InjectGenerationResult) also gained a defaulted TInput generic across all five frameworks, so generate is precisely typed (input: TInput) => Promise<void> — this deleted the unsound internal narrow-to-wide casts and every wrapper-level generate as cast (twenty-five in total), with existing single-generic references unaffected.

Snapshot lifecycle is fully specified: stop() writes a terminal (no longer resumable) record, transport-level failures write an error record, reset() deletes the persisted record, a new RUN_STARTED drops stale result/error/artifacts from the previous run, plain (non-Response) fetcher runs record a complete snapshot built from the fetcher result, and video runs capture jobId from video:job:created mid-run. Persisted snapshots carry schemaVersion: 1; writes are skipped when nothing material changed (no more one-write-per-chunk), and persistence failures warn once per failure transition.

There is no resume() action and no restart of provider work — generation still only begins when generate(...) is called. Reconnecting to an in-flight stream is the delivery layer's job (resumable streams, #955), wired via a durability adapter + GET handler exactly as for chat.

PersistedArtifactRef already exists on the base branch as dormant wire surface, so this half consumes it and is independently mergeable — the generation:artifacts event simply never fires until the stacked byte-storage PR lands. The hook JSDoc now says so explicitly.

Packages

  • @tanstack/ai-client — the resume-snapshot reducer, parseGenerationResumeSnapshot, hydration, the persistence option, artifact refs.
  • framework hooks — react / solid / vue / svelte / angular (each re-exports the persistence types).
  • Docsdocs/persistence/generation-persistence.md (kiira-verified) + agent-skill updates (ai-core/client-persistence, ai-core/media-generation).
  • E2Etesting/e2e/tests/generation-persistence.spec.ts: provider-free reload-and-rehydrate spec (write → reload → hydrate → reset).

✅ Resolution of the previously-listed known issues

All items from the earlier revision of this PR body have been addressed:

  • React StrictMode blockermountDevtools() now revives a disposed client (the hooks call it from their mount effects); generate() checks disposed before mounting devtools. Covered by a <StrictMode> hook test and client-level revive tests.
  • No hydration — the client now reads persistence.getItem('generation:<id>') on construction, validated; docs/example/E2E all exercise the reload path for real.
  • Write-per-chunk + warn spam — writes are gated on material change (lastEvent-only churn is skipped); failures warn once per transition and getResumePersistenceError() self-clears on the next successful write.
  • Fetcher path recorded nothing — plain fetcher runs now record a complete snapshot built from the fetcher result (no seed required).
  • Key collision with chat — generation records are namespaced generation:<id>.
  • threadId/runId on 18 devtools event interfaces — reverted entirely: BaseEventContext already declares both fields, the additions were redundant redeclarations. @tanstack/ai-event-client is untouched and dropped from the changeset.
  • TValue = any widening — reverted: the factory defaults are back to ChatPersistedState as shipped in feat(persistence): server persistence + client browser-refresh durability #984. Inline usage infers the value type contextually for both chat and generation, so nothing is lost; a standalone generation store states its type explicitly (localStoragePersistence<GenerationResumeSnapshot>()), and the three oxlint suppressions are gone. Hydrated reads remain runtime-validated regardless.
  • No E2E — added (see above), plus hydration/reset tests in all five framework packages.
  • Cross-run contamination (stale error/result carried into the next run) — fixed in the reducer with RUN_STARTED boundaries; stop()/reset()/transport errors leave consistent durable records.

getResumePersistenceError() remains client-only observability (now self-clearing); exposing it through the hooks is deliberately deferred.

✅ Checklist

  • I have followed the steps in the Contributing guide.
  • I have tested this code locally with pnpm run test:pr. — full run green (287 tasks incl. sherif/knip/docs/kiira/oxlint/lib/types/build), plus the complete Playwright E2E suite.

🚀 Release Impact

  • This change affects published code, and I have generated a changeset.
  • This change is docs/CI/dev-only (no release).

Summary by CodeRabbit

  • New Features

    • Added client-side persistence for media generation status and result metadata across reloads.
    • Generation hooks across supported frameworks now expose resume state, snapshots, and artifact references.
    • Added storage adapter support for local, session, and IndexedDB persistence.
    • Added a persisted image-generation example and end-to-end refresh testing.
  • Documentation

    • Added comprehensive Generation Persistence documentation, examples, navigation, and guidance on stored metadata and reset behavior.
  • Bug Fixes

    • Prevented late stream updates after stopping or disposing generation runs.
    • Persisted terminal states for stopped and failed runs.

@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

Generation clients now persist validated, lightweight resume snapshots without generated media bytes. The feature is wired through React, Vue, Solid, Svelte, and Angular APIs, documented, demonstrated in examples, and covered by unit, integration, and browser-refresh tests.

Changes

Generation persistence

Layer / File(s) Summary
Snapshot contracts and client lifecycle
packages/ai-client/src/generation-types.ts, packages/ai-client/src/generation-client.ts, packages/ai-client/src/video-generation-client.ts
Adds snapshot types, stream reduction, validation, hydration, persistence de-duplication, terminal state handling, reset cleanup, and defensive getters.
Framework API integration
packages/ai-react/..., packages/ai-vue/..., packages/ai-solid/..., packages/ai-svelte/..., packages/ai-angular/...
Adds persistence and initialResumeSnapshot options and exposes resume snapshot, resume state, and artifact fields through framework-specific APIs.
Validation and examples
packages/ai-client/tests/..., packages/ai-react/tests/..., packages/ai-vue/tests/..., packages/ai-solid/tests/..., packages/ai-svelte/tests/..., packages/ai-angular/tests/..., examples/ts-react-chat/...
Tests hydration, seed precedence, no auto-start behavior, abort handling, persistence ordering, reset cleanup, and persisted image-generation display.
Documentation and e2e flow
docs/persistence/..., packages/ai/skills/..., testing/e2e/...
Documents server/client persistence behavior and adds a deterministic browser-refresh test that verifies metadata persistence without image bytes.

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

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant GenerationHook
  participant GenerationClient
  participant StorageAdapter
  User->>GenerationHook: generate(input)
  GenerationHook->>GenerationClient: start generation
  GenerationClient->>GenerationClient: reduce stream chunks
  GenerationClient->>StorageAdapter: persist generation:<id> snapshot
  StorageAdapter-->>GenerationClient: stored snapshot
  User->>GenerationHook: reload application
  GenerationHook->>StorageAdapter: hydrate snapshot
  StorageAdapter-->>GenerationHook: resume metadata
Loading

Possibly related PRs

  • TanStack/ai#999: Extends the same generation persistence plumbing with server-side artifact and byte persistence.

Suggested labels: waiting-on: author, persistence

Suggested reviewers: alemtuzlak

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.63% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
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.
Title check ✅ Passed The title is concise and accurately summarizes the main change: client-side generation resume snapshot persistence.
Description check ✅ Passed The PR description follows the template and includes changes, checklist, and release impact sections with concrete details.
✨ 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-client

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

16 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

🟩 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 a1fb436

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

☁️ Nx Cloud last updated this comment at 2026-07-28 06:45:42 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@997

@tanstack/ai-acp

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

@tanstack/ai-angular

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

@tanstack/ai-anthropic

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

@tanstack/ai-bedrock

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

@tanstack/ai-claude-code

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

@tanstack/ai-client

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

@tanstack/ai-code-mode

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

@tanstack/ai-code-mode-skills

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

@tanstack/ai-codex

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

@tanstack/ai-devtools-core

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

@tanstack/ai-durable-stream

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

@tanstack/ai-elevenlabs

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

@tanstack/ai-event-client

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

@tanstack/ai-fal

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

@tanstack/ai-gemini

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

@tanstack/ai-grok

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

@tanstack/ai-grok-build

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

@tanstack/ai-groq

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

@tanstack/ai-isolate-cloudflare

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

@tanstack/ai-isolate-node

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

@tanstack/ai-isolate-quickjs

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

@tanstack/ai-mcp

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

@tanstack/ai-memory

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

@tanstack/ai-mistral

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

@tanstack/ai-ollama

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

@tanstack/ai-openai

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

@tanstack/ai-opencode

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

@tanstack/ai-openrouter

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

@tanstack/ai-persistence

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

@tanstack/ai-preact

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

@tanstack/ai-react

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

@tanstack/ai-react-ui

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

@tanstack/ai-sandbox

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

@tanstack/ai-sandbox-cloudflare

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

@tanstack/ai-sandbox-daytona

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

@tanstack/ai-sandbox-docker

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

@tanstack/ai-sandbox-local-process

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

@tanstack/ai-sandbox-sprites

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

@tanstack/ai-sandbox-vercel

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

@tanstack/ai-solid

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

@tanstack/ai-solid-ui

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

@tanstack/ai-svelte

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

@tanstack/ai-utils

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

@tanstack/ai-vue

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

@tanstack/ai-vue-ui

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

@tanstack/openai-base

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

@tanstack/preact-ai-devtools

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

@tanstack/react-ai-devtools

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

@tanstack/solid-ai-devtools

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

commit: a1fb436

Base automatically changed from feat/persistence-core to main July 27, 2026 16:27
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 and others added 9 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.
@tombeckenham
tombeckenham marked this pull request as ready for review July 28, 2026 07:25
@tombeckenham
tombeckenham requested review from a team and AlemTuzlak July 28, 2026 07:34

@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: 5

🧹 Nitpick comments (10)
packages/ai-react/src/use-generate-audio.ts (1)

73-80: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

React wrappers hand-copy the resume/artifact surface instead of inheriting it. The same four fields (resumeSnapshot, resumeState, pendingArtifacts, resultArtifacts) and their caveat JSDoc are duplicated across four React return types, while the Vue and Angular wrappers in this PR derive them via extends Omit<UseGenerationReturn<TOutput>, 'generate'> / Omit<InjectGenerationResult<TOutput>, 'generate'>. Any future change to the surface now needs four synchronized edits, and the "always empty until the artifact pipeline ships" note will drift.

  • packages/ai-react/src/use-generate-audio.ts#L73-L80: replace the four copied fields with extends Omit<UseGenerationReturn<TOutput, AudioGenerateInput>, 'generate'> and keep only the narrowed generate.
  • packages/ai-react/src/use-generate-image.ts#L73-L80: same, inheriting from UseGenerationReturn<TOutput, ImageGenerateInput>.
  • packages/ai-react/src/use-summarize.ts#L73-L80: same, inheriting from UseGenerationReturn<TOutput, SummarizeGenerateInput>.
  • packages/ai-react/src/use-generate-video.ts#L83-L90: useGenerateVideo builds its own client, so extract the four field declarations into a shared GenerationResumeSurface interface and extend that here instead of re-declaring them.
🤖 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-audio.ts` around lines 73 - 80, Replace
the duplicated resume/artifact field declarations with shared inherited types:
in packages/ai-react/src/use-generate-audio.ts lines 73-80,
packages/ai-react/src/use-generate-image.ts lines 73-80, and
packages/ai-react/src/use-summarize.ts lines 73-80, extend
Omit<UseGenerationReturn<TOutput, respective input type>, 'generate'> while
retaining only the narrowed generate member. In
packages/ai-react/src/use-generate-video.ts lines 83-90, extract those four
fields and their caveat JSDoc into a shared GenerationResumeSurface interface,
then extend it instead of re-declaring them.
packages/ai-client/src/video-generation-client.ts (1)

641-828: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Extract resume-snapshot persistence into a shared helper.

GenerationClient and VideoGenerationClient both contain near-identical hydration/persist/dedupe/queue/clear/error-warn resume-snapshot lifecycle code. Move that implementation to a shared ResumeSnapshotStore (constructed with the storage adapter and key) so the two clients share the same semantics and avoid prefix-only naming drift like resumeSnapshotSignature vs videoResumeSnapshotSignature.

🤖 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/video-generation-client.ts` around lines 641 - 828,
Extract the resume snapshot lifecycle currently implemented by methods such as
maybeHydrateResumeSnapshot, hydrateResumeSnapshot, persistResumeSnapshot,
writeResumeSnapshot, removePersistedResumeSnapshot, and clearResumeSnapshot into
a shared ResumeSnapshotStore that accepts the storage adapter and snapshot key.
Update GenerationClient and VideoGenerationClient to delegate hydration,
persistence, deduplication, queued writes, clearing, and warning/error state to
this store, and replace the client-specific videoResumeSnapshotSignature with
the shared implementation so both clients retain identical semantics.
packages/ai-client/src/generation-client.ts (1)

439-464: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider extracting a snapshot-clone/carry helper.

This nested conditional-spread clone is repeated in shape at Lines 574-587 (completePlainFetcherResumeSnapshot) and Lines 602-612 (recordResumeSnapshotError). A single cloneResumeSnapshot(snapshot) / carryForward(previous) helper would remove the duplication and keep the three sites from drifting when a new snapshot field is added.

🤖 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 439 - 464, Extract
the repeated resume-snapshot cloning logic from getResumeSnapshot,
completePlainFetcherResumeSnapshot, and recordResumeSnapshotError into a shared
cloneResumeSnapshot or carryForward helper. Update all three call sites to use
it, preserving the existing shallow-copy behavior for nested pendingArtifacts,
result.artifacts, error, and lastEvent fields so future snapshot fields remain
consistent.
packages/ai-client/tests/generation-client.test.ts (1)

1329-1361: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Also assert the new getResumePersistenceError() surface.

This case covers the console.warn side effect but not the public getter that exposes the failure (and its reset back to undefined after a later successful write). Adding those assertions here would lock in the only user-visible signal of persistence failure.

🤖 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-client.test.ts` around lines 1329 - 1361,
Extend the rejected-write test around GenerationClient.generate to assert
getResumePersistenceError() returns persistenceError after the failed setItem
call. Then make a subsequent successful persistence write and assert the getter
resets to undefined, preserving the existing warning assertion and generation
resolution behavior.
packages/ai-react/src/use-generate-speech.ts (1)

33-36: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

React wrapper hooks hand-copy the resume surface instead of deriving it. Both hooks re-declare persistence / initialResumeSnapshot and the four resume/artifact return fields verbatim, while the Vue and Svelte wrappers now derive them (Pick<...Options, 'persistence' | 'initialResumeSnapshot'>, Omit<...Return, 'generate'>). The next field added to useGeneration will silently miss these public types.

  • packages/ai-react/src/use-generate-speech.ts#L33-L36: have UseGenerateSpeechOptions extend Pick<UseGenerationOptions<SpeechGenerateInput, TTSResult, TOutput>, 'persistence' | 'initialResumeSnapshot'> and UseGenerateSpeechReturn extend Omit<UseGenerationReturn<TOutput, SpeechGenerateInput>, 'generate'>.
  • packages/ai-react/src/use-transcription.ts#L33-L36: apply the same derivation with TranscriptionGenerateInput / TranscriptionResult.
🤖 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, Derive
the React wrapper types from the shared generation types instead of hand-copying
resume and artifact fields. In packages/ai-react/src/use-generate-speech.ts at
lines 33-36, make UseGenerateSpeechOptions extend
Pick<UseGenerationOptions<SpeechGenerateInput, TTSResult, TOutput>,
'persistence' | 'initialResumeSnapshot'> and UseGenerateSpeechReturn extend
Omit<UseGenerationReturn<TOutput, SpeechGenerateInput>, 'generate'>; apply the
same change in packages/ai-react/src/use-transcription.ts at lines 33-36 using
TranscriptionGenerateInput and TranscriptionResult.
packages/ai-svelte/tests/create-generation.test.ts (1)

248-273: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use flushAsync() instead of a single await Promise.resolve() in these negative-assertion tests.

Hydration and persistence work run through detached promise queues (that's why flushAsync exists). One microtask isn't enough to prove getItem/connect were never called — a regression that hydrates a macrotask later would still pass.

💚 Suggested change (both tests)
-      await Promise.resolve()
+      await flushAsync()

Also applies to: 808-828

🤖 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-svelte/tests/create-generation.test.ts` around lines 248 - 273,
Update both negative-assertion tests in create-generation.test.ts, including the
test near the referenced additional range, to await the existing flushAsync()
helper instead of a single Promise.resolve() microtask. Keep the assertions
verifying that getItem and connect were not called, so detached hydration and
persistence queues are fully drained before checking behavior.
packages/ai-svelte/src/create-generate-video.svelte.ts (1)

311-322: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Empty-artifact getters allocate a new array per read.

packages/ai-vue/src/use-generate-video.ts deliberately uses shared EMPTY_* constants so empty lists keep a stable identity for downstream watchers; these getters return a fresh [] on every access, which defeats reference comparison in $derived/$effect consumers.

♻️ Use shared fallbacks
     get pendingArtifacts() {
-      return resumeSnapshot?.pendingArtifacts ?? []
+      return resumeSnapshot?.pendingArtifacts ?? EMPTY_PENDING_ARTIFACTS
     },
     get resultArtifacts() {
-      return resumeSnapshot?.result?.artifacts ?? []
+      return resumeSnapshot?.result?.artifacts ?? EMPTY_RESULT_ARTIFACTS
     },

Add at module scope:

const EMPTY_PENDING_ARTIFACTS: Array<GenerationPendingArtifact> = []
const EMPTY_RESULT_ARTIFACTS: Array<PersistedArtifactRef> = []
🤖 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-svelte/src/create-generate-video.svelte.ts` around lines 311 -
322, Update the module containing the resumeSnapshot getters to define shared
EMPTY_PENDING_ARTIFACTS and EMPTY_RESULT_ARTIFACTS constants at module scope,
then use them as the nullish fallbacks in pendingArtifacts and resultArtifacts
instead of allocating [] on each read. Keep the existing non-empty artifact
values unchanged.
packages/ai-angular/tests/inject-generation.test.ts (2)

73-103: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Persistence/adapter test helpers copy-pasted per framework suite and already diverging. The same createMapPersistence / createRunContextCaptureAdapter / flushPromises trio is redefined in each framework test file with different return shapes, so the persistence contract is asserted slightly differently in each.

  • packages/ai-angular/tests/inject-generation.test.ts#L73-L103: import a shared GenerationPersistence mock util instead of the local wrapper-object variant.
  • packages/ai-vue/tests/use-generation.test.ts#L126-L142: replace the adapter-with-store variant with the same shared util.
🤖 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/tests/inject-generation.test.ts` around lines 73 - 103,
Replace the local createMapPersistence helper in
packages/ai-angular/tests/inject-generation.test.ts (lines 73-103) and
packages/ai-vue/tests/use-generation.test.ts (lines 126-142) with the shared
GenerationPersistence mock utility, importing it wherever needed and updating
call sites to its return shape. Remove the duplicated framework-specific
wrapper/store variants so both suites assert the same persistence contract.

105-111: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Prefer vi.waitFor over a fixed microtask count.

Hydration and removal both go through chained promise queues inside the client; a hard-coded 5-tick drain will silently become insufficient if another .then link is added, producing intermittent failures rather than a clear one.

♻️ Suggested alternative
-// Hydration and snapshot removal both run through awaited promise chains, so
-// drain the microtask queue rather than awaiting a single tick.
-async function flushPromises(): Promise<void> {
-  for (let i = 0; i < 5; i++) {
-    await Promise.resolve()
-  }
-}
+// Hydration and snapshot removal both run through awaited promise chains;
+// poll for the expected state instead of guessing a microtask count.
+async function flushPromises(): Promise<void> {
+  await vi.waitFor(() => Promise.resolve())
+}

Or assert with await vi.waitFor(() => expect(getItem).toHaveBeenCalledTimes(1)) at each call site.

🤖 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/tests/inject-generation.test.ts` around lines 105 - 111,
Replace the fixed five-iteration flushPromises helper with vi.waitFor-based
synchronization at the hydration and snapshot-removal assertions, waiting for
the expected client call or state before continuing. Remove the hard-coded
microtask drain while preserving the existing assertions and behavior.
packages/ai-angular/src/inject-generate-video.ts (1)

95-117: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Optional: extract the resume-signal wiring into a shared internal helper.

This block (signals seeded from initialResumeSnapshot + setResumeSnapshotState) is byte-for-byte identical to packages/ai-angular/src/inject-generation.ts Lines 127-149. A small internal/create-resume-signals.ts returning the four signals plus the setter (and taking a () => boolean disposed accessor) would keep the two clients from drifting.

🤖 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-generate-video.ts` around lines 95 - 117,
Extract the duplicated resume-signal initialization and update logic from
injectGenerateVideo and injectGeneration into a shared internal
create-resume-signals helper. Have the helper accept the initial snapshot and a
disposed-state accessor, return the four signals plus setResumeSnapshotState,
and update both clients to use it while preserving current initialization and
disposal 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 `@examples/ts-react-chat/src/routes/generations.image.tsx`:
- Around line 119-125: Update the ImageGenerationUI reset-button visibility
logic so the “Clear” action remains available when a hydrated resumeSnapshot
exists, even if result is empty. Ensure the button invokes the existing reset()
behavior and preserve the current visibility behavior for non-hydrated states.

In `@packages/ai-client/src/generation-types.ts`:
- Around line 602-607: Update the expiresAt handling in observeResumeSnapshot to
validate Date values before calling toISOString, so invalid dates are ignored
without throwing. Preserve the existing string handling and valid Date
serialization behavior.

In `@packages/ai-react/src/use-generation.ts`:
- Around line 193-195: Update the onResumeSnapshotChange callback in the React
hook and the corresponding Vue/Svelte integrations to suppress state updates
when the resume snapshot has not materially changed. Reuse the existing
resumeSnapshotSignature comparison or equivalent logic so lastEvent-only changes
are ignored while meaningful snapshot changes still call setResumeSnapshot and
update the reactive state.

In `@packages/ai-svelte/src/create-generation.svelte.ts`:
- Around line 298-303: Define module-level EMPTY_PENDING_ARTIFACTS and
EMPTY_RESULT_ARTIFACTS constants with the appropriate artifact types, then
update the pendingArtifacts and resultArtifacts getters to reuse these stable
fallbacks instead of allocating a new empty array on each read.

In `@packages/ai-vue/src/use-generate-video.ts`:
- Around line 93-100: The artifact JSDoc incorrectly claims pendingArtifacts and
resultArtifacts are always empty; update the documentation in
packages/ai-vue/src/use-generate-video.ts lines 93-100 and
packages/ai-svelte/src/create-generate-video.svelte.ts lines 88-95 to describe
when each list is populated, and mirror the correction in other framework hooks
with the copied text. Keep packages/ai-react/tests/use-generation.test.ts lines
475-493 as the behavioral source of truth and make no test change unless the
corrected documentation reveals the expectation is wrong.

---

Nitpick comments:
In `@packages/ai-angular/src/inject-generate-video.ts`:
- Around line 95-117: Extract the duplicated resume-signal initialization and
update logic from injectGenerateVideo and injectGeneration into a shared
internal create-resume-signals helper. Have the helper accept the initial
snapshot and a disposed-state accessor, return the four signals plus
setResumeSnapshotState, and update both clients to use it while preserving
current initialization and disposal behavior.

In `@packages/ai-angular/tests/inject-generation.test.ts`:
- Around line 73-103: Replace the local createMapPersistence helper in
packages/ai-angular/tests/inject-generation.test.ts (lines 73-103) and
packages/ai-vue/tests/use-generation.test.ts (lines 126-142) with the shared
GenerationPersistence mock utility, importing it wherever needed and updating
call sites to its return shape. Remove the duplicated framework-specific
wrapper/store variants so both suites assert the same persistence contract.
- Around line 105-111: Replace the fixed five-iteration flushPromises helper
with vi.waitFor-based synchronization at the hydration and snapshot-removal
assertions, waiting for the expected client call or state before continuing.
Remove the hard-coded microtask drain while preserving the existing assertions
and behavior.

In `@packages/ai-client/src/generation-client.ts`:
- Around line 439-464: Extract the repeated resume-snapshot cloning logic from
getResumeSnapshot, completePlainFetcherResumeSnapshot, and
recordResumeSnapshotError into a shared cloneResumeSnapshot or carryForward
helper. Update all three call sites to use it, preserving the existing
shallow-copy behavior for nested pendingArtifacts, result.artifacts, error, and
lastEvent fields so future snapshot fields remain consistent.

In `@packages/ai-client/src/video-generation-client.ts`:
- Around line 641-828: Extract the resume snapshot lifecycle currently
implemented by methods such as maybeHydrateResumeSnapshot,
hydrateResumeSnapshot, persistResumeSnapshot, writeResumeSnapshot,
removePersistedResumeSnapshot, and clearResumeSnapshot into a shared
ResumeSnapshotStore that accepts the storage adapter and snapshot key. Update
GenerationClient and VideoGenerationClient to delegate hydration, persistence,
deduplication, queued writes, clearing, and warning/error state to this store,
and replace the client-specific videoResumeSnapshotSignature with the shared
implementation so both clients retain identical semantics.

In `@packages/ai-client/tests/generation-client.test.ts`:
- Around line 1329-1361: Extend the rejected-write test around
GenerationClient.generate to assert getResumePersistenceError() returns
persistenceError after the failed setItem call. Then make a subsequent
successful persistence write and assert the getter resets to undefined,
preserving the existing warning assertion and generation resolution behavior.

In `@packages/ai-react/src/use-generate-audio.ts`:
- Around line 73-80: Replace the duplicated resume/artifact field declarations
with shared inherited types: in packages/ai-react/src/use-generate-audio.ts
lines 73-80, packages/ai-react/src/use-generate-image.ts lines 73-80, and
packages/ai-react/src/use-summarize.ts lines 73-80, extend
Omit<UseGenerationReturn<TOutput, respective input type>, 'generate'> while
retaining only the narrowed generate member. In
packages/ai-react/src/use-generate-video.ts lines 83-90, extract those four
fields and their caveat JSDoc into a shared GenerationResumeSurface interface,
then extend it instead of re-declaring them.

In `@packages/ai-react/src/use-generate-speech.ts`:
- Around line 33-36: Derive the React wrapper types from the shared generation
types instead of hand-copying resume and artifact fields. In
packages/ai-react/src/use-generate-speech.ts at lines 33-36, make
UseGenerateSpeechOptions extend Pick<UseGenerationOptions<SpeechGenerateInput,
TTSResult, TOutput>, 'persistence' | 'initialResumeSnapshot'> and
UseGenerateSpeechReturn extend Omit<UseGenerationReturn<TOutput,
SpeechGenerateInput>, 'generate'>; apply the same change in
packages/ai-react/src/use-transcription.ts at lines 33-36 using
TranscriptionGenerateInput and TranscriptionResult.

In `@packages/ai-svelte/src/create-generate-video.svelte.ts`:
- Around line 311-322: Update the module containing the resumeSnapshot getters
to define shared EMPTY_PENDING_ARTIFACTS and EMPTY_RESULT_ARTIFACTS constants at
module scope, then use them as the nullish fallbacks in pendingArtifacts and
resultArtifacts instead of allocating [] on each read. Keep the existing
non-empty artifact values unchanged.

In `@packages/ai-svelte/tests/create-generation.test.ts`:
- Around line 248-273: Update both negative-assertion tests in
create-generation.test.ts, including the test near the referenced additional
range, to await the existing flushAsync() helper instead of a single
Promise.resolve() microtask. Keep the assertions verifying that getItem and
connect were not called, so detached hydration and persistence queues are fully
drained before checking behavior.
🪄 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: b2e31f65-38c8-47c9-8113-258d0c9c7aec

📥 Commits

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

📒 Files selected for processing (62)
  • .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/index.ts
  • 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-react/src/index.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/index.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/src/index.ts
  • packages/ai-svelte/tests/create-generation.test.ts
  • packages/ai-vue/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/skills/ai-core/client-persistence/SKILL.md
  • packages/ai/skills/ai-core/media-generation/SKILL.md
  • testing/e2e/src/routeTree.gen.ts
  • testing/e2e/src/routes/api.generation-persistence.ts
  • testing/e2e/src/routes/generation-persistence.tsx
  • testing/e2e/tests/generation-persistence.spec.ts

Comment on lines +119 to +125
<ImageGenerationUI
{...hookReturn}
prompt={prompt}
setPrompt={setPrompt}
numberOfImages={numberOfImages}
setNumberOfImages={setNumberOfImages}
/>

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 | 🟡 Minor | ⚡ Quick win

Keep “Clear” available for hydrated snapshots.

After reload, resumeSnapshot is restored but result is intentionally empty, so ImageGenerationUI hides its only reset() button. Users cannot clear the persisted record from this mode.

Proposed fix
 function ImageGenerationUI({
   prompt,
   setPrompt,
   numberOfImages,
   setNumberOfImages,
   generate,
   result,
+  resumeSnapshot,
   isLoading,
   error,
   reset,
 }: UseGenerateImageReturn & {
@@
-        {result && (
+        {(result || 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 `@examples/ts-react-chat/src/routes/generations.image.tsx` around lines 119 -
125, Update the ImageGenerationUI reset-button visibility logic so the “Clear”
action remains available when a hydrated resumeSnapshot exists, even if result
is empty. Ensure the button invokes the existing reset() behavior and preserve
the current visibility behavior for non-hydrated states.

Comment on lines +602 to +607
const expiresAt = Reflect.get(value, 'expiresAt')
if (typeof expiresAt === 'string') {
snapshot.expiresAt = expiresAt
} else if (expiresAt instanceof Date) {
snapshot.expiresAt = expiresAt.toISOString()
}

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

Guard against an invalid Date in expiresAt.

toISOString() throws RangeError on an invalid Date. This runs inside observeResumeSnapshot on live chunk values, so a malformed provider expiresAt would throw out of the stream loop rather than just being dropped like every other unusable field here.

🛡️ Proposed fix
   } else if (expiresAt instanceof Date) {
-    snapshot.expiresAt = expiresAt.toISOString()
+    if (!Number.isNaN(expiresAt.getTime())) {
+      snapshot.expiresAt = expiresAt.toISOString()
+    }
   }
📝 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
const expiresAt = Reflect.get(value, 'expiresAt')
if (typeof expiresAt === 'string') {
snapshot.expiresAt = expiresAt
} else if (expiresAt instanceof Date) {
snapshot.expiresAt = expiresAt.toISOString()
}
const expiresAt = Reflect.get(value, 'expiresAt')
if (typeof expiresAt === 'string') {
snapshot.expiresAt = expiresAt
} else if (expiresAt instanceof Date) {
if (!Number.isNaN(expiresAt.getTime())) {
snapshot.expiresAt = expiresAt.toISOString()
}
}
🤖 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 602 - 607, Update
the expiresAt handling in observeResumeSnapshot to validate Date values before
calling toISOString, so invalid dates are ignored without throwing. Preserve the
existing string handling and valid Date serialization behavior.

Comment on lines +193 to 195
onResumeSnapshotChange: (snapshot) => {
if (!disposedRef.current) setResumeSnapshot(snapshot)
},

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.

🚀 Performance & Scalability | 🟠 Major | ⚡ Quick win

Per-chunk setResumeSnapshot re-renders the component on every stream event.

GenerationClient.observeResumeSnapshot invokes onResumeSnapshotChange for every chunk with a freshly built object, so this always schedules a state update — including for lastEvent-only changes that the persistence layer deliberately dedupes (resumeSnapshotSignature). For a long streaming run that is one React render per chunk, on top of the existing progress/result renders.

Consider suppressing the notification when the snapshot has not materially changed (reuse the signature comparison, or skip when only lastEvent differs) so the reactive surfaces stay as cheap as the writes. This also affects the Vue and Svelte hooks that wire the same callback.

🤖 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-generation.ts` around lines 193 - 195, Update the
onResumeSnapshotChange callback in the React hook and the corresponding
Vue/Svelte integrations to suppress state updates when the resume snapshot has
not materially changed. Reuse the existing resumeSnapshotSignature comparison or
equivalent logic so lastEvent-only changes are ignored while meaningful snapshot
changes still call setResumeSnapshot and update the reactive state.

Comment on lines +298 to +303
get pendingArtifacts() {
return resumeSnapshot?.pendingArtifacts ?? []
},
get resultArtifacts() {
return resumeSnapshot?.result?.artifacts ?? []
},

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.

🚀 Performance & Scalability | 🟡 Minor | ⚡ Quick win

Use stable empty-array fallbacks here for parity with React/Vue.

Each read allocates a fresh [], so any $derived/$effect depending on pendingArtifacts/resultArtifacts sees a new reference on every access. packages/ai-react/src/use-generation.ts and packages/ai-vue/src/use-generation.ts deliberately hoist EMPTY_PENDING_ARTIFACTS / EMPTY_RESULT_ARTIFACTS constants for exactly this reason.

♻️ Proposed fix
     get pendingArtifacts() {
-      return resumeSnapshot?.pendingArtifacts ?? []
+      return resumeSnapshot?.pendingArtifacts ?? EMPTY_PENDING_ARTIFACTS
     },
     get resultArtifacts() {
-      return resumeSnapshot?.result?.artifacts ?? []
+      return resumeSnapshot?.result?.artifacts ?? EMPTY_RESULT_ARTIFACTS
     },

Add module-level constants:

const EMPTY_PENDING_ARTIFACTS: Array<GenerationPendingArtifact> = []
const EMPTY_RESULT_ARTIFACTS: Array<PersistedArtifactRef> = []
📝 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
get pendingArtifacts() {
return resumeSnapshot?.pendingArtifacts ?? []
},
get resultArtifacts() {
return resumeSnapshot?.result?.artifacts ?? []
},
const EMPTY_PENDING_ARTIFACTS: Array<GenerationPendingArtifact> = []
const EMPTY_RESULT_ARTIFACTS: Array<PersistedArtifactRef> = []
get pendingArtifacts() {
return resumeSnapshot?.pendingArtifacts ?? EMPTY_PENDING_ARTIFACTS
},
get resultArtifacts() {
return resumeSnapshot?.result?.artifacts ?? EMPTY_RESULT_ARTIFACTS
},
🤖 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-svelte/src/create-generation.svelte.ts` around lines 298 - 303,
Define module-level EMPTY_PENDING_ARTIFACTS and EMPTY_RESULT_ARTIFACTS constants
with the appropriate artifact types, then update the pendingArtifacts and
resultArtifacts getters to reuse these stable fallbacks instead of allocating a
new empty array on each read.

Comment on lines +93 to +100
/** Lightweight generation resume snapshot, if one is available */
resumeSnapshot: DeepReadonly<ShallowRef<GenerationResumeSnapshot | undefined>>
/** Identity of the in-flight run while one is streaming, or null after it ends */
resumeState: DeepReadonly<ShallowRef<GenerationResumeState | null>>
/** Pending persisted artifact refs observed mid-run. Currently always empty: nothing emits `generation:artifacts` until the server-side artifact pipeline ships in a follow-up */
pendingArtifacts: DeepReadonly<ShallowRef<Array<GenerationPendingArtifact>>>
/** Persisted artifact refs from the final result. Currently always empty: results carry no artifacts until the server-side artifact pipeline ships in a follow-up */
resultArtifacts: DeepReadonly<ShallowRef<Array<PersistedArtifactRef>>>

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

Stale "Currently always empty" artifact docs contradict the new tests. The shared root cause is JSDoc copied across framework return types asserting that pendingArtifacts/resultArtifacts stay empty until the server-side artifact pipeline ships, while the client already surfaces artifacts from generation:result.

  • packages/ai-vue/src/use-generate-video.ts#L93-L100: drop the "Currently always empty" claim and describe when the lists are populated.
  • packages/ai-svelte/src/create-generate-video.svelte.ts#L88-L95: apply the same doc correction (and mirror it in the other framework hooks carrying the copied text).
  • packages/ai-react/tests/use-generation.test.ts#L475-L493: keep as the behavioral source of truth; if the docs are actually correct, this expectation needs revisiting instead.
📍 Affects 3 files
  • packages/ai-vue/src/use-generate-video.ts#L93-L100 (this comment)
  • packages/ai-svelte/src/create-generate-video.svelte.ts#L88-L95
  • packages/ai-react/tests/use-generation.test.ts#L475-L493
🤖 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-generate-video.ts` around lines 93 - 100, The
artifact JSDoc incorrectly claims pendingArtifacts and resultArtifacts are
always empty; update the documentation in
packages/ai-vue/src/use-generate-video.ts lines 93-100 and
packages/ai-svelte/src/create-generate-video.svelte.ts lines 88-95 to describe
when each list is populated, and mirror the correction in other framework hooks
with the copied text. Keep packages/ai-react/tests/use-generation.test.ts lines
475-493 as the behavioral source of truth and make no test change unless the
corrected documentation reveals the expectation is wrong.

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