From bbf71bfead30be08a2a606da8c45f926663d3274 Mon Sep 17 00:00:00 2001 From: acamilo Date: Wed, 23 Sep 2026 08:31:58 +0000 Subject: [PATCH 1/5] session types: the legacy Game Boy profile and the RT-01a extension methods PROF-02a and RT-01a, machine-readable half, per the operator's port decisions of 2026-09-23. Generic (in the session schema set, so contractDigest moves): - AgentTelemetry.stimulusRemainingMs (number|null): sugar admission reads the pulse from the last commit. - EpisodeRequest.kind is terminal | rollback. - Environment.SaveSlot / Environment.RestoreSlot (capability gameboy-slots-v1) and Agent.Rollback (capability legacy-ratchet-rollback-v1) payloads, with their scope checks; maxSlots 4. Legacy Game Boy (module gameboy, digested apart from contractDigest): - registered payload schemas gameboy-readout-context-v1, gameboy-channels-v1, gameboy-joypad-v1, gameboy-memory-inspection-v1, legacy-ratchet-rollback-v1, each SchemaRef digest over its canonical declaration; - the one legacy profile gameboy-legacy-fafb-v783-v1 embedding today's schema-1 fingerprint, lif-1ms-f64-v2 and fly-kc-mbon-rstdp-v2, with its AssetRef digest; - the composition declaration carrying the decoder and macro-channel configuration, the executor pokered-macros-v1, gameboy-slots-v1, legacy-ratchet-rollback-v1, legacy-transient-reset and FLYSIM01 as format of record, cross-checked against the FLYSIM01 compatibility string. Fixtures regenerated by update_fixtures (new derived gameboy-legacy.json); valid/invalid cases for every new type in both languages; the frame clock proven identical to the legacy f64 accumulator. flysim gains only a test (and a dev-dependency) that recomputes the pinned fingerprint, versions, frame size, warm-up, clock and button order. The synthetic fly-session agent reports stimulusRemainingMs null and its task names kind terminal; no runtime change. --- packages/session-types/README.md | 2 + packages/session-types/src/extensions.ts | 184 +++ packages/session-types/src/gameboy.ts | 488 ++++++ packages/session-types/src/index.ts | 2 + packages/session-types/src/workers.ts | 23 +- packages/session-types/tests/gameboy.test.ts | 146 ++ packages/session-types/tests/readers.ts | 28 + packages/session-types/tests/schema.test.ts | 3 + services/flysim/Cargo.lock | 1 + .../flysim/crates/fly-session-types/README.md | 11 +- .../examples/update_fixtures.rs | 95 +- .../fixtures/contract-digest.json | 8 +- .../fixtures/descriptor-checks.json | 5 + .../fixtures/gameboy-legacy.json | 548 +++++++ .../fly-session-types/fixtures/invalid.json | 1365 +++++++++++++++++ .../fixtures/schema-set.json | 2 +- .../fly-session-types/fixtures/valid.json | 578 ++++++- .../fly-session-types/src/extensions.rs | 414 +++++ .../crates/fly-session-types/src/gameboy.rs | 1238 +++++++++++++++ .../crates/fly-session-types/src/lib.rs | 10 +- .../crates/fly-session-types/src/schema.rs | 92 +- .../crates/fly-session-types/src/workers.rs | 67 +- .../fly-session-types/tests/common/mod.rs | 29 + .../fly-session-types/tests/gameboy_legacy.rs | 350 +++++ .../fly-session-types/tests/schema_set.rs | 23 +- .../flysim/crates/fly-session/src/agent.rs | 3 + .../flysim/crates/fly-session/src/task.rs | 4 +- .../flysim/crates/fly-session/src/types.rs | 3 +- services/flysim/crates/flysim/Cargo.toml | 3 + .../flysim/tests/legacy_profile_identity.rs | 58 + 30 files changed, 5742 insertions(+), 41 deletions(-) create mode 100644 packages/session-types/src/extensions.ts create mode 100644 packages/session-types/src/gameboy.ts create mode 100644 packages/session-types/tests/gameboy.test.ts create mode 100644 services/flysim/crates/fly-session-types/fixtures/gameboy-legacy.json create mode 100644 services/flysim/crates/fly-session-types/src/extensions.rs create mode 100644 services/flysim/crates/fly-session-types/src/gameboy.rs create mode 100644 services/flysim/crates/fly-session-types/tests/gameboy_legacy.rs create mode 100644 services/flysim/crates/flysim/tests/legacy_profile_identity.rs diff --git a/packages/session-types/README.md b/packages/session-types/README.md index c49c758..ee4e608 100644 --- a/packages/session-types/README.md +++ b/packages/session-types/README.md @@ -26,6 +26,8 @@ control contracts are unchanged and still live in [`@flybrain/feed`](../feed). | `trace` | The step-v1 section 8 record and the behaviour-only comparator | | `seed` | `seed-derivation-v1` | | `checkpoint` | The `FLYSESS1` envelope layout | +| `extensions` | `Environment.SaveSlot`/`RestoreSlot` and `Agent.Rollback` payloads (2026-09-23) | +| `gameboy` | The legacy Game Boy composition: registered schemas, profile, composition declaration | | `fixtures` | Loading the shared corpus | ## Reading a payload diff --git a/packages/session-types/src/extensions.ts b/packages/session-types/src/extensions.ts new file mode 100644 index 0000000..025e6f5 --- /dev/null +++ b/packages/session-types/src/extensions.ts @@ -0,0 +1,184 @@ +/** + * The extension methods of the 2026-09-23 amendments (RT-01a, workers-v1 section 7): + * `Environment.SaveSlot`, `Environment.RestoreSlot` and `Agent.Rollback`. + * + * The Rust twin is `fly-session-types/src/extensions.rs`. The shapes are generic; which + * composition may use them is a capability negotiated by `Worker.Hello`. Nothing console + * specific is in these payloads: the Game Boy lives in the registered schemas of `gameboy`. + */ +import { fail } from './canonical'; +import { readTypedValue } from './common'; +import { Reader, u64 } from './reader'; +import type { Digest, Id, Scope, TypedValue, U64 } from './scalar'; +import { + type AgentTelemetry, + type SensoryInput, + type WorldObservation, + readAgentTelemetry, + readSensoryInput, + readWorldObservation, +} from './workers'; + +export const SLOTS_CAPABILITY = 'gameboy-slots-v1'; +export const ROLLBACK_CAPABILITY = 'legacy-ratchet-rollback-v1'; +export const ROLLBACK_POLICY = 'legacy-ratchet-rollback-v1'; +export const METHOD_SAVE_SLOT = 'Environment.SaveSlot'; +export const METHOD_RESTORE_SLOT = 'Environment.RestoreSlot'; +export const METHOD_AGENT_ROLLBACK = 'Agent.Rollback'; +/** Slots one environment may hold. Not a stated bound; recorded in the schema set. */ +export const MAX_SLOTS = 4; + +function requirePolicy(policy: string, what: string): void { + if (policy !== ROLLBACK_POLICY) { + fail(`${what}: policy must be ${ROLLBACK_POLICY}, the only rollback policy defined`); + } +} + +export interface SaveSlotParams { + slotId: Id; +} + +export function readSaveSlotParams(value: unknown): SaveSlotParams { + const reader = new Reader(value, 'SaveSlotParams'); + const params: SaveSlotParams = { slotId: reader.id('slotId') }; + reader.finish(); + return params; +} + +export interface SaveSlotResult { + slotId: Id; + boundary: U64; + stateDigest: Digest; + byteLength: U64; +} + +export function readSaveSlotResult(value: unknown): SaveSlotResult { + const reader = new Reader(value, 'SaveSlotResult'); + const result: SaveSlotResult = { + slotId: reader.id('slotId'), + boundary: reader.u64('boundary'), + stateDigest: reader.digest('stateDigest'), + byteLength: reader.u64('byteLength'), + }; + reader.finish(); + if (u64(result.byteLength) === 0n) fail('SaveSlotResult: byteLength must be positive'); + return result; +} + +/** The slot records the committed boundary the call was scoped to. */ +export function validateSaveSlotAgainstScope(result: SaveSlotResult, scope: Scope): void { + if (result.boundary !== scope.step) { + fail(`SaveSlotResult: boundary ${result.boundary} must be the scoped committed step ${scope.step}`); + } +} + +export interface RestoreSlotParams { + slotId: Id; + priorEpoch: Id; + policy: Id; +} + +export function readRestoreSlotParams(value: unknown): RestoreSlotParams { + const reader = new Reader(value, 'RestoreSlotParams'); + const params: RestoreSlotParams = { + slotId: reader.id('slotId'), + priorEpoch: reader.id('priorEpoch'), + policy: reader.id('policy'), + }; + reader.finish(); + requirePolicy(params.policy, 'RestoreSlotParams'); + return params; +} + +/** A rollback always moves to a new epoch. */ +export function validateRestoreSlotAgainstScope(params: RestoreSlotParams, scope: Scope): void { + if (params.priorEpoch === scope.epoch) { + fail('RestoreSlotParams: priorEpoch must differ from the scoped (new) epoch'); + } +} + +export interface RestoreSlotResult { + slotId: Id; + committedStep: U64; + observation: WorldObservation; +} + +export function readRestoreSlotResult(value: unknown): RestoreSlotResult { + const reader = new Reader(value, 'RestoreSlotResult'); + const result: RestoreSlotResult = { + slotId: reader.id('slotId'), + committedStep: reader.u64('committedStep'), + observation: readWorldObservation(reader.value('observation')), + }; + reader.finish(); + if (result.observation.boundary !== result.committedStep) { + fail('RestoreSlotResult: the observation boundary must be the committed step'); + } + if (result.observation.audio.length !== 0) { + fail('RestoreSlotResult: a restored slot ran no transition and carries no audio chunk'); + } + return result; +} + +export interface AgentRollbackParams { + agentId: Id; + priorEpoch: Id; + policy: Id; + input: SensoryInput; + decisionContext: TypedValue; +} + +export function readAgentRollbackParams(value: unknown): AgentRollbackParams { + const reader = new Reader(value, 'AgentRollbackParams'); + const params: AgentRollbackParams = { + agentId: reader.id('agentId'), + priorEpoch: reader.id('priorEpoch'), + policy: reader.id('policy'), + input: readSensoryInput(reader.value('input')), + decisionContext: readTypedValue(reader.value('decisionContext')), + }; + reader.finish(); + requirePolicy(params.policy, 'AgentRollbackParams'); + return params; +} + +/** A new epoch, and the installed input is the restored boundary: the scoped step. */ +export function validateAgentRollbackAgainstScope(params: AgentRollbackParams, scope: Scope): void { + if (params.priorEpoch === scope.epoch) { + fail('AgentRollbackParams: priorEpoch must differ from the scoped (new) epoch'); + } + if (params.input.boundary !== scope.step) { + fail(`AgentRollbackParams: input.boundary ${params.input.boundary} must be the scoped step ${scope.step}`); + } +} + +export interface AgentRollbackResult { + agentId: Id; + committedStep: U64; + decisionContextDigest: Digest; + telemetry: AgentTelemetry; +} + +export function readAgentRollbackResult(value: unknown): AgentRollbackResult { + const reader = new Reader(value, 'AgentRollbackResult'); + const result: AgentRollbackResult = { + agentId: reader.id('agentId'), + committedStep: reader.u64('committedStep'), + decisionContextDigest: reader.digest('decisionContextDigest'), + telemetry: readAgentTelemetry(reader.value('telemetry')), + }; + reader.finish(); + return result; +} + +/** A rollback runs no tick: the acknowledged step is the scoped one. */ +export function validateAgentRollbackResultAgainstScope( + result: AgentRollbackResult, + scope: Scope, +): void { + if (result.committedStep !== scope.step) { + fail( + `AgentRollbackResult: committedStep ${result.committedStep} must be the scoped step ${scope.step}; a rollback runs no tick`, + ); + } +} diff --git a/packages/session-types/src/gameboy.ts b/packages/session-types/src/gameboy.ts new file mode 100644 index 0000000..b81cd5f --- /dev/null +++ b/packages/session-types/src/gameboy.ts @@ -0,0 +1,488 @@ +/** + * The legacy Game Boy composition's registered schemas and declarations (PROF-02a, + * `docs/design/session-framework/legacy-gameboy-v1.md`). + * + * The Rust twin is `fly-session-types/src/gameboy.rs`, which renders every declaration and + * digest into `fixtures/gameboy-legacy.json`. This side keeps the references as constants and + * its tests recompute each digest from that file with this package's own canonical JSON, so a + * drift in either language fails. + * + * Nothing here is a generic session type: these values travel inside `TypedValue`s or are + * documents an `AssetRef` or the composition digest names. + */ +import { digestOf, fail } from './canonical'; +import { readRational, readSchemaRef } from './common'; +import { Reader, readArtifactRef, requireUnique, u64 } from './reader'; +import type { ArtifactRef, Digest, Id, RationalNs, SchemaRef, TypedValue } from './scalar'; +import { isDigest } from './scalar'; +import { type AssetRef, readAssetRef } from './workers'; +import { MAX_SLOTS, ROLLBACK_POLICY, SLOTS_CAPABILITY } from './extensions'; + +export const PROFILE_ID = 'gameboy-legacy-fafb-v783-v1'; +export const DATASET_ID = 'fafb-v783'; +export const FINGERPRINT_SCHEMA = 1; +/** Today's schema-1 fingerprint of `data/fafb-v783`: seven SHA-256 digests joined with ':'. */ +export const FAFB_V783_FINGERPRINT = [ + '75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3', + '1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e', + '63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5', + 'f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7', + 'ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62', + 'b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634', + 'dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc', +].join(':'); +export const KERNEL_VERSION = 'lif-1ms-f64-v2'; +export const PLASTICITY_VERSION = 'fly-kc-mbon-rstdp-v2'; +export const WARMUP_MS = 2500; +export const STIMULUS_REWARD_PULSE = 'reward-pulse'; +export const EXCEPTION_MACRO_ROLES = 'macro-roles-outside-fingerprint'; +export const PROFILE_FORMAT = 'fly-profile-v1'; +export const VIEW_ID = 'lcd'; +export const VIEW_WIDTH = 160; +export const VIEW_HEIGHT = 144; +export const GAMEBOY_BUTTONS = ['up', 'down', 'left', 'right', 'a', 'b', 'start', 'select'] as const; +export const MEMORY_IMAGE_BYTES = 65_536n; +export const EXECUTOR_ID = 'pokered-macros-v1'; +export const RESTORE_SEMANTICS = 'legacy-transient-reset'; +export const CHECKPOINT_FORMAT_OF_RECORD = 'FLYSIM01'; +export const SCHEDULER = 'lockstep-v1'; +export const SETUP_FRAMES = 1; +export const MAX_MACRO_CHANNELS = 64; +export const MAX_COMPATIBILITY_BYTES = 1024; +export const MACRO_MODES = ['raw', 'macros'] as const; +export const ROLLBACK_TRIGGERS = ['stall', 'game-over'] as const; + +/** One Game Boy frame, 70224 cycles at 4194304 Hz: `8572265625/512` ns exactly. */ +export const STEP_DURATION: RationalNs = { numerator: '8572265625', denominator: '512' }; +/** One model tick, 1 ms. */ +export const TICK_DURATION: RationalNs = { numerator: '1000000', denominator: '1' }; + +/** The registered payload schema references, digests over their canonical declarations. */ +export const READOUT_CONTEXT_SCHEMA: SchemaRef = { + id: 'gameboy-readout-context-v1', + version: 1, + digest: '78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d', +}; +export const CHANNELS_SCHEMA: SchemaRef = { + id: 'gameboy-channels-v1', + version: 1, + digest: '28bd89bfa41ec73fb2785959a64d9975b4a932e10cd9e51dc4bc29020c823595', +}; +export const JOYPAD_SCHEMA: SchemaRef = { + id: 'gameboy-joypad-v1', + version: 1, + digest: '1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e', +}; +export const MEMORY_INSPECTION_SCHEMA: SchemaRef = { + id: 'gameboy-memory-inspection-v1', + version: 1, + digest: 'd6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6', +}; +export const ROLLBACK_REQUEST_SCHEMA: SchemaRef = { + id: 'legacy-ratchet-rollback-v1', + version: 1, + digest: '0610f899a4746e5991a07dbc3c847ada15d7c68cedf0664244a3c1d87c175c70', +}; +export const PAYLOAD_SCHEMAS: readonly SchemaRef[] = [ + READOUT_CONTEXT_SCHEMA, + CHANNELS_SCHEMA, + JOYPAD_SCHEMA, + MEMORY_INSPECTION_SCHEMA, + ROLLBACK_REQUEST_SCHEMA, +]; +/** The legacy profile document's AssetRef digest: SHA-256 of its canonical JSON. */ +export const PROFILE_DIGEST = '41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878'; + +/** `ChannelName`: a decoder channel or rate-role name. Not an Id: legacy names carry '_'. */ +export function isChannelName(value: unknown): value is string { + return typeof value === 'string' && /^[a-z][a-z0-9_]{0,63}$/.test(value); +} + +function sameSchema(found: SchemaRef, expected: SchemaRef, what: string): void { + if (found.id !== expected.id || found.version !== expected.version || found.digest !== expected.digest) { + fail(`${what} must be the registered ${expected.id} reference`); + } +} + +function exact(found: unknown, expected: unknown, what: string): void { + if (found !== expected) fail(`${what} must be ${JSON.stringify(expected)}`); +} + +function channelList(reader: Reader, key: string, high: number): string[] { + const items = reader.list(key, 0, high, (item) => { + if (!isChannelName(item)) fail('every entry must be a channel name'); + return item; + }); + requireUnique(items, key); + return items; +} + +function uniqueIds(reader: Reader, key: string, low: number, high: number): Id[] { + const ids = reader.idList(key, low, high); + requireUnique(ids, key); + return ids; +} + +function typedAs(value: TypedValue, schema: SchemaRef, read: (v: unknown) => T): T { + sameSchema(value.schema, schema, 'TypedValue.schema'); + return read(value.value); +} + +// gameboy-readout-context-v1 --------------------------------------------------------------- + +export interface GameboyLocation { + area: number; + x: number; + y: number; +} + +export interface GameboyReadoutContext { + boot: boolean; + bound: string[]; + location: GameboyLocation | null; +} + +export function readGameboyReadoutContext(value: unknown): GameboyReadoutContext { + const reader = new Reader(value, 'GameboyReadoutContext'); + const boot = reader.boolean('boot'); + const bound = channelList(reader, 'bound', MAX_MACRO_CHANNELS); + const raw = reader.value('location'); + let location: GameboyLocation | null = null; + if (raw !== null) { + const l = new Reader(raw, 'GameboyReadoutContext.location'); + location = { + area: l.int('area', 0, 4_294_967_295), + x: l.int('x', 0, 4_294_967_295), + y: l.int('y', 0, 4_294_967_295), + }; + l.finish(); + } + reader.finish(); + return { boot, bound, location }; +} + +export function readGameboyReadoutContextTyped(value: TypedValue): GameboyReadoutContext { + return typedAs(value, READOUT_CONTEXT_SCHEMA, readGameboyReadoutContext); +} + +/** `bound` is a subset of the composition's macro channels, in their order. */ +export function validateReadoutContextAgainst( + context: GameboyReadoutContext, + macroChannels: readonly string[], +): void { + let cursor = 0; + for (const channel of context.bound) { + const offset = macroChannels.slice(cursor).indexOf(channel); + if (offset < 0) { + fail( + `GameboyReadoutContext: bound channel "${channel}" is not a macro channel of the composition, or is out of order`, + ); + } + cursor += offset + 1; + } +} + +// gameboy-channels-v1 ---------------------------------------------------------------------- + +export interface GameboyChannelsDecision { + buttons: { id: (typeof GAMEBOY_BUTTONS)[number]; down: boolean }[]; + macro: string | null; +} + +export function readGameboyChannelsDecision(value: unknown): GameboyChannelsDecision { + const reader = new Reader(value, 'GameboyChannelsDecision'); + const buttons = reader.list('buttons', 8, 8, (item) => { + const b = new Reader(item, 'GameboyChannelsDecision.buttons'); + const entry = { id: b.id('id'), down: b.boolean('down') }; + b.finish(); + return entry; + }); + buttons.forEach((button, index) => { + if (button.id !== GAMEBOY_BUTTONS[index]) { + fail(`GameboyChannelsDecision: buttons must list ${GAMEBOY_BUTTONS.join(',')} in that order`); + } + }); + const macro = reader.value('macro'); + if (macro !== null && !isChannelName(macro)) { + fail('GameboyChannelsDecision: macro must be null or a channel name'); + } + reader.finish(); + return { + buttons: buttons as GameboyChannelsDecision['buttons'], + macro: macro as string | null, + }; +} + +export function readGameboyChannelsDecisionTyped(value: TypedValue): GameboyChannelsDecision { + return typedAs(value, CHANNELS_SCHEMA, readGameboyChannelsDecision); +} + +/** The joypad mask, bit `i` for `GAMEBOY_BUTTONS[i]`. */ +export function channelsMask(decision: GameboyChannelsDecision): number { + return decision.buttons.reduce((mask, button, bit) => (button.down ? mask | (1 << bit) : mask), 0); +} + +/** The macro group only ever activates a bound channel. */ +export function validateDecisionAgainst( + decision: GameboyChannelsDecision, + context: GameboyReadoutContext, +): void { + if (decision.macro !== null && !context.bound.includes(decision.macro)) { + fail(`GameboyChannelsDecision: macro "${decision.macro}" is not bound in the decision context`); + } +} + +// gameboy-memory-inspection-v1 ------------------------------------------------------------- + +export interface GameboyMemoryInspection { + memory: ArtifactRef; + romDigest: Digest; +} + +export function readGameboyMemoryInspection(value: unknown): GameboyMemoryInspection { + const reader = new Reader(value, 'GameboyMemoryInspection'); + const inspection: GameboyMemoryInspection = { + memory: readArtifactRef(reader.value('memory')), + romDigest: reader.digest('romDigest'), + }; + reader.finish(); + if (u64(inspection.memory.byteLength) !== MEMORY_IMAGE_BYTES) { + fail(`GameboyMemoryInspection: the memory image is exactly ${MEMORY_IMAGE_BYTES} bytes`); + } + return inspection; +} + +export function readGameboyMemoryInspectionTyped(value: TypedValue): GameboyMemoryInspection { + return typedAs(value, MEMORY_INSPECTION_SCHEMA, readGameboyMemoryInspection); +} + +/** The ROM the executor reads is the content the environment runs. */ +export function validateInspectionAgainst( + inspection: GameboyMemoryInspection, + contentDigest: Digest, + rom: AssetRef, +): void { + if (inspection.romDigest !== contentDigest || rom.digest !== contentDigest) { + fail( + 'GameboyMemoryInspection: romDigest, the environment contentDigest and the executor rom must agree', + ); + } +} + +// legacy-ratchet-rollback-v1 --------------------------------------------------------------- + +export interface LegacyRatchetRollbackRequest { + slotId: Id; + trigger: (typeof ROLLBACK_TRIGGERS)[number]; +} + +export function readLegacyRatchetRollbackRequest(value: unknown): LegacyRatchetRollbackRequest { + const reader = new Reader(value, 'LegacyRatchetRollbackRequest'); + const request: LegacyRatchetRollbackRequest = { + slotId: reader.id('slotId'), + trigger: reader.enumeration('trigger', ROLLBACK_TRIGGERS), + }; + reader.finish(); + return request; +} + +export function readLegacyRatchetRollbackRequestTyped( + value: TypedValue, +): LegacyRatchetRollbackRequest { + return typedAs(value, ROLLBACK_REQUEST_SCHEMA, readLegacyRatchetRollbackRequest); +} + +// The legacy profile ------------------------------------------------------------------------ + +export interface LegacyGameboyProfile { + profileId: string; + datasetId: string; + fingerprintSchema: number; + datasetFingerprint: string; + kernelVersion: string; + plasticityVersion: string; + tickDuration: RationalNs; + warmupMs: number; + view: { viewId: Id; width: number; height: number }; + supportedStimuli: Id[]; + readoutContextSchema: SchemaRef; + decisionSchema: SchemaRef; + legacyExceptions: Id[]; +} + +function sameRational(found: RationalNs, expected: RationalNs, what: string): void { + if (found.numerator !== expected.numerator || found.denominator !== expected.denominator) { + fail(`${what} must be ${expected.numerator}/${expected.denominator}`); + } +} + +export function readLegacyGameboyProfile(value: unknown): LegacyGameboyProfile { + const reader = new Reader(value, 'LegacyGameboyProfile'); + const viewReader = (raw: unknown) => { + const v = new Reader(raw, 'LegacyGameboyProfile.view'); + const view = { + viewId: v.id('viewId'), + width: v.int('width', VIEW_WIDTH, VIEW_WIDTH), + height: v.int('height', VIEW_HEIGHT, VIEW_HEIGHT), + }; + v.finish(); + return view; + }; + const profile: LegacyGameboyProfile = { + profileId: reader.string('profileId'), + datasetId: reader.string('datasetId'), + fingerprintSchema: reader.int('fingerprintSchema', FINGERPRINT_SCHEMA, FINGERPRINT_SCHEMA), + datasetFingerprint: reader.string('datasetFingerprint'), + kernelVersion: reader.string('kernelVersion'), + plasticityVersion: reader.string('plasticityVersion'), + tickDuration: readRational(reader.value('tickDuration')), + warmupMs: reader.int('warmupMs', WARMUP_MS, WARMUP_MS), + view: viewReader(reader.value('view')), + supportedStimuli: uniqueIds(reader, 'supportedStimuli', 1, 1), + readoutContextSchema: readSchemaRef(reader.value('readoutContextSchema')), + decisionSchema: readSchemaRef(reader.value('decisionSchema')), + legacyExceptions: uniqueIds(reader, 'legacyExceptions', 1, 1), + }; + reader.finish(); + exact(profile.profileId, PROFILE_ID, 'profileId'); + exact(profile.datasetId, DATASET_ID, 'datasetId'); + exact(profile.kernelVersion, KERNEL_VERSION, 'kernelVersion'); + exact(profile.plasticityVersion, PLASTICITY_VERSION, 'plasticityVersion'); + sameRational(profile.tickDuration, TICK_DURATION, 'LegacyGameboyProfile: tickDuration'); + exact(profile.view.viewId, VIEW_ID, 'view.viewId'); + exact(profile.supportedStimuli[0], STIMULUS_REWARD_PULSE, 'supportedStimuli[0]'); + exact(profile.legacyExceptions[0], EXCEPTION_MACRO_ROLES, 'legacyExceptions[0]'); + if (profile.datasetFingerprint !== FAFB_V783_FINGERPRINT) { + fail( + "LegacyGameboyProfile: datasetFingerprint must be today's fafb-v783 schema-1 fingerprint; another fingerprint is another profile", + ); + } + sameSchema(profile.readoutContextSchema, READOUT_CONTEXT_SCHEMA, 'readoutContextSchema'); + sameSchema(profile.decisionSchema, CHANNELS_SCHEMA, 'decisionSchema'); + return profile; +} + +// The legacy composition -------------------------------------------------------------------- + +export interface LegacyGameboyComposition { + compositionId: Id; + scheduler: string; + profile: AssetRef; + executor: { + id: string; + rom: AssetRef; + adapter: Id; + symbolProvenance: string; + mode: (typeof MACRO_MODES)[number]; + macroChannels: string[]; + }; + decoderConfigDigest: Digest; + environment: { + extensions: Id[]; + slots: Id[]; + stepDuration: RationalNs; + inspectionSchema: SchemaRef; + controllerSchema: SchemaRef; + setupFrames: number; + audio: { sampleRate: number; channels: number }; + }; + episodePolicy: string; + restore: string; + checkpointFormatOfRecord: string; + flysimCompatibility: string; +} + +export function readLegacyGameboyComposition(value: unknown): LegacyGameboyComposition { + const reader = new Reader(value, 'LegacyGameboyComposition'); + const compositionId = reader.id('compositionId'); + const scheduler = reader.string('scheduler'); + const profile = readAssetRef(reader.value('profile')); + const e = new Reader(reader.value('executor'), 'LegacyGameboyComposition.executor'); + const executor = { + id: e.string('id'), + rom: readAssetRef(e.value('rom')), + adapter: e.id('adapter'), + symbolProvenance: e.boundedString('symbolProvenance', 64), + mode: e.enumeration('mode', MACRO_MODES), + macroChannels: channelList(e, 'macroChannels', MAX_MACRO_CHANNELS), + }; + e.finish(); + const decoderConfigDigest = reader.digest('decoderConfigDigest'); + const n = new Reader(reader.value('environment'), 'LegacyGameboyComposition.environment'); + const extensions = uniqueIds(n, 'extensions', 1, 1); + const slots = uniqueIds(n, 'slots', 1, MAX_SLOTS); + const stepDuration = readRational(n.value('stepDuration')); + const inspectionSchema = readSchemaRef(n.value('inspectionSchema')); + const controllerSchema = readSchemaRef(n.value('controllerSchema')); + const setupFrames = n.int('setupFrames', SETUP_FRAMES, SETUP_FRAMES); + const a = new Reader(n.value('audio'), 'environment.audio'); + const audio = { sampleRate: a.int('sampleRate', 8000, 192_000), channels: a.int('channels', 2, 2) }; + a.finish(); + n.finish(); + const composition: LegacyGameboyComposition = { + compositionId, + scheduler, + profile, + executor, + decoderConfigDigest, + environment: { + extensions, + slots, + stepDuration, + inspectionSchema, + controllerSchema, + setupFrames, + audio, + }, + episodePolicy: reader.string('episodePolicy'), + restore: reader.string('restore'), + checkpointFormatOfRecord: reader.string('checkpointFormatOfRecord'), + flysimCompatibility: reader.string('flysimCompatibility'), + }; + reader.finish(); + exact(scheduler, SCHEDULER, 'scheduler'); + exact(executor.id, EXECUTOR_ID, 'executor.id'); + exact(extensions[0], SLOTS_CAPABILITY, 'environment.extensions[0]'); + sameRational(stepDuration, STEP_DURATION, 'LegacyGameboyComposition: environment.stepDuration'); + sameSchema(inspectionSchema, MEMORY_INSPECTION_SCHEMA, 'environment.inspectionSchema'); + sameSchema(controllerSchema, JOYPAD_SCHEMA, 'environment.controllerSchema'); + exact(composition.episodePolicy, ROLLBACK_POLICY, 'episodePolicy'); + exact(composition.restore, RESTORE_SEMANTICS, 'restore'); + exact(composition.checkpointFormatOfRecord, CHECKPOINT_FORMAT_OF_RECORD, 'checkpointFormatOfRecord'); + if (profile.format !== PROFILE_FORMAT || profile.digest !== PROFILE_DIGEST) { + fail( + 'LegacyGameboyComposition: profile must name the legacy profile document (format fly-profile-v1, its digest)', + ); + } + if (executor.mode === 'raw' && executor.macroChannels.length !== 0) { + fail('LegacyGameboyComposition: raw mode deals no macro channels'); + } + if (executor.mode === 'macros' && executor.macroChannels.length === 0) { + fail('LegacyGameboyComposition: macros mode needs its macro channels'); + } + const text = composition.flysimCompatibility; + if (text.length === 0 || Buffer.byteLength(text, 'utf8') > MAX_COMPATIBILITY_BYTES) { + fail('LegacyGameboyComposition: flysimCompatibility must be 1..=1024 bytes'); + } + const segments = text.split('/'); + const agrees = + segments.length >= 6 && + segments[0] === KERNEL_VERSION && + segments[1] === executor.adapter && + segments[2] === FAFB_V783_FINGERPRINT && + segments[3] === PLASTICITY_VERSION && + segments[5] === `pokered:${executor.symbolProvenance}`; + if (!agrees) { + fail( + "LegacyGameboyComposition: flysimCompatibility's kernel, adapter, fingerprint, plasticity and pokered segments must agree with the declaration", + ); + } + return composition; +} + +/** SHA-256 of the canonical declaration: the `declaration=` line of the composition digest. */ +export function compositionDeclarationDigest(composition: LegacyGameboyComposition): Digest { + const digest = digestOf(composition); + if (!isDigest(digest)) fail('digest'); + return digest; +} diff --git a/packages/session-types/src/index.ts b/packages/session-types/src/index.ts index 72e96f9..fc496ac 100644 --- a/packages/session-types/src/index.ts +++ b/packages/session-types/src/index.ts @@ -14,6 +14,8 @@ export * from './workers'; export * from './rpc'; export * from './publishing'; export * from './trace'; +export * from './extensions'; export * as seed from './seed'; export * as checkpoint from './checkpoint'; +export * as gameboy from './gameboy'; export * as fixtures from './fixtures'; diff --git a/packages/session-types/src/workers.ts b/packages/session-types/src/workers.ts index 904c5f2..885fabc 100644 --- a/packages/session-types/src/workers.ts +++ b/packages/session-types/src/workers.ts @@ -111,6 +111,12 @@ export interface AgentTelemetry { populationRateHz: number; rates: { roleId: Id; hz: number }[]; learning: { enabled: boolean; updates: U64; changed: U64; signal: number }; + /** + * Milliseconds of stimulation pulse still running after the operation, or null for an agent + * that reports none. Amendment 2026-09-23 (RT-01a): sugar admission reads it from the last + * commit (workers-v1 section 5). + */ + stimulusRemainingMs: number | null; } export function readAssetRef(value: unknown): AssetRef { @@ -211,6 +217,10 @@ export function readAgentTelemetry(value: unknown): AgentTelemetry { const reader = new Reader(value, 'AgentTelemetry'); const brainTicks = reader.u64('brainTicks'); const populationRateHz = reader.finiteIn('populationRateHz', 0, Number.MAX_VALUE); + const stimulusRemainingMs = + reader.value('stimulusRemainingMs') === null + ? null + : reader.finiteIn('stimulusRemainingMs', 0, Number.MAX_VALUE); const rates = reader.list(('rates'), 0, MAX_RATE_ROLES, (item) => { const rate = new Reader(item, 'AgentTelemetry.rates'); const entry = { roleId: rate.id('roleId'), hz: rate.finiteIn('hz', 0, Number.MAX_VALUE) }; @@ -233,7 +243,7 @@ export function readAgentTelemetry(value: unknown): AgentTelemetry { if (u64(learning.changed) > u64(learning.updates)) { fail('AgentTelemetry: learning.changed cannot exceed learning.updates'); } - return { brainTicks, populationRateHz, rates, learning }; + return { brainTicks, populationRateHz, rates, learning, stimulusRemainingMs }; } /** Rates are in profile-defined order (workers-v1 section 1). */ @@ -848,8 +858,15 @@ export interface TaskEvent { payload: TypedValue; } +/** + * `rollback` is the amendment of 2026-09-23 (RT-01a): only a composition that declares a + * rollback policy may act on it. + */ +export const EPISODE_REQUEST_KINDS = ['terminal', 'rollback'] as const; +export type EpisodeRequestKind = (typeof EPISODE_REQUEST_KINDS)[number]; + export interface EpisodeRequest { - kind: 'terminal'; + kind: EpisodeRequestKind; reason: Id; outcome: TypedValue; } @@ -990,7 +1007,7 @@ export function readTaskEvent(value: unknown): TaskEvent { export function readEpisodeRequest(value: unknown): EpisodeRequest { const reader = new Reader(value, 'EpisodeRequest'); const request: EpisodeRequest = { - kind: reader.constant('kind', 'terminal'), + kind: reader.enumeration('kind', EPISODE_REQUEST_KINDS), reason: reader.id('reason'), outcome: readTypedValue(reader.value('outcome')), }; diff --git a/packages/session-types/tests/gameboy.test.ts b/packages/session-types/tests/gameboy.test.ts new file mode 100644 index 0000000..a38b9e4 --- /dev/null +++ b/packages/session-types/tests/gameboy.test.ts @@ -0,0 +1,146 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; + +import { canonicalize, digestOf, sha256Hex } from '../src/canonical'; +import { + readAgentRollbackParams, + readAgentRollbackResult, + readRestoreSlotParams, + validateAgentRollbackAgainstScope, + validateAgentRollbackResultAgainstScope, + validateRestoreSlotAgainstScope, +} from '../src/extensions'; +import * as fixtures from '../src/fixtures'; +import * as gameboy from '../src/gameboy'; +import { addRational, divideFloor, RATIONAL_ZERO, type Scope } from '../src/scalar'; +import { EPISODE_REQUEST_KINDS, readEpisodeRequest } from '../src/workers'; + +const legacy = () => fixtures.load('gameboy-legacy.json') as Record; + +test('every registered schema reference is the digest of its declaration, in this language', () => { + const file = legacy(); + const declared = new Map( + (file.extensionSet.payloadSchemas as Record[]).map((entry) => [ + entry.declaration.id as string, + entry.declaration, + ]), + ); + for (const schema of gameboy.PAYLOAD_SCHEMAS) { + assert.deepEqual(file.schemaRefs[schema.id], schema, `${schema.id}: the constant is the fixture`); + assert.equal(digestOf(declared.get(schema.id)), schema.digest, `${schema.id}: recomputed here`); + } + assert.equal(digestOf(file.extensionSet), file.extensionSetDigest); +}); + +test('the legacy profile document is the one profile and its digest is the constant', () => { + const file = legacy(); + const document = file.profile.document; + const profile = gameboy.readLegacyGameboyProfile(document); + assert.equal(canonicalize(profile), file.profile.canonical, 'reading keeps every field'); + assert.equal(sha256Hex(file.profile.canonical), gameboy.PROFILE_DIGEST); + assert.equal(file.profile.assetRef.digest, gameboy.PROFILE_DIGEST); + assert.equal(file.profile.assetRef.byteLength, String(file.profile.canonical.length)); + assert.equal(profile.kernelVersion, 'lif-1ms-f64-v2'); + assert.equal(profile.plasticityVersion, 'fly-kc-mbon-rstdp-v2'); + assert.equal(profile.datasetFingerprint.split(':').length, 7); +}); + +test('the frame clock is exact and matches the legacy f64 accumulator', () => { + const legacyMsPerFrame = 1000 / (4_194_304 / 70_224); + assert.equal(legacyMsPerFrame, 548_625 / 32_768, 'the legacy constant is dyadic, so exact'); + const frames = legacy().clock.frames as { ticks: string; remainder: unknown }[]; + let exact = RATIONAL_ZERO; + let float = 0; + for (let frame = 0; frame < 20_000; frame += 1) { + exact = addRational(exact, gameboy.STEP_DURATION); + const { ticks, remainder } = divideFloor(exact, gameboy.TICK_DURATION); + exact = remainder; + float += legacyMsPerFrame; + const steps = Math.floor(float); + float -= steps; + assert.equal(ticks, String(steps), `frame ${frame}: tick counts agree`); + // The f64 remainder is a multiple of 2^-15 ms; compare it as an exact fraction of ns. + const scaled = float * 32_768; + assert.equal(scaled, Math.floor(scaled), `frame ${frame}: dyadic`); + assert.equal( + BigInt(remainder.numerator) * 32_768n, + BigInt(scaled) * 1_000_000n * BigInt(remainder.denominator), + `frame ${frame}: remainders agree`, + ); + if (frame < frames.length) { + assert.equal(frames[frame]!.ticks, ticks); + assert.deepEqual(frames[frame]!.remainder, remainder); + } + } +}); + +test('the example composition digest is the recorded one, and the decoder moves it', () => { + const file = legacy(); + const composition = gameboy.readLegacyGameboyComposition(file.composition.example); + assert.equal(gameboy.compositionDeclarationDigest(composition), file.composition.digest); + const other = structuredClone(composition); + other.decoderConfigDigest = sha256Hex('another decoder'); + assert.notEqual(gameboy.compositionDeclarationDigest(other), file.composition.digest); + assert.equal(other.flysimCompatibility, composition.flysimCompatibility); +}); + +test('bound is an ordered subset, and the macro winner is always bound', () => { + const channels = ['macro_go_objective', 'macro_talk', 'macro_next']; + const context = gameboy.readGameboyReadoutContext({ + boot: false, + bound: ['macro_go_objective', 'macro_next'], + location: null, + }); + gameboy.validateReadoutContextAgainst(context, channels); + assert.throws(() => + gameboy.validateReadoutContextAgainst({ ...context, bound: ['macro_next', 'macro_go_objective'] }, channels), + ); + const decision = gameboy.readGameboyChannelsDecision({ + buttons: gameboy.GAMEBOY_BUTTONS.map((id) => ({ id, down: id === 'up' || id === 'a' })), + macro: 'macro_next', + }); + assert.equal(gameboy.channelsMask(decision), 0x11); + gameboy.validateDecisionAgainst(decision, context); + assert.throws(() => gameboy.validateDecisionAgainst({ ...decision, macro: 'macro_talk' }, context)); +}); + +test('typed values are read only under their registered schema', () => { + const value = { boot: true, bound: [], location: null }; + gameboy.readGameboyReadoutContextTyped({ schema: gameboy.READOUT_CONTEXT_SCHEMA, value }); + assert.throws(() => gameboy.readGameboyReadoutContextTyped({ schema: gameboy.CHANNELS_SCHEMA, value })); + assert.throws(() => + gameboy.readGameboyReadoutContextTyped({ + schema: { ...gameboy.READOUT_CONTEXT_SCHEMA, digest: sha256Hex('x') }, + value, + }), + ); +}); + +test('a rollback request and the extension methods check what they must', () => { + assert.deepEqual([...EPISODE_REQUEST_KINDS], ['terminal', 'rollback']); + const request = readEpisodeRequest({ + kind: 'rollback', + reason: 'stall', + outcome: { schema: gameboy.ROLLBACK_REQUEST_SCHEMA, value: { slotId: 'best', trigger: 'stall' } }, + }); + assert.equal(gameboy.readLegacyRatchetRollbackRequestTyped(request.outcome).slotId, 'best'); + + const newEpoch: Scope = { sessionId: 'live', epoch: 'epoch-8', step: '4101' }; + const sameEpoch: Scope = { sessionId: 'live', epoch: 'epoch-7', step: '4101' }; + const restore = readRestoreSlotParams({ + slotId: 'best', + priorEpoch: 'epoch-7', + policy: 'legacy-ratchet-rollback-v1', + }); + validateRestoreSlotAgainstScope(restore, newEpoch); + assert.throws(() => validateRestoreSlotAgainstScope(restore, sameEpoch)); + + const cases = fixtures.cases(fixtures.load('valid.json')) as Record[]; + const find = (name: string) => cases.find((item) => item.name === name)!.value; + const rollback = readAgentRollbackParams(find('agent rollback params')); + validateAgentRollbackAgainstScope(rollback, newEpoch); + assert.throws(() => validateAgentRollbackAgainstScope(rollback, { ...newEpoch, step: '4102' })); + const result = readAgentRollbackResult(find('agent rollback result')); + validateAgentRollbackResultAgainstScope(result, newEpoch); + assert.throws(() => validateAgentRollbackResultAgainstScope(result, { ...newEpoch, step: '4100' })); +}); diff --git a/packages/session-types/tests/readers.ts b/packages/session-types/tests/readers.ts index e3dba75..890ecf4 100644 --- a/packages/session-types/tests/readers.ts +++ b/packages/session-types/tests/readers.ts @@ -17,6 +17,22 @@ import { readViewDescriptor, readViewRef, } from '../src/media'; +import { + readAgentRollbackParams, + readAgentRollbackResult, + readRestoreSlotParams, + readRestoreSlotResult, + readSaveSlotParams, + readSaveSlotResult, +} from '../src/extensions'; +import { + readGameboyChannelsDecision, + readGameboyMemoryInspection, + readGameboyReadoutContext, + readLegacyGameboyComposition, + readLegacyGameboyProfile, + readLegacyRatchetRollbackRequest, +} from '../src/gameboy'; import { readCommittedSnapshot, readSessionDescriptor } from '../src/publishing'; import { readSessionRpcFailure, @@ -112,6 +128,18 @@ export const READERS: Record unknown> = { TraceBehaviour: readTraceBehaviour, TraceOperational: readTraceOperational, TransitionTrace: readTransitionTrace, + SaveSlotParams: readSaveSlotParams, + SaveSlotResult: readSaveSlotResult, + RestoreSlotParams: readRestoreSlotParams, + RestoreSlotResult: readRestoreSlotResult, + AgentRollbackParams: readAgentRollbackParams, + AgentRollbackResult: readAgentRollbackResult, + GameboyReadoutContext: readGameboyReadoutContext, + GameboyChannelsDecision: readGameboyChannelsDecision, + GameboyMemoryInspection: readGameboyMemoryInspection, + LegacyRatchetRollbackRequest: readLegacyRatchetRollbackRequest, + LegacyGameboyProfile: readLegacyGameboyProfile, + LegacyGameboyComposition: readLegacyGameboyComposition, }; /** Reads the value as `typeName` and hands back what the reader reconstructed. */ diff --git a/packages/session-types/tests/schema.test.ts b/packages/session-types/tests/schema.test.ts index 4579056..6f16f10 100644 --- a/packages/session-types/tests/schema.test.ts +++ b/packages/session-types/tests/schema.test.ts @@ -76,6 +76,8 @@ test('the schema set publishes the limits this package enforces', async () => { assert.equal(limits.get('maxAudioStreams'), media.MAX_AUDIO_STREAMS); assert.equal(limits.get('maxTypedValueBytes'), scalar.MAX_TYPED_VALUE_BYTES); assert.equal(limits.get('maxEnvelopeBytes'), canonical.MAX_ENVELOPE_BYTES); + const extensions = await import('../src/extensions'); + assert.equal(limits.get('maxSlots'), extensions.MAX_SLOTS); }); test('the closed enums this package knows are the ones the schema set declares', async () => { @@ -97,4 +99,5 @@ test('the closed enums this package knows are the ones the schema set declares', assert.deepEqual(enums.get('Recovery'), [...workers.RECOVERY]); assert.deepEqual(enums.get('Determinism'), [...workers.DETERMINISM]); assert.deepEqual(enums.get('AxisRange'), [...workers.AXIS_RANGES]); + assert.deepEqual(enums.get('EpisodeRequestKind'), [...workers.EPISODE_REQUEST_KINDS]); }); diff --git a/services/flysim/Cargo.lock b/services/flysim/Cargo.lock index bb2bbe5..7399620 100644 --- a/services/flysim/Cargo.lock +++ b/services/flysim/Cargo.lock @@ -481,6 +481,7 @@ dependencies = [ "anyhow", "axum", "clap", + "fly-session-types", "flybrain-core", "flybrain-gb", "futures-util", diff --git a/services/flysim/crates/fly-session-types/README.md b/services/flysim/crates/fly-session-types/README.md index 72ff894..8bc46df 100644 --- a/services/flysim/crates/fly-session-types/README.md +++ b/services/flysim/crates/fly-session-types/README.md @@ -23,6 +23,8 @@ file other than its own fixtures. The bus owns the wire | `schema` | The canonical schema set and `contract_digest()` | | `seed` | `seed-derivation-v1` | | `checkpoint` | The `FLYSESS1` envelope layout | +| `extensions` | The 2026-09-23 extension methods: `Environment.SaveSlot`/`RestoreSlot` (`gameboy-slots-v1`) and `Agent.Rollback` (`legacy-ratchet-rollback-v1`) | +| `gameboy` | The legacy Game Boy composition ([`legacy-gameboy-v1`](../../../../docs/design/session-framework/legacy-gameboy-v1.md)): registered payload schemas, the legacy profile, the composition declaration. Not part of `contractDigest` | | `fixtures` | Loading `fixtures/`, shared with `packages/session-types` | `Id`, `U64` and `Digest` are the bus encodings: `scalar` calls into `flybus::wire` instead of @@ -80,10 +82,11 @@ once and holds both languages to it. | `schema-set.json`, `contract-digest.json` | The canonical schema set and its digest | | `seed-vectors.json` | `seed-derivation-v1` test vectors | | `checkpoint-envelope.json` | One `FLYSESS1` envelope, its layout and the corruptions a reader refuses | +| `gameboy-legacy.json` | The legacy Game Boy extension set and digest, every registered `SchemaRef`, the legacy profile and its `AssetRef`, the frame clock, an example composition and its digest | The derived files (`schema-set.json`, `contract-digest.json`, the `canonical`/`digest` fields -of `valid.json`, the digests in `operations.json`, `seed-vectors.json` and -`checkpoint-envelope.json`) come from +of `valid.json`, the digests in `operations.json`, `seed-vectors.json`, +`checkpoint-envelope.json` and `gameboy-legacy.json`) come from `cargo run -p fly-session-types --example update_fixtures`; `tests/schema_set.rs` fails if the checked-in files are stale. @@ -96,8 +99,8 @@ cargo clippy -p fly-session-types --all-targets ## Bounds this crate chose -Every bound in the schema set names its source. Six are marked `crate` because no document +Every bound in the schema set names its source. Seven are marked `crate` because no document states them: `maxAudioStreams` (8), `maxCapabilities` (32), `maxSupportedMajors` (8), -`maxSupportedStimuli` (64), `maxAssets` (64) and `maxSnapshotEvents` (64). They exist so an +`maxSupportedStimuli` (64), `maxAssets` (64), `maxSnapshotEvents` (64) and `maxSlots` (4). They exist so an unbounded array cannot fill an envelope, and they are in the digest, so widening one is a contract change rather than a quiet edit. diff --git a/services/flysim/crates/fly-session-types/examples/update_fixtures.rs b/services/flysim/crates/fly-session-types/examples/update_fixtures.rs index bad3693..153b4cc 100644 --- a/services/flysim/crates/fly-session-types/examples/update_fixtures.rs +++ b/services/flysim/crates/fly-session-types/examples/update_fixtures.rs @@ -6,7 +6,9 @@ use std::collections::BTreeMap; -use fly_session_types::scalar::{DomainType, Scope}; +use fly_session_types::gameboy::{self, LegacyComposition}; +use fly_session_types::scalar::{DomainType, RationalNs, Scope}; +use fly_session_types::workers::AssetRef; use fly_session_types::{canonical, checkpoint, fixtures, schema, seed}; use serde_json::{Map, Value, json}; @@ -28,9 +30,100 @@ pub fn derived() -> Vec<(String, String)> { ("operations.json".to_owned(), operations()), ("seed-vectors.json".to_owned(), seed_vectors()), ("checkpoint-envelope.json".to_owned(), checkpoint_envelope()), + ("gameboy-legacy.json".to_owned(), gameboy_legacy()), ] } +/// An example legacy composition. The ROM and decoder digests are placeholders -- the real +/// ones are computed by the composition that runs, and no ROM identity belongs in a fixture -- +/// and the macro channels are a short excerpt of the Pokemon Red set. The compatibility +/// string is today's, byte for byte, because its segments must agree with the declaration. +pub fn example_composition() -> LegacyComposition { + let pokered = "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b"; + LegacyComposition { + composition_id: "pokered-live".to_owned(), + profile: gameboy::profile_asset_ref(), + executor: gameboy::ExecutorDeclaration { + rom: AssetRef { + id: "pokered-rom".to_owned(), + digest: canonical::sha256_hex(b"placeholder: the cartridge digest is the operator's"), + byte_length: 1_048_576, + format: "gb-rom".to_owned(), + }, + adapter: "pokered-unique8-v6".to_owned(), + symbol_provenance: pokered.to_owned(), + mode: "macros".to_owned(), + macro_channels: ["macro_go_objective", "macro_talk", "macro_next", "macro_move_1"] + .iter() + .map(|c| (*c).to_owned()) + .collect(), + }, + decoder_config_digest: canonical::sha256_hex(b"placeholder: the effective DecoderConfig"), + environment: gameboy::EnvironmentDeclaration { + slots: vec!["best".to_owned()], + audio_sample_rate: 48_000, + }, + flysim_compatibility: format!( + "{}/pokered-unique8-v6/{}/{}/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:{pokered}/statefmt:199616-x86_64-unknown-linux-gnu", + gameboy::KERNEL_VERSION, + gameboy::FAFB_V783_FINGERPRINT, + gameboy::PLASTICITY_VERSION, + ), + } +} + +/// The legacy Game Boy extension set, the profile document and its AssetRef, the clock +/// vector and an example composition with its digest. +fn gameboy_legacy() -> String { + let profile = gameboy::legacy_profile().to_json(); + let schema_refs: Map = gameboy::PAYLOAD_SCHEMAS + .iter() + .map(|p| (p.id.to_owned(), p.schema_ref().to_json())) + .collect(); + // The first frames from a zero remainder: the rational accumulator of step-v1 section 5, + // which the legacy f64 accumulator equals exactly (legacy-gameboy-v1 section 3). + let step = gameboy::step_duration(); + let tick = gameboy::tick_duration(); + let mut accumulator = RationalNs::ZERO; + let mut frames = Vec::new(); + for _ in 0..12 { + accumulator = accumulator.checked_add(&step).expect("no overflow"); + let (ticks, remainder) = accumulator.divide_floor(&tick).expect("positive tick"); + accumulator = remainder; + frames.push(json!({"ticks": ticks.to_string(), "remainder": remainder.to_json()})); + } + let composition = example_composition(); + write(&json!({ + "description": "The legacy Game Boy composition (legacy-gameboy-v1): registered payload schemas with their SchemaRef digests, the one legacy profile document and its AssetRef, the frame clock, and an example composition declaration with its digest.", + "extensionSetDigest": gameboy::extension_set_digest(), + "extensionSet": gameboy::extension_set(), + "schemaRefs": schema_refs, + "profile": { + "document": profile, + "canonical": canonical::canonicalize(&profile).expect("canonicalizable"), + "assetRef": gameboy::profile_asset_ref().to_json(), + }, + "clock": { + "stepDuration": step.to_json(), + "tickDuration": tick.to_json(), + "legacyMsPerFrame": "1000 / (4194304 / 70224) == 548625/32768 exactly", + "frames": frames, + }, + "composition": { + "example": composition.to_json(), + "digest": composition.digest().expect("digest"), + "recipeLines": [ + "fly-session/composition-v1", + "session=", + "epoch=", + "contract=", + "agent= port= profile= (one line per agent)", + "declaration= (added by the 2026-09-23 amendment)", + ], + }, + })) +} + fn write(value: &Value) -> String { let mut text = serde_json::to_string_pretty(value).expect("serializable"); text.push('\n'); diff --git a/services/flysim/crates/fly-session-types/fixtures/contract-digest.json b/services/flysim/crates/fly-session-types/fixtures/contract-digest.json index 90e4419..c81a576 100644 --- a/services/flysim/crates/fly-session-types/fixtures/contract-digest.json +++ b/services/flysim/crates/fly-session-types/fixtures/contract-digest.json @@ -1,9 +1,9 @@ { "description": "contractDigest is the SHA-256 of the canonical schema set in schema-set.json.", - "contractDigest": "7f4b11d6737e5097c6889657479527174ddf496e50b60322b281e6c7490bcb4a", + "contractDigest": "f61e8f4fa9336184ebb6f0305f9d6135872707ef5536d51b9933df8943653c77", "schemaSetVersion": 1, - "schemaSetBytes": 27470, - "types": 54, + "schemaSetBytes": 30184, + "types": 60, "enums": 11, - "limits": 26 + "limits": 27 } diff --git a/services/flysim/crates/fly-session-types/fixtures/descriptor-checks.json b/services/flysim/crates/fly-session-types/fixtures/descriptor-checks.json index c536619..93715d5 100644 --- a/services/flysim/crates/fly-session-types/fixtures/descriptor-checks.json +++ b/services/flysim/crates/fly-session-types/fixtures/descriptor-checks.json @@ -1385,6 +1385,7 @@ "telemetry": { "brainTicks": "2534", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -1516,6 +1517,7 @@ "telemetry": { "brainTicks": "2534", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -1647,6 +1649,7 @@ "telemetry": { "brainTicks": "2534", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -1778,6 +1781,7 @@ "telemetry": { "brainTicks": "2534", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "mbon", @@ -1909,6 +1913,7 @@ "telemetry": { "brainTicks": "2534", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", diff --git a/services/flysim/crates/fly-session-types/fixtures/gameboy-legacy.json b/services/flysim/crates/fly-session-types/fixtures/gameboy-legacy.json new file mode 100644 index 0000000..7b16fbb --- /dev/null +++ b/services/flysim/crates/fly-session-types/fixtures/gameboy-legacy.json @@ -0,0 +1,548 @@ +{ + "description": "The legacy Game Boy composition (legacy-gameboy-v1): registered payload schemas with their SchemaRef digests, the one legacy profile document and its AssetRef, the frame clock, and an example composition declaration with its digest.", + "extensionSetDigest": "7bb9578d89b0a35a2914e699aad86d35ac8859d9ad4367632bd1fdfbd3e942ef", + "extensionSet": { + "contract": "fly-session-types/legacy-gameboy", + "version": 1, + "scalars": { + "ChannelName": "^[a-z][a-z0-9_]{0,63}$: a decoder channel or rate-role name; not an Id, because the legacy role names carry '_'" + }, + "enums": { + "MacroMode": [ + "raw", + "macros" + ], + "RollbackTrigger": [ + "stall", + "game-over" + ] + }, + "payloadSchemas": [ + { + "declaration": { + "registry": "fly-session-payload-schema-v1", + "id": "gameboy-channels-v1", + "version": 1, + "source": "legacy-gameboy-v1 6", + "fields": [ + { + "name": "buttons", + "kind": "array<{id:Id,down:bool}>", + "required": true, + "constraint": "exactly up,down,left,right,a,b,start,select in that order" + }, + { + "name": "macro", + "kind": "ChannelName|null", + "required": false, + "constraint": "the macro-group channel active in this decode; one of the context's bound" + } + ] + }, + "schemaRef": { + "id": "gameboy-channels-v1", + "version": 1, + "digest": "28bd89bfa41ec73fb2785959a64d9975b4a932e10cd9e51dc4bc29020c823595" + } + }, + { + "declaration": { + "registry": "fly-session-payload-schema-v1", + "id": "gameboy-joypad-v1", + "version": 1, + "source": "legacy-gameboy-v1 7", + "fields": [ + { + "name": "buttons", + "kind": "const", + "required": true, + "constraint": "up,down,left,right,a,b,start,select; no axes; one port" + } + ] + }, + "schemaRef": { + "id": "gameboy-joypad-v1", + "version": 1, + "digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e" + } + }, + { + "declaration": { + "registry": "fly-session-payload-schema-v1", + "id": "gameboy-memory-inspection-v1", + "version": 1, + "source": "legacy-gameboy-v1 8", + "fields": [ + { + "name": "memory", + "kind": "ArtifactRef", + "required": true, + "constraint": "listed attachment; byteLength 65536; the CPU address space $0000..=$FFFF at this boundary, read-only" + }, + { + "name": "romDigest", + "kind": "Digest", + "required": true, + "constraint": "== EnvironmentDescriptor.contentDigest == the executor's rom AssetRef digest" + } + ] + }, + "schemaRef": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + } + }, + { + "declaration": { + "registry": "fly-session-payload-schema-v1", + "id": "gameboy-readout-context-v1", + "version": 1, + "source": "legacy-gameboy-v1 5", + "fields": [ + { + "name": "boot", + "kind": "bool", + "required": true, + "constraint": "the adapter's boot gate after the last transition: permissive Start/Select variant" + }, + { + "name": "bound", + "kind": "array", + "required": true, + "constraint": "<= 64, unique, a subset of the composition's macroChannels in their order; [] in raw mode" + }, + { + "name": "location", + "kind": "{area:int,x:int,y:int}|null", + "required": false, + "constraint": "each 0..=4294967295; the adapter's location after the last transition; null is no information" + } + ] + }, + "schemaRef": { + "id": "gameboy-readout-context-v1", + "version": 1, + "digest": "78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d" + } + }, + { + "declaration": { + "registry": "fly-session-payload-schema-v1", + "id": "legacy-ratchet-rollback-v1", + "version": 1, + "source": "legacy-gameboy-v1 11", + "fields": [ + { + "name": "slotId", + "kind": "Id", + "required": true, + "constraint": "one of the composition's declared slots, saved earlier" + }, + { + "name": "trigger", + "kind": "RollbackTrigger", + "required": true, + "constraint": "stall or game-over" + } + ] + }, + "schemaRef": { + "id": "legacy-ratchet-rollback-v1", + "version": 1, + "digest": "0610f899a4746e5991a07dbc3c847ada15d7c68cedf0664244a3c1d87c175c70" + } + } + ], + "declarations": [ + { + "name": "LegacyGameboyComposition", + "source": "legacy-gameboy-v1 12", + "fields": [ + { + "name": "compositionId", + "kind": "Id", + "required": true, + "constraint": "" + }, + { + "name": "scheduler", + "kind": "const", + "required": true, + "constraint": "\"lockstep-v1\"" + }, + { + "name": "profile", + "kind": "AssetRef", + "required": true, + "constraint": "format fly-profile-v1; digest == the legacy profile's" + }, + { + "name": "executor", + "kind": "{id:const,rom:AssetRef,adapter:Id,symbolProvenance:string,mode:MacroMode,macroChannels:array}", + "required": true, + "constraint": "pokered-macros-v1; task and executor one object; macroChannels [] iff mode raw, else <= 64 unique in decoder order" + }, + { + "name": "decoderConfigDigest", + "kind": "Digest", + "required": true, + "constraint": "SHA-256 of the canonical JSON of the effective DecoderConfig (TypeScript shape)" + }, + { + "name": "environment", + "kind": "{extensions:array,slots:array,stepDuration:RationalNs,inspectionSchema:SchemaRef,controllerSchema:SchemaRef,setupFrames:const,audio:{sampleRate:int,channels:const}}", + "required": true, + "constraint": "[\"gameboy-slots-v1\"]; 1..=4 unique slots; 8572265625/512; memory inspection; joypad; 1 setup frame; 2 channels" + }, + { + "name": "episodePolicy", + "kind": "const", + "required": true, + "constraint": "\"legacy-ratchet-rollback-v1\"" + }, + { + "name": "restore", + "kind": "const", + "required": true, + "constraint": "\"legacy-transient-reset\"" + }, + { + "name": "checkpointFormatOfRecord", + "kind": "const", + "required": true, + "constraint": "\"FLYSIM01\"" + }, + { + "name": "flysimCompatibility", + "kind": "string", + "required": true, + "constraint": "<= 1024 bytes; the FLYSIM01 string; its kernel, adapter, fingerprint, plasticity and pokered segments agree with this declaration" + } + ] + }, + { + "name": "LegacyGameboyProfile", + "source": "legacy-gameboy-v1 2", + "fields": [ + { + "name": "profileId", + "kind": "const", + "required": true, + "constraint": "\"gameboy-legacy-fafb-v783-v1\"" + }, + { + "name": "datasetId", + "kind": "const", + "required": true, + "constraint": "\"fafb-v783\"" + }, + { + "name": "fingerprintSchema", + "kind": "const", + "required": true, + "constraint": "1" + }, + { + "name": "datasetFingerprint", + "kind": "string", + "required": true, + "constraint": "today's schema-1 fingerprint, seven digests joined with ':'" + }, + { + "name": "kernelVersion", + "kind": "const", + "required": true, + "constraint": "\"lif-1ms-f64-v2\"" + }, + { + "name": "plasticityVersion", + "kind": "const", + "required": true, + "constraint": "\"fly-kc-mbon-rstdp-v2\"" + }, + { + "name": "tickDuration", + "kind": "RationalNs", + "required": true, + "constraint": "1000000/1" + }, + { + "name": "warmupMs", + "kind": "const", + "required": true, + "constraint": "2500" + }, + { + "name": "view", + "kind": "{viewId:Id,width:int,height:int}", + "required": true, + "constraint": "lcd, 160, 144" + }, + { + "name": "supportedStimuli", + "kind": "array", + "required": true, + "constraint": "[\"reward-pulse\"]" + }, + { + "name": "readoutContextSchema", + "kind": "SchemaRef", + "required": true, + "constraint": "gameboy-readout-context-v1" + }, + { + "name": "decisionSchema", + "kind": "SchemaRef", + "required": true, + "constraint": "gameboy-channels-v1" + }, + { + "name": "legacyExceptions", + "kind": "array", + "required": true, + "constraint": "[\"macro-roles-outside-fingerprint\"]" + } + ] + } + ] + }, + "schemaRefs": { + "gameboy-readout-context-v1": { + "id": "gameboy-readout-context-v1", + "version": 1, + "digest": "78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d" + }, + "gameboy-channels-v1": { + "id": "gameboy-channels-v1", + "version": 1, + "digest": "28bd89bfa41ec73fb2785959a64d9975b4a932e10cd9e51dc4bc29020c823595" + }, + "gameboy-joypad-v1": { + "id": "gameboy-joypad-v1", + "version": 1, + "digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e" + }, + "gameboy-memory-inspection-v1": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "legacy-ratchet-rollback-v1": { + "id": "legacy-ratchet-rollback-v1", + "version": 1, + "digest": "0610f899a4746e5991a07dbc3c847ada15d7c68cedf0664244a3c1d87c175c70" + } + }, + "profile": { + "document": { + "profileId": "gameboy-legacy-fafb-v783-v1", + "datasetId": "fafb-v783", + "fingerprintSchema": 1, + "datasetFingerprint": "75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc", + "kernelVersion": "lif-1ms-f64-v2", + "plasticityVersion": "fly-kc-mbon-rstdp-v2", + "tickDuration": { + "numerator": "1000000", + "denominator": "1" + }, + "warmupMs": 2500, + "view": { + "viewId": "lcd", + "width": 160, + "height": 144 + }, + "supportedStimuli": [ + "reward-pulse" + ], + "readoutContextSchema": { + "id": "gameboy-readout-context-v1", + "version": 1, + "digest": "78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d" + }, + "decisionSchema": { + "id": "gameboy-channels-v1", + "version": 1, + "digest": "28bd89bfa41ec73fb2785959a64d9975b4a932e10cd9e51dc4bc29020c823595" + }, + "legacyExceptions": [ + "macro-roles-outside-fingerprint" + ] + }, + "canonical": "{\"datasetFingerprint\":\"75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc\",\"datasetId\":\"fafb-v783\",\"decisionSchema\":{\"digest\":\"28bd89bfa41ec73fb2785959a64d9975b4a932e10cd9e51dc4bc29020c823595\",\"id\":\"gameboy-channels-v1\",\"version\":1},\"fingerprintSchema\":1,\"kernelVersion\":\"lif-1ms-f64-v2\",\"legacyExceptions\":[\"macro-roles-outside-fingerprint\"],\"plasticityVersion\":\"fly-kc-mbon-rstdp-v2\",\"profileId\":\"gameboy-legacy-fafb-v783-v1\",\"readoutContextSchema\":{\"digest\":\"78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d\",\"id\":\"gameboy-readout-context-v1\",\"version\":1},\"supportedStimuli\":[\"reward-pulse\"],\"tickDuration\":{\"denominator\":\"1\",\"numerator\":\"1000000\"},\"view\":{\"height\":144,\"viewId\":\"lcd\",\"width\":160},\"warmupMs\":2500}", + "assetRef": { + "id": "gameboy-legacy-fafb-v783-v1", + "digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878", + "byteLength": "1137", + "format": "fly-profile-v1" + } + }, + "clock": { + "stepDuration": { + "numerator": "8572265625", + "denominator": "512" + }, + "tickDuration": { + "numerator": "1000000", + "denominator": "1" + }, + "legacyMsPerFrame": "1000 / (4194304 / 70224) == 548625/32768 exactly", + "frames": [ + { + "ticks": "16", + "remainder": { + "numerator": "380265625", + "denominator": "512" + } + }, + { + "ticks": "17", + "remainder": { + "numerator": "124265625", + "denominator": "256" + } + }, + { + "ticks": "17", + "remainder": { + "numerator": "116796875", + "denominator": "512" + } + }, + { + "ticks": "16", + "remainder": { + "numerator": "124265625", + "denominator": "128" + } + }, + { + "ticks": "17", + "remainder": { + "numerator": "365328125", + "denominator": "512" + } + }, + { + "ticks": "17", + "remainder": { + "numerator": "116796875", + "denominator": "256" + } + }, + { + "ticks": "17", + "remainder": { + "numerator": "101859375", + "denominator": "512" + } + }, + { + "ticks": "16", + "remainder": { + "numerator": "60265625", + "denominator": "64" + } + }, + { + "ticks": "17", + "remainder": { + "numerator": "350390625", + "denominator": "512" + } + }, + { + "ticks": "17", + "remainder": { + "numerator": "109328125", + "denominator": "256" + } + }, + { + "ticks": "17", + "remainder": { + "numerator": "86921875", + "denominator": "512" + } + }, + { + "ticks": "16", + "remainder": { + "numerator": "116796875", + "denominator": "128" + } + } + ] + }, + "composition": { + "example": { + "compositionId": "pokered-live", + "scheduler": "lockstep-v1", + "profile": { + "id": "gameboy-legacy-fafb-v783-v1", + "digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878", + "byteLength": "1137", + "format": "fly-profile-v1" + }, + "executor": { + "id": "pokered-macros-v1", + "rom": { + "id": "pokered-rom", + "digest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20", + "byteLength": "1048576", + "format": "gb-rom" + }, + "adapter": "pokered-unique8-v6", + "symbolProvenance": "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b", + "mode": "macros", + "macroChannels": [ + "macro_go_objective", + "macro_talk", + "macro_next", + "macro_move_1" + ] + }, + "decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854", + "environment": { + "extensions": [ + "gameboy-slots-v1" + ], + "slots": [ + "best" + ], + "stepDuration": { + "numerator": "8572265625", + "denominator": "512" + }, + "inspectionSchema": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "controllerSchema": { + "id": "gameboy-joypad-v1", + "version": 1, + "digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e" + }, + "setupFrames": 1, + "audio": { + "sampleRate": 48000, + "channels": 2 + } + }, + "episodePolicy": "legacy-ratchet-rollback-v1", + "restore": "legacy-transient-reset", + "checkpointFormatOfRecord": "FLYSIM01", + "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" + }, + "digest": "f0142c7f09a1319453b472cdddbc1855062af5733f89d1fbc0b82c0adb52b0c7", + "recipeLines": [ + "fly-session/composition-v1", + "session=", + "epoch=", + "contract=", + "agent= port= profile= (one line per agent)", + "declaration= (added by the 2026-09-23 amendment)" + ] + } +} diff --git a/services/flysim/crates/fly-session-types/fixtures/invalid.json b/services/flysim/crates/fly-session-types/fixtures/invalid.json index b43edec..c7456fd 100644 --- a/services/flysim/crates/fly-session-types/fixtures/invalid.json +++ b/services/flysim/crates/fly-session-types/fixtures/invalid.json @@ -227,6 +227,7 @@ "value": { "brainTicks": "17", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -248,6 +249,7 @@ "value": { "brainTicks": "17", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -273,6 +275,7 @@ "value": { "brainTicks": "17", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "role-0", @@ -550,6 +553,7 @@ "value": { "brainTicks": "17", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -2730,6 +2734,7 @@ "telemetry": { "brainTicks": "2500", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -2779,6 +2784,7 @@ "telemetry": { "brainTicks": "2500", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -2828,6 +2834,7 @@ "telemetry": { "brainTicks": "2500", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -2864,6 +2871,7 @@ "telemetry": { "brainTicks": "2500", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -2913,6 +2921,7 @@ "telemetry": { "brainTicks": "2500", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -2962,6 +2971,7 @@ "telemetry": { "brainTicks": "2500", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -3012,6 +3022,7 @@ "telemetry": { "brainTicks": "2500", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -4051,6 +4062,7 @@ "telemetry": { "brainTicks": "2500", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -4122,6 +4134,7 @@ "telemetry": { "brainTicks": "2534", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -4193,6 +4206,7 @@ "telemetry": { "brainTicks": "2534", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -4273,6 +4287,7 @@ "telemetry": { "brainTicks": "2534", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -4394,6 +4409,1356 @@ } }, "reason": "a commit acknowledges the transition's next boundary" + }, + { + "name": "telemetry with a negative stimulation remainder", + "type": "AgentTelemetry", + "value": { + "brainTicks": "70002", + "populationRateHz": 12.5, + "stimulusRemainingMs": -1.0, + "rates": [ + { + "roleId": "kenyon", + "hz": 3.25 + } + ], + "learning": { + "enabled": true, + "updates": "4", + "changed": "2", + "signal": 0.0 + } + }, + "reason": "stimulusRemainingMs is finite and nonnegative" + }, + { + "name": "telemetry without stimulusRemainingMs", + "type": "AgentTelemetry", + "value": { + "brainTicks": "70002", + "populationRateHz": 12.5, + "rates": [ + { + "roleId": "kenyon", + "hz": 3.25 + } + ], + "learning": { + "enabled": true, + "updates": "4", + "changed": "2", + "signal": 0.0 + } + }, + "reason": "the field is required; null says the agent reports none" + }, + { + "name": "episode request of an unknown kind", + "type": "EpisodeRequest", + "value": { + "kind": "restart", + "reason": "stall", + "outcome": { + "schema": { + "id": "legacy-ratchet-rollback-v1", + "version": 1, + "digest": "0610f899a4746e5991a07dbc3c847ada15d7c68cedf0664244a3c1d87c175c70" + }, + "value": { + "slotId": "best", + "trigger": "stall" + } + } + }, + "reason": "EpisodeRequestKind is closed: terminal or rollback" + }, + { + "name": "save slot params with a bad slot id", + "type": "SaveSlotParams", + "value": { + "slotId": "Best" + }, + "reason": "slotId is an Id" + }, + { + "name": "save slot result with no bytes", + "type": "SaveSlotResult", + "value": { + "slotId": "best", + "boundary": "4101", + "stateDigest": "5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d", + "byteLength": "0" + }, + "reason": "byteLength is positive" + }, + { + "name": "restore slot params naming another policy", + "type": "RestoreSlotParams", + "value": { + "slotId": "best", + "priorEpoch": "epoch-7", + "policy": "round-reset-v1" + }, + "reason": "legacy-ratchet-rollback-v1 is the only rollback policy defined" + }, + { + "name": "restore slot result carrying an audio chunk", + "type": "RestoreSlotResult", + "value": { + "slotId": "best", + "committedStep": "4101", + "observation": { + "boundary": "4101", + "worldTime": { + "numerator": "35154861328125", + "denominator": "512" + }, + "engineFrame": "4102", + "sensoryViews": [ + { + "viewId": "lcd", + "producedStep": "4101", + "pixels": { + "storeId": "store-1", + "artifactId": "slot-frame-1", + "generation": "1", + "byteLength": "92160", + "contentType": "image/x-rgba8", + "digest": null + } + } + ], + "inspection": { + "schema": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "value": { + "memory": { + "storeId": "store-1", + "artifactId": "wram-4101", + "generation": "1", + "byteLength": "65536", + "contentType": "application/octet-stream", + "digest": null + }, + "romDigest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20" + } + }, + "broadcastViews": [ + { + "viewId": "lcd", + "producedStep": "4101", + "pixels": { + "storeId": "store-1", + "artifactId": "slot-frame-1", + "generation": "1", + "byteLength": "92160", + "contentType": "image/x-rgba8", + "digest": null + } + } + ], + "audio": [ + { + "streamId": "mix", + "firstSample": "800", + "sampleFrames": 800, + "samples": { + "storeId": "store-1", + "artifactId": "audio-1", + "generation": "1", + "byteLength": "6400", + "contentType": "audio/x-f32le", + "digest": null + }, + "discontinuity": true + } + ] + } + }, + "reason": "a restored slot ran no transition" + }, + { + "name": "restore slot result at another boundary", + "type": "RestoreSlotResult", + "value": { + "slotId": "best", + "committedStep": "4102", + "observation": { + "boundary": "4101", + "worldTime": { + "numerator": "35154861328125", + "denominator": "512" + }, + "engineFrame": "4102", + "sensoryViews": [ + { + "viewId": "lcd", + "producedStep": "4101", + "pixels": { + "storeId": "store-1", + "artifactId": "slot-frame-1", + "generation": "1", + "byteLength": "92160", + "contentType": "image/x-rgba8", + "digest": null + } + } + ], + "inspection": { + "schema": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "value": { + "memory": { + "storeId": "store-1", + "artifactId": "wram-4101", + "generation": "1", + "byteLength": "65536", + "contentType": "application/octet-stream", + "digest": null + }, + "romDigest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20" + } + }, + "broadcastViews": [ + { + "viewId": "lcd", + "producedStep": "4101", + "pixels": { + "storeId": "store-1", + "artifactId": "slot-frame-1", + "generation": "1", + "byteLength": "92160", + "contentType": "image/x-rgba8", + "digest": null + } + } + ], + "audio": [] + } + }, + "reason": "the observation boundary is the committed step" + }, + { + "name": "agent rollback params naming another policy", + "type": "AgentRollbackParams", + "value": { + "agentId": "fly", + "priorEpoch": "epoch-7", + "policy": "fresh-brain-v1", + "input": { + "boundary": "4101", + "views": [], + "structured": null + }, + "decisionContext": { + "schema": { + "id": "gameboy-readout-context-v1", + "version": 1, + "digest": "78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d" + }, + "value": { + "boot": false, + "bound": [ + "macro_go_objective", + "macro_talk" + ], + "location": { + "area": 2, + "x": 17, + "y": 9 + } + } + } + }, + "reason": "legacy-ratchet-rollback-v1 is the only rollback policy defined" + }, + { + "name": "agent rollback result with a bad digest", + "type": "AgentRollbackResult", + "value": { + "agentId": "fly", + "committedStep": "4101", + "decisionContextDigest": "xyz", + "telemetry": { + "brainTicks": "70002", + "populationRateHz": 12.5, + "stimulusRemainingMs": 312.5, + "rates": [ + { + "roleId": "kenyon", + "hz": 3.25 + } + ], + "learning": { + "enabled": true, + "updates": "4", + "changed": "2", + "signal": 0.0 + } + } + }, + "reason": "decisionContextDigest is a Digest" + }, + { + "name": "readout context with a repeated bound channel", + "type": "GameboyReadoutContext", + "value": { + "boot": false, + "bound": [ + "macro_talk", + "macro_talk" + ], + "location": { + "area": 2, + "x": 17, + "y": 9 + } + }, + "reason": "bound is unique" + }, + { + "name": "readout context with a bound channel that is not a channel name", + "type": "GameboyReadoutContext", + "value": { + "boot": false, + "bound": [ + "Macro-Talk" + ], + "location": { + "area": 2, + "x": 17, + "y": 9 + } + }, + "reason": "a channel name is ^[a-z][a-z0-9_]{0,63}$" + }, + { + "name": "readout context with a negative coordinate", + "type": "GameboyReadoutContext", + "value": { + "boot": false, + "bound": [ + "macro_go_objective", + "macro_talk" + ], + "location": { + "area": 2, + "x": -1, + "y": 9 + } + }, + "reason": "coordinates are 0..=4294967295" + }, + { + "name": "readout context without location", + "type": "GameboyReadoutContext", + "value": { + "boot": false, + "bound": [ + "macro_go_objective", + "macro_talk" + ] + }, + "reason": "location is required; null is no information" + }, + { + "name": "readout context with an extra field", + "type": "GameboyReadoutContext", + "value": { + "boot": false, + "bound": [ + "macro_go_objective", + "macro_talk" + ], + "location": { + "area": 2, + "x": 17, + "y": 9 + }, + "blocked": "up" + }, + "reason": "the blocked direction is the agent's own; the context does not carry it" + }, + { + "name": "channels decision with seven buttons", + "type": "GameboyChannelsDecision", + "value": { + "buttons": [ + { + "id": "up", + "down": true + }, + { + "id": "down", + "down": false + }, + { + "id": "left", + "down": false + }, + { + "id": "right", + "down": false + }, + { + "id": "a", + "down": false + }, + { + "id": "b", + "down": false + }, + { + "id": "start", + "down": false + } + ], + "macro": "macro_talk" + }, + "reason": "exactly eight buttons" + }, + { + "name": "channels decision with buttons out of order", + "type": "GameboyChannelsDecision", + "value": { + "buttons": [ + { + "id": "select", + "down": false + }, + { + "id": "start", + "down": false + }, + { + "id": "b", + "down": false + }, + { + "id": "a", + "down": false + }, + { + "id": "right", + "down": false + }, + { + "id": "left", + "down": false + }, + { + "id": "down", + "down": false + }, + { + "id": "up", + "down": true + } + ], + "macro": "macro_talk" + }, + "reason": "GAMEBOY_BUTTON_BITS order" + }, + { + "name": "channels decision with an invalid macro", + "type": "GameboyChannelsDecision", + "value": { + "buttons": [ + { + "id": "up", + "down": true + }, + { + "id": "down", + "down": false + }, + { + "id": "left", + "down": false + }, + { + "id": "right", + "down": false + }, + { + "id": "a", + "down": false + }, + { + "id": "b", + "down": false + }, + { + "id": "start", + "down": false + }, + { + "id": "select", + "down": false + } + ], + "macro": "TALK" + }, + "reason": "macro is a channel name" + }, + { + "name": "memory inspection of the wrong size", + "type": "GameboyMemoryInspection", + "value": { + "memory": { + "storeId": "store-1", + "artifactId": "wram-4101", + "generation": "1", + "byteLength": "8192", + "contentType": "application/octet-stream", + "digest": null + }, + "romDigest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20" + }, + "reason": "the image is the whole 64 KiB address space" + }, + { + "name": "memory inspection without a rom digest", + "type": "GameboyMemoryInspection", + "value": { + "memory": { + "storeId": "store-1", + "artifactId": "wram-4101", + "generation": "1", + "byteLength": "65536", + "contentType": "application/octet-stream", + "digest": null + } + }, + "reason": "the executor reads ROM banks by the content digest" + }, + { + "name": "rollback request with an unknown trigger", + "type": "LegacyRatchetRollbackRequest", + "value": { + "slotId": "best", + "trigger": "boredom" + }, + "reason": "stall or game-over" + }, + { + "name": "legacy profile with another fingerprint", + "type": "LegacyGameboyProfile", + "value": { + "profileId": "gameboy-legacy-fafb-v783-v1", + "datasetId": "fafb-v783", + "fingerprintSchema": 1, + "datasetFingerprint": "75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eef0", + "kernelVersion": "lif-1ms-f64-v2", + "plasticityVersion": "fly-kc-mbon-rstdp-v2", + "tickDuration": { + "numerator": "1000000", + "denominator": "1" + }, + "warmupMs": 2500, + "view": { + "viewId": "lcd", + "width": 160, + "height": 144 + }, + "supportedStimuli": [ + "reward-pulse" + ], + "readoutContextSchema": { + "id": "gameboy-readout-context-v1", + "version": 1, + "digest": "78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d" + }, + "decisionSchema": { + "id": "gameboy-channels-v1", + "version": 1, + "digest": "28bd89bfa41ec73fb2785959a64d9975b4a932e10cd9e51dc4bc29020c823595" + }, + "legacyExceptions": [ + "macro-roles-outside-fingerprint" + ] + }, + "reason": "another fingerprint is another profile" + }, + { + "name": "legacy profile with another kernel", + "type": "LegacyGameboyProfile", + "value": { + "profileId": "gameboy-legacy-fafb-v783-v1", + "datasetId": "fafb-v783", + "fingerprintSchema": 1, + "datasetFingerprint": "75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc", + "kernelVersion": "lif-1ms-f64-v3", + "plasticityVersion": "fly-kc-mbon-rstdp-v2", + "tickDuration": { + "numerator": "1000000", + "denominator": "1" + }, + "warmupMs": 2500, + "view": { + "viewId": "lcd", + "width": 160, + "height": 144 + }, + "supportedStimuli": [ + "reward-pulse" + ], + "readoutContextSchema": { + "id": "gameboy-readout-context-v1", + "version": 1, + "digest": "78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d" + }, + "decisionSchema": { + "id": "gameboy-channels-v1", + "version": 1, + "digest": "28bd89bfa41ec73fb2785959a64d9975b4a932e10cd9e51dc4bc29020c823595" + }, + "legacyExceptions": [ + "macro-roles-outside-fingerprint" + ] + }, + "reason": "the pinned default kernel is unchanged" + }, + { + "name": "legacy profile with another plasticity version", + "type": "LegacyGameboyProfile", + "value": { + "profileId": "gameboy-legacy-fafb-v783-v1", + "datasetId": "fafb-v783", + "fingerprintSchema": 1, + "datasetFingerprint": "75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc", + "kernelVersion": "lif-1ms-f64-v2", + "plasticityVersion": "fly-kc-mbon-rstdp-v3", + "tickDuration": { + "numerator": "1000000", + "denominator": "1" + }, + "warmupMs": 2500, + "view": { + "viewId": "lcd", + "width": 160, + "height": 144 + }, + "supportedStimuli": [ + "reward-pulse" + ], + "readoutContextSchema": { + "id": "gameboy-readout-context-v1", + "version": 1, + "digest": "78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d" + }, + "decisionSchema": { + "id": "gameboy-channels-v1", + "version": 1, + "digest": "28bd89bfa41ec73fb2785959a64d9975b4a932e10cd9e51dc4bc29020c823595" + }, + "legacyExceptions": [ + "macro-roles-outside-fingerprint" + ] + }, + "reason": "the pinned plasticity is unchanged" + }, + { + "name": "legacy profile with a 2 ms tick", + "type": "LegacyGameboyProfile", + "value": { + "profileId": "gameboy-legacy-fafb-v783-v1", + "datasetId": "fafb-v783", + "fingerprintSchema": 1, + "datasetFingerprint": "75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc", + "kernelVersion": "lif-1ms-f64-v2", + "plasticityVersion": "fly-kc-mbon-rstdp-v2", + "tickDuration": { + "numerator": "2000000", + "denominator": "1" + }, + "warmupMs": 2500, + "view": { + "viewId": "lcd", + "width": 160, + "height": 144 + }, + "supportedStimuli": [ + "reward-pulse" + ], + "readoutContextSchema": { + "id": "gameboy-readout-context-v1", + "version": 1, + "digest": "78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d" + }, + "decisionSchema": { + "id": "gameboy-channels-v1", + "version": 1, + "digest": "28bd89bfa41ec73fb2785959a64d9975b4a932e10cd9e51dc4bc29020c823595" + }, + "legacyExceptions": [ + "macro-roles-outside-fingerprint" + ] + }, + "reason": "one model tick is 1 ms" + }, + { + "name": "legacy profile naming a synthetic context schema", + "type": "LegacyGameboyProfile", + "value": { + "profileId": "gameboy-legacy-fafb-v783-v1", + "datasetId": "fafb-v783", + "fingerprintSchema": 1, + "datasetFingerprint": "75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc", + "kernelVersion": "lif-1ms-f64-v2", + "plasticityVersion": "fly-kc-mbon-rstdp-v2", + "tickDuration": { + "numerator": "1000000", + "denominator": "1" + }, + "warmupMs": 2500, + "view": { + "viewId": "lcd", + "width": 160, + "height": 144 + }, + "supportedStimuli": [ + "reward-pulse" + ], + "readoutContextSchema": { + "id": "gameboy-readout-context-v1", + "version": 1, + "digest": "0000000000000000000000000000000000000000000000000000000000000000" + }, + "decisionSchema": { + "id": "gameboy-channels-v1", + "version": 1, + "digest": "28bd89bfa41ec73fb2785959a64d9975b4a932e10cd9e51dc4bc29020c823595" + }, + "legacyExceptions": [ + "macro-roles-outside-fingerprint" + ] + }, + "reason": "the context schema is the registered gameboy-readout-context-v1" + }, + { + "name": "legacy profile without its declared exception", + "type": "LegacyGameboyProfile", + "value": { + "profileId": "gameboy-legacy-fafb-v783-v1", + "datasetId": "fafb-v783", + "fingerprintSchema": 1, + "datasetFingerprint": "75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc", + "kernelVersion": "lif-1ms-f64-v2", + "plasticityVersion": "fly-kc-mbon-rstdp-v2", + "tickDuration": { + "numerator": "1000000", + "denominator": "1" + }, + "warmupMs": 2500, + "view": { + "viewId": "lcd", + "width": 160, + "height": 144 + }, + "supportedStimuli": [ + "reward-pulse" + ], + "readoutContextSchema": { + "id": "gameboy-readout-context-v1", + "version": 1, + "digest": "78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d" + }, + "decisionSchema": { + "id": "gameboy-channels-v1", + "version": 1, + "digest": "28bd89bfa41ec73fb2785959a64d9975b4a932e10cd9e51dc4bc29020c823595" + }, + "legacyExceptions": [] + }, + "reason": "the macro-role exception is declared, not hidden" + }, + { + "name": "composition naming another profile", + "type": "LegacyGameboyComposition", + "value": { + "compositionId": "pokered-live", + "scheduler": "lockstep-v1", + "profile": { + "id": "gameboy-legacy-fafb-v783-v1", + "digest": "0000000000000000000000000000000000000000000000000000000000000000", + "byteLength": "1137", + "format": "fly-profile-v1" + }, + "executor": { + "id": "pokered-macros-v1", + "rom": { + "id": "pokered-rom", + "digest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20", + "byteLength": "1048576", + "format": "gb-rom" + }, + "adapter": "pokered-unique8-v6", + "symbolProvenance": "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b", + "mode": "macros", + "macroChannels": [ + "macro_go_objective", + "macro_talk", + "macro_next", + "macro_move_1" + ] + }, + "decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854", + "environment": { + "extensions": [ + "gameboy-slots-v1" + ], + "slots": [ + "best" + ], + "stepDuration": { + "numerator": "8572265625", + "denominator": "512" + }, + "inspectionSchema": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "controllerSchema": { + "id": "gameboy-joypad-v1", + "version": 1, + "digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e" + }, + "setupFrames": 1, + "audio": { + "sampleRate": 48000, + "channels": 2 + } + }, + "episodePolicy": "legacy-ratchet-rollback-v1", + "restore": "legacy-transient-reset", + "checkpointFormatOfRecord": "FLYSIM01", + "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" + }, + "reason": "the profile AssetRef digest is the legacy profile's" + }, + { + "name": "composition in raw mode with macro channels", + "type": "LegacyGameboyComposition", + "value": { + "compositionId": "pokered-live", + "scheduler": "lockstep-v1", + "profile": { + "id": "gameboy-legacy-fafb-v783-v1", + "digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878", + "byteLength": "1137", + "format": "fly-profile-v1" + }, + "executor": { + "id": "pokered-macros-v1", + "rom": { + "id": "pokered-rom", + "digest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20", + "byteLength": "1048576", + "format": "gb-rom" + }, + "adapter": "pokered-unique8-v6", + "symbolProvenance": "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b", + "mode": "raw", + "macroChannels": [ + "macro_go_objective", + "macro_talk", + "macro_next", + "macro_move_1" + ] + }, + "decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854", + "environment": { + "extensions": [ + "gameboy-slots-v1" + ], + "slots": [ + "best" + ], + "stepDuration": { + "numerator": "8572265625", + "denominator": "512" + }, + "inspectionSchema": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "controllerSchema": { + "id": "gameboy-joypad-v1", + "version": 1, + "digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e" + }, + "setupFrames": 1, + "audio": { + "sampleRate": 48000, + "channels": 2 + } + }, + "episodePolicy": "legacy-ratchet-rollback-v1", + "restore": "legacy-transient-reset", + "checkpointFormatOfRecord": "FLYSIM01", + "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" + }, + "reason": "raw mode deals no macro channels" + }, + { + "name": "composition in macros mode with no channels", + "type": "LegacyGameboyComposition", + "value": { + "compositionId": "pokered-live", + "scheduler": "lockstep-v1", + "profile": { + "id": "gameboy-legacy-fafb-v783-v1", + "digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878", + "byteLength": "1137", + "format": "fly-profile-v1" + }, + "executor": { + "id": "pokered-macros-v1", + "rom": { + "id": "pokered-rom", + "digest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20", + "byteLength": "1048576", + "format": "gb-rom" + }, + "adapter": "pokered-unique8-v6", + "symbolProvenance": "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b", + "mode": "macros", + "macroChannels": [] + }, + "decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854", + "environment": { + "extensions": [ + "gameboy-slots-v1" + ], + "slots": [ + "best" + ], + "stepDuration": { + "numerator": "8572265625", + "denominator": "512" + }, + "inspectionSchema": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "controllerSchema": { + "id": "gameboy-joypad-v1", + "version": 1, + "digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e" + }, + "setupFrames": 1, + "audio": { + "sampleRate": 48000, + "channels": 2 + } + }, + "episodePolicy": "legacy-ratchet-rollback-v1", + "restore": "legacy-transient-reset", + "checkpointFormatOfRecord": "FLYSIM01", + "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" + }, + "reason": "macros mode needs its channels" + }, + { + "name": "composition with another executor", + "type": "LegacyGameboyComposition", + "value": { + "compositionId": "pokered-live", + "scheduler": "lockstep-v1", + "profile": { + "id": "gameboy-legacy-fafb-v783-v1", + "digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878", + "byteLength": "1137", + "format": "fly-profile-v1" + }, + "executor": { + "id": "identity-v1", + "rom": { + "id": "pokered-rom", + "digest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20", + "byteLength": "1048576", + "format": "gb-rom" + }, + "adapter": "pokered-unique8-v6", + "symbolProvenance": "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b", + "mode": "macros", + "macroChannels": [ + "macro_go_objective", + "macro_talk", + "macro_next", + "macro_move_1" + ] + }, + "decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854", + "environment": { + "extensions": [ + "gameboy-slots-v1" + ], + "slots": [ + "best" + ], + "stepDuration": { + "numerator": "8572265625", + "denominator": "512" + }, + "inspectionSchema": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "controllerSchema": { + "id": "gameboy-joypad-v1", + "version": 1, + "digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e" + }, + "setupFrames": 1, + "audio": { + "sampleRate": 48000, + "channels": 2 + } + }, + "episodePolicy": "legacy-ratchet-rollback-v1", + "restore": "legacy-transient-reset", + "checkpointFormatOfRecord": "FLYSIM01", + "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" + }, + "reason": "the executor extension is pokered-macros-v1" + }, + { + "name": "composition with exact restore", + "type": "LegacyGameboyComposition", + "value": { + "compositionId": "pokered-live", + "scheduler": "lockstep-v1", + "profile": { + "id": "gameboy-legacy-fafb-v783-v1", + "digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878", + "byteLength": "1137", + "format": "fly-profile-v1" + }, + "executor": { + "id": "pokered-macros-v1", + "rom": { + "id": "pokered-rom", + "digest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20", + "byteLength": "1048576", + "format": "gb-rom" + }, + "adapter": "pokered-unique8-v6", + "symbolProvenance": "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b", + "mode": "macros", + "macroChannels": [ + "macro_go_objective", + "macro_talk", + "macro_next", + "macro_move_1" + ] + }, + "decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854", + "environment": { + "extensions": [ + "gameboy-slots-v1" + ], + "slots": [ + "best" + ], + "stepDuration": { + "numerator": "8572265625", + "denominator": "512" + }, + "inspectionSchema": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "controllerSchema": { + "id": "gameboy-joypad-v1", + "version": 1, + "digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e" + }, + "setupFrames": 1, + "audio": { + "sampleRate": 48000, + "channels": 2 + } + }, + "episodePolicy": "legacy-ratchet-rollback-v1", + "restore": "exact", + "checkpointFormatOfRecord": "FLYSIM01", + "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" + }, + "reason": "the legacy composition declares legacy-transient-reset" + }, + { + "name": "composition without the slots extension", + "type": "LegacyGameboyComposition", + "value": { + "compositionId": "pokered-live", + "scheduler": "lockstep-v1", + "profile": { + "id": "gameboy-legacy-fafb-v783-v1", + "digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878", + "byteLength": "1137", + "format": "fly-profile-v1" + }, + "executor": { + "id": "pokered-macros-v1", + "rom": { + "id": "pokered-rom", + "digest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20", + "byteLength": "1048576", + "format": "gb-rom" + }, + "adapter": "pokered-unique8-v6", + "symbolProvenance": "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b", + "mode": "macros", + "macroChannels": [ + "macro_go_objective", + "macro_talk", + "macro_next", + "macro_move_1" + ] + }, + "decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854", + "environment": { + "extensions": [ + "world-step-v1" + ], + "slots": [ + "best" + ], + "stepDuration": { + "numerator": "8572265625", + "denominator": "512" + }, + "inspectionSchema": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "controllerSchema": { + "id": "gameboy-joypad-v1", + "version": 1, + "digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e" + }, + "setupFrames": 1, + "audio": { + "sampleRate": 48000, + "channels": 2 + } + }, + "episodePolicy": "legacy-ratchet-rollback-v1", + "restore": "legacy-transient-reset", + "checkpointFormatOfRecord": "FLYSIM01", + "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" + }, + "reason": "the rollback policy needs gameboy-slots-v1" + }, + { + "name": "composition with a 60 Hz step", + "type": "LegacyGameboyComposition", + "value": { + "compositionId": "pokered-live", + "scheduler": "lockstep-v1", + "profile": { + "id": "gameboy-legacy-fafb-v783-v1", + "digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878", + "byteLength": "1137", + "format": "fly-profile-v1" + }, + "executor": { + "id": "pokered-macros-v1", + "rom": { + "id": "pokered-rom", + "digest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20", + "byteLength": "1048576", + "format": "gb-rom" + }, + "adapter": "pokered-unique8-v6", + "symbolProvenance": "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b", + "mode": "macros", + "macroChannels": [ + "macro_go_objective", + "macro_talk", + "macro_next", + "macro_move_1" + ] + }, + "decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854", + "environment": { + "extensions": [ + "gameboy-slots-v1" + ], + "slots": [ + "best" + ], + "stepDuration": { + "numerator": "50000000", + "denominator": "3" + }, + "inspectionSchema": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "controllerSchema": { + "id": "gameboy-joypad-v1", + "version": 1, + "digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e" + }, + "setupFrames": 1, + "audio": { + "sampleRate": 48000, + "channels": 2 + } + }, + "episodePolicy": "legacy-ratchet-rollback-v1", + "restore": "legacy-transient-reset", + "checkpointFormatOfRecord": "FLYSIM01", + "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" + }, + "reason": "one Game Boy frame is 8572265625/512 ns, not 1/60 s" + }, + { + "name": "composition whose compatibility string names another adapter", + "type": "LegacyGameboyComposition", + "value": { + "compositionId": "pokered-live", + "scheduler": "lockstep-v1", + "profile": { + "id": "gameboy-legacy-fafb-v783-v1", + "digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878", + "byteLength": "1137", + "format": "fly-profile-v1" + }, + "executor": { + "id": "pokered-macros-v1", + "rom": { + "id": "pokered-rom", + "digest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20", + "byteLength": "1048576", + "format": "gb-rom" + }, + "adapter": "pokered-unique8-v6", + "symbolProvenance": "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b", + "mode": "macros", + "macroChannels": [ + "macro_go_objective", + "macro_talk", + "macro_next", + "macro_move_1" + ] + }, + "decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854", + "environment": { + "extensions": [ + "gameboy-slots-v1" + ], + "slots": [ + "best" + ], + "stepDuration": { + "numerator": "8572265625", + "denominator": "512" + }, + "inspectionSchema": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "controllerSchema": { + "id": "gameboy-joypad-v1", + "version": 1, + "digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e" + }, + "setupFrames": 1, + "audio": { + "sampleRate": 48000, + "channels": 2 + } + }, + "episodePolicy": "legacy-ratchet-rollback-v1", + "restore": "legacy-transient-reset", + "checkpointFormatOfRecord": "FLYSIM01", + "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v5/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" + }, + "reason": "the FLYSIM01 string and the declaration describe the same fly" + }, + { + "name": "composition with FLYSESS1 as format of record", + "type": "LegacyGameboyComposition", + "value": { + "compositionId": "pokered-live", + "scheduler": "lockstep-v1", + "profile": { + "id": "gameboy-legacy-fafb-v783-v1", + "digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878", + "byteLength": "1137", + "format": "fly-profile-v1" + }, + "executor": { + "id": "pokered-macros-v1", + "rom": { + "id": "pokered-rom", + "digest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20", + "byteLength": "1048576", + "format": "gb-rom" + }, + "adapter": "pokered-unique8-v6", + "symbolProvenance": "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b", + "mode": "macros", + "macroChannels": [ + "macro_go_objective", + "macro_talk", + "macro_next", + "macro_move_1" + ] + }, + "decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854", + "environment": { + "extensions": [ + "gameboy-slots-v1" + ], + "slots": [ + "best" + ], + "stepDuration": { + "numerator": "8572265625", + "denominator": "512" + }, + "inspectionSchema": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "controllerSchema": { + "id": "gameboy-joypad-v1", + "version": 1, + "digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e" + }, + "setupFrames": 1, + "audio": { + "sampleRate": 48000, + "channels": 2 + } + }, + "episodePolicy": "legacy-ratchet-rollback-v1", + "restore": "legacy-transient-reset", + "checkpointFormatOfRecord": "FLYSESS1", + "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" + }, + "reason": "FLYSIM01 stays until RETIRE-01" } ] } diff --git a/services/flysim/crates/fly-session-types/fixtures/schema-set.json b/services/flysim/crates/fly-session-types/fixtures/schema-set.json index 7078001..7f3c83e 100644 --- a/services/flysim/crates/fly-session-types/fixtures/schema-set.json +++ b/services/flysim/crates/fly-session-types/fixtures/schema-set.json @@ -1 +1 @@ -{"contract":"fly-session-types","enums":[{"members":["f32le-interleaved"],"name":"AudioFormat","source":"state-media-v1 2"},{"members":["bipolar","unit"],"name":"AxisRange","source":"workers-v1 3"},{"members":["fixed-build","unverified"],"name":"Determinism","source":"workers-v1 3"},{"members":["terminal"],"name":"EpisodeRequestKind","source":"workers-v1 4"},{"members":["INVALID_ARGUMENT","UNSUPPORTED","IDENTITY_MISMATCH","STALE_EPOCH","STALE_STEP","FUTURE_STEP","INVALID_PHASE","CONFLICT","IN_PROGRESS","BUSY","BUFFER_INVALID","RESULT_EXPIRED","INCOMPATIBLE_STATE","BACKEND_FAILURE","INTERNAL"],"name":"ErrorCode","source":"ipc-v1 7"},{"members":["none","applied","unknown"],"name":"MutationCertainty","source":"ipc-v1 3"},{"members":["exact-checkpoint","episode-restart"],"name":"Recovery","source":"workers-v1 3"},{"members":["agent","environment","coordinator"],"name":"Role","source":"ipc-v1 4"},{"members":["lockstep-v1"],"name":"SchedulerId","source":"publishing-v1 3"},{"members":["rgba8"],"name":"ViewFormat","source":"state-media-v1 2"},{"members":["uninitialized","ready","preparing","prepared","advancing","committing","capturing","staged-restore","restoring","failed","stopping"],"name":"WorkerState","source":"ipc-v1 4"}],"limits":[{"name":"maxAcknowledge","source":"ipc-v1 5","value":16},{"name":"maxAgents","source":"ipc-v1 2","value":4},{"name":"maxAssets","source":"crate","value":64},{"name":"maxAttachments","source":"bus-v1 4","value":32},{"name":"maxAudioStreams","source":"crate","value":8},{"name":"maxAxes","source":"workers-v1 3","value":16},{"name":"maxButtons","source":"workers-v1 3","value":32},{"name":"maxCapabilities","source":"crate","value":32},{"name":"maxEngineFrameLength","source":"workers-v1 3","value":64},{"name":"maxEnvelopeBytes","source":"bus-v1 4","value":65536},{"name":"maxMessageCodePoints","source":"ipc-v1 7","value":512},{"name":"maxObservationDelaySteps","source":"state-media-v1 2","value":8},{"name":"maxPixelAspectPart","source":"state-media-v1 2","value":65535},{"name":"maxPorts","source":"ipc-v1 2","value":4},{"name":"maxRateRoles","source":"ipc-v1 2","value":64},{"name":"maxRewardsPerOperation","source":"workers-v1 1","value":64},{"name":"maxSampleFrames","source":"state-media-v1 2","value":192000},{"name":"maxSchemaVersion","source":"ipc-v1 2","value":65535},{"name":"maxSnapshotEvents","source":"crate","value":64},{"name":"maxStimuliPerOperation","source":"workers-v1 1","value":64},{"name":"maxSupportedMajors","source":"crate","value":8},{"name":"maxSupportedStimuli","source":"crate","value":64},{"name":"maxTypedValueBytes","source":"ipc-v1 2","value":32768},{"name":"maxViewDimension","source":"state-media-v1 2","value":4096},{"name":"maxViews","source":"workers-v1 1","value":8},{"name":"maxWorkerThreads","source":"workers-v1 2","value":4096}],"scalars":{"ArtifactIdentity":"storeId, artifactId and generation of a bus ArtifactRef","BusCallId":"call-","Digest":"64 lowercase hexadecimal digits (SHA-256)","DomainRequestId":"req-","Id":"^[a-z0-9][a-z0-9._-]{0,63}$","OwnerToken":"dlv- or own-","U64":"\"0\" or [1-9][0-9]*, <= 18446744073709551615"},"types":[{"fields":[{"constraint":"1..=16, unique","kind":"array","name":"requestIds","required":true}],"name":"AcknowledgeParams","source":"ipc-v1 5"},{"fields":[{"constraint":"<= 16, unique, a subset of the request","kind":"array","name":"acknowledged","required":true}],"name":"AcknowledgeResult","source":"ipc-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"restoreToken","required":true}],"name":"ActivateRestoreParams","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"U64","name":"committedStep","required":true},{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"required from an environment, null from an agent","kind":"WorldObservation|null","name":"observation","required":false}],"name":"ActivateRestoreResult","source":"state-media-v1 5"},{"fields":[{"constraint":"unique within an epoch","kind":"Id","name":"batchId","required":true},{"constraint":"one complete batch: every declared port once, descriptor order","kind":"array","name":"controls","required":true}],"name":"AdvanceParams","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"k+1","kind":"U64","name":"committedStep","required":true},{"constraint":"","kind":"Digest","name":"decisionContextDigest","required":true},{"constraint":"","kind":"AgentTelemetry","name":"telemetry","required":true}],"name":"AgentCommitResult","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Id","name":"portId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"","kind":"Digest","name":"datasetDigest","required":true},{"constraint":"geometry mapping needs this, not neuronCount","kind":"Digest","name":"indexDigest","required":true},{"constraint":"","kind":"U64","name":"neuronCount","required":true},{"constraint":"<= 64, unique","kind":"array","name":"rateRoles","required":true},{"constraint":"unique","kind":"array","name":"supportedStimuli","required":true}],"name":"AgentDescriptor","source":"publishing-v1 3"},{"fields":[{"constraint":"","kind":"Digest","name":"datasetDigest","required":true},{"constraint":"geometry mapping needs this, not neuronCount","kind":"Digest","name":"indexDigest","required":true},{"constraint":"","kind":"U64","name":"neuronCount","required":true},{"constraint":"<= 64, unique","kind":"array","name":"rateRoles","required":true},{"constraint":"<= 64, unique","kind":"array","name":"supportedStimuli","required":true}],"name":"AgentGraph","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"AssetRef","name":"profile","required":true},{"constraint":"signed 32-bit","kind":"int","name":"seed","required":true},{"constraint":"","kind":"SensoryInput","name":"initialInput","required":true},{"constraint":"","kind":"TypedValue","name":"initialDecisionContext","required":true},{"constraint":">= 1","kind":"int","name":"workerThreads","required":true}],"name":"AgentInitializeParams","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"positive","kind":"RationalNs","name":"tickDuration","required":true},{"constraint":"","kind":"U64","name":"warmupTicks","required":true},{"constraint":"\"0\"","kind":"U64","name":"committedStep","required":true},{"constraint":"","kind":"Digest","name":"decisionContextDigest","required":true},{"constraint":"","kind":"AgentTelemetry","name":"telemetry","required":true},{"constraint":"rates are in graph.rateRoles order","kind":"AgentGraph","name":"graph","required":true}],"name":"AgentInitializeResult","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"U64","name":"brainTicks","required":true},{"constraint":"finite and nonnegative","kind":"number","name":"populationRateHz","required":true},{"constraint":"<= 64, unique roleId, profile order, finite nonnegative hz","kind":"array<{roleId:Id,hz:number}>","name":"rates","required":true},{"constraint":"changed <= updates; signal finite","kind":"{enabled:bool,updates:U64,changed:U64,signal:number}","name":"learning","required":true}],"name":"AgentTelemetry","source":"workers-v1 1"},{"fields":[{"constraint":"","kind":"Id","name":"id","required":true},{"constraint":"","kind":"Digest","name":"digest","required":true},{"constraint":"positive","kind":"U64","name":"byteLength","required":true},{"constraint":"","kind":"Id","name":"format","required":true}],"name":"AssetRef","source":"workers-v1 1"},{"fields":[{"constraint":"","kind":"Id","name":"streamId","required":true},{"constraint":"8000..=192000","kind":"int","name":"sampleRate","required":true},{"constraint":"1..=8","kind":"int","name":"channels","required":true},{"constraint":"","kind":"AudioFormat","name":"format","required":true}],"name":"AudioDescriptor","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"streamId","required":true},{"constraint":"no overlap or rewind within an epoch","kind":"U64","name":"firstSample","required":true},{"constraint":"0..=192000","kind":"int","name":"sampleFrames","required":true},{"constraint":"byteLength == sampleFrames x channels x 4, finite f32","kind":"ArtifactRef","name":"samples","required":true},{"constraint":"true on the first chunk after restore","kind":"bool","name":"discontinuity","required":true}],"name":"AudioRef","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true}],"name":"CaptureParams","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"the committed boundary","kind":"U64","name":"boundary","required":true},{"constraint":"","kind":"Digest","name":"compatibilityDigest","required":true},{"constraint":"listed attachment; digest required","kind":"ArtifactRef","name":"payload","required":true}],"name":"CaptureResult","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"DomainRequestId","name":"preparedRequestId","required":true},{"constraint":"boundary == scope.step + 1","kind":"SensoryInput","name":"nextInput","required":true},{"constraint":"","kind":"TypedValue","name":"nextDecisionContext","required":true},{"constraint":"<= 64, unique eventId, order retained","kind":"array","name":"rewards","required":true},{"constraint":"<= 64, unique id, order retained","kind":"array","name":"taskStimulations","required":true}],"name":"CommitParams","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"U64","name":"descriptorRevision","required":true},{"constraint":"","kind":"Id","name":"publisherIncarnation","required":true},{"constraint":"the committed boundary","kind":"Scope","name":"scope","required":true},{"constraint":"","kind":"Id","name":"episodeId","required":true},{"constraint":"monotonic within publisherIncarnation","kind":"U64","name":"sequence","required":true},{"constraint":"","kind":"RationalNs","name":"worldTime","required":true},{"constraint":"1..=4, unique agentId, telemetry in profile role order","kind":"array","name":"agents","required":true},{"constraint":"","kind":"TypedValue","name":"progress","required":true},{"constraint":"declared attachments held through publication admission","kind":"{views:array,audio:array}","name":"media","required":true},{"constraint":"unique, task order","kind":"array","name":"eventIds","required":true}],"name":"CommittedSnapshot","source":"publishing-v1 3"},{"fields":[{"constraint":"","kind":"SchemaRef","name":"schema","required":true},{"constraint":"<= 32, unique, fixed order","kind":"array","name":"buttons","required":true},{"constraint":"<= 16, unique id, neutral inside its range","kind":"array<{id:Id,range:AxisRange,neutral:number}>","name":"axes","required":true}],"name":"ControllerSchema","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"Digest","name":"backendDigest","required":true},{"constraint":"","kind":"Digest","name":"contentDigest","required":true},{"constraint":"","kind":"Digest","name":"configurationDigest","required":true},{"constraint":"fixed, reduced, positive","kind":"RationalNs","name":"stepDuration","required":true},{"constraint":"1..=4, unique portId, fixed order","kind":"array<{portId:Id,controls:ControllerSchema}>","name":"ports","required":true},{"constraint":"","kind":"SchemaRef","name":"inspectionSchema","required":true},{"constraint":"<= 8, unique viewId","kind":"array","name":"views","required":true},{"constraint":"unique streamId","kind":"array","name":"audio","required":true},{"constraint":"","kind":"Recovery","name":"recovery","required":true},{"constraint":"","kind":"Determinism","name":"determinism","required":true}],"name":"EnvironmentDescriptor","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"AssetRef","name":"backendConfig","required":true},{"constraint":"","kind":"AssetRef","name":"taskConfig","required":true},{"constraint":"","kind":"Id","name":"episodeId","required":true},{"constraint":"1..=4, unique portId and unique agentId","kind":"array<{portId:Id,agentId:Id}>","name":"portBindings","required":true}],"name":"EnvironmentInitializeParams","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"EnvironmentDescriptor","name":"descriptor","required":true},{"constraint":"boundary 0 and worldTime 0/1","kind":"WorldObservation","name":"observation","required":true}],"name":"EnvironmentInitializeResult","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"EpisodeRequestKind","name":"kind","required":true},{"constraint":"","kind":"Id","name":"reason","required":true},{"constraint":"","kind":"TypedValue","name":"outcome","required":true}],"name":"EpisodeRequest","source":"workers-v1 4"},{"fields":[{"constraint":"","kind":"Id","name":"sessionId","required":true},{"constraint":"","kind":"Id","name":"expectedWorkerId","required":true},{"constraint":"","kind":"Role","name":"role","required":true},{"constraint":"nonempty, unique, 1..=65535","kind":"array","name":"supportedMajors","required":true}],"name":"HelloParams","source":"ipc-v1 4"},{"fields":[{"constraint":"1","kind":"const","name":"selectedMajor","required":true},{"constraint":"0","kind":"const","name":"selectedMinor","required":true},{"constraint":"","kind":"Id","name":"workerId","required":true},{"constraint":"","kind":"Id","name":"incarnationId","required":true},{"constraint":"","kind":"Role","name":"role","required":true},{"constraint":"","kind":"Digest","name":"buildDigest","required":true},{"constraint":"","kind":"Digest","name":"contractDigest","required":true},{"constraint":"unique; agent-step-v1 or world-step-v1 required for that role","kind":"array","name":"capabilities","required":true},{"constraint":"1..=4 agents, 1..=4 ports, and the launcher allocation this worker runs in","kind":"{maxAgents:int,maxPorts:int,workerThreads:int}","name":"limits","required":true}],"name":"HelloResult","source":"ipc-v1 4"},{"fields":[{"constraint":"","kind":"Id","name":"portId","required":true},{"constraint":"every declared button, descriptor order, no extras","kind":"array<{id:Id,down:bool}>","name":"buttons","required":true},{"constraint":"every declared axis in order; bipolar [-1,1], unit [0,1], refused not clamped","kind":"array<{id:Id,value:number}>","name":"axes","required":true}],"name":"PortControl","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"positive","kind":"RationalNs","name":"interval","required":true},{"constraint":"","kind":"Digest","name":"decisionContextDigest","required":true},{"constraint":"<= 64, unique id, supplied order retained","kind":"array","name":"preStepStimulations","required":true}],"name":"PrepareParams","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"<= brainTicks","kind":"U64","name":"ticksAdvanced","required":true},{"constraint":"","kind":"U64","name":"brainTicks","required":true},{"constraint":">= 0 and < one model tick","kind":"RationalNs","name":"remainder","required":true},{"constraint":"the profile's registered intent schema","kind":"TypedValue","name":"decision","required":true}],"name":"PreparedDecision","source":"workers-v1 2"},{"fields":[{"constraint":"reduced against denominator","kind":"U64","name":"numerator","required":true},{"constraint":"positive; zero is encoded 0/1; arithmetic is checked","kind":"U64","name":"denominator","required":true}],"name":"RationalNs","source":"ipc-v1 2"},{"fields":[{"constraint":"unique within its outcome namespace","kind":"Id","name":"eventId","required":true},{"constraint":"","kind":"Id","name":"ruleId","required":true},{"constraint":"finite; positive-only profiles reject negatives","kind":"number","name":"value","required":true}],"name":"Reward","source":"workers-v1 1"},{"fields":[{"constraint":"","kind":"Id","name":"id","required":true},{"constraint":"1..=65535","kind":"int","name":"version","required":true},{"constraint":"64 lowercase hex digits","kind":"Digest","name":"digest","required":true}],"name":"SchemaRef","source":"ipc-v1 2"},{"fields":[{"constraint":"^[a-z0-9][a-z0-9._-]{0,63}$","kind":"Id","name":"sessionId","required":true},{"constraint":"^[a-z0-9][a-z0-9._-]{0,63}$","kind":"Id","name":"epoch","required":true},{"constraint":"decimal string, <= 18446744073709551615","kind":"U64","name":"step","required":true}],"name":"Scope","source":"ipc-v1 2"},{"fields":[{"constraint":"the observed environment boundary","kind":"U64","name":"boundary","required":true},{"constraint":"<= 8, unique viewId, producedStep <= boundary","kind":"array","name":"views","required":true},{"constraint":"a pixel-only profile rejects non-null","kind":"TypedValue|null","name":"structured","required":false}],"name":"SensoryInput","source":"workers-v1 1"},{"fields":[{"constraint":"","kind":"Id","name":"sessionId","required":true},{"constraint":"","kind":"U64","name":"revision","required":true},{"constraint":"","kind":"Digest","name":"compositionDigest","required":true},{"constraint":"","kind":"SchedulerId","name":"schedulerId","required":true},{"constraint":"","kind":"EnvironmentDescriptor","name":"environment","required":true},{"constraint":"","kind":"SchemaRef","name":"taskSchema","required":true},{"constraint":"1..=4, unique agentId and portId, each portId declared by the environment","kind":"array","name":"agents","required":true},{"constraint":"unique id","kind":"array","name":"assets","required":true}],"name":"SessionDescriptor","source":"publishing-v1 3"},{"fields":[{"constraint":"\"error\"","kind":"const","name":"type","required":true},{"constraint":"echoes the request","kind":"DomainRequestId","name":"requestId","required":true},{"constraint":"","kind":"Id","name":"workerId","required":true},{"constraint":"","kind":"Id","name":"incarnationId","required":true},{"constraint":"echoes the request scope","kind":"Scope|null","name":"scope","required":false},{"constraint":"","kind":"ErrorCode","name":"error.code","required":true},{"constraint":"<= 512 code points","kind":"string","name":"error.message","required":true},{"constraint":"none for every code raised before mutation","kind":"MutationCertainty","name":"error.mutation","required":true}],"name":"SessionRpcFailure","source":"ipc-v1 3"},{"fields":[{"constraint":"req-","kind":"DomainRequestId","name":"requestId","required":true},{"constraint":"null for lifecycle calls","kind":"Scope|null","name":"scope","required":false},{"constraint":"no bus callId/deliveryId/ownerId keys","kind":"object","name":"params","required":true}],"name":"SessionRpcRequest","source":"ipc-v1 2"},{"fields":[{"constraint":"\"result\"","kind":"const","name":"type","required":true},{"constraint":"echoes the request","kind":"DomainRequestId","name":"requestId","required":true},{"constraint":"","kind":"Id","name":"workerId","required":true},{"constraint":"","kind":"Id","name":"incarnationId","required":true},{"constraint":"echoes the request scope","kind":"Scope|null","name":"scope","required":false},{"constraint":"","kind":"object","name":"result","required":true}],"name":"SessionRpcSuccess","source":"ipc-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"reason","required":true}],"name":"ShutdownParams","source":"ipc-v1 7"},{"fields":[{"constraint":"true","kind":"const","name":"stopping","required":true}],"name":"ShutdownResult","source":"ipc-v1 7"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"AgentTelemetry","name":"telemetry","required":true},{"constraint":"null at boundary 0 and at an installed boundary; null or present for every agent together","kind":"TypedValue|null","name":"selectedDecision","required":false},{"constraint":"null with selectedDecision; the agent's assigned port","kind":"PortControl|null","name":"appliedControls","required":false}],"name":"SnapshotAgent","source":"publishing-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"provenance, not the new handles","kind":"Scope","name":"sourceScope","required":true},{"constraint":"","kind":"Digest","name":"compatibilityDigest","required":true},{"constraint":"newly imported; digest required","kind":"ArtifactRef","name":"payload","required":true}],"name":"StageRestoreParams","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"activates once, bound to scope and payload","kind":"Id","name":"restoreToken","required":true}],"name":"StageRestoreResult","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"WorkerState","name":"state","required":true},{"constraint":"null before initialization","kind":"Scope|null","name":"currentScope","required":false},{"constraint":"","kind":"DomainRequestId|null","name":"activeRequestId","required":false},{"constraint":"","kind":"DomainRequestId|null","name":"lastCompletedRequestId","required":false},{"constraint":"","kind":"Id|null","name":"lastBatchId","required":false},{"constraint":"advances on progress, not on status queries","kind":"U64","name":"progressCounter","required":true}],"name":"StatusResult","source":"ipc-v1 4"},{"fields":[{"constraint":"echoes the request","kind":"Id","name":"batchId","required":true},{"constraint":"k","kind":"U64","name":"appliedFromStep","required":true},{"constraint":"appliedFromStep + 1","kind":"U64","name":"nextStep","required":true},{"constraint":"over validated canonical requested controls","kind":"Digest","name":"appliedControlsDigest","required":true},{"constraint":"boundary == nextStep","kind":"WorldObservation","name":"observation","required":true}],"name":"StepResult","source":"workers-v1 3"},{"fields":[{"constraint":"unique within its command namespace","kind":"Id","name":"id","required":true},{"constraint":"resolved through a profile capability","kind":"Id","name":"kindId","required":true},{"constraint":"finite and > 0","kind":"number","name":"durationMs","required":true}],"name":"Stimulus","source":"workers-v1 1"},{"fields":[{"constraint":"derived from epoch, source step, rule and ordinal","kind":"Id","name":"id","required":true},{"constraint":"","kind":"Id","name":"kindId","required":true},{"constraint":"the newly reached boundary, 0 for bootstrap","kind":"U64","name":"sourceStep","required":true},{"constraint":"","kind":"Id|null","name":"agentId","required":false},{"constraint":"","kind":"TypedValue","name":"payload","required":true}],"name":"TaskEvent","source":"workers-v1 4"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"","kind":"U64","name":"ticksAdvanced","required":true},{"constraint":"","kind":"U64","name":"brainTicks","required":true},{"constraint":"","kind":"RationalNs","name":"remainder","required":true},{"constraint":"","kind":"Digest","name":"decisionDigest","required":true},{"constraint":"the commit acknowledgment","kind":"U64","name":"committedStep","required":true}],"name":"TraceAgent","source":"step-v1 8"},{"fields":[{"constraint":"","kind":"Scope","name":"scope","required":true},{"constraint":"sorted by agentId, independent of dispatch order","kind":"array","name":"agents","required":true},{"constraint":"","kind":"Id","name":"batchId","required":true},{"constraint":"","kind":"Digest","name":"controlDigest","required":true},{"constraint":"the world boundary the environment acknowledged","kind":"U64","name":"acknowledgedBoundary","required":true},{"constraint":"sorted by viewId","kind":"array<{viewId:Id,producedStep:U64}>","name":"observationBoundaries","required":true},{"constraint":"task outcome ids in task order","kind":"array","name":"outcomeIds","required":true},{"constraint":"task event ids in task order","kind":"array","name":"eventIds","required":true},{"constraint":"","kind":"U64","name":"publishedBoundary","required":true}],"name":"TraceBehaviour","source":"step-v1 8"},{"fields":[{"constraint":"","kind":"U64","name":"wallTimeNs","required":true},{"constraint":"","kind":"array<{agentId:Id,requestId:DomainRequestId}>","name":"prepareRequestIds","required":true},{"constraint":"","kind":"DomainRequestId","name":"advanceRequestId","required":true},{"constraint":"","kind":"array<{agentId:Id,requestId:DomainRequestId}>","name":"commitRequestIds","required":true},{"constraint":"call-","kind":"array","name":"busCallIds","required":true},{"constraint":"dlv- or own-","kind":"array","name":"deliveryIds","required":true}],"name":"TraceOperational","source":"step-v1 8"},{"fields":[{"constraint":"compared between runs","kind":"TraceBehaviour","name":"behaviour","required":true},{"constraint":"recorded, never compared: wall time and transport identities","kind":"TraceOperational","name":"operational","required":true}],"name":"TransitionTrace","source":"step-v1 8"},{"fields":[{"constraint":"","kind":"SchemaRef","name":"schema","required":true},{"constraint":"canonical JSON of the whole TypedValue <= 32768 bytes","kind":"object","name":"value","required":true}],"name":"TypedValue","source":"ipc-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"viewId","required":true},{"constraint":"1..=4096","kind":"int","name":"width","required":true},{"constraint":"1..=4096","kind":"int","name":"height","required":true},{"constraint":"","kind":"ViewFormat","name":"format","required":true},{"constraint":"exactly 4 x width","kind":"int","name":"rowStride","required":true},{"constraint":"positive integers <= 65535","kind":"{numerator:int,denominator:int}","name":"pixelAspect","required":true},{"constraint":"0..=8","kind":"int","name":"observationDelaySteps","required":true}],"name":"ViewDescriptor","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"viewId","required":true},{"constraint":"max(0, boundary - observationDelaySteps) for required sensory views","kind":"U64","name":"producedStep","required":true},{"constraint":"listed attachment; byteLength == rowStride x height","kind":"ArtifactRef","name":"pixels","required":true}],"name":"ViewRef","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"U64","name":"boundary","required":true},{"constraint":"logical time since episode start","kind":"RationalNs","name":"worldTime","required":true},{"constraint":"<= 64 characters","kind":"string|null","name":"engineFrame","required":false},{"constraint":"<= 8, unique viewId","kind":"array","name":"sensoryViews","required":true},{"constraint":"the descriptor's inspectionSchema","kind":"TypedValue","name":"inspection","required":true},{"constraint":"<= 8, unique viewId","kind":"array","name":"broadcastViews","required":true},{"constraint":"unique streamId","kind":"array","name":"audio","required":true}],"name":"WorldObservation","source":"workers-v1 3"}],"version":1} +{"contract":"fly-session-types","enums":[{"members":["f32le-interleaved"],"name":"AudioFormat","source":"state-media-v1 2"},{"members":["bipolar","unit"],"name":"AxisRange","source":"workers-v1 3"},{"members":["fixed-build","unverified"],"name":"Determinism","source":"workers-v1 3"},{"members":["terminal","rollback"],"name":"EpisodeRequestKind","source":"workers-v1 4"},{"members":["INVALID_ARGUMENT","UNSUPPORTED","IDENTITY_MISMATCH","STALE_EPOCH","STALE_STEP","FUTURE_STEP","INVALID_PHASE","CONFLICT","IN_PROGRESS","BUSY","BUFFER_INVALID","RESULT_EXPIRED","INCOMPATIBLE_STATE","BACKEND_FAILURE","INTERNAL"],"name":"ErrorCode","source":"ipc-v1 7"},{"members":["none","applied","unknown"],"name":"MutationCertainty","source":"ipc-v1 3"},{"members":["exact-checkpoint","episode-restart"],"name":"Recovery","source":"workers-v1 3"},{"members":["agent","environment","coordinator"],"name":"Role","source":"ipc-v1 4"},{"members":["lockstep-v1"],"name":"SchedulerId","source":"publishing-v1 3"},{"members":["rgba8"],"name":"ViewFormat","source":"state-media-v1 2"},{"members":["uninitialized","ready","preparing","prepared","advancing","committing","capturing","staged-restore","restoring","failed","stopping"],"name":"WorkerState","source":"ipc-v1 4"}],"limits":[{"name":"maxAcknowledge","source":"ipc-v1 5","value":16},{"name":"maxAgents","source":"ipc-v1 2","value":4},{"name":"maxAssets","source":"crate","value":64},{"name":"maxAttachments","source":"bus-v1 4","value":32},{"name":"maxAudioStreams","source":"crate","value":8},{"name":"maxAxes","source":"workers-v1 3","value":16},{"name":"maxButtons","source":"workers-v1 3","value":32},{"name":"maxCapabilities","source":"crate","value":32},{"name":"maxEngineFrameLength","source":"workers-v1 3","value":64},{"name":"maxEnvelopeBytes","source":"bus-v1 4","value":65536},{"name":"maxMessageCodePoints","source":"ipc-v1 7","value":512},{"name":"maxObservationDelaySteps","source":"state-media-v1 2","value":8},{"name":"maxPixelAspectPart","source":"state-media-v1 2","value":65535},{"name":"maxPorts","source":"ipc-v1 2","value":4},{"name":"maxRateRoles","source":"ipc-v1 2","value":64},{"name":"maxRewardsPerOperation","source":"workers-v1 1","value":64},{"name":"maxSampleFrames","source":"state-media-v1 2","value":192000},{"name":"maxSchemaVersion","source":"ipc-v1 2","value":65535},{"name":"maxSlots","source":"crate","value":4},{"name":"maxSnapshotEvents","source":"crate","value":64},{"name":"maxStimuliPerOperation","source":"workers-v1 1","value":64},{"name":"maxSupportedMajors","source":"crate","value":8},{"name":"maxSupportedStimuli","source":"crate","value":64},{"name":"maxTypedValueBytes","source":"ipc-v1 2","value":32768},{"name":"maxViewDimension","source":"state-media-v1 2","value":4096},{"name":"maxViews","source":"workers-v1 1","value":8},{"name":"maxWorkerThreads","source":"workers-v1 2","value":4096}],"scalars":{"ArtifactIdentity":"storeId, artifactId and generation of a bus ArtifactRef","BusCallId":"call-","Digest":"64 lowercase hexadecimal digits (SHA-256)","DomainRequestId":"req-","Id":"^[a-z0-9][a-z0-9._-]{0,63}$","OwnerToken":"dlv- or own-","U64":"\"0\" or [1-9][0-9]*, <= 18446744073709551615"},"types":[{"fields":[{"constraint":"1..=16, unique","kind":"array","name":"requestIds","required":true}],"name":"AcknowledgeParams","source":"ipc-v1 5"},{"fields":[{"constraint":"<= 16, unique, a subset of the request","kind":"array","name":"acknowledged","required":true}],"name":"AcknowledgeResult","source":"ipc-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"restoreToken","required":true}],"name":"ActivateRestoreParams","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"U64","name":"committedStep","required":true},{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"required from an environment, null from an agent","kind":"WorldObservation|null","name":"observation","required":false}],"name":"ActivateRestoreResult","source":"state-media-v1 5"},{"fields":[{"constraint":"unique within an epoch","kind":"Id","name":"batchId","required":true},{"constraint":"one complete batch: every declared port once, descriptor order","kind":"array","name":"controls","required":true}],"name":"AdvanceParams","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"k+1","kind":"U64","name":"committedStep","required":true},{"constraint":"","kind":"Digest","name":"decisionContextDigest","required":true},{"constraint":"","kind":"AgentTelemetry","name":"telemetry","required":true}],"name":"AgentCommitResult","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Id","name":"portId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"","kind":"Digest","name":"datasetDigest","required":true},{"constraint":"geometry mapping needs this, not neuronCount","kind":"Digest","name":"indexDigest","required":true},{"constraint":"","kind":"U64","name":"neuronCount","required":true},{"constraint":"<= 64, unique","kind":"array","name":"rateRoles","required":true},{"constraint":"unique","kind":"array","name":"supportedStimuli","required":true}],"name":"AgentDescriptor","source":"publishing-v1 3"},{"fields":[{"constraint":"","kind":"Digest","name":"datasetDigest","required":true},{"constraint":"geometry mapping needs this, not neuronCount","kind":"Digest","name":"indexDigest","required":true},{"constraint":"","kind":"U64","name":"neuronCount","required":true},{"constraint":"<= 64, unique","kind":"array","name":"rateRoles","required":true},{"constraint":"<= 64, unique","kind":"array","name":"supportedStimuli","required":true}],"name":"AgentGraph","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"AssetRef","name":"profile","required":true},{"constraint":"signed 32-bit","kind":"int","name":"seed","required":true},{"constraint":"","kind":"SensoryInput","name":"initialInput","required":true},{"constraint":"","kind":"TypedValue","name":"initialDecisionContext","required":true},{"constraint":">= 1","kind":"int","name":"workerThreads","required":true}],"name":"AgentInitializeParams","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"positive","kind":"RationalNs","name":"tickDuration","required":true},{"constraint":"","kind":"U64","name":"warmupTicks","required":true},{"constraint":"\"0\"","kind":"U64","name":"committedStep","required":true},{"constraint":"","kind":"Digest","name":"decisionContextDigest","required":true},{"constraint":"","kind":"AgentTelemetry","name":"telemetry","required":true},{"constraint":"rates are in graph.rateRoles order","kind":"AgentGraph","name":"graph","required":true}],"name":"AgentInitializeResult","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"the epoch the agent is Ready in; differs from scope.epoch","kind":"Id","name":"priorEpoch","required":true},{"constraint":"\"legacy-ratchet-rollback-v1\"; capability of the same name","kind":"Id","name":"policy","required":true},{"constraint":"boundary == scope.step; installed without a tick","kind":"SensoryInput","name":"input","required":true},{"constraint":"the context for the next Prepare","kind":"TypedValue","name":"decisionContext","required":true}],"name":"AgentRollbackParams","source":"workers-v1 7"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"the scoped step; a rollback runs no tick","kind":"U64","name":"committedStep","required":true},{"constraint":"","kind":"Digest","name":"decisionContextDigest","required":true},{"constraint":"","kind":"AgentTelemetry","name":"telemetry","required":true}],"name":"AgentRollbackResult","source":"workers-v1 7"},{"fields":[{"constraint":"","kind":"U64","name":"brainTicks","required":true},{"constraint":"finite and nonnegative","kind":"number","name":"populationRateHz","required":true},{"constraint":"<= 64, unique roleId, profile order, finite nonnegative hz","kind":"array<{roleId:Id,hz:number}>","name":"rates","required":true},{"constraint":"changed <= updates; signal finite","kind":"{enabled:bool,updates:U64,changed:U64,signal:number}","name":"learning","required":true},{"constraint":"finite and nonnegative; the pulse still running after the operation; null when the agent reports none","kind":"number|null","name":"stimulusRemainingMs","required":false}],"name":"AgentTelemetry","source":"workers-v1 1"},{"fields":[{"constraint":"","kind":"Id","name":"id","required":true},{"constraint":"","kind":"Digest","name":"digest","required":true},{"constraint":"positive","kind":"U64","name":"byteLength","required":true},{"constraint":"","kind":"Id","name":"format","required":true}],"name":"AssetRef","source":"workers-v1 1"},{"fields":[{"constraint":"","kind":"Id","name":"streamId","required":true},{"constraint":"8000..=192000","kind":"int","name":"sampleRate","required":true},{"constraint":"1..=8","kind":"int","name":"channels","required":true},{"constraint":"","kind":"AudioFormat","name":"format","required":true}],"name":"AudioDescriptor","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"streamId","required":true},{"constraint":"no overlap or rewind within an epoch","kind":"U64","name":"firstSample","required":true},{"constraint":"0..=192000","kind":"int","name":"sampleFrames","required":true},{"constraint":"byteLength == sampleFrames x channels x 4, finite f32","kind":"ArtifactRef","name":"samples","required":true},{"constraint":"true on the first chunk after restore","kind":"bool","name":"discontinuity","required":true}],"name":"AudioRef","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true}],"name":"CaptureParams","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"the committed boundary","kind":"U64","name":"boundary","required":true},{"constraint":"","kind":"Digest","name":"compatibilityDigest","required":true},{"constraint":"listed attachment; digest required","kind":"ArtifactRef","name":"payload","required":true}],"name":"CaptureResult","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"DomainRequestId","name":"preparedRequestId","required":true},{"constraint":"boundary == scope.step + 1","kind":"SensoryInput","name":"nextInput","required":true},{"constraint":"","kind":"TypedValue","name":"nextDecisionContext","required":true},{"constraint":"<= 64, unique eventId, order retained","kind":"array","name":"rewards","required":true},{"constraint":"<= 64, unique id, order retained","kind":"array","name":"taskStimulations","required":true}],"name":"CommitParams","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"U64","name":"descriptorRevision","required":true},{"constraint":"","kind":"Id","name":"publisherIncarnation","required":true},{"constraint":"the committed boundary","kind":"Scope","name":"scope","required":true},{"constraint":"","kind":"Id","name":"episodeId","required":true},{"constraint":"monotonic within publisherIncarnation","kind":"U64","name":"sequence","required":true},{"constraint":"","kind":"RationalNs","name":"worldTime","required":true},{"constraint":"1..=4, unique agentId, telemetry in profile role order","kind":"array","name":"agents","required":true},{"constraint":"","kind":"TypedValue","name":"progress","required":true},{"constraint":"declared attachments held through publication admission","kind":"{views:array,audio:array}","name":"media","required":true},{"constraint":"unique, task order","kind":"array","name":"eventIds","required":true}],"name":"CommittedSnapshot","source":"publishing-v1 3"},{"fields":[{"constraint":"","kind":"SchemaRef","name":"schema","required":true},{"constraint":"<= 32, unique, fixed order","kind":"array","name":"buttons","required":true},{"constraint":"<= 16, unique id, neutral inside its range","kind":"array<{id:Id,range:AxisRange,neutral:number}>","name":"axes","required":true}],"name":"ControllerSchema","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"Digest","name":"backendDigest","required":true},{"constraint":"","kind":"Digest","name":"contentDigest","required":true},{"constraint":"","kind":"Digest","name":"configurationDigest","required":true},{"constraint":"fixed, reduced, positive","kind":"RationalNs","name":"stepDuration","required":true},{"constraint":"1..=4, unique portId, fixed order","kind":"array<{portId:Id,controls:ControllerSchema}>","name":"ports","required":true},{"constraint":"","kind":"SchemaRef","name":"inspectionSchema","required":true},{"constraint":"<= 8, unique viewId","kind":"array","name":"views","required":true},{"constraint":"unique streamId","kind":"array","name":"audio","required":true},{"constraint":"","kind":"Recovery","name":"recovery","required":true},{"constraint":"","kind":"Determinism","name":"determinism","required":true}],"name":"EnvironmentDescriptor","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"AssetRef","name":"backendConfig","required":true},{"constraint":"","kind":"AssetRef","name":"taskConfig","required":true},{"constraint":"","kind":"Id","name":"episodeId","required":true},{"constraint":"1..=4, unique portId and unique agentId","kind":"array<{portId:Id,agentId:Id}>","name":"portBindings","required":true}],"name":"EnvironmentInitializeParams","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"EnvironmentDescriptor","name":"descriptor","required":true},{"constraint":"boundary 0 and worldTime 0/1","kind":"WorldObservation","name":"observation","required":true}],"name":"EnvironmentInitializeResult","source":"workers-v1 3"},{"fields":[{"constraint":"rollback only under a composition that declares a rollback policy","kind":"EpisodeRequestKind","name":"kind","required":true},{"constraint":"","kind":"Id","name":"reason","required":true},{"constraint":"","kind":"TypedValue","name":"outcome","required":true}],"name":"EpisodeRequest","source":"workers-v1 4"},{"fields":[{"constraint":"","kind":"Id","name":"sessionId","required":true},{"constraint":"","kind":"Id","name":"expectedWorkerId","required":true},{"constraint":"","kind":"Role","name":"role","required":true},{"constraint":"nonempty, unique, 1..=65535","kind":"array","name":"supportedMajors","required":true}],"name":"HelloParams","source":"ipc-v1 4"},{"fields":[{"constraint":"1","kind":"const","name":"selectedMajor","required":true},{"constraint":"0","kind":"const","name":"selectedMinor","required":true},{"constraint":"","kind":"Id","name":"workerId","required":true},{"constraint":"","kind":"Id","name":"incarnationId","required":true},{"constraint":"","kind":"Role","name":"role","required":true},{"constraint":"","kind":"Digest","name":"buildDigest","required":true},{"constraint":"","kind":"Digest","name":"contractDigest","required":true},{"constraint":"unique; agent-step-v1 or world-step-v1 required for that role","kind":"array","name":"capabilities","required":true},{"constraint":"1..=4 agents, 1..=4 ports, and the launcher allocation this worker runs in","kind":"{maxAgents:int,maxPorts:int,workerThreads:int}","name":"limits","required":true}],"name":"HelloResult","source":"ipc-v1 4"},{"fields":[{"constraint":"","kind":"Id","name":"portId","required":true},{"constraint":"every declared button, descriptor order, no extras","kind":"array<{id:Id,down:bool}>","name":"buttons","required":true},{"constraint":"every declared axis in order; bipolar [-1,1], unit [0,1], refused not clamped","kind":"array<{id:Id,value:number}>","name":"axes","required":true}],"name":"PortControl","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"positive","kind":"RationalNs","name":"interval","required":true},{"constraint":"","kind":"Digest","name":"decisionContextDigest","required":true},{"constraint":"<= 64, unique id, supplied order retained","kind":"array","name":"preStepStimulations","required":true}],"name":"PrepareParams","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"<= brainTicks","kind":"U64","name":"ticksAdvanced","required":true},{"constraint":"","kind":"U64","name":"brainTicks","required":true},{"constraint":">= 0 and < one model tick","kind":"RationalNs","name":"remainder","required":true},{"constraint":"the profile's registered intent schema","kind":"TypedValue","name":"decision","required":true}],"name":"PreparedDecision","source":"workers-v1 2"},{"fields":[{"constraint":"reduced against denominator","kind":"U64","name":"numerator","required":true},{"constraint":"positive; zero is encoded 0/1; arithmetic is checked","kind":"U64","name":"denominator","required":true}],"name":"RationalNs","source":"ipc-v1 2"},{"fields":[{"constraint":"a slot saved in priorEpoch or carried by its restore","kind":"Id","name":"slotId","required":true},{"constraint":"the epoch the environment is Ready in; differs from scope.epoch","kind":"Id","name":"priorEpoch","required":true},{"constraint":"\"legacy-ratchet-rollback-v1\"","kind":"Id","name":"policy","required":true}],"name":"RestoreSlotParams","source":"workers-v1 7"},{"fields":[{"constraint":"echoes the request","kind":"Id","name":"slotId","required":true},{"constraint":"the scoped step; no transition ran","kind":"U64","name":"committedStep","required":true},{"constraint":"boundary == committedStep; no audio chunk; worldTime and engineFrame continue","kind":"WorldObservation","name":"observation","required":true}],"name":"RestoreSlotResult","source":"workers-v1 7"},{"fields":[{"constraint":"unique within its outcome namespace","kind":"Id","name":"eventId","required":true},{"constraint":"","kind":"Id","name":"ruleId","required":true},{"constraint":"finite; positive-only profiles reject negatives","kind":"number","name":"value","required":true}],"name":"Reward","source":"workers-v1 1"},{"fields":[{"constraint":"one of the composition's declared slots; capability gameboy-slots-v1","kind":"Id","name":"slotId","required":true}],"name":"SaveSlotParams","source":"workers-v1 7"},{"fields":[{"constraint":"echoes the request","kind":"Id","name":"slotId","required":true},{"constraint":"the scoped committed step","kind":"U64","name":"boundary","required":true},{"constraint":"SHA-256 of the saved state bytes","kind":"Digest","name":"stateDigest","required":true},{"constraint":"positive","kind":"U64","name":"byteLength","required":true}],"name":"SaveSlotResult","source":"workers-v1 7"},{"fields":[{"constraint":"","kind":"Id","name":"id","required":true},{"constraint":"1..=65535","kind":"int","name":"version","required":true},{"constraint":"64 lowercase hex digits","kind":"Digest","name":"digest","required":true}],"name":"SchemaRef","source":"ipc-v1 2"},{"fields":[{"constraint":"^[a-z0-9][a-z0-9._-]{0,63}$","kind":"Id","name":"sessionId","required":true},{"constraint":"^[a-z0-9][a-z0-9._-]{0,63}$","kind":"Id","name":"epoch","required":true},{"constraint":"decimal string, <= 18446744073709551615","kind":"U64","name":"step","required":true}],"name":"Scope","source":"ipc-v1 2"},{"fields":[{"constraint":"the observed environment boundary","kind":"U64","name":"boundary","required":true},{"constraint":"<= 8, unique viewId, producedStep <= boundary","kind":"array","name":"views","required":true},{"constraint":"a pixel-only profile rejects non-null","kind":"TypedValue|null","name":"structured","required":false}],"name":"SensoryInput","source":"workers-v1 1"},{"fields":[{"constraint":"","kind":"Id","name":"sessionId","required":true},{"constraint":"","kind":"U64","name":"revision","required":true},{"constraint":"","kind":"Digest","name":"compositionDigest","required":true},{"constraint":"","kind":"SchedulerId","name":"schedulerId","required":true},{"constraint":"","kind":"EnvironmentDescriptor","name":"environment","required":true},{"constraint":"","kind":"SchemaRef","name":"taskSchema","required":true},{"constraint":"1..=4, unique agentId and portId, each portId declared by the environment","kind":"array","name":"agents","required":true},{"constraint":"unique id","kind":"array","name":"assets","required":true}],"name":"SessionDescriptor","source":"publishing-v1 3"},{"fields":[{"constraint":"\"error\"","kind":"const","name":"type","required":true},{"constraint":"echoes the request","kind":"DomainRequestId","name":"requestId","required":true},{"constraint":"","kind":"Id","name":"workerId","required":true},{"constraint":"","kind":"Id","name":"incarnationId","required":true},{"constraint":"echoes the request scope","kind":"Scope|null","name":"scope","required":false},{"constraint":"","kind":"ErrorCode","name":"error.code","required":true},{"constraint":"<= 512 code points","kind":"string","name":"error.message","required":true},{"constraint":"none for every code raised before mutation","kind":"MutationCertainty","name":"error.mutation","required":true}],"name":"SessionRpcFailure","source":"ipc-v1 3"},{"fields":[{"constraint":"req-","kind":"DomainRequestId","name":"requestId","required":true},{"constraint":"null for lifecycle calls","kind":"Scope|null","name":"scope","required":false},{"constraint":"no bus callId/deliveryId/ownerId keys","kind":"object","name":"params","required":true}],"name":"SessionRpcRequest","source":"ipc-v1 2"},{"fields":[{"constraint":"\"result\"","kind":"const","name":"type","required":true},{"constraint":"echoes the request","kind":"DomainRequestId","name":"requestId","required":true},{"constraint":"","kind":"Id","name":"workerId","required":true},{"constraint":"","kind":"Id","name":"incarnationId","required":true},{"constraint":"echoes the request scope","kind":"Scope|null","name":"scope","required":false},{"constraint":"","kind":"object","name":"result","required":true}],"name":"SessionRpcSuccess","source":"ipc-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"reason","required":true}],"name":"ShutdownParams","source":"ipc-v1 7"},{"fields":[{"constraint":"true","kind":"const","name":"stopping","required":true}],"name":"ShutdownResult","source":"ipc-v1 7"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"AgentTelemetry","name":"telemetry","required":true},{"constraint":"null at boundary 0 and at an installed boundary; null or present for every agent together","kind":"TypedValue|null","name":"selectedDecision","required":false},{"constraint":"null with selectedDecision; the agent's assigned port","kind":"PortControl|null","name":"appliedControls","required":false}],"name":"SnapshotAgent","source":"publishing-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"provenance, not the new handles","kind":"Scope","name":"sourceScope","required":true},{"constraint":"","kind":"Digest","name":"compatibilityDigest","required":true},{"constraint":"newly imported; digest required","kind":"ArtifactRef","name":"payload","required":true}],"name":"StageRestoreParams","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"activates once, bound to scope and payload","kind":"Id","name":"restoreToken","required":true}],"name":"StageRestoreResult","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"WorkerState","name":"state","required":true},{"constraint":"null before initialization","kind":"Scope|null","name":"currentScope","required":false},{"constraint":"","kind":"DomainRequestId|null","name":"activeRequestId","required":false},{"constraint":"","kind":"DomainRequestId|null","name":"lastCompletedRequestId","required":false},{"constraint":"","kind":"Id|null","name":"lastBatchId","required":false},{"constraint":"advances on progress, not on status queries","kind":"U64","name":"progressCounter","required":true}],"name":"StatusResult","source":"ipc-v1 4"},{"fields":[{"constraint":"echoes the request","kind":"Id","name":"batchId","required":true},{"constraint":"k","kind":"U64","name":"appliedFromStep","required":true},{"constraint":"appliedFromStep + 1","kind":"U64","name":"nextStep","required":true},{"constraint":"over validated canonical requested controls","kind":"Digest","name":"appliedControlsDigest","required":true},{"constraint":"boundary == nextStep","kind":"WorldObservation","name":"observation","required":true}],"name":"StepResult","source":"workers-v1 3"},{"fields":[{"constraint":"unique within its command namespace","kind":"Id","name":"id","required":true},{"constraint":"resolved through a profile capability","kind":"Id","name":"kindId","required":true},{"constraint":"finite and > 0","kind":"number","name":"durationMs","required":true}],"name":"Stimulus","source":"workers-v1 1"},{"fields":[{"constraint":"derived from epoch, source step, rule and ordinal","kind":"Id","name":"id","required":true},{"constraint":"","kind":"Id","name":"kindId","required":true},{"constraint":"the newly reached boundary, 0 for bootstrap","kind":"U64","name":"sourceStep","required":true},{"constraint":"","kind":"Id|null","name":"agentId","required":false},{"constraint":"","kind":"TypedValue","name":"payload","required":true}],"name":"TaskEvent","source":"workers-v1 4"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"","kind":"U64","name":"ticksAdvanced","required":true},{"constraint":"","kind":"U64","name":"brainTicks","required":true},{"constraint":"","kind":"RationalNs","name":"remainder","required":true},{"constraint":"","kind":"Digest","name":"decisionDigest","required":true},{"constraint":"the commit acknowledgment","kind":"U64","name":"committedStep","required":true}],"name":"TraceAgent","source":"step-v1 8"},{"fields":[{"constraint":"","kind":"Scope","name":"scope","required":true},{"constraint":"sorted by agentId, independent of dispatch order","kind":"array","name":"agents","required":true},{"constraint":"","kind":"Id","name":"batchId","required":true},{"constraint":"","kind":"Digest","name":"controlDigest","required":true},{"constraint":"the world boundary the environment acknowledged","kind":"U64","name":"acknowledgedBoundary","required":true},{"constraint":"sorted by viewId","kind":"array<{viewId:Id,producedStep:U64}>","name":"observationBoundaries","required":true},{"constraint":"task outcome ids in task order","kind":"array","name":"outcomeIds","required":true},{"constraint":"task event ids in task order","kind":"array","name":"eventIds","required":true},{"constraint":"","kind":"U64","name":"publishedBoundary","required":true}],"name":"TraceBehaviour","source":"step-v1 8"},{"fields":[{"constraint":"","kind":"U64","name":"wallTimeNs","required":true},{"constraint":"","kind":"array<{agentId:Id,requestId:DomainRequestId}>","name":"prepareRequestIds","required":true},{"constraint":"","kind":"DomainRequestId","name":"advanceRequestId","required":true},{"constraint":"","kind":"array<{agentId:Id,requestId:DomainRequestId}>","name":"commitRequestIds","required":true},{"constraint":"call-","kind":"array","name":"busCallIds","required":true},{"constraint":"dlv- or own-","kind":"array","name":"deliveryIds","required":true}],"name":"TraceOperational","source":"step-v1 8"},{"fields":[{"constraint":"compared between runs","kind":"TraceBehaviour","name":"behaviour","required":true},{"constraint":"recorded, never compared: wall time and transport identities","kind":"TraceOperational","name":"operational","required":true}],"name":"TransitionTrace","source":"step-v1 8"},{"fields":[{"constraint":"","kind":"SchemaRef","name":"schema","required":true},{"constraint":"canonical JSON of the whole TypedValue <= 32768 bytes","kind":"object","name":"value","required":true}],"name":"TypedValue","source":"ipc-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"viewId","required":true},{"constraint":"1..=4096","kind":"int","name":"width","required":true},{"constraint":"1..=4096","kind":"int","name":"height","required":true},{"constraint":"","kind":"ViewFormat","name":"format","required":true},{"constraint":"exactly 4 x width","kind":"int","name":"rowStride","required":true},{"constraint":"positive integers <= 65535","kind":"{numerator:int,denominator:int}","name":"pixelAspect","required":true},{"constraint":"0..=8","kind":"int","name":"observationDelaySteps","required":true}],"name":"ViewDescriptor","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"viewId","required":true},{"constraint":"max(0, boundary - observationDelaySteps) for required sensory views","kind":"U64","name":"producedStep","required":true},{"constraint":"listed attachment; byteLength == rowStride x height","kind":"ArtifactRef","name":"pixels","required":true}],"name":"ViewRef","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"U64","name":"boundary","required":true},{"constraint":"logical time since episode start","kind":"RationalNs","name":"worldTime","required":true},{"constraint":"<= 64 characters","kind":"string|null","name":"engineFrame","required":false},{"constraint":"<= 8, unique viewId","kind":"array","name":"sensoryViews","required":true},{"constraint":"the descriptor's inspectionSchema","kind":"TypedValue","name":"inspection","required":true},{"constraint":"<= 8, unique viewId","kind":"array","name":"broadcastViews","required":true},{"constraint":"unique streamId","kind":"array","name":"audio","required":true}],"name":"WorldObservation","source":"workers-v1 3"}],"version":1} diff --git a/services/flysim/crates/fly-session-types/fixtures/valid.json b/services/flysim/crates/fly-session-types/fixtures/valid.json index 8f13023..9c2c111 100644 --- a/services/flysim/crates/fly-session-types/fixtures/valid.json +++ b/services/flysim/crates/fly-session-types/fixtures/valid.json @@ -253,6 +253,7 @@ "value": { "brainTicks": "17", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -271,8 +272,8 @@ } }, "note": "", - "canonical": "{\"brainTicks\":\"17\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}]}", - "digest": "de4c4e0a2cc3cac459929480a057495029a74e67e9444ea4e197b403659155ed" + "canonical": "{\"brainTicks\":\"17\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}],\"stimulusRemainingMs\":null}", + "digest": "8b1157faa3b7c12a06622fafdcf69406c5120fa667d0060501d2c6b7b6df308d" }, { "name": "agent telemetry with no tracked roles", @@ -280,6 +281,7 @@ "value": { "brainTicks": "0", "populationRateHz": 0.0, + "stimulusRemainingMs": null, "rates": [], "learning": { "enabled": false, @@ -289,8 +291,8 @@ } }, "note": "", - "canonical": "{\"brainTicks\":\"0\",\"learning\":{\"changed\":\"0\",\"enabled\":false,\"signal\":0,\"updates\":\"0\"},\"populationRateHz\":0,\"rates\":[]}", - "digest": "51c684931ea3d7c54f67a78a90d676944d9527b0bc4465d5a6d4e02b87ff7485" + "canonical": "{\"brainTicks\":\"0\",\"learning\":{\"changed\":\"0\",\"enabled\":false,\"signal\":0,\"updates\":\"0\"},\"populationRateHz\":0,\"rates\":[],\"stimulusRemainingMs\":null}", + "digest": "59bfa4d05fe3f5eb8f2e3bc9361feabb0c5e465735d958b9210c26162b53394d" }, { "name": "session rpc request with a scope", @@ -454,6 +456,7 @@ "telemetry": { "brainTicks": "2500", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -486,8 +489,8 @@ } }, "note": "", - "canonical": "{\"agentId\":\"fly-a\",\"committedStep\":\"0\",\"decisionContextDigest\":\"ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4\",\"graph\":{\"datasetDigest\":\"6c0af1f0784ef63a393ee77d614e8246c625051360f3f1a48838374c5d355b52\",\"indexDigest\":\"52a7a9b441f686ebea398e3d65dbaa87b4fde4f8b2231fff951700548f6a4e42\",\"neuronCount\":\"139255\",\"rateRoles\":[\"kenyon\",\"mbon\"],\"supportedStimuli\":[\"sugar\",\"shock\"]},\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"telemetry\":{\"brainTicks\":\"2500\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}]},\"tickDuration\":{\"denominator\":\"1\",\"numerator\":\"1000000\"},\"warmupTicks\":\"2500\"}", - "digest": "707803be4867dc2f0f3d4bccedfaa7b97b1e52b9af78d21ae6957caba1a7e3f8" + "canonical": "{\"agentId\":\"fly-a\",\"committedStep\":\"0\",\"decisionContextDigest\":\"ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4\",\"graph\":{\"datasetDigest\":\"6c0af1f0784ef63a393ee77d614e8246c625051360f3f1a48838374c5d355b52\",\"indexDigest\":\"52a7a9b441f686ebea398e3d65dbaa87b4fde4f8b2231fff951700548f6a4e42\",\"neuronCount\":\"139255\",\"rateRoles\":[\"kenyon\",\"mbon\"],\"supportedStimuli\":[\"sugar\",\"shock\"]},\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"telemetry\":{\"brainTicks\":\"2500\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}],\"stimulusRemainingMs\":null},\"tickDuration\":{\"denominator\":\"1\",\"numerator\":\"1000000\"},\"warmupTicks\":\"2500\"}", + "digest": "372ee03780ef476ef7e89d8b6a48a1c6afe75394b494f6bc80f6a1aa9f805215" }, { "name": "prepare params", @@ -687,6 +690,7 @@ "telemetry": { "brainTicks": "2534", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -706,8 +710,8 @@ } }, "note": "", - "canonical": "{\"agentId\":\"fly-a\",\"committedStep\":\"42\",\"decisionContextDigest\":\"ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4\",\"telemetry\":{\"brainTicks\":\"2534\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}]}}", - "digest": "9f0a8812101f8f41a147846f6a94b67ae7898ca143fa3cf44ae3a3d5924cd17c" + "canonical": "{\"agentId\":\"fly-a\",\"committedStep\":\"42\",\"decisionContextDigest\":\"ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4\",\"telemetry\":{\"brainTicks\":\"2534\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}],\"stimulusRemainingMs\":null}}", + "digest": "a78233a4bbd20c061cb40f9cf9491060e149379f5d1e6b9036115c0c88014de9" }, { "name": "controller schema", @@ -2408,6 +2412,7 @@ "telemetry": { "brainTicks": "2500", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -2459,8 +2464,8 @@ "eventIds": [] }, "note": "", - "canonical": "{\"agents\":[{\"agentId\":\"fly-a\",\"appliedControls\":null,\"selectedDecision\":null,\"telemetry\":{\"brainTicks\":\"2500\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}]}}],\"descriptorRevision\":\"7\",\"episodeId\":\"episode-1\",\"eventIds\":[],\"media\":{\"audio\":[],\"views\":[{\"pixels\":{\"artifactId\":\"frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"0\",\"viewId\":\"screen\"}]},\"progress\":{\"schema\":{\"digest\":\"80fa973157f334bb3208dfff6f7a2f298e79f73f07c72c33f5725975c6cff8e1\",\"id\":\"pokemon.progress.v1\",\"version\":1},\"value\":{\"rank\":0}},\"publisherIncarnation\":\"pub-1\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"0\"},\"sequence\":\"0\",\"worldTime\":{\"denominator\":\"1\",\"numerator\":\"0\"}}", - "digest": "e9c432eee00e95c28d8a12428aaa9cb71568bcaf079b557b01fb1fde26723da9" + "canonical": "{\"agents\":[{\"agentId\":\"fly-a\",\"appliedControls\":null,\"selectedDecision\":null,\"telemetry\":{\"brainTicks\":\"2500\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}],\"stimulusRemainingMs\":null}}],\"descriptorRevision\":\"7\",\"episodeId\":\"episode-1\",\"eventIds\":[],\"media\":{\"audio\":[],\"views\":[{\"pixels\":{\"artifactId\":\"frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"0\",\"viewId\":\"screen\"}]},\"progress\":{\"schema\":{\"digest\":\"80fa973157f334bb3208dfff6f7a2f298e79f73f07c72c33f5725975c6cff8e1\",\"id\":\"pokemon.progress.v1\",\"version\":1},\"value\":{\"rank\":0}},\"publisherIncarnation\":\"pub-1\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"0\"},\"sequence\":\"0\",\"worldTime\":{\"denominator\":\"1\",\"numerator\":\"0\"}}", + "digest": "8b3ea179376f0dbf44ddbffdc3a93a7751ef03983dfe81df131e415cb8d506a0" }, { "name": "committed snapshot after a transition", @@ -2485,6 +2490,7 @@ "telemetry": { "brainTicks": "2534", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -2608,8 +2614,8 @@ ] }, "note": "", - "canonical": "{\"agents\":[{\"agentId\":\"fly-a\",\"appliedControls\":{\"axes\":[{\"id\":\"stick-x\",\"value\":0},{\"id\":\"trigger\",\"value\":0}],\"buttons\":[{\"down\":true,\"id\":\"a\"},{\"down\":false,\"id\":\"b\"},{\"down\":false,\"id\":\"start\"},{\"down\":false,\"id\":\"select\"},{\"down\":false,\"id\":\"up\"},{\"down\":false,\"id\":\"down\"},{\"down\":false,\"id\":\"left\"},{\"down\":false,\"id\":\"right\"}],\"portId\":\"port-1\"},\"selectedDecision\":{\"schema\":{\"digest\":\"e70451b26fb5462ec63729d8355912dad01c3963a95bec0e6a8452375717ad9d\",\"id\":\"gameboy.intent.v1\",\"version\":1},\"value\":{\"press\":\"a\"}},\"telemetry\":{\"brainTicks\":\"2534\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}]}}],\"descriptorRevision\":\"7\",\"episodeId\":\"episode-1\",\"eventIds\":[\"evt-1\"],\"media\":{\"audio\":[{\"discontinuity\":false,\"firstSample\":\"33600\",\"sampleFrames\":800,\"samples\":{\"artifactId\":\"audio-1\",\"byteLength\":\"6400\",\"contentType\":\"audio/x-f32le\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"streamId\":\"mix\"}],\"views\":[{\"pixels\":{\"artifactId\":\"frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"42\",\"viewId\":\"screen\"}]},\"progress\":{\"schema\":{\"digest\":\"80fa973157f334bb3208dfff6f7a2f298e79f73f07c72c33f5725975c6cff8e1\",\"id\":\"pokemon.progress.v1\",\"version\":1},\"value\":{\"rank\":10}},\"publisherIncarnation\":\"pub-1\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"42\"},\"sequence\":\"42\",\"worldTime\":{\"denominator\":\"1\",\"numerator\":\"700000000\"}}", - "digest": "d5ed83ccb866318b3cf17b53c8a5b1dea2f404b10649f6b640bf5f2b88a31435" + "canonical": "{\"agents\":[{\"agentId\":\"fly-a\",\"appliedControls\":{\"axes\":[{\"id\":\"stick-x\",\"value\":0},{\"id\":\"trigger\",\"value\":0}],\"buttons\":[{\"down\":true,\"id\":\"a\"},{\"down\":false,\"id\":\"b\"},{\"down\":false,\"id\":\"start\"},{\"down\":false,\"id\":\"select\"},{\"down\":false,\"id\":\"up\"},{\"down\":false,\"id\":\"down\"},{\"down\":false,\"id\":\"left\"},{\"down\":false,\"id\":\"right\"}],\"portId\":\"port-1\"},\"selectedDecision\":{\"schema\":{\"digest\":\"e70451b26fb5462ec63729d8355912dad01c3963a95bec0e6a8452375717ad9d\",\"id\":\"gameboy.intent.v1\",\"version\":1},\"value\":{\"press\":\"a\"}},\"telemetry\":{\"brainTicks\":\"2534\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}],\"stimulusRemainingMs\":null}}],\"descriptorRevision\":\"7\",\"episodeId\":\"episode-1\",\"eventIds\":[\"evt-1\"],\"media\":{\"audio\":[{\"discontinuity\":false,\"firstSample\":\"33600\",\"sampleFrames\":800,\"samples\":{\"artifactId\":\"audio-1\",\"byteLength\":\"6400\",\"contentType\":\"audio/x-f32le\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"streamId\":\"mix\"}],\"views\":[{\"pixels\":{\"artifactId\":\"frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"42\",\"viewId\":\"screen\"}]},\"progress\":{\"schema\":{\"digest\":\"80fa973157f334bb3208dfff6f7a2f298e79f73f07c72c33f5725975c6cff8e1\",\"id\":\"pokemon.progress.v1\",\"version\":1},\"value\":{\"rank\":10}},\"publisherIncarnation\":\"pub-1\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"42\"},\"sequence\":\"42\",\"worldTime\":{\"denominator\":\"1\",\"numerator\":\"700000000\"}}", + "digest": "aaef320591a2db47d46b3b0a6b3f2faf563ef7edd73b6b02c7604bb7a50c88bf" }, { "name": "committed snapshot at a boundary installed by a restore", @@ -2634,6 +2640,7 @@ "telemetry": { "brainTicks": "2534", "populationRateHz": 12.5, + "stimulusRemainingMs": null, "rates": [ { "roleId": "kenyon", @@ -2702,8 +2709,8 @@ ] }, "note": "a restore re-establishes a committed boundary this epoch did not run a transition into", - "canonical": "{\"agents\":[{\"agentId\":\"fly-a\",\"appliedControls\":null,\"selectedDecision\":null,\"telemetry\":{\"brainTicks\":\"2534\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}]}}],\"descriptorRevision\":\"7\",\"episodeId\":\"episode-1\",\"eventIds\":[\"evt-1\"],\"media\":{\"audio\":[{\"discontinuity\":false,\"firstSample\":\"33600\",\"sampleFrames\":800,\"samples\":{\"artifactId\":\"audio-1\",\"byteLength\":\"6400\",\"contentType\":\"audio/x-f32le\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"streamId\":\"mix\"}],\"views\":[{\"pixels\":{\"artifactId\":\"frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"42\",\"viewId\":\"screen\"}]},\"progress\":{\"schema\":{\"digest\":\"80fa973157f334bb3208dfff6f7a2f298e79f73f07c72c33f5725975c6cff8e1\",\"id\":\"pokemon.progress.v1\",\"version\":1},\"value\":{\"rank\":10}},\"publisherIncarnation\":\"pub-1\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"42\"},\"sequence\":\"42\",\"worldTime\":{\"denominator\":\"1\",\"numerator\":\"700000000\"}}", - "digest": "79a0ef10ed65c0720c34c7d99de1fe87ec2f0c26028994562cc4655b0f6acd1f" + "canonical": "{\"agents\":[{\"agentId\":\"fly-a\",\"appliedControls\":null,\"selectedDecision\":null,\"telemetry\":{\"brainTicks\":\"2534\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}],\"stimulusRemainingMs\":null}}],\"descriptorRevision\":\"7\",\"episodeId\":\"episode-1\",\"eventIds\":[\"evt-1\"],\"media\":{\"audio\":[{\"discontinuity\":false,\"firstSample\":\"33600\",\"sampleFrames\":800,\"samples\":{\"artifactId\":\"audio-1\",\"byteLength\":\"6400\",\"contentType\":\"audio/x-f32le\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"streamId\":\"mix\"}],\"views\":[{\"pixels\":{\"artifactId\":\"frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"42\",\"viewId\":\"screen\"}]},\"progress\":{\"schema\":{\"digest\":\"80fa973157f334bb3208dfff6f7a2f298e79f73f07c72c33f5725975c6cff8e1\",\"id\":\"pokemon.progress.v1\",\"version\":1},\"value\":{\"rank\":10}},\"publisherIncarnation\":\"pub-1\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"42\"},\"sequence\":\"42\",\"worldTime\":{\"denominator\":\"1\",\"numerator\":\"700000000\"}}", + "digest": "39fd3d104ee42c6d4cb832c8923415952d8b9f686f0d0f3281a6c5bfea56d622" }, { "name": "transition trace", @@ -2891,6 +2898,549 @@ "note": "recorded by step-v1 section 8, excluded from its comparison", "canonical": "{\"advanceRequestId\":\"req-43\",\"busCallIds\":[\"call-100\",\"call-101\",\"call-102\"],\"commitRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-44\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-45\"}],\"deliveryIds\":[\"dlv-7\",\"own-9\"],\"prepareRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-41\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-42\"}],\"wallTimeNs\":\"1234567890\"}", "digest": "07c3402ab0e48b186d56f667da980a15b2a48b21381235cff37523a24ff0dfff" + }, + { + "name": "agent telemetry reporting a running stimulation pulse", + "type": "AgentTelemetry", + "value": { + "brainTicks": "70002", + "populationRateHz": 12.5, + "stimulusRemainingMs": 312.5, + "rates": [ + { + "roleId": "kenyon", + "hz": 3.25 + } + ], + "learning": { + "enabled": true, + "updates": "4", + "changed": "2", + "signal": 0.0 + } + }, + "note": "stimulusRemainingMs is what the coordinator's sugar admission reads from the last commit (2026-09-23)", + "canonical": "{\"brainTicks\":\"70002\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"}],\"stimulusRemainingMs\":312.5}", + "digest": "3f11ace6a1d79c429397d300f45a1ad9f601e89648986b32bf1de410ea73d986" + }, + { + "name": "rollback episode request", + "type": "EpisodeRequest", + "value": { + "kind": "rollback", + "reason": "stall", + "outcome": { + "schema": { + "id": "legacy-ratchet-rollback-v1", + "version": 1, + "digest": "0610f899a4746e5991a07dbc3c847ada15d7c68cedf0664244a3c1d87c175c70" + }, + "value": { + "slotId": "best", + "trigger": "stall" + } + } + }, + "note": "only a composition declaring legacy-ratchet-rollback-v1 may act on it", + "canonical": "{\"kind\":\"rollback\",\"outcome\":{\"schema\":{\"digest\":\"0610f899a4746e5991a07dbc3c847ada15d7c68cedf0664244a3c1d87c175c70\",\"id\":\"legacy-ratchet-rollback-v1\",\"version\":1},\"value\":{\"slotId\":\"best\",\"trigger\":\"stall\"}},\"reason\":\"stall\"}", + "digest": "089505ca3db7f5ab546dc391d0b4ea0a6ca610cd06641d248ad5c9139457a3da" + }, + { + "name": "save slot params", + "type": "SaveSlotParams", + "value": { + "slotId": "best" + }, + "note": "", + "canonical": "{\"slotId\":\"best\"}", + "digest": "f448ea1813ae73979392f4d6a86edfe56d17d1347efef919a1ec2ed949f63b8c" + }, + { + "name": "save slot result", + "type": "SaveSlotResult", + "value": { + "slotId": "best", + "boundary": "4101", + "stateDigest": "5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d", + "byteLength": "199616" + }, + "note": "", + "canonical": "{\"boundary\":\"4101\",\"byteLength\":\"199616\",\"slotId\":\"best\",\"stateDigest\":\"5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d\"}", + "digest": "602100c80393c58536bd5251df42f476c059e42ea0d671fc6683fc1cf3aeab2a" + }, + { + "name": "restore slot params", + "type": "RestoreSlotParams", + "value": { + "slotId": "best", + "priorEpoch": "epoch-7", + "policy": "legacy-ratchet-rollback-v1" + }, + "note": "sent with the new epoch in scope", + "canonical": "{\"policy\":\"legacy-ratchet-rollback-v1\",\"priorEpoch\":\"epoch-7\",\"slotId\":\"best\"}", + "digest": "356e7503f75e3c1722ac68d7f8f95de370f4c4e914c651d24efe7e19954f07f3" + }, + { + "name": "restore slot result", + "type": "RestoreSlotResult", + "value": { + "slotId": "best", + "committedStep": "4101", + "observation": { + "boundary": "4101", + "worldTime": { + "numerator": "35154861328125", + "denominator": "512" + }, + "engineFrame": "4102", + "sensoryViews": [ + { + "viewId": "lcd", + "producedStep": "4101", + "pixels": { + "storeId": "store-1", + "artifactId": "slot-frame-1", + "generation": "1", + "byteLength": "92160", + "contentType": "image/x-rgba8", + "digest": null + } + } + ], + "inspection": { + "schema": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "value": { + "memory": { + "storeId": "store-1", + "artifactId": "wram-4101", + "generation": "1", + "byteLength": "65536", + "contentType": "application/octet-stream", + "digest": null + }, + "romDigest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20" + } + }, + "broadcastViews": [ + { + "viewId": "lcd", + "producedStep": "4101", + "pixels": { + "storeId": "store-1", + "artifactId": "slot-frame-1", + "generation": "1", + "byteLength": "92160", + "contentType": "image/x-rgba8", + "digest": null + } + } + ], + "audio": [] + } + }, + "note": "same boundary, worldTime and engineFrame continue, the slot's archived frame, no audio chunk", + "canonical": "{\"committedStep\":\"4101\",\"observation\":{\"audio\":[],\"boundary\":\"4101\",\"broadcastViews\":[{\"pixels\":{\"artifactId\":\"slot-frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"4101\",\"viewId\":\"lcd\"}],\"engineFrame\":\"4102\",\"inspection\":{\"schema\":{\"digest\":\"d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6\",\"id\":\"gameboy-memory-inspection-v1\",\"version\":1},\"value\":{\"memory\":{\"artifactId\":\"wram-4101\",\"byteLength\":\"65536\",\"contentType\":\"application/octet-stream\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"romDigest\":\"c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20\"}},\"sensoryViews\":[{\"pixels\":{\"artifactId\":\"slot-frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"4101\",\"viewId\":\"lcd\"}],\"worldTime\":{\"denominator\":\"512\",\"numerator\":\"35154861328125\"}},\"slotId\":\"best\"}", + "digest": "81511c014fc260c0230b0e9eeaffe2897f32aef1b811adc18abc2c94a65c8d69" + }, + { + "name": "agent rollback params", + "type": "AgentRollbackParams", + "value": { + "agentId": "fly", + "priorEpoch": "epoch-7", + "policy": "legacy-ratchet-rollback-v1", + "input": { + "boundary": "4101", + "views": [ + { + "viewId": "lcd", + "producedStep": "4101", + "pixels": { + "storeId": "store-1", + "artifactId": "slot-frame-1", + "generation": "1", + "byteLength": "92160", + "contentType": "image/x-rgba8", + "digest": null + } + } + ], + "structured": null + }, + "decisionContext": { + "schema": { + "id": "gameboy-readout-context-v1", + "version": 1, + "digest": "78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d" + }, + "value": { + "boot": false, + "bound": [ + "macro_go_objective", + "macro_talk" + ], + "location": { + "area": 2, + "x": 17, + "y": 9 + } + } + } + }, + "note": "", + "canonical": "{\"agentId\":\"fly\",\"decisionContext\":{\"schema\":{\"digest\":\"78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d\",\"id\":\"gameboy-readout-context-v1\",\"version\":1},\"value\":{\"boot\":false,\"bound\":[\"macro_go_objective\",\"macro_talk\"],\"location\":{\"area\":2,\"x\":17,\"y\":9}}},\"input\":{\"boundary\":\"4101\",\"structured\":null,\"views\":[{\"pixels\":{\"artifactId\":\"slot-frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"4101\",\"viewId\":\"lcd\"}]},\"policy\":\"legacy-ratchet-rollback-v1\",\"priorEpoch\":\"epoch-7\"}", + "digest": "d3683e4a7b297cda897b1408155d86685903016009bd82e1ec2e5d1d35a8e46d" + }, + { + "name": "agent rollback result", + "type": "AgentRollbackResult", + "value": { + "agentId": "fly", + "committedStep": "4101", + "decisionContextDigest": "ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4", + "telemetry": { + "brainTicks": "70002", + "populationRateHz": 12.5, + "stimulusRemainingMs": 312.5, + "rates": [ + { + "roleId": "kenyon", + "hz": 3.25 + } + ], + "learning": { + "enabled": true, + "updates": "4", + "changed": "2", + "signal": 0.0 + } + } + }, + "note": "no tick: committedStep is the scoped step", + "canonical": "{\"agentId\":\"fly\",\"committedStep\":\"4101\",\"decisionContextDigest\":\"ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4\",\"telemetry\":{\"brainTicks\":\"70002\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"}],\"stimulusRemainingMs\":312.5}}", + "digest": "8a109ad370b8a9c00333ea281092c04ed296c473d9189a71a718ec12e89a8d52" + }, + { + "name": "readout context in macros mode", + "type": "GameboyReadoutContext", + "value": { + "boot": false, + "bound": [ + "macro_go_objective", + "macro_talk" + ], + "location": { + "area": 2, + "x": 17, + "y": 9 + } + }, + "note": "", + "canonical": "{\"boot\":false,\"bound\":[\"macro_go_objective\",\"macro_talk\"],\"location\":{\"area\":2,\"x\":17,\"y\":9}}", + "digest": "a11982ed1f461efc82c6dec38254faf94a526991dc5d4c5f9c640aa45338381c" + }, + { + "name": "readout context in raw mode on the title screen", + "type": "GameboyReadoutContext", + "value": { + "boot": true, + "bound": [], + "location": null + }, + "note": "no macro group, boot gate open, no location", + "canonical": "{\"boot\":true,\"bound\":[],\"location\":null}", + "digest": "6d87ccb265f3f8698d993b5338556d8182aaa34acc04af5c32d155cecca58d69" + }, + { + "name": "channels decision with a macro", + "type": "GameboyChannelsDecision", + "value": { + "buttons": [ + { + "id": "up", + "down": true + }, + { + "id": "down", + "down": false + }, + { + "id": "left", + "down": false + }, + { + "id": "right", + "down": false + }, + { + "id": "a", + "down": false + }, + { + "id": "b", + "down": false + }, + { + "id": "start", + "down": false + }, + { + "id": "select", + "down": false + } + ], + "macro": "macro_talk" + }, + "note": "", + "canonical": "{\"buttons\":[{\"down\":true,\"id\":\"up\"},{\"down\":false,\"id\":\"down\"},{\"down\":false,\"id\":\"left\"},{\"down\":false,\"id\":\"right\"},{\"down\":false,\"id\":\"a\"},{\"down\":false,\"id\":\"b\"},{\"down\":false,\"id\":\"start\"},{\"down\":false,\"id\":\"select\"}],\"macro\":\"macro_talk\"}", + "digest": "6a0428a108f8fcd6b08883bc918ed414242f064ef287edd9426407f8ec51f2f1" + }, + { + "name": "channels decision with nothing held", + "type": "GameboyChannelsDecision", + "value": { + "buttons": [ + { + "id": "up", + "down": false + }, + { + "id": "down", + "down": false + }, + { + "id": "left", + "down": false + }, + { + "id": "right", + "down": false + }, + { + "id": "a", + "down": false + }, + { + "id": "b", + "down": false + }, + { + "id": "start", + "down": false + }, + { + "id": "select", + "down": false + } + ], + "macro": null + }, + "note": "", + "canonical": "{\"buttons\":[{\"down\":false,\"id\":\"up\"},{\"down\":false,\"id\":\"down\"},{\"down\":false,\"id\":\"left\"},{\"down\":false,\"id\":\"right\"},{\"down\":false,\"id\":\"a\"},{\"down\":false,\"id\":\"b\"},{\"down\":false,\"id\":\"start\"},{\"down\":false,\"id\":\"select\"}],\"macro\":null}", + "digest": "af16b20c291a798048d1f5459a7316c3ced9dce6a34515204e6e18fc8c70e45a" + }, + { + "name": "memory inspection", + "type": "GameboyMemoryInspection", + "value": { + "memory": { + "storeId": "store-1", + "artifactId": "wram-4101", + "generation": "1", + "byteLength": "65536", + "contentType": "application/octet-stream", + "digest": null + }, + "romDigest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20" + }, + "note": "", + "canonical": "{\"memory\":{\"artifactId\":\"wram-4101\",\"byteLength\":\"65536\",\"contentType\":\"application/octet-stream\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"romDigest\":\"c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20\"}", + "digest": "5423dfb6dde26e1d2f4abd5094fbea79ce5499d71b32e883e4c43140cce8dec2" + }, + { + "name": "rollback request on a game over", + "type": "LegacyRatchetRollbackRequest", + "value": { + "slotId": "best", + "trigger": "game-over" + }, + "note": "", + "canonical": "{\"slotId\":\"best\",\"trigger\":\"game-over\"}", + "digest": "567846fcbbc1373e89d4f9265e1c183786ef55e866c7e475e44ce315b0ddb072" + }, + { + "name": "the legacy profile", + "type": "LegacyGameboyProfile", + "value": { + "profileId": "gameboy-legacy-fafb-v783-v1", + "datasetId": "fafb-v783", + "fingerprintSchema": 1, + "datasetFingerprint": "75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc", + "kernelVersion": "lif-1ms-f64-v2", + "plasticityVersion": "fly-kc-mbon-rstdp-v2", + "tickDuration": { + "numerator": "1000000", + "denominator": "1" + }, + "warmupMs": 2500, + "view": { + "viewId": "lcd", + "width": 160, + "height": 144 + }, + "supportedStimuli": [ + "reward-pulse" + ], + "readoutContextSchema": { + "id": "gameboy-readout-context-v1", + "version": 1, + "digest": "78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d" + }, + "decisionSchema": { + "id": "gameboy-channels-v1", + "version": 1, + "digest": "28bd89bfa41ec73fb2785959a64d9975b4a932e10cd9e51dc4bc29020c823595" + }, + "legacyExceptions": [ + "macro-roles-outside-fingerprint" + ] + }, + "note": "the one document; its digest is the profile AssetRef digest", + "canonical": "{\"datasetFingerprint\":\"75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc\",\"datasetId\":\"fafb-v783\",\"decisionSchema\":{\"digest\":\"28bd89bfa41ec73fb2785959a64d9975b4a932e10cd9e51dc4bc29020c823595\",\"id\":\"gameboy-channels-v1\",\"version\":1},\"fingerprintSchema\":1,\"kernelVersion\":\"lif-1ms-f64-v2\",\"legacyExceptions\":[\"macro-roles-outside-fingerprint\"],\"plasticityVersion\":\"fly-kc-mbon-rstdp-v2\",\"profileId\":\"gameboy-legacy-fafb-v783-v1\",\"readoutContextSchema\":{\"digest\":\"78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d\",\"id\":\"gameboy-readout-context-v1\",\"version\":1},\"supportedStimuli\":[\"reward-pulse\"],\"tickDuration\":{\"denominator\":\"1\",\"numerator\":\"1000000\"},\"view\":{\"height\":144,\"viewId\":\"lcd\",\"width\":160},\"warmupMs\":2500}", + "digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878" + }, + { + "name": "an example legacy composition", + "type": "LegacyGameboyComposition", + "value": { + "compositionId": "pokered-live", + "scheduler": "lockstep-v1", + "profile": { + "id": "gameboy-legacy-fafb-v783-v1", + "digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878", + "byteLength": "1137", + "format": "fly-profile-v1" + }, + "executor": { + "id": "pokered-macros-v1", + "rom": { + "id": "pokered-rom", + "digest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20", + "byteLength": "1048576", + "format": "gb-rom" + }, + "adapter": "pokered-unique8-v6", + "symbolProvenance": "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b", + "mode": "macros", + "macroChannels": [ + "macro_go_objective", + "macro_talk", + "macro_next", + "macro_move_1" + ] + }, + "decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854", + "environment": { + "extensions": [ + "gameboy-slots-v1" + ], + "slots": [ + "best" + ], + "stepDuration": { + "numerator": "8572265625", + "denominator": "512" + }, + "inspectionSchema": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "controllerSchema": { + "id": "gameboy-joypad-v1", + "version": 1, + "digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e" + }, + "setupFrames": 1, + "audio": { + "sampleRate": 48000, + "channels": 2 + } + }, + "episodePolicy": "legacy-ratchet-rollback-v1", + "restore": "legacy-transient-reset", + "checkpointFormatOfRecord": "FLYSIM01", + "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" + }, + "note": "placeholder ROM and decoder digests", + "canonical": "{\"checkpointFormatOfRecord\":\"FLYSIM01\",\"compositionId\":\"pokered-live\",\"decoderConfigDigest\":\"5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854\",\"environment\":{\"audio\":{\"channels\":2,\"sampleRate\":48000},\"controllerSchema\":{\"digest\":\"1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e\",\"id\":\"gameboy-joypad-v1\",\"version\":1},\"extensions\":[\"gameboy-slots-v1\"],\"inspectionSchema\":{\"digest\":\"d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6\",\"id\":\"gameboy-memory-inspection-v1\",\"version\":1},\"setupFrames\":1,\"slots\":[\"best\"],\"stepDuration\":{\"denominator\":\"512\",\"numerator\":\"8572265625\"}},\"episodePolicy\":\"legacy-ratchet-rollback-v1\",\"executor\":{\"adapter\":\"pokered-unique8-v6\",\"id\":\"pokered-macros-v1\",\"macroChannels\":[\"macro_go_objective\",\"macro_talk\",\"macro_next\",\"macro_move_1\"],\"mode\":\"macros\",\"rom\":{\"byteLength\":\"1048576\",\"digest\":\"c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20\",\"format\":\"gb-rom\",\"id\":\"pokered-rom\"},\"symbolProvenance\":\"0cd19d3b877b7dc66d12c7050bed9a7f38154d4b\"},\"flysimCompatibility\":\"lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu\",\"profile\":{\"byteLength\":\"1137\",\"digest\":\"41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878\",\"format\":\"fly-profile-v1\",\"id\":\"gameboy-legacy-fafb-v783-v1\"},\"restore\":\"legacy-transient-reset\",\"scheduler\":\"lockstep-v1\"}", + "digest": "f0142c7f09a1319453b472cdddbc1855062af5733f89d1fbc0b82c0adb52b0c7" + }, + { + "name": "an example legacy composition in raw mode", + "type": "LegacyGameboyComposition", + "value": { + "compositionId": "pokered-live", + "scheduler": "lockstep-v1", + "profile": { + "id": "gameboy-legacy-fafb-v783-v1", + "digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878", + "byteLength": "1137", + "format": "fly-profile-v1" + }, + "executor": { + "id": "pokered-macros-v1", + "rom": { + "id": "pokered-rom", + "digest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20", + "byteLength": "1048576", + "format": "gb-rom" + }, + "adapter": "pokered-unique8-v6", + "symbolProvenance": "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b", + "mode": "raw", + "macroChannels": [] + }, + "decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854", + "environment": { + "extensions": [ + "gameboy-slots-v1" + ], + "slots": [ + "best" + ], + "stepDuration": { + "numerator": "8572265625", + "denominator": "512" + }, + "inspectionSchema": { + "id": "gameboy-memory-inspection-v1", + "version": 1, + "digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6" + }, + "controllerSchema": { + "id": "gameboy-joypad-v1", + "version": 1, + "digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e" + }, + "setupFrames": 1, + "audio": { + "sampleRate": 48000, + "channels": 2 + } + }, + "episodePolicy": "legacy-ratchet-rollback-v1", + "restore": "legacy-transient-reset", + "checkpointFormatOfRecord": "FLYSIM01", + "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" + }, + "note": "", + "canonical": "{\"checkpointFormatOfRecord\":\"FLYSIM01\",\"compositionId\":\"pokered-live\",\"decoderConfigDigest\":\"5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854\",\"environment\":{\"audio\":{\"channels\":2,\"sampleRate\":48000},\"controllerSchema\":{\"digest\":\"1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e\",\"id\":\"gameboy-joypad-v1\",\"version\":1},\"extensions\":[\"gameboy-slots-v1\"],\"inspectionSchema\":{\"digest\":\"d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6\",\"id\":\"gameboy-memory-inspection-v1\",\"version\":1},\"setupFrames\":1,\"slots\":[\"best\"],\"stepDuration\":{\"denominator\":\"512\",\"numerator\":\"8572265625\"}},\"episodePolicy\":\"legacy-ratchet-rollback-v1\",\"executor\":{\"adapter\":\"pokered-unique8-v6\",\"id\":\"pokered-macros-v1\",\"macroChannels\":[],\"mode\":\"raw\",\"rom\":{\"byteLength\":\"1048576\",\"digest\":\"c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20\",\"format\":\"gb-rom\",\"id\":\"pokered-rom\"},\"symbolProvenance\":\"0cd19d3b877b7dc66d12c7050bed9a7f38154d4b\"},\"flysimCompatibility\":\"lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu\",\"profile\":{\"byteLength\":\"1137\",\"digest\":\"41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878\",\"format\":\"fly-profile-v1\",\"id\":\"gameboy-legacy-fafb-v783-v1\"},\"restore\":\"legacy-transient-reset\",\"scheduler\":\"lockstep-v1\"}", + "digest": "efd699cf8cf2485dd2ac8a84ff43c9bc3711a78181f32285ed31a2ee86e16925" } ] } diff --git a/services/flysim/crates/fly-session-types/src/extensions.rs b/services/flysim/crates/fly-session-types/src/extensions.rs new file mode 100644 index 0000000..89962dd --- /dev/null +++ b/services/flysim/crates/fly-session-types/src/extensions.rs @@ -0,0 +1,414 @@ +//! The extension methods of the 2026-09-23 amendments (RT-01a): environment slots and the +//! agent side of a declared rollback policy. +//! +//! `workers-v1` section 7 specifies them. They exist because the operator decided (2026-09-23) +//! that the live Game Boy fly runs on this framework rather than beside it, and its ratchet +//! rolls the game back to a saved slot while the brain continues. The shapes are generic: a +//! slot is an environment-held saved state named by an id, and a rollback is an agent +//! installing a new input and context under a new epoch without a tick. Which composition may +//! use them is a capability question, answered by `Worker.Hello`: +//! +//! - an environment that answers `Environment.SaveSlot` / `Environment.RestoreSlot` advertises +//! [`SLOTS_CAPABILITY`]; +//! - an agent that answers `Agent.Rollback` advertises [`ROLLBACK_CAPABILITY`]. +//! +//! A worker that does not advertise the capability answers `UNSUPPORTED`, mutation none. Nothing +//! Game Boy specific is in these payloads: the console lives in the registered schemas of +//! [`crate::gameboy`], carried inside `TypedValue`s. + +use flybus::wire::Fields; +use serde_json::Value; + +use crate::scalar::{DomainType, Result, Scope, TypedValue, err, is_digest, is_id, obj, u64_json}; +use crate::workers::{AgentTelemetry, SensoryInput, WorldObservation}; + +/// The environment capability that carries `Environment.SaveSlot` and `Environment.RestoreSlot`. +pub const SLOTS_CAPABILITY: &str = "gameboy-slots-v1"; +/// The agent capability that carries `Agent.Rollback`, and the policy id it applies. +pub const ROLLBACK_CAPABILITY: &str = "legacy-ratchet-rollback-v1"; +/// The only rollback policy this contract defines. +pub const ROLLBACK_POLICY: &str = "legacy-ratchet-rollback-v1"; + +pub const METHOD_SAVE_SLOT: &str = "Environment.SaveSlot"; +pub const METHOD_RESTORE_SLOT: &str = "Environment.RestoreSlot"; +pub const METHOD_AGENT_ROLLBACK: &str = "Agent.Rollback"; + +/// Slots one environment may hold. Not a stated bound; recorded in the schema set. +pub const MAX_SLOTS: usize = 4; + +fn policy_ok(policy: &str, what: &str) -> Result<()> { + if policy != ROLLBACK_POLICY { + return err(format!( + "{what}: policy must be {ROLLBACK_POLICY}, the only rollback policy defined" + )); + } + Ok(()) +} + +// --------------------------------------------------------------------------------------------- +// Environment.SaveSlot + +/// `Environment.SaveSlot` params. Scope is the committed boundary the slot records. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SaveSlotParams { + pub slot_id: String, +} + +impl DomainType for SaveSlotParams { + const TYPE_NAME: &'static str = "SaveSlotParams"; + + fn from_json(value: &Value) -> Result { + let mut f = Fields::new(value, "SaveSlotParams")?; + let slot_id = f.id("slotId")?; + f.finish()?; + let p = SaveSlotParams { slot_id }; + p.validate()?; + Ok(p) + } + + fn to_json(&self) -> Value { + obj(vec![("slotId", self.slot_id.clone().into())]) + } + + fn validate(&self) -> Result<()> { + if !is_id(&self.slot_id) { + return err("SaveSlotParams: slotId is not a valid id"); + } + Ok(()) + } +} + +/// `Environment.SaveSlot` result: which boundary the slot now holds, and the digest and length +/// of the saved state bytes, so a later restore can be tied to exactly this save. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct SaveSlotResult { + pub slot_id: String, + pub boundary: u64, + pub state_digest: String, + pub byte_length: u64, +} + +impl SaveSlotResult { + /// The slot records the committed boundary the call was scoped to. + pub fn validate_against_scope(&self, scope: &Scope) -> Result<()> { + self.validate()?; + if self.boundary != scope.step { + return err(format!( + "SaveSlotResult: boundary {} must be the scoped committed step {}", + self.boundary, scope.step + )); + } + Ok(()) + } +} + +impl DomainType for SaveSlotResult { + const TYPE_NAME: &'static str = "SaveSlotResult"; + + fn from_json(value: &Value) -> Result { + let mut f = Fields::new(value, "SaveSlotResult")?; + let slot_id = f.id("slotId")?; + let boundary = f.u64_string("boundary")?; + let state_digest = f.string("stateDigest")?.to_owned(); + let byte_length = f.u64_string("byteLength")?; + f.finish()?; + let r = SaveSlotResult { + slot_id, + boundary, + state_digest, + byte_length, + }; + r.validate()?; + Ok(r) + } + + fn to_json(&self) -> Value { + obj(vec![ + ("slotId", self.slot_id.clone().into()), + ("boundary", u64_json(self.boundary)), + ("stateDigest", self.state_digest.clone().into()), + ("byteLength", u64_json(self.byte_length)), + ]) + } + + fn validate(&self) -> Result<()> { + if !is_id(&self.slot_id) { + return err("SaveSlotResult: slotId is not a valid id"); + } + if !is_digest(&self.state_digest) { + return err("SaveSlotResult: stateDigest must be 64 lowercase hex digits"); + } + if self.byte_length == 0 { + return err("SaveSlotResult: byteLength must be positive"); + } + Ok(()) + } +} + +// --------------------------------------------------------------------------------------------- +// Environment.RestoreSlot + +/// `Environment.RestoreSlot` params. Scope is the NEW epoch at the committed boundary the +/// rollback is applied at; `priorEpoch` names the epoch the environment must currently be in. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RestoreSlotParams { + pub slot_id: String, + pub prior_epoch: String, + pub policy: String, +} + +impl RestoreSlotParams { + /// A rollback moves to a new epoch: the prior epoch cannot be the scoped one. + pub fn validate_against_scope(&self, scope: &Scope) -> Result<()> { + self.validate()?; + if self.prior_epoch == scope.epoch { + return err("RestoreSlotParams: priorEpoch must differ from the scoped (new) epoch"); + } + Ok(()) + } +} + +impl DomainType for RestoreSlotParams { + const TYPE_NAME: &'static str = "RestoreSlotParams"; + + fn from_json(value: &Value) -> Result { + let mut f = Fields::new(value, "RestoreSlotParams")?; + let slot_id = f.id("slotId")?; + let prior_epoch = f.id("priorEpoch")?; + let policy = f.id("policy")?; + f.finish()?; + let p = RestoreSlotParams { + slot_id, + prior_epoch, + policy, + }; + p.validate()?; + Ok(p) + } + + fn to_json(&self) -> Value { + obj(vec![ + ("slotId", self.slot_id.clone().into()), + ("priorEpoch", self.prior_epoch.clone().into()), + ("policy", self.policy.clone().into()), + ]) + } + + fn validate(&self) -> Result<()> { + if !is_id(&self.slot_id) { + return err("RestoreSlotParams: slotId is not a valid id"); + } + if !is_id(&self.prior_epoch) { + return err("RestoreSlotParams: priorEpoch is not a valid id"); + } + policy_ok(&self.policy, "RestoreSlotParams") + } +} + +/// `Environment.RestoreSlot` result: the restored world at the same boundary number under the +/// new epoch. It ran no transition, so it carries no audio chunk. +#[derive(Clone, Debug, PartialEq)] +pub struct RestoreSlotResult { + pub slot_id: String, + pub committed_step: u64, + pub observation: WorldObservation, +} + +impl RestoreSlotResult { + pub fn validate_against_scope(&self, scope: &Scope) -> Result<()> { + self.validate()?; + if self.committed_step != scope.step { + return err(format!( + "RestoreSlotResult: committedStep {} must be the scoped step {}", + self.committed_step, scope.step + )); + } + Ok(()) + } +} + +impl DomainType for RestoreSlotResult { + const TYPE_NAME: &'static str = "RestoreSlotResult"; + + fn from_json(value: &Value) -> Result { + let mut f = Fields::new(value, "RestoreSlotResult")?; + let slot_id = f.id("slotId")?; + let committed_step = f.u64_string("committedStep")?; + let observation = WorldObservation::from_json(f.value("observation")?)?; + f.finish()?; + let r = RestoreSlotResult { + slot_id, + committed_step, + observation, + }; + r.validate()?; + Ok(r) + } + + fn to_json(&self) -> Value { + obj(vec![ + ("slotId", self.slot_id.clone().into()), + ("committedStep", u64_json(self.committed_step)), + ("observation", self.observation.to_json()), + ]) + } + + fn validate(&self) -> Result<()> { + if !is_id(&self.slot_id) { + return err("RestoreSlotResult: slotId is not a valid id"); + } + self.observation.validate()?; + if self.observation.boundary != self.committed_step { + return err("RestoreSlotResult: the observation boundary must be the committed step"); + } + if !self.observation.audio.is_empty() { + return err( + "RestoreSlotResult: a restored slot ran no transition and carries no audio chunk", + ); + } + Ok(()) + } +} + +// --------------------------------------------------------------------------------------------- +// Agent.Rollback + +/// `Agent.Rollback` params. Scope is the new epoch at the committed boundary; the agent must be +/// Ready at `priorEpoch` and that same step. +#[derive(Clone, Debug, PartialEq)] +pub struct AgentRollbackParams { + pub agent_id: String, + pub prior_epoch: String, + pub policy: String, + pub input: SensoryInput, + pub decision_context: TypedValue, +} + +impl AgentRollbackParams { + /// The installed input is the restored boundary, which is the scoped step. + pub fn validate_against_scope(&self, scope: &Scope) -> Result<()> { + self.validate()?; + if self.prior_epoch == scope.epoch { + return err("AgentRollbackParams: priorEpoch must differ from the scoped (new) epoch"); + } + if self.input.boundary != scope.step { + return err(format!( + "AgentRollbackParams: input.boundary {} must be the scoped step {}", + self.input.boundary, scope.step + )); + } + Ok(()) + } +} + +impl DomainType for AgentRollbackParams { + const TYPE_NAME: &'static str = "AgentRollbackParams"; + + fn from_json(value: &Value) -> Result { + let mut f = Fields::new(value, "AgentRollbackParams")?; + let agent_id = f.id("agentId")?; + let prior_epoch = f.id("priorEpoch")?; + let policy = f.id("policy")?; + let input = SensoryInput::from_json(f.value("input")?)?; + let decision_context = TypedValue::from_json(f.value("decisionContext")?)?; + f.finish()?; + let p = AgentRollbackParams { + agent_id, + prior_epoch, + policy, + input, + decision_context, + }; + p.validate()?; + Ok(p) + } + + fn to_json(&self) -> Value { + obj(vec![ + ("agentId", self.agent_id.clone().into()), + ("priorEpoch", self.prior_epoch.clone().into()), + ("policy", self.policy.clone().into()), + ("input", self.input.to_json()), + ("decisionContext", self.decision_context.to_json()), + ]) + } + + fn validate(&self) -> Result<()> { + if !is_id(&self.agent_id) { + return err("AgentRollbackParams: agentId is not a valid id"); + } + if !is_id(&self.prior_epoch) { + return err("AgentRollbackParams: priorEpoch is not a valid id"); + } + policy_ok(&self.policy, "AgentRollbackParams")?; + self.input.validate()?; + self.decision_context.validate() + } +} + +/// `Agent.Rollback` result: the same acknowledgment shape as a commit, at the same boundary. +#[derive(Clone, Debug, PartialEq)] +pub struct AgentRollbackResult { + pub agent_id: String, + pub committed_step: u64, + pub decision_context_digest: String, + pub telemetry: AgentTelemetry, +} + +impl AgentRollbackResult { + pub fn validate_against_scope(&self, scope: &Scope) -> Result<()> { + self.validate()?; + if self.committed_step != scope.step { + return err(format!( + "AgentRollbackResult: committedStep {} must be the scoped step {}; a rollback runs no tick", + self.committed_step, scope.step + )); + } + Ok(()) + } +} + +impl DomainType for AgentRollbackResult { + const TYPE_NAME: &'static str = "AgentRollbackResult"; + + fn from_json(value: &Value) -> Result { + let mut f = Fields::new(value, "AgentRollbackResult")?; + let agent_id = f.id("agentId")?; + let committed_step = f.u64_string("committedStep")?; + let decision_context_digest = f.string("decisionContextDigest")?.to_owned(); + let telemetry = AgentTelemetry::from_json(f.value("telemetry")?)?; + f.finish()?; + let r = AgentRollbackResult { + agent_id, + committed_step, + decision_context_digest, + telemetry, + }; + r.validate()?; + Ok(r) + } + + fn to_json(&self) -> Value { + obj(vec![ + ("agentId", self.agent_id.clone().into()), + ("committedStep", u64_json(self.committed_step)), + ( + "decisionContextDigest", + self.decision_context_digest.clone().into(), + ), + ("telemetry", self.telemetry.to_json()), + ]) + } + + fn validate(&self) -> Result<()> { + if !is_id(&self.agent_id) { + return err("AgentRollbackResult: agentId is not a valid id"); + } + if !is_digest(&self.decision_context_digest) { + return err( + "AgentRollbackResult: decisionContextDigest must be 64 lowercase hex digits", + ); + } + self.telemetry.validate() + } +} diff --git a/services/flysim/crates/fly-session-types/src/gameboy.rs b/services/flysim/crates/fly-session-types/src/gameboy.rs new file mode 100644 index 0000000..5e2d630 --- /dev/null +++ b/services/flysim/crates/fly-session-types/src/gameboy.rs @@ -0,0 +1,1238 @@ +//! The legacy Game Boy composition's registered schemas and declarations (PROF-02a). +//! +//! [`legacy-gameboy-v1`] is the contract. The operator decided on 2026-09-23 that the live fly +//! is ported onto the session framework as a declared legacy profile rather than kept outside +//! it; this module is the machine-readable half of that decision. +//! +//! Nothing here is a generic session type. Every value below travels inside a `TypedValue` +//! (whose [`SchemaRef`] names one of [`PAYLOAD_SCHEMAS`]) or is a document an `AssetRef` or the +//! composition digest names. The generic types in [`crate::workers`] stay free of console +//! state, which is what `workers-v1` section 6 and ENV-01's acceptance require. +//! +//! - A **payload schema** is a declaration `(id, version, source, fields)`. Its `SchemaRef` +//! digest is the SHA-256 of that declaration's canonical JSON, so the reference changes when +//! a field, kind or constraint does and not when this file is reformatted -- the rule +//! `contractDigest` follows for the session schema set. +//! - The **legacy profile** `gameboy-legacy-fafb-v783-v1` is one fixed document. Its +//! `AssetRef.digest` is the SHA-256 of its canonical JSON ([`profile_digest`]). +//! - The **legacy composition** declaration carries everything that is behaviour but is not +//! in the legacy compatibility string -- the decoder configuration and the macro channels +//! among them -- and its digest joins the session's `compositionDigest`. +//! +//! `fixtures/gameboy-legacy.json` is the rendered set with every digest, regenerated by the +//! `update_fixtures` example and read by `packages/session-types`. +//! +//! [`legacy-gameboy-v1`]: ../../../../docs/design/session-framework/legacy-gameboy-v1.md + +use flybus::wire::{ArtifactRef, Fields}; +use serde_json::Value; + +use crate::canonical; +use crate::extensions::{MAX_SLOTS, ROLLBACK_POLICY, SLOTS_CAPABILITY}; +use crate::scalar::{ + DomainType, RationalNs, Result, SchemaRef, TypedValue, bounded_string, enumeration, err, + is_digest, is_id, obj, require_unique, wire_err, +}; +use crate::schema::{FieldSchema, TypeSchema, opt, req}; +use crate::workers::AssetRef; + +// --------------------------------------------------------------------------------------------- +// Constants of the legacy profile + +/// The legacy profile id. +pub const PROFILE_ID: &str = "gameboy-legacy-fafb-v783-v1"; +/// The dataset the profile is pinned to. +pub const DATASET_ID: &str = "fafb-v783"; +/// The dataset fingerprint schema: the seven SHA-256 digests joined with `:`. +pub const FINGERPRINT_SCHEMA: u64 = 1; +/// Today's schema-1 fingerprint of `data/fafb-v783`, byte for byte what +/// `flybrain_core::dataset::fingerprint_dataset` computes and what segment 2 of the legacy +/// compatibility string carries. `flysim`'s `legacy_profile_identity` test recomputes it from +/// the committed dataset. +pub const FAFB_V783_FINGERPRINT: &str = "75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:\ +1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:\ +63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:\ +f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:\ +ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:\ +b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:\ +dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc"; +/// The pinned default kernel version (CLAUDE.md): unchanged by this profile. +pub const KERNEL_VERSION: &str = "lif-1ms-f64-v2"; +/// The pinned default plasticity version: unchanged by this profile. +pub const PLASTICITY_VERSION: &str = "fly-kc-mbon-rstdp-v2"; +/// Warm-up with learning disabled on a fresh start, in brain milliseconds. +pub const WARMUP_MS: u64 = 2_500; +/// The one stimulus kind: the reward-population pulse that both sugar and task reward events +/// drive through `stimulate(durationMs)`. +pub const STIMULUS_REWARD_PULSE: &str = "reward-pulse"; +/// The legacy exception the profile declares rather than repairs: the `macro_*` roles are +/// merged after the fingerprint is taken (`docs/design/malecns-modular-sessions.md` 2.2). +pub const EXCEPTION_MACRO_ROLES: &str = "macro-roles-outside-fingerprint"; +/// The profile document's AssetRef format. +pub const PROFILE_FORMAT: &str = "fly-profile-v1"; + +/// The sensory view: the LCD, native size. +pub const VIEW_ID: &str = "lcd"; +pub const VIEW_WIDTH: u64 = 160; +pub const VIEW_HEIGHT: u64 = 144; + +/// The joypad, in the bit order of `GAMEBOY_BUTTON_BITS` (`docs/readout.md`). +pub const GAMEBOY_BUTTONS: [&str; 8] = ["up", "down", "left", "right", "a", "b", "start", "select"]; + +/// The memory image is the whole CPU address space, `$0000..=$FFFF`. +pub const MEMORY_IMAGE_BYTES: u64 = 65_536; + +/// The executor extension. +pub const EXECUTOR_ID: &str = "pokered-macros-v1"; +/// The restore semantics the composition declares. +pub const RESTORE_SEMANTICS: &str = "legacy-transient-reset"; +/// The checkpoint format of record until RETIRE-01. +pub const CHECKPOINT_FORMAT_OF_RECORD: &str = "FLYSIM01"; +/// The scheduler: the legacy composition runs in lockstep-v1. +pub const SCHEDULER: &str = "lockstep-v1"; +/// The environment's setup scaffold: one frame with no button down before `O[0]`. +pub const SETUP_FRAMES: u64 = 1; +/// Macro channels in one decoder configuration (the 64-role limit). +pub const MAX_MACRO_CHANNELS: usize = 64; +/// The legacy compatibility string is 648 bytes today; bounded so a declaration stays small. +pub const MAX_COMPATIBILITY_BYTES: usize = 1_024; + +/// One Game Boy frame, 70224 cycles of a 4194304 Hz clock, in nanoseconds: +/// `8572265625/512` exactly. The legacy `f64` constant `1000 / (4194304 / 70224)` is exactly +/// `548625/32768` ms, which is this value, so the legacy accumulator and the rational one of +/// `step-v1` section 5 produce the same ticks and remainders (`legacy-gameboy-v1` section 3). +pub fn step_duration() -> RationalNs { + RationalNs::new(8_572_265_625, 512).expect("the Game Boy frame is a reduced rational") +} + +/// One model tick: 1 ms. +pub fn tick_duration() -> RationalNs { + RationalNs::new(1_000_000, 1).expect("1 ms") +} + +// --------------------------------------------------------------------------------------------- +// Payload schemas + +/// A registered `TypedValue` payload schema. +#[derive(Clone, Copy, Debug)] +pub struct PayloadSchema { + pub id: &'static str, + pub version: u16, + /// Where the schema is specified. + pub source: &'static str, + pub fields: &'static [FieldSchema], +} + +impl PayloadSchema { + /// The declaration the digest is taken over. + pub fn declaration(&self) -> Value { + obj(vec![ + ("registry", "fly-session-payload-schema-v1".into()), + ("id", self.id.into()), + ("version", Value::from(u64::from(self.version))), + ("source", self.source.into()), + ("fields", fields_json(self.fields)), + ]) + } + + /// `SchemaRef` whose digest is the SHA-256 of the canonical declaration. + pub fn schema_ref(&self) -> SchemaRef { + let digest = canonical::digest_of(&self.declaration()).expect("canonicalizable"); + SchemaRef::new(self.id, self.version, &digest).expect("a registered schema is valid") + } +} + +fn fields_json(fields: &[FieldSchema]) -> Value { + Value::Array( + fields + .iter() + .map(|f| { + obj(vec![ + ("name", f.name.into()), + ("kind", f.kind.into()), + ("required", Value::Bool(f.required)), + ("constraint", f.constraint.into()), + ]) + }) + .collect(), + ) +} + +/// The readout context the task hands the decoder with every Prepare. +pub const READOUT_CONTEXT: PayloadSchema = PayloadSchema { + id: "gameboy-readout-context-v1", + version: 1, + source: "legacy-gameboy-v1 5", + fields: &[ + req( + "boot", + "bool", + "the adapter's boot gate after the last transition: permissive Start/Select variant", + ), + req( + "bound", + "array", + "<= 64, unique, a subset of the composition's macroChannels in their order; [] in raw mode", + ), + opt( + "location", + "{area:int,x:int,y:int}|null", + "each 0..=4294967295; the adapter's location after the last transition; null is no information", + ), + ], +}; + +/// The decision the agent returns from Prepare. +pub const CHANNELS: PayloadSchema = PayloadSchema { + id: "gameboy-channels-v1", + version: 1, + source: "legacy-gameboy-v1 6", + fields: &[ + req( + "buttons", + "array<{id:Id,down:bool}>", + "exactly up,down,left,right,a,b,start,select in that order", + ), + opt( + "macro", + "ChannelName|null", + "the macro-group channel active in this decode; one of the context's bound", + ), + ], +}; + +/// The controller schema of the one port. +pub const JOYPAD: PayloadSchema = PayloadSchema { + id: "gameboy-joypad-v1", + version: 1, + source: "legacy-gameboy-v1 7", + fields: &[req( + "buttons", + "const", + "up,down,left,right,a,b,start,select; no axes; one port", + )], +}; + +/// The inspection the environment returns with every observation. +pub const MEMORY_INSPECTION: PayloadSchema = PayloadSchema { + id: "gameboy-memory-inspection-v1", + version: 1, + source: "legacy-gameboy-v1 8", + fields: &[ + req( + "memory", + "ArtifactRef", + "listed attachment; byteLength 65536; the CPU address space $0000..=$FFFF at this boundary, read-only", + ), + req( + "romDigest", + "Digest", + "== EnvironmentDescriptor.contentDigest == the executor's rom AssetRef digest", + ), + ], +}; + +/// The outcome of an `EpisodeRequest` of kind `rollback`. +pub const ROLLBACK_REQUEST: PayloadSchema = PayloadSchema { + id: "legacy-ratchet-rollback-v1", + version: 1, + source: "legacy-gameboy-v1 11", + fields: &[ + req( + "slotId", + "Id", + "one of the composition's declared slots, saved earlier", + ), + req("trigger", "RollbackTrigger", "stall or game-over"), + ], +}; + +/// Every registered payload schema of the legacy composition. +pub const PAYLOAD_SCHEMAS: &[PayloadSchema] = &[ + READOUT_CONTEXT, + CHANNELS, + JOYPAD, + MEMORY_INSPECTION, + ROLLBACK_REQUEST, +]; + +/// The Rust type names of this module and what they are declared as in [`extension_set`]: +/// a payload schema id or a declaration name. Every readable type is either in the session +/// schema set or here, so none can ship outside a digest. +pub const EXTENSION_TYPES: &[(&str, &str)] = &[ + ("GameboyReadoutContext", "gameboy-readout-context-v1"), + ("GameboyChannelsDecision", "gameboy-channels-v1"), + ("GameboyMemoryInspection", "gameboy-memory-inspection-v1"), + ("LegacyRatchetRollbackRequest", "legacy-ratchet-rollback-v1"), + ("LegacyGameboyProfile", "LegacyGameboyProfile"), + ("LegacyGameboyComposition", "LegacyGameboyComposition"), +]; + +/// The rollback triggers the ratchet has (`flybrain-gb` `ratchet.rs`). +pub const ROLLBACK_TRIGGERS: &[&str] = &["stall", "game-over"]; +/// The macro modes a composition may run. +pub const MACRO_MODES: &[&str] = &["raw", "macros"]; + +// --------------------------------------------------------------------------------------------- +// Declarations + +/// The profile document and the composition declaration, as rows. +pub const DECLARATIONS: &[TypeSchema] = &[ + TypeSchema { + name: "LegacyGameboyProfile", + source: "legacy-gameboy-v1 2", + fields: &[ + req("profileId", "const", "\"gameboy-legacy-fafb-v783-v1\""), + req("datasetId", "const", "\"fafb-v783\""), + req("fingerprintSchema", "const", "1"), + req( + "datasetFingerprint", + "string", + "today's schema-1 fingerprint, seven digests joined with ':'", + ), + req("kernelVersion", "const", "\"lif-1ms-f64-v2\""), + req("plasticityVersion", "const", "\"fly-kc-mbon-rstdp-v2\""), + req("tickDuration", "RationalNs", "1000000/1"), + req("warmupMs", "const", "2500"), + req("view", "{viewId:Id,width:int,height:int}", "lcd, 160, 144"), + req("supportedStimuli", "array", "[\"reward-pulse\"]"), + req( + "readoutContextSchema", + "SchemaRef", + "gameboy-readout-context-v1", + ), + req("decisionSchema", "SchemaRef", "gameboy-channels-v1"), + req( + "legacyExceptions", + "array", + "[\"macro-roles-outside-fingerprint\"]", + ), + ], + }, + TypeSchema { + name: "LegacyGameboyComposition", + source: "legacy-gameboy-v1 12", + fields: &[ + req("compositionId", "Id", ""), + req("scheduler", "const", "\"lockstep-v1\""), + req( + "profile", + "AssetRef", + "format fly-profile-v1; digest == the legacy profile's", + ), + req( + "executor", + "{id:const,rom:AssetRef,adapter:Id,symbolProvenance:string,mode:MacroMode,macroChannels:array}", + "pokered-macros-v1; task and executor one object; macroChannels [] iff mode raw, else <= 64 unique in decoder order", + ), + req( + "decoderConfigDigest", + "Digest", + "SHA-256 of the canonical JSON of the effective DecoderConfig (TypeScript shape)", + ), + req( + "environment", + "{extensions:array,slots:array,stepDuration:RationalNs,inspectionSchema:SchemaRef,controllerSchema:SchemaRef,setupFrames:const,audio:{sampleRate:int,channels:const}}", + "[\"gameboy-slots-v1\"]; 1..=4 unique slots; 8572265625/512; memory inspection; joypad; 1 setup frame; 2 channels", + ), + req("episodePolicy", "const", "\"legacy-ratchet-rollback-v1\""), + req("restore", "const", "\"legacy-transient-reset\""), + req("checkpointFormatOfRecord", "const", "\"FLYSIM01\""), + req( + "flysimCompatibility", + "string", + "<= 1024 bytes; the FLYSIM01 string; its kernel, adapter, fingerprint, plasticity and pokered segments agree with this declaration", + ), + ], + }, +]; + +/// The whole extension set: payload schemas with their references, and the declaration rows. +pub fn extension_set() -> Value { + let mut payloads: Vec<&PayloadSchema> = PAYLOAD_SCHEMAS.iter().collect(); + payloads.sort_by_key(|p| p.id); + let mut declarations: Vec<&TypeSchema> = DECLARATIONS.iter().collect(); + declarations.sort_by_key(|d| d.name); + obj(vec![ + ("contract", "fly-session-types/legacy-gameboy".into()), + ("version", Value::from(1u64)), + ( + "scalars", + obj(vec![( + "ChannelName", + "^[a-z][a-z0-9_]{0,63}$: a decoder channel or rate-role name; not an Id, because the legacy role names carry '_'".into(), + )]), + ), + ( + "enums", + obj(vec![ + ( + "MacroMode", + Value::Array(MACRO_MODES.iter().map(|m| (*m).into()).collect()), + ), + ( + "RollbackTrigger", + Value::Array(ROLLBACK_TRIGGERS.iter().map(|m| (*m).into()).collect()), + ), + ]), + ), + ( + "payloadSchemas", + Value::Array( + payloads + .iter() + .map(|p| { + obj(vec![ + ("declaration", p.declaration()), + ("schemaRef", p.schema_ref().to_json()), + ]) + }) + .collect(), + ), + ), + ( + "declarations", + Value::Array( + declarations + .iter() + .map(|d| { + obj(vec![ + ("name", d.name.into()), + ("source", d.source.into()), + ("fields", fields_json(d.fields)), + ]) + }) + .collect(), + ), + ), + ]) +} + +/// SHA-256 of the canonical extension set. +pub fn extension_set_digest() -> String { + canonical::digest_of(&extension_set()).expect("canonicalizable") +} + +// --------------------------------------------------------------------------------------------- +// Scalars and helpers + +/// `ChannelName`: a decoder channel or rate-role name, `^[a-z][a-z0-9_]{0,63}$`. +pub fn is_channel_name(s: &str) -> bool { + let bytes = s.as_bytes(); + !bytes.is_empty() + && bytes.len() <= 64 + && bytes[0].is_ascii_lowercase() + && bytes + .iter() + .all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || *b == b'_') +} + +fn channel_list(f: &mut Fields<'_>, key: &'static str, hi: usize) -> Result> { + let items = f.array(key, 0, hi)?; + let mut out = Vec::with_capacity(items.len()); + for item in items { + match item.as_str() { + Some(s) if is_channel_name(s) => out.push(s.to_owned()), + _ => return err(format!("{key}: every entry must be a channel name")), + } + } + require_unique(out.iter().map(String::as_str), key)?; + Ok(out) +} + +fn u32_int(f: &mut Fields<'_>, key: &'static str) -> Result { + Ok(f.int(key, 0, u64::from(u32::MAX))? as u32) +} + +fn exact(found: &str, expected: &str, what: &str) -> Result<()> { + if found != expected { + return err(format!("{what} must be {expected:?}, found {found:?}")); + } + Ok(()) +} + +fn id_array(f: &mut Fields<'_>, key: &'static str, lo: usize, hi: usize) -> Result> { + let ids = crate::scalar::id_list(f, key, lo, hi)?; + require_unique(ids.iter().map(String::as_str), key)?; + Ok(ids) +} + +fn schema_matches(found: &SchemaRef, schema: &PayloadSchema, what: &str) -> Result<()> { + if *found != schema.schema_ref() { + return err(format!( + "{what} must be the registered {} reference", + schema.id + )); + } + Ok(()) +} + +/// Reads a `TypedValue` of a registered schema, refusing any other reference. +fn typed(value: &TypedValue, schema: &PayloadSchema) -> Result { + schema_matches(&value.schema, schema, "TypedValue.schema")?; + T::from_json(&value.value) +} + +fn wrap(value: &T, schema: &PayloadSchema) -> TypedValue { + TypedValue::new(schema.schema_ref(), value.to_json()).expect("a registered value fits") +} + +// --------------------------------------------------------------------------------------------- +// gameboy-readout-context-v1 + +/// Where the player is: the adapter's `(area, x, y)`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct Location { + pub area: u32, + pub x: u32, + pub y: u32, +} + +/// `gameboy-readout-context-v1`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ReadoutContext { + pub boot: bool, + pub bound: Vec, + pub location: Option, +} + +impl ReadoutContext { + pub fn from_typed(value: &TypedValue) -> Result { + typed(value, &READOUT_CONTEXT) + } + + pub fn to_typed(&self) -> TypedValue { + wrap(self, &READOUT_CONTEXT) + } + + /// `bound` is a subset of the composition's macro channels, in their order. + pub fn validate_against(&self, macro_channels: &[String]) -> Result<()> { + self.validate()?; + let mut cursor = 0; + for channel in &self.bound { + match macro_channels[cursor..].iter().position(|c| c == channel) { + Some(offset) => cursor += offset + 1, + None => { + return err(format!( + "ReadoutContext: bound channel {channel:?} is not a macro channel of the composition, or is out of order" + )); + } + } + } + Ok(()) + } +} + +impl DomainType for ReadoutContext { + const TYPE_NAME: &'static str = "GameboyReadoutContext"; + + fn from_json(value: &Value) -> Result { + let mut f = Fields::new(value, "GameboyReadoutContext")?; + let boot = f.boolean("boot")?; + let bound = channel_list(&mut f, "bound", MAX_MACRO_CHANNELS)?; + let location = match f.value("location")? { + Value::Null => None, + v => { + let mut l = Fields::new(v, "GameboyReadoutContext.location")?; + let area = u32_int(&mut l, "area")?; + let x = u32_int(&mut l, "x")?; + let y = u32_int(&mut l, "y")?; + l.finish()?; + Some(Location { area, x, y }) + } + }; + f.finish()?; + let c = ReadoutContext { + boot, + bound, + location, + }; + c.validate()?; + Ok(c) + } + + fn to_json(&self) -> Value { + obj(vec![ + ("boot", Value::Bool(self.boot)), + ( + "bound", + Value::Array(self.bound.iter().map(|b| b.clone().into()).collect()), + ), + ( + "location", + self.location.map_or(Value::Null, |l| { + obj(vec![ + ("area", Value::from(l.area)), + ("x", Value::from(l.x)), + ("y", Value::from(l.y)), + ]) + }), + ), + ]) + } + + fn validate(&self) -> Result<()> { + if self.bound.len() > MAX_MACRO_CHANNELS { + return err("GameboyReadoutContext: at most 64 bound channels"); + } + if let Some(bad) = self.bound.iter().find(|b| !is_channel_name(b)) { + return err(format!( + "GameboyReadoutContext: {bad:?} is not a channel name" + )); + } + require_unique( + self.bound.iter().map(String::as_str), + "GameboyReadoutContext.bound", + ) + } +} + +// --------------------------------------------------------------------------------------------- +// gameboy-channels-v1 + +/// `gameboy-channels-v1`: the eight joypad channels and the macro group's active channel. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ChannelsDecision { + /// In [`GAMEBOY_BUTTONS`] order. + pub buttons: [bool; 8], + pub macro_channel: Option, +} + +impl ChannelsDecision { + pub fn from_typed(value: &TypedValue) -> Result { + typed(value, &CHANNELS) + } + + pub fn to_typed(&self) -> TypedValue { + wrap(self, &CHANNELS) + } + + /// The joypad mask, bit `i` for `GAMEBOY_BUTTONS[i]`. + pub fn mask(&self) -> u8 { + self.buttons.iter().enumerate().fold( + 0u8, + |mask, (bit, down)| if *down { mask | (1 << bit) } else { mask }, + ) + } + + /// The macro group only ever activates a bound channel (`docs/readout.md`, "Macro group"). + pub fn validate_against(&self, context: &ReadoutContext) -> Result<()> { + self.validate()?; + if let Some(channel) = &self.macro_channel + && !context.bound.contains(channel) + { + return err(format!( + "GameboyChannelsDecision: macro {channel:?} is not bound in the decision context" + )); + } + Ok(()) + } +} + +impl DomainType for ChannelsDecision { + const TYPE_NAME: &'static str = "GameboyChannelsDecision"; + + fn from_json(value: &Value) -> Result { + let mut f = Fields::new(value, "GameboyChannelsDecision")?; + let items = f.array("buttons", 8, 8)?; + let mut buttons = [false; 8]; + for (index, item) in items.iter().enumerate() { + let mut b = Fields::new(item, "GameboyChannelsDecision.buttons")?; + let id = b.id("id")?; + let down = b.boolean("down")?; + b.finish()?; + if id != GAMEBOY_BUTTONS[index] { + return err(format!( + "GameboyChannelsDecision: buttons must list {} in that order", + GAMEBOY_BUTTONS.join(",") + )); + } + buttons[index] = down; + } + let macro_channel = match f.value("macro")? { + Value::Null => None, + Value::String(s) if is_channel_name(s) => Some(s.clone()), + _ => return err("GameboyChannelsDecision: macro must be null or a channel name"), + }; + f.finish()?; + let d = ChannelsDecision { + buttons, + macro_channel, + }; + d.validate()?; + Ok(d) + } + + fn to_json(&self) -> Value { + obj(vec![ + ( + "buttons", + Value::Array( + GAMEBOY_BUTTONS + .iter() + .zip(self.buttons) + .map(|(id, down)| obj(vec![("id", (*id).into()), ("down", down.into())])) + .collect(), + ), + ), + ( + "macro", + self.macro_channel + .clone() + .map_or(Value::Null, Value::String), + ), + ]) + } + + fn validate(&self) -> Result<()> { + if let Some(channel) = &self.macro_channel + && !is_channel_name(channel) + { + return err("GameboyChannelsDecision: macro must be a channel name"); + } + Ok(()) + } +} + +// --------------------------------------------------------------------------------------------- +// gameboy-memory-inspection-v1 + +/// `gameboy-memory-inspection-v1`: the per-boundary memory image and the ROM it runs. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct MemoryInspection { + pub memory: ArtifactRef, + pub rom_digest: String, +} + +impl MemoryInspection { + pub fn from_typed(value: &TypedValue) -> Result { + typed(value, &MEMORY_INSPECTION) + } + + pub fn to_typed(&self) -> TypedValue { + wrap(self, &MEMORY_INSPECTION) + } + + /// The ROM the executor reads is the content the environment runs. + pub fn validate_against(&self, content_digest: &str, rom: &AssetRef) -> Result<()> { + self.validate()?; + if self.rom_digest != content_digest || rom.digest != content_digest { + return err( + "GameboyMemoryInspection: romDigest, the environment contentDigest and the executor's rom must agree", + ); + } + Ok(()) + } +} + +impl DomainType for MemoryInspection { + const TYPE_NAME: &'static str = "GameboyMemoryInspection"; + + fn from_json(value: &Value) -> Result { + let mut f = Fields::new(value, "GameboyMemoryInspection")?; + let memory = ArtifactRef::from_json(f.value("memory")?)?; + let rom_digest = f.string("romDigest")?.to_owned(); + f.finish()?; + let i = MemoryInspection { memory, rom_digest }; + i.validate()?; + Ok(i) + } + + fn to_json(&self) -> Value { + obj(vec![ + ("memory", self.memory.to_json()), + ("romDigest", self.rom_digest.clone().into()), + ]) + } + + fn validate(&self) -> Result<()> { + if self.memory.byte_length != MEMORY_IMAGE_BYTES { + return err(format!( + "GameboyMemoryInspection: the memory image is exactly {MEMORY_IMAGE_BYTES} bytes, found {}", + self.memory.byte_length + )); + } + if !is_digest(&self.rom_digest) { + return err("GameboyMemoryInspection: romDigest must be 64 lowercase hex digits"); + } + Ok(()) + } +} + +// --------------------------------------------------------------------------------------------- +// legacy-ratchet-rollback-v1 (EpisodeRequest.outcome) + +/// The outcome of an `EpisodeRequest` of kind `rollback`. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct RollbackRequest { + pub slot_id: String, + pub trigger: String, +} + +impl RollbackRequest { + pub fn from_typed(value: &TypedValue) -> Result { + typed(value, &ROLLBACK_REQUEST) + } + + pub fn to_typed(&self) -> TypedValue { + wrap(self, &ROLLBACK_REQUEST) + } +} + +impl DomainType for RollbackRequest { + const TYPE_NAME: &'static str = "LegacyRatchetRollbackRequest"; + + fn from_json(value: &Value) -> Result { + let mut f = Fields::new(value, "LegacyRatchetRollbackRequest")?; + let slot_id = f.id("slotId")?; + let trigger = enumeration(&mut f, "trigger", ROLLBACK_TRIGGERS)?; + f.finish()?; + let r = RollbackRequest { slot_id, trigger }; + r.validate()?; + Ok(r) + } + + fn to_json(&self) -> Value { + obj(vec![ + ("slotId", self.slot_id.clone().into()), + ("trigger", self.trigger.clone().into()), + ]) + } + + fn validate(&self) -> Result<()> { + if !is_id(&self.slot_id) { + return err("LegacyRatchetRollbackRequest: slotId is not a valid id"); + } + if !ROLLBACK_TRIGGERS.contains(&self.trigger.as_str()) { + return err("LegacyRatchetRollbackRequest: trigger must be stall or game-over"); + } + Ok(()) + } +} + +// --------------------------------------------------------------------------------------------- +// The legacy profile + +/// The legacy profile document. There is exactly one: every field is fixed, so the document +/// and its digest are constants of this contract, and a different value is a different +/// profile that needs its own id. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LegacyProfile { + pub dataset_fingerprint: String, + pub readout_context_schema: SchemaRef, + pub decision_schema: SchemaRef, +} + +/// The one legacy profile. +pub fn legacy_profile() -> LegacyProfile { + LegacyProfile { + dataset_fingerprint: FAFB_V783_FINGERPRINT.to_owned(), + readout_context_schema: READOUT_CONTEXT.schema_ref(), + decision_schema: CHANNELS.schema_ref(), + } +} + +/// SHA-256 of the legacy profile's canonical JSON: its `AssetRef.digest`. +pub fn profile_digest() -> String { + canonical::digest_of(&legacy_profile().to_json()).expect("canonicalizable") +} + +/// The canonical byte length of the profile document: its `AssetRef.byteLength`. +pub fn profile_byte_length() -> u64 { + canonical::canonicalize(&legacy_profile().to_json()) + .expect("canonicalizable") + .len() as u64 +} + +/// The `AssetRef` naming the profile document. +pub fn profile_asset_ref() -> AssetRef { + AssetRef { + id: PROFILE_ID.to_owned(), + digest: profile_digest(), + byte_length: profile_byte_length(), + format: PROFILE_FORMAT.to_owned(), + } +} + +impl DomainType for LegacyProfile { + const TYPE_NAME: &'static str = "LegacyGameboyProfile"; + + fn from_json(value: &Value) -> Result { + let mut f = Fields::new(value, "LegacyGameboyProfile")?; + exact(f.string("profileId")?, PROFILE_ID, "profileId")?; + exact(f.string("datasetId")?, DATASET_ID, "datasetId")?; + f.int("fingerprintSchema", FINGERPRINT_SCHEMA, FINGERPRINT_SCHEMA)?; + let dataset_fingerprint = f.string("datasetFingerprint")?.to_owned(); + exact(f.string("kernelVersion")?, KERNEL_VERSION, "kernelVersion")?; + exact( + f.string("plasticityVersion")?, + PLASTICITY_VERSION, + "plasticityVersion", + )?; + let tick = RationalNs::from_json(f.value("tickDuration")?)?; + if tick != tick_duration() { + return err("LegacyGameboyProfile: tickDuration must be 1 ms (1000000/1)"); + } + f.int("warmupMs", WARMUP_MS, WARMUP_MS)?; + { + let mut v = Fields::new(f.value("view")?, "LegacyGameboyProfile.view")?; + exact(&v.id("viewId")?, VIEW_ID, "view.viewId")?; + v.int("width", VIEW_WIDTH, VIEW_WIDTH)?; + v.int("height", VIEW_HEIGHT, VIEW_HEIGHT)?; + v.finish()?; + } + let stimuli = id_array(&mut f, "supportedStimuli", 1, 1)?; + exact(&stimuli[0], STIMULUS_REWARD_PULSE, "supportedStimuli[0]")?; + let readout_context_schema = SchemaRef::from_json(f.value("readoutContextSchema")?)?; + let decision_schema = SchemaRef::from_json(f.value("decisionSchema")?)?; + let exceptions = id_array(&mut f, "legacyExceptions", 1, 1)?; + exact(&exceptions[0], EXCEPTION_MACRO_ROLES, "legacyExceptions[0]")?; + f.finish()?; + let p = LegacyProfile { + dataset_fingerprint, + readout_context_schema, + decision_schema, + }; + p.validate()?; + Ok(p) + } + + fn to_json(&self) -> Value { + obj(vec![ + ("profileId", PROFILE_ID.into()), + ("datasetId", DATASET_ID.into()), + ("fingerprintSchema", Value::from(FINGERPRINT_SCHEMA)), + ( + "datasetFingerprint", + self.dataset_fingerprint.clone().into(), + ), + ("kernelVersion", KERNEL_VERSION.into()), + ("plasticityVersion", PLASTICITY_VERSION.into()), + ("tickDuration", tick_duration().to_json()), + ("warmupMs", Value::from(WARMUP_MS)), + ( + "view", + obj(vec![ + ("viewId", VIEW_ID.into()), + ("width", Value::from(VIEW_WIDTH)), + ("height", Value::from(VIEW_HEIGHT)), + ]), + ), + ( + "supportedStimuli", + Value::Array(vec![STIMULUS_REWARD_PULSE.into()]), + ), + ( + "readoutContextSchema", + self.readout_context_schema.to_json(), + ), + ("decisionSchema", self.decision_schema.to_json()), + ( + "legacyExceptions", + Value::Array(vec![EXCEPTION_MACRO_ROLES.into()]), + ), + ]) + } + + fn validate(&self) -> Result<()> { + if self.dataset_fingerprint != FAFB_V783_FINGERPRINT { + return err( + "LegacyGameboyProfile: datasetFingerprint must be today's fafb-v783 schema-1 fingerprint; another fingerprint is another profile", + ); + } + schema_matches( + &self.readout_context_schema, + &READOUT_CONTEXT, + "LegacyGameboyProfile.readoutContextSchema", + )?; + schema_matches( + &self.decision_schema, + &CHANNELS, + "LegacyGameboyProfile.decisionSchema", + ) + } +} + +// --------------------------------------------------------------------------------------------- +// The legacy composition + +/// The executor block of the composition. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ExecutorDeclaration { + pub rom: AssetRef, + pub adapter: String, + pub symbol_provenance: String, + pub mode: String, + pub macro_channels: Vec, +} + +/// The environment block of the composition. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct EnvironmentDeclaration { + pub slots: Vec, + pub audio_sample_rate: u64, +} + +/// `LegacyGameboyComposition`: what the legacy composition runs, beyond the profile. Its +/// digest joins the session's `compositionDigest`; the FLYSIM01 compatibility string is +/// recorded beside it, unchanged. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LegacyComposition { + pub composition_id: String, + pub profile: AssetRef, + pub executor: ExecutorDeclaration, + pub decoder_config_digest: String, + pub environment: EnvironmentDeclaration, + pub flysim_compatibility: String, +} + +impl LegacyComposition { + /// SHA-256 of the canonical declaration: the line `declaration=` of the + /// session's composition digest (`legacy-gameboy-v1` section 12). + pub fn digest(&self) -> Result { + canonical::digest_of(&self.to_json()) + } +} + +impl DomainType for LegacyComposition { + const TYPE_NAME: &'static str = "LegacyGameboyComposition"; + + fn from_json(value: &Value) -> Result { + let mut f = Fields::new(value, "LegacyGameboyComposition")?; + let composition_id = f.id("compositionId")?; + exact(f.string("scheduler")?, SCHEDULER, "scheduler")?; + let profile = AssetRef::from_json(f.value("profile")?)?; + let executor = { + let mut e = Fields::new(f.value("executor")?, "LegacyGameboyComposition.executor")?; + exact(e.string("id")?, EXECUTOR_ID, "executor.id")?; + let rom = AssetRef::from_json(e.value("rom")?)?; + let adapter = e.id("adapter")?; + let symbol_provenance = bounded_string(&mut e, "symbolProvenance", 64)?; + let mode = enumeration(&mut e, "mode", MACRO_MODES)?; + let macro_channels = channel_list(&mut e, "macroChannels", MAX_MACRO_CHANNELS)?; + e.finish()?; + ExecutorDeclaration { + rom, + adapter, + symbol_provenance, + mode, + macro_channels, + } + }; + let decoder_config_digest = f.string("decoderConfigDigest")?.to_owned(); + let environment = { + let mut e = Fields::new( + f.value("environment")?, + "LegacyGameboyComposition.environment", + )?; + let extensions = id_array(&mut e, "extensions", 1, 1)?; + exact( + &extensions[0], + SLOTS_CAPABILITY, + "environment.extensions[0]", + )?; + let slots = id_array(&mut e, "slots", 1, MAX_SLOTS)?; + let step = RationalNs::from_json(e.value("stepDuration")?)?; + if step != step_duration() { + return err( + "LegacyGameboyComposition: environment.stepDuration must be one Game Boy frame, 8572265625/512 ns", + ); + } + schema_matches( + &SchemaRef::from_json(e.value("inspectionSchema")?)?, + &MEMORY_INSPECTION, + "environment.inspectionSchema", + )?; + schema_matches( + &SchemaRef::from_json(e.value("controllerSchema")?)?, + &JOYPAD, + "environment.controllerSchema", + )?; + e.int("setupFrames", SETUP_FRAMES, SETUP_FRAMES)?; + let audio_sample_rate = { + let mut a = Fields::new(e.value("audio")?, "environment.audio")?; + let rate = a.int("sampleRate", 8_000, 192_000)?; + a.int("channels", 2, 2)?; + a.finish()?; + rate + }; + e.finish()?; + EnvironmentDeclaration { + slots, + audio_sample_rate, + } + }; + exact(f.string("episodePolicy")?, ROLLBACK_POLICY, "episodePolicy")?; + exact(f.string("restore")?, RESTORE_SEMANTICS, "restore")?; + exact( + f.string("checkpointFormatOfRecord")?, + CHECKPOINT_FORMAT_OF_RECORD, + "checkpointFormatOfRecord", + )?; + let flysim_compatibility = f.string("flysimCompatibility")?.to_owned(); + f.finish()?; + let c = LegacyComposition { + composition_id, + profile, + executor, + decoder_config_digest, + environment, + flysim_compatibility, + }; + c.validate()?; + Ok(c) + } + + fn to_json(&self) -> Value { + obj(vec![ + ("compositionId", self.composition_id.clone().into()), + ("scheduler", SCHEDULER.into()), + ("profile", self.profile.to_json()), + ( + "executor", + obj(vec![ + ("id", EXECUTOR_ID.into()), + ("rom", self.executor.rom.to_json()), + ("adapter", self.executor.adapter.clone().into()), + ( + "symbolProvenance", + self.executor.symbol_provenance.clone().into(), + ), + ("mode", self.executor.mode.clone().into()), + ( + "macroChannels", + Value::Array( + self.executor + .macro_channels + .iter() + .map(|c| c.clone().into()) + .collect(), + ), + ), + ]), + ), + ( + "decoderConfigDigest", + self.decoder_config_digest.clone().into(), + ), + ( + "environment", + obj(vec![ + ("extensions", Value::Array(vec![SLOTS_CAPABILITY.into()])), + ( + "slots", + Value::Array( + self.environment + .slots + .iter() + .map(|s| s.clone().into()) + .collect(), + ), + ), + ("stepDuration", step_duration().to_json()), + ("inspectionSchema", MEMORY_INSPECTION.schema_ref().to_json()), + ("controllerSchema", JOYPAD.schema_ref().to_json()), + ("setupFrames", Value::from(SETUP_FRAMES)), + ( + "audio", + obj(vec![ + ( + "sampleRate", + Value::from(self.environment.audio_sample_rate), + ), + ("channels", Value::from(2u64)), + ]), + ), + ]), + ), + ("episodePolicy", ROLLBACK_POLICY.into()), + ("restore", RESTORE_SEMANTICS.into()), + ( + "checkpointFormatOfRecord", + CHECKPOINT_FORMAT_OF_RECORD.into(), + ), + ( + "flysimCompatibility", + self.flysim_compatibility.clone().into(), + ), + ]) + } + + fn validate(&self) -> Result<()> { + if !is_id(&self.composition_id) { + return err("LegacyGameboyComposition: compositionId is not a valid id"); + } + self.profile.validate()?; + if self.profile.format != PROFILE_FORMAT || self.profile.digest != profile_digest() { + return err( + "LegacyGameboyComposition: profile must name the legacy profile document (format fly-profile-v1, its digest)", + ); + } + self.executor.rom.validate()?; + if !is_id(&self.executor.adapter) { + return err("LegacyGameboyComposition: executor.adapter is not a valid id"); + } + match self.executor.mode.as_str() { + "raw" if !self.executor.macro_channels.is_empty() => { + return err("LegacyGameboyComposition: raw mode deals no macro channels"); + } + "macros" if self.executor.macro_channels.is_empty() => { + return err("LegacyGameboyComposition: macros mode needs its macro channels"); + } + "raw" | "macros" => {} + _ => return err("LegacyGameboyComposition: executor.mode must be raw or macros"), + } + if let Some(bad) = self + .executor + .macro_channels + .iter() + .find(|c| !is_channel_name(c)) + { + return err(format!( + "LegacyGameboyComposition: {bad:?} is not a channel name" + )); + } + require_unique( + self.executor.macro_channels.iter().map(String::as_str), + "executor.macroChannels", + )?; + if !is_digest(&self.decoder_config_digest) { + return err( + "LegacyGameboyComposition: decoderConfigDigest must be 64 lowercase hex digits", + ); + } + if self.environment.slots.is_empty() || self.environment.slots.len() > MAX_SLOTS { + return err("LegacyGameboyComposition: 1..=4 slots"); + } + require_unique( + self.environment.slots.iter().map(String::as_str), + "environment.slots", + )?; + self.validate_compatibility() + } +} + +impl LegacyComposition { + /// The FLYSIM01 string is recorded, not reinterpreted; but the segments that name what + /// this declaration also names must agree with it, so the two cannot describe two flies. + fn validate_compatibility(&self) -> Result<()> { + let text = &self.flysim_compatibility; + if text.is_empty() || text.len() > MAX_COMPATIBILITY_BYTES { + return err("LegacyGameboyComposition: flysimCompatibility must be 1..=1024 bytes"); + } + let segments: Vec<&str> = text.split('/').collect(); + let pokered = format!("pokered:{}", self.executor.symbol_provenance); + let agrees = segments.len() >= 6 + && segments[0] == KERNEL_VERSION + && segments[1] == self.executor.adapter + && segments[2] == FAFB_V783_FINGERPRINT + && segments[3] == PLASTICITY_VERSION + && segments[5] == pokered; + if !agrees { + return Err(wire_err( + "LegacyGameboyComposition: flysimCompatibility's kernel, adapter, fingerprint, plasticity and pokered segments must agree with the declaration", + )); + } + Ok(()) + } +} diff --git a/services/flysim/crates/fly-session-types/src/lib.rs b/services/flysim/crates/fly-session-types/src/lib.rs index bacac0f..60a5905 100644 --- a/services/flysim/crates/fly-session-types/src/lib.rs +++ b/services/flysim/crates/fly-session-types/src/lib.rs @@ -13,12 +13,16 @@ //! - the documented canonical schema set and `contractDigest` ([`schema`]); //! - the trace format of [step-v1] section 8, with behaviour separated from operational //! metadata and a comparator over behaviour alone ([`trace`]); +//! - the 2026-09-23 extension methods ([`extensions`]) and the legacy Game Boy composition +//! ([`gameboy`]), whose registered schemas are digested apart from `contractDigest`; //! - `seed-derivation-v1` ([`seed`]) and the `FLYSESS1` checkpoint envelope layout //! ([`checkpoint`]), the two specifications CONTRACT-01 has to settle before the real-agent //! and store slices. //! -//! What it is not: a transport, a worker, a coordinator or a store. It holds no Game Boy FFI, -//! no Melee parser and no console-specific state, and it never reaches the network. +//! What it is not: a transport, a worker, a coordinator or a store. It holds no Game Boy FFI +//! and no Melee parser, its generic types hold no console-specific state (the [`gameboy`] +//! declarations travel only inside `TypedValue`s and composition documents), and it never +//! reaches the network. //! //! Every type implements [`scalar::DomainType`]: `from_json` reads and validates, `to_json` //! writes the canonical shape, and `validate` re-checks the rules that span fields. Reading @@ -33,7 +37,9 @@ pub mod canonical; pub mod checkpoint; +pub mod extensions; pub mod fixtures; +pub mod gameboy; pub mod media; pub mod publishing; pub mod rpc; diff --git a/services/flysim/crates/fly-session-types/src/schema.rs b/services/flysim/crates/fly-session-types/src/schema.rs index 57ed6a7..ac64bd4 100644 --- a/services/flysim/crates/fly-session-types/src/schema.rs +++ b/services/flysim/crates/fly-session-types/src/schema.rs @@ -57,7 +57,7 @@ pub struct LimitSchema { pub source: &'static str, } -const fn req(name: &'static str, kind: &'static str, constraint: &'static str) -> FieldSchema { +pub(crate) const fn req(name: &'static str, kind: &'static str, constraint: &'static str) -> FieldSchema { FieldSchema { name, kind, @@ -66,7 +66,7 @@ const fn req(name: &'static str, kind: &'static str, constraint: &'static str) - } } -const fn opt(name: &'static str, kind: &'static str, constraint: &'static str) -> FieldSchema { +pub(crate) const fn opt(name: &'static str, kind: &'static str, constraint: &'static str) -> FieldSchema { FieldSchema { name, kind, @@ -129,7 +129,7 @@ pub const ENUMS: &[EnumSchema] = &[ EnumSchema { name: "EpisodeRequestKind", source: "workers-v1 4", - members: &["terminal"], + members: crate::workers::EpisodeRequestKind::ALL, }, ]; @@ -259,6 +259,11 @@ pub const LIMITS: &[LimitSchema] = &[ value: crate::publishing::MAX_ASSETS as u64, source: "crate", }, + LimitSchema { + name: "maxSlots", + value: crate::extensions::MAX_SLOTS as u64, + source: "crate", + }, LimitSchema { name: "maxSnapshotEvents", value: crate::publishing::MAX_SNAPSHOT_EVENTS as u64, @@ -413,6 +418,11 @@ pub const SCHEMAS: &[TypeSchema] = &[ "{enabled:bool,updates:U64,changed:U64,signal:number}", "changed <= updates; signal finite", ), + opt( + "stimulusRemainingMs", + "number|null", + "finite and nonnegative; the pulse still running after the operation; null when the agent reports none", + ), ], }, TypeSchema { @@ -744,7 +754,11 @@ pub const SCHEMAS: &[TypeSchema] = &[ name: "EpisodeRequest", source: "workers-v1 4", fields: &[ - req("kind", "EpisodeRequestKind", ""), + req( + "kind", + "EpisodeRequestKind", + "rollback only under a composition that declares a rollback policy", + ), req("reason", "Id", ""), req("outcome", "TypedValue", ""), ], @@ -949,6 +963,76 @@ pub const SCHEMAS: &[TypeSchema] = &[ ), ], }, + TypeSchema { + name: "SaveSlotParams", + source: "workers-v1 7", + fields: &[req( + "slotId", + "Id", + "one of the composition's declared slots; capability gameboy-slots-v1", + )], + }, + TypeSchema { + name: "SaveSlotResult", + source: "workers-v1 7", + fields: &[ + req("slotId", "Id", "echoes the request"), + req("boundary", "U64", "the scoped committed step"), + req("stateDigest", "Digest", "SHA-256 of the saved state bytes"), + req("byteLength", "U64", "positive"), + ], + }, + TypeSchema { + name: "RestoreSlotParams", + source: "workers-v1 7", + fields: &[ + req("slotId", "Id", "a slot saved in priorEpoch or carried by its restore"), + req( + "priorEpoch", + "Id", + "the epoch the environment is Ready in; differs from scope.epoch", + ), + req("policy", "Id", "\"legacy-ratchet-rollback-v1\""), + ], + }, + TypeSchema { + name: "RestoreSlotResult", + source: "workers-v1 7", + fields: &[ + req("slotId", "Id", "echoes the request"), + req("committedStep", "U64", "the scoped step; no transition ran"), + req( + "observation", + "WorldObservation", + "boundary == committedStep; no audio chunk; worldTime and engineFrame continue", + ), + ], + }, + TypeSchema { + name: "AgentRollbackParams", + source: "workers-v1 7", + fields: &[ + req("agentId", "Id", ""), + req( + "priorEpoch", + "Id", + "the epoch the agent is Ready in; differs from scope.epoch", + ), + req("policy", "Id", "\"legacy-ratchet-rollback-v1\"; capability of the same name"), + req("input", "SensoryInput", "boundary == scope.step; installed without a tick"), + req("decisionContext", "TypedValue", "the context for the next Prepare"), + ], + }, + TypeSchema { + name: "AgentRollbackResult", + source: "workers-v1 7", + fields: &[ + req("agentId", "Id", ""), + req("committedStep", "U64", "the scoped step; a rollback runs no tick"), + req("decisionContextDigest", "Digest", ""), + req("telemetry", "AgentTelemetry", ""), + ], + }, TypeSchema { name: "TransitionTrace", source: "step-v1 8", diff --git a/services/flysim/crates/fly-session-types/src/workers.rs b/services/flysim/crates/fly-session-types/src/workers.rs index 9a785c9..90cb658 100644 --- a/services/flysim/crates/fly-session-types/src/workers.rs +++ b/services/flysim/crates/fly-session-types/src/workers.rs @@ -8,7 +8,7 @@ use serde_json::Value; use crate::media::{AudioDescriptor, MAX_VIEWS, ViewDescriptor, ViewRef, audio_list, view_list}; use crate::scalar::{ - DomainRequestId, DomainType, RationalNs, Result, SchemaRef, TypedValue, constant, + DomainRequestId, DomainType, RationalNs, Result, SchemaRef, TypedValue, constant_true, enumeration, err, finite, finite_in, i32_field, id_list, is_digest, is_id, list, obj, require_same_order, require_unique, u64_json, }; @@ -505,6 +505,10 @@ pub struct AgentTelemetry { pub population_rate_hz: f64, pub rates: Vec, pub learning: LearningTelemetry, + /// Milliseconds of stimulation pulse still running after this operation, or `None` for + /// an agent that reports none. Amendment 2026-09-23 (RT-01a): the coordinator's sugar + /// admission reads it from the last commit (workers-v1 section 5). + pub stimulus_remaining_ms: Option, } impl AgentTelemetry { @@ -526,6 +530,10 @@ impl DomainType for AgentTelemetry { let mut f = Fields::new(value, "AgentTelemetry")?; let brain_ticks = f.u64_string("brainTicks")?; let population_rate_hz = finite_in(&mut f, "populationRateHz", 0.0, f64::MAX)?; + let stimulus_remaining_ms = match f.value("stimulusRemainingMs")? { + Value::Null => None, + _ => Some(finite_in(&mut f, "stimulusRemainingMs", 0.0, f64::MAX)?), + }; let rates = list(&mut f, "rates", 0, MAX_RATE_ROLES, |v| { let mut r = Fields::new(v, "AgentTelemetry.rates")?; let role_id = r.id("roleId")?; @@ -554,6 +562,7 @@ impl DomainType for AgentTelemetry { population_rate_hz, rates, learning, + stimulus_remaining_ms, }; t.validate()?; Ok(t) @@ -586,6 +595,10 @@ impl DomainType for AgentTelemetry { ("signal", Value::from(self.learning.signal)), ]), ), + ( + "stimulusRemainingMs", + self.stimulus_remaining_ms.map_or(Value::Null, Value::from), + ), ]) } @@ -614,6 +627,11 @@ impl DomainType for AgentTelemetry { if !self.learning.signal.is_finite() { return err("AgentTelemetry: learning.signal must be finite"); } + if let Some(remaining) = self.stimulus_remaining_ms + && (!remaining.is_finite() || remaining < 0.0) + { + return err("AgentTelemetry: stimulusRemainingMs must be null or finite and nonnegative"); + } Ok(()) } } @@ -2474,9 +2492,40 @@ impl DomainType for TaskEvent { } } -/// `episodeRequest`: null, or a terminal request the coordinator may act on. +/// The kind of an `episodeRequest` (workers-v1 section 4). +/// +/// `Rollback` is the amendment of 2026-09-23 (RT-01a): a request for the composition's declared +/// rollback policy, applied at the committed boundary the transition just reached. Only a +/// composition that declares such a policy may act on it; any other refuses it. +#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] +pub enum EpisodeRequestKind { + Terminal, + Rollback, +} + +impl EpisodeRequestKind { + pub const ALL: &'static [&'static str] = &["terminal", "rollback"]; + + pub fn as_str(self) -> &'static str { + match self { + EpisodeRequestKind::Terminal => "terminal", + EpisodeRequestKind::Rollback => "rollback", + } + } + + pub fn parse(s: &str) -> Result { + match s { + "terminal" => Ok(EpisodeRequestKind::Terminal), + "rollback" => Ok(EpisodeRequestKind::Rollback), + _ => err("EpisodeRequest: kind must be terminal or rollback"), + } + } +} + +/// `episodeRequest`: null, or a request the coordinator may act on. #[derive(Clone, Debug, PartialEq)] pub struct EpisodeRequest { + pub kind: EpisodeRequestKind, pub reason: String, pub outcome: TypedValue, } @@ -2486,18 +2535,26 @@ impl DomainType for EpisodeRequest { fn from_json(value: &Value) -> Result { let mut f = Fields::new(value, "EpisodeRequest")?; - constant(&mut f, "kind", "terminal")?; + let kind = EpisodeRequestKind::parse(&enumeration( + &mut f, + "kind", + EpisodeRequestKind::ALL, + )?)?; let reason = f.id("reason")?; let outcome = TypedValue::from_json(f.value("outcome")?)?; f.finish()?; - let r = EpisodeRequest { reason, outcome }; + let r = EpisodeRequest { + kind, + reason, + outcome, + }; r.validate()?; Ok(r) } fn to_json(&self) -> Value { obj(vec![ - ("kind", "terminal".into()), + ("kind", self.kind.as_str().into()), ("reason", self.reason.clone().into()), ("outcome", self.outcome.to_json()), ]) diff --git a/services/flysim/crates/fly-session-types/tests/common/mod.rs b/services/flysim/crates/fly-session-types/tests/common/mod.rs index 3d2458e..272049d 100644 --- a/services/flysim/crates/fly-session-types/tests/common/mod.rs +++ b/services/flysim/crates/fly-session-types/tests/common/mod.rs @@ -1,6 +1,11 @@ //! One place that knows how to read every type named by a fixture. use flybus::wire::WireError; +use fly_session_types::extensions::*; +use fly_session_types::gameboy::{ + ChannelsDecision, LegacyComposition, LegacyProfile, MemoryInspection, ReadoutContext, + RollbackRequest, +}; use fly_session_types::media::*; use fly_session_types::publishing::*; use fly_session_types::rpc::*; @@ -73,6 +78,18 @@ pub fn round_trip(type_name: &str, value: &Value) -> std::result::Result Value { + fixtures::load("gameboy-legacy.json").expect("gameboy-legacy.json") +} + +#[test] +fn every_registered_schema_reference_is_the_digest_of_its_declaration() { + let file = legacy(); + for schema in gameboy::PAYLOAD_SCHEMAS { + let recorded = SchemaRef::from_json(&file["schemaRefs"][schema.id]).expect("recorded ref"); + assert_eq!(recorded, schema.schema_ref(), "{}", schema.id); + assert_eq!( + recorded.digest, + canonical::digest_of(&schema.declaration()).expect("digest"), + "{}: the digest is over the declaration", + schema.id + ); + } + assert_eq!( + file["extensionSetDigest"].as_str(), + Some(gameboy::extension_set_digest().as_str()) + ); + assert_eq!( + canonical::digest_of(&file["extensionSet"]).expect("digest"), + gameboy::extension_set_digest(), + "the checked-in set hashes to the recorded digest" + ); +} + +#[test] +fn a_changed_constraint_is_a_new_schema_reference() { + let mut changed = gameboy::READOUT_CONTEXT; + let fields: &'static [_] = Box::leak( + gameboy::READOUT_CONTEXT + .fields + .iter() + .map(|f| { + let mut f = *f; + if f.name == "bound" { + f.constraint = "<= 32"; + } + f + }) + .collect::>() + .into_boxed_slice(), + ); + changed.fields = fields; + assert_ne!( + changed.schema_ref().digest, + gameboy::READOUT_CONTEXT.schema_ref().digest + ); +} + +#[test] +fn the_profile_document_is_the_one_legacy_profile_and_its_asset_ref_is_its_digest() { + let file = legacy(); + let document = &file["profile"]["document"]; + let parsed = LegacyProfile::from_json(document).expect("the legacy profile"); + assert_eq!(parsed, gameboy::legacy_profile()); + let canonical_text = canonical::canonicalize(document).expect("canonical"); + assert_eq!( + file["profile"]["canonical"].as_str(), + Some(canonical_text.as_str()) + ); + let asset = gameboy::profile_asset_ref(); + assert_eq!( + asset.digest, + canonical::sha256_hex(canonical_text.as_bytes()) + ); + assert_eq!(asset.byte_length, canonical_text.len() as u64); + assert_eq!(asset.id, gameboy::PROFILE_ID); + assert_eq!(file["profile"]["assetRef"], asset.to_json()); + // The pinned identities are the defaults CLAUDE.md fixes, and the fingerprint is today's + // schema-1 value (flysim's legacy_profile_identity test recomputes it from the dataset). + assert_eq!(document["kernelVersion"], "lif-1ms-f64-v2"); + assert_eq!(document["plasticityVersion"], "fly-kc-mbon-rstdp-v2"); + assert_eq!(gameboy::FAFB_V783_FINGERPRINT.split(':').count(), 7); + assert!( + gameboy::FAFB_V783_FINGERPRINT + .split(':') + .all(fly_session_types::scalar::is_digest) + ); +} + +/// The legacy loop's f64 accumulator and step-v1's rational one are the same numbers. +#[test] +fn the_frame_clock_is_exact_and_matches_the_legacy_f64_accumulator() { + let legacy_ms_per_frame: f64 = 1000.0 / (4_194_304.0 / 70_224.0); + assert_eq!( + legacy_ms_per_frame, + 548_625.0 / 32_768.0, + "the constant is dyadic, so exact" + ); + let step = gameboy::step_duration(); + assert_eq!( + RationalNs::reduced(70_224 * 1_000_000_000, 4_194_304).expect("frame"), + step + ); + let tick = gameboy::tick_duration(); + let file = legacy(); + let frames = file["clock"]["frames"].as_array().expect("frames"); + let mut exact = RationalNs::ZERO; + let mut float = 0.0f64; + let mut total = 0u64; + for frame in 0..100_000u64 { + exact = exact.checked_add(&step).expect("no overflow"); + let (ticks, remainder) = exact.divide_floor(&tick).expect("tick"); + exact = remainder; + float += legacy_ms_per_frame; + let steps = float.floor(); + float -= steps; + assert_eq!(ticks, steps as u64, "frame {frame}: tick counts agree"); + // The f64 remainder in ms, as an exact nanosecond rational. + let float_ns = + RationalNs::reduced((float * 32_768.0) as u128 * 1_000_000, 32_768).expect("dyadic"); + assert_eq!( + float * 32_768.0, + (float * 32_768.0).floor(), + "frame {frame}: dyadic" + ); + assert_eq!(remainder, float_ns, "frame {frame}: remainders agree"); + total += ticks; + if let Some(recorded) = frames.get(frame as usize) { + assert_eq!(recorded["ticks"], Value::String(ticks.to_string())); + assert_eq!(recorded["remainder"], remainder.to_json()); + } + } + assert!( + total > 1_674_000, + "100000 frames is about 1674 brain seconds" + ); +} + +#[test] +fn the_example_composition_digest_is_recorded_and_moves_with_the_decoder() { + let file = legacy(); + let example = LegacyComposition::from_json(&file["composition"]["example"]).expect("example"); + assert_eq!( + file["composition"]["digest"].as_str(), + Some(example.digest().expect("digest").as_str()) + ); + // The decoder configuration and the macro channels are in the composition digest, not in + // the legacy compatibility string: changing either moves the digest and leaves the string. + let mut decoder = example.clone(); + decoder.decoder_config_digest = canonical::sha256_hex(b"another decoder"); + assert_ne!(decoder.digest().expect("d"), example.digest().expect("d")); + assert_eq!(decoder.flysim_compatibility, example.flysim_compatibility); + let mut channels = example.clone(); + channels.executor.macro_channels.pop(); + channels.validate().expect("still a valid composition"); + assert_ne!(channels.digest().expect("d"), example.digest().expect("d")); + assert_eq!(channels.flysim_compatibility, example.flysim_compatibility); +} + +#[test] +fn typed_values_are_read_only_under_their_registered_schema() { + let context = ReadoutContext { + boot: false, + bound: vec!["macro_talk".to_owned()], + location: Some(Location { + area: 2, + x: 17, + y: 9, + }), + }; + let typed = context.to_typed(); + assert_eq!(typed.schema, gameboy::READOUT_CONTEXT.schema_ref()); + assert_eq!( + ReadoutContext::from_typed(&typed).expect("round trip"), + context + ); + let wrong = TypedValue::new(gameboy::CHANNELS.schema_ref(), typed.value.clone()).expect("tv"); + assert!( + ReadoutContext::from_typed(&wrong).is_err(), + "another schema is refused" + ); + let synthetic = TypedValue::new( + SchemaRef::new( + "gameboy-readout-context-v1", + 1, + &canonical::sha256_hex(b"x"), + ) + .expect("r"), + typed.value, + ) + .expect("tv"); + assert!( + ReadoutContext::from_typed(&synthetic).is_err(), + "the same id with another digest is another schema" + ); +} + +#[test] +fn bound_is_an_ordered_subset_and_the_macro_winner_is_always_bound() { + let channels: Vec = ["macro_go_objective", "macro_talk", "macro_next"] + .iter() + .map(|c| (*c).to_owned()) + .collect(); + let context = ReadoutContext { + boot: false, + bound: vec!["macro_go_objective".to_owned(), "macro_next".to_owned()], + location: None, + }; + context.validate_against(&channels).expect("ordered subset"); + let reversed = ReadoutContext { + bound: vec!["macro_next".to_owned(), "macro_go_objective".to_owned()], + ..context.clone() + }; + assert!( + reversed.validate_against(&channels).is_err(), + "order is the decoder's" + ); + let foreign = ReadoutContext { + bound: vec!["macro_heal".to_owned()], + ..context.clone() + }; + assert!(foreign.validate_against(&channels).is_err()); + + let mut decision = ChannelsDecision { + buttons: [true, false, false, false, true, false, false, false], + macro_channel: Some("macro_next".to_owned()), + }; + assert_eq!(decision.mask(), 0x11, "up and a, GAMEBOY_BUTTON_BITS"); + decision.validate_against(&context).expect("bound"); + decision.macro_channel = Some("macro_talk".to_owned()); + assert!( + decision.validate_against(&context).is_err(), + "an unbound channel cannot be the macro group's winner" + ); +} + +#[test] +fn the_inspection_rom_is_the_environment_content() { + let file = legacy(); + let example = LegacyComposition::from_json(&file["composition"]["example"]).expect("example"); + let rom = example.executor.rom.clone(); + let inspection = MemoryInspection::from_json(&json!({ + "memory": {"storeId": "store-1", "artifactId": "wram-1", "generation": "1", + "byteLength": "65536", "contentType": "application/octet-stream", "digest": null}, + "romDigest": rom.digest, + })) + .expect("inspection"); + inspection + .validate_against(&rom.digest, &rom) + .expect("agree"); + assert!( + inspection + .validate_against(&canonical::sha256_hex(b"other cartridge"), &rom) + .is_err() + ); +} + +#[test] +fn a_rollback_request_carries_the_registered_outcome() { + let request = EpisodeRequest { + kind: EpisodeRequestKind::Rollback, + reason: "stall".to_owned(), + outcome: RollbackRequest { + slot_id: "best".to_owned(), + trigger: "stall".to_owned(), + } + .to_typed(), + }; + let read = EpisodeRequest::from_json(&request.to_json()).expect("round trip"); + assert_eq!(read.kind, EpisodeRequestKind::Rollback); + let outcome = RollbackRequest::from_typed(&read.outcome).expect("registered outcome"); + assert_eq!(outcome.slot_id, "best"); +} + +#[test] +fn the_extension_methods_check_their_scope() { + let new_epoch = Scope::new("live", "epoch-8", 4101).expect("scope"); + let same_epoch = Scope::new("live", "epoch-7", 4101).expect("scope"); + let restore = RestoreSlotParams { + slot_id: "best".to_owned(), + prior_epoch: "epoch-7".to_owned(), + policy: ROLLBACK_POLICY.to_owned(), + }; + restore + .validate_against_scope(&new_epoch) + .expect("a new epoch"); + assert!( + restore.validate_against_scope(&same_epoch).is_err(), + "a rollback always moves to a new epoch" + ); + + let saved = SaveSlotResult { + slot_id: "best".to_owned(), + boundary: 4101, + state_digest: canonical::sha256_hex(b"state"), + byte_length: 199_616, + }; + saved + .validate_against_scope(&same_epoch) + .expect("the scoped boundary"); + assert!( + saved + .validate_against_scope(&Scope::new("live", "epoch-7", 4100).expect("s")) + .is_err() + ); + + let file = fixtures::load("valid.json").expect("valid.json"); + let case = |name: &str| -> Value { + fixtures::cases(&file) + .expect("cases") + .iter() + .find(|c| c["name"] == Value::String(name.to_owned())) + .unwrap_or_else(|| panic!("case {name}"))["value"] + .clone() + }; + let rollback = AgentRollbackParams::from_json(&case("agent rollback params")).expect("params"); + rollback + .validate_against_scope(&new_epoch) + .expect("input is the scoped boundary"); + assert!( + rollback + .validate_against_scope(&Scope::new("live", "epoch-8", 4102).expect("s")) + .is_err(), + "the installed input is the restored boundary, not the next one" + ); + let result = AgentRollbackResult::from_json(&case("agent rollback result")).expect("result"); + result.validate_against_scope(&new_epoch).expect("no tick"); + assert!( + result + .validate_against_scope(&Scope::new("live", "epoch-8", 4100).expect("s")) + .is_err() + ); + let restored = RestoreSlotResult::from_json(&case("restore slot result")).expect("result"); + restored + .validate_against_scope(&new_epoch) + .expect("same boundary"); + let inspection = MemoryInspection::from_typed(&restored.observation.inspection) + .expect("the observation's inspection is the registered memory image"); + assert_eq!(inspection.memory.byte_length, gameboy::MEMORY_IMAGE_BYTES); + assert_eq!(SLOTS_CAPABILITY, "gameboy-slots-v1"); + assert_eq!(ROLLBACK_CAPABILITY, ROLLBACK_POLICY); +} diff --git a/services/flysim/crates/fly-session-types/tests/schema_set.rs b/services/flysim/crates/fly-session-types/tests/schema_set.rs index 179ed34..c3c3af3 100644 --- a/services/flysim/crates/fly-session-types/tests/schema_set.rs +++ b/services/flysim/crates/fly-session-types/tests/schema_set.rs @@ -118,13 +118,33 @@ fn the_schema_set_names_every_type_the_crate_reads() { .iter() .map(|t| t["name"].as_str().expect("name")) .collect(); + // The legacy Game Boy types are registered payload schemas and declarations, digested by + // their own extension set rather than by contractDigest (legacy-gameboy-v1 section 13). + let extension = fly_session_types::gameboy::extension_set(); + let declared_in_extension = |declared: &str| { + extension["payloadSchemas"] + .as_array() + .expect("payloadSchemas") + .iter() + .any(|p| p["declaration"]["id"] == Value::String(declared.to_owned())) + || extension["declarations"] + .as_array() + .expect("declarations") + .iter() + .any(|d| d["name"] == Value::String(declared.to_owned())) + }; let missing: Vec<&&str> = common::READABLE_TYPES .iter() .filter(|expected| !names.contains(*expected)) + .filter(|expected| { + !fly_session_types::gameboy::EXTENSION_TYPES + .iter() + .any(|(name, declared)| name == *expected && declared_in_extension(declared)) + }) .collect(); assert!( missing.is_empty(), - "every readable type must be in the schema set; missing {missing:?}" + "every readable type must be in the schema set or the legacy extension set; missing {missing:?}" ); let mut sorted = names.clone(); sorted.sort_unstable(); @@ -173,6 +193,7 @@ fn published_limits_match_the_constants_and_name_their_source() { "maxAssets", "maxAudioStreams", "maxCapabilities", + "maxSlots", "maxSnapshotEvents", "maxSupportedMajors", "maxSupportedStimuli", diff --git a/services/flysim/crates/fly-session/src/agent.rs b/services/flysim/crates/fly-session/src/agent.rs index 5e94143..0467ef0 100644 --- a/services/flysim/crates/fly-session/src/agent.rs +++ b/services/flysim/crates/fly-session/src/agent.rs @@ -179,6 +179,9 @@ impl FakeModel { changed: self.learning_changed, signal: self.last_signal, }, + // The synthetic model has no stimulation pulse to report (workers-v1 section 1, + // amendment 2026-09-23): only a profile with a pulse reports what is left of it. + stimulus_remaining_ms: None, } } } diff --git a/services/flysim/crates/fly-session/src/task.rs b/services/flysim/crates/fly-session/src/task.rs index 2eebbef..db8d8cd 100644 --- a/services/flysim/crates/fly-session/src/task.rs +++ b/services/flysim/crates/fly-session/src/task.rs @@ -406,8 +406,10 @@ impl Task for CounterTask { Terminal::Counter(target) => new >= target, Terminal::AfterTransitions(n) => self.transitions >= n, }; - // The contract's `EpisodeRequest` is terminal by construction: `kind` is a constant. + // The synthetic task only ever asks for the end of the episode; `rollback` belongs to a + // composition that declares a rollback policy (workers-v1 section 4, 2026-09-23). let episode = terminal.then(|| EpisodeRequest { + kind: EpisodeRequestKind::Terminal, reason: id("counter-target"), outcome: TypedValue::new(episode_schema(), json!({"counter": new, "transitions": self.transitions})) .expect("a synthetic typed value fits the contract"), diff --git a/services/flysim/crates/fly-session/src/types.rs b/services/flysim/crates/fly-session/src/types.rs index 8648308..5822991 100644 --- a/services/flysim/crates/fly-session/src/types.rs +++ b/services/flysim/crates/fly-session/src/types.rs @@ -39,7 +39,8 @@ pub use fly_session_types::workers::{ AcknowledgeParams, AcknowledgeResult, AdvanceParams, AgentCommitResult, AgentInitializeParams, AgentGraph, AgentInitializeResult, AgentTelemetry, AssetRef, AxisRange, AxisSchema, AxisValue, ButtonState, CommitParams, ControllerSchema, Determinism, EnvironmentDescriptor, - EnvironmentInitializeParams, EnvironmentInitializeResult, EpisodeRequest, HelloParams, + EnvironmentInitializeParams, EnvironmentInitializeResult, EpisodeRequest, EpisodeRequestKind, + HelloParams, HelloResult, LearningTelemetry, MAX_ACKNOWLEDGE, MAX_AGENTS, MAX_PORTS, MAX_RATE_ROLES, MAX_REWARDS, MAX_STIMULI, PortControl, PortDescriptor, PrepareParams, PreparedDecision, RateSample, Recovery, Reward, Role, SensoryInput, ShutdownParams, ShutdownResult, StatusResult, diff --git a/services/flysim/crates/flysim/Cargo.toml b/services/flysim/crates/flysim/Cargo.toml index 56499c8..4d3109e 100644 --- a/services/flysim/crates/flysim/Cargo.toml +++ b/services/flysim/crates/flysim/Cargo.toml @@ -50,6 +50,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] } unicode-normalization = "0.1" [dev-dependencies] +# The legacy profile's pinned identities (`tests/legacy_profile_identity.rs`): test-only, so the +# service itself does not link the session types. +fly-session-types = { path = "../fly-session-types" } futures-util = "0.3" # `default-features = false` drops the remote-reference resolver (and with it reqwest and a # TLS stack): `packages/feed/src/schema.json` is self-contained, so nothing has to be fetched. diff --git a/services/flysim/crates/flysim/tests/legacy_profile_identity.rs b/services/flysim/crates/flysim/tests/legacy_profile_identity.rs new file mode 100644 index 0000000..14f58cf --- /dev/null +++ b/services/flysim/crates/flysim/tests/legacy_profile_identity.rs @@ -0,0 +1,58 @@ +//! The legacy profile `gameboy-legacy-fafb-v783-v1` pins identities this service computes. +//! `fly-session-types` records them as constants (`legacy-gameboy-v1` section 2); this test is +//! where they are recomputed from the committed dataset and the service's own defaults, so the +//! profile cannot silently describe another fly. + +mod common; + +use std::sync::Arc; + +use fly_session_types::gameboy; +use fly_session_types::scalar::RationalNs; +use flybrain_core::agent::{AgentConfig, DEFAULT_WARMUP_MS, GAMEBOY_MS_PER_FRAME, NeuralAgent}; +use flybrain_core::dataset::load_brain_dataset_from_dir; +use flybrain_core::decoder::gameboy::{GAMEBOY_BUTTON_BITS, gameboy_decoder_config_with_macros}; + +#[test] +fn the_profile_fingerprint_and_versions_are_the_ones_this_build_computes() { + let path = common::repo_root().join("data/fafb-v783"); + if !path.join("meta.json").is_file() { + eprintln!("skipping: data/fafb-v783 is not present in this checkout"); + return; + } + let dataset = load_brain_dataset_from_dir(&path).expect("the committed dataset loads"); + assert_eq!( + dataset.fingerprint.as_deref(), + Some(gameboy::FAFB_V783_FINGERPRINT), + "the legacy profile embeds today's schema-1 fingerprint; a new one is a new profile id" + ); + let agent = NeuralAgent::new( + Arc::new(dataset), + AgentConfig::with_decoder(gameboy_decoder_config_with_macros(&[])), + ) + .expect("the default agent builds"); + assert_eq!(agent.network.version, gameboy::KERNEL_VERSION); + assert_eq!( + agent.network.plasticity.version, + gameboy::PLASTICITY_VERSION + ); + assert_eq!( + (u64::from(agent.frame.width), u64::from(agent.frame.height)), + (gameboy::VIEW_WIDTH, gameboy::VIEW_HEIGHT) + ); +} + +#[test] +fn the_profile_clock_warmup_and_joypad_are_the_service_defaults() { + assert_eq!(DEFAULT_WARMUP_MS, gameboy::WARMUP_MS); + // The legacy f64 frame period is exactly the rational step duration, in ms. + let step = gameboy::step_duration(); + let ms = RationalNs::reduced(548_625 * 1_000_000, 32_768).expect("ns"); + assert_eq!(step, ms); + assert_eq!(GAMEBOY_MS_PER_FRAME, 548_625.0 / 32_768.0); + let order: Vec<&str> = GAMEBOY_BUTTON_BITS.iter().map(|(name, _)| *name).collect(); + assert_eq!(order, gameboy::GAMEBOY_BUTTONS); + for (index, (_, bit)) in GAMEBOY_BUTTON_BITS.iter().enumerate() { + assert_eq!(*bit, 1 << index, "bit i is GAMEBOY_BUTTONS[i]"); + } +} From 24ba93350038a82776ed66c6e845914186052ed9 Mon Sep 17 00:00:00 2001 From: acamilo Date: Wed, 23 Sep 2026 08:32:05 +0000 Subject: [PATCH 2/5] docs: PROF-02a and RT-01a, the legacy Game Boy composition on the session framework The operator decided on 2026-09-23 to port the live fly onto the session framework in full. New contract legacy-gameboy-v1: the profile gameboy-legacy-fafb-v783-v1, the proof that the legacy f64 frame clock equals the rational one, the step-by-step placement of step_frame in lockstep-v1, the readout context (location allowed and declared), the channels decision, the memory-image inspection and ROM AssetRef, the environment (one no-button setup frame, u8->f32 audio, DC blocker at the edge, gameboy-slots-v1), the pokered-macros-v1 executor as one object with its task, the legacy-ratchet-rollback-v1 policy, the composition digest carrying decoder and macro-channel configuration, legacy-transient-reset restore semantics, sugar admission with a one-commit lag, and FLYSIM01 as format of record until RETIRE-01. PROF-02b is a stub. Dated amendments, each citing the decision, where earlier text kept the legacy loop outside lockstep or had no place for it: workers-v1 (telemetry, Initialize, executor, episode request kind, admission, new section 7 extension methods), step-v1 (rollback edge, Phase B/C, clock, episode policy, sugar lag), state-media-v1 (audio, memory-image retention, restore semantics, format of record, section 7 ratchet), README section 4, implementation.md (AGENT-01 and ENV-01 unblocked), the MaleCNS backlog (FOUNDATION-02 split, RUNTIME-01 contract) and analysis (5.2, 5.4), and readout.md (where the location comes from). --- docs/design/malecns-modular-implementation.md | 19 + docs/design/malecns-modular-sessions.md | 12 +- docs/design/session-framework/README.md | 22 ++ .../session-framework/implementation.md | 33 ++ .../session-framework/legacy-gameboy-v1.md | 366 ++++++++++++++++++ .../session-framework/state-media-v1.md | 48 +++ docs/design/session-framework/step-v1.md | 72 ++++ docs/design/session-framework/workers-v1.md | 120 ++++++ docs/readout.md | 11 + 9 files changed, 702 insertions(+), 1 deletion(-) create mode 100644 docs/design/session-framework/legacy-gameboy-v1.md diff --git a/docs/design/malecns-modular-implementation.md b/docs/design/malecns-modular-implementation.md index 7b66b88..5f0c7b8 100644 --- a/docs/design/malecns-modular-implementation.md +++ b/docs/design/malecns-modular-implementation.md @@ -68,6 +68,15 @@ already created. Builders use separate worktrees; the coordinator reviews contra - **Done:** empty required populations, malformed CSR and incorrect profile restores fail; legacy FAFB artifacts and default numerical version strings remain unchanged. +**2026-09-23: split by operator decision.** PROF-02a, the legacy profile, is done as a contract: +[legacy Game Boy composition v1](session-framework/legacy-gameboy-v1.md) defines +`gameboy-legacy-fafb-v783-v1` (today's schema-1 fingerprint embedded, `lif-1ms-f64-v2` and +`fly-kc-mbon-rstdp-v2` unchanged, the macro-role exception declared), the readout context +`gameboy-readout-context-v1`, the decision `gameboy-channels-v1`, and a composition declaration +whose digest carries the decoder and macro-channel configuration instead of the legacy +compatibility string. PROF-02b -- the bundle manifests, role mapping, strict graph validation and +profile-mismatch fixtures above -- is later and gates DATA-01, not the session port. + ### DATA-01 — Acquire and normalize MaleCNS - **Branch:** `feat/malecns-import` @@ -108,6 +117,16 @@ already created. Builders use separate worktrees; the coordinator reviews contra - **Done:** existing single-agent action/reward traces match and a fake environment can be driven through the same boundary without importing binjgb or task-specific addresses. +**2026-09-23: contract written (RT-01a), implementation pending.** The operator decided the +boundary: macros run in the coordinator's action executor over a per-boundary 64-KiB memory +image carried as an inspection artifact plus the ROM as an `AssetRef`; the emulator shim gains +one read-only bulk read and the joypad stays its only write; the Pokémon task and executor are +one object (`pokered-macros-v1`); the ratchet is the `legacy-ratchet-rollback-v1` episode policy +over the environment extension `gameboy-slots-v1`. The dated amendments are in +[workers-v1](session-framework/workers-v1.md), [step-v1](session-framework/step-v1.md) and +[state-media-v1](session-framework/state-media-v1.md); the session implementation guide's +ENV-01 carries the build. + ### RUNTIME-02 — Extract the single-agent session - **Branch:** `refactor/session-runtime` diff --git a/docs/design/malecns-modular-sessions.md b/docs/design/malecns-modular-sessions.md index 9be1214..095ad4b 100644 --- a/docs/design/malecns-modular-sessions.md +++ b/docs/design/malecns-modular-sessions.md @@ -548,6 +548,14 @@ clock may lead environment time by warm-up; persist that offset instead of prete clocks start at zero. Rendering, physics and decision cadence may differ, but the backend must define their relationship. +**Amendment, 2026-09-23 (operator decision of 2026-09-23).** "Preserve the detailed legacy +ordering inside the legacy single-agent composition" now means inside a legacy composition that +runs on the session framework's `lockstep-v1`, not beside it: the operator decided on a full port. +The detailed ordering is preserved because it is already the lockstep transaction order with one +agent ([legacy-gameboy-v1](session-framework/legacy-gameboy-v1.md) section 4), and "keep the +legacy floating remainder arithmetic" costs nothing, because the legacy `f64` frame constant is +exactly the rational Game Boy frame and the two accumulators are identical (section 3 there). + Start with sequential agent evaluation for reproducibility. Then compare parallel agent evaluation against the same action trace. Cap total worker budget: `agents × brain_threads` can otherwise oversubscribe the machine. Use private pools for concurrent agents or serialize @@ -605,7 +613,9 @@ jobs so repeated copies cannot exhaust memory under slow storage. Exact replay requires action-executor and admission state, not just the neural envelope. Legacy macros intentionally discard transient execution on restart; preserve that behavior -for v1 and label it as legacy continuation semantics, not exact session replay. New sessions +for v1 and label it as legacy continuation semantics, not exact session replay. +(*2026-09-23:* the label is `restore: legacy-transient-reset`, declared by the legacy +composition, [legacy-gameboy-v1](session-framework/legacy-gameboy-v1.md) section 14.) New sessions persist all behavior-affecting state or explicitly restart an episode under a documented rule. Keep `FLYSIM01` readable through a legacy adapter. Never silently rewrite a checkpoint on diff --git a/docs/design/session-framework/README.md b/docs/design/session-framework/README.md index af0796c..f4cbc1b 100644 --- a/docs/design/session-framework/README.md +++ b/docs/design/session-framework/README.md @@ -46,6 +46,15 @@ be built without them: envelope and the durable commit sequence. `FLYSIM01` is unchanged and stays separately readable. +One contract added by the 2026-09-23 amendments (PROF-02a, RT-01a), after the operator decided +to port the live fly onto this framework: + +- [Legacy Game Boy composition v1](legacy-gameboy-v1.md) — the legacy profile + `gameboy-legacy-fafb-v783-v1`, its readout context and decision schemas, the memory-image + inspection, the `pokered-macros-v1` executor, the `gameboy-slots-v1` environment extension, + the `legacy-ratchet-rollback-v1` episode policy, `legacy-transient-reset` restore semantics and + the composition digest. MaleCNS bundles (PROF-02b) come later. + For context: [modular-session analysis](../malecns-modular-sessions.md) and [Melee audit](../melee-framework-audit.md). Each contract owns its named subject; step ordering wins over an informal diagram, and Flybus owns transport/resource rules. Resolve contradictions @@ -126,6 +135,19 @@ versions, historical arithmetic/fingerprints and FLYSIM01 reader. New identities readout/executor/task/scheduler semantics. New public feed v2 is an application/presentation gateway contract built on the same internal bus; it does not replace the bus or expose it raw. +**Amendment, 2026-09-23 (operator decision of 2026-09-23).** "Keep legacy-gameboy-v1 distinct +from lockstep-v1" is superseded for scheduling. The operator decided on a **full port** of the +live fly onto the session framework: the legacy composition runs under `lockstep-v1` with one +agent, one port and one world, declared as [legacy-gameboy-v1](legacy-gameboy-v1.md). What the +sentence protected is kept, and now written down rather than implied: the legacy frame order is +this framework's transaction order (legacy-gameboy-v1 section 4); the historical fingerprint, +the default version strings and the compatibility string are unchanged and embedded, not +recomputed; the legacy clock is proven identical to the rational one (section 3); `FLYSIM01` +stays the format of record and its reader untouched until RETIRE-01; and the legacy restore and +rollback semantics are declared (`legacy-transient-reset`, `legacy-ratchet-rollback-v1`) rather +than approximated. The new identity covers what the old string never did: readout context, +decision, decoder configuration and macro channels, executor, slots and restore semantics. + ## 5. Reuse criterion Adding a third environment/application requires a backend, task/profile, composition and diff --git a/docs/design/session-framework/implementation.md b/docs/design/session-framework/implementation.md index bfb408a..a11fa4c 100644 --- a/docs/design/session-framework/implementation.md +++ b/docs/design/session-framework/implementation.md @@ -164,6 +164,18 @@ delayed rendering retains its handle. Distinguish AssetRef from transient Artifa (`feat/brain-profile-contract`) in the [MaleCNS backlog](../malecns-modular-implementation.md), which has not been built. Not started. +**2026-09-23:** unblocked. The operator split FOUNDATION-02 (decision of 2026-09-23): its legacy +half, PROF-02a, is the [legacy Game Boy composition](legacy-gameboy-v1.md) contract with +machine-readable profile, readout-context and decision schemas in `fly-session-types` and +`@flybrain/session-types`; the MaleCNS half (PROF-02b) is later and does not gate this slice. +AGENT-01 builds the legacy profile `gameboy-legacy-fafb-v783-v1` first. It must: report +`brainTicks` equal to the legacy `network.ms` (legacy-gameboy-v1 section 3); consume +`gameboy-readout-context-v1` and return `gameboy-channels-v1`; keep the held channel, blocked +window and last location as private readout state; report `stimulusRemainingMs`; answer +`Agent.Rollback` under capability `legacy-ratchet-rollback-v1`; and choose the mapping from the +legacy rate-role names (`command_0`, `macro_*`, which are not `Id`s) to `AgentGraph.rateRoles`, +which that contract leaves open. + **Implement:** adapter over existing LIF, plasticity, retina and fixed readout primitives; reference-first composition/goldens; independently seeded agent state and shared immutable data. Avoid using the old whole-frame `tick` wrapper if it changes the specified phase ordering. @@ -180,6 +192,27 @@ dispatch order and varying worker count preserves results. Keep 64-role limits e RUNTIME-01 (`refactor/environment-task-boundary`) in the same backlog, which has not been built. Not started. +**2026-09-23:** unblocked. RUNTIME-01's contract is the RT-01a amendments of 2026-09-23 to +[workers-v1](workers-v1.md) (sections 1, 3, 4, 5 and the new section 7), +[step-v1](step-v1.md) (sections 2, 3, 5 and 6) and [state-media-v1](state-media-v1.md) +(sections 2, 3, 4, 5 and 7), with the Game Boy specifics in +[legacy-gameboy-v1](legacy-gameboy-v1.md), all implementing the operator's decisions of +2026-09-23. ENV-01 and AGENT-01 may proceed in parallel against the schemas and fixtures; ENV-01 +needs FND-01's trace harness for its "legacy fixtures unchanged" acceptance. ENV-01 must also: +add the one read-only bulk memory read to the shim and prove it mutates nothing; publish the +memory image per boundary; declare the one-frame setup scaffold; convert audio to f32 and leave +the DC blocker to the edge; implement `gameboy-slots-v1`; and make the coordinator refuse +`episodeRequest.kind = "rollback"` in any composition that declares no rollback policy (the +synthetic coordinator today pauses on every episode request, which is safe but not the rule). + +**Amendment, 2026-09-23 (operator decision of 2026-09-23).** The "Implement" paragraph below said +to keep `legacy-gameboy-v1` separately routed. The operator decided on a full port instead: the +legacy composition runs on `lockstep-v1` as declared in legacy-gameboy-v1, and "exact old +ordering/hash semantics" is kept by construction and by test rather than by a separate route -- +the frame order maps one to one (section 4), the legacy clock equals the rational one +(section 3), and the fingerprint and compatibility string are embedded unchanged (sections 2 +and 12). The acceptance criteria below stand. + **Implement:** binjgb environment, task-local memory inspector and identity/existing action adapter. Keep `legacy-gameboy-v1` separately routed with exact old ordering/hash semantics. diff --git a/docs/design/session-framework/legacy-gameboy-v1.md b/docs/design/session-framework/legacy-gameboy-v1.md new file mode 100644 index 0000000..a41d36f --- /dev/null +++ b/docs/design/session-framework/legacy-gameboy-v1.md @@ -0,0 +1,366 @@ +# Legacy Game Boy composition v1 + +Status: **contract**, 2026-09-23. It covers PROF-02a, the legacy half of the FOUNDATION-02 split, +and the Game Boy parts of RT-01a. The generic parts of RT-01a are dated amendments to +[worker interfaces](workers-v1.md), [step protocol](step-v1.md) and +[session media/state](state-media-v1.md), and they point back here. This document changes no +runtime behaviour: `flysim` is unchanged and so is its compatibility string, 648 bytes, sha256 +`4929f3409b591ae21cf4a6d53e8e758b975c70f424eabb0b37db75c658b9ebd9`. + +## 1. The decision and what it replaces + +On 2026-09-23 the operator decided that the live fly gets a **full port** onto the session +framework. It is not kept outside the framework as a separately routed legacy loop. The +decisions this document implements are: + +| Subject | Decision | +| --- | --- | +| FOUNDATION-02 | Split. The legacy profile ships now; MaleCNS bundles ship later (02b, section 17) | +| Profile | `gameboy-legacy-fafb-v783-v1`. It embeds today's schema-1 fingerprint, and the kernel `lif-1ms-f64-v2` and plasticity `fly-kc-mbon-rstdp-v2` are unchanged | +| Readout context | Location is allowed and declared: `gameboy-readout-context-v1 {boot, bound[], location\|null}` | +| Decision | `gameboy-channels-v1`: the eight buttons plus the macro group's winner | +| Decoder identity | The decoder and macro-channel configuration go in the composition digest, not the legacy compatibility string | +| Environment boundary | Macros run in the coordinator's ActionExecutor. It reads a 64 KiB memory image each boundary, carried as an artifact in `inspection`, plus the ROM as an AssetRef | +| Emulator shim | One read-only bulk memory read is added. The joypad stays the only write | +| Task and executor | One object, declared as the extension `executor: pokered-macros-v1` | +| Audio | The environment converts u8 samples to f32, and the edge applies the DC blocker | +| Initialize | `Environment.Initialize` runs one frame with no button pressed | +| Rollback | `legacy-ratchet-rollback-v1` plus the environment extension `gameboy-slots-v1` | +| Restore | `restore: legacy-transient-reset`, which clears the ledgers, the location and the held channel | +| Sugar | Admission reads `reward_remaining` from the last commit's telemetry. A lag of one commit is accepted | +| Checkpoint | FLYSIM01 stays the format of record until RETIRE-01 | + +Several earlier statements said the legacy composition stays outside `lockstep-v1`: +[README](README.md) section 4, [implementation guide](implementation.md) ENV-01, +[state-media-v1](state-media-v1.md) section 7 and the +[modular-session analysis](../malecns-modular-sessions.md) section 5.2. Each now carries a +dated amendment that cites this decision. None was silently rewritten. The reason for the change is in +section 4: the legacy frame order already *is* the lockstep order, with one agent, one port +and one world. Moving it onto the framework therefore keeps every ordering fact those +statements protected. + +## 2. The profile `gameboy-legacy-fafb-v783-v1` + +Exactly one document exists. Every field of it is fixed, so both the document and its digest are constants of +this contract. Any other value is another profile and needs another id. The document is canonical +JSON (RFC 8785), its `AssetRef` is `{id: "gameboy-legacy-fafb-v783-v1", format: +"fly-profile-v1", digest, byteLength}`, and the digest and length are taken over the canonical +bytes. The current values are in `fixtures/gameboy-legacy.json`: digest `41e5d1ac…c60878`, length 1137. + +| Field | Value | Why it is fixed | +| --- | --- | --- | +| `profileId` | `gameboy-legacy-fafb-v783-v1` | | +| `datasetId`, `fingerprintSchema` | `fafb-v783`, `1` | The schema-1 fingerprint is the seven SHA-256 digests joined with `:` | +| `datasetFingerprint` | Today's value, byte for byte the compatibility string's segment 2 | This is the "embeds today's schema-1 fingerprint" of the decision. `flysim`'s `legacy_profile_identity` test recomputes it from `data/fafb-v783` | +| `kernelVersion`, `plasticityVersion` | `lif-1ms-f64-v2`, `fly-kc-mbon-rstdp-v2` | Pinned defaults (CLAUDE.md). The same test compares them with the built network | +| `tickDuration` | `1000000/1` ns | One model tick | +| `warmupMs` | `2500` | Fresh-start warm-up with learning disabled, `DEFAULT_WARMUP_MS` | +| `view` | `lcd`, 160 x 144 | The retina's native frame | +| `supportedStimuli` | `["reward-pulse"]` | Sugar and task reward events both drive `stimulate(durationMs)` | +| `readoutContextSchema`, `decisionSchema` | The registered references of sections 5 and 6 | | +| `legacyExceptions` | `["macro-roles-outside-fingerprint"]` | The `macro_*` roles are merged after the fingerprint is taken ([modular analysis](../malecns-modular-sessions.md) 2.2). This profile declares the gap instead of repairing it | + +The profile does not name the decoder timings or the macro channels. Those belong to the +composition (section 12), because the legacy compatibility string never covered them and the +decision puts them in the composition digest. + +## 3. Clock + +`stepDuration` is one Game Boy frame: 70224 cycles of a 4194304 Hz clock, which is +**`8572265625/512` ns** exactly. The legacy loop accumulates the `f64` constant +`1000 / (4194304 / 70224)`. That constant is exactly `548625/32768` ms, because it is dyadic and +the division rounds to the true value. Every remainder of the loop's `remainder += ms_per_frame` +is therefore a multiple of 2^-15 ms below 32, which is exact in `f64`. As a result, the legacy accumulator and the rational +accumulator of [step-v1](step-v1.md) section 5 produce **identical** tick counts and remainders. +Both languages assert this over the first 100,000 (Rust) and 20,000 (TypeScript) frames, and +the fixture records the first twelve frames: 16, 17, 17, 16, and so on. No separate legacy +arithmetic is needed, and step-v1's rule that the clock must not accumulate rounded time holds unchanged. A +FLYSIM01 remainder (`f64` ms) converts exactly to a `RationalNs`. + +The task's clock is the agent's brain time. `PreparedDecision.brainTicks` times 1 ms is the +legacy `network.ms`, and it counts warm-up. AGENT-01 must make `brainTicks` equal to that value, +because the ratchet windows and the reward adapter's timing read it. + +## 4. Placement in `lockstep-v1` + +The legacy `Sim::step_frame` order maps onto the transaction phases one to one: + +| Legacy step | Lockstep phase | +| --- | --- | +| Drain commands (sugar) | Admission cut at `Ready(k)`. The sugar enters `Prepare.preStepStimulations` (section 15) | +| `network.step(ticks)` | Phase A: `Agent.Prepare` advances the ticks | +| `decode_bound(rates, ms, boot, blocked, bound)` | Phase A: readout with the context of section 5. The blocked rule stays inside the agent | +| `MacroLayer::decide` | Phase B: the `pokered-macros-v1` executor reads O[k]'s memory image (section 10) | +| `set_buttons`, `run_frame` | Phase B: one `Environment.Advance` with the complete joypad batch | +| Framebuffer, `take_audio_u8` | The environment returns O[k+1]: view, audio chunk, memory image | +| `adapter.sample` | Phase C: the task, which is the same object, evaluates old/new inspection once | +| `stimulate` per event, `reinforce(sum)` | Phase D: `Agent.Commit` installs the input, then the stimulations in event order, then one reinforcement | +| `MacroLayer::observe`, `location()` | Phase C: this produces the next context's `bound` and `location` | +| Ratchet observe, capture, recover | Phase C decides. `Environment.SaveSlot` and the rollback run at `Ready(k+1)` (section 11) | +| Milestone archive | A durable save at `Ready(k+1)`, exported as FLYSIM01 (section 16) | + +The input installed at Commit and ticked at the next Prepare is the frame the legacy loop +hands `set_visual_frame` before it samples rewards. Rewards are sampled from the frame just +produced. This is the ordering that [modular analysis](../malecns-modular-sessions.md) 2.1 says must not +move, and it does not. FND-01's trace harness is where this mapping is proved against the running +loop. + +## 5. Readout context `gameboy-readout-context-v1` + +```ts +interface GameboyReadoutContext { + boot: boolean; // the adapter's boot gate after the last transition + bound: ChannelName[]; // the executor's bound macro channels, composition order; [] in raw mode + location: { area: number; x: number; y: number } | null; // the adapter's location, or no information +} +``` + +`ChannelName` is `^[a-z][a-z0-9_]{0,63}$`. Decoder channel and rate-role names carry `_`, so +they are not `Id`s. The context is what the task hands the decoder with each Prepare (the +`initialDecisionContext`, then every `nextDecisionContext`). `bound` is an ordered subset of the +composition's `macroChannels`. + +**Location is allowed and declared.** It is task inspection data, and it reaches exactly one +place: the readout's blocked-direction window ([readout](../../readout.md), "Blocked-direction +cooldown"), which restarts when the location changes. It never reaches the network, it +changes no score and it is not neural input. Declaring it here satisfies +[workers-v1](workers-v1.md) section 4: the context is typed, bounded, versioned and +allowlisted by the profile. The blocked direction itself is **not** in the context. The agent +computes it from its own held channel, its own clock, `blockedMs` and the location history. +The held channel, the start of the blocked window and the last location are private readout +state of the agent. + +## 6. Decision `gameboy-channels-v1` + +```ts +interface GameboyChannelsDecision { + buttons: { id: "up"|"down"|"left"|"right"|"a"|"b"|"start"|"select"; down: boolean }[]; // all eight, this order + macro: ChannelName | null; // the macro-group channel active in this decode +} +``` + +The `buttons` array is the decoder's active set packed in `GAMEBOY_BUTTON_BITS` order, so bit *i* is +`buttons[i]`. `macro` is the macro group's channel in the active set, which is always one of the +context's `bound` channels. Together they are everything `MacroLayer::decide` reads: the raw +mask and the active macro. No port assignment or inspection field is in the decision. + +## 7. Controller + +One port, `ControllerSchema {schema: gameboy-joypad-v1, buttons: [up, down, left, right, a, b, +start, select], axes: []}`. The executor's `ControllerIntent` is the joypad mask, and it +becomes the port's `PortControl`. It is the only input the environment applies to a running game. + +## 8. Inspection `gameboy-memory-inspection-v1` + +```ts +interface GameboyMemoryInspection { + memory: ArtifactRef; // 65,536 bytes: $0000..=$FFFF as the CPU sees it at this boundary + romDigest: Digest; // == EnvironmentDescriptor.contentDigest == the executor's rom AssetRef digest +} +``` + +- **The image.** Byte *i* is what `fly_gb_read_mem(i)` returns at this boundary, which is + what every task and executor read in the legacy loop sees through the per-frame read cache. + It is a listed bus attachment, content type `application/octet-stream`. Its digest is optional, + because it is a transient live artifact ([state-media-v1](state-media-v1.md) section 1). At + 59.73 frames per second it is about 3.9 MB/s. +- **The shim.** The emulator shim gains one function that fills a 65,536-byte buffer from + `emulator_read_mem` in address order. It is read-only. The implementing slice proves this by + exporting the emulator state before and after the call, comparing the bytes, and comparing + the buffer with 65,536 single reads. Nothing else in the shim changes. `fly_gb_set_buttons` stays + the only write, and there is no memory-write path. `read_uncached` is a probe tool and is + not available to a task or executor. +- **The ROM.** Macros read ROM banks (`MemoryReader::read_rom(bank, address)`) that the image + does not map. The executor gets the cartridge as a persistent `AssetRef` in the composition + (section 12), and it is refused unless its digest is the environment's `contentDigest`. The + image never carries ROM banks beyond the ones the CPU has mapped. +- **Retention.** The coordinator keeps O[k]'s image until transition k→k+1 has been evaluated. + The executor reads it in Phase B, and the task reads old and new images in Phase C. + +## 9. Environment + +- **Initialize.** The backend configuration declares a setup scaffold of **one frame with no + button down**, as the legacy fresh start runs. O[0] follows it, with `engineFrame` `"1"` and + `worldTime` `0/1`. That frame's audio is not published: O[0] carries no chunk + ([state-media-v1](state-media-v1.md) 2), and the audio origin is the first sample of + transition 0→1. +- **Advance.** Apply the mask, run one frame, and return O[k+1]. The view is `lcd` 160 x 144 `rgba8` + (row stride 640) with `observationDelaySteps` 0. `engineFrame` is the legacy frame counter as a + decimal string. +- **Audio.** The environment converts binjgb's unsigned 8-bit interleaved stereo to f32 with + binjgb's host rule, `sample / 255`. The result is unipolar in [0, 1] with silence at 0.0. The + stream is `f32le-interleaved`, 2 channels, at the configured rate (48,000 by default). The + environment does not filter. The **edge** applies the DC blocker (pole 0.995, per channel) + before presentation. It is presentation state: it is reset only when the edge restarts, it + is never in a checkpoint, and it never reaches an agent. +- **Descriptor.** `stepDuration` is `8572265625/512`, `inspectionSchema` is section 8, `recovery` is + `exact-checkpoint`, and `determinism` is `fixed-build`. +- **Slots, `gameboy-slots-v1`.** This is an environment capability for `Environment.SaveSlot` and + `Environment.RestoreSlot` ([workers-v1](workers-v1.md) section 7). A slot holds the emulator's + exported state and the framebuffer that was on screen, as the ratchet's `Snapshot` does. Slot + ids are declared by the composition (the legacy composition declares one, `best`). A save + replaces the slot. A restore imports the state, releases the buttons and returns the + archived framebuffer as a fresh view artifact, with a memory image read after the import. + It runs no frame. Every slot is part of the environment's `State.Capture` payload, as + FLYSIM01's `ratchet_game` and `ratchet_frame` are today. + +## 10. Executor `pokered-macros-v1` + +The Pokémon Red task (reward adapter, ladder, ratchet ledger) and its action executor (the +macro layer) are **one object**. It implements both [workers-v1](workers-v1.md) section 4 +interfaces and is declared as the extension `executor: pokered-macros-v1`. They cannot be +split without an undeclared channel between them, for three reasons. The executor's scene +observation produces the `bound` set the task hands the decoder. The macros read the adapter's +exploration and boundary ledgers. The macro layer's "nearer the objective" is a ratchet progress +signal. The object serves exactly one agent on one port. + +- **Phase B** (`ActionExecutor.apply`): inputs are the decision (section 6), O[k]'s memory + image, the ROM and the brain clock. The output is a joypad mask plus the macro start and finish + events. In raw mode it passes the decision's mask through. While a macro runs, the macro owns the pad. +- **Phase C** (`Task.evaluate_transition`): the adapter samples O[k+1]. Each reward event + becomes one `Reward` (its value) and one `Stimulus` of kind `reward-pulse` (its + `stimulation_ms`), in event order. The macro layer observes O[k+1]. The location is read. The + ratchet observes, and may request `SaveSlot` at `Ready(k+1)` or a rollback (section 11). Then + the next contexts `{boot, bound, location}` are produced. +- **Capture.** The task ledger is the adapter state (FLYSIM01's `reward` chunk) and the + ratchet state. The executor's ledgers (blocked, reached, talked, errand) and a running macro + are session state. They are **not** captured (section 14). +- **Cancel.** `cancel(ms)` abandons a running macro. It is followed by `observe` on the world + the fly now stands in. + +## 11. Episode policy `legacy-ratchet-rollback-v1` + +The ratchet's game-only rollback is a declared episode policy. When the ratchet fires during +transition k→k+1, the task returns `episodeRequest {kind: "rollback", reason, outcome}`. The +outcome is `legacy-ratchet-rollback-v1 {slotId, trigger: "stall" | "game-over"}`. The +transition's rewards commit first. The coordinator then applies the policy **at `Ready(k+1)`, +before the next Prepare, without pausing**: + +1. If this boundary also has a slot save due, `Environment.SaveSlot` runs first. +2. It picks a new epoch e'. `Environment.RestoreSlot(scope e',k+1; slotId, priorEpoch e)` restores + the slot and returns O'[k+1]. The boundary number stays the same. `worldTime` continues, + `engineFrame` continues, the view is the slot's archived frame, and there is no audio chunk. +3. Coordinator-local: the task clears the adapter's transient reward observations. The + executor runs `cancel`, then `observe(O'[k+1])`, which gives the next context. +4. `Agent.Rollback(scope e',k+1; priorEpoch e, input O'[k+1], context)` runs on every agent. + It clears the decoder holds (`clearHolds(now)`: holds, winners, fatigue, lockout) and the + plastic eligibility. It installs the slot frame as the next input, drops the held channel, + restarts the blocked window at now, and takes the location from the context. It runs **no + tick**, no reinforcement, no stimulation and no calibration. +5. With every reply in hand, the session is `Ready(e', k+1)`. Any failure fails the epoch and the + group restores from the last durable checkpoint. No participant resets alone. +6. A durable save follows, as the legacy loop checkpoints after a recovery. + +The following continue through the rollback: brain clock, membrane, RNG, learned gains, rates and +reward history, the ratchet ledger (its attempt and lifetime budgets were spent in Phase C), and +the adapter's persistent state. The first audio chunk after the rollback marks a +discontinuity. This is the legacy `recover_game` sequence with the neural half moved into the agent. +Each half touches only its own state, so the order between the halves does not matter. + +## 12. Composition declaration and digest + +```ts +interface LegacyGameboyComposition { + compositionId: Id; + scheduler: "lockstep-v1"; + profile: AssetRef; // the section 2 document + executor: { id: "pokered-macros-v1"; rom: AssetRef; adapter: Id; symbolProvenance: string; + mode: "raw" | "macros"; macroChannels: ChannelName[] }; // [] iff raw + decoderConfigDigest: Digest; // SHA-256 of the canonical effective DecoderConfig + environment: { extensions: ["gameboy-slots-v1"]; slots: Id[]; stepDuration: RationalNs; + inspectionSchema: SchemaRef; controllerSchema: SchemaRef; setupFrames: 1; + audio: { sampleRate: number; channels: 2 } }; + episodePolicy: "legacy-ratchet-rollback-v1"; + restore: "legacy-transient-reset"; + checkpointFormatOfRecord: "FLYSIM01"; + flysimCompatibility: string; // the FLYSIM01 string, recorded, not reinterpreted +} +``` + +- `decoderConfigDigest` is taken over the canonical JSON of the effective `DecoderConfig` in the + TypeScript oracle's shape (the Game Boy preset with its macro group). A change to a decoder + timing, a threshold or the macro channel set therefore changes the composition digest. + None of those changes touches the legacy compatibility string, which never covered them. +- The declaration's digest is the SHA-256 of its canonical JSON. The coordinator's + `compositionDigest` recipe (`fly-session/composition-v1`: session, epoch, contract, one line + per agent) gains one final line, `declaration=`, for a composition that has a + declaration. The synthetic composition has none and gains no line. +- `flysimCompatibility` must agree with the declaration in its kernel, adapter, fingerprint, + plasticity and `pokered:` segments, so the two cannot describe two different flies. Until RETIRE-01 + the **restore gate** is still that string and the legacy decision rules + (`flybrain_gb::compatibility::decide`, `FLY_ACCEPT_ADAPTERS`). The composition digest is the identity + for publication, traces and descriptor revisions, not a restore gate. This matches today's + behaviour, where a decoder change does not refuse a checkpoint. + +## 13. Machine-readable parts + +| Where | What | +| --- | --- | +| `fly-session-types/src/gameboy.rs`, `packages/session-types/src/gameboy.ts` | The five registered payload schemas, the profile, the composition declaration and their readers and cross-checks | +| `fly-session-types/src/extensions.rs`, `packages/session-types/src/extensions.ts` | `SaveSlot*`, `RestoreSlot*` and `AgentRollback*` payloads, which are in the session schema set | +| `fixtures/gameboy-legacy.json` (derived) | The extension set and its digest, every `SchemaRef`, the profile document with its canonical bytes and `AssetRef`, the frame clock, and an example composition with its digest and the digest recipe | +| `fixtures/valid.json`, `invalid.json` | Accepted and refused cases for every new type, held to both languages | +| `flysim/tests/legacy_profile_identity.rs` | Recomputes the fingerprint, versions, frame size, warm-up, clock and button order from the committed dataset and the service defaults | + +A payload schema's `SchemaRef.digest` is the SHA-256 of its canonical declaration +`{registry, id, version, source, fields}`. The legacy schemas are digested by their own +extension set, not by `contractDigest`: the session contract stays free of console state. +The generic changes of RT-01a (`stimulusRemainingMs`, `EpisodeRequestKind.rollback`, the six +extension payloads, `maxSlots`) *are* in the session schema set, and they moved +`contractDigest` to the value in `fixtures/contract-digest.json`. Regenerate with +`cargo run -p fly-session-types --example update_fixtures`. `tests/schema_set.rs` refuses +stale files. + +**Open for AGENT-01:** the legacy rate roles (`command_0`, `macro_go_item`, and so on) are not +valid `Id`s, while `AgentGraph.rateRoles` and `AgentTelemetry.rates[].roleId` are `Id`s. The +mapping belongs to the agent adapter. This contract does not choose it. + +## 14. Restore `legacy-transient-reset` + +The legacy composition declares that a restore (a FLYSIM01 load today, and any group restore +under this composition) is **not** an exact replay. The restored parts are those FLYSIM01 +carries: agent state, emulator, framebuffer, adapter state, ratchet state and slots, frame +counter, remainder, buttons and event watermark. The rest starts cleared, as it does in a fresh +process: + +- the executor's ledgers (blocked, reached, talked, errand) are empty, with no macro running; +- the agent's readout transient is cleared: no held channel, and no last location (the first + observed location starts the blocked window); +- the adapter's transient observations are cleared, as they are on a rollback. + +Resume tests compare against the legacy restore outcome, not against an uninterrupted trace +([state-media-v1](state-media-v1.md) section 4 amendment). This is the property the operator's +unstick procedure depends on: restarting the service clears the ledger-shaped traps and keeps +the rung. + +## 15. Sugar admission + +The coordinator owns admission, with the legacy rules: the per-minute limiter, and "no overlap +with an active pulse". The pulse is read from `AgentTelemetry.stimulusRemainingMs` of the **last completed +commit** (an `Agent.Rollback` reply counts as one). The value can therefore be one commit old, and the operator accepted this lag. In one direction a +pulse that ended inside the in-flight transition reads as still running, and the request is refused and retried. +In the other direction a reward pulse added by the in-flight transition is not yet visible, so a sugar can be +admitted over it, where the legacy loop would have refused. Each case is bounded to one frame. After a restore, and before the first commit, the pulse is unknown and admission +refuses with a retry. The duration is clamped to `[1, sugar_max_ms]`. An admitted sugar is a +`reward-pulse` `Stimulus` in the next Prepare's `preStepStimulations`, which is the position of the legacy +drain at the top of a frame. Legacy admission *is* application. Here, the epoch can fail between the two, so each +admission record carries its interaction id. If the Prepare that applies it never commits, the +admission is reported aborted and the edge refunds it. The bridge's fulfil path and refund path both get tests +in the slice that wires them ([workers-v1](workers-v1.md) section 5 amendment). + +## 16. Checkpoint format of record + +FLYSIM01 stays the format of record until RETIRE-01. Every durable save of this composition +(periodic, milestone archive, after a rollback) **exports a FLYSIM01 envelope** that the +current `flysim` reads, under the unchanged compatibility string. The deploy gate +(`--print-compatibility`) and `fly-reset-to-milestone` keep working on those files. A FLYSESS1 +checkpoint may be written beside it, but it is not what a restore selects until RETIRE-01 +says so. + +## 17. PROF-02b: MaleCNS bundles (later) + +Dataset manifests, original-ID mapping, anatomical roles, sensory and readout bindings, strict +graph validation for new bundles, and the composite behaviour identity of FOUNDATION-02 are +**not** in this document. They ship with PROF-02b, before DATA-01, as their own contract. +Nothing here constrains them, except that a new profile never reuses this profile's id or its +legacy exception. diff --git a/docs/design/session-framework/state-media-v1.md b/docs/design/session-framework/state-media-v1.md index 0285287..ac52f6c 100644 --- a/docs/design/session-framework/state-media-v1.md +++ b/docs/design/session-framework/state-media-v1.md @@ -87,6 +87,16 @@ profile. Resizing for viewers, overlays, composition, audio mixing/resampling, e browser delivery and streaming belong to the application/presentation layer. No bus or generic session configuration assumes a 1080p show or Twitch output. +**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** For the Game Boy +environment of the legacy composition ([legacy-gameboy-v1](legacy-gameboy-v1.md) section 9): +the environment converts binjgb's unsigned 8-bit interleaved stereo to the declared +`f32le-interleaved` with binjgb's host rule `sample / 255` (unipolar, silence at 0.0), and does +not filter. The DC blocker the running service applies before publishing (pole 0.995, per +channel) moves to the **edge**, as presentation: its state is the edge's, never checkpointed, +reset only when the edge restarts, and never seen by an agent. The observations a slot restore +returns carry no audio chunk, like the ones `ActivateRestore` returns (section 5 amendment of +2026-09-22), and the next chunk marks the discontinuity. + 640×480 RGBA at 60 fps produces 73.728 MB/s of raw image data. Artifact fan-out references one stored object; reads and any staging/seal copy still consume memory bandwidth. This is reasonable to measure before introducing codecs or pooled GPU buffers. Native dimensions @@ -117,6 +127,13 @@ If it violates configured resource policy, disconnect/restart that observer inst live data or silently skipping simulation input. Global store exhaustion is an explicit fault or pause condition; the router cannot guess that a particular live object is disposable. +**Amendment, 2026-09-23 (RT-01a).** An artifact-backed inspection -- the legacy composition's +64-KiB memory image per boundary -- is required coordinator input, not spectator data: the +coordinator retains `O[k]`'s image through the executor's use in the next transition's Phase B +and the task's old/new evaluation in its Phase C, and drops it after. It is never coalesced and +never published to observers by the session; about 3.9 MB/s at the Game Boy's cadence, budgeted +with the cached step observations above. + **Amendment, 2026-09-22 (PUBLISH-01).** "Disconnect/restart that observer" names an action no participant can take under [Flybus v1](bus-v1.md). Section 5 there makes publish admission all or nothing -- "for a bounded subscriber overflow, reject the **whole** publish; no partial @@ -152,6 +169,19 @@ The manifest records: or reproducible reconstruction inputs, admission state and event watermarks. - Payload names, lengths and hashes, including external-helper state required for exact resume. +**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** A composition may declare +**restore semantics** other than exact. The legacy composition declares +`restore: legacy-transient-reset` ([legacy-gameboy-v1](legacy-gameboy-v1.md) section 14): its +executor's ledgers and running macro, the agent's readout transient (held channel, blocked +window, last location) and the task's transient observations are **not** captured and start +cleared on every restore, exactly as the running service does on a restart. The +[modular analysis](../malecns-modular-sessions.md) section 5.4 already asked for this to be +labelled "legacy continuation semantics, not exact session replay"; the declaration is that +label, in the composition digest. The consequence is explicit: for this composition the +uninterrupted-versus-resumed trace equality of STATE-01 does not apply; its resume tests +compare against the legacy restore outcome. The operator's unstick procedure depends on the +reset. An environment's slots (`gameboy-slots-v1`) **are** world state and are captured. + Use a new envelope version; specify exact byte layout before production files. The historical letter-only chunk-name constraint is not silently widened, and FLYSIM01 remains separately readable. Persist payload **bytes and durable content identity**, not transient bus storeId, @@ -232,6 +262,13 @@ provide externally atomic resume, advertise episode-restart, not exact-checkpoin activation acknowledgments, install the coordinator's staged task/executor/admission state and establish Paused(new epoch,k). Failure during activation never permits half a group to run. +**Amendment, 2026-09-23 (operator decision of 2026-09-23).** `FLYSIM01` remains the **format of +record** for the legacy composition until RETIRE-01: every durable save exports a `FLYSIM01` +envelope the current service reads under its unchanged compatibility string, and restore +selects from those files through the legacy compatibility decision. A `FLYSESS1` checkpoint +may be written beside it; it is not a restore candidate for this composition until RETIRE-01 +says so ([legacy-gameboy-v1](legacy-gameboy-v1.md) section 16). + ## 6. Durable commit, router failure and recovery Write payload/envelope temporary generation, fsync, rename, fsync directory, then atomically @@ -264,3 +301,14 @@ state and retained/fresh brain components are explicit. Gain retention, eligibil clearing, calibration and first sensory input are part of the policy, tested independently. Legacy Pokémon ratchet behavior remains in the legacy composition. Shared competitive worlds never restore one player's environment independently of the other players. + +**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** The sentence above kept the +ratchet outside the framework. The operator decided on a full port instead, so the ratchet's +game-only rollback is now a declared episode policy of the legacy composition, +`legacy-ratchet-rollback-v1`, with the environment capability `gameboy-slots-v1` +([workers-v1](workers-v1.md) section 7, [step-v1](step-v1.md) section 6 amendment, +[legacy-gameboy-v1](legacy-gameboy-v1.md) section 11). It is a rollback, not a reset: a new +epoch at the same boundary, the episode and the brain continuing, the environment restoring a +slot, agents clearing holds and eligibility and installing the slot frame without a tick, the +executor cancelling then observing. What the original sentence protected still holds: the +policy is single-agent and a shared competitive world must not declare it. diff --git a/docs/design/session-framework/step-v1.md b/docs/design/session-framework/step-v1.md index ab8554b..d9edf2f 100644 --- a/docs/design/session-framework/step-v1.md +++ b/docs/design/session-framework/step-v1.md @@ -5,6 +5,13 @@ arrows below are RPCs through the same [Flybus router](bus-v1.md); the router it implements the barrier. Read [architecture](README.md) and [session RPC](ipc-v1.md) first. Method payloads are in [worker interfaces](workers-v1.md). +**Amendment, 2026-09-23 (operator decision of 2026-09-23).** The live Game Boy fly is ported +onto this protocol as the legacy composition of [legacy-gameboy-v1](legacy-gameboy-v1.md), +scheduled by `lockstep-v1` with one agent, one port and one world; it is no longer a separate +ordering. Its frame order is this document's transaction order (legacy-gameboy-v1 section 4 +maps it step by step), so the amendments below add capabilities to the protocol and change +none of its ordering rules. + ## 1. Committed boundary At `Ready(epoch, k)`: @@ -45,6 +52,19 @@ during a transition is served by the ordinary `Committing(k) → Ready(k+1)` edg transition to finish first, so the only boundary such a pause can land on is the one the transition just committed. +**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** One edge is added, for a +composition that declares a rollback policy: + +```text +Ready(e, k) → RollingBack(e', k) → Ready(e', k) +``` + +It is taken only when the transition that reached `k` returned `episodeRequest.kind = +"rollback"`, after that transition fully committed and before the next Prepare (section 6 +amendment). The boundary number does not change; the epoch does. A pause requested during it +lands on `Ready(e', k)`; any failure inside it is `Failed → Restoring(new epoch)`. Capture is +not allowed in `RollingBack`. + ## 3. Transaction sequence ### Phase A: prepare all agents concurrently @@ -76,6 +96,12 @@ After every PreparedDecision arrives: 2. Run each task-local action executor once, in sorted agent-ID order, against coherent current game state, task progress/objectives and clock from this boundary. Direct-control profiles use an identity executor. Macro profiles are explicit extensions. + + *Amendment, 2026-09-23 (RT-01a):* the legacy composition's extension is + `pokered-macros-v1`. "Coherent current game state" is the boundary's 64-KiB memory image + from `O[k].inspection` plus the ROM `AssetRef` -- never a live emulator read -- and "clock" + is the agent's brain time after its Prepare ([legacy-gameboy-v1](legacy-gameboy-v1.md) + sections 8 and 10). 3. Assemble all configured port controls in descriptor port order; reject duplicates/missing ports. Uncontrolled ports are configured neutral before the epoch, not supplied ad hoc. 4. Send exactly one `Environment.Advance(scope=k, batchId, controls)`. @@ -96,6 +122,15 @@ controls. It returns scoped rewards/stimulation, next decision contexts, progres an optional episode request. Commit its ledger update in memory and retain the result for this transition. No task output directly writes controllers or neural state. +**Amendment, 2026-09-23 (RT-01a).** The task may also ask for two things that happen at the +boundary this transition reaches, after Phase D, never inside it: a slot save +(`Environment.SaveSlot`, composition capability `gameboy-slots-v1`) and a rollback +(`episodeRequest.kind = "rollback"`). Both are recorded with the transition's result and +applied by the coordinator in the order *save, then rollback* (section 6 amendment). The +retained old inspection is what makes "evaluate once against old/new inspection" possible when +the inspection is artifact-backed: the coordinator keeps `O[k]`'s image until this evaluation +finishes. + ### Phase D: commit all agent outcomes concurrently Send `Agent.Commit(scope=k)` with that agent's next sensory observation and routed outcomes. @@ -171,6 +206,15 @@ Example: a synthetic 60-Hz environment with a 1-ms model tick produces 16,17,17 over three steps, totaling 50. A real backend's measured/declared emulated cadence may differ; never substitute this example's duration for Game Boy or Dolphin clocks. +**Amendment, 2026-09-23 (PROF-02a).** The Game Boy's declared cadence is one frame of 70224 +cycles at 4194304 Hz, `stepDuration = 8572265625/512` ns. The legacy service accumulates the +`f64` constant `1000 / (4194304 / 70224)` ms, which is exactly `548625/32768` ms, and every +remainder it produces is a multiple of 2^-15 ms below 32 -- exact in `f64`. The legacy +"floating remainder arithmetic" and this section's rational accumulator therefore give identical +ticks and remainders for every frame; both implementations assert it and +`fixtures/gameboy-legacy.json` records the first twelve frames (16, 17, 17, 16, ...). No legacy +exception to this section is needed ([legacy-gameboy-v1](legacy-gameboy-v1.md) section 3). + Wall time is only for pacing, health and presentation. The coordinator schedules absolute deadlines after committed boundaries; when behind, it omits sleep and reports lag. It does not skip world steps, drop neural ticks, or let one agent advance more slowly than another. @@ -200,6 +244,34 @@ exactly what is retained, cleared, warmed or recalibrated. No worker independent Changing port assignment, agent membership, model/profile, cadence or task schema requires a new composition/epoch. Hot-join and hot-swap during an active match are not v1 capabilities. +**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** A declared rollback policy +is an episode policy that does **not** pass through Paused and does **not** start a new episode. +For `legacy-ratchet-rollback-v1` ([legacy-gameboy-v1](legacy-gameboy-v1.md) section 11), once +every Commit of the transition that reached `k` has succeeded: + +1. If the same transition asked for a slot save, `Environment.SaveSlot(scope e,k)` first. +2. Choose a new epoch `e'`. `Environment.RestoreSlot(scope e',k; priorEpoch e)` returns the + restored `O'[k]`: same boundary, `worldTime` and `engineFrame` continue, no audio chunk. +3. Coordinator-local: the task clears its transient observations; the executor cancels any + running action and observes `O'[k]`, which yields the next decision contexts. +4. `Agent.Rollback(scope e',k; priorEpoch e)` on every agent concurrently: holds and + eligibility cleared, `O'[k]`'s view installed, no tick. +5. With every reply in hand: `Ready(e', k)`, then the usual durable save. + +Exactly what is retained, cleared and installed is named by the policy, as this section already +requires; nothing is reset by a worker on its own initiative, and a failure at any step fails +the epoch and restores the group coherently. "Reset uses a new epoch/episode and step 0" +remains the rule for terminal episodes; a rollback keeps the episode and the step numbering +because the brain, the audience's history and the frame counter all continue across it, as they +do in the running service. The policy is single-agent: a shared competitive world must not +declare it, because it would rewind one world under every player on one player's stall, which +[state-media-v1](state-media-v1.md) section 7 forbids. + +**Amendment, 2026-09-23 (RT-01a).** Sugar admission in the legacy composition reads the pulse +from the last completed commit's telemetry, so an admission decided while a transition is in +flight is at most one commit stale; the admitted stimulus still enters only at the next +admission cut of section 3, Phase A ([workers-v1](workers-v1.md) section 5 amendment). + ## 7. Failure rules | Failure point | Required response | diff --git a/docs/design/session-framework/workers-v1.md b/docs/design/session-framework/workers-v1.md index 9692dc6..9769270 100644 --- a/docs/design/session-framework/workers-v1.md +++ b/docs/design/session-framework/workers-v1.md @@ -22,6 +22,9 @@ rpc.result. Large inputs/outputs use owned bus attachments, never another worker | `State.Capture` | Coordinator → agent/environment | Immutable snapshot of committed boundary | | `State.StageRestore` | Coordinator → agent/environment | Validate replacement state under new epoch | | `State.ActivateRestore` | Coordinator → agent/environment | Install staged state; remain quiescent | +| `Environment.SaveSlot` | Coordinator → environment | Capability `gameboy-slots-v1`; record a slot at the committed boundary (section 7) | +| `Environment.RestoreSlot` | Coordinator → environment | Capability `gameboy-slots-v1`; boundary k under a new epoch (section 7) | +| `Agent.Rollback` | Coordinator → agent | Capability `legacy-ratchet-rollback-v1`; Ready(e,k) → Ready(e',k), no tick (section 7) | State methods have payloads in [state and media](state-media-v1.md). Artifact lifetime and message consumption are bus operations managed by the SDK, not Worker/Coordinator methods. @@ -51,6 +54,8 @@ interface AgentTelemetry { rates: { roleId: Id; hz: number }[]; learning: { enabled: boolean; updates: U64; changed: U64; signal: number }; } +// Amendment 2026-09-23: AgentTelemetry also carries +// stimulusRemainingMs: number | null; // pulse still running after the operation; null = reports none ``` `AssetRef` names persistent content in a preprovisioned local registry; it is not an arbitrary path or @@ -75,6 +80,15 @@ finite; shipped positive-only task profiles reject negatives. Empty rewards do n different numerical rule. `id`/`eventId` is unique within its outcome or command namespace; the coordinator assigns stable IDs before sending a mutating request. +**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** `AgentTelemetry` gains +`stimulusRemainingMs: number | null`: the milliseconds of stimulation pulse still running when +the operation that reports the telemetry completed, or `null` for an agent that has no pulse to +report. The operator decided that sugar admission reads the legacy `reward_remaining` from the +last commit's telemetry (section 5 amendment), and no field carried it. It is finite and +nonnegative; it is a report, never an input. The field is required-and-nullable like every +optional field in these contracts, so every existing producer now writes `null`. It changes +`contractDigest`, which [session RPC](ipc-v1.md) section 4 already provides for. + ## 2. Agent methods ### Agent.Initialize @@ -283,6 +297,25 @@ The environment only needs backend-relevant portions of task setup, not reward r neural policies. `taskConfig` resolves a declared setup configuration; the complete task implementation and ledger stay in the coordinator. +**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** Three readings for the +Game Boy environment of the legacy composition ([legacy-gameboy-v1](legacy-gameboy-v1.md) +sections 8 and 9), stated here because they are about this method's shape: + +- *Setup scaffold.* The backend configuration may declare setup frames the environment runs + with every control neutral before it returns O[0]. The legacy composition declares exactly + **one frame with no button down**, which is what its fresh start does; O[0] then has + `engineFrame` `"1"`, `worldTime` `0/1` and no audio chunk. These frames are declared + scaffold, attributed to no fly, and never a transition. +- *Inspection may be artifact-backed.* `inspection` is a `TypedValue` under the descriptor's + schema; the legacy schema `gameboy-memory-inspection-v1` carries a 64-KiB memory image as an + `ArtifactRef` (listed in the bus attachments) plus the ROM's digest. This is the "explicit + artifact-backed schema" section 1 requires for typed state over 32 KiB, and it is the one + bulk transfer per boundary the section 4 rule against per-byte remote reads asks for. The + environment's only write to a running game remains the controller batch. +- *Audio.* The environment publishes native samples in the declared f32 format; converting a + backend's integer samples to f32 is the environment's job (binjgb: `sample / 255`), and any + filtering for listening -- the legacy DC blocker -- is presentation, applied by the edge. + ### Environment.Advance ```ts @@ -349,6 +382,18 @@ but not a port assignment. The coordinator supplies the port. Per-agent executor private; a running macro may emit controls according to its declared policy, but only after neural selection. The first implementation supports the stateless identity executor only. +**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** The legacy composition +declares the extension **`executor: pokered-macros-v1`**: its task and its action executor are +**one object** implementing both interfaces above, serving one agent on one port +([legacy-gameboy-v1](legacy-gameboy-v1.md) section 10). They share state that neither could +reach through a declared channel if split: the executor's scene observation is the `bound` set +the task hands the decoder, the macros read the task's exploration ledgers, and the macro layer's +progress signal feeds the ratchet. The executor reads the boundary's memory image and the ROM +`AssetRef`, never the emulator. "Stateless identity executor only" remains true of the +synthetic composition; a stateful executor is permitted exactly where a composition declares +one by name, and its state is captured or declared transient by that composition's restore +semantics (state-media-v1 section 4 amendment). + The executor's currentGameState is a coherent read-only inspector view at this boundary; progressView supplies task history/objectives. It updates its selected action every step (movement, path replanning, interaction, completion), not merely replaying a blind button @@ -372,6 +417,15 @@ not arbitrary raw inspector memory or incoming chat. It requests a coordinator-owned policy transition after final reward commit; it cannot reset the environment directly. Generic progress is a TypedValue, not mandatory Pokémon ladder data. +**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** `episodeRequest.kind` is +`"terminal"` or **`"rollback"`**. A rollback request asks for the composition's declared +rollback policy; its `outcome` is that policy's registered schema. The only policy defined is +`legacy-ratchet-rollback-v1` (outcome `{slotId, trigger}`), applied at the boundary the +transition just committed without a pause ([step-v1](step-v1.md) section 6 amendment). A +composition that declares no rollback policy treats the request as a task failure. It still +"cannot reset the environment directly": the coordinator applies the policy through the +section 7 methods. + ## 5. Admission and audience boundary The first synthetic implementation has no audience input. Later integration maps permitted @@ -386,6 +440,19 @@ v2 contract must specify accepted/applied/rolled-back/aborted states and reconci paid interactions are enabled. Do not inherit a claim of durable exactly-once stimulation from these in-memory worker request caches. +**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** Sugar in the legacy +composition is admitted by the coordinator with the legacy rules -- the rate limiter and "no +overlap with an active pulse" -- reading the pulse from `stimulusRemainingMs` of the **last +completed commit**. The operator accepted that this value can be one commit old; both +consequences are bounded to one frame and are listed in +[legacy-gameboy-v1](legacy-gameboy-v1.md) section 15. Admitted sugar is a profile-supported +`reward-pulse` stimulus in the next Prepare's `preStepStimulations`. Because admission and +application are now separate, each admission record carries its interaction id and ends +`applied` or, if the epoch fails before that Prepare commits, `aborted`; the edge fulfils the +first and refunds the second, and the slice wiring the bridge tests both. This is the +accepted/applied/aborted distinction the paragraph above asks for, for this one interaction +kind; paid interactions in general still need the public v2 contract. + ## 6. Health, shutdown and extensions All workers implement the common Hello/Status/Shutdown/Acknowledge methods. Capture/restore @@ -395,3 +462,56 @@ advertised. Unsupported methods return `UNSUPPORTED`, mutation none. New task-specific fields belong in registered TypedValue schemas. New worker capabilities, variable-duration stepping, subscriptions or additional sensor modalities require a contract change and shared fixtures. An unconstrained plugin dictionary is not a substitute for that. + +## 7. Extension methods (amendment, 2026-09-23) + +**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** The operator decided on a +full port of the live fly, whose ratchet rolls the *game* back to a saved state while the brain +continues. Neither half of that is expressible with sections 2 and 3: there is no method that +saves or restores world state outside a coherent group checkpoint, and none that installs a new +input in an agent without a transition. These three methods are that, generically shaped, each +behind a capability a worker advertises in `Worker.Hello`; a worker without it answers +`UNSUPPORTED`, mutation none. Payloads are in `fly-session-types` (`extensions`) and +`@flybrain/session-types`, in the session schema set. + +```ts +// Environment.SaveSlot -- capability gameboy-slots-v1. Scope: the committed boundary (e, k). +interface SaveSlotParams { slotId: Id } // a slot the composition declares +interface SaveSlotResult { slotId: Id; boundary: U64; // == scope.step + stateDigest: Digest; byteLength: U64 } + +// Environment.RestoreSlot -- capability gameboy-slots-v1. Scope: (e', k), a NEW epoch. +interface RestoreSlotParams { slotId: Id; priorEpoch: Id; // the environment is Ready(e, k) + policy: "legacy-ratchet-rollback-v1" } +interface RestoreSlotResult { slotId: Id; committedStep: U64; // == k: no transition ran + observation: WorldObservation } // boundary k, no audio chunk + +// Agent.Rollback -- capability legacy-ratchet-rollback-v1. Scope: (e', k), the same new epoch. +interface AgentRollbackParams { agentId: Id; priorEpoch: Id; // the agent is Ready(e, k) + policy: "legacy-ratchet-rollback-v1"; + input: SensoryInput; // boundary k: the restored world + decisionContext: TypedValue } // for the next Prepare +interface AgentRollbackResult { agentId: Id; committedStep: U64; // == k: no tick ran + decisionContextDigest: Digest; telemetry: AgentTelemetry } +``` + +- Both environment methods run only at a committed boundary with no Advance outstanding. + `SaveSlot` replaces the slot's contents with the world state and the frame on screen at that + boundary; slots are environment state and belong to its `State.Capture` payload. It is one + operation per boundary under the section-5 operation key of [session RPC](ipc-v1.md). +- `RestoreSlot` and `Agent.Rollback` move a participant from `(priorEpoch, k)` to + `(scope.epoch, k)`; `priorEpoch` must differ from the scoped epoch, and after the move every + request scoped to the prior epoch is `STALE_EPOCH`. The restored observation keeps the + boundary number, `worldTime` and `engineFrame`, and returns fresh artifacts; it carries no + audio chunk, and the next chunk marks a discontinuity (state-media-v1 section 2). +- `Agent.Rollback` applies the policy's agent half and nothing else: for + `legacy-ratchet-rollback-v1`, clear decoder holds and plastic eligibility, install `input` + without a tick, reset the readout's transient (held channel, blocked window, location from + the context), keep the brain clock, membrane, RNG, rates and gains. No reward, stimulation or + calibration. It is the only way to install an input outside a Commit. +- A failure of any of these methods mid-policy fails the epoch; recovery is the coherent group + restore of [state-media-v1](state-media-v1.md) section 6. There is no partial rollback. +- `maxSlots` is 4 (a crate-chosen bound, published in the schema set). + +The sequence that uses them is [step-v1](step-v1.md) section 6's amendment and +[legacy-gameboy-v1](legacy-gameboy-v1.md) section 11. diff --git a/docs/readout.md b/docs/readout.md index 1b4ef52..e8a4a6d 100644 --- a/docs/readout.md +++ b/docs/readout.md @@ -99,6 +99,17 @@ argmax of the same normalized scores over the same rates. It is the same kind of the retina already sees, arriving through a much narrower channel, and it is disclosed on the honesty panel with the rest of the readout. +**Where the location comes from on the session framework (2026-09-23).** When the live fly runs +on the session framework (the operator's port decision of 2026-09-23), the sim loop that owned +the position is split: the task reads the location, the agent owns the decoder. The location +then reaches the decoder as a **declared** field of its decision context, +`gameboy-readout-context-v1 {boot, bound, location}` +([legacy Game Boy composition](design/session-framework/legacy-gameboy-v1.md) section 5). The rule is +unchanged: the location only restarts the blocked window, `null` is still no information, the +held channel and the window stay the readout's own state, and the blocked channel is still +computed here, never handed in. The profile allowlists the field, and the task cannot put +anything else in it. + ## Macro group (2026-09-16) `DecoderConfig.macros` is a second `ExclusiveGroup` with the same fields and the same decision rules From a6e1623698769c2c289777c433a528b55718780b Mon Sep 17 00:00:00 2001 From: acamilo Date: Wed, 23 Sep 2026 09:00:11 +0000 Subject: [PATCH 3/5] session types: boundary actions and captures in the step trace, a capture never precedes a slot save Review round 1, B1 and N4. TraceBehaviour.boundaryActions records the slot saves and the rollback at the reached boundary in application order; TraceOperational.captures records each capture with the number of boundary actions before it; TransitionTrace refuses a capture taken before the boundary's slot saves. Both languages, fixtures for the rule, synthetic coordinator records both lists empty. contractDigest moves to a56e25e6. --- packages/session-types/src/trace.ts | 97 +++ .../fixtures/contract-digest.json | 6 +- .../fly-session-types/fixtures/invalid.json | 719 +++++++++++++++++- .../fixtures/schema-set.json | 2 +- .../fly-session-types/fixtures/traces.json | 74 +- .../fly-session-types/fixtures/valid.json | 284 ++++++- .../crates/fly-session-types/src/schema.rs | 15 + .../crates/fly-session-types/src/trace.rs | 191 +++++ .../crates/fly-session/src/coordinator.rs | 5 + 9 files changed, 1344 insertions(+), 49 deletions(-) diff --git a/packages/session-types/src/trace.ts b/packages/session-types/src/trace.ts index 339e31b..e14b2e8 100644 --- a/packages/session-types/src/trace.ts +++ b/packages/session-types/src/trace.ts @@ -23,6 +23,25 @@ import { ownerToken, } from './scalar'; import { MAX_AGENTS, MAX_RATE_ROLES } from './workers'; +import { MAX_SLOTS } from './extensions'; + +/** Boundary actions at the reached boundary (amendment of 2026-09-23, RT-01a). */ +export const BOUNDARY_ACTION_KINDS = ['save-slot', 'rollback'] as const; +export type BoundaryActionKind = (typeof BOUNDARY_ACTION_KINDS)[number]; +/** Every slot at most once, plus one rollback. */ +export const MAX_BOUNDARY_ACTIONS = MAX_SLOTS + 1; + +export interface BoundaryAction { + kind: BoundaryActionKind; + slotId: Id; + stateDigest: Digest | null; +} + +/** A checkpoint capture at the reached boundary and how many boundary actions preceded it. */ +export interface TraceCapture { + checkpointId: Id; + afterActions: number; +} export interface TraceAgent { agentId: Id; @@ -49,6 +68,7 @@ export interface TraceBehaviour { outcomeIds: Id[]; eventIds: Id[]; publishedBoundary: U64; + boundaryActions: BoundaryAction[]; } export interface TraceRequest { @@ -63,6 +83,7 @@ export interface TraceOperational { commitRequestIds: TraceRequest[]; busCallIds: BusCallId[]; deliveryIds: OwnerToken[]; + captures: TraceCapture[]; } export interface TransitionTrace { @@ -107,7 +128,18 @@ export function readTraceBehaviour(value: unknown): TraceBehaviour { const outcomeIds = reader.idList('outcomeIds', 0, MAX_RATE_ROLES); const eventIds = reader.idList('eventIds', 0, MAX_RATE_ROLES); const publishedBoundary = reader.u64('publishedBoundary'); + const boundaryActions = reader.list('boundaryActions', 0, MAX_BOUNDARY_ACTIONS, (item) => { + const action = new Reader(item, 'TraceBehaviour.boundaryActions'); + const entry: BoundaryAction = { + kind: action.enumeration('kind', BOUNDARY_ACTION_KINDS), + slotId: action.id('slotId'), + stateDigest: action.value('stateDigest') === null ? null : action.digest('stateDigest'), + }; + action.finish(); + return entry; + }); reader.finish(); + validateBoundaryActions(boundaryActions); requireUnique( agents.map((agent) => agent.agentId), @@ -141,9 +173,39 @@ export function readTraceBehaviour(value: unknown): TraceBehaviour { outcomeIds, eventIds, publishedBoundary, + boundaryActions, }; } +/** Slot saves first, each slot once with its digest; at most one rollback, last, no digest. */ +function validateBoundaryActions(actions: readonly BoundaryAction[]): void { + let rolledBack = false; + const saved: string[] = []; + for (const action of actions) { + if (rolledBack) fail('TraceBehaviour: nothing follows a rollback at the same boundary'); + if (action.kind === 'save-slot') { + if (action.stateDigest === null) fail('TraceBehaviour: a slot save records its state digest'); + if (saved.includes(action.slotId)) { + fail('TraceBehaviour: a slot is saved at most once per boundary'); + } + saved.push(action.slotId); + } else { + if (action.stateDigest !== null) fail('TraceBehaviour: a rollback records no state digest'); + rolledBack = true; + } + } +} + +/** How many leading boundary actions are slot saves. */ +export function slotSaves(behaviour: TraceBehaviour): number { + let count = 0; + for (const action of behaviour.boundaryActions) { + if (action.kind !== 'save-slot') break; + count += 1; + } + return count; +} + export function readTraceOperational(value: unknown): TraceOperational { const reader = new Reader(value, 'TraceOperational'); const readRequests = (item: unknown): TraceRequest => { @@ -162,6 +224,15 @@ export function readTraceOperational(value: unknown): TraceOperational { commitRequestIds: reader.list('commitRequestIds', 1, MAX_AGENTS, readRequests), busCallIds: reader.list('busCallIds', 0, 64, busCallId), deliveryIds: reader.list('deliveryIds', 0, 64, ownerToken), + captures: reader.list('captures', 0, MAX_BOUNDARY_ACTIONS + 1, (item) => { + const capture = new Reader(item, 'TraceOperational.captures'); + const entry: TraceCapture = { + checkpointId: capture.id('checkpointId'), + afterActions: capture.int('afterActions', 0, MAX_BOUNDARY_ACTIONS), + }; + capture.finish(); + return entry; + }), }; reader.finish(); requireUnique( @@ -174,6 +245,17 @@ export function readTraceOperational(value: unknown): TraceOperational { ); requireUnique(operational.busCallIds, 'TraceOperational.busCallIds'); requireUnique(operational.deliveryIds, 'TraceOperational.deliveryIds'); + requireUnique( + operational.captures.map((capture) => capture.checkpointId), + 'TraceOperational.captures', + ); + let last = 0; + for (const capture of operational.captures) { + if (capture.afterActions < last) { + fail('TraceOperational: captures are recorded in the order they were taken'); + } + last = capture.afterActions; + } return operational; } @@ -194,6 +276,20 @@ export function readTransitionTrace(value: unknown): TransitionTrace { } } } + // A slot save due at a boundary completes before any capture there (legacy-gameboy-v1 16). + const saves = slotSaves(trace.behaviour); + for (const capture of trace.operational.captures) { + if (capture.afterActions < saves) { + fail( + `TransitionTrace: capture "${capture.checkpointId}" was taken before this boundary's slot saves completed`, + ); + } + if (capture.afterActions > trace.behaviour.boundaryActions.length) { + fail( + `TransitionTrace: capture "${capture.checkpointId}" counts more boundary actions than were applied`, + ); + } + } return trace; } @@ -242,6 +338,7 @@ export function behaviourDiff(left: TransitionTrace, right: TransitionTrace): st } if (differs(a.outcomeIds, b.outcomeIds)) out.push('outcomeIds differ'); if (differs(a.eventIds, b.eventIds)) out.push('eventIds differ'); + if (differs(a.boundaryActions, b.boundaryActions)) out.push('boundaryActions differ'); const idsA = a.agents.map((agent) => agent.agentId); const idsB = b.agents.map((agent) => agent.agentId); if (differs(idsA, idsB)) { diff --git a/services/flysim/crates/fly-session-types/fixtures/contract-digest.json b/services/flysim/crates/fly-session-types/fixtures/contract-digest.json index c81a576..06695d7 100644 --- a/services/flysim/crates/fly-session-types/fixtures/contract-digest.json +++ b/services/flysim/crates/fly-session-types/fixtures/contract-digest.json @@ -1,9 +1,9 @@ { "description": "contractDigest is the SHA-256 of the canonical schema set in schema-set.json.", - "contractDigest": "f61e8f4fa9336184ebb6f0305f9d6135872707ef5536d51b9933df8943653c77", + "contractDigest": "a56e25e6289a6f9e6720ba5f727651e9e4e2c5cf7a68990fc1f8d37fe12f2006", "schemaSetVersion": 1, - "schemaSetBytes": 30184, + "schemaSetBytes": 30711, "types": 60, - "enums": 11, + "enums": 12, "limits": 27 } diff --git a/services/flysim/crates/fly-session-types/fixtures/invalid.json b/services/flysim/crates/fly-session-types/fixtures/invalid.json index c7456fd..c0ef279 100644 --- a/services/flysim/crates/fly-session-types/fixtures/invalid.json +++ b/services/flysim/crates/fly-session-types/fixtures/invalid.json @@ -4387,7 +4387,8 @@ "observationBoundaries": [], "outcomeIds": [], "eventIds": [], - "publishedBoundary": "42" + "publishedBoundary": "42", + "boundaryActions": [] }, "operational": { "wallTimeNs": "1", @@ -4405,7 +4406,8 @@ } ], "busCallIds": [], - "deliveryIds": [] + "deliveryIds": [], + "captures": [] } }, "reason": "a commit acknowledges the transition's next boundary" @@ -5759,6 +5761,719 @@ "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" }, "reason": "FLYSIM01 stays until RETIRE-01" + }, + { + "name": "a capture taken before the boundary's slot save", + "type": "TransitionTrace", + "value": { + "behaviour": { + "scope": { + "sessionId": "demo", + "epoch": "epoch-1", + "step": "41" + }, + "agents": [ + { + "agentId": "fly-a", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2517", + "remainder": { + "numerator": "1000000", + "denominator": "3" + }, + "decisionDigest": "a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b", + "committedStep": "42" + }, + { + "agentId": "fly-b", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2550", + "remainder": { + "numerator": "0", + "denominator": "1" + }, + "decisionDigest": "ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e", + "committedStep": "42" + } + ], + "batchId": "batch-41", + "controlDigest": "1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6", + "acknowledgedBoundary": "42", + "observationBoundaries": [ + { + "viewId": "screen", + "producedStep": "42" + } + ], + "outcomeIds": [ + "outcome-1" + ], + "eventIds": [ + "evt-1" + ], + "publishedBoundary": "42", + "boundaryActions": [ + { + "kind": "save-slot", + "slotId": "best", + "stateDigest": "5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d" + } + ] + }, + "operational": { + "wallTimeNs": "1234567890", + "prepareRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-41" + }, + { + "agentId": "fly-b", + "requestId": "req-42" + } + ], + "advanceRequestId": "req-43", + "commitRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-44" + }, + { + "agentId": "fly-b", + "requestId": "req-45" + } + ], + "busCallIds": [ + "call-100", + "call-101", + "call-102" + ], + "deliveryIds": [ + "dlv-7", + "own-9" + ], + "captures": [ + { + "checkpointId": "milestone-11", + "afterActions": 0 + } + ] + } + }, + "reason": "a checkpoint must not pair the new ledger with the old slot (review round 1, B1)" + }, + { + "name": "a capture counting actions that were never applied", + "type": "TransitionTrace", + "value": { + "behaviour": { + "scope": { + "sessionId": "demo", + "epoch": "epoch-1", + "step": "41" + }, + "agents": [ + { + "agentId": "fly-a", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2517", + "remainder": { + "numerator": "1000000", + "denominator": "3" + }, + "decisionDigest": "a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b", + "committedStep": "42" + }, + { + "agentId": "fly-b", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2550", + "remainder": { + "numerator": "0", + "denominator": "1" + }, + "decisionDigest": "ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e", + "committedStep": "42" + } + ], + "batchId": "batch-41", + "controlDigest": "1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6", + "acknowledgedBoundary": "42", + "observationBoundaries": [ + { + "viewId": "screen", + "producedStep": "42" + } + ], + "outcomeIds": [ + "outcome-1" + ], + "eventIds": [ + "evt-1" + ], + "publishedBoundary": "42", + "boundaryActions": [ + { + "kind": "save-slot", + "slotId": "best", + "stateDigest": "5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d" + } + ] + }, + "operational": { + "wallTimeNs": "1234567890", + "prepareRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-41" + }, + { + "agentId": "fly-b", + "requestId": "req-42" + } + ], + "advanceRequestId": "req-43", + "commitRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-44" + }, + { + "agentId": "fly-b", + "requestId": "req-45" + } + ], + "busCallIds": [ + "call-100", + "call-101", + "call-102" + ], + "deliveryIds": [ + "dlv-7", + "own-9" + ], + "captures": [ + { + "checkpointId": "milestone-11", + "afterActions": 2 + } + ] + } + }, + "reason": "afterActions <= the boundary's actions" + }, + { + "name": "a slot save after a rollback", + "type": "TransitionTrace", + "value": { + "behaviour": { + "scope": { + "sessionId": "demo", + "epoch": "epoch-1", + "step": "41" + }, + "agents": [ + { + "agentId": "fly-a", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2517", + "remainder": { + "numerator": "1000000", + "denominator": "3" + }, + "decisionDigest": "a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b", + "committedStep": "42" + }, + { + "agentId": "fly-b", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2550", + "remainder": { + "numerator": "0", + "denominator": "1" + }, + "decisionDigest": "ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e", + "committedStep": "42" + } + ], + "batchId": "batch-41", + "controlDigest": "1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6", + "acknowledgedBoundary": "42", + "observationBoundaries": [ + { + "viewId": "screen", + "producedStep": "42" + } + ], + "outcomeIds": [ + "outcome-1" + ], + "eventIds": [ + "evt-1" + ], + "publishedBoundary": "42", + "boundaryActions": [ + { + "kind": "rollback", + "slotId": "best", + "stateDigest": null + }, + { + "kind": "save-slot", + "slotId": "best", + "stateDigest": "5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d" + } + ] + }, + "operational": { + "wallTimeNs": "1234567890", + "prepareRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-41" + }, + { + "agentId": "fly-b", + "requestId": "req-42" + } + ], + "advanceRequestId": "req-43", + "commitRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-44" + }, + { + "agentId": "fly-b", + "requestId": "req-45" + } + ], + "busCallIds": [ + "call-100", + "call-101", + "call-102" + ], + "deliveryIds": [ + "dlv-7", + "own-9" + ], + "captures": [] + } + }, + "reason": "nothing follows a rollback at the same boundary" + }, + { + "name": "a slot save without its state digest", + "type": "TransitionTrace", + "value": { + "behaviour": { + "scope": { + "sessionId": "demo", + "epoch": "epoch-1", + "step": "41" + }, + "agents": [ + { + "agentId": "fly-a", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2517", + "remainder": { + "numerator": "1000000", + "denominator": "3" + }, + "decisionDigest": "a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b", + "committedStep": "42" + }, + { + "agentId": "fly-b", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2550", + "remainder": { + "numerator": "0", + "denominator": "1" + }, + "decisionDigest": "ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e", + "committedStep": "42" + } + ], + "batchId": "batch-41", + "controlDigest": "1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6", + "acknowledgedBoundary": "42", + "observationBoundaries": [ + { + "viewId": "screen", + "producedStep": "42" + } + ], + "outcomeIds": [ + "outcome-1" + ], + "eventIds": [ + "evt-1" + ], + "publishedBoundary": "42", + "boundaryActions": [ + { + "kind": "save-slot", + "slotId": "best", + "stateDigest": null + } + ] + }, + "operational": { + "wallTimeNs": "1234567890", + "prepareRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-41" + }, + { + "agentId": "fly-b", + "requestId": "req-42" + } + ], + "advanceRequestId": "req-43", + "commitRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-44" + }, + { + "agentId": "fly-b", + "requestId": "req-45" + } + ], + "busCallIds": [ + "call-100", + "call-101", + "call-102" + ], + "deliveryIds": [ + "dlv-7", + "own-9" + ], + "captures": [] + } + }, + "reason": "a slot save records the saved state's digest" + }, + { + "name": "the same slot saved twice at one boundary", + "type": "TransitionTrace", + "value": { + "behaviour": { + "scope": { + "sessionId": "demo", + "epoch": "epoch-1", + "step": "41" + }, + "agents": [ + { + "agentId": "fly-a", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2517", + "remainder": { + "numerator": "1000000", + "denominator": "3" + }, + "decisionDigest": "a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b", + "committedStep": "42" + }, + { + "agentId": "fly-b", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2550", + "remainder": { + "numerator": "0", + "denominator": "1" + }, + "decisionDigest": "ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e", + "committedStep": "42" + } + ], + "batchId": "batch-41", + "controlDigest": "1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6", + "acknowledgedBoundary": "42", + "observationBoundaries": [ + { + "viewId": "screen", + "producedStep": "42" + } + ], + "outcomeIds": [ + "outcome-1" + ], + "eventIds": [ + "evt-1" + ], + "publishedBoundary": "42", + "boundaryActions": [ + { + "kind": "save-slot", + "slotId": "best", + "stateDigest": "5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d" + }, + { + "kind": "save-slot", + "slotId": "best", + "stateDigest": "5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d" + } + ] + }, + "operational": { + "wallTimeNs": "1234567890", + "prepareRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-41" + }, + { + "agentId": "fly-b", + "requestId": "req-42" + } + ], + "advanceRequestId": "req-43", + "commitRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-44" + }, + { + "agentId": "fly-b", + "requestId": "req-45" + } + ], + "busCallIds": [ + "call-100", + "call-101", + "call-102" + ], + "deliveryIds": [ + "dlv-7", + "own-9" + ], + "captures": [] + } + }, + "reason": "a slot is saved at most once per boundary" + }, + { + "name": "an unknown boundary action", + "type": "TransitionTrace", + "value": { + "behaviour": { + "scope": { + "sessionId": "demo", + "epoch": "epoch-1", + "step": "41" + }, + "agents": [ + { + "agentId": "fly-a", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2517", + "remainder": { + "numerator": "1000000", + "denominator": "3" + }, + "decisionDigest": "a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b", + "committedStep": "42" + }, + { + "agentId": "fly-b", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2550", + "remainder": { + "numerator": "0", + "denominator": "1" + }, + "decisionDigest": "ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e", + "committedStep": "42" + } + ], + "batchId": "batch-41", + "controlDigest": "1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6", + "acknowledgedBoundary": "42", + "observationBoundaries": [ + { + "viewId": "screen", + "producedStep": "42" + } + ], + "outcomeIds": [ + "outcome-1" + ], + "eventIds": [ + "evt-1" + ], + "publishedBoundary": "42", + "boundaryActions": [ + { + "kind": "reset", + "slotId": "best", + "stateDigest": null + } + ] + }, + "operational": { + "wallTimeNs": "1234567890", + "prepareRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-41" + }, + { + "agentId": "fly-b", + "requestId": "req-42" + } + ], + "advanceRequestId": "req-43", + "commitRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-44" + }, + { + "agentId": "fly-b", + "requestId": "req-45" + } + ], + "busCallIds": [ + "call-100", + "call-101", + "call-102" + ], + "deliveryIds": [ + "dlv-7", + "own-9" + ], + "captures": [] + } + }, + "reason": "BoundaryActionKind is closed" + }, + { + "name": "captures recorded out of order", + "type": "TransitionTrace", + "value": { + "behaviour": { + "scope": { + "sessionId": "demo", + "epoch": "epoch-1", + "step": "41" + }, + "agents": [ + { + "agentId": "fly-a", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2517", + "remainder": { + "numerator": "1000000", + "denominator": "3" + }, + "decisionDigest": "a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b", + "committedStep": "42" + }, + { + "agentId": "fly-b", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2550", + "remainder": { + "numerator": "0", + "denominator": "1" + }, + "decisionDigest": "ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e", + "committedStep": "42" + } + ], + "batchId": "batch-41", + "controlDigest": "1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6", + "acknowledgedBoundary": "42", + "observationBoundaries": [ + { + "viewId": "screen", + "producedStep": "42" + } + ], + "outcomeIds": [ + "outcome-1" + ], + "eventIds": [ + "evt-1" + ], + "publishedBoundary": "42", + "boundaryActions": [ + { + "kind": "save-slot", + "slotId": "best", + "stateDigest": "5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d" + }, + { + "kind": "rollback", + "slotId": "best", + "stateDigest": null + } + ] + }, + "operational": { + "wallTimeNs": "1234567890", + "prepareRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-41" + }, + { + "agentId": "fly-b", + "requestId": "req-42" + } + ], + "advanceRequestId": "req-43", + "commitRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-44" + }, + { + "agentId": "fly-b", + "requestId": "req-45" + } + ], + "busCallIds": [ + "call-100", + "call-101", + "call-102" + ], + "deliveryIds": [ + "dlv-7", + "own-9" + ], + "captures": [ + { + "checkpointId": "a", + "afterActions": 2 + }, + { + "checkpointId": "b", + "afterActions": 1 + } + ] + } + }, + "reason": "captures are recorded in the order taken" } ] } diff --git a/services/flysim/crates/fly-session-types/fixtures/schema-set.json b/services/flysim/crates/fly-session-types/fixtures/schema-set.json index 7f3c83e..2cb6c44 100644 --- a/services/flysim/crates/fly-session-types/fixtures/schema-set.json +++ b/services/flysim/crates/fly-session-types/fixtures/schema-set.json @@ -1 +1 @@ -{"contract":"fly-session-types","enums":[{"members":["f32le-interleaved"],"name":"AudioFormat","source":"state-media-v1 2"},{"members":["bipolar","unit"],"name":"AxisRange","source":"workers-v1 3"},{"members":["fixed-build","unverified"],"name":"Determinism","source":"workers-v1 3"},{"members":["terminal","rollback"],"name":"EpisodeRequestKind","source":"workers-v1 4"},{"members":["INVALID_ARGUMENT","UNSUPPORTED","IDENTITY_MISMATCH","STALE_EPOCH","STALE_STEP","FUTURE_STEP","INVALID_PHASE","CONFLICT","IN_PROGRESS","BUSY","BUFFER_INVALID","RESULT_EXPIRED","INCOMPATIBLE_STATE","BACKEND_FAILURE","INTERNAL"],"name":"ErrorCode","source":"ipc-v1 7"},{"members":["none","applied","unknown"],"name":"MutationCertainty","source":"ipc-v1 3"},{"members":["exact-checkpoint","episode-restart"],"name":"Recovery","source":"workers-v1 3"},{"members":["agent","environment","coordinator"],"name":"Role","source":"ipc-v1 4"},{"members":["lockstep-v1"],"name":"SchedulerId","source":"publishing-v1 3"},{"members":["rgba8"],"name":"ViewFormat","source":"state-media-v1 2"},{"members":["uninitialized","ready","preparing","prepared","advancing","committing","capturing","staged-restore","restoring","failed","stopping"],"name":"WorkerState","source":"ipc-v1 4"}],"limits":[{"name":"maxAcknowledge","source":"ipc-v1 5","value":16},{"name":"maxAgents","source":"ipc-v1 2","value":4},{"name":"maxAssets","source":"crate","value":64},{"name":"maxAttachments","source":"bus-v1 4","value":32},{"name":"maxAudioStreams","source":"crate","value":8},{"name":"maxAxes","source":"workers-v1 3","value":16},{"name":"maxButtons","source":"workers-v1 3","value":32},{"name":"maxCapabilities","source":"crate","value":32},{"name":"maxEngineFrameLength","source":"workers-v1 3","value":64},{"name":"maxEnvelopeBytes","source":"bus-v1 4","value":65536},{"name":"maxMessageCodePoints","source":"ipc-v1 7","value":512},{"name":"maxObservationDelaySteps","source":"state-media-v1 2","value":8},{"name":"maxPixelAspectPart","source":"state-media-v1 2","value":65535},{"name":"maxPorts","source":"ipc-v1 2","value":4},{"name":"maxRateRoles","source":"ipc-v1 2","value":64},{"name":"maxRewardsPerOperation","source":"workers-v1 1","value":64},{"name":"maxSampleFrames","source":"state-media-v1 2","value":192000},{"name":"maxSchemaVersion","source":"ipc-v1 2","value":65535},{"name":"maxSlots","source":"crate","value":4},{"name":"maxSnapshotEvents","source":"crate","value":64},{"name":"maxStimuliPerOperation","source":"workers-v1 1","value":64},{"name":"maxSupportedMajors","source":"crate","value":8},{"name":"maxSupportedStimuli","source":"crate","value":64},{"name":"maxTypedValueBytes","source":"ipc-v1 2","value":32768},{"name":"maxViewDimension","source":"state-media-v1 2","value":4096},{"name":"maxViews","source":"workers-v1 1","value":8},{"name":"maxWorkerThreads","source":"workers-v1 2","value":4096}],"scalars":{"ArtifactIdentity":"storeId, artifactId and generation of a bus ArtifactRef","BusCallId":"call-","Digest":"64 lowercase hexadecimal digits (SHA-256)","DomainRequestId":"req-","Id":"^[a-z0-9][a-z0-9._-]{0,63}$","OwnerToken":"dlv- or own-","U64":"\"0\" or [1-9][0-9]*, <= 18446744073709551615"},"types":[{"fields":[{"constraint":"1..=16, unique","kind":"array","name":"requestIds","required":true}],"name":"AcknowledgeParams","source":"ipc-v1 5"},{"fields":[{"constraint":"<= 16, unique, a subset of the request","kind":"array","name":"acknowledged","required":true}],"name":"AcknowledgeResult","source":"ipc-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"restoreToken","required":true}],"name":"ActivateRestoreParams","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"U64","name":"committedStep","required":true},{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"required from an environment, null from an agent","kind":"WorldObservation|null","name":"observation","required":false}],"name":"ActivateRestoreResult","source":"state-media-v1 5"},{"fields":[{"constraint":"unique within an epoch","kind":"Id","name":"batchId","required":true},{"constraint":"one complete batch: every declared port once, descriptor order","kind":"array","name":"controls","required":true}],"name":"AdvanceParams","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"k+1","kind":"U64","name":"committedStep","required":true},{"constraint":"","kind":"Digest","name":"decisionContextDigest","required":true},{"constraint":"","kind":"AgentTelemetry","name":"telemetry","required":true}],"name":"AgentCommitResult","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Id","name":"portId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"","kind":"Digest","name":"datasetDigest","required":true},{"constraint":"geometry mapping needs this, not neuronCount","kind":"Digest","name":"indexDigest","required":true},{"constraint":"","kind":"U64","name":"neuronCount","required":true},{"constraint":"<= 64, unique","kind":"array","name":"rateRoles","required":true},{"constraint":"unique","kind":"array","name":"supportedStimuli","required":true}],"name":"AgentDescriptor","source":"publishing-v1 3"},{"fields":[{"constraint":"","kind":"Digest","name":"datasetDigest","required":true},{"constraint":"geometry mapping needs this, not neuronCount","kind":"Digest","name":"indexDigest","required":true},{"constraint":"","kind":"U64","name":"neuronCount","required":true},{"constraint":"<= 64, unique","kind":"array","name":"rateRoles","required":true},{"constraint":"<= 64, unique","kind":"array","name":"supportedStimuli","required":true}],"name":"AgentGraph","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"AssetRef","name":"profile","required":true},{"constraint":"signed 32-bit","kind":"int","name":"seed","required":true},{"constraint":"","kind":"SensoryInput","name":"initialInput","required":true},{"constraint":"","kind":"TypedValue","name":"initialDecisionContext","required":true},{"constraint":">= 1","kind":"int","name":"workerThreads","required":true}],"name":"AgentInitializeParams","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"positive","kind":"RationalNs","name":"tickDuration","required":true},{"constraint":"","kind":"U64","name":"warmupTicks","required":true},{"constraint":"\"0\"","kind":"U64","name":"committedStep","required":true},{"constraint":"","kind":"Digest","name":"decisionContextDigest","required":true},{"constraint":"","kind":"AgentTelemetry","name":"telemetry","required":true},{"constraint":"rates are in graph.rateRoles order","kind":"AgentGraph","name":"graph","required":true}],"name":"AgentInitializeResult","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"the epoch the agent is Ready in; differs from scope.epoch","kind":"Id","name":"priorEpoch","required":true},{"constraint":"\"legacy-ratchet-rollback-v1\"; capability of the same name","kind":"Id","name":"policy","required":true},{"constraint":"boundary == scope.step; installed without a tick","kind":"SensoryInput","name":"input","required":true},{"constraint":"the context for the next Prepare","kind":"TypedValue","name":"decisionContext","required":true}],"name":"AgentRollbackParams","source":"workers-v1 7"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"the scoped step; a rollback runs no tick","kind":"U64","name":"committedStep","required":true},{"constraint":"","kind":"Digest","name":"decisionContextDigest","required":true},{"constraint":"","kind":"AgentTelemetry","name":"telemetry","required":true}],"name":"AgentRollbackResult","source":"workers-v1 7"},{"fields":[{"constraint":"","kind":"U64","name":"brainTicks","required":true},{"constraint":"finite and nonnegative","kind":"number","name":"populationRateHz","required":true},{"constraint":"<= 64, unique roleId, profile order, finite nonnegative hz","kind":"array<{roleId:Id,hz:number}>","name":"rates","required":true},{"constraint":"changed <= updates; signal finite","kind":"{enabled:bool,updates:U64,changed:U64,signal:number}","name":"learning","required":true},{"constraint":"finite and nonnegative; the pulse still running after the operation; null when the agent reports none","kind":"number|null","name":"stimulusRemainingMs","required":false}],"name":"AgentTelemetry","source":"workers-v1 1"},{"fields":[{"constraint":"","kind":"Id","name":"id","required":true},{"constraint":"","kind":"Digest","name":"digest","required":true},{"constraint":"positive","kind":"U64","name":"byteLength","required":true},{"constraint":"","kind":"Id","name":"format","required":true}],"name":"AssetRef","source":"workers-v1 1"},{"fields":[{"constraint":"","kind":"Id","name":"streamId","required":true},{"constraint":"8000..=192000","kind":"int","name":"sampleRate","required":true},{"constraint":"1..=8","kind":"int","name":"channels","required":true},{"constraint":"","kind":"AudioFormat","name":"format","required":true}],"name":"AudioDescriptor","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"streamId","required":true},{"constraint":"no overlap or rewind within an epoch","kind":"U64","name":"firstSample","required":true},{"constraint":"0..=192000","kind":"int","name":"sampleFrames","required":true},{"constraint":"byteLength == sampleFrames x channels x 4, finite f32","kind":"ArtifactRef","name":"samples","required":true},{"constraint":"true on the first chunk after restore","kind":"bool","name":"discontinuity","required":true}],"name":"AudioRef","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true}],"name":"CaptureParams","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"the committed boundary","kind":"U64","name":"boundary","required":true},{"constraint":"","kind":"Digest","name":"compatibilityDigest","required":true},{"constraint":"listed attachment; digest required","kind":"ArtifactRef","name":"payload","required":true}],"name":"CaptureResult","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"DomainRequestId","name":"preparedRequestId","required":true},{"constraint":"boundary == scope.step + 1","kind":"SensoryInput","name":"nextInput","required":true},{"constraint":"","kind":"TypedValue","name":"nextDecisionContext","required":true},{"constraint":"<= 64, unique eventId, order retained","kind":"array","name":"rewards","required":true},{"constraint":"<= 64, unique id, order retained","kind":"array","name":"taskStimulations","required":true}],"name":"CommitParams","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"U64","name":"descriptorRevision","required":true},{"constraint":"","kind":"Id","name":"publisherIncarnation","required":true},{"constraint":"the committed boundary","kind":"Scope","name":"scope","required":true},{"constraint":"","kind":"Id","name":"episodeId","required":true},{"constraint":"monotonic within publisherIncarnation","kind":"U64","name":"sequence","required":true},{"constraint":"","kind":"RationalNs","name":"worldTime","required":true},{"constraint":"1..=4, unique agentId, telemetry in profile role order","kind":"array","name":"agents","required":true},{"constraint":"","kind":"TypedValue","name":"progress","required":true},{"constraint":"declared attachments held through publication admission","kind":"{views:array,audio:array}","name":"media","required":true},{"constraint":"unique, task order","kind":"array","name":"eventIds","required":true}],"name":"CommittedSnapshot","source":"publishing-v1 3"},{"fields":[{"constraint":"","kind":"SchemaRef","name":"schema","required":true},{"constraint":"<= 32, unique, fixed order","kind":"array","name":"buttons","required":true},{"constraint":"<= 16, unique id, neutral inside its range","kind":"array<{id:Id,range:AxisRange,neutral:number}>","name":"axes","required":true}],"name":"ControllerSchema","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"Digest","name":"backendDigest","required":true},{"constraint":"","kind":"Digest","name":"contentDigest","required":true},{"constraint":"","kind":"Digest","name":"configurationDigest","required":true},{"constraint":"fixed, reduced, positive","kind":"RationalNs","name":"stepDuration","required":true},{"constraint":"1..=4, unique portId, fixed order","kind":"array<{portId:Id,controls:ControllerSchema}>","name":"ports","required":true},{"constraint":"","kind":"SchemaRef","name":"inspectionSchema","required":true},{"constraint":"<= 8, unique viewId","kind":"array","name":"views","required":true},{"constraint":"unique streamId","kind":"array","name":"audio","required":true},{"constraint":"","kind":"Recovery","name":"recovery","required":true},{"constraint":"","kind":"Determinism","name":"determinism","required":true}],"name":"EnvironmentDescriptor","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"AssetRef","name":"backendConfig","required":true},{"constraint":"","kind":"AssetRef","name":"taskConfig","required":true},{"constraint":"","kind":"Id","name":"episodeId","required":true},{"constraint":"1..=4, unique portId and unique agentId","kind":"array<{portId:Id,agentId:Id}>","name":"portBindings","required":true}],"name":"EnvironmentInitializeParams","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"EnvironmentDescriptor","name":"descriptor","required":true},{"constraint":"boundary 0 and worldTime 0/1","kind":"WorldObservation","name":"observation","required":true}],"name":"EnvironmentInitializeResult","source":"workers-v1 3"},{"fields":[{"constraint":"rollback only under a composition that declares a rollback policy","kind":"EpisodeRequestKind","name":"kind","required":true},{"constraint":"","kind":"Id","name":"reason","required":true},{"constraint":"","kind":"TypedValue","name":"outcome","required":true}],"name":"EpisodeRequest","source":"workers-v1 4"},{"fields":[{"constraint":"","kind":"Id","name":"sessionId","required":true},{"constraint":"","kind":"Id","name":"expectedWorkerId","required":true},{"constraint":"","kind":"Role","name":"role","required":true},{"constraint":"nonempty, unique, 1..=65535","kind":"array","name":"supportedMajors","required":true}],"name":"HelloParams","source":"ipc-v1 4"},{"fields":[{"constraint":"1","kind":"const","name":"selectedMajor","required":true},{"constraint":"0","kind":"const","name":"selectedMinor","required":true},{"constraint":"","kind":"Id","name":"workerId","required":true},{"constraint":"","kind":"Id","name":"incarnationId","required":true},{"constraint":"","kind":"Role","name":"role","required":true},{"constraint":"","kind":"Digest","name":"buildDigest","required":true},{"constraint":"","kind":"Digest","name":"contractDigest","required":true},{"constraint":"unique; agent-step-v1 or world-step-v1 required for that role","kind":"array","name":"capabilities","required":true},{"constraint":"1..=4 agents, 1..=4 ports, and the launcher allocation this worker runs in","kind":"{maxAgents:int,maxPorts:int,workerThreads:int}","name":"limits","required":true}],"name":"HelloResult","source":"ipc-v1 4"},{"fields":[{"constraint":"","kind":"Id","name":"portId","required":true},{"constraint":"every declared button, descriptor order, no extras","kind":"array<{id:Id,down:bool}>","name":"buttons","required":true},{"constraint":"every declared axis in order; bipolar [-1,1], unit [0,1], refused not clamped","kind":"array<{id:Id,value:number}>","name":"axes","required":true}],"name":"PortControl","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"positive","kind":"RationalNs","name":"interval","required":true},{"constraint":"","kind":"Digest","name":"decisionContextDigest","required":true},{"constraint":"<= 64, unique id, supplied order retained","kind":"array","name":"preStepStimulations","required":true}],"name":"PrepareParams","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"<= brainTicks","kind":"U64","name":"ticksAdvanced","required":true},{"constraint":"","kind":"U64","name":"brainTicks","required":true},{"constraint":">= 0 and < one model tick","kind":"RationalNs","name":"remainder","required":true},{"constraint":"the profile's registered intent schema","kind":"TypedValue","name":"decision","required":true}],"name":"PreparedDecision","source":"workers-v1 2"},{"fields":[{"constraint":"reduced against denominator","kind":"U64","name":"numerator","required":true},{"constraint":"positive; zero is encoded 0/1; arithmetic is checked","kind":"U64","name":"denominator","required":true}],"name":"RationalNs","source":"ipc-v1 2"},{"fields":[{"constraint":"a slot saved in priorEpoch or carried by its restore","kind":"Id","name":"slotId","required":true},{"constraint":"the epoch the environment is Ready in; differs from scope.epoch","kind":"Id","name":"priorEpoch","required":true},{"constraint":"\"legacy-ratchet-rollback-v1\"","kind":"Id","name":"policy","required":true}],"name":"RestoreSlotParams","source":"workers-v1 7"},{"fields":[{"constraint":"echoes the request","kind":"Id","name":"slotId","required":true},{"constraint":"the scoped step; no transition ran","kind":"U64","name":"committedStep","required":true},{"constraint":"boundary == committedStep; no audio chunk; worldTime and engineFrame continue","kind":"WorldObservation","name":"observation","required":true}],"name":"RestoreSlotResult","source":"workers-v1 7"},{"fields":[{"constraint":"unique within its outcome namespace","kind":"Id","name":"eventId","required":true},{"constraint":"","kind":"Id","name":"ruleId","required":true},{"constraint":"finite; positive-only profiles reject negatives","kind":"number","name":"value","required":true}],"name":"Reward","source":"workers-v1 1"},{"fields":[{"constraint":"one of the composition's declared slots; capability gameboy-slots-v1","kind":"Id","name":"slotId","required":true}],"name":"SaveSlotParams","source":"workers-v1 7"},{"fields":[{"constraint":"echoes the request","kind":"Id","name":"slotId","required":true},{"constraint":"the scoped committed step","kind":"U64","name":"boundary","required":true},{"constraint":"SHA-256 of the saved state bytes","kind":"Digest","name":"stateDigest","required":true},{"constraint":"positive","kind":"U64","name":"byteLength","required":true}],"name":"SaveSlotResult","source":"workers-v1 7"},{"fields":[{"constraint":"","kind":"Id","name":"id","required":true},{"constraint":"1..=65535","kind":"int","name":"version","required":true},{"constraint":"64 lowercase hex digits","kind":"Digest","name":"digest","required":true}],"name":"SchemaRef","source":"ipc-v1 2"},{"fields":[{"constraint":"^[a-z0-9][a-z0-9._-]{0,63}$","kind":"Id","name":"sessionId","required":true},{"constraint":"^[a-z0-9][a-z0-9._-]{0,63}$","kind":"Id","name":"epoch","required":true},{"constraint":"decimal string, <= 18446744073709551615","kind":"U64","name":"step","required":true}],"name":"Scope","source":"ipc-v1 2"},{"fields":[{"constraint":"the observed environment boundary","kind":"U64","name":"boundary","required":true},{"constraint":"<= 8, unique viewId, producedStep <= boundary","kind":"array","name":"views","required":true},{"constraint":"a pixel-only profile rejects non-null","kind":"TypedValue|null","name":"structured","required":false}],"name":"SensoryInput","source":"workers-v1 1"},{"fields":[{"constraint":"","kind":"Id","name":"sessionId","required":true},{"constraint":"","kind":"U64","name":"revision","required":true},{"constraint":"","kind":"Digest","name":"compositionDigest","required":true},{"constraint":"","kind":"SchedulerId","name":"schedulerId","required":true},{"constraint":"","kind":"EnvironmentDescriptor","name":"environment","required":true},{"constraint":"","kind":"SchemaRef","name":"taskSchema","required":true},{"constraint":"1..=4, unique agentId and portId, each portId declared by the environment","kind":"array","name":"agents","required":true},{"constraint":"unique id","kind":"array","name":"assets","required":true}],"name":"SessionDescriptor","source":"publishing-v1 3"},{"fields":[{"constraint":"\"error\"","kind":"const","name":"type","required":true},{"constraint":"echoes the request","kind":"DomainRequestId","name":"requestId","required":true},{"constraint":"","kind":"Id","name":"workerId","required":true},{"constraint":"","kind":"Id","name":"incarnationId","required":true},{"constraint":"echoes the request scope","kind":"Scope|null","name":"scope","required":false},{"constraint":"","kind":"ErrorCode","name":"error.code","required":true},{"constraint":"<= 512 code points","kind":"string","name":"error.message","required":true},{"constraint":"none for every code raised before mutation","kind":"MutationCertainty","name":"error.mutation","required":true}],"name":"SessionRpcFailure","source":"ipc-v1 3"},{"fields":[{"constraint":"req-","kind":"DomainRequestId","name":"requestId","required":true},{"constraint":"null for lifecycle calls","kind":"Scope|null","name":"scope","required":false},{"constraint":"no bus callId/deliveryId/ownerId keys","kind":"object","name":"params","required":true}],"name":"SessionRpcRequest","source":"ipc-v1 2"},{"fields":[{"constraint":"\"result\"","kind":"const","name":"type","required":true},{"constraint":"echoes the request","kind":"DomainRequestId","name":"requestId","required":true},{"constraint":"","kind":"Id","name":"workerId","required":true},{"constraint":"","kind":"Id","name":"incarnationId","required":true},{"constraint":"echoes the request scope","kind":"Scope|null","name":"scope","required":false},{"constraint":"","kind":"object","name":"result","required":true}],"name":"SessionRpcSuccess","source":"ipc-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"reason","required":true}],"name":"ShutdownParams","source":"ipc-v1 7"},{"fields":[{"constraint":"true","kind":"const","name":"stopping","required":true}],"name":"ShutdownResult","source":"ipc-v1 7"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"AgentTelemetry","name":"telemetry","required":true},{"constraint":"null at boundary 0 and at an installed boundary; null or present for every agent together","kind":"TypedValue|null","name":"selectedDecision","required":false},{"constraint":"null with selectedDecision; the agent's assigned port","kind":"PortControl|null","name":"appliedControls","required":false}],"name":"SnapshotAgent","source":"publishing-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"provenance, not the new handles","kind":"Scope","name":"sourceScope","required":true},{"constraint":"","kind":"Digest","name":"compatibilityDigest","required":true},{"constraint":"newly imported; digest required","kind":"ArtifactRef","name":"payload","required":true}],"name":"StageRestoreParams","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"activates once, bound to scope and payload","kind":"Id","name":"restoreToken","required":true}],"name":"StageRestoreResult","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"WorkerState","name":"state","required":true},{"constraint":"null before initialization","kind":"Scope|null","name":"currentScope","required":false},{"constraint":"","kind":"DomainRequestId|null","name":"activeRequestId","required":false},{"constraint":"","kind":"DomainRequestId|null","name":"lastCompletedRequestId","required":false},{"constraint":"","kind":"Id|null","name":"lastBatchId","required":false},{"constraint":"advances on progress, not on status queries","kind":"U64","name":"progressCounter","required":true}],"name":"StatusResult","source":"ipc-v1 4"},{"fields":[{"constraint":"echoes the request","kind":"Id","name":"batchId","required":true},{"constraint":"k","kind":"U64","name":"appliedFromStep","required":true},{"constraint":"appliedFromStep + 1","kind":"U64","name":"nextStep","required":true},{"constraint":"over validated canonical requested controls","kind":"Digest","name":"appliedControlsDigest","required":true},{"constraint":"boundary == nextStep","kind":"WorldObservation","name":"observation","required":true}],"name":"StepResult","source":"workers-v1 3"},{"fields":[{"constraint":"unique within its command namespace","kind":"Id","name":"id","required":true},{"constraint":"resolved through a profile capability","kind":"Id","name":"kindId","required":true},{"constraint":"finite and > 0","kind":"number","name":"durationMs","required":true}],"name":"Stimulus","source":"workers-v1 1"},{"fields":[{"constraint":"derived from epoch, source step, rule and ordinal","kind":"Id","name":"id","required":true},{"constraint":"","kind":"Id","name":"kindId","required":true},{"constraint":"the newly reached boundary, 0 for bootstrap","kind":"U64","name":"sourceStep","required":true},{"constraint":"","kind":"Id|null","name":"agentId","required":false},{"constraint":"","kind":"TypedValue","name":"payload","required":true}],"name":"TaskEvent","source":"workers-v1 4"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"","kind":"U64","name":"ticksAdvanced","required":true},{"constraint":"","kind":"U64","name":"brainTicks","required":true},{"constraint":"","kind":"RationalNs","name":"remainder","required":true},{"constraint":"","kind":"Digest","name":"decisionDigest","required":true},{"constraint":"the commit acknowledgment","kind":"U64","name":"committedStep","required":true}],"name":"TraceAgent","source":"step-v1 8"},{"fields":[{"constraint":"","kind":"Scope","name":"scope","required":true},{"constraint":"sorted by agentId, independent of dispatch order","kind":"array","name":"agents","required":true},{"constraint":"","kind":"Id","name":"batchId","required":true},{"constraint":"","kind":"Digest","name":"controlDigest","required":true},{"constraint":"the world boundary the environment acknowledged","kind":"U64","name":"acknowledgedBoundary","required":true},{"constraint":"sorted by viewId","kind":"array<{viewId:Id,producedStep:U64}>","name":"observationBoundaries","required":true},{"constraint":"task outcome ids in task order","kind":"array","name":"outcomeIds","required":true},{"constraint":"task event ids in task order","kind":"array","name":"eventIds","required":true},{"constraint":"","kind":"U64","name":"publishedBoundary","required":true}],"name":"TraceBehaviour","source":"step-v1 8"},{"fields":[{"constraint":"","kind":"U64","name":"wallTimeNs","required":true},{"constraint":"","kind":"array<{agentId:Id,requestId:DomainRequestId}>","name":"prepareRequestIds","required":true},{"constraint":"","kind":"DomainRequestId","name":"advanceRequestId","required":true},{"constraint":"","kind":"array<{agentId:Id,requestId:DomainRequestId}>","name":"commitRequestIds","required":true},{"constraint":"call-","kind":"array","name":"busCallIds","required":true},{"constraint":"dlv- or own-","kind":"array","name":"deliveryIds","required":true}],"name":"TraceOperational","source":"step-v1 8"},{"fields":[{"constraint":"compared between runs","kind":"TraceBehaviour","name":"behaviour","required":true},{"constraint":"recorded, never compared: wall time and transport identities","kind":"TraceOperational","name":"operational","required":true}],"name":"TransitionTrace","source":"step-v1 8"},{"fields":[{"constraint":"","kind":"SchemaRef","name":"schema","required":true},{"constraint":"canonical JSON of the whole TypedValue <= 32768 bytes","kind":"object","name":"value","required":true}],"name":"TypedValue","source":"ipc-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"viewId","required":true},{"constraint":"1..=4096","kind":"int","name":"width","required":true},{"constraint":"1..=4096","kind":"int","name":"height","required":true},{"constraint":"","kind":"ViewFormat","name":"format","required":true},{"constraint":"exactly 4 x width","kind":"int","name":"rowStride","required":true},{"constraint":"positive integers <= 65535","kind":"{numerator:int,denominator:int}","name":"pixelAspect","required":true},{"constraint":"0..=8","kind":"int","name":"observationDelaySteps","required":true}],"name":"ViewDescriptor","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"viewId","required":true},{"constraint":"max(0, boundary - observationDelaySteps) for required sensory views","kind":"U64","name":"producedStep","required":true},{"constraint":"listed attachment; byteLength == rowStride x height","kind":"ArtifactRef","name":"pixels","required":true}],"name":"ViewRef","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"U64","name":"boundary","required":true},{"constraint":"logical time since episode start","kind":"RationalNs","name":"worldTime","required":true},{"constraint":"<= 64 characters","kind":"string|null","name":"engineFrame","required":false},{"constraint":"<= 8, unique viewId","kind":"array","name":"sensoryViews","required":true},{"constraint":"the descriptor's inspectionSchema","kind":"TypedValue","name":"inspection","required":true},{"constraint":"<= 8, unique viewId","kind":"array","name":"broadcastViews","required":true},{"constraint":"unique streamId","kind":"array","name":"audio","required":true}],"name":"WorldObservation","source":"workers-v1 3"}],"version":1} +{"contract":"fly-session-types","enums":[{"members":["f32le-interleaved"],"name":"AudioFormat","source":"state-media-v1 2"},{"members":["bipolar","unit"],"name":"AxisRange","source":"workers-v1 3"},{"members":["save-slot","rollback"],"name":"BoundaryActionKind","source":"step-v1 8"},{"members":["fixed-build","unverified"],"name":"Determinism","source":"workers-v1 3"},{"members":["terminal","rollback"],"name":"EpisodeRequestKind","source":"workers-v1 4"},{"members":["INVALID_ARGUMENT","UNSUPPORTED","IDENTITY_MISMATCH","STALE_EPOCH","STALE_STEP","FUTURE_STEP","INVALID_PHASE","CONFLICT","IN_PROGRESS","BUSY","BUFFER_INVALID","RESULT_EXPIRED","INCOMPATIBLE_STATE","BACKEND_FAILURE","INTERNAL"],"name":"ErrorCode","source":"ipc-v1 7"},{"members":["none","applied","unknown"],"name":"MutationCertainty","source":"ipc-v1 3"},{"members":["exact-checkpoint","episode-restart"],"name":"Recovery","source":"workers-v1 3"},{"members":["agent","environment","coordinator"],"name":"Role","source":"ipc-v1 4"},{"members":["lockstep-v1"],"name":"SchedulerId","source":"publishing-v1 3"},{"members":["rgba8"],"name":"ViewFormat","source":"state-media-v1 2"},{"members":["uninitialized","ready","preparing","prepared","advancing","committing","capturing","staged-restore","restoring","failed","stopping"],"name":"WorkerState","source":"ipc-v1 4"}],"limits":[{"name":"maxAcknowledge","source":"ipc-v1 5","value":16},{"name":"maxAgents","source":"ipc-v1 2","value":4},{"name":"maxAssets","source":"crate","value":64},{"name":"maxAttachments","source":"bus-v1 4","value":32},{"name":"maxAudioStreams","source":"crate","value":8},{"name":"maxAxes","source":"workers-v1 3","value":16},{"name":"maxButtons","source":"workers-v1 3","value":32},{"name":"maxCapabilities","source":"crate","value":32},{"name":"maxEngineFrameLength","source":"workers-v1 3","value":64},{"name":"maxEnvelopeBytes","source":"bus-v1 4","value":65536},{"name":"maxMessageCodePoints","source":"ipc-v1 7","value":512},{"name":"maxObservationDelaySteps","source":"state-media-v1 2","value":8},{"name":"maxPixelAspectPart","source":"state-media-v1 2","value":65535},{"name":"maxPorts","source":"ipc-v1 2","value":4},{"name":"maxRateRoles","source":"ipc-v1 2","value":64},{"name":"maxRewardsPerOperation","source":"workers-v1 1","value":64},{"name":"maxSampleFrames","source":"state-media-v1 2","value":192000},{"name":"maxSchemaVersion","source":"ipc-v1 2","value":65535},{"name":"maxSlots","source":"crate","value":4},{"name":"maxSnapshotEvents","source":"crate","value":64},{"name":"maxStimuliPerOperation","source":"workers-v1 1","value":64},{"name":"maxSupportedMajors","source":"crate","value":8},{"name":"maxSupportedStimuli","source":"crate","value":64},{"name":"maxTypedValueBytes","source":"ipc-v1 2","value":32768},{"name":"maxViewDimension","source":"state-media-v1 2","value":4096},{"name":"maxViews","source":"workers-v1 1","value":8},{"name":"maxWorkerThreads","source":"workers-v1 2","value":4096}],"scalars":{"ArtifactIdentity":"storeId, artifactId and generation of a bus ArtifactRef","BusCallId":"call-","Digest":"64 lowercase hexadecimal digits (SHA-256)","DomainRequestId":"req-","Id":"^[a-z0-9][a-z0-9._-]{0,63}$","OwnerToken":"dlv- or own-","U64":"\"0\" or [1-9][0-9]*, <= 18446744073709551615"},"types":[{"fields":[{"constraint":"1..=16, unique","kind":"array","name":"requestIds","required":true}],"name":"AcknowledgeParams","source":"ipc-v1 5"},{"fields":[{"constraint":"<= 16, unique, a subset of the request","kind":"array","name":"acknowledged","required":true}],"name":"AcknowledgeResult","source":"ipc-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"restoreToken","required":true}],"name":"ActivateRestoreParams","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"U64","name":"committedStep","required":true},{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"required from an environment, null from an agent","kind":"WorldObservation|null","name":"observation","required":false}],"name":"ActivateRestoreResult","source":"state-media-v1 5"},{"fields":[{"constraint":"unique within an epoch","kind":"Id","name":"batchId","required":true},{"constraint":"one complete batch: every declared port once, descriptor order","kind":"array","name":"controls","required":true}],"name":"AdvanceParams","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"k+1","kind":"U64","name":"committedStep","required":true},{"constraint":"","kind":"Digest","name":"decisionContextDigest","required":true},{"constraint":"","kind":"AgentTelemetry","name":"telemetry","required":true}],"name":"AgentCommitResult","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Id","name":"portId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"","kind":"Digest","name":"datasetDigest","required":true},{"constraint":"geometry mapping needs this, not neuronCount","kind":"Digest","name":"indexDigest","required":true},{"constraint":"","kind":"U64","name":"neuronCount","required":true},{"constraint":"<= 64, unique","kind":"array","name":"rateRoles","required":true},{"constraint":"unique","kind":"array","name":"supportedStimuli","required":true}],"name":"AgentDescriptor","source":"publishing-v1 3"},{"fields":[{"constraint":"","kind":"Digest","name":"datasetDigest","required":true},{"constraint":"geometry mapping needs this, not neuronCount","kind":"Digest","name":"indexDigest","required":true},{"constraint":"","kind":"U64","name":"neuronCount","required":true},{"constraint":"<= 64, unique","kind":"array","name":"rateRoles","required":true},{"constraint":"<= 64, unique","kind":"array","name":"supportedStimuli","required":true}],"name":"AgentGraph","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"AssetRef","name":"profile","required":true},{"constraint":"signed 32-bit","kind":"int","name":"seed","required":true},{"constraint":"","kind":"SensoryInput","name":"initialInput","required":true},{"constraint":"","kind":"TypedValue","name":"initialDecisionContext","required":true},{"constraint":">= 1","kind":"int","name":"workerThreads","required":true}],"name":"AgentInitializeParams","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"positive","kind":"RationalNs","name":"tickDuration","required":true},{"constraint":"","kind":"U64","name":"warmupTicks","required":true},{"constraint":"\"0\"","kind":"U64","name":"committedStep","required":true},{"constraint":"","kind":"Digest","name":"decisionContextDigest","required":true},{"constraint":"","kind":"AgentTelemetry","name":"telemetry","required":true},{"constraint":"rates are in graph.rateRoles order","kind":"AgentGraph","name":"graph","required":true}],"name":"AgentInitializeResult","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"the epoch the agent is Ready in; differs from scope.epoch","kind":"Id","name":"priorEpoch","required":true},{"constraint":"\"legacy-ratchet-rollback-v1\"; capability of the same name","kind":"Id","name":"policy","required":true},{"constraint":"boundary == scope.step; installed without a tick","kind":"SensoryInput","name":"input","required":true},{"constraint":"the context for the next Prepare","kind":"TypedValue","name":"decisionContext","required":true}],"name":"AgentRollbackParams","source":"workers-v1 7"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"the scoped step; a rollback runs no tick","kind":"U64","name":"committedStep","required":true},{"constraint":"","kind":"Digest","name":"decisionContextDigest","required":true},{"constraint":"","kind":"AgentTelemetry","name":"telemetry","required":true}],"name":"AgentRollbackResult","source":"workers-v1 7"},{"fields":[{"constraint":"","kind":"U64","name":"brainTicks","required":true},{"constraint":"finite and nonnegative","kind":"number","name":"populationRateHz","required":true},{"constraint":"<= 64, unique roleId, profile order, finite nonnegative hz","kind":"array<{roleId:Id,hz:number}>","name":"rates","required":true},{"constraint":"changed <= updates; signal finite","kind":"{enabled:bool,updates:U64,changed:U64,signal:number}","name":"learning","required":true},{"constraint":"finite and nonnegative; the pulse still running after the operation; null when the agent reports none","kind":"number|null","name":"stimulusRemainingMs","required":false}],"name":"AgentTelemetry","source":"workers-v1 1"},{"fields":[{"constraint":"","kind":"Id","name":"id","required":true},{"constraint":"","kind":"Digest","name":"digest","required":true},{"constraint":"positive","kind":"U64","name":"byteLength","required":true},{"constraint":"","kind":"Id","name":"format","required":true}],"name":"AssetRef","source":"workers-v1 1"},{"fields":[{"constraint":"","kind":"Id","name":"streamId","required":true},{"constraint":"8000..=192000","kind":"int","name":"sampleRate","required":true},{"constraint":"1..=8","kind":"int","name":"channels","required":true},{"constraint":"","kind":"AudioFormat","name":"format","required":true}],"name":"AudioDescriptor","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"streamId","required":true},{"constraint":"no overlap or rewind within an epoch","kind":"U64","name":"firstSample","required":true},{"constraint":"0..=192000","kind":"int","name":"sampleFrames","required":true},{"constraint":"byteLength == sampleFrames x channels x 4, finite f32","kind":"ArtifactRef","name":"samples","required":true},{"constraint":"true on the first chunk after restore","kind":"bool","name":"discontinuity","required":true}],"name":"AudioRef","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true}],"name":"CaptureParams","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"the committed boundary","kind":"U64","name":"boundary","required":true},{"constraint":"","kind":"Digest","name":"compatibilityDigest","required":true},{"constraint":"listed attachment; digest required","kind":"ArtifactRef","name":"payload","required":true}],"name":"CaptureResult","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"DomainRequestId","name":"preparedRequestId","required":true},{"constraint":"boundary == scope.step + 1","kind":"SensoryInput","name":"nextInput","required":true},{"constraint":"","kind":"TypedValue","name":"nextDecisionContext","required":true},{"constraint":"<= 64, unique eventId, order retained","kind":"array","name":"rewards","required":true},{"constraint":"<= 64, unique id, order retained","kind":"array","name":"taskStimulations","required":true}],"name":"CommitParams","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"U64","name":"descriptorRevision","required":true},{"constraint":"","kind":"Id","name":"publisherIncarnation","required":true},{"constraint":"the committed boundary","kind":"Scope","name":"scope","required":true},{"constraint":"","kind":"Id","name":"episodeId","required":true},{"constraint":"monotonic within publisherIncarnation","kind":"U64","name":"sequence","required":true},{"constraint":"","kind":"RationalNs","name":"worldTime","required":true},{"constraint":"1..=4, unique agentId, telemetry in profile role order","kind":"array","name":"agents","required":true},{"constraint":"","kind":"TypedValue","name":"progress","required":true},{"constraint":"declared attachments held through publication admission","kind":"{views:array,audio:array}","name":"media","required":true},{"constraint":"unique, task order","kind":"array","name":"eventIds","required":true}],"name":"CommittedSnapshot","source":"publishing-v1 3"},{"fields":[{"constraint":"","kind":"SchemaRef","name":"schema","required":true},{"constraint":"<= 32, unique, fixed order","kind":"array","name":"buttons","required":true},{"constraint":"<= 16, unique id, neutral inside its range","kind":"array<{id:Id,range:AxisRange,neutral:number}>","name":"axes","required":true}],"name":"ControllerSchema","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"Digest","name":"backendDigest","required":true},{"constraint":"","kind":"Digest","name":"contentDigest","required":true},{"constraint":"","kind":"Digest","name":"configurationDigest","required":true},{"constraint":"fixed, reduced, positive","kind":"RationalNs","name":"stepDuration","required":true},{"constraint":"1..=4, unique portId, fixed order","kind":"array<{portId:Id,controls:ControllerSchema}>","name":"ports","required":true},{"constraint":"","kind":"SchemaRef","name":"inspectionSchema","required":true},{"constraint":"<= 8, unique viewId","kind":"array","name":"views","required":true},{"constraint":"unique streamId","kind":"array","name":"audio","required":true},{"constraint":"","kind":"Recovery","name":"recovery","required":true},{"constraint":"","kind":"Determinism","name":"determinism","required":true}],"name":"EnvironmentDescriptor","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"AssetRef","name":"backendConfig","required":true},{"constraint":"","kind":"AssetRef","name":"taskConfig","required":true},{"constraint":"","kind":"Id","name":"episodeId","required":true},{"constraint":"1..=4, unique portId and unique agentId","kind":"array<{portId:Id,agentId:Id}>","name":"portBindings","required":true}],"name":"EnvironmentInitializeParams","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"EnvironmentDescriptor","name":"descriptor","required":true},{"constraint":"boundary 0 and worldTime 0/1","kind":"WorldObservation","name":"observation","required":true}],"name":"EnvironmentInitializeResult","source":"workers-v1 3"},{"fields":[{"constraint":"rollback only under a composition that declares a rollback policy","kind":"EpisodeRequestKind","name":"kind","required":true},{"constraint":"","kind":"Id","name":"reason","required":true},{"constraint":"","kind":"TypedValue","name":"outcome","required":true}],"name":"EpisodeRequest","source":"workers-v1 4"},{"fields":[{"constraint":"","kind":"Id","name":"sessionId","required":true},{"constraint":"","kind":"Id","name":"expectedWorkerId","required":true},{"constraint":"","kind":"Role","name":"role","required":true},{"constraint":"nonempty, unique, 1..=65535","kind":"array","name":"supportedMajors","required":true}],"name":"HelloParams","source":"ipc-v1 4"},{"fields":[{"constraint":"1","kind":"const","name":"selectedMajor","required":true},{"constraint":"0","kind":"const","name":"selectedMinor","required":true},{"constraint":"","kind":"Id","name":"workerId","required":true},{"constraint":"","kind":"Id","name":"incarnationId","required":true},{"constraint":"","kind":"Role","name":"role","required":true},{"constraint":"","kind":"Digest","name":"buildDigest","required":true},{"constraint":"","kind":"Digest","name":"contractDigest","required":true},{"constraint":"unique; agent-step-v1 or world-step-v1 required for that role","kind":"array","name":"capabilities","required":true},{"constraint":"1..=4 agents, 1..=4 ports, and the launcher allocation this worker runs in","kind":"{maxAgents:int,maxPorts:int,workerThreads:int}","name":"limits","required":true}],"name":"HelloResult","source":"ipc-v1 4"},{"fields":[{"constraint":"","kind":"Id","name":"portId","required":true},{"constraint":"every declared button, descriptor order, no extras","kind":"array<{id:Id,down:bool}>","name":"buttons","required":true},{"constraint":"every declared axis in order; bipolar [-1,1], unit [0,1], refused not clamped","kind":"array<{id:Id,value:number}>","name":"axes","required":true}],"name":"PortControl","source":"workers-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"positive","kind":"RationalNs","name":"interval","required":true},{"constraint":"","kind":"Digest","name":"decisionContextDigest","required":true},{"constraint":"<= 64, unique id, supplied order retained","kind":"array","name":"preStepStimulations","required":true}],"name":"PrepareParams","source":"workers-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"<= brainTicks","kind":"U64","name":"ticksAdvanced","required":true},{"constraint":"","kind":"U64","name":"brainTicks","required":true},{"constraint":">= 0 and < one model tick","kind":"RationalNs","name":"remainder","required":true},{"constraint":"the profile's registered intent schema","kind":"TypedValue","name":"decision","required":true}],"name":"PreparedDecision","source":"workers-v1 2"},{"fields":[{"constraint":"reduced against denominator","kind":"U64","name":"numerator","required":true},{"constraint":"positive; zero is encoded 0/1; arithmetic is checked","kind":"U64","name":"denominator","required":true}],"name":"RationalNs","source":"ipc-v1 2"},{"fields":[{"constraint":"a slot saved in priorEpoch or carried by its restore","kind":"Id","name":"slotId","required":true},{"constraint":"the epoch the environment is Ready in; differs from scope.epoch","kind":"Id","name":"priorEpoch","required":true},{"constraint":"\"legacy-ratchet-rollback-v1\"","kind":"Id","name":"policy","required":true}],"name":"RestoreSlotParams","source":"workers-v1 7"},{"fields":[{"constraint":"echoes the request","kind":"Id","name":"slotId","required":true},{"constraint":"the scoped step; no transition ran","kind":"U64","name":"committedStep","required":true},{"constraint":"boundary == committedStep; no audio chunk; worldTime and engineFrame continue","kind":"WorldObservation","name":"observation","required":true}],"name":"RestoreSlotResult","source":"workers-v1 7"},{"fields":[{"constraint":"unique within its outcome namespace","kind":"Id","name":"eventId","required":true},{"constraint":"","kind":"Id","name":"ruleId","required":true},{"constraint":"finite; positive-only profiles reject negatives","kind":"number","name":"value","required":true}],"name":"Reward","source":"workers-v1 1"},{"fields":[{"constraint":"one of the composition's declared slots; capability gameboy-slots-v1","kind":"Id","name":"slotId","required":true}],"name":"SaveSlotParams","source":"workers-v1 7"},{"fields":[{"constraint":"echoes the request","kind":"Id","name":"slotId","required":true},{"constraint":"the scoped committed step","kind":"U64","name":"boundary","required":true},{"constraint":"SHA-256 of the saved state bytes","kind":"Digest","name":"stateDigest","required":true},{"constraint":"positive","kind":"U64","name":"byteLength","required":true}],"name":"SaveSlotResult","source":"workers-v1 7"},{"fields":[{"constraint":"","kind":"Id","name":"id","required":true},{"constraint":"1..=65535","kind":"int","name":"version","required":true},{"constraint":"64 lowercase hex digits","kind":"Digest","name":"digest","required":true}],"name":"SchemaRef","source":"ipc-v1 2"},{"fields":[{"constraint":"^[a-z0-9][a-z0-9._-]{0,63}$","kind":"Id","name":"sessionId","required":true},{"constraint":"^[a-z0-9][a-z0-9._-]{0,63}$","kind":"Id","name":"epoch","required":true},{"constraint":"decimal string, <= 18446744073709551615","kind":"U64","name":"step","required":true}],"name":"Scope","source":"ipc-v1 2"},{"fields":[{"constraint":"the observed environment boundary","kind":"U64","name":"boundary","required":true},{"constraint":"<= 8, unique viewId, producedStep <= boundary","kind":"array","name":"views","required":true},{"constraint":"a pixel-only profile rejects non-null","kind":"TypedValue|null","name":"structured","required":false}],"name":"SensoryInput","source":"workers-v1 1"},{"fields":[{"constraint":"","kind":"Id","name":"sessionId","required":true},{"constraint":"","kind":"U64","name":"revision","required":true},{"constraint":"","kind":"Digest","name":"compositionDigest","required":true},{"constraint":"","kind":"SchedulerId","name":"schedulerId","required":true},{"constraint":"","kind":"EnvironmentDescriptor","name":"environment","required":true},{"constraint":"","kind":"SchemaRef","name":"taskSchema","required":true},{"constraint":"1..=4, unique agentId and portId, each portId declared by the environment","kind":"array","name":"agents","required":true},{"constraint":"unique id","kind":"array","name":"assets","required":true}],"name":"SessionDescriptor","source":"publishing-v1 3"},{"fields":[{"constraint":"\"error\"","kind":"const","name":"type","required":true},{"constraint":"echoes the request","kind":"DomainRequestId","name":"requestId","required":true},{"constraint":"","kind":"Id","name":"workerId","required":true},{"constraint":"","kind":"Id","name":"incarnationId","required":true},{"constraint":"echoes the request scope","kind":"Scope|null","name":"scope","required":false},{"constraint":"","kind":"ErrorCode","name":"error.code","required":true},{"constraint":"<= 512 code points","kind":"string","name":"error.message","required":true},{"constraint":"none for every code raised before mutation","kind":"MutationCertainty","name":"error.mutation","required":true}],"name":"SessionRpcFailure","source":"ipc-v1 3"},{"fields":[{"constraint":"req-","kind":"DomainRequestId","name":"requestId","required":true},{"constraint":"null for lifecycle calls","kind":"Scope|null","name":"scope","required":false},{"constraint":"no bus callId/deliveryId/ownerId keys","kind":"object","name":"params","required":true}],"name":"SessionRpcRequest","source":"ipc-v1 2"},{"fields":[{"constraint":"\"result\"","kind":"const","name":"type","required":true},{"constraint":"echoes the request","kind":"DomainRequestId","name":"requestId","required":true},{"constraint":"","kind":"Id","name":"workerId","required":true},{"constraint":"","kind":"Id","name":"incarnationId","required":true},{"constraint":"echoes the request scope","kind":"Scope|null","name":"scope","required":false},{"constraint":"","kind":"object","name":"result","required":true}],"name":"SessionRpcSuccess","source":"ipc-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"reason","required":true}],"name":"ShutdownParams","source":"ipc-v1 7"},{"fields":[{"constraint":"true","kind":"const","name":"stopping","required":true}],"name":"ShutdownResult","source":"ipc-v1 7"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"AgentTelemetry","name":"telemetry","required":true},{"constraint":"null at boundary 0 and at an installed boundary; null or present for every agent together","kind":"TypedValue|null","name":"selectedDecision","required":false},{"constraint":"null with selectedDecision; the agent's assigned port","kind":"PortControl|null","name":"appliedControls","required":false}],"name":"SnapshotAgent","source":"publishing-v1 3"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"provenance, not the new handles","kind":"Scope","name":"sourceScope","required":true},{"constraint":"","kind":"Digest","name":"compatibilityDigest","required":true},{"constraint":"newly imported; digest required","kind":"ArtifactRef","name":"payload","required":true}],"name":"StageRestoreParams","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"Id","name":"checkpointId","required":true},{"constraint":"activates once, bound to scope and payload","kind":"Id","name":"restoreToken","required":true}],"name":"StageRestoreResult","source":"state-media-v1 5"},{"fields":[{"constraint":"","kind":"WorkerState","name":"state","required":true},{"constraint":"null before initialization","kind":"Scope|null","name":"currentScope","required":false},{"constraint":"","kind":"DomainRequestId|null","name":"activeRequestId","required":false},{"constraint":"","kind":"DomainRequestId|null","name":"lastCompletedRequestId","required":false},{"constraint":"","kind":"Id|null","name":"lastBatchId","required":false},{"constraint":"advances on progress, not on status queries","kind":"U64","name":"progressCounter","required":true}],"name":"StatusResult","source":"ipc-v1 4"},{"fields":[{"constraint":"echoes the request","kind":"Id","name":"batchId","required":true},{"constraint":"k","kind":"U64","name":"appliedFromStep","required":true},{"constraint":"appliedFromStep + 1","kind":"U64","name":"nextStep","required":true},{"constraint":"over validated canonical requested controls","kind":"Digest","name":"appliedControlsDigest","required":true},{"constraint":"boundary == nextStep","kind":"WorldObservation","name":"observation","required":true}],"name":"StepResult","source":"workers-v1 3"},{"fields":[{"constraint":"unique within its command namespace","kind":"Id","name":"id","required":true},{"constraint":"resolved through a profile capability","kind":"Id","name":"kindId","required":true},{"constraint":"finite and > 0","kind":"number","name":"durationMs","required":true}],"name":"Stimulus","source":"workers-v1 1"},{"fields":[{"constraint":"derived from epoch, source step, rule and ordinal","kind":"Id","name":"id","required":true},{"constraint":"","kind":"Id","name":"kindId","required":true},{"constraint":"the newly reached boundary, 0 for bootstrap","kind":"U64","name":"sourceStep","required":true},{"constraint":"","kind":"Id|null","name":"agentId","required":false},{"constraint":"","kind":"TypedValue","name":"payload","required":true}],"name":"TaskEvent","source":"workers-v1 4"},{"fields":[{"constraint":"","kind":"Id","name":"agentId","required":true},{"constraint":"","kind":"Digest","name":"profileDigest","required":true},{"constraint":"","kind":"U64","name":"ticksAdvanced","required":true},{"constraint":"","kind":"U64","name":"brainTicks","required":true},{"constraint":"","kind":"RationalNs","name":"remainder","required":true},{"constraint":"","kind":"Digest","name":"decisionDigest","required":true},{"constraint":"the commit acknowledgment","kind":"U64","name":"committedStep","required":true}],"name":"TraceAgent","source":"step-v1 8"},{"fields":[{"constraint":"","kind":"Scope","name":"scope","required":true},{"constraint":"sorted by agentId, independent of dispatch order","kind":"array","name":"agents","required":true},{"constraint":"","kind":"Id","name":"batchId","required":true},{"constraint":"","kind":"Digest","name":"controlDigest","required":true},{"constraint":"the world boundary the environment acknowledged","kind":"U64","name":"acknowledgedBoundary","required":true},{"constraint":"sorted by viewId","kind":"array<{viewId:Id,producedStep:U64}>","name":"observationBoundaries","required":true},{"constraint":"task outcome ids in task order","kind":"array","name":"outcomeIds","required":true},{"constraint":"task event ids in task order","kind":"array","name":"eventIds","required":true},{"constraint":"","kind":"U64","name":"publishedBoundary","required":true},{"constraint":"<= 5, application order: slot saves first (each slot once, digest set), then at most one rollback (no digest)","kind":"array<{kind:BoundaryActionKind,slotId:Id,stateDigest:Digest|null}>","name":"boundaryActions","required":true}],"name":"TraceBehaviour","source":"step-v1 8"},{"fields":[{"constraint":"","kind":"U64","name":"wallTimeNs","required":true},{"constraint":"","kind":"array<{agentId:Id,requestId:DomainRequestId}>","name":"prepareRequestIds","required":true},{"constraint":"","kind":"DomainRequestId","name":"advanceRequestId","required":true},{"constraint":"","kind":"array<{agentId:Id,requestId:DomainRequestId}>","name":"commitRequestIds","required":true},{"constraint":"call-","kind":"array","name":"busCallIds","required":true},{"constraint":"dlv- or own-","kind":"array","name":"deliveryIds","required":true},{"constraint":"taken order, unique checkpointId; afterActions >= the boundary's slot saves and <= its actions","kind":"array<{checkpointId:Id,afterActions:int}>","name":"captures","required":true}],"name":"TraceOperational","source":"step-v1 8"},{"fields":[{"constraint":"compared between runs","kind":"TraceBehaviour","name":"behaviour","required":true},{"constraint":"recorded, never compared: wall time and transport identities","kind":"TraceOperational","name":"operational","required":true}],"name":"TransitionTrace","source":"step-v1 8"},{"fields":[{"constraint":"","kind":"SchemaRef","name":"schema","required":true},{"constraint":"canonical JSON of the whole TypedValue <= 32768 bytes","kind":"object","name":"value","required":true}],"name":"TypedValue","source":"ipc-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"viewId","required":true},{"constraint":"1..=4096","kind":"int","name":"width","required":true},{"constraint":"1..=4096","kind":"int","name":"height","required":true},{"constraint":"","kind":"ViewFormat","name":"format","required":true},{"constraint":"exactly 4 x width","kind":"int","name":"rowStride","required":true},{"constraint":"positive integers <= 65535","kind":"{numerator:int,denominator:int}","name":"pixelAspect","required":true},{"constraint":"0..=8","kind":"int","name":"observationDelaySteps","required":true}],"name":"ViewDescriptor","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"Id","name":"viewId","required":true},{"constraint":"max(0, boundary - observationDelaySteps) for required sensory views","kind":"U64","name":"producedStep","required":true},{"constraint":"listed attachment; byteLength == rowStride x height","kind":"ArtifactRef","name":"pixels","required":true}],"name":"ViewRef","source":"state-media-v1 2"},{"fields":[{"constraint":"","kind":"U64","name":"boundary","required":true},{"constraint":"logical time since episode start","kind":"RationalNs","name":"worldTime","required":true},{"constraint":"<= 64 characters","kind":"string|null","name":"engineFrame","required":false},{"constraint":"<= 8, unique viewId","kind":"array","name":"sensoryViews","required":true},{"constraint":"the descriptor's inspectionSchema","kind":"TypedValue","name":"inspection","required":true},{"constraint":"<= 8, unique viewId","kind":"array","name":"broadcastViews","required":true},{"constraint":"unique streamId","kind":"array","name":"audio","required":true}],"name":"WorldObservation","source":"workers-v1 3"}],"version":1} diff --git a/services/flysim/crates/fly-session-types/fixtures/traces.json b/services/flysim/crates/fly-session-types/fixtures/traces.json index 71d8189..f6aefd1 100644 --- a/services/flysim/crates/fly-session-types/fixtures/traces.json +++ b/services/flysim/crates/fly-session-types/fixtures/traces.json @@ -54,7 +54,8 @@ "evt-1", "evt-2" ], - "publishedBoundary": "42" + "publishedBoundary": "42", + "boundaryActions": [] }, "operational": { "wallTimeNs": "1234567890", @@ -87,7 +88,8 @@ "deliveryIds": [ "dlv-7", "own-9" - ] + ], + "captures": [] } }, "variants": [ @@ -147,7 +149,8 @@ "evt-1", "evt-2" ], - "publishedBoundary": "42" + "publishedBoundary": "42", + "boundaryActions": [] }, "operational": { "wallTimeNs": "1234567890", @@ -180,7 +183,8 @@ "deliveryIds": [ "dlv-7", "own-9" - ] + ], + "captures": [] } }, "behaviourEquals": true, @@ -242,7 +246,8 @@ "evt-1", "evt-2" ], - "publishedBoundary": "42" + "publishedBoundary": "42", + "boundaryActions": [] }, "operational": { "wallTimeNs": "9999999999", @@ -276,7 +281,8 @@ "deliveryIds": [ "dlv-91", "own-92" - ] + ], + "captures": [] } }, "behaviourEquals": true, @@ -338,7 +344,8 @@ "evt-1", "evt-2" ], - "publishedBoundary": "42" + "publishedBoundary": "42", + "boundaryActions": [] }, "operational": { "wallTimeNs": "1234567890", @@ -371,7 +378,8 @@ "deliveryIds": [ "dlv-7", "own-9" - ] + ], + "captures": [] } }, "behaviourEquals": true, @@ -433,7 +441,8 @@ "evt-1", "evt-2" ], - "publishedBoundary": "42" + "publishedBoundary": "42", + "boundaryActions": [] }, "operational": { "wallTimeNs": "1234567890", @@ -466,7 +475,8 @@ "deliveryIds": [ "dlv-7", "own-9" - ] + ], + "captures": [] } }, "behaviourEquals": false, @@ -528,7 +538,8 @@ "evt-1", "evt-2" ], - "publishedBoundary": "42" + "publishedBoundary": "42", + "boundaryActions": [] }, "operational": { "wallTimeNs": "1234567890", @@ -561,7 +572,8 @@ "deliveryIds": [ "dlv-7", "own-9" - ] + ], + "captures": [] } }, "behaviourEquals": false, @@ -623,7 +635,8 @@ "evt-1", "evt-2" ], - "publishedBoundary": "42" + "publishedBoundary": "42", + "boundaryActions": [] }, "operational": { "wallTimeNs": "1234567890", @@ -656,7 +669,8 @@ "deliveryIds": [ "dlv-7", "own-9" - ] + ], + "captures": [] } }, "behaviourEquals": false, @@ -718,7 +732,8 @@ "evt-1", "evt-2" ], - "publishedBoundary": "42" + "publishedBoundary": "42", + "boundaryActions": [] }, "operational": { "wallTimeNs": "1234567890", @@ -751,7 +766,8 @@ "deliveryIds": [ "dlv-7", "own-9" - ] + ], + "captures": [] } }, "behaviourEquals": false, @@ -813,7 +829,8 @@ "evt-1", "evt-2" ], - "publishedBoundary": "42" + "publishedBoundary": "42", + "boundaryActions": [] }, "operational": { "wallTimeNs": "1234567890", @@ -846,7 +863,8 @@ "deliveryIds": [ "dlv-7", "own-9" - ] + ], + "captures": [] } }, "behaviourEquals": false, @@ -908,7 +926,8 @@ "evt-1", "evt-2" ], - "publishedBoundary": "42" + "publishedBoundary": "42", + "boundaryActions": [] }, "operational": { "wallTimeNs": "1234567890", @@ -941,7 +960,8 @@ "deliveryIds": [ "dlv-7", "own-9" - ] + ], + "captures": [] } }, "behaviourEquals": false, @@ -1003,7 +1023,8 @@ "evt-2", "evt-1" ], - "publishedBoundary": "42" + "publishedBoundary": "42", + "boundaryActions": [] }, "operational": { "wallTimeNs": "1234567890", @@ -1036,7 +1057,8 @@ "deliveryIds": [ "dlv-7", "own-9" - ] + ], + "captures": [] } }, "behaviourEquals": false, @@ -1098,7 +1120,8 @@ "evt-1", "evt-2" ], - "publishedBoundary": "42" + "publishedBoundary": "42", + "boundaryActions": [] }, "operational": { "wallTimeNs": "1234567890", @@ -1131,11 +1154,12 @@ "deliveryIds": [ "dlv-7", "own-9" - ] + ], + "captures": [] } }, "behaviourEquals": false, "diffContains": "agents" } ] -} \ No newline at end of file +} diff --git a/services/flysim/crates/fly-session-types/fixtures/valid.json b/services/flysim/crates/fly-session-types/fixtures/valid.json index 9c2c111..acc44de 100644 --- a/services/flysim/crates/fly-session-types/fixtures/valid.json +++ b/services/flysim/crates/fly-session-types/fixtures/valid.json @@ -2763,7 +2763,8 @@ "eventIds": [ "evt-1" ], - "publishedBoundary": "42" + "publishedBoundary": "42", + "boundaryActions": [] }, "operational": { "wallTimeNs": "1234567890", @@ -2796,12 +2797,13 @@ "deliveryIds": [ "dlv-7", "own-9" - ] + ], + "captures": [] } }, "note": "", - "canonical": "{\"behaviour\":{\"acknowledgedBoundary\":\"42\",\"agents\":[{\"agentId\":\"fly-a\",\"brainTicks\":\"2517\",\"committedStep\":\"42\",\"decisionDigest\":\"a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"3\",\"numerator\":\"1000000\"},\"ticksAdvanced\":\"17\"},{\"agentId\":\"fly-b\",\"brainTicks\":\"2550\",\"committedStep\":\"42\",\"decisionDigest\":\"ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"1\",\"numerator\":\"0\"},\"ticksAdvanced\":\"17\"}],\"batchId\":\"batch-41\",\"controlDigest\":\"1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6\",\"eventIds\":[\"evt-1\"],\"observationBoundaries\":[{\"producedStep\":\"42\",\"viewId\":\"screen\"}],\"outcomeIds\":[\"outcome-1\"],\"publishedBoundary\":\"42\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"41\"}},\"operational\":{\"advanceRequestId\":\"req-43\",\"busCallIds\":[\"call-100\",\"call-101\",\"call-102\"],\"commitRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-44\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-45\"}],\"deliveryIds\":[\"dlv-7\",\"own-9\"],\"prepareRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-41\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-42\"}],\"wallTimeNs\":\"1234567890\"}}", - "digest": "707701d6dcf5529a67cc9b1e300d5e1a273b9a7b02d91154d4a2a1978cd8e549" + "canonical": "{\"behaviour\":{\"acknowledgedBoundary\":\"42\",\"agents\":[{\"agentId\":\"fly-a\",\"brainTicks\":\"2517\",\"committedStep\":\"42\",\"decisionDigest\":\"a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"3\",\"numerator\":\"1000000\"},\"ticksAdvanced\":\"17\"},{\"agentId\":\"fly-b\",\"brainTicks\":\"2550\",\"committedStep\":\"42\",\"decisionDigest\":\"ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"1\",\"numerator\":\"0\"},\"ticksAdvanced\":\"17\"}],\"batchId\":\"batch-41\",\"boundaryActions\":[],\"controlDigest\":\"1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6\",\"eventIds\":[\"evt-1\"],\"observationBoundaries\":[{\"producedStep\":\"42\",\"viewId\":\"screen\"}],\"outcomeIds\":[\"outcome-1\"],\"publishedBoundary\":\"42\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"41\"}},\"operational\":{\"advanceRequestId\":\"req-43\",\"busCallIds\":[\"call-100\",\"call-101\",\"call-102\"],\"captures\":[],\"commitRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-44\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-45\"}],\"deliveryIds\":[\"dlv-7\",\"own-9\"],\"prepareRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-41\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-42\"}],\"wallTimeNs\":\"1234567890\"}}", + "digest": "36ecd5d39f81863c30c0fd52a12d72eb3ba71f0f3b72b2349e2c7f02a1d8e3e5" }, { "name": "trace behaviour on its own", @@ -2853,11 +2855,12 @@ "eventIds": [ "evt-1" ], - "publishedBoundary": "42" + "publishedBoundary": "42", + "boundaryActions": [] }, "note": "the half two runs must agree on", - "canonical": "{\"acknowledgedBoundary\":\"42\",\"agents\":[{\"agentId\":\"fly-a\",\"brainTicks\":\"2517\",\"committedStep\":\"42\",\"decisionDigest\":\"a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"3\",\"numerator\":\"1000000\"},\"ticksAdvanced\":\"17\"},{\"agentId\":\"fly-b\",\"brainTicks\":\"2550\",\"committedStep\":\"42\",\"decisionDigest\":\"ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"1\",\"numerator\":\"0\"},\"ticksAdvanced\":\"17\"}],\"batchId\":\"batch-41\",\"controlDigest\":\"1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6\",\"eventIds\":[\"evt-1\"],\"observationBoundaries\":[{\"producedStep\":\"42\",\"viewId\":\"screen\"}],\"outcomeIds\":[\"outcome-1\"],\"publishedBoundary\":\"42\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"41\"}}", - "digest": "d938188fe3006537dbc0038fabf2a05df584e64b361b1106d79f3f965dff414a" + "canonical": "{\"acknowledgedBoundary\":\"42\",\"agents\":[{\"agentId\":\"fly-a\",\"brainTicks\":\"2517\",\"committedStep\":\"42\",\"decisionDigest\":\"a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"3\",\"numerator\":\"1000000\"},\"ticksAdvanced\":\"17\"},{\"agentId\":\"fly-b\",\"brainTicks\":\"2550\",\"committedStep\":\"42\",\"decisionDigest\":\"ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"1\",\"numerator\":\"0\"},\"ticksAdvanced\":\"17\"}],\"batchId\":\"batch-41\",\"boundaryActions\":[],\"controlDigest\":\"1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6\",\"eventIds\":[\"evt-1\"],\"observationBoundaries\":[{\"producedStep\":\"42\",\"viewId\":\"screen\"}],\"outcomeIds\":[\"outcome-1\"],\"publishedBoundary\":\"42\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"41\"}}", + "digest": "59ed2ef812fe44a5808d8251b253c76a642cdc6828ee964a1c90ef302b66c9e4" }, { "name": "trace operational metadata on its own", @@ -2893,11 +2896,12 @@ "deliveryIds": [ "dlv-7", "own-9" - ] + ], + "captures": [] }, "note": "recorded by step-v1 section 8, excluded from its comparison", - "canonical": "{\"advanceRequestId\":\"req-43\",\"busCallIds\":[\"call-100\",\"call-101\",\"call-102\"],\"commitRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-44\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-45\"}],\"deliveryIds\":[\"dlv-7\",\"own-9\"],\"prepareRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-41\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-42\"}],\"wallTimeNs\":\"1234567890\"}", - "digest": "07c3402ab0e48b186d56f667da980a15b2a48b21381235cff37523a24ff0dfff" + "canonical": "{\"advanceRequestId\":\"req-43\",\"busCallIds\":[\"call-100\",\"call-101\",\"call-102\"],\"captures\":[],\"commitRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-44\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-45\"}],\"deliveryIds\":[\"dlv-7\",\"own-9\"],\"prepareRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-41\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-42\"}],\"wallTimeNs\":\"1234567890\"}", + "digest": "d9fd215dbea34237559ff5a5826beca16d3646340f2ce76ee62115d21f7c27d6" }, { "name": "agent telemetry reporting a running stimulation pulse", @@ -3338,12 +3342,39 @@ "mode": "macros", "macroChannels": [ "macro_go_objective", + "macro_go_out", + "macro_go_warp", + "macro_go_route", + "macro_go_item", + "macro_go_npc", + "macro_go_frontier", + "macro_go_shop", + "macro_go_heal", "macro_talk", + "macro_menu", "macro_next", - "macro_move_1" + "macro_yes", + "macro_no", + "macro_close", + "macro_confirm", + "macro_back", + "macro_move_1", + "macro_move_2", + "macro_move_3", + "macro_move_4", + "macro_switch", + "macro_item", + "macro_throw_ball", + "macro_run", + "macro_buy_potion", + "macro_buy_ball", + "macro_buy_antidote", + "macro_buy_repel", + "macro_heal", + "macro_leave" ] }, - "decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854", + "decoderConfigDigest": "82b6601f0390b742a67eca44ef935b49e16a73315db70f5644f1cbd21f7c8a52", "environment": { "extensions": [ "gameboy-slots-v1" @@ -3376,9 +3407,9 @@ "checkpointFormatOfRecord": "FLYSIM01", "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" }, - "note": "placeholder ROM and decoder digests", - "canonical": "{\"checkpointFormatOfRecord\":\"FLYSIM01\",\"compositionId\":\"pokered-live\",\"decoderConfigDigest\":\"5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854\",\"environment\":{\"audio\":{\"channels\":2,\"sampleRate\":48000},\"controllerSchema\":{\"digest\":\"1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e\",\"id\":\"gameboy-joypad-v1\",\"version\":1},\"extensions\":[\"gameboy-slots-v1\"],\"inspectionSchema\":{\"digest\":\"d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6\",\"id\":\"gameboy-memory-inspection-v1\",\"version\":1},\"setupFrames\":1,\"slots\":[\"best\"],\"stepDuration\":{\"denominator\":\"512\",\"numerator\":\"8572265625\"}},\"episodePolicy\":\"legacy-ratchet-rollback-v1\",\"executor\":{\"adapter\":\"pokered-unique8-v6\",\"id\":\"pokered-macros-v1\",\"macroChannels\":[\"macro_go_objective\",\"macro_talk\",\"macro_next\",\"macro_move_1\"],\"mode\":\"macros\",\"rom\":{\"byteLength\":\"1048576\",\"digest\":\"c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20\",\"format\":\"gb-rom\",\"id\":\"pokered-rom\"},\"symbolProvenance\":\"0cd19d3b877b7dc66d12c7050bed9a7f38154d4b\"},\"flysimCompatibility\":\"lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu\",\"profile\":{\"byteLength\":\"1137\",\"digest\":\"41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878\",\"format\":\"fly-profile-v1\",\"id\":\"gameboy-legacy-fafb-v783-v1\"},\"restore\":\"legacy-transient-reset\",\"scheduler\":\"lockstep-v1\"}", - "digest": "f0142c7f09a1319453b472cdddbc1855062af5733f89d1fbc0b82c0adb52b0c7" + "note": "placeholder ROM digest; real macros-mode decoder digest and channels", + "canonical": "{\"checkpointFormatOfRecord\":\"FLYSIM01\",\"compositionId\":\"pokered-live\",\"decoderConfigDigest\":\"82b6601f0390b742a67eca44ef935b49e16a73315db70f5644f1cbd21f7c8a52\",\"environment\":{\"audio\":{\"channels\":2,\"sampleRate\":48000},\"controllerSchema\":{\"digest\":\"1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e\",\"id\":\"gameboy-joypad-v1\",\"version\":1},\"extensions\":[\"gameboy-slots-v1\"],\"inspectionSchema\":{\"digest\":\"d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6\",\"id\":\"gameboy-memory-inspection-v1\",\"version\":1},\"setupFrames\":1,\"slots\":[\"best\"],\"stepDuration\":{\"denominator\":\"512\",\"numerator\":\"8572265625\"}},\"episodePolicy\":\"legacy-ratchet-rollback-v1\",\"executor\":{\"adapter\":\"pokered-unique8-v6\",\"id\":\"pokered-macros-v1\",\"macroChannels\":[\"macro_go_objective\",\"macro_go_out\",\"macro_go_warp\",\"macro_go_route\",\"macro_go_item\",\"macro_go_npc\",\"macro_go_frontier\",\"macro_go_shop\",\"macro_go_heal\",\"macro_talk\",\"macro_menu\",\"macro_next\",\"macro_yes\",\"macro_no\",\"macro_close\",\"macro_confirm\",\"macro_back\",\"macro_move_1\",\"macro_move_2\",\"macro_move_3\",\"macro_move_4\",\"macro_switch\",\"macro_item\",\"macro_throw_ball\",\"macro_run\",\"macro_buy_potion\",\"macro_buy_ball\",\"macro_buy_antidote\",\"macro_buy_repel\",\"macro_heal\",\"macro_leave\"],\"mode\":\"macros\",\"rom\":{\"byteLength\":\"1048576\",\"digest\":\"c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20\",\"format\":\"gb-rom\",\"id\":\"pokered-rom\"},\"symbolProvenance\":\"0cd19d3b877b7dc66d12c7050bed9a7f38154d4b\"},\"flysimCompatibility\":\"lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu\",\"profile\":{\"byteLength\":\"1137\",\"digest\":\"41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878\",\"format\":\"fly-profile-v1\",\"id\":\"gameboy-legacy-fafb-v783-v1\"},\"restore\":\"legacy-transient-reset\",\"scheduler\":\"lockstep-v1\"}", + "digest": "77d8a88ff7fea51eb29b1ae75fc9cf8c17c184e6a1e5a585e86399f41fd9c9b0" }, { "name": "an example legacy composition in raw mode", @@ -3405,7 +3436,7 @@ "mode": "raw", "macroChannels": [] }, - "decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854", + "decoderConfigDigest": "6234e4a0363cfe82b8d515943c4f645a3960eda8fa5bf9465009061f80d50812", "environment": { "extensions": [ "gameboy-slots-v1" @@ -3439,8 +3470,225 @@ "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" }, "note": "", - "canonical": "{\"checkpointFormatOfRecord\":\"FLYSIM01\",\"compositionId\":\"pokered-live\",\"decoderConfigDigest\":\"5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854\",\"environment\":{\"audio\":{\"channels\":2,\"sampleRate\":48000},\"controllerSchema\":{\"digest\":\"1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e\",\"id\":\"gameboy-joypad-v1\",\"version\":1},\"extensions\":[\"gameboy-slots-v1\"],\"inspectionSchema\":{\"digest\":\"d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6\",\"id\":\"gameboy-memory-inspection-v1\",\"version\":1},\"setupFrames\":1,\"slots\":[\"best\"],\"stepDuration\":{\"denominator\":\"512\",\"numerator\":\"8572265625\"}},\"episodePolicy\":\"legacy-ratchet-rollback-v1\",\"executor\":{\"adapter\":\"pokered-unique8-v6\",\"id\":\"pokered-macros-v1\",\"macroChannels\":[],\"mode\":\"raw\",\"rom\":{\"byteLength\":\"1048576\",\"digest\":\"c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20\",\"format\":\"gb-rom\",\"id\":\"pokered-rom\"},\"symbolProvenance\":\"0cd19d3b877b7dc66d12c7050bed9a7f38154d4b\"},\"flysimCompatibility\":\"lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu\",\"profile\":{\"byteLength\":\"1137\",\"digest\":\"41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878\",\"format\":\"fly-profile-v1\",\"id\":\"gameboy-legacy-fafb-v783-v1\"},\"restore\":\"legacy-transient-reset\",\"scheduler\":\"lockstep-v1\"}", - "digest": "efd699cf8cf2485dd2ac8a84ff43c9bc3711a78181f32285ed31a2ee86e16925" + "canonical": "{\"checkpointFormatOfRecord\":\"FLYSIM01\",\"compositionId\":\"pokered-live\",\"decoderConfigDigest\":\"6234e4a0363cfe82b8d515943c4f645a3960eda8fa5bf9465009061f80d50812\",\"environment\":{\"audio\":{\"channels\":2,\"sampleRate\":48000},\"controllerSchema\":{\"digest\":\"1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e\",\"id\":\"gameboy-joypad-v1\",\"version\":1},\"extensions\":[\"gameboy-slots-v1\"],\"inspectionSchema\":{\"digest\":\"d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6\",\"id\":\"gameboy-memory-inspection-v1\",\"version\":1},\"setupFrames\":1,\"slots\":[\"best\"],\"stepDuration\":{\"denominator\":\"512\",\"numerator\":\"8572265625\"}},\"episodePolicy\":\"legacy-ratchet-rollback-v1\",\"executor\":{\"adapter\":\"pokered-unique8-v6\",\"id\":\"pokered-macros-v1\",\"macroChannels\":[],\"mode\":\"raw\",\"rom\":{\"byteLength\":\"1048576\",\"digest\":\"c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20\",\"format\":\"gb-rom\",\"id\":\"pokered-rom\"},\"symbolProvenance\":\"0cd19d3b877b7dc66d12c7050bed9a7f38154d4b\"},\"flysimCompatibility\":\"lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu\",\"profile\":{\"byteLength\":\"1137\",\"digest\":\"41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878\",\"format\":\"fly-profile-v1\",\"id\":\"gameboy-legacy-fafb-v783-v1\"},\"restore\":\"legacy-transient-reset\",\"scheduler\":\"lockstep-v1\"}", + "digest": "ac42e702ec6c9b09482b1ee335743ef85fe599a72e2c34a4d32aea67d67d0201" + }, + { + "name": "a rank climb: slot saved, then the milestone capture", + "type": "TransitionTrace", + "value": { + "behaviour": { + "scope": { + "sessionId": "demo", + "epoch": "epoch-1", + "step": "41" + }, + "agents": [ + { + "agentId": "fly-a", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2517", + "remainder": { + "numerator": "1000000", + "denominator": "3" + }, + "decisionDigest": "a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b", + "committedStep": "42" + }, + { + "agentId": "fly-b", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2550", + "remainder": { + "numerator": "0", + "denominator": "1" + }, + "decisionDigest": "ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e", + "committedStep": "42" + } + ], + "batchId": "batch-41", + "controlDigest": "1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6", + "acknowledgedBoundary": "42", + "observationBoundaries": [ + { + "viewId": "screen", + "producedStep": "42" + } + ], + "outcomeIds": [ + "outcome-1" + ], + "eventIds": [ + "evt-1" + ], + "publishedBoundary": "42", + "boundaryActions": [ + { + "kind": "save-slot", + "slotId": "best", + "stateDigest": "5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d" + } + ] + }, + "operational": { + "wallTimeNs": "1234567890", + "prepareRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-41" + }, + { + "agentId": "fly-b", + "requestId": "req-42" + } + ], + "advanceRequestId": "req-43", + "commitRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-44" + }, + { + "agentId": "fly-b", + "requestId": "req-45" + } + ], + "busCallIds": [ + "call-100", + "call-101", + "call-102" + ], + "deliveryIds": [ + "dlv-7", + "own-9" + ], + "captures": [ + { + "checkpointId": "milestone-11", + "afterActions": 1 + } + ] + } + }, + "note": "legacy-gameboy-v1 section 16: a slot save due at a boundary completes before any capture there", + "canonical": "{\"behaviour\":{\"acknowledgedBoundary\":\"42\",\"agents\":[{\"agentId\":\"fly-a\",\"brainTicks\":\"2517\",\"committedStep\":\"42\",\"decisionDigest\":\"a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"3\",\"numerator\":\"1000000\"},\"ticksAdvanced\":\"17\"},{\"agentId\":\"fly-b\",\"brainTicks\":\"2550\",\"committedStep\":\"42\",\"decisionDigest\":\"ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"1\",\"numerator\":\"0\"},\"ticksAdvanced\":\"17\"}],\"batchId\":\"batch-41\",\"boundaryActions\":[{\"kind\":\"save-slot\",\"slotId\":\"best\",\"stateDigest\":\"5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d\"}],\"controlDigest\":\"1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6\",\"eventIds\":[\"evt-1\"],\"observationBoundaries\":[{\"producedStep\":\"42\",\"viewId\":\"screen\"}],\"outcomeIds\":[\"outcome-1\"],\"publishedBoundary\":\"42\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"41\"}},\"operational\":{\"advanceRequestId\":\"req-43\",\"busCallIds\":[\"call-100\",\"call-101\",\"call-102\"],\"captures\":[{\"afterActions\":1,\"checkpointId\":\"milestone-11\"}],\"commitRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-44\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-45\"}],\"deliveryIds\":[\"dlv-7\",\"own-9\"],\"prepareRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-41\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-42\"}],\"wallTimeNs\":\"1234567890\"}}", + "digest": "68c6f1e21cc6f63c36e69b73dbd5c11313b2a76e57f1e5e8468984516a6f96d5" + }, + { + "name": "save, capture, rollback, capture at one boundary", + "type": "TransitionTrace", + "value": { + "behaviour": { + "scope": { + "sessionId": "demo", + "epoch": "epoch-1", + "step": "41" + }, + "agents": [ + { + "agentId": "fly-a", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2517", + "remainder": { + "numerator": "1000000", + "denominator": "3" + }, + "decisionDigest": "a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b", + "committedStep": "42" + }, + { + "agentId": "fly-b", + "profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0", + "ticksAdvanced": "17", + "brainTicks": "2550", + "remainder": { + "numerator": "0", + "denominator": "1" + }, + "decisionDigest": "ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e", + "committedStep": "42" + } + ], + "batchId": "batch-41", + "controlDigest": "1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6", + "acknowledgedBoundary": "42", + "observationBoundaries": [ + { + "viewId": "screen", + "producedStep": "42" + } + ], + "outcomeIds": [ + "outcome-1" + ], + "eventIds": [ + "evt-1" + ], + "publishedBoundary": "42", + "boundaryActions": [ + { + "kind": "save-slot", + "slotId": "best", + "stateDigest": "5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d" + }, + { + "kind": "rollback", + "slotId": "best", + "stateDigest": null + } + ] + }, + "operational": { + "wallTimeNs": "1234567890", + "prepareRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-41" + }, + { + "agentId": "fly-b", + "requestId": "req-42" + } + ], + "advanceRequestId": "req-43", + "commitRequestIds": [ + { + "agentId": "fly-a", + "requestId": "req-44" + }, + { + "agentId": "fly-b", + "requestId": "req-45" + } + ], + "busCallIds": [ + "call-100", + "call-101", + "call-102" + ], + "deliveryIds": [ + "dlv-7", + "own-9" + ], + "captures": [ + { + "checkpointId": "periodic-7", + "afterActions": 1 + }, + { + "checkpointId": "after-rollback-7", + "afterActions": 2 + } + ] + } + }, + "note": "a durable save follows the rollback, as the legacy loop checkpoints after a recovery", + "canonical": "{\"behaviour\":{\"acknowledgedBoundary\":\"42\",\"agents\":[{\"agentId\":\"fly-a\",\"brainTicks\":\"2517\",\"committedStep\":\"42\",\"decisionDigest\":\"a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"3\",\"numerator\":\"1000000\"},\"ticksAdvanced\":\"17\"},{\"agentId\":\"fly-b\",\"brainTicks\":\"2550\",\"committedStep\":\"42\",\"decisionDigest\":\"ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"1\",\"numerator\":\"0\"},\"ticksAdvanced\":\"17\"}],\"batchId\":\"batch-41\",\"boundaryActions\":[{\"kind\":\"save-slot\",\"slotId\":\"best\",\"stateDigest\":\"5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d\"},{\"kind\":\"rollback\",\"slotId\":\"best\",\"stateDigest\":null}],\"controlDigest\":\"1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6\",\"eventIds\":[\"evt-1\"],\"observationBoundaries\":[{\"producedStep\":\"42\",\"viewId\":\"screen\"}],\"outcomeIds\":[\"outcome-1\"],\"publishedBoundary\":\"42\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"41\"}},\"operational\":{\"advanceRequestId\":\"req-43\",\"busCallIds\":[\"call-100\",\"call-101\",\"call-102\"],\"captures\":[{\"afterActions\":1,\"checkpointId\":\"periodic-7\"},{\"afterActions\":2,\"checkpointId\":\"after-rollback-7\"}],\"commitRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-44\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-45\"}],\"deliveryIds\":[\"dlv-7\",\"own-9\"],\"prepareRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-41\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-42\"}],\"wallTimeNs\":\"1234567890\"}}", + "digest": "ae22a2c5e3eb5e65c6ca82c27b2f5242bd94983fca4b11978d7067dc4389d0bb" } ] } diff --git a/services/flysim/crates/fly-session-types/src/schema.rs b/services/flysim/crates/fly-session-types/src/schema.rs index ac64bd4..a7a6e9c 100644 --- a/services/flysim/crates/fly-session-types/src/schema.rs +++ b/services/flysim/crates/fly-session-types/src/schema.rs @@ -126,6 +126,11 @@ pub const ENUMS: &[EnumSchema] = &[ source: "publishing-v1 3", members: &["lockstep-v1"], }, + EnumSchema { + name: "BoundaryActionKind", + source: "step-v1 8", + members: crate::trace::BoundaryActionKind::ALL, + }, EnumSchema { name: "EpisodeRequestKind", source: "workers-v1 4", @@ -1070,6 +1075,11 @@ pub const SCHEMAS: &[TypeSchema] = &[ req("outcomeIds", "array", "task outcome ids in task order"), req("eventIds", "array", "task event ids in task order"), req("publishedBoundary", "U64", ""), + req( + "boundaryActions", + "array<{kind:BoundaryActionKind,slotId:Id,stateDigest:Digest|null}>", + "<= 5, application order: slot saves first (each slot once, digest set), then at most one rollback (no digest)", + ), ], }, TypeSchema { @@ -1103,6 +1113,11 @@ pub const SCHEMAS: &[TypeSchema] = &[ ), req("busCallIds", "array", "call-"), req("deliveryIds", "array", "dlv- or own-"), + req( + "captures", + "array<{checkpointId:Id,afterActions:int}>", + "taken order, unique checkpointId; afterActions >= the boundary's slot saves and <= its actions", + ), ], }, ]; diff --git a/services/flysim/crates/fly-session-types/src/trace.rs b/services/flysim/crates/fly-session-types/src/trace.rs index da48f85..c8bb33d 100644 --- a/services/flysim/crates/fly-session-types/src/trace.rs +++ b/services/flysim/crates/fly-session-types/src/trace.rs @@ -44,6 +44,42 @@ pub struct TraceObservation { pub produced_step: u64, } +/// The kind of an action taken at the boundary a transition reached, after all of its commits +/// (amendment of 2026-09-23, RT-01a). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum BoundaryActionKind { + SaveSlot, + Rollback, +} + +impl BoundaryActionKind { + pub const ALL: &'static [&'static str] = &["save-slot", "rollback"]; + + pub fn as_str(self) -> &'static str { + match self { + BoundaryActionKind::SaveSlot => "save-slot", + BoundaryActionKind::Rollback => "rollback", + } + } + + pub fn parse(s: &str) -> Result { + match s { + "save-slot" => Ok(BoundaryActionKind::SaveSlot), + "rollback" => Ok(BoundaryActionKind::Rollback), + _ => err("boundary action kind must be save-slot or rollback"), + } + } +} + +/// One boundary action, in the order the coordinator applied it: an `Environment.SaveSlot` +/// (with the saved state's digest) or a rollback to a slot (no digest). +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct BoundaryAction { + pub kind: BoundaryActionKind, + pub slot_id: String, + pub state_digest: Option, +} + /// The fields two runs of the same transition must agree on. #[derive(Clone, Debug, PartialEq, Eq)] pub struct TraceBehaviour { @@ -60,6 +96,9 @@ pub struct TraceBehaviour { /// Task event ids in task order. pub event_ids: Vec, pub published_boundary: u64, + /// Slot saves and a rollback at the reached boundary, in application order. Empty for a + /// composition without them. + pub boundary_actions: Vec, } impl TraceBehaviour { @@ -126,6 +165,22 @@ impl DomainType for TraceBehaviour { let outcome_ids = crate::scalar::id_list(&mut f, "outcomeIds", 0, MAX_RATE_ROLES)?; let event_ids = crate::scalar::id_list(&mut f, "eventIds", 0, MAX_RATE_ROLES)?; let published_boundary = f.u64_string("publishedBoundary")?; + let boundary_actions = list(&mut f, "boundaryActions", 0, MAX_BOUNDARY_ACTIONS, |v| { + let mut a = Fields::new(v, "TraceBehaviour.boundaryActions")?; + let kind = BoundaryActionKind::parse(a.string("kind")?)?; + let slot_id = a.id("slotId")?; + let state_digest = match a.value("stateDigest")? { + Value::Null => None, + Value::String(d) => Some(d.clone()), + _ => return err("stateDigest must be null or a digest"), + }; + a.finish()?; + Ok(BoundaryAction { + kind, + slot_id, + state_digest, + }) + })?; f.finish()?; let b = TraceBehaviour { scope, @@ -137,6 +192,7 @@ impl DomainType for TraceBehaviour { outcome_ids, event_ids, published_boundary, + boundary_actions, }; b.validate()?; Ok(b) @@ -190,6 +246,24 @@ impl DomainType for TraceBehaviour { Value::Array(self.event_ids.iter().map(|i| i.clone().into()).collect()), ), ("publishedBoundary", u64_json(self.published_boundary)), + ( + "boundaryActions", + Value::Array( + self.boundary_actions + .iter() + .map(|a| { + obj(vec![ + ("kind", a.kind.as_str().into()), + ("slotId", a.slot_id.clone().into()), + ( + "stateDigest", + a.state_digest.clone().map_or(Value::Null, Value::String), + ), + ]) + }) + .collect(), + ), + ), ]) } @@ -244,8 +318,55 @@ impl DomainType for TraceBehaviour { self.outcome_ids.iter().map(String::as_str), "TraceBehaviour.outcomeIds", )?; + self.validate_boundary_actions() + } +} + +/// Boundary actions per transition: every slot at most once, plus one rollback. +pub const MAX_BOUNDARY_ACTIONS: usize = crate::extensions::MAX_SLOTS + 1; + +impl TraceBehaviour { + /// Slot saves come first, each slot at most once, with the saved state's digest; at most + /// one rollback, last, naming a slot and no digest (step-v1 section 6 amendment). + fn validate_boundary_actions(&self) -> Result<()> { + let mut rolled_back = false; + let mut saved: Vec<&str> = Vec::new(); + for action in &self.boundary_actions { + if !is_id(&action.slot_id) { + return err("TraceBehaviour: boundary action slotId is not a valid id"); + } + if rolled_back { + return err("TraceBehaviour: nothing follows a rollback at the same boundary"); + } + match action.kind { + BoundaryActionKind::SaveSlot => { + match &action.state_digest { + Some(d) if is_digest(d) => {} + _ => return err("TraceBehaviour: a slot save records its state digest"), + } + if saved.contains(&action.slot_id.as_str()) { + return err("TraceBehaviour: a slot is saved at most once per boundary"); + } + saved.push(&action.slot_id); + } + BoundaryActionKind::Rollback => { + if action.state_digest.is_some() { + return err("TraceBehaviour: a rollback records no state digest"); + } + rolled_back = true; + } + } + } Ok(()) } + + /// How many leading boundary actions are slot saves. + pub fn slot_saves(&self) -> usize { + self.boundary_actions + .iter() + .take_while(|a| a.kind == BoundaryActionKind::SaveSlot) + .count() + } } /// One agent's domain request id for one phase. @@ -267,6 +388,18 @@ pub struct TraceOperational { /// these and nothing in [`TraceBehaviour`]. pub bus_call_ids: Vec, pub delivery_ids: Vec, + /// Checkpoint captures (and FLYSIM01 exports) taken at the reached boundary, each with the + /// number of boundary actions already applied when it was taken. Operational because a + /// capture's schedule is wall-clock policy; the ordering rule against slot saves is checked + /// by [`TransitionTrace`] (amendment of 2026-09-23, legacy-gameboy-v1 section 16). + pub captures: Vec, +} + +/// One checkpoint capture at the reached boundary. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TraceCapture { + pub checkpoint_id: String, + pub after_actions: u64, } impl DomainType for TraceOperational { @@ -296,6 +429,16 @@ impl DomainType for TraceOperational { Some(s) => OwnerToken::parse(s), None => err("every deliveryId must be a string"), })?; + let captures = list(&mut f, "captures", 0, MAX_BOUNDARY_ACTIONS + 1, |v| { + let mut c = Fields::new(v, "TraceOperational.captures")?; + let checkpoint_id = c.id("checkpointId")?; + let after_actions = c.int("afterActions", 0, MAX_BOUNDARY_ACTIONS as u64)?; + c.finish()?; + Ok(TraceCapture { + checkpoint_id, + after_actions, + }) + })?; f.finish()?; let o = TraceOperational { wall_time_ns, @@ -304,6 +447,7 @@ impl DomainType for TraceOperational { commit_request_ids, bus_call_ids, delivery_ids, + captures, }; o.validate()?; Ok(o) @@ -341,6 +485,20 @@ impl DomainType for TraceOperational { .collect(), ), ), + ( + "captures", + Value::Array( + self.captures + .iter() + .map(|c| { + obj(vec![ + ("checkpointId", c.checkpoint_id.clone().into()), + ("afterActions", Value::from(c.after_actions)), + ]) + }) + .collect(), + ), + ), ]) } @@ -361,6 +519,17 @@ impl DomainType for TraceOperational { self.delivery_ids.iter().map(OwnerToken::as_str), "TraceOperational.deliveryIds", )?; + require_unique( + self.captures.iter().map(|c| c.checkpoint_id.as_str()), + "TraceOperational.captures", + )?; + let mut last = 0; + for capture in &self.captures { + if capture.after_actions < last { + return err("TraceOperational: captures are recorded in the order they were taken"); + } + last = capture.after_actions; + } Ok(()) } } @@ -412,6 +581,9 @@ impl TransitionTrace { if a.event_ids != b.event_ids { out.push("eventIds differ".to_owned()); } + if a.boundary_actions != b.boundary_actions { + out.push("boundaryActions differ".to_owned()); + } let ids_a: Vec<&str> = a.agents.iter().map(|x| x.agent_id.as_str()).collect(); let ids_b: Vec<&str> = b.agents.iter().map(|x| x.agent_id.as_str()).collect(); if ids_a != ids_b { @@ -481,6 +653,25 @@ impl DomainType for TransitionTrace { } } } + // A slot save due at a boundary completes before any capture at that boundary, so a + // checkpoint never pairs a task ledger that names the new slot with the old slot + // contents (legacy-gameboy-v1 section 16, review round 1 of 2026-09-23). + let saves = self.behaviour.slot_saves() as u64; + let actions = self.behaviour.boundary_actions.len() as u64; + for capture in &self.operational.captures { + if capture.after_actions < saves { + return err(format!( + "TransitionTrace: capture {:?} was taken before this boundary's slot saves completed", + capture.checkpoint_id + )); + } + if capture.after_actions > actions { + return err(format!( + "TransitionTrace: capture {:?} counts more boundary actions than were applied", + capture.checkpoint_id + )); + } + } Ok(()) } } diff --git a/services/flysim/crates/fly-session/src/coordinator.rs b/services/flysim/crates/fly-session/src/coordinator.rs index 7683ec4..7e6189f 100644 --- a/services/flysim/crates/fly-session/src/coordinator.rs +++ b/services/flysim/crates/fly-session/src/coordinator.rs @@ -2659,6 +2659,8 @@ impl Coordinator { outcome_ids, event_ids: event_ids.to_vec(), published_boundary: k + 1, + // The synthetic composition takes no boundary actions (step-v1 section 8 amendment). + boundary_actions: Vec::new(), }; let operational = TraceOperational { // Wall time is for pacing, health and presentation only. @@ -2673,6 +2675,9 @@ impl Coordinator { // and nothing in the behaviour above. bus_call_ids: Vec::new(), delivery_ids: Vec::new(), + // Captures are recorded by the store path, not by the transition that reached the + // boundary; the synthetic trace records none. + captures: Vec::new(), }; self.trace.transition(TransitionTrace { behaviour, operational }); } From 2f8ad4914aeaa457150e5d7f254d824620b1f8a1 Mon Sep 17 00:00:00 2001 From: acamilo Date: Wed, 23 Sep 2026 09:00:12 +0000 Subject: [PATCH 4/5] session types: shared decoderConfigDigest vectors from both decoder presets Review round 1, N2. The gameboy-decoder-config-v1 form (channels as ordered arrays), vectors for raw mode and the Pokemon Red macro group computed by flysim's legacy_profile_identity test from gameboy_decoder_config_with_macros and reproduced by @flybrain/session-types from the oracle's gameboyDecoderConfig. The example composition now carries the real macros-mode digest and channel set. --- package-lock.json | 1 + packages/session-types/package.json | 1 + packages/session-types/src/gameboy.ts | 72 ++++ packages/session-types/tests/gameboy.test.ts | 30 ++ .../flysim/crates/fly-session-types/README.md | 1 + .../examples/update_fixtures.rs | 32 +- .../fixtures/gameboy-decoder-config.json | 335 ++++++++++++++++++ .../fixtures/gameboy-legacy.json | 37 +- .../crates/fly-session-types/src/gameboy.rs | 2 +- .../flysim/tests/legacy_profile_identity.rs | 86 +++++ 10 files changed, 584 insertions(+), 13 deletions(-) create mode 100644 services/flysim/crates/fly-session-types/fixtures/gameboy-decoder-config.json diff --git a/package-lock.json b/package-lock.json index 9cc64fc..0edf4ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3708,6 +3708,7 @@ "version": "0.1.0", "license": "Apache-2.0", "devDependencies": { + "@flybrain/brain": "*", "@types/node": "22.17.0", "tsx": "4.20.3", "typescript": "5.9.2" diff --git a/packages/session-types/package.json b/packages/session-types/package.json index 6a388f4..0164827 100644 --- a/packages/session-types/package.json +++ b/packages/session-types/package.json @@ -15,6 +15,7 @@ "typecheck": "tsc -p tsconfig.json --pretty false" }, "devDependencies": { + "@flybrain/brain": "*", "@types/node": "22.17.0", "tsx": "4.20.3", "typescript": "5.9.2" diff --git a/packages/session-types/src/gameboy.ts b/packages/session-types/src/gameboy.ts index b81cd5f..261e708 100644 --- a/packages/session-types/src/gameboy.ts +++ b/packages/session-types/src/gameboy.ts @@ -486,3 +486,75 @@ export function compositionDeclarationDigest(composition: LegacyGameboyCompositi if (!isDigest(digest)) fail('digest'); return digest; } + +// The decoder configuration digest ------------------------------------------------------------ + +/** The name of the canonical decoder-configuration form (legacy-gameboy-v1 section 12). */ +export const DECODER_CONFIG_FORM = 'gameboy-decoder-config-v1'; + +interface GroupLike { + channels: Record; + decisionMs: number; + holdMs: number; + hysteresis: number; + fatigueGain: number; + fatigueDecay: number; + blockedFatigue: number; + blockedMs: number; +} + +/** Structurally the oracle's `DecoderConfig` (`packages/brain/src/readout/decoder.ts`). */ +export interface DecoderConfigLike { + exclusive?: GroupLike; + macros?: GroupLike; + pulses: { + channel: string; + role: string; + holdMs: number; + cooldownMs: number; + threshold: number; + boot?: { cooldownMs: number; threshold: number }; + throttleGroup?: string; + }[]; + clearLockoutMs: number; +} + +function groupForm(group: GroupLike | undefined): unknown { + if (!group) return null; + return { + // Channel order breaks argmax ties, so it is part of the identity: an array, not a map, + // because canonical JSON sorts object keys. + channels: Object.entries(group.channels).map(([channel, role]) => ({ channel, role })), + decisionMs: group.decisionMs, + holdMs: group.holdMs, + hysteresis: group.hysteresis, + fatigueGain: group.fatigueGain, + fatigueDecay: group.fatigueDecay, + blockedFatigue: group.blockedFatigue, + blockedMs: group.blockedMs, + }; +} + +/** The canonical form `decoderConfigDigest` is taken over. */ +export function decoderConfigForm(config: DecoderConfigLike): unknown { + return { + form: DECODER_CONFIG_FORM, + exclusive: groupForm(config.exclusive), + macros: groupForm(config.macros), + pulses: config.pulses.map((pulse) => ({ + channel: pulse.channel, + role: pulse.role, + holdMs: pulse.holdMs, + cooldownMs: pulse.cooldownMs, + threshold: pulse.threshold, + boot: pulse.boot ? { cooldownMs: pulse.boot.cooldownMs, threshold: pulse.boot.threshold } : null, + throttleGroup: pulse.throttleGroup ?? null, + })), + clearLockoutMs: config.clearLockoutMs, + }; +} + +/** `LegacyGameboyComposition.decoderConfigDigest`: SHA-256 of the canonical form. */ +export function decoderConfigDigest(config: DecoderConfigLike): Digest { + return digestOf(decoderConfigForm(config)); +} diff --git a/packages/session-types/tests/gameboy.test.ts b/packages/session-types/tests/gameboy.test.ts index a38b9e4..b92ebd9 100644 --- a/packages/session-types/tests/gameboy.test.ts +++ b/packages/session-types/tests/gameboy.test.ts @@ -144,3 +144,33 @@ test('a rollback request and the extension methods check what they must', () => validateAgentRollbackResultAgainstScope(result, newEpoch); assert.throws(() => validateAgentRollbackResultAgainstScope(result, { ...newEpoch, step: '4100' })); }); + +test('the decoderConfigDigest vectors are what the TypeScript oracle preset computes', async () => { + const { gameboyDecoderConfig } = await import('@flybrain/brain'); + const file = fixtures.load('gameboy-decoder-config.json') as Record; + const cases = file.cases as Record[]; + assert.deepEqual( + cases.map((item) => item.name), + ['raw', 'macros'], + ); + for (const item of cases) { + const form = gameboy.decoderConfigForm(gameboyDecoderConfig(item.macroChannels as string[])); + assert.deepEqual(form, item.form, `${item.name}: the oracle's form is the Rust twin's`); + assert.equal(canonicalize(form), item.canonical, `${item.name}: canonical bytes`); + assert.equal( + gameboy.decoderConfigDigest(gameboyDecoderConfig(item.macroChannels as string[])), + item.digest, + `${item.name}: digest`, + ); + } + const reversed = [...(cases[1]!.macroChannels as string[])].reverse(); + assert.notEqual( + gameboy.decoderConfigDigest(gameboyDecoderConfig(reversed)), + cases[1]!.digest, + 'channel order is identity', + ); + // The example composition declares the real macros-mode digest and channel set. + const example = legacy().composition.example; + assert.equal(example.decoderConfigDigest, cases[1]!.digest); + assert.deepEqual(example.executor.macroChannels, cases[1]!.macroChannels); +}); diff --git a/services/flysim/crates/fly-session-types/README.md b/services/flysim/crates/fly-session-types/README.md index 8bc46df..6e56558 100644 --- a/services/flysim/crates/fly-session-types/README.md +++ b/services/flysim/crates/fly-session-types/README.md @@ -82,6 +82,7 @@ once and holds both languages to it. | `schema-set.json`, `contract-digest.json` | The canonical schema set and its digest | | `seed-vectors.json` | `seed-derivation-v1` test vectors | | `checkpoint-envelope.json` | One `FLYSESS1` envelope, its layout and the corruptions a reader refuses | +| `gameboy-decoder-config.json` | The `decoderConfigDigest` vectors; written and checked by `flysim`'s `legacy_profile_identity` test (`FLY_UPDATE_FIXTURES=1` rewrites), reproduced by `@flybrain/session-types` from the oracle preset | | `gameboy-legacy.json` | The legacy Game Boy extension set and digest, every registered `SchemaRef`, the legacy profile and its `AssetRef`, the frame clock, an example composition and its digest | The derived files (`schema-set.json`, `contract-digest.json`, the `canonical`/`digest` fields diff --git a/services/flysim/crates/fly-session-types/examples/update_fixtures.rs b/services/flysim/crates/fly-session-types/examples/update_fixtures.rs index 153b4cc..d5b8e5b 100644 --- a/services/flysim/crates/fly-session-types/examples/update_fixtures.rs +++ b/services/flysim/crates/fly-session-types/examples/update_fixtures.rs @@ -34,10 +34,12 @@ pub fn derived() -> Vec<(String, String)> { ] } -/// An example legacy composition. The ROM and decoder digests are placeholders -- the real -/// ones are computed by the composition that runs, and no ROM identity belongs in a fixture -- -/// and the macro channels are a short excerpt of the Pokemon Red set. The compatibility -/// string is today's, byte for byte, because its segments must agree with the declaration. +/// An example legacy composition. The ROM digest is a placeholder -- the real one is computed +/// by the composition that runs, and no ROM identity belongs in a fixture. The macro channels and +/// the decoder digest are the real macros-mode vector of `gameboy-decoder-config.json`, which +/// `flysim`'s `legacy_profile_identity` test computes from `gameboy_decoder_config_with_macros` +/// and the TypeScript test from the oracle preset. The compatibility string is today's, byte for +/// byte, because its segments must agree with the declaration. pub fn example_composition() -> LegacyComposition { let pokered = "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b"; LegacyComposition { @@ -53,12 +55,17 @@ pub fn example_composition() -> LegacyComposition { adapter: "pokered-unique8-v6".to_owned(), symbol_provenance: pokered.to_owned(), mode: "macros".to_owned(), - macro_channels: ["macro_go_objective", "macro_talk", "macro_next", "macro_move_1"] + macro_channels: decoder_vector("macros")["macroChannels"] + .as_array() + .expect("macroChannels") .iter() - .map(|c| (*c).to_owned()) + .map(|c| c.as_str().expect("channel").to_owned()) .collect(), }, - decoder_config_digest: canonical::sha256_hex(b"placeholder: the effective DecoderConfig"), + decoder_config_digest: decoder_vector("macros")["digest"] + .as_str() + .expect("digest") + .to_owned(), environment: gameboy::EnvironmentDeclaration { slots: vec!["best".to_owned()], audio_sample_rate: 48_000, @@ -72,6 +79,17 @@ pub fn example_composition() -> LegacyComposition { } } +/// One case of the (flysim-written) decoder-config vectors. +fn decoder_vector(name: &str) -> Value { + let file = fixtures::load("gameboy-decoder-config.json").expect("gameboy-decoder-config.json"); + fixtures::cases(&file) + .expect("cases") + .iter() + .find(|c| c["name"] == Value::String(name.to_owned())) + .unwrap_or_else(|| panic!("decoder vector {name}")) + .clone() +} + /// The legacy Game Boy extension set, the profile document and its AssetRef, the clock /// vector and an example composition with its digest. fn gameboy_legacy() -> String { diff --git a/services/flysim/crates/fly-session-types/fixtures/gameboy-decoder-config.json b/services/flysim/crates/fly-session-types/fixtures/gameboy-decoder-config.json new file mode 100644 index 0000000..a1a1edc --- /dev/null +++ b/services/flysim/crates/fly-session-types/fixtures/gameboy-decoder-config.json @@ -0,0 +1,335 @@ +{ + "description": "decoderConfigDigest vectors (legacy-gameboy-v1 section 12): the canonical form of gameboy_decoder_config_with_macros / gameboyDecoderConfig for raw mode and the Pokemon Red macro group. Written by FLY_UPDATE_FIXTURES=1 cargo test -p flysim --test legacy_profile_identity; both languages must reproduce every form and digest.", + "cases": [ + { + "name": "raw", + "macroChannels": [], + "form": { + "form": "gameboy-decoder-config-v1", + "exclusive": { + "channels": [ + { + "channel": "up", + "role": "command_0" + }, + { + "channel": "down", + "role": "command_1" + }, + { + "channel": "left", + "role": "command_2" + }, + { + "channel": "right", + "role": "command_3" + } + ], + "decisionMs": 800.0, + "holdMs": 800.0, + "hysteresis": 1.05, + "fatigueGain": 0.08, + "fatigueDecay": 0.8, + "blockedFatigue": 0.35, + "blockedMs": 800.0 + }, + "macros": null, + "pulses": [ + { + "channel": "a", + "role": "command_4", + "holdMs": 85.0, + "cooldownMs": 480.0, + "threshold": 1.0, + "boot": null, + "throttleGroup": null + }, + { + "channel": "b", + "role": "command_5", + "holdMs": 85.0, + "cooldownMs": 480.0, + "threshold": 1.0, + "boot": null, + "throttleGroup": null + }, + { + "channel": "start", + "role": "command_6", + "holdMs": 55.0, + "cooldownMs": 30000.0, + "threshold": 1.35, + "boot": { + "cooldownMs": 2500.0, + "threshold": 1.0 + }, + "throttleGroup": "system" + }, + { + "channel": "select", + "role": "command_7", + "holdMs": 55.0, + "cooldownMs": 30000.0, + "threshold": 1.35, + "boot": { + "cooldownMs": 2500.0, + "threshold": 1.0 + }, + "throttleGroup": "system" + } + ], + "clearLockoutMs": 480.0 + }, + "canonical": "{\"clearLockoutMs\":480,\"exclusive\":{\"blockedFatigue\":0.35,\"blockedMs\":800,\"channels\":[{\"channel\":\"up\",\"role\":\"command_0\"},{\"channel\":\"down\",\"role\":\"command_1\"},{\"channel\":\"left\",\"role\":\"command_2\"},{\"channel\":\"right\",\"role\":\"command_3\"}],\"decisionMs\":800,\"fatigueDecay\":0.8,\"fatigueGain\":0.08,\"holdMs\":800,\"hysteresis\":1.05},\"form\":\"gameboy-decoder-config-v1\",\"macros\":null,\"pulses\":[{\"boot\":null,\"channel\":\"a\",\"cooldownMs\":480,\"holdMs\":85,\"role\":\"command_4\",\"threshold\":1,\"throttleGroup\":null},{\"boot\":null,\"channel\":\"b\",\"cooldownMs\":480,\"holdMs\":85,\"role\":\"command_5\",\"threshold\":1,\"throttleGroup\":null},{\"boot\":{\"cooldownMs\":2500,\"threshold\":1},\"channel\":\"start\",\"cooldownMs\":30000,\"holdMs\":55,\"role\":\"command_6\",\"threshold\":1.35,\"throttleGroup\":\"system\"},{\"boot\":{\"cooldownMs\":2500,\"threshold\":1},\"channel\":\"select\",\"cooldownMs\":30000,\"holdMs\":55,\"role\":\"command_7\",\"threshold\":1.35,\"throttleGroup\":\"system\"}]}", + "digest": "6234e4a0363cfe82b8d515943c4f645a3960eda8fa5bf9465009061f80d50812" + }, + { + "name": "macros", + "macroChannels": [ + "macro_go_objective", + "macro_go_out", + "macro_go_warp", + "macro_go_route", + "macro_go_item", + "macro_go_npc", + "macro_go_frontier", + "macro_go_shop", + "macro_go_heal", + "macro_talk", + "macro_menu", + "macro_next", + "macro_yes", + "macro_no", + "macro_close", + "macro_confirm", + "macro_back", + "macro_move_1", + "macro_move_2", + "macro_move_3", + "macro_move_4", + "macro_switch", + "macro_item", + "macro_throw_ball", + "macro_run", + "macro_buy_potion", + "macro_buy_ball", + "macro_buy_antidote", + "macro_buy_repel", + "macro_heal", + "macro_leave" + ], + "form": { + "form": "gameboy-decoder-config-v1", + "exclusive": { + "channels": [ + { + "channel": "up", + "role": "command_0" + }, + { + "channel": "down", + "role": "command_1" + }, + { + "channel": "left", + "role": "command_2" + }, + { + "channel": "right", + "role": "command_3" + } + ], + "decisionMs": 800.0, + "holdMs": 800.0, + "hysteresis": 1.05, + "fatigueGain": 0.08, + "fatigueDecay": 0.8, + "blockedFatigue": 0.35, + "blockedMs": 800.0 + }, + "macros": { + "channels": [ + { + "channel": "macro_go_objective", + "role": "macro_go_objective" + }, + { + "channel": "macro_go_out", + "role": "macro_go_out" + }, + { + "channel": "macro_go_warp", + "role": "macro_go_warp" + }, + { + "channel": "macro_go_route", + "role": "macro_go_route" + }, + { + "channel": "macro_go_item", + "role": "macro_go_item" + }, + { + "channel": "macro_go_npc", + "role": "macro_go_npc" + }, + { + "channel": "macro_go_frontier", + "role": "macro_go_frontier" + }, + { + "channel": "macro_go_shop", + "role": "macro_go_shop" + }, + { + "channel": "macro_go_heal", + "role": "macro_go_heal" + }, + { + "channel": "macro_talk", + "role": "macro_talk" + }, + { + "channel": "macro_menu", + "role": "macro_menu" + }, + { + "channel": "macro_next", + "role": "macro_next" + }, + { + "channel": "macro_yes", + "role": "macro_yes" + }, + { + "channel": "macro_no", + "role": "macro_no" + }, + { + "channel": "macro_close", + "role": "macro_close" + }, + { + "channel": "macro_confirm", + "role": "macro_confirm" + }, + { + "channel": "macro_back", + "role": "macro_back" + }, + { + "channel": "macro_move_1", + "role": "macro_move_1" + }, + { + "channel": "macro_move_2", + "role": "macro_move_2" + }, + { + "channel": "macro_move_3", + "role": "macro_move_3" + }, + { + "channel": "macro_move_4", + "role": "macro_move_4" + }, + { + "channel": "macro_switch", + "role": "macro_switch" + }, + { + "channel": "macro_item", + "role": "macro_item" + }, + { + "channel": "macro_throw_ball", + "role": "macro_throw_ball" + }, + { + "channel": "macro_run", + "role": "macro_run" + }, + { + "channel": "macro_buy_potion", + "role": "macro_buy_potion" + }, + { + "channel": "macro_buy_ball", + "role": "macro_buy_ball" + }, + { + "channel": "macro_buy_antidote", + "role": "macro_buy_antidote" + }, + { + "channel": "macro_buy_repel", + "role": "macro_buy_repel" + }, + { + "channel": "macro_heal", + "role": "macro_heal" + }, + { + "channel": "macro_leave", + "role": "macro_leave" + } + ], + "decisionMs": 800.0, + "holdMs": 800.0, + "hysteresis": 1.05, + "fatigueGain": 0.08, + "fatigueDecay": 0.8, + "blockedFatigue": 0.35, + "blockedMs": 800.0 + }, + "pulses": [ + { + "channel": "a", + "role": "command_4", + "holdMs": 85.0, + "cooldownMs": 480.0, + "threshold": 1.0, + "boot": null, + "throttleGroup": null + }, + { + "channel": "b", + "role": "command_5", + "holdMs": 85.0, + "cooldownMs": 480.0, + "threshold": 1.0, + "boot": null, + "throttleGroup": null + }, + { + "channel": "start", + "role": "command_6", + "holdMs": 55.0, + "cooldownMs": 30000.0, + "threshold": 1.35, + "boot": { + "cooldownMs": 2500.0, + "threshold": 1.0 + }, + "throttleGroup": "system" + }, + { + "channel": "select", + "role": "command_7", + "holdMs": 55.0, + "cooldownMs": 30000.0, + "threshold": 1.35, + "boot": { + "cooldownMs": 2500.0, + "threshold": 1.0 + }, + "throttleGroup": "system" + } + ], + "clearLockoutMs": 480.0 + }, + "canonical": "{\"clearLockoutMs\":480,\"exclusive\":{\"blockedFatigue\":0.35,\"blockedMs\":800,\"channels\":[{\"channel\":\"up\",\"role\":\"command_0\"},{\"channel\":\"down\",\"role\":\"command_1\"},{\"channel\":\"left\",\"role\":\"command_2\"},{\"channel\":\"right\",\"role\":\"command_3\"}],\"decisionMs\":800,\"fatigueDecay\":0.8,\"fatigueGain\":0.08,\"holdMs\":800,\"hysteresis\":1.05},\"form\":\"gameboy-decoder-config-v1\",\"macros\":{\"blockedFatigue\":0.35,\"blockedMs\":800,\"channels\":[{\"channel\":\"macro_go_objective\",\"role\":\"macro_go_objective\"},{\"channel\":\"macro_go_out\",\"role\":\"macro_go_out\"},{\"channel\":\"macro_go_warp\",\"role\":\"macro_go_warp\"},{\"channel\":\"macro_go_route\",\"role\":\"macro_go_route\"},{\"channel\":\"macro_go_item\",\"role\":\"macro_go_item\"},{\"channel\":\"macro_go_npc\",\"role\":\"macro_go_npc\"},{\"channel\":\"macro_go_frontier\",\"role\":\"macro_go_frontier\"},{\"channel\":\"macro_go_shop\",\"role\":\"macro_go_shop\"},{\"channel\":\"macro_go_heal\",\"role\":\"macro_go_heal\"},{\"channel\":\"macro_talk\",\"role\":\"macro_talk\"},{\"channel\":\"macro_menu\",\"role\":\"macro_menu\"},{\"channel\":\"macro_next\",\"role\":\"macro_next\"},{\"channel\":\"macro_yes\",\"role\":\"macro_yes\"},{\"channel\":\"macro_no\",\"role\":\"macro_no\"},{\"channel\":\"macro_close\",\"role\":\"macro_close\"},{\"channel\":\"macro_confirm\",\"role\":\"macro_confirm\"},{\"channel\":\"macro_back\",\"role\":\"macro_back\"},{\"channel\":\"macro_move_1\",\"role\":\"macro_move_1\"},{\"channel\":\"macro_move_2\",\"role\":\"macro_move_2\"},{\"channel\":\"macro_move_3\",\"role\":\"macro_move_3\"},{\"channel\":\"macro_move_4\",\"role\":\"macro_move_4\"},{\"channel\":\"macro_switch\",\"role\":\"macro_switch\"},{\"channel\":\"macro_item\",\"role\":\"macro_item\"},{\"channel\":\"macro_throw_ball\",\"role\":\"macro_throw_ball\"},{\"channel\":\"macro_run\",\"role\":\"macro_run\"},{\"channel\":\"macro_buy_potion\",\"role\":\"macro_buy_potion\"},{\"channel\":\"macro_buy_ball\",\"role\":\"macro_buy_ball\"},{\"channel\":\"macro_buy_antidote\",\"role\":\"macro_buy_antidote\"},{\"channel\":\"macro_buy_repel\",\"role\":\"macro_buy_repel\"},{\"channel\":\"macro_heal\",\"role\":\"macro_heal\"},{\"channel\":\"macro_leave\",\"role\":\"macro_leave\"}],\"decisionMs\":800,\"fatigueDecay\":0.8,\"fatigueGain\":0.08,\"holdMs\":800,\"hysteresis\":1.05},\"pulses\":[{\"boot\":null,\"channel\":\"a\",\"cooldownMs\":480,\"holdMs\":85,\"role\":\"command_4\",\"threshold\":1,\"throttleGroup\":null},{\"boot\":null,\"channel\":\"b\",\"cooldownMs\":480,\"holdMs\":85,\"role\":\"command_5\",\"threshold\":1,\"throttleGroup\":null},{\"boot\":{\"cooldownMs\":2500,\"threshold\":1},\"channel\":\"start\",\"cooldownMs\":30000,\"holdMs\":55,\"role\":\"command_6\",\"threshold\":1.35,\"throttleGroup\":\"system\"},{\"boot\":{\"cooldownMs\":2500,\"threshold\":1},\"channel\":\"select\",\"cooldownMs\":30000,\"holdMs\":55,\"role\":\"command_7\",\"threshold\":1.35,\"throttleGroup\":\"system\"}]}", + "digest": "82b6601f0390b742a67eca44ef935b49e16a73315db70f5644f1cbd21f7c8a52" + } + ] +} diff --git a/services/flysim/crates/fly-session-types/fixtures/gameboy-legacy.json b/services/flysim/crates/fly-session-types/fixtures/gameboy-legacy.json index 7b16fbb..d0023fc 100644 --- a/services/flysim/crates/fly-session-types/fixtures/gameboy-legacy.json +++ b/services/flysim/crates/fly-session-types/fixtures/gameboy-legacy.json @@ -1,6 +1,6 @@ { "description": "The legacy Game Boy composition (legacy-gameboy-v1): registered payload schemas with their SchemaRef digests, the one legacy profile document and its AssetRef, the frame clock, and an example composition declaration with its digest.", - "extensionSetDigest": "7bb9578d89b0a35a2914e699aad86d35ac8859d9ad4367632bd1fdfbd3e942ef", + "extensionSetDigest": "a35823ecf607a218b0c9f9e7ec86585d355cb311f8d7a3ada8aa793e1c9fe47c", "extensionSet": { "contract": "fly-session-types/legacy-gameboy", "version": 1, @@ -187,7 +187,7 @@ "name": "decoderConfigDigest", "kind": "Digest", "required": true, - "constraint": "SHA-256 of the canonical JSON of the effective DecoderConfig (TypeScript shape)" + "constraint": "SHA-256 of the canonical gameboy-decoder-config-v1 form of the effective decoder configuration" }, { "name": "environment", @@ -497,12 +497,39 @@ "mode": "macros", "macroChannels": [ "macro_go_objective", + "macro_go_out", + "macro_go_warp", + "macro_go_route", + "macro_go_item", + "macro_go_npc", + "macro_go_frontier", + "macro_go_shop", + "macro_go_heal", "macro_talk", + "macro_menu", "macro_next", - "macro_move_1" + "macro_yes", + "macro_no", + "macro_close", + "macro_confirm", + "macro_back", + "macro_move_1", + "macro_move_2", + "macro_move_3", + "macro_move_4", + "macro_switch", + "macro_item", + "macro_throw_ball", + "macro_run", + "macro_buy_potion", + "macro_buy_ball", + "macro_buy_antidote", + "macro_buy_repel", + "macro_heal", + "macro_leave" ] }, - "decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854", + "decoderConfigDigest": "82b6601f0390b742a67eca44ef935b49e16a73315db70f5644f1cbd21f7c8a52", "environment": { "extensions": [ "gameboy-slots-v1" @@ -535,7 +562,7 @@ "checkpointFormatOfRecord": "FLYSIM01", "flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu" }, - "digest": "f0142c7f09a1319453b472cdddbc1855062af5733f89d1fbc0b82c0adb52b0c7", + "digest": "77d8a88ff7fea51eb29b1ae75fc9cf8c17c184e6a1e5a585e86399f41fd9c9b0", "recipeLines": [ "fly-session/composition-v1", "session=", diff --git a/services/flysim/crates/fly-session-types/src/gameboy.rs b/services/flysim/crates/fly-session-types/src/gameboy.rs index 5e2d630..29e88f8 100644 --- a/services/flysim/crates/fly-session-types/src/gameboy.rs +++ b/services/flysim/crates/fly-session-types/src/gameboy.rs @@ -328,7 +328,7 @@ pub const DECLARATIONS: &[TypeSchema] = &[ req( "decoderConfigDigest", "Digest", - "SHA-256 of the canonical JSON of the effective DecoderConfig (TypeScript shape)", + "SHA-256 of the canonical gameboy-decoder-config-v1 form of the effective decoder configuration", ), req( "environment", diff --git a/services/flysim/crates/flysim/tests/legacy_profile_identity.rs b/services/flysim/crates/flysim/tests/legacy_profile_identity.rs index 14f58cf..e397bbd 100644 --- a/services/flysim/crates/flysim/tests/legacy_profile_identity.rs +++ b/services/flysim/crates/flysim/tests/legacy_profile_identity.rs @@ -9,9 +9,12 @@ use std::sync::Arc; use fly_session_types::gameboy; use fly_session_types::scalar::RationalNs; +use fly_session_types::{canonical, fixtures}; use flybrain_core::agent::{AgentConfig, DEFAULT_WARMUP_MS, GAMEBOY_MS_PER_FRAME, NeuralAgent}; use flybrain_core::dataset::load_brain_dataset_from_dir; use flybrain_core::decoder::gameboy::{GAMEBOY_BUTTON_BITS, gameboy_decoder_config_with_macros}; +use flybrain_core::decoder::{DecoderConfig, ExclusiveGroup}; +use serde_json::{Value, json}; #[test] fn the_profile_fingerprint_and_versions_are_the_ones_this_build_computes() { @@ -56,3 +59,86 @@ fn the_profile_clock_warmup_and_joypad_are_the_service_defaults() { assert_eq!(*bit, 1 << index, "bit i is GAMEBOY_BUTTONS[i]"); } } + +fn group_form(group: Option<&ExclusiveGroup>) -> Value { + match group { + None => Value::Null, + Some(g) => json!({ + "channels": g.channels.iter() + .map(|(channel, role)| json!({"channel": channel, "role": role})) + .collect::>(), + "decisionMs": g.decision_ms, + "holdMs": g.hold_ms, + "hysteresis": g.hysteresis, + "fatigueGain": g.fatigue_gain, + "fatigueDecay": g.fatigue_decay, + "blockedFatigue": g.blocked_fatigue, + "blockedMs": g.blocked_ms, + }), + } +} + +/// The canonical decoder-configuration form of legacy-gameboy-v1 section 12, built from the +/// Rust twin. `@flybrain/session-types` builds the same value from the TypeScript oracle's +/// preset (`decoderConfigForm`), and both are held to `fixtures/gameboy-decoder-config.json`. +fn decoder_config_form(config: &DecoderConfig) -> Value { + json!({ + "form": "gameboy-decoder-config-v1", + "exclusive": group_form(config.exclusive.as_ref()), + "macros": group_form(config.macros.as_ref()), + "pulses": config.pulses.iter().map(|p| json!({ + "channel": p.channel, + "role": p.role, + "holdMs": p.hold_ms, + "cooldownMs": p.cooldown_ms, + "threshold": p.threshold, + "boot": p.boot.map_or(Value::Null, |b| json!({"cooldownMs": b.cooldown_ms, "threshold": b.threshold})), + "throttleGroup": p.throttle_group.clone().map_or(Value::Null, Value::String), + })).collect::>(), + "clearLockoutMs": config.clear_lockout_ms, + }) +} + +/// The shared `decoderConfigDigest` vectors: raw mode and the Pokemon Red macro group, from +/// `gameboy_decoder_config_with_macros`. `FLY_UPDATE_FIXTURES=1` rewrites the file; otherwise +/// the checked-in values must be exactly what this build computes. +#[test] +fn the_decoder_config_digest_vectors_are_what_the_rust_preset_computes() { + let pokered: Vec<&str> = flybrain_gb::macro_channels("pokemon-red"); + let cases: Vec = [("raw", Vec::new()), ("macros", pokered)] + .into_iter() + .map(|(name, channels)| { + let form = decoder_config_form(&gameboy_decoder_config_with_macros(&channels)); + json!({ + "name": name, + "macroChannels": channels, + "form": form, + "canonical": canonical::canonicalize(&form).expect("canonical"), + "digest": canonical::digest_of(&form).expect("digest"), + }) + }) + .collect(); + let file = json!({ + "description": "decoderConfigDigest vectors (legacy-gameboy-v1 section 12): the canonical form of gameboy_decoder_config_with_macros / gameboyDecoderConfig for raw mode and the Pokemon Red macro group. Written by FLY_UPDATE_FIXTURES=1 cargo test -p flysim --test legacy_profile_identity; both languages must reproduce every form and digest.", + "cases": cases, + }); + let mut text = serde_json::to_string_pretty(&file).expect("json"); + text.push('\n'); + let path = fixtures::dir().join("gameboy-decoder-config.json"); + if std::env::var_os("FLY_UPDATE_FIXTURES").is_some() { + std::fs::write(&path, &text).expect("write the fixture"); + } + let found = std::fs::read_to_string(&path).expect("the checked-in fixture"); + assert_eq!( + found, text, + "gameboy-decoder-config.json is stale; rerun with FLY_UPDATE_FIXTURES=1" + ); + // Channel order is identity: reversing the macro group moves the digest. + let mut reversed: Vec<&str> = flybrain_gb::macro_channels("pokemon-red"); + reversed.reverse(); + let other = canonical::digest_of(&decoder_config_form(&gameboy_decoder_config_with_macros( + &reversed, + ))) + .expect("digest"); + assert_ne!(Some(other.as_str()), cases[1]["digest"].as_str()); +} From 3c4bb30bed5c5a1b1bd4e4aae0d3de06bbff612b Mon Sep 17 00:00:00 2001 From: acamilo Date: Wed, 23 Sep 2026 09:00:12 +0000 Subject: [PATCH 5/5] docs: review round 1 on the port contracts B1: a slot save due at a boundary completes before any State.Capture or FLYSIM01 export there; the ported milestone archive holds the post-capture ratchet and slot where legacy holds the pre-capture ones, declared in legacy-gameboy-v1 sections 4 and 16 and step-v1 sections 3 and 6. N1 the exact restore transient (blocked window from 0 ms, restored winner reported blocked on the first decode); N2 the decoder-config form; N3 worker status and lost-reply resolution for the extension methods; N4 boundary actions and captures in the step-v1 section 8 trace; N5 a warm-up override is another profile; N6 sugar refused until the first commit after a restore. --- .../session-framework/legacy-gameboy-v1.md | 108 ++++++++++++++---- docs/design/session-framework/step-v1.md | 24 +++- 2 files changed, 110 insertions(+), 22 deletions(-) diff --git a/docs/design/session-framework/legacy-gameboy-v1.md b/docs/design/session-framework/legacy-gameboy-v1.md index a41d36f..a6e5391 100644 --- a/docs/design/session-framework/legacy-gameboy-v1.md +++ b/docs/design/session-framework/legacy-gameboy-v1.md @@ -54,7 +54,7 @@ bytes. The current values are in `fixtures/gameboy-legacy.json`: digest `41e5d1a | `datasetFingerprint` | Today's value, byte for byte the compatibility string's segment 2 | This is the "embeds today's schema-1 fingerprint" of the decision. `flysim`'s `legacy_profile_identity` test recomputes it from `data/fafb-v783` | | `kernelVersion`, `plasticityVersion` | `lif-1ms-f64-v2`, `fly-kc-mbon-rstdp-v2` | Pinned defaults (CLAUDE.md). The same test compares them with the built network | | `tickDuration` | `1000000/1` ns | One model tick | -| `warmupMs` | `2500` | Fresh-start warm-up with learning disabled, `DEFAULT_WARMUP_MS` | +| `warmupMs` | `2500` | Fresh-start warm-up with learning disabled, `DEFAULT_WARMUP_MS`. A service configured with another warm-up (`loop.warmup_ms`, `FLYSIM_LOOP_WARMUP_MS`) is **another profile** and needs its own id; the legacy composition refuses to start this profile with any other value. It matters only on a fresh start, because a restore never warms up, but it is identity all the same | | `view` | `lcd`, 160 x 144 | The retina's native frame | | `supportedStimuli` | `["reward-pulse"]` | Sugar and task reward events both drive `stimulate(durationMs)` | | `readoutContextSchema`, `decisionSchema` | The registered references of sections 5 and 6 | | @@ -97,7 +97,16 @@ The legacy `Sim::step_frame` order maps onto the transaction phases one to one: | `stimulate` per event, `reinforce(sum)` | Phase D: `Agent.Commit` installs the input, then the stimulations in event order, then one reinforcement | | `MacroLayer::observe`, `location()` | Phase C: this produces the next context's `bound` and `location` | | Ratchet observe, capture, recover | Phase C decides. `Environment.SaveSlot` and the rollback run at `Ready(k+1)` (section 11) | -| Milestone archive | A durable save at `Ready(k+1)`, exported as FLYSIM01 (section 16) | +| Milestone archive | A durable save at `Ready(k+1)`, exported as FLYSIM01, **after** that boundary's slot save (section 16) | + +**Amended 2026-09-23, review round 1.** One order differs from the legacy loop and is declared. +Legacy `track_rank` archives the milestone *before* the ratchet captures, in the same frame, so a +legacy archive holds the pre-capture ratchet (`best` = the previous rung) with the previous +snapshot. Here the ratchet ledger commits `best = r` in Phase C, and the slot is only filled by +`Environment.SaveSlot` at `Ready(k+1)`. A capture ordered before that save would pair `best = r` +with the previous slot's contents -- or an empty slot on the first climb -- on every rank climb. +Section 16 therefore orders the save first, and a ported archive holds the **post-capture** +ratchet and slot. Both are internally consistent; they are not the same bytes. The input installed at Commit and ticked at the next Prepare is the frame the legacy loop hands `set_visual_frame` before it samples rewards. Rewards are sampled from the frame just @@ -202,7 +211,8 @@ interface GameboyMemoryInspection { replaces the slot. A restore imports the state, releases the buttons and returns the archived framebuffer as a fresh view artifact, with a memory image read after the import. It runs no frame. Every slot is part of the environment's `State.Capture` payload, as - FLYSIM01's `ratchet_game` and `ratchet_frame` are today. + FLYSIM01's `ratchet_game` and `ratchet_frame` are today. A slot save due at a boundary + completes before any `State.Capture` or FLYSIM01 export at that boundary (section 16). ## 10. Executor `pokered-macros-v1` @@ -249,7 +259,21 @@ before the next Prepare, without pausing**: tick**, no reinforcement, no stimulation and no calibration. 5. With every reply in hand, the session is `Ready(e', k+1)`. Any failure fails the epoch and the group restores from the last durable checkpoint. No participant resets alone. -6. A durable save follows, as the legacy loop checkpoints after a recovery. +6. A durable save follows, as the legacy loop checkpoints after a recovery. Any capture at this + boundary -- before or after the rollback -- is taken after step 1's slot save. + +**Worker status and lost replies (amended 2026-09-23, review round 1).** While it executes +`Environment.SaveSlot` a worker's `Worker.Status` reports `capturing`; while it executes +`Environment.RestoreSlot` or `Agent.Rollback` it reports `restoring`, with `currentScope` still +the prior `(e, k+1)`. After the reply it reports `ready` at `(e', k+1)`. These are mutations +under the [session RPC](ipc-v1.md) section 5 operation key `(session, e', k+1, method, worker)` +(`SaveSlot`: `(session, e, k+1, …)`), so a lost reply goes through ipc-v1 section 6 first: stop +dispatch, query `Worker.Status` or retransmit the same request id and body to the same +incarnation, and resolve only the matching cached result. Only an unresolvable outcome -- a +changed incarnation, lost routes or ownership, `RESULT_EXPIRED` -- fails the epoch, and then the +group restores from the last durable checkpoint. No participant is ever left in `e'` while +another continues in `e`: the coordinator issues no Prepare until every rollback reply is +resolved. The following continue through the rollback: brain clock, membrane, RNG, learned gains, rates and reward history, the ratchet ledger (its attempt and lifetime budgets were spent in Phase C), and @@ -266,7 +290,7 @@ interface LegacyGameboyComposition { profile: AssetRef; // the section 2 document executor: { id: "pokered-macros-v1"; rom: AssetRef; adapter: Id; symbolProvenance: string; mode: "raw" | "macros"; macroChannels: ChannelName[] }; // [] iff raw - decoderConfigDigest: Digest; // SHA-256 of the canonical effective DecoderConfig + decoderConfigDigest: Digest; // SHA-256 of the gameboy-decoder-config-v1 form environment: { extensions: ["gameboy-slots-v1"]; slots: Id[]; stepDuration: RationalNs; inspectionSchema: SchemaRef; controllerSchema: SchemaRef; setupFrames: 1; audio: { sampleRate: number; channels: 2 } }; @@ -277,8 +301,18 @@ interface LegacyGameboyComposition { } ``` -- `decoderConfigDigest` is taken over the canonical JSON of the effective `DecoderConfig` in the - TypeScript oracle's shape (the Game Boy preset with its macro group). A change to a decoder +- `decoderConfigDigest` is the SHA-256 of the canonical JSON of the form + `gameboy-decoder-config-v1` of the effective decoder configuration (amended 2026-09-23, + review round 1): `{form, exclusive, macros, pulses, clearLockoutMs}`, each group + `{channels: [{channel, role}], decisionMs, holdMs, hysteresis, fatigueGain, fatigueDecay, + blockedFatigue, blockedMs}` or `null`, each pulse `{channel, role, holdMs, cooldownMs, + threshold, boot: {cooldownMs, threshold} | null, throttleGroup: string | null}`. Channels are + an array because their order breaks argmax ties and canonical JSON sorts object keys. The + shared vectors are `fixtures/gameboy-decoder-config.json` (raw mode and the 31-channel + Pokémon Red group): `flysim`'s `legacy_profile_identity` test computes them from + `gameboy_decoder_config_with_macros` (and rewrites them under `FLY_UPDATE_FIXTURES=1`), and + `@flybrain/session-types` reproduces them from the oracle's `gameboyDecoderConfig`. The + example composition carries the real macros-mode digest. A change to a decoder timing, a threshold or the macro channel set therefore changes the composition digest. None of those changes touches the legacy compatibility string, which never covered them. - The declaration's digest is the SHA-256 of its canonical JSON. The coordinator's @@ -299,6 +333,8 @@ interface LegacyGameboyComposition { | `fly-session-types/src/gameboy.rs`, `packages/session-types/src/gameboy.ts` | The five registered payload schemas, the profile, the composition declaration and their readers and cross-checks | | `fly-session-types/src/extensions.rs`, `packages/session-types/src/extensions.ts` | `SaveSlot*`, `RestoreSlot*` and `AgentRollback*` payloads, which are in the session schema set | | `fixtures/gameboy-legacy.json` (derived) | The extension set and its digest, every `SchemaRef`, the profile document with its canonical bytes and `AssetRef`, the frame clock, and an example composition with its digest and the digest recipe | +| `fixtures/gameboy-decoder-config.json` | The `decoderConfigDigest` vectors (raw and Pokémon Red macros), written and checked by `flysim`'s `legacy_profile_identity` test, reproduced by the TypeScript oracle | +| `TraceBehaviour.boundaryActions`, `TraceOperational.captures` | The section 16 order rule, refused by `TransitionTrace` validation in both languages | | `fixtures/valid.json`, `invalid.json` | Accepted and refused cases for every new type, held to both languages | | `flysim/tests/legacy_profile_identity.rs` | Recomputes the fingerprint, versions, frame size, warm-up, clock and button order from the committed dataset and the service defaults | @@ -324,8 +360,16 @@ counter, remainder, buttons and event watermark. The rest starts cleared, as it process: - the executor's ledgers (blocked, reached, talked, errand) are empty, with no macro running; -- the agent's readout transient is cleared: no held channel, and no last location (the first - observed location starts the blocked window); +- the agent's readout transient starts exactly as a fresh legacy process has it (amended + 2026-09-23, review round 1): no held channel, no last location, and the blocked window + starting at brain time **0 ms**, not at the restored clock. The decoder state itself + (`DecoderState`: holds, winners, fatigue) *is* restored. The consequence AGENT-01 must + reproduce: on the first decode after a restore, `now - 0 >= blockedMs`, so the restored + direction winner, if any, is passed as `blocked` and its fatigue is raised to + `blockedFatigue`. Only after that decode does the window restart, because the held channel + changed from none to the winner, and again when the first location is observed. A rollback + (section 11) differs: it clears the holds and winners and restarts the window at the + current brain time; - the adapter's transient observations are cleared, as they are on a rollback. Resume tests compare against the legacy restore outcome, not against an uninterrupted trace @@ -336,20 +380,44 @@ the rung. ## 15. Sugar admission The coordinator owns admission, with the legacy rules: the per-minute limiter, and "no overlap -with an active pulse". The pulse is read from `AgentTelemetry.stimulusRemainingMs` of the **last completed -commit** (an `Agent.Rollback` reply counts as one). The value can therefore be one commit old, and the operator accepted this lag. In one direction a -pulse that ended inside the in-flight transition reads as still running, and the request is refused and retried. -In the other direction a reward pulse added by the in-flight transition is not yet visible, so a sugar can be -admitted over it, where the legacy loop would have refused. Each case is bounded to one frame. After a restore, and before the first commit, the pulse is unknown and admission -refuses with a retry. The duration is clamped to `[1, sugar_max_ms]`. An admitted sugar is a -`reward-pulse` `Stimulus` in the next Prepare's `preStepStimulations`, which is the position of the legacy -drain at the top of a frame. Legacy admission *is* application. Here, the epoch can fail between the two, so each -admission record carries its interaction id. If the Prepare that applies it never commits, the -admission is reported aborted and the edge refunds it. The bridge's fulfil path and refund path both get tests -in the slice that wires them ([workers-v1](workers-v1.md) section 5 amendment). +with an active pulse". The pulse is read from `AgentTelemetry.stimulusRemainingMs` of the **last +completed commit** (an `Agent.Rollback` reply counts as one). The value can therefore be one +commit old, and the operator accepted this lag. The consequences, each bounded to one frame: + +- a pulse that ended inside the in-flight transition still reads as running, so the request is + refused and retried; +- a reward pulse added by the in-flight transition is not yet visible, so a sugar can be + admitted over it where the legacy loop would have refused; +- after a restore, and until the first commit of the new epoch, the pulse is unknown and every + request is refused with a retry, where the legacy loop admits against the restored pulse at + once (amended 2026-09-23, review round 1). + +The duration is clamped to `[1, sugar_max_ms]`. An admitted sugar is a `reward-pulse` +`Stimulus` in the next Prepare's `preStepStimulations`, which is the position of the legacy +drain at the top of a frame. Legacy admission *is* application. Here, the epoch can fail +between the two, so each admission record carries its interaction id. If the Prepare that +applies it never commits, the admission is reported aborted and the edge refunds it. The +bridge's fulfil path and refund path both get tests in the slice that wires them +([workers-v1](workers-v1.md) section 5 amendment). ## 16. Checkpoint format of record +**Order at a boundary (amended 2026-09-23, review round 1).** A slot save due at `Ready(k)` +completes -- its `Environment.SaveSlot` reply in hand -- before any `State.Capture` or FLYSIM01 +export at `Ready(k)`, whether that export is periodic, a milestone archive or the post-rollback +save. So a checkpoint whose ratchet ledger names `best = r` always carries the slot saved for +rung `r`. This is a **declared difference** from the legacy loop, which archives a milestone +before capturing the ratchet snapshot. A legacy milestone archive holds `best = r-1` and the +rung `r-1` snapshot; a ported one holds `best = r` and the rung `r` snapshot. After +`fly-reset-to-milestone` onto a ported archive, a stall rollback therefore returns to the +milestone boundary itself rather than to the previous rung's save, and the attempt counter +starts at the new rung. The operator's confirmation of this difference is requested with the +CUT-01 shadow run. The rule is machine-checked in the step trace: `TraceBehaviour.boundaryActions` +records the saves and the rollback in order, `TraceOperational.captures` records each capture +with the number of boundary actions before it, and a `TransitionTrace` in which a capture +precedes a slot save is refused ([step-v1](step-v1.md) section 8 amendment; fixtures in +`valid.json` and `invalid.json`). + FLYSIM01 stays the format of record until RETIRE-01. Every durable save of this composition (periodic, milestone archive, after a rollback) **exports a FLYSIM01 envelope** that the current `flysim` reads, under the unchanged compatibility string. The deploy gate diff --git a/docs/design/session-framework/step-v1.md b/docs/design/session-framework/step-v1.md index d9edf2f..bf8d167 100644 --- a/docs/design/session-framework/step-v1.md +++ b/docs/design/session-framework/step-v1.md @@ -126,7 +126,9 @@ this transition. No task output directly writes controllers or neural state. boundary this transition reaches, after Phase D, never inside it: a slot save (`Environment.SaveSlot`, composition capability `gameboy-slots-v1`) and a rollback (`episodeRequest.kind = "rollback"`). Both are recorded with the transition's result and -applied by the coordinator in the order *save, then rollback* (section 6 amendment). The +applied by the coordinator in the order *save, then rollback* (section 6 amendment). A slot +save due at a boundary completes before any `State.Capture` or FLYSIM01 export at that boundary +(amended 2026-09-23, review round 1; [legacy-gameboy-v1](legacy-gameboy-v1.md) section 16). The retained old inspection is what makes "evaluate once against old/new inspection" possible when the inspection is artifact-backed: the coordinator keeps `O[k]`'s image until this evaluation finishes. @@ -256,7 +258,12 @@ every Commit of the transition that reached `k` has succeeded: running action and observes `O'[k]`, which yields the next decision contexts. 4. `Agent.Rollback(scope e',k; priorEpoch e)` on every agent concurrently: holds and eligibility cleared, `O'[k]`'s view installed, no tick. -5. With every reply in hand: `Ready(e', k)`, then the usual durable save. +5. With every reply in hand: `Ready(e', k)`, then the usual durable save. Every capture at + this boundary, before or after the rollback, follows step 1. + +A worker reports `capturing` during `SaveSlot` and `restoring` during `RestoreSlot` or +`Agent.Rollback`. A lost reply is resolved by [session RPC](ipc-v1.md) section 6 against the +same operation key before anything fails the epoch (legacy-gameboy-v1 section 11). Exactly what is retained, cleared and installed is named by the policy, as this section already requires; nothing is reset by a worker on its own initiative, and a failure at any step fails @@ -299,6 +306,19 @@ The synthetic integration test must record, for every transition: - Observation producing boundaries and task event/outcome IDs in order. - All Commit acknowledgments and published committed boundary. +**Amendment, 2026-09-23 (RT-01a, review round 1).** For a composition with boundary actions the +record also carries, for the boundary the transition reached: + +- in behaviour, `boundaryActions`: every `Environment.SaveSlot` (slot id and saved state + digest) and a rollback (slot id), in the order applied -- saves first, each slot once, at most + one rollback, last. FND-01's harness compares it like any other behaviour field; +- in operational metadata, `captures`: every checkpoint capture or FLYSIM01 export at that + boundary, in the order taken, each with the number of boundary actions already applied. + Captures are operational because their schedule is wall-clock policy. + +A trace whose capture precedes one of the boundary's slot saves, or counts more actions than +were applied, is refused. The synthetic composition records both lists empty. + Evaluate agents sequentially, concurrently, and in reversed dispatch/completion order. All committed state/action/reward results must match, excluding wall time, request IDs and other explicitly operational metadata. Delayed/lost/duplicate messages must not add a neural tick,