Disciplines · Integrations

Instrumented-session SDK — upgrade guide

Two calls, in this order, every release:

8sections5 minread

On this page

Scope: engine connectors that deliver instrumented gameplay sessions to the Yemaya study workspace (proposal §12.2, YSD-12020/12024/12027/12028).

Everything a study claims about a game rests on what a connector sent. This guide covers what may change between connector releases, what may not, and how to prove a release is safe before it ships.

Reference material:

  • The SDK you embedlibs/yemaya/instrumented-session-sdk (@yemaya/instrumented-session-sdk)
  • Envelope contract — libs/contracts/src/study/instrumented-session-envelope.ts
  • Event families — libs/contracts/src/study/session-event-families.ts
  • Registry rules — libs/yemaya/study-workspace/src/policies/event-schema-registry.ts
  • Registry record — libs/contracts/src/study/entities/connector-registry.ts
  • Conformance suite — libs/yemaya/study-workspace/src/testing/instrumented-sdk-conformance.ts
  • First-party reference integration (V2, decision YSD-0127)libs/aphrodite/game-runtime/src/study-connector
  • Minimal shape-only connector — libs/yemaya/study-workspace/src/testing/sample-instrumented-connector.ts

0. Before you send anything#

Two calls, in this order, every release:

  1. GET /api/study/connectors/protocol — the envelope-version window this workspace decodes. The SDK's negotiateProtocol compares it against SDK_PROTOCOL_VERSION and refuses to send outside it. A refused upload is recoverable; a mis-decoded session is not.
  2. POST /api/study/connectors/registry — publish the streams this release declares.

The registry is stored, one live release per connector per tenant, revised rather than edited. You do not supply the release you supersede: the workspace reads it. That is the difference between a compatibility check and a compatibility ritual — a connector that could name its own predecessor could always name a flattering one.

Sessions then pin your connectorId at registration, and every batch they carry is checked against what you published. A connector that has published nothing leaves the cross-version check reporting itself unrun, which is honest, and is not the same as a pass.


1. The one rule that has no exceptions#

A stream's declaration may not change while its version stays the same.

Consumers cache decoders by schemaVersion. A stream that changes shape, ownership, clock basis, sequencing guarantee, or privacy class under an unchanged version silently mis-decodes on every consumer that has already seen it. checkRegistryUpgrade refuses this in every compatibility mode — there is no configuration flag that permits it.

If you changed a stream, advance its version. If you are not sure whether you changed it, run the suite; it will tell you.

2. What counts as breaking#

A change is breaking — requiring a major version bump, and only permitted under version-negotiated compatibility with migration notes on record — when it does any of:

Change Why it breaks
schemaRef moves the shape moved; old readers decode the wrong fields
ownerDomain moves the domain accountable for the semantics changed
clockBasis moves every alignment built on the old basis silently shifts
sequenced goes truefalse consumers that ordered on it keep working and order wrongly
privacyClass loosens data collected under one promise is handled under a weaker one

Loosening a privacy class additionally requires an explicit relaxation authorization naming the stream, and moving ownerDomain requires an ownership transfer on record. Neither is inferred from the diff.

Compatible changes still need a version bump, just not a major one:

  • a new producing build (producerBuild),
  • sequenced going falsetrue (strengthening),
  • privacyClass tightening,
  • a new stream (under additive-only or version-negotiated).

3. Choosing a compatibility mode#

Declared in the envelope's compatibility.eventSchemaCompatMode:

  • strict — only the producing build may change. Nothing is added, nothing is removed. Use while a study is mid-flight and its results must stay comparable.
  • additive-only — new streams and strengthening revisions are admitted; removals and breaking revisions are not. The normal mode for a shipping connector.
  • version-negotiated — breaking changes and removals are admitted, provided compatibility.migrationNotesRef points at notes consumers can negotiate against. Use for a major connector release.

Removing a stream additionally requires a retirement on record, in any mode.

4. Clocks#

A connector that stamps events on any clock other than session-monotonic must supply sync points for that clock. Without them the workspace cannot place the events on the session timeline at all, and the conformance suite fails the connector rather than guessing.

Bounds the suite enforces by default:

  • drift: ±500 ppm — a crystal drifting further cannot hold a frame over a minute;
  • worst-case sync residual: 8 ms — half a frame at 60 Hz, the bound a per-frame claim needs.

Both are overridable per run for hardware with known-worse characteristics, but the loosened bound then travels with the study, not with the connector.

Three sync points is the floor. Two fit a line exactly and leave no residual to estimate error from — the workspace reports such a span as underdetermined rather than as a perfect fit.

5. Privacy#

The ingest plan is an allowlist. A field the plan does not classify is held back, not stored. That inversion is deliberate: a connector upgrade that starts emitting player.email must not have its new field flow through on the strength of nobody having forbidden it yet.

Practical consequence for an upgrade: any new field is a plan change. Add the classification in the same release that adds the field, or the field is quarantined and the connector fails its privacy check.

Classes that cannot be pseudonymized into acceptability: chat content and voice content. A pseudonymized chat line is still a chat line; a voice is its own identifier. Both are dropped.

6. Before you ship#

Run the suite:

ts
import { runSdkConformance } from '@oshun/yemaya-study-workspace';

const report = runSdkConformance(
  myConnector,
  { pseudonymize },
  { maxDriftPpm: 500 }
);
if (!report.conformant) {
  for (const check of report.results.filter((r) => !r.passed)) {
    console.error(check.check, check.issues);
  }
}

Six checks, all of which must pass — there is no partial credit, because a study cannot use half a session:

  1. handshake — every stream you emit is declared, and the handshake and envelope name the same connector.
  2. envelope — parses, sits inside its own compatibility window, manifest covers every stream.
  3. fixtures — every stream passes the nine per-family dimensions.
  4. clock-drift — sync points fit within the drift and error bounds.
  5. backwards-compatibility — the registry is a legal successor under your own declared policy.
  6. privacy — nothing emitted falls outside the ingest plan.

7. Upgrade checklist#

  • negotiateProtocol accepts the workspace's advertised window.
  • The release is published to POST /connectors/registry and admitted.
  • Every changed stream has an advanced schemaVersion.
  • Every breaking change has a major bump, version-negotiated compatibility, and migrationNotesRef pointing at this file or its successor.
  • Every privacy relaxation has an authorization naming the stream.
  • Every ownership move has a transfer on record naming the receiving domain.
  • Every removed stream has a retirement on record.
  • Every new field appears in the ingest plan in the same release.
  • Sync points exist for every non-session clock basis you stamp on.
  • runSdkConformance reports conformant: true.