Skip to content
59 changes: 50 additions & 9 deletions appendix/patterns.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
`<owner_id>.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 |
| `<owner_id>.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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Expand Down
18 changes: 18 additions & 0 deletions appendix/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,21 @@ Three properties hold across all four:
All four surfaces share the `ovos.<domain>.` 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 |
| `<owner_id>.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 |
6 changes: 5 additions & 1 deletion ovos-pipeline-1.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
126 changes: 118 additions & 8 deletions ovos-session-2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Fix incorrect section cross-reference in Non-goals.

Line 627 points to §5.3 for restart-loss, but restart semantics are defined in §5.2. This should be updated to avoid implementer confusion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@ovos-session-2.md` at line 627, In the "Non-goals" paragraph referencing
restart semantics, update the incorrect section cross-reference: change the
pointer from "§5.3" to "§5.2" so that the mention of "restart-loss" correctly
links to the section where restart semantics are defined; locate the string
"restart-loss" or the sentence mentioning "§5.3" in the Non-goals text and
replace the section number with "§5.2".

explicit and intentional; persistence is deployer policy if
desired.