session types: the legacy Game Boy profile and the RT-01a extension methods

PROF-02a and RT-01a, machine-readable half, per the operator's port decisions of 2026-09-23.

Generic (in the session schema set, so contractDigest moves):
- AgentTelemetry.stimulusRemainingMs (number|null): sugar admission reads the pulse from the
  last commit.
- EpisodeRequest.kind is terminal | rollback.
- Environment.SaveSlot / Environment.RestoreSlot (capability gameboy-slots-v1) and
  Agent.Rollback (capability legacy-ratchet-rollback-v1) payloads, with their scope checks;
  maxSlots 4.

Legacy Game Boy (module gameboy, digested apart from contractDigest):
- registered payload schemas gameboy-readout-context-v1, gameboy-channels-v1,
  gameboy-joypad-v1, gameboy-memory-inspection-v1, legacy-ratchet-rollback-v1, each SchemaRef
  digest over its canonical declaration;
- the one legacy profile gameboy-legacy-fafb-v783-v1 embedding today's schema-1 fingerprint,
  lif-1ms-f64-v2 and fly-kc-mbon-rstdp-v2, with its AssetRef digest;
- the composition declaration carrying the decoder and macro-channel configuration, the
  executor pokered-macros-v1, gameboy-slots-v1, legacy-ratchet-rollback-v1,
  legacy-transient-reset and FLYSIM01 as format of record, cross-checked against the FLYSIM01
  compatibility string.

Fixtures regenerated by update_fixtures (new derived gameboy-legacy.json); valid/invalid cases
for every new type in both languages; the frame clock proven identical to the legacy f64
accumulator. flysim gains only a test (and a dev-dependency) that recomputes the pinned
fingerprint, versions, frame size, warm-up, clock and button order. The synthetic fly-session
agent reports stimulusRemainingMs null and its task names kind terminal; no runtime change.
This commit is contained in:
acamilo 2026-09-23 08:31:58 +00:00
parent d5d9249ea9
commit bbf71bfead
30 changed files with 5742 additions and 41 deletions

View file

@ -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

View file

@ -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`,
);
}
}

View file

@ -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<T>(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;
}

View file

@ -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';

View file

@ -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')),
};

View file

@ -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<string, any>;
test('every registered schema reference is the digest of its declaration, in this language', () => {
const file = legacy();
const declared = new Map<string, unknown>(
(file.extensionSet.payloadSchemas as Record<string, any>[]).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<string, any>[];
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' }));
});

View file

@ -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<string, (value: unknown) => 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. */

View file

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

View file

@ -481,6 +481,7 @@ dependencies = [
"anyhow",
"axum",
"clap",
"fly-session-types",
"flybrain-core",
"flybrain-gb",
"futures-util",

View file

@ -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.

View file

@ -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<String, Value> = 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=<sessionId>",
"epoch=<epoch>",
"contract=<contractDigest>",
"agent=<agentId> port=<portId> profile=<profileDigest> (one line per agent)",
"declaration=<this digest> (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');

View file

@ -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
}

View file

@ -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",

View file

@ -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<ChannelName>",
"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<ChannelName>}",
"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<Id>,slots:array<Id>,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<Id>",
"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<Id>",
"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=<sessionId>",
"epoch=<epoch>",
"contract=<contractDigest>",
"agent=<agentId> port=<portId> profile=<profileDigest> (one line per agent)",
"declaration=<this digest> (added by the 2026-09-23 amendment)"
]
}
}

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

View file

@ -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"
}
]
}

View file

@ -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<SaveSlotParams> {
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<SaveSlotResult> {
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<RestoreSlotParams> {
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<RestoreSlotResult> {
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<AgentRollbackParams> {
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<AgentRollbackResult> {
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()
}
}

File diff suppressed because it is too large Load diff

View file

@ -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;

View file

@ -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",

View file

@ -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<RateSample>,
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<f64>,
}
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<EpisodeRequestKind> {
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<EpisodeRequest> {
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()),
])

View file

@ -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,
arm!(TraceBehaviour);
arm!(TraceOperational);
arm!(TransitionTrace);
arm!(SaveSlotParams);
arm!(SaveSlotResult);
arm!(RestoreSlotParams);
arm!(RestoreSlotResult);
arm!(AgentRollbackParams);
arm!(AgentRollbackResult);
arm!(ReadoutContext);
arm!(ChannelsDecision);
arm!(MemoryInspection);
arm!(RollbackRequest);
arm!(LegacyProfile);
arm!(LegacyComposition);
Err(WireError(format!(
"no fixture reader for type {type_name:?}"
)))
@ -130,4 +147,16 @@ pub const READABLE_TYPES: &[&str] = &[
"TraceBehaviour",
"TraceOperational",
"TransitionTrace",
"SaveSlotParams",
"SaveSlotResult",
"RestoreSlotParams",
"RestoreSlotResult",
"AgentRollbackParams",
"AgentRollbackResult",
"GameboyReadoutContext",
"GameboyChannelsDecision",
"GameboyMemoryInspection",
"LegacyRatchetRollbackRequest",
"LegacyGameboyProfile",
"LegacyGameboyComposition",
];

View file

@ -0,0 +1,350 @@
//! `legacy-gameboy-v1` and the 2026-09-23 extension methods: every digest in
//! `fixtures/gameboy-legacy.json` is what the code computes, and every rule that needs a second
//! value in hand refuses what it must.
use fly_session_types::extensions::*;
use fly_session_types::gameboy::{
self, ChannelsDecision, LegacyComposition, LegacyProfile, Location, MemoryInspection,
ReadoutContext, RollbackRequest,
};
use fly_session_types::scalar::{DomainType, RationalNs, SchemaRef, Scope, TypedValue};
use fly_session_types::workers::{EpisodeRequest, EpisodeRequestKind};
use fly_session_types::{canonical, fixtures};
use serde_json::{Value, json};
fn legacy() -> 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::<Vec<_>>()
.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<String> = ["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);
}

View file

@ -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",

View file

@ -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,
}
}
}

View file

@ -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"),

View file

@ -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,

View file

@ -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.

View file

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