"Even in the middle of a normal conversation, switching modes has to be free. Voice to normal, normal to voice." — Dad, 2026-09-25
The Tempting Wrong Model
The obvious design is a voice conversation: a flag on the thread, a separate screen, maybe a separate history. It falls apart the first time you use it for real. Dad talks for ten minutes, then wants to paste a link. He types it. Is that still a voice conversation? Then he asks, in writing, for a table of what they just discussed. Does the table go into the voice thread, or a new one? A conversation-level flag forces a choice at every one of those moments, and every choice splits the record.
Two Independent Facts
The design that holds puts voice on the turn, as two facts that vary independently:
input_modality:typedordictated. Where Dad's words came from. A dictated turn went through a transcriber, so it may carry transcription errors.reply_modality:writtenorspoken. How the soul should answer. A spoken turn is composed for the ear and played aloud.
That gives four combinations, and every one is real. Typed and written is an ordinary turn. Dictated and written is the composer's microphone in normal mode, or a voice note. Typed and spoken is typing a link while voice mode is on. Dictated and spoken is voice mode proper. Voice mode is only a client preset: dictated, spoken, and a hands-free loop. Switching it sets the attributes of the next turn and nothing else.
So there is one conversation, one record, one memory. After twenty minutes of talking, "put what we just said in a table" in normal mode just works, because the talk is right there in the thread. Dad would call it polymorphism in his own sense: the conversation object stays the same, and each reply takes one of two forms.
Absent Means Ordinary
Both fields are optional on the wire. Absent means typed and written, so every existing client keeps working without a change, and an unknown value normalizes to absent instead of failing the turn. The server holds one small module that owns these rules, and every chat route calls it instead of restating them.
Record Both, in Both Places
The user's row records input_modality; the assistant's row records reply_modality. The per-conversation JSONL log, which is ground truth, carries the same facts in a voice object built from the same values as the database row, so the two can never tell different stories about one turn.
Restoring the Switch
Reopen a conversation and voice mode should come back the way you left it. The first rule was simple: if the last reply was spoken, voice is on. On 2026-09-28 Dad switched voice off, reloaded, and found it on again, because the last reply had been spoken. Now his own switch is remembered with the reply count at the moment he flipped it, and it wins until a newer reply settles the question.