Owner decision, recorded as an invariant
"Mute should be available at any time. I should be able to mute at any moment — and unmute
also."
Written up as ADR 0001 — Mute is available at every moment of a voice session
(docs/adr/0001-mute-is-always-available.md), which also establishes the ADR convention for this
repo (docs/adr/README.md). platform-docs/voice.md now documents the invariant, the full command
vocabulary, the settings hierarchy, the hands-free lifecycle, the guards and the tunables, and links
the ADR from the place someone would break it.
This issue is the enforcement half: the invariant currently rests on nothing but the code
happening to be right today, and it has already been one review comment away from being removed.
The four rules (full text in the ADR)
- M1 — mute reachable by voice AND touch during
listening / transcribing / processing /
speaking, and unmute while muted. No phase is a dead zone.
- M2 — mute silences both directions at once: mic closed, in-flight speech cancelled, queue
dropped.
- M3 — no condition may reduce to "not while the agent is speaking". Echo/pause guards must
not be applied wholesale to the control path.
- M4 — unmute is symmetric and matched by the one listener that runs while muted.
Why this needs a test and not just a document
The near-miss is on record. #386 is a real defect — the control listener has no echo guard — and
its first proposed fix was "drop control results while isEchoing". That is one line, it reuses an
existing tested helper, it matches three neighbouring call sites, and it deletes mute-during-TTS,
which is the entire reason #153 built that listener. Nothing in the type system, the tests, or the
file being edited would have objected.
Do
Explicitly out of scope
exit and next are not covered by ADR 0001. They end or redirect a session rather than
quieting it, and holding them to a stricter bar (final-only, whole-utterance, or confirmation) is
legitimate — see #385 and #386. Mute is the one that must never be hard to reach, because it is what
a user reaches for when something has already gone wrong.
Related: #153 (built the listener), #386 (fix the echo defect without breaking this),
#385 (blank-field semantics + phrase collisions), #387 (hands-free ending silently).
Owner decision, recorded as an invariant
Written up as ADR 0001 — Mute is available at every moment of a voice session
(
docs/adr/0001-mute-is-always-available.md), which also establishes the ADR convention for thisrepo (
docs/adr/README.md).platform-docs/voice.mdnow documents the invariant, the full commandvocabulary, the settings hierarchy, the hands-free lifecycle, the guards and the tunables, and links
the ADR from the place someone would break it.
This issue is the enforcement half: the invariant currently rests on nothing but the code
happening to be right today, and it has already been one review comment away from being removed.
The four rules (full text in the ADR)
listening/transcribing/processing/speaking, and unmute whilemuted. No phase is a dead zone.dropped.
not be applied wholesale to the control path.
Why this needs a test and not just a document
The near-miss is on record. #386 is a real defect — the control listener has no echo guard — and
its first proposed fix was "drop control results while
isEchoing". That is one line, it reuses anexisting tested helper, it matches three neighbouring call sites, and it deletes mute-during-TTS,
which is the entire reason #153 built that listener. Nothing in the type system, the tests, or the
file being edited would have objected.
Do
packages/sdk/src/voice/convo.test.ts—matchVoiceCommandreturnsmutefor a configured mute phrase with
ttsSpeaking: true, andunmutewithmuted: true. Pure,cheap, and the first thing to fail on a regression.
there, a deliberate mute utterance during TTS still dispatches
mute. This is the testthat would have caught [bug] The agent's own voice can issue commands — the always-on control listener is the only path with no echo guard, and a partial "stop" exits hands-free #386's first draft.
handleControlResult(packages/sdk/src/voice/use-voice.ts:834) pointing atADR 0001 — one hop from the place the constraint would be broken.
reachable in every phase on the Assistant tab and the Coder Co-pilot, including while the
agent is speaking and on a phone. M1 requires two channels; today the voice channel is the
one that is documented and the touch channel is the one that is assumed.
SpeechRecognition,ensureControlSttreturnsnulland voice-mute during TTS cannot exist —the invariant then rests entirely on the on-screen control. Options: accept it and make the
touch control unmissable in that case; detect it and say so once in the UI ("voice commands
are not available in this browser") rather than letting configured commands silently do
nothing; or add a non-Web-Speech fallback. This one is a decision, not a task.
Explicitly out of scope
exitandnextare not covered by ADR 0001. They end or redirect a session rather thanquieting it, and holding them to a stricter bar (final-only, whole-utterance, or confirmation) is
legitimate — see #385 and #386. Mute is the one that must never be hard to reach, because it is what
a user reaches for when something has already gone wrong.
Related: #153 (built the listener), #386 (fix the echo defect without breaking this),
#385 (blank-field semantics + phrase collisions), #387 (hands-free ending silently).