diff --git a/appendix/patterns.md b/appendix/patterns.md index 49cb3e4..b32d727 100644 --- a/appendix/patterns.md +++ b/appendix/patterns.md @@ -20,10 +20,12 @@ the bus is not just an internal transport — it is the **substrate higher-level systems plug into without modifying the assistant core**. Two mechanics make that work: **single-flip routing** (§3.1.1), which keeps the routing pair -correct end-to-end without per-component effort; and **no -central state or correlation** (§3.1.2), which makes layer-2 -systems composable. HiveMind is the canonical example of what -both together enable (§3.1.3). +correct end-to-end without per-component effort; **forward vs +reply discipline** (§3.1.2), which ensures non-dispatch +emissions are routed correctly through layer-2 transports; and +**no central state or correlation** (§3.1.3), which makes +layer-2 systems composable. HiveMind is the canonical example +of what all three together enable (§3.1.4). #### 3.1.1 The single-flip routing model @@ -83,7 +85,45 @@ Implementers using `.reply` where `.forward` is appropriate produce mis-routed messages that work in local tests but silently break layer-2 routing. -#### 3.1.2 No central correlation, no central state +#### 3.1.2 Choosing `forward` vs `reply` for non-dispatch emissions + +The single-flip model above applies to the dispatch path, but the +same choice arises whenever any component emits a Message derived +from one it received. The rule is: + +- **Use `forward`** when the new Message travels in the **same + direction** as the source — toward the same destination. A handler + emitting `speak`, a session sync, or a lifecycle event during a + dispatch uses `forward`; the user-side client receives it because + the `destination` was already set by the earlier `reply` flip. +- **Use `reply`** when the new Message travels **back toward the + sender** of the source — a responder answering a requester. A + skill responding to `ovos.stop.ping`, a handler answering a + `.converse.request` poll, or a plugin answering an + introspection request all use `reply`; the flip routes the answer + back to whoever asked. + +The practical consequence for layer-2 systems (satellites, gateways, +HiveMind nodes): routing metadata is the only mechanism a transport +has to decide where a Message goes. A component that uses `reply` +where `forward` is correct sends the Message back at itself instead +of toward the user; a component that uses `forward` where `reply` is +correct broadcasts with no destination instead of targeting the +requester. Both mistakes work on a single-node local bus (where +everything is broadcast anyway) and silently fail in layer-2 +deployments. + +The specs enforce this consistently: + +| Emission | Derivation | Reason | +|---|---|---| +| Handler lifecycle trio (PIPELINE-1 §8) | `forward` | Travels toward the client alongside the dispatch | +| `ovos.session.sync` from a handler (SESSION-2 §2.7) | `forward` | Session update travels toward the client | +| `ovos.stop.pong` (STOP-1 §4.2) | `reply` | Response back to the stop plugin that pinged | +| `.converse.response` (CONVERSE-1 §4.2) | `reply` | Response back to the converse plugin that polled | +| Pipeline introspection response (PIPELINE-1 §10.2) | `reply` | Response back to the observer that requested | + +#### 3.1.3 No central correlation, no central state The bus is **fully asynchronous**. OVOS does not centrally correlate request/response chains, and does not centrally @@ -121,7 +161,7 @@ of conversational state. The async-by-default model means those future specs only need to define *what* the state is, not *how* it travels. -#### 3.1.3 Layer-2 substrates +#### 3.1.4 Layer-2 substrates The single-flip routing model and the no-central-state design make layer-2 federation composable without modifying @@ -257,9 +297,10 @@ to and from an external transport, and either operates entirely external (Wyoming-style audio / STT / TTS services talking over TCP to a bridge that proxies the OVOS bus) or remotes the whole bus (HiveMind-style layer-2 substrates). The -single-flip routing of §3.1.1 and the no-central-state stance -of §3.1.2 are what make the bus-boundary adapter feasible -without modifying the assistant core. +single-flip routing of §3.1.1, the forward/reply discipline +of §3.1.2, and the no-central-state stance of §3.1.3 are what +make the bus-boundary adapter feasible without modifying the +assistant core. #### Per-protocol notes diff --git a/appendix/reference.md b/appendix/reference.md index dd9f77b..677ddbf 100644 --- a/appendix/reference.md +++ b/appendix/reference.md @@ -144,3 +144,21 @@ Three properties hold across all four: All four surfaces share the `ovos..` prefix; verb segments vary by domain (some nest, some don't). The uniformity is in the namespace, not in a fixed depth. + +### 6.5 Message derivation cheat-sheet — `forward` vs `reply` + +Use `forward` when a Message travels in the **same direction** +as the source (handler → client). Use `reply` when a Message +travels **back toward the sender** of the source (responder → +requester). Using the wrong derivation produces messages that +work on a single-node local bus but silently mis-route through +layer-2 transports (see appendix/patterns.md §3.1.2). + +| Emission | Derivation | Rule | +|---|---|---| +| Handler `speak`, GUI events, session mutations during dispatch (PIPELINE-1 §7) | `forward` | Same direction as the inbound dispatch | +| Handler-lifecycle trio `.start` / `.complete` / `.error` (PIPELINE-1 §8) | `forward` | Same direction as the inbound dispatch | +| `ovos.session.sync` emitted inside a handler (SESSION-2 §2.7) | `forward` | Session update travels toward the originating client | +| `ovos.stop.pong` (STOP-1 §4.2) | `reply` | Skill answers back to the stop plugin that sent the ping | +| `.converse.response` (CONVERSE-1 §4.2) | `reply` | Owner answers back to the converse plugin that polled | +| Pipeline introspection response (PIPELINE-1 §10.2) | `reply` | Plugin answers back to the observer that requested | diff --git a/ovos-pipeline-1.md b/ovos-pipeline-1.md index 68e2ba4..16c56e2 100644 --- a/ovos-pipeline-1.md +++ b/ovos-pipeline-1.md @@ -1186,7 +1186,11 @@ a `pipeline_id` from any of these sources can query it directly. ### 10.2 Response payload -The plugin **MUST** reply with the currently-loaded intent set: +The plugin **MUST** emit the response derived via `reply` +(OVOS-MSG-1 §5.2), so that routing metadata is preserved and +the response reaches the requester through any layer-2 +transport. The response carries the currently-loaded intent +set: ```json { diff --git a/ovos-session-2.md b/ovos-session-2.md index db9ab4c..047be71 100644 --- a/ovos-session-2.md +++ b/ovos-session-2.md @@ -61,10 +61,11 @@ The key words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, This specification defines: - the **state-ownership model** (§2) — who holds session state, - what is permitted to mutate it, and when components SHOULD + what is permitted to mutate it, when components SHOULD project their cross-utterance state into session-resident - fields vs hold it internally with best-effort resumption - semantics; + fields vs hold it internally, the session mutation discipline + (§2.6), and the explicit out-of-utterance sync mechanism + `ovos.session.sync` (§2.7); - the **client-side merge rules** (§3) — how a client tracks session updates from assistant-emitted Messages, keyed on `session_id` alone; @@ -158,7 +159,6 @@ This is the one exception to §2.2. The local device is a client of the orchestrator that runs in the same process tree as the orchestrator itself; making the orchestrator hold its state is the simplest representation of that physical -co-location. This is the simplest representation of that physical co-location. Behaviour rules for the default-session store are in §5. @@ -257,6 +257,18 @@ happen only at these boundaries: `response` (OVOS-MSG-1 §5) carry the mutated session forward. +**Session mutation discipline.** A handler SHOULD NOT mutate +session fields unless the mutation is necessary for the +handler's function or is explicitly prescribed by another +specification. Incidental mutations add state that clients and +observers must track, increase the risk of session-state races +in multi-component deployments, and make session evolution +harder to reason about. When another spec prescribes a +mutation (e.g. a handler removing itself from +`session.active_handlers` per OVOS-STOP-1 §4.4), that +prescription is the authority; this discipline rule does not +override it. + Bus events emitted *outside* these boundaries — the asynchronous, normal-event-handler kind that any component may emit at any time — **MUST NOT** be expected to mutate session @@ -269,6 +281,81 @@ session is received by the client and merged per §3), but **MUST NOT** be expected to affect the utterance during which it was emitted. +A component that needs to propagate a session update outside +the normal utterance lifecycle SHOULD use `ovos.session.sync` +(§2.7) rather than relying on an unrelated Message to carry +the update incidentally. + +### 2.7 Out-of-utterance session sync — `ovos.session.sync` + +When a component needs to broadcast a session update outside +the utterance lifecycle it SHOULD use the dedicated topic +`ovos.session.sync`. The updated session snapshot is the +**payload** of the Message — carried in `Message.data` as a +`session` object, not in `Message.context.session`. +`Message.context.session` remains the ambient carrier (per +OVOS-MSG-1) and continues to identify the session for routing; +`Message.data.session` is the explicit sync content. + +`Message.data` shape: + +| Key | Type | Required | Meaning | +|-----|------|----------|---------| +| `session` | object | yes | The updated session snapshot. Follows SESSION-1 wire shape; omitted fields leave the receiver's current values unchanged (§5.1 merge rule). | + +`ovos.session.sync` is a plain broadcast — not a PIPELINE-1 +§7 dispatch, not a round-trip. It does not fire the +handler-lifecycle trio and does not activate any owner. + +A handler emitting `ovos.session.sync` from within a +dispatched handler invocation MUST derive the Message via +`forward` (OVOS-MSG-1 §5). `forward` preserves the routing +metadata of the inbound dispatch, ensuring the sync reaches +the originating client through any layer-2 transport +(satellite, gateway, or equivalent) that routes by those +fields. An `ovos.session.sync` emitted without `forward` +inside a handler carries no routing metadata and will not +reach remote clients. + +**When to emit.** A component MAY emit `ovos.session.sync` +at any time for any reason. It SHOULD do so only when: + +- the session update cannot ride on a Message already being + emitted in the normal flow (i.e. no `speak`, `forward`, or + other emission is available to carry it); or +- another specification explicitly prescribes using it for a + specific state change (opportunistic self-removal from + `session.active_handlers`, `session.converse_handlers`, + or equivalent). + +A component SHOULD NOT emit `ovos.session.sync` gratuitously. +The normal derivation chain (§2.6) is the preferred +propagation path; `ovos.session.sync` exists for cases where +no in-utterance emission is available. + +**Consumer obligations.** + +- The **orchestrator** MUST merge `Message.data.session` from + a received `ovos.session.sync` into its working session + snapshot for the affected `session_id`. The merge follows + §5.1's field-replacement rule: present fields in the synced + snapshot replace current values; absent fields leave current + values unchanged. For `session_id == "default"` the working + snapshot is the default-session store (§5); for named + sessions it is the transient per-utterance session in + progress (§2.2). The orchestrator MUST reflect the merged + state in any terminal events it subsequently emits for the + same utterance — specifically the handler-lifecycle + `.complete` event (OVOS-PIPELINE-1 §8) and the universal + end-marker `ovos.utterance.handled` (PIPELINE-1 §9.5) — + so that clients and observers receive a session snapshot that + includes the sync update. +- **Clients** SHOULD update their local session store when + they observe `ovos.session.sync` whose `Message.context.session` + carries a `session_id` matching their own, merging + `Message.data.session` using the same field-replacement + semantics as §3. + --- ## 3. Client-side merge rules @@ -388,7 +475,7 @@ orchestrator operation: default state on the dispatch they receive; - session mutations during the lifecycle (transformer boundaries §2.6, `Match.updated_session` per PIPELINE-1 - §5.2, in-handler mutations) propagate into the store + §4.2, in-handler mutations) propagate into the store through the standard derivation chain. The merge semantics for inbound default-session Messages follow @@ -467,7 +554,12 @@ An orchestrator that claims conformance to this specification the §2.6 boundaries dictate mutation; - emit the universal end-marker `ovos.utterance.handled` carrying the final round session (PIPELINE-1 §9), as the - client-side convergence point of §3.3. + client-side convergence point of §3.3; +- merge `ovos.session.sync` Messages per §2.7 into the + working session snapshot for the affected `session_id` on + receipt, and reflect the merged state in the subsequent + handler-lifecycle `.complete` and `ovos.utterance.handled` + terminal events for the same utterance. An orchestrator **MUST NOT** require any client to declare session-start / session-end / session-id-allocation events @@ -498,6 +590,12 @@ session state in the current utterance (§2.6). It MAY emit such events to communicate with other components; their effect on session, if any, lands on subsequent utterances. +A component **SHOULD NOT** mutate session fields in its handler +unless the mutation is necessary or prescribed by another +specification (§2.6 discipline rule). When a session update +must be propagated outside the normal utterance flow, the +component SHOULD use `ovos.session.sync` (§2.7). + ### 6.4 Client A **client** (any participant on the user side of the bus @@ -531,10 +629,22 @@ default-session store *is* that state for the local device. --- -## 7. Non-goals +## 7. Bus topics + +| Topic | Direction | Purpose | +|-------|-----------|---------| +| `ovos.session.sync` | component → all | Broadcast an explicit session update outside the utterance lifecycle (§2.7). Updated snapshot in `Message.data.session`; `session_id` identified via `Message.context.session` per MSG-1. | + +No other normative bus topic is defined by this specification. +The per-utterance session propagation (§2.6) and end-marker +(§3.3) travel on topics owned by OVOS-PIPELINE-1. + +--- + +## 8. Non-goals See §1 for the full list of non-goals. This section adds one clarification: **default-session persistence across orchestrator -restart** is not defined here. §5.2 makes restart-loss +restart** is not defined here. §5.3 makes restart-loss explicit and intentional; persistence is deployer policy if desired.