fix(web): fold per-LLM-call tool calls and deltas into one assistant bubble#32746
Conversation
…bubble The daemon reserves a fresh messageId per LLM call within an agent turn, and the backend merge collapses the run onto the first row's id with the rest as mergedMessageIds aliases. The streaming updaters looked up rows by primary id only, so a later LLM call's events opened a duplicate streaming bubble for an id the run already owned. Resolve target rows by id OR merged alias, and when no row owns the id, fold into the assistant tail (recording the id as an alias) instead of opening a new bubble. A new turn always begins with a user row, so a non-assistant tail still opens a fresh bubble. Applies to text deltas, tool calls, and ui surfaces. Co-Authored-By: vargas@vellum.ai <vargas@vellum.ai>
🤖 Devin AI EngineerI'll be helping with this pull request! Here's what you should know: ✅ I will automatically:
Note: I can only respond to comments from users who have write access to this repository. ⚙️ Control Options:
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: f1145e64e7
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if (tailIsAssistant(prev)) { | ||
| return appendTextIntoRow(prev, prev.length - 1, text, messageId); | ||
| } | ||
| return createStreamingBubble(prev, text, messageId); |
There was a problem hiding this comment.
Fold unmatched deltas into the active assistant row
When the user queues another message while a turn is still streaming, useSendMessage appends that queued user row to the end of messages (apps/web/src/domains/chat/hooks/use-send-message.ts:568). If the current turn then advances to a later LLM call, its fresh messageId is not owned by any row yet, so this branch sees a non-assistant tail and creates a new assistant bubble after the queued user instead of folding into the in-flight assistant message; the same per-LLM-call split this change is trying to avoid still occurs for queued-message conversations. Search backward for the current/latest assistant row before falling back to createStreamingBubble.
Useful? React with 👍 / 👎.
Streaming tool calls and text deltas from later LLM calls in an agent turn were opening new client message bubbles instead of consolidating into the previous assistant message; this resolves SSE events by merged-alias id (not just primary id) and folds unmatched events into the assistant tail so a turn renders as one bubble. This matches the shape the backend merge already produces, keeping post-turn reconcile-by-id stable.
Root cause analysis
messageIdper LLM call and the backend'smergeConsecutiveAssistantMessagescollapses the run onto the first row's id, listing the rest inmergedMessageIds. fix(chat): wire assistant_turn_start, key appendTextDelta on messageId (B5) #32612 then keyedappendTextDelta/upsertToolCallon the eventmessageIdbut matched rows withm.id === messageIdonly — ignoring the alias set the merge produces.isStreamingbubble opened per LLM call — the reported screenshot.reconcile.tsandapplyUserMessageEchoalready matched on identity (id +mergedMessageIds); the streaming updaters diverged from that established pattern. fix(chat): wire assistant_turn_start, key appendTextDelta on messageId (B5) #32612's own description framed "new-bubble-on-mismatched-id" as intended, which baked the regression into a test.findAssistantRowIndexByMessageId).Follow-up: removing the
isStreamingflag entirely (its bubble-boundary role is gone here; remaining uses are the thinking-indicator/stop-button derivation and reconcile's mid-stream live-row preservation) will be a focused separate PR.