Skip to content

fix(client): don't stream text into an activity message with the same id#2201

Open
TheSeydiCharyyev wants to merge 1 commit into
ag-ui-protocol:mainfrom
TheSeydiCharyyev:fix/1350-activity-text-id-collision
Open

fix(client): don't stream text into an activity message with the same id#2201
TheSeydiCharyyev wants to merge 1 commit into
ag-ui-protocol:mainfrom
TheSeydiCharyyev:fix/1350-activity-text-id-collision

Conversation

@TheSeydiCharyyev

Copy link
Copy Markdown
Contributor

Description

Fixes #1350.

When a text message arrives under an id an activity message already holds, the TEXT_MESSAGE_* handlers match on id alone and write into the activity message. Its structured content object is replaced with the streamed string, so the message ends up as role: "activity" with content: "Hello, world!" — no longer a valid ActivityMessage — and the assistant's reply is never created as its own message. Reproduced on main with the repro from the issue:

before:  id=shared-message-id-123  role=activity  content="Hello, world!"
after:   id=shared-message-id-123  role=activity  content={"message":"Generating plan..."}

Message ids are unique across a conversation — every lookup in this file is a bare find on id — so an activity message under a text message's id means the producer reused the id. The client cannot guess the intent, and it should not silently corrupt the message it already holds.

The guard already exists in this file for the same situation. ACTIVITY_DELTA checks the role, warns, and returns when it lands on a non-activity message; resolveOrCreateAssistantMessage does the same for TOOL_CALL_START, warning and falling back rather than writing into whatever it found. The TEXT_MESSAGE_* handlers are the ones missing it. This PR adds the same check to them.

Note: ACTIVITY_SNAPSHOT is left alone. Its replace branch overwrites a non-activity message under the same id, which is intentional and covered by "replaces non-activity message when replace is true" and "does not alter non-activity message when replace is false". My first comment on the issue called the bug symmetric — that was wrong, and the correction is here.

Changes

  • sdks/typescript/packages/client/src/apply/default.tsTEXT_MESSAGE_START, TEXT_MESSAGE_CONTENT and TEXT_MESSAGE_END warn and skip when the id belongs to an activity message, instead of creating/appending/announcing over it.
  • sdks/typescript/packages/client/src/apply/__tests__/default.activity.test.ts — new TEXT_MESSAGE_* against an activity message's id block: the activity message survives untouched, each of the three handlers warns, and text still streams normally when the id is free.
  • docs/concepts/events.mdx — states that an activity messageId must not be reused by a text message and vice versa, and what a client should do if it is.

Verification

Windows 11, Node 22.17.1, pnpm 10.33.4:

  • before: the activity message's content object is replaced by the text string and no assistant message exists
  • after: the activity message keeps {message: "Generating plan..."}, the colliding text is dropped with a warning from each handler, and an uncolliding text message still streams into its own assistant message
  • default.activity.test.ts — 30 tests pass (27 existing, unchanged, plus 3 new)
  • the rest of the @ag-ui/client suite is unaffected

The pre-existing failure of esm-interop.test.ts on Windows is unrelated and is fixed separately in #2183.

@TheSeydiCharyyev
TheSeydiCharyyev requested a review from a team as a code owner July 17, 2026 03:04
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.

[Bug]: ACTIVITY and TEXT_MESSAGE events with colliding messageId result in unexpected behaviour

1 participant