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 |
|
||||
| `seed` | `seed-derivation-v1` |
|
||||
| `checkpoint` | The `FLYSESS1` envelope layout |
|
||||
| `extensions` | `Environment.SaveSlot`/`RestoreSlot` and `Agent.Rollback` payloads (2026-09-23) |
|
||||
| `gameboy` | The legacy Game Boy composition: registered schemas, profile, composition declaration |
|
||||
| `fixtures` | Loading the shared corpus |
|
||||
|
||||
## Reading a payload
|
||||
|
|
|
|||
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 './publishing';
|
||||
export * from './trace';
|
||||
export * from './extensions';
|
||||
export * as seed from './seed';
|
||||
export * as checkpoint from './checkpoint';
|
||||
export * as gameboy from './gameboy';
|
||||
export * as fixtures from './fixtures';
|
||||
|
|
|
|||
|
|
@ -111,6 +111,12 @@ export interface AgentTelemetry {
|
|||
populationRateHz: number;
|
||||
rates: { roleId: Id; hz: number }[];
|
||||
learning: { enabled: boolean; updates: U64; changed: U64; signal: number };
|
||||
/**
|
||||
* Milliseconds of stimulation pulse still running after the operation, or null for an agent
|
||||
* that reports none. Amendment 2026-09-23 (RT-01a): sugar admission reads it from the last
|
||||
* commit (workers-v1 section 5).
|
||||
*/
|
||||
stimulusRemainingMs: number | null;
|
||||
}
|
||||
|
||||
export function readAssetRef(value: unknown): AssetRef {
|
||||
|
|
@ -211,6 +217,10 @@ export function readAgentTelemetry(value: unknown): AgentTelemetry {
|
|||
const reader = new Reader(value, 'AgentTelemetry');
|
||||
const brainTicks = reader.u64('brainTicks');
|
||||
const populationRateHz = reader.finiteIn('populationRateHz', 0, Number.MAX_VALUE);
|
||||
const stimulusRemainingMs =
|
||||
reader.value('stimulusRemainingMs') === null
|
||||
? null
|
||||
: reader.finiteIn('stimulusRemainingMs', 0, Number.MAX_VALUE);
|
||||
const rates = reader.list(('rates'), 0, MAX_RATE_ROLES, (item) => {
|
||||
const rate = new Reader(item, 'AgentTelemetry.rates');
|
||||
const entry = { roleId: rate.id('roleId'), hz: rate.finiteIn('hz', 0, Number.MAX_VALUE) };
|
||||
|
|
@ -233,7 +243,7 @@ export function readAgentTelemetry(value: unknown): AgentTelemetry {
|
|||
if (u64(learning.changed) > u64(learning.updates)) {
|
||||
fail('AgentTelemetry: learning.changed cannot exceed learning.updates');
|
||||
}
|
||||
return { brainTicks, populationRateHz, rates, learning };
|
||||
return { brainTicks, populationRateHz, rates, learning, stimulusRemainingMs };
|
||||
}
|
||||
|
||||
/** Rates are in profile-defined order (workers-v1 section 1). */
|
||||
|
|
@ -848,8 +858,15 @@ export interface TaskEvent {
|
|||
payload: TypedValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* `rollback` is the amendment of 2026-09-23 (RT-01a): only a composition that declares a
|
||||
* rollback policy may act on it.
|
||||
*/
|
||||
export const EPISODE_REQUEST_KINDS = ['terminal', 'rollback'] as const;
|
||||
export type EpisodeRequestKind = (typeof EPISODE_REQUEST_KINDS)[number];
|
||||
|
||||
export interface EpisodeRequest {
|
||||
kind: 'terminal';
|
||||
kind: EpisodeRequestKind;
|
||||
reason: Id;
|
||||
outcome: TypedValue;
|
||||
}
|
||||
|
|
@ -990,7 +1007,7 @@ export function readTaskEvent(value: unknown): TaskEvent {
|
|||
export function readEpisodeRequest(value: unknown): EpisodeRequest {
|
||||
const reader = new Reader(value, 'EpisodeRequest');
|
||||
const request: EpisodeRequest = {
|
||||
kind: reader.constant('kind', 'terminal'),
|
||||
kind: reader.enumeration('kind', EPISODE_REQUEST_KINDS),
|
||||
reason: reader.id('reason'),
|
||||
outcome: readTypedValue(reader.value('outcome')),
|
||||
};
|
||||
|
|
|
|||
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,
|
||||
readViewRef,
|
||||
} from '../src/media';
|
||||
import {
|
||||
readAgentRollbackParams,
|
||||
readAgentRollbackResult,
|
||||
readRestoreSlotParams,
|
||||
readRestoreSlotResult,
|
||||
readSaveSlotParams,
|
||||
readSaveSlotResult,
|
||||
} from '../src/extensions';
|
||||
import {
|
||||
readGameboyChannelsDecision,
|
||||
readGameboyMemoryInspection,
|
||||
readGameboyReadoutContext,
|
||||
readLegacyGameboyComposition,
|
||||
readLegacyGameboyProfile,
|
||||
readLegacyRatchetRollbackRequest,
|
||||
} from '../src/gameboy';
|
||||
import { readCommittedSnapshot, readSessionDescriptor } from '../src/publishing';
|
||||
import {
|
||||
readSessionRpcFailure,
|
||||
|
|
@ -112,6 +128,18 @@ export const READERS: Record<string, (value: unknown) => unknown> = {
|
|||
TraceBehaviour: readTraceBehaviour,
|
||||
TraceOperational: readTraceOperational,
|
||||
TransitionTrace: readTransitionTrace,
|
||||
SaveSlotParams: readSaveSlotParams,
|
||||
SaveSlotResult: readSaveSlotResult,
|
||||
RestoreSlotParams: readRestoreSlotParams,
|
||||
RestoreSlotResult: readRestoreSlotResult,
|
||||
AgentRollbackParams: readAgentRollbackParams,
|
||||
AgentRollbackResult: readAgentRollbackResult,
|
||||
GameboyReadoutContext: readGameboyReadoutContext,
|
||||
GameboyChannelsDecision: readGameboyChannelsDecision,
|
||||
GameboyMemoryInspection: readGameboyMemoryInspection,
|
||||
LegacyRatchetRollbackRequest: readLegacyRatchetRollbackRequest,
|
||||
LegacyGameboyProfile: readLegacyGameboyProfile,
|
||||
LegacyGameboyComposition: readLegacyGameboyComposition,
|
||||
};
|
||||
|
||||
/** Reads the value as `typeName` and hands back what the reader reconstructed. */
|
||||
|
|
|
|||
|
|
@ -76,6 +76,8 @@ test('the schema set publishes the limits this package enforces', async () => {
|
|||
assert.equal(limits.get('maxAudioStreams'), media.MAX_AUDIO_STREAMS);
|
||||
assert.equal(limits.get('maxTypedValueBytes'), scalar.MAX_TYPED_VALUE_BYTES);
|
||||
assert.equal(limits.get('maxEnvelopeBytes'), canonical.MAX_ENVELOPE_BYTES);
|
||||
const extensions = await import('../src/extensions');
|
||||
assert.equal(limits.get('maxSlots'), extensions.MAX_SLOTS);
|
||||
});
|
||||
|
||||
test('the closed enums this package knows are the ones the schema set declares', async () => {
|
||||
|
|
@ -97,4 +99,5 @@ test('the closed enums this package knows are the ones the schema set declares',
|
|||
assert.deepEqual(enums.get('Recovery'), [...workers.RECOVERY]);
|
||||
assert.deepEqual(enums.get('Determinism'), [...workers.DETERMINISM]);
|
||||
assert.deepEqual(enums.get('AxisRange'), [...workers.AXIS_RANGES]);
|
||||
assert.deepEqual(enums.get('EpisodeRequestKind'), [...workers.EPISODE_REQUEST_KINDS]);
|
||||
});
|
||||
|
|
|
|||
1
services/flysim/Cargo.lock
generated
1
services/flysim/Cargo.lock
generated
|
|
@ -481,6 +481,7 @@ dependencies = [
|
|||
"anyhow",
|
||||
"axum",
|
||||
"clap",
|
||||
"fly-session-types",
|
||||
"flybrain-core",
|
||||
"flybrain-gb",
|
||||
"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()` |
|
||||
| `seed` | `seed-derivation-v1` |
|
||||
| `checkpoint` | The `FLYSESS1` envelope layout |
|
||||
| `extensions` | The 2026-09-23 extension methods: `Environment.SaveSlot`/`RestoreSlot` (`gameboy-slots-v1`) and `Agent.Rollback` (`legacy-ratchet-rollback-v1`) |
|
||||
| `gameboy` | The legacy Game Boy composition ([`legacy-gameboy-v1`](../../../../docs/design/session-framework/legacy-gameboy-v1.md)): registered payload schemas, the legacy profile, the composition declaration. Not part of `contractDigest` |
|
||||
| `fixtures` | Loading `fixtures/`, shared with `packages/session-types` |
|
||||
|
||||
`Id`, `U64` and `Digest` are the bus encodings: `scalar` calls into `flybus::wire` instead of
|
||||
|
|
@ -80,10 +82,11 @@ once and holds both languages to it.
|
|||
| `schema-set.json`, `contract-digest.json` | The canonical schema set and its digest |
|
||||
| `seed-vectors.json` | `seed-derivation-v1` test vectors |
|
||||
| `checkpoint-envelope.json` | One `FLYSESS1` envelope, its layout and the corruptions a reader refuses |
|
||||
| `gameboy-legacy.json` | The legacy Game Boy extension set and digest, every registered `SchemaRef`, the legacy profile and its `AssetRef`, the frame clock, an example composition and its digest |
|
||||
|
||||
The derived files (`schema-set.json`, `contract-digest.json`, the `canonical`/`digest` fields
|
||||
of `valid.json`, the digests in `operations.json`, `seed-vectors.json` and
|
||||
`checkpoint-envelope.json`) come from
|
||||
of `valid.json`, the digests in `operations.json`, `seed-vectors.json`,
|
||||
`checkpoint-envelope.json` and `gameboy-legacy.json`) come from
|
||||
`cargo run -p fly-session-types --example update_fixtures`;
|
||||
`tests/schema_set.rs` fails if the checked-in files are stale.
|
||||
|
||||
|
|
@ -96,8 +99,8 @@ cargo clippy -p fly-session-types --all-targets
|
|||
|
||||
## Bounds this crate chose
|
||||
|
||||
Every bound in the schema set names its source. Six are marked `crate` because no document
|
||||
Every bound in the schema set names its source. Seven are marked `crate` because no document
|
||||
states them: `maxAudioStreams` (8), `maxCapabilities` (32), `maxSupportedMajors` (8),
|
||||
`maxSupportedStimuli` (64), `maxAssets` (64) and `maxSnapshotEvents` (64). They exist so an
|
||||
`maxSupportedStimuli` (64), `maxAssets` (64), `maxSnapshotEvents` (64) and `maxSlots` (4). They exist so an
|
||||
unbounded array cannot fill an envelope, and they are in the digest, so widening one is a
|
||||
contract change rather than a quiet edit.
|
||||
|
|
|
|||
|
|
@ -6,7 +6,9 @@
|
|||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use fly_session_types::scalar::{DomainType, Scope};
|
||||
use fly_session_types::gameboy::{self, LegacyComposition};
|
||||
use fly_session_types::scalar::{DomainType, RationalNs, Scope};
|
||||
use fly_session_types::workers::AssetRef;
|
||||
use fly_session_types::{canonical, checkpoint, fixtures, schema, seed};
|
||||
use serde_json::{Map, Value, json};
|
||||
|
||||
|
|
@ -28,9 +30,100 @@ pub fn derived() -> Vec<(String, String)> {
|
|||
("operations.json".to_owned(), operations()),
|
||||
("seed-vectors.json".to_owned(), seed_vectors()),
|
||||
("checkpoint-envelope.json".to_owned(), checkpoint_envelope()),
|
||||
("gameboy-legacy.json".to_owned(), gameboy_legacy()),
|
||||
]
|
||||
}
|
||||
|
||||
/// An example legacy composition. The ROM and decoder digests are placeholders -- the real
|
||||
/// ones are computed by the composition that runs, and no ROM identity belongs in a fixture --
|
||||
/// and the macro channels are a short excerpt of the Pokemon Red set. The compatibility
|
||||
/// string is today's, byte for byte, because its segments must agree with the declaration.
|
||||
pub fn example_composition() -> LegacyComposition {
|
||||
let pokered = "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b";
|
||||
LegacyComposition {
|
||||
composition_id: "pokered-live".to_owned(),
|
||||
profile: gameboy::profile_asset_ref(),
|
||||
executor: gameboy::ExecutorDeclaration {
|
||||
rom: AssetRef {
|
||||
id: "pokered-rom".to_owned(),
|
||||
digest: canonical::sha256_hex(b"placeholder: the cartridge digest is the operator's"),
|
||||
byte_length: 1_048_576,
|
||||
format: "gb-rom".to_owned(),
|
||||
},
|
||||
adapter: "pokered-unique8-v6".to_owned(),
|
||||
symbol_provenance: pokered.to_owned(),
|
||||
mode: "macros".to_owned(),
|
||||
macro_channels: ["macro_go_objective", "macro_talk", "macro_next", "macro_move_1"]
|
||||
.iter()
|
||||
.map(|c| (*c).to_owned())
|
||||
.collect(),
|
||||
},
|
||||
decoder_config_digest: canonical::sha256_hex(b"placeholder: the effective DecoderConfig"),
|
||||
environment: gameboy::EnvironmentDeclaration {
|
||||
slots: vec!["best".to_owned()],
|
||||
audio_sample_rate: 48_000,
|
||||
},
|
||||
flysim_compatibility: format!(
|
||||
"{}/pokered-unique8-v6/{}/{}/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:{pokered}/statefmt:199616-x86_64-unknown-linux-gnu",
|
||||
gameboy::KERNEL_VERSION,
|
||||
gameboy::FAFB_V783_FINGERPRINT,
|
||||
gameboy::PLASTICITY_VERSION,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// The legacy Game Boy extension set, the profile document and its AssetRef, the clock
|
||||
/// vector and an example composition with its digest.
|
||||
fn gameboy_legacy() -> String {
|
||||
let profile = gameboy::legacy_profile().to_json();
|
||||
let schema_refs: Map<String, Value> = gameboy::PAYLOAD_SCHEMAS
|
||||
.iter()
|
||||
.map(|p| (p.id.to_owned(), p.schema_ref().to_json()))
|
||||
.collect();
|
||||
// The first frames from a zero remainder: the rational accumulator of step-v1 section 5,
|
||||
// which the legacy f64 accumulator equals exactly (legacy-gameboy-v1 section 3).
|
||||
let step = gameboy::step_duration();
|
||||
let tick = gameboy::tick_duration();
|
||||
let mut accumulator = RationalNs::ZERO;
|
||||
let mut frames = Vec::new();
|
||||
for _ in 0..12 {
|
||||
accumulator = accumulator.checked_add(&step).expect("no overflow");
|
||||
let (ticks, remainder) = accumulator.divide_floor(&tick).expect("positive tick");
|
||||
accumulator = remainder;
|
||||
frames.push(json!({"ticks": ticks.to_string(), "remainder": remainder.to_json()}));
|
||||
}
|
||||
let composition = example_composition();
|
||||
write(&json!({
|
||||
"description": "The legacy Game Boy composition (legacy-gameboy-v1): registered payload schemas with their SchemaRef digests, the one legacy profile document and its AssetRef, the frame clock, and an example composition declaration with its digest.",
|
||||
"extensionSetDigest": gameboy::extension_set_digest(),
|
||||
"extensionSet": gameboy::extension_set(),
|
||||
"schemaRefs": schema_refs,
|
||||
"profile": {
|
||||
"document": profile,
|
||||
"canonical": canonical::canonicalize(&profile).expect("canonicalizable"),
|
||||
"assetRef": gameboy::profile_asset_ref().to_json(),
|
||||
},
|
||||
"clock": {
|
||||
"stepDuration": step.to_json(),
|
||||
"tickDuration": tick.to_json(),
|
||||
"legacyMsPerFrame": "1000 / (4194304 / 70224) == 548625/32768 exactly",
|
||||
"frames": frames,
|
||||
},
|
||||
"composition": {
|
||||
"example": composition.to_json(),
|
||||
"digest": composition.digest().expect("digest"),
|
||||
"recipeLines": [
|
||||
"fly-session/composition-v1",
|
||||
"session=<sessionId>",
|
||||
"epoch=<epoch>",
|
||||
"contract=<contractDigest>",
|
||||
"agent=<agentId> port=<portId> profile=<profileDigest> (one line per agent)",
|
||||
"declaration=<this digest> (added by the 2026-09-23 amendment)",
|
||||
],
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
fn write(value: &Value) -> String {
|
||||
let mut text = serde_json::to_string_pretty(value).expect("serializable");
|
||||
text.push('\n');
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
{
|
||||
"description": "contractDigest is the SHA-256 of the canonical schema set in schema-set.json.",
|
||||
"contractDigest": "7f4b11d6737e5097c6889657479527174ddf496e50b60322b281e6c7490bcb4a",
|
||||
"contractDigest": "f61e8f4fa9336184ebb6f0305f9d6135872707ef5536d51b9933df8943653c77",
|
||||
"schemaSetVersion": 1,
|
||||
"schemaSetBytes": 27470,
|
||||
"types": 54,
|
||||
"schemaSetBytes": 30184,
|
||||
"types": 60,
|
||||
"enums": 11,
|
||||
"limits": 26
|
||||
"limits": 27
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1385,6 +1385,7 @@
|
|||
"telemetry": {
|
||||
"brainTicks": "2534",
|
||||
"populationRateHz": 12.5,
|
||||
"stimulusRemainingMs": null,
|
||||
"rates": [
|
||||
{
|
||||
"roleId": "kenyon",
|
||||
|
|
@ -1516,6 +1517,7 @@
|
|||
"telemetry": {
|
||||
"brainTicks": "2534",
|
||||
"populationRateHz": 12.5,
|
||||
"stimulusRemainingMs": null,
|
||||
"rates": [
|
||||
{
|
||||
"roleId": "kenyon",
|
||||
|
|
@ -1647,6 +1649,7 @@
|
|||
"telemetry": {
|
||||
"brainTicks": "2534",
|
||||
"populationRateHz": 12.5,
|
||||
"stimulusRemainingMs": null,
|
||||
"rates": [
|
||||
{
|
||||
"roleId": "kenyon",
|
||||
|
|
@ -1778,6 +1781,7 @@
|
|||
"telemetry": {
|
||||
"brainTicks": "2534",
|
||||
"populationRateHz": 12.5,
|
||||
"stimulusRemainingMs": null,
|
||||
"rates": [
|
||||
{
|
||||
"roleId": "mbon",
|
||||
|
|
@ -1909,6 +1913,7 @@
|
|||
"telemetry": {
|
||||
"brainTicks": "2534",
|
||||
"populationRateHz": 12.5,
|
||||
"stimulusRemainingMs": null,
|
||||
"rates": [
|
||||
{
|
||||
"roleId": "kenyon",
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
"brainTicks": "17",
|
||||
"populationRateHz": 12.5,
|
||||
"stimulusRemainingMs": null,
|
||||
"rates": [
|
||||
{
|
||||
"roleId": "kenyon",
|
||||
|
|
@ -271,8 +272,8 @@
|
|||
}
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"brainTicks\":\"17\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}]}",
|
||||
"digest": "de4c4e0a2cc3cac459929480a057495029a74e67e9444ea4e197b403659155ed"
|
||||
"canonical": "{\"brainTicks\":\"17\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}],\"stimulusRemainingMs\":null}",
|
||||
"digest": "8b1157faa3b7c12a06622fafdcf69406c5120fa667d0060501d2c6b7b6df308d"
|
||||
},
|
||||
{
|
||||
"name": "agent telemetry with no tracked roles",
|
||||
|
|
@ -280,6 +281,7 @@
|
|||
"value": {
|
||||
"brainTicks": "0",
|
||||
"populationRateHz": 0.0,
|
||||
"stimulusRemainingMs": null,
|
||||
"rates": [],
|
||||
"learning": {
|
||||
"enabled": false,
|
||||
|
|
@ -289,8 +291,8 @@
|
|||
}
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"brainTicks\":\"0\",\"learning\":{\"changed\":\"0\",\"enabled\":false,\"signal\":0,\"updates\":\"0\"},\"populationRateHz\":0,\"rates\":[]}",
|
||||
"digest": "51c684931ea3d7c54f67a78a90d676944d9527b0bc4465d5a6d4e02b87ff7485"
|
||||
"canonical": "{\"brainTicks\":\"0\",\"learning\":{\"changed\":\"0\",\"enabled\":false,\"signal\":0,\"updates\":\"0\"},\"populationRateHz\":0,\"rates\":[],\"stimulusRemainingMs\":null}",
|
||||
"digest": "59bfa4d05fe3f5eb8f2e3bc9361feabb0c5e465735d958b9210c26162b53394d"
|
||||
},
|
||||
{
|
||||
"name": "session rpc request with a scope",
|
||||
|
|
@ -454,6 +456,7 @@
|
|||
"telemetry": {
|
||||
"brainTicks": "2500",
|
||||
"populationRateHz": 12.5,
|
||||
"stimulusRemainingMs": null,
|
||||
"rates": [
|
||||
{
|
||||
"roleId": "kenyon",
|
||||
|
|
@ -486,8 +489,8 @@
|
|||
}
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"agentId\":\"fly-a\",\"committedStep\":\"0\",\"decisionContextDigest\":\"ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4\",\"graph\":{\"datasetDigest\":\"6c0af1f0784ef63a393ee77d614e8246c625051360f3f1a48838374c5d355b52\",\"indexDigest\":\"52a7a9b441f686ebea398e3d65dbaa87b4fde4f8b2231fff951700548f6a4e42\",\"neuronCount\":\"139255\",\"rateRoles\":[\"kenyon\",\"mbon\"],\"supportedStimuli\":[\"sugar\",\"shock\"]},\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"telemetry\":{\"brainTicks\":\"2500\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}]},\"tickDuration\":{\"denominator\":\"1\",\"numerator\":\"1000000\"},\"warmupTicks\":\"2500\"}",
|
||||
"digest": "707803be4867dc2f0f3d4bccedfaa7b97b1e52b9af78d21ae6957caba1a7e3f8"
|
||||
"canonical": "{\"agentId\":\"fly-a\",\"committedStep\":\"0\",\"decisionContextDigest\":\"ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4\",\"graph\":{\"datasetDigest\":\"6c0af1f0784ef63a393ee77d614e8246c625051360f3f1a48838374c5d355b52\",\"indexDigest\":\"52a7a9b441f686ebea398e3d65dbaa87b4fde4f8b2231fff951700548f6a4e42\",\"neuronCount\":\"139255\",\"rateRoles\":[\"kenyon\",\"mbon\"],\"supportedStimuli\":[\"sugar\",\"shock\"]},\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"telemetry\":{\"brainTicks\":\"2500\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}],\"stimulusRemainingMs\":null},\"tickDuration\":{\"denominator\":\"1\",\"numerator\":\"1000000\"},\"warmupTicks\":\"2500\"}",
|
||||
"digest": "372ee03780ef476ef7e89d8b6a48a1c6afe75394b494f6bc80f6a1aa9f805215"
|
||||
},
|
||||
{
|
||||
"name": "prepare params",
|
||||
|
|
@ -687,6 +690,7 @@
|
|||
"telemetry": {
|
||||
"brainTicks": "2534",
|
||||
"populationRateHz": 12.5,
|
||||
"stimulusRemainingMs": null,
|
||||
"rates": [
|
||||
{
|
||||
"roleId": "kenyon",
|
||||
|
|
@ -706,8 +710,8 @@
|
|||
}
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"agentId\":\"fly-a\",\"committedStep\":\"42\",\"decisionContextDigest\":\"ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4\",\"telemetry\":{\"brainTicks\":\"2534\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}]}}",
|
||||
"digest": "9f0a8812101f8f41a147846f6a94b67ae7898ca143fa3cf44ae3a3d5924cd17c"
|
||||
"canonical": "{\"agentId\":\"fly-a\",\"committedStep\":\"42\",\"decisionContextDigest\":\"ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4\",\"telemetry\":{\"brainTicks\":\"2534\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}],\"stimulusRemainingMs\":null}}",
|
||||
"digest": "a78233a4bbd20c061cb40f9cf9491060e149379f5d1e6b9036115c0c88014de9"
|
||||
},
|
||||
{
|
||||
"name": "controller schema",
|
||||
|
|
@ -2408,6 +2412,7 @@
|
|||
"telemetry": {
|
||||
"brainTicks": "2500",
|
||||
"populationRateHz": 12.5,
|
||||
"stimulusRemainingMs": null,
|
||||
"rates": [
|
||||
{
|
||||
"roleId": "kenyon",
|
||||
|
|
@ -2459,8 +2464,8 @@
|
|||
"eventIds": []
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"agents\":[{\"agentId\":\"fly-a\",\"appliedControls\":null,\"selectedDecision\":null,\"telemetry\":{\"brainTicks\":\"2500\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}]}}],\"descriptorRevision\":\"7\",\"episodeId\":\"episode-1\",\"eventIds\":[],\"media\":{\"audio\":[],\"views\":[{\"pixels\":{\"artifactId\":\"frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"0\",\"viewId\":\"screen\"}]},\"progress\":{\"schema\":{\"digest\":\"80fa973157f334bb3208dfff6f7a2f298e79f73f07c72c33f5725975c6cff8e1\",\"id\":\"pokemon.progress.v1\",\"version\":1},\"value\":{\"rank\":0}},\"publisherIncarnation\":\"pub-1\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"0\"},\"sequence\":\"0\",\"worldTime\":{\"denominator\":\"1\",\"numerator\":\"0\"}}",
|
||||
"digest": "e9c432eee00e95c28d8a12428aaa9cb71568bcaf079b557b01fb1fde26723da9"
|
||||
"canonical": "{\"agents\":[{\"agentId\":\"fly-a\",\"appliedControls\":null,\"selectedDecision\":null,\"telemetry\":{\"brainTicks\":\"2500\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}],\"stimulusRemainingMs\":null}}],\"descriptorRevision\":\"7\",\"episodeId\":\"episode-1\",\"eventIds\":[],\"media\":{\"audio\":[],\"views\":[{\"pixels\":{\"artifactId\":\"frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"0\",\"viewId\":\"screen\"}]},\"progress\":{\"schema\":{\"digest\":\"80fa973157f334bb3208dfff6f7a2f298e79f73f07c72c33f5725975c6cff8e1\",\"id\":\"pokemon.progress.v1\",\"version\":1},\"value\":{\"rank\":0}},\"publisherIncarnation\":\"pub-1\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"0\"},\"sequence\":\"0\",\"worldTime\":{\"denominator\":\"1\",\"numerator\":\"0\"}}",
|
||||
"digest": "8b3ea179376f0dbf44ddbffdc3a93a7751ef03983dfe81df131e415cb8d506a0"
|
||||
},
|
||||
{
|
||||
"name": "committed snapshot after a transition",
|
||||
|
|
@ -2485,6 +2490,7 @@
|
|||
"telemetry": {
|
||||
"brainTicks": "2534",
|
||||
"populationRateHz": 12.5,
|
||||
"stimulusRemainingMs": null,
|
||||
"rates": [
|
||||
{
|
||||
"roleId": "kenyon",
|
||||
|
|
@ -2608,8 +2614,8 @@
|
|||
]
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"agents\":[{\"agentId\":\"fly-a\",\"appliedControls\":{\"axes\":[{\"id\":\"stick-x\",\"value\":0},{\"id\":\"trigger\",\"value\":0}],\"buttons\":[{\"down\":true,\"id\":\"a\"},{\"down\":false,\"id\":\"b\"},{\"down\":false,\"id\":\"start\"},{\"down\":false,\"id\":\"select\"},{\"down\":false,\"id\":\"up\"},{\"down\":false,\"id\":\"down\"},{\"down\":false,\"id\":\"left\"},{\"down\":false,\"id\":\"right\"}],\"portId\":\"port-1\"},\"selectedDecision\":{\"schema\":{\"digest\":\"e70451b26fb5462ec63729d8355912dad01c3963a95bec0e6a8452375717ad9d\",\"id\":\"gameboy.intent.v1\",\"version\":1},\"value\":{\"press\":\"a\"}},\"telemetry\":{\"brainTicks\":\"2534\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}]}}],\"descriptorRevision\":\"7\",\"episodeId\":\"episode-1\",\"eventIds\":[\"evt-1\"],\"media\":{\"audio\":[{\"discontinuity\":false,\"firstSample\":\"33600\",\"sampleFrames\":800,\"samples\":{\"artifactId\":\"audio-1\",\"byteLength\":\"6400\",\"contentType\":\"audio/x-f32le\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"streamId\":\"mix\"}],\"views\":[{\"pixels\":{\"artifactId\":\"frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"42\",\"viewId\":\"screen\"}]},\"progress\":{\"schema\":{\"digest\":\"80fa973157f334bb3208dfff6f7a2f298e79f73f07c72c33f5725975c6cff8e1\",\"id\":\"pokemon.progress.v1\",\"version\":1},\"value\":{\"rank\":10}},\"publisherIncarnation\":\"pub-1\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"42\"},\"sequence\":\"42\",\"worldTime\":{\"denominator\":\"1\",\"numerator\":\"700000000\"}}",
|
||||
"digest": "d5ed83ccb866318b3cf17b53c8a5b1dea2f404b10649f6b640bf5f2b88a31435"
|
||||
"canonical": "{\"agents\":[{\"agentId\":\"fly-a\",\"appliedControls\":{\"axes\":[{\"id\":\"stick-x\",\"value\":0},{\"id\":\"trigger\",\"value\":0}],\"buttons\":[{\"down\":true,\"id\":\"a\"},{\"down\":false,\"id\":\"b\"},{\"down\":false,\"id\":\"start\"},{\"down\":false,\"id\":\"select\"},{\"down\":false,\"id\":\"up\"},{\"down\":false,\"id\":\"down\"},{\"down\":false,\"id\":\"left\"},{\"down\":false,\"id\":\"right\"}],\"portId\":\"port-1\"},\"selectedDecision\":{\"schema\":{\"digest\":\"e70451b26fb5462ec63729d8355912dad01c3963a95bec0e6a8452375717ad9d\",\"id\":\"gameboy.intent.v1\",\"version\":1},\"value\":{\"press\":\"a\"}},\"telemetry\":{\"brainTicks\":\"2534\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}],\"stimulusRemainingMs\":null}}],\"descriptorRevision\":\"7\",\"episodeId\":\"episode-1\",\"eventIds\":[\"evt-1\"],\"media\":{\"audio\":[{\"discontinuity\":false,\"firstSample\":\"33600\",\"sampleFrames\":800,\"samples\":{\"artifactId\":\"audio-1\",\"byteLength\":\"6400\",\"contentType\":\"audio/x-f32le\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"streamId\":\"mix\"}],\"views\":[{\"pixels\":{\"artifactId\":\"frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"42\",\"viewId\":\"screen\"}]},\"progress\":{\"schema\":{\"digest\":\"80fa973157f334bb3208dfff6f7a2f298e79f73f07c72c33f5725975c6cff8e1\",\"id\":\"pokemon.progress.v1\",\"version\":1},\"value\":{\"rank\":10}},\"publisherIncarnation\":\"pub-1\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"42\"},\"sequence\":\"42\",\"worldTime\":{\"denominator\":\"1\",\"numerator\":\"700000000\"}}",
|
||||
"digest": "aaef320591a2db47d46b3b0a6b3f2faf563ef7edd73b6b02c7604bb7a50c88bf"
|
||||
},
|
||||
{
|
||||
"name": "committed snapshot at a boundary installed by a restore",
|
||||
|
|
@ -2634,6 +2640,7 @@
|
|||
"telemetry": {
|
||||
"brainTicks": "2534",
|
||||
"populationRateHz": 12.5,
|
||||
"stimulusRemainingMs": null,
|
||||
"rates": [
|
||||
{
|
||||
"roleId": "kenyon",
|
||||
|
|
@ -2702,8 +2709,8 @@
|
|||
]
|
||||
},
|
||||
"note": "a restore re-establishes a committed boundary this epoch did not run a transition into",
|
||||
"canonical": "{\"agents\":[{\"agentId\":\"fly-a\",\"appliedControls\":null,\"selectedDecision\":null,\"telemetry\":{\"brainTicks\":\"2534\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}]}}],\"descriptorRevision\":\"7\",\"episodeId\":\"episode-1\",\"eventIds\":[\"evt-1\"],\"media\":{\"audio\":[{\"discontinuity\":false,\"firstSample\":\"33600\",\"sampleFrames\":800,\"samples\":{\"artifactId\":\"audio-1\",\"byteLength\":\"6400\",\"contentType\":\"audio/x-f32le\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"streamId\":\"mix\"}],\"views\":[{\"pixels\":{\"artifactId\":\"frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"42\",\"viewId\":\"screen\"}]},\"progress\":{\"schema\":{\"digest\":\"80fa973157f334bb3208dfff6f7a2f298e79f73f07c72c33f5725975c6cff8e1\",\"id\":\"pokemon.progress.v1\",\"version\":1},\"value\":{\"rank\":10}},\"publisherIncarnation\":\"pub-1\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"42\"},\"sequence\":\"42\",\"worldTime\":{\"denominator\":\"1\",\"numerator\":\"700000000\"}}",
|
||||
"digest": "79a0ef10ed65c0720c34c7d99de1fe87ec2f0c26028994562cc4655b0f6acd1f"
|
||||
"canonical": "{\"agents\":[{\"agentId\":\"fly-a\",\"appliedControls\":null,\"selectedDecision\":null,\"telemetry\":{\"brainTicks\":\"2534\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0.5,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"},{\"hz\":0,\"roleId\":\"mbon\"}],\"stimulusRemainingMs\":null}}],\"descriptorRevision\":\"7\",\"episodeId\":\"episode-1\",\"eventIds\":[\"evt-1\"],\"media\":{\"audio\":[{\"discontinuity\":false,\"firstSample\":\"33600\",\"sampleFrames\":800,\"samples\":{\"artifactId\":\"audio-1\",\"byteLength\":\"6400\",\"contentType\":\"audio/x-f32le\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"streamId\":\"mix\"}],\"views\":[{\"pixels\":{\"artifactId\":\"frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"42\",\"viewId\":\"screen\"}]},\"progress\":{\"schema\":{\"digest\":\"80fa973157f334bb3208dfff6f7a2f298e79f73f07c72c33f5725975c6cff8e1\",\"id\":\"pokemon.progress.v1\",\"version\":1},\"value\":{\"rank\":10}},\"publisherIncarnation\":\"pub-1\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"42\"},\"sequence\":\"42\",\"worldTime\":{\"denominator\":\"1\",\"numerator\":\"700000000\"}}",
|
||||
"digest": "39fd3d104ee42c6d4cb832c8923415952d8b9f686f0d0f3281a6c5bfea56d622"
|
||||
},
|
||||
{
|
||||
"name": "transition trace",
|
||||
|
|
@ -2891,6 +2898,549 @@
|
|||
"note": "recorded by step-v1 section 8, excluded from its comparison",
|
||||
"canonical": "{\"advanceRequestId\":\"req-43\",\"busCallIds\":[\"call-100\",\"call-101\",\"call-102\"],\"commitRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-44\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-45\"}],\"deliveryIds\":[\"dlv-7\",\"own-9\"],\"prepareRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-41\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-42\"}],\"wallTimeNs\":\"1234567890\"}",
|
||||
"digest": "07c3402ab0e48b186d56f667da980a15b2a48b21381235cff37523a24ff0dfff"
|
||||
},
|
||||
{
|
||||
"name": "agent telemetry reporting a running stimulation pulse",
|
||||
"type": "AgentTelemetry",
|
||||
"value": {
|
||||
"brainTicks": "70002",
|
||||
"populationRateHz": 12.5,
|
||||
"stimulusRemainingMs": 312.5,
|
||||
"rates": [
|
||||
{
|
||||
"roleId": "kenyon",
|
||||
"hz": 3.25
|
||||
}
|
||||
],
|
||||
"learning": {
|
||||
"enabled": true,
|
||||
"updates": "4",
|
||||
"changed": "2",
|
||||
"signal": 0.0
|
||||
}
|
||||
},
|
||||
"note": "stimulusRemainingMs is what the coordinator's sugar admission reads from the last commit (2026-09-23)",
|
||||
"canonical": "{\"brainTicks\":\"70002\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"}],\"stimulusRemainingMs\":312.5}",
|
||||
"digest": "3f11ace6a1d79c429397d300f45a1ad9f601e89648986b32bf1de410ea73d986"
|
||||
},
|
||||
{
|
||||
"name": "rollback episode request",
|
||||
"type": "EpisodeRequest",
|
||||
"value": {
|
||||
"kind": "rollback",
|
||||
"reason": "stall",
|
||||
"outcome": {
|
||||
"schema": {
|
||||
"id": "legacy-ratchet-rollback-v1",
|
||||
"version": 1,
|
||||
"digest": "0610f899a4746e5991a07dbc3c847ada15d7c68cedf0664244a3c1d87c175c70"
|
||||
},
|
||||
"value": {
|
||||
"slotId": "best",
|
||||
"trigger": "stall"
|
||||
}
|
||||
}
|
||||
},
|
||||
"note": "only a composition declaring legacy-ratchet-rollback-v1 may act on it",
|
||||
"canonical": "{\"kind\":\"rollback\",\"outcome\":{\"schema\":{\"digest\":\"0610f899a4746e5991a07dbc3c847ada15d7c68cedf0664244a3c1d87c175c70\",\"id\":\"legacy-ratchet-rollback-v1\",\"version\":1},\"value\":{\"slotId\":\"best\",\"trigger\":\"stall\"}},\"reason\":\"stall\"}",
|
||||
"digest": "089505ca3db7f5ab546dc391d0b4ea0a6ca610cd06641d248ad5c9139457a3da"
|
||||
},
|
||||
{
|
||||
"name": "save slot params",
|
||||
"type": "SaveSlotParams",
|
||||
"value": {
|
||||
"slotId": "best"
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"slotId\":\"best\"}",
|
||||
"digest": "f448ea1813ae73979392f4d6a86edfe56d17d1347efef919a1ec2ed949f63b8c"
|
||||
},
|
||||
{
|
||||
"name": "save slot result",
|
||||
"type": "SaveSlotResult",
|
||||
"value": {
|
||||
"slotId": "best",
|
||||
"boundary": "4101",
|
||||
"stateDigest": "5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d",
|
||||
"byteLength": "199616"
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"boundary\":\"4101\",\"byteLength\":\"199616\",\"slotId\":\"best\",\"stateDigest\":\"5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d\"}",
|
||||
"digest": "602100c80393c58536bd5251df42f476c059e42ea0d671fc6683fc1cf3aeab2a"
|
||||
},
|
||||
{
|
||||
"name": "restore slot params",
|
||||
"type": "RestoreSlotParams",
|
||||
"value": {
|
||||
"slotId": "best",
|
||||
"priorEpoch": "epoch-7",
|
||||
"policy": "legacy-ratchet-rollback-v1"
|
||||
},
|
||||
"note": "sent with the new epoch in scope",
|
||||
"canonical": "{\"policy\":\"legacy-ratchet-rollback-v1\",\"priorEpoch\":\"epoch-7\",\"slotId\":\"best\"}",
|
||||
"digest": "356e7503f75e3c1722ac68d7f8f95de370f4c4e914c651d24efe7e19954f07f3"
|
||||
},
|
||||
{
|
||||
"name": "restore slot result",
|
||||
"type": "RestoreSlotResult",
|
||||
"value": {
|
||||
"slotId": "best",
|
||||
"committedStep": "4101",
|
||||
"observation": {
|
||||
"boundary": "4101",
|
||||
"worldTime": {
|
||||
"numerator": "35154861328125",
|
||||
"denominator": "512"
|
||||
},
|
||||
"engineFrame": "4102",
|
||||
"sensoryViews": [
|
||||
{
|
||||
"viewId": "lcd",
|
||||
"producedStep": "4101",
|
||||
"pixels": {
|
||||
"storeId": "store-1",
|
||||
"artifactId": "slot-frame-1",
|
||||
"generation": "1",
|
||||
"byteLength": "92160",
|
||||
"contentType": "image/x-rgba8",
|
||||
"digest": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"inspection": {
|
||||
"schema": {
|
||||
"id": "gameboy-memory-inspection-v1",
|
||||
"version": 1,
|
||||
"digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6"
|
||||
},
|
||||
"value": {
|
||||
"memory": {
|
||||
"storeId": "store-1",
|
||||
"artifactId": "wram-4101",
|
||||
"generation": "1",
|
||||
"byteLength": "65536",
|
||||
"contentType": "application/octet-stream",
|
||||
"digest": null
|
||||
},
|
||||
"romDigest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20"
|
||||
}
|
||||
},
|
||||
"broadcastViews": [
|
||||
{
|
||||
"viewId": "lcd",
|
||||
"producedStep": "4101",
|
||||
"pixels": {
|
||||
"storeId": "store-1",
|
||||
"artifactId": "slot-frame-1",
|
||||
"generation": "1",
|
||||
"byteLength": "92160",
|
||||
"contentType": "image/x-rgba8",
|
||||
"digest": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"audio": []
|
||||
}
|
||||
},
|
||||
"note": "same boundary, worldTime and engineFrame continue, the slot's archived frame, no audio chunk",
|
||||
"canonical": "{\"committedStep\":\"4101\",\"observation\":{\"audio\":[],\"boundary\":\"4101\",\"broadcastViews\":[{\"pixels\":{\"artifactId\":\"slot-frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"4101\",\"viewId\":\"lcd\"}],\"engineFrame\":\"4102\",\"inspection\":{\"schema\":{\"digest\":\"d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6\",\"id\":\"gameboy-memory-inspection-v1\",\"version\":1},\"value\":{\"memory\":{\"artifactId\":\"wram-4101\",\"byteLength\":\"65536\",\"contentType\":\"application/octet-stream\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"romDigest\":\"c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20\"}},\"sensoryViews\":[{\"pixels\":{\"artifactId\":\"slot-frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"4101\",\"viewId\":\"lcd\"}],\"worldTime\":{\"denominator\":\"512\",\"numerator\":\"35154861328125\"}},\"slotId\":\"best\"}",
|
||||
"digest": "81511c014fc260c0230b0e9eeaffe2897f32aef1b811adc18abc2c94a65c8d69"
|
||||
},
|
||||
{
|
||||
"name": "agent rollback params",
|
||||
"type": "AgentRollbackParams",
|
||||
"value": {
|
||||
"agentId": "fly",
|
||||
"priorEpoch": "epoch-7",
|
||||
"policy": "legacy-ratchet-rollback-v1",
|
||||
"input": {
|
||||
"boundary": "4101",
|
||||
"views": [
|
||||
{
|
||||
"viewId": "lcd",
|
||||
"producedStep": "4101",
|
||||
"pixels": {
|
||||
"storeId": "store-1",
|
||||
"artifactId": "slot-frame-1",
|
||||
"generation": "1",
|
||||
"byteLength": "92160",
|
||||
"contentType": "image/x-rgba8",
|
||||
"digest": null
|
||||
}
|
||||
}
|
||||
],
|
||||
"structured": null
|
||||
},
|
||||
"decisionContext": {
|
||||
"schema": {
|
||||
"id": "gameboy-readout-context-v1",
|
||||
"version": 1,
|
||||
"digest": "78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d"
|
||||
},
|
||||
"value": {
|
||||
"boot": false,
|
||||
"bound": [
|
||||
"macro_go_objective",
|
||||
"macro_talk"
|
||||
],
|
||||
"location": {
|
||||
"area": 2,
|
||||
"x": 17,
|
||||
"y": 9
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"agentId\":\"fly\",\"decisionContext\":{\"schema\":{\"digest\":\"78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d\",\"id\":\"gameboy-readout-context-v1\",\"version\":1},\"value\":{\"boot\":false,\"bound\":[\"macro_go_objective\",\"macro_talk\"],\"location\":{\"area\":2,\"x\":17,\"y\":9}}},\"input\":{\"boundary\":\"4101\",\"structured\":null,\"views\":[{\"pixels\":{\"artifactId\":\"slot-frame-1\",\"byteLength\":\"92160\",\"contentType\":\"image/x-rgba8\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"producedStep\":\"4101\",\"viewId\":\"lcd\"}]},\"policy\":\"legacy-ratchet-rollback-v1\",\"priorEpoch\":\"epoch-7\"}",
|
||||
"digest": "d3683e4a7b297cda897b1408155d86685903016009bd82e1ec2e5d1d35a8e46d"
|
||||
},
|
||||
{
|
||||
"name": "agent rollback result",
|
||||
"type": "AgentRollbackResult",
|
||||
"value": {
|
||||
"agentId": "fly",
|
||||
"committedStep": "4101",
|
||||
"decisionContextDigest": "ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4",
|
||||
"telemetry": {
|
||||
"brainTicks": "70002",
|
||||
"populationRateHz": 12.5,
|
||||
"stimulusRemainingMs": 312.5,
|
||||
"rates": [
|
||||
{
|
||||
"roleId": "kenyon",
|
||||
"hz": 3.25
|
||||
}
|
||||
],
|
||||
"learning": {
|
||||
"enabled": true,
|
||||
"updates": "4",
|
||||
"changed": "2",
|
||||
"signal": 0.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"note": "no tick: committedStep is the scoped step",
|
||||
"canonical": "{\"agentId\":\"fly\",\"committedStep\":\"4101\",\"decisionContextDigest\":\"ea7792a26f405e2ae9c6f49ca93bbe6076ceac0a1fc53d83426c7d7f2d9377e4\",\"telemetry\":{\"brainTicks\":\"70002\",\"learning\":{\"changed\":\"2\",\"enabled\":true,\"signal\":0,\"updates\":\"4\"},\"populationRateHz\":12.5,\"rates\":[{\"hz\":3.25,\"roleId\":\"kenyon\"}],\"stimulusRemainingMs\":312.5}}",
|
||||
"digest": "8a109ad370b8a9c00333ea281092c04ed296c473d9189a71a718ec12e89a8d52"
|
||||
},
|
||||
{
|
||||
"name": "readout context in macros mode",
|
||||
"type": "GameboyReadoutContext",
|
||||
"value": {
|
||||
"boot": false,
|
||||
"bound": [
|
||||
"macro_go_objective",
|
||||
"macro_talk"
|
||||
],
|
||||
"location": {
|
||||
"area": 2,
|
||||
"x": 17,
|
||||
"y": 9
|
||||
}
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"boot\":false,\"bound\":[\"macro_go_objective\",\"macro_talk\"],\"location\":{\"area\":2,\"x\":17,\"y\":9}}",
|
||||
"digest": "a11982ed1f461efc82c6dec38254faf94a526991dc5d4c5f9c640aa45338381c"
|
||||
},
|
||||
{
|
||||
"name": "readout context in raw mode on the title screen",
|
||||
"type": "GameboyReadoutContext",
|
||||
"value": {
|
||||
"boot": true,
|
||||
"bound": [],
|
||||
"location": null
|
||||
},
|
||||
"note": "no macro group, boot gate open, no location",
|
||||
"canonical": "{\"boot\":true,\"bound\":[],\"location\":null}",
|
||||
"digest": "6d87ccb265f3f8698d993b5338556d8182aaa34acc04af5c32d155cecca58d69"
|
||||
},
|
||||
{
|
||||
"name": "channels decision with a macro",
|
||||
"type": "GameboyChannelsDecision",
|
||||
"value": {
|
||||
"buttons": [
|
||||
{
|
||||
"id": "up",
|
||||
"down": true
|
||||
},
|
||||
{
|
||||
"id": "down",
|
||||
"down": false
|
||||
},
|
||||
{
|
||||
"id": "left",
|
||||
"down": false
|
||||
},
|
||||
{
|
||||
"id": "right",
|
||||
"down": false
|
||||
},
|
||||
{
|
||||
"id": "a",
|
||||
"down": false
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"down": false
|
||||
},
|
||||
{
|
||||
"id": "start",
|
||||
"down": false
|
||||
},
|
||||
{
|
||||
"id": "select",
|
||||
"down": false
|
||||
}
|
||||
],
|
||||
"macro": "macro_talk"
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"buttons\":[{\"down\":true,\"id\":\"up\"},{\"down\":false,\"id\":\"down\"},{\"down\":false,\"id\":\"left\"},{\"down\":false,\"id\":\"right\"},{\"down\":false,\"id\":\"a\"},{\"down\":false,\"id\":\"b\"},{\"down\":false,\"id\":\"start\"},{\"down\":false,\"id\":\"select\"}],\"macro\":\"macro_talk\"}",
|
||||
"digest": "6a0428a108f8fcd6b08883bc918ed414242f064ef287edd9426407f8ec51f2f1"
|
||||
},
|
||||
{
|
||||
"name": "channels decision with nothing held",
|
||||
"type": "GameboyChannelsDecision",
|
||||
"value": {
|
||||
"buttons": [
|
||||
{
|
||||
"id": "up",
|
||||
"down": false
|
||||
},
|
||||
{
|
||||
"id": "down",
|
||||
"down": false
|
||||
},
|
||||
{
|
||||
"id": "left",
|
||||
"down": false
|
||||
},
|
||||
{
|
||||
"id": "right",
|
||||
"down": false
|
||||
},
|
||||
{
|
||||
"id": "a",
|
||||
"down": false
|
||||
},
|
||||
{
|
||||
"id": "b",
|
||||
"down": false
|
||||
},
|
||||
{
|
||||
"id": "start",
|
||||
"down": false
|
||||
},
|
||||
{
|
||||
"id": "select",
|
||||
"down": false
|
||||
}
|
||||
],
|
||||
"macro": null
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"buttons\":[{\"down\":false,\"id\":\"up\"},{\"down\":false,\"id\":\"down\"},{\"down\":false,\"id\":\"left\"},{\"down\":false,\"id\":\"right\"},{\"down\":false,\"id\":\"a\"},{\"down\":false,\"id\":\"b\"},{\"down\":false,\"id\":\"start\"},{\"down\":false,\"id\":\"select\"}],\"macro\":null}",
|
||||
"digest": "af16b20c291a798048d1f5459a7316c3ced9dce6a34515204e6e18fc8c70e45a"
|
||||
},
|
||||
{
|
||||
"name": "memory inspection",
|
||||
"type": "GameboyMemoryInspection",
|
||||
"value": {
|
||||
"memory": {
|
||||
"storeId": "store-1",
|
||||
"artifactId": "wram-4101",
|
||||
"generation": "1",
|
||||
"byteLength": "65536",
|
||||
"contentType": "application/octet-stream",
|
||||
"digest": null
|
||||
},
|
||||
"romDigest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20"
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"memory\":{\"artifactId\":\"wram-4101\",\"byteLength\":\"65536\",\"contentType\":\"application/octet-stream\",\"digest\":null,\"generation\":\"1\",\"storeId\":\"store-1\"},\"romDigest\":\"c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20\"}",
|
||||
"digest": "5423dfb6dde26e1d2f4abd5094fbea79ce5499d71b32e883e4c43140cce8dec2"
|
||||
},
|
||||
{
|
||||
"name": "rollback request on a game over",
|
||||
"type": "LegacyRatchetRollbackRequest",
|
||||
"value": {
|
||||
"slotId": "best",
|
||||
"trigger": "game-over"
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"slotId\":\"best\",\"trigger\":\"game-over\"}",
|
||||
"digest": "567846fcbbc1373e89d4f9265e1c183786ef55e866c7e475e44ce315b0ddb072"
|
||||
},
|
||||
{
|
||||
"name": "the legacy profile",
|
||||
"type": "LegacyGameboyProfile",
|
||||
"value": {
|
||||
"profileId": "gameboy-legacy-fafb-v783-v1",
|
||||
"datasetId": "fafb-v783",
|
||||
"fingerprintSchema": 1,
|
||||
"datasetFingerprint": "75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc",
|
||||
"kernelVersion": "lif-1ms-f64-v2",
|
||||
"plasticityVersion": "fly-kc-mbon-rstdp-v2",
|
||||
"tickDuration": {
|
||||
"numerator": "1000000",
|
||||
"denominator": "1"
|
||||
},
|
||||
"warmupMs": 2500,
|
||||
"view": {
|
||||
"viewId": "lcd",
|
||||
"width": 160,
|
||||
"height": 144
|
||||
},
|
||||
"supportedStimuli": [
|
||||
"reward-pulse"
|
||||
],
|
||||
"readoutContextSchema": {
|
||||
"id": "gameboy-readout-context-v1",
|
||||
"version": 1,
|
||||
"digest": "78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d"
|
||||
},
|
||||
"decisionSchema": {
|
||||
"id": "gameboy-channels-v1",
|
||||
"version": 1,
|
||||
"digest": "28bd89bfa41ec73fb2785959a64d9975b4a932e10cd9e51dc4bc29020c823595"
|
||||
},
|
||||
"legacyExceptions": [
|
||||
"macro-roles-outside-fingerprint"
|
||||
]
|
||||
},
|
||||
"note": "the one document; its digest is the profile AssetRef digest",
|
||||
"canonical": "{\"datasetFingerprint\":\"75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc\",\"datasetId\":\"fafb-v783\",\"decisionSchema\":{\"digest\":\"28bd89bfa41ec73fb2785959a64d9975b4a932e10cd9e51dc4bc29020c823595\",\"id\":\"gameboy-channels-v1\",\"version\":1},\"fingerprintSchema\":1,\"kernelVersion\":\"lif-1ms-f64-v2\",\"legacyExceptions\":[\"macro-roles-outside-fingerprint\"],\"plasticityVersion\":\"fly-kc-mbon-rstdp-v2\",\"profileId\":\"gameboy-legacy-fafb-v783-v1\",\"readoutContextSchema\":{\"digest\":\"78a5312f8608a1399e7a16549a1b95a43b87137618d59815b9b06c1bb184014d\",\"id\":\"gameboy-readout-context-v1\",\"version\":1},\"supportedStimuli\":[\"reward-pulse\"],\"tickDuration\":{\"denominator\":\"1\",\"numerator\":\"1000000\"},\"view\":{\"height\":144,\"viewId\":\"lcd\",\"width\":160},\"warmupMs\":2500}",
|
||||
"digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878"
|
||||
},
|
||||
{
|
||||
"name": "an example legacy composition",
|
||||
"type": "LegacyGameboyComposition",
|
||||
"value": {
|
||||
"compositionId": "pokered-live",
|
||||
"scheduler": "lockstep-v1",
|
||||
"profile": {
|
||||
"id": "gameboy-legacy-fafb-v783-v1",
|
||||
"digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878",
|
||||
"byteLength": "1137",
|
||||
"format": "fly-profile-v1"
|
||||
},
|
||||
"executor": {
|
||||
"id": "pokered-macros-v1",
|
||||
"rom": {
|
||||
"id": "pokered-rom",
|
||||
"digest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20",
|
||||
"byteLength": "1048576",
|
||||
"format": "gb-rom"
|
||||
},
|
||||
"adapter": "pokered-unique8-v6",
|
||||
"symbolProvenance": "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b",
|
||||
"mode": "macros",
|
||||
"macroChannels": [
|
||||
"macro_go_objective",
|
||||
"macro_talk",
|
||||
"macro_next",
|
||||
"macro_move_1"
|
||||
]
|
||||
},
|
||||
"decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854",
|
||||
"environment": {
|
||||
"extensions": [
|
||||
"gameboy-slots-v1"
|
||||
],
|
||||
"slots": [
|
||||
"best"
|
||||
],
|
||||
"stepDuration": {
|
||||
"numerator": "8572265625",
|
||||
"denominator": "512"
|
||||
},
|
||||
"inspectionSchema": {
|
||||
"id": "gameboy-memory-inspection-v1",
|
||||
"version": 1,
|
||||
"digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6"
|
||||
},
|
||||
"controllerSchema": {
|
||||
"id": "gameboy-joypad-v1",
|
||||
"version": 1,
|
||||
"digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e"
|
||||
},
|
||||
"setupFrames": 1,
|
||||
"audio": {
|
||||
"sampleRate": 48000,
|
||||
"channels": 2
|
||||
}
|
||||
},
|
||||
"episodePolicy": "legacy-ratchet-rollback-v1",
|
||||
"restore": "legacy-transient-reset",
|
||||
"checkpointFormatOfRecord": "FLYSIM01",
|
||||
"flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu"
|
||||
},
|
||||
"note": "placeholder ROM and decoder digests",
|
||||
"canonical": "{\"checkpointFormatOfRecord\":\"FLYSIM01\",\"compositionId\":\"pokered-live\",\"decoderConfigDigest\":\"5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854\",\"environment\":{\"audio\":{\"channels\":2,\"sampleRate\":48000},\"controllerSchema\":{\"digest\":\"1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e\",\"id\":\"gameboy-joypad-v1\",\"version\":1},\"extensions\":[\"gameboy-slots-v1\"],\"inspectionSchema\":{\"digest\":\"d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6\",\"id\":\"gameboy-memory-inspection-v1\",\"version\":1},\"setupFrames\":1,\"slots\":[\"best\"],\"stepDuration\":{\"denominator\":\"512\",\"numerator\":\"8572265625\"}},\"episodePolicy\":\"legacy-ratchet-rollback-v1\",\"executor\":{\"adapter\":\"pokered-unique8-v6\",\"id\":\"pokered-macros-v1\",\"macroChannels\":[\"macro_go_objective\",\"macro_talk\",\"macro_next\",\"macro_move_1\"],\"mode\":\"macros\",\"rom\":{\"byteLength\":\"1048576\",\"digest\":\"c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20\",\"format\":\"gb-rom\",\"id\":\"pokered-rom\"},\"symbolProvenance\":\"0cd19d3b877b7dc66d12c7050bed9a7f38154d4b\"},\"flysimCompatibility\":\"lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu\",\"profile\":{\"byteLength\":\"1137\",\"digest\":\"41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878\",\"format\":\"fly-profile-v1\",\"id\":\"gameboy-legacy-fafb-v783-v1\"},\"restore\":\"legacy-transient-reset\",\"scheduler\":\"lockstep-v1\"}",
|
||||
"digest": "f0142c7f09a1319453b472cdddbc1855062af5733f89d1fbc0b82c0adb52b0c7"
|
||||
},
|
||||
{
|
||||
"name": "an example legacy composition in raw mode",
|
||||
"type": "LegacyGameboyComposition",
|
||||
"value": {
|
||||
"compositionId": "pokered-live",
|
||||
"scheduler": "lockstep-v1",
|
||||
"profile": {
|
||||
"id": "gameboy-legacy-fafb-v783-v1",
|
||||
"digest": "41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878",
|
||||
"byteLength": "1137",
|
||||
"format": "fly-profile-v1"
|
||||
},
|
||||
"executor": {
|
||||
"id": "pokered-macros-v1",
|
||||
"rom": {
|
||||
"id": "pokered-rom",
|
||||
"digest": "c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20",
|
||||
"byteLength": "1048576",
|
||||
"format": "gb-rom"
|
||||
},
|
||||
"adapter": "pokered-unique8-v6",
|
||||
"symbolProvenance": "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b",
|
||||
"mode": "raw",
|
||||
"macroChannels": []
|
||||
},
|
||||
"decoderConfigDigest": "5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854",
|
||||
"environment": {
|
||||
"extensions": [
|
||||
"gameboy-slots-v1"
|
||||
],
|
||||
"slots": [
|
||||
"best"
|
||||
],
|
||||
"stepDuration": {
|
||||
"numerator": "8572265625",
|
||||
"denominator": "512"
|
||||
},
|
||||
"inspectionSchema": {
|
||||
"id": "gameboy-memory-inspection-v1",
|
||||
"version": 1,
|
||||
"digest": "d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6"
|
||||
},
|
||||
"controllerSchema": {
|
||||
"id": "gameboy-joypad-v1",
|
||||
"version": 1,
|
||||
"digest": "1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e"
|
||||
},
|
||||
"setupFrames": 1,
|
||||
"audio": {
|
||||
"sampleRate": 48000,
|
||||
"channels": 2
|
||||
}
|
||||
},
|
||||
"episodePolicy": "legacy-ratchet-rollback-v1",
|
||||
"restore": "legacy-transient-reset",
|
||||
"checkpointFormatOfRecord": "FLYSIM01",
|
||||
"flysimCompatibility": "lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu"
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"checkpointFormatOfRecord\":\"FLYSIM01\",\"compositionId\":\"pokered-live\",\"decoderConfigDigest\":\"5c3f2f084bef674fed492b82e5e0951e993c856ae98f5453d180cd06143dc854\",\"environment\":{\"audio\":{\"channels\":2,\"sampleRate\":48000},\"controllerSchema\":{\"digest\":\"1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e\",\"id\":\"gameboy-joypad-v1\",\"version\":1},\"extensions\":[\"gameboy-slots-v1\"],\"inspectionSchema\":{\"digest\":\"d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6\",\"id\":\"gameboy-memory-inspection-v1\",\"version\":1},\"setupFrames\":1,\"slots\":[\"best\"],\"stepDuration\":{\"denominator\":\"512\",\"numerator\":\"8572265625\"}},\"episodePolicy\":\"legacy-ratchet-rollback-v1\",\"executor\":{\"adapter\":\"pokered-unique8-v6\",\"id\":\"pokered-macros-v1\",\"macroChannels\":[],\"mode\":\"raw\",\"rom\":{\"byteLength\":\"1048576\",\"digest\":\"c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20\",\"format\":\"gb-rom\",\"id\":\"pokered-rom\"},\"symbolProvenance\":\"0cd19d3b877b7dc66d12c7050bed9a7f38154d4b\"},\"flysimCompatibility\":\"lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu\",\"profile\":{\"byteLength\":\"1137\",\"digest\":\"41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878\",\"format\":\"fly-profile-v1\",\"id\":\"gameboy-legacy-fafb-v783-v1\"},\"restore\":\"legacy-transient-reset\",\"scheduler\":\"lockstep-v1\"}",
|
||||
"digest": "efd699cf8cf2485dd2ac8a84ff43c9bc3711a78181f32285ed31a2ee86e16925"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
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 trace format of [step-v1] section 8, with behaviour separated from operational
|
||||
//! metadata and a comparator over behaviour alone ([`trace`]);
|
||||
//! - the 2026-09-23 extension methods ([`extensions`]) and the legacy Game Boy composition
|
||||
//! ([`gameboy`]), whose registered schemas are digested apart from `contractDigest`;
|
||||
//! - `seed-derivation-v1` ([`seed`]) and the `FLYSESS1` checkpoint envelope layout
|
||||
//! ([`checkpoint`]), the two specifications CONTRACT-01 has to settle before the real-agent
|
||||
//! and store slices.
|
||||
//!
|
||||
//! What it is not: a transport, a worker, a coordinator or a store. It holds no Game Boy FFI,
|
||||
//! no Melee parser and no console-specific state, and it never reaches the network.
|
||||
//! What it is not: a transport, a worker, a coordinator or a store. It holds no Game Boy FFI
|
||||
//! and no Melee parser, its generic types hold no console-specific state (the [`gameboy`]
|
||||
//! declarations travel only inside `TypedValue`s and composition documents), and it never
|
||||
//! reaches the network.
|
||||
//!
|
||||
//! Every type implements [`scalar::DomainType`]: `from_json` reads and validates, `to_json`
|
||||
//! writes the canonical shape, and `validate` re-checks the rules that span fields. Reading
|
||||
|
|
@ -33,7 +37,9 @@
|
|||
|
||||
pub mod canonical;
|
||||
pub mod checkpoint;
|
||||
pub mod extensions;
|
||||
pub mod fixtures;
|
||||
pub mod gameboy;
|
||||
pub mod media;
|
||||
pub mod publishing;
|
||||
pub mod rpc;
|
||||
|
|
|
|||
|
|
@ -57,7 +57,7 @@ pub struct LimitSchema {
|
|||
pub source: &'static str,
|
||||
}
|
||||
|
||||
const fn req(name: &'static str, kind: &'static str, constraint: &'static str) -> FieldSchema {
|
||||
pub(crate) const fn req(name: &'static str, kind: &'static str, constraint: &'static str) -> FieldSchema {
|
||||
FieldSchema {
|
||||
name,
|
||||
kind,
|
||||
|
|
@ -66,7 +66,7 @@ const fn req(name: &'static str, kind: &'static str, constraint: &'static str) -
|
|||
}
|
||||
}
|
||||
|
||||
const fn opt(name: &'static str, kind: &'static str, constraint: &'static str) -> FieldSchema {
|
||||
pub(crate) const fn opt(name: &'static str, kind: &'static str, constraint: &'static str) -> FieldSchema {
|
||||
FieldSchema {
|
||||
name,
|
||||
kind,
|
||||
|
|
@ -129,7 +129,7 @@ pub const ENUMS: &[EnumSchema] = &[
|
|||
EnumSchema {
|
||||
name: "EpisodeRequestKind",
|
||||
source: "workers-v1 4",
|
||||
members: &["terminal"],
|
||||
members: crate::workers::EpisodeRequestKind::ALL,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -259,6 +259,11 @@ pub const LIMITS: &[LimitSchema] = &[
|
|||
value: crate::publishing::MAX_ASSETS as u64,
|
||||
source: "crate",
|
||||
},
|
||||
LimitSchema {
|
||||
name: "maxSlots",
|
||||
value: crate::extensions::MAX_SLOTS as u64,
|
||||
source: "crate",
|
||||
},
|
||||
LimitSchema {
|
||||
name: "maxSnapshotEvents",
|
||||
value: crate::publishing::MAX_SNAPSHOT_EVENTS as u64,
|
||||
|
|
@ -413,6 +418,11 @@ pub const SCHEMAS: &[TypeSchema] = &[
|
|||
"{enabled:bool,updates:U64,changed:U64,signal:number}",
|
||||
"changed <= updates; signal finite",
|
||||
),
|
||||
opt(
|
||||
"stimulusRemainingMs",
|
||||
"number|null",
|
||||
"finite and nonnegative; the pulse still running after the operation; null when the agent reports none",
|
||||
),
|
||||
],
|
||||
},
|
||||
TypeSchema {
|
||||
|
|
@ -744,7 +754,11 @@ pub const SCHEMAS: &[TypeSchema] = &[
|
|||
name: "EpisodeRequest",
|
||||
source: "workers-v1 4",
|
||||
fields: &[
|
||||
req("kind", "EpisodeRequestKind", ""),
|
||||
req(
|
||||
"kind",
|
||||
"EpisodeRequestKind",
|
||||
"rollback only under a composition that declares a rollback policy",
|
||||
),
|
||||
req("reason", "Id", ""),
|
||||
req("outcome", "TypedValue", ""),
|
||||
],
|
||||
|
|
@ -949,6 +963,76 @@ pub const SCHEMAS: &[TypeSchema] = &[
|
|||
),
|
||||
],
|
||||
},
|
||||
TypeSchema {
|
||||
name: "SaveSlotParams",
|
||||
source: "workers-v1 7",
|
||||
fields: &[req(
|
||||
"slotId",
|
||||
"Id",
|
||||
"one of the composition's declared slots; capability gameboy-slots-v1",
|
||||
)],
|
||||
},
|
||||
TypeSchema {
|
||||
name: "SaveSlotResult",
|
||||
source: "workers-v1 7",
|
||||
fields: &[
|
||||
req("slotId", "Id", "echoes the request"),
|
||||
req("boundary", "U64", "the scoped committed step"),
|
||||
req("stateDigest", "Digest", "SHA-256 of the saved state bytes"),
|
||||
req("byteLength", "U64", "positive"),
|
||||
],
|
||||
},
|
||||
TypeSchema {
|
||||
name: "RestoreSlotParams",
|
||||
source: "workers-v1 7",
|
||||
fields: &[
|
||||
req("slotId", "Id", "a slot saved in priorEpoch or carried by its restore"),
|
||||
req(
|
||||
"priorEpoch",
|
||||
"Id",
|
||||
"the epoch the environment is Ready in; differs from scope.epoch",
|
||||
),
|
||||
req("policy", "Id", "\"legacy-ratchet-rollback-v1\""),
|
||||
],
|
||||
},
|
||||
TypeSchema {
|
||||
name: "RestoreSlotResult",
|
||||
source: "workers-v1 7",
|
||||
fields: &[
|
||||
req("slotId", "Id", "echoes the request"),
|
||||
req("committedStep", "U64", "the scoped step; no transition ran"),
|
||||
req(
|
||||
"observation",
|
||||
"WorldObservation",
|
||||
"boundary == committedStep; no audio chunk; worldTime and engineFrame continue",
|
||||
),
|
||||
],
|
||||
},
|
||||
TypeSchema {
|
||||
name: "AgentRollbackParams",
|
||||
source: "workers-v1 7",
|
||||
fields: &[
|
||||
req("agentId", "Id", ""),
|
||||
req(
|
||||
"priorEpoch",
|
||||
"Id",
|
||||
"the epoch the agent is Ready in; differs from scope.epoch",
|
||||
),
|
||||
req("policy", "Id", "\"legacy-ratchet-rollback-v1\"; capability of the same name"),
|
||||
req("input", "SensoryInput", "boundary == scope.step; installed without a tick"),
|
||||
req("decisionContext", "TypedValue", "the context for the next Prepare"),
|
||||
],
|
||||
},
|
||||
TypeSchema {
|
||||
name: "AgentRollbackResult",
|
||||
source: "workers-v1 7",
|
||||
fields: &[
|
||||
req("agentId", "Id", ""),
|
||||
req("committedStep", "U64", "the scoped step; a rollback runs no tick"),
|
||||
req("decisionContextDigest", "Digest", ""),
|
||||
req("telemetry", "AgentTelemetry", ""),
|
||||
],
|
||||
},
|
||||
TypeSchema {
|
||||
name: "TransitionTrace",
|
||||
source: "step-v1 8",
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use serde_json::Value;
|
|||
|
||||
use crate::media::{AudioDescriptor, MAX_VIEWS, ViewDescriptor, ViewRef, audio_list, view_list};
|
||||
use crate::scalar::{
|
||||
DomainRequestId, DomainType, RationalNs, Result, SchemaRef, TypedValue, constant,
|
||||
DomainRequestId, DomainType, RationalNs, Result, SchemaRef, TypedValue,
|
||||
constant_true, enumeration, err, finite, finite_in, i32_field, id_list, is_digest, is_id, list,
|
||||
obj, require_same_order, require_unique, u64_json,
|
||||
};
|
||||
|
|
@ -505,6 +505,10 @@ pub struct AgentTelemetry {
|
|||
pub population_rate_hz: f64,
|
||||
pub rates: Vec<RateSample>,
|
||||
pub learning: LearningTelemetry,
|
||||
/// Milliseconds of stimulation pulse still running after this operation, or `None` for
|
||||
/// an agent that reports none. Amendment 2026-09-23 (RT-01a): the coordinator's sugar
|
||||
/// admission reads it from the last commit (workers-v1 section 5).
|
||||
pub stimulus_remaining_ms: Option<f64>,
|
||||
}
|
||||
|
||||
impl AgentTelemetry {
|
||||
|
|
@ -526,6 +530,10 @@ impl DomainType for AgentTelemetry {
|
|||
let mut f = Fields::new(value, "AgentTelemetry")?;
|
||||
let brain_ticks = f.u64_string("brainTicks")?;
|
||||
let population_rate_hz = finite_in(&mut f, "populationRateHz", 0.0, f64::MAX)?;
|
||||
let stimulus_remaining_ms = match f.value("stimulusRemainingMs")? {
|
||||
Value::Null => None,
|
||||
_ => Some(finite_in(&mut f, "stimulusRemainingMs", 0.0, f64::MAX)?),
|
||||
};
|
||||
let rates = list(&mut f, "rates", 0, MAX_RATE_ROLES, |v| {
|
||||
let mut r = Fields::new(v, "AgentTelemetry.rates")?;
|
||||
let role_id = r.id("roleId")?;
|
||||
|
|
@ -554,6 +562,7 @@ impl DomainType for AgentTelemetry {
|
|||
population_rate_hz,
|
||||
rates,
|
||||
learning,
|
||||
stimulus_remaining_ms,
|
||||
};
|
||||
t.validate()?;
|
||||
Ok(t)
|
||||
|
|
@ -586,6 +595,10 @@ impl DomainType for AgentTelemetry {
|
|||
("signal", Value::from(self.learning.signal)),
|
||||
]),
|
||||
),
|
||||
(
|
||||
"stimulusRemainingMs",
|
||||
self.stimulus_remaining_ms.map_or(Value::Null, Value::from),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -614,6 +627,11 @@ impl DomainType for AgentTelemetry {
|
|||
if !self.learning.signal.is_finite() {
|
||||
return err("AgentTelemetry: learning.signal must be finite");
|
||||
}
|
||||
if let Some(remaining) = self.stimulus_remaining_ms
|
||||
&& (!remaining.is_finite() || remaining < 0.0)
|
||||
{
|
||||
return err("AgentTelemetry: stimulusRemainingMs must be null or finite and nonnegative");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -2474,9 +2492,40 @@ impl DomainType for TaskEvent {
|
|||
}
|
||||
}
|
||||
|
||||
/// `episodeRequest`: null, or a terminal request the coordinator may act on.
|
||||
/// The kind of an `episodeRequest` (workers-v1 section 4).
|
||||
///
|
||||
/// `Rollback` is the amendment of 2026-09-23 (RT-01a): a request for the composition's declared
|
||||
/// rollback policy, applied at the committed boundary the transition just reached. Only a
|
||||
/// composition that declares such a policy may act on it; any other refuses it.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
|
||||
pub enum EpisodeRequestKind {
|
||||
Terminal,
|
||||
Rollback,
|
||||
}
|
||||
|
||||
impl EpisodeRequestKind {
|
||||
pub const ALL: &'static [&'static str] = &["terminal", "rollback"];
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
EpisodeRequestKind::Terminal => "terminal",
|
||||
EpisodeRequestKind::Rollback => "rollback",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(s: &str) -> Result<EpisodeRequestKind> {
|
||||
match s {
|
||||
"terminal" => Ok(EpisodeRequestKind::Terminal),
|
||||
"rollback" => Ok(EpisodeRequestKind::Rollback),
|
||||
_ => err("EpisodeRequest: kind must be terminal or rollback"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `episodeRequest`: null, or a request the coordinator may act on.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct EpisodeRequest {
|
||||
pub kind: EpisodeRequestKind,
|
||||
pub reason: String,
|
||||
pub outcome: TypedValue,
|
||||
}
|
||||
|
|
@ -2486,18 +2535,26 @@ impl DomainType for EpisodeRequest {
|
|||
|
||||
fn from_json(value: &Value) -> Result<EpisodeRequest> {
|
||||
let mut f = Fields::new(value, "EpisodeRequest")?;
|
||||
constant(&mut f, "kind", "terminal")?;
|
||||
let kind = EpisodeRequestKind::parse(&enumeration(
|
||||
&mut f,
|
||||
"kind",
|
||||
EpisodeRequestKind::ALL,
|
||||
)?)?;
|
||||
let reason = f.id("reason")?;
|
||||
let outcome = TypedValue::from_json(f.value("outcome")?)?;
|
||||
f.finish()?;
|
||||
let r = EpisodeRequest { reason, outcome };
|
||||
let r = EpisodeRequest {
|
||||
kind,
|
||||
reason,
|
||||
outcome,
|
||||
};
|
||||
r.validate()?;
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
fn to_json(&self) -> Value {
|
||||
obj(vec![
|
||||
("kind", "terminal".into()),
|
||||
("kind", self.kind.as_str().into()),
|
||||
("reason", self.reason.clone().into()),
|
||||
("outcome", self.outcome.to_json()),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
//! One place that knows how to read every type named by a fixture.
|
||||
|
||||
use flybus::wire::WireError;
|
||||
use fly_session_types::extensions::*;
|
||||
use fly_session_types::gameboy::{
|
||||
ChannelsDecision, LegacyComposition, LegacyProfile, MemoryInspection, ReadoutContext,
|
||||
RollbackRequest,
|
||||
};
|
||||
use fly_session_types::media::*;
|
||||
use fly_session_types::publishing::*;
|
||||
use fly_session_types::rpc::*;
|
||||
|
|
@ -73,6 +78,18 @@ pub fn round_trip(type_name: &str, value: &Value) -> std::result::Result<Value,
|
|||
arm!(TraceBehaviour);
|
||||
arm!(TraceOperational);
|
||||
arm!(TransitionTrace);
|
||||
arm!(SaveSlotParams);
|
||||
arm!(SaveSlotResult);
|
||||
arm!(RestoreSlotParams);
|
||||
arm!(RestoreSlotResult);
|
||||
arm!(AgentRollbackParams);
|
||||
arm!(AgentRollbackResult);
|
||||
arm!(ReadoutContext);
|
||||
arm!(ChannelsDecision);
|
||||
arm!(MemoryInspection);
|
||||
arm!(RollbackRequest);
|
||||
arm!(LegacyProfile);
|
||||
arm!(LegacyComposition);
|
||||
Err(WireError(format!(
|
||||
"no fixture reader for type {type_name:?}"
|
||||
)))
|
||||
|
|
@ -130,4 +147,16 @@ pub const READABLE_TYPES: &[&str] = &[
|
|||
"TraceBehaviour",
|
||||
"TraceOperational",
|
||||
"TransitionTrace",
|
||||
"SaveSlotParams",
|
||||
"SaveSlotResult",
|
||||
"RestoreSlotParams",
|
||||
"RestoreSlotResult",
|
||||
"AgentRollbackParams",
|
||||
"AgentRollbackResult",
|
||||
"GameboyReadoutContext",
|
||||
"GameboyChannelsDecision",
|
||||
"GameboyMemoryInspection",
|
||||
"LegacyRatchetRollbackRequest",
|
||||
"LegacyGameboyProfile",
|
||||
"LegacyGameboyComposition",
|
||||
];
|
||||
|
|
|
|||
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()
|
||||
.map(|t| t["name"].as_str().expect("name"))
|
||||
.collect();
|
||||
// The legacy Game Boy types are registered payload schemas and declarations, digested by
|
||||
// their own extension set rather than by contractDigest (legacy-gameboy-v1 section 13).
|
||||
let extension = fly_session_types::gameboy::extension_set();
|
||||
let declared_in_extension = |declared: &str| {
|
||||
extension["payloadSchemas"]
|
||||
.as_array()
|
||||
.expect("payloadSchemas")
|
||||
.iter()
|
||||
.any(|p| p["declaration"]["id"] == Value::String(declared.to_owned()))
|
||||
|| extension["declarations"]
|
||||
.as_array()
|
||||
.expect("declarations")
|
||||
.iter()
|
||||
.any(|d| d["name"] == Value::String(declared.to_owned()))
|
||||
};
|
||||
let missing: Vec<&&str> = common::READABLE_TYPES
|
||||
.iter()
|
||||
.filter(|expected| !names.contains(*expected))
|
||||
.filter(|expected| {
|
||||
!fly_session_types::gameboy::EXTENSION_TYPES
|
||||
.iter()
|
||||
.any(|(name, declared)| name == *expected && declared_in_extension(declared))
|
||||
})
|
||||
.collect();
|
||||
assert!(
|
||||
missing.is_empty(),
|
||||
"every readable type must be in the schema set; missing {missing:?}"
|
||||
"every readable type must be in the schema set or the legacy extension set; missing {missing:?}"
|
||||
);
|
||||
let mut sorted = names.clone();
|
||||
sorted.sort_unstable();
|
||||
|
|
@ -173,6 +193,7 @@ fn published_limits_match_the_constants_and_name_their_source() {
|
|||
"maxAssets",
|
||||
"maxAudioStreams",
|
||||
"maxCapabilities",
|
||||
"maxSlots",
|
||||
"maxSnapshotEvents",
|
||||
"maxSupportedMajors",
|
||||
"maxSupportedStimuli",
|
||||
|
|
|
|||
|
|
@ -179,6 +179,9 @@ impl FakeModel {
|
|||
changed: self.learning_changed,
|
||||
signal: self.last_signal,
|
||||
},
|
||||
// The synthetic model has no stimulation pulse to report (workers-v1 section 1,
|
||||
// amendment 2026-09-23): only a profile with a pulse reports what is left of it.
|
||||
stimulus_remaining_ms: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -406,8 +406,10 @@ impl Task for CounterTask {
|
|||
Terminal::Counter(target) => new >= target,
|
||||
Terminal::AfterTransitions(n) => self.transitions >= n,
|
||||
};
|
||||
// The contract's `EpisodeRequest` is terminal by construction: `kind` is a constant.
|
||||
// The synthetic task only ever asks for the end of the episode; `rollback` belongs to a
|
||||
// composition that declares a rollback policy (workers-v1 section 4, 2026-09-23).
|
||||
let episode = terminal.then(|| EpisodeRequest {
|
||||
kind: EpisodeRequestKind::Terminal,
|
||||
reason: id("counter-target"),
|
||||
outcome: TypedValue::new(episode_schema(), json!({"counter": new, "transitions": self.transitions}))
|
||||
.expect("a synthetic typed value fits the contract"),
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ pub use fly_session_types::workers::{
|
|||
AcknowledgeParams, AcknowledgeResult, AdvanceParams, AgentCommitResult, AgentInitializeParams,
|
||||
AgentGraph, AgentInitializeResult, AgentTelemetry, AssetRef, AxisRange, AxisSchema, AxisValue, ButtonState,
|
||||
CommitParams, ControllerSchema, Determinism, EnvironmentDescriptor,
|
||||
EnvironmentInitializeParams, EnvironmentInitializeResult, EpisodeRequest, HelloParams,
|
||||
EnvironmentInitializeParams, EnvironmentInitializeResult, EpisodeRequest, EpisodeRequestKind,
|
||||
HelloParams,
|
||||
HelloResult, LearningTelemetry, MAX_ACKNOWLEDGE, MAX_AGENTS, MAX_PORTS, MAX_RATE_ROLES,
|
||||
MAX_REWARDS, MAX_STIMULI, PortControl, PortDescriptor, PrepareParams, PreparedDecision,
|
||||
RateSample, Recovery, Reward, Role, SensoryInput, ShutdownParams, ShutdownResult, StatusResult,
|
||||
|
|
|
|||
|
|
@ -50,6 +50,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|||
unicode-normalization = "0.1"
|
||||
|
||||
[dev-dependencies]
|
||||
# The legacy profile's pinned identities (`tests/legacy_profile_identity.rs`): test-only, so the
|
||||
# service itself does not link the session types.
|
||||
fly-session-types = { path = "../fly-session-types" }
|
||||
futures-util = "0.3"
|
||||
# `default-features = false` drops the remote-reference resolver (and with it reqwest and a
|
||||
# TLS stack): `packages/feed/src/schema.json` is self-contained, so nothing has to be fetched.
|
||||
|
|
|
|||
|
|
@ -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