Skip to content

fix(ai-chat): strip messageId from continuation start chunks (#1229)#1235

Merged
threepointone merged 2 commits intomainfrom
fix/issue-1229-tool-continuation-message-id
Mar 31, 2026
Merged

fix(ai-chat): strip messageId from continuation start chunks (#1229)#1235
threepointone merged 2 commits intomainfrom
fix/issue-1229-tool-continuation-message-id

Conversation

@whoiskatrin
Copy link
Copy Markdown
Contributor

@whoiskatrin whoiskatrin commented Mar 30, 2026

Summary

When a tool continuation stream starts after a client tool result, the AI SDK emits a start chunk with a fresh messageId. On the client, processUIMessageStream uses this to override the assistant message ID, which causes the AI SDK's write() to push a new message instead of replacing the existing one — resulting in a brief duplicate assistant message.

The server already had a !continuation guard on its own message-building path (skipping messageId for its internal message.id), but was still forwarding the raw messageId to clients in the broadcast. This PR strips messageId from start chunks server-side in _streamSSEReply before both storage and broadcast, so all clients — not just WebSocketChatTransport — are protected.

Changes

  • packages/ai-chat/src/index.ts — In the chunk-rewriting section of _streamSSEReply, strip messageId from start chunks when continuation is true. This sits alongside the existing finishReason → messageMetadata transform. Updated the comment to document both transforms.
  • packages/ai-chat/src/tests/worker.ts — Added sseWithMessageId body flag to TestChatAgent so it can return an SSE response with a start chunk containing messageId (simulating real streamText output).
  • packages/ai-chat/src/tests/client-tools-continuation.test.ts — End-to-end test: triggers a tool-result auto-continuation with an SSE response, uses a second connection to passively observe broadcast chunks, and asserts that continuation start chunks have messageId stripped.
  • .changeset/cool-oranges-drum.md — Patch changeset for @cloudflare/ai-chat.

Closes #1229

Testing

npm run test:workers -- src/tests/client-tools-continuation.test.ts

Open with Devin

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 30, 2026

🦋 Changeset detected

Latest commit: e0ab802

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cloudflare/ai-chat Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 30, 2026

Open in StackBlitz

agents

npm i https://pkg.pr.new/agents@1235

@cloudflare/ai-chat

npm i https://pkg.pr.new/@cloudflare/ai-chat@1235

@cloudflare/codemode

npm i https://pkg.pr.new/@cloudflare/codemode@1235

hono-agents

npm i https://pkg.pr.new/hono-agents@1235

@cloudflare/shell

npm i https://pkg.pr.new/@cloudflare/shell@1235

@cloudflare/think

npm i https://pkg.pr.new/@cloudflare/think@1235

@cloudflare/voice

npm i https://pkg.pr.new/@cloudflare/voice@1235

@cloudflare/worker-bundler

npm i https://pkg.pr.new/@cloudflare/worker-bundler@1235

commit: 675d02b

whoiskatrin and others added 2 commits March 31, 2026 13:45
Move stripping of messageId for continuation start chunks to the server-side broadcast logic so clients reuse the existing assistant message (fixes duplicate assistant message / #1229). Remove the client-side stripping in the WebSocket transport and add a test that verifies the server strips messageId. Also add an SSE stub in the test agent to simulate streams containing messageId and update tests accordingly; update changelog entry.
@threepointone threepointone force-pushed the fix/issue-1229-tool-continuation-message-id branch from e0ab802 to 675d02b Compare March 31, 2026 12:45
@threepointone threepointone changed the title Fix duplicate assistant messages in tool continuations fix(ai-chat): strip messageId from continuation start chunks (#1229) Mar 31, 2026
@threepointone
Copy link
Copy Markdown
Contributor

Rewrote the approach in this PR — here's what changed and why:

Moved the fix from client-side to server-side. The original PR stripped messageId in WebSocketChatTransport._createToolContinuationStream() on the client. The new version strips it in _streamSSEReply() on the server, before both storage and broadcast. This is the right layer because:

  • The server already knew continuation=true and was guarding its own message.id with !continuation at line 3260 — but was still forwarding the raw messageId to clients. That inconsistency was the actual bug.
  • The server-side fix protects all clients (other transports, third-party consumers, different SDK versions), not just WebSocketChatTransport.
  • Stored stream chunks (used for replay on reconnect) are also cleaned, avoiding the same duplicate on reconnection.

Removed the client-side transport fix and its test (ws-chat-transport.ts, ws-transport-resume.test.ts). No longer needed — the problematic messageId never reaches the client.

Removed the React integration test (use-agent-chat.test.tsx). It was testing a scenario that can't happen with the server-side fix.

Rewrote the server-side test to actually exercise the SSE code path. The original test used TestChatAgent's default text/plain response, which goes through _sendPlaintextReply (no start chunks). The fix lives in _streamSSEReply. So the test was passing vacuously — the for loop over start chunks ran zero iterations. The new test:

  • Adds an sseWithMessageId flag to TestChatAgent that returns an SSE response with { type: "start", messageId: "..." }.
  • Uses two connections (A triggers the tool result, B passively observes broadcasts) because the originating connection is excluded from live broadcasts via _pendingResumeConnections until it ACKs.
  • Asserts startChunks.length > 0 to prove the SSE path is actually exercised, then asserts messageId is absent.

@threepointone threepointone marked this pull request as ready for review March 31, 2026 12:48
Copy link
Copy Markdown
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no potential bugs to report.

View in Devin Review to see 2 additional findings.

Open in Devin Review

@threepointone threepointone merged commit 4f79280 into main Mar 31, 2026
2 checks passed
@threepointone threepointone deleted the fix/issue-1229-tool-continuation-message-id branch March 31, 2026 12:53
@github-actions github-actions bot mentioned this pull request Mar 31, 2026
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.

ai-chat: tool continuation sends new messageId in start chunk, causing duplicate assistant message

2 participants