[ai] Fix "missing text part" stream crash by repairing UI message framing#2537
[ai] Fix "missing text part" stream crash by repairing UI message framing#2537VaguelySerious wants to merge 1 commit into
Conversation
…ming Duplicated or interleaved durable stream writes (step retry/redelivery or concurrent-worker duplication) can land a finish-step in the middle of a text part that reuses id "0", orphaning the rest. The AI SDK consumer then throws `Received text-delta for missing text part with ID "0"` and kills the turn. WorkflowChatTransport now normalizes UI message stream part framing — mirroring the consumer's per-step part-lifetime state machine — so orphaned deltas/ends get a synthesized start and replayed chunks are dropped. Well-formed streams pass through unchanged. Closes #2422 Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
🦋 Changeset detectedLatest commit: f4f35c5 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
🧪 E2E Test Results✅ All tests passed Summary
Details by Category✅ ▲ Vercel Production
✅ 💻 Local Development
✅ 📦 Local Production
✅ 🐘 Local Postgres
✅ 🪟 Windows
✅ 📋 Other
|
📊 Benchmark Results
workflow with no steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 1 step💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express | Nitro workflow with 10 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express | Nitro workflow with 25 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 50 sequential steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) Promise.all with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) | Nitro Promise.all with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) | Express Promise.all with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) Promise.race with 10 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express | Nitro Promise.race with 25 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) Promise.race with 50 concurrent steps💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Next.js (Turbopack) | Express workflow with 10 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 25 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 50 sequential data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Next.js (Turbopack) | Nitro workflow with 10 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) workflow with 25 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) workflow with 50 concurrent data payload steps (10KB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) Stream Benchmarks (includes TTFB metrics)workflow with stream💻 Local Development
▲ Production (Vercel)
🔍 Observability: Next.js (Turbopack) | Express | Nitro stream pipeline with 5 transform steps (1MB)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) 10 parallel streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Express | Nitro | Next.js (Turbopack) fan-out fan-in 10 streams (1MB each)💻 Local Development
▲ Production (Vercel)
🔍 Observability: Nitro | Express | Next.js (Turbopack) SummaryFastest Framework by WorldWinner determined by most benchmark wins
Fastest World by FrameworkWinner determined by most benchmark wins
Column Definitions
Worlds:
|
Closes #2422
Problem
A workflow run owns a single shared UI message stream, and the AI SDK's stream consumer (
processUIMessageStream, backinguseChat/readUIMessageStream) resets its active text/reasoning part maps on everyfinish-step. Multi-step turns reuse the same part id (commonly"0") in each step. So if atext-startis dropped or afinish-steplands in the middle of a text part, the consumer hits atext-delta/text-endfor an id it has no open part for and throws:…which kills the whole turn.
That malformed framing is reachable whenever a stream-producing step's output is duplicated or interleaved on the shared stream — e.g. step retry/redelivery, reconnect/replay overlap, or the concurrent-worker duplication tracked in #2331 / #2039 (a
finish-stepfrom one execution landing inside another's text part).Fix
WorkflowChatTransportnow passes the stream it hands to the AI SDK throughnormalizeUIMessageStreamParts, which mirrors the consumer's per-step part-lifetime state machine:finish-step(exactly where the consumer resets);*-startwhen an orphaned*-delta/*-endarrives;*-start/*-delta/*-endfor a part already open/ended in the current step (reconnect/replay overlap).It wraps the output of the send/reconnect iterators, so the reconnect
chunkIndex/gotFinishaccounting is untouched. A well-formed stream passes through unchanged; the worst case degrades to "text begins slightly into the step" or "a duplicated tail is dropped" instead of a dead turn. Applies to both text and reasoning parts.This addresses the client-visible symptom for all
WorkflowChatTransportconsumers regardless of the upstream duplication cause. The underlying server-side duplicate-execution races (#2331, #2039) are separate and remain tracked there.Tests
packages/ai/src/workflow-chat-transport.stream-repair.test.tsdrives the real transport (mockfetchreturning chunks via the AI SDK's ownJsonToSseTransformStream) into the realreadUIMessageStreamconsumer:missing text parterror;"Hello world");"0"reused across steps) is unchanged.🤖 Generated with Claude Code