StreamProcessor ignores the stream's message ids and files agent-loop calls on the wrong assistant message
I maintainer di solito rispondono entro 1 giorno
@tombeckenham ci sta già lavorando.
Dal 27/9/2026.
Valutazione
Questa issue non è ancora stata valutata.
Descrizione
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.
- Lingua principale
- TypeScript
- Stelle
- 3.1k
- Fork
- 340
- Merge medio
- 2g 10h
- PR unite (30g)
- 175
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di TanStack/ai
-
update elevenlabsForse già presa @tombeckenham l’ha presa oggi. Aperta
Difficoltà 4/5 3-5 giorni Idoneità per principianti 25/100
I maintainer di solito rispondono entro 1 giorno
-
onAfterToolCall failure records a second, contradictory result for a successful server toolForse già presa @AlemTuzlak l’ha presa oggi. Apertahas-pr waiting-on: maintainer
TanStack/ai#1558 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
SSE and NDJSON response streams drain unread sources without backpressureForse già presa @tombeckenham l’ha presa oggi. Apertahas-pr waiting-on: maintainer
TanStack/ai#1556 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
Solid useChat drops earlier turns after a reactive request option changesForse già presa @AlemTuzlak l’ha presa oggi. Apertahas-pr waiting-on: maintainer
TanStack/ai#1552 · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
-
Tool calls from one model step run one at a time, but the docs say they run in parallelForse già presa @tombeckenham l’ha presa oggi. Apertawaiting-on: maintainer
TanStack/ai#1547 · 1 reazione · 1 assegnatario ·
I maintainer di solito rispondono entro 1 giorno
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
openedx/frontend-app-authoring#3274 ·
I maintainer di solito rispondono entro 1 giorno
-
🎙️ task - fix(deployer): deploy --env prep runs deploy:dev where the repo declares deploy:prepAperta
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
-
area/documentation status/need-triage
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 95/100
google-gemini/gemini-cli#29548 ·
I maintainer di solito rispondono entro 1 giorno
-
sdk-typescript vector-store
Difficoltà 2/5 Mezza giornata Idoneità per principianti 82/100
mem0ai/mem0#7495 · 1 commento ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 82/100
I maintainer di solito rispondono entro 1 giorno