fix: useChat status stays ready during stream resumption#999
Merged
threepointone merged 1 commit intomainfrom Feb 28, 2026
Merged
fix: useChat status stays ready during stream resumption#999threepointone merged 1 commit intomainfrom
threepointone merged 1 commit intomainfrom
Conversation
🦋 Changeset detectedLatest commit: ff7026b The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
commit: |
This was referenced Feb 26, 2026
e369b0e to
4b4d4b0
Compare
Implement WebSocketChatTransport.reconnectToStream() to return a proper ReadableStream for resumed streams, and forward the resume option to useChat. This lets the AI SDK's pipeline process resumed chunks natively, correctly managing status, isLoading, and abort during stream resumption. - reconnectToStream sends RESUME_REQUEST, waits for RESUMING, sends ACK, returns ReadableStream fed by replayed + live chunks - 100ms delayed explicit request avoids double-RESUMING race with onConnect - onAgentMessage guards with localRequestIdsRef to skip transport-handled chunks - Removed duplicate RESUME_REQUEST from useEffect (transport owns it now) - Updated test to verify progressive chunk processing
4b4d4b0 to
ff7026b
Compare
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
useChatstatus stayed"ready"during stream resumption after page refresh —isLoadingwas false, no abort button or thinking indicator appeared.Four root causes:
1. addEventListener race
The transport registered its own
addEventListenerlistener forCF_AGENT_STREAM_RESUMINGdetection, butonAgentMessage(also viaaddEventListener) always handled the message first. The fallback path ran — bypassing the AI SDK pipeline entirely. Chunks flowed throughonAgentMessage→setMessagesdirectly, souseChatnever saw them and never set status to"streaming".2. Transport instance instability
useMemocreated new transport instances across renders and Strict Mode cycles. When_pkchanged (async queries, socket recreation), the resolver was stranded on the old transport whileonAgentMessagecalledhandleStreamResumingon the new one — it never found the resolver.3. Chat recreation on
_pkchangeUsing
agent._pkas theuseChatidcaused the AI SDK to recreate the Chat when the socket changed (shouldRecreateChat: chatRef.current.id !== options.id). This abandoned the in-flightmakeRequest(including resume). The resume effect wouldn't re-fire because its deps are[resume, chatRef]—chatRefis the same ref object, so the effect never re-runs.4. Double STREAM_RESUMING
The server sends
CF_AGENT_STREAM_RESUMINGfrom bothonConnect(line 348) and theRESUME_REQUESThandler (line 551). Without deduplication, the second message triggered a duplicate ACK and double chunk replay.Fix
addEventListener race → synchronous callback
Replaced the transport's
addEventListener-based detection withhandleStreamResuming()— a public method thatonAgentMessagecalls directly:Transport instability → true singleton
The transport is now a true singleton (
useRef, created once, never recreated).transport.agentis updated every render to point at the latest socket. The resolver survives_pkchanges because the transport instance never changes — bothreconnectToStream(via ChatStore) andhandleStreamResuming(viaonAgentMessage) always operate on the same instance.The
agentproperty was changed fromprivateto public, andreconnectToStream's resolver usesthis.agent(not a captured local) so ACK and chunk listeners go through the latest socket.Chat recreation → stable ID
Replaced
id: agent._pkwithid: initialMessagesCacheKey(based on URL + agent namespace + instance name). This identifier is stable across socket recreations, so the AI SDK's Chat is never recreated when_pkchanges. The in-flightmakeRequestsurvives and correctly transitions status to "streaming".Double STREAM_RESUMING → localRequestIdsRef guard
Added
localRequestIdsRef.current.has(data.id)check before the fallback path to prevent duplicate ACK/replay.Edge cases handled
localRequestIdsRefguard prevents duplicate ACK/replayuseRefsingleton survives mount/unmount/remount; secondreconnectToStreamoverwrites resolver; first times out harmlessly_pkchange (async queries) — transport singleton survives; agent ref updated; ACK/listeners use latest socket; stable Chat ID prevents Chat recreationsend(); works regardless of readyStatenullafter 5s, resolver clearedagentref updated every render; old resolver orphanedTesting