Skip to content

feat(persistence): server persistence + client browser-refresh durability#984

Open
AlemTuzlak wants to merge 28 commits into
mainfrom
feat/persistence-core
Open

feat(persistence): server persistence + client browser-refresh durability#984
AlemTuzlak wants to merge 28 commits into
mainfrom
feat/persistence-core

Conversation

@AlemTuzlak

@AlemTuzlak AlemTuzlak commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Lands the persistence story on top of the shipped ephemeral interrupts (#970), resumable streams (#955), and the shared Scope type (#980), using #785 as reference. Persistence is the final piece: durable state on the server and full-page-reload durability on the client.

Server: four new packages

  • @tanstack/ai-persistence — store contracts, withChatPersistence / withGenerationPersistence middleware, memoryPersistence reference store, and a conformance testkit.
  • @tanstack/ai-persistence-drizzle — Drizzle stores (SQLite via node:sqlite, BYO-schema) + migration/schema CLIs.
  • @tanstack/ai-persistence-prisma — Prisma stores + models CLI.
  • @tanstack/ai-persistence-cloudflare — D1 stores (delegating to Drizzle) + a Durable-Object lock store.

Client: browser-refresh durability

The persistence option now stores one combined { messages, resume? } record per chat id, so a full page reload restores the transcript, rehydrates pending interrupts, and rejoins an in-flight run via joinRun on a durability-backed connection. Adds localStoragePersistence / sessionStoragePersistence / indexedDBPersistence. Rides the existing option, so all six framework integrations get it with no framework-specific code.

Example, docs, skills, e2e

  • examples/ts-react-chat /persistent-chat route (SQLite server + localStorage client) to test reload durability end to end.
  • New docs/persistence/ section + updated client persistence page.
  • Updated middleware + chat-experience agent skills.
  • E2E: provider-free durable harness route + spec (message restore after reload, interrupt survives reload).

Reconciliation decisions (vs #785)

  • Locks live in @tanstack/ai-persistence, not core (main keeps them in ai-sandbox). The sandbox-consumer bridge is deferred with sandbox persistence, since capability identity is by reference.
  • The middleware no longer builds ChatResumeToolState — the shipped ephemeral engine reconstructs resume state from the resume batch + interrupt bindings in the (server-loaded) history. Persistence records interrupts and gates new input.
  • Client resume snapshot folded into the existing persistence option (one adapter, one record) rather than a second adapter.

Testing

Green locally: test:sherif, test:knip, test:docs, test:kiira, test:oxlint (changed packages), test:types (packages + frameworks + example + e2e), test:lib (all four persistence packages incl. conformance, and @tanstack/ai-client incl. new combined-record + auto-rejoin tests), and build for the four persistence packages.

Not run locally — deferred to CI due to a saturated dev machine (heavy vite/Playwright startup timed out): the full bundle build for ai-client/frameworks/example and the full Playwright test:e2e run. The e2e spec is authored and type-checks.

Deferred (not in this batch)

Generation-artifact persistence (blob/R2 media), MCP persistence, and sandbox persistence.

Release

Changesets: minor for the four persistence packages and @tanstack/ai-client.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Chat conversations now survive browser refreshes, including pending interrupts and in-progress streaming runs.
    • Added localStorage, sessionStorage, and IndexedDB persistence adapters, with an option to store only resume information.
    • Added durable server-side chat persistence for transcripts, runs, interrupts, and metadata.
    • Added Drizzle, Prisma, and Cloudflare persistence backends, migration tools, and locking support.
    • Added a persistent chat example and browser-refresh durability coverage.
  • Documentation

    • Added comprehensive guides for client/server persistence, migrations, controls, supported backends, and custom stores.

…kends

Server-side persistence for chat(): durable thread messages, run records, and
interrupts via the withChatPersistence middleware, with pluggable backends.

- @tanstack/ai-persistence: store contracts, withChatPersistence /
  withGenerationPersistence middleware, memoryPersistence reference store,
  conformance testkit. Locks (LockStore/InMemoryLockStore/LocksCapability) live
  here rather than core; the sandbox-consumer bridge is deferred.
- -drizzle / -prisma / -cloudflare: backend store implementations + migration /
  schema / models CLIs. Cloudflare D1 delegates to the drizzle backend.

Reconciled against the shipped ephemeral-interrupt engine: the middleware
records interrupts and gates new input, and delegates resume-tool-state
reconstruction to the engine (resume batch + interrupt bindings in history).

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
The persistence adapter now stores one combined { messages, resume? } record
per chat id, so a full page reload restores the transcript, rehydrates pending
interrupts, and rejoins an in-flight run through joinRun when the connection is
durability-backed. Legacy bare-array records are still read.

Adds localStoragePersistence / sessionStoragePersistence / indexedDBPersistence
(+ StorageUnavailableError and the ChatPersistedState / ChatStorageAdapter
types). Durability rides the existing option, so every framework integration
gets it with no framework-specific code.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
New /persistent-chat route: useChat with localStoragePersistence on the client
and withChatPersistence(sqlitePersistence) on the server, so a full page reload
restores the conversation on both ends. Adds a nav link and README section.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
…ility

New docs/persistence section: overview, chat-persistence, browser-refresh,
controls, custom-stores, sql-backends, drizzle, prisma, cloudflare, migrations,
internals. Wires the nav and updates the client chat/persistence page for the
combined { messages, resume } record and built-in storage adapters.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
Update the agent skills for the new surface: withChatPersistence server
middleware and its backends, and the client browser-refresh durability
(combined persistence record, storage adapters, joinRun rejoin, all frameworks).

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
Provider-free durable harness route + client page + spec proving message
restore after reload and interrupt-survives-reload via localStorage. Mid-stream
joinRun rejoin is covered by ai-client unit tests and delivery-durability.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
@coderabbitai

coderabbitai Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This PR adds durable client chat persistence, server-side transcript/run/interrupt persistence, Drizzle, Prisma, and Cloudflare backends, browser storage adapters, resumable run rejoining, migration tooling, framework exports, documentation, examples, and end-to-end browser-refresh coverage.

Changes

Persistence durability

Layer / File(s) Summary
Client persistence and run rejoin
packages/ai-client/*
Persists combined transcript and resume state, supports legacy records, browser storage adapters, and joinRun reattachment.
Server persistence contracts and middleware
packages/ai-persistence/*
Adds store contracts, middleware lifecycle handling, interrupt recovery, composition, in-memory stores, and chat reconstruction.
Persistence backend adapters
packages/ai-persistence-drizzle/*, packages/ai-persistence-prisma/*, packages/ai-persistence-cloudflare/*
Adds SQL and Cloudflare persistence implementations, schemas, migration CLIs, Durable Object locks, and conformance coverage.
Examples and browser durability tests
examples/ts-react-chat/*, testing/e2e/*
Adds persistent chat examples and deterministic reload tests for transcript and interrupt restoration.
Documentation and release wiring
docs/persistence/*, packages/ai-*/src/index.ts, .changeset/*
Documents persistence configuration and publishes new client and backend APIs across framework packages.

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

Possibly related PRs

  • TanStack/ai#661 — Earlier client persistence and hydration changes.
  • TanStack/ai#955 — Resumable stream delivery used for in-flight run reattachment.
  • TanStack/ai#970 — Interrupt and resume protocol changes used by persisted resume state.

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant ChatClient
  participant Storage
  participant Server
  participant Persistence
  Browser->>ChatClient: Reload chat
  ChatClient->>Storage: Read persisted transcript and resume snapshot
  ChatClient->>Server: GET thread or joinRun(runId)
  Server->>Persistence: Load thread/run/interrupt state
  Persistence-->>Server: Stored state
  Server-->>ChatClient: Transcript or replayed stream chunks
  ChatClient-->>Browser: Restore messages and interrupts
Loading
🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description is detailed but does not follow the required template; it omits the 🎯 Changes, ✅ Checklist, and 🚀 Release Impact sections. Rewrite the PR description using the repository template and include the missing Changes, Checklist, and Release Impact sections.
Docstring Coverage ⚠️ Warning Docstring coverage is 28.95% 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 is concise and accurately summarizes the main change: server persistence plus client browser-refresh durability.
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 unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/persistence-core

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.

@socket-security

socket-security Bot commented Jul 22, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​drizzle-kit@​0.31.10991009697100

View full report

@socket-security

socket-security Bot commented Jul 22, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm drizzle-orm is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/@tanstack/nitro-v2-vite-plugin@1.155.0npm/nitro@3.0.260610-betanpm/@tanstack/start@1.120.20npm/drizzle-orm@0.45.2

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/drizzle-orm@0.45.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn High
Obfuscated code: npm effect is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: pnpm-lock.yamlnpm/prisma@6.19.3npm/@tanstack/nitro-v2-vite-plugin@1.155.0npm/nitro@3.0.260610-betanpm/@tanstack/start@1.120.20npm/effect@3.21.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/effect@3.21.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Medium
Deprecated by its maintainer: npm @esbuild-kit/core-utils

Reason: Merged into tsx: https://tsx.hirok.io

From: pnpm-lock.yamlnpm/drizzle-kit@0.31.10npm/@esbuild-kit/core-utils@3.3.2

ℹ Read more on: This package | This alert | What is a deprecated package?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Research the state of the package and determine if there are non-deprecated versions that can be used, or if it should be replaced with a new, supported solution.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@esbuild-kit/core-utils@3.3.2. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

Warn Medium
Deprecated by its maintainer: npm @esbuild-kit/esm-loader

Reason: Merged into tsx: https://tsx.hirok.io

From: pnpm-lock.yamlnpm/drizzle-kit@0.31.10npm/@esbuild-kit/esm-loader@2.6.5

ℹ Read more on: This package | This alert | What is a deprecated package?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Research the state of the package and determine if there are non-deprecated versions that can be used, or if it should be replaced with a new, supported solution.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/@esbuild-kit/esm-loader@2.6.5. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

@github-actions

Copy link
Copy Markdown
Contributor

🚀 Changeset Version Preview

14 package(s) bumped directly, 37 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-openrouter 0.15.10 → 1.0.0 Changeset
@tanstack/ai-persistence 0.0.0 → 1.0.0 Changeset
@tanstack/ai-persistence-cloudflare 0.0.0 → 1.0.0 Changeset
@tanstack/ai-persistence-drizzle 0.0.0 → 1.0.0 Changeset
@tanstack/ai-persistence-prisma 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-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 0.2.4 → 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

🟩 Patch bumps

Package Version Reason
@tanstack/ai-devtools-core 0.4.24 → 0.4.25 Dependent
@tanstack/ai-isolate-cloudflare 0.2.38 → 0.2.39 Dependent
@tanstack/ai-mcp 0.2.5 → 0.2.6 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 22, 2026

Copy link
Copy Markdown

🤖 Nx Cloud AI Fix Eligible

An automatically generated fix could have helped fix failing tasks for this run, but Self-healing CI is disabled for this workspace. Visit workspace settings to enable it and get automatic fixes in future runs.

To disable these notifications, a workspace admin can disable them in workspace settings.


View your CI Pipeline Execution ↗ for commit cea0a83

Command Status Duration Result
nx affected --targets=test:sherif,test:knip,tes... ❌ Failed 2m 52s View ↗
nx run-many --targets=build --exclude=examples/... ❌ Failed 27s View ↗

☁️ Nx Cloud last updated this comment at 2026-07-23 10:45:02 UTC

@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

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/ai-persistence-cloudflare/tests/migration-cli.test.ts (1)

1-66: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Colocate this test with migration-cli.ts.

Move this to packages/ai-persistence-cloudflare/src/migration-cli.test.ts so its location follows the repository test-layout rule.

As per coding guidelines, “Place unit tests in *.test.ts files alongside the source they cover.”

🤖 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-cloudflare/tests/migration-cli.test.ts` around lines
1 - 66, Move the migration CLI test suite from the tests directory to a
*.test.ts file alongside migration-cli.ts under src, preserving its existing
imports, test cases, and behavior.

Source: Coding guidelines

packages/ai-client/src/chat-client.ts (1)

1-1: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

IndexedDB rejoin needs an async callback path
readInitial() only computes rejoinRunId on the synchronous path. For promise-backed persistence, hydrateAsync() reapplies the resume snapshot but never calls resumeInFlightRun(), so reloads from indexedDBPersistence won’t reattach to an in-flight run. Thread a rejoin callback through the async hydration path and invoke it once the processor is ready.

🤖 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/chat-client.ts` at line 1, Update the async hydration
flow around readInitial() and hydrateAsync() to carry the computed rejoinRunId
through promise-backed persistence, then invoke resumeInFlightRun() once the
processor is ready. Preserve the existing synchronous rejoin behavior and ensure
the callback runs only after the resume snapshot has been reapplied.
🟡 Minor comments (10)
examples/ts-react-chat/src/routes/persistent-chat.tsx-20-23 (1)

20-23: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Restore Date fields when hydrating persisted chat state. JSON.stringify converts Date values to strings and JSON.parse does not revive them, so rehydrated messages no longer match the ChatPersistedState runtime shape.

  • examples/ts-react-chat/src/routes/persistent-chat.tsx#L20-L23: use a matching serializer/reviver that restores persisted message dates.
  • testing/e2e/src/routes/persistence-durability.tsx#L25-L28: use the same codec and assert a rehydrated timestamp remains a Date.
🤖 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/persistent-chat.tsx` around lines 20 - 23,
Update the localStorage persistence codec in
examples/ts-react-chat/src/routes/persistent-chat.tsx (lines 20-23) to revive
persisted message date fields as Date instances, while preserving JSON
serialization. Apply the same codec in
testing/e2e/src/routes/persistence-durability.tsx (lines 25-28) and add an
assertion that the rehydrated timestamp is a Date.
packages/ai-persistence-cloudflare/bin/tanstack-ai-cloudflare-migrations.mjs-1-2 (1)

1-2: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Wrap the CLI entrypoint in a try/catch. packages/ai-persistence-cloudflare/src/cli.ts awaits runCloudflareMigrationsCli(...) directly, so MigrationCliError rejections will surface as raw stack traces. Print the message to stderr and set process.exitCode = 1 instead.

🤖 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-cloudflare/bin/tanstack-ai-cloudflare-migrations.mjs`
around lines 1 - 2, Wrap the CLI import and execution in the migration
entrypoint around runCloudflareMigrationsCli with try/catch handling for
MigrationCliError rejections; print the error message to stderr and set
process.exitCode to 1 instead of allowing a raw stack trace. Preserve normal
successful CLI execution.
packages/ai-persistence-prisma/package.json-81-82 (1)

81-82: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use workspace:* for internal peer dependencies.

Replace workspace:^ with the required workspace:* protocol.

🤖 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-prisma/package.json` around lines 81 - 82, Update the
internal dependencies "`@tanstack/ai`" and "`@tanstack/ai-persistence`" in
package.json to use the workspace:* protocol instead of workspace:^.

Source: Coding guidelines

packages/ai-persistence-drizzle/tests/package-contract.test.ts-42-49 (1)

42-49: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Detect dynamic Node and SQLite imports too.

await import('node:sqlite') or await import('./sqlite') passes these from-only checks while making the root entry unsafe for edge runtimes.

Suggested update
-      expect(contents, filename).not.toMatch(/from ['"]node:/)
+      expect(contents, filename).not.toMatch(
+        /(?:from\s*|import\s*\()\s*['"]node:/,
+      )
...
-    expect(root).not.toMatch(/from ['"].*sqlite/)
+    expect(root).not.toMatch(
+      /(?:from\s*|import\s*\()\s*['"][^'"]*sqlite/,
+    )
🤖 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-drizzle/tests/package-contract.test.ts` around lines
42 - 49, Extend the import checks in the package contract test to reject dynamic
import syntax as well as static from imports. Update the assertions covering
package files and the root index loaded via fileURLToPath so await import
references to node: modules, Buffer usage, and SQLite paths such as node:sqlite
or relative sqlite imports are detected.
packages/ai/skills/ai-core/chat-experience/SKILL.md-506-508 (1)

506-508: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Garbled sentence — fix the parenthetical.

"(the one exception to mistake j below)" doesn't parse; likely a leftover editing artifact.

🤖 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/skills/ai-core/chat-experience/SKILL.md` around lines 506 - 508,
Fix the parenthetical sentence in the storage adapter and ChatPersistedState
guidance by removing the garbled “one exception to mistake j below” wording and
replacing it with a clear, grammatically correct statement consistent with the
intended import guidance. Keep the surrounding distinction between
`@tanstack/ai-client` imports and framework-package useChat imports unchanged.
packages/ai-persistence/package.json-49-51 (1)

49-51: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Use the required workspace protocol.

Change @tanstack/ai from workspace:^ to workspace:*.

As per coding guidelines, “Use the workspace:* protocol for internal package dependencies in package.json.”

🤖 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/package.json` around lines 49 - 51, Update the
`@tanstack/ai` entry in the peerDependencies object of package.json from the
workspace:^ protocol to workspace:* while leaving the vitest dependency
unchanged.

Source: Coding guidelines

docs/persistence/browser-refresh.md-45-47 (1)

45-47: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Do not promise restoration before first paint for async storage.

indexedDBPersistence is documented as async on Lines 55-63, so its record cannot be guaranteed before the initial paint. Describe restoration as completing after hydration/storage loading and recommend a loading state where needed.

🤖 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/browser-refresh.md` around lines 45 - 47, Update the
persistence documentation around the “next load” description to avoid promising
transcript or interrupt restoration before first paint when using async storage
such as indexedDBPersistence. State that restoration completes after
hydration/storage loading, and recommend showing a loading state when the UI
must wait for restored data.
packages/ai-client/tests/resume-snapshot.test.ts-1-16 (1)

1-16: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Colocate these unit tests with their source modules.

Split this new cross-module suite into tests beside src/client-persistor.ts, src/connection-adapters.ts, and src/chat-client.ts.

As per coding guidelines, “Place unit tests in *.test.ts files alongside the source they cover.”

🤖 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/resume-snapshot.test.ts` around lines 1 - 16, Split
the cross-module tests in resume-snapshot.test.ts into colocated *.test.ts files
beside client-persistor.ts, connection-adapters.ts, and chat-client.ts, placing
each test with the source module it covers. Preserve the existing assertions and
shared test setup while removing the standalone cross-module suite.

Source: Coding guidelines

docs/persistence/browser-refresh.md-69-77 (1)

69-77: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the server-side resumable endpoint example.

This section requires a server route that records and replays the stream, but only shows client consumption. Include the replay endpoint alongside the useChat example.

As per coding guidelines, “When a documentation page covers both server and client behavior, include snippets for both halves: the server endpoint and the client consumption.”

🤖 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/browser-refresh.md` around lines 69 - 77, Update the “Rejoin
an in-flight run” section to include a server-side resumable endpoint example
alongside the existing useChat/joinRun client explanation. Show the route’s
stream recording and GET replay behavior, reusing the documented resumable
connection pattern from “Resumable streams,” while preserving the existing
client flow.

Source: Coding guidelines

docs/persistence/internals.md-31-35 (1)

31-35: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Correct the documented onConfig ordering.

Pending interrupts are loaded and resume input is validated before createOrResumeRun. As written, the page incorrectly implies invalid resumes create a run record first.

🤖 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/internals.md` around lines 31 - 35, Update the onConfig
description in the persistence internals documentation to state that pending
interrupts are loaded and the request’s resume batch is validated before
createOrResumeRun, then describe run creation or resumption and stored-message
merging in the correct order.
🧹 Nitpick comments (6)
packages/ai-persistence-cloudflare/migrations/0000_tanstack_ai_initial.sql (1)

1-32: 🚀 Performance & Scalability | 🔵 Trivial

Consider secondary indexes for thread/run lookups.

Interrupts and runs are keyed only by their primary IDs, but list-style access (e.g. interrupts for a run/thread, runs for a thread) filters on interrupts.run_id, interrupts.thread_id, and runs.thread_id. Without indexes these become full table scans as rows accumulate. Since this file must stay byte-for-byte identical to the Drizzle asset, add the indexes in the Drizzle schema source and regenerate both assets rather than editing here.

🤖 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-cloudflare/migrations/0000_tanstack_ai_initial.sql`
around lines 1 - 32, Add secondary indexes for interrupts.run_id,
interrupts.thread_id, and runs.thread_id in the corresponding Drizzle schema
definitions, then regenerate the migration assets so this SQL file remains
byte-for-byte identical to generated output.
packages/ai-persistence-cloudflare/tsconfig.json (1)

1-9: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Global "node" types leak into src, past the Workers-compatibility guard.

types: ["node", ...] applies across include: ["src", "tests"], so Node's ambient globals (process, __dirname, etc.) typecheck fine inside src even though this package targets Cloudflare Workers. The sibling package-contract.test.ts only regex-checks for from 'node:' imports and the literal Buffer, so use of other Node globals in src wouldn't be caught by that guard and would only fail at runtime on Workers.

Consider scoping "node" types to a tests-only tsconfig (or project reference) so src typechecking reflects the actual Workers runtime.

🤖 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-cloudflare/tsconfig.json` around lines 1 - 9, Remove
the global "node" type declaration from the package tsconfig's shared
compilerOptions so src is checked only against Cloudflare Workers types. Add a
tests-only tsconfig or project reference that supplies "node" types for tests,
while preserving the existing source and test includes and Workers-compatible
source typechecking.
packages/ai-persistence-drizzle/src/sqlite.ts (1)

32-44: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the native drizzle-orm/node-sqlite driver here
drizzle-orm/node-sqlite already accepts DatabaseSync directly, so this custom sqlite-proxy callback and row-to-array conversion can be removed.

🤖 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-drizzle/src/sqlite.ts` around lines 32 - 44, Replace
the custom drizzle sqlite-proxy callback around the database initialization with
the native drizzle-orm/node-sqlite driver, passing the existing DatabaseSync
instance directly to drizzle. Remove the statement preparation, method
branching, and Object.values row conversion while preserving the resulting
database handle used by the rest of the module.
packages/ai-persistence-prisma/src/assets/tanstack-ai.prisma (1)

15-40: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Add indexes for threadId/runId on Interrupt.

stores.ts queries Interrupt via findMany({ where: { threadId } }) and findMany({ where: { runId } }) (list/listPending/listByRun/listPendingByRun), but no @@index is declared for either column here. As interrupt volume grows this forces full table scans on every listing call.

⚡ Proposed indexes
 model Interrupt {
   interruptId  String  `@id` `@map`("interrupt_id")
   runId        String  `@map`("run_id")
   threadId     String  `@map`("thread_id")
   status       String
   requestedAt  BigInt  `@map`("requested_at")
   resolvedAt   BigInt? `@map`("resolved_at")
   payloadJson  String  `@map`("payload_json")
   responseJson String? `@map`("response_json")

+  @@index([threadId])
+  @@index([runId])
   @@map("interrupts")
 }
🤖 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-prisma/src/assets/tanstack-ai.prisma` around lines 15
- 40, Add separate Prisma indexes for both threadId and runId in the Interrupt
model, alongside its existing fields and mapping, so the
list/listPending/listByRun/listPendingByRun queries can use indexed lookups.
packages/ai-persistence/tests/memory.conformance.test.ts (1)

1-4: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Colocate the memory-store unit tests with their covered source.

  • packages/ai-persistence/tests/memory.conformance.test.ts#L1-L4: move beside packages/ai-persistence/src/memory.ts.
  • packages/ai-persistence/tests/memory.test.ts#L1-L132: move beside packages/ai-persistence/src/memory.ts.

As per coding guidelines, “Place unit tests in *.test.ts files alongside the source they cover.”

🤖 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/memory.conformance.test.ts` around lines 1 - 4,
Move the tests covering memoryPersistence from
packages/ai-persistence/tests/memory.conformance.test.ts (lines 1-4) and
packages/ai-persistence/tests/memory.test.ts (lines 1-132) to *.test.ts files
alongside packages/ai-persistence/src/memory.ts, preserving their existing test
behavior and updating relative imports as needed.

Source: Coding guidelines

packages/ai-persistence/src/middleware.ts (1)

333-338: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Prefer EventType.RUN_FINISHED here. Using the enum keeps this guard aligned with the shared event type and avoids a silent mismatch if the event name changes.

🤖 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 333 - 338, Update the
guard in the RUN_FINISHED handling flow to compare chunk.type against the shared
EventType.RUN_FINISHED enum member instead of the string literal, while
preserving the existing interrupt outcome check and early return 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/api.persistent-chat.ts`:
- Around line 51-53: Scope persisted chat threads to the authenticated tenant:
in examples/ts-react-chat/src/routes/api.persistent-chat.ts#L51-L53, derive and
authorize the persistence thread identity server-side before invoking
withChatPersistence instead of trusting caller-controlled params.threadId; in
examples/ts-react-chat/src/routes/persistent-chat.tsx#L28-L32, replace the
globally shared persistent ID demonstration with an authenticated/session-scoped
thread ID.

In `@packages/ai-client/src/chat-client.ts`:
- Around line 445-466: Update the chat client initialization and hydrateAsync
flow so asynchronously loaded persistence also extracts a bare in-flight run’s
resumeState.runId and passes it to resumeInFlightRun() after the processor is
ready. Preserve pending-interrupt handling through applyResumeSnapshot(), while
ensuring the synchronous and asynchronous paths use consistent rejoin behavior.

In `@packages/ai-client/src/connection-adapters.ts`:
- Around line 875-885: Update the normalized adapter’s conditional spread around
joinRun to expose the wrapper only when typeof connection.joinRun is "function",
rather than merely checking property presence. Preserve the existing
ResumableConnectConnectionAdapter delegation and ensure an explicit undefined
joinRun is omitted.

In `@packages/ai-persistence-drizzle/package.json`:
- Around line 55-56: Update the internal dependency declarations in package.json
for `@tanstack/ai` and `@tanstack/ai-persistence` from workspace:^ to workspace:*.
Leave external dependencies unchanged.

In `@packages/ai-persistence-drizzle/src/sqlite.ts`:
- Around line 20-30: Add an engines.node declaration to
packages/ai-persistence-drizzle/package.json requiring Node 22.13.0 or newer, or
the equivalent supported Node release range that excludes versions where
node:sqlite requires the experimental flag. Keep the sqlitePersistence
implementation unchanged.

In `@packages/ai-persistence-drizzle/tests/custom-schema.test.ts`:
- Around line 1-113: Move the tests beside the source modules they cover,
preserving their contents and behavior: relocate
packages/ai-persistence-drizzle/tests/custom-schema.test.ts (lines 1-113) beside
the Drizzle persistence/schema sources,
packages/ai-persistence-drizzle/tests/package-contract.test.ts (lines 1-67)
beside the Drizzle package-contract surface, and
packages/ai-persistence-prisma/tests/package-contract.test.ts (lines 1-28)
beside the Prisma package-contract surface.

In `@packages/ai-persistence-drizzle/tests/migration-cli.test.ts`:
- Around line 1-6: Move the migration CLI tests from
packages/ai-persistence-drizzle/tests/migration-cli.test.ts to
packages/ai-persistence-drizzle/src/migration-cli.test.ts alongside
runDrizzleMigrationsCli. Update packages/ai-persistence-drizzle/vite.config.ts
at line 13 to discover colocated tests such as src/**/*.test.ts, preserving test
execution after the move.

In `@packages/ai-persistence-drizzle/tests/schema-source.test.ts`:
- Around line 1-31: Move the schema comparison test from the package tests
directory to a *.test.ts file alongside src/schema.ts, preserving normalize and
the emitted-schema structural assertions. Update the package’s test include
configuration only if needed so the colocated test is discovered.

In `@packages/ai-persistence-prisma/src/index.ts`:
- Around line 44-48: Update the prismaPersistence parameter to accept the
minimal delegate-surface type required by resolveDelegates instead of this
package’s generated PrismaClient type. Ensure consumer-generated Prisma clients,
including clients with renamed models, can be passed directly without casts,
while preserving the existing options?.models delegation flow.

In `@packages/ai-persistence-prisma/vite.config.ts`:
- Line 13: Update the test include configuration in vite.config.ts to discover
colocated *.test.ts files throughout the source tree, not only
tests/**/*.test.ts. Move existing package tests alongside the modules they cover
while preserving their test names and coverage.

In `@packages/ai-persistence/src/locks.ts`:
- Around line 46-57: Update the lock-chain cleanup around the chains map so a
key is removed once its current run settles, while preserving newer work that
may have been queued for the same key. Use the existing key and chain identity
to ensure an older completion cannot delete a replacement chain, and keep
rejection swallowing behavior unchanged.

In `@packages/ai-persistence/src/memory.ts`:
- Around line 98-127: Update the interrupt query methods list, listPending,
listByRun, and listPendingByRun to sort their filtered results by ascending
requestedAt before returning them. Preserve each method’s existing threadId,
runId, and pending-status filters while ensuring all query results follow the
InterruptStore ordering contract rather than Map insertion order.
- Around line 131-143: Replace the flat string-key storage in the metadata
methods get, set, and delete with nested maps or another collision-free
representation that preserves scope and key as separate components. Ensure
distinct pairs such as scope “a:b” with key “c” and scope “a” with key “b:c”
remain independently retrievable, writable, and deletable.

In `@testing/e2e/tests/persistence-durability.spec.ts`:
- Around line 17-21: Extend the persistence durability E2E fixture to pause
deterministically mid-stream, reload during the active run, and verify useChat
issues a resume/joinRun request and renders exactly one continuation. During the
interrupt reload, block API traffic to prove recovery uses local storage only,
then restore traffic for the resumed request; update the existing excluded-path
note and assertions accordingly.

---

Outside diff comments:
In `@packages/ai-client/src/chat-client.ts`:
- Line 1: Update the async hydration flow around readInitial() and
hydrateAsync() to carry the computed rejoinRunId through promise-backed
persistence, then invoke resumeInFlightRun() once the processor is ready.
Preserve the existing synchronous rejoin behavior and ensure the callback runs
only after the resume snapshot has been reapplied.

In `@packages/ai-persistence-cloudflare/tests/migration-cli.test.ts`:
- Around line 1-66: Move the migration CLI test suite from the tests directory
to a *.test.ts file alongside migration-cli.ts under src, preserving its
existing imports, test cases, and behavior.

---

Minor comments:
In `@docs/persistence/browser-refresh.md`:
- Around line 45-47: Update the persistence documentation around the “next load”
description to avoid promising transcript or interrupt restoration before first
paint when using async storage such as indexedDBPersistence. State that
restoration completes after hydration/storage loading, and recommend showing a
loading state when the UI must wait for restored data.
- Around line 69-77: Update the “Rejoin an in-flight run” section to include a
server-side resumable endpoint example alongside the existing useChat/joinRun
client explanation. Show the route’s stream recording and GET replay behavior,
reusing the documented resumable connection pattern from “Resumable streams,”
while preserving the existing client flow.

In `@docs/persistence/internals.md`:
- Around line 31-35: Update the onConfig description in the persistence
internals documentation to state that pending interrupts are loaded and the
request’s resume batch is validated before createOrResumeRun, then describe run
creation or resumption and stored-message merging in the correct order.

In `@examples/ts-react-chat/src/routes/persistent-chat.tsx`:
- Around line 20-23: Update the localStorage persistence codec in
examples/ts-react-chat/src/routes/persistent-chat.tsx (lines 20-23) to revive
persisted message date fields as Date instances, while preserving JSON
serialization. Apply the same codec in
testing/e2e/src/routes/persistence-durability.tsx (lines 25-28) and add an
assertion that the rehydrated timestamp is a Date.

In `@packages/ai-client/tests/resume-snapshot.test.ts`:
- Around line 1-16: Split the cross-module tests in resume-snapshot.test.ts into
colocated *.test.ts files beside client-persistor.ts, connection-adapters.ts,
and chat-client.ts, placing each test with the source module it covers. Preserve
the existing assertions and shared test setup while removing the standalone
cross-module suite.

In
`@packages/ai-persistence-cloudflare/bin/tanstack-ai-cloudflare-migrations.mjs`:
- Around line 1-2: Wrap the CLI import and execution in the migration entrypoint
around runCloudflareMigrationsCli with try/catch handling for MigrationCliError
rejections; print the error message to stderr and set process.exitCode to 1
instead of allowing a raw stack trace. Preserve normal successful CLI execution.

In `@packages/ai-persistence-drizzle/tests/package-contract.test.ts`:
- Around line 42-49: Extend the import checks in the package contract test to
reject dynamic import syntax as well as static from imports. Update the
assertions covering package files and the root index loaded via fileURLToPath so
await import references to node: modules, Buffer usage, and SQLite paths such as
node:sqlite or relative sqlite imports are detected.

In `@packages/ai-persistence-prisma/package.json`:
- Around line 81-82: Update the internal dependencies "`@tanstack/ai`" and
"`@tanstack/ai-persistence`" in package.json to use the workspace:* protocol
instead of workspace:^.

In `@packages/ai-persistence/package.json`:
- Around line 49-51: Update the `@tanstack/ai` entry in the peerDependencies
object of package.json from the workspace:^ protocol to workspace:* while
leaving the vitest dependency unchanged.

In `@packages/ai/skills/ai-core/chat-experience/SKILL.md`:
- Around line 506-508: Fix the parenthetical sentence in the storage adapter and
ChatPersistedState guidance by removing the garbled “one exception to mistake j
below” wording and replacing it with a clear, grammatically correct statement
consistent with the intended import guidance. Keep the surrounding distinction
between `@tanstack/ai-client` imports and framework-package useChat imports
unchanged.

---

Nitpick comments:
In `@packages/ai-persistence-cloudflare/migrations/0000_tanstack_ai_initial.sql`:
- Around line 1-32: Add secondary indexes for interrupts.run_id,
interrupts.thread_id, and runs.thread_id in the corresponding Drizzle schema
definitions, then regenerate the migration assets so this SQL file remains
byte-for-byte identical to generated output.

In `@packages/ai-persistence-cloudflare/tsconfig.json`:
- Around line 1-9: Remove the global "node" type declaration from the package
tsconfig's shared compilerOptions so src is checked only against Cloudflare
Workers types. Add a tests-only tsconfig or project reference that supplies
"node" types for tests, while preserving the existing source and test includes
and Workers-compatible source typechecking.

In `@packages/ai-persistence-drizzle/src/sqlite.ts`:
- Around line 32-44: Replace the custom drizzle sqlite-proxy callback around the
database initialization with the native drizzle-orm/node-sqlite driver, passing
the existing DatabaseSync instance directly to drizzle. Remove the statement
preparation, method branching, and Object.values row conversion while preserving
the resulting database handle used by the rest of the module.

In `@packages/ai-persistence-prisma/src/assets/tanstack-ai.prisma`:
- Around line 15-40: Add separate Prisma indexes for both threadId and runId in
the Interrupt model, alongside its existing fields and mapping, so the
list/listPending/listByRun/listPendingByRun queries can use indexed lookups.

In `@packages/ai-persistence/src/middleware.ts`:
- Around line 333-338: Update the guard in the RUN_FINISHED handling flow to
compare chunk.type against the shared EventType.RUN_FINISHED enum member instead
of the string literal, while preserving the existing interrupt outcome check and
early return behavior.

In `@packages/ai-persistence/tests/memory.conformance.test.ts`:
- Around line 1-4: Move the tests covering memoryPersistence from
packages/ai-persistence/tests/memory.conformance.test.ts (lines 1-4) and
packages/ai-persistence/tests/memory.test.ts (lines 1-132) to *.test.ts files
alongside packages/ai-persistence/src/memory.ts, preserving their existing test
behavior and updating relative imports as needed.
🪄 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

Run ID: e3a12367-1bdc-4ac2-b7bf-8d3f28b91b6d

📥 Commits

Reviewing files that changed from the base of the PR and between 50d7a7d and b443a9f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (134)
  • .changeset/client-browser-refresh-durability.md
  • .changeset/persistence-packages.md
  • docs/chat/persistence.md
  • docs/config.json
  • docs/persistence/browser-refresh.md
  • docs/persistence/chat-persistence.md
  • docs/persistence/cloudflare.md
  • docs/persistence/controls.md
  • docs/persistence/custom-stores.md
  • docs/persistence/drizzle.md
  • docs/persistence/internals.md
  • docs/persistence/migrations.md
  • docs/persistence/overview.md
  • docs/persistence/prisma.md
  • docs/persistence/sql-backends.md
  • examples/ts-react-chat/.gitignore
  • examples/ts-react-chat/README.md
  • examples/ts-react-chat/package.json
  • examples/ts-react-chat/src/components/Header.tsx
  • examples/ts-react-chat/src/routes/api.persistent-chat.ts
  • examples/ts-react-chat/src/routes/persistent-chat.tsx
  • packages/ai-client/src/chat-client.ts
  • packages/ai-client/src/client-persistor.ts
  • packages/ai-client/src/connection-adapters.ts
  • packages/ai-client/src/index.ts
  • packages/ai-client/src/storage-adapters.ts
  • packages/ai-client/src/types.ts
  • packages/ai-client/tests/resume-snapshot.test.ts
  • packages/ai-persistence-cloudflare/bin/tanstack-ai-cloudflare-migrations.mjs
  • packages/ai-persistence-cloudflare/migrations/0000_tanstack_ai_initial.sql
  • packages/ai-persistence-cloudflare/package.json
  • packages/ai-persistence-cloudflare/src/assets.d.ts
  • packages/ai-persistence-cloudflare/src/assets/0000_tanstack_ai_initial.sql
  • packages/ai-persistence-cloudflare/src/bindings.ts
  • packages/ai-persistence-cloudflare/src/cli.ts
  • packages/ai-persistence-cloudflare/src/d1.ts
  • packages/ai-persistence-cloudflare/src/index.ts
  • packages/ai-persistence-cloudflare/src/locks.ts
  • packages/ai-persistence-cloudflare/src/migration-cli.ts
  • packages/ai-persistence-cloudflare/src/migrations.ts
  • packages/ai-persistence-cloudflare/tests/api-types.test-d.ts
  • packages/ai-persistence-cloudflare/tests/locks.test.ts
  • packages/ai-persistence-cloudflare/tests/migration-cli.test.ts
  • packages/ai-persistence-cloudflare/tests/migrations.test.ts
  • packages/ai-persistence-cloudflare/tests/package-contract.test.ts
  • packages/ai-persistence-cloudflare/tests/runtime.conformance.test.ts
  • packages/ai-persistence-cloudflare/tsconfig.json
  • packages/ai-persistence-cloudflare/vite.config.ts
  • packages/ai-persistence-drizzle/bin/tanstack-ai-drizzle-migrations.mjs
  • packages/ai-persistence-drizzle/bin/tanstack-ai-drizzle-schema.mjs
  • packages/ai-persistence-drizzle/drizzle.config.ts
  • packages/ai-persistence-drizzle/drizzle/0000_tanstack_ai_initial.sql
  • packages/ai-persistence-drizzle/drizzle/meta/0000_snapshot.json
  • packages/ai-persistence-drizzle/drizzle/meta/_journal.json
  • packages/ai-persistence-drizzle/package.json
  • packages/ai-persistence-drizzle/src/assets.d.ts
  • packages/ai-persistence-drizzle/src/assets/0000_tanstack_ai_initial.sql
  • packages/ai-persistence-drizzle/src/assets/tanstack-ai-schema.ts
  • packages/ai-persistence-drizzle/src/cli.ts
  • packages/ai-persistence-drizzle/src/index.ts
  • packages/ai-persistence-drizzle/src/migration-cli.ts
  • packages/ai-persistence-drizzle/src/migrations.ts
  • packages/ai-persistence-drizzle/src/schema-cli-main.ts
  • packages/ai-persistence-drizzle/src/schema-cli.ts
  • packages/ai-persistence-drizzle/src/schema-contract.ts
  • packages/ai-persistence-drizzle/src/schema-source.ts
  • packages/ai-persistence-drizzle/src/schema.ts
  • packages/ai-persistence-drizzle/src/sqlite-migrations.ts
  • packages/ai-persistence-drizzle/src/sqlite.ts
  • packages/ai-persistence-drizzle/src/stores.ts
  • packages/ai-persistence-drizzle/tests/api-types.test-d.ts
  • packages/ai-persistence-drizzle/tests/custom-schema.test.ts
  • packages/ai-persistence-drizzle/tests/drizzle.conformance.test.ts
  • packages/ai-persistence-drizzle/tests/migration-cli.test.ts
  • packages/ai-persistence-drizzle/tests/migrations.test.ts
  • packages/ai-persistence-drizzle/tests/package-contract.test.ts
  • packages/ai-persistence-drizzle/tests/schema-cli.test.ts
  • packages/ai-persistence-drizzle/tests/schema-source.test.ts
  • packages/ai-persistence-drizzle/tests/sqlite.test.ts
  • packages/ai-persistence-drizzle/tests/store-behavior.test.ts
  • packages/ai-persistence-drizzle/tests/variant-schema.ts
  • packages/ai-persistence-drizzle/tsconfig.json
  • packages/ai-persistence-drizzle/vite.config.ts
  • packages/ai-persistence-prisma/.gitignore
  • packages/ai-persistence-prisma/bin/tanstack-ai-prisma-models.mjs
  • packages/ai-persistence-prisma/package.json
  • packages/ai-persistence-prisma/prisma/schema.prisma
  • packages/ai-persistence-prisma/prisma/tanstack-ai.prisma
  • packages/ai-persistence-prisma/src/assets.d.ts
  • packages/ai-persistence-prisma/src/assets/tanstack-ai.prisma
  • packages/ai-persistence-prisma/src/cli.ts
  • packages/ai-persistence-prisma/src/index.ts
  • packages/ai-persistence-prisma/src/model-contract.ts
  • packages/ai-persistence-prisma/src/models-cli.ts
  • packages/ai-persistence-prisma/src/models.ts
  • packages/ai-persistence-prisma/src/stores.ts
  • packages/ai-persistence-prisma/tests/api-types.test-d.ts
  • packages/ai-persistence-prisma/tests/model-mapping.test.ts
  • packages/ai-persistence-prisma/tests/models-cli.test.ts
  • packages/ai-persistence-prisma/tests/models.test.ts
  • packages/ai-persistence-prisma/tests/package-contract.test.ts
  • packages/ai-persistence-prisma/tests/prisma.conformance.test.ts
  • packages/ai-persistence-prisma/tests/store-behavior.test.ts
  • packages/ai-persistence-prisma/tsconfig.json
  • packages/ai-persistence-prisma/vite.config.ts
  • packages/ai-persistence/package.json
  • packages/ai-persistence/src/capabilities.ts
  • packages/ai-persistence/src/index.ts
  • packages/ai-persistence/src/interrupts.ts
  • packages/ai-persistence/src/locks.ts
  • packages/ai-persistence/src/memory.ts
  • packages/ai-persistence/src/middleware.ts
  • packages/ai-persistence/src/testkit/conformance.ts
  • packages/ai-persistence/src/types.ts
  • packages/ai-persistence/tests/capabilities.test.ts
  • packages/ai-persistence/tests/error-abort.test.ts
  • packages/ai-persistence/tests/interrupts.test.ts
  • packages/ai-persistence/tests/memory.conformance.test.ts
  • packages/ai-persistence/tests/memory.test.ts
  • packages/ai-persistence/tests/persistence-composition.test.ts
  • packages/ai-persistence/tests/persistence-fixtures.ts
  • packages/ai-persistence/tests/persistence-types.test-d.ts
  • packages/ai-persistence/tests/persistence-validation.test.ts
  • packages/ai-persistence/tests/state-only.test.ts
  • packages/ai-persistence/tests/with-persistence.test.ts
  • packages/ai-persistence/tsconfig.json
  • packages/ai-persistence/vite.config.ts
  • packages/ai/skills/ai-core/chat-experience/SKILL.md
  • packages/ai/skills/ai-core/middleware/SKILL.md
  • pnpm-workspace.yaml
  • testing/e2e/src/routeTree.gen.ts
  • testing/e2e/src/routes/api.persistence-durability.ts
  • testing/e2e/src/routes/persistence-durability.tsx
  • testing/e2e/tests/persistence-durability.spec.ts

Comment thread examples/ts-react-chat/src/routes/api.persistent-chat.ts Outdated
Comment thread packages/ai-client/src/chat-client.ts
Comment on lines +875 to +885
// Expose joinRun only when the underlying connection is resumable. Returns
// the replay iterable directly; the client consumes it like a subscription.
...('joinRun' in connection
? {
joinRun: (runId: string, abortSignal?: AbortSignal) =>
(connection as ResumableConnectConnectionAdapter).joinRun(
runId,
abortSignal,
),
}
: {}),

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

Require joinRun to be a function before exposing it.

'joinRun' in connection is true for { joinRun: undefined }, so the normalized adapter exposes a callable wrapper that throws when rehydration invokes it. Check typeof connection.joinRun === 'function' instead, and cover the explicit-undefined case.

🤖 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/connection-adapters.ts` around lines 875 - 885, Update
the normalized adapter’s conditional spread around joinRun to expose the wrapper
only when typeof connection.joinRun is "function", rather than merely checking
property presence. Preserve the existing ResumableConnectConnectionAdapter
delegation and ensure an explicit undefined joinRun is omitted.

Comment on lines +55 to +56
"@tanstack/ai": "workspace:^",
"@tanstack/ai-persistence": "workspace:^",

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

Use workspace:* for internal peer dependencies.

Proposed fix
-    "`@tanstack/ai`": "workspace:^",
-    "`@tanstack/ai-persistence`": "workspace:^",
+    "`@tanstack/ai`": "workspace:*",
+    "`@tanstack/ai-persistence`": "workspace:*",
📝 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
"@tanstack/ai": "workspace:^",
"@tanstack/ai-persistence": "workspace:^",
"`@tanstack/ai`": "workspace:*",
"`@tanstack/ai-persistence`": "workspace:*",
🤖 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-drizzle/package.json` around lines 55 - 56, Update
the internal dependency declarations in package.json for `@tanstack/ai` and
`@tanstack/ai-persistence` from workspace:^ to workspace:*. Leave external
dependencies unchanged.

Source: Coding guidelines

Comment on lines +20 to +30
/** Build persistence over Node's built-in SQLite driver. */
export function sqlitePersistence(options: SqlitePersistenceOptions) {
const filename = normalizeSqliteUrl(options.url)
ensureParentDirectory(filename)
const sqlite = new DatabaseSync(filename)
try {
if (options.migrate) applySqliteMigrations(sqlite, sqliteMigrations)
} catch (error) {
sqlite.close()
throw error
}

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

🧩 Analysis chain

🌐 Web query:

What Node.js version made node:sqlite (DatabaseSync) stable without a flag?

💡 Result:

As of July 2026, the node:sqlite module (including the DatabaseSync class) has not been marked as "stable" in the Node.js documentation; it is currently classified as a "Release candidate" (Stability index 1.2) [1][2]. The node:sqlite module was originally introduced behind the --experimental-sqlite flag in Node.js v22.5.0 [1][3]. It was subsequently made available without the requirement of that experimental flag starting in Node.js v23.4.0 and v22.13.0 [3][2]. Following its unflagging, the module remained experimental until it was officially promoted to a "Release candidate" status in early 2026 [4]. While it no longer requires a flag to be used, it has not yet reached full "Stable" (Stability index 2) status [1][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate package manifests and the target source file.
git ls-files 'packages/**/package.json' 'package.json' 'packages/ai-persistence-drizzle/src/sqlite.ts'

# Show the relevant package manifest(s) and any engine declarations.
for f in $(git ls-files 'packages/**/package.json' 'package.json'); do
  if grep -q '"ai-persistence-drizzle"' "$f" || grep -q '"engines"' "$f"; then
    echo "===== $f ====="
    cat -n "$f" | sed -n '1,220p'
  fi
done

echo "===== packages/ai-persistence-drizzle/src/sqlite.ts ====="
cat -n packages/ai-persistence-drizzle/src/sqlite.ts | sed -n '1,220p'

Repository: TanStack/ai

Length of output: 28299


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Find any references to sqlite driver imports and Node version requirements in the repo.
rg -n --hidden --glob '!**/node_modules/**' --glob '!**/dist/**' --glob '!**/build/**' \
  'node:sqlite|DatabaseSync|--experimental-sqlite|engines' \
  packages .github . || true

Repository: TanStack/ai

Length of output: 50367


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the package directory layout to find any nearby docs or metadata that may declare support policy.
fd -t f -a 'README*|package.json|*.md' packages/ai-persistence-drizzle packages | sed -n '1,200p'

Repository: TanStack/ai

Length of output: 475


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Check whether the package manifest is nested or the repo root declares a global engine floor.
for f in package.json packages/ai-persistence-drizzle/package.json; do
  if [ -f "$f" ]; then
    echo "===== $f ====="
    cat -n "$f" | sed -n '1,220p'
  fi
done

Repository: TanStack/ai

Length of output: 7183


Add a Node engine floor to packages/ai-persistence-drizzle/package.json
node:sqlite is available without the experimental flag only from Node 22.13.0 / 23.4.0+, and this package doesn’t declare any engines.node range. Older Node versions will fail at runtime.

🤖 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-drizzle/src/sqlite.ts` around lines 20 - 30, Add an
engines.node declaration to packages/ai-persistence-drizzle/package.json
requiring Node 22.13.0 or newer, or the equivalent supported Node release range
that excludes versions where node:sqlite requires the experimental flag. Keep
the sqlitePersistence implementation unchanged.


globals: true,
environment: 'node',
include: ['tests/**/*.test.ts'],

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

Discover source-colocated unit tests.

This only runs tests/**/*.test.ts, conflicting with the required colocated *.test.ts layout. Include colocated source tests and move the package tests alongside their covered modules.

As per coding guidelines, “Place unit tests in *.test.ts files alongside the source they cover.”

🤖 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-prisma/vite.config.ts` at line 13, Update the test
include configuration in vite.config.ts to discover colocated *.test.ts files
throughout the source tree, not only tests/**/*.test.ts. Move existing package
tests alongside the modules they cover while preserving their test names and
coverage.

Source: Coding guidelines

Comment on lines +46 to +57
const prior = this.chains.get(key) ?? Promise.resolve()
const runCriticalSection = () => fn(new AbortController().signal)
// Chain after the prior holder regardless of how it settled.
const run = prior.then(runCriticalSection, runCriticalSection)
// Keep the chain alive but swallow rejections so one failure doesn't poison the lock.
this.chains.set(
key,
run.then(
() => undefined,
() => undefined,
),
)

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

Remove completed lock chains.

Every distinct key remains in chains forever. Long-lived processes locking unbounded thread/run IDs will accumulate settled promises indefinitely.

Proposed fix
     const run = prior.then(runCriticalSection, runCriticalSection)
-    // Keep the chain alive but swallow rejections so one failure doesn't poison the lock.
-    this.chains.set(
-      key,
-      run.then(
-        () => undefined,
-        () => undefined,
-      ),
-    )
+    const tail = run.then(
+      () => undefined,
+      () => undefined,
+    )
+    this.chains.set(key, tail)
+    void tail.finally(() => {
+      if (this.chains.get(key) === tail) {
+        this.chains.delete(key)
+      }
+    })
     return run
📝 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 prior = this.chains.get(key) ?? Promise.resolve()
const runCriticalSection = () => fn(new AbortController().signal)
// Chain after the prior holder regardless of how it settled.
const run = prior.then(runCriticalSection, runCriticalSection)
// Keep the chain alive but swallow rejections so one failure doesn't poison the lock.
this.chains.set(
key,
run.then(
() => undefined,
() => undefined,
),
)
const prior = this.chains.get(key) ?? Promise.resolve()
const runCriticalSection = () => fn(new AbortController().signal)
// Chain after the prior holder regardless of how it settled.
const run = prior.then(runCriticalSection, runCriticalSection)
const tail = run.then(
() => undefined,
() => undefined,
)
this.chains.set(key, tail)
void tail.finally(() => {
if (this.chains.get(key) === tail) {
this.chains.delete(key)
}
})
🤖 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/locks.ts` around lines 46 - 57, Update the
lock-chain cleanup around the chains map so a key is removed once its current
run settles, while preserving newer work that may have been queued for the same
key. Use the existing key and chain identity to ensure an older completion
cannot delete a replacement chain, and keep rejection swallowing behavior
unchanged.

Comment on lines +98 to +127
list(threadId: string): Promise<Array<InterruptRecord>> {
return Promise.resolve(
[...this.interrupts.values()].filter(
(interrupt) => interrupt.threadId === threadId,
),
)
}
listPending(threadId: string): Promise<Array<InterruptRecord>> {
return Promise.resolve(
[...this.interrupts.values()].filter(
(interrupt) =>
interrupt.threadId === threadId && interrupt.status === 'pending',
),
)
}
listByRun(runId: string): Promise<Array<InterruptRecord>> {
return Promise.resolve(
[...this.interrupts.values()].filter(
(interrupt) => interrupt.runId === runId,
),
)
}
listPendingByRun(runId: string): Promise<Array<InterruptRecord>> {
return Promise.resolve(
[...this.interrupts.values()].filter(
(interrupt) =>
interrupt.runId === runId && interrupt.status === 'pending',
),
)
}

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

Sort interrupt query results by requestedAt.

These methods return Map insertion order, but the InterruptStore contract requires ascending requestedAt. Out-of-order inserts therefore produce backend-inconsistent resume/interrupt ordering.

🤖 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/memory.ts` around lines 98 - 127, Update the
interrupt query methods list, listPending, listByRun, and listPendingByRun to
sort their filtered results by ascending requestedAt before returning them.
Preserve each method’s existing threadId, runId, and pending-status filters
while ensuring all query results follow the InterruptStore ordering contract
rather than Map insertion order.

Comment on lines +131 to +143
private readonly values = new Map<string, unknown>()
get(scope: string, key: string): Promise<unknown | null> {
const storageKey = `${scope}:${key}`
return Promise.resolve(
this.values.has(storageKey) ? this.values.get(storageKey) : null,
)
}
set(scope: string, key: string, value: unknown): Promise<void> {
this.values.set(`${scope}:${key}`, value)
return Promise.resolve()
}
delete(scope: string, key: string): Promise<void> {
this.values.delete(`${scope}:${key}`)

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

Use an unambiguous metadata key representation.

${scope}:${key} aliases distinct composite identities: ('a:b', 'c') and ('a', 'b:c') both map to a:b:c. This can overwrite or delete unrelated metadata.

Use nested maps or a collision-free tuple encoding.

🤖 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/memory.ts` around lines 131 - 143, Replace the
flat string-key storage in the metadata methods get, set, and delete with nested
maps or another collision-free representation that preserves scope and key as
separate components. Ensure distinct pairs such as scope “a:b” with key “c” and
scope “a” with key “b:c” remain independently retrievable, writable, and
deletable.

Comment on lines +17 to +21
* The mid-stream "rejoin an in-flight run via joinRun after reload" path is NOT
* covered here: the harness stream completes in a single tick, so there is no
* deterministic window to reload while a run is still producing. That resume
* cursor is covered at the transport layer by `delivery-durability.spec.ts` and
* in `@tanstack/ai-client` unit tests.

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 | 🏗️ Heavy lift

Add E2E coverage for useChat rejoining an in-flight run.

This explicitly excludes the new reload-and-joinRun behavior. Make the fixture pause mid-stream, reload, assert the client issues a resume request, and verify exactly one continuation is rendered. Also block API traffic during the interrupt reload to prove that case is local-storage-only.

As per coding guidelines, “Every feature, bug fix, or behavior change must include E2E test coverage in testing/e2e/.”

🤖 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 `@testing/e2e/tests/persistence-durability.spec.ts` around lines 17 - 21,
Extend the persistence durability E2E fixture to pause deterministically
mid-stream, reload during the active run, and verify useChat issues a
resume/joinRun request and renders exactly one continuation. During the
interrupt reload, block API traffic to prove recovery uses local storage only,
then restore traffic for the resumed request; update the existing excluded-path
note and assertions accordingly.

Source: Coding guidelines

@tombeckenham tombeckenham added the waiting-on: author Waiting for the author to respond or update label Jul 23, 2026
Add the object form `persistence: { store, messages?: boolean }`. `messages:
false` caches only the tiny resume pointer, keeping large transcripts off the
client while durability rejoin and interrupt restore still work and the server
stays authoritative for history. A bare adapter remains shorthand for
`{ store, messages: true }`, so this is backward compatible and every framework
passthrough is unchanged.

The persistent-chat example gains a history branch on its GET route
(loadThread by threadId, distinct from the per-run delivery replay) so a
server-authoritative reload can hydrate the transcript. Docs + chat-experience
skill document the lever.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
Rewrite the persistence overview into a concept + decision page: the three
problems (dropped stream, lost-on-reload, no durable record), the two
independent layers (delivery durability vs state persistence), client vs server
halves, the reload/rehydration timeline, and a when-to-pick-each guide. Add a
back-link from the resumable-streams overview so the two sections cross-reference.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
packages/ai/skills/ai-core/chat-experience/SKILL.md (2)

507-508: 📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Fix the malformed import guidance sentence.

“the one exception to mistake j below” is unclear and appears to contain an accidental edit. Replace it with the intended wording before publishing the skill.

🤖 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/skills/ai-core/chat-experience/SKILL.md` around lines 507 - 508,
Correct the import guidance sentence in the skill documentation so it clearly
states that framework packages should be imported from `@tanstack/ai-client`, with
useChat remaining the exception sourced from the framework package. Remove the
malformed “mistake j below” wording without changing the surrounding guidance.

517-520: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use a Date-aware deserializer.

UIMessage.createdAt is a Date, but this example uses JSON.stringify / JSON.parse, so the timestamp is restored as a string on reload and does not preserve the documented ChatPersistedState shape. Use the repository’s Date-aware codec or a deserializer that reconstructs Date fields.

🤖 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/skills/ai-core/chat-experience/SKILL.md` around lines 517 - 520,
Update the localStoragePersistence configuration around ChatPersistedState to
use the repository’s Date-aware serialization/deserialization codec, or
reconstruct Date fields during deserialize, so UIMessage.createdAt is restored
as a Date while preserving the existing persisted state shape.
docs/persistence/browser-refresh.md (1)

28-30: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Make the localStorage codec Date-aware.

UIMessage.createdAt is typed as Date, so JSON.stringify stores it as a string and JSON.parse returns that string instead of a Date; the useChat transcript is then restored with mutated timestamps. Use a Date-aware validated codec here, or switch this sample to indexedDBPersistence since it already supports structured cloning.

🤖 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/browser-refresh.md` around lines 28 - 30, Update the
localStoragePersistence codec for ChatPersistedState so UIMessage.createdAt
values are restored as Date instances rather than strings; use an existing
Date-aware validated codec, or replace localStoragePersistence with
indexedDBPersistence to preserve structured-cloned dates.
🤖 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/browser-refresh.md`:
- Around line 99-102: Expand the server-authoritative reload example around the
router-loader discussion to include a concrete server endpoint that derives and
authorizes the thread ID, calls
persistence.stores.messages.loadThread(threadId), and returns the messages. Add
the corresponding client-side consumption showing how the endpoint response
seeds initialMessages, while preserving the distinction from the delivery replay
endpoint.

In `@examples/ts-react-chat/src/routes/api.persistent-chat.ts`:
- Around line 78-80: Update the history-loading branch around loadThread so it
does not trust the caller-controlled threadId query parameter. Derive the thread
identity from the authenticated session and authorize it for the current tenant
before calling persistence.stores.messages.loadThread, preserving the existing
JSON response only for authorized sessions.

---

Outside diff comments:
In `@docs/persistence/browser-refresh.md`:
- Around line 28-30: Update the localStoragePersistence codec for
ChatPersistedState so UIMessage.createdAt values are restored as Date instances
rather than strings; use an existing Date-aware validated codec, or replace
localStoragePersistence with indexedDBPersistence to preserve structured-cloned
dates.

In `@packages/ai/skills/ai-core/chat-experience/SKILL.md`:
- Around line 507-508: Correct the import guidance sentence in the skill
documentation so it clearly states that framework packages should be imported
from `@tanstack/ai-client`, with useChat remaining the exception sourced from the
framework package. Remove the malformed “mistake j below” wording without
changing the surrounding guidance.
- Around line 517-520: Update the localStoragePersistence configuration around
ChatPersistedState to use the repository’s Date-aware
serialization/deserialization codec, or reconstruct Date fields during
deserialize, so UIMessage.createdAt is restored as a Date while preserving the
existing persisted state shape.
🪄 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: 177c8b89-cf85-45bd-9516-9a7d3ad5c65e

📥 Commits

Reviewing files that changed from the base of the PR and between b443a9f and 89932a8.

📒 Files selected for processing (13)
  • .changeset/client-browser-refresh-durability.md
  • docs/chat/persistence.md
  • docs/config.json
  • docs/persistence/browser-refresh.md
  • docs/persistence/controls.md
  • examples/ts-react-chat/src/routes/api.persistent-chat.ts
  • examples/ts-react-chat/src/routes/persistent-chat.tsx
  • packages/ai-client/src/chat-client.ts
  • packages/ai-client/src/client-persistor.ts
  • packages/ai-client/src/index.ts
  • packages/ai-client/src/types.ts
  • packages/ai-client/tests/resume-snapshot.test.ts
  • packages/ai/skills/ai-core/chat-experience/SKILL.md
🚧 Files skipped from review as they are similar to previous changes (10)
  • .changeset/client-browser-refresh-durability.md
  • docs/config.json
  • packages/ai-client/src/index.ts
  • docs/persistence/controls.md
  • packages/ai-client/tests/resume-snapshot.test.ts
  • docs/chat/persistence.md
  • packages/ai-client/src/types.ts
  • examples/ts-react-chat/src/routes/persistent-chat.tsx
  • packages/ai-client/src/chat-client.ts
  • packages/ai-client/src/client-persistor.ts

Comment thread docs/persistence/browser-refresh.md Outdated
Comment thread examples/ts-react-chat/src/routes/api.persistent-chat.ts Outdated

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

🤖 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/overview.md`:
- Line 40: Update the Client row in the persistence options table to mention
sessionStorage alongside localStorage and IndexedDB, preserving the existing
description and formatting.
- Line 32: Update the persistence-layer comparison around the sentence stating
that the layers share “no code.” Replace it with wording that preserves their
separate responsibilities while acknowledging the integration contract between
client-persisted resume/runId metadata and delivery-durable run rejoining.
- Around line 81-84: Update the localStoragePersistence example’s deserialize
callback to validate and narrow the JSON.parse result to ChatPersistedState
before returning it, using the project’s established Standard Schema parser or
type guard rather than trusting the parsed value directly; preserve the existing
serialization 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: 4bab97e0-76be-49ac-b3f5-d16b75985bdd

📥 Commits

Reviewing files that changed from the base of the PR and between 89932a8 and e8aef35.

📒 Files selected for processing (3)
  • docs/config.json
  • docs/persistence/overview.md
  • docs/resumable-streams/overview.md

Comment thread docs/persistence/overview.md
Comment thread docs/persistence/overview.md Outdated

| Half | Stores | Survives | Use it for |
| --- | --- | --- | --- |
| **Client** ([Browser refresh](./browser-refresh)) | the transcript + a resume pointer, in `localStorage` / `IndexedDB` | a page reload in that browser | instant restore on reload, SPA / offline apps |

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

Mention sessionStorage in the client options table.

The PR includes local-storage, session-storage, and IndexedDB adapters, but this overview lists only localStorage and IndexedDB. Add sessionStorage so the overview reflects the supported client options.

🤖 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/overview.md` at line 40, Update the Client row in the
persistence options table to mention sessionStorage alongside localStorage and
IndexedDB, preserving the existing description and formatting.

Comment thread docs/persistence/overview.md Outdated
Comment on lines +81 to +84
const store = localStoragePersistence<ChatPersistedState>({
serialize: (value) => JSON.stringify(value),
deserialize: (value) => JSON.parse(value),
})

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

Validate the persisted value before returning it.

JSON.parse accepts arbitrary storage contents, but this sample passes the result to useChat as ChatPersistedState; stale or corrupt data can break hydration. Use a Standard Schema parser or type guard.

Suggested adjustment
-  deserialize: (value) => JSON.parse(value),
+  deserialize: (value) => persistedStateSchema.parse(JSON.parse(value)),

As per coding guidelines, documentation examples should narrow or validate parsed values instead of trusting them as the target type.

🤖 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/overview.md` around lines 81 - 84, Update the
localStoragePersistence example’s deserialize callback to validate and narrow
the JSON.parse result to ChatPersistedState before returning it, using the
project’s established Standard Schema parser or type guard rather than trusting
the parsed value directly; preserve the existing serialization behavior.

Source: Coding guidelines

AlemTuzlak and others added 4 commits July 23, 2026 11:56
…tence

localStoragePersistence / sessionStoragePersistence / indexedDBPersistence now
default their type parameter to ChatPersistedState and to a JSON codec, so
`persistence: localStoragePersistence()` needs no type argument and no
serialize/deserialize pair. Drops the IsJsonSerializable type gate that forced a
codec for the chat record (UIMessage already round-trips as JSON on the wire).

Client persistence now keys on `threadId` (the conversation identity), so a
reload with the same threadId restores the same record; `id` becomes an optional
storage-key override. The storage adapters and persistence types are re-exported
from every framework package, so a single import from @tanstack/ai-react (etc.)
works.

Claude-Session: https://claude.ai/code/session_01RqjWdHxvmMrhjbd8dYvENp
reconstructChat(persistence, request) returns a thread's stored messages as a
JSON Response, so a server-authoritative client can hydrate its transcript on
load from a one-line GET handler instead of hand-rolling loadThread + Response.
…curate resume

Add a "What we recommend" section to the overview: client resume-pointer-only
plus server persistence plus one GET that rehydrates history and resumes durable
streams, with the reasoning. Update every snippet to the zero-config
localStoragePersistence() and threadId, use reconstructChat for history, and
discriminate the resume GET with durability.resumeFrom() instead of sniffing
query params (the run id rides the X-Run-Id header, the offset the Last-Event-ID
header). Example, e2e page, and chat-experience skill match.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/ai/skills/ai-core/chat-experience/SKILL.md (1)

495-506: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Use threadId consistently as the persistence key.

This section alternates between chat id, threadId, and stable id. Align the prose and framework example on threadId, and document id only as the explicit storage-key override if that is the intended API; otherwise users may fail to restore records after reload.

Also applies to: 548-551

🤖 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/skills/ai-core/chat-experience/SKILL.md` around lines 495 - 506,
Update the persistence documentation and framework example to use threadId
consistently as the stable persistence key. Remove ambiguous references to chat
id or stable id, and document id only if it is the explicit storage-key override
supported by the API; otherwise ensure the example passes threadId so reloads
restore the same record.
packages/ai-client/src/storage-adapters.ts (1)

149-160: 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Handle existing IndexedDB databases whose configured object store is missing.

factory.open(databaseName) without a version only raises onupgradeneeded for a new database; an existing database opened at its current version will create no object store in this branch. If objectStoreName was changed later, runRequest() will open a transaction for a non-existent store and start failing. Add a schema/version migration path, or clear/reject before caching the connection.

🤖 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/storage-adapters.ts` around lines 149 - 160, The
IndexedDB open flow does not migrate existing databases when the configured
object store is missing. Update the database-opening logic around factory.open
and request.onupgradeneeded to detect absent objectStoreName stores and trigger
a versioned schema upgrade that creates the store before caching or using the
connection; otherwise reject and avoid caching an unusable connection.
🤖 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/overview.md`:
- Around line 216-218: The “A cheap client” bullet incorrectly claims the
browser never parses or stores the transcript. Update that bullet to state that
the browser does not persist the long transcript in browser storage, while
preserving the existing claims about avoiding localStorage quota and
startup-persistence costs.
- Around line 85-87: Update the persistence examples in the affected sections to
derive thread IDs from the authenticated user rather than using the shared
literal support-chat or caller-controlled params.threadId. Ensure both POST and
GET history flows authorize the requested thread ID against the authenticated
user before persisting, reading, or reconstructing conversation history.

In `@packages/ai-persistence/src/reconstruct.ts`:
- Around line 18-35: The reconstructChat function must enforce authorization or
tenant scoping before calling persistence.stores.messages.loadThread(threadId).
Add a mandatory authorization/scoping contract, such as an auth callback or
caller-provided validated thread ID, and ensure unauthorized or out-of-scope
requests return without loading or exposing the transcript.
- Around line 31-35: The reconstructChat hydration response currently returns
stored ModelMessage values instead of the UIMessage contract. Update the
messages flow around persistence.stores.messages.loadThread and the returned
Response to convert loaded model messages with the existing
modelMessagesToUIMessages or equivalent typed converter, and declare the
response as the supported ChatPersistedState<{ messages: Array<UIMessage> }> or
Array<UIMessage> contract while preserving the empty-message behavior.
- Around line 35-37: Update the Response construction in reconstruct.ts to
include a no-store cache-control header alongside the existing content-type
header, ensuring transcript responses are not cached or reused while preserving
the serialized messages payload.

---

Outside diff comments:
In `@packages/ai-client/src/storage-adapters.ts`:
- Around line 149-160: The IndexedDB open flow does not migrate existing
databases when the configured object store is missing. Update the
database-opening logic around factory.open and request.onupgradeneeded to detect
absent objectStoreName stores and trigger a versioned schema upgrade that
creates the store before caching or using the connection; otherwise reject and
avoid caching an unusable connection.

In `@packages/ai/skills/ai-core/chat-experience/SKILL.md`:
- Around line 495-506: Update the persistence documentation and framework
example to use threadId consistently as the stable persistence key. Remove
ambiguous references to chat id or stable id, and document id only if it is the
explicit storage-key override supported by the API; otherwise ensure the example
passes threadId so reloads restore the same record.
🪄 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: 4f79e751-1747-4eb2-942c-33aacb3c8bcb

📥 Commits

Reviewing files that changed from the base of the PR and between e8aef35 and e5c4064.

📒 Files selected for processing (22)
  • .changeset/client-browser-refresh-durability.md
  • .changeset/persistence-packages.md
  • docs/chat/persistence.md
  • docs/persistence/browser-refresh.md
  • docs/persistence/overview.md
  • examples/ts-react-chat/src/routes/api.persistent-chat.ts
  • examples/ts-react-chat/src/routes/persistent-chat.tsx
  • packages/ai-angular/src/index.ts
  • packages/ai-client/src/chat-client.ts
  • packages/ai-client/src/index.ts
  • packages/ai-client/src/storage-adapters.ts
  • packages/ai-client/src/types.ts
  • packages/ai-client/tests/resume-snapshot.test.ts
  • packages/ai-persistence/src/index.ts
  • packages/ai-persistence/src/reconstruct.ts
  • packages/ai-preact/src/index.ts
  • packages/ai-react/src/index.ts
  • packages/ai-solid/src/index.ts
  • packages/ai-svelte/src/index.ts
  • packages/ai-vue/src/index.ts
  • packages/ai/skills/ai-core/chat-experience/SKILL.md
  • testing/e2e/src/routes/persistence-durability.tsx
💤 Files with no reviewable changes (1)
  • packages/ai-client/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/ai-persistence/src/index.ts
  • .changeset/persistence-packages.md
  • .changeset/client-browser-refresh-durability.md
  • examples/ts-react-chat/src/routes/persistent-chat.tsx
  • packages/ai-client/tests/resume-snapshot.test.ts
  • docs/persistence/browser-refresh.md
  • docs/chat/persistence.md
  • testing/e2e/src/routes/persistence-durability.tsx
  • packages/ai-client/src/types.ts
  • packages/ai-client/src/chat-client.ts

Comment on lines +85 to +87
threadId: 'support-chat',
connection: fetchServerSentEvents('/api/chat'),
persistence: localStoragePersistence(),

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 | 🏗️ Heavy lift

Do not recommend a shared thread ID for a multi-user app.

The examples use the literal support-chat, pass caller-controlled params.threadId into persistence, and reconstruct history from ?threadId. Any user can collide with, read, or overwrite another user’s persisted conversation. Use an authentication-derived thread ID and authorize it on both POST and GET.

Also applies to: 132-136, 164-166, 175-186, 203-208

🤖 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/overview.md` around lines 85 - 87, Update the persistence
examples in the affected sections to derive thread IDs from the authenticated
user rather than using the shared literal support-chat or caller-controlled
params.threadId. Ensure both POST and GET history flows authorize the requested
thread ID against the authenticated user before persisting, reading, or
reconstructing conversation history.

Comment thread docs/persistence/overview.md
Comment on lines +18 to +35
* ```ts
* export async function GET(request: Request) {
* return reconstructChat(persistence, request)
* }
* ```
*/
export async function reconstructChat(
persistence: AIPersistence,
request: Request,
options?: ReconstructChatOptions,
): Promise<Response> {
const param = options?.param ?? 'threadId'
const threadId = new URL(request.url).searchParams.get(param) ?? ''
const messages =
threadId && persistence.stores.messages
? await persistence.stores.messages.loadThread(threadId)
: []
return new Response(JSON.stringify(messages), {

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== candidate files =="
fd -a 'reconstruct\.(ts|tsx|js|jsx)$' . || true

echo "== git status/stat =="
git diff --stat || true

echo "== inspect files =="
for f in $(fd 'reconstruct\.(ts|tsx|js|jsx)$' .); do
  echo "--- $f ($(wc -l < "$f") lines) ---"
  cat -n "$f"
done

echo "== references to reconstructChat =="
rg -n "reconstructChat|ReconstructChatOptions|threadId|authorization|authorize|requiresAuthorization|Authorization|Auth" packages test tests src || true

Repository: TanStack/ai

Length of output: 50370


Require authorization or scope before loading the returned thread.

reconstructChat reads threadId from the query and calls loadThread(threadId) with no auth/tenant context. A mounted GET route can therefore expose another user’s full transcript when the ID is guessed or leaked. Gate reconstruction by an auth callback, caller-provided scoped threadId, or make caller-side authorization an explicit mandatory contract.

🤖 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/reconstruct.ts` around lines 18 - 35, The
reconstructChat function must enforce authorization or tenant scoping before
calling persistence.stores.messages.loadThread(threadId). Add a mandatory
authorization/scoping contract, such as an auth callback or caller-provided
validated thread ID, and ensure unauthorized or out-of-scope requests return
without loading or exposing the transcript.

Comment thread packages/ai-persistence/src/reconstruct.ts
Comment on lines +35 to +37
return new Response(JSON.stringify(messages), {
headers: { 'content-type': 'application/json' },
})

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Locate reconstruct.ts =="
fd -a 'reconstruct\.ts$' . | sed 's#^\./##' || true

echo "== Relevant file snippet =="
if [ -f packages/ai-persistence/src/reconstruct.ts ]; then
  nl -ba packages/ai-persistence/src/reconstruct.ts | sed -n '1,120p'
fi

echo "== Search for route definitions/usages of reconstruct handler =="
rg -n "reconstruct|/reconstruct|get\\(" packages/ai-persistence -S || true

Repository: TanStack/ai

Length of output: 307


🌐 Web query:

Next.js app router defaultResponseHeaders cache-control no-store GET responses personalized user data recommendation

💡 Result:

In the Next.js App Router, dynamic routes that handle personalized user data—such as those accessing cookies, headers, or searchParams—are automatically treated as dynamic by Next.js [1][2][3]. This causes Next.js to omit caching for those specific routes, resulting in a default Cache-Control header of private, no-cache, no-store, max-age=0, must-revalidate [1]. Key points regarding this behavior: 1. Automatic Dynamic Opt-out: When a route uses Request-time APIs (like cookies() or headers()), Next.js switches the rendering mode for that route to dynamic [2]. Consequently, the response is not cached, and the server generates the content at request time to ensure personalized data is never exposed to other users [1][3]. 2. Manual Configuration: If you want to force dynamic behavior for a route—even if it does not strictly require cookies or headers—you can export the segment config: export const dynamic = 'force-dynamic'; [2][4]. This explicitly tells Next.js to bypass all caches for that route [3][4]. 3. Controlling Cache-Control: You generally do not manually set Cache-Control headers in the App Router via next.config.js or standard response headers, as Next.js manages these automatically based on the route's rendering strategy [5][1]. If you must enforce specific caching behavior for authenticated or personalized routes, ensure you are relying on the framework's automatic dynamic handling, which prevents sensitive data from being cached in shared locations like CDNs [1]. 4. Version Changes: Starting in Next.js 15, the default behavior for fetch requests changed from caching (force-cache) to no-store [3][6][4]. While this affects individual data fetches, it complements the existing route-level dynamic behavior, where any route relying on dynamic inputs (cookies/headers) remains un-cached by default [1][3]. For personalized data, it is recommended to keep routes dynamic to ensure data security and freshness, rather than attempting to override headers to force caching [6][4]. If you need to cache parts of a page while keeping others dynamic, consider using the 'use cache' directive or Partial Prerendering to keep personalized components dynamic while caching the rest [7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== read reconstruct.ts =="
awk '{printf "%6d\t%s\n", NR, $0}' packages/ai-persistence/src/reconstruct.ts | sed -n '1,120p'

echo "== route/search files =="
find packages/ai-persistence -type f \( -name '*route*' -o -name '*page*' -o -name '*.ts' \) | sed -n '1,120p'
rg -n "reconstruct|api/persistence|messages|user|cookies\\(|headers\\(" packages/ai-persistence packages -S | sed -n '1,220p'

echo "== package next version =="
rg -n '"next"' package.json packages -s .

Repository: TanStack/ai

Length of output: 24717


🌐 Web query:

TanStack AI reconstructChat example GET route /api/persistence/reconstruct

💡 Result:

There is no standard or built-in GET route at /api/persistence/reconstruct in TanStack AI [1][2][3][4]. TanStack AI handles persistence through a flexible, user-defined persistence adapter provided to the ChatClient [3]. This adapter must implement three specific methods—getItem(id), setItem(id, messages), and removeItem(id)—which allows you to define exactly how your chat history is stored, retrieved, and reconstructed, whether in localStorage, IndexedDB, a remote database, or elsewhere [3]. If you are seeing an endpoint like /api/persistence/reconstruct, it is likely: 1. A custom application-specific route implemented in your own codebase to support your custom persistence logic [5]. 2. A misunderstanding or misinterpretation of the TanStack AI documentation, which describes how the client uses the provided persistence adapter to "hydrate" or reconstruct chat state automatically upon construction [3]. For robust chat persistence, you should implement the persistence option in your ChatClient configuration as described in the official TanStack AI persistence documentation [3]. This ensures the client handles the lifecycle of saving and loading messages, including automatic hydration, without requiring manual intervention from individual GET routes [3].

Citations:


🌐 Web query:

TanStack AI reconstructChat threadId loadThread message store documentation

💡 Result:

TanStack AI is a framework-agnostic, type-safe SDK for building AI experiences, including chat applications [1][2]. Based on the current documentation, there is no single function named "reconstructChat" or "loadThread" in the public API [3][4][5]. Instead, chat state persistence and historical loading are managed via a dedicated persistence adapter system [6][7]. Key concepts for chat thread management in TanStack AI: 1. ChatClient Persistence: You can persist chat conversations across page reloads by providing a persistence adapter to the ChatClient (or framework hooks like useChat) [6][4]. This adapter implements getItem, setItem, and removeItem methods [6][7]. 2. Hydration: When a ChatClient is instantiated with a persistence adapter, it automatically calls getItem(id) to load existing messages [6][7]. By providing a stable id (e.g., a conversation identifier), the client restores the previous state [6]. 3. threadId: The threadId is used for correlating runs within the AG-UI protocol [4][8]. It persists across messages sent during a chat instance but is not the primary mechanism for loading historical message state from a storage backend; that is the role of the id option and the persistence adapter [6][4][9]. 4. Message Storage: The ChatClient manages the messages array in memory [6]. When persistence is configured, it automatically calls setItem(id, messages) whenever the message history updates [6][9]. For your requirements, you should configure your ChatClient with a stable id and a persistence adapter that retrieves the desired message history from your database or storage layer [6][7]. For more details, refer to the official TanStack AI documentation on Persistence [6] and the ChatClient API reference [4].

Citations:


🌐 Web query:

site:github.com/TanStack/ai "reconstructChat" "loadThread" "threadId"

💡 Result:

The terms reconstructChat and loadThread are not standard public-facing API functions within the TanStack AI library (as of July 2026). The SDK manages chat state and thread identification primarily through the ChatClient and framework-specific hooks like useChat [1][2][3]. Recent updates to TanStack AI have focused on the following mechanisms regarding threads and chat persistence: 1. Thread Identification: The threadId option is a documented parameter used to identify or resume conversation threads [1]. A fix was merged in June 2026 (PR #689) to ensure that threadId is correctly forwarded to the ChatClient across all framework wrappers (React, Vue, Svelte, Solid, and Preact), ensuring consistent thread-scoped behavior [1][2]. 2. Chat Persistence: TanStack AI introduced an optional, opt-in persistence adapter that allows conversations to survive page reloads without requiring manual initialMessages or onFinish boilerplate [3]. You configure this via the persistence option on the ChatClient: const client = new ChatClient({ id: 'conversation-123', connection, persistence: { getItem: (id) =>..., setItem: (id, messages) =>..., removeItem: (id) =>... } }); When provided, the ChatClient automatically handles hydration from getItem on construction and saves state via setItem on message changes [3]. If you are encountering reconstructChat or loadThread in specific documentation or legacy code, it is possible they are internal utilities or part of a non-standard implementation. For most applications, you should use the official ChatClient persistence adapter or standard threadId forwarding to manage conversation state [1][3].

Citations:


🌐 Web query:

TanStack AI server-authoritative client reconstruct message history

💡 Result:

TanStack AI utilizes the AG-UI (Agent-User Interaction) protocol to manage communication between the client and server [1][2]. To reconstruct message history in a server-authoritative context, the SDK uses standardized wire formats and message transformation utilities [3][4]. Key mechanisms for message history reconstruction include: 1. AG-UI Protocol and Wire Format: The @tanstack/ai-client communicates with the server by sending a RunAgentInput object, which includes threadId, runId, and the current messages array [4]. The server uses helpers like chatParamsFromRequestBody to parse these requests and reconstruct the conversation state [4]. 2. Message Transformation: The SDK provides utilities such as uiMessageToModelMessages to convert client-side UIMessages into the format expected by LLM providers [5]. This function processes the parts array in sequence—preserving text, tool calls, and tool results—which is essential for maintaining the correct interleaving required for multi-turn tool interactions [5]. 3. Server-Side Compatibility: The SDK is designed to ensure foreign AG-UI clients can hit a TanStack server and have their history reassembled correctly [4]. This includes deduping fan-out tool messages, collapsing system roles, and dropping client-specific metadata (like reasoning or activity traces) that are not needed by the model [3][4]. 4. Persistence Adapters: If you need to persist message history across reloads (a form of client-side reconstruction), you can provide a persistence adapter to the ChatClient [6]. The client handles hydration by calling getItem(id) upon construction, which overrides initialMessages and populates the client with the retrieved conversation state [6]. 5. Event-Driven Reconstruction: For observability or telemetry, you can reconstruct a chat timeline by subscribing to the event bus [7]. Key events include text:message:created, text:chunk:, and various tool- events, which together provide enough information to replay the conversation end-to-end [7]. For implementation, ensure your server-side routes derive the model context from forwardedProps and pass the reconstructed messages to your chat function [4]. The official documentation provides comprehensive details on the wire format and adapter contracts [8][1].

Citations:


Prevent caching of transcript responses by default.

This response is intended to carry thread-specific chat history, but it still starts with only Content-Type. Add a no-store cache header so callers do not reuse intercepted transcript data.

Proposed fix
  return new Response(JSON.stringify(messages), {
-   headers: { 'content-type': 'application/json' },
+   headers: {
+     'content-type': 'application/json',
+     'cache-control': 'no-store',
+   },
  })
📝 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
return new Response(JSON.stringify(messages), {
headers: { 'content-type': 'application/json' },
})
return new Response(JSON.stringify(messages), {
headers: {
'content-type': 'application/json',
'cache-control': 'no-store',
},
})
🤖 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/reconstruct.ts` around lines 35 - 37, Update the
Response construction in reconstruct.ts to include a no-store cache-control
header alongside the existing content-type header, ensuring transcript responses
are not cached or reused while preserving the serialized messages payload.

Rename browser-refresh to client-persistence and make it the single home for
the client story: turning it on, what a reload restores, the two cache modes
(everything vs resume-pointer-only) with when to use each, and the three storage
backends with when to use each. Remove the legacy docs/chat/persistence page
(client content now lives in the persistence section) and repoint its links.

Make the other persistence docs server-only: drop the client rows from the
controls decision table and the browser-storage section from internals, leaving
a pointer to the client guide. The overview stays the cross-cutting map. Update
all cross-links, the chat-experience skill source, and the reconstructChat doc
reference.

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

🤖 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/client-persistence.md`:
- Around line 75-77: The documentation section describing server-authoritative
persistence needs a concrete server example. Add a minimal GET endpoint snippet
using reconstructChat(persistence, request), positioned alongside the existing
client usage and aligned with the documented loader flow; keep the example
focused on returning the reconstructed transcript.
- Around line 41-50: Update the “Repaints the transcript” documentation in the
client persistence overview to describe initialization-time hydration rather
than hydration before the first render. Explicitly distinguish synchronous Web
Storage, which may hydrate immediately, from asynchronous lazily opened
IndexedDB, whose transcript and resume snapshot apply only after hydration
resolves; keep the pending-interrupt and in-flight-run behavior unchanged.
🪄 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: 11e079f7-59fc-4078-984b-71126e311805

📥 Commits

Reviewing files that changed from the base of the PR and between e5c4064 and 8f408cb.

📒 Files selected for processing (11)
  • docs/chat/persistence.md
  • docs/config.json
  • docs/persistence/chat-persistence.md
  • docs/persistence/client-persistence.md
  • docs/persistence/controls.md
  • docs/persistence/internals.md
  • docs/persistence/overview.md
  • docs/resumable-streams/advanced.md
  • packages/ai-persistence/src/reconstruct.ts
  • packages/ai/skills/ai-core/chat-experience/SKILL.md
  • packages/ai/skills/ai-core/middleware/SKILL.md
💤 Files with no reviewable changes (1)
  • docs/chat/persistence.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • docs/persistence/chat-persistence.md
  • packages/ai-persistence/src/reconstruct.ts
  • docs/persistence/overview.md
  • docs/persistence/controls.md
  • docs/config.json

Comment thread docs/persistence/client-persistence.md
Comment on lines +75 to +77
The transcript is not in storage, so hydrate it from the server on load: expose a
`GET` endpoint with `reconstructChat(persistence, request)` and read it from a
router loader to seed `initialMessages`. See [Chat persistence](./chat-persistence).

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

Add the server endpoint snippet for server-authoritative mode.

This page documents both client consumption and the GET/reconstructChat(...) server flow, but only provides prose for the server half. Add a minimal server endpoint example alongside the existing client usage.

🤖 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/client-persistence.md` around lines 75 - 77, The
documentation section describing server-authoritative persistence needs a
concrete server example. Add a minimal GET endpoint snippet using
reconstructChat(persistence, request), positioned alongside the existing client
usage and aligned with the documented loader flow; keep the example focused on
returning the reconstructed transcript.

Source: Coding guidelines

AlemTuzlak and others added 3 commits July 23, 2026 12:27
In `{ messages: false }` mode a prior session's persisted record is
`{ messages: [], resume }`. The constructor treated that empty transcript as
authoritative and clobbered host-provided `initialMessages`, and the async
hydrate path applied `[]` on top, so a server-authoritative reload dropped the
history the app had fetched from the server.

The persisted transcript is now adopted only when the client actually caches it
(`cachesMessages`); in messages:false mode the client keeps `initialMessages`
and takes only the resume pointer from storage. This makes the recommended
server-authoritative flow work: on a mid-stream reload the app seeds history via
initialMessages (the reconstruct GET) while the client separately rejoins the
live run via joinRun (the resume GET), and the replayed run merges into the
seeded history by message id. Adds a test covering both together.

Also document in the overview that history hydration and run rejoin are two
separate GET requests, so the handler's if/else routes each and neither blocks
the other.
The primary chat persistence middleware is now `withPersistence`.
`withGenerationPersistence` is unchanged. Unreleased, so no alias is kept.
Updates all call sites, docs, skills, the example, and the changeset.

fix(ai-client): rejoin an in-flight run from an async persistence store

Auto-rejoin was gated on the synchronous read, so an async store
(indexedDBPersistence) restored messages and interrupts on reload but never
rejoined a mid-stream run. A guarded maybeRejoinInFlight now fires from both the
sync read and the async hydrate path; it rejoins a run at most once and never
while another run is already active (a fresh send wins). Adds a test that a
run rejoins from an async (Promise-returning) adapter.
A stale local build had masked real breakage: the ported persistence layer was
behind the current core. Reconciled against a clean build:

- withGenerationPersistence keys runs on `requestId` (the current
  GenerationMiddlewareContext has no runId/threadId).
- Restore server-authoritative resume: withPersistence translates persisted
  interrupts into resumeToolState and clears `config.resume`, so the engine
  skips its ephemeral (client-history) reconstruction, which the empty-messages
  persistence flow can't satisfy. (Reverts an incorrect earlier removal.)
- ai-client: never persist an empty record (no messages, no resume), so a
  cleared conversation is removed, not left as `{ messages: [] }` — fixes the
  clear/suppression regressions from the combined-record change.
- Tests updated to the combined-record shape, LockStore imported from
  @tanstack/ai-persistence, generation-context mocks and event shapes aligned to
  the current core.

The two-phase approval->client-tool continuation test is skipped with a TODO:
its exact resume-execution semantics depend on the engine and need reconciling
with the engine owner; single-phase approval and client-tool resume are covered.
The persisted-state resume path already works end-to-end: withPersistence
rehydrates the paused thread into config.messages, so the engine reprocesses
the pending tool call from server state (not the omitted client history).
Approving a client tool therefore advances straight to the client-execution
interrupt without re-invoking the model, and feeding the client output drives
one final model call. The prior skip assumed a model re-invocation that does
not happen; assertions now match the real engine behavior.
…authoritative persistence

Switch the demo to the recommended setup: `persistence: { store, messages: false }`
so the client caches only the resume pointer and the server (SQLite) owns history.
A route loader hydrates the transcript from a server function that reads the
stored thread — SSR-safe (no relative fetch), sharing one lazily-opened store
with the API route.
Type `prismaPersistence`'s client argument structurally (`PrismaClientLike`)
instead of importing `PrismaClient` from `@prisma/client`. The runtime was
already structural and the delegate query API is identical across majors, so
this accepts a client from either the v6 `prisma-client-js` generator or the v7
`prisma-client` generator (emitted to a custom output, not `@prisma/client`).
Docs note both versions.
withPersistence.onFinish saved ctx.messages, but the chat engine only appends
an assistant turn to the middleware message list when it carries tool calls —
a run's terminal text reply is never appended. So a stored thread dropped the
assistant's final answer and a server-authoritative reload showed only user
messages. Reattach the terminal reply from info.content (the last turn's
accumulated text), guarded against duplication. Strengthen the unit test to
assert the assistant reply is stored (it previously only checked length > 0,
which masked this).
Add getWeather / rollDice server tools so the demo exercises the agent loop and
tool-call persistence (tool calls + results are stored and rehydrated on
reload), and replace the bare inline styles with a self-contained dark chat UI
that renders tool-call cards (input/output) alongside message bubbles, plus
suggestion chips and auto-scroll. Regenerate routeTree.gen.ts to register the
persistent-chat routes.
Persist the pending turn at run start and stamp each assistant turn with its
stream messageId, add an opt-in `snapshotStreaming` for partial-output
durability, carry an optional `id` on ModelMessage that the converter preserves,
and rebuild an in-flight assistant from the delivery log on reload (batch-apply
the backlog, drop the hydrated partial) so a reload shows one clean bubble that
catches up and continues instead of a frozen or duplicated partial.

Verified via unit tests, a headless ChatClient rejoin (tails live + full-replays
a completed run), and curl against the join endpoint (replays + tails to
completion). Known follow-up: in the browser through the vite dev server the
long-lived SSE connection can drop mid-stream and resumableStream's reconnect
does not always tail to completion after catch-up; server + client logic are
correct (Node/curl tail fine), so this is a transport/reconnect issue to close
against a production-style server.

Only those two stores move to the custom database; D1 still owns messages and
metadata. Composition does not create a transaction across those systems;
design related writes accordingly.

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.

It would be good to have specifics for each of the events that trigger persistence and what the flow is through the persistence store at that point. (e.g. this is when/how we load the thread, this is when/how we store messages, this is what happens when there is an interrupt, etc.) Maybe as swim lanes.

Basically I don't think folks are going to have a different store for chats. They are going to want to integrate it in with the rest of the customer data. So we should invest pretty heavily in this doc.

useChat's `[client, live]` effect called `client.unsubscribe()` on every mount
when `live` was false, which cancels the shared in-flight stream — aborting the
delivery resume the client constructor had just started for a reloaded run. A
mid-stream reload therefore caught up to the buffered point and froze. Only tear
down a subscription we actually started, so the rejoin streams to completion.

Verified end-to-end in a real browser (reload mid-stream now grows continuously
to completion in one clean bubble) with a regression test that fails without the
fix (the rejoined run never reaches the message list).
- memoryStream first-chunk deadline defaults to 100ms (was 30s): a reload
  rejoins a run produced in a prior request, so an empty log means gone —
  fail fast instead of holding a dead connection ~30s. Raise
  firstChunkDeadlineMs for producers that start well after a joiner attaches.
- ChatClient rejoin: bound the first-chunk wait + clear a dead pointer (no UI
  pinned loading, no retry-on-next-load); drop the hydrated partial only on real
  content (never RUN_STARTED alone) so a no-content rejoin can't leave an empty
  bubble; and stop a replayed RUN_STARTED (provider run id) from overwriting the
  persisted pointer with an id the log is not keyed by, so a SECOND reload still
  re-attaches.

Verified in a real browser: reload#1 and reload#2 both continue to completion in
one clean bubble; a dead/evicted pointer frees the input in ~380ms (was 30s);
no empty-bubble corruption. Unit tests cover the pointer-preservation and the
100ms default.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

waiting-on: author Waiting for the author to respond or update

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants