From e57c092a9cfbc03914572bef8b1e0a796b20705f Mon Sep 17 00:00:00 2001 From: JarbasAi Date: Sat, 4 Jul 2026 13:48:37 +0100 Subject: [PATCH] =?UTF-8?q?spec:=20OVOS-CONVERSE-1=20=E2=80=94=20RECOMMEND?= =?UTF-8?q?ED=20tuning=20defaults,=20identity-gated=20response=20mode,=20n?= =?UTF-8?q?o=20in-place=20session=20mutation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 --- CHANGELOG.md | 17 +++++++ converse.md | 129 ++++++++++++++++++++++++++++++++++----------------- 2 files changed, 103 insertions(+), 43 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad47b45..63c290a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/converse.md b/converse.md index 95ba3e4..e5c9dad 100644 --- a/converse.md +++ b/converse.md @@ -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* @@ -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. @@ -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 @@ -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 `:converse` / `:response`. The plugin **SHOULD** publish the intent_names it produces matches @@ -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. @@ -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 @@ -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 (`.`). 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 @@ -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 @@ -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 @@ -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. @@ -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). @@ -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 `:` prefix and + suffix (PIPELINE-1 §7.1). 4. **Handler.** The handler subscribed to `:response` runs and processes the awaited utterance. When it returns, @@ -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 @@ -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 @@ -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. @@ -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;