From f3abb8125f7830455f790111e6f11f5f5a8bf055 Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Fri, 3 Jul 2026 01:07:05 +0100 Subject: [PATCH] fix: stop double-emitting ovos.utterance.handled from fallback/converse when core owns it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The matched-intent path already guards its ovos.utterance.handled emission on _core_owns_utterance_handled() (PIPELINE-1 §9.5 — ovos-core >=2.3.0a1 owns the universal end-marker on every terminal path). The FallbackSkill and ConversationalSkill terminal paths still emitted it unconditionally, so against a core that owns it the end-marker fired twice (skill + orchestrator). Guard both on the same version check so exactly one ovos.utterance.handled terminates the utterance; keep emitting for an older/absent core during the migration window. Co-Authored-By: Claude Opus 4.8 (1M context) --- ovos_workshop/skills/converse.py | 15 ++++++++++----- ovos_workshop/skills/fallback.py | 9 +++++++-- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/ovos_workshop/skills/converse.py b/ovos_workshop/skills/converse.py index 08110785..373a3dfb 100644 --- a/ovos_workshop/skills/converse.py +++ b/ovos_workshop/skills/converse.py @@ -14,7 +14,7 @@ from ovos_workshop.decorators.killable import AbortEvent, killable_event, AbortQuestion from ovos_workshop.resource_files import ResourceFile -from ovos_workshop.skills.ovos import OVOSSkill +from ovos_workshop.skills.ovos import _core_owns_utterance_handled, OVOSSkill class ConversationalSkill(OVOSSkill): @@ -200,10 +200,15 @@ def _handle_converse_request(self, message: Message): response_message.data["error"] = repr(e) self.bus.emit(response_message) - if is_latest: - self.bus.emit(message.forward(SpecMessage.UTTERANCE_HANDLED)) - else: - self.bus.emit(message.reply(SpecMessage.UTTERANCE_HANDLED)) + if not _core_owns_utterance_handled(): + # PIPELINE-1 §9.5: the orchestrator owns ovos.utterance.handled. With a + # core that emits it on every terminal path (>=2.3.0a1) we must not also + # emit it here, or consumers see the end-marker twice; only emit for an + # older/absent core during the migration window. + if is_latest: + self.bus.emit(message.forward(SpecMessage.UTTERANCE_HANDLED)) + else: + self.bus.emit(message.reply(SpecMessage.UTTERANCE_HANDLED)) def _handle_converse_intents(self, message): """ called before converse method diff --git a/ovos_workshop/skills/fallback.py b/ovos_workshop/skills/fallback.py index 3ebf9002..a192b376 100644 --- a/ovos_workshop/skills/fallback.py +++ b/ovos_workshop/skills/fallback.py @@ -23,7 +23,7 @@ from ovos_utils.skills import get_non_properties from ovos_workshop.decorators.killable import AbortEvent, killable_event -from ovos_workshop.skills.ovos import OVOSSkill +from ovos_workshop.skills.ovos import _core_owns_utterance_handled, OVOSSkill class FallbackSkill(OVOSSkill): @@ -158,7 +158,12 @@ def _handle_fallback_request(self, message: Message): self.bus.emit(message.forward( f"ovos.skills.fallback.{self.skill_id}.response", data={"result": status, "fallback_handler": handler_name})) - self.bus.emit(message.forward(SpecMessage.UTTERANCE_HANDLED)) + if not _core_owns_utterance_handled(): + # PIPELINE-1 §9.5: the orchestrator owns ovos.utterance.handled. With a + # core that emits it on every terminal path (>=2.3.0a1) we must not also + # emit it here, or consumers see the end-marker twice; only emit for an + # older/absent core during the migration window. + self.bus.emit(message.forward(SpecMessage.UTTERANCE_HANDLED)) def register_fallback(self, handler: Callable, priority: int) -> None: """