Patch Changes
-
#1374
a6e22c3Thanks @threepointone! - FixuseAgentChatrecreating the AI SDK Chat instance — and orphaning any in-flightresumeStream— wheneveragent.nametransitions in place.The
useAgent({ basePath })+static options = { sendIdentityOnConnect: true }pattern lets the server own the Durable Object instance name. The browser starts with a placeholder ("default"), thenuseAgentmutates the agent object's.nameto the server-assigned value when the identity frame arrives.useAgentChatpreviously includedagent.namein the stable chat id it passed touseChat({ id }), so the transition changed the id and the AI SDK recreated the underlying Chat instance. The useEffect that fireschatRef.current.resumeStream()is keyed on the ref object, not the Chat instance, so it does not re-fire on recreation — the resumed stream kept feeding chunks into the orphaned Chat's state while React subscribed to the new Chat's state, so the user saw an empty assistant reply after a mid-stream refresh until the server's finalCF_AGENT_CHAT_MESSAGESbroadcast landed.useAgentChatnow distinguishes an in-placeagent.namemutation from a genuine "consumer switched chats" event by checking the agent object's reference identity:- same
agentreference,namemutation → not a chat switch; keep the Chat instance stable. - new
agentreference → chat switch; recompute the stable chat id so the AI SDK recreates the Chat against the new conversation.
The stable id is also still upgraded once from the identity-only fallback to the URL-resolved key when the WebSocket handshake completes.
Consumers who want to switch chats without remounting should pass a different
agentobject (e.g. a newuseAgent({...})call with a differentname). To get a completely fresh Chat (e.g. when mounting a different chat tab), the conventional React pattern —key={chatId}on the parent or swapping the subtree — continues to work. - same
-
#1395
63cfae6Thanks @threepointone! - Share submit concurrency bookkeeping throughagents/chatand use it from both chat agents.This extracts the
latest/merge/drop/debounceadmission state machine into aSubmitConcurrencyControllerexported fromagents/chat.AIChatAgentsemantics (including merge persistence) are preserved.Thinknow picks up the same pending-enqueue protection, so an overlapping submit is still detected while an accepted request is between admission and turn queue registration.Additional fixes:
Thinknow captures the turn generation immediately after admission and threads it into_turnQueue.enqueue, so a clear that lands between admission and queue registration cannot run a stale turn.- Pending-enqueue tracking is now bound to a release function tied to the controller's reset epoch, so a release from a pre-reset submit can no longer erase a post-reset submit's marker and let a third submit slip through as non-overlapping.
- Debounce cancellation correctly resolves all in-flight waiters instead of overwriting a single timer slot.
-
#1396
fdf5a8aThanks @threepointone! - Fix Think persisting a duplicate orphan assistant row when a user submits during a streaming tool turn (#1381).When
useAgentChatposts an in-flight assistant snapshot it minted optimistically (client-generated ID,state: "input-available"), Session's INSERT-OR-IGNORE-by-ID would store it as a separate row alongside the eventual server-owned assistant for the sametoolCallId. The next turn'sconvertToModelMessagesthen produced a malformed Anthropic prompt and the provider rejected it.reconcileMessagesandresolveToolMergeIdnow live inagents/chatand Think runs them in_handleChatRequestbefore persistence. Staleinput-availablesnapshots pick up the server's tool output viamergeServerToolOutputs, and any incoming assistant whosetoolCallIdalready exists on a server row adopts the server's ID so persistence updates the existing row instead of inserting an orphan.@cloudflare/ai-chatkeeps its existing reconciler behavior; the only change is that it now importsreconcileMessages/resolveToolMergeIdfromagents/chatinstead of a local file.