Conversation
Voice playback could read the same turn twice: the final reply first, and then
the whole turn (narration + reply) once the audio drained.
`spoken-reply.ts` stores the anchor as `{id, ordinal}` because the id is not
stable: the live-tail row is rewritten to a durable id on hydration, which is
why `assistantReplyOrdinal` exists. `collectUnspokenTurnSpeech` still resolved
the boundary by id alone, so a reassigned id made the lookup miss and the
"clamp to the current turn" fallback treated an already-read turn as unread.
Resolve by id, then by the anchor's ordinal (the durable slot), and treat an
anchor that cannot be located at all as nothing to say — never as "everything
since the last user message". Replaying a turn is the failure a listener
hears; staying silent is not.
Contributor
Related: #109077 (open) targets the same re-speak-after-id-rewrite class from |
Contributor
|
Comment assumes head KEEP — Desktop spoken-anchor collection boundary (voice-loop replay)
CHECK
One-click close / ordering
Landing-evidence
|
Contributor
|
Comment assumes head KEEP — Desktop spoken-anchor collection boundary (voice-loop replay)
CHECK
One-click close / ordering
Landing-evidence
|
This branch has not been deployed
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.
What happens
With the voice conversation loop on, one turn can be read aloud twice: the final reply first, and then — exactly when that audio drains — the whole turn again (narration interstitials and the reply).
Captured at synthesis time on a real session (one line per sentence handed to TTS):
The second pass starts the moment the first one ends, which is what makes it audible as a repeat rather than as a duplicate.
Root cause
spoken-reply.tsstores the spoken anchor as{id, ordinal}because the row id is not stable: the live-tail row is rewritten to a durable id on hydration, andassistantReplyOrdinal()exists to survive that rewrite.collectUnspokenTurnSpeech()still resolved the boundary by id alone:A reassigned id makes that lookup miss,
spokenIndexstays-1, and the fallback re-collects the turn that was just read.use-composer-voice.tsmade it easier to hit by passinganchor?.id ?? null— dropping the ordinal the anchor type exists to carry.Earlier fixes closed neighbouring variants of this class (
a2e0ca7fc9"don't replay earlier turns in voice conversation",63565fa26b"keep auto-speak silent across the stream-id rewrite"). This is the remaining current turn variant.Fix
Resolve the anchor by id, then by its ordinal (the durable slot); if neither lands on a visible assistant bubble, there is nothing to say. Replaying a turn is the failure a listener notices; staying silent is not, because the text is already on screen.
Tests
In
apps/desktop/src/lib/chat-messages.test.ts:nullinstead of the whole turn;Proven red on the base: with the old code the new cases fail with
expected 'assistant-stream-n1' to be 'durable-answer-9'— the slice started at the unread narration, i.e. the turn replayed. Green after the fix: 89 tests acrosschat-messages+spoken-reply, no errors.Note on authorship
The diff was produced by Hermes Agent (an AI coding agent) at the direction of the reporter, who reviewed the diagnosis, the failing case and the numbers above. Submitted from the account that hit the bug in daily use — happy to adjust style or scope, or to hand it to a maintainer.