Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,23 @@ version 2: its `{{ … }}` sequences become substitution points, and its
Activation lifecycle (§6), stop integration (§7), and the bus surface
(§8). Dispatch on the reserved intent_names `converse` and `response`
(OVOS-PIPELINE-1 §7.3) follows ordinary §7 routing.
- Consistency audit: tuning values demoted from mandates to RECOMMENDED
defaults with rationale — §2.1 list cap (default 10), §3.2 TTL (default
600 s, applied when unconfigured), §4.2 per-owner poll timeout (default
0.5 s); §4.1/§5.2 response-mode delivery gated on the §2.2 identity
invariant against the post-prune `converse_handlers` (a pruned or
unrecognised holder is discarded, never delivered to); §4.4 `"done"`
removal on a null return no longer mutates the inbound session in-place
— the PIPELINE-1 §4.2 channel discipline holds without exception, and
removal is deferred to the next claiming match or the §3.2 TTL; §4.1
parallel polling upgraded to SHOULD (serial critical path); `Match`
slot map named `slots` per OVOS-PIPELINE-1 §4.1; §5.2 dispatch payload
reduced to the standard PIPELINE-1 §7.1 shape (`skill_id` /
`intent_name` live in the topic, not the payload); §7 global-stop
interaction corrected (the stop plugin re-stamps itself via uniform
§3.1 activation); §4.2 notes the poll boolean's field name is
protocol-specific (`result` here); OVOS-SESSION-1 cited by its
canonical title.
## OVOS-PIPELINE-1 — Utterance Lifecycle and Pipeline

### 2
Expand Down
129 changes: 86 additions & 43 deletions converse.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ It builds on six companion specifications:
`Message.context`, the `session` carrier, and the
`forward` / `reply` / `response` derivations every Message
defined here travels in;
- the *Session Carrier Wire Shape Specification* (OVOS-SESSION-1) —
- the *Session Specification* (OVOS-SESSION-1) —
the field-registry mechanism under which this spec claims its
session fields (§2), and the omission-not-`null` rule;
- the *Session Lifecycle and State Ownership Specification*
Expand Down Expand Up @@ -118,8 +118,16 @@ re-activation of an already-listed owner removes the prior entry
and re-inserts at the head (§3.1).

Deployments **SHOULD** bound the list length. The
**default maximum is 64 entries**, which a deployer **MAY** raise,
lower, or set to "unbounded". When the cap would be exceeded by an
**RECOMMENDED default maximum is 10 entries**, which a deployer
**MAY** raise, lower, or set to "unbounded". The cap is a tuning
value, not an interoperability invariant — any bound (or none)
is conformant. The default is deliberately small: every
listed owner is a candidate for the per-utterance poll (§4.2), and
the poll sits on the serial critical path of every utterance — a
large eligibility list converts directly into user-perceived
latency, while genuine multi-turn engagement rarely involves more
than a handful of recently active owners. When the cap would be
exceeded by an
insertion, the orchestrator **MUST** drop the tail entry (the
least-recent surviving owner) before inserting the new head, and
**SHOULD** log the eviction.
Expand Down Expand Up @@ -227,10 +235,13 @@ exceeds `T` at two boundaries:

The orchestrator MAY run the prune at additional boundaries; doing
so MUST NOT produce observably different behaviour from running it
only at the two boundaries above. When no TTL is configured, no
time-based pruning occurs and the list ages only by the §2.1 size
cap. A deployment with no TTL is conformant but not recommended —
it is effectively hardcoding TTL to "forever".
only at the two boundaries above. The **RECOMMENDED default TTL is
600 seconds**; an implementation SHOULD apply it when the deployer
has not configured a value. Absent a TTL the list ages only by the
§2.1 size cap — which never triggers while the owner count stays
below the cap, so a stale owner would be polled on every utterance
forever. A deployer MAY explicitly configure "no TTL", accepting
that behaviour.

Because `activated_at` is session-resident, TTL pruning is
**resumption-safe**: a session re-sent after an orchestrator restart
Expand Down Expand Up @@ -299,10 +310,12 @@ PIPELINE-1 §4.2 mechanism), and its returned `Match` dispatches
per PIPELINE-1 §7 normally. There is no dispatch suppression and
no out-of-band signalling between the plugin and the orchestrator.

The plugin is a **pure matcher** in OVOS-PIPELINE-1 §7.0 terms —
its `match` produces a `Match` whose `skill_id` is some *other*
component's identity (the claiming handler, or the
response-mode holder), never its own `pipeline_id`. The reserved
The plugin is a **pure matcher** — a matcher whose only output is
its return value: its `match` produces a `Match` whose `skill_id`
is some *other* component's identity (the claiming handler, or the
response-mode holder), never its own `pipeline_id`
(OVOS-PIPELINE-1 §7.0), and the plugin bundles no handler of its
own. The reserved
intent_names are dispatched to those handler-owners, which
subscribe to `<own_skill_id>:converse` / `<own_skill_id>:response`.
The plugin **SHOULD** publish the intent_names it produces matches
Expand Down Expand Up @@ -330,7 +343,13 @@ When invoked, a converse plugin MUST proceed in this order:

1. **Response-mode pre-emption.** If `session.response_mode` is
present and its `expires_at` is in the future, the plugin
MUST return a `Match` per §5.2 for the holder, on
MUST first verify the §2.2 identity invariant: after the §3.2
TTL prune (when configured), the holder's `skill_id` MUST
appear in `session.converse_handlers` — a pruned or
unrecognised holder MUST NOT receive delivery; the plugin
discards the entry via `Match.updated_session` (log and
discard) and proceeds to step 2. For a valid holder, the
plugin MUST return a `Match` per §5.2 for the holder, on
`intent_name == "response"`, and SKIP the converse-handler poll.
The orchestrator dispatches per PIPELINE-1 §7; the holder's
response handler runs.
Expand All @@ -347,10 +366,12 @@ When invoked, a converse plugin MUST proceed in this order:
by the PIPELINE-1 §5.3–§5.4 backstop, not by this plugin.

3. **Poll iteration.** The plugin polls the eligible set via
§4.2. The plugin MAY issue poll requests in parallel; when
it does, it MUST select the claimer with the **highest
`activated_at`** among those that returned `result: true`,
not by response arrival order. The plugin SHOULD skip any
§4.2. The plugin SHOULD issue poll requests in parallel —
the poll runs on the serial critical path of every utterance,
and sequential per-owner waits multiply the per-owner timeout
by the list length. When polling in parallel, it MUST select
the claimer with the **highest `activated_at`** among those
that returned `result: true`, not by response arrival order. The plugin SHOULD skip any
owner whose `skill_id` appears in
`session.blacklisted_skills` — doing so avoids an
unnecessary round-trip before the PIPELINE-1 §5.3
Expand Down Expand Up @@ -394,6 +415,11 @@ not affect the topic name. The response carries `data`:
| `result` | boolean | yes | `true` ⇒ the owner claims the utterance; `false` ⇒ the owner declines. |
| `error_code` | string | no | Optional structured reason (see §4.4) when `result` is `false`. |

The boolean's field name is protocol-specific: this spec's poll
uses `result`, while the analogous polls of OVOS-FALLBACK-1 /
OVOS-STOP-1 use `can_handle` and OVOS-COMMON-QUERY-1 uses
`can_answer`. Each name is normative only within its own protocol.

Both topics use the **dotted addressed** form (`<skill_id>.<verb>`).
The poll is **not** a PIPELINE-1 §7 dispatch, so it avoids the `:`
separator that OVOS-MSG-1 §2.1.1 reserves for the dispatch shape
Expand All @@ -418,8 +444,10 @@ SHOULD include `error_code: "done"` in a declining response
self-deactivation requests.

A converse plugin MUST wait for the poll reply with a
**deployer-configured per-owner timeout**. The default timeout
is `0.5` seconds, which a deployer MAY raise or lower. An owner
**deployer-configured per-owner timeout** — an unbounded wait
would stall the utterance's serial critical path indefinitely.
The **RECOMMENDED default timeout is `0.5` seconds**, which a
deployer MAY raise or lower. An owner
that does not respond within the timeout is treated as
`result: false`. A converse plugin MUST NOT use the reply from
owner *i* for utterance *u* to satisfy a different utterance
Expand All @@ -436,7 +464,7 @@ returns a `Match` (PIPELINE-1 §4.1) shaped as follows:
- `intent_name` = the reserved value `converse`;
- `lang` = the active language;
- `utterance` = the chosen candidate;
- `captures` = `{}` (a converse claim is not a slot-bearing
- `slots` = `{}` (a converse claim is not a slot-bearing
match);
- `updated_session` = OPTIONAL. The converse plugin MAY use
this PIPELINE-1 §4.2 channel to pre-promote the claimer to
Expand Down Expand Up @@ -471,19 +499,24 @@ the value SHOULD be drawn from:
| `killed` | The poll was terminated by an interrupt signal (see §5.4) before the owner could decide. |
| `done` | The owner explicitly signals it is finished with this conversation thread and requests removal from `session.converse_handlers`. |

When a plugin receives `error_code: "done"` it **MUST** remove
the declining owner from `session.converse_handlers`:
When a plugin receives `error_code: "done"` it removes the
declining owner from `session.converse_handlers`:

- **When returning a `Match`** (another owner claimed on the same
iteration): remove the `"done"` owner via `Match.updated_session`
(the PIPELINE-1 §4.2 channel).
- **When returning `null`** (no owner claimed): there is no Match
to carry `updated_session`. The plugin MUST mutate the inbound
session object in-place, removing the `"done"` owner from
`converse_handlers`. This in-place mutation on a null-return is
the sole permitted exception to PIPELINE-1 §4.2's non-mutation
rule; it is scoped strictly to `"done"` removal and has no effect
on the current utterance's dispatch path.
to carry `updated_session`, and in-place mutation of the inbound
session object is not visible past the plugin boundary
(PIPELINE-1 §4.2). A pipeline plugin does not write session state
outside `Match.updated_session` — that channel discipline is what
makes declined-plugin mutations safely discardable (PIPELINE-1
§4.2). The plugin therefore MAY simply let the entry age out via
the §3.2 TTL, and SHOULD remember the pending removal and apply
it via `Match.updated_session` on its next claiming match for the
session. Until then the `"done"` owner may be polled again; a
well-behaved owner keeps answering `result: false` /
`error_code: "done"`, so the redundant polls are cheap.

Removal is the only effect of `"done"` — it does not affect any
other iteration step.
Expand Down Expand Up @@ -560,7 +593,7 @@ converse plugin's `match`, the plugin consults
- `intent_name` = the reserved value `response`;
- `lang` = the active language;
- `utterance` = the first candidate;
- `captures` = `{}`;
- `slots` = `{}`;
- `updated_session` = the inbound session with
`session.response_mode` removed (single-shot delivery).

Expand All @@ -576,16 +609,17 @@ converse plugin's `match`, the plugin consults
any other intent dispatch, with the full handler-trio and
§3.1 activation.

The dispatch `data`:
The dispatch `data` is the standard PIPELINE-1 §7.1 payload:

| Key | Type | Required | Meaning |
|-----|------|----------|---------|
| `skill_id` | string | yes | The holder; equals the topic prefix. |
| `intent_name` | string | yes | The reserved value `response`. |
| `lang` | string | yes | The active language. |
| `utterance` | string | yes | The first candidate. |
| `utterances` | array of strings | yes | The candidate list per PIPELINE-1 §4.1, post utterance-transformer chain. |
| `captures` | object | yes | `{}` — response-mode delivery is not slot-bearing. |
| `slots` | object | yes | `{}` — response-mode delivery is not slot-bearing. |

`skill_id` and `intent_name` are not repeated in the payload —
they are the topic's `<skill_id>:<intent_name>` prefix and
suffix (PIPELINE-1 §7.1).

4. **Handler.** The handler subscribed to `<skill_id>:response`
runs and processes the awaited utterance. When it returns,
Expand Down Expand Up @@ -644,8 +678,11 @@ OVOS-STOP-1 distinguishes two stop paths with different effects on
in `session.converse_handlers` and may still claim converse turns.
- A **global stop** (`intent_name: "global_stop"`, STOP-1 §5) empties
both `session.active_handlers` and `session.converse_handlers` via
`Match.updated_session`. The converse plugin will see an empty list on
the next utterance and produce no converse Match.
`Match.updated_session`. The subsequent `global_stop` dispatch then
stamps the stop plugin itself back onto the lists (§3.1 here is
uniform, and `global_stop` is not a reserved intent_name), so the
converse plugin sees at most the stop plugin on the next
utterance's poll.

When a stop signal arrives in a deployment, the converse plugin SHOULD
remove `session.response_mode` for affected sessions on its next match
Expand Down Expand Up @@ -782,11 +819,15 @@ A **converse plugin** that claims the role defined in §4 MUST:
`skill_id` is another component's identity; the plugin bundles
no handler of its own;
- follow the §4.1 iteration order in `match`:
- check `session.response_mode` first; if present and
non-expired, return a `Match` on `intent_name == "response"`
clearing the field via `Match.updated_session` (§5.2); if
expired, clear via `Match.updated_session` and continue;
- conduct the §4.2 poll on the eligible set (MAY run in
- check `session.response_mode` first; verify the §2.2 identity
invariant against the post-prune `session.converse_handlers`
(a pruned or unrecognised holder MUST NOT receive delivery —
discard the entry via `Match.updated_session` and continue);
if present, non-expired, and held by a listed owner, return a
`Match` on `intent_name == "response"` clearing the field via
`Match.updated_session` (§5.2); if expired, clear via
`Match.updated_session` and continue;
- conduct the §4.2 poll on the eligible set (SHOULD run in
parallel; MUST select the claimer with the highest
`activated_at`; SHOULD skip `blacklisted_skills`); return a
`Match` on `intent_name == "converse"` for the claimer, or
Expand Down Expand Up @@ -829,8 +870,10 @@ field is the wire surface.

A handler that wants to be removed from
`session.converse_handlers` SHOULD decline its next converse poll
with `error_code: "done"` (§4.4) — the plugin will remove it
immediately via `Match.updated_session`. Without `"done"`, a
with `error_code: "done"` (§4.4) — the plugin removes it via
`Match.updated_session` (immediately when another owner claims the
same utterance, otherwise on its next claiming match or via the
§3.2 TTL). Without `"done"`, a
declining owner's position decays naturally once newer owners
are activated or the §3.2 deployer TTL expires.

Expand Down Expand Up @@ -878,7 +921,7 @@ A **transformer** (OVOS-TRANSFORM-1) that mutates
This specification deliberately does not:

- prescribe **how** a converse plugin implements its poll
round-trips beyond §4.1 (parallelism permitted; selection
round-trips beyond §4.1 (parallelism recommended; selection
by highest `activated_at`);
- prescribe **what** a handler should say or do when it claims,
leaves response mode, or its response window times out;
Expand Down