Describe the bug
When a chat client's WebSocket closes mid-turn and PartySocket auto-reconnects, the resumed stream's replayed chunks drive a synchronous re-render cycle inside the shared chat hook (agents/chat/react, consumed via @cloudflare/think/react): onAgentMessage → ReactChatState set messages → forceStoreRerender → … repeating until React's update-depth guard throws Maximum update depth exceeded.
The exception escapes through useAgentChat's catch path, so the client lands in status: "error" with error set mid-turn — while the durable turn on the agent DO is healthy and completes normally (verified in the DO message store: stream metadata completed, full assistant reply persisted).
#1838 does not fix this: with resumes serialized, a single close + its one resumed stream is still enough to trigger the loop. Reproduced in 3 separate runs on the versions below.
To Reproduce
- A
Think-based agent with chat over useAgent + useAgentChat (defaults — resume on).
- Start a long streaming turn (a multi-second reply is enough).
- Close the underlying WebSocket once mid-stream (e.g. grab the raw socket and call
.close(); PartySocket auto-reconnects).
- On reconnect the stream resumes and replays; the browser console shows the
Maximum update depth exceeded React error, and useAgentChat reports status: "error" + a set error while the server-side turn keeps running.
Expected behavior
A resumed stream replays without tripping React's update-depth guard — and in general, a client-side rendering failure should not mark a turn as terminally errored while it is still running (and later completes) server-side. The false terminal error also cascades into a second bug (filed separately, linked below): once the client sits in "error", stream resume never re-arms, so isServerStreaming can stick true forever.
Version:
agents@0.17.3, @cloudflare/think@0.12.1, ai@6.0.205, @ai-sdk/react@3.0.207, react@19.2.7, partysocket@1.3.0. Observed both under local wrangler dev and in production.
Additional context
Found while adopting the #1799 connectionError surface. Downstream we mitigate with an error-triggered debounce + a bounded getMessages reconciliation loop, but the root loop lives in the shared hook's message-replay path. Happy to provide the full captured stack / more repro detail if useful.
Describe the bug
When a chat client's WebSocket closes mid-turn and PartySocket auto-reconnects, the resumed stream's replayed chunks drive a synchronous re-render cycle inside the shared chat hook (
agents/chat/react, consumed via@cloudflare/think/react):onAgentMessage → ReactChatState set messages → forceStoreRerender → …repeating until React's update-depth guard throwsMaximum update depth exceeded.The exception escapes through
useAgentChat's catch path, so the client lands instatus: "error"witherrorset mid-turn — while the durable turn on the agent DO is healthy and completes normally (verified in the DO message store: stream metadatacompleted, full assistant reply persisted).#1838 does not fix this: with resumes serialized, a single close + its one resumed stream is still enough to trigger the loop. Reproduced in 3 separate runs on the versions below.
To Reproduce
Think-based agent with chat overuseAgent+useAgentChat(defaults —resumeon)..close(); PartySocket auto-reconnects).Maximum update depth exceededReact error, anduseAgentChatreportsstatus: "error"+ a seterrorwhile the server-side turn keeps running.Expected behavior
A resumed stream replays without tripping React's update-depth guard — and in general, a client-side rendering failure should not mark a turn as terminally errored while it is still running (and later completes) server-side. The false terminal error also cascades into a second bug (filed separately, linked below): once the client sits in
"error", stream resume never re-arms, soisServerStreamingcan sticktrueforever.Version:
agents@0.17.3,@cloudflare/think@0.12.1,ai@6.0.205,@ai-sdk/react@3.0.207,react@19.2.7,partysocket@1.3.0. Observed both under localwrangler devand in production.Additional context
Found while adopting the #1799
connectionErrorsurface. Downstream we mitigate with an error-triggered debounce + a boundedgetMessagesreconciliation loop, but the root loop lives in the shared hook's message-replay path. Happy to provide the full captured stack / more repro detail if useful.