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]"); + } +}