fix(workflow): drop orphan UI chunks after non-zero startIndex reconnect#15543
Open
VaguelySerious wants to merge 2 commits into
Open
fix(workflow): drop orphan UI chunks after non-zero startIndex reconnect#15543VaguelySerious wants to merge 2 commits into
VaguelySerious wants to merge 2 commits into
Conversation
When `WorkflowChatTransport` resumes with a non-zero `initialStartIndex`, the resolved chunk can land in the middle of an open `text-*` / `reasoning-*` / `tool-input-*` part. AI SDK's client enforces the start→delta→end grammar and throws on the orphan delta, breaking the chat. Add a one-pass filter on the resumed stream that drops chunks referencing a part whose start chunk wasn't in the resumed window, and warn once with a pointer to docs on server-side rewinding. Filter only activates for non-zero startIndex — startIndex 0 replays from the beginning and is always safe. Refs: vercel/workflow#1835 Ports vercel/workflow#2082 to @ai-sdk/workflow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ol-output-denied` and `tool-approval-request` chunk types, causing `UIMessageStreamError` crashes when resuming a stream mid-part. This commit fixes the issue reported at packages/workflow/src/workflow-chat-transport.ts:76 **Bug explanation:** The `createOrphanFilter()` function in `workflow-chat-transport.ts` is designed to prevent crashes when a stream resume lands mid-part — it drops chunks that reference a `toolCallId` whose `tool-input-start` was emitted before the resume window. The filter correctly handles these tool chunk types against `seenStartedToolCallIds`: - `tool-input-delta` - `tool-input-available` - `tool-input-error` - `tool-output-available` - `tool-output-error` But it's missing two chunk types that also use `toolCallId`: - `tool-approval-request` — calls `getToolInvocation(chunk.toolCallId)` at line 705 of `process-ui-message-stream.ts` - `tool-output-denied` — calls `getToolInvocation(chunk.toolCallId)` at line 742 of `process-ui-message-stream.ts` Both types have a `toolCallId` field (confirmed in `ui-message-chunks.ts` schema and type definitions). When `getToolInvocation()` is called with a `toolCallId` that has no matching tool invocation (because the `tool-input-start` was before the resume window), it throws a `UIMessageStreamError` with message `No tool invocation found for tool call ID`. This is exactly the crash scenario the orphan filter was designed to prevent. This would manifest when: 1. A tool call involves an approval flow (using `tool-approval-request`) or denial (`tool-output-denied`) 2. The stream is interrupted and resumed 3. The resume position lands after the `tool-input-start` but before/at the `tool-approval-request` or `tool-output-denied` chunk **Fix explanation:** Added `case 'tool-approval-request':` and `case 'tool-output-denied':` to the switch statement's tool-related branch that checks `seenStartedToolCallIds`. These fall through to the same logic as the other tool chunk types — if the `toolCallId` was seen in a `tool-input-start`, the chunk is kept; otherwise it's dropped with a warning. This is consistent with how all other `toolCallId`-based chunks are handled. Co-authored-by: Vercel <vercel[bot]@users.noreply.github.com> Co-authored-by: VaguelySerious <mittgfu@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
When
WorkflowChatTransportresumes with a non-zeroinitialStartIndex(positive or negative), the resolved chunk index can land in the middle of an opentext-*/reasoning-*/tool-input-*part. AI SDK's client enforces the*-start → *-delta → *-endgrammar and throws on the orphan delta — breaking the chat.This PR adds a one-pass filter on the resumed stream that drops chunks referencing a part whose start chunk wasn't in the resumed window, and emits a one-time warning pointing at docs on server-side rewinding. The filter only activates when
useExplicitStartIndexis true —startIndex: 0replays from the beginning and is always safe.Reported and reproduced in vercel/workflow#1835. This is the port of the corresponding fix in vercel/workflow#2082 to
@ai-sdk/workflow.Changes
packages/workflow/src/workflow-chat-transport.ts: addcreateOrphanFilter()and wire into the reconnect iterator.packages/workflow/src/workflow-chat-transport.test.ts: 6 new tests covering orphan reasoning / text / tool-input chunks, the pass-through path, positive-startIndex activation, and the startIndex-0 skip path..changeset/orphan-ui-chunks-on-resume.md: patch bump for@ai-sdk/workflow.Test plan
pnpm test:nodepasses (125 tests, 6 new)pnpm test:edgepasses (125 tests, 6 new)pnpm type-checkpassespnpm check(lint/format) clean