Skip to content

feat!: OVOS-STOP-1 reserved-intent_name dispatch + separable legacy bridge - #802

Open
JarbasAl wants to merge 4 commits into
devfrom
feat/stop-1-spec
Open

feat!: OVOS-STOP-1 reserved-intent_name dispatch + separable legacy bridge#802
JarbasAl wants to merge 4 commits into
devfrom
feat/stop-1-spec

Conversation

@JarbasAl

@JarbasAl JarbasAl commented Jul 3, 2026

Copy link
Copy Markdown
Member

OVOS-STOP-1 conformance: reserved-intent_name dispatch + separable legacy bridge

Reworks the stop pipeline to implement OVOS-STOP-1 cleanly as a single spec
path, with backward compatibility isolated as one deletable unit.

The single spec design (ovos_core/intent_services/stop_service.py)

  • Targeted stopIntentHandlerMatch(match_type="<skill_id>:stop", skill_id=<skill_id>, suppress_activation=True, updated_session=<§6-drained>) (§2/§3.1). The skill is reached on <skill_id>:stop; the ovos-spec-tools NamespaceTranslator bridges it to the legacy <skill_id>.stop for un-migrated skills.
  • Global stopIntentHandlerMatch(match_type="<pipeline_id>:global_stop", skill_id=<pipeline_id>, suppress_activation=True, updated_session=<§5.2-drained>); pipeline_id="ovos-stop-pipeline-plugin" is shared across confidence tiers so exactly one ovos.stop broadcast is emitted (§3.1). handle_global_stop emits ovos.stop (§5.3).
  • The §4.1 recency target selection + §5.2/§6 session drain are reused unchanged.
  • The orchestrator now honours the new IntentHandlerMatch.suppress_activation field directly (no getattr): a suppress-activation dispatch registers no active_handlers push and no {skill_id}.activate (§6.2/§7.3). Requires ovos-plugin-manager>=2.9.0a1.

Separable back-compat (ovos_core/intent_services/stop_service_legacy.py)

_LegacyStopBridge observes ovos.intent.matched (§9.2) and re-emits the
pre-spec stop:global/stop:skill dispatch, and owns the legacy handlers that
fan out to mycroft.stop and <skill_id>.stop. It holds no place in the spec
path — the whole module plus three wiring lines in StopService are removed in
one move once skills consume <skill_id>:stop and ovos.stop directly (removal
version derived from version.py). A one-time deprecation warning is logged.

TDD (commit order)

  1. test: — both e2e suites (test_stop_spec_e2e.py, test_stop_legacy_e2e.py).
  2. feat!: — spec core (breaking: reserved-intent_name dispatch).
  3. feat: — the droppable _LegacyStopBridge.

Verified transitions: spec-core-only (no bridge) → spec 2 pass / legacy 2 fail; with the bridge → all 4 pass (xdist -n4). Unit: 302 pass.

Summary by CodeRabbit

  • New Features

    • Added OVOS-STOP-1 compliant stop handling for global and skill-specific stop requests.
    • Global stops now clear active conversations and handlers.
    • Targeted stops can deactivate a selected skill and provide confirmation handling.
    • Stop matches can suppress activation callbacks when appropriate.
  • Bug Fixes

    • Improved fallback behavior when no skill responds to a stop request.
  • Compatibility

    • Preserved support for legacy stop events and dispatch formats.

@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

StopService now implements OVOS-STOP-1 global and targeted routing. Match-level activation suppression prevents stopped skills from activating. _LegacyStopBridge translates STOP-1 matches to legacy events. Unit and end-to-end tests cover both dispatch paths.

Changes

Stop pipeline migration

Layer / File(s) Summary
Match-level activation suppression
ovos_core/intent_services/service.py, test/unittests/test_intent_service_extended.py
Intent dispatch uses suppress_activation to skip active-handler registration and activation callbacks. The stop pipeline is removed from reserved-name handling.
OVOS-STOP-1 routing
ovos_core/intent_services/stop_service.py, test/unittests/test_stop_service.py
StopService builds <skill_id>:stop and <pipeline_id>:global_stop matches. Targeted stops deactivate skills and update session state. Global stops clear active state and emit SpecMessage.STOP.
Legacy stop compatibility
ovos_core/intent_services/stop_service_legacy.py, test/unittests/test_stop_service.py, test/end2end/test_stop_legacy_e2e.py
_LegacyStopBridge translates STOP-1 matches to legacy global and skill-specific events. Tests cover filtering, lifecycle events, re-emission, and shutdown cleanup.
STOP-1 end-to-end validation
test/end2end/test_stop_spec_e2e.py
End-to-end tests validate global and targeted dispatch, suppressed activation, session draining, and expected utterance lifecycle messages.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant StopService
  participant IntentService
  participant Session
  participant _LegacyStopBridge
  participant LegacySkill
  StopService->>Session: create targeted or global stop match
  StopService->>IntentService: dispatch match with suppress_activation
  IntentService->>Session: skip activation and active-handler registration
  StopService->>_LegacyStopBridge: publish STOP-1 intent match
  _LegacyStopBridge->>LegacySkill: emit legacy stop event
  LegacySkill-->>_LegacyStopBridge: emit HandlerLifecycle events
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the OVOS-STOP-1 dispatch change and the separate legacy compatibility bridge.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/stop-1-spec

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added breaking breaks backwards compatibility (kinda) feature labels Jul 3, 2026
@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

Tada! The results of the latest automation run are here. 🎉

I've aggregated the results of the automated checks for this PR below.

📚 Docs

Processing complete! Details follow. 📬

✅ All required documentation files present.

README.md

🔎 Type Check

Checking the alignment of your contribution. 📏

mypy: 276 error(s) found

ovos_core/main.py:24:1: error: Skipping analyzing "ovos_utils.log": module is installed, but missing library stubs or py.typed marker [import-untyped]

Errors (showing first 10/276)
test/unittests/test_skill/__init__.py:15:1: error: Skipping analyzing "ovos_workshop.skills.ovos": module is installed, but missing library stubs or py.typed marker  [import-untyped]
test/end2end/conftest.py:10:1: error: Cannot find implementation or library stub for module named "pytest"  [import-not-found]
ovos_core/transformers.py:3:1: error: Skipping analyzing "ovos_config": module is installed, but missing library stubs or py.typed marker  [import-untyped]
ovos_core/transformers.py:4:1: error: Skipping analyzing "ovos_plugin_manager.intent_transformers": module is installed, but missing library stubs or py.typed marker  [import-untyped]
ovos_core/transformers.py:5:1: error: Skipping analyzing "ovos_plugin_manager.metadata_transformers": module is installed, but missing library stubs or py.typed marker  [import-untyped]
ovos_core/transformers.py:6:1: error: Skipping analyzing "ovos_plugin_manager.text_transformers": module is installed, but missing library stubs or py.typed marker  [import-untyped]
ovos_core/transformers.py:7:1: error: Skipping analyzing "ovos_plugin_manager.transformer_services": module is installed, but missing library stubs or py.typed marker  [import-untyped]
ovos_core/intent_services/manifest.py:15:1: error: Skipping analyzing "ovos_bus_client.message": module is installed, but missing library stubs or py.typed marker  [import-untyped]
ovos_core/intent_services/manifest.py:16:1: error: Skipping analyzing "ovos_spec_tools": module is installed, but missing library stubs or py.typed marker  [import-untyped]
ovos_core/intent_services/manifest.py:17:1: error: Skipping analyzing "ovos_utils.log": module is installed, but missing library stubs or py.typed marker  [import-untyped]

🌍 Locale Build

Checking if there's anything else we need to do. 📋

✅ Locale properly configured (64 files, 17 languages)

Locale directories found:

  • ovos_core/intent_services/locale

Localization coverage:

  • ovos_core/intent_services/locale: 64 files in 17 languages (pt-pt, en-us, it-it, de-de, ca-es...)

pyproject.toml:[tool.setuptools.package-data.ovos_core] includes locale

  • intent_services/locale/*/*.voc

Build manifest: ✅ 31 locale files included in package

🏷️ Release Preview

Setting the stage for the upcoming deployment. 🎭

Caution

Breaking change — this PR will bump the MAJOR version (2.5.9a13.0.0a1).
Downstream dependents may break. Double-check compatibility before merging.

Current: 2.5.9a1Next: 3.0.0a1

Signal Value
Label breaking, feature
PR title feat!: OVOS-STOP-1 reserved-intent_name dispatch + separable legacy bridge
Bump major

✅ PR title follows conventional commit format.


🚀 Release Channel Compatibility

Predicted next version: 3.0.0a1

Channel Status Note Current Constraint
Stable Too new (must be <1.4.0) ovos-core>=1.3.1,<1.4.0
Testing Too new (must be <3.0.0) ovos-core>=2.1.1,<3.0.0
Alpha Compatible ovos-core>=2.2.4a1

📋 Repo Health

A detailed health report for the project. 📝

✅ All required files present.

Latest Version: 2.5.6a1

ovos_core/version.py — Version file
README.md — README
LICENSE — License file
pyproject.toml — pyproject.toml
⚠️ setup.py — setup.py
CHANGELOG.md — Changelog
ovos_core/version.py has valid version block markers

📊 Coverage

Measuring the breadth of our automated checks. 📏

⚠️ 65.4% total coverage

Files below 80% coverage (8 files)
File Coverage Missing lines
ovos_core/__init__.py 0.0% 7
ovos_core/__main__.py 0.0% 26
ovos_core/intent_services/__init__.py 0.0% 1
ovos_core/transformers.py 33.3% 18
ovos_core/skill_installer.py 45.7% 132
ovos_core/intent_services/service.py 51.2% 186
ovos_core/skill_manager.py 62.0% 156
ovos_core/intent_services/dispatcher.py 73.4% 29

Full report: download the coverage-report artifact.

🔌 Plugin Detection

I've checked the plugin's 'error messages' for clarity. 🗣️

Plugin Status: ERRORS (1)

Plugin Info:

  • Name: ovos-core
  • Description: The spiritual successor to Mycroft AI, OVOS is flexible voice assistant software that can be run almost anywhere!

OPM Detection:

Plugin Type Wheel Editable
pipeline

Entry Point Validation:

Entry Point Type Import Interface
ovos-converse-pipeline-plugin pipeline ✅ 1666ms
ovos-fallback-pipeline-plugin pipeline ✅ 2ms
ovos-stop-pipeline-plugin pipeline ✅ 47ms

⊘ No settingsmeta.json
requires-python >=3.10 — running Python 3.11

Issues:

  • ❌ Import time for ovos-converse-pipeline-plugin exceeds 500ms (1666ms)
  • ⚠️ No settingsmeta.json found
  • ⚠️ No settingsmeta.json found

🔨 Build Tests

Checking the plumbing of your data flows. 🚰

✅ All versions pass

Python Build Install Tests
3.10
3.11
3.12
3.13
3.14

⚖️ License Check

Verifying the SPDX identifiers for correctness. 🆔

✅ No license violations found.

Policy: Apache 2.0 (universal donor). StrongCopyleft / NetworkCopyleft / WeakCopyleft / Other / Error categories fail. MPL allowed.

🔌 Skill Tests (ovoscope)

I've checked the skill's 'response time' to rapid fire questions. ⏱️

8/32 passed

❌ **TestAdaptIntent** — 0/4
Test Result
test_padatious_no_match ❌ subtests passed
test_skill_blacklist ❌ subtests passed
test_adapt_match ❌ subtests passed
test_intent_blacklist ❌ subtests passed

test_padatious_no_match failure:

[gw3] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python

test_skill_blacklist failure:

[gw0] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python

test_adapt_match failure:

[gw1] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python
❌ **TestCancelIntentMidSentence** — 0/1
Test Result
test_cancel_match ❌ subtests passed

test_cancel_match failure:

[gw2] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python
❌ **TestConverse** — 0/1
Test Result
test_parrot_mode ❌ subtests passed

test_parrot_mode failure:

[gw0] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python
❌ **TestDeactivate** — 2/3
Test Result
test_deactivate ✅ passed
test_activate ✅ passed
test_deactivate_inside_converse ❌ subtests passed

test_deactivate_inside_converse failure:

[gw3] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python
❌ **TestFallback** — 0/1
Test Result
test_fallback_match ❌ subtests passed

test_fallback_match failure:

[gw0] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python
✅ **TestGGWaveSkillInstall** — 2/2 ✅ **TestGlobalStopSpec** — 1/1
❌ **TestIntentPipelineRouting** — 0/4
Test Result
test_padatious_intent_matched ❌ subtests passed
test_high_priority_stage_handles_before_low ❌ subtests passed
test_blacklisted_skill_falls_through_to_failure ❌ subtests passed
test_no_match_produces_intent_failure ❌ subtests passed

test_padatious_intent_matched failure:

[gw2] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python

test_high_priority_stage_handles_before_low failure:

[gw2] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python

test_blacklisted_skill_falls_through_to_failure failure:

[gw0] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python
❌ **TestLangDisambiguation** — 0/4
Test Result
test_invalid_lang_detection ❌ subtests passed
test_stt_lang ❌ subtests passed
test_lang_text_detection ❌ subtests passed
test_metadata_preferred_over_text_detection ❌ subtests passed

test_invalid_lang_detection failure:

[gw1] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python

test_stt_lang failure:

[gw3] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python

test_lang_text_detection failure:

[gw1] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python
✅ **TestLegacyGlobalStop** — 1/1
❌ **TestLegacyIntentIdBackCompat** — 0/2
Test Result
test_legacy_dispatch_topic_fires_handler ❌ subtests passed
test_legacy_blacklist_id_suppresses ❌ subtests passed

test_legacy_dispatch_topic_fires_handler failure:

[gw2] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python

test_legacy_blacklist_id_suppresses failure:

[gw3] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python
✅ **TestLegacyTargetedStop** — 1/1
❌ **TestNoSkills** — 0/2
Test Result
test_routing ❌ subtests passed
test_complete_failure ❌ subtests passed

test_routing failure:

[gw0] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python

test_complete_failure failure:

[gw1] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python
❌ **TestPadatiousIntent** — 0/4
Test Result
test_adapt_no_match ❌ subtests passed
test_skill_blacklist ❌ subtests passed
test_padatious_match ❌ subtests passed
test_intent_blacklist ❌ subtests passed

test_adapt_no_match failure:

[gw1] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python

test_skill_blacklist failure:

[gw3] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python

test_padatious_match failure:

[gw2] linux -- Python 3.11.15 /opt/hostedtoolcache/Python/3.11.15/x64/bin/python
✅ **TestTargetedStopSpec** — 1/1

🚌 Bus Coverage

The bus coverage audit is now available for inspection. 📋

🔴 Coverage Summary

Metric Status Coverage
Listeners ░░░░░░░░░░ 8.1% 16/197 handlers
Emitters ██████████ 100% 33/33 observed
Assertions ██████████ 100% 33/33 asserted

📊 Per-Skill Breakdown

Skill Listeners Observed Asserted
AdaptPipeline 0/14 (0.0%) 0/0 0/0
ConverseService 0/4 (0.0%) 0/0 0/0
DomainAdaptPipeline 0/14 (0.0%) 0/0 0/0
FallbackService 0/2 (0.0%) 0/0 0/0
HierarchicalAdaptPipeline 0/14 (0.0%) 0/0 0/0
IntentDispatcher 1/2 (50.0%) 0/0 0/0
IntentManifest 0/8 (0.0%) 0/0 0/0
IntentService 1/4 (25.0%) 0/0 0/0
Model2VecIntentPipeline 0/12 (0.0%) 0/0 0/0
Model2VecPrototypePipeline 0/12 (0.0%) 0/0 0/0
PadaciosoPipeline 0/11 (0.0%) 0/0 0/0
PadatiousPipeline 0/15 (0.0%) 0/0 0/0
SkillManager 0/4 (0.0%) 0/0 0/0
StopService 1/1 (100.0%) 0/0 0/0
_LegacyStopBridge 3/3 (100.0%) 0/0 0/0
__core__ 2/26 (7.7%) 6/6 6/6
ovos-skill-count.openvoiceos 4/21 (19.0%) 8/8 8/8
ovos-skill-hello-world.openvoiceos 2/26 (7.7%) 9/9 9/9
ovos-stop-pipeline-plugin 0/0 (0.0%) 3/3 3/3
stop.openvoiceos 0/0 (0.0%) 7/7 7/7
type 2/4 (50.0%) 0/0 0/0
🔍 Detailed Message Type Breakdown

AdaptPipeline

⚠️ Uncovered Listeners:

  • ovos.intent.deregister (Intent)
  • ovos.intent.disable (Intent)
  • ovos.intent.enable (Intent)
  • ovos.intent.register.keyword (Intent)
  • detach_intent
  • detach_skill
  • intent.service.adapt.get
  • intent.service.adapt.manifest.get
  • intent.service.adapt.vocab.manifest.get
  • ovos.entity.deregister
  • ovos.entity.register
  • ovos.skill.deregister
  • register_intent
  • register_vocab

ConverseService

⚠️ Uncovered Listeners:

  • converse:skill (Intent)
  • intent.service.active_skills.get
  • intent.service.skills.activate
  • intent.service.skills.deactivate

DomainAdaptPipeline

⚠️ Uncovered Listeners:

  • ovos.intent.deregister (Intent)
  • ovos.intent.disable (Intent)
  • ovos.intent.enable (Intent)
  • ovos.intent.register.keyword (Intent)
  • detach_intent
  • detach_skill
  • intent.service.adapt.get
  • intent.service.adapt.manifest.get
  • intent.service.adapt.vocab.manifest.get
  • ovos.entity.deregister
  • ovos.entity.register
  • ovos.skill.deregister
  • register_intent
  • register_vocab

FallbackService

⚠️ Uncovered Listeners:

  • ovos.skills.fallback.deregister
  • ovos.skills.fallback.register

HierarchicalAdaptPipeline

⚠️ Uncovered Listeners:

  • ovos.intent.deregister (Intent)
  • ovos.intent.disable (Intent)
  • ovos.intent.enable (Intent)
  • ovos.intent.register.keyword (Intent)
  • detach_intent
  • detach_skill
  • intent.service.adapt.get
  • intent.service.adapt.manifest.get
  • intent.service.adapt.vocab.manifest.get
  • ovos.entity.deregister
  • ovos.entity.register
  • ovos.skill.deregister
  • register_intent
  • register_vocab

IntentDispatcher

⚠️ Uncovered Listeners:

  • mycroft.skill.handler.error
    ✅ Covered Listeners:
  • mycroft.skill.handler.complete (14x)

IntentManifest

⚠️ Uncovered Listeners:

  • ovos.intent.deregister (Intent)
  • ovos.intent.describe (Intent)
  • ovos.intent.disable (Intent)
  • ovos.intent.enable (Intent)
  • ovos.intent.list (Intent)
  • ovos.intent.register.keyword (Intent)
  • ovos.intent.register.template (Intent)
  • ovos.skill.deregister

IntentService

⚠️ Uncovered Listeners:

  • intent.service.intent.get (Intent)
  • intent.service.pipelines.reload
  • intent.service.skills.deactivate
    ✅ Covered Listeners:
  • ovos.utterance.handle (8x)

Model2VecIntentPipeline

⚠️ Uncovered Listeners:

  • ovos.intent.deregister (Intent)
  • ovos.intent.disable (Intent)
  • ovos.intent.enable (Intent)
  • ovos.intent.register.template (Intent)
  • padatious:register_intent (Intent)
  • detach_intent
  • detach_skill
  • mycroft.ready
  • ovos.entity.deregister
  • ovos.entity.register
  • ovos.skill.deregister
  • register_intent

Model2VecPrototypePipeline

⚠️ Uncovered Listeners:

  • ovos.intent.deregister (Intent)
  • ovos.intent.disable (Intent)
  • ovos.intent.enable (Intent)
  • ovos.intent.register.template (Intent)
  • padatious:register_intent (Intent)
  • detach_intent
  • detach_skill
  • mycroft.ready
  • ovos.entity.deregister
  • ovos.entity.register
  • ovos.skill.deregister
  • register_intent

PadaciosoPipeline

⚠️ Uncovered Listeners:

  • ovos.intent.deregister (Intent)
  • ovos.intent.disable (Intent)
  • ovos.intent.enable (Intent)
  • ovos.intent.register.template (Intent)
  • padatious:register_entity (Intent)
  • padatious:register_intent (Intent)
  • detach_intent
  • detach_skill
  • ovos.entity.deregister
  • ovos.entity.register
  • ovos.skill.deregister

PadatiousPipeline

⚠️ Uncovered Listeners:

  • ovos.intent.deregister (Intent)
  • ovos.intent.disable (Intent)
  • ovos.intent.enable (Intent)
  • ovos.intent.register.template (Intent)
  • padatious:register_entity (Intent)
  • padatious:register_intent (Intent)
  • detach_intent
  • detach_skill
  • intent.service.padatious.entities.manifest.get
  • intent.service.padatious.get
  • intent.service.padatious.manifest.get
  • mycroft.skills.train
  • ovos.entity.deregister
  • ovos.entity.register
  • ovos.skill.deregister

SkillManager

⚠️ Uncovered Listeners:

  • skillmanager.activate
  • skillmanager.deactivate
  • skillmanager.keep
  • skillmanager.list

StopService

✅ Covered Listeners:

  • ovos-stop-pipeline-plugin:global_stop (2x)

_LegacyStopBridge

✅ Covered Listeners:

  • ovos.intent.matched (9x)
  • stop:global (2x)
  • stop:skill (1x)

__core__

⚠️ Uncovered Listeners:

  • add_context
  • clear_context
  • message
  • mycroft.ovos-skill-count.openvoiceos.all_loaded
  • mycroft.ovos-skill-count.openvoiceos.is_alive
  • mycroft.ovos-skill-count.openvoiceos.is_ready
  • mycroft.ovos-skill-hello-world.openvoiceos.all_loaded
  • mycroft.ovos-skill-hello-world.openvoiceos.is_alive
  • mycroft.ovos-skill-hello-world.openvoiceos.is_ready
  • ovos-skill-count.openvoiceos.set
  • ovos-skill-count.openvoiceos.stop.response
  • ovos-skill-hello-world.openvoiceos.set
  • ovos.ggwave.disable
  • ovos.ggwave.enable
  • ovos.pip.install
  • ovos.pip.uninstall
  • ovos.session.sync
  • ovos.session.update_default
  • ovos.skills.install
  • ovos.skills.uninstall
  • remove_context
  • skill.converse.get_response.disable
  • skill.converse.get_response.enable
  • skill.stop.pong
    ✅ Covered Listeners:
  • ovos.utterance.handled (13x)
  • ovos.utterance.speak (16x)

📤 Emitters:

  • mycroft.audio.play_sound (Asserted ✅)
  • ovos-skill-hello-world.openvoiceos:Greetings.intent (Asserted ✅)
  • ovos.utterance.cancelled (Asserted ✅)
  • ovos.utterance.handle (Asserted ✅)
  • ovos.utterance.handled (Asserted ✅)
  • recognizer_loop:utterance (Asserted ✅)

ovos-skill-count.openvoiceos

⚠️ Uncovered Listeners:

  • ovos-skill-count.openvoiceos:count_to_n.intent (Intent)
  • question:action (Intent)
  • question:action.ovos-skill-count.openvoiceos (Intent)
  • question:query (Intent)
  • homescreen.metadata.get
  • mycroft.ovos-skill-count.openvoiceos.all_loaded
  • mycroft.ovos-skill-count.openvoiceos.is_alive
  • mycroft.ovos-skill-count.openvoiceos.is_ready
  • mycroft.skill.disable_intent
  • mycroft.skill.enable_intent
  • mycroft.skill.remove_cross_context
  • mycroft.skill.set_cross_context
  • mycroft.skills.settings.changed
  • ovos-skill-count.openvoiceos.converse.get_response
  • ovos-skill-count.openvoiceos.set
  • ovos.common_query.ping
  • ovos.skills.settings_changed
    ✅ Covered Listeners:
  • mycroft.stop (1x)
  • ovos-skill-count.openvoiceos.stop (1x)
  • ovos-skill-count.openvoiceos.stop.ping (1x)
  • ovos-skill-count.openvoiceos:count_to_n (4x)

📤 Emitters:

  • mycroft.skill.handler.complete (Asserted ✅)
  • mycroft.skill.handler.start (Asserted ✅)
  • ovos-skill-count.openvoiceos.activate (Asserted ✅)
  • ovos-skill-count.openvoiceos:count_to_n (Asserted ✅)
  • ovos.intent.handler.complete (Asserted ✅)
  • ovos.intent.handler.start (Asserted ✅)
  • ovos.intent.matched (Asserted ✅)
  • ovos.utterance.handled (Asserted ✅)

ovos-skill-hello-world.openvoiceos

⚠️ Uncovered Listeners:

  • ovos-skill-hello-world.openvoiceos:HelloWorldIntent (Intent)
  • ovos-skill-hello-world.openvoiceos:HowAreYou (Intent)
  • ovos-skill-hello-world.openvoiceos:HowAreYou.intent (Intent)
  • ovos-skill-hello-world.openvoiceos:ThankYouIntent (Intent)
  • question:action (Intent)
  • question:action.ovos-skill-hello-world.openvoiceos (Intent)
  • question:query (Intent)
  • hello.world
  • homescreen.metadata.get
  • mycroft.ovos-skill-hello-world.openvoiceos.all_loaded
  • mycroft.ovos-skill-hello-world.openvoiceos.is_alive
  • mycroft.ovos-skill-hello-world.openvoiceos.is_ready
  • mycroft.skill.disable_intent
  • mycroft.skill.enable_intent
  • mycroft.skill.remove_cross_context
  • mycroft.skill.set_cross_context
  • mycroft.skills.settings.changed
  • mycroft.stop
  • ovos-skill-hello-world.openvoiceos.converse.get_response
  • ovos-skill-hello-world.openvoiceos.set
  • ovos-skill-hello-world.openvoiceos.stop
  • ovos-skill-hello-world.openvoiceos.stop.ping
  • ovos.common_query.ping
  • ovos.skills.settings_changed
    ✅ Covered Listeners:
  • ovos-skill-hello-world.openvoiceos:Greetings (2x)
  • ovos-skill-hello-world.openvoiceos:Greetings.intent (2x)

📤 Emitters:

  • mycroft.skill.handler.complete (Asserted ✅)
  • mycroft.skill.handler.start (Asserted ✅)
  • ovos-skill-hello-world.openvoiceos.activate (Asserted ✅)
  • ovos-skill-hello-world.openvoiceos:Greetings (Asserted ✅)
  • ovos.intent.handler.complete (Asserted ✅)
  • ovos.intent.handler.start (Asserted ✅)
  • ovos.intent.matched (Asserted ✅)
  • ovos.utterance.handled (Asserted ✅)
  • ovos.utterance.speak (Asserted ✅)

ovos-stop-pipeline-plugin

📤 Emitters:

  • ovos-stop-pipeline-plugin:global_stop (Asserted ✅)
  • ovos.stop (Asserted ✅)
  • ovos.utterance.handled (Asserted ✅)

stop.openvoiceos

📤 Emitters:

  • mycroft.skill.handler.complete (Asserted ✅)
  • mycroft.skill.handler.start (Asserted ✅)
  • mycroft.stop (Asserted ✅)
  • ovos-skill-count.openvoiceos.stop (Asserted ✅)
  • stop.openvoiceos.activate (Asserted ✅)
  • stop:global (Asserted ✅)
  • stop:skill (Asserted ✅)

type

⚠️ Uncovered Listeners:

  • recognizer_loop:record_begin (Intent)
  • recognizer_loop:record_end (Intent)
    ✅ Covered Listeners:
  • recognizer_loop:audio_output_end (13x)
  • recognizer_loop:audio_output_start (16x)

🔒 Security (pip-audit)

Ensuring our data is safe and secure. 🔐

✅ No known vulnerabilities found (113 packages scanned).


Beep boop. See you in the next PR! 👋

@JarbasAl
JarbasAl marked this pull request as ready for review July 16, 2026 07:21
JarbasAl and others added 3 commits July 23, 2026 23:31
Add test_stop_spec_e2e.py asserting the spec dispatch (<skill_id>:stop with
Match.skill_id==skill_id, <pipeline_id>:global_stop with skill_id==pipeline_id,
suppress_activation suppressing {skill_id}.activate, ovos.stop broadcast, and the
§5.2/§6 session drain) and test_stop_legacy_e2e.py asserting the pre-spec
stop:global/stop:skill dispatch re-emit onto mycroft.stop / <skill_id>.stop.

Supersede the prior test_stop.py / test_stop_refactor.py suites.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Return the spec dispatch shape from the stop pipeline: a targeted stop on
<skill_id>:stop with Match.skill_id==skill_id (§2/§3.1) and a global stop on
<pipeline_id>:global_stop with skill_id==pipeline_id (§5). Both set
IntentHandlerMatch.suppress_activation, and the orchestrator honours it by
registering no activation (no active_handlers push, no {skill_id}.activate) for
such a dispatch (§6.2/§7.3). The §5.2/§6 session drain (active_handlers,
converse_handlers, response_mode) is committed via Match.updated_session before
dispatch. handle_global_stop broadcasts ovos.stop (§5.3).

BREAKING CHANGE: the stop pipeline no longer dispatches stop:global/stop:skill
with skill_id=stop.openvoiceos; it dispatches the reserved intent_names
<skill_id>:stop and <pipeline_id>:global_stop. Requires ovos-plugin-manager
>=2.9.0a1 for IntentHandlerMatch.suppress_activation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Compose a self-contained shim that observes the §9.2 ovos.intent.matched
notification and re-emits the pre-spec stop:global/stop:skill dispatch, and
owns the legacy stop:global/stop:skill handlers that fan out to mycroft.stop
and <skill_id>.stop. Un-migrated skills still consuming <skill_id>.stop keep
working when the ovos-spec-tools namespace translator is inactive.

The unit lives in its own module and is wired via three lines in StopService,
so it is removed in one move once every skill consumes <skill_id>:stop and
ovos.stop directly. A one-time deprecation warning is logged while active.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@JarbasAl
JarbasAl marked this pull request as draft July 24, 2026 11:14
Resolve test_stop.py conflict by keeping the STOP-1 spec deletion (replaced
by test_stop_spec_e2e.py + test_stop_legacy_e2e.py). Carry dev's INTENT-4
alias-collapse fix into test_stop_spec_e2e.py (:count_to_n, not the legacy
.intent-suffixed id).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@JarbasAl
JarbasAl marked this pull request as ready for review August 1, 2026 18:44
@github-actions github-actions Bot added breaking breaks backwards compatibility (kinda) feature and removed feature breaking breaks backwards compatibility (kinda) labels Aug 1, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (6)
ovos_core/intent_services/stop_service.py (1)

258-263: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace both single-iteration loops with a first-item selection. _collect_stop_skills returns a candidate list, and both call sites return inside the first loop iteration, so the loop hides the "take the first candidate" intent.

  • ovos_core/intent_services/stop_service.py#L258-L263: in match_high, assign skill_id = next(iter(self._collect_stop_skills(message)), None) and return self._targeted_stop(...) when it is set.
  • ovos_core/intent_services/stop_service.py#L337-L342: apply the same change in match_low, keeping the _global_stop escalation when no candidate exists.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ovos_core/intent_services/stop_service.py` around lines 258 - 263, Replace
the single-iteration skill loops in match_high and match_low with first-item
selection using next(iter(self._collect_stop_skills(message)), None); call
_targeted_stop when a skill_id is found, and preserve _global_stop escalation in
match_low when no candidate exists. Apply this in
ovos_core/intent_services/stop_service.py at lines 258-263 and 337-342.
ovos_core/intent_services/stop_service_legacy.py (1)

56-67: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Log the deprecation warning only when the bridge re-emits.

Lines 61-67 set _warned and log before the branch test on lines 68-74. Any match from the stop pipeline whose intent_name is neither :global_stop nor :stop triggers the deprecation warning without any legacy re-emission. Move the warning into the two branches, or gate it on the branch result.

♻️ Proposed reordering
         intent_name = message.data.get("intent_name") or ""
-        if not self._warned:
-            self._warned = True
-            LOG.warning(...)
         if intent_name.endswith(":global_stop"):
+            self._warn_once()
             self.bus.emit(self._forward_legacy(message, f"{self.LEGACY_SKILL_ID}.activate"))
             self.bus.emit(self._forward_legacy(message, "stop:global"))
         elif intent_name.endswith(":stop"):
+            self._warn_once()
             skill_id = message.data.get("skill_id")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ovos_core/intent_services/stop_service_legacy.py` around lines 56 - 67, The
_on_intent_matched method currently logs the deprecation warning before
confirming a legacy dispatch will be re-emitted. Move the _warned update and
LOG.warning call into the :global_stop and :stop handling branches, or otherwise
gate them on a successful re-emission, so unrelated intent names never trigger
the warning.
test/end2end/test_stop_spec_e2e.py (2)

174-179: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use iterable unpacking for the ignore list.

Ruff reports RUF005 on the _IGNORE + [...] concatenation.

♻️ Proposed fix
-                ignore_messages=_IGNORE + [
-                    f"{self.skill_id}.stop.response",
-                    f"{self.skill_id}.stop.ping",
-                    "skill.stop.pong",
-                    f"{self.skill_id}:count_to_n",
-                ],
+                ignore_messages=[
+                    *_IGNORE,
+                    f"{self.skill_id}.stop.response",
+                    f"{self.skill_id}.stop.ping",
+                    "skill.stop.pong",
+                    f"{self.skill_id}:count_to_n",
+                ],
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/end2end/test_stop_spec_e2e.py` around lines 174 - 179, Update the
ignore_messages construction in the test to use iterable unpacking for _IGNORE
and the additional message entries instead of list concatenation, while
preserving the current ignored-message contents and order.

Source: Linters/SAST tools


69-76: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Share the duplicated end-to-end stop helpers. Both new end-to-end modules copy the same _wait_for_active_skill polling helper and a largely identical _IGNORE topic list. One shared test helper module keeps the two suites in sync when the ignored topic set changes.

  • test/end2end/test_stop_spec_e2e.py#L69-L76: import _wait_for_active_skill and the common ignore entries from a shared helper instead of defining them locally.
  • test/end2end/test_stop_legacy_e2e.py#L53-L60: import the same shared helper and extend the ignore list with the legacy-only entries.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/end2end/test_stop_spec_e2e.py` around lines 69 - 76, The duplicated
end-to-end stop helpers should be centralized. In
test/end2end/test_stop_spec_e2e.py lines 69-76, remove the local
_wait_for_active_skill and common _IGNORE entries and import them from a shared
helper module; in test/end2end/test_stop_legacy_e2e.py lines 53-60, import the
same shared helper and retain only the legacy-specific additions to _IGNORE.
test/unittests/test_stop_service.py (1)

531-539: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the bridge listener registration and removal.

_make_bridge bypasses __init__, so no test asserts that _LegacyStopBridge registers ovos.intent.matched, stop:global, and stop:skill, or that shutdown() removes exactly those three. The bridge is the removable compatibility unit, so a test that pins its bus surface makes the future removal safe and verifiable.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unittests/test_stop_service.py` around lines 531 - 539, Add tests for
_LegacyStopBridge listener lifecycle without using _make_bridge: instantiate the
bridge normally and assert registration of exactly ovos.intent.matched,
stop:global, and stop:skill, then call shutdown() and assert those same three
listeners are removed from the bus.
test/unittests/test_intent_service_extended.py (1)

691-698: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Also assert that no {skill_id}.activate event is emitted.

The new gate in service.py suppresses two things: the active_handlers push and the {skill_id}.activate callback. This test covers only the push. Add an assertion on the bus emissions so a regression that keeps emitting the activate event is caught.

♻️ Suggested extra assertion
     def test_suppress_activation_match_suppresses_push(self):
         # OVOS-STOP-1 §6.2/§7.3: a Match.suppress_activation dispatch (a stop)
         # must NOT push onto active_handlers regardless of its pipeline_id.
-        sess = self._dispatch("ovos-adapt-pipeline-plugin-high",
-                              suppress_activation=True)
+        emitted = []
+        with patch.object(FakeBus, "emit", lambda _self, m: emitted.append(m.msg_type)):
+            sess = self._dispatch("ovos-adapt-pipeline-plugin-high",
+                                  suppress_activation=True)
         ids = [h.get("skill_id") if isinstance(h, dict) else getattr(h, "skill_id", h)
                for h in sess.active_handlers]
         self.assertNotIn("test.skill", ids)
+        self.assertNotIn("test.skill.activate", emitted)

Adapt the patch target to the bus object _make_service uses.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/unittests/test_intent_service_extended.py` around lines 691 - 698,
Extend test_suppress_activation_match_suppresses_push to inspect emissions from
the bus object configured by _make_service, and assert that no
test.skill.activate event is emitted when dispatching with
suppress_activation=True. Keep the existing active_handlers assertion unchanged
and target the actual bus instance used by the service.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@ovos_core/intent_services/stop_service_legacy.py`:
- Around line 83-89: Move the skill_id lookup inside the HandlerLifecycle
context in handle_skill_stop, and handle a missing key through the lifecycle’s
existing error path so the handler emits its terminal error event instead of
raising before lifecycle setup. Preserve the current reply emission for messages
containing a valid skill_id.

In `@ovos_core/intent_services/stop_service.py`:
- Around line 185-205: The _targeted_stop method mutates the live Session and
registers an unbounded once listener before dispatch is confirmed. Build the
stopped state on a copied session or apply it only from the successful dispatch
path, and move confirmation-listener registration into the relevant dispatch
handler; ensure the listener is removed or expires using the same 0.5-second
timeout as _collect_stop_skills, including dispatch rejection or failure paths.

---

Nitpick comments:
In `@ovos_core/intent_services/stop_service_legacy.py`:
- Around line 56-67: The _on_intent_matched method currently logs the
deprecation warning before confirming a legacy dispatch will be re-emitted. Move
the _warned update and LOG.warning call into the :global_stop and :stop handling
branches, or otherwise gate them on a successful re-emission, so unrelated
intent names never trigger the warning.

In `@ovos_core/intent_services/stop_service.py`:
- Around line 258-263: Replace the single-iteration skill loops in match_high
and match_low with first-item selection using
next(iter(self._collect_stop_skills(message)), None); call _targeted_stop when a
skill_id is found, and preserve _global_stop escalation in match_low when no
candidate exists. Apply this in ovos_core/intent_services/stop_service.py at
lines 258-263 and 337-342.

In `@test/end2end/test_stop_spec_e2e.py`:
- Around line 174-179: Update the ignore_messages construction in the test to
use iterable unpacking for _IGNORE and the additional message entries instead of
list concatenation, while preserving the current ignored-message contents and
order.
- Around line 69-76: The duplicated end-to-end stop helpers should be
centralized. In test/end2end/test_stop_spec_e2e.py lines 69-76, remove the local
_wait_for_active_skill and common _IGNORE entries and import them from a shared
helper module; in test/end2end/test_stop_legacy_e2e.py lines 53-60, import the
same shared helper and retain only the legacy-specific additions to _IGNORE.

In `@test/unittests/test_intent_service_extended.py`:
- Around line 691-698: Extend test_suppress_activation_match_suppresses_push to
inspect emissions from the bus object configured by _make_service, and assert
that no test.skill.activate event is emitted when dispatching with
suppress_activation=True. Keep the existing active_handlers assertion unchanged
and target the actual bus instance used by the service.

In `@test/unittests/test_stop_service.py`:
- Around line 531-539: Add tests for _LegacyStopBridge listener lifecycle
without using _make_bridge: instantiate the bridge normally and assert
registration of exactly ovos.intent.matched, stop:global, and stop:skill, then
call shutdown() and assert those same three listeners are removed from the bus.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 86ad7934-05b2-43d9-8fee-f6bb702ec9fe

📥 Commits

Reviewing files that changed from the base of the PR and between 9799e22 and a8348bc.

📒 Files selected for processing (9)
  • ovos_core/intent_services/service.py
  • ovos_core/intent_services/stop_service.py
  • ovos_core/intent_services/stop_service_legacy.py
  • test/end2end/test_stop.py
  • test/end2end/test_stop_legacy_e2e.py
  • test/end2end/test_stop_refactor.py
  • test/end2end/test_stop_spec_e2e.py
  • test/unittests/test_intent_service_extended.py
  • test/unittests/test_stop_service.py
💤 Files with no reviewable changes (2)
  • test/end2end/test_stop_refactor.py
  • test/end2end/test_stop.py

Comment on lines +83 to +89
def handle_skill_stop(self, message: Message) -> None:
"""Legacy ``stop:skill`` handler — re-emit the skill-directed ``<skill_id>.stop``."""
skill_id = message.data["skill_id"]
with HandlerLifecycle(self.bus, message,
skill_id=self.LEGACY_SKILL_ID,
data={"name": "StopService.handle_skill_stop"}):
self.bus.emit(message.reply(f"{skill_id}.stop"))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Guard the missing skill_id and keep the failure inside HandlerLifecycle.

stop:skill is a public legacy topic that any component can emit. Line 85 indexes message.data["skill_id"] outside the HandlerLifecycle block. A message without that key raises KeyError before the lifecycle opens, so no mycroft.skill.handler.error terminal reaches the bus and the caller waits without a result.

🐛 Proposed fix
     def handle_skill_stop(self, message: Message) -> None:
         """Legacy ``stop:skill`` handler — re-emit the skill-directed ``<skill_id>.stop``."""
-        skill_id = message.data["skill_id"]
         with HandlerLifecycle(self.bus, message,
                               skill_id=self.LEGACY_SKILL_ID,
                               data={"name": "StopService.handle_skill_stop"}):
+            skill_id = message.data.get("skill_id")
+            if not skill_id:
+                raise ValueError("'stop:skill' requires a 'skill_id' in message.data")
             self.bus.emit(message.reply(f"{skill_id}.stop"))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def handle_skill_stop(self, message: Message) -> None:
"""Legacy ``stop:skill`` handler — re-emit the skill-directed ``<skill_id>.stop``."""
skill_id = message.data["skill_id"]
with HandlerLifecycle(self.bus, message,
skill_id=self.LEGACY_SKILL_ID,
data={"name": "StopService.handle_skill_stop"}):
self.bus.emit(message.reply(f"{skill_id}.stop"))
def handle_skill_stop(self, message: Message) -> None:
"""Legacy ``stop:skill`` handler — re-emit the skill-directed ``<skill_id>.stop``."""
with HandlerLifecycle(self.bus, message,
skill_id=self.LEGACY_SKILL_ID,
data={"name": "StopService.handle_skill_stop"}):
skill_id = message.data.get("skill_id")
if not skill_id:
raise ValueError("'stop:skill' requires a 'skill_id' in message.data")
self.bus.emit(message.reply(f"{skill_id}.stop"))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ovos_core/intent_services/stop_service_legacy.py` around lines 83 - 89, Move
the skill_id lookup inside the HandlerLifecycle context in handle_skill_stop,
and handle a missing key through the lifecycle’s existing error path so the
handler emits its terminal error event instead of raising before lifecycle
setup. Preserve the current reply emission for messages containing a valid
skill_id.

Comment on lines +185 to +205
def _targeted_stop(self, skill_id: str, conf: float, utterance: str,
sess: Session) -> IntentHandlerMatch:
"""Build the OVOS-STOP-1 §2 targeted ``<skill_id>:stop`` Match.

Drains the dispatch target from ``active_handlers`` and clears its
``response_mode`` entry (§6.1/§6.2) via ``Match.updated_session``. The
§7.1 stamping push is suppressed (``suppress_activation``, §7.3), so the
removal is the final state.
"""
LOG.debug(f"Telling skill to stop: {skill_id}")
sess.disable_response_mode(skill_id)
sess.deactivate_skill(skill_id)
self.bus.once(f"{skill_id}.stop.response", self.handle_stop_confirmation)
return IntentHandlerMatch(
match_type=f"{skill_id}:stop",
match_data={"conf": conf, "skill_id": skill_id},
updated_session=sess,
utterance=utterance,
skill_id=skill_id,
suppress_activation=self.suppress_activation,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

_targeted_stop mutates the live session and registers a once handler before dispatch is certain.

sess comes from SessionManager.get(message), so lines 195-196 drain the live session during the match phase. Line 197 registers a one-shot listener for <skill_id>.stop.response.

Two consequences:

  1. The orchestrator can discard the match after match() returns. IntentService.handle_utterance skips a match whose match_type is in session.blacklisted_intents, and it also skips on a _dispatch_match exception. In those paths the skill is already deactivated and response_mode is already cleared, but no stop was dispatched.
  2. bus.once removes the handler only when the event arrives. A skill that never emits <skill_id>.stop.response leaves the handler registered. Repeated stops then accumulate handlers on the bus for the process lifetime.

Consider building the drained state on a copy or applying it from a dispatch-time hook, and bounding the confirmation listener (register in the global/targeted dispatch handler, or remove it after the same 0.5 s window that _collect_stop_skills uses).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ovos_core/intent_services/stop_service.py` around lines 185 - 205, The
_targeted_stop method mutates the live Session and registers an unbounded once
listener before dispatch is confirmed. Build the stopped state on a copied
session or apply it only from the successful dispatch path, and move
confirmation-listener registration into the relevant dispatch handler; ensure
the listener is removed or expires using the same 0.5-second timeout as
_collect_stop_skills, including dispatch rejection or failure paths.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking breaks backwards compatibility (kinda) feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant