Skip to content

fix(web): fold per-LLM-call tool calls and deltas into one assistant bubble#32746

Merged
dvargasfuertes merged 1 commit into
mainfrom
devin/1780256504-fold-tool-calls-into-assistant-bubble
May 31, 2026
Merged

fix(web): fold per-LLM-call tool calls and deltas into one assistant bubble#32746
dvargasfuertes merged 1 commit into
mainfrom
devin/1780256504-fold-tool-calls-into-assistant-bubble

Conversation

@dvargasfuertes

Copy link
Copy Markdown
Contributor

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

  • How it got here: The daemon (B3, B3: pre-allocate assistant row at llm_call_started boundary #32326) reserves a fresh messageId per LLM call and the backend's mergeConsecutiveAssistantMessages collapses the run onto the first row's id, listing the rest in mergedMessageIds. fix(chat): wire assistant_turn_start, key appendTextDelta on messageId (B5) #32612 then keyed appendTextDelta/upsertToolCall on the event messageId but matched rows with m.id === messageId only — ignoring the alias set the merge produces.
  • What that caused: A later LLM call's events carried an id the anchor already owned as an alias (or a brand-new id), the lookup missed, and a duplicate isStreaming bubble opened per LLM call — the reported screenshot.
  • Warning signs missed: reconcile.ts and applyUserMessageEcho already 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.
  • Fix: 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) rather than opening a new bubble — a new turn always begins with a user row, so a non-assistant tail still opens a fresh bubble. Applied consistently to text deltas, tool calls, and ui surfaces.
  • Prevention: Identity-based row matching (id + aliases) should be the single rule for routing SSE events to rows. No AGENTS.md change warranted — this is module-scoped and captured in the helper docstrings (findAssistantRowIndexByMessageId).

Follow-up: removing the isStreaming flag 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.


…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-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +201 to 204
if (tailIsAssistant(prev)) {
return appendTextIntoRow(prev, prev.length - 1, text, messageId);
}
return createStreamingBubble(prev, text, messageId);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@dvargasfuertes dvargasfuertes merged commit 04ade39 into main May 31, 2026
7 checks passed
@dvargasfuertes dvargasfuertes deleted the devin/1780256504-fold-tool-calls-into-assistant-bubble branch May 31, 2026 19:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant