From 1d39ad45409b66ce3a94bacbb130c6725ad86b3b Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sun, 28 Jun 2026 04:46:19 +0100 Subject: [PATCH] fix: stop emitting ovos.utterance.handled when ovos-core owns it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PIPELINE-1 §9.5 makes ovos.utterance.handled the orchestrator's universal end-marker, emitted by ovos-core on EVERY terminal path (matched included). ovos-core began emitting it on the matched path in 2.3.0 (the change landed on the 2.2.x line; by semver the first release carrying it is the next minor, 2.3.0a1). Guard the framework's matched-path emission on a version check so it only fires for an older/absent core during the migration window; with a core that owns it the framework no longer double-emits. Co-Authored-By: Claude Opus 4.8 --- ovos_workshop/skills/ovos.py | 29 ++++++++++++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/ovos_workshop/skills/ovos.py b/ovos_workshop/skills/ovos.py index 9e871093..86edb626 100644 --- a/ovos_workshop/skills/ovos.py +++ b/ovos_workshop/skills/ovos.py @@ -68,6 +68,30 @@ from ovos_workshop.skills.util import join_word_list, simple_trace +def _core_owns_utterance_handled() -> bool: + """PIPELINE-1 §9.5: ``ovos.utterance.handled`` is the orchestrator's universal + end-marker — ovos-core owns it on EVERY terminal path, the matched path included. + + ovos-core began emitting it on the matched path in **2.3.0** (the §9.5 change + landed on the 2.2.x line, so by semver the first release carrying it is the next + minor, 2.3.0a1). When the installed core is that version or newer this returns + ``True`` and the skill framework must NOT also emit it, or consumers would see + the end-marker twice. + + During the migration window an older core (or none, e.g. a workshop-only test + env) does not emit it on the matched path, so the framework still must. This + returns ``False`` then — including when core is absent/unknown — so the framework + keeps emitting (back-compat). Double emits are tolerated by spec consumers, so + erring toward the framework emitting is the safe default. + """ + try: + from importlib.metadata import version + from packaging.version import parse + return parse(version("ovos-core")) >= parse("2.3.0a1") + except Exception: + return False # core absent/unknown -> framework keeps emitting (back-compat) + + class OVOSSkill: """ Base class for OpenVoiceOS skills providing common behaviour and parameters @@ -1473,7 +1497,10 @@ def _on_event_end(self, message: Message, handler_info: str, # the shared HandlerLifecycle util (same topic/payload/context). HandlerLifecycle(self.bus, message, skill_id=self.skill_id, data=skill_data, handler_info=handler_info).complete() - if is_intent: + if is_intent and not _core_owns_utterance_handled(): + # PIPELINE-1 §9.5: the orchestrator owns ovos.utterance.handled. With a + # core that emits it on the matched path (>=2.3.0a1) we must not also + # emit it; only emit for an older/absent core during the migration window. self.bus.emit(message.forward(SpecMessage.UTTERANCE_HANDLED, skill_data)) try: