Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
83c026a
feat(persistence): add ai-persistence + drizzle/prisma/cloudflare bac…
AlemTuzlak Jul 22, 2026
6165a30
feat(ai-client): browser-refresh durability via the persistence option
AlemTuzlak Jul 22, 2026
fd4742d
example(ts-react-chat): persistent chat with SQLite + reload durability
AlemTuzlak Jul 22, 2026
022efd1
docs(persistence): add persistence section for server + browser durab…
AlemTuzlak Jul 22, 2026
205e803
docs(skills): cover persistence in middleware + chat-experience skills
AlemTuzlak Jul 22, 2026
1a44001
test(e2e): persistence browser-refresh durability spec
AlemTuzlak Jul 22, 2026
5a187d2
ci: apply automated fixes
autofix-ci[bot] Jul 22, 2026
4937235
feat(ai-client): message-storage lever for the persistence option
AlemTuzlak Jul 23, 2026
2367ad2
docs(persistence): explain durability + persistence as one story
AlemTuzlak Jul 23, 2026
d18072b
feat(ai-client): zero-config storage adapters + threadId-keyed persis…
AlemTuzlak Jul 23, 2026
bdb112b
feat(ai-persistence): reconstructChat server helper
AlemTuzlak Jul 23, 2026
98e6878
docs(persistence): recommended setup, zero-config adapters, header-ac…
AlemTuzlak Jul 23, 2026
ac34676
ci: apply automated fixes
autofix-ci[bot] Jul 23, 2026
ad02850
docs(persistence): consolidate client persistence into one page
AlemTuzlak Jul 23, 2026
c11994b
docs(persistence): lead client-persistence with both use-cases
AlemTuzlak Jul 23, 2026
f004d46
fix(ai-client): keep host initialMessages in messages:false mode
AlemTuzlak Jul 23, 2026
7bc79fd
ci: apply automated fixes
autofix-ci[bot] Jul 23, 2026
428b4c3
feat(ai-persistence)!: rename withChatPersistence to withPersistence
AlemTuzlak Jul 23, 2026
1700669
ci: apply automated fixes
autofix-ci[bot] Jul 23, 2026
9eac120
fix(persistence): reconcile with current @tanstack/ai core
AlemTuzlak Jul 23, 2026
93f3a75
test(persistence): unskip two-phase approval->client-tool resume
AlemTuzlak Jul 23, 2026
26d6a36
docs(example): ts-react-chat persistent-chat uses recommended server-…
AlemTuzlak Jul 23, 2026
38d518f
feat(persistence-prisma): support Prisma 7 alongside Prisma 6
AlemTuzlak Jul 23, 2026
d55b1be
fix(persistence): persist the terminal assistant reply on finish
AlemTuzlak Jul 23, 2026
7bcf775
docs(example): add server tools and a styled UI to persistent-chat
AlemTuzlak Jul 23, 2026
9b66b1e
feat(persistence): seamless resume of a mid-stream reload
AlemTuzlak Jul 23, 2026
64c7bdd
fix(ai-react): don't abort an in-flight rejoin on mount
AlemTuzlak Jul 23, 2026
6f12ce3
fix(persistence): fast, robust, repeatable reload rejoin
AlemTuzlak Jul 23, 2026
d2d08e8
fix(persistence): review blockers and CodeRabbit follow-ups
tombeckenham Jul 24, 2026
f8594e3
ci: apply automated fixes
autofix-ci[bot] Jul 24, 2026
d64dfff
docs(persistence): drop PR number references from Scope docs
tombeckenham Jul 24, 2026
bd243e1
feat(ai-client): tail an in-flight run on a fresh client via initialR…
AlemTuzlak Jul 24, 2026
9426796
feat(ai-persistence-drizzle): schema-first ownership, Postgres provid…
tombeckenham Jul 24, 2026
6470807
feat(persistence): thread-keyed automatic reconnect (no client run po…
AlemTuzlak Jul 24, 2026
470c37f
example(ts-react-chat): multi-thread persistent chat + full-screen; d…
AlemTuzlak Jul 24, 2026
922eb02
fix(ai): memoryStream delivers the full agent-loop run, not just the …
AlemTuzlak Jul 24, 2026
c2c96b9
example(ts-react-chat): show reasoning in persistent chat (effort low)
AlemTuzlak Jul 24, 2026
e87998d
test(e2e): guard full agent-loop delivery through durable streams
AlemTuzlak Jul 24, 2026
83f3c5c
example(ts-react-chat): durable tool-approval interrupt in persistent…
AlemTuzlak Jul 24, 2026
0a3be05
feat(persistence): server-authoritative interrupt reconstruction; thr…
AlemTuzlak Jul 24, 2026
3f2c9fb
refactor(examples,docs): migrate approvals off deprecated addToolAppr…
AlemTuzlak Jul 24, 2026
f3c4ff1
feat(examples): persistent-chat-drizzle and persistent-chat-prisma
AlemTuzlak Jul 24, 2026
43cd409
refactor(persistence): separate locks from state; export named chat s…
tombeckenham Jul 25, 2026
e98d985
ci: apply automated fixes
autofix-ci[bot] Jul 25, 2026
b0eb5b5
refactor(ai-persistence-drizzle): split core/sqlite/pg layout; codege…
tombeckenham Jul 25, 2026
6a97cb4
ci: apply automated fixes
autofix-ci[bot] Jul 25, 2026
14f6cfa
refactor(ai-persistence-cloudflare): drop package-owned D1 migrations
tombeckenham Jul 25, 2026
d8da78f
ci: apply automated fixes
autofix-ci[bot] Jul 25, 2026
b1b92ea
docs(skills): add ai-core persistence skill suite
tombeckenham Jul 25, 2026
622204b
ci: apply automated fixes
autofix-ci[bot] Jul 25, 2026
3efe0c1
fix(react-native-smoke): use threadId instead of removed useChat id
tombeckenham Jul 25, 2026
0de85e4
fix(persistence): unbreak CI codegen check and prisma example generate
tombeckenham Jul 25, 2026
715bc2c
ci: apply automated fixes
autofix-ci[bot] Jul 25, 2026
c5f391e
docs(persistence): replace per-backend pages with a build-your-own-ad…
jherr Jul 24, 2026
7cd50ec
docs(skills): move persistence skills into @tanstack/ai-persistence
tombeckenham Jul 26, 2026
62c9975
refactor(persistence)!: ship the contract, not the backends
tombeckenham Jul 26, 2026
062f7c6
docs(persistence): make the adapter skills write the app's own file
tombeckenham Jul 26, 2026
a73a1d5
ci: apply automated fixes
autofix-ci[bot] Jul 26, 2026
d96ea0e
fix(e2e,skills): unbreak persisted-reload specs and skill discovery
tombeckenham Jul 26, 2026
b2dbef5
ci: apply automated fixes
autofix-ci[bot] Jul 26, 2026
cd8b731
refactor: move locks to @tanstack/ai; nest ai-persistence skills
tombeckenham Jul 27, 2026
6b8a3a6
ci: apply automated fixes
autofix-ci[bot] Jul 27, 2026
8926064
refactor(skills): move locks skill to @tanstack/ai as ai-core/locks
tombeckenham Jul 27, 2026
d020992
ci: apply automated fixes
autofix-ci[bot] Jul 27, 2026
e82349b
refactor(ai): expose locks via the @tanstack/ai/locks subpath
AlemTuzlak Jul 27, 2026
fc99c4e
docs(persistence): type-check the build-your-own-adapter samples
AlemTuzlak Jul 27, 2026
5880123
feat(ai-persistence): add define*Store typer helpers
AlemTuzlak Jul 27, 2026
cbf654b
docs(persistence): stop framing locks by what they are not
AlemTuzlak Jul 27, 2026
2902216
feat(ai): add defineLock; show lock impl + record shapes in docs
AlemTuzlak Jul 27, 2026
9f43c4b
docs(persistence): use async store methods in the adapter guide
AlemTuzlak Jul 27, 2026
8a89037
feat(ai-client): server-authoritative persistence via `persistence: t…
AlemTuzlak Jul 27, 2026
e00a2f5
docs: correct client-persistence framing in persistence overview
AlemTuzlak Jul 27, 2026
b5020e4
docs: lead the persistence overview client setup with persistence: true
AlemTuzlak Jul 27, 2026
41f7d52
fix(example): implement RunStore.findActiveRun so server-authoritativ…
AlemTuzlak Jul 27, 2026
2020ee2
ci: apply automated fixes
autofix-ci[bot] Jul 27, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions .changeset/client-browser-refresh-durability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'@tanstack/ai-client': minor
'@tanstack/ai-react': minor
'@tanstack/ai-solid': minor
'@tanstack/ai-vue': minor
'@tanstack/ai-svelte': minor
'@tanstack/ai-angular': minor
'@tanstack/ai-preact': minor
---

Add browser-refresh durability to the `persistence` option.

The client `persistence` adapter now stores one combined record per chat id, the message transcript plus a resume snapshot, so a full page reload restores the conversation, rehydrates any pending interrupt, and rejoins a run that was still streaming (via `joinRun`, when the connection is durability-backed). A bare `UIMessage[]` from an older store is still read for backward compatibility.

**If you hand-rolled a `persistence` adapter, update its write path.** `setItem` now receives the combined `{ messages, resume? }` record where it used to receive a bare `UIMessage[]`, so an adapter that assumed an array will write the new shape and then fail to parse it back β€” and because adapter reads are best-effort, the failure is silent: the conversation simply does not restore. Read `{ messages, resume? }` in `getItem` (a bare array is still accepted), or switch to the `localStoragePersistence` / `sessionStoragePersistence` / `indexedDBPersistence` adapters below, which handle it for you.

The `persistence` option also accepts `true` for a server-authoritative chat: the client caches nothing, and on mount it hydrates the thread from the server by its `threadId` (painting the stored transcript and tailing any run still generating). Use it to keep large transcripts off the client while the server stays authoritative for history; it needs a connection with a `hydrate` handler and a server GET endpoint (`reconstructChat`). Passing an adapter is client-authoritative; omitting `persistence` (or `false`) is ephemeral, in-memory only.

New web storage adapters are exported for this: `localStoragePersistence`, `sessionStoragePersistence`, and `indexedDBPersistence` (plus `StorageUnavailableError` and the `ChatPersistedState` / `ChatStorageAdapter` / `ChatPersistenceOption` types). Because durability rides the existing `persistence` option, every framework integration (`react`, `solid`, `vue`, `svelte`, `angular`, `preact`) gets it with no framework-specific code.
25 changes: 25 additions & 0 deletions .changeset/define-lock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
'@tanstack/ai': minor
---

Add `defineLock` to `@tanstack/ai/locks`: an identity typer for a `LockStore`
implementation, matching the `define*Store` helpers in `@tanstack/ai-persistence`.
Pass a `withLock` object and get autocomplete and contract checking inline, with
no `: LockStore` annotation, then hand it to `withLocks`.

```ts
import { defineLock, withLocks } from '@tanstack/ai/locks'

const locks = defineLock({
async withLock(key, fn) {
const { release, signal } = await acquire(key)
try {
return await fn(signal)
} finally {
release()
}
},
})

const middleware = [withLocks(locks)]
```
31 changes: 31 additions & 0 deletions .changeset/define-store-helpers.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
'@tanstack/ai-persistence': minor
---

Add per-store typer helpers: `defineMessageStore`, `defineRunStore`,
`defineInterruptStore`, `defineMetadataStore`.

Each takes a store implementation and returns it typed against the contract, so
you get autocomplete and checking on the object literal inline β€” no separate
`: MessageStore` return annotation. They compose into `defineAIPersistence`,
which already infers **exact presence**: a store you define is a defined,
non-optional, autocompleted key on `persistence.stores`, and accessing a store
you did not define is a compile error.

```ts
import {
defineAIPersistence,
defineMessageStore,
defineRunStore,
} from '@tanstack/ai-persistence'

const persistence = defineAIPersistence({
stores: {
messages: defineMessageStore({ loadThread, saveThread }),
runs: defineRunStore({ createOrResume, update, get }),
},
})

persistence.stores.runs // RunStore (defined)
persistence.stores.interrupts // compile error β€” not provided
```
23 changes: 23 additions & 0 deletions .changeset/fast-fail-rejoin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
---
'@tanstack/ai': minor
'@tanstack/ai-client': patch
---

Make a reload rejoin fast, robust, and repeatable.

- **`memoryStream` first-chunk deadline now defaults to 100ms** (was 30s). The
common from-start join is a reload rejoining a run whose producer ran in a
prior request: an in-flight run's log already holds chunks (it streams
immediately, the deadline never applies), and an empty log means the run is
gone β€” so failing fast lets the client re-enable input near-instantly instead
of holding a dead connection open. Raise `firstChunkDeadlineMs` for a backend
whose producer can legitimately start well after a joiner attaches.
- **`ChatClient` reload rejoin hardened:** it bounds the wait for the first
chunk and clears a dead resume pointer (so a stale pointer can't pin the UI in
a loading state and can't be retried on the next load); it drops the hydrated
in-flight partial only when real content arrives (never on `RUN_STARTED`
alone), so a rejoin that connects but delivers nothing can't leave an empty
assistant bubble; and it no longer lets a replayed `RUN_STARTED` (which
carries the provider run id) overwrite the persisted resume pointer with an id
the durability log isn't keyed by β€” so a SECOND consecutive reload still
re-attaches and continues.
41 changes: 41 additions & 0 deletions .changeset/fresh-client-tail.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
---
'@tanstack/ai-persistence': minor
'@tanstack/ai-client': minor
---

Server-authoritative reconnect is now automatic and keyed on the thread, not the run.

A chat's durable identity is its **thread**; run ids are ephemeral (a single turn
can span several runs via interrupts or tool continuations), so basing reconnect
on a client-cached run id goes stale the moment a turn rolls to a new run. This
moves the whole reconnect story onto the stable thread id, resolved by the server.

- **`RunStore.findActiveRun(threadId)`** β€” new optional, feature-detected store
method returning the most recent `'running'` run for a thread. Implemented by
the in-memory reference backend and covered by the conformance testkit, so any
adapter that provides it is held to the same invariants (most-recent-running
wins, thread-scoped, null when idle).
- **`reconstructChat` now returns `{ messages, activeRun, interrupts }`** (was a
bare message array): the stored transcript as UI messages, a cursor to an
in-flight run if one exists, and any pending human-in-the-loop interrupts (tool
approvals / waits) plus the run they paused. It reads the active run before the
transcript so observing "no active run" guarantees the transcript is final
(closing a finish-window race).
- **`@tanstack/ai-client` hydrates itself on mount.** In server-authoritative
mode (`persistence: true`) the client caches no transcript and no run
pointer: on mount `useChat`/`ChatClient` calls the connection's new
`hydrate(threadId)` (a JSON GET against the same endpoint), paints the returned
transcript, and β€” if a run is in flight β€” tails it via the existing `joinRun`
durability replay. A reload and the same thread opened on another device are the
identical, server-resolved path. No loader, no `initialMessages`, no
`initialResumeSnapshot`, no app-side fetching required.
- **Interrupts reconstruct from the server too.** A paused approval (a tool with
`needsApproval`) is restored from `reconstructChat`'s `interrupts` exactly as a
persisted resume snapshot would be, so a reload β€” or another device β€” re-prompts
the same approve/reject decision and resumes the run it paused. Previously the
pending interrupt was only recoverable from client storage, so a fresh client
showed the paused tool call with no way to resolve it.

Apps keep the single GET endpoint they already have (durability replay when a
resume cursor is present, else `reconstructChat`); everything else is handled by
the hook.
13 changes: 13 additions & 0 deletions .changeset/locks-to-core.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
---
'@tanstack/ai': minor
'@tanstack/ai-persistence': minor
'@tanstack/ai-sandbox': minor
---

Move multi-instance **locks** to `@tanstack/ai` under a dedicated `@tanstack/ai/locks` subpath, and nest persistence agent skills like `ai-core`.

- **`LockStore` / `InMemoryLockStore` / `LocksCapability` / `getLocks` / `provideLocks` / `withLocks`** live in `@tanstack/ai/locks` (not the main `@tanstack/ai` barrel, and not `@tanstack/ai-persistence`).
- `@tanstack/ai-sandbox` consumes the core `LocksCapability` token (no local lock re-export).
- The locks agent skill moves with the code: `ai-core/locks` in `@tanstack/ai`, not `ai-persistence/locks`.
- Agent skills under `@tanstack/ai-persistence` nest as `skills/ai-persistence/{stores,server,build-*-adapter}/`.
- Docs: locks guide under advanced middleware.
22 changes: 22 additions & 0 deletions .changeset/memorystream-agent-loop-delivery.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
---
'@tanstack/ai': patch
---

Fix `memoryStream` truncating a tool-calling (agent-loop) run at its first tool
call.

An agent-loop run emits one `RUN_STARTED`/`RUN_FINISHED` pair per iteration
(`finishReason: "tool_calls"` for a turn that calls a tool, then `"stop"` for the
final answer). `memoryStream` treated the _first_ terminal chunk as the end of
the log β€” both marking the log complete on append and ending the reader on read β€”
so a run that called a tool was delivered only up to that first `RUN_FINISHED`:
the tool result and everything after (the model's actual answer) never reached
the client, leaving the tool call stuck "running" and the reply missing, on the
initial stream and on any reconnect/reload.

Completion is now driven solely by the producer calling `close()` (which it does
on every exit β€” the documented `StreamDurability.close` contract, honored by
`toServerSentEventsResponse`/`resumeServerSentEventsResponse` and detached
producers). The reader tails across per-iteration terminals and ends when the
producer closes, so a tool-calling run is delivered in full β€” live, on rejoin,
and on a server-authoritative reload.
21 changes: 21 additions & 0 deletions .changeset/persistence-packages.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@tanstack/ai-persistence': minor
---

Add server-side persistence for `chat()`: durable thread messages, run records, and interrupts.

`withPersistence(persistence)` is a chat middleware that stores the conversation transcript, tracks each run's status, and records interrupt state so a paused run (tool approval, client-tool execution, generic interrupt) survives a server restart.

`@tanstack/ai-persistence` ships the **contract**, not a backend for your database:

- The four store interfaces β€” `MessageStore`, `RunStore`, `InterruptStore`, `MetadataStore` β€” with the invariants the middleware depends on (full-replace `saveThread`, idempotent `createOrResume`, insert-if-absent interrupt `create`, `requestedAt`-ascending listings).
- The `withPersistence` / `withGenerationPersistence` middleware, plus `composePersistence` to assemble stores that live in different systems.
- `memoryPersistence()`, an in-process reference backend for dev and tests.
- `LockStore` / `withLocks` / `InMemoryLockStore` for cross-worker coordination β€” deliberately **not** a state store, and not composable through `composePersistence`.
- A shared conformance testkit at `@tanstack/ai-persistence/testkit`. `runPersistenceConformance` exercises every method of every store you provide and fails loudly on a store that is missing without being declared in `skip`.

Implement the stores against whatever database you already run and hand the result to `withPersistence` β€” the core never inspects your tables, so the schema stays yours. The [Build Your Own Adapter](https://tanstack.com/ai/latest/docs/persistence/build-your-own-adapter) guide walks through a complete `node:sqlite` backend end to end, and the package ships Agent Skills with worked Drizzle, Prisma, and Cloudflare D1 recipes (`npx @tanstack/intent@latest install`). `examples/ts-react-chat` runs on a self-contained `node:sqlite` adapter built this way and verified by the conformance testkit.

Resume reconstruction is delegated to the chat engine: persistence records interrupts and gates new input on a thread with pending interrupts, while the engine rebuilds the resume tool state from the resume batch and the interrupt bindings carried in the (server-loaded) message history.

`reconstructChat(persistence, request)` is a server helper that 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.
11 changes: 11 additions & 0 deletions .changeset/rejoin-not-aborted-on-mount.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@tanstack/ai-react': patch
---

Fix `useChat` aborting an in-flight delivery resume on mount. When `live` was
not enabled, the mount effect called `client.unsubscribe()` unconditionally,
which cancelled the shared in-flight stream β€” including the `joinRun` rejoin the
client had just started for a reloaded run. The result was a mid-stream reload
that caught up to the buffered point and then froze instead of continuing.
`useChat` now only tears down a subscription it actually started, so a reload
rejoins and streams the run through to completion.
21 changes: 21 additions & 0 deletions .changeset/seamless-reload-resume.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
---
'@tanstack/ai': minor
'@tanstack/ai-client': minor
'@tanstack/ai-persistence': minor
---

Make a mid-stream reload resume the same conversation cleanly.

- `withPersistence` now persists the pending turn at the start of a run (so a
reload during generation still shows the user's message), stamps each
assistant turn with its stream `messageId`, and accepts
`withPersistence(persistence, { snapshotStreaming: true })` to also persist the
in-progress reply on a throttled interval (`snapshotIntervalMs`, default
`1000`) for partial-output durability.
- `ModelMessage` gains an optional `id`; `modelMessagesToUIMessages` preserves
it, so a hydrated message keeps the same identity as its live stream.
- On reload, the chat client rebuilds an in-flight assistant turn from the
delivery log (replaying from the start and applying the buffered backlog in one
batch) instead of reconciling against the persisted partial, so the reload
shows one clean bubble that catches up and continues rather than a frozen or
duplicated partial.
29 changes: 29 additions & 0 deletions .changeset/skill-discoverability.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
'@tanstack/ai': patch
'@tanstack/ai-mcp': patch
'@tanstack/ai-sandbox': patch
'@tanstack/ai-memory': patch
---

**Make every bundled Agent Skill discoverable by TanStack Intent.**

Intent finds skills by scanning `node_modules` for packages that carry the
`tanstack-intent` keyword, and can only load what npm actually publishes. Three
packages shipped skills that failed one half of that contract:

- `@tanstack/ai-mcp` wrote its skill into a `skills/` directory that was missing
from `files`, so it was never published at all.
- `@tanstack/ai-memory` and `@tanstack/ai-sandbox` published their skills but
lacked the keyword, so Intent never looked at them.

All three now publish `skills` and carry the keyword, matching `@tanstack/ai`,
`@tanstack/ai-code-mode`, and `@tanstack/ai-persistence`.

The client persistence skill also moves from `@tanstack/ai-persistence` to
`@tanstack/ai` as `ai-core/client-persistence`. It teaches
`localStoragePersistence` / `sessionStoragePersistence` / `indexedDBPersistence`
and the `persistence` option on `useChat` β€” all of which live in the framework
packages, not in `@tanstack/ai-persistence`. An app doing browser-only
persistence never installs that package, so the guidance was unreachable for
exactly the people who needed it, and `ai-core` routed to a path that did not
exist on disk. Skills now follow the code that owns them.
32 changes: 32 additions & 0 deletions .changeset/usechat-threadid-identity.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
'@tanstack/ai-react': minor
'@tanstack/ai-preact': minor
'@tanstack/ai-solid': minor
'@tanstack/ai-vue': minor
'@tanstack/ai-svelte': minor
'@tanstack/ai-client': minor
---

The chat hooks no longer take an `id` option β€” a hook's identity is its `threadId`.

`useChat` / `createChat` previously accepted a separate `id` that keyed client
persistence and named the devtools instance, defaulting to a framework
`useId()` when omitted. That meant persistence keyed on an ephemeral render-tree
id even when you passed a stable `threadId`, so a reload found nothing under the
thread's key.

Now the `threadId` is the single identity:

- The hooks drop the `id` option. Pass `threadId` to persist a conversation and
restore it on reload; omit it for an ephemeral chat.
- Persistence keys on `threadId` (unchanged in `ChatClient`, which already
resolved `id ?? threadId` β€” the hooks simply stop overriding it).
- `ChatClient.uniqueId` (the devtools instance id) now falls back to `threadId`
instead of a generated id, so a thread shows up in devtools under its own id.
- Changing `threadId` on a mounted `useChat` (react/preact/solid) now recreates
the client so the new thread takes effect; previously the change was ignored.

`ChatClient` still accepts `id` directly as a lower-level escape hatch for
keying storage separately from the wire thread; only the framework hooks drop it.

Migration: replace `useChat({ id })` with `useChat({ threadId })`.
Loading
Loading