diff --git a/CHANGELOG.md b/CHANGELOG.md index ad47b454..4480f35c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -334,6 +334,25 @@ version 2: its `{{ … }}` sequences become substitution points, and its OVOS-PIPELINE-1 §7.3), the global-stop path (`global_stop` → `ovos.stop` broadcast), and the session-scoping obligations over `session.active_handlers`. +- Consistency audit: §4.1 step 5 — a generic stop with no positive pong + responder falls back to the most recently activated `active_handlers` + entry (recency-targeted stop) and MUST NOT escalate to `global_stop`; + a pong can be absent for reasons unrelated to stoppability, and a + missed sub-second reply must never trigger a session-wide wipe of + converse state, response modes, and unrelated skills' activity — + `global_stop` is reserved for explicit global-stop vocabulary (§3.2) + and the empty-`active_handlers` case (§5.1 lists exactly these two + triggers); §4.1 ping-pong timeout stated as a RECOMMENDED default + (0.5 s, SHOULD NOT exceed 1 s) with the hardcoded window value removed + from the step-5 rationale; §5.2 `` defined + precisely — the `pipeline_id` of the stop plugin instance whose + `match` produced the Match, shared across multiple §3.1 pipeline + entries resolving to that instance, which MUST deduplicate to exactly + one `ovos.stop` broadcast per global stop event per session; §6.3 + `blacklisted_intents` rule restated operationally (never return a + Match whose `:` appears in the list); + cross-spec note that the pong boolean's field name (`can_handle`) is + protocol-local. ## OVOS-COMMON-QUERY-1 — Common Query Pipeline Plugin diff --git a/stop-1.md b/stop-1.md index 0051fd47..d870b667 100644 --- a/stop-1.md +++ b/stop-1.md @@ -39,7 +39,7 @@ interaction teardown (a skill-side or orchestrator-side concern). | Reserved intent_name | Meaning | |----------------------|---------| -| `stop` | Cease activity for the inbound `session_id`. Dispatched on `:stop` where the target is the most recently activated (highest `activated_at`) positive pong responder (§4). | +| `stop` | Cease activity for the inbound `session_id`. Dispatched on `:stop` where the target is the most recently activated (highest `activated_at`) positive pong responder, or — when no positive pong arrives in time — the most recently activated `active_handlers` entry (§4). | Skills and other pipelines **MUST NOT** register `stop` under OVOS-INTENT-4. A registration naming this intent_name is malformed per @@ -67,8 +67,9 @@ entries are deployed, the implementation MUST ensure exactly one `Match.skill_id` MUST equal: -- for `intent_name: "stop"` — the most recently activated positive pong - responder selected per §4; +- for `intent_name: "stop"` — the target selected per §4 (the most + recently activated positive pong responder, or the recency fallback + of §4.1 step 5); - for `intent_name: "global_stop"` — the `pipeline_id` whose handler emits `ovos.stop`. @@ -96,7 +97,9 @@ Inside `match`: 1. Read `session.active_handlers`. If empty, return a `global_stop` Match per §5. 2. Emit `ovos.stop.ping` and collect `ovos.stop.pong` responses within - a deployer-configured timeout (default: 0.5 s; SHOULD NOT exceed 1 s). + a deployer-configured timeout (RECOMMENDED default: 0.5 s; SHOULD + NOT exceed 1 s — stop sits on the most latency-sensitive user + path there is: the user is asking for something to cease). 3. Identify positive responders: pongs where `can_handle: true` and `skill_id` appears in `session.active_handlers`. Pongs from skills not in `active_handlers` MUST be ignored; late pongs MAY be ignored. @@ -107,7 +110,22 @@ Inside `match`: `updated_session` removing that `skill_id` from `active_handlers` and clearing any `response_mode` entry it owns. Return `Match(skill_id=, intent_name="stop", updated_session=...)`. -5. If no positive responder exists, return a `global_stop` Match per §5. +5. If no positive responder exists but `active_handlers` is non-empty, + the stop plugin MUST fall back to recency: select the entry with the + highest `activated_at` in `session.active_handlers` (ties broken as + in step 4) and return a + `Match(skill_id=, intent_name="stop", updated_session=...)` + constructed exactly as in step 4. The plugin MUST NOT escalate to + `global_stop` on a missed pong: a pong can be absent for reasons + unrelated to stoppability — a skill busy in its handler thread, or a + satellite whose round-trip exceeds the ping window — and escalating + a missed sub-second reply to a session-wide wipe destroys converse + state, + response modes, and activity belonging to unrelated skills. + Recency-targeted stop degrades gracefully: at worst the target has + nothing to stop and the dispatch is a no-op (§4.3). `global_stop` + remains reserved for explicit global-stop vocabulary (§3.2) and the + empty-`active_handlers` case (step 1). ### 4.2 Ping and pong shape @@ -130,6 +148,11 @@ topic name. | `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 `:stop`. @@ -137,9 +160,10 @@ session-affecting activity in progress for the inbound `session_id` A handler with no current activity MUST 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` and is ineligible as a stop target for that -ping round. If no handler declares stoppability, the cascade escalates -to `global_stop` per §4.1 step 5. +as `can_handle: false` for that ping round. If no handler declares +stoppability, the cascade falls back to the most recently activated +`active_handlers` entry per §4.1 step 5 — it does not escalate to +`global_stop`. ### 4.3 Dispatch and stop handler obligations @@ -173,11 +197,14 @@ with the updated list, so that future ping rounds bypass it. ### 5.1 Trigger conditions -A `global_stop` Match is returned in three cases: +A `global_stop` Match is returned in two cases: - explicit "stop everything" vocabulary (§3.2); -- generic stop with empty `active_handlers` (§4.1 step 1); -- generic stop with no positive pong responders (§4.1 step 5). +- generic stop with empty `active_handlers` (§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 @@ -197,6 +224,15 @@ Match( All three fields are cleared atomically at match time via `Match.updated_session` (PIPELINE-1 §4.2), before dispatch. +`` is defined as 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. +It is "shared" in the §3.1 sense: when a deployment lists several +stop entries in `session.pipeline` that resolve to the same plugin +instance, they share this one `pipeline_id`, and the instance MUST +deduplicate so that exactly one `ovos.stop` broadcast is emitted per +global stop event per session. + PIPELINE-1 §7.1 stamps `` onto `active_handlers` at dispatch time (the name `global_stop` is not reserved, so stamping suppression does not apply). This is intentional: the stop plugin MAY @@ -252,9 +288,10 @@ A stop plugin MUST honour `session.blacklisted_skills` and - `blacklisted_skills`: a handler whose `skill_id` appears in this list MUST NOT be selected as a stop target; - `blacklisted_intents`: applies to the dispatched intent_name (`"stop"` - or `"global_stop"`). A stop plugin MUST not resolve a intent_name - that appears in `blacklisted_intents`. A `stop` utterance that - would resolve to `global_stop` (§4.1 steps 1 or 5) is subject to the + or `"global_stop"`). A stop plugin MUST NOT return a `Match` whose + `:` appears in + `blacklisted_intents`. A `stop` utterance that + would resolve to `global_stop` (§4.1 step 1) is subject to the `global_stop` entry, not the `stop` entry. This list does not affect the ping broadcast. @@ -311,7 +348,8 @@ handler-lifecycle trio. No other topic in this table does. - clear `session.response_mode` for the dispatch target via `Match.updated_session` (§6.1); - drain `active_handlers` via `Match.updated_session` (§6.2); - on `global_stop`, also empty `converse_handlers` via `Match.updated_session` (§6.2); -- return `global_stop` when `active_handlers` is empty or no positive pong responder exists (§4.1 steps 1, 5); +- return `global_stop` only for empty `active_handlers` or explicit global-stop vocabulary (§4.1 step 1, §3.2); +- with no positive pong responder and non-empty `active_handlers`, target the highest-`activated_at` entry with `intent_name: "stop"` rather than escalating (§4.1 step 5); - honour `session.blacklisted_skills` and `session.blacklisted_intents` per §6.3; - subscribe to `:global_stop` and emit `ovos.stop` (§5.3); - emit exactly one `ovos.stop` broadcast per global stop event per session (§3.1).