Skip to content

Commit 6ff7ea8

Browse files
JarbasAlclaude
andcommitted
fix(stop): rely on bus bridge for legacy mycroft.stop; drain active_skills
Address CodeRabbit review on #777 (OVOS-STOP-1 conformance): - handle_global_stop: drop the hand-rolled `mycroft.stop` emit. The spec-tools MIGRATION_MAP bridges `ovos.stop -> mycroft.stop` and the bus re-emits the legacy counterpart by default (emit_legacy ON during the migration window), so the manual emit double-delivered to anyone on both topics. Same fix as ovos-audio #171. Also switch the bare `ovos.utterance.handled` string to `SpecMessage.UTTERANCE_HANDLED` for a single source of truth (the only other spec-topic member in this file; `mycroft.*` / `ovos.skills.converse.force_timeout` are not SpecMessage members and stay as strings). - _drain_global_stop_session: also clear `session.active_skills` (the legacy recency list `get_active_skills` reads for stop-target routing), atomically with active_handlers/converse_handlers/response_mode. Left stale, a committed global_stop could route a later targeted stop to a skill it already drained. The two CI/pyproject nitpicks (dead bus-client git-ref + floor bump) were already resolved upstream by 851c6e1 + 87b940d. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 87b940d commit 6ff7ea8

2 files changed

Lines changed: 27 additions & 21 deletions

File tree

ovos_core/intent_services/stop_service.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -82,21 +82,18 @@ def handle_global_stop(self, message: Message) -> None:
8282
OVOS-STOP-1 §5.3: the global-stop handler emits the spec broadcast
8383
``ovos.stop`` (``SpecMessage.STOP``).
8484
85-
Back-compat: the MIGRATION_MAP bus bridge only re-delivers ``ovos.stop``
86-
on the legacy ``mycroft.stop`` topic when the bridge's legacy direction is
87-
enabled, which is NOT guaranteed (it is an opt-in on MessageBusClient and
88-
off in the pure-spec path). Skills have NOT migrated their stop handler —
89-
ovos-workshop still subscribes ONLY ``mycroft.stop`` — so a spec-only
90-
broadcast would silently fail to stop them. We therefore also emit the
91-
legacy ``mycroft.stop`` directly until the skill side migrates, mirroring
92-
the back-compat per-skill ``{skill_id}.stop.ping`` kept in
93-
``_collect_stop_skills``. The two topics target disjoint subscriber sets
94-
(spec vs un-migrated), so this is not a double broadcast to any one skill.
85+
Back-compat: ``mycroft.stop ↔ ovos.stop`` is a payload-compatible rename
86+
in the spec-tools MIGRATION_MAP, and the bus bridge re-delivers a spec
87+
emit on its legacy counterpart by default (``emit_legacy`` defaults ON
88+
during the migration window). So emitting ``ovos.stop`` ALSO reaches
89+
un-migrated skills still listening on ``mycroft.stop`` — without us
90+
hand-rolling a second emit. A manual ``mycroft.stop`` here would
91+
double-deliver to anyone subscribed on both topics, so we rely on the
92+
bridge (same fix as ovos-audio #171).
9593
"""
9694
self.bus.emit(message.forward(SpecMessage.STOP))
97-
self.bus.emit(message.forward("mycroft.stop"))
9895
# TODO - this needs a confirmation dialog if nothing was stopped
99-
self.bus.emit(message.forward("ovos.utterance.handled"))
96+
self.bus.emit(message.forward(SpecMessage.UTTERANCE_HANDLED))
10097

10198
@staticmethod
10299
def _drain_global_stop_session(session) -> "Session":
@@ -107,13 +104,17 @@ def _drain_global_stop_session(session) -> "Session":
107104
108105
- ``active_handlers`` → ``[]`` (§5.2 / §6.2)
109106
- ``converse_handlers`` → ``[]`` (§5.2 / §6.2, OVOS-CONVERSE-1 §2.1)
107+
- ``active_skills`` → ``[]`` (legacy recency list read by
108+
``get_active_skills`` → stop-target routing; left stale it could
109+
route a later targeted stop to a skill this global_stop drained)
110110
- ``response_mode`` → absent (§5.2 / §6.1)
111111
112-
All three are cleared atomically at match time so the drained state is
112+
All four are cleared atomically at match time so the drained state is
113113
committed before dispatch (PIPELINE-1 §4.2).
114114
"""
115115
session.active_handlers = []
116116
session.converse_handlers = []
117+
session.active_skills = []
117118
session.clear_response_mode()
118119
return session
119120

test/unittests/test_stop_service.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from ovos_bus_client.message import Message
2020
from ovos_bus_client.session import Session, SessionManager, UtteranceState
21+
from ovos_spec_tools.messages import SpecMessage
2122
from ovos_utils.fakebus import FakeBus
2223

2324
from ovos_core.intent_services.stop_service import StopService
@@ -548,20 +549,20 @@ class TestBusHandlers(unittest.TestCase):
548549

549550
def test_handle_global_stop_emits_ovos_stop(self):
550551
# OVOS-STOP-1 §5.3: global-stop handler emits the spec topic ``ovos.stop``.
551-
# Back-compat: it ALSO emits the legacy ``mycroft.stop`` directly, because the
552-
# spec->legacy bus bridge is not guaranteed (opt-in / off on the pure-spec
553-
# path) and skills have not migrated their stop handler off ``mycroft.stop``.
552+
# Back-compat to un-migrated ``mycroft.stop`` listeners is provided by the
553+
# bus MIGRATION_MAP bridge (emit_legacy ON by default), NOT a manual
554+
# second emit — hand-rolling ``mycroft.stop`` here would double-deliver to
555+
# anyone on both topics (cf. ovos-audio #171).
554556
svc = _make_service()
555557
emitted = []
556558
svc.bus.emit = lambda m: emitted.append(m)
557559
msg = Message("stop:global", {})
558560
svc.handle_global_stop(msg)
559561
types = [m.msg_type for m in emitted]
560562
self.assertIn("ovos.stop", types)
561-
self.assertIn("mycroft.stop", types)
562-
self.assertIn("ovos.utterance.handled", types)
563-
# the spec broadcast is emitted before the legacy back-compat one
564-
self.assertLess(types.index("ovos.stop"), types.index("mycroft.stop"))
563+
self.assertIn(SpecMessage.UTTERANCE_HANDLED.value, types)
564+
# the handler does NOT hand-roll a legacy emit; the bridge mirrors it
565+
self.assertNotIn("mycroft.stop", types)
565566

566567
def test_handle_skill_stop_forwards_to_skill(self):
567568
svc = _make_service()
@@ -695,10 +696,11 @@ def test_targeted_stop_clears_target_response_mode_only(self):
695696

696697
def test_global_stop_drains_both_lists_and_response_mode(self):
697698
"""§5.2: global_stop updated_session sets active_handlers=[],
698-
converse_handlers=[], and removes response_mode."""
699+
converse_handlers=[], active_skills=[], and removes response_mode."""
699700
sess = Session("s")
700701
sess.active_handlers = [{"skill_id": "a", "activated_at": 1.0}]
701702
sess.converse_handlers = [{"skill_id": "b", "activated_at": 1.0}]
703+
sess.active_skills = [["a", 1.0]]
702704
sess.set_response_mode("c", 9999999999.0)
703705

704706
with patch.object(self.svc, "voc_match",
@@ -711,6 +713,9 @@ def test_global_stop_drains_both_lists_and_response_mode(self):
711713
self.assertEqual(result.match_type, "stop:global")
712714
self.assertEqual(result.updated_session.active_handlers, [])
713715
self.assertEqual(result.updated_session.converse_handlers, [])
716+
# §5.2: the legacy recency list is drained too, so a later targeted stop
717+
# cannot route to a skill this global_stop already cleared
718+
self.assertEqual(result.updated_session.active_skills, [])
714719
self.assertIsNone(result.updated_session.response_mode)
715720

716721
def test_global_stop_no_positive_pong_drains_session(self):

0 commit comments

Comments
 (0)