Skip to content

"<wake word>, stop": cancel the run it drops; optionally end listening (stop_ends_listening, off by default) - #56

Merged
Gamer92000 merged 2 commits into
Gamer92000:mainfrom
Nils3311:stop-ends-listening
Oct 21, 2026
Merged

Gamer92000 merged 2 commits into
Gamer92000:mainfrom
Nils3311:stop-ends-listening

Conversation

@Nils3311

Copy link
Copy Markdown
Contributor

Closes #55. Builds on #51 / f8926ab.

Two commits; the first can be merged on its own

1. "<wake word>, stop" cancels the run it drops instead of ending it (always on, no setting)

stop_word's stop: pipeline dropped branch handles a "stop" within 4 s of the wake word cutting a reply, a ring
or a THINKING run. It used to end the new run with core_pipeline_finish(), which sends end of audio and then
start=false. Nothing marked the run as cancelled (after a THINKING cancel, the new run's RUN_START clears the
mark), so hassmic kept taking its events. HA's STT_START a moment later put the Echo back to LISTENING with the mic
closed, which the tests reproduce on main. Once, on a device running a fork build, HA also carried out the "stop"
from the replayed audio. That is plausibly because the end of audio (_stop_pipeline) arrives before the abort.

It now uses quiet_cancel() instead, the same path as the button: proto->cancel() (start=false alone, HA's
_abort_pipeline), events dropped until the next RUN_START, and quiet_abort. The button's cancel and the wake
word's cancel in trigger() use the helper too. Wyoming has no cancel, so it keeps the old path.

Why this is safe without a setting: upstream's own comment says this run "is dropped again". It never meant to send
that audio to Home Assistant. The abort is what the button already sends.

2. "<wake word>, stop" can end listening (setting stop_ends_listening, off by default)

A new setting on the settings page (Voice): "Stop after the wake word ends listening", page only, exported, off by
default
. When it is off, the behaviour is exactly upstream's. When it is on, everything f8926ab does happens first
(Drop In hang-up, ring off, reply cut, Sendspin or Bluetooth paused), and then:

  • stop_word: a "stop" within 4 s of the last detection (heard_ms, stop_ms, stopped_now()) cancels a
    LISTENING run with quiet_cancel() (stop: listening ended). That includes a run that was listening before this
    wake word.
  • trigger: when the "stop" came during the arbitration round, nothing starts (stop: came during the round, nothing started). A THINKING run that the wake word cancels is not followed by a new one.

Known limitation with the setting off (unchanged from main): a STOP that lands during the round while the Echo is
THINKING still gets a fresh run, because stop_word sees THINKING and the cancel and restart happen after it.

Why it is off by default: on one Echo over 7 days, STOP came in 24 of 700 wake words, and 2 of them were false in
354 answered requests. Also, ", stop the timer" no longer reaches Home Assistant when it is on.

Docs: CHANGELOG (2026-10-20), GUIDE ("Alexa, stop"), PLAN, the setting's help in web/app.js, and the test lists in
CLAUDE.md and DEVELOPMENT.md.

Test plan

  • tests/run.py -j 8 in Docker: 26 of 28 suites pass. unit (unit-sbc segfaults in an arm64 container) and
    update (no .git for the version) fail exactly as on main (a4fe0bd: 24 of 27, those two plus a flaky
    dropin envelope check that passed on both runs of this branch). Commit 1 alone: 25 of 27, the same two.
  • New tests/fake_ha_stop_word.py (stop-word, two Echos in one network):
    • with the setting off, a stop out of silence leaves the run to HA;
    • THINKING + wake word + stop gives one abort, and a late STT_START opens nothing;
    • with the setting on: stop out of silence; stop during the round (nothing starts); both Echos hear it (no run
      left); listening + wake word + stop; thinking + wake word + stop, both during the round and after the new run
      started; a stop 4.5 s after the wake word changes nothing.
  • fake_ha_esphome_timers.py: ring + wake word + stop gives stopped == [True], and after a late STT_START the
    next wake word starts a run. This fails on main, which is the bug. The fake HA now sends RUN_START after a
    cancelled run, as HA does.
  • fake_ha_esphome.py: reply cut + wake word + stop gives one abort.
  • fake_ma_sendspin.py: f8926ab's stop_music part unchanged with the setting off, plus an "on" part (music
    paused, run cancelled, nothing restarted).
  • tests/arbecho.py: records handle_stop and sends RUN_START on each start, as HA does. All arbitration suites
    still pass.
  • make DEVICE=biscuit STUBS=1 and make DEVICE=donut STUBS=1 build without warnings.
  • On a device: this exact build has not been run on one yet. The same stop logic ran on three Echo Dot 2s in a
    fork: listening ended 5 to 7 ms after Pryon's STOP.

🤖 Generated with Claude Code

Nils3311 and others added 2 commits October 21, 2026 21:54
stop_word's "pipeline dropped" (a "stop" within 4 s of the wake word cutting a reply, a ring or a THINKING
run) ended the new run with core_pipeline_finish: end of audio, then start=false.  Nothing marked the run
cancelled (after a THINKING cancel the new run's RUN_START clears the mark), so its events kept coming:
Home Assistant's STT_START a moment later put the Echo back to LISTENING with the mic closed.  Seen once on
a device, the "stop" in the replayed audio was also carried out (a media player elsewhere paused): the end
of audio reaches Home Assistant before the abort, and its STT may finish on what it has.

Now it is cancelled as the action button does (quiet_cancel, which the button's and the wake word's
cancel in trigger() use too): start=false alone, the run's events dropped until the next RUN_START,
quiet_abort.  Wyoming has no cancel: ended there as before.

Tests: fake_ha_esphome_timers.py (ring + wake word + stop: one abort, a late STT_START leaves the Echo
idle, the next wake word starts a run; fails before), fake_ha_esphome.py (reply cut + stop: one abort).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ff by default)

An Echo that woke up by mistake (the TV, talk in the room) listens for up to 15 s (Home Assistant's VAD
limit), and Home Assistant carries out whatever speech to text made of it; the only quiet way out was the
action button.  With the new setting stop_ends_listening (Voice, page only, exported, off by default),
Pryon's "stop" right behind the wake word also ends listening, after everything f8926ab's stop does
(Drop In, ring, reply, music on the Echo itself):

- stop_word: a "stop" within 4 s of the last detection (heard_ms, set in on_wake and for SIGUSR1;
  stop_heard) cancels a LISTENING run as the button does (quiet_cancel; "stop: listening ended"), also
  one that was listening before this wake word.
- trigger: when the "stop" came during the arbitration round, the winner starts nothing ("stop: came
  during the round, nothing started"), and a THINKING run the wake word cancels is not followed by a new
  one.

Always, setting or not: a "stop" that ended what its wake word cut drops that wake word's run also when
it comes during the round.  The "pipeline dropped" rule only saw runs that had started; a "stop" in the
round (window up to 1 s, all of it when no other Echo heard the wake word) found the Echo IDLE or
THINKING, and the winner then opened a run that listened, the "stop" in its replayed audio (PC test:
ring, wake word, "stop" 50 ms later -> alarm off, idle -> listening).  stop_word notes stop_cut (it ended
a ring or a call, cut a reply, found THINKING, or dropped a listening run); trigger's stopped_now = its
"stop" came, and stop_cut or the setting; a reply that began before the round ended is cut, not barged
into (that listened anew).  Music the "stop" paused does not count (Gamer92000#51: its run goes on).  Drop In had the gap
after the round too: trigger set wake_cut_ms for a ring only, so "<wake word>, stop" in a call hung up
and its run listened on; now for a call as well (dropin_was, under the lock).

ESPHome only (Wyoming has no cancel).  One Echo's log over 7 days: "stop" in 24 of 700 wake words, 2 of
them false in 354 requests it answered (a short command heard as "stop": lost, said again), hence off
by default.

Tests: fake_ha_stop_word.py (stop-word, two Echos: off: out of silence as before, ring + stop and
thinking + stop during the round; on: out of silence, during the round, both Echos, listening + wake
word + stop, thinking + wake word + stop, stop after 4.5 s); fake_ha_dropin.py (wake word + stop in a
call after the round: hung up, one abort); fake_ma_sendspin.py: f8926ab's part with the setting off,
and on (music paused, the run cancelled).  tests/arbecho.py records stops and sends RUN_START as Home
Assistant does (after a cancel the Echo waits for it).  The new checks fail without the main.c change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Gamer92000
Gamer92000 merged commit 52b0f83 into Gamer92000:main Oct 21, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature: "<wake word>, stop" as a quiet end of a false wake

2 participants