Skip to content

Release 9.8.0a1 - #626

Closed
github-actions[bot] wants to merge 287 commits into
masterfrom
release-9.8.0a1
Closed

github-actions[bot] wants to merge 287 commits into
masterfrom
release-9.8.0a1

Conversation

@github-actions

@github-actions github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown
Contributor

Human review requested! This supersedes any earlier open release proposal for this repository.

JarbasAl and others added 30 commits April 8, 2026 16:17
* chore: remove deprecated class

* chore: remove deprecated class
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
* refactor: use JSON-based euphony rules for word list joining

Extracted word joining logic into ovos_workshop/skills/util.py with
generic, language-configurable euphony rules loaded from JSON:

- join_word_list() now supports per-language euphony.json config files
- Italian and Spanish euphony rules defined in locale/{lang}/euphony.json
- Removed hardcoded _join_word_list_it/es() special case handlers
- Rules engine supports: starts_with_vowel, starts_with_letter,
  starts_with_any_except conditions with accent normalization
- Moved simple_trace() to util.py for cleaner ovos.py module

This enables language teams to contribute euphony rules without modifying
Python code, improving i18n scalability for ask_selection() formatting.

All existing tests pass; word joining behavior is identical.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* mark functions as private, not meant to be imported by downstream

* feat: add word_connectors.json for 26 new languages and comprehensive tests

Added word connectors (and/or translations) for: ar-SA, bg-BG, el-GR,
et-EE, eu-ES, fi-FI, he-IL, hi-IN, hr-HR, hu-HU, id-ID, ja-JP, ko-KR,
lt-LT, lv-LV, ms-MY, nb-NO, pt-BR, ro-RO, ru-RU, sk-SK, sv-SE, sw-KE,
th-TH, tr-TR, vi-VN, zh-CN.

Tests added:
- simple_trace() formatting
- _normalize_word() accent/h-stripping
- _apply_euphony() rule engine (all condition types)
- euphony.json schema validation for all locales
- word_connectors.json presence and schema for all locales
- join_word_list() for 9 new languages (ru, tr, ja, zh, ar, sv, hu, pt-BR)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* remove unused files

* feat: add Occitan, Asturian, and Aragonese locale support with euphony

- oc-FR: "e" → "et" before any vowel
- ast-ES: "y" → "e" before /i/ (Spanish-like), "o" → "u" before /o/
- an-ES: "y" → "e" before /i/ (Spanish-like), "o" → "u" before /o/

All three are the remaining languages with productive conjunction euphony.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: update test_euphony.py to use new join_word_list API

The old test imported removed internal functions (_join_word_list_it,
_join_word_list_es). Updated to use join_word_list from util.py with
the lang parameter, preserving all original test cases.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* 📝 Add docstrings to `feat/word-join-euphony-json`

Docstrings generation was requested by @JarbasAl.

The following files were modified:

* `ovos_workshop/skills/ovos.py`
* `ovos_workshop/skills/util.py`

These files were kept as they were:
* `test/unittests/test_euphony.py`
* `test/unittests/test_locale_lookup.py`

These file types are not supported:
* `ovos_workshop/locale/an-ES/euphony.json`
* `ovos_workshop/locale/an-ES/word_connectors.json`
* `ovos_workshop/locale/ar-SA/word_connectors.json`
* `ovos_workshop/locale/ast-ES/euphony.json`
* `ovos_workshop/locale/ast-ES/word_connectors.json`
* `ovos_workshop/locale/bg-BG/word_connectors.json`
* `ovos_workshop/locale/el-GR/word_connectors.json`
* `ovos_workshop/locale/es-ES/euphony.json`
* `ovos_workshop/locale/et-EE/word_connectors.json`
* `ovos_workshop/locale/eu-ES/word_connectors.json`
* `ovos_workshop/locale/fi-FI/word_connectors.json`
* `ovos_workshop/locale/he-IL/word_connectors.json`
* `ovos_workshop/locale/hi-IN/word_connectors.json`
* `ovos_workshop/locale/hr-HR/word_connectors.json`
* `ovos_workshop/locale/hu-HU/word_connectors.json`
* `ovos_workshop/locale/id-ID/word_connectors.json`
* `ovos_workshop/locale/it-IT/euphony.json`
* `ovos_workshop/locale/ja-JP/word_connectors.json`
* `ovos_workshop/locale/ko-KR/word_connectors.json`
* `ovos_workshop/locale/lt-LT/word_connectors.json`
* `ovos_workshop/locale/lv-LV/word_connectors.json`
* `ovos_workshop/locale/ms-MY/word_connectors.json`
* `ovos_workshop/locale/nb-NO/word_connectors.json`
* `ovos_workshop/locale/oc-FR/euphony.json`
* `ovos_workshop/locale/oc-FR/word_connectors.json`
* `ovos_workshop/locale/pt-BR/word_connectors.json`
* `ovos_workshop/locale/ro-RO/word_connectors.json`
* `ovos_workshop/locale/ru-RU/word_connectors.json`
* `ovos_workshop/locale/sk-SK/word_connectors.json`
* `ovos_workshop/locale/sv-SE/word_connectors.json`
* `ovos_workshop/locale/sw-KE/word_connectors.json`
* `ovos_workshop/locale/th-TH/word_connectors.json`
* `ovos_workshop/locale/tr-TR/word_connectors.json`
* `ovos_workshop/locale/vi-VN/word_connectors.json`
* `ovos_workshop/locale/zh-CN/word_connectors.json`

* fix: apply CodeRabbit auto-fixes

Fixed 2 file(s) based on 2 unresolved review comments.

Co-authored-by: CodeRabbit <noreply@coderabbit.ai>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
* feat: yesno/selection agent plugins

* fix: default plugins

* fix: dependencies

* chore: drop requirements.txt, deps now in pyproject.toml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: correct test patch paths and expected fallback types

- patch load_yesno_plugin/load_option_matcher_plugin at ovos_workshop.skills.ovos
  (not ovos_plugin_manager.agents) since they are imported into the module
- _get_yesno_engine falls back to HeuristicYesNoEngine, not None
- _get_selection_engine falls back to FuzzyOptionMatcherPlugin, not None
- replace YesNoSolver references with patch.object on _get_yesno_engine

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: drop duplicate unit_tests workflow

build_tests already runs the same test path; unit_tests only differed by
adding audio system deps not needed for these tests

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: add ask_yesno and ask_selection tests

- test_base.py: fill in TODO stubs with mock-based tests on real OVOSSkill
  (yes/no/unmatched/timeout for ask_yesno; empty/single/invalid/fuzzy/timeout
  for ask_selection)
- test_ask_e2e.py: ovoscope end-to-end tests — inline AskYesNoSkill and
  AskSelectionSkill loaded via get_minicroft; user response injected via
  {skill_id}.converse.get_response after hearing speak; skipped when
  ovoscope/ovos-core are not installed

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: require ovoscope for e2e tests, add [test] extras

- remove pytest.importorskip — e2e tests are mandatory, not optional
- add [project.optional-dependencies] test group to pyproject.toml
  (ovos-core, ovoscope, pytest, pytest-cov, ovos-translate-server-plugin)
- build_tests.yml: pass install_extras=test so CI installs test deps
- keep requirements/test.txt in sync for local pip install -r workflows

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: remove unsupported message kwarg from ask_yesno/ask_selection calls

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* .

* fix: forward min_conf to OptionMatcher engine via config dict

The ask_selection min_conf parameter was documented but silently ignored.
FuzzyOptionMatcherPlugin reads threshold from self.config["min_conf"],
so set it on the cached engine instance before each match_option call.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: sync documentation after yesno/selection plugin integration

AI-Generated Change:
- Model: claude-sonnet-4-6
- Intent: keep docs accurate after feat: yesno/selection agent plugins
- Impact: updated README.md, docs/index.md, docs/ovos-skill.md, docs/skill-interaction.md
- Changes:
  - README.md: rewritten with install instructions, config table, and docs links
  - docs/skill-interaction.md: fixed fallback class name (HeuristicYesNoEngine not YesNoSolver), corrected ask_yesno_plugin default (ovos-solver-yes-no-plugin not None), added line-number citations, corrected failure behaviour table
  - docs/ovos-skill.md: noted plugin configurability for ask_yesno/ask_selection
  - docs/index.md: added skill-interaction.md to navigation table
- Verified via: manual review against ovos_workshop/skills/ovos.py:1932-2040

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: move ovoscope e2e tests out of build_tests path to prevent hangs

get_minicroft blocks up to 60s waiting for READY — too heavy for the
standard build_tests matrix. Move test/unittests/test_ask_e2e.py to
test/ovoscope/ (not scanned by build_tests) and add a dedicated
ovoscope_tests workflow that installs [test] extras and runs that path.
Revert install_extras from build_tests.yml.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: unblock speak(wait=True) in e2e tests by emitting audio_output_end

ask_selection speaks each option with wait=True, blocking 15s per speak
waiting for recognizer_loop:audio_output_end that never arrives in tests.
Emit that event immediately on every speak message so the skill proceeds
without delay, then inject the user response once speaking settles.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Delete MAINTENANCE_REPORT.md

* fix: use bus.on("speak") not on("message") to avoid recursion in e2e tests

Listening on "message" caught the audio_output_end we emitted, causing
infinite recursion. Listen on "speak" specifically instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: merge ovoscope e2e tests back into build_tests

No need for a separate workflow. Move test_ask_e2e.py back to
test/unittests/ and install [test] extras in build_tests so ovoscope
and ovos-core are available.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: delay audio_output_end emission past sess.is_speaking=True

FakeBus is synchronous — on_speak fires inline during bus.emit(speak),
before the caller sets sess.is_speaking=True. Emitting audio_output_end
in the same stack means wait_while_speaking misses it and hangs 15s.
Use a daemon thread with a 20ms delay so is_speaking is set first.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: remove fragile ovoscope e2e tests for ask_yesno/ask_selection

The tests fought TTS wait machinery (speak(wait=True) blocks on
recognizer_loop:audio_output_end) without adding coverage beyond what
test_skill_interaction.py already provides via mocked get_response.
Unit tests are sufficient here.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore: drop ovoscope from test deps, revert install_extras

No ovoscope e2e tests remain, no need for the heavy dependency.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* test: restore ovoscope e2e tests with proper TTS wait fix

Patch SessionManager.wait_while_speaking to a no-op so speak(wait=True)
doesn't block on TTS completion. Inject user response via
skill.converse.get_response.enable event instead of timing hacks.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix: restore SessionManager.bus after e2e tests to prevent state leak

get_minicroft sets SessionManager.bus globally. Without restoring it,
subsequent tests get real wait_while_speaking behaviour instead of the
early-return they expect (bus=None). Also scope wait_while_speaking
patch to setUp/tearDown so it can't leak if tearDownClass throws.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2.x only removed the bundled hivemind agent protocol + messagebus solver
(ovos-bus-client#207); no API break (#215). Widening keeps 1.x as default
while permitting 2.x, unblocking the HiveMind stack's 2.x adoption.
OVOSSkill.speak() now emits SpecMessage.SPEAK ('ovos.utterance.speak')
instead of the legacy 'speak' bus topic. The MessageBusClient namespace
migration also-emits the legacy 'speak' topic (emit_legacy, default on),
so existing audio consumers still receive it.

Add ovos-spec-tools>=0.9.0a1 direct dep; bump ovos-bus-client>=2.2.0a1.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…427)

* feat!: intent layers gate via intent context, not enable/disable

Redesign IntentLayers so a layer maps to an intent-context token instead
of mutating the global enabled/disabled intent set.

- activate_layer -> skill.set_context(<layer token>)
- deactivate_layer -> skill.remove_context(<layer token>)
- reset() (kept aliased as disable()) removes every active layer context

`@layer_intent` now injects the layer context token as a `.require()` on
the IntentBuilder, so the intent only validates while its layer is active.
Intents stay registered for the skill lifetime; gating is per-session via
adapt context, with no detach/attach churn.

resets_layers() now calls reset(); the enable_intent/disable_intent paths
are gone from the layer machinery.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: game skills converse via context-gated layers + skill_will_match probe

Make ConversationalGameSkill work with the new context-gated layers:

- OVOSGameSkill now also inherits ConversationalSkill so the converse
  ping/request bus handlers are actually wired for game skills (they were
  not, so converse never ran for games).
- Add OVOSSkill.skill_will_match(): a read-only probe over
  intent.service.intent.get that reports whether one of THIS skill's
  intents would match an utterance under the current session context.
  Supports exclude_pipeline so a conversing skill can skip the converse
  stage and avoid re-entrancy.
- ConversationalGameSkill.can_converse() returns True only while playing
  AND no context-gated layer intent would match (so layer intents are
  handled by adapt, not swallowed by converse). converse() uses the same
  probe. calc_intent() now ignores non-intent-parser pipeline results.
- on_play_game restart semantics doc fix in the game base.

Add a 4-layer demo skill e2e (ovoscope MiniCroft / FakeBus) proving the
layer mechanism: utterances advance layer0->layer3, only the active
layer's intent matches at each step (others are gated off via missing
context), always-on intents match throughout, and reset removes the
context so layer intents stop matching.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: session-aware playback + per-session layer gating (OCP is session aware)

OCP tracks player state per session, so game/playback skills must too.

- OVOSCommonPlaybackSkill: _playing/_paused Events -> per-session sets
  (_playing_sessions/_paused_sessions) keyed by SessionManager.get(msg)
  .session_id. New get_session_id(), is_playing_in/is_paused_in,
  playing_sessions; is_playing/is_paused properties now resolve the current
  session. OCP play/pause/resume/stop handlers key state by session and reply
  with message.reply so the session round-trips.
- OVOSGameSkill.stop_game is per-session; on_pause_game/on_resume_game no
  longer mutate paused state (the framework does, per session);
  on_game_command(self, utterance, lang, message=None) - the message (and thus
  session) is now passed so games can key per-session state.
- IntentLayers.is_active(layer, session=None) reads the layer's context token
  from the session (the per-session source of truth), falling back to the
  skill-level active_layers when no session is resolvable. This makes layer
  gating correct under concurrent multi-session play.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test: adapt layer tests to context gating

- common_play: assert per-session playback state (no playing/paused
  sessions on init) instead of the removed _playing/_paused Event objects
- intent_layers_e2e: restore the global SessionManager.bus in tearDownClass
  so the stopped MiniCroft bus does not leak into get_response/killable
  intent tests sharing the class-level SessionManager

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: add ovos-adapt-parser to test deps for layer e2e pipeline

The intent-layers e2e test drives a live ADAPT pipeline via MiniCroft; CI
installs only the declared test extras, where ovos-adapt-parser (which
provides the ovos-adapt-pipeline-plugin matcher) was missing, so the
pipeline matched nothing ('Unknown pipeline matcher'). Add it to both the
[test] extra and requirements/test.txt.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: bump ovos-adapt-parser test dep to >=1.3.1a1 for workshop 8.x

Stable ovos-adapt-parser 1.0.9 caps ovos-workshop<8.0.0, which conflicts
with this branch (8.3.0a1) and made the install unresolvable in CI.
1.3.1a1 relaxes the cap to <9.0.0; pin it as the floor (prerelease floor
pin) so pip resolves it without --pre.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor: keep #427 to IntentLayers only; game-skill + OCP playback move to a stacked PR

IntentLayers stays standalone and backwards-compatible for decorator consumers
(disable() aliases reset(); all decorators + methods preserved). The session-aware
game-skill + OCPCommonPlayback changes move to feat/gameskill-and-ocp-deprecation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat: skill_will_match accepts a Session to probe under that session's context

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
padacioso 2.0 (OVOS-INTENT-1 grammar) is compatible; validated end-to-end in
the OVOS spec-compliance harness against the workshop-9 stack.
…#432)

* refactor: re-export intent-definition primitives from ovos-spec-tools

Intent, IntentBuilder and open_intent_envelope are now re-exported from
ovos-spec-tools (OVOS-INTENT-4 adapt-free keyword-intent definition model)
instead of being defined locally as adapt metaclass wrappers. Skills keep
their `from ovos_workshop.intents import IntentBuilder` import; the canonical
implementation lives in the spec.

IntentServiceInterface and the munge_* helpers are unchanged — they consume
the re-exported definitions. The spec-tools IntentBuilder is API-compatible
(.require/.optionally/.one_of/.exclude/.build/.name) and Intent keeps the
same attribute surface (name/requires/at_least_one/optional/excludes).

The local matching logic (Intent.validate / _resolve_one_of) is dropped: per
OVOS-PIPELINE-1 matching is an engine-plugin concern, not part of the
definition primitive. test_intent.py (which exercised that matching logic) is
removed accordingly.

pyproject: bump ovos-spec-tools floor to >=0.14.0a1 (the version carrying the
intent primitives). CI pre-installs the unpublished spec-tools branch.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix: pin published ovos-spec-tools>=0.16.1a2, drop git-dep

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Update build_tests.yml

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
)

These are NOT spec topics — they are an internal ovos-workshop -> ovos-core
synchronization signal (workshop reporting 'I started/ended/errored' running a
handler). They exist only because skills run out-of-process from the
orchestrator; if core manipulated skill objects directly this bus round-trip
would not be needed. ovos-core consumes them as a private done-signal to emit
the authoritative PIPELINE-1 §8 spec trio (ovos.intent.handler.*); the legacy
names are permanently ovos-workshop event-wrapper signals and do not bridge to
the spec namespace (spec-tools MIGRATION_MAP excludes the trio). Doc-only.
JarbasAl and others added 28 commits September 5, 2026 12:38
STOP-1 §4.3 (as amended by #189): "The orchestrator dispatches
<target_skill_id>:stop per PIPELINE-1 §7, firing the handler-lifecycle
trio (...). The stop handler completes like any dispatched handler
(PIPELINE-1 §8): the orchestrator alone emits .complete on normal
return or .error on exception, and that terminal event resolves the
stop round." PIPELINE-1 §8 defines that trio as the orchestrator's
ovos.intent.handler.{start,complete,error}, which ovos-core derives
from a private workshop->core done-signal
(mycroft.skill.handler.{start,complete,error}) emitted around every
other dispatched handler via OVOSSkill.add_event(handler_info=...).

The targeted <skill_id>.stop registration was the one dispatch that
never passed handler_info, so ovos-core's IntentDispatcher never saw
a completion signal for it and had no faster path than the PIPELINE-1
§8.3 timeout. Route it through the same HandlerLifecycle wrapper as
every other dispatched handler, stamping data["intent_name"] = "stop"
so ovos-core's _resolve_entry can disambiguate this dispatch from any
other in-flight dispatch for the same skill_id (LIFO-by-skill_id alone
is ambiguous when a stop races an already-running intent handler). The
existing .stop.response reply is unchanged (STOP-1 §4.2).

STOP-1 §5.3/§9 also requires a skill performing user-visible activity
to subscribe to the ovos.stop global broadcast, not only its own
targeted <skill_id>.stop dispatch. The killable_event/killable_intent
decorators only listened on <skill_id>.stop; ovos.stop is now added
alongside for one deprecation cycle, wired through its own closure
because ovos.stop is a namespace-migrated topic and the shared bus's
legacy<->ovos.* dedup guard is keyed per handler object, so reusing
the existing abort closure would have folded it into the same dedup
entry as the non-migrated topics and hijacked their removal (caught by
a full-suite regression: test_no_leak_on_natural_completion). That
same test is extended to assert the ovos.stop listener is also cleaned
up on natural completion, not just the legacy <skill_id>.stop one —
verified by deleting the corresponding bus.remove("ovos.stop", ...)
line and confirming the extended assertion fails, then restoring it.

Fail-before: reverting the two source files and re-running the new/
modified regression tests makes test_handle_stop and
test_skill_stop_ovos_stop_broadcast fail (0 handler.start/.complete
signals observed; ovos.stop never aborts the killable thread), while
test_no_leak_on_natural_completion still passes both ways. A minimal
IntentDispatcher+OVOSSkill integration probe on a FakeBus confirms the
end-to-end effect: dispatching "<skill_id>:stop" resolves within
microseconds with the fix and never resolves within a 3s window
without it. Full suite on a fresh clone, single worker, foreground:
690 passed, 0 failed, reproduced across two consecutive runs; no
order-dependent flake observed in this environment.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
The broadcast-poll candidacy test in _handle_converse_broadcast_ack read
session.active_handlers, but OVOS-CONVERSE-1 §9.3 requires the handler to
check "whether its own skill_id appears in context.session.converse_handlers"
and §2.1 states that converse_handlers "is distinct from
session.active_handlers". A skill named in converse_handlers but not
active_handlers (or vice versa) got the wrong answer to ovos.converse.ping.

The same handler silently suppressed its pong when it held
session.response_mode. §2.1's "Response-mode holder exemption" only says the
holder MUST NOT be evicted or pruned "so the §2.2 identity invariant can
recognise it"; there is no rule that the holder stays silent on a poll, so
the early return was deleted and the holder now answers like any other
listed candidate.

_handle_converse_request also carried a version probe importing
ovos_core.version.OVOS_VERSION_TUPLE to special-case cores older than
2.0.3. Per the backcompat target ("latest-alpha x latest-stable, for ONE
stable cycle"), cores that old are out of scope, so the dead branch and its
try/except ImportError were removed and the reply-derived routing path
always runs.

Fail-before: reverting only the source change against the new/updated tests
in test/unittests/skills/test_converse_extended.py produced 8 failures
(test_candidacy_is_converse_handlers_not_active_handlers,
test_response_mode_holder_still_pongs,
test_converse_request_does_not_import_ovos_core, and five pre-existing
broadcast-poll tests updated to seed converse_handlers instead of
active_handlers). Re-applying the fix turned all 8 green; the full suite
(test/unittests) passed 678/678 after the fix.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…er (INTENT-4 §6.1, INTENT-1 §5.6) (#599)

A template may type a slot as `{type:name}` (OVOS-INTENT-1 §3.4). The type
prefix is not part of the slot name, so the samples that reach an engine must
carry the bare `{name}` and the declared types travel separately in the
OVOS-INTENT-4 §6.1 registration's optional `slot_types` map.

register_template reads the map with ovos-spec-tools' declared_slot_types and
then folds the prefixes away with strip_type_prefixes, before validation and
before either emit, so the legacy padatious registration and the spec topic
receive byte-identical bare-name samples and an unregistered type degrades
with a single warning. register_intent_file's slot-blacklist scan takes slot
names from declared_slots instead of a brace regex that captured `color:shade`
and silently stopped matching a sibling `.blacklist`.

Skills read the values an engine computed with two message-data reads on the
skill base: typed_slot(message, name) takes the §5.6 entry whose `surface`
equals the slot value (first entry on repeats), and typed_slots(message, type)
returns every entry of a type in span order. Both degrade to None / [] on a
missing key, a non-dict map, non-list entries or a malformed entry.

A slot's declared type comes from the registration bookkeeping, which keeps
the `slot_types` map alongside the samples of each registered intent, so a
surface string several engines reported under different types resolves to the
type the intent declared for that slot. A slot with no declared type falls
back to searching every registered type in REGISTERED_TYPES order.

Fail-before: 19 of 21 new tests fail against the unfixed source (samples keep
`{color:shade}`, no `slot_types` key, no helper), all pass after. Full suite green:
701 unit + 13 end2end, plus a GC-timing flake in test_abstract_app that
reproduces the same way on dev.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
remove_intent forwarded the deregister off the ambient dug message and
kept whatever context.skill_id that message carried, filling in
self.skill_id only when none was present. A skill disabling its own
intent while handling another component's message therefore emitted
{skill_id: "victim.skill"} under context.skill_id = "other.skill".
OVOS-INTENT-4 §3.2 requires payload skill_id to equal context.skill_id
on ovos.intent.deregister, and consumers enforcing that rule (and the
detach_intent legacy twin the bus client derives from it) drop the
skill's own detach as a forgery.

A skill only ever detaches its own intents, so the forward now always
carries self.skill_id in context; the dug message is left untouched.

Regression test: remove_intent called from a handler whose message
carries context.skill_id = "other.skill". Before the fix the emitted
context.skill_id was 'other.skill' (AssertionError: 'other.skill' !=
'test.skill'); after it is the calling skill's id and the ambient
message is unmodified.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
OVOS-STOP-1 §4.2 makes `ovos.stop.ping` the stop cascade's poll topic and
treats a handler that does not subscribe to it as `can_handle: false` for
that round. OVOSSkill subscribed only to the pre-spec per-skill
`<skill_id>.stop.ping`, so every skill's stoppability rested on the
compatibility twin ovos-core still emits alongside the broadcast; once that
twin goes, generic stop falls back to the recency rule for the whole fleet.

OVOSSkill now subscribes to both pings and answers on `ovos.stop.pong` with
the §4.2 payload. A core emitting both in one round draws exactly one pong:
STOP-1 §4.2 binds the poll to PIPELINE-1 §4.5, which keys round state by the
`session_id` of `context.session` and by `context.utterance_id` (§9.1.1) —
both propagate to either ping by reply derivation — and rules that the first
valid pong per candidate wins. A pre-spec core that stamps no `utterance_id`
names no round, so its pings are answered one for one as before.

Fail-before: the broadcast test fails on dev (0 pongs, nothing subscribes);
with the subscription but no dedupe the single-pong test fails 2 != 1; a
session-only round key fails two rounds in one session, 1 != 2.
Suite: 708 passed, 1 pre-existing failure in test_scheduler_delegation
(KeyError 'owner', PR #608).

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
OVOS-INTENT-4 §8.6 fixes `ovos.skill.loaded`: `{"skill_id": "...",
"capabilities": ["converse", "fallback"]}`, session-keyed via
context.session.session_id, capability vocabulary limited to
fallback/common_query/converse.

skill_launcher.py's load-status announcement kept the legacy
`mycroft.skills.loaded` payload unchanged and, beside it, now emits
`ovos.skill.loaded` through the loaded instance's own bus so
context.skill_id rides along and the bus auto-fills context.session.
OVOSSkill.on_ready_status re-emits the same announcement, covering
cold-start recovery when a skill process outlives a core restart.

Capabilities are read off the instance, never guessed: fallback via
isinstance(FallbackSkill), common_query via the _cq_handler a
@common_query-decorated method sets at bind time, converse via
isinstance(ConversationalSkill) -- the only base that wires the
CONVERSE-1 §4 surface (ovos.converse.ping, <skill_id>.converse.ping/
.request). A callable-attribute check was rejected: OVOSGameSkill and
ConversationalGameSkill define their own converse() outside
ConversationalSkill's MRO, and that surface never binds for them, so
"has a converse method" would announce a capability the skill cannot
answer on. The helper lives in ovos_workshop/skills/capabilities.py so
tests can assert its return value directly.

10 tests in test/unittests/test_ovos_skill_loaded.py assert the
capability values (not just that a message fired) for a plain skill
([]), a FallbackSkill (["fallback"]), a common-query skill
(["common_query"]), a ConversationalSkill (["converse"]), and a game
skill that defines converse() outside ConversationalSkill's MRO ([],
proving the callable-attribute check was rejected), plus the unchanged
legacy payload and the readiness re-announcement. Full suite 728
passed, no regressions.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
@github-actions github-actions Bot closed this Sep 10, 2026
@github-actions
github-actions Bot deleted the release-9.8.0a1 branch September 10, 2026 17:53
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.

1 participant