-
Notifications
You must be signed in to change notification settings - Fork 173
Description
Describe the bug
ReceivedMessage does not expose whether a segment has been finalized. The lk.transcription_final attribute is read internally by TranscriptionStreamReceiver (to clean up partialMessages), but it is stripped before the message reaches consumers via Session.messages.
This makes it impossible for UI code to distinguish a segment that is still streaming from one that is complete. Common use cases that require this signal:
- Showing action buttons (play, translate) only after a segment finishes
- Triggering auto-translation of completed segments
- Gating per-message interactions to avoid acting on partial text
Observed behavior
For user transcripts, lk.transcription_final transitions from "false" to "true" in the stream attributes — so the attribute-based path works.
For agent transcripts, the attribute is "false" on every chunk. Finality is implicit: the text stream closes when the segment is done. But this stream-close event is not surfaced to consumers — the for try await message in reader loop simply ends, and no final ReceivedMessage is emitted.
Additionally, the last agent segment's stream often does not close promptly. In testing, it stays open until external activity occurs (e.g. the user starts speaking or the next agent turn begins), leaving the final segment stuck in a "streaming" state indefinitely.
Server
- LiveKit Cloud, version 1.9.12
- Region: Germany 2
Client
- SDK: iOS (
client-sdk-swift) - Version: 2.12.1
To Reproduce
- Connect to a room with a voice agent (livekit-agents Python, agents >= 1.0)
- Agent speaks a multi-segment response (3+ bubbles)
- Observe
Session.messages— allReceivedMessagevalues have noisFinalproperty - There is no way to determine when each segment's text is complete vs. still being appended
Expected behavior
ReceivedMessage should expose an isFinal: Bool property so consumers can react to segment completion. Two signals should set it to true:
- Attribute-based: When
lk.transcription_finalis"true"in the stream attributes (already works for user transcripts) - Stream-close: When the text stream ends, the segment is implicitly finalized — a final
ReceivedMessagewithisFinal: trueshould be emitted
Proposed fix
We've been running a local patch that works well. The change is minimal (3 files, additive only, non-breaking):
- Add
public let isFinal: BooltoReceivedMessage(defaultfalse, backward-compatible Codable) - Pass through
isFinalfrom attributes inprocessIncoming - After the stream's
for try awaitloop ends, yield one finalReceivedMessage(isFinal: true)if not already marked
Remaining issue even with the patch
The stream-close signal works reliably for middle segments (the stream closes when the next segment's stream opens). However, the last segment's stream often stays open well beyond when the agent has finished speaking — sometimes not closing until the user speaks next. A client-side debounce (act after ~1.5s of no text changes) works as a fallback, but ideally the server/agent would close the stream promptly when the segment is complete.
Additional context
TranscriptionSegment(used in the deprecated delegate path) already hasisFinal: Bool— this is just about surfacing the equivalent on the stream-basedReceivedMessage- The default value of
falsemakes this fully backward-compatible - Happy to open a PR if this direction looks right