Cross-service contracts lived as prose in CONTRACTS.md and as copied code; nothing validated them.
Decision
OpenVibe.Contracts is the machine-readable authority: JSON Schema 2020-12 contracts with a catalog (id, semver, owner, visibility, compatibility, status), fixtures for every contract, generated TypeScript types, service/capability/namespace manifests.
A contract id is permanent; minor versions only add optional fields; a breaking change is a new major file plus a deprecation record with a replacement and window.
scripts/compat.js fails CI on breaking changes against the previous tag; services run openvibe-contracts-check in their CI.
Consumers pin a release tarball (codeload.github.com/.../tar.gz/refs/tags/vX), so production installs need no GitHub credentials.
Alternatives considered
Publish to the npm registry: deferred; tarball pins are immutable enough and need no registry account.
OpenAPI-first: rejected as the source of truth; OpenAPI fragments can be generated later from the schemas.
Migration consequences
Contracts grow additively; each service moves to a new tag by bumping its pin.
Rollback
A bad release is fixed forward (new patch tag); consumers can pin the previous tag.
Acceptance tests
Contracts npm test (fixtures, catalog integrity, manifests cross-checked, compat gate), CI green on every tag; the compat gate was shown to fail on a deliberate breaking edit.
ADR-016: Active client update safety and the supported mixed-version window
Status: Accepted 2026-09-23. Governs Track R.
Context and current evidence
Live serves content-hashed asset URLs (server/web/assets.js) and posts one rolling deploy notice in chat.
/shared/* on Network is served from the pinned OpenVibe.Shared release:
plain URLs carry max-age=300;
?v=<hash> and /shared/v1/* URLs are immutable for a year.
Cloudflare currently raises short browser TTLs to 4 hours, a zone setting that needs changing to "respect existing headers".
Open tabs can run a client built several releases ago against a new server.
Decision
Mixed-version window: a server supports clients from the current and the previous release of its API contract for at least 24 hours after a deploy, and never breaks a contract version inside its major (ADR-002).
Assets: every asset a page loads is either content-hash addressed (immutable) or explicitly short-lived. An HTML document never references a mutable URL for code.
Release manifest: each web surface serves /release.json (service, release id, contract versions, min supported client release). Clients compare on focus/reconnect. When they have fallen outside the window, they show a non-blocking "new version" prompt, and reload automatically only when idle with no unsent input, upload, call or stream.
Protected sessions: a deploy never ends an upload, a call, a live broadcast or a recording. Host (Wave 21) drains or waits, as Live's --wait-idle does today.
Shared chrome: a release of OpenVibe.Shared must not break a page that pinned the previous minor.
Alternatives considered
Forced reload on deploy: rejected; it loses input and interrupts sessions.
Service workers for version pinning: deferred; the release manifest is enough and is simpler to reason about.
Migration consequences
Live, Network, Community, Media and Tools add /release.json. The Cloudflare browser-TTL setting is changed so origin cache headers are respected.
Rollback
The manifest is advisory; removing it returns clients to today's behaviour.
Acceptance tests
A client one release behind keeps working for 24 hours after a deploy.
A deploy during an upload, call or stream does not interrupt it.
An outdated tab prompts and does not reload while the user is typing.
Amendment 1 (2026-09-26): release notifications over the Events realtime plane
Roadmap WS-P task 9.
Event. When a network service's release goes live, OpenVibe.Host publishes host.release.published with subject { type: release, id: <service>:<release> } and visibility public. The payload contract is host.release.published@1: service, release, commit, origin, deployed_at, and optionally components and rollback. It carries identifiers only; what changed stays in /release.json.
Sent by:ovhost deploy|rollback after a release went live, and ovhost announce <service> for services deployed by their own scripts.
Frequency: one event per service and release.
Delivery: best effort, and never fails a deploy. Polling is the fallback.
Stage B: tenant activations stay host.deploy.activated (subject deploy, visibility internal), a separate event type with its own payload. The two are never mixed.
Client. openvibe-shared release-watch.js (1.17.0) opens one anonymous EventSource per tab on topics=host.release.published.
It acts only on events whose payload.service is the page's service.
It ignores the release it already runs or already knows (a hex prefix counts as the same release), and ignores repeats of an event id.
It collapses bursts: at most one check per 30 s, after a random 0–20 s delay so tabs do not all fetch at once. The check is the usual /release.json path: the release window, the update plan and the safety rules are unchanged.
The stream is public, so a change of account changes nothing and never opens a second stream.
A tab hidden for 5 minutes closes its stream; hidden tabs are covered by the poll and the check on becoming visible. Errors back off, from 30 s to 15 minutes, and after repeated failures the tab keeps only the poll.
Rollback. Stop announcing (--no-announce, or remove the credentials): tabs fall back to polling, as before.
Amendment 2 (2026-09-26): client generations and the shell (manifest 1.2.0)
Roadmap WS-P task 7 completes the release manifest with three optional fields.
client_generation: the generation of the client a release serves. It goes up only when an older client can no longer work against the server. The page states it in the ov-release meta tag (data-generation).
min_client_generation: the oldest generation still supported. A tab below it reloads at the next safe moment with reason required, without waiting for the mixed-version window. min_client_release stays as it is: it names one exact release, while a generation covers every release since the break.
shell: the page shell, { version, components }. The components are ids from components, and the version is the first 12 hex of SHA-256 over their ids and versions. A tab whose shell version differs never updates in place: it is prompted, and reloads only when it must.
The fields are additive; a client that does not know them behaves as under 1.1.0.