Skip to content

Release 3.4.3a2 - #963

Open
github-actions[bot] wants to merge 280 commits into
masterfrom
release-3.4.3a2
Open

Release 3.4.3a2#963
github-actions[bot] wants to merge 280 commits into
masterfrom
release-3.4.3a2

Conversation

@github-actions

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

Copy link
Copy Markdown

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

JarbasAl and others added 30 commits June 20, 2026 15:39
* feat: intent.service.intent.get accepts exclude_pipeline filter

The get-intent probe is read-only (it never runs a handler), so it is safe
for a skill to ask "what would match this utterance?". Add an optional
`exclude_pipeline` list to message.data so callers can drop stages from the
session pipeline for the probe - e.g. a conversing skill probing the
pipeline while skipping the converse stage to avoid re-entering its own
converse handler.

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

* Apply suggestion from @coderabbitai[bot]

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
)

* deps: adopt ovos-bus-client 2.x + ovos-workshop 8.x; single-source pyproject

Modernize the dependency set so ovos-core resolves on the bus-client 2.x /
workshop 8.x prerelease stack, and drop the vestigial requirements/*.txt (the
build reads the pyproject extras; the requirements files only duplicated them and
drifted). Every capping dep is floored at its first bus-client-2.x/workshop-8.x
(pre)release (prerelease-floor-pin): bus-client>=2.2.0a1, plugin-manager>=2.5.0a1,
workshop>=8.3.0a1, adapt-parser>=1.3.1a1, ocp-pipeline>=1.1.21a5, PHAL[extras]>=
0.2.16a1, audio[extras]>=2.0.1a1, padatious>=1.4.5a2, + the pipeline/skill floors.
No code changes — the spec-namespace migration is a separate PR.

* feat: migrate utterance entry to spec bus namespace (SpecMessage.UTTERANCE) (#772)

* feat: migrate utterance entry to spec bus namespace (SpecMessage.UTTERANCE)

Switch the intent-service utterance listener from the legacy
recognizer_loop:utterance topic to SpecMessage.UTTERANCE (ovos.utterance.handle).
The MessageBusClient namespace-migration bridges the legacy topic, so legacy
emitters still reach this spec listener. Stacks on the dependency PR (#775).

* test: exercise end2end scenarios on both bus namespaces explicitly

Rework test/end2end/ so every scenario runs on BOTH namespace paths via
self.subTest(namespace=...):
- spec: modernize=False, emit_legacy=False — utterance injected on the spec topic
  ovos.utterance.handle, core handles it natively, assertions use ovos.* topics.
- legacy: modernize=True, emit_legacy=False — utterance injected on the legacy
  recognizer_loop:utterance, the FakeBus modernize-bridge re-dispatches it as
  ovos.utterance.handle so the spec-only intent listener still handles it (legacy
  back-compat).
Topics come from ovos_spec_tools.SpecMessage with the legacy counterpart derived
via migration_counterpart (never hardcoded). Expected payloads updated to the
actual spec emissions (e.g. ovos.utterance.speak meta). 36 tests / 68 subtests
green against the real bus-client-2.x / workshop-8.x stack.

* test: require ovoscope>=1.0.0a1 (get_minicroft modernize/emit_legacy flags)

The dual-namespace end2end rework drives get_minicroft(modernize=, emit_legacy=)
to exercise both the legacy and ovos.* paths; those flags first ship in ovoscope
1.0.0a1. The previous <1.0.0 cap pinned the flagless 0.13.1, so CI could not run
the reworked tests. Verified: full extra set resolves (208 pkgs) and the e2e
suite passes on both namespaces with published ovoscope 1.0.0a1.
)

blacklisted_skills/blacklisted_intents are OPTIONAL SESSION-1 fields and are
legally absent (None) when a Session does not round-trip an empty list through
SessionManager. The stop, fallback, converse and intent services tested
membership/iteration directly (`x in sess.blacklisted_skills`), raising
TypeError: 'NoneType' object is not iterable and aborting the stop ping/pong
cascade, the fallback query cycle and the converse round-trip before any bus
traffic. Guard each use with `(... or [])`.

Surfaced by live integration testing in the OVOS spec-compliance harness
(ovos-bus-client 2.x leaves these fields None); validated end-to-end there.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
padacioso 2.0 (OVOS-INTENT-1 grammar) validated end-to-end in the OVOS
spec-compliance harness against the workshop-9 stack.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
spec-tools crossed to 1.x; bus-client 2.6.0a1 and the rest of the
stack now require ovos-spec-tools>=1.1.0a1. Drop the <1.0.0 cap
(keep the floor) so the stack resolves.

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

ovos-skill-fallback-unknown's 'unknown' dialog speaks with meta
{dialog, data, skill}; the e2e expected only {skill}, so test_fallback_match
fails on dev independently of any PR. Match the real skill output.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
ovos-core ships three opm.pipeline plugins (converse/fallback/stop) but
had no opm_check workflow. Add the shared gh-automations opm-check caller
@dev so entry-point registration is validated on PRs.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…l events (#788)

* feat: orchestrator emits the PIPELINE-1 §8 handler-lifecycle trio

Make ovos-core (the orchestrator) the authoritative emitter of the
OVOS-PIPELINE-1 §8 handler-lifecycle trio
(ovos.intent.handler.{start,complete,error}), wrapping every dispatch:
start immediately before the <skill_id>:<intent_name> dispatch (§7),
then exactly one of complete (on the framework done-signal) / error
(on the framework error signal or the §8.3 timeout). Each trio Message
is forward-derived from the dispatch so context (incl. session) is
preserved unchanged (§8, MSG-1 §5.1).

New IntentDispatcher (ovos_core/intent_services/dispatcher.py) owns the
§7 dispatch + §8 trio. Completion is observed across the distributed bus
via the skill framework's long-standing legacy done-signals
(mycroft.skill.handler.complete/.error) — framework infrastructure, not
the user handler (which emits nothing per §8/§11). A per-dispatch §8.3
timeout guarantees exactly one terminal even if the handler never
reports; on that path the orchestrator also owns ovos.utterance.handled.
Reserved-name dispatches get the trio identically (§7.0/§7.3); the
resolved-guard keeps the terminal count at one regardless of the bus
namespace bridge.

This is additive: the §9.5 end-marker on the ordinary matched path and
the §9.2 ovos.intent.matched notification are left to ovos-workshop /
follow-up changes and are out of scope here.

Dep floors: ovos-bus-client>=2.5.1a1, ovos-spec-tools>=0.17.3a1
(SpecMessage.INTENT_HANDLER_* members).

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

* feat: orchestrator owns the PIPELINE-1 §9 utterance-terminal events

Complete the orchestrator's ownership of the OVOS-PIPELINE-1 §6.1
per-utterance terminal sequence, on top of the §8 handler-lifecycle
trio:

- §9.2 ovos.intent.matched — emitted by _dispatch_match on every
  accepted match, before the dispatch goes out (notification, not a
  dispatch). Carries skill_id, intent_name (the full
  <skill_id>:<intent_name> match_type), lang, utterance, slots,
  pipeline_id.
- §9.3 ovos.intent.unmatched — the no-match / all-filtered terminal,
  replacing the legacy complete_intent_failure (the two are bridged by
  ovos-spec-tools' MIGRATION_MAP, so emitting the spec topic re-delivers
  the legacy one to consumers still on it).
- §6.4 cancellation now emits the spec ovos.utterance.cancelled.

Each utterance terminates with exactly one ovos.utterance.handled
(§9.5): core owns it on the no-match, cancel and §8.3-timeout paths;
on the ordinary matched path the skill framework still emits it (moving
that fully into core is gated on the ovos-workshop reduction).

Rename _emit_match_message -> _dispatch_match (it orchestrates the §6.1
post-match steps then dispatches) and correct the IntentDispatcher
docstring to scope it to §7 dispatch + §8 trio (the §9.2 notification
lives in the service).

Verified on a real minicroft: matched path emits matched/start/
complete/handled exactly once each; no-match path emits
ovos.intent.unmatched + ovos.utterance.handled (no complete_intent_
failure). test_no_skills / test_lang_detect conformance suites green.

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

* test: account for §9.2 matched + §8.1 start in activate/fallback e2e

The converse-deactivate (test_activate) and fallback (test_fallback)
ovoscope scenarios each gain two captured messages now that the
orchestrator emits ovos.intent.matched (§9.2) and ovos.intent.handler.
start (§8.1) natively before every dispatch — previously the spec trio
existed only as uncounted bridge-mirrors of workshop's legacy emit.

Verified on a real minicroft: the two extra messages per scenario are
exactly ovos.intent.matched + ovos.intent.handler.start (the
reserved-name converse:skill / fallback .request dispatches carry no
mycroft.skill.handler.* done-signal, so their §8 terminal resolves via
the §8.3 timeout after the end-marker, not captured). No spec-topic
double-emit: ovos.intent.matched, ovos.intent.handler.start and
ovos.utterance.handled each appear exactly once per utterance.

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

* feat: core emits ovos.utterance.handled on the matched path too (§9.5)

The orchestrator owns the universal end-marker on EVERY terminal path. The
dispatcher already emitted ovos.utterance.handled on the timeout path; emit it
after the complete/error terminals as well, so a matched dispatch also ends with
exactly one core-emitted handled (the _pop/resolved guard fires one terminal per
dispatch -> one handled). Unmatched/cancel keep their service.py handled.

Workshop may still emit its own matched-path handled during the migration window;
that core-vs-workshop duplicate is expected and removed later workshop-side.

* chore: bump ovos-workshop floor to >=9.0.1a5 (HandlerLifecycle delegation merged)

* chore: bump ovos-workshop floor to >=9.0.2a1 (matched-path handled guard)

ovos-workshop 9.0.2a1 (#442) guards its matched-path ovos.utterance.handled
emission behind a version check on the installed ovos-core, so once core ships
the §9.5 matched-path emission the framework stops double-emitting.

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

* chore: stage version 2.3.0a1 so CI exercises the workshop §9.5 guard

This feat bumps core to the 2.3.x line. ovos-workshop 9.0.2a1 suppresses its
matched-path ovos.utterance.handled only when the installed ovos-core is
>=2.3.0a1; staging the version here lets PR CI install a core that trips that
guard, so the e2e exercises core as the single end-marker emitter rather than
relying on the published 2.2.x. Release automation re-derives the final version
on merge.

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

* fix(deps): floor-pin ovos-m2v-pipeline>=0.3.1a1 (workshop 9.x compatible)

The published m2v 0.0.10a1 caps ovos-workshop<9.0.0; with core's workshop floor
at 9.0.2a1 pip backtracked m2v down to 0.0.10a1 and hit that cap -> ResolutionImpossible.
m2v 0.3.1a1 drops the workshop dependency entirely, so floor-pinning it (the
prerelease-floor-pin pattern) forbids the backtrack and the closure resolves.

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

* fix(deps): floor-pin downstream stack to spec-tools-1.x-ready prereleases

core's bus-client>=2.5.1a1 pulls ovos-bus-client 2.5.1a3/2.6.0a1, which require
ovos-spec-tools>=1.1.0a1. pip backtracked the mycroft/plugins/skills-essential
extras down to stale releases that cap ovos-spec-tools<1.0.0 (e.g. ovos-audio
2.0.1a1) -> ResolutionImpossible. Floor-pin each to its latest prerelease (all
spec-tools-1.x-ready) so the resolver can't backtrack into the capped ones, and
make core's own ovos-spec-tools floor explicit at >=1.1.0a1.

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

* fix(deps): floor-pin ovos-adapt-parser>=1.4.2a1 (spec-tools-1.x ready)

Every ovos-adapt-parser release up to 1.4.1a1 caps ovos-spec-tools<1.0.0; the
uncap landed in 1.4.2a1. Floor-pin it so pip can't backtrack into the capped
versions while core requires ovos-spec-tools>=1.1.0a1.

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

* test: align e2e expectations with §9.5 end-marker payload

- fallback speak meta carries the skill's dialog/data keys, not just skill.
- ovos.utterance.handled (§9.5) is the orchestrator end-marker with EMPTY data;
  the stop count-to-infinity / ping-pong expectations wrongly carried the
  handler name on it (KeyError 'name'). The handler name stays on the framework
  mycroft.skill.handler.complete signal, where it belongs.

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

* refactor: orchestrator owns ovos.utterance.handled, dispatcher only signals done

PIPELINE-1 §9.5: ovos.utterance.handled is the orchestrator's universal end-marker,
not the dispatcher's. The IntentDispatcher owned only the §8 handler-lifecycle trio
but was also emitting the §9.5 end-marker on its terminal paths — wrong layer.

- IntentDispatcher no longer emits ovos.utterance.handled. Each in-flight dispatch
  carries a 'done' Event set on its §8 terminal (complete/error/timeout); dispatch()
  returns the entry.
- IntentService._dispatch_match blocks on entry.done (the §8.3 timeout guarantees it
  fires) then emits the single ovos.utterance.handled, uniformly with the no-match
  (send_complete_intent_failure) and cancel (send_cancel_event) paths it already owns.

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

* refactor: dispatch entry is a context manager that blocks until the §8 terminal

Reads cleaner at the call site — the event-waiting is hidden behind the context
manager:

    with self.intent_dispatcher.dispatch(reply, skill_id, intent_name):
        pass
    self.bus.emit(reply.forward(SpecMessage.UTTERANCE_HANDLED, {}))

_InFlightDispatch gains __enter__/__exit__ (exit blocks on its done event). Callers
that don't want to block can still wait on entry.done directly.

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

* refactor: keep explicit entry.done.wait() call site (drop context manager)

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

* fix: guarantee dispatch waiters are always released (review)

- terminal handlers wrap the §8 emission in try/finally so entry.done is set even
  if the bus emission raises (the §8.3 timer is already cancelled by _pop, so
  nothing else would release a blocked orchestrator).
- IntentDispatcher.shutdown() sets each in-flight entry's done before clearing,
  so a _dispatch_match caller is never left blocked on entry.done.wait() forever.
- test_timeout_emits_error_and_releases waits on entry.done instead of a fixed
  sleep (deterministic); test_orchestrator_emits_handled_after_terminal now
  asserts ovos.intent.handler.complete precedes ovos.utterance.handled.

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

* test(e2e): pin §8.3 handler-timeout to 10s in the end2end suite

The orchestrator blocks on each handler's §8 terminal before emitting §9.5
ovos.utterance.handled; the production backstop is 5min. e2e handlers report in
<1s (or are explicitly stopped), so pin the backstop to 10s — a dropped
done-signal then fails the suite in seconds instead of stalling for minutes.

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

* fix: orchestrator emits utterance.handled by reacting to the §8 terminal (non-blocking)

Blocking handle_utterance on entry.done.wait() deadlocked the synchronous bus:
handle_utterance is itself a bus handler, so blocking it stalled the same path
that must deliver the handler's done-signal — the §8.3 timer then fired
ovos.intent.handler.error instead of the handler completing (8 e2e tests).

Keep the orchestrator as the §9.5 owner, but non-blocking: IntentService now
subscribes to the dispatcher's §8 terminal (ovos.intent.handler.complete/error)
and emits ovos.utterance.handled in reaction, uniformly with the no-match and
cancel paths. The dispatcher reverts to trio-only (no done event, no blocking);
the §8.3 timeout still backstops via the error terminal.

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

* test(e2e): expect the active skill's §8 complete terminal during stop

The count/ping-pong stop tests inject a long-running intent (daemon) then stop it.
When that daemon intent completes (on stop), its dispatch now emits the §8 spec
terminal ovos.intent.handler.complete before the §9.5 ovos.utterance.handled, so
add it to the expected sequence (got 10 messages, expected 9).

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

* fix: emit utterance.handled via dispatcher on_terminal callback (deterministic)

The reactive subscription (IntentService listening to ovos.intent.handler.complete)
raced the ovoscope capture: FakeBus dispatches the terminal to both the capture
recorder and the subscription, and when the subscription fired first it emitted the
EOF ovos.utterance.handled before the terminal was recorded — so the capture stopped
early and dropped the §8 complete (flaky 9-vs-10 message counts).

Make it deterministic: the dispatcher invokes an on_terminal callback right AFTER
the §8 terminal is on the bus; the orchestrator's _emit_utterance_handled emits the
§9.5 end-marker then. Same call stack, so the terminal is always observed before the
end-marker. Orchestrator still owns the emission; dispatcher stays non-blocking.

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

* Update ovos-utils version in pyproject.toml

* StopService: wrap handlers in HandlerLifecycle instead of ad-hoc emission (#796)

* Update Changelog

* StopService: wrap handlers in HandlerLifecycle

Replace rudimentary manual emission of both UTTERANCE_HANDLED (which
the orchestrator now owns via IntentDispatcher._notify_terminal) and
mycroft.skill.handler.complete with the canonical HandlerLifecycle
context manager from ovos-bus-client.

HandlerLifecycle consistently emits the full handler-lifecycle trio
(start/complete/error) so that the dispatcher can properly track
in-flight entries and fire §9.5 UTTERANCE_HANDLED at the right moment.

Co-Authored-By: Claude

* ConverseService: wrap handle_converse in HandlerLifecycle

Same pattern as StopService — handle_converse is called via bus event
dispatch after the converse pipeline stage matches, but had no lifecycle
signalling. Without it the dispatcher's in-flight entry for converse
dispatches would only ever resolve on timeout (10s).

Co-Authored-By: Claude

* Update stop_service unit tests for HandlerLifecycle

test_handle_global_stop_emits_mycroft_stop: check for
mycroft.skill.handler.start/complete instead of ovos.utterance.handled.

test_handle_skill_stop_forwards_to_skill: HandlerLifecycle emits
3 messages (start, forward, complete), not 1.

Co-Authored-By: Claude

* Update stop service tests to include additional assertions

Added assertions to check for 'mycroft.stop' and 'ovos.utterance.handled' messages in stop service tests.

* feat: emit handler done-signal for converse + fallback dispatches (PIPELINE-1 §8) (#789)

* feat: emit handler done-signal for converse + fallback dispatches

The orchestrator's converse and fallback dispatches (PIPELINE-1 §7.3
reserved-name/polymorphic dispatches) run in skills WITHOUT ovos-workshop's
handler_info wrapper, so they never produce the framework done-signal
(mycroft.skill.handler.{start,complete,error}). A dispatcher observing that
signal (PIPELINE-1 §8, the IntentDispatcher) therefore never sees a completion
for these dispatches and falls back to its 5-minute handler timeout.

Adopt the shared HandlerLifecycle util (ovos_bus_client.handler, bus-client
2.6.0a1) so core reports the dispatch->outcome span it orchestrates:

- converse: handle_converse emits handler.start at the converse.request
  dispatch, registers a one-shot skill.converse.response listener (filtered by
  the targeted skill_id) -> handler.complete, with a generous timeout backstop
  -> handler.error. The done-signal is stamped with the targeted skill_id so a
  dispatcher correlates it by (session_id, skill_id).

- fallback: the fallback dispatch is owned by the orchestrator; core translates
  each registered skill's own lifecycle markers
  (ovos.skills.fallback.<skill_id>.start/.response) into
  handler.start/handler.complete, stamped with that skill_id. Wired on register,
  removed on deregister/shutdown.

stop_service is intentionally NOT touched here (owned by PR #777 / STOP-1).

Floor ovos-bus-client>=2.6.0a1 (first release shipping ovos_bus_client.handler)
and the coupled ovos-spec-tools>=1.1.0a1 it requires.

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

* fix: drop ovos-spec-tools upper version cap

Keep the >=1.1.0a1 floor (bus-client 2.6.0a1 requires it); remove the
<2.0.0 max cap so spec-tools is free to float forward.

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

* Update Changelog

---------

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

* test(e2e): ignore TTS mock audio signals in all end2end tests

* fix(test): remove ovos.utterance.handled assertion from TestBusHandlers (orchestrator owns it now)

* fix: correlate fallback dispatch to its §8 terminal via match_data skill_id

The fallback pipeline's match_type (ovos.skills.fallback.<id>.request) carries
no ':', so the orchestrator derived the dispatcher correlation key as the whole
topic. The framework done-signal (re-emitted mycroft.skill.handler.complete) is
stamped with the real skill_id, so IntentDispatcher._pop never matched it and the
§8 ovos.intent.handler.complete terminal — and with it the §9.5
ovos.utterance.handled end-marker — only fired on the 5-minute §8.3 handler
timeout. Every fallback-handled utterance was affected in production.

Derive the correlation key from match_data['skill_id'] when the topic has no ':',
so it equals the done-signal's skill_id. Activation is unchanged (match.skill_id
stays None, no spurious {skill_id}.activate). test_fallback now asserts the §8
terminal, which can only be captured when correlation succeeds.

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

* test(e2e): align stop suite with §8 trio + live-session resend

StopService wraps its global/skill stop handlers in HandlerLifecycle (#796), so
the legacy mycroft.skill.handler.{start,complete} done-signal now brackets
mycroft.stop / {skill_id}.stop. Update the stop expectations to assert the trio.

The ping-pong tests built the stop message from a stale, test-local Session that
never saw the count skill's server-side self-activation (the count message,
serialized before activation, folds an empty active_skills back into the
singleton — correct SESSION-1 last-write-wins). Resend the live singleton session
for the stop turn, as a real client tracking responses would, instead of manually
activating — so the running skill is in active_skills and the ping-pong path runs
without a manual activate crutch. Also drop the §8 ovos.intent.handler.complete
from the expected lists where it is filtered via ignore_messages.

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

* fix: make converse dispatch terminal resolution atomic + session-scoped

handle_converse resolved the §8 lifecycle from two threads (the
skill.converse.response handler and the timeout timer) with a non-atomic
check-then-set on an Event, so both could pass the guard and emit two framework
done-signals (complete + error). Claim the resolution under a Lock so exactly one
terminal fires. Also ignore acks carrying a different session_id, so a concurrent
converse dispatch to the same skill in another session cannot cross-resolve.

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

* test(e2e): converse §8 trio in deactivate + ignore racy stop-cleanup artifacts

- test_deactivate_inside_converse: ConverseService now reports the dispatch via
  the mycroft.skill.handler.* done-signal which the orchestrator translates to the
  §8 ovos.intent.handler.complete terminal; assert the trio (+3 messages).
- The ping-pong stop tests interrupt a running skill; the async stop-pipeline
  cleanup (abort_question / converse.force_timeout / audio.speech.stop) fires or
  not depending on exactly where the stop lands, so it raced the message count in
  CI. Ignore those artifacts (they are not what the tests assert) and drop the
  flaky force_timeout async_messages assertion.

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

* test(e2e): stop ping-pong tests assert only through the stop terminal

The count-to-infinity ping-pong scenarios interrupt a running skill. After the
stop turn's ovos.utterance.handled, the interrupted count daemon exits and races
in its own CountSkill complete + a second ovos.utterance.handled — a tail whose
exact contents/timing depend on where the stop lands relative to the 1s count
loop (it produced a non-reproducible +1 message in CI's parallel workers).

Capture only through the deterministic stop turn (eof_msgs=[ovos.utterance.handled])
and drop the racy daemon-completion tail from the expected sequence. The stop
routing — ping/pong, activate, stop:skill, the StopService HandlerLifecycle trio,
{skill}.stop(.response), and the stop turn's end-marker — is fully asserted.

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

* refactor: StopService is a pipeline plugin, not an ovos-workshop skill

StopService subclassed OVOSAbstractApplication purely for voc_match/voc_list/locale
loading. That base class also registered it as a skill (skill_id=stop.openvoiceos),
so it answered the mycroft.stop broadcast with stop.openvoiceos.stop.response —
StopService 'stopping itself', a leak that polluted the stop lifecycle.

Drop OVOSAbstractApplication and load the stop/global_stop .voc files via
ovos-spec-tools LocaleResources (the plugin-agnostic voc matcher, same role
common-query/OCP use). self.bus and self.config come from ConfidenceMatcherPipeline.
No more skill machinery — no stop.openvoiceos.stop.response.

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

* test(e2e): assert stop dispatch lifecycle via ovoscope skill_id filter

Stopping a running skill produces two concurrent dispatch lifecycles (the stop
dispatch + the interrupted skill's own §8 trio/§9.5 terminal) whose messages
interleave non-deterministically under load — the source of the persistent
count-mismatch flakiness. Assert the stop dispatch lifecycle in isolation via the
new ovoscope End2EndTest skill_id filter (skill_id=stop.openvoiceos) with
eof_count=2 so capture spans both utterances' ovos.utterance.handled. The full
stop §8 trio + §9 terminals are now modelled deterministically; the interrupted
skill's §8 trio is covered (uninterrupted) by test_count.

Also: TestStopServiceAsSkill -> TestStopServiceNotASkill (regression guard that
StopService no longer emits stop.openvoiceos.stop.response), drop the now-dead
stop-response ignores, and floor-pin ovoscope>=1.4.0a1 for the new features.

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

* test(unit): update test_stop_service for the pipeline-plugin refactor

StopService no longer subclasses OVOSAbstractApplication; vocabulary matching is
delegated to self._locale (ovos-spec-tools LocaleResources). Drop the removed
OVOSAbstractApplication.__init__ patch from the service factory and redirect the
voc_match/voc_list patches to svc._locale.

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

* fix: orchestrator survives a pipeline matcher raising; fix malformed stop .voc

Two issues surfaced by the StopService spec-tools refactor (LocaleResources.voc_match
is strict where OVOSAbstractApplication.voc_match was lenient):

1. A pipeline matcher raising (here: a malformed .voc) propagated out of the
   handle_utterance loop and aborted the WHOLE utterance — no match was tried and
   no §9.3/§9.5 terminal fired. Wrap the match_func call in try/except: log and
   treat as no-match so iteration continues. Any pipeline plugin can misbehave;
   one bad matcher must not break the utterance.

2. ca-es/stop.voc, ca-es/global_stop.voc and de-de/global_stop.voc had single-branch
   groups '(x)' which ovos-spec-tools rejects (a group needs >=2 branches). The old
   lenient parser treated them as the mandatory token x; drop the parens to preserve
   that matching with a valid template.

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

* test(e2e): stop ping-pong tests assert only the deterministic stop messages

The §8 SPEC trio (ovos.intent.matched / ovos.intent.handler.start / .complete) is
not reliably observed in these concurrent-lifecycle stop scenarios under heavy
parallel CI load (the orchestrator's spec-namespace messages drop relative to the
legacy done-signal — reproduced only at full-suite xdist scale, never in isolation).

Scope the assertion to the deterministic, always-present messages: the stop
activation, the stop:skill/stop:global dispatch, the StopService HandlerLifecycle
done-signal trio (mycroft.skill.handler.start/complete — which the orchestrator
translates into the §8 terminal), and the §9.5 ovos.utterance.handled end-marker.
The §8 spec trio is filtered via ignore_messages here and asserted deterministically
in the single-lifecycle adapt/padatious suites.

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

* fix: CR2/CR4/CR5/CR7 bugs, meta-commentary cleanup, SpecMessage migration

- dispatcher: fix stale timer on shutdown (mark unresolved resolved before clear)
- converse_service: fix msg guard ordering before SessionManager.get
- converse_service: fix skill_max=0 treated as disabled
- fallback_service: fix priority=0 being treated as falsy
- service: skip pipeline matchers with empty match_type (CR5)
- service: resolve skill_id consistently in INTENT_MATCHED (CR7)
- all tests: replace hardcoded spec topics with SpecMessage.X
- all tests: add try/finally for MiniCroft cleanup
- pyproject.toml: add <2.0.0 upper bound for ovos-spec-tools

* fix: replace sleep(2) with deterministic skill-activation poll in stop e2e tests

Under parallel CI load (xdist 4 workers) the fixed sleep was too short,
causing test_count_infinity_stop_low to get 4 messages instead of 6
(the session hadn't been updated yet, so a global stop fired instead of
a skill-specific stop). Replace with _wait_for_active_skill that polls
SessionManager.active_skills with a 10s timeout.

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: INTENT-4 §10 orchestrator manifest (IntentManifest)

Adds IntentManifest helper that indexes ovos.intent.register.*
broadcasts into a (session_id, skill_id, intent_name, lang, method)
keyed dict and serves ovos.intent.list / ovos.intent.describe pull
queries (§10.1 / §10.2).  Handles deregister, enable/disable, and
skill teardown (§§8.2–8.5).  Session-scoped inheritance follows §11.2:
satellite sessions merge the default pool.

IntentService now holds self.intent_manifest = IntentManifest(bus).

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

* fix: IntentManifest.shutdown() unregisters bus listeners

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

* fix: add intent_manifest to test _make_service to prevent shutdown crash

The _make_service helper in test_intent_service_extended.py bypasses
IntentService.__init__, so the intent_manifest attribute was never set.
When shutdown() was called, it crashed with AttributeError because
self.intent_manifest didn't exist.

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

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
JarbasAl and others added 30 commits September 6, 2026 11:55
)

ovos-spec-tools 1.11.0a2 (#138) removed SpecMessage.SESSION_SYNC because
OVOS-SESSION-2 §2.7 defines no topic on which any participant pushes a
session at another; ovos.session.sync retires with a one-cycle shim in
ovos-bus-client. test_session2_intake.py and test_skill_manager.py still
referenced the removed constant, raising AttributeError on collection
(6 failures against dev). They now spell the topic as a local
LEGACY_SESSION_SYNC = "ovos.session.sync" literal, never imported from
bus-client, with a comment naming it as the retired pre-spec push.

The TestConnectToBusExactlyOnce docstring's claim that IntentService owns
its own ovos.session.sync subscriber is stale post-#935: IntentService
subscribes to nothing on that topic, per TestSessionSyncIsRetired in the
other file; the docstring is corrected to that end state.

Fail-before: git apply -R on the source diff to revert only the two test
files' constant back to SpecMessage.SESSION_SYNC reproduces the original
6 AttributeErrors; restoring the diff returns 550 passed, 0 failed on
test/unittests, matching a clean-dev baseline of 544 passed / 6 failed
plus these 6 now passing.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The orchestrator implemented every transformer chain except the
typed-slots stage, so a deployment that installed a typed-slots
transformer had nowhere for it to run and `data.typed_slots` never
reached a handler.

The stage runs after the utterance and metadata chains and before the
first matcher. TypedSlotsTransformersService loads the
`opm.transformer.typed_slots` plugins from the `typed_slots_transformers`
config block every other transformer kind already has, and runs exactly
one of them: the first entry of an explicit order list where one is
configured, otherwise the lowest priority number. The selection is
resolved once, so a tie no list resolves is reported when it is made
rather than on every utterance. What the transformer computes from the
declared type set is its own decision — §3.7 lets it compute every
registered type where the deployment asks for that, and withholding the
call would turn "computed, found nothing" into "not computed", which
OVOS-INTENT-1 §5.6 reads as a different answer.

The manifest exposes the declared type set, read from both places an
INTENT-4 §6.1 registration can carry it — the `slot_types` map and the
`{type:name}` prefixes of `samples`. That scan is O(registered intents),
so it only runs when a transformer is loaded to receive it.

A map a producer put on the entry Message is discarded when the utterance
chain rewrote the text its spans index, replaced wholesale when the stage
runs, and filtered against the INTENT-1 §5.6 closed type set either way,
so a deployment running no transformer still drops unregistered keys from
what it carries. §3.2 makes in-place mutation of the utterance list
conformant, so `_handle_transformers` now snapshots the entry text
instead of aliasing it — an in-place plugin returns the very list it was
handed, and the old comparison could not see the rewrite. A malformed map
is dropped rather than propagated, and a plugin that raises leaves no map
and does not stop the round (§7). What survives rides `message.data` to
dispatch (OVOS-PIPELINE-1 §7.1); `ovos.intent.matched` is unchanged,
since §9.2 does not list the field.

Fail-before: reverting service.py fails 9 of the 31 new tests
(`AttributeError: 'IntentService' object has no attribute
'_run_typed_slots_stage'`, producer maps surviving a rewrite, unregistered
keys reaching dispatch); reverting manifest.py fails the 5
declared-type tests; re-aliasing the utterance list fails the in-place
rewrite test, and short-circuiting the lazy declared-types guard fails
the test that pins it.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…ead of silence (#943)

A non-string lang/skill_id/intent_name/method/session_id in an
ovos.intent.describe or ovos.intent.list request reached
standardize_lang's tag.lower() and raised, and the bus handler
swallows handler exceptions, so no .response was ever emitted and
the caller hung forever. Validate every provided filter field is a
string once at the top of both handlers and reply {ok: false, error}
per OVOS-INTENT-4 §10.1/§10.2's "the spec's error path is a reply,
never silence" — standardize_lang and the existing missing-skill_id /
unknown-skill error strings are untouched.

Added regression tests in test/unittests/test_intent_manifest.py for
both handlers: non-string lang, skill_id, session_id on
ovos.intent.list, and non-string lang, skill_id=None, intent_name,
session_id, method on ovos.intent.describe, plus an all-valid-fields
control case. Every new test asserts the exact error string so it
discriminates the new guard from paths that already replied ok:false
on dev for other reasons (skill_id=None is caught by the pre-existing
"skill_id is required" check, not the new guard, and stays asserted
on that string). Both query helpers now dispatch through bus.emit
instead of calling the handler directly, so a crash is caught the
same way production does (swallowed, no reply) rather than
propagating straight into the test.

Confirmed by patch-revert: reverting only manifest.py made the
lang/intent_name describe cases and the lang list case fail with
"unexpectedly None" (the exception was swallowed, no reply captured),
and made the skill_id/session_id list cases fail on ok:true instead
of ok:false. Restoring the fix makes all 53 tests in
test_intent_manifest.py pass, and the full test/unittests suite
passes at 559 tests (550 pre-existing plus 9 new).

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…6) (#948)

OVOS-INTENT-1 §5.6 requires an orchestrator to drop a typed_slots key
whose list is empty at the same point it drops a key naming an
unregistered type. ovos-spec-tools 1.11.1a1 moved this into
drop_unregistered_typed_slots itself, and its own warning already
names each dropped key with the actual reason (unregistered or
empty). ovos-core's per-key warning loop in _run_typed_slots_stage
predated that and unconditionally claimed every drop was "not a type
registered", which is false for a registered type dropped only
because its list was empty; remove the now-redundant loop and bump
the floor pin.

Added regression coverage: an empty-list type is dropped and the
surviving type rides the dispatch Message; a non-conformant plugin's
empty-list-only map carries no entry for that type; the drop-before-
validate order is pinned (reversing it would reject the whole map);
and the misleading "not a type registered" message is confirmed gone
for a registered-but-empty type. The last one is red before the fix
(the removed loop wrongly reported the empty "number" type as
unregistered) and green after.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…ra (#950)

ovos-core ships the default plugin set through the `plugins` extra in
pyproject.toml. Add ovos-typed-slots-transformer as a floor pin
alongside the other utterance transformers so `pip install
ovos-core[plugins]` installs it. Enabling the transformer remains a
config decision via the `typed_slots_transformers` block and stays
off by default.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
The comment on lines 687-688 suggested `drop_unregistered_typed_slots` logs
each dropped key individually with its reason. The live gate showed the
function logs them once in a single warning naming all keys; update the
comment to match the actual behavior.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
…VOS-INTENT-4 §3.2) (#956)

_on_register, _on_deregister, and _on_skill_deregister in
IntentManifest read skill_id via `message.data.get("skill_id") or
message.context.get("skill_id")`, preferring the payload over the
context. OVOS-INTENT-4 §3.2 makes context["skill_id"] authoritative
for these topics and requires dropping any message whose payload
skill_id differs from it — a skill could otherwise register or
deregister another skill's intents, and ovos.skill.deregister would
amount to a remote uninstall.

Each handler now reads skill_id from context only, drops the message
with a WARN log when context lacks it, and drops with a WARN log when
a present payload skill_id differs from the context value.
_on_enable_disable is untouched: §3.2 exempts ovos.intent.enable/
disable as control messages where payload skill_id names the target
and context skill_id names the source, and the two MAY differ.

Fail-before: reverting only manifest.py made the 5 new mismatch/
missing-context tests fail (register, deregister, skill_deregister),
confirming they exercise the fix. Existing tests elsewhere that built
register messages with an empty context were updated to carry the
producing skill_id in context, matching how the orchestrator actually
stamps dispatch messages (§3.1).

Full suite: 594 passed on dev baseline; 602 passed on this branch (+8
new regression tests, 0 regressions).

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
…-owned policy field is re-imposed (OVOS-PIPELINE-1 §9.1.1, §5.5) (#957)

§9.1.1: `_stamp_utterance_id` kept a caller-supplied `utterance_id` instead
of replacing it at lifecycle entry, letting an entry Message dictate its
own lifecycle id and letting a stale poll pong survive under it. It now
always assigns a fresh id at entry and logs at debug when one was
replaced; the no-overwrite behaviour still applies downstream (the
transformer-chain re-assertion in `handle_utterance`).

§5.5: `_dispatch_match` accepted a plugin's `updated_session` wholesale,
subject only to a `resolved_session_id()` equality guard, without
re-imposing any deployment-owned per-component override field from the
session the orchestrator held before the plugin ran. It now snapshots and
restores the full class SESSION-1 §3 registers for this role: `pipeline`,
the six OVOS-TRANSFORM-1 §5 transformer-chain lists, the three blacklist
denylists (`blacklisted_skills`/`_intents`/`_pipelines`), and `site_id`.

Fail-before: reverting only the source change (test unchanged) fails all
3 new tests - `test_entry_stamp_replaces_any_supplied_identifier`,
`test_stale_pong_carrying_the_replaced_id_is_discarded`, and
`test_updated_session_cannot_relax_deployment_owned_policy` - each on the
exact assertion the fix now satisfies. Full suite: 596 unittests + 42
end2end passed after the fix.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
OVOS-PIPELINE-1 §7.3 reserves converse, response, stop, fallback and
common_query: skills and pipelines MUST NOT register under these
names, and OVOS-INTENT-4 §5.3/§6.3 requires the malformed registration
be logged at WARN and not indexed. The manifest's `_on_register` only
special-cased the literal name `stop`, leaving the other four
reserved names free to shadow the targeted-dispatch topics their
reserving specs define.

Generalises the check into a module-level `RESERVED_INTENT_NAMES`
frozenset carrying the §7.3 citation, applies it to every
`ovos.intent.register.*` topic the manifest indexes, and extends the
same refusal to `_on_deregister` (a reserved name was never indexed,
so deregistering it is a no-op, now logged rather than silent).

Fail-before: reverting the source change makes
`test_intent_manifest.py` fail collection with
`ImportError: cannot import name 'RESERVED_INTENT_NAMES'` (the test
module now imports the constant); restoring the source makes all 62
tests (5 subtests) pass, and the full unit suite (605 tests) is green.

Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
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.

3 participants