feat(persistence): server persistence + client browser-refresh durability#984
feat(persistence): server persistence + client browser-refresh durability#984AlemTuzlak wants to merge 23 commits into
Conversation
…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
|
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 Preview14 package(s) bumped directly, 37 bumped as dependents. 🟥 Major bumps
🟨 Minor bumps
🟩 Patch bumps
|
|
| 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
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
| // 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, | ||
| ), | ||
| } | ||
| : {}), |
There was a problem hiding this comment.
🎯 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.
| "@tanstack/ai": "workspace:^", | ||
| "@tanstack/ai-persistence": "workspace:^", |
There was a problem hiding this comment.
📐 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.
| "@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
| /** 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 | ||
| } |
There was a problem hiding this comment.
🩺 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:
- 1: https://nodejs.org/api/sqlite.html
- 2: http://nodejs.org/dist/latest/docs/api/sqlite.html
- 3: https://nodejs.org/dist/latest-v24.x/docs/api/sqlite.html
- 4: sqlite: mark as release candidate nodejs/node#61262
🏁 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 . || trueRepository: 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
doneRepository: 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'], |
There was a problem hiding this comment.
📐 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
| 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, | ||
| ), | ||
| ) |
There was a problem hiding this comment.
🩺 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.
| 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.
| 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', | ||
| ), | ||
| ) | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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}`) |
There was a problem hiding this comment.
🗄️ 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.
| * 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
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
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
|
|
||
| | 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 | |
There was a problem hiding this comment.
📐 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.
| const store = localStoragePersistence<ChatPersistedState>({ | ||
| serialize: (value) => JSON.stringify(value), | ||
| deserialize: (value) => JSON.parse(value), | ||
| }) |
There was a problem hiding this comment.
🩺 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
…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.
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
| threadId: 'support-chat', | ||
| connection: fetchServerSentEvents('/api/chat'), | ||
| persistence: localStoragePersistence(), |
There was a problem hiding this comment.
🔒 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.
| * ```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), { |
There was a problem hiding this comment.
🔒 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 || trueRepository: 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.
| const messages = | ||
| threadId && persistence.stores.messages | ||
| ? await persistence.stores.messages.loadThread(threadId) | ||
| : [] | ||
| return new Response(JSON.stringify(messages), { |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== locate files =="
git ls-files | rg 'packages/ai-persistence/src/reconstruct\.ts|packages/.*\.(ts|tsx|js)$|types|persist|message' || true
echo "== reconstruct.ts =="
if [ -f packages/ai-persistence/src/reconstruct.ts ]; then
nl -ba packages/ai-persistence/src/reconstruct.ts | sed -n '1,120p'
else
echo "missing packages/ai-persistence/src/reconstruct.ts"
fi
echo "== search relevant symbols =="
rg -n "loadThread|ChatPersistedState|UIMessage|ModelMessage|reconstruct|messages" packages -S || trueRepository: TanStack/ai
Length of output: 50369
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== reconstruct.ts =="
if [ -f packages/ai-persistence/src/reconstruct.ts ]; then
nl -ba packages/ai-persistence/src/reconstruct.ts | sed -n '1,120p'
else
echo "missing packages/ai-persistence/src/reconstruct.ts"
fi
echo "== persistence types =="
if [ -f packages/ai-persistence/src/types.ts ]; then
nl -ba packages/ai-persistence/src/types.ts | sed -n '1,220p'
else
echo "missing packages/ai-persistence/src/types.ts"
fi
echo "== relevant symbol definitions =="
rg -n "interface .*ModelMessage|type .*ModelMessage|interface ChatPersistedState|type ChatPersistedState|interface UIMessage|type UIMessage|function loadThread|loadThread:" packages/ai-persistence/src packages -S || true
echo "== usages of /api and reconstruct =="
rg -n "reconstruct|/api/persistence|persistence|loadThread|browser-refresh|ChatPersistedState" packages/ai-client packages/ai-react packages/ai-solid packages/ai-svelte packages/ai-preact packages packages docs -S --glob '!node_modules/**' --glob '!dist/**' | head -n 200 || trueRepository: TanStack/ai
Length of output: 210
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== reconstruct.ts =="
if [ -f packages/ai-persistence/src/reconstruct.ts ]; then
awk '{printf "%6d\t%s\n", NR, $0}' packages/ai-persistence/src/reconstruct.ts | sed -n '1,120p'
else
echo "missing packages/ai-persistence/src/reconstruct.ts"
fi
echo "== persistence types =="
if [ -f packages/ai-persistence/src/types.ts ]; then
awk '{printf "%6d\t%s\n", NR, $0}' packages/ai-persistence/src/types.ts | sed -n '1,240p'
else
echo "missing packages/ ai-persistence/src/types.ts"
fi
echo "== relevant symbol definitions =="
rg -n "interface .*ModelMessage|type .*ModelMessage|interface ChatPersistedState|type ChatPersistedState|interface UIMessage|type UIMessage|function loadThread|loadThread:" packages/ai-persistence/src packages -S || true
echo "== usages of /api and reconstruct =="
rg -n "reconstruct|/api/persistence|persistence|loadThread|browser-refresh|ChatPersistedState" packages/ai-client packages/ai-react packages/ai-solid packages/ai-svelte packages/ai-preact packages packages docs - S --glob '!node_modules/**' --glob '!dist/**' | head -n 200 || trueRepository: TanStack/ai
Length of output: 45307
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "== core types around ModelMessage/UIMessage/ContentPart =="
awk '{printf "%6d\t%s\n", NR, $0}' packages/ai/src/types.ts | sed -n '300,560p'
echo "== core message converter implementations/tests =="
rg -n "function [A-Za-z0-9_]*UIMessage|function [A-Za-z0-9_]*ModelMessage|convert.*Message|UIMessage|ModelMessage|ContentPart|parts|id:" packages/ai/src packages/ai/tests -S | head -n 220 || true
echo "== client persistor type/usages =="
awk '{printf "%6d\t%s\n", NR, $0}' packages/ai-client/src/client-persistor.ts | sed -n '1,180p'
awk '{printf "%6d\t%s\n", NR, $0}' packages/ai-client/src/types.ts | sed -n '530,630p'
echo "== persistence tests around hydrate =="
awk '{printf "%6d\t%s\n", NR, $0}' packages/ai-client/tests/chat-client.test.ts | sed -n '75,135p;2680,2688p'Repository: TanStack/ai
Length of output: 46503
🌐 Web query:
TanStack AI modelMessageToUIMessage @tanstack/ai``
💡 Result:
modelMessageToUIMessage is a function provided by the TanStack AI SDK (@tanstack/ai) used to convert a model-specific message (ModelMessage) into a UI-friendly message format (UIMessage) [1][2]. Key details about this function include: Functionality: It transforms the raw message structure received from LLM providers into a parts-based structure required for rendering in UI components [1][2]. Structure Conversion: Text Content: Converts standard string or null content into a TextPart [1][2]. Tool Calls: Maps tool calls into a series of ToolCallPart entries [1][2]. Role Mapping: Specifically handles messages with the role of tool by converting them into tool-result parts, ensuring they are correctly treated as results rather than standard assistant text [3][2]. Thinking: Preserves thinking blocks if they are present in the model message [3]. Parameters: modelMessage (ModelMessage): The raw message from the AI model to be converted [1][2]. id (string, optional): An optional unique identifier for the resulting UIMessage; if not provided, one is generated automatically [1][2]. Related Utilities: modelMessagesToUIMessages: A related function that converts an array of model messages into an array of UI messages, including logic to merge tool result messages with their corresponding assistant messages [4]. uiMessageToModelMessages: The inverse function that converts a UIMessage back into ModelMessage(s) while preserving the interleaving order of text, tool calls, and tool results, which is essential for multi-round tool interactions [5].
Citations:
- 1: https://tanstack.com/ai/latest/docs/reference/functions/modelMessageToUIMessage
- 2: https://github.com/TanStack/ai/blob/main/packages/typescript/ai/src/activities/chat/messages.ts
- 3: https://github.com/TanStack/ai/blob/main/packages/ai/src/activities/chat/messages.ts
- 4: https://tanstack.com/ai/latest/docs/reference/functions/modelMessagesToUIMessages
- 5: https://tanstack.com/ai/latest/docs/reference/functions/uiMessageToModelMessages
Convert stored messages before returning the hydration payload.
reconstructChat returns Array<ModelMessage> while the client hydration contract accepts ChatPersistedState<{ messages: Array<UIMessage> }> or a bare Array<UIMessage>. A persisted UIMessage with parts, meta, or a role mapping like tool → tool-result can be dropped or malformed after being stored as ModelMessage; return a typed response contract or convert with modelMessagesToUIMessages/a typed converter and cover it.
🤖 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 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.
| return new Response(JSON.stringify(messages), { | ||
| headers: { 'content-type': 'application/json' }, | ||
| }) |
There was a problem hiding this comment.
🔒 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 || trueRepository: 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:
- 1: https://nextjs.org/docs/app/guides/cdn-caching
- 2: https://nextjs.org/docs/app/guides/caching-without-cache-components
- 3: https://juanchi.dev/en/blog/nextjs-app-router-caching-revalidate-dynamic-no-store
- 4: https://wonsukchoi.co/en/blog/nextjs-app-router-caching
- 5: https://stackoverflow.com/questions/76228269/setting-cache-control-header-in-nextjs-app-router
- 6: https://devcheolu.com/en/posts/PDLUukMyNNYQ0WlXSiYC
- 7: https://nextjs.org/docs/app/getting-started/caching
🏁 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:
- 1: https://tanstack.com/ai/latest/docs/api/ai-react
- 2: https://tanstack.com/ai/latest/docs/reference/functions/chat
- 3: https://tanstack.com/ai/latest/docs/chat/persistence
- 4: https://tanstack.com/ai/latest/docs/api/ai-client
- 5: https://github.com/marufrahmanlive/tanstack-ai-chatbot
🌐 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:
- 1: https://tanstack.com/ai/latest
- 2: https://tanstack.com/ai/latest/docs/getting-started/overview
- 3: https://tanstack.com/ai/latest/docs/api/ai
- 4: https://tanstack.com/ai/latest/docs/api/ai-client
- 5: https://tanstack.com/ai/latest/docs/reference/functions/chat
- 6: https://tanstack.com/ai/latest/docs/chat/persistence
- 7: Persistence adapter for ChatClient #374
- 8: https://tanstack.com/ai/latest/docs/getting-started/quick-start
- 9: feat(ai-client): client-side chat persistence adapter #661
🌐 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:
- 1: fix: pass and use threadId #689
- 2: d5cb4b9
- 3: feat(ai-client): client-side chat persistence adapter #661
🌐 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:
- 1: https://tanstack.com/ai/latest/docs/protocol/chunk-definitions
- 2: https://tanstack.com/ai/latest
- 3: https://tanstack.com/ai/latest/docs/migration/ag-ui-compliance
- 4: feat: AG-UI client-to-server compliance #511
- 5: https://tanstack.com/ai/latest/docs/reference/functions/uiMessageToModelMessages
- 6: https://tanstack.com/ai/latest/docs/chat/persistence
- 7: https://tanstack.com/ai/latest/docs/advanced/observability
- 8: https://tanstack.com/ai/latest/docs/reference/classes/StreamProcessor
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.
| 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.
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
| 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). |
There was a problem hiding this comment.
📐 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
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.

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