Skip to content

Surface text stream status and handle stream read failures - #1508

Open
1egoman wants to merge 6 commits into
mainfrom
fix/textstream-error-status
Open

1egoman wants to merge 6 commits into
mainfrom
fix/textstream-error-status

Conversation

@1egoman

@1egoman 1egoman commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

What

Today, when a participant disconnects while one of its text streams is still open, livekit-client errors the open reader with a DataStreamError (AbnormalEnd), by design.

However, that means that when setupTextStream read each stream with from(reader).pipe(scan(...)).subscribe(next), which has no error handler, any errors that do get exposed get handled by this and end up as uncachable global errors.

For voice-agent apps, an error fires whenever the agent leaves mid-segment (interrupted farewell, process exit, crash), which happens enough to be a nuisance and clog up error reporting.

This issue has already been attempted in #1426 but has been dormant for a few months. The existing solution aimed to fix this fully within setupTextStream, which meant that it inherited a lot of additional complexity (ie, attachments) which made it quite large and complex. In this PR, instead I've opted to expose data stream error states up to the user via useTextStream, and then consume this in useTranscription. If more complex error handling is needed then that can all live in useTranscriptions instead of the rx-heavy setupTextStream.

Changes

  • TextStreamData is now a discriminated union carrying a status: 'pending' | 'complete' | 'failed'. Failed entries expose another property error which is the error that ended the read.
  • The per-reader subscribe gets full next / complete / error handling:
    • next → pending, accumulating text as before.
    • complete (writer closed cleanly) → complete; the text is whole.
    • error (any read failure) → failed, keeping whatever text arrived. A stream that fails before emitting still surfaces as a failed entry rather than disappearing silently.
  • No error type is special-cased. If a read errored, it's failed. Any segment-level interpretation (e.g. treating a benign AbnormalEnd after a complete transcript as acceptable) belongs in useTranscriptions, not core.

Scope

Text / transcription streams only. Attachments were intentionally omitted, because useTextStream exposes only streamInfo.attachedStreamIds (IDs), never attachment bytes.

Tests

New tests drive setupTextStream with readers that complete normally, error mid-stream, and error before emitting: status transitions pending → complete, failed streams keep their accumulated text and expose the error, and nothing rethrows globally (the error-path tests fail on the unfixed code with the uncaught error). Full @livekit/components-core suite (123) and @livekit/components-react suite (36) pass; both packages typecheck and build.

setupTextStream subscribed to each reader without an error handler, so
when a sender disconnected mid-stream the errored reader's DataStreamError
was reported by RxJS as an unhandled observable error — rethrown globally
from a timer and surfacing as an uncaught DataStreamError that no
application code could catch.

Give the subscribe full next/complete/error handling and model the result
on TextStreamData via a new `status` field ('pending' | 'complete' |
'failed'), with the error exposed on failed entries. A clean close marks a
stream complete; any read error marks it failed while keeping the text
accumulated so far. No error type is special-cased here — consumers such
as useTranscriptions decide what a partial or failed stream means.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@vercel

vercel Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
components-js-storybook-5kld Ready Ready Preview Nov 9, 2026 12:20am UTC

Request Review

@changeset-bot

changeset-bot Bot commented Nov 8, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: cb1289a

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

This PR includes changesets to release 7 packages
Name Type
@livekit/components-core Patch
@livekit/components-react Patch
@livekit/agents-ui Patch
@livekit/component-example-next Patch
@livekit/components-js-docs Patch
@livekit/component-docs-storybook Patch
@livekit/components-docs-gen 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

@github-actions

github-actions Bot commented Nov 8, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
LiveKitRoom only 7.88 KB (0%)
LiveKitRoom with VideoConference 32.67 KB (+0.15% 🔺)
All exports 44.6 KB (+0.38% 🔺)

Split the inline union into internal TextStreamDataCommon plus
Pending/Complete/Failed variants and a Legacy (status?: never) member for
backward compatibility, unioned as the exported TextStreamData. Variants
stay unexported; only TextStreamData is public.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@1egoman 1egoman changed the title feat: surface text stream status and handle stream read failures Surface text stream status and handle stream read failures Nov 8, 2026
The chat text-stream handler in setupChat subscribed without an error
handler, so a sender disconnecting mid-message errored the reader and
RxJS rethrew it globally from a timer — the same uncaught DataStreamError
that useTextStream had. Give that subscribe an error handler too.

Every error is treated equally, with no special-casing by reason: text
streams surface the failure through status, and chat has no such channel
so it logs. The attachment byte-stream hang/leak is a separate failure
mode and remains a follow-up.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rather than logging chat stream read errors, model them in the data the
same way TextStreamData does. ReceivedChatMessage becomes a discriminated
union with a status of 'pending' | 'complete' | 'failed' (error exposed on
failed), plus a legacy member with no status so the other construction
sites keep type-checking.

The chat text-stream subscribe now emits pending as the message streams,
complete on a clean close, and failed (keeping the text that arrived) when
the read errors — which also stops RxJS from rethrowing the error globally
as an uncaught exception. Every error is treated the same; no reason is
special-cased. LegacyReceivedChatMessage becomes a type intersection since
an interface cannot extend a union.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Without an explicit return type, `send`'s inferred type narrowed the
`const receivedChatMsg: ReceivedChatMessage` to the no-status (legacy)
member of the new discriminated union and emitted it expanded inline,
which broke the react api-extractor report. Annotating the return as
`Promise<ReceivedChatMessage>` restores the named reference.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

This branch was successfully deployed

1 active deployment
Preview — cb1289ac Deployed Nov 9, 2026 by vercel[bot]
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