Spec ID: OVOS-STOP-1 · Version: 2 · Status: Draft
This specification defines the stop pipeline plugin — a pipeline plugin that matches utterances expressing the user's intention to interrupt the assistant's current activity — and the bus surface by which it cascades a stop request across the recency-ordered list of active handlers or broadcasts a global stop signal.
The intent_name stop is reserved at OVOS-PIPELINE-1 §7.3.
Dependencies: OVOS-MSG-1 (envelope and derivations), OVOS-PIPELINE-1
(pipeline-plugin contract, dispatch shape, reserved-name registry,
active_handlers stamping), OVOS-SESSION-1 (session field registry),
OVOS-SESSION-2 (mutation boundaries), OVOS-CONVERSE-1 (response_mode
and converse_handlers field definitions).
The key words MUST, MUST NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY are used as in RFC 2119.
This specification defines: the stop plugin role, the reserved
intent_name stop, the stoppability discovery and cascade algorithm,
the global broadcast, and the session-scoping obligations of stop
subscribers.
It does not define: vocabulary file formats, matching algorithms, confidence thresholds, audio capture control, handler-side framework APIs, wake-word and barge-in policies, or post-stop in-flight interaction teardown (a skill-side or orchestrator-side concern).
| Reserved intent_name | Meaning |
|---|---|
stop |
Cease activity for the inbound session_id. Dispatched on <target_skill_id>:stop where the target is the most recently activated positive pong responder, or — when no positive pong arrives in time — the most recently activated remaining active_handlers entry (§4.1). |
Skills and other pipelines MUST NOT register stop under
OVOS-INTENT-4. A registration naming this intent_name is malformed per
OVOS-INTENT-4 §5.3/§6.3 — consumers log at WARN and do not index.
The intent_name global_stop is not reserved. The stop plugin
uses it for its own self-dispatch (<stop_plugin_id>:global_stop, §5),
namespaced under its own pipeline_id.
A stop plugin is an ordinary pipeline plugin (PIPELINE-1 §3) that matches stop-command utterances and returns Matches under §2. It is subject to the same denylist filtering, first-match-wins iteration, and circuit-breaker rules as every other pipeline plugin.
A stop plugin instance has one pipeline_id, in the same
namespace as a skill_id and indistinguishable from one
(OVOS-PIPELINE-1 §3). A deployment MAY reference that plugin from
several session.pipeline entries — separate confidence tiers, for
example — but an entry is a reference to a match configuration, not
an actor: every such entry resolves to the same plugin instance and
the same pipeline_id, and the tiers have no identities of their
own.
Because the tiers are one actor, the obligation is trivially
satisfiable and stays a MUST: a stop plugin instance MUST
emit exactly one ovos.stop broadcast per global stop event per
session, however many entries reference it.
Match.skill_id MUST equal:
- for
intent_name: "stop"— the target selected per §4.1 (the most recently activated positive pong responder, or the recency fallback of step 5); - for
intent_name: "global_stop"— the stop plugin's ownpipeline_id(§5.2).
A stop plugin MUST:
- return
Nonefor any language for which it cannot resolve stop vocabulary; - read
session.active_handlersto drive the §4 cascade; - perform the ping-pong exchange (§4.2) inside
match.
A stop plugin SHOULD provide explicit "stop everything" vocabulary that
maps directly to intent_name: "global_stop" without running the §4
cascade. Generic stop utterances run the cascade per §4.
Inside match:
- Read
session.active_handlers. If it is empty, or if its only entries name the stop plugin's ownpipeline_id(§5.2), return aglobal_stopMatch per §5. - Emit
ovos.stop.ping, derived viareplyfrom the inbound utterance Message (OVOS-MSG-1 §5.2), and collectovos.stop.pongresponses within a deployer-configured timeout (RECOMMENDED default: 0.5 s; SHOULD NOT exceed 1 s). - Identify positive responders: valid pongs (§4.2) where
can_handleistrueandskill_idappears insession.active_handlers. Pongs from skills not inactive_handlersMUST be ignored. - If at least one positive responder exists, form the candidate set
from the
active_handlersentries whoseskill_idis a positive responder, apply the candidate filter and the recency rule below, and constructupdated_sessionremoving the selectedskill_idfromactive_handlersand clearing anyresponse_modeentry it owns. ReturnMatch(skill_id=<that_skill_id>, intent_name="stop", updated_session=...). Selection MUST be restricted to positive responders: an entry that did not answer the ping positively MUST NOT be selected at this step, however recent it is. - If no positive responder exists but
active_handlersis non-empty, the stop plugin MUST fall back to recency: form the candidate set from everyactive_handlersentry, apply the candidate filter and the recency rule below, and return aMatchconstructed exactly as in step 4. The plugin MUST NOT escalate toglobal_stopwhen no pong arrives.global_stopremains reserved for explicit global-stop vocabulary (§3.2) and the empty-active_handlerscase (step 1).
Candidate set. The candidate set assembled at step 4 or step 5
includes the skill_id named by session.response_mode
(OVOS-CONVERSE-1 §2.2), when that field is present — CONVERSE-1 §2.2
sets response_mode as session-resident state with no bus event and
no active_handlers push, so its holder would otherwise be invisible
to this cascade. The candidate filter below applies to this entry
exactly as to any active_handlers entry.
session.response_mode carries no activated_at and therefore
cannot be placed by the recency rule below, which orders only
active_handlers entries. Its holder MUST instead be selected by a
fixed rule ahead of recency: if the response_mode entry survives
the candidate filter, it MUST be selected as the target and the
recency rule below is not consulted; only when response_mode is
absent, filtered out, or its holder does not survive selection at
step 4 does the recency rule choose among the remaining
active_handlers entries.
Candidate filter. Before any recency comparison, an
active_handlers entry MUST be skipped when:
- its
skill_idappears insession.blacklisted_skills(§6.3); or - its
skill_idequals the stop plugin's ownpipeline_id— the entry PIPELINE-1 §7.1 stamps for a precedingglobal_stopdispatch (§5.2). The stop plugin is never its own stop target.
If no candidate survives the filter at step 4 or step 5, match MUST
return None; the orchestrator continues iteration to the next
pipeline stage.
Recency rule. Among the surviving candidates, select the entry
with the highest activated_at. If two entries share the same
activated_at, select the entry nearest the head of active_handlers
— the most recently pushed. PIPELINE-1 §7.1 is the normative home of
both the push mechanics and this tie-break; this section only applies
them.
Exactly one skill is stopped per stop utterance. match returns a
single Match naming a single target, and the orchestrator dispatches
stop to that target only.
The stop cascade polls its candidates with a candidate poll (PIPELINE-1 §4.5), on the topic pair
ovos.stop.ping → ovos.stop.pong
with the ping, pong, correlation, validity, duplicate and window
rules of that section, including the requirement that identity is
the payload skill_id verified against the round's candidate set.
What follows is what is specific to stop.
The candidate set is the active_handlers pool the round
queried (§4.1 step 3). The ping payload MAY be empty: the pool is
plugin-side state, and nothing in the question is candidate-specific.
The pong payload is the PIPELINE-1 §4.5 shape, with no field of this specification's own:
{ "skill_id": "example.skill", "can_handle": true }What a claim means. can_handle: true asserts that the handler
has user-visible or session-affecting activity in progress for the
inbound session_id and is prepared to cease it on receipt of
<skill_id>:stop. A handler with no current activity for that
session_id MUST NOT respond can_handle: true. It MAY respond
can_handle: false or remain silent. A handler that does not
subscribe to ovos.stop.ping is treated as can_handle: false for
that round.
The window. RECOMMENDED default 0.5 s, deployer-configured, and it SHOULD NOT exceed 1 s.
The ordering rule is the recency rule of §4.1, applied to the
positive responders. If no handler declares stoppability, the
cascade falls back to the most recently activated remaining
active_handlers entry per §4.1 step 5 — it does not escalate to
global_stop. The selection made at step 4 or step 5 is final for
that utterance.
The orchestrator dispatches <target_skill_id>:stop per PIPELINE-1 §7,
firing the handler-lifecycle trio (ovos.intent.handler.start,
.complete, .error). 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. The stop handler does not emit either event
itself. The PIPELINE-1 §8.3 timeout applies only if the handler fails
to return within the deployment-defined bound.
The stop handler MUST:
- cease the activity it declared stoppable, scoped to the inbound
session_id; - not initiate a second stop sequence if a stop dispatch arrives while already stopping — the duplicate MUST be treated as a no-op.
The stop handler MUST NOT interrupt activity belonging to a different
session_id.
Match.updated_session is committed before dispatch (PIPELINE-1 §4.2)
and is not rolled back if the orchestrator emits .error for the
stop handler.
A handler that cannot be stopped MAY remove its own entry from
session.active_handlers by mutating the session it holds during its
dispatch, before it completes — the handler boundary (OVOS-SESSION-2
§2.6). The orchestrator syncs that mutation into the round's working
session at handler completion, so that future ping rounds bypass it.
A global_stop Match is returned in two cases:
- explicit "stop everything" vocabulary (§3.2);
- generic stop with an
active_handlerslist that is empty or holds only the stop plugin's own entry (§4.1 step 1).
A generic stop with no positive pong responders does not trigger
global_stop; it falls back to the recency-selected target (§4.1
step 5).
The global_stop Match MUST carry a fully-cleaned updated_session:
Match(
skill_id = <the stop plugin's pipeline_id>,
intent_name = "global_stop",
updated_session = <session with:
active_handlers → []
converse_handlers → []
response_mode → absent>
)
All three fields are cleared atomically at match time via
Match.updated_session (PIPELINE-1 §4.2), before dispatch.
Match.skill_id here is the pipeline_id of the stop plugin
instance whose match produced this Match — the identity the
global_stop dispatch topic addresses and PIPELINE-1 §7.1 stamps.
There is no second or shared identifier: a plugin has one
pipeline_id whatever the number of session.pipeline entries that
reference it (§3.1, PIPELINE-1 §3), so "the stop plugin's
pipeline_id" is unambiguous everywhere it appears.
global_stop is not a reserved intent_name, so PIPELINE-1 §7.1
stamping suppression does not apply: the orchestrator pushes the
stop plugin's pipeline_id onto active_handlers after committing
this updated_session. The committed post-dispatch state is therefore not
an empty list — active_handlers holds exactly one entry, the stop
plugin's own. §4.1 excludes that entry from stop candidacy and §4.1
step 1 treats such a list as empty, so a following generic stop
resolves to global_stop again rather than to the stop plugin
itself.
The handler dispatched by <pipeline_id>:global_stop MUST emit
ovos.stop. Every component performing user-visible activity MUST
subscribe to ovos.stop and cease activity for the session_id
carried in Message context per OVOS-MSG-1.
ovos.stop is not a dispatch topic — it does not follow the
<skill_id>:<intent_name> shape and does not fire the handler-lifecycle
trio. The namespace ovos.stop.* is reserved by this specification.
For intent_name: "stop", a stop plugin MUST clear the
session.response_mode entry whose skill_id matches the dispatch
target, via Match.updated_session. If no such entry exists, the
field is left unchanged. For intent_name: "global_stop",
response_mode is removed entirely as part of the §5.2 Match
construction.
session.active_handlers (OVOS-PIPELINE-1 §7.1) is the stop
cascade's recency input. It is distinct from session.converse_handlers
(OVOS-CONVERSE-1 §2.1), the converse plugin's eligibility list.
A stop plugin MUST drain active_handlers via Match.updated_session
(committed pre-dispatch per PIPELINE-1 §4.2):
stopMatch — remove the dispatch target entry only;global_stopMatch — emptyactive_handlersentirely and emptyconverse_handlers(OVOS-CONVERSE-1 §2.1) entirely.
The stamping push (PIPELINE-1 §7.1) is suppressed for the reserved
intent_name stop, so the removal in updated_session is the final
state. It is not suppressed for global_stop: the committed state
after a global_stop dispatch is active_handlers == [<the stop plugin's own entry>], not [] (§5.2). The same push
stamps the stop plugin onto converse_handlers, whose stamping rule
is uniform for every dispatch (OVOS-CONVERSE-1 §3.1 owns that
field); the emptying required here is therefore the state the stop
plugin commits, not the state that survives the dispatch.
A stop plugin MUST honour session.blacklisted_skills and
session.blacklisted_intents (PIPELINE-1 §5):
blacklisted_skills: a handler whoseskill_idappears in this list MUST NOT be selected as a stop target. The plugin skips such entries in the §4.1 candidate filter, before any recency comparison;blacklisted_intents: entries are qualified<skill_id>:<intent_name>pairs (PIPELINE-1 §5.4); a bare intent_name never matches. A stop plugin MUST NOT return aMatchwhose<Match.skill_id>:<Match.intent_name>appears inblacklisted_intents. Astoputterance that would resolve toglobal_stop(§4.1 step 1) is subject to the<pipeline_id>:global_stopentry, not to astopentry. This list does not affect the ping broadcast.
Both rules are plugin-side obligations that duplicate, at match time, the filter PIPELINE-1 §5.4 places on the orchestrator. The strengthening is intentional: it keeps a blacklisted target out of the recency selection instead of discarding the whole Match after the fact.
A deployment that includes the stop plugin SHOULD place the
highest-confidence stop stage first in session.pipeline, ahead
of the converse plugin and every intent-matching stage. Lower-confidence
stop stages MAY be interleaved with intent-matching stages.
| Topic | Direction | Purpose |
|---|---|---|
ovos.stop.ping |
stop plugin → all | Stoppability query (broadcast) |
ovos.stop.pong |
skill → stop plugin | Stoppability response |
<target_skill_id>:stop |
orchestrator → skill | Skill-directed stop dispatch |
<stop_plugin_id>:global_stop |
orchestrator → stop handler | Global stop dispatch |
ovos.stop |
stop handler → all | Universal stop broadcast |
Dispatch topics (<…>:stop, <…>:global_stop) fire the
handler-lifecycle trio. No other topic in this table does.
- return
intent_nameof exactly"stop"or"global_stop"(§2, §3.1); - set
Match.skill_idper §3.1; - return
Nonewhen no stop vocabulary matches orlangis unsupported (§3.1); - derive
ovos.stop.pingviareplyfrom the inbound utterance Message (§4.2); - collect pong responses within a deployer-configured timeout (§4.1);
- ignore pongs from skills absent from
session.active_handlers(§4.1); - ignore malformed, duplicate and late pongs, and treat the responder as not stoppable (§4.2);
- treat non-responding handlers as
can_handle: false(§4.2); - select the stop target among positive pong responders only, when any exist (§4.1 step 4);
- break
activated_atties in favour of the entry nearest the head ofactive_handlers(§4.1, PIPELINE-1 §7.1); - exclude its own
pipeline_idand blacklisted skills from the candidate set before the recency comparison, and returnNonewhen no candidate survives (§4.1); - stop exactly one skill per stop utterance (§4.1);
- clear
session.response_modefor the dispatch target viaMatch.updated_session(§6.1); - drain
active_handlersviaMatch.updated_session(§6.2); - on
global_stop, also emptyconverse_handlersviaMatch.updated_session(§6.2); - return
global_stoponly for explicit global-stop vocabulary or anactive_handlerslist that is empty or holds only its own entry (§3.2, §4.1 step 1); - with no positive pong responder and non-empty
active_handlers, target the highest-activated_atsurviving candidate withintent_name: "stop"rather than escalating (§4.1 step 5); - honour
session.blacklisted_skillsandsession.blacklisted_intentsper §6.3; - subscribe to
<own_pipeline_id>:global_stopand emitovos.stop(§5.3); - emit exactly one
ovos.stopbroadcast per global stop event per session, however manysession.pipelineentries reference it (§3.1).
- configure the ping-pong timeout to not exceed 1 s (§4.1);
- provide explicit "stop everything" vocabulary mapping to
global_stopwithout cascade (§3.2).
- place the highest-confidence stop stage first in
session.pipeline(§7); - configure stop vocabulary for every supported language.
- subscribe to both
<own_skill_id>:stopandovos.stop; - on
<own_skill_id>:stop, cease stoppable activity for the inboundsession_id(§4.3); - on
ovos.stop, cease all activity for the inboundsession_id; - treat a duplicate
<own_skill_id>:stoporovos.stopas a no-op (§4.3).
- subscribe to
ovos.stop.pingand respond with areply-derivedovos.stop.pongcarryingcan_handlefor the inboundsession_id(§4.2).
- remove its own entry from
session.active_handlers, via an in-place handler-boundary mutation, when it cannot be stopped (§4.4).
- subscribe to
ovos.stopand cease activity for the inboundsession_id.
- treat OVOS-INTENT-4 registrations naming
stopas malformed — log at WARN and decline to index (§2).
- OVOS-PIPELINE-1 — pipeline contract, dispatch, active_handlers
- OVOS-SESSION-1 — session field registry
- OVOS-SESSION-2 — mutation boundaries
- OVOS-MSG-1 — Message envelope and derivations