feat(voice): message listening versions persist and sync across clients - #176
Merged
Merged
Conversation
…guards across layers
… on unreadable pending requests
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 freeto 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.
Creating a listening version was one long HTTP request owned by the message row. The spinner lived in component state, so leaving the thread hid all progress, a disconnect aborted the synthesis, and other clients never learned a recording existed because the server wrote the projection table directly and emitted no event.
Speech synthesis is now an event-sourced job. A
thread.message.speech.requestcommand validates against the SQL projection under a shared per-message lock and emits a requested event that marks pending state on the message. A reactor synthesizes the audio (concurrent across messages, serialized per message, 3-minute timeout) and completes throughthread.message.speech.complete; both projectors store the artifact and clear pending, and subscribed clients receive both transitions as live deltas. Recordings survive navigation, reconnects, and device switches, user recordings are event-owned and survive projection replay, and interrupted jobs are cleared on startup. The legacy HTTP endpoint joins the same jobs and keeps typed failure reasons, so old clients keep working. Web and mobile derive button state from the projection when the server advertisestextToSpeech.persistentJobsand keep the old path against older servers.Implemented by gpt-5.6-sol subagents, orchestrated and reviewed (gpt-5.6-sol + Opus 5 panels) by Claude Fable 5 in Claude Code.