Skip to content

[bug] "stop stop" exits hands-free instead of stopping speech — a blank exit field means "use ours", and the built-in outranks the phrase the user bound themselves #385

Description

@serge-ivo

Reported

In hands-free, saying "stop stop" exited hands-free mode. In my preferences "stop stop" is the
stop-speech keyword. My exit command is empty — I never set one. An empty command should
not apply at all.

Correct on every point. Live account config (GET /v1/preferences):

"exitWords":         [],            ← never set
"stopSpeechKeyword": "stop stop",   ← explicitly set, to stop SPEECH
"muteWords": ["mute mute"], "repeatWords": ["Repeat that"], "stopWords": ["Copy Copy"]

What actually happens

Both dispatch sites check stop-speech first, and that check is gated on the agent currently
speaking
(convo.ts matchesStopSpeech: if (!s.ttsSpeaking) return false). So:

when you say "stop stop" what runs result
agent is speaking matchesStopSpeechtts.cancel() speech stops — what you configured
agent is not speaking (mic hot, thinking, muted) falls through to matchVoiceCommandexit hands-free torn down

The fall-through happens because pick() (convo.ts:384) treats an empty list as unset:

const pick = (custom, table) => custom?.length ? custom : (table[l] ?? table.en);

exitWords: [] → length 0 → built-in EXIT_BY_LANG.en, which contains "stop" and
"stop stop"
(added by #331, to fix bare "stop" being sent to the agent as chat).

So one phrase has two meanings, selected by whether TTS happens to be playing at that instant —
and the meaning the user never chose is the destructive one.

Three defects, in order of how much they cost

1. A blank field means the opposite thing on the same panel. VoiceFields.tsx:

:381  exitWords          placeholder "exit voice, text mode  (blank = built-in defaults)"
:396  stopWords          placeholder "e.g. copy, over  (blank = off)"
:401  stopSpeechKeyword  placeholder "e.g. stop stop  (blank = off)"

Three adjacent inputs, two opposite conventions, documented only in placeholder text that
disappears the moment you type. repeat/mute/unmute/exit/next/scrap all fall back to
built-ins; stopWords/stopSpeechKeyword are off. The user's expectation — blank means off — is
the convention two of the five follow.

2. The UI suggests the colliding phrase. The stop-speech placeholder is literally
e.g. stop stop. The product recommends binding a phrase that the built-in exit list already owns,
and nothing anywhere warns that the phrase is taken. There is no collision check at save time and
no ranking rule — the built-in simply wins whenever the gentler check is inactive.

3. Exit cannot be turned off at all. Blank → built-ins. The only way to stop "stop" from
ending your voice session is to bind exitWords to some other phrase, i.e. you must keep the
feature and move it. There is no off.

Why the built-in exists (keep the reason, change the mechanism)

#331 added bare "stop"/"stop stop" for a real failure: the words matched nothing, went to the
agent as chat, and it replied "Got it. Stopped." — confirming an action nobody performed while
the mic stayed open. That reasoning stands. What it did not anticipate is a user binding the same
phrase to a different action, and the destructive one silently outranking it.

Suggested fix

  1. Blank = off, for every command list. One convention across the panel. Keep the built-ins
    discoverable as a "Use suggested" button that fills the field with them — so defaults are
    visible, editable and removable, instead of invisible and mandatory.
  2. Refuse the collision. A phrase already bound to another action should be rejected on save
    with the conflict named, or — at minimum — the explicit user binding must win over any built-in.
    Today the opposite is true.
  3. Change the stop-speech placeholder, whichever way (1) goes: suggesting stop stop is
    actively steering users into this.
  4. Consider making the stop-speech keyword work when TTS is not playing too, as a no-op rather
    than a fall-through — the gate is what makes the same phrase mean two things.

The decision inside this is (1): does a blank command field mean "use ours" or "off"? It changes
behaviour for anyone who left a field blank — today they have built-in exit words they never chose,
and after the change they would have none. Recommendation: blank = off, with the "Use suggested"
button, because the current behaviour cannot be turned off and its most common word is "stop".

Files: packages/sdk/src/voice/convo.ts (pick :384, EXIT_BY_LANG :~180, matchesStopSpeech
:471), packages/sdk/src/voice/use-voice.ts:836,850,891,1126,
store/console/src/components/VoiceFields.tsx:381,396,401, packages/sdk/src/voice/config.ts:141.

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