Stop Pipeline Plugin Specification
August 13, 2026 · View on GitHub
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, MAY are used as in RFC 2119.
1. Scope
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).
2. Reserved intent_name
| 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 — 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.
3. The stop plugin role
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.
3.1 Pipeline identity and dispatch target
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.
3.2 Vocabulary
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.
4. Generic stop — intent_name: "stop"
4.1 Algorithm
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 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.
4.2 Ping and pong shape
ovos.stop.ping — broadcast. Payload MAY be empty. The stop
plugin MUST derive the ping via reply from the inbound utterance
Message (OVOS-MSG-1 §5.2), so that the ping carries the inbound
session_id and the routing metadata of the utterance emitter.
ovos.stop.pong — shared reply topic. A handler MUST emit a
Message of type ovos.stop.pong derived via reply from the ping
(OVOS-MSG-1 §5.2), so that the pong reaches the stop plugin
regardless of where the skill is running (local or remote). source
and destination are layer-2 metadata and do not affect the topic
name.
{ "skill_id": "example.skill", "can_handle": true }
| Field | Type | Required | Meaning |
|---|---|---|---|
skill_id | string | yes | The skill_id of the responding handler. |
can_handle | boolean | yes | Whether the handler has stoppable activity for the inbound session_id. |
The boolean's field name is protocol-specific: this spec and
OVOS-FALLBACK-1 use can_handle, OVOS-CONVERSE-1's poll uses
result, and OVOS-COMMON-QUERY-1 uses can_answer. Each name is
normative only within its own protocol.
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 the inbound 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, or does not respond within the timeout,
is treated as can_handle: false for that ping round. 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.
Malformed and duplicate pongs. A pong is valid only when it
carries a skill_id string and a can_handle boolean. The stop
plugin MUST treat the responding handler as not stoppable for that
ping round when:
can_handleis absent, or is present but not a JSON boolean — a truthy non-boolean value MUST NOT be coerced totrue;skill_idis absent, or does not match theskill_idof the handler that emitted the Message as identified by the MSG-1 derivation metadata.
When a handler emits more than one pong in a ping round, the first
valid pong wins and later pongs from the same skill_id MUST be
ignored. Pongs arriving after the timeout MUST be ignored: the
selection made at step 4 or step 5 is final for that utterance.
4.3 Dispatch and stop handler obligations
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 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 stop handler emits the .error
lifecycle event.
4.4 Self-pruning of active_handlers
A handler that cannot be stopped SHOULD remove itself from
session.active_handlers by emitting any session-carrying Message
with the updated list, so that future ping rounds bypass it.
5. Global stop — intent_name: "global_stop"
5.1 Trigger conditions
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).
5.2 Match construction
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.
5.3 Broadcast
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.
6. Session interaction
6.1 response_mode
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.
6.2 active_handlers
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). converse_handlers
carries no such stamp and stays empty.
6.3 Denylists
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.
7. Pipeline positioning
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.
8. Bus surface
| 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.
9. Conformance
Stop pipeline plugin — MUST:
- 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).
Stop pipeline plugin — SHOULD:
- configure the ping-pong timeout to not exceed 1 s (§4.1);
- provide explicit "stop everything" vocabulary mapping to
global_stopwithout cascade (§3.2).
Deployment — SHOULD:
- place the highest-confidence stop stage first in
session.pipeline(§7); - configure stop vocabulary for every supported language.
Skill — MUST:
- 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).
Skill — SHOULD:
- subscribe to
ovos.stop.pingand respond with areply-derivedovos.stop.pongcarryingcan_handlefor the inboundsession_id(§4.2); - remove itself from
session.active_handlerswhen it cannot be stopped (§4.4).
Non-skill component performing user-visible activity — MUST:
- subscribe to
ovos.stopand cease activity for the inboundsession_id.
Orchestrator — MUST:
- treat OVOS-INTENT-4 registrations naming
stopas malformed — log at WARN and decline to index (§2).
See also
- 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