feat(persistence): server persistence + client browser-refresh durability#984
feat(persistence): server persistence + client browser-refresh durability#984AlemTuzlak wants to merge 53 commits into
Conversation
|
Note Reviews pausedIt 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 Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThis 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. ChangesPersistence durability
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related PRs
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
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
|
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.
|
🚀 Changeset Version Preview17 package(s) bumped directly, 36 bumped as dependents. 🟥 Major bumps
🟨 Minor bumps
🟩 Patch bumps
|
|
| Command | Status | Duration | Result |
|---|---|---|---|
nx affected --targets=test:sherif,test:knip,tes... |
❌ Failed | 50s | View ↗ |
nx run-many --targets=build --exclude=examples/... |
✅ Succeeded | 3s | View ↗ |
☁️ Nx Cloud last updated this comment at 2026-07-25 06:02:12 UTC
There was a problem hiding this comment.
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 winColocate this test with
migration-cli.ts.Move this to
packages/ai-persistence-cloudflare/src/migration-cli.test.tsso its location follows the repository test-layout rule.As per coding guidelines, “Place unit tests in
*.test.tsfiles 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 winIndexedDB rejoin needs an async callback path
readInitial()only computesrejoinRunIdon the synchronous path. For promise-backed persistence,hydrateAsync()reapplies the resume snapshot but never callsresumeInFlightRun(), so reloads fromindexedDBPersistencewon’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 winRestore
Datefields when hydrating persisted chat state.JSON.stringifyconvertsDatevalues to strings andJSON.parsedoes not revive them, so rehydrated messages no longer match theChatPersistedStateruntime 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 aDate.🤖 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 winWrap the CLI entrypoint in a
try/catch.packages/ai-persistence-cloudflare/src/cli.tsawaitsrunCloudflareMigrationsCli(...)directly, soMigrationCliErrorrejections will surface as raw stack traces. Print the message tostderrand setprocess.exitCode = 1instead.🤖 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 winUse
workspace:*for internal peer dependencies.Replace
workspace:^with the requiredworkspace:*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 winDetect dynamic Node and SQLite imports too.
await import('node:sqlite')orawait import('./sqlite')passes thesefrom-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 winGarbled sentence — fix the parenthetical.
"(the one exception to mistake
jbelow)" 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 winUse the required workspace protocol.
Change
@tanstack/aifromworkspace:^toworkspace:*.As per coding guidelines, “Use the
workspace:*protocol for internal package dependencies inpackage.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 winDo not promise restoration before first paint for async storage.
indexedDBPersistenceis 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 winColocate these unit tests with their source modules.
Split this new cross-module suite into tests beside
src/client-persistor.ts,src/connection-adapters.ts, andsrc/chat-client.ts.As per coding guidelines, “Place unit tests in
*.test.tsfiles 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 winAdd 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
useChatexample.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 winCorrect the documented
onConfigordering.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 | 🔵 TrivialConsider 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, andruns.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 winGlobal
"node"types leak intosrc, past the Workers-compatibility guard.
types: ["node", ...]applies acrossinclude: ["src", "tests"], so Node's ambient globals (process,__dirname, etc.) typecheck fine insidesrceven though this package targets Cloudflare Workers. The siblingpackage-contract.test.tsonly regex-checks forfrom 'node:'imports and the literalBuffer, so use of other Node globals insrcwouldn'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) sosrctypechecking 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 winUse the native
drizzle-orm/node-sqlitedriver here
drizzle-orm/node-sqlitealready acceptsDatabaseSyncdirectly, so this customsqlite-proxycallback 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 winAdd indexes for
threadId/runIdonInterrupt.
stores.tsqueriesInterruptviafindMany({ where: { threadId } })andfindMany({ where: { runId } })(list/listPending/listByRun/listPendingByRun), but no@@indexis 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 valueColocate the memory-store unit tests with their covered source.
packages/ai-persistence/tests/memory.conformance.test.ts#L1-L4: move besidepackages/ai-persistence/src/memory.ts.packages/ai-persistence/tests/memory.test.ts#L1-L132: move besidepackages/ai-persistence/src/memory.ts.As per coding guidelines, “Place unit tests in
*.test.tsfiles 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 winPrefer
EventType.RUN_FINISHEDhere. 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis excluded by!**/pnpm-lock.yaml
📒 Files selected for processing (134)
.changeset/client-browser-refresh-durability.md.changeset/persistence-packages.mddocs/chat/persistence.mddocs/config.jsondocs/persistence/browser-refresh.mddocs/persistence/chat-persistence.mddocs/persistence/cloudflare.mddocs/persistence/controls.mddocs/persistence/custom-stores.mddocs/persistence/drizzle.mddocs/persistence/internals.mddocs/persistence/migrations.mddocs/persistence/overview.mddocs/persistence/prisma.mddocs/persistence/sql-backends.mdexamples/ts-react-chat/.gitignoreexamples/ts-react-chat/README.mdexamples/ts-react-chat/package.jsonexamples/ts-react-chat/src/components/Header.tsxexamples/ts-react-chat/src/routes/api.persistent-chat.tsexamples/ts-react-chat/src/routes/persistent-chat.tsxpackages/ai-client/src/chat-client.tspackages/ai-client/src/client-persistor.tspackages/ai-client/src/connection-adapters.tspackages/ai-client/src/index.tspackages/ai-client/src/storage-adapters.tspackages/ai-client/src/types.tspackages/ai-client/tests/resume-snapshot.test.tspackages/ai-persistence-cloudflare/bin/tanstack-ai-cloudflare-migrations.mjspackages/ai-persistence-cloudflare/migrations/0000_tanstack_ai_initial.sqlpackages/ai-persistence-cloudflare/package.jsonpackages/ai-persistence-cloudflare/src/assets.d.tspackages/ai-persistence-cloudflare/src/assets/0000_tanstack_ai_initial.sqlpackages/ai-persistence-cloudflare/src/bindings.tspackages/ai-persistence-cloudflare/src/cli.tspackages/ai-persistence-cloudflare/src/d1.tspackages/ai-persistence-cloudflare/src/index.tspackages/ai-persistence-cloudflare/src/locks.tspackages/ai-persistence-cloudflare/src/migration-cli.tspackages/ai-persistence-cloudflare/src/migrations.tspackages/ai-persistence-cloudflare/tests/api-types.test-d.tspackages/ai-persistence-cloudflare/tests/locks.test.tspackages/ai-persistence-cloudflare/tests/migration-cli.test.tspackages/ai-persistence-cloudflare/tests/migrations.test.tspackages/ai-persistence-cloudflare/tests/package-contract.test.tspackages/ai-persistence-cloudflare/tests/runtime.conformance.test.tspackages/ai-persistence-cloudflare/tsconfig.jsonpackages/ai-persistence-cloudflare/vite.config.tspackages/ai-persistence-drizzle/bin/tanstack-ai-drizzle-migrations.mjspackages/ai-persistence-drizzle/bin/tanstack-ai-drizzle-schema.mjspackages/ai-persistence-drizzle/drizzle.config.tspackages/ai-persistence-drizzle/drizzle/0000_tanstack_ai_initial.sqlpackages/ai-persistence-drizzle/drizzle/meta/0000_snapshot.jsonpackages/ai-persistence-drizzle/drizzle/meta/_journal.jsonpackages/ai-persistence-drizzle/package.jsonpackages/ai-persistence-drizzle/src/assets.d.tspackages/ai-persistence-drizzle/src/assets/0000_tanstack_ai_initial.sqlpackages/ai-persistence-drizzle/src/assets/tanstack-ai-schema.tspackages/ai-persistence-drizzle/src/cli.tspackages/ai-persistence-drizzle/src/index.tspackages/ai-persistence-drizzle/src/migration-cli.tspackages/ai-persistence-drizzle/src/migrations.tspackages/ai-persistence-drizzle/src/schema-cli-main.tspackages/ai-persistence-drizzle/src/schema-cli.tspackages/ai-persistence-drizzle/src/schema-contract.tspackages/ai-persistence-drizzle/src/schema-source.tspackages/ai-persistence-drizzle/src/schema.tspackages/ai-persistence-drizzle/src/sqlite-migrations.tspackages/ai-persistence-drizzle/src/sqlite.tspackages/ai-persistence-drizzle/src/stores.tspackages/ai-persistence-drizzle/tests/api-types.test-d.tspackages/ai-persistence-drizzle/tests/custom-schema.test.tspackages/ai-persistence-drizzle/tests/drizzle.conformance.test.tspackages/ai-persistence-drizzle/tests/migration-cli.test.tspackages/ai-persistence-drizzle/tests/migrations.test.tspackages/ai-persistence-drizzle/tests/package-contract.test.tspackages/ai-persistence-drizzle/tests/schema-cli.test.tspackages/ai-persistence-drizzle/tests/schema-source.test.tspackages/ai-persistence-drizzle/tests/sqlite.test.tspackages/ai-persistence-drizzle/tests/store-behavior.test.tspackages/ai-persistence-drizzle/tests/variant-schema.tspackages/ai-persistence-drizzle/tsconfig.jsonpackages/ai-persistence-drizzle/vite.config.tspackages/ai-persistence-prisma/.gitignorepackages/ai-persistence-prisma/bin/tanstack-ai-prisma-models.mjspackages/ai-persistence-prisma/package.jsonpackages/ai-persistence-prisma/prisma/schema.prismapackages/ai-persistence-prisma/prisma/tanstack-ai.prismapackages/ai-persistence-prisma/src/assets.d.tspackages/ai-persistence-prisma/src/assets/tanstack-ai.prismapackages/ai-persistence-prisma/src/cli.tspackages/ai-persistence-prisma/src/index.tspackages/ai-persistence-prisma/src/model-contract.tspackages/ai-persistence-prisma/src/models-cli.tspackages/ai-persistence-prisma/src/models.tspackages/ai-persistence-prisma/src/stores.tspackages/ai-persistence-prisma/tests/api-types.test-d.tspackages/ai-persistence-prisma/tests/model-mapping.test.tspackages/ai-persistence-prisma/tests/models-cli.test.tspackages/ai-persistence-prisma/tests/models.test.tspackages/ai-persistence-prisma/tests/package-contract.test.tspackages/ai-persistence-prisma/tests/prisma.conformance.test.tspackages/ai-persistence-prisma/tests/store-behavior.test.tspackages/ai-persistence-prisma/tsconfig.jsonpackages/ai-persistence-prisma/vite.config.tspackages/ai-persistence/package.jsonpackages/ai-persistence/src/capabilities.tspackages/ai-persistence/src/index.tspackages/ai-persistence/src/interrupts.tspackages/ai-persistence/src/locks.tspackages/ai-persistence/src/memory.tspackages/ai-persistence/src/middleware.tspackages/ai-persistence/src/testkit/conformance.tspackages/ai-persistence/src/types.tspackages/ai-persistence/tests/capabilities.test.tspackages/ai-persistence/tests/error-abort.test.tspackages/ai-persistence/tests/interrupts.test.tspackages/ai-persistence/tests/memory.conformance.test.tspackages/ai-persistence/tests/memory.test.tspackages/ai-persistence/tests/persistence-composition.test.tspackages/ai-persistence/tests/persistence-fixtures.tspackages/ai-persistence/tests/persistence-types.test-d.tspackages/ai-persistence/tests/persistence-validation.test.tspackages/ai-persistence/tests/state-only.test.tspackages/ai-persistence/tests/with-persistence.test.tspackages/ai-persistence/tsconfig.jsonpackages/ai-persistence/vite.config.tspackages/ai/skills/ai-core/chat-experience/SKILL.mdpackages/ai/skills/ai-core/middleware/SKILL.mdpnpm-workspace.yamltesting/e2e/src/routeTree.gen.tstesting/e2e/src/routes/api.persistence-durability.tstesting/e2e/src/routes/persistence-durability.tsxtesting/e2e/tests/persistence-durability.spec.ts
| * 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. |
There was a problem hiding this comment.
🎯 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
There was a problem hiding this comment.
Acknowledged — mid-stream reload/joinRun is intentionally out of this E2E file (see the header comment in persistence-durability.spec.ts).
What is covered today:
- Unit (
@tanstack/ai-client): combined{ messages, resume }record, async hydrate rejoin,resumeInFlightRun/applyResumeSnapshot, and the explicit-undefinedjoinRunguard on the connection adapter. - E2E: transcript restore after clean finish, and interrupt survival across reload (local resume snapshot + server-authoritative reconstruction).
A deterministic mid-stream pause + reload harness needs a fixture that freezes the stream mid-token, reloads, asserts the client issues the resume/join request, and checks single continuation — that is real work (aimock fixture + transport coordination) and is tracked as follow-up rather than blocking this PR. The durable-stream e2e (e87998db8) already guards full agent-loop delivery through durability-backed streams; the missing piece is specifically the browser reload while streaming path.
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo '--- current persistence durability spec header and test names ---'
sed -n '1,140p' testing/e2e/tests/persistence-durability.spec.ts
echo '--- client unit-test references to in-flight rejoin ---'
rg -n -C 4 'resumeInFlightRun|joinRun|in-flight|rejoin' packages/ai-client/tests packages/ai-client/src
echo '--- referenced commit metadata and changed paths (if available) ---'
git show --no-patch --format='%H%n%s%n%b' e87998db8 2>&1 || true
git diff-tree --no-commit-id --name-status -r e87998db8 2>&1 || trueLength of output: 99489
@tombeckenham, verified. The unit suite covers persisted and async rejoin paths, including duplicate-message handling, and e87998db8 covers durable agent-loop delivery plus a second-tab join.
The missing coverage is specifically the composed browser scenario—page.reload() during an active run—which is not exercised by either suite. Given the focused coverage already present and the fixture work required, I’m okay treating it as non-blocking follow-up rather than requiring it in this PR.
Would you like me to open a GitHub issue for the deterministic mid-stream reload/rejoin E2E scenario, with the acceptance criteria from the original finding?
🧠 Learnings used
Learnt from: season179
Repo: TanStack/ai PR: 734
File: testing/e2e/tests/workerd-console-logger.spec.ts:57-100
Timestamp: 2026-06-10T00:45:26.781Z
Learning: In `testing/e2e/**/*.spec.ts`, apply the “Playwright + aimock” policy for E2E features/fixes that exercise LLM provider HTTP calls: wire aimock to mock LLM provider responses instead of hitting the provider over the network. Exception: if the E2E spec only tests runtime/infrastructure behavior (e.g., rendering/logging inside real `workerd` via Miniflare) and the code path under test never reaches the provider HTTP layer, do not add aimock wiring. For these exempt specs, document the policy exception in the spec’s header comment (i.e., explain why the test does not reach the LLM provider HTTP layer).
There was a problem hiding this comment.
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 winFix the malformed import guidance sentence.
“the one exception to mistake
jbelow” 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 winUse a Date-aware deserializer.
UIMessage.createdAtis aDate, but this example usesJSON.stringify/JSON.parse, so the timestamp is restored as a string on reload and does not preserve the documentedChatPersistedStateshape. 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 winMake the localStorage codec Date-aware.
UIMessage.createdAtis typed asDate, soJSON.stringifystores it as a string andJSON.parsereturns that string instead of aDate; theuseChattranscript is then restored with mutated timestamps. Use a Date-aware validated codec here, or switch this sample toindexedDBPersistencesince 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
📒 Files selected for processing (13)
.changeset/client-browser-refresh-durability.mddocs/chat/persistence.mddocs/config.jsondocs/persistence/browser-refresh.mddocs/persistence/controls.mdexamples/ts-react-chat/src/routes/api.persistent-chat.tsexamples/ts-react-chat/src/routes/persistent-chat.tsxpackages/ai-client/src/chat-client.tspackages/ai-client/src/client-persistor.tspackages/ai-client/src/index.tspackages/ai-client/src/types.tspackages/ai-client/tests/resume-snapshot.test.tspackages/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
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
docs/config.jsondocs/persistence/overview.mddocs/resumable-streams/overview.md
There was a problem hiding this comment.
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 winUse
threadIdconsistently as the persistence key.This section alternates between chat
id,threadId, and stableid. Align the prose and framework example onthreadId, and documentidonly 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 liftHandle existing IndexedDB databases whose configured object store is missing.
factory.open(databaseName)without a version only raisesonupgradeneededfor a new database; an existing database opened at its current version will create no object store in this branch. IfobjectStoreNamewas 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
📒 Files selected for processing (22)
.changeset/client-browser-refresh-durability.md.changeset/persistence-packages.mddocs/chat/persistence.mddocs/persistence/browser-refresh.mddocs/persistence/overview.mdexamples/ts-react-chat/src/routes/api.persistent-chat.tsexamples/ts-react-chat/src/routes/persistent-chat.tsxpackages/ai-angular/src/index.tspackages/ai-client/src/chat-client.tspackages/ai-client/src/index.tspackages/ai-client/src/storage-adapters.tspackages/ai-client/src/types.tspackages/ai-client/tests/resume-snapshot.test.tspackages/ai-persistence/src/index.tspackages/ai-persistence/src/reconstruct.tspackages/ai-preact/src/index.tspackages/ai-react/src/index.tspackages/ai-solid/src/index.tspackages/ai-svelte/src/index.tspackages/ai-vue/src/index.tspackages/ai/skills/ai-core/chat-experience/SKILL.mdtesting/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
There was a problem hiding this comment.
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
📒 Files selected for processing (11)
docs/chat/persistence.mddocs/config.jsondocs/persistence/chat-persistence.mddocs/persistence/client-persistence.mddocs/persistence/controls.mddocs/persistence/internals.mddocs/persistence/overview.mddocs/resumable-streams/advanced.mdpackages/ai-persistence/src/reconstruct.tspackages/ai/skills/ai-core/chat-experience/SKILL.mdpackages/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
|
|
||
| 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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
Agreed — most apps fold chat into the existing customer DB rather than standing up a separate store.
That is now spelled out in docs/persistence/custom-stores.md under When each store is called:
- Chat middleware lifecycle table — which store methods fire on
onConfig/onStart/ streamonChunk/ interrupt boundary /onFinish/onError/onAbort, and hard-fail vs best-effort. - Swimlanes (request → store) — ASCII flow from client POST through load/createOrResume/listPending → saveThread/update/commit.
- Invariants — full-overwrite
saveThread, idempotentcreateOrResume, insert-if-absent interrupts, authorize-at-boundary, locks viawithLocksnot the state bag.
If anything is still thin for the integrate-with-my-tables path (e.g. mapping each method onto a concrete threads/messages/approvals schema, or transaction boundaries across stores), say which event you want expanded and we can add a worked example in a follow-up.
…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
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
…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.
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.
@tanstack/ai
@tanstack/ai-acp
@tanstack/ai-angular
@tanstack/ai-anthropic
@tanstack/ai-bedrock
@tanstack/ai-claude-code
@tanstack/ai-client
@tanstack/ai-code-mode
@tanstack/ai-code-mode-skills
@tanstack/ai-codex
@tanstack/ai-devtools-core
@tanstack/ai-durable-stream
@tanstack/ai-elevenlabs
@tanstack/ai-event-client
@tanstack/ai-fal
@tanstack/ai-gemini
@tanstack/ai-grok
@tanstack/ai-grok-build
@tanstack/ai-groq
@tanstack/ai-isolate-cloudflare
@tanstack/ai-isolate-node
@tanstack/ai-isolate-quickjs
@tanstack/ai-mcp
@tanstack/ai-memory
@tanstack/ai-mistral
@tanstack/ai-ollama
@tanstack/ai-openai
@tanstack/ai-opencode
@tanstack/ai-openrouter
@tanstack/ai-persistence
@tanstack/ai-persistence-cloudflare
@tanstack/ai-persistence-drizzle
@tanstack/ai-persistence-prisma
@tanstack/ai-preact
@tanstack/ai-react
@tanstack/ai-react-ui
@tanstack/ai-sandbox
@tanstack/ai-sandbox-cloudflare
@tanstack/ai-sandbox-daytona
@tanstack/ai-sandbox-docker
@tanstack/ai-sandbox-local-process
@tanstack/ai-sandbox-sprites
@tanstack/ai-sandbox-vercel
@tanstack/ai-solid
@tanstack/ai-solid-ui
@tanstack/ai-svelte
@tanstack/ai-utils
@tanstack/ai-vue
@tanstack/ai-vue-ui
@tanstack/openai-base
@tanstack/preact-ai-devtools
@tanstack/react-ai-devtools
@tanstack/solid-ai-devtools
commit: |
Product docs should describe the API, not merge history.
|
|
||
| # Persistence with Drizzle | ||
|
|
||
| `@tanstack/ai-persistence-drizzle` supports SQLite-family Drizzle databases. |
There was a problem hiding this comment.
Drizzle should support every type of database shouldn't it?
There was a problem hiding this comment.
Not every Drizzle dialect is first-class in this package (yet).
Shipped providers:
provider: 'sqlite'— including the Nodenode:sqliteconvenience factory and D1 (via the Cloudflare package, which delegates to the drizzle SQLite path).provider: 'pg'— BYO Postgres Drizzle DB (node-postgres, postgres.js, Neon, etc.) +/pg-schema.
MySQL / libsql / other Drizzle dialects are not typed or schema-codegen’d here. The runtime needs dialect-specific table definitions and type maps; we intentionally narrowed to the two backends we test and document rather than claiming “any Drizzle DB.” Adding another dialect is mostly schema + provider discriminant + conformance — happy to take MySQL as a follow-up if that is the missing one for you.
…esumeSnapshot The resume pointer is per-browser, so a fresh client (the same thread opened on a second device / another browser) had no pointer and stopped at the hydrated snapshot even while the run was still generating. initialResumeSnapshot previously restored only interrupts; now a snapshot carrying a bare in-flight run (resumeState.runId, no pending interrupts) is rejoined too, exactly like a persisted pointer - reusing the existing drop-and-rebuild / fast-fail rejoin machinery. A run named by the persisted store still wins. The persistent-chat example wires the server side: an in-process active-run registry (keyed by runId, reverse-looked-up by thread) lets the history loader report the thread's activeRunId, which the page hands to useChat as initialResumeSnapshot. Verified in a real browser - a fresh context with cleared localStorage tailed a live run to completion in one clean bubble. Covered by an @tanstack/ai-client unit test (initialResumeSnapshot -> joinRun), consistent with the repo convention of covering rejoin/in-flight paths at the client-unit + transport layers rather than e2e (see persistence-durability.spec header). Docs: client-persistence gains a "Tail an in-flight run on a fresh client" section (both server and client snippets).
…er, schema subpaths (#994) * feat(ai-persistence-drizzle): schema-first ownership, drop shipped migrations Stop publishing SQL migrations and a default bundled schema from the drizzle adapter. Apps own DDL via tanstack-ai-drizzle-schema + their drizzle-kit journal; drizzlePersistence requires an injected schema. sqlitePersistence keeps local defaults via createDefaultSqliteSchema and optional runtime CREATE TABLE IF NOT EXISTS bootstrap. * feat(ai-persistence-drizzle): Postgres provider, schema subpaths, interrupt indexes - drizzlePersistence(db, { provider: 'sqlite' | 'pg', schema }): one entry, overload-discriminated so db/provider/schema must agree at compile time, with a runtime dialect guard (is PgDatabase / BaseSQLiteDatabase) and a provider-aware schema assertion - Dialect-neutral contract: TanstackAiTableShapes single source of truth, projected as TanstackAiSqliteSchema and TanstackAiPgSchema - Postgres: createDefaultPgSchema (jsonb/bigint), ensurePgTables bootstrap, pg starter asset via tanstack-ai-drizzle-schema --dialect pg, and a conformance suite against real Postgres (pglite) - Schema subpath exports /sqlite-schema and /pg-schema so stock-table users re-export one line into their drizzle-kit paths instead of copying a file - Default lookup indexes on interrupts (thread_id, run_id) across defaults, starters, and the ensure* bootstraps - Stores stay a single SQLite-typed implementation; the pg pair is re-faced at one documented seam in drizzlePersistence - Docs (drizzle/sql-backends/migrations), middleware skill, changeset, and Cloudflare D1 call site updated * fix(stack): resolve CI type failures and dts-scan false positive - ai-persistence: TEXT_MESSAGE_START event literal now carries the required ag-ui `role` field - ai-client resume-snapshot tests: construct the joinRun:undefined adapter without an invalid cast; remove a dead never-typed else-branch assertion - ts-react-chat: project hydrated history onto an explicit JSON-safe wire shape (server fns require serializable payloads; UIMessage part metadata / tool-call input/output are typed unknown/any); default zod-defaulted tool inputs at the destructure - docs: mark the router-loader resume snippet as a schematic fence for kiira - scan-dangling-dts: strip comments so JSDoc @example imports (rewritten to .js by declaration emit) are not scanned as real imports
…inter, no app glue)
Reconnect is now resolved by the server from the STABLE thread id, and the client
hydrates itself on mount — no loader, no initialMessages, no initialResumeSnapshot,
no app-side fetching. Run ids never leave the server (a single turn can span
several runs, so a client-cached run id goes stale).
- RunStore.findActiveRun(threadId): new optional, feature-detected store method
returning the most recent 'running' run for a thread. Implemented for memory,
drizzle/SQLite (also covers Cloudflare D1), and prisma; conformance test added.
- reconstructChat now returns { messages, activeRun } (was a bare array): the
transcript as UI messages plus a cursor to an in-flight run. It reads the active
run BEFORE the transcript, so "no active run" guarantees the transcript is final
(closes a finish-window race that otherwise stuck the UI on a stale snapshot).
- @tanstack/ai-client: connection adapters gain hydrate(threadId) (a JSON GET);
in messages:false mode ChatClient auto-hydrates on mount — paints the transcript
and tails any in-flight run via the existing joinRun replay. The client-cached
bare-run pointer is no longer written/read in messages:false (interrupts, which
are run-scoped, are unchanged in both modes). Reload and a fresh device are the
identical server-resolved path.
- example: deletes the in-memory registry, the loader, and the initialResumeSnapshot
wiring; the GET endpoint stays (durability replay when a resume cursor is present,
else reconstructChat). Keeps only a local producer-dedup Set.
Verified in a real browser on a clean DB: mount hydrates the transcript with zero
localStorage; a mid-run reload tails live to completion (UI length matches the
server exactly, one clean bubble); findActiveRun is populated during a run and null
after. Unit + conformance suites green (ai-persistence 81, drizzle 37, prisma 32,
ai-client 536).
…ocs refresh
- persistent-chat is now multi-thread: a sidebar lists conversations, "+ New
chat" starts one, and clicking a thread switches to it. Each thread is keyed
by id, so switching remounts the pane and useChat re-hydrates that thread from
the server (transcript + any in-flight run). Threads auto-title from their
first user message. The thread index lives in localStorage; the transcripts
stay server-authoritative, one per thread id.
- Full-screen layout: the page fills the viewport below the app header instead
of a centered card (no max-width, margin, border, radius, or shadow).
- Docs refreshed for the automatic server-authoritative reconnect: client
persistence and the persistence overview no longer describe a router loader,
seeded initialMessages, or initialResumeSnapshot. They now show useChat
hydrating a thread from the GET endpoint by threadId on mount, and document
reconstructChat's { messages, activeRun } shape. Resumable-streams and
chat-persistence were reviewed and remain accurate.
Verified in a real browser: create/switch threads, per-thread server-hydrated
resume on switch and reload, auto-titling, and the full-screen layout (fills
width, sits below the 72px header, no scroll). Docs link check + kiira snippet
typecheck both pass.
…first tool call A tool-calling run emits a RUN_STARTED/RUN_FINISHED pair per iteration (finishReason "tool_calls" for a tool turn, then "stop" for the answer). memoryStream treated the first terminal chunk as the end — marking the log complete on append and returning from read on it — so a run that called a tool was delivered only up to that first RUN_FINISHED. The tool result and the final reply never reached the client: the tool call stayed stuck "running" and the answer was missing, on the initial stream and on reconnect/reload. Completion is now driven solely by the producer's close() (called on every exit per the StreamDurability.close contract, honored by toServerSentEventsResponse/resumeServerSentEventsResponse and detached producers). read() tails across per-iteration terminals and ends on close, so a tool-calling run is delivered in full. Verified in a real browser: "write a story and roll a dice" now renders the tool call, its result, and the story on the initial send and re-hydrates the same on reload (previously truncated at the dice roll). New unit test covers a two-iteration run delivering both terminals through to close; full @tanstack/ai suite (1287) green.
The persistent-chat run now requests reasoning (modelOptions.reasoning, effort "low" to stay fast, summary "auto"), and the pane renders assistant `thinking` parts as a "reasoning" block — persisted and reconstructed on reload like text and tool calls. Verified: effort "low" is honored (usage reasoningTokens drops from ~104 to ~11), and the tool-call + text flow is unaffected. When OpenAI returns a reasoning summary it streams as REASONING events and renders. Note: OpenAI only returns reasoning summary TEXT to organizations verified for it; an unverified key still reasons (reasoning tokens appear in usage) but returns no summary to display.
Adds a `?scenario=agent-loop` variant to the durable-delivery harness that emits a tool-calling run with a RUN_FINISHED per iteration (finishReason tool_calls then stop), plus two specs asserting the durable stream delivers the tool result and the second iteration that follow the FIRST terminal — on the initial produce and on a second-tab join. Regression guard for the memoryStream fix (a sink that ended the log on the first terminal stranded tool-calling runs).
… chat Adds a `sendEmail` tool with `needsApproval: true` (shared isomorphic definition in lib/persistent-chat-tools so the server implements it and the client binds the approval). The run pauses on an interrupt; the pane renders an approve/reject box; the server forwards `resume`/`parentRunId` into the detached chat() so the paused call continues on approval. withPersistence persists the interrupt, so it is durable: verified in a browser that a reload mid-approval restores the pending decision, approving then resumes (tool runs, model finishes), and a later reload reconstructs the full tool-call + result + reply.
…eadId is the chat hook identity
Interrupt reconstruction:
- reconstructChat now returns { messages, activeRun, interrupts }; pending
approvals resolve from stores.interrupts.listPending and restore on the client
via the connection hydrate() path, so a reload (or another device) re-prompts
the approve/reject decision from the server, not from client storage.
threadId is the chat hook identity:
- useChat/createChat drop the `id` option across react, preact, solid, vue, and
svelte; a hook's identity (and persistence key) is its threadId.
- ChatClient.uniqueId falls back to threadId; the hooks stop overriding the
persistence key with a useId(), so a reload with the same threadId restores
the conversation. Changing threadId now recreates the client so it takes effect.
Adds the server-interrupt e2e scenario and updates examples/e2e call sites.
…ovalResponse Use the bound `interrupts` API (resolveInterrupt) for tool approvals instead of the deprecated `addToolApprovalResponse` / `part.approval` pattern. - threads.tsx: render approvals from the `interrupts` array; drop the addToolApprovalResponse threading and the per-part approval block. - docs/tools/tool-approval.md: replace the deprecated Approval UI, Generic Approval Handlers, and compatibility sections with an interrupt-based Approval UI and a short "Migrating from addToolApprovalResponse" pointer. - docs/getting-started/devtools.md: useChat id -> threadId.
Two minimal TanStack Start apps replicating the ts-react-chat persistent-chat demo (server-authoritative persistence, streaming, tool calls, durable approval interrupts) backed by real database migrations instead of the runtime table bootstrap. - persistent-chat-drizzle: @tanstack/ai-persistence-drizzle over SQLite. Owns an emitted schema, generates committed drizzle-kit SQL, and applies it with a node:sqlite migrate script (no native driver); runtime uses ensureTables:false. - persistent-chat-prisma: @tanstack/ai-persistence-prisma on Prisma 7 over SQLite. prisma.config.ts + prisma-client generator (ESM output) + copied models fragment + native migrations + the better-sqlite3 driver adapter. Both verified end to end (send -> tool call -> approval interrupt -> reload reconstructs from the DB -> approve -> resume), typecheck, and build. Workspace: allow the better-sqlite3 native build; sherif ignores prisma / @prisma/client (the app uses v7 while the package stays on v6). Docs: point the drizzle and prisma persistence guides at the runnable examples.
| runs?: RunStore | ||
| interrupts?: InterruptStore | ||
| metadata?: MetadataStore | ||
| locks?: LockStore |
There was a problem hiding this comment.
This feels off. Locks is not really chat state. It shoudn't be on this type
There was a problem hiding this comment.
It also feels off that all other fields are optional. Looking further
There was a problem hiding this comment.
Agreed — and this is already landed on the branch (see 43cd409d5 and the current packages/ai-persistence/src/types.ts).
AIPersistenceStores/ named chat shapes hold only state stores (messages/runs/interrupts/metadata). Locks are not on these types.- Locks are a separate capability:
LockStore+withLocks(...)/LocksCapability(and the shared core token used by sandbox in the stacked PR). - Optional vs required: the sparse bag stays optional for composition, but the named product shapes require what they claim — e.g.
ChatTranscriptStores.messagesis required,ChatPersistenceStoresrequires all four state stores. Prefer those named types at call sites over the sparse bag.
On the “all other fields optional” feel: that was the sparse internal bag; packaged backends return ChatPersistenceStores (all present). Custom apps that only need a transcript use ChatTranscriptStores.
…hapes Locks are coordination, not chat state: remove them from AIPersistenceStores, add withLocks middleware, and keep Cloudflare DO locks as a separate export. Export ChatTranscript/ChatPersistence/ChatWithInterrupts shapes; stop re-exporting the sparse bag. Require messages for chat entrypoints and document that generation must not fake threadId = requestId.
…n pg schemas Restructure the Drizzle package into shared stores (core/), SQLite sources of truth (sqlite/), and Postgres projections (pg/). Generate PG default schemas and CLI assets from SQLite via a TypeScript codegen script so dialect table defs stay in lockstep without forking store logic.
Stop shipping SQL, the migrations CLI, and d1Migrations. D1 state stays a thin Drizzle wrapper; schema DDL follows the same schema-first path as SQLite (drizzle-kit + Wrangler migrations_dir). Keep Durable Object locks and the cloudflarePersistence / createD1Stores convenience API.
Add agent skills covering server/client state persistence, packaged backends, custom stores, and locks, and route them from ai-core, chat-experience, and middleware skills.
tombeckenham
left a comment
There was a problem hiding this comment.
I'm happy with this now. I've reworked drizzle a bit. I've added skills and I'm happy with the type structure.
Chat client identity is threadId; the smoke App still passed id, which fails test:pr typecheck after the persistence identity rename.
- Regenerate drizzle pg schema artifacts so codegen:pg --check is clean - Default DATABASE_URL in persistent-chat-prisma prisma.config.ts so `prisma generate` works without a local .env (CI / clean checkouts)

Summary
Lands the persistence story on top of the shipped ephemeral interrupts (#970), resumable streams (#955), and the shared
Scopetype (#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/withGenerationPersistencemiddleware,memoryPersistencereference store, and a conformance testkit.@tanstack/ai-persistence-drizzle— Drizzle stores (SQLite vianode: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
persistenceoption 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 viajoinRunon a durability-backed connection. AddslocalStoragePersistence/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-chatroute (SQLite server + localStorage client) to test reload durability end to end.docs/persistence/section + updated client persistence page.middleware+chat-experienceagent skills.Reconciliation decisions (vs #785)
@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.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.persistenceoption (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-clientincl. new combined-record + auto-rejoin tests), andbuildfor 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
buildfor ai-client/frameworks/example and the full Playwrighttest:e2erun. 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
Documentation