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:
parent
d5d9249ea9
commit
bbf71bfead
30 changed files with 5742 additions and 41 deletions
|
|
@ -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 |
|
| `trace` | The step-v1 section 8 record and the behaviour-only comparator |
|
||||||
| `seed` | `seed-derivation-v1` |
|
| `seed` | `seed-derivation-v1` |
|
||||||
| `checkpoint` | The `FLYSESS1` envelope layout |
|
| `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 |
|
| `fixtures` | Loading the shared corpus |
|
||||||
|
|
||||||
## Reading a payload
|
## Reading a payload
|
||||||
|
|
|
||||||
184
packages/session-types/src/extensions.ts
Normal file
184
packages/session-types/src/extensions.ts
Normal 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`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
488
packages/session-types/src/gameboy.ts
Normal file
488
packages/session-types/src/gameboy.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,8 @@ export * from './workers';
|
||||||
export * from './rpc';
|
export * from './rpc';
|
||||||
export * from './publishing';
|
export * from './publishing';
|
||||||
export * from './trace';
|
export * from './trace';
|
||||||
|
export * from './extensions';
|
||||||
export * as seed from './seed';
|
export * as seed from './seed';
|
||||||
export * as checkpoint from './checkpoint';
|
export * as checkpoint from './checkpoint';
|
||||||
|
export * as gameboy from './gameboy';
|
||||||
export * as fixtures from './fixtures';
|
export * as fixtures from './fixtures';
|
||||||
|
|
|
||||||
|
|
@ -111,6 +111,12 @@ export interface AgentTelemetry {
|
||||||
populationRateHz: number;
|
populationRateHz: number;
|
||||||
rates: { roleId: Id; hz: number }[];
|
rates: { roleId: Id; hz: number }[];
|
||||||
learning: { enabled: boolean; updates: U64; changed: U64; signal: 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 {
|
export function readAssetRef(value: unknown): AssetRef {
|
||||||
|
|
@ -211,6 +217,10 @@ export function readAgentTelemetry(value: unknown): AgentTelemetry {
|
||||||
const reader = new Reader(value, 'AgentTelemetry');
|
const reader = new Reader(value, 'AgentTelemetry');
|
||||||
const brainTicks = reader.u64('brainTicks');
|
const brainTicks = reader.u64('brainTicks');
|
||||||
const populationRateHz = reader.finiteIn('populationRateHz', 0, Number.MAX_VALUE);
|
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 rates = reader.list(('rates'), 0, MAX_RATE_ROLES, (item) => {
|
||||||
const rate = new Reader(item, 'AgentTelemetry.rates');
|
const rate = new Reader(item, 'AgentTelemetry.rates');
|
||||||
const entry = { roleId: rate.id('roleId'), hz: rate.finiteIn('hz', 0, Number.MAX_VALUE) };
|
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)) {
|
if (u64(learning.changed) > u64(learning.updates)) {
|
||||||
fail('AgentTelemetry: learning.changed cannot exceed 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). */
|
/** Rates are in profile-defined order (workers-v1 section 1). */
|
||||||
|
|
@ -848,8 +858,15 @@ export interface TaskEvent {
|
||||||
payload: TypedValue;
|
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 {
|
export interface EpisodeRequest {
|
||||||
kind: 'terminal';
|
kind: EpisodeRequestKind;
|
||||||
reason: Id;
|
reason: Id;
|
||||||
outcome: TypedValue;
|
outcome: TypedValue;
|
||||||
}
|
}
|
||||||
|
|
@ -990,7 +1007,7 @@ export function readTaskEvent(value: unknown): TaskEvent {
|
||||||
export function readEpisodeRequest(value: unknown): EpisodeRequest {
|
export function readEpisodeRequest(value: unknown): EpisodeRequest {
|
||||||
const reader = new Reader(value, 'EpisodeRequest');
|
const reader = new Reader(value, 'EpisodeRequest');
|
||||||
const request: EpisodeRequest = {
|
const request: EpisodeRequest = {
|
||||||
kind: reader.constant('kind', 'terminal'),
|
kind: reader.enumeration('kind', EPISODE_REQUEST_KINDS),
|
||||||
reason: reader.id('reason'),
|
reason: reader.id('reason'),
|
||||||
outcome: readTypedValue(reader.value('outcome')),
|
outcome: readTypedValue(reader.value('outcome')),
|
||||||
};
|
};
|
||||||
|
|
|
||||||
146
packages/session-types/tests/gameboy.test.ts
Normal file
146
packages/session-types/tests/gameboy.test.ts
Normal 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' }));
|
||||||
|
});
|
||||||
|
|
@ -17,6 +17,22 @@ import {
|
||||||
readViewDescriptor,
|
readViewDescriptor,
|
||||||
readViewRef,
|
readViewRef,
|
||||||
} from '../src/media';
|
} 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 { readCommittedSnapshot, readSessionDescriptor } from '../src/publishing';
|
||||||
import {
|
import {
|
||||||
readSessionRpcFailure,
|
readSessionRpcFailure,
|
||||||
|
|
@ -112,6 +128,18 @@ export const READERS: Record<string, (value: unknown) => unknown> = {
|
||||||
TraceBehaviour: readTraceBehaviour,
|
TraceBehaviour: readTraceBehaviour,
|
||||||
TraceOperational: readTraceOperational,
|
TraceOperational: readTraceOperational,
|
||||||
TransitionTrace: readTransitionTrace,
|
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. */
|
/** Reads the value as `typeName` and hands back what the reader reconstructed. */
|
||||||
|
|
|
||||||
|
|
@ -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('maxAudioStreams'), media.MAX_AUDIO_STREAMS);
|
||||||
assert.equal(limits.get('maxTypedValueBytes'), scalar.MAX_TYPED_VALUE_BYTES);
|
assert.equal(limits.get('maxTypedValueBytes'), scalar.MAX_TYPED_VALUE_BYTES);
|
||||||
assert.equal(limits.get('maxEnvelopeBytes'), canonical.MAX_ENVELOPE_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 () => {
|
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('Recovery'), [...workers.RECOVERY]);
|
||||||
assert.deepEqual(enums.get('Determinism'), [...workers.DETERMINISM]);
|
assert.deepEqual(enums.get('Determinism'), [...workers.DETERMINISM]);
|
||||||
assert.deepEqual(enums.get('AxisRange'), [...workers.AXIS_RANGES]);
|
assert.deepEqual(enums.get('AxisRange'), [...workers.AXIS_RANGES]);
|
||||||
|
assert.deepEqual(enums.get('EpisodeRequestKind'), [...workers.EPISODE_REQUEST_KINDS]);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
1
services/flysim/Cargo.lock
generated
1
services/flysim/Cargo.lock
generated
|
|
@ -481,6 +481,7 @@ dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"axum",
|
"axum",
|
||||||
"clap",
|
"clap",
|
||||||
|
"fly-session-types",
|
||||||
"flybrain-core",
|
"flybrain-core",
|
||||||
"flybrain-gb",
|
"flybrain-gb",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,8 @@ file other than its own fixtures. The bus owns the wire
|
||||||
| `schema` | The canonical schema set and `contract_digest()` |
|
| `schema` | The canonical schema set and `contract_digest()` |
|
||||||
| `seed` | `seed-derivation-v1` |
|
| `seed` | `seed-derivation-v1` |
|
||||||
| `checkpoint` | The `FLYSESS1` envelope layout |
|
| `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` |
|
| `fixtures` | Loading `fixtures/`, shared with `packages/session-types` |
|
||||||
|
|
||||||
`Id`, `U64` and `Digest` are the bus encodings: `scalar` calls into `flybus::wire` instead of
|
`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 |
|
| `schema-set.json`, `contract-digest.json` | The canonical schema set and its digest |
|
||||||
| `seed-vectors.json` | `seed-derivation-v1` test vectors |
|
| `seed-vectors.json` | `seed-derivation-v1` test vectors |
|
||||||
| `checkpoint-envelope.json` | One `FLYSESS1` envelope, its layout and the corruptions a reader refuses |
|
| `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
|
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
|
of `valid.json`, the digests in `operations.json`, `seed-vectors.json`,
|
||||||
`checkpoint-envelope.json`) come from
|
`checkpoint-envelope.json` and `gameboy-legacy.json`) come from
|
||||||
`cargo run -p fly-session-types --example update_fixtures`;
|
`cargo run -p fly-session-types --example update_fixtures`;
|
||||||
`tests/schema_set.rs` fails if the checked-in files are stale.
|
`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
|
## 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),
|
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
|
unbounded array cannot fill an envelope, and they are in the digest, so widening one is a
|
||||||
contract change rather than a quiet edit.
|
contract change rather than a quiet edit.
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,9 @@
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
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 fly_session_types::{canonical, checkpoint, fixtures, schema, seed};
|
||||||
use serde_json::{Map, Value, json};
|
use serde_json::{Map, Value, json};
|
||||||
|
|
||||||
|
|
@ -28,9 +30,100 @@ pub fn derived() -> Vec<(String, String)> {
|
||||||
("operations.json".to_owned(), operations()),
|
("operations.json".to_owned(), operations()),
|
||||||
("seed-vectors.json".to_owned(), seed_vectors()),
|
("seed-vectors.json".to_owned(), seed_vectors()),
|
||||||
("checkpoint-envelope.json".to_owned(), checkpoint_envelope()),
|
("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 {
|
fn write(value: &Value) -> String {
|
||||||
let mut text = serde_json::to_string_pretty(value).expect("serializable");
|
let mut text = serde_json::to_string_pretty(value).expect("serializable");
|
||||||
text.push('\n');
|
text.push('\n');
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
{
|
{
|
||||||
"description": "contractDigest is the SHA-256 of the canonical schema set in schema-set.json.",
|
"description": "contractDigest is the SHA-256 of the canonical schema set in schema-set.json.",
|
||||||
"contractDigest": "7f4b11d6737e5097c6889657479527174ddf496e50b60322b281e6c7490bcb4a",
|
"contractDigest": "f61e8f4fa9336184ebb6f0305f9d6135872707ef5536d51b9933df8943653c77",
|
||||||
"schemaSetVersion": 1,
|
"schemaSetVersion": 1,
|
||||||
"schemaSetBytes": 27470,
|
"schemaSetBytes": 30184,
|
||||||
"types": 54,
|
"types": 60,
|
||||||
"enums": 11,
|
"enums": 11,
|
||||||
"limits": 26
|
"limits": 27
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1385,6 +1385,7 @@
|
||||||
"telemetry": {
|
"telemetry": {
|
||||||
"brainTicks": "2534",
|
"brainTicks": "2534",
|
||||||
"populationRateHz": 12.5,
|
"populationRateHz": 12.5,
|
||||||
|
"stimulusRemainingMs": null,
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
"roleId": "kenyon",
|
"roleId": "kenyon",
|
||||||
|
|
@ -1516,6 +1517,7 @@
|
||||||
"telemetry": {
|
"telemetry": {
|
||||||
"brainTicks": "2534",
|
"brainTicks": "2534",
|
||||||
"populationRateHz": 12.5,
|
"populationRateHz": 12.5,
|
||||||
|
"stimulusRemainingMs": null,
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
"roleId": "kenyon",
|
"roleId": "kenyon",
|
||||||
|
|
@ -1647,6 +1649,7 @@
|
||||||
"telemetry": {
|
"telemetry": {
|
||||||
"brainTicks": "2534",
|
"brainTicks": "2534",
|
||||||
"populationRateHz": 12.5,
|
"populationRateHz": 12.5,
|
||||||
|
"stimulusRemainingMs": null,
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
"roleId": "kenyon",
|
"roleId": "kenyon",
|
||||||
|
|
@ -1778,6 +1781,7 @@
|
||||||
"telemetry": {
|
"telemetry": {
|
||||||
"brainTicks": "2534",
|
"brainTicks": "2534",
|
||||||
"populationRateHz": 12.5,
|
"populationRateHz": 12.5,
|
||||||
|
"stimulusRemainingMs": null,
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
"roleId": "mbon",
|
"roleId": "mbon",
|
||||||
|
|
@ -1909,6 +1913,7 @@
|
||||||
"telemetry": {
|
"telemetry": {
|
||||||
"brainTicks": "2534",
|
"brainTicks": "2534",
|
||||||
"populationRateHz": 12.5,
|
"populationRateHz": 12.5,
|
||||||
|
"stimulusRemainingMs": null,
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
"roleId": "kenyon",
|
"roleId": "kenyon",
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
@ -253,6 +253,7 @@
|
||||||
"value": {
|
"value": {
|
||||||
"brainTicks": "17",
|
"brainTicks": "17",
|
||||||
"populationRateHz": 12.5,
|
"populationRateHz": 12.5,
|
||||||
|
"stimulusRemainingMs": null,
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
"roleId": "kenyon",
|
"roleId": "kenyon",
|
||||||
|
|
@ -271,8 +272,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"note": "",
|
"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\"}]}",
|
"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": "de4c4e0a2cc3cac459929480a057495029a74e67e9444ea4e197b403659155ed"
|
"digest": "8b1157faa3b7c12a06622fafdcf69406c5120fa667d0060501d2c6b7b6df308d"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "agent telemetry with no tracked roles",
|
"name": "agent telemetry with no tracked roles",
|
||||||
|
|
@ -280,6 +281,7 @@
|
||||||
"value": {
|
"value": {
|
||||||
"brainTicks": "0",
|
"brainTicks": "0",
|
||||||
"populationRateHz": 0.0,
|
"populationRateHz": 0.0,
|
||||||
|
"stimulusRemainingMs": null,
|
||||||
"rates": [],
|
"rates": [],
|
||||||
"learning": {
|
"learning": {
|
||||||
"enabled": false,
|
"enabled": false,
|
||||||
|
|
@ -289,8 +291,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"note": "",
|
"note": "",
|
||||||
"canonical": "{\"brainTicks\":\"0\",\"learning\":{\"changed\":\"0\",\"enabled\":false,\"signal\":0,\"updates\":\"0\"},\"populationRateHz\":0,\"rates\":[]}",
|
"canonical": "{\"brainTicks\":\"0\",\"learning\":{\"changed\":\"0\",\"enabled\":false,\"signal\":0,\"updates\":\"0\"},\"populationRateHz\":0,\"rates\":[],\"stimulusRemainingMs\":null}",
|
||||||
"digest": "51c684931ea3d7c54f67a78a90d676944d9527b0bc4465d5a6d4e02b87ff7485"
|
"digest": "59bfa4d05fe3f5eb8f2e3bc9361feabb0c5e465735d958b9210c26162b53394d"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "session rpc request with a scope",
|
"name": "session rpc request with a scope",
|
||||||
|
|
@ -454,6 +456,7 @@
|
||||||
"telemetry": {
|
"telemetry": {
|
||||||
"brainTicks": "2500",
|
"brainTicks": "2500",
|
||||||
"populationRateHz": 12.5,
|
"populationRateHz": 12.5,
|
||||||
|
"stimulusRemainingMs": null,
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
"roleId": "kenyon",
|
"roleId": "kenyon",
|
||||||
|
|
@ -486,8 +489,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"note": "",
|
"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\"}",
|
"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": "707803be4867dc2f0f3d4bccedfaa7b97b1e52b9af78d21ae6957caba1a7e3f8"
|
"digest": "372ee03780ef476ef7e89d8b6a48a1c6afe75394b494f6bc80f6a1aa9f805215"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "prepare params",
|
"name": "prepare params",
|
||||||
|
|
@ -687,6 +690,7 @@
|
||||||
"telemetry": {
|
"telemetry": {
|
||||||
"brainTicks": "2534",
|
"brainTicks": "2534",
|
||||||
"populationRateHz": 12.5,
|
"populationRateHz": 12.5,
|
||||||
|
"stimulusRemainingMs": null,
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
"roleId": "kenyon",
|
"roleId": "kenyon",
|
||||||
|
|
@ -706,8 +710,8 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"note": "",
|
"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\"}]}}",
|
"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": "9f0a8812101f8f41a147846f6a94b67ae7898ca143fa3cf44ae3a3d5924cd17c"
|
"digest": "a78233a4bbd20c061cb40f9cf9491060e149379f5d1e6b9036115c0c88014de9"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "controller schema",
|
"name": "controller schema",
|
||||||
|
|
@ -2408,6 +2412,7 @@
|
||||||
"telemetry": {
|
"telemetry": {
|
||||||
"brainTicks": "2500",
|
"brainTicks": "2500",
|
||||||
"populationRateHz": 12.5,
|
"populationRateHz": 12.5,
|
||||||
|
"stimulusRemainingMs": null,
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
"roleId": "kenyon",
|
"roleId": "kenyon",
|
||||||
|
|
@ -2459,8 +2464,8 @@
|
||||||
"eventIds": []
|
"eventIds": []
|
||||||
},
|
},
|
||||||
"note": "",
|
"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\"}}",
|
"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": "e9c432eee00e95c28d8a12428aaa9cb71568bcaf079b557b01fb1fde26723da9"
|
"digest": "8b3ea179376f0dbf44ddbffdc3a93a7751ef03983dfe81df131e415cb8d506a0"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "committed snapshot after a transition",
|
"name": "committed snapshot after a transition",
|
||||||
|
|
@ -2485,6 +2490,7 @@
|
||||||
"telemetry": {
|
"telemetry": {
|
||||||
"brainTicks": "2534",
|
"brainTicks": "2534",
|
||||||
"populationRateHz": 12.5,
|
"populationRateHz": 12.5,
|
||||||
|
"stimulusRemainingMs": null,
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
"roleId": "kenyon",
|
"roleId": "kenyon",
|
||||||
|
|
@ -2608,8 +2614,8 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"note": "",
|
"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\"}}",
|
"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": "d5ed83ccb866318b3cf17b53c8a5b1dea2f404b10649f6b640bf5f2b88a31435"
|
"digest": "aaef320591a2db47d46b3b0a6b3f2faf563ef7edd73b6b02c7604bb7a50c88bf"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "committed snapshot at a boundary installed by a restore",
|
"name": "committed snapshot at a boundary installed by a restore",
|
||||||
|
|
@ -2634,6 +2640,7 @@
|
||||||
"telemetry": {
|
"telemetry": {
|
||||||
"brainTicks": "2534",
|
"brainTicks": "2534",
|
||||||
"populationRateHz": 12.5,
|
"populationRateHz": 12.5,
|
||||||
|
"stimulusRemainingMs": null,
|
||||||
"rates": [
|
"rates": [
|
||||||
{
|
{
|
||||||
"roleId": "kenyon",
|
"roleId": "kenyon",
|
||||||
|
|
@ -2702,8 +2709,8 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"note": "a restore re-establishes a committed boundary this epoch did not run a transition into",
|
"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\"}}",
|
"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": "79a0ef10ed65c0720c34c7d99de1fe87ec2f0c26028994562cc4655b0f6acd1f"
|
"digest": "39fd3d104ee42c6d4cb832c8923415952d8b9f686f0d0f3281a6c5bfea56d622"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "transition trace",
|
"name": "transition trace",
|
||||||
|
|
@ -2891,6 +2898,549 @@
|
||||||
"note": "recorded by step-v1 section 8, excluded from its comparison",
|
"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\"}",
|
"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"
|
"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"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
||||||
414
services/flysim/crates/fly-session-types/src/extensions.rs
Normal file
414
services/flysim/crates/fly-session-types/src/extensions.rs
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
1238
services/flysim/crates/fly-session-types/src/gameboy.rs
Normal file
1238
services/flysim/crates/fly-session-types/src/gameboy.rs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -13,12 +13,16 @@
|
||||||
//! - the documented canonical schema set and `contractDigest` ([`schema`]);
|
//! - the documented canonical schema set and `contractDigest` ([`schema`]);
|
||||||
//! - the trace format of [step-v1] section 8, with behaviour separated from operational
|
//! - the trace format of [step-v1] section 8, with behaviour separated from operational
|
||||||
//! metadata and a comparator over behaviour alone ([`trace`]);
|
//! 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
|
//! - `seed-derivation-v1` ([`seed`]) and the `FLYSESS1` checkpoint envelope layout
|
||||||
//! ([`checkpoint`]), the two specifications CONTRACT-01 has to settle before the real-agent
|
//! ([`checkpoint`]), the two specifications CONTRACT-01 has to settle before the real-agent
|
||||||
//! and store slices.
|
//! and store slices.
|
||||||
//!
|
//!
|
||||||
//! What it is not: a transport, a worker, a coordinator or a store. It holds no Game Boy FFI,
|
//! 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.
|
//! 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`
|
//! 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
|
//! writes the canonical shape, and `validate` re-checks the rules that span fields. Reading
|
||||||
|
|
@ -33,7 +37,9 @@
|
||||||
|
|
||||||
pub mod canonical;
|
pub mod canonical;
|
||||||
pub mod checkpoint;
|
pub mod checkpoint;
|
||||||
|
pub mod extensions;
|
||||||
pub mod fixtures;
|
pub mod fixtures;
|
||||||
|
pub mod gameboy;
|
||||||
pub mod media;
|
pub mod media;
|
||||||
pub mod publishing;
|
pub mod publishing;
|
||||||
pub mod rpc;
|
pub mod rpc;
|
||||||
|
|
|
||||||
|
|
@ -57,7 +57,7 @@ pub struct LimitSchema {
|
||||||
pub source: &'static str,
|
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 {
|
FieldSchema {
|
||||||
name,
|
name,
|
||||||
kind,
|
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 {
|
FieldSchema {
|
||||||
name,
|
name,
|
||||||
kind,
|
kind,
|
||||||
|
|
@ -129,7 +129,7 @@ pub const ENUMS: &[EnumSchema] = &[
|
||||||
EnumSchema {
|
EnumSchema {
|
||||||
name: "EpisodeRequestKind",
|
name: "EpisodeRequestKind",
|
||||||
source: "workers-v1 4",
|
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,
|
value: crate::publishing::MAX_ASSETS as u64,
|
||||||
source: "crate",
|
source: "crate",
|
||||||
},
|
},
|
||||||
|
LimitSchema {
|
||||||
|
name: "maxSlots",
|
||||||
|
value: crate::extensions::MAX_SLOTS as u64,
|
||||||
|
source: "crate",
|
||||||
|
},
|
||||||
LimitSchema {
|
LimitSchema {
|
||||||
name: "maxSnapshotEvents",
|
name: "maxSnapshotEvents",
|
||||||
value: crate::publishing::MAX_SNAPSHOT_EVENTS as u64,
|
value: crate::publishing::MAX_SNAPSHOT_EVENTS as u64,
|
||||||
|
|
@ -413,6 +418,11 @@ pub const SCHEMAS: &[TypeSchema] = &[
|
||||||
"{enabled:bool,updates:U64,changed:U64,signal:number}",
|
"{enabled:bool,updates:U64,changed:U64,signal:number}",
|
||||||
"changed <= updates; signal finite",
|
"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 {
|
TypeSchema {
|
||||||
|
|
@ -744,7 +754,11 @@ pub const SCHEMAS: &[TypeSchema] = &[
|
||||||
name: "EpisodeRequest",
|
name: "EpisodeRequest",
|
||||||
source: "workers-v1 4",
|
source: "workers-v1 4",
|
||||||
fields: &[
|
fields: &[
|
||||||
req("kind", "EpisodeRequestKind", ""),
|
req(
|
||||||
|
"kind",
|
||||||
|
"EpisodeRequestKind",
|
||||||
|
"rollback only under a composition that declares a rollback policy",
|
||||||
|
),
|
||||||
req("reason", "Id", ""),
|
req("reason", "Id", ""),
|
||||||
req("outcome", "TypedValue", ""),
|
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 {
|
TypeSchema {
|
||||||
name: "TransitionTrace",
|
name: "TransitionTrace",
|
||||||
source: "step-v1 8",
|
source: "step-v1 8",
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use serde_json::Value;
|
||||||
|
|
||||||
use crate::media::{AudioDescriptor, MAX_VIEWS, ViewDescriptor, ViewRef, audio_list, view_list};
|
use crate::media::{AudioDescriptor, MAX_VIEWS, ViewDescriptor, ViewRef, audio_list, view_list};
|
||||||
use crate::scalar::{
|
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,
|
constant_true, enumeration, err, finite, finite_in, i32_field, id_list, is_digest, is_id, list,
|
||||||
obj, require_same_order, require_unique, u64_json,
|
obj, require_same_order, require_unique, u64_json,
|
||||||
};
|
};
|
||||||
|
|
@ -505,6 +505,10 @@ pub struct AgentTelemetry {
|
||||||
pub population_rate_hz: f64,
|
pub population_rate_hz: f64,
|
||||||
pub rates: Vec<RateSample>,
|
pub rates: Vec<RateSample>,
|
||||||
pub learning: LearningTelemetry,
|
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 {
|
impl AgentTelemetry {
|
||||||
|
|
@ -526,6 +530,10 @@ impl DomainType for AgentTelemetry {
|
||||||
let mut f = Fields::new(value, "AgentTelemetry")?;
|
let mut f = Fields::new(value, "AgentTelemetry")?;
|
||||||
let brain_ticks = f.u64_string("brainTicks")?;
|
let brain_ticks = f.u64_string("brainTicks")?;
|
||||||
let population_rate_hz = finite_in(&mut f, "populationRateHz", 0.0, f64::MAX)?;
|
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 rates = list(&mut f, "rates", 0, MAX_RATE_ROLES, |v| {
|
||||||
let mut r = Fields::new(v, "AgentTelemetry.rates")?;
|
let mut r = Fields::new(v, "AgentTelemetry.rates")?;
|
||||||
let role_id = r.id("roleId")?;
|
let role_id = r.id("roleId")?;
|
||||||
|
|
@ -554,6 +562,7 @@ impl DomainType for AgentTelemetry {
|
||||||
population_rate_hz,
|
population_rate_hz,
|
||||||
rates,
|
rates,
|
||||||
learning,
|
learning,
|
||||||
|
stimulus_remaining_ms,
|
||||||
};
|
};
|
||||||
t.validate()?;
|
t.validate()?;
|
||||||
Ok(t)
|
Ok(t)
|
||||||
|
|
@ -586,6 +595,10 @@ impl DomainType for AgentTelemetry {
|
||||||
("signal", Value::from(self.learning.signal)),
|
("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() {
|
if !self.learning.signal.is_finite() {
|
||||||
return err("AgentTelemetry: learning.signal must be 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(())
|
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)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct EpisodeRequest {
|
pub struct EpisodeRequest {
|
||||||
|
pub kind: EpisodeRequestKind,
|
||||||
pub reason: String,
|
pub reason: String,
|
||||||
pub outcome: TypedValue,
|
pub outcome: TypedValue,
|
||||||
}
|
}
|
||||||
|
|
@ -2486,18 +2535,26 @@ impl DomainType for EpisodeRequest {
|
||||||
|
|
||||||
fn from_json(value: &Value) -> Result<EpisodeRequest> {
|
fn from_json(value: &Value) -> Result<EpisodeRequest> {
|
||||||
let mut f = Fields::new(value, "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 reason = f.id("reason")?;
|
||||||
let outcome = TypedValue::from_json(f.value("outcome")?)?;
|
let outcome = TypedValue::from_json(f.value("outcome")?)?;
|
||||||
f.finish()?;
|
f.finish()?;
|
||||||
let r = EpisodeRequest { reason, outcome };
|
let r = EpisodeRequest {
|
||||||
|
kind,
|
||||||
|
reason,
|
||||||
|
outcome,
|
||||||
|
};
|
||||||
r.validate()?;
|
r.validate()?;
|
||||||
Ok(r)
|
Ok(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn to_json(&self) -> Value {
|
fn to_json(&self) -> Value {
|
||||||
obj(vec![
|
obj(vec![
|
||||||
("kind", "terminal".into()),
|
("kind", self.kind.as_str().into()),
|
||||||
("reason", self.reason.clone().into()),
|
("reason", self.reason.clone().into()),
|
||||||
("outcome", self.outcome.to_json()),
|
("outcome", self.outcome.to_json()),
|
||||||
])
|
])
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,11 @@
|
||||||
//! One place that knows how to read every type named by a fixture.
|
//! One place that knows how to read every type named by a fixture.
|
||||||
|
|
||||||
use flybus::wire::WireError;
|
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::media::*;
|
||||||
use fly_session_types::publishing::*;
|
use fly_session_types::publishing::*;
|
||||||
use fly_session_types::rpc::*;
|
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!(TraceBehaviour);
|
||||||
arm!(TraceOperational);
|
arm!(TraceOperational);
|
||||||
arm!(TransitionTrace);
|
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!(
|
Err(WireError(format!(
|
||||||
"no fixture reader for type {type_name:?}"
|
"no fixture reader for type {type_name:?}"
|
||||||
)))
|
)))
|
||||||
|
|
@ -130,4 +147,16 @@ pub const READABLE_TYPES: &[&str] = &[
|
||||||
"TraceBehaviour",
|
"TraceBehaviour",
|
||||||
"TraceOperational",
|
"TraceOperational",
|
||||||
"TransitionTrace",
|
"TransitionTrace",
|
||||||
|
"SaveSlotParams",
|
||||||
|
"SaveSlotResult",
|
||||||
|
"RestoreSlotParams",
|
||||||
|
"RestoreSlotResult",
|
||||||
|
"AgentRollbackParams",
|
||||||
|
"AgentRollbackResult",
|
||||||
|
"GameboyReadoutContext",
|
||||||
|
"GameboyChannelsDecision",
|
||||||
|
"GameboyMemoryInspection",
|
||||||
|
"LegacyRatchetRollbackRequest",
|
||||||
|
"LegacyGameboyProfile",
|
||||||
|
"LegacyGameboyComposition",
|
||||||
];
|
];
|
||||||
|
|
|
||||||
350
services/flysim/crates/fly-session-types/tests/gameboy_legacy.rs
Normal file
350
services/flysim/crates/fly-session-types/tests/gameboy_legacy.rs
Normal 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);
|
||||||
|
}
|
||||||
|
|
@ -118,13 +118,33 @@ fn the_schema_set_names_every_type_the_crate_reads() {
|
||||||
.iter()
|
.iter()
|
||||||
.map(|t| t["name"].as_str().expect("name"))
|
.map(|t| t["name"].as_str().expect("name"))
|
||||||
.collect();
|
.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
|
let missing: Vec<&&str> = common::READABLE_TYPES
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|expected| !names.contains(*expected))
|
.filter(|expected| !names.contains(*expected))
|
||||||
|
.filter(|expected| {
|
||||||
|
!fly_session_types::gameboy::EXTENSION_TYPES
|
||||||
|
.iter()
|
||||||
|
.any(|(name, declared)| name == *expected && declared_in_extension(declared))
|
||||||
|
})
|
||||||
.collect();
|
.collect();
|
||||||
assert!(
|
assert!(
|
||||||
missing.is_empty(),
|
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();
|
let mut sorted = names.clone();
|
||||||
sorted.sort_unstable();
|
sorted.sort_unstable();
|
||||||
|
|
@ -173,6 +193,7 @@ fn published_limits_match_the_constants_and_name_their_source() {
|
||||||
"maxAssets",
|
"maxAssets",
|
||||||
"maxAudioStreams",
|
"maxAudioStreams",
|
||||||
"maxCapabilities",
|
"maxCapabilities",
|
||||||
|
"maxSlots",
|
||||||
"maxSnapshotEvents",
|
"maxSnapshotEvents",
|
||||||
"maxSupportedMajors",
|
"maxSupportedMajors",
|
||||||
"maxSupportedStimuli",
|
"maxSupportedStimuli",
|
||||||
|
|
|
||||||
|
|
@ -179,6 +179,9 @@ impl FakeModel {
|
||||||
changed: self.learning_changed,
|
changed: self.learning_changed,
|
||||||
signal: self.last_signal,
|
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,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -406,8 +406,10 @@ impl Task for CounterTask {
|
||||||
Terminal::Counter(target) => new >= target,
|
Terminal::Counter(target) => new >= target,
|
||||||
Terminal::AfterTransitions(n) => self.transitions >= n,
|
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 {
|
let episode = terminal.then(|| EpisodeRequest {
|
||||||
|
kind: EpisodeRequestKind::Terminal,
|
||||||
reason: id("counter-target"),
|
reason: id("counter-target"),
|
||||||
outcome: TypedValue::new(episode_schema(), json!({"counter": new, "transitions": self.transitions}))
|
outcome: TypedValue::new(episode_schema(), json!({"counter": new, "transitions": self.transitions}))
|
||||||
.expect("a synthetic typed value fits the contract"),
|
.expect("a synthetic typed value fits the contract"),
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,8 @@ pub use fly_session_types::workers::{
|
||||||
AcknowledgeParams, AcknowledgeResult, AdvanceParams, AgentCommitResult, AgentInitializeParams,
|
AcknowledgeParams, AcknowledgeResult, AdvanceParams, AgentCommitResult, AgentInitializeParams,
|
||||||
AgentGraph, AgentInitializeResult, AgentTelemetry, AssetRef, AxisRange, AxisSchema, AxisValue, ButtonState,
|
AgentGraph, AgentInitializeResult, AgentTelemetry, AssetRef, AxisRange, AxisSchema, AxisValue, ButtonState,
|
||||||
CommitParams, ControllerSchema, Determinism, EnvironmentDescriptor,
|
CommitParams, ControllerSchema, Determinism, EnvironmentDescriptor,
|
||||||
EnvironmentInitializeParams, EnvironmentInitializeResult, EpisodeRequest, HelloParams,
|
EnvironmentInitializeParams, EnvironmentInitializeResult, EpisodeRequest, EpisodeRequestKind,
|
||||||
|
HelloParams,
|
||||||
HelloResult, LearningTelemetry, MAX_ACKNOWLEDGE, MAX_AGENTS, MAX_PORTS, MAX_RATE_ROLES,
|
HelloResult, LearningTelemetry, MAX_ACKNOWLEDGE, MAX_AGENTS, MAX_PORTS, MAX_RATE_ROLES,
|
||||||
MAX_REWARDS, MAX_STIMULI, PortControl, PortDescriptor, PrepareParams, PreparedDecision,
|
MAX_REWARDS, MAX_STIMULI, PortControl, PortDescriptor, PrepareParams, PreparedDecision,
|
||||||
RateSample, Recovery, Reward, Role, SensoryInput, ShutdownParams, ShutdownResult, StatusResult,
|
RateSample, Recovery, Reward, Role, SensoryInput, ShutdownParams, ShutdownResult, StatusResult,
|
||||||
|
|
|
||||||
|
|
@ -50,6 +50,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
unicode-normalization = "0.1"
|
unicode-normalization = "0.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[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"
|
futures-util = "0.3"
|
||||||
# `default-features = false` drops the remote-reference resolver (and with it reqwest and a
|
# `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.
|
# TLS stack): `packages/feed/src/schema.json` is self-contained, so nothing has to be fetched.
|
||||||
|
|
|
||||||
|
|
@ -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]");
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Reference in a new issue