diff --git a/CHANGELOG.md b/CHANGELOG.md index 6377ce0..5eb6867 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -266,6 +266,74 @@ version 2: its `{{ … }}` sequences become substitution points, and its ## OVOS-SESSION-2 — Session Lifecycle and State Ownership +### 2 + +Version 2 is **not compatible with version 1**. Version 1 left +the write model under-determined: the two write paths onto the +default session were not distinguished, no ordering was defined +across the four in-utterance write paths, and `ovos.session.sync` +was simultaneously required to reach terminal events (§2.7) and +forbidden to mutate session mid-utterance (§2.6). An +implementation built against version 1 can satisfy version 1 and +violate version 2. + +- §2.6 — `ovos.session.sync` carved out of the + incidental-bus-event rule that previously contradicted §2.7. + The visibility split is now stated: a merged sync reaches + terminal-event snapshots and any matcher or hook not yet run, + and never revises a snapshot a consumer already holds. +- §2.6 — the handler-boundary in-place path is bounded to fields + the handler owns and cites OVOS-MSG-1 §4.1's owned-field + allowance as what makes it conformant. The + `Match.updated_session` commit mechanic is now cited to + PIPELINE-1 §4.2 rather than restated. +- §2.7 — field replacement stated as the default merge rule, + with claiming specifications permitted to define a finer + intra-field merge; `session.intent_context` named as the one + such field at this version (OVOS-CONTEXT-1 §5.3). +- §2.7 — a sync on a named session with no utterance in flight + is an orchestrator no-op; the owning client still consumes it. + Nested lifecycles bind the innermost lifecycle in flight + (PIPELINE-1 §6.5). +- §2.8 (new) — normative write-ordering timeline across the + inbound merge, transformer hooks, match-phase writes, + dispatch-time orchestrator writes and handler/sync writes, + plus last-writer-wins resolved **per field**, its two + exceptions, and the statement that unorderable ties have no + defined winner. +- §3.2 — "latest received" retired as a client policy: the bus + guarantees no order. Adoption at `ovos.utterance.handled` is + RECOMMENDED; incremental adoption stays a MAY, with an + explicit staleness warning and a SHOULD to converge at §3.3. +- §5.1 — the omitted-field-leaves-stored-value rule is owned as + a deliberate deviation from SESSION-1 §2.1, confined to writes + into the default-session store; the false attribution of the + rule to SESSION-1 §2.1 is dropped. +- §5.1 — default-store write acceptance: the orchestrator merges + a session-less inbound Message into the store only from a + local source, with OVOS-BRIDGE-1 §3.4 named as the enforcement + point for relayed traffic. SESSION-1 §2.4 field tolerance + stated as applying at store-write. +- §5.1 — the match-pathway wholesale replacement now says a + field absent from `Match.updated_session` **reverts to the + deployment default at consumption**, not that it is deleted, + and carves out the PIPELINE-1 §7.1 `active_handlers` push and + the OVOS-CONTEXT-1 §5.3 entry-level `intent_context` merge. +- §5.4 folded into §7 as a non-normative deployment note; the + end-marker citation is pinned to PIPELINE-1 §9.5 throughout. +- RFC 2119 usage — "MUST NOT be expected to" reworded as + behaviour required of a named actor (§2.2, §2.4, §2.6, §6.4). +- Descriptive passages in §2.3, §2.4, §2.5, §4.3 and §5.2 marked + informative. +- §2.4 / §6.3 — clarified that only a specification claims a + field (SESSION-1 §2.2); a component projecting into an + unclaimed field rides SESSION-1 §2.4 tolerance and gets no + normative reading from anyone else. +- Citations — SESSION-1 registry claims corrected to §2.2 (§2.4, + §7); restart-loss citation corrected to §5.2; companion-spec + count corrected to six; CONTEXT-1/CONVERSE-1 field attribution + in the intro untangled. + ### 1 - The state-ownership model (stateless bus, stateless orchestrator for diff --git a/session-2.md b/session-2.md index 341cffd..0047807 100644 --- a/session-2.md +++ b/session-2.md @@ -1,6 +1,6 @@ # Session Lifecycle and State Ownership Specification -**Spec ID:** OVOS-SESSION-2 · **Version:** 1 · **Status:** Draft +**Spec ID:** OVOS-SESSION-2 · **Version:** 2 · **Status:** Draft This document defines **who owns session state**, **when it is mutated**, **how it propagates between client and assistant**, and @@ -26,7 +26,7 @@ time, lets an orchestrator restart without losing client-side continuity, and lets multiple orchestrators in a deployment serve the same session without coordination. -It builds on five companion specifications: +It builds on six companion specifications: - the *Bus Message Specification* (OVOS-MSG-1) — the envelope, routing keys, `forward` / `reply` / `response` derivations, @@ -48,8 +48,9 @@ It builds on five companion specifications: *Active Handlers and Interactive Response Specification* (OVOS-CONVERSE-1) — both elect the §2.4 SHOULD-project pathway for their cross-utterance state (intent-context - entries, active-handler list, response-mode wait window - respectively), making it resumption-safe by construction. + entries for CONTEXT-1; the converse-handler list and + response-mode wait window for CONVERSE-1), making it + resumption-safe by construction. The key words **MUST**, **MUST NOT**, **SHOULD**, **SHOULD NOT**, **MAY**, and **RECOMMENDED** are used as in RFC 2119. @@ -64,8 +65,9 @@ This specification defines: what is permitted to mutate it, when components SHOULD project their cross-utterance state into session-resident fields vs hold it internally, the session mutation discipline - (§2.6), and the explicit out-of-utterance sync mechanism - `ovos.session.sync` (§2.7); + (§2.6), the explicit out-of-utterance sync mechanism + `ovos.session.sync` (§2.7), and the write-ordering and + conflict rules that apply across all of them (§2.8); - the **client-side merge rules** (§3) — how a client tracks session updates from assistant-emitted Messages, keyed on `session_id` alone; @@ -138,8 +140,9 @@ The orchestrator **MAY** maintain a transient per-utterance cache (the inbound session it is currently processing, the Match it has produced, etc.); such caches are utterance-scoped and discarded at end-of-utterance. They are **not** -cross-utterance state and **MUST NOT** be relied upon by any -component as durable. +cross-utterance state: a component **MUST** continue to function +when such a cache is absent, and **MUST NOT** read one as +durable state. A consequence: any orchestrator in a deployment can serve any inbound Message on any named session. No coordination is @@ -155,8 +158,8 @@ means "interact with the device-local session." The orchestrator session, keyed under `"default"` — the authoritative default-session store. -This is the one exception to §2.2. The local device is a -client of the orchestrator that runs in the same process tree +*(Rationale, informative.)* 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. @@ -168,17 +171,28 @@ Behaviour rules for the default-session store are in §5. A component (a pipeline plugin, a transformer, any other participant) that holds `session_id`-keyed state **across** utterances **SHOULD** project that state into a session-resident -field it owns (claimed under SESSION-1 §2.1) when projection is -practical. Projection flows through the pipeline plugin's -`Match.updated_session` channel (PIPELINE-1 §4.2) or through -in-place mutation at transformer / handler boundaries (§2.6). -Projected state is **resumption-safe by construction** — it -travels with the session, survives orchestrator restart, and -moves transparently across multi-orchestrator deployments. +field it owns when projection is practical. Projection flows +through the pipeline plugin's `Match.updated_session` channel +(PIPELINE-1 §4.2) or through in-place mutation at transformer / +handler boundaries (§2.6). Projected state is **resumption-safe +by construction** — it travels with the session, survives +orchestrator restart, and moves transparently across +multi-orchestrator deployments. + +Ownership of a session field is established only by the claiming +mechanic of SESSION-1 §2.2, which is available to a +*specification*, not to a component. A component whose projected +field is claimed by no specification is still conformant: that +field is non-normative per SESSION-1 §2.3 and rides the +unknown-field tolerance of SESSION-1 §2.4 — every consumer +carries it and none is bound to interpret it. What such a +component does **not** get is a normative reading by anyone +else. A component that wants its field honoured by other +participants needs a specification to claim it. A component **MAY** instead hold authoritative cross-utterance -state internally when projection is impractical. Realistic -examples: +state internally when projection is impractical. *(The following +examples are informative.)* - a **language-model plugin** holding a multi-turn conversation transcript that is too large to ride on every session-carrying @@ -207,10 +221,12 @@ A component that takes this path: may have persisted the state and handle the resume cleanly. The spec does not bind the outcome of any plugin-internal resumption attempt; -- MUST NOT expect other components or clients to know its - state exists or to compensate for its absence. +- **MUST** continue to function correctly when no other + component or client knows the state exists or compensates for + its absence — it MUST NOT make its own correctness depend on + such knowledge or compensation. -The CONVERSE-1 converse plugin (§5 there) is one example of a +*(Informative.)* The CONVERSE-1 converse plugin (§5 there) is one example of a plugin that chooses to project all its cross-utterance state — the response-mode wait window is small, simple, and naturally session-coupled, so the SHOULD-project path is the obvious fit. @@ -238,7 +254,10 @@ the user's cloud, anything else. Trust and authorization are layer-2 concerns (§1); this spec places no constraint on what `session_id` or `session` value a -client sends. +client sends. *(Informative.)* This rule is what obliges a +boundary component that governs a client by injecting policy +fields into its session to keep doing so on every Message; +OVOS-BRIDGE-1 §4.1 states that obligation as the gate invariant. ### 2.6 When session mutates in place @@ -248,14 +267,18 @@ happen only at these boundaries: - **transformer boundaries** — any of OVOS-TRANSFORM-1's six hooks (audio, utterance, metadata, intent, dialog, TTS); - **pipeline boundaries** — a pipeline plugin's `match` may - return a `Match.updated_session` per PIPELINE-1 §4.2; the - orchestrator MUST apply it as `session = match.updated_session - or session` immediately on a non-null match; + return a `Match.updated_session`. The commit mechanic is + PIPELINE-1 §4.2's and is not restated here; - **handler boundaries** — a dispatched handler (skill or plugin-bundled handler per PIPELINE-1 §7.0) MAY mutate - session in-place; its emissions via `forward` / `reply` / - `response` (OVOS-MSG-1 §5) carry the mutated session - forward. **A handler that emits no Message has no + session in-place, within the limit that it writes only fields + it owns. OVOS-MSG-1 §4.1 otherwise forbids a producer to + modify a session present on the source Message; the + owned-field allowance in that section is what makes this + boundary conformant, and a handler write to a field it does + not own remains forbidden. The handler's emissions via + `forward` / `reply` / `response` (OVOS-MSG-1 §5) carry the + mutated session forward. **A handler that emits no Message has no bus-visible way to propagate its session mutations.** The handler-lifecycle trio `.complete` (PIPELINE-1 §8) is orchestrator-emitted from the dispatch context the @@ -278,22 +301,43 @@ mutation (e.g. a handler removing itself from prescription is the authority; this discipline rule does not override it. +**Incidental bus events do not mutate the working session.** 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 -state in the current utterance. The bus is asynchronous and -not part of the utterance lifecycle (§2.1). - -A bus-emitted Message that carries a mutated session **MAY** -affect subsequent utterances on that session (its updated -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. +emit at any time — carry a session but do not update anyone's +working state. The orchestrator **MUST NOT** merge the session +of such a Message into the working session snapshot for the +utterance in progress, and a component **MUST NOT** make its own +behaviour depend on such a merge having happened. The bus is +asynchronous and not part of the utterance lifecycle (§2.1). + +Such a Message **MAY** still affect subsequent utterances on the +session: the client receives it and merges per §3, and the +merged state arrives on the next inbound Message. A component +that relies on that effect **MUST** tolerate it landing no +earlier than the next utterance. + +`ovos.session.sync` (§2.7) is the one exception to this +paragraph, and is the mechanism a component **SHOULD** use when +it needs to propagate a session update outside the normal +utterance lifecycle, rather than relying on an unrelated Message +to carry the update incidentally. A sync **is** merged into the +working session snapshot on receipt, per the orchestrator +obligation in §2.7. + +**What a merged sync is visible to.** The merge changes the +working snapshot from the moment it is applied, and no earlier. +It therefore reaches the terminal-event snapshots the +orchestrator emits after that moment — the handler-lifecycle +`.complete` event (PIPELINE-1 §8) and the universal end-marker +`ovos.utterance.handled` (PIPELINE-1 §9.5) — and it reaches any +pipeline `match` call or transformer hook that has not yet run. +It does **not** reach a matcher, hook, or dispatch that has +already read the snapshot: those consumers hold their own copy +and this specification defines no mechanism to revise it. A +component that emits a sync mid-utterance therefore gets +terminal-event visibility, not retroactive re-evaluation of the +utterance in flight. ### 2.7 Out-of-utterance session sync — `ovos.session.sync` @@ -310,7 +354,7 @@ OVOS-MSG-1) and continues to identify the session for routing; | 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). | +| `session` | object | yes | The updated session snapshot. Follows SESSION-1 wire shape. The merge a receiver performs on it is defined per receiver below under *Consumer obligations* — it is not SESSION-1 §2.1 default-filling, and §5.1's rules bind the default-session store only. | `ovos.session.sync` is a plain broadcast — not a PIPELINE-1 §7 dispatch, not a round-trip. It does not fire the @@ -346,25 +390,125 @@ no in-utterance emission is available. - 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. + snapshot for the affected `session_id`, **when it holds + one**. The merge is **field-replacement**: a field present in + the synced snapshot replaces the current value; a field + absent from it leaves the current value unchanged. An absent + field is *not* read as a request to restore the deployment + default — SESSION-1 §2.1's default-filling governs + consumption of a session, not this merge. For + `session_id == "default"` the working snapshot is the + default-session store (§5), which always exists. For a named + session 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. §2.6 fixes the limits of that visibility. - **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. +**A finer merge may be prescribed per field.** Field replacement +is the **default** rule, and it is what applies to any field +whose claiming specification says nothing further. A claiming +specification **MAY** define a finer intra-field merge for the +field it claims, and where it does, that rule governs both this +sync merge and the §5.1 store merge. One such field exists at +this version: `session.intent_context`, which OVOS-CONTEXT-1 +§5.3 merges **entry-by-entry** — a key present with an entry +object sets or replaces that key, a key present with a `null` +entry removes it, and keys absent from the payload are left +unchanged. A receiver implementing this specification MUST +apply CONTEXT-1 §5.3's rule to `intent_context` rather than +replacing the whole map. + +**Named session with no utterance in flight.** A sync may arrive +on a named `session_id` at a moment when the orchestrator has no +working snapshot for it — the ordinary state between rounds, +since the orchestrator is stateless for named sessions (§2.2). +The orchestrator **MUST** treat the sync as a **no-op**: it +**MUST NOT** create a snapshot, a store entry, or any other +cross-utterance state for that `session_id` in order to hold the +update, because doing so would break §2.2. The sync is not lost: +it is a broadcast, and the client that owns the session consumes +it directly per the client obligation above and carries the +merged state back on its next inbound Message. This is the +intended path for a named session, not a degraded one. + +**Nested lifecycles.** When lifecycles are nested (PIPELINE-1 +§6.5), a sync on the shared `session_id` binds to the +**innermost lifecycle in flight** — the one whose working +snapshot is current at the moment of receipt. The outer +lifecycle picks the update up only where it reads the session +again after the inner lifecycle returns; a snapshot the outer +handler already holds is not revised, per §2.6. + +### 2.8 Write ordering and conflicting writes + +§2.6 and §2.7 name four ways a session field gets written during +a round. This section fixes the order they apply in and what +happens when two of them write the same field. + +**The timeline.** For one utterance on one `session_id`, the +orchestrator **MUST** apply writes in this order: + +1. **Inbound merge.** The session arriving on the entry Message + is merged. For `session_id == "default"` this is the + default-session store merge of §5.1; for a named session the + inbound session simply *is* the working snapshot (§2.2). +2. **Transformer hooks**, in the order the chain runs + (OVOS-TRANSFORM-1). Each hook writes in place and the next + hook sees the result. +3. **Match-phase writes.** Each pipeline plugin's + `Match.updated_session` is committed as PIPELINE-1 §4.2 + defines, at the point that plugin's match is accepted. +4. **Dispatch-time orchestrator writes.** Writes the + orchestrator makes while building the dispatch — notably the + `session.active_handlers` push of PIPELINE-1 §7.1, which + that section already fixes as applying *after* + `Match.updated_session` is committed. +5. **Handler writes**, in-place per §2.6, and any + `ovos.session.sync` merges (§2.7), in the order the + orchestrator receives them. + +Steps 2 through 5 are the §2.6 boundaries; step 1 is the only +one that is not. Writes from a *different* utterance are not on +this timeline at all — they reach this session only through the +client, on a later inbound Message (§2.6). + +**Last writer wins, per field.** When two writes on this +timeline touch the same field, the later one in timeline order +is the value that survives. Resolution is **per field**, never +per snapshot: a write that sets field `A` leaves field `B` +holding whatever the previous writer left there. Two components +writing disjoint fields therefore never conflict, whatever order +they run in. + +Two exceptions bound the rule: + +- a field whose claiming specification defines a finer + intra-field merge resolves by that rule instead — at this + version, `session.intent_context` per OVOS-CONTEXT-1 §5.3 + (§2.7); +- a wholesale-replace pathway replaces the snapshot rather than + a field, so the per-field statement does not apply to it. The + only such pathway is `Match.updated_session`; §5.1 defines + its effect and its carve-outs. + +**Ties are not resolvable.** Two writes the orchestrator cannot +order — most realistically two `ovos.session.sync` Messages from +different components arriving concurrently on the same field — +have no defined winner. The bus is asynchronous and carries no +ordering guarantee (§2.1), so this specification defines none +either. Components that write a shared field concurrently +**SHOULD** coordinate among themselves; CONTEXT-1 §5.3 gives the +worked example of that advice for shared-scope context keys. + --- ## 3. Client-side merge rules @@ -388,16 +532,35 @@ carries a valid session at its emission point. A client that adopts any one such Message's session has a snapshot consistent with the assistant's view at that point in the round. -Adopting the **latest received** session is the simplest client -policy. More elaborate policies (field-by-field merge, selecting -by emitter identity) are also conformant; the spec does not -prescribe. +**Where to adopt.** "Latest received" is not a well-defined +selector on this bus. The bus is asynchronous and guarantees no +delivery order (§2.1), so two Messages emitted in a known order +may be observed in the opposite one; a client that adopts +whichever arrived last can therefore install an *older* +assistant view over a newer one. This specification consequently +does not define an ordering, and no client may assume one. + +It is instead **RECOMMENDED** that a client adopt at the +universal end-marker `ovos.utterance.handled` (PIPELINE-1 §9.5), +which is emitted exactly once per utterance (§3.3) and so needs +no ordering to be unambiguous. + +A client **MAY** adopt incrementally from Messages observed +mid-round, and **MAY** run a more elaborate policy +(field-by-field merge, selecting by emitter identity). Both are +conformant, with one warning that applies to every incremental +policy: **a session adopted mid-round may be stale by the time +it is stored**, and the client has no reliable way to detect +that it is. A client that adopts incrementally **SHOULD** +therefore also adopt at the §3.3 convergence point, which +supersedes whatever the incremental policy accumulated during +the round. ### 3.3 `ovos.utterance.handled` is the canonical convergence point When a client wants a single canonical "round is over" snapshot, -the PIPELINE-1 §9 universal end-marker `ovos.utterance.handled` -is the recommended adoption point: emitted exactly once per +the universal end-marker `ovos.utterance.handled` (PIPELINE-1 +§9.5) is the recommended adoption point: emitted exactly once per utterance on every terminal path, carrying the assistant's final session for the round. A client may also adopt incrementally per §3.1, or combine both; all are conformant. @@ -439,7 +602,8 @@ pathway is governed by the holding component's own design. The spec defines no protocol for plugin-internal state; the plugin chooses what to persist, what to evict, and what "resume" means for its own state. A client cannot expect -parity across components: +parity across components. *(The following illustrations are +informative.)* - a chat-history-holding LLM plugin may resume a months-old conversation seamlessly because it persisted the transcript; @@ -487,14 +651,89 @@ orchestrator operation: §4.2, in-handler mutations) propagate into the store through the standard derivation chain. -The merge semantics for inbound default-session Messages follow -SESSION-1 §2.1's omission rule: **omitted inbound fields leave -the stored field unchanged** (the stored value is the -orchestrator's last authoritative value for that field); a -**present inbound field replaces the stored value** for that -field. This is the natural complement to the stateless-named- -session rule: the default-session store fills the role the -client plays for named sessions. +**Merge semantics for inbound default-session Messages.** An +**omitted inbound field leaves the stored field unchanged** — +the stored value is the orchestrator's last authoritative value +for that field. A **present inbound field replaces the stored +value** for that field. + +This is a deliberate **deviation** from SESSION-1 §2.1, and this +specification owns it. SESSION-1 §2.1 says an omitted field is +filled at consumption with the consumer's deployment default. +That rule is about *consuming* a session; it presumes the +consumer has no better answer than its configuration. For the +default session the orchestrator does have a better answer: it +is the authoritative holder of the session, so it fills an +omission from its store instead of from its defaults. The +deviation is confined to writes into the default-session store. +SESSION-1 §2.1 continues to govern everywhere else, including +every named session and including how components downstream +consume the session the orchestrator then emits. This is the +natural complement to the stateless-named-session rule: the +default-session store fills the role the client plays for named +sessions. + +Field replacement is the **default**; a field whose claiming +specification defines a finer intra-field merge resolves by that +rule instead. At this version that is `session.intent_context`, +merged entry-by-entry per OVOS-CONTEXT-1 §5.3 (§2.7). + +**Which writes the store accepts.** The store holds the local +device's session, so the orchestrator **MUST** accept a +default-session write only where the writer plausibly *is* the +local device. Concretely, the orchestrator **MUST** merge a +session-less or explicitly-default inbound Message into the +store only when the Message originates locally — from the +device's own components, or from the orchestrator's own process +tree. A Message that reached the bus across a deployment +boundary **MUST NOT** be merged into the default-session store +on the strength of carrying no `session_id`. + +The enforcement point for the remote side of this requirement is +the bridge: a boundary component relaying inbound traffic from +an external participant is required to stamp a non-default +`session_id` on it, so that a remote participant cannot capture +the local device's session simply by omitting `session`. +OVOS-BRIDGE-1 §3.4 is where that obligation lives. An +orchestrator that cannot distinguish local from relayed origin +**SHOULD** treat every inbound Message it cannot attribute to a +local source as a named session with an orchestrator-assigned +`session_id`, rather than merging it into the store. + +**Field tolerance applies at store-write.** A session written +into the store may carry keys the orchestrator does not +recognise. SESSION-1 §2.4's unknown-field tolerance applies: +the orchestrator **MUST NOT** reject the write and **MUST NOT** +strip the unrecognised keys, and it **MUST** carry them on the +sessions it subsequently derives from the store. SESSION-1 +§2.5's malformed-carrier rules apply unchanged. + +**Wholesale replacement on the match pathway.** The +field-by-field merge above governs **inbound client Messages**. +A committed `Match.updated_session` (PIPELINE-1 §4.2) is a +complete snapshot instead: the orchestrator **MUST** replace the +working session snapshot with it wholesale. A field absent from +`Match.updated_session` is therefore not preserved — it **reverts +to the deployment default at consumption**, exactly as any +omitted field does under SESSION-1 §2.1. It is not deleted in +any stronger sense, and the distinction matters: a consumer +never sees a hole, it sees a field that resolves to the default. + +Two writes escape wholesale replacement, because other +specifications fix them as applying on top of the committed +snapshot rather than inside it: + +- the `session.active_handlers` push of PIPELINE-1 §7.1, which + that section defines as applied *after* `Match.updated_session` + is committed; +- the entry-level `intent_context` merge of OVOS-CONTEXT-1 §5.3, + which merges entry-by-entry and so is not expressible as a + whole-map replacement. + +Wholesale replacement exists only on the match pathway. An +inbound client Message cannot drop a stored default-session +field by omitting it — it can only overwrite the field by +sending a new value. ### 5.2 Restart semantics @@ -504,7 +743,7 @@ defaults (the empty session, with every field falling back per SESSION-1 §2.1). Components keyed on the default session lose their state. -This is acceptable for the default session by design: the +*(Rationale, informative.)* This is acceptable for the default session by design: the default session represents the local device, which is typically co-located with the orchestrator process. A restart of the orchestrator is a restart of the device's voice stack; @@ -526,14 +765,6 @@ holds it. For named sessions the same field is preserved only as long as the client holds it locally — best-effort on remote peers. -### 5.4 Default-session sync to clients - -The orchestrator **MAY** emit the default-session state as a -diagnostic on a deployer-defined topic, so that interested -observers can track default-session evolution without processing -every response Message. No normative topic name or consumer is -defined here; this is deployment policy. - --- ## 6. Conformance @@ -554,21 +785,18 @@ An orchestrator that claims conformance to this specification stateless per §2.2 — no cross-utterance state held outside what the inbound Message brings; - hold the default session as persistent in-process state per - §5, with the merge / derive / restart semantics of §5.1 / - §5.2; + §5, with the merge, write-acceptance, derive and restart + semantics of §5.1 and §5.2; - apply in-place session mutations only at the boundaries of - §2.6 (transformer, pipeline-match, handler); + §2.6 (transformer, pipeline-match, handler), in the order + §2.8 fixes; - propagate session forward unchanged on every Message derivation per OVOS-MSG-1 §5 and SESSION-1 §4, except where the §2.6 boundaries dictate mutation; - emit the universal end-marker `ovos.utterance.handled` - carrying the final round session (PIPELINE-1 §9), as the + carrying the final round session (PIPELINE-1 §9.5), as the 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. +- merge `ovos.session.sync` Messages on receipt per §2.7. An orchestrator **MUST NOT** require any client to declare session-start / session-end / session-id-allocation events @@ -580,11 +808,13 @@ send; the orchestrator processes what arrives. A component that holds `session_id`-keyed state across utterances **SHOULD**: -- project that state into a session-resident field it claims - under SESSION-1 §2.1 (per §2.4), via the appropriate - in-utterance pathway — `Match.updated_session` for pipeline - plugins per PIPELINE-1 §4.2, direct mutation for - transformers and handlers per §2.6; +- project that state into a session-resident field per §2.4 — + a field claimed by a specification under SESSION-1 §2.2 where + one exists, otherwise an unclaimed field carried under + SESSION-1 §2.4's tolerance with no normative reading by + anyone else — via the appropriate in-utterance pathway: + `Match.updated_session` for pipeline plugins per PIPELINE-1 + §4.2, direct mutation for transformers and handlers per §2.6; - on every inbound Message, read its state from `session` rather than from a cross-utterance internal store. @@ -595,7 +825,8 @@ accept best-effort resumption (§4.3). A component **MUST NOT** rely on bus events (the asynchronous kind that fire outside the utterance lifecycle) to mutate -session state in the current utterance (§2.6). It MAY emit such +session state in the current utterance (§2.6); +`ovos.session.sync` (§2.7) is the one exception. It MAY emit such events to communicate with other components; their effect on session, if any, lands on subsequent utterances. @@ -619,11 +850,10 @@ A client **MAY** update its local session per §3, choose any persistence format and lifetime, and re-emit a previously-used `session_id` at any time (§4). -A client **MUST NOT**: - -- expect the orchestrator to remember any session state for it - between rounds — every round MUST be self-sufficient via the - inbound session. +A client **MUST** make every round self-sufficient: it **MUST** +carry on the inbound session all state it needs the assistant to +act on, and it **MUST NOT** omit state on the assumption that +the orchestrator retained it from a previous round. ### 6.5 Default-session client @@ -648,12 +878,20 @@ 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. +A deployment **MAY** define a further topic of its own on which +the orchestrator publishes the default-session state (§5) as a +diagnostic, so that interested observers can track +default-session evolution without processing every response +Message. This specification defines no name, payload, or +consumer for such a topic; it is deployment policy and carries +no conformance weight. + --- ## 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.3 makes restart-loss +restart** is not defined here. §5.2 makes restart-loss explicit and intentional; persistence is deployer policy if desired.