Describe the bug
With useAgentChat({ resume: true }), the SDK calls resumeStream() from its WebSocket onAgentOpen handler on every reconnect. When the socket reconnects repeatedly while a turn is in flight (flaky/mobile links, or a backend redeploy that bounces the Durable Object), overlapping resumes interleave and trip a race in the underlying AI SDK's Chat.makeRequest finalizer, throwing a handled browser TypeError: Cannot read properties of undefined (reading 'state').
The finally block in Chat.makeRequest (Vercel ai) finalizes against the shared mutable this.activeResponse and then clears it:
} finally {
this.onFinish?.call(this, {
message: this.activeResponse.state.message, // bare read — unguarded
...
finishReason: this.activeResponse?.state.finishReason // adjacent field IS optional-chained
});
this.activeResponse = void 0;
}
If a second resume starts/settles before the first's finally runs, the first reads this.activeResponse.state after it's been cleared/replaced → the throw. The guard is half-applied: finishReason uses this.activeResponse?.state…, but message: this.activeResponse.state.message is a bare read. The error aborts the resume that was meant to recover the stream, so onFinish may not fire and the chat status can be left unsettled.
To Reproduce
- Mount
useAgentChat({ resume: true }) and start a streaming turn.
- Drop and re-establish the WebSocket repeatedly while the turn is in flight (redeploy the worker / toggle the network) so
onAgentOpen fires several resumeStream() calls in quick succession.
- Observe
TypeError: Cannot read properties of undefined (reading 'state') from the AI SDK Chat.makeRequest finalizer.
Expected behavior
Overlapping reconnect-driven resumes should not throw; a resume that loses the race should settle cleanly without leaving the chat status unsettled.
Version: agents 0.17.0 / @cloudflare/ai-chat 0.9.0 (with ai 6.0.191).
Suggested resolution
Make the Chat.makeRequest finalizer request-local: capture the activeResponse at the start of the request and have onFinish read from that local, clearing this.activeResponse only if it still owns it (mirroring the optional-chaining already on the adjacent finishReason field would, at minimum, stop the throw). The bare read lives in the Vercel ai package, so the definitive fix may be upstream — but since the reconnect-driven resumeStream() is what produces the overlap, serializing resumes here (don't issue a new resumeStream() while one is in flight) would also avoid the interleaving. Happy to send a PR.
Describe the bug
With
useAgentChat({ resume: true }), the SDK callsresumeStream()from its WebSocketonAgentOpenhandler on every reconnect. When the socket reconnects repeatedly while a turn is in flight (flaky/mobile links, or a backend redeploy that bounces the Durable Object), overlapping resumes interleave and trip a race in the underlying AI SDK'sChat.makeRequestfinalizer, throwing a handled browserTypeError: Cannot read properties of undefined (reading 'state').The
finallyblock inChat.makeRequest(Vercelai) finalizes against the shared mutablethis.activeResponseand then clears it:If a second resume starts/settles before the first's
finallyruns, the first readsthis.activeResponse.stateafter it's been cleared/replaced → the throw. The guard is half-applied:finishReasonusesthis.activeResponse?.state…, butmessage: this.activeResponse.state.messageis a bare read. The error aborts the resume that was meant to recover the stream, soonFinishmay not fire and the chat status can be left unsettled.To Reproduce
useAgentChat({ resume: true })and start a streaming turn.onAgentOpenfires severalresumeStream()calls in quick succession.TypeError: Cannot read properties of undefined (reading 'state')from the AI SDKChat.makeRequestfinalizer.Expected behavior
Overlapping reconnect-driven resumes should not throw; a resume that loses the race should settle cleanly without leaving the chat status unsettled.
Version:
agents0.17.0 /@cloudflare/ai-chat0.9.0 (withai6.0.191).Suggested resolution
Make the
Chat.makeRequestfinalizer request-local: capture theactiveResponseat the start of the request and haveonFinishread from that local, clearingthis.activeResponseonly if it still owns it (mirroring the optional-chaining already on the adjacentfinishReasonfield would, at minimum, stop the throw). The bare read lives in the Vercelaipackage, so the definitive fix may be upstream — but since the reconnect-drivenresumeStream()is what produces the overlap, serializing resumes here (don't issue a newresumeStream()while one is in flight) would also avoid the interleaving. Happy to send a PR.