Skip to content

[bug] "Do it" is transcribed "Duet" and "go, send" becomes "context:" — a tmux agent gets no vocabulary bias, and the divergence guard is off below four words #371

Closed

Description

@serge-ivo

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:

export function dictationDiverged(heard: string, final: string): boolean {
	const h = words(heard);
	if (h.length < 4) return false;          // ← silent below four heard words
	return words(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).

Activity

  1. added
    bugSomething isn't working
    voiceVoice / STT / TTS
    on Aug 7, 2026
  2. serge-ivo commented on Aug 7, 2026

    @serge-ivo
    ContributorAuthor

    Self-correction on point 3 of "What to change" — it contradicts the root-cause section above it and should be struck.

    I first assumed context: was a bias-prompt fragment coming back (the #332 shape), and wrote point 3 on that basis. Then I read prompt.ts and disproved it: for this instance the prompt is the single string "Heartfull (tmux)", because surfaces: ["tmux"] misses both term-list branches. The word context is not in the bias list, so it cannot be an echo of it.

    context: is just a bad transcription of a ~1-second clip with no vocabulary steering it — the same failure as Do it → Duet, not a separate mechanism. The trailing colon is suggestive but is not evidence of anything I can substantiate.

    So the fixes are points 1 and 2, plus the root cause: put tmux in the coding-terms branch (or gate on runtime rather than surfaces). Point 3 as written asks someone to hunt a leak that this investigation ruled out.

    Worth keeping from it, restated honestly: #332's guard passes single-word echoes deliberately, so if an agent's name is one word it would still surface verbatim on a silent clip. That is a real gap, but it is not what happened here.

  3. serge-ivo commented on Aug 7, 2026

    @serge-ivo
    ContributorAuthor

    Follow-ups filed for the vocabulary side of this: #372 (derive the proper nouns the platform already holds — tmux targets, repo names, subordinate names — into the prompt's existing extra slot, which today receives only the instance name) and #373 (a user-editable vocabulary list at profile + agent scope).

    Worth recording here because it constrains both: the browser Web Speech recogniser cannot be given a vocabulary at all. There is no SpeechGrammarList in packages/sdk/src, and adding one would not help — the API is specced but Chrome's default recogniser ignores JSGF grammars. Since the browser gate is what returned Timo for tmux in this very thread, a bias-only solution cannot fix the live-dictation half. #373 handles that with a post-transcript fuzzy correction over the same list, which is the only lever that reaches both engines.

  4. serge-ivo commented on Aug 7, 2026

    @serge-ivo
    ContributorAuthor

    Fixed in bb11a69.

    Which layer failed: transcription, both times — not the command matcher.

    Send → context: looked like it might be a command-matching problem. It is not: there is no send in convo.ts's closed vocabulary (repeat/mute/unmute/exit/next/scrap), so "send" was never a candidate for matching and was always going to be sent as speech. Do it → Duet is the same layer. Both are the decoder returning a word for a short clip it had no vocabulary to constrain.

    Root cause 1 — the gate read the wrong axis. buildTranscribePrompt gated on surfaces; a tmux operator is surfaces: ["tmux"], in neither branch, so CODING_TERMS was skipped for an agent whose runtime was "coding" the whole time. Both axes are read now, plus a new TMUX_TERMS list carrying tmux, pane, detach, stdout — the shell nouns with a strong wrong English prior. Deliberately excluded: run, open, list, show — said constantly, transcribed correctly, and single words, which is the shape that comes back as a phantom turn under #332 for no accuracy gain.

    Root cause 2 — the guard was off for exactly these turns. dictationDiverged is a volume test, silent below four heard words, so the two turns that came back as nonsense were the two it was forbidden to comment on. Below four words it now judges by OVERLAP: no word in common means the two engines did not hear the same utterance, at any length. colour/color does not trip it — nearWord (new voice/vocabulary.ts) counts a spelling variant as the same word, with 0 edits allowed below four letters so go/no stay different. The volume test is untouched above four words.

    Both reported turns now flag on the bubble (SpokenMessage renders the warning icon off this predicate).

    Item 3 of the ticket ("prefer the gate's reading" / a one-tap "use what I heard instead") is NOT done — it needs an edit-and-resend path on a sent message, which is a bigger change than either fix here and belongs in its own ticket. What ships is that the divergence is now stated rather than silently rendered as a dim word count.

    The bias prompt was re-examined as asked (#372/#373, bfbfd29): the term-entry rule and the #332 interaction are written down in packages/sdk/src/voice/prompt.ts.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingvoiceVoice / STT / TTS

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions