A voice companion can sound convincing while maintaining a fictional conversation history.
The usual demo implementation appends everything to one transcript: partial speech recognition, the final user utterance, the LLM response, and whatever text was sent to speech synthesis. That transcript then becomes the next prompt.
The tension is subtle: retaining more context appears to improve continuity, but some of that context was never actually said or heard. A partial recognition result may be wrong. An interrupted model response may never reach the user. A late callback may belong to an abandoned turn.
The model cannot repair this reliably because it only sees the history your application presents. The practical fix is to treat conversation history as committed application state, not as a log of every generated string.
In this tutorial, we will build a small TypeScript boundary that applies four rules:






