TanStack AI version
0.53.0
Framework/Library version
None (plain Node / @tanstack/ai-client 0.31.0)
Describe the bug and the steps to reproduce it
Reload the page while a reasoning turn is still streaming and the turn ends up with an extra "Thought for a few seconds" after the answer. Normal streaming and a full reload are both fine; only the mid-stream reconnect does it.
On the reconnect the client hydrates the saved message and then replays the run. The saved reasoning part has no stepId, because the store form drops it (modelMessageToUIMessage). The replay re-sends that reasoning's REASONING_ENCRYPTED_VALUE, which is keyed by stepId, and updateThinkingPart matches parts by stepId. It can't match the stepId-less hydrated part, so it appends a second thinking part after the text.
This only surfaces when the summary is empty (no REASONING_MESSAGE_CONTENT), so the thinking part exists only because of the encrypted value.
Steps: open the linked sandbox (it prints the result and also serves it on the preview). Same events, three reconstructions:
NORMAL STREAM : ["thinking","text"]
RESUME : ["thinking","text","thinking"] (extra trailing thinking)
DB RELOAD : ["thinking","text"]
The resumed message's parts show it: the hydrated thinking with no stepId, and a duplicate keyed to the step.
thinking stepId=null content=""
text "ASO = App Store Optimization."
thinking stepId="openai-step-1" content=""
Fix: on resume, match the replayed reasoning to the hydrated part even when it has no stepId, or carry the stepId through the store form so it can match. The match is in updateThinkingPart (activities/chat/stream/message-updaters.ts); the stepId is dropped in modelMessageToUIMessage (activities/chat/messages.ts).
Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
https://codesandbox.io/s/9nlkxm
Screenshots or Videos (Optional)
No response
Do you intend to try to help solve this bug with your own PR?
No, because I do not have time to dig into it
Terms & Code of Conduct
TanStack AI version
0.53.0
Framework/Library version
None (plain Node / @tanstack/ai-client 0.31.0)
Describe the bug and the steps to reproduce it
Reload the page while a reasoning turn is still streaming and the turn ends up with an extra "Thought for a few seconds" after the answer. Normal streaming and a full reload are both fine; only the mid-stream reconnect does it.
On the reconnect the client hydrates the saved message and then replays the run. The saved reasoning part has no
stepId, because the store form drops it (modelMessageToUIMessage). The replay re-sends that reasoning'sREASONING_ENCRYPTED_VALUE, which is keyed bystepId, andupdateThinkingPartmatches parts bystepId. It can't match the stepId-less hydrated part, so it appends a second thinking part after the text.This only surfaces when the summary is empty (no
REASONING_MESSAGE_CONTENT), so the thinking part exists only because of the encrypted value.Steps: open the linked sandbox (it prints the result and also serves it on the preview). Same events, three reconstructions:
The resumed message's parts show it: the hydrated thinking with no
stepId, and a duplicate keyed to the step.Fix: on resume, match the replayed reasoning to the hydrated part even when it has no
stepId, or carry thestepIdthrough the store form so it can match. The match is inupdateThinkingPart(activities/chat/stream/message-updaters.ts); thestepIdis dropped inmodelMessageToUIMessage(activities/chat/messages.ts).Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
https://codesandbox.io/s/9nlkxm
Screenshots or Videos (Optional)
No response
Do you intend to try to help solve this bug with your own PR?
No, because I do not have time to dig into it
Terms & Code of Conduct