The message was recorded. It just isn't the message.
Reported as "it showed as I dictated it, then never stayed on the thread — just disappeared."
Nothing was dropped. Instance cda75e28 (Heartfull tmux Operator), message 0f6c8f6f:
role: user
content: "Duet" ← what was stored, sent to the agent, and shown on the thread
dictation: "Do it" ← what the browser gate heard
audioKey: f220871a-… ← the recording, intact
createdAt: 2026-08-07T06:28:58Z
The user said "Do it". The live bubble showed "Do it". Whisper returned "Duet", the bubble
cleared, and the thread got a word the user never said — which reads exactly like the message
vanishing, because the thing on screen is not theirs.
It broke the conversation, not just the record
The turn before, the agent asked: "…shall I do that now?" — a yes/no. The user answered "Do it".
The agent received "Duet", could not resolve it, and replied by re-listing the same five tmux
sessions it had already listed twice. The task never started.
The pattern across the whole thread — 4 of 5 voice turns are wrong
| when | stored + sent (Whisper) | heard live (browser gate) |
|---|
| 03:35 | Is tmux ready? | Yes Timo is ready is the agent replying in the terminal or not |
| 05:35 | Heartfull | (agreed) |
| 06:26 | Do you see the console output? | Do you see the console output what's there right now |
| 06:28:19 | context: | Send |
| 06:28:58 | Duet | Do it |
The browser gate was more accurate than Whisper on every turn where they disagreed, and the
platform always ships Whisper's reading and files the gate's into a footnote badge.
Root cause: a tmux agent is given no vocabulary at all
buildTranscribePrompt (packages/sdk/src/voice/prompt.ts:27) gates its term lists on surfaces:
if(surfaces.includes("coding")||surfaces.includes("repo"))parts.push(CODING_TERMS);if(surfaces.includes("apply"))parts.push(APPLY_TERMS);This instance is surfaces: ["tmux"], runtime: "coding". tmux is in neither branch, so
CODING_TERMS is skipped. The call site passes the instance name as extra
(InstanceDetail.tsx:237), so the entire transcription bias for this agent is the literal string
"Heartfull (tmux)" — its own name, and nothing else.
Meanwhile CODING_TERMS already contains the exact words this user says to a terminal agent:
console, terminal, git, npm, pnpm, commit, branch, deploy, build, lint, endpoint. They are sitting
in the file, unused, because the gate asks about the surface rather than the runtime.
That is why Do you see the console output… lost half its sentence, and why two-word utterances had
nothing at all steering them. tmux should be in the coding branch — a one-line change — and the
gate is arguably reading the wrong axis entirely, since runtime: "coding" was true here the whole
time.
Note the second-order risk this creates: the ONE term the agent is biased toward is a proper noun,
which is precisely the #332 failure mode (a proper noun in the bias list coming back verbatim on a
low-information clip). isTranscribeBiasEcho would catch Heartfull (tmux) because it is
multi-word — but it deliberately passes single-word echoes, so a one-word agent name would not
be caught.
The guard that exists is switched off for exactly these turns
dictationDiverged (packages/sdk/src/voice/machine.ts:322) is what flags a transcript as
materially short:
exportfunctiondictationDiverged(heard: string,final: string): boolean{consth=words(heard);if(h.length<4)returnfalse;// ← silent below four heard wordsreturnwords(final).length<Math.ceil(h.length*0.6);}"Do it" → h.length === 2 → false. No warning."Send" → h.length === 1 → false. No warning.
So the two turns that came back as pure nonsense are the two the guard is contractually forbidden to
comment on. The floor is defensible in isolation — four words is "too little signal to accuse
anything" on a volume test — but the result is that short utterances, which are the most
likely to be mistranscribed, are the only ones with no check at all.dictationLoss still renders
a dim "2 words not in the transcript", which is not a signal anyone reads as "this is the wrong
word".
What to change
1. A short utterance needs a different test, not the volume one. When the heard capture is under
four words, compare by overlap instead: if the final transcript shares no word with what the
gate heard, that is divergence regardless of length. Do it/Duet and Send/context: both trip
it; colour/color and ordinary engine disagreement do not, because they overlap.
2. Prefer the gate's reading when the final is a bare unknown token. The gate got it right every
time here. At minimum, a one-tap "use what I heard instead" on the bubble — the data is already
stored (dictation) and already rendered behind the Show what was heard toggle
(SpokenMessage.tsx); today it is archaeology, not a correction path.
3. Re-examine the bias prompt.context: is a prompt fragment surfacing as a transcript. #332
treated the symptom (agent names); the general shape — a low-information clip echoing the prompt —
is still live and produces text the user cannot distinguish from their own words.
Related
#364 (the composer showed nothing while this happened, so the live capture was only visible in the
thread bubble), #332 (prompt-token echo), #319 (the stored live capture that made this diagnosable at
all — without it this report would have been unanswerable).
The message was recorded. It just isn't the message.
Reported as "it showed as I dictated it, then never stayed on the thread — just disappeared."
Nothing was dropped. Instance
cda75e28(Heartfull tmux Operator), message0f6c8f6f:The user said "Do it". The live bubble showed "Do it". Whisper returned "Duet", the bubble
cleared, and the thread got a word the user never said — which reads exactly like the message
vanishing, because the thing on screen is not theirs.
It broke the conversation, not just the record
The turn before, the agent asked: "…shall I do that now?" — a yes/no. The user answered "Do it".
The agent received "Duet", could not resolve it, and replied by re-listing the same five tmux
sessions it had already listed twice. The task never started.
The pattern across the whole thread — 4 of 5 voice turns are wrong
Is tmux ready?Yes Timo is ready is the agent replying in the terminal or notHeartfullDo you see the console output?Do you see the console output what's there right nowcontext:SendDuetDo itThe browser gate was more accurate than Whisper on every turn where they disagreed, and the
platform always ships Whisper's reading and files the gate's into a footnote badge.
Root cause: a tmux agent is given no vocabulary at all
buildTranscribePrompt(packages/sdk/src/voice/prompt.ts:27) gates its term lists on surfaces:This instance is
surfaces: ["tmux"],runtime: "coding".tmuxis in neither branch, soCODING_TERMSis skipped. The call site passes the instance name asextra(
InstanceDetail.tsx:237), so the entire transcription bias for this agent is the literal string"Heartfull (tmux)"— its own name, and nothing else.Meanwhile
CODING_TERMSalready contains the exact words this user says to a terminal agent:console, terminal, git, npm, pnpm, commit, branch, deploy, build, lint, endpoint. They are sitting
in the file, unused, because the gate asks about the surface rather than the runtime.
That is why
Do you see the console output…lost half its sentence, and why two-word utterances hadnothing at all steering them.
tmuxshould be in the coding branch — a one-line change — and thegate is arguably reading the wrong axis entirely, since
runtime: "coding"was true here the wholetime.
Note the second-order risk this creates: the ONE term the agent is biased toward is a proper noun,
which is precisely the #332 failure mode (a proper noun in the bias list coming back verbatim on a
low-information clip).
isTranscribeBiasEchowould catchHeartfull (tmux)because it ismulti-word — but it deliberately passes single-word echoes, so a one-word agent name would not
be caught.
The guard that exists is switched off for exactly these turns
dictationDiverged(packages/sdk/src/voice/machine.ts:322) is what flags a transcript asmaterially short:
"Do it"→h.length === 2→ false. No warning."Send"→h.length === 1→ false. No warning.So the two turns that came back as pure nonsense are the two the guard is contractually forbidden to
comment on. The floor is defensible in isolation — four words is "too little signal to accuse
anything" on a volume test — but the result is that short utterances, which are the most
likely to be mistranscribed, are the only ones with no check at all.
dictationLossstill rendersa dim "2 words not in the transcript", which is not a signal anyone reads as "this is the wrong
word".
What to change
1. A short utterance needs a different test, not the volume one. When the heard capture is under
four words, compare by overlap instead: if the final transcript shares no word with what the
gate heard, that is divergence regardless of length.
Do it/DuetandSend/context:both tripit;
colour/colorand ordinary engine disagreement do not, because they overlap.2. Prefer the gate's reading when the final is a bare unknown token. The gate got it right every
time here. At minimum, a one-tap "use what I heard instead" on the bubble — the data is already
stored (
dictation) and already rendered behind theShow what was heardtoggle(
SpokenMessage.tsx); today it is archaeology, not a correction path.3. Re-examine the bias prompt.
context:is a prompt fragment surfacing as a transcript. #332treated the symptom (agent names); the general shape — a low-information clip echoing the prompt —
is still live and produces text the user cannot distinguish from their own words.
Related
#364 (the composer showed nothing while this happened, so the live capture was only visible in the
thread bubble), #332 (prompt-token echo), #319 (the stored live capture that made this diagnosable at
all — without it this report would have been unanswerable).