What
In voice mode the model is instructed to answer with a JSON envelope,
{"speak": "...", "display": "..."}, and extractJsonReply in server.js
unwraps it. When a generation is cut short (finish_reason: "length", or any
other early finish; see #695) the envelope arrives unterminated:
{"speak":"You have three cards and the sleep one is
extractJsonReply cannot parse that (the direct parse fails and the brace walker
finds no balanced candidate), so it returns null and the fallback branch runs:
const speak = finalText.replace(/\p{Extended_Pictographic}/gu, '').trim();
return withFollowup({ reply: finalText || EMPTY_REPLY_MESSAGE, speak, ... });
So the raw JSON fragment becomes both the displayed reply and the spoken text.
The user hears the punctuation and the key names read aloud, and #695's
explanatory suffix is appended inside the broken envelope where it reads as part
of the fragment.
Why
Voice is the mode with the least recovery affordance: there is no visible text to
squint at, so an unparseable envelope is pure noise. The fallback exists for a
model that ignored the format instruction and answered in prose, which is a
reasonable thing to speak. A truncated envelope is not prose and must not go
through the same door.
Acceptance criteria
Notes
Fall-out from #695, which made an early finish a first-class outcome. The bug
predates it (any truncation hit this), but #695 is what makes the path routine.
What
In voice mode the model is instructed to answer with a JSON envelope,
{"speak": "...", "display": "..."}, andextractJsonReplyinserver.jsunwraps it. When a generation is cut short (
finish_reason: "length", or anyother early finish; see #695) the envelope arrives unterminated:
extractJsonReplycannot parse that (the direct parse fails and the brace walkerfinds no balanced candidate), so it returns null and the fallback branch runs:
So the raw JSON fragment becomes both the displayed reply and the spoken text.
The user hears the punctuation and the key names read aloud, and #695's
explanatory suffix is appended inside the broken envelope where it reads as part
of the fragment.
Why
Voice is the mode with the least recovery affordance: there is no visible text to
squint at, so an unparseable envelope is pure noise. The fallback exists for a
model that ignored the format instruction and answered in prose, which is a
reasonable thing to speak. A truncated envelope is not prose and must not go
through the same door.
Acceptance criteria
parse as an envelope speaks a short human sentence explaining the cut,
not the raw fragment
is not thrown away), and the reply is still flagged
cappedso "keep going"is offered
instead of the envelope, both behave exactly as they do today
lengthfinishasserts the spoken text contains no brace or key name
Notes
Fall-out from #695, which made an early finish a first-class outcome. The bug
predates it (any truncation hit this), but #695 is what makes the path routine.