What happens
Pick any SayPi voice for Pi in Settings → Voices (e.g. Shimmer, shown "IN USE"), then talk to Pi on pi.ai/talk. Pi answers in her own native voice. The chosen voice is never heard. Every new reply instead gets a "Generate Audio" button (the speech-incomplete state), and pressing it doesn't help.
The same voice, same account, same extension build works on claude.ai — so this is pi.ai-specific, not auth, quota, catalog, or preference storage.
Why
Only ChatHistoryAdditionsObserver synthesises speech. The "old message" observers replay from the speech history and, finding nothing for a brand-new message, mark it speech-incomplete. So whichever observer reaches a new reply first decides whether the user's voice ever speaks.
ChatHistorySpeechManager.registerPresentChatHistoryListener() runs once, in the constructor. On pi.ai the present container isn't mounted yet at that moment (the very fact #309 documents), so the additions observer resolves a null target and dies silently — permanently, for the life of that chat history.
#309's fix restored decoration of the most-recent message by attaching a ChatHistoryOldMessageObserver to the present container as it appears. That fixed the missing controls and, in the same stroke, gave the present container the past container's semantics: every new reply is treated as history. Hence a Generate-Audio button where the voice should have been.
Measured live (pi.ai, 2026-08-05): a new reply is mounted, decorated assistant-message, then speech-incomplete within ~10ms — far too fast for a synthesis round-trip — and no /speak request is ever made. The extension knows the voice is Shimmer throughout (the speech-incomplete branch is only reachable when the active provider is SayPi).
Impact
SayPi voices are dead on pi.ai for anyone on a build with #309 in it. Pi's own voice keeps playing, so it reads as "my custom voice is ignored" rather than as silence.
What happens
Pick any SayPi voice for Pi in Settings → Voices (e.g. Shimmer, shown "IN USE"), then talk to Pi on pi.ai/talk. Pi answers in her own native voice. The chosen voice is never heard. Every new reply instead gets a "Generate Audio" button (the
speech-incompletestate), and pressing it doesn't help.The same voice, same account, same extension build works on claude.ai — so this is pi.ai-specific, not auth, quota, catalog, or preference storage.
Why
Only
ChatHistoryAdditionsObserversynthesises speech. The "old message" observers replay from the speech history and, finding nothing for a brand-new message, mark itspeech-incomplete. So whichever observer reaches a new reply first decides whether the user's voice ever speaks.ChatHistorySpeechManager.registerPresentChatHistoryListener()runs once, in the constructor. On pi.ai the present container isn't mounted yet at that moment (the very fact #309 documents), so the additions observer resolves a null target and dies silently — permanently, for the life of that chat history.#309's fix restored decoration of the most-recent message by attaching a
ChatHistoryOldMessageObserverto the present container as it appears. That fixed the missing controls and, in the same stroke, gave the present container the past container's semantics: every new reply is treated as history. Hence a Generate-Audio button where the voice should have been.Measured live (pi.ai, 2026-08-05): a new reply is mounted, decorated
assistant-message, thenspeech-incompletewithin ~10ms — far too fast for a synthesis round-trip — and no/speakrequest is ever made. The extension knows the voice is Shimmer throughout (thespeech-incompletebranch is only reachable when the active provider is SayPi).Impact
SayPi voices are dead on pi.ai for anyone on a build with #309 in it. Pi's own voice keeps playing, so it reads as "my custom voice is ignored" rather than as silence.