StreamProcessor ignores the stream's message ids and files agent-loop calls on the wrong assistant message
Maintainers usually reply within 1 day
@tombeckenham is already working on this.
Since Sep 27, 2026.
Assessment
This issue has not been assessed yet.
Description
TanStack AI version
@tanstack/ai 0.63.0, @tanstack/ai-client 0.36.0, @tanstack/ai-openai 0.25.1, @tanstack/openai-base 0.12.1, @tanstack/ai-persistence 0.7.1. Also in prod on 0.61.0.
Framework/Library version
React (useChat, persistence: true), withPersistence on Postgres, OpenAI Responses API, gpt-5.6-terra, store: false
Describe the bug and the steps to reproduce it
StreamProcessor doesn't use the message ids the stream carries. In a multi-step agent turn it files one model call's output on another call's assistant message. The server stores one assistant message per call, so the live list and a reload disagree. With persistence: true the client posts its list back and withPersistence saves it (incoming wins by id), so the misfiled copy becomes the stored thread and the model reads it on every later turn.
What goes wrong
Every text adapter (Responses, Chat Completions, Anthropic, Gemini) streams a call's reasoning before anything names the call's message, and only emits TEXT_MESSAGE_START with the first text. The processor then guesses "the active message":
- Reasoning events carry their own id but no owner, and go to the active message: the previous call's.
- A tool call with an unseen
parentMessageIdalso falls back to the active message. AG-UI's reference client creates it instead (resolveOrCreateAssistantMessage: "parentMessageId not found, create new, keyed by parentMessageId"). - A call that reasons and calls tools but writes no text keeps its placeholder id until the next call's
TEXT_MESSAGE_STARTrenames it. The whole call ends up under the next call's id. - The step signature re-sent from
response.completedgoes to the active step, not the step it names (entityId/stepId), leaving an empty thinking part.
TanStack's inbound path already assigns reasoning to the assistant message that follows it (convertOwnMessages). The live processor assigns it to the one before, and uiMessagesToWire then derives the reasoning ids from that wrong owner (${messageId}-reasoning-...).
Context
Same area as #1247, #903, #1344 and #1422: the processor inferring which message an event belongs to from event order.
We hit the symptom first in #1345. The "same reasoning id, two different encrypted blobs seconds apart" there is the adapter sending each step's signature twice, with the first copy landing on the previous call. All 32 duplicated reasoning ids in our store match: same turn, the call right before, earlier blob on the earlier message. #1365 makes those threads replayable, but they still get created.
Repro
Sandbox linked below (output in the preview): TanStack's real OpenAI adapter, client and persistence, with only OpenAI's HTTP response scripted, so no API key. One turn, three calls:
- reasoning
rs_A, text, callcall_S - call
call_G, no text - reasoning
rs_B, text
live: [rs_A, text, call_S, result, call_G, result, rs_B] [text, (empty reasoning)]
reload: [rs_A, text, call_S, result] [call_G, result] [rs_B, text]
Second turn through ChatClient with persistence: true, stored call 1:
after turn 1: reasoning=[rs_A] calls=[call_S]
after turn 2: reasoning=[rs_A,rs_B] calls=[call_S,call_G] plus tool(call_G) appended again
Fix
Route by identity wherever the stream provides it:
TOOL_CALL_START.parentMessageIdnames the message. Unseen means new (or it names this call's reasoning placeholder).- A signature goes to the message that owns its step.
- Reasoning has no owner id, so its owner is the message of the same model call. After an intermediate
RUN_FINISHED, new reasoning opens a placeholder that the call's first named message claims through the existingpendingManualMessageIdrename.
I have this implemented with five processor tests covering the cases above. Four compare the live list with the engine's stored messages for a real chat() turn. All five fail on main and pass with the fix, and ai, ai-client, ai-persistence, ai-openai and the e2e suite pass with no new failures.
One open question before a PR: the reasoning part leans on the per-call RUN_FINISHED, which #1201 proposed collapsing. Would you rather adapters stamp the owning message id on reasoning events so the processor never has to infer it? Happy to do it either way.
Your Minimal, Reproducible Example - (Sandbox Highly Recommended)
https://codesandbox.io/s/q5733c
Screenshots or Videos (Optional)
No response
Do you intend to try to help solve this bug with your own PR?
No response
Terms & Code of Conduct
- I agree to follow this project's Code of Conduct
- I understand that if my bug cannot be reliable reproduced in a debuggable environment, it will probably not be fixed and this issue may even be closed.
- Dominant language
- TypeScript
- Stars
- 3.1k
- Forks
- 340
- Avg merge
- 2d 10h
- Merged PRs (30d)
- 175
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from TanStack/ai
-
update elevenlabsPossibly taken @tombeckenham claimed this today. Open
Difficulty 4/5 3-5 days Newbie friendliness 25/100
Maintainers usually reply within 1 day
-
onAfterToolCall failure records a second, contradictory result for a successful server toolPossibly taken @AlemTuzlak claimed this today. Openhas-pr waiting-on: maintainer
TanStack/ai#1558 · 1 assignee ·
Maintainers usually reply within 1 day
-
SSE and NDJSON response streams drain unread sources without backpressurePossibly taken @tombeckenham claimed this today. Openhas-pr waiting-on: maintainer
TanStack/ai#1556 · 1 assignee ·
Maintainers usually reply within 1 day
-
Solid useChat drops earlier turns after a reactive request option changesPossibly taken @AlemTuzlak claimed this today. Openhas-pr waiting-on: maintainer
TanStack/ai#1552 · 1 assignee ·
Maintainers usually reply within 1 day
-
Tool calls from one model step run one at a time, but the docs say they run in parallelPossibly taken @tombeckenham claimed this today. Openwaiting-on: maintainer
TanStack/ai#1547 · 1 reaction · 1 assignee ·
Maintainers usually reply within 1 day
Similar issues
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
openedx/frontend-app-authoring#3274 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 88/100
-
area/documentation status/need-triage
Difficulty 1/5 Under an hour Newbie friendliness 95/100
google-gemini/gemini-cli#29548 ·
Maintainers usually reply within 1 day
-
sdk-typescript vector-store
Difficulty 2/5 Half a day Newbie friendliness 82/100
mem0ai/mem0#7495 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Maintainers usually reply within 1 day