Merge docs/port-contracts: PROF-02a and RT-01a, the legacy Game Boy profile and environment boundary, per the operator's 2026-09-23 decisions
This commit is contained in:
commit
4b6624757b
46 changed files with 8436 additions and 80 deletions
|
|
@ -68,6 +68,15 @@ already created. Builders use separate worktrees; the coordinator reviews contra
|
|||
- **Done:** empty required populations, malformed CSR and incorrect profile restores fail;
|
||||
legacy FAFB artifacts and default numerical version strings remain unchanged.
|
||||
|
||||
**2026-09-23: split by operator decision.** PROF-02a, the legacy profile, is done as a contract:
|
||||
[legacy Game Boy composition v1](session-framework/legacy-gameboy-v1.md) defines
|
||||
`gameboy-legacy-fafb-v783-v1` (today's schema-1 fingerprint embedded, `lif-1ms-f64-v2` and
|
||||
`fly-kc-mbon-rstdp-v2` unchanged, the macro-role exception declared), the readout context
|
||||
`gameboy-readout-context-v1`, the decision `gameboy-channels-v1`, and a composition declaration
|
||||
whose digest carries the decoder and macro-channel configuration instead of the legacy
|
||||
compatibility string. PROF-02b -- the bundle manifests, role mapping, strict graph validation and
|
||||
profile-mismatch fixtures above -- is later and gates DATA-01, not the session port.
|
||||
|
||||
### DATA-01 — Acquire and normalize MaleCNS
|
||||
|
||||
- **Branch:** `feat/malecns-import`
|
||||
|
|
@ -108,6 +117,16 @@ already created. Builders use separate worktrees; the coordinator reviews contra
|
|||
- **Done:** existing single-agent action/reward traces match and a fake environment can be
|
||||
driven through the same boundary without importing binjgb or task-specific addresses.
|
||||
|
||||
**2026-09-23: contract written (RT-01a), implementation pending.** The operator decided the
|
||||
boundary: macros run in the coordinator's action executor over a per-boundary 64-KiB memory
|
||||
image carried as an inspection artifact plus the ROM as an `AssetRef`; the emulator shim gains
|
||||
one read-only bulk read and the joypad stays its only write; the Pokémon task and executor are
|
||||
one object (`pokered-macros-v1`); the ratchet is the `legacy-ratchet-rollback-v1` episode policy
|
||||
over the environment extension `gameboy-slots-v1`. The dated amendments are in
|
||||
[workers-v1](session-framework/workers-v1.md), [step-v1](session-framework/step-v1.md) and
|
||||
[state-media-v1](session-framework/state-media-v1.md); the session implementation guide's
|
||||
ENV-01 carries the build.
|
||||
|
||||
### RUNTIME-02 — Extract the single-agent session
|
||||
|
||||
- **Branch:** `refactor/session-runtime`
|
||||
|
|
|
|||
|
|
@ -548,6 +548,14 @@ clock may lead environment time by warm-up; persist that offset instead of prete
|
|||
clocks start at zero. Rendering, physics and decision cadence may differ, but the backend
|
||||
must define their relationship.
|
||||
|
||||
**Amendment, 2026-09-23 (operator decision of 2026-09-23).** "Preserve the detailed legacy
|
||||
ordering inside the legacy single-agent composition" now means inside a legacy composition that
|
||||
runs on the session framework's `lockstep-v1`, not beside it: the operator decided on a full port.
|
||||
The detailed ordering is preserved because it is already the lockstep transaction order with one
|
||||
agent ([legacy-gameboy-v1](session-framework/legacy-gameboy-v1.md) section 4), and "keep the
|
||||
legacy floating remainder arithmetic" costs nothing, because the legacy `f64` frame constant is
|
||||
exactly the rational Game Boy frame and the two accumulators are identical (section 3 there).
|
||||
|
||||
Start with sequential agent evaluation for reproducibility. Then compare parallel agent
|
||||
evaluation against the same action trace. Cap total worker budget: `agents × brain_threads`
|
||||
can otherwise oversubscribe the machine. Use private pools for concurrent agents or serialize
|
||||
|
|
@ -605,7 +613,9 @@ jobs so repeated copies cannot exhaust memory under slow storage.
|
|||
|
||||
Exact replay requires action-executor and admission state, not just the neural envelope.
|
||||
Legacy macros intentionally discard transient execution on restart; preserve that behavior
|
||||
for v1 and label it as legacy continuation semantics, not exact session replay. New sessions
|
||||
for v1 and label it as legacy continuation semantics, not exact session replay.
|
||||
(*2026-09-23:* the label is `restore: legacy-transient-reset`, declared by the legacy
|
||||
composition, [legacy-gameboy-v1](session-framework/legacy-gameboy-v1.md) section 14.) New sessions
|
||||
persist all behavior-affecting state or explicitly restart an episode under a documented rule.
|
||||
|
||||
Keep `FLYSIM01` readable through a legacy adapter. Never silently rewrite a checkpoint on
|
||||
|
|
|
|||
|
|
@ -46,6 +46,15 @@ be built without them:
|
|||
envelope and the durable commit sequence. `FLYSIM01` is unchanged and stays separately
|
||||
readable.
|
||||
|
||||
One contract added by the 2026-09-23 amendments (PROF-02a, RT-01a), after the operator decided
|
||||
to port the live fly onto this framework:
|
||||
|
||||
- [Legacy Game Boy composition v1](legacy-gameboy-v1.md) — the legacy profile
|
||||
`gameboy-legacy-fafb-v783-v1`, its readout context and decision schemas, the memory-image
|
||||
inspection, the `pokered-macros-v1` executor, the `gameboy-slots-v1` environment extension,
|
||||
the `legacy-ratchet-rollback-v1` episode policy, `legacy-transient-reset` restore semantics and
|
||||
the composition digest. MaleCNS bundles (PROF-02b) come later.
|
||||
|
||||
For context: [modular-session analysis](../malecns-modular-sessions.md) and
|
||||
[Melee audit](../melee-framework-audit.md). Each contract owns its named subject; step ordering
|
||||
wins over an informal diagram, and Flybus owns transport/resource rules. Resolve contradictions
|
||||
|
|
@ -126,6 +135,19 @@ versions, historical arithmetic/fingerprints and FLYSIM01 reader. New identities
|
|||
readout/executor/task/scheduler semantics. New public feed v2 is an application/presentation
|
||||
gateway contract built on the same internal bus; it does not replace the bus or expose it raw.
|
||||
|
||||
**Amendment, 2026-09-23 (operator decision of 2026-09-23).** "Keep legacy-gameboy-v1 distinct
|
||||
from lockstep-v1" is superseded for scheduling. The operator decided on a **full port** of the
|
||||
live fly onto the session framework: the legacy composition runs under `lockstep-v1` with one
|
||||
agent, one port and one world, declared as [legacy-gameboy-v1](legacy-gameboy-v1.md). What the
|
||||
sentence protected is kept, and now written down rather than implied: the legacy frame order is
|
||||
this framework's transaction order (legacy-gameboy-v1 section 4); the historical fingerprint,
|
||||
the default version strings and the compatibility string are unchanged and embedded, not
|
||||
recomputed; the legacy clock is proven identical to the rational one (section 3); `FLYSIM01`
|
||||
stays the format of record and its reader untouched until RETIRE-01; and the legacy restore and
|
||||
rollback semantics are declared (`legacy-transient-reset`, `legacy-ratchet-rollback-v1`) rather
|
||||
than approximated. The new identity covers what the old string never did: readout context,
|
||||
decision, decoder configuration and macro channels, executor, slots and restore semantics.
|
||||
|
||||
## 5. Reuse criterion
|
||||
|
||||
Adding a third environment/application requires a backend, task/profile, composition and
|
||||
|
|
|
|||
|
|
@ -164,6 +164,18 @@ delayed rendering retains its handle. Distinguish AssetRef from transient Artifa
|
|||
(`feat/brain-profile-contract`) in the [MaleCNS backlog](../malecns-modular-implementation.md),
|
||||
which has not been built. Not started.
|
||||
|
||||
**2026-09-23:** unblocked. The operator split FOUNDATION-02 (decision of 2026-09-23): its legacy
|
||||
half, PROF-02a, is the [legacy Game Boy composition](legacy-gameboy-v1.md) contract with
|
||||
machine-readable profile, readout-context and decision schemas in `fly-session-types` and
|
||||
`@flybrain/session-types`; the MaleCNS half (PROF-02b) is later and does not gate this slice.
|
||||
AGENT-01 builds the legacy profile `gameboy-legacy-fafb-v783-v1` first. It must: report
|
||||
`brainTicks` equal to the legacy `network.ms` (legacy-gameboy-v1 section 3); consume
|
||||
`gameboy-readout-context-v1` and return `gameboy-channels-v1`; keep the held channel, blocked
|
||||
window and last location as private readout state; report `stimulusRemainingMs`; answer
|
||||
`Agent.Rollback` under capability `legacy-ratchet-rollback-v1`; and choose the mapping from the
|
||||
legacy rate-role names (`command_0`, `macro_*`, which are not `Id`s) to `AgentGraph.rateRoles`,
|
||||
which that contract leaves open.
|
||||
|
||||
**Implement:** adapter over existing LIF, plasticity, retina and fixed readout primitives;
|
||||
reference-first composition/goldens; independently seeded agent state and shared immutable data.
|
||||
Avoid using the old whole-frame `tick` wrapper if it changes the specified phase ordering.
|
||||
|
|
@ -180,6 +192,27 @@ dispatch order and varying worker count preserves results. Keep 64-role limits e
|
|||
RUNTIME-01 (`refactor/environment-task-boundary`) in the same backlog, which has not been
|
||||
built. Not started.
|
||||
|
||||
**2026-09-23:** unblocked. RUNTIME-01's contract is the RT-01a amendments of 2026-09-23 to
|
||||
[workers-v1](workers-v1.md) (sections 1, 3, 4, 5 and the new section 7),
|
||||
[step-v1](step-v1.md) (sections 2, 3, 5 and 6) and [state-media-v1](state-media-v1.md)
|
||||
(sections 2, 3, 4, 5 and 7), with the Game Boy specifics in
|
||||
[legacy-gameboy-v1](legacy-gameboy-v1.md), all implementing the operator's decisions of
|
||||
2026-09-23. ENV-01 and AGENT-01 may proceed in parallel against the schemas and fixtures; ENV-01
|
||||
needs FND-01's trace harness for its "legacy fixtures unchanged" acceptance. ENV-01 must also:
|
||||
add the one read-only bulk memory read to the shim and prove it mutates nothing; publish the
|
||||
memory image per boundary; declare the one-frame setup scaffold; convert audio to f32 and leave
|
||||
the DC blocker to the edge; implement `gameboy-slots-v1`; and make the coordinator refuse
|
||||
`episodeRequest.kind = "rollback"` in any composition that declares no rollback policy (the
|
||||
synthetic coordinator today pauses on every episode request, which is safe but not the rule).
|
||||
|
||||
**Amendment, 2026-09-23 (operator decision of 2026-09-23).** The "Implement" paragraph below said
|
||||
to keep `legacy-gameboy-v1` separately routed. The operator decided on a full port instead: the
|
||||
legacy composition runs on `lockstep-v1` as declared in legacy-gameboy-v1, and "exact old
|
||||
ordering/hash semantics" is kept by construction and by test rather than by a separate route --
|
||||
the frame order maps one to one (section 4), the legacy clock equals the rational one
|
||||
(section 3), and the fingerprint and compatibility string are embedded unchanged (sections 2
|
||||
and 12). The acceptance criteria below stand.
|
||||
|
||||
**Implement:** binjgb environment, task-local memory inspector and identity/existing action
|
||||
adapter. Keep `legacy-gameboy-v1` separately routed with exact old ordering/hash semantics.
|
||||
|
||||
|
|
|
|||
434
docs/design/session-framework/legacy-gameboy-v1.md
Normal file
434
docs/design/session-framework/legacy-gameboy-v1.md
Normal file
|
|
@ -0,0 +1,434 @@
|
|||
# Legacy Game Boy composition v1
|
||||
|
||||
Status: **contract**, 2026-09-23. It covers PROF-02a, the legacy half of the FOUNDATION-02 split,
|
||||
and the Game Boy parts of RT-01a. The generic parts of RT-01a are dated amendments to
|
||||
[worker interfaces](workers-v1.md), [step protocol](step-v1.md) and
|
||||
[session media/state](state-media-v1.md), and they point back here. This document changes no
|
||||
runtime behaviour: `flysim` is unchanged and so is its compatibility string, 648 bytes, sha256
|
||||
`4929f3409b591ae21cf4a6d53e8e758b975c70f424eabb0b37db75c658b9ebd9`.
|
||||
|
||||
## 1. The decision and what it replaces
|
||||
|
||||
On 2026-09-23 the operator decided that the live fly gets a **full port** onto the session
|
||||
framework. It is not kept outside the framework as a separately routed legacy loop. The
|
||||
decisions this document implements are:
|
||||
|
||||
| Subject | Decision |
|
||||
| --- | --- |
|
||||
| FOUNDATION-02 | Split. The legacy profile ships now; MaleCNS bundles ship later (02b, section 17) |
|
||||
| Profile | `gameboy-legacy-fafb-v783-v1`. It embeds today's schema-1 fingerprint, and the kernel `lif-1ms-f64-v2` and plasticity `fly-kc-mbon-rstdp-v2` are unchanged |
|
||||
| Readout context | Location is allowed and declared: `gameboy-readout-context-v1 {boot, bound[], location\|null}` |
|
||||
| Decision | `gameboy-channels-v1`: the eight buttons plus the macro group's winner |
|
||||
| Decoder identity | The decoder and macro-channel configuration go in the composition digest, not the legacy compatibility string |
|
||||
| Environment boundary | Macros run in the coordinator's ActionExecutor. It reads a 64 KiB memory image each boundary, carried as an artifact in `inspection`, plus the ROM as an AssetRef |
|
||||
| Emulator shim | One read-only bulk memory read is added. The joypad stays the only write |
|
||||
| Task and executor | One object, declared as the extension `executor: pokered-macros-v1` |
|
||||
| Audio | The environment converts u8 samples to f32, and the edge applies the DC blocker |
|
||||
| Initialize | `Environment.Initialize` runs one frame with no button pressed |
|
||||
| Rollback | `legacy-ratchet-rollback-v1` plus the environment extension `gameboy-slots-v1` |
|
||||
| Restore | `restore: legacy-transient-reset`, which clears the ledgers, the location and the held channel |
|
||||
| Sugar | Admission reads `reward_remaining` from the last commit's telemetry. A lag of one commit is accepted |
|
||||
| Checkpoint | FLYSIM01 stays the format of record until RETIRE-01 |
|
||||
|
||||
Several earlier statements said the legacy composition stays outside `lockstep-v1`:
|
||||
[README](README.md) section 4, [implementation guide](implementation.md) ENV-01,
|
||||
[state-media-v1](state-media-v1.md) section 7 and the
|
||||
[modular-session analysis](../malecns-modular-sessions.md) section 5.2. Each now carries a
|
||||
dated amendment that cites this decision. None was silently rewritten. The reason for the change is in
|
||||
section 4: the legacy frame order already *is* the lockstep order, with one agent, one port
|
||||
and one world. Moving it onto the framework therefore keeps every ordering fact those
|
||||
statements protected.
|
||||
|
||||
## 2. The profile `gameboy-legacy-fafb-v783-v1`
|
||||
|
||||
Exactly one document exists. Every field of it is fixed, so both the document and its digest are constants of
|
||||
this contract. Any other value is another profile and needs another id. The document is canonical
|
||||
JSON (RFC 8785), its `AssetRef` is `{id: "gameboy-legacy-fafb-v783-v1", format:
|
||||
"fly-profile-v1", digest, byteLength}`, and the digest and length are taken over the canonical
|
||||
bytes. The current values are in `fixtures/gameboy-legacy.json`: digest `41e5d1ac…c60878`, length 1137.
|
||||
|
||||
| Field | Value | Why it is fixed |
|
||||
| --- | --- | --- |
|
||||
| `profileId` | `gameboy-legacy-fafb-v783-v1` | |
|
||||
| `datasetId`, `fingerprintSchema` | `fafb-v783`, `1` | The schema-1 fingerprint is the seven SHA-256 digests joined with `:` |
|
||||
| `datasetFingerprint` | Today's value, byte for byte the compatibility string's segment 2 | This is the "embeds today's schema-1 fingerprint" of the decision. `flysim`'s `legacy_profile_identity` test recomputes it from `data/fafb-v783` |
|
||||
| `kernelVersion`, `plasticityVersion` | `lif-1ms-f64-v2`, `fly-kc-mbon-rstdp-v2` | Pinned defaults (CLAUDE.md). The same test compares them with the built network |
|
||||
| `tickDuration` | `1000000/1` ns | One model tick |
|
||||
| `warmupMs` | `2500` | Fresh-start warm-up with learning disabled, `DEFAULT_WARMUP_MS`. A service configured with another warm-up (`loop.warmup_ms`, `FLYSIM_LOOP_WARMUP_MS`) is **another profile** and needs its own id; the legacy composition refuses to start this profile with any other value. It matters only on a fresh start, because a restore never warms up, but it is identity all the same |
|
||||
| `view` | `lcd`, 160 x 144 | The retina's native frame |
|
||||
| `supportedStimuli` | `["reward-pulse"]` | Sugar and task reward events both drive `stimulate(durationMs)` |
|
||||
| `readoutContextSchema`, `decisionSchema` | The registered references of sections 5 and 6 | |
|
||||
| `legacyExceptions` | `["macro-roles-outside-fingerprint"]` | The `macro_*` roles are merged after the fingerprint is taken ([modular analysis](../malecns-modular-sessions.md) 2.2). This profile declares the gap instead of repairing it |
|
||||
|
||||
The profile does not name the decoder timings or the macro channels. Those belong to the
|
||||
composition (section 12), because the legacy compatibility string never covered them and the
|
||||
decision puts them in the composition digest.
|
||||
|
||||
## 3. Clock
|
||||
|
||||
`stepDuration` is one Game Boy frame: 70224 cycles of a 4194304 Hz clock, which is
|
||||
**`8572265625/512` ns** exactly. The legacy loop accumulates the `f64` constant
|
||||
`1000 / (4194304 / 70224)`. That constant is exactly `548625/32768` ms, because it is dyadic and
|
||||
the division rounds to the true value. Every remainder of the loop's `remainder += ms_per_frame`
|
||||
is therefore a multiple of 2^-15 ms below 32, which is exact in `f64`. As a result, the legacy accumulator and the rational
|
||||
accumulator of [step-v1](step-v1.md) section 5 produce **identical** tick counts and remainders.
|
||||
Both languages assert this over the first 100,000 (Rust) and 20,000 (TypeScript) frames, and
|
||||
the fixture records the first twelve frames: 16, 17, 17, 16, and so on. No separate legacy
|
||||
arithmetic is needed, and step-v1's rule that the clock must not accumulate rounded time holds unchanged. A
|
||||
FLYSIM01 remainder (`f64` ms) converts exactly to a `RationalNs`.
|
||||
|
||||
The task's clock is the agent's brain time. `PreparedDecision.brainTicks` times 1 ms is the
|
||||
legacy `network.ms`, and it counts warm-up. AGENT-01 must make `brainTicks` equal to that value,
|
||||
because the ratchet windows and the reward adapter's timing read it.
|
||||
|
||||
## 4. Placement in `lockstep-v1`
|
||||
|
||||
The legacy `Sim::step_frame` order maps onto the transaction phases one to one:
|
||||
|
||||
| Legacy step | Lockstep phase |
|
||||
| --- | --- |
|
||||
| Drain commands (sugar) | Admission cut at `Ready(k)`. The sugar enters `Prepare.preStepStimulations` (section 15) |
|
||||
| `network.step(ticks)` | Phase A: `Agent.Prepare` advances the ticks |
|
||||
| `decode_bound(rates, ms, boot, blocked, bound)` | Phase A: readout with the context of section 5. The blocked rule stays inside the agent |
|
||||
| `MacroLayer::decide` | Phase B: the `pokered-macros-v1` executor reads O[k]'s memory image (section 10) |
|
||||
| `set_buttons`, `run_frame` | Phase B: one `Environment.Advance` with the complete joypad batch |
|
||||
| Framebuffer, `take_audio_u8` | The environment returns O[k+1]: view, audio chunk, memory image |
|
||||
| `adapter.sample` | Phase C: the task, which is the same object, evaluates old/new inspection once |
|
||||
| `stimulate` per event, `reinforce(sum)` | Phase D: `Agent.Commit` installs the input, then the stimulations in event order, then one reinforcement |
|
||||
| `MacroLayer::observe`, `location()` | Phase C: this produces the next context's `bound` and `location` |
|
||||
| Ratchet observe, capture, recover | Phase C decides. `Environment.SaveSlot` and the rollback run at `Ready(k+1)` (section 11) |
|
||||
| Milestone archive | A durable save at `Ready(k+1)`, exported as FLYSIM01, **after** that boundary's slot save (section 16) |
|
||||
|
||||
**Amended 2026-09-23, review round 1.** One order differs from the legacy loop and is declared.
|
||||
Legacy `track_rank` archives the milestone *before* the ratchet captures, in the same frame, so a
|
||||
legacy archive holds the pre-capture ratchet (`best` = the previous rung) with the previous
|
||||
snapshot. Here the ratchet ledger commits `best = r` in Phase C, and the slot is only filled by
|
||||
`Environment.SaveSlot` at `Ready(k+1)`. A capture ordered before that save would pair `best = r`
|
||||
with the previous slot's contents -- or an empty slot on the first climb -- on every rank climb.
|
||||
Section 16 therefore orders the save first, and a ported archive holds the **post-capture**
|
||||
ratchet and slot. Both are internally consistent; they are not the same bytes.
|
||||
|
||||
The input installed at Commit and ticked at the next Prepare is the frame the legacy loop
|
||||
hands `set_visual_frame` before it samples rewards. Rewards are sampled from the frame just
|
||||
produced. This is the ordering that [modular analysis](../malecns-modular-sessions.md) 2.1 says must not
|
||||
move, and it does not. FND-01's trace harness is where this mapping is proved against the running
|
||||
loop.
|
||||
|
||||
## 5. Readout context `gameboy-readout-context-v1`
|
||||
|
||||
```ts
|
||||
interface GameboyReadoutContext {
|
||||
boot: boolean; // the adapter's boot gate after the last transition
|
||||
bound: ChannelName[]; // the executor's bound macro channels, composition order; [] in raw mode
|
||||
location: { area: number; x: number; y: number } | null; // the adapter's location, or no information
|
||||
}
|
||||
```
|
||||
|
||||
`ChannelName` is `^[a-z][a-z0-9_]{0,63}$`. Decoder channel and rate-role names carry `_`, so
|
||||
they are not `Id`s. The context is what the task hands the decoder with each Prepare (the
|
||||
`initialDecisionContext`, then every `nextDecisionContext`). `bound` is an ordered subset of the
|
||||
composition's `macroChannels`.
|
||||
|
||||
**Location is allowed and declared.** It is task inspection data, and it reaches exactly one
|
||||
place: the readout's blocked-direction window ([readout](../../readout.md), "Blocked-direction
|
||||
cooldown"), which restarts when the location changes. It never reaches the network, it
|
||||
changes no score and it is not neural input. Declaring it here satisfies
|
||||
[workers-v1](workers-v1.md) section 4: the context is typed, bounded, versioned and
|
||||
allowlisted by the profile. The blocked direction itself is **not** in the context. The agent
|
||||
computes it from its own held channel, its own clock, `blockedMs` and the location history.
|
||||
The held channel, the start of the blocked window and the last location are private readout
|
||||
state of the agent.
|
||||
|
||||
## 6. Decision `gameboy-channels-v1`
|
||||
|
||||
```ts
|
||||
interface GameboyChannelsDecision {
|
||||
buttons: { id: "up"|"down"|"left"|"right"|"a"|"b"|"start"|"select"; down: boolean }[]; // all eight, this order
|
||||
macro: ChannelName | null; // the macro-group channel active in this decode
|
||||
}
|
||||
```
|
||||
|
||||
The `buttons` array is the decoder's active set packed in `GAMEBOY_BUTTON_BITS` order, so bit *i* is
|
||||
`buttons[i]`. `macro` is the macro group's channel in the active set, which is always one of the
|
||||
context's `bound` channels. Together they are everything `MacroLayer::decide` reads: the raw
|
||||
mask and the active macro. No port assignment or inspection field is in the decision.
|
||||
|
||||
## 7. Controller
|
||||
|
||||
One port, `ControllerSchema {schema: gameboy-joypad-v1, buttons: [up, down, left, right, a, b,
|
||||
start, select], axes: []}`. The executor's `ControllerIntent` is the joypad mask, and it
|
||||
becomes the port's `PortControl`. It is the only input the environment applies to a running game.
|
||||
|
||||
## 8. Inspection `gameboy-memory-inspection-v1`
|
||||
|
||||
```ts
|
||||
interface GameboyMemoryInspection {
|
||||
memory: ArtifactRef; // 65,536 bytes: $0000..=$FFFF as the CPU sees it at this boundary
|
||||
romDigest: Digest; // == EnvironmentDescriptor.contentDigest == the executor's rom AssetRef digest
|
||||
}
|
||||
```
|
||||
|
||||
- **The image.** Byte *i* is what `fly_gb_read_mem(i)` returns at this boundary, which is
|
||||
what every task and executor read in the legacy loop sees through the per-frame read cache.
|
||||
It is a listed bus attachment, content type `application/octet-stream`. Its digest is optional,
|
||||
because it is a transient live artifact ([state-media-v1](state-media-v1.md) section 1). At
|
||||
59.73 frames per second it is about 3.9 MB/s.
|
||||
- **The shim.** The emulator shim gains one function that fills a 65,536-byte buffer from
|
||||
`emulator_read_mem` in address order. It is read-only. The implementing slice proves this by
|
||||
exporting the emulator state before and after the call, comparing the bytes, and comparing
|
||||
the buffer with 65,536 single reads. Nothing else in the shim changes. `fly_gb_set_buttons` stays
|
||||
the only write, and there is no memory-write path. `read_uncached` is a probe tool and is
|
||||
not available to a task or executor.
|
||||
- **The ROM.** Macros read ROM banks (`MemoryReader::read_rom(bank, address)`) that the image
|
||||
does not map. The executor gets the cartridge as a persistent `AssetRef` in the composition
|
||||
(section 12), and it is refused unless its digest is the environment's `contentDigest`. The
|
||||
image never carries ROM banks beyond the ones the CPU has mapped.
|
||||
- **Retention.** The coordinator keeps O[k]'s image until transition k→k+1 has been evaluated.
|
||||
The executor reads it in Phase B, and the task reads old and new images in Phase C.
|
||||
|
||||
## 9. Environment
|
||||
|
||||
- **Initialize.** The backend configuration declares a setup scaffold of **one frame with no
|
||||
button down**, as the legacy fresh start runs. O[0] follows it, with `engineFrame` `"1"` and
|
||||
`worldTime` `0/1`. That frame's audio is not published: O[0] carries no chunk
|
||||
([state-media-v1](state-media-v1.md) 2), and the audio origin is the first sample of
|
||||
transition 0→1.
|
||||
- **Advance.** Apply the mask, run one frame, and return O[k+1]. The view is `lcd` 160 x 144 `rgba8`
|
||||
(row stride 640) with `observationDelaySteps` 0. `engineFrame` is the legacy frame counter as a
|
||||
decimal string.
|
||||
- **Audio.** The environment converts binjgb's unsigned 8-bit interleaved stereo to f32 with
|
||||
binjgb's host rule, `sample / 255`. The result is unipolar in [0, 1] with silence at 0.0. The
|
||||
stream is `f32le-interleaved`, 2 channels, at the configured rate (48,000 by default). The
|
||||
environment does not filter. The **edge** applies the DC blocker (pole 0.995, per channel)
|
||||
before presentation. It is presentation state: it is reset only when the edge restarts, it
|
||||
is never in a checkpoint, and it never reaches an agent.
|
||||
- **Descriptor.** `stepDuration` is `8572265625/512`, `inspectionSchema` is section 8, `recovery` is
|
||||
`exact-checkpoint`, and `determinism` is `fixed-build`.
|
||||
- **Slots, `gameboy-slots-v1`.** This is an environment capability for `Environment.SaveSlot` and
|
||||
`Environment.RestoreSlot` ([workers-v1](workers-v1.md) section 7). A slot holds the emulator's
|
||||
exported state and the framebuffer that was on screen, as the ratchet's `Snapshot` does. Slot
|
||||
ids are declared by the composition (the legacy composition declares one, `best`). A save
|
||||
replaces the slot. A restore imports the state, releases the buttons and returns the
|
||||
archived framebuffer as a fresh view artifact, with a memory image read after the import.
|
||||
It runs no frame. Every slot is part of the environment's `State.Capture` payload, as
|
||||
FLYSIM01's `ratchet_game` and `ratchet_frame` are today. A slot save due at a boundary
|
||||
completes before any `State.Capture` or FLYSIM01 export at that boundary (section 16).
|
||||
|
||||
## 10. Executor `pokered-macros-v1`
|
||||
|
||||
The Pokémon Red task (reward adapter, ladder, ratchet ledger) and its action executor (the
|
||||
macro layer) are **one object**. It implements both [workers-v1](workers-v1.md) section 4
|
||||
interfaces and is declared as the extension `executor: pokered-macros-v1`. They cannot be
|
||||
split without an undeclared channel between them, for three reasons. The executor's scene
|
||||
observation produces the `bound` set the task hands the decoder. The macros read the adapter's
|
||||
exploration and boundary ledgers. The macro layer's "nearer the objective" is a ratchet progress
|
||||
signal. The object serves exactly one agent on one port.
|
||||
|
||||
- **Phase B** (`ActionExecutor.apply`): inputs are the decision (section 6), O[k]'s memory
|
||||
image, the ROM and the brain clock. The output is a joypad mask plus the macro start and finish
|
||||
events. In raw mode it passes the decision's mask through. While a macro runs, the macro owns the pad.
|
||||
- **Phase C** (`Task.evaluate_transition`): the adapter samples O[k+1]. Each reward event
|
||||
becomes one `Reward` (its value) and one `Stimulus` of kind `reward-pulse` (its
|
||||
`stimulation_ms`), in event order. The macro layer observes O[k+1]. The location is read. The
|
||||
ratchet observes, and may request `SaveSlot` at `Ready(k+1)` or a rollback (section 11). Then
|
||||
the next contexts `{boot, bound, location}` are produced.
|
||||
- **Capture.** The task ledger is the adapter state (FLYSIM01's `reward` chunk) and the
|
||||
ratchet state. The executor's ledgers (blocked, reached, talked, errand) and a running macro
|
||||
are session state. They are **not** captured (section 14).
|
||||
- **Cancel.** `cancel(ms)` abandons a running macro. It is followed by `observe` on the world
|
||||
the fly now stands in.
|
||||
|
||||
## 11. Episode policy `legacy-ratchet-rollback-v1`
|
||||
|
||||
The ratchet's game-only rollback is a declared episode policy. When the ratchet fires during
|
||||
transition k→k+1, the task returns `episodeRequest {kind: "rollback", reason, outcome}`. The
|
||||
outcome is `legacy-ratchet-rollback-v1 {slotId, trigger: "stall" | "game-over"}`. The
|
||||
transition's rewards commit first. The coordinator then applies the policy **at `Ready(k+1)`,
|
||||
before the next Prepare, without pausing**:
|
||||
|
||||
1. If this boundary also has a slot save due, `Environment.SaveSlot` runs first.
|
||||
2. It picks a new epoch e'. `Environment.RestoreSlot(scope e',k+1; slotId, priorEpoch e)` restores
|
||||
the slot and returns O'[k+1]. The boundary number stays the same. `worldTime` continues,
|
||||
`engineFrame` continues, the view is the slot's archived frame, and there is no audio chunk.
|
||||
3. Coordinator-local: the task clears the adapter's transient reward observations. The
|
||||
executor runs `cancel`, then `observe(O'[k+1])`, which gives the next context.
|
||||
4. `Agent.Rollback(scope e',k+1; priorEpoch e, input O'[k+1], context)` runs on every agent.
|
||||
It clears the decoder holds (`clearHolds(now)`: holds, winners, fatigue, lockout) and the
|
||||
plastic eligibility. It installs the slot frame as the next input, drops the held channel,
|
||||
restarts the blocked window at now, and takes the location from the context. It runs **no
|
||||
tick**, no reinforcement, no stimulation and no calibration.
|
||||
5. With every reply in hand, the session is `Ready(e', k+1)`. Any failure fails the epoch and the
|
||||
group restores from the last durable checkpoint. No participant resets alone.
|
||||
6. A durable save follows, as the legacy loop checkpoints after a recovery. Any capture at this
|
||||
boundary -- before or after the rollback -- is taken after step 1's slot save.
|
||||
|
||||
**Worker status and lost replies (amended 2026-09-23, review round 1).** While it executes
|
||||
`Environment.SaveSlot` a worker's `Worker.Status` reports `capturing`; while it executes
|
||||
`Environment.RestoreSlot` or `Agent.Rollback` it reports `restoring`, with `currentScope` still
|
||||
the prior `(e, k+1)`. After the reply it reports `ready` at `(e', k+1)`. These are mutations
|
||||
under the [session RPC](ipc-v1.md) section 5 operation key `(session, e', k+1, method, worker)`
|
||||
(`SaveSlot`: `(session, e, k+1, …)`), so a lost reply goes through ipc-v1 section 6 first: stop
|
||||
dispatch, query `Worker.Status` or retransmit the same request id and body to the same
|
||||
incarnation, and resolve only the matching cached result. Only an unresolvable outcome -- a
|
||||
changed incarnation, lost routes or ownership, `RESULT_EXPIRED` -- fails the epoch, and then the
|
||||
group restores from the last durable checkpoint. No participant is ever left in `e'` while
|
||||
another continues in `e`: the coordinator issues no Prepare until every rollback reply is
|
||||
resolved.
|
||||
|
||||
The following continue through the rollback: brain clock, membrane, RNG, learned gains, rates and
|
||||
reward history, the ratchet ledger (its attempt and lifetime budgets were spent in Phase C), and
|
||||
the adapter's persistent state. The first audio chunk after the rollback marks a
|
||||
discontinuity. This is the legacy `recover_game` sequence with the neural half moved into the agent.
|
||||
Each half touches only its own state, so the order between the halves does not matter.
|
||||
|
||||
## 12. Composition declaration and digest
|
||||
|
||||
```ts
|
||||
interface LegacyGameboyComposition {
|
||||
compositionId: Id;
|
||||
scheduler: "lockstep-v1";
|
||||
profile: AssetRef; // the section 2 document
|
||||
executor: { id: "pokered-macros-v1"; rom: AssetRef; adapter: Id; symbolProvenance: string;
|
||||
mode: "raw" | "macros"; macroChannels: ChannelName[] }; // [] iff raw
|
||||
decoderConfigDigest: Digest; // SHA-256 of the gameboy-decoder-config-v1 form
|
||||
environment: { extensions: ["gameboy-slots-v1"]; slots: Id[]; stepDuration: RationalNs;
|
||||
inspectionSchema: SchemaRef; controllerSchema: SchemaRef; setupFrames: 1;
|
||||
audio: { sampleRate: number; channels: 2 } };
|
||||
episodePolicy: "legacy-ratchet-rollback-v1";
|
||||
restore: "legacy-transient-reset";
|
||||
checkpointFormatOfRecord: "FLYSIM01";
|
||||
flysimCompatibility: string; // the FLYSIM01 string, recorded, not reinterpreted
|
||||
}
|
||||
```
|
||||
|
||||
- `decoderConfigDigest` is the SHA-256 of the canonical JSON of the form
|
||||
`gameboy-decoder-config-v1` of the effective decoder configuration (amended 2026-09-23,
|
||||
review round 1): `{form, exclusive, macros, pulses, clearLockoutMs}`, each group
|
||||
`{channels: [{channel, role}], decisionMs, holdMs, hysteresis, fatigueGain, fatigueDecay,
|
||||
blockedFatigue, blockedMs}` or `null`, each pulse `{channel, role, holdMs, cooldownMs,
|
||||
threshold, boot: {cooldownMs, threshold} | null, throttleGroup: string | null}`. Channels are
|
||||
an array because their order breaks argmax ties and canonical JSON sorts object keys. The
|
||||
shared vectors are `fixtures/gameboy-decoder-config.json` (raw mode and the 31-channel
|
||||
Pokémon Red group): `flysim`'s `legacy_profile_identity` test computes them from
|
||||
`gameboy_decoder_config_with_macros` (and rewrites them under `FLY_UPDATE_FIXTURES=1`), and
|
||||
`@flybrain/session-types` reproduces them from the oracle's `gameboyDecoderConfig`. The
|
||||
example composition carries the real macros-mode digest. A change to a decoder
|
||||
timing, a threshold or the macro channel set therefore changes the composition digest.
|
||||
None of those changes touches the legacy compatibility string, which never covered them.
|
||||
- The declaration's digest is the SHA-256 of its canonical JSON. The coordinator's
|
||||
`compositionDigest` recipe (`fly-session/composition-v1`: session, epoch, contract, one line
|
||||
per agent) gains one final line, `declaration=<digest>`, for a composition that has a
|
||||
declaration. The synthetic composition has none and gains no line.
|
||||
- `flysimCompatibility` must agree with the declaration in its kernel, adapter, fingerprint,
|
||||
plasticity and `pokered:` segments, so the two cannot describe two different flies. Until RETIRE-01
|
||||
the **restore gate** is still that string and the legacy decision rules
|
||||
(`flybrain_gb::compatibility::decide`, `FLY_ACCEPT_ADAPTERS`). The composition digest is the identity
|
||||
for publication, traces and descriptor revisions, not a restore gate. This matches today's
|
||||
behaviour, where a decoder change does not refuse a checkpoint.
|
||||
|
||||
## 13. Machine-readable parts
|
||||
|
||||
| Where | What |
|
||||
| --- | --- |
|
||||
| `fly-session-types/src/gameboy.rs`, `packages/session-types/src/gameboy.ts` | The five registered payload schemas, the profile, the composition declaration and their readers and cross-checks |
|
||||
| `fly-session-types/src/extensions.rs`, `packages/session-types/src/extensions.ts` | `SaveSlot*`, `RestoreSlot*` and `AgentRollback*` payloads, which are in the session schema set |
|
||||
| `fixtures/gameboy-legacy.json` (derived) | The extension set and its digest, every `SchemaRef`, the profile document with its canonical bytes and `AssetRef`, the frame clock, and an example composition with its digest and the digest recipe |
|
||||
| `fixtures/gameboy-decoder-config.json` | The `decoderConfigDigest` vectors (raw and Pokémon Red macros), written and checked by `flysim`'s `legacy_profile_identity` test, reproduced by the TypeScript oracle |
|
||||
| `TraceBehaviour.boundaryActions`, `TraceOperational.captures` | The section 16 order rule, refused by `TransitionTrace` validation in both languages |
|
||||
| `fixtures/valid.json`, `invalid.json` | Accepted and refused cases for every new type, held to both languages |
|
||||
| `flysim/tests/legacy_profile_identity.rs` | Recomputes the fingerprint, versions, frame size, warm-up, clock and button order from the committed dataset and the service defaults |
|
||||
|
||||
A payload schema's `SchemaRef.digest` is the SHA-256 of its canonical declaration
|
||||
`{registry, id, version, source, fields}`. The legacy schemas are digested by their own
|
||||
extension set, not by `contractDigest`: the session contract stays free of console state.
|
||||
The generic changes of RT-01a (`stimulusRemainingMs`, `EpisodeRequestKind.rollback`, the six
|
||||
extension payloads, `maxSlots`) *are* in the session schema set, and they moved
|
||||
`contractDigest` to the value in `fixtures/contract-digest.json`. Regenerate with
|
||||
`cargo run -p fly-session-types --example update_fixtures`. `tests/schema_set.rs` refuses
|
||||
stale files.
|
||||
|
||||
**Open for AGENT-01:** the legacy rate roles (`command_0`, `macro_go_item`, and so on) are not
|
||||
valid `Id`s, while `AgentGraph.rateRoles` and `AgentTelemetry.rates[].roleId` are `Id`s. The
|
||||
mapping belongs to the agent adapter. This contract does not choose it.
|
||||
|
||||
## 14. Restore `legacy-transient-reset`
|
||||
|
||||
The legacy composition declares that a restore (a FLYSIM01 load today, and any group restore
|
||||
under this composition) is **not** an exact replay. The restored parts are those FLYSIM01
|
||||
carries: agent state, emulator, framebuffer, adapter state, ratchet state and slots, frame
|
||||
counter, remainder, buttons and event watermark. The rest starts cleared, as it does in a fresh
|
||||
process:
|
||||
|
||||
- the executor's ledgers (blocked, reached, talked, errand) are empty, with no macro running;
|
||||
- the agent's readout transient starts exactly as a fresh legacy process has it (amended
|
||||
2026-09-23, review round 1): no held channel, no last location, and the blocked window
|
||||
starting at brain time **0 ms**, not at the restored clock. The decoder state itself
|
||||
(`DecoderState`: holds, winners, fatigue) *is* restored. The consequence AGENT-01 must
|
||||
reproduce: on the first decode after a restore, `now - 0 >= blockedMs`, so the restored
|
||||
direction winner, if any, is passed as `blocked` and its fatigue is raised to
|
||||
`blockedFatigue`. Only after that decode does the window restart, because the held channel
|
||||
changed from none to the winner, and again when the first location is observed. A rollback
|
||||
(section 11) differs: it clears the holds and winners and restarts the window at the
|
||||
current brain time;
|
||||
- the adapter's transient observations are cleared, as they are on a rollback.
|
||||
|
||||
Resume tests compare against the legacy restore outcome, not against an uninterrupted trace
|
||||
([state-media-v1](state-media-v1.md) section 4 amendment). This is the property the operator's
|
||||
unstick procedure depends on: restarting the service clears the ledger-shaped traps and keeps
|
||||
the rung.
|
||||
|
||||
## 15. Sugar admission
|
||||
|
||||
The coordinator owns admission, with the legacy rules: the per-minute limiter, and "no overlap
|
||||
with an active pulse". The pulse is read from `AgentTelemetry.stimulusRemainingMs` of the **last
|
||||
completed commit** (an `Agent.Rollback` reply counts as one). The value can therefore be one
|
||||
commit old, and the operator accepted this lag. The consequences, each bounded to one frame:
|
||||
|
||||
- a pulse that ended inside the in-flight transition still reads as running, so the request is
|
||||
refused and retried;
|
||||
- a reward pulse added by the in-flight transition is not yet visible, so a sugar can be
|
||||
admitted over it where the legacy loop would have refused;
|
||||
- after a restore, and until the first commit of the new epoch, the pulse is unknown and every
|
||||
request is refused with a retry, where the legacy loop admits against the restored pulse at
|
||||
once (amended 2026-09-23, review round 1).
|
||||
|
||||
The duration is clamped to `[1, sugar_max_ms]`. An admitted sugar is a `reward-pulse`
|
||||
`Stimulus` in the next Prepare's `preStepStimulations`, which is the position of the legacy
|
||||
drain at the top of a frame. Legacy admission *is* application. Here, the epoch can fail
|
||||
between the two, so each admission record carries its interaction id. If the Prepare that
|
||||
applies it never commits, the admission is reported aborted and the edge refunds it. The
|
||||
bridge's fulfil path and refund path both get tests in the slice that wires them
|
||||
([workers-v1](workers-v1.md) section 5 amendment).
|
||||
|
||||
## 16. Checkpoint format of record
|
||||
|
||||
**Order at a boundary (amended 2026-09-23, review round 1).** A slot save due at `Ready(k)`
|
||||
completes -- its `Environment.SaveSlot` reply in hand -- before any `State.Capture` or FLYSIM01
|
||||
export at `Ready(k)`, whether that export is periodic, a milestone archive or the post-rollback
|
||||
save. So a checkpoint whose ratchet ledger names `best = r` always carries the slot saved for
|
||||
rung `r`. This is a **declared difference** from the legacy loop, which archives a milestone
|
||||
before capturing the ratchet snapshot. A legacy milestone archive holds `best = r-1` and the
|
||||
rung `r-1` snapshot; a ported one holds `best = r` and the rung `r` snapshot. After
|
||||
`fly-reset-to-milestone` onto a ported archive, a stall rollback therefore returns to the
|
||||
milestone boundary itself rather than to the previous rung's save, and the attempt counter
|
||||
starts at the new rung. The operator's confirmation of this difference is requested with the
|
||||
CUT-01 shadow run. The rule is machine-checked in the step trace: `TraceBehaviour.boundaryActions`
|
||||
records the saves and the rollback in order, `TraceOperational.captures` records each capture
|
||||
with the number of boundary actions before it, and a `TransitionTrace` in which a capture
|
||||
precedes a slot save is refused ([step-v1](step-v1.md) section 8 amendment; fixtures in
|
||||
`valid.json` and `invalid.json`).
|
||||
|
||||
FLYSIM01 stays the format of record until RETIRE-01. Every durable save of this composition
|
||||
(periodic, milestone archive, after a rollback) **exports a FLYSIM01 envelope** that the
|
||||
current `flysim` reads, under the unchanged compatibility string. The deploy gate
|
||||
(`--print-compatibility`) and `fly-reset-to-milestone` keep working on those files. A FLYSESS1
|
||||
checkpoint may be written beside it, but it is not what a restore selects until RETIRE-01
|
||||
says so.
|
||||
|
||||
## 17. PROF-02b: MaleCNS bundles (later)
|
||||
|
||||
Dataset manifests, original-ID mapping, anatomical roles, sensory and readout bindings, strict
|
||||
graph validation for new bundles, and the composite behaviour identity of FOUNDATION-02 are
|
||||
**not** in this document. They ship with PROF-02b, before DATA-01, as their own contract.
|
||||
Nothing here constrains them, except that a new profile never reuses this profile's id or its
|
||||
legacy exception.
|
||||
|
|
@ -87,6 +87,16 @@ profile. Resizing for viewers, overlays, composition, audio mixing/resampling, e
|
|||
browser delivery and streaming belong to the application/presentation layer. No bus or
|
||||
generic session configuration assumes a 1080p show or Twitch output.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** For the Game Boy
|
||||
environment of the legacy composition ([legacy-gameboy-v1](legacy-gameboy-v1.md) section 9):
|
||||
the environment converts binjgb's unsigned 8-bit interleaved stereo to the declared
|
||||
`f32le-interleaved` with binjgb's host rule `sample / 255` (unipolar, silence at 0.0), and does
|
||||
not filter. The DC blocker the running service applies before publishing (pole 0.995, per
|
||||
channel) moves to the **edge**, as presentation: its state is the edge's, never checkpointed,
|
||||
reset only when the edge restarts, and never seen by an agent. The observations a slot restore
|
||||
returns carry no audio chunk, like the ones `ActivateRestore` returns (section 5 amendment of
|
||||
2026-09-22), and the next chunk marks the discontinuity.
|
||||
|
||||
640×480 RGBA at 60 fps produces 73.728 MB/s of raw image data. Artifact fan-out references
|
||||
one stored object; reads and any staging/seal copy still consume memory bandwidth. This is
|
||||
reasonable to measure before introducing codecs or pooled GPU buffers. Native dimensions
|
||||
|
|
@ -117,6 +127,13 @@ If it violates configured resource policy, disconnect/restart that observer inst
|
|||
live data or silently skipping simulation input. Global store exhaustion is an explicit fault
|
||||
or pause condition; the router cannot guess that a particular live object is disposable.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a).** An artifact-backed inspection -- the legacy composition's
|
||||
64-KiB memory image per boundary -- is required coordinator input, not spectator data: the
|
||||
coordinator retains `O[k]`'s image through the executor's use in the next transition's Phase B
|
||||
and the task's old/new evaluation in its Phase C, and drops it after. It is never coalesced and
|
||||
never published to observers by the session; about 3.9 MB/s at the Game Boy's cadence, budgeted
|
||||
with the cached step observations above.
|
||||
|
||||
**Amendment, 2026-09-22 (PUBLISH-01).** "Disconnect/restart that observer" names an action
|
||||
no participant can take under [Flybus v1](bus-v1.md). Section 5 there makes publish admission
|
||||
all or nothing -- "for a bounded subscriber overflow, reject the **whole** publish; no partial
|
||||
|
|
@ -152,6 +169,19 @@ The manifest records:
|
|||
or reproducible reconstruction inputs, admission state and event watermarks.
|
||||
- Payload names, lengths and hashes, including external-helper state required for exact resume.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** A composition may declare
|
||||
**restore semantics** other than exact. The legacy composition declares
|
||||
`restore: legacy-transient-reset` ([legacy-gameboy-v1](legacy-gameboy-v1.md) section 14): its
|
||||
executor's ledgers and running macro, the agent's readout transient (held channel, blocked
|
||||
window, last location) and the task's transient observations are **not** captured and start
|
||||
cleared on every restore, exactly as the running service does on a restart. The
|
||||
[modular analysis](../malecns-modular-sessions.md) section 5.4 already asked for this to be
|
||||
labelled "legacy continuation semantics, not exact session replay"; the declaration is that
|
||||
label, in the composition digest. The consequence is explicit: for this composition the
|
||||
uninterrupted-versus-resumed trace equality of STATE-01 does not apply; its resume tests
|
||||
compare against the legacy restore outcome. The operator's unstick procedure depends on the
|
||||
reset. An environment's slots (`gameboy-slots-v1`) **are** world state and are captured.
|
||||
|
||||
Use a new envelope version; specify exact byte layout before production files. The historical
|
||||
letter-only chunk-name constraint is not silently widened, and FLYSIM01 remains separately
|
||||
readable. Persist payload **bytes and durable content identity**, not transient bus storeId,
|
||||
|
|
@ -232,6 +262,13 @@ provide externally atomic resume, advertise episode-restart, not exact-checkpoin
|
|||
activation acknowledgments, install the coordinator's staged task/executor/admission state
|
||||
and establish Paused(new epoch,k). Failure during activation never permits half a group to run.
|
||||
|
||||
**Amendment, 2026-09-23 (operator decision of 2026-09-23).** `FLYSIM01` remains the **format of
|
||||
record** for the legacy composition until RETIRE-01: every durable save exports a `FLYSIM01`
|
||||
envelope the current service reads under its unchanged compatibility string, and restore
|
||||
selects from those files through the legacy compatibility decision. A `FLYSESS1` checkpoint
|
||||
may be written beside it; it is not a restore candidate for this composition until RETIRE-01
|
||||
says so ([legacy-gameboy-v1](legacy-gameboy-v1.md) section 16).
|
||||
|
||||
## 6. Durable commit, router failure and recovery
|
||||
|
||||
Write payload/envelope temporary generation, fsync, rename, fsync directory, then atomically
|
||||
|
|
@ -264,3 +301,14 @@ state and retained/fresh brain components are explicit. Gain retention, eligibil
|
|||
clearing, calibration and first sensory input are part of the policy, tested independently.
|
||||
Legacy Pokémon ratchet behavior remains in the legacy composition. Shared competitive worlds
|
||||
never restore one player's environment independently of the other players.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** The sentence above kept the
|
||||
ratchet outside the framework. The operator decided on a full port instead, so the ratchet's
|
||||
game-only rollback is now a declared episode policy of the legacy composition,
|
||||
`legacy-ratchet-rollback-v1`, with the environment capability `gameboy-slots-v1`
|
||||
([workers-v1](workers-v1.md) section 7, [step-v1](step-v1.md) section 6 amendment,
|
||||
[legacy-gameboy-v1](legacy-gameboy-v1.md) section 11). It is a rollback, not a reset: a new
|
||||
epoch at the same boundary, the episode and the brain continuing, the environment restoring a
|
||||
slot, agents clearing holds and eligibility and installing the slot frame without a tick, the
|
||||
executor cancelling then observing. What the original sentence protected still holds: the
|
||||
policy is single-agent and a shared competitive world must not declare it.
|
||||
|
|
|
|||
|
|
@ -5,6 +5,13 @@ arrows below are RPCs through the same [Flybus router](bus-v1.md); the router it
|
|||
implements the barrier. Read [architecture](README.md) and [session RPC](ipc-v1.md) first. Method payloads are in
|
||||
[worker interfaces](workers-v1.md).
|
||||
|
||||
**Amendment, 2026-09-23 (operator decision of 2026-09-23).** The live Game Boy fly is ported
|
||||
onto this protocol as the legacy composition of [legacy-gameboy-v1](legacy-gameboy-v1.md),
|
||||
scheduled by `lockstep-v1` with one agent, one port and one world; it is no longer a separate
|
||||
ordering. Its frame order is this document's transaction order (legacy-gameboy-v1 section 4
|
||||
maps it step by step), so the amendments below add capabilities to the protocol and change
|
||||
none of its ordering rules.
|
||||
|
||||
## 1. Committed boundary
|
||||
|
||||
At `Ready(epoch, k)`:
|
||||
|
|
@ -45,6 +52,19 @@ during a transition is served by the ordinary `Committing(k) → Ready(k+1)` edg
|
|||
transition to finish first, so the only boundary such a pause can land on is the one the
|
||||
transition just committed.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** One edge is added, for a
|
||||
composition that declares a rollback policy:
|
||||
|
||||
```text
|
||||
Ready(e, k) → RollingBack(e', k) → Ready(e', k)
|
||||
```
|
||||
|
||||
It is taken only when the transition that reached `k` returned `episodeRequest.kind =
|
||||
"rollback"`, after that transition fully committed and before the next Prepare (section 6
|
||||
amendment). The boundary number does not change; the epoch does. A pause requested during it
|
||||
lands on `Ready(e', k)`; any failure inside it is `Failed → Restoring(new epoch)`. Capture is
|
||||
not allowed in `RollingBack`.
|
||||
|
||||
## 3. Transaction sequence
|
||||
|
||||
### Phase A: prepare all agents concurrently
|
||||
|
|
@ -76,6 +96,12 @@ After every PreparedDecision arrives:
|
|||
2. Run each task-local action executor once, in sorted agent-ID order, against coherent current
|
||||
game state, task progress/objectives and clock from this boundary.
|
||||
Direct-control profiles use an identity executor. Macro profiles are explicit extensions.
|
||||
|
||||
*Amendment, 2026-09-23 (RT-01a):* the legacy composition's extension is
|
||||
`pokered-macros-v1`. "Coherent current game state" is the boundary's 64-KiB memory image
|
||||
from `O[k].inspection` plus the ROM `AssetRef` -- never a live emulator read -- and "clock"
|
||||
is the agent's brain time after its Prepare ([legacy-gameboy-v1](legacy-gameboy-v1.md)
|
||||
sections 8 and 10).
|
||||
3. Assemble all configured port controls in descriptor port order; reject duplicates/missing
|
||||
ports. Uncontrolled ports are configured neutral before the epoch, not supplied ad hoc.
|
||||
4. Send exactly one `Environment.Advance(scope=k, batchId, controls)`.
|
||||
|
|
@ -96,6 +122,17 @@ controls. It returns scoped rewards/stimulation, next decision contexts, progres
|
|||
an optional episode request. Commit its ledger update in memory and retain the result for
|
||||
this transition. No task output directly writes controllers or neural state.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a).** The task may also ask for two things that happen at the
|
||||
boundary this transition reaches, after Phase D, never inside it: a slot save
|
||||
(`Environment.SaveSlot`, composition capability `gameboy-slots-v1`) and a rollback
|
||||
(`episodeRequest.kind = "rollback"`). Both are recorded with the transition's result and
|
||||
applied by the coordinator in the order *save, then rollback* (section 6 amendment). A slot
|
||||
save due at a boundary completes before any `State.Capture` or FLYSIM01 export at that boundary
|
||||
(amended 2026-09-23, review round 1; [legacy-gameboy-v1](legacy-gameboy-v1.md) section 16). The
|
||||
retained old inspection is what makes "evaluate once against old/new inspection" possible when
|
||||
the inspection is artifact-backed: the coordinator keeps `O[k]`'s image until this evaluation
|
||||
finishes.
|
||||
|
||||
### Phase D: commit all agent outcomes concurrently
|
||||
|
||||
Send `Agent.Commit(scope=k)` with that agent's next sensory observation and routed outcomes.
|
||||
|
|
@ -171,6 +208,15 @@ Example: a synthetic 60-Hz environment with a 1-ms model tick produces 16,17,17
|
|||
over three steps, totaling 50. A real backend's measured/declared emulated cadence may
|
||||
differ; never substitute this example's duration for Game Boy or Dolphin clocks.
|
||||
|
||||
**Amendment, 2026-09-23 (PROF-02a).** The Game Boy's declared cadence is one frame of 70224
|
||||
cycles at 4194304 Hz, `stepDuration = 8572265625/512` ns. The legacy service accumulates the
|
||||
`f64` constant `1000 / (4194304 / 70224)` ms, which is exactly `548625/32768` ms, and every
|
||||
remainder it produces is a multiple of 2^-15 ms below 32 -- exact in `f64`. The legacy
|
||||
"floating remainder arithmetic" and this section's rational accumulator therefore give identical
|
||||
ticks and remainders for every frame; both implementations assert it and
|
||||
`fixtures/gameboy-legacy.json` records the first twelve frames (16, 17, 17, 16, ...). No legacy
|
||||
exception to this section is needed ([legacy-gameboy-v1](legacy-gameboy-v1.md) section 3).
|
||||
|
||||
Wall time is only for pacing, health and presentation. The coordinator schedules absolute
|
||||
deadlines after committed boundaries; when behind, it omits sleep and reports lag. It does
|
||||
not skip world steps, drop neural ticks, or let one agent advance more slowly than another.
|
||||
|
|
@ -200,6 +246,39 @@ exactly what is retained, cleared, warmed or recalibrated. No worker independent
|
|||
Changing port assignment, agent membership, model/profile, cadence or task schema requires
|
||||
a new composition/epoch. Hot-join and hot-swap during an active match are not v1 capabilities.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** A declared rollback policy
|
||||
is an episode policy that does **not** pass through Paused and does **not** start a new episode.
|
||||
For `legacy-ratchet-rollback-v1` ([legacy-gameboy-v1](legacy-gameboy-v1.md) section 11), once
|
||||
every Commit of the transition that reached `k` has succeeded:
|
||||
|
||||
1. If the same transition asked for a slot save, `Environment.SaveSlot(scope e,k)` first.
|
||||
2. Choose a new epoch `e'`. `Environment.RestoreSlot(scope e',k; priorEpoch e)` returns the
|
||||
restored `O'[k]`: same boundary, `worldTime` and `engineFrame` continue, no audio chunk.
|
||||
3. Coordinator-local: the task clears its transient observations; the executor cancels any
|
||||
running action and observes `O'[k]`, which yields the next decision contexts.
|
||||
4. `Agent.Rollback(scope e',k; priorEpoch e)` on every agent concurrently: holds and
|
||||
eligibility cleared, `O'[k]`'s view installed, no tick.
|
||||
5. With every reply in hand: `Ready(e', k)`, then the usual durable save. Every capture at
|
||||
this boundary, before or after the rollback, follows step 1.
|
||||
|
||||
A worker reports `capturing` during `SaveSlot` and `restoring` during `RestoreSlot` or
|
||||
`Agent.Rollback`. A lost reply is resolved by [session RPC](ipc-v1.md) section 6 against the
|
||||
same operation key before anything fails the epoch (legacy-gameboy-v1 section 11).
|
||||
|
||||
Exactly what is retained, cleared and installed is named by the policy, as this section already
|
||||
requires; nothing is reset by a worker on its own initiative, and a failure at any step fails
|
||||
the epoch and restores the group coherently. "Reset uses a new epoch/episode and step 0"
|
||||
remains the rule for terminal episodes; a rollback keeps the episode and the step numbering
|
||||
because the brain, the audience's history and the frame counter all continue across it, as they
|
||||
do in the running service. The policy is single-agent: a shared competitive world must not
|
||||
declare it, because it would rewind one world under every player on one player's stall, which
|
||||
[state-media-v1](state-media-v1.md) section 7 forbids.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a).** Sugar admission in the legacy composition reads the pulse
|
||||
from the last completed commit's telemetry, so an admission decided while a transition is in
|
||||
flight is at most one commit stale; the admitted stimulus still enters only at the next
|
||||
admission cut of section 3, Phase A ([workers-v1](workers-v1.md) section 5 amendment).
|
||||
|
||||
## 7. Failure rules
|
||||
|
||||
| Failure point | Required response |
|
||||
|
|
@ -227,6 +306,19 @@ The synthetic integration test must record, for every transition:
|
|||
- Observation producing boundaries and task event/outcome IDs in order.
|
||||
- All Commit acknowledgments and published committed boundary.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a, review round 1).** For a composition with boundary actions the
|
||||
record also carries, for the boundary the transition reached:
|
||||
|
||||
- in behaviour, `boundaryActions`: every `Environment.SaveSlot` (slot id and saved state
|
||||
digest) and a rollback (slot id), in the order applied -- saves first, each slot once, at most
|
||||
one rollback, last. FND-01's harness compares it like any other behaviour field;
|
||||
- in operational metadata, `captures`: every checkpoint capture or FLYSIM01 export at that
|
||||
boundary, in the order taken, each with the number of boundary actions already applied.
|
||||
Captures are operational because their schedule is wall-clock policy.
|
||||
|
||||
A trace whose capture precedes one of the boundary's slot saves, or counts more actions than
|
||||
were applied, is refused. The synthetic composition records both lists empty.
|
||||
|
||||
Evaluate agents sequentially, concurrently, and in reversed dispatch/completion order. All
|
||||
committed state/action/reward results must match, excluding wall time, request IDs and other
|
||||
explicitly operational metadata. Delayed/lost/duplicate messages must not add a neural tick,
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ rpc.result. Large inputs/outputs use owned bus attachments, never another worker
|
|||
| `State.Capture` | Coordinator → agent/environment | Immutable snapshot of committed boundary |
|
||||
| `State.StageRestore` | Coordinator → agent/environment | Validate replacement state under new epoch |
|
||||
| `State.ActivateRestore` | Coordinator → agent/environment | Install staged state; remain quiescent |
|
||||
| `Environment.SaveSlot` | Coordinator → environment | Capability `gameboy-slots-v1`; record a slot at the committed boundary (section 7) |
|
||||
| `Environment.RestoreSlot` | Coordinator → environment | Capability `gameboy-slots-v1`; boundary k under a new epoch (section 7) |
|
||||
| `Agent.Rollback` | Coordinator → agent | Capability `legacy-ratchet-rollback-v1`; Ready(e,k) → Ready(e',k), no tick (section 7) |
|
||||
|
||||
State methods have payloads in [state and media](state-media-v1.md). Artifact lifetime and
|
||||
message consumption are bus operations managed by the SDK, not Worker/Coordinator methods.
|
||||
|
|
@ -51,6 +54,8 @@ interface AgentTelemetry {
|
|||
rates: { roleId: Id; hz: number }[];
|
||||
learning: { enabled: boolean; updates: U64; changed: U64; signal: number };
|
||||
}
|
||||
// Amendment 2026-09-23: AgentTelemetry also carries
|
||||
// stimulusRemainingMs: number | null; // pulse still running after the operation; null = reports none
|
||||
```
|
||||
|
||||
`AssetRef` names persistent content in a preprovisioned local registry; it is not an arbitrary path or
|
||||
|
|
@ -75,6 +80,15 @@ finite; shipped positive-only task profiles reject negatives. Empty rewards do n
|
|||
different numerical rule. `id`/`eventId` is unique within its outcome or command namespace;
|
||||
the coordinator assigns stable IDs before sending a mutating request.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** `AgentTelemetry` gains
|
||||
`stimulusRemainingMs: number | null`: the milliseconds of stimulation pulse still running when
|
||||
the operation that reports the telemetry completed, or `null` for an agent that has no pulse to
|
||||
report. The operator decided that sugar admission reads the legacy `reward_remaining` from the
|
||||
last commit's telemetry (section 5 amendment), and no field carried it. It is finite and
|
||||
nonnegative; it is a report, never an input. The field is required-and-nullable like every
|
||||
optional field in these contracts, so every existing producer now writes `null`. It changes
|
||||
`contractDigest`, which [session RPC](ipc-v1.md) section 4 already provides for.
|
||||
|
||||
## 2. Agent methods
|
||||
|
||||
### Agent.Initialize
|
||||
|
|
@ -283,6 +297,25 @@ The environment only needs backend-relevant portions of task setup, not reward r
|
|||
neural policies. `taskConfig` resolves a declared setup configuration; the complete task
|
||||
implementation and ledger stay in the coordinator.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** Three readings for the
|
||||
Game Boy environment of the legacy composition ([legacy-gameboy-v1](legacy-gameboy-v1.md)
|
||||
sections 8 and 9), stated here because they are about this method's shape:
|
||||
|
||||
- *Setup scaffold.* The backend configuration may declare setup frames the environment runs
|
||||
with every control neutral before it returns O[0]. The legacy composition declares exactly
|
||||
**one frame with no button down**, which is what its fresh start does; O[0] then has
|
||||
`engineFrame` `"1"`, `worldTime` `0/1` and no audio chunk. These frames are declared
|
||||
scaffold, attributed to no fly, and never a transition.
|
||||
- *Inspection may be artifact-backed.* `inspection` is a `TypedValue` under the descriptor's
|
||||
schema; the legacy schema `gameboy-memory-inspection-v1` carries a 64-KiB memory image as an
|
||||
`ArtifactRef` (listed in the bus attachments) plus the ROM's digest. This is the "explicit
|
||||
artifact-backed schema" section 1 requires for typed state over 32 KiB, and it is the one
|
||||
bulk transfer per boundary the section 4 rule against per-byte remote reads asks for. The
|
||||
environment's only write to a running game remains the controller batch.
|
||||
- *Audio.* The environment publishes native samples in the declared f32 format; converting a
|
||||
backend's integer samples to f32 is the environment's job (binjgb: `sample / 255`), and any
|
||||
filtering for listening -- the legacy DC blocker -- is presentation, applied by the edge.
|
||||
|
||||
### Environment.Advance
|
||||
|
||||
```ts
|
||||
|
|
@ -349,6 +382,18 @@ but not a port assignment. The coordinator supplies the port. Per-agent executor
|
|||
private; a running macro may emit controls according to its declared policy, but only after
|
||||
neural selection. The first implementation supports the stateless identity executor only.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** The legacy composition
|
||||
declares the extension **`executor: pokered-macros-v1`**: its task and its action executor are
|
||||
**one object** implementing both interfaces above, serving one agent on one port
|
||||
([legacy-gameboy-v1](legacy-gameboy-v1.md) section 10). They share state that neither could
|
||||
reach through a declared channel if split: the executor's scene observation is the `bound` set
|
||||
the task hands the decoder, the macros read the task's exploration ledgers, and the macro layer's
|
||||
progress signal feeds the ratchet. The executor reads the boundary's memory image and the ROM
|
||||
`AssetRef`, never the emulator. "Stateless identity executor only" remains true of the
|
||||
synthetic composition; a stateful executor is permitted exactly where a composition declares
|
||||
one by name, and its state is captured or declared transient by that composition's restore
|
||||
semantics (state-media-v1 section 4 amendment).
|
||||
|
||||
The executor's currentGameState is a coherent read-only inspector view at this boundary;
|
||||
progressView supplies task history/objectives. It updates its selected action every step
|
||||
(movement, path replanning, interaction, completion), not merely replaying a blind button
|
||||
|
|
@ -372,6 +417,15 @@ not arbitrary raw inspector memory or incoming chat.
|
|||
It requests a coordinator-owned policy transition after final reward commit; it cannot reset
|
||||
the environment directly. Generic progress is a TypedValue, not mandatory Pokémon ladder data.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** `episodeRequest.kind` is
|
||||
`"terminal"` or **`"rollback"`**. A rollback request asks for the composition's declared
|
||||
rollback policy; its `outcome` is that policy's registered schema. The only policy defined is
|
||||
`legacy-ratchet-rollback-v1` (outcome `{slotId, trigger}`), applied at the boundary the
|
||||
transition just committed without a pause ([step-v1](step-v1.md) section 6 amendment). A
|
||||
composition that declares no rollback policy treats the request as a task failure. It still
|
||||
"cannot reset the environment directly": the coordinator applies the policy through the
|
||||
section 7 methods.
|
||||
|
||||
## 5. Admission and audience boundary
|
||||
|
||||
The first synthetic implementation has no audience input. Later integration maps permitted
|
||||
|
|
@ -386,6 +440,19 @@ v2 contract must specify accepted/applied/rolled-back/aborted states and reconci
|
|||
paid interactions are enabled. Do not inherit a claim of durable exactly-once stimulation
|
||||
from these in-memory worker request caches.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** Sugar in the legacy
|
||||
composition is admitted by the coordinator with the legacy rules -- the rate limiter and "no
|
||||
overlap with an active pulse" -- reading the pulse from `stimulusRemainingMs` of the **last
|
||||
completed commit**. The operator accepted that this value can be one commit old; both
|
||||
consequences are bounded to one frame and are listed in
|
||||
[legacy-gameboy-v1](legacy-gameboy-v1.md) section 15. Admitted sugar is a profile-supported
|
||||
`reward-pulse` stimulus in the next Prepare's `preStepStimulations`. Because admission and
|
||||
application are now separate, each admission record carries its interaction id and ends
|
||||
`applied` or, if the epoch fails before that Prepare commits, `aborted`; the edge fulfils the
|
||||
first and refunds the second, and the slice wiring the bridge tests both. This is the
|
||||
accepted/applied/aborted distinction the paragraph above asks for, for this one interaction
|
||||
kind; paid interactions in general still need the public v2 contract.
|
||||
|
||||
## 6. Health, shutdown and extensions
|
||||
|
||||
All workers implement the common Hello/Status/Shutdown/Acknowledge methods. Capture/restore
|
||||
|
|
@ -395,3 +462,56 @@ advertised. Unsupported methods return `UNSUPPORTED`, mutation none.
|
|||
New task-specific fields belong in registered TypedValue schemas. New worker capabilities,
|
||||
variable-duration stepping, subscriptions or additional sensor modalities require a contract
|
||||
change and shared fixtures. An unconstrained plugin dictionary is not a substitute for that.
|
||||
|
||||
## 7. Extension methods (amendment, 2026-09-23)
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** The operator decided on a
|
||||
full port of the live fly, whose ratchet rolls the *game* back to a saved state while the brain
|
||||
continues. Neither half of that is expressible with sections 2 and 3: there is no method that
|
||||
saves or restores world state outside a coherent group checkpoint, and none that installs a new
|
||||
input in an agent without a transition. These three methods are that, generically shaped, each
|
||||
behind a capability a worker advertises in `Worker.Hello`; a worker without it answers
|
||||
`UNSUPPORTED`, mutation none. Payloads are in `fly-session-types` (`extensions`) and
|
||||
`@flybrain/session-types`, in the session schema set.
|
||||
|
||||
```ts
|
||||
// Environment.SaveSlot -- capability gameboy-slots-v1. Scope: the committed boundary (e, k).
|
||||
interface SaveSlotParams { slotId: Id } // a slot the composition declares
|
||||
interface SaveSlotResult { slotId: Id; boundary: U64; // == scope.step
|
||||
stateDigest: Digest; byteLength: U64 }
|
||||
|
||||
// Environment.RestoreSlot -- capability gameboy-slots-v1. Scope: (e', k), a NEW epoch.
|
||||
interface RestoreSlotParams { slotId: Id; priorEpoch: Id; // the environment is Ready(e, k)
|
||||
policy: "legacy-ratchet-rollback-v1" }
|
||||
interface RestoreSlotResult { slotId: Id; committedStep: U64; // == k: no transition ran
|
||||
observation: WorldObservation } // boundary k, no audio chunk
|
||||
|
||||
// Agent.Rollback -- capability legacy-ratchet-rollback-v1. Scope: (e', k), the same new epoch.
|
||||
interface AgentRollbackParams { agentId: Id; priorEpoch: Id; // the agent is Ready(e, k)
|
||||
policy: "legacy-ratchet-rollback-v1";
|
||||
input: SensoryInput; // boundary k: the restored world
|
||||
decisionContext: TypedValue } // for the next Prepare
|
||||
interface AgentRollbackResult { agentId: Id; committedStep: U64; // == k: no tick ran
|
||||
decisionContextDigest: Digest; telemetry: AgentTelemetry }
|
||||
```
|
||||
|
||||
- Both environment methods run only at a committed boundary with no Advance outstanding.
|
||||
`SaveSlot` replaces the slot's contents with the world state and the frame on screen at that
|
||||
boundary; slots are environment state and belong to its `State.Capture` payload. It is one
|
||||
operation per boundary under the section-5 operation key of [session RPC](ipc-v1.md).
|
||||
- `RestoreSlot` and `Agent.Rollback` move a participant from `(priorEpoch, k)` to
|
||||
`(scope.epoch, k)`; `priorEpoch` must differ from the scoped epoch, and after the move every
|
||||
request scoped to the prior epoch is `STALE_EPOCH`. The restored observation keeps the
|
||||
boundary number, `worldTime` and `engineFrame`, and returns fresh artifacts; it carries no
|
||||
audio chunk, and the next chunk marks a discontinuity (state-media-v1 section 2).
|
||||
- `Agent.Rollback` applies the policy's agent half and nothing else: for
|
||||
`legacy-ratchet-rollback-v1`, clear decoder holds and plastic eligibility, install `input`
|
||||
without a tick, reset the readout's transient (held channel, blocked window, location from
|
||||
the context), keep the brain clock, membrane, RNG, rates and gains. No reward, stimulation or
|
||||
calibration. It is the only way to install an input outside a Commit.
|
||||
- A failure of any of these methods mid-policy fails the epoch; recovery is the coherent group
|
||||
restore of [state-media-v1](state-media-v1.md) section 6. There is no partial rollback.
|
||||
- `maxSlots` is 4 (a crate-chosen bound, published in the schema set).
|
||||
|
||||
The sequence that uses them is [step-v1](step-v1.md) section 6's amendment and
|
||||
[legacy-gameboy-v1](legacy-gameboy-v1.md) section 11.
|
||||
|
|
|
|||
|
|
@ -99,6 +99,17 @@ argmax of the same normalized scores over the same rates. It is the same kind of
|
|||
the retina already sees, arriving through a much narrower channel, and it is disclosed on the
|
||||
honesty panel with the rest of the readout.
|
||||
|
||||
**Where the location comes from on the session framework (2026-09-23).** When the live fly runs
|
||||
on the session framework (the operator's port decision of 2026-09-23), the sim loop that owned
|
||||
the position is split: the task reads the location, the agent owns the decoder. The location
|
||||
then reaches the decoder as a **declared** field of its decision context,
|
||||
`gameboy-readout-context-v1 {boot, bound, location}`
|
||||
([legacy Game Boy composition](design/session-framework/legacy-gameboy-v1.md) section 5). The rule is
|
||||
unchanged: the location only restarts the blocked window, `null` is still no information, the
|
||||
held channel and the window stay the readout's own state, and the blocked channel is still
|
||||
computed here, never handed in. The profile allowlists the field, and the task cannot put
|
||||
anything else in it.
|
||||
|
||||
## Macro group (2026-09-16)
|
||||
|
||||
`DecoderConfig.macros` is a second `ExclusiveGroup` with the same fields and the same decision rules
|
||||
|
|
|
|||
1
package-lock.json
generated
1
package-lock.json
generated
|
|
@ -3708,6 +3708,7 @@
|
|||
"version": "0.1.0",
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
"@flybrain/brain": "*",
|
||||
"@types/node": "22.17.0",
|
||||
"tsx": "4.20.3",
|
||||
"typescript": "5.9.2"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"typecheck": "tsc -p tsconfig.json --pretty false"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@flybrain/brain": "*",
|
||||
"@types/node": "22.17.0",
|
||||
"tsx": "4.20.3",
|
||||
"typescript": "5.9.2"
|
||||
|
|
|
|||
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`,
|
||||
);
|
||||
}
|
||||
}
|
||||
560
packages/session-types/src/gameboy.ts
Normal file
560
packages/session-types/src/gameboy.ts
Normal file
|
|
@ -0,0 +1,560 @@
|
|||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
// The decoder configuration digest ------------------------------------------------------------
|
||||
|
||||
/** The name of the canonical decoder-configuration form (legacy-gameboy-v1 section 12). */
|
||||
export const DECODER_CONFIG_FORM = 'gameboy-decoder-config-v1';
|
||||
|
||||
interface GroupLike {
|
||||
channels: Record<string, string>;
|
||||
decisionMs: number;
|
||||
holdMs: number;
|
||||
hysteresis: number;
|
||||
fatigueGain: number;
|
||||
fatigueDecay: number;
|
||||
blockedFatigue: number;
|
||||
blockedMs: number;
|
||||
}
|
||||
|
||||
/** Structurally the oracle's `DecoderConfig` (`packages/brain/src/readout/decoder.ts`). */
|
||||
export interface DecoderConfigLike {
|
||||
exclusive?: GroupLike;
|
||||
macros?: GroupLike;
|
||||
pulses: {
|
||||
channel: string;
|
||||
role: string;
|
||||
holdMs: number;
|
||||
cooldownMs: number;
|
||||
threshold: number;
|
||||
boot?: { cooldownMs: number; threshold: number };
|
||||
throttleGroup?: string;
|
||||
}[];
|
||||
clearLockoutMs: number;
|
||||
}
|
||||
|
||||
function groupForm(group: GroupLike | undefined): unknown {
|
||||
if (!group) return null;
|
||||
return {
|
||||
// Channel order breaks argmax ties, so it is part of the identity: an array, not a map,
|
||||
// because canonical JSON sorts object keys.
|
||||
channels: Object.entries(group.channels).map(([channel, role]) => ({ channel, role })),
|
||||
decisionMs: group.decisionMs,
|
||||
holdMs: group.holdMs,
|
||||
hysteresis: group.hysteresis,
|
||||
fatigueGain: group.fatigueGain,
|
||||
fatigueDecay: group.fatigueDecay,
|
||||
blockedFatigue: group.blockedFatigue,
|
||||
blockedMs: group.blockedMs,
|
||||
};
|
||||
}
|
||||
|
||||
/** The canonical form `decoderConfigDigest` is taken over. */
|
||||
export function decoderConfigForm(config: DecoderConfigLike): unknown {
|
||||
return {
|
||||
form: DECODER_CONFIG_FORM,
|
||||
exclusive: groupForm(config.exclusive),
|
||||
macros: groupForm(config.macros),
|
||||
pulses: config.pulses.map((pulse) => ({
|
||||
channel: pulse.channel,
|
||||
role: pulse.role,
|
||||
holdMs: pulse.holdMs,
|
||||
cooldownMs: pulse.cooldownMs,
|
||||
threshold: pulse.threshold,
|
||||
boot: pulse.boot ? { cooldownMs: pulse.boot.cooldownMs, threshold: pulse.boot.threshold } : null,
|
||||
throttleGroup: pulse.throttleGroup ?? null,
|
||||
})),
|
||||
clearLockoutMs: config.clearLockoutMs,
|
||||
};
|
||||
}
|
||||
|
||||
/** `LegacyGameboyComposition.decoderConfigDigest`: SHA-256 of the canonical form. */
|
||||
export function decoderConfigDigest(config: DecoderConfigLike): Digest {
|
||||
return digestOf(decoderConfigForm(config));
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -23,6 +23,25 @@ import {
|
|||
ownerToken,
|
||||
} from './scalar';
|
||||
import { MAX_AGENTS, MAX_RATE_ROLES } from './workers';
|
||||
import { MAX_SLOTS } from './extensions';
|
||||
|
||||
/** Boundary actions at the reached boundary (amendment of 2026-09-23, RT-01a). */
|
||||
export const BOUNDARY_ACTION_KINDS = ['save-slot', 'rollback'] as const;
|
||||
export type BoundaryActionKind = (typeof BOUNDARY_ACTION_KINDS)[number];
|
||||
/** Every slot at most once, plus one rollback. */
|
||||
export const MAX_BOUNDARY_ACTIONS = MAX_SLOTS + 1;
|
||||
|
||||
export interface BoundaryAction {
|
||||
kind: BoundaryActionKind;
|
||||
slotId: Id;
|
||||
stateDigest: Digest | null;
|
||||
}
|
||||
|
||||
/** A checkpoint capture at the reached boundary and how many boundary actions preceded it. */
|
||||
export interface TraceCapture {
|
||||
checkpointId: Id;
|
||||
afterActions: number;
|
||||
}
|
||||
|
||||
export interface TraceAgent {
|
||||
agentId: Id;
|
||||
|
|
@ -49,6 +68,7 @@ export interface TraceBehaviour {
|
|||
outcomeIds: Id[];
|
||||
eventIds: Id[];
|
||||
publishedBoundary: U64;
|
||||
boundaryActions: BoundaryAction[];
|
||||
}
|
||||
|
||||
export interface TraceRequest {
|
||||
|
|
@ -63,6 +83,7 @@ export interface TraceOperational {
|
|||
commitRequestIds: TraceRequest[];
|
||||
busCallIds: BusCallId[];
|
||||
deliveryIds: OwnerToken[];
|
||||
captures: TraceCapture[];
|
||||
}
|
||||
|
||||
export interface TransitionTrace {
|
||||
|
|
@ -107,7 +128,18 @@ export function readTraceBehaviour(value: unknown): TraceBehaviour {
|
|||
const outcomeIds = reader.idList('outcomeIds', 0, MAX_RATE_ROLES);
|
||||
const eventIds = reader.idList('eventIds', 0, MAX_RATE_ROLES);
|
||||
const publishedBoundary = reader.u64('publishedBoundary');
|
||||
const boundaryActions = reader.list('boundaryActions', 0, MAX_BOUNDARY_ACTIONS, (item) => {
|
||||
const action = new Reader(item, 'TraceBehaviour.boundaryActions');
|
||||
const entry: BoundaryAction = {
|
||||
kind: action.enumeration('kind', BOUNDARY_ACTION_KINDS),
|
||||
slotId: action.id('slotId'),
|
||||
stateDigest: action.value('stateDigest') === null ? null : action.digest('stateDigest'),
|
||||
};
|
||||
action.finish();
|
||||
return entry;
|
||||
});
|
||||
reader.finish();
|
||||
validateBoundaryActions(boundaryActions);
|
||||
|
||||
requireUnique(
|
||||
agents.map((agent) => agent.agentId),
|
||||
|
|
@ -141,9 +173,39 @@ export function readTraceBehaviour(value: unknown): TraceBehaviour {
|
|||
outcomeIds,
|
||||
eventIds,
|
||||
publishedBoundary,
|
||||
boundaryActions,
|
||||
};
|
||||
}
|
||||
|
||||
/** Slot saves first, each slot once with its digest; at most one rollback, last, no digest. */
|
||||
function validateBoundaryActions(actions: readonly BoundaryAction[]): void {
|
||||
let rolledBack = false;
|
||||
const saved: string[] = [];
|
||||
for (const action of actions) {
|
||||
if (rolledBack) fail('TraceBehaviour: nothing follows a rollback at the same boundary');
|
||||
if (action.kind === 'save-slot') {
|
||||
if (action.stateDigest === null) fail('TraceBehaviour: a slot save records its state digest');
|
||||
if (saved.includes(action.slotId)) {
|
||||
fail('TraceBehaviour: a slot is saved at most once per boundary');
|
||||
}
|
||||
saved.push(action.slotId);
|
||||
} else {
|
||||
if (action.stateDigest !== null) fail('TraceBehaviour: a rollback records no state digest');
|
||||
rolledBack = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** How many leading boundary actions are slot saves. */
|
||||
export function slotSaves(behaviour: TraceBehaviour): number {
|
||||
let count = 0;
|
||||
for (const action of behaviour.boundaryActions) {
|
||||
if (action.kind !== 'save-slot') break;
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
export function readTraceOperational(value: unknown): TraceOperational {
|
||||
const reader = new Reader(value, 'TraceOperational');
|
||||
const readRequests = (item: unknown): TraceRequest => {
|
||||
|
|
@ -162,6 +224,15 @@ export function readTraceOperational(value: unknown): TraceOperational {
|
|||
commitRequestIds: reader.list('commitRequestIds', 1, MAX_AGENTS, readRequests),
|
||||
busCallIds: reader.list('busCallIds', 0, 64, busCallId),
|
||||
deliveryIds: reader.list('deliveryIds', 0, 64, ownerToken),
|
||||
captures: reader.list('captures', 0, MAX_BOUNDARY_ACTIONS + 1, (item) => {
|
||||
const capture = new Reader(item, 'TraceOperational.captures');
|
||||
const entry: TraceCapture = {
|
||||
checkpointId: capture.id('checkpointId'),
|
||||
afterActions: capture.int('afterActions', 0, MAX_BOUNDARY_ACTIONS),
|
||||
};
|
||||
capture.finish();
|
||||
return entry;
|
||||
}),
|
||||
};
|
||||
reader.finish();
|
||||
requireUnique(
|
||||
|
|
@ -174,6 +245,17 @@ export function readTraceOperational(value: unknown): TraceOperational {
|
|||
);
|
||||
requireUnique(operational.busCallIds, 'TraceOperational.busCallIds');
|
||||
requireUnique(operational.deliveryIds, 'TraceOperational.deliveryIds');
|
||||
requireUnique(
|
||||
operational.captures.map((capture) => capture.checkpointId),
|
||||
'TraceOperational.captures',
|
||||
);
|
||||
let last = 0;
|
||||
for (const capture of operational.captures) {
|
||||
if (capture.afterActions < last) {
|
||||
fail('TraceOperational: captures are recorded in the order they were taken');
|
||||
}
|
||||
last = capture.afterActions;
|
||||
}
|
||||
return operational;
|
||||
}
|
||||
|
||||
|
|
@ -194,6 +276,20 @@ export function readTransitionTrace(value: unknown): TransitionTrace {
|
|||
}
|
||||
}
|
||||
}
|
||||
// A slot save due at a boundary completes before any capture there (legacy-gameboy-v1 16).
|
||||
const saves = slotSaves(trace.behaviour);
|
||||
for (const capture of trace.operational.captures) {
|
||||
if (capture.afterActions < saves) {
|
||||
fail(
|
||||
`TransitionTrace: capture "${capture.checkpointId}" was taken before this boundary's slot saves completed`,
|
||||
);
|
||||
}
|
||||
if (capture.afterActions > trace.behaviour.boundaryActions.length) {
|
||||
fail(
|
||||
`TransitionTrace: capture "${capture.checkpointId}" counts more boundary actions than were applied`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return trace;
|
||||
}
|
||||
|
||||
|
|
@ -242,6 +338,7 @@ export function behaviourDiff(left: TransitionTrace, right: TransitionTrace): st
|
|||
}
|
||||
if (differs(a.outcomeIds, b.outcomeIds)) out.push('outcomeIds differ');
|
||||
if (differs(a.eventIds, b.eventIds)) out.push('eventIds differ');
|
||||
if (differs(a.boundaryActions, b.boundaryActions)) out.push('boundaryActions differ');
|
||||
const idsA = a.agents.map((agent) => agent.agentId);
|
||||
const idsB = b.agents.map((agent) => agent.agentId);
|
||||
if (differs(idsA, idsB)) {
|
||||
|
|
|
|||
|
|
@ -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')),
|
||||
};
|
||||
|
|
|
|||
176
packages/session-types/tests/gameboy.test.ts
Normal file
176
packages/session-types/tests/gameboy.test.ts
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
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' }));
|
||||
});
|
||||
|
||||
test('the decoderConfigDigest vectors are what the TypeScript oracle preset computes', async () => {
|
||||
const { gameboyDecoderConfig } = await import('@flybrain/brain');
|
||||
const file = fixtures.load('gameboy-decoder-config.json') as Record<string, any>;
|
||||
const cases = file.cases as Record<string, any>[];
|
||||
assert.deepEqual(
|
||||
cases.map((item) => item.name),
|
||||
['raw', 'macros'],
|
||||
);
|
||||
for (const item of cases) {
|
||||
const form = gameboy.decoderConfigForm(gameboyDecoderConfig(item.macroChannels as string[]));
|
||||
assert.deepEqual(form, item.form, `${item.name}: the oracle's form is the Rust twin's`);
|
||||
assert.equal(canonicalize(form), item.canonical, `${item.name}: canonical bytes`);
|
||||
assert.equal(
|
||||
gameboy.decoderConfigDigest(gameboyDecoderConfig(item.macroChannels as string[])),
|
||||
item.digest,
|
||||
`${item.name}: digest`,
|
||||
);
|
||||
}
|
||||
const reversed = [...(cases[1]!.macroChannels as string[])].reverse();
|
||||
assert.notEqual(
|
||||
gameboy.decoderConfigDigest(gameboyDecoderConfig(reversed)),
|
||||
cases[1]!.digest,
|
||||
'channel order is identity',
|
||||
);
|
||||
// The example composition declares the real macros-mode digest and channel set.
|
||||
const example = legacy().composition.example;
|
||||
assert.equal(example.decoderConfigDigest, cases[1]!.digest);
|
||||
assert.deepEqual(example.executor.macroChannels, cases[1]!.macroChannels);
|
||||
});
|
||||
|
|
@ -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,12 @@ once and holds both languages to it.
|
|||
| `schema-set.json`, `contract-digest.json` | The canonical schema set and its digest |
|
||||
| `seed-vectors.json` | `seed-derivation-v1` test vectors |
|
||||
| `checkpoint-envelope.json` | One `FLYSESS1` envelope, its layout and the corruptions a reader refuses |
|
||||
| `gameboy-decoder-config.json` | The `decoderConfigDigest` vectors; written and checked by `flysim`'s `legacy_profile_identity` test (`FLY_UPDATE_FIXTURES=1` rewrites), reproduced by `@flybrain/session-types` from the oracle preset |
|
||||
| `gameboy-legacy.json` | The legacy Game Boy extension set and digest, every registered `SchemaRef`, the legacy profile and its `AssetRef`, the frame clock, an example composition and its digest |
|
||||
|
||||
The derived files (`schema-set.json`, `contract-digest.json`, the `canonical`/`digest` fields
|
||||
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 +100,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,118 @@ 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 digest is a placeholder -- the real one is computed
|
||||
/// by the composition that runs, and no ROM identity belongs in a fixture. The macro channels and
|
||||
/// the decoder digest are the real macros-mode vector of `gameboy-decoder-config.json`, which
|
||||
/// `flysim`'s `legacy_profile_identity` test computes from `gameboy_decoder_config_with_macros`
|
||||
/// and the TypeScript test from the oracle preset. The compatibility string is today's, byte for
|
||||
/// byte, because its segments must agree with the declaration.
|
||||
pub fn example_composition() -> LegacyComposition {
|
||||
let pokered = "0cd19d3b877b7dc66d12c7050bed9a7f38154d4b";
|
||||
LegacyComposition {
|
||||
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: decoder_vector("macros")["macroChannels"]
|
||||
.as_array()
|
||||
.expect("macroChannels")
|
||||
.iter()
|
||||
.map(|c| c.as_str().expect("channel").to_owned())
|
||||
.collect(),
|
||||
},
|
||||
decoder_config_digest: decoder_vector("macros")["digest"]
|
||||
.as_str()
|
||||
.expect("digest")
|
||||
.to_owned(),
|
||||
environment: gameboy::EnvironmentDeclaration {
|
||||
slots: vec!["best".to_owned()],
|
||||
audio_sample_rate: 48_000,
|
||||
},
|
||||
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,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// One case of the (flysim-written) decoder-config vectors.
|
||||
fn decoder_vector(name: &str) -> Value {
|
||||
let file = fixtures::load("gameboy-decoder-config.json").expect("gameboy-decoder-config.json");
|
||||
fixtures::cases(&file)
|
||||
.expect("cases")
|
||||
.iter()
|
||||
.find(|c| c["name"] == Value::String(name.to_owned()))
|
||||
.unwrap_or_else(|| panic!("decoder vector {name}"))
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// The legacy Game Boy extension set, the profile document and its AssetRef, the clock
|
||||
/// vector and an example composition with its digest.
|
||||
fn gameboy_legacy() -> String {
|
||||
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": "a56e25e6289a6f9e6720ba5f727651e9e4e2c5cf7a68990fc1f8d37fe12f2006",
|
||||
"schemaSetVersion": 1,
|
||||
"schemaSetBytes": 27470,
|
||||
"types": 54,
|
||||
"enums": 11,
|
||||
"limits": 26
|
||||
"schemaSetBytes": 30711,
|
||||
"types": 60,
|
||||
"enums": 12,
|
||||
"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,335 @@
|
|||
{
|
||||
"description": "decoderConfigDigest vectors (legacy-gameboy-v1 section 12): the canonical form of gameboy_decoder_config_with_macros / gameboyDecoderConfig for raw mode and the Pokemon Red macro group. Written by FLY_UPDATE_FIXTURES=1 cargo test -p flysim --test legacy_profile_identity; both languages must reproduce every form and digest.",
|
||||
"cases": [
|
||||
{
|
||||
"name": "raw",
|
||||
"macroChannels": [],
|
||||
"form": {
|
||||
"form": "gameboy-decoder-config-v1",
|
||||
"exclusive": {
|
||||
"channels": [
|
||||
{
|
||||
"channel": "up",
|
||||
"role": "command_0"
|
||||
},
|
||||
{
|
||||
"channel": "down",
|
||||
"role": "command_1"
|
||||
},
|
||||
{
|
||||
"channel": "left",
|
||||
"role": "command_2"
|
||||
},
|
||||
{
|
||||
"channel": "right",
|
||||
"role": "command_3"
|
||||
}
|
||||
],
|
||||
"decisionMs": 800.0,
|
||||
"holdMs": 800.0,
|
||||
"hysteresis": 1.05,
|
||||
"fatigueGain": 0.08,
|
||||
"fatigueDecay": 0.8,
|
||||
"blockedFatigue": 0.35,
|
||||
"blockedMs": 800.0
|
||||
},
|
||||
"macros": null,
|
||||
"pulses": [
|
||||
{
|
||||
"channel": "a",
|
||||
"role": "command_4",
|
||||
"holdMs": 85.0,
|
||||
"cooldownMs": 480.0,
|
||||
"threshold": 1.0,
|
||||
"boot": null,
|
||||
"throttleGroup": null
|
||||
},
|
||||
{
|
||||
"channel": "b",
|
||||
"role": "command_5",
|
||||
"holdMs": 85.0,
|
||||
"cooldownMs": 480.0,
|
||||
"threshold": 1.0,
|
||||
"boot": null,
|
||||
"throttleGroup": null
|
||||
},
|
||||
{
|
||||
"channel": "start",
|
||||
"role": "command_6",
|
||||
"holdMs": 55.0,
|
||||
"cooldownMs": 30000.0,
|
||||
"threshold": 1.35,
|
||||
"boot": {
|
||||
"cooldownMs": 2500.0,
|
||||
"threshold": 1.0
|
||||
},
|
||||
"throttleGroup": "system"
|
||||
},
|
||||
{
|
||||
"channel": "select",
|
||||
"role": "command_7",
|
||||
"holdMs": 55.0,
|
||||
"cooldownMs": 30000.0,
|
||||
"threshold": 1.35,
|
||||
"boot": {
|
||||
"cooldownMs": 2500.0,
|
||||
"threshold": 1.0
|
||||
},
|
||||
"throttleGroup": "system"
|
||||
}
|
||||
],
|
||||
"clearLockoutMs": 480.0
|
||||
},
|
||||
"canonical": "{\"clearLockoutMs\":480,\"exclusive\":{\"blockedFatigue\":0.35,\"blockedMs\":800,\"channels\":[{\"channel\":\"up\",\"role\":\"command_0\"},{\"channel\":\"down\",\"role\":\"command_1\"},{\"channel\":\"left\",\"role\":\"command_2\"},{\"channel\":\"right\",\"role\":\"command_3\"}],\"decisionMs\":800,\"fatigueDecay\":0.8,\"fatigueGain\":0.08,\"holdMs\":800,\"hysteresis\":1.05},\"form\":\"gameboy-decoder-config-v1\",\"macros\":null,\"pulses\":[{\"boot\":null,\"channel\":\"a\",\"cooldownMs\":480,\"holdMs\":85,\"role\":\"command_4\",\"threshold\":1,\"throttleGroup\":null},{\"boot\":null,\"channel\":\"b\",\"cooldownMs\":480,\"holdMs\":85,\"role\":\"command_5\",\"threshold\":1,\"throttleGroup\":null},{\"boot\":{\"cooldownMs\":2500,\"threshold\":1},\"channel\":\"start\",\"cooldownMs\":30000,\"holdMs\":55,\"role\":\"command_6\",\"threshold\":1.35,\"throttleGroup\":\"system\"},{\"boot\":{\"cooldownMs\":2500,\"threshold\":1},\"channel\":\"select\",\"cooldownMs\":30000,\"holdMs\":55,\"role\":\"command_7\",\"threshold\":1.35,\"throttleGroup\":\"system\"}]}",
|
||||
"digest": "6234e4a0363cfe82b8d515943c4f645a3960eda8fa5bf9465009061f80d50812"
|
||||
},
|
||||
{
|
||||
"name": "macros",
|
||||
"macroChannels": [
|
||||
"macro_go_objective",
|
||||
"macro_go_out",
|
||||
"macro_go_warp",
|
||||
"macro_go_route",
|
||||
"macro_go_item",
|
||||
"macro_go_npc",
|
||||
"macro_go_frontier",
|
||||
"macro_go_shop",
|
||||
"macro_go_heal",
|
||||
"macro_talk",
|
||||
"macro_menu",
|
||||
"macro_next",
|
||||
"macro_yes",
|
||||
"macro_no",
|
||||
"macro_close",
|
||||
"macro_confirm",
|
||||
"macro_back",
|
||||
"macro_move_1",
|
||||
"macro_move_2",
|
||||
"macro_move_3",
|
||||
"macro_move_4",
|
||||
"macro_switch",
|
||||
"macro_item",
|
||||
"macro_throw_ball",
|
||||
"macro_run",
|
||||
"macro_buy_potion",
|
||||
"macro_buy_ball",
|
||||
"macro_buy_antidote",
|
||||
"macro_buy_repel",
|
||||
"macro_heal",
|
||||
"macro_leave"
|
||||
],
|
||||
"form": {
|
||||
"form": "gameboy-decoder-config-v1",
|
||||
"exclusive": {
|
||||
"channels": [
|
||||
{
|
||||
"channel": "up",
|
||||
"role": "command_0"
|
||||
},
|
||||
{
|
||||
"channel": "down",
|
||||
"role": "command_1"
|
||||
},
|
||||
{
|
||||
"channel": "left",
|
||||
"role": "command_2"
|
||||
},
|
||||
{
|
||||
"channel": "right",
|
||||
"role": "command_3"
|
||||
}
|
||||
],
|
||||
"decisionMs": 800.0,
|
||||
"holdMs": 800.0,
|
||||
"hysteresis": 1.05,
|
||||
"fatigueGain": 0.08,
|
||||
"fatigueDecay": 0.8,
|
||||
"blockedFatigue": 0.35,
|
||||
"blockedMs": 800.0
|
||||
},
|
||||
"macros": {
|
||||
"channels": [
|
||||
{
|
||||
"channel": "macro_go_objective",
|
||||
"role": "macro_go_objective"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_out",
|
||||
"role": "macro_go_out"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_warp",
|
||||
"role": "macro_go_warp"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_route",
|
||||
"role": "macro_go_route"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_item",
|
||||
"role": "macro_go_item"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_npc",
|
||||
"role": "macro_go_npc"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_frontier",
|
||||
"role": "macro_go_frontier"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_shop",
|
||||
"role": "macro_go_shop"
|
||||
},
|
||||
{
|
||||
"channel": "macro_go_heal",
|
||||
"role": "macro_go_heal"
|
||||
},
|
||||
{
|
||||
"channel": "macro_talk",
|
||||
"role": "macro_talk"
|
||||
},
|
||||
{
|
||||
"channel": "macro_menu",
|
||||
"role": "macro_menu"
|
||||
},
|
||||
{
|
||||
"channel": "macro_next",
|
||||
"role": "macro_next"
|
||||
},
|
||||
{
|
||||
"channel": "macro_yes",
|
||||
"role": "macro_yes"
|
||||
},
|
||||
{
|
||||
"channel": "macro_no",
|
||||
"role": "macro_no"
|
||||
},
|
||||
{
|
||||
"channel": "macro_close",
|
||||
"role": "macro_close"
|
||||
},
|
||||
{
|
||||
"channel": "macro_confirm",
|
||||
"role": "macro_confirm"
|
||||
},
|
||||
{
|
||||
"channel": "macro_back",
|
||||
"role": "macro_back"
|
||||
},
|
||||
{
|
||||
"channel": "macro_move_1",
|
||||
"role": "macro_move_1"
|
||||
},
|
||||
{
|
||||
"channel": "macro_move_2",
|
||||
"role": "macro_move_2"
|
||||
},
|
||||
{
|
||||
"channel": "macro_move_3",
|
||||
"role": "macro_move_3"
|
||||
},
|
||||
{
|
||||
"channel": "macro_move_4",
|
||||
"role": "macro_move_4"
|
||||
},
|
||||
{
|
||||
"channel": "macro_switch",
|
||||
"role": "macro_switch"
|
||||
},
|
||||
{
|
||||
"channel": "macro_item",
|
||||
"role": "macro_item"
|
||||
},
|
||||
{
|
||||
"channel": "macro_throw_ball",
|
||||
"role": "macro_throw_ball"
|
||||
},
|
||||
{
|
||||
"channel": "macro_run",
|
||||
"role": "macro_run"
|
||||
},
|
||||
{
|
||||
"channel": "macro_buy_potion",
|
||||
"role": "macro_buy_potion"
|
||||
},
|
||||
{
|
||||
"channel": "macro_buy_ball",
|
||||
"role": "macro_buy_ball"
|
||||
},
|
||||
{
|
||||
"channel": "macro_buy_antidote",
|
||||
"role": "macro_buy_antidote"
|
||||
},
|
||||
{
|
||||
"channel": "macro_buy_repel",
|
||||
"role": "macro_buy_repel"
|
||||
},
|
||||
{
|
||||
"channel": "macro_heal",
|
||||
"role": "macro_heal"
|
||||
},
|
||||
{
|
||||
"channel": "macro_leave",
|
||||
"role": "macro_leave"
|
||||
}
|
||||
],
|
||||
"decisionMs": 800.0,
|
||||
"holdMs": 800.0,
|
||||
"hysteresis": 1.05,
|
||||
"fatigueGain": 0.08,
|
||||
"fatigueDecay": 0.8,
|
||||
"blockedFatigue": 0.35,
|
||||
"blockedMs": 800.0
|
||||
},
|
||||
"pulses": [
|
||||
{
|
||||
"channel": "a",
|
||||
"role": "command_4",
|
||||
"holdMs": 85.0,
|
||||
"cooldownMs": 480.0,
|
||||
"threshold": 1.0,
|
||||
"boot": null,
|
||||
"throttleGroup": null
|
||||
},
|
||||
{
|
||||
"channel": "b",
|
||||
"role": "command_5",
|
||||
"holdMs": 85.0,
|
||||
"cooldownMs": 480.0,
|
||||
"threshold": 1.0,
|
||||
"boot": null,
|
||||
"throttleGroup": null
|
||||
},
|
||||
{
|
||||
"channel": "start",
|
||||
"role": "command_6",
|
||||
"holdMs": 55.0,
|
||||
"cooldownMs": 30000.0,
|
||||
"threshold": 1.35,
|
||||
"boot": {
|
||||
"cooldownMs": 2500.0,
|
||||
"threshold": 1.0
|
||||
},
|
||||
"throttleGroup": "system"
|
||||
},
|
||||
{
|
||||
"channel": "select",
|
||||
"role": "command_7",
|
||||
"holdMs": 55.0,
|
||||
"cooldownMs": 30000.0,
|
||||
"threshold": 1.35,
|
||||
"boot": {
|
||||
"cooldownMs": 2500.0,
|
||||
"threshold": 1.0
|
||||
},
|
||||
"throttleGroup": "system"
|
||||
}
|
||||
],
|
||||
"clearLockoutMs": 480.0
|
||||
},
|
||||
"canonical": "{\"clearLockoutMs\":480,\"exclusive\":{\"blockedFatigue\":0.35,\"blockedMs\":800,\"channels\":[{\"channel\":\"up\",\"role\":\"command_0\"},{\"channel\":\"down\",\"role\":\"command_1\"},{\"channel\":\"left\",\"role\":\"command_2\"},{\"channel\":\"right\",\"role\":\"command_3\"}],\"decisionMs\":800,\"fatigueDecay\":0.8,\"fatigueGain\":0.08,\"holdMs\":800,\"hysteresis\":1.05},\"form\":\"gameboy-decoder-config-v1\",\"macros\":{\"blockedFatigue\":0.35,\"blockedMs\":800,\"channels\":[{\"channel\":\"macro_go_objective\",\"role\":\"macro_go_objective\"},{\"channel\":\"macro_go_out\",\"role\":\"macro_go_out\"},{\"channel\":\"macro_go_warp\",\"role\":\"macro_go_warp\"},{\"channel\":\"macro_go_route\",\"role\":\"macro_go_route\"},{\"channel\":\"macro_go_item\",\"role\":\"macro_go_item\"},{\"channel\":\"macro_go_npc\",\"role\":\"macro_go_npc\"},{\"channel\":\"macro_go_frontier\",\"role\":\"macro_go_frontier\"},{\"channel\":\"macro_go_shop\",\"role\":\"macro_go_shop\"},{\"channel\":\"macro_go_heal\",\"role\":\"macro_go_heal\"},{\"channel\":\"macro_talk\",\"role\":\"macro_talk\"},{\"channel\":\"macro_menu\",\"role\":\"macro_menu\"},{\"channel\":\"macro_next\",\"role\":\"macro_next\"},{\"channel\":\"macro_yes\",\"role\":\"macro_yes\"},{\"channel\":\"macro_no\",\"role\":\"macro_no\"},{\"channel\":\"macro_close\",\"role\":\"macro_close\"},{\"channel\":\"macro_confirm\",\"role\":\"macro_confirm\"},{\"channel\":\"macro_back\",\"role\":\"macro_back\"},{\"channel\":\"macro_move_1\",\"role\":\"macro_move_1\"},{\"channel\":\"macro_move_2\",\"role\":\"macro_move_2\"},{\"channel\":\"macro_move_3\",\"role\":\"macro_move_3\"},{\"channel\":\"macro_move_4\",\"role\":\"macro_move_4\"},{\"channel\":\"macro_switch\",\"role\":\"macro_switch\"},{\"channel\":\"macro_item\",\"role\":\"macro_item\"},{\"channel\":\"macro_throw_ball\",\"role\":\"macro_throw_ball\"},{\"channel\":\"macro_run\",\"role\":\"macro_run\"},{\"channel\":\"macro_buy_potion\",\"role\":\"macro_buy_potion\"},{\"channel\":\"macro_buy_ball\",\"role\":\"macro_buy_ball\"},{\"channel\":\"macro_buy_antidote\",\"role\":\"macro_buy_antidote\"},{\"channel\":\"macro_buy_repel\",\"role\":\"macro_buy_repel\"},{\"channel\":\"macro_heal\",\"role\":\"macro_heal\"},{\"channel\":\"macro_leave\",\"role\":\"macro_leave\"}],\"decisionMs\":800,\"fatigueDecay\":0.8,\"fatigueGain\":0.08,\"holdMs\":800,\"hysteresis\":1.05},\"pulses\":[{\"boot\":null,\"channel\":\"a\",\"cooldownMs\":480,\"holdMs\":85,\"role\":\"command_4\",\"threshold\":1,\"throttleGroup\":null},{\"boot\":null,\"channel\":\"b\",\"cooldownMs\":480,\"holdMs\":85,\"role\":\"command_5\",\"threshold\":1,\"throttleGroup\":null},{\"boot\":{\"cooldownMs\":2500,\"threshold\":1},\"channel\":\"start\",\"cooldownMs\":30000,\"holdMs\":55,\"role\":\"command_6\",\"threshold\":1.35,\"throttleGroup\":\"system\"},{\"boot\":{\"cooldownMs\":2500,\"threshold\":1},\"channel\":\"select\",\"cooldownMs\":30000,\"holdMs\":55,\"role\":\"command_7\",\"threshold\":1.35,\"throttleGroup\":\"system\"}]}",
|
||||
"digest": "82b6601f0390b742a67eca44ef935b49e16a73315db70f5644f1cbd21f7c8a52"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -0,0 +1,575 @@
|
|||
{
|
||||
"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": "a35823ecf607a218b0c9f9e7ec86585d355cb311f8d7a3ada8aa793e1c9fe47c",
|
||||
"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 gameboy-decoder-config-v1 form of the effective decoder configuration"
|
||||
},
|
||||
{
|
||||
"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_go_out",
|
||||
"macro_go_warp",
|
||||
"macro_go_route",
|
||||
"macro_go_item",
|
||||
"macro_go_npc",
|
||||
"macro_go_frontier",
|
||||
"macro_go_shop",
|
||||
"macro_go_heal",
|
||||
"macro_talk",
|
||||
"macro_menu",
|
||||
"macro_next",
|
||||
"macro_yes",
|
||||
"macro_no",
|
||||
"macro_close",
|
||||
"macro_confirm",
|
||||
"macro_back",
|
||||
"macro_move_1",
|
||||
"macro_move_2",
|
||||
"macro_move_3",
|
||||
"macro_move_4",
|
||||
"macro_switch",
|
||||
"macro_item",
|
||||
"macro_throw_ball",
|
||||
"macro_run",
|
||||
"macro_buy_potion",
|
||||
"macro_buy_ball",
|
||||
"macro_buy_antidote",
|
||||
"macro_buy_repel",
|
||||
"macro_heal",
|
||||
"macro_leave"
|
||||
]
|
||||
},
|
||||
"decoderConfigDigest": "82b6601f0390b742a67eca44ef935b49e16a73315db70f5644f1cbd21f7c8a52",
|
||||
"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": "77d8a88ff7fea51eb29b1ae75fc9cf8c17c184e6a1e5a585e86399f41fd9c9b0",
|
||||
"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
|
|
@ -54,7 +54,8 @@
|
|||
"evt-1",
|
||||
"evt-2"
|
||||
],
|
||||
"publishedBoundary": "42"
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": []
|
||||
},
|
||||
"operational": {
|
||||
"wallTimeNs": "1234567890",
|
||||
|
|
@ -87,7 +88,8 @@
|
|||
"deliveryIds": [
|
||||
"dlv-7",
|
||||
"own-9"
|
||||
]
|
||||
],
|
||||
"captures": []
|
||||
}
|
||||
},
|
||||
"variants": [
|
||||
|
|
@ -147,7 +149,8 @@
|
|||
"evt-1",
|
||||
"evt-2"
|
||||
],
|
||||
"publishedBoundary": "42"
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": []
|
||||
},
|
||||
"operational": {
|
||||
"wallTimeNs": "1234567890",
|
||||
|
|
@ -180,7 +183,8 @@
|
|||
"deliveryIds": [
|
||||
"dlv-7",
|
||||
"own-9"
|
||||
]
|
||||
],
|
||||
"captures": []
|
||||
}
|
||||
},
|
||||
"behaviourEquals": true,
|
||||
|
|
@ -242,7 +246,8 @@
|
|||
"evt-1",
|
||||
"evt-2"
|
||||
],
|
||||
"publishedBoundary": "42"
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": []
|
||||
},
|
||||
"operational": {
|
||||
"wallTimeNs": "9999999999",
|
||||
|
|
@ -276,7 +281,8 @@
|
|||
"deliveryIds": [
|
||||
"dlv-91",
|
||||
"own-92"
|
||||
]
|
||||
],
|
||||
"captures": []
|
||||
}
|
||||
},
|
||||
"behaviourEquals": true,
|
||||
|
|
@ -338,7 +344,8 @@
|
|||
"evt-1",
|
||||
"evt-2"
|
||||
],
|
||||
"publishedBoundary": "42"
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": []
|
||||
},
|
||||
"operational": {
|
||||
"wallTimeNs": "1234567890",
|
||||
|
|
@ -371,7 +378,8 @@
|
|||
"deliveryIds": [
|
||||
"dlv-7",
|
||||
"own-9"
|
||||
]
|
||||
],
|
||||
"captures": []
|
||||
}
|
||||
},
|
||||
"behaviourEquals": true,
|
||||
|
|
@ -433,7 +441,8 @@
|
|||
"evt-1",
|
||||
"evt-2"
|
||||
],
|
||||
"publishedBoundary": "42"
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": []
|
||||
},
|
||||
"operational": {
|
||||
"wallTimeNs": "1234567890",
|
||||
|
|
@ -466,7 +475,8 @@
|
|||
"deliveryIds": [
|
||||
"dlv-7",
|
||||
"own-9"
|
||||
]
|
||||
],
|
||||
"captures": []
|
||||
}
|
||||
},
|
||||
"behaviourEquals": false,
|
||||
|
|
@ -528,7 +538,8 @@
|
|||
"evt-1",
|
||||
"evt-2"
|
||||
],
|
||||
"publishedBoundary": "42"
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": []
|
||||
},
|
||||
"operational": {
|
||||
"wallTimeNs": "1234567890",
|
||||
|
|
@ -561,7 +572,8 @@
|
|||
"deliveryIds": [
|
||||
"dlv-7",
|
||||
"own-9"
|
||||
]
|
||||
],
|
||||
"captures": []
|
||||
}
|
||||
},
|
||||
"behaviourEquals": false,
|
||||
|
|
@ -623,7 +635,8 @@
|
|||
"evt-1",
|
||||
"evt-2"
|
||||
],
|
||||
"publishedBoundary": "42"
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": []
|
||||
},
|
||||
"operational": {
|
||||
"wallTimeNs": "1234567890",
|
||||
|
|
@ -656,7 +669,8 @@
|
|||
"deliveryIds": [
|
||||
"dlv-7",
|
||||
"own-9"
|
||||
]
|
||||
],
|
||||
"captures": []
|
||||
}
|
||||
},
|
||||
"behaviourEquals": false,
|
||||
|
|
@ -718,7 +732,8 @@
|
|||
"evt-1",
|
||||
"evt-2"
|
||||
],
|
||||
"publishedBoundary": "42"
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": []
|
||||
},
|
||||
"operational": {
|
||||
"wallTimeNs": "1234567890",
|
||||
|
|
@ -751,7 +766,8 @@
|
|||
"deliveryIds": [
|
||||
"dlv-7",
|
||||
"own-9"
|
||||
]
|
||||
],
|
||||
"captures": []
|
||||
}
|
||||
},
|
||||
"behaviourEquals": false,
|
||||
|
|
@ -813,7 +829,8 @@
|
|||
"evt-1",
|
||||
"evt-2"
|
||||
],
|
||||
"publishedBoundary": "42"
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": []
|
||||
},
|
||||
"operational": {
|
||||
"wallTimeNs": "1234567890",
|
||||
|
|
@ -846,7 +863,8 @@
|
|||
"deliveryIds": [
|
||||
"dlv-7",
|
||||
"own-9"
|
||||
]
|
||||
],
|
||||
"captures": []
|
||||
}
|
||||
},
|
||||
"behaviourEquals": false,
|
||||
|
|
@ -908,7 +926,8 @@
|
|||
"evt-1",
|
||||
"evt-2"
|
||||
],
|
||||
"publishedBoundary": "42"
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": []
|
||||
},
|
||||
"operational": {
|
||||
"wallTimeNs": "1234567890",
|
||||
|
|
@ -941,7 +960,8 @@
|
|||
"deliveryIds": [
|
||||
"dlv-7",
|
||||
"own-9"
|
||||
]
|
||||
],
|
||||
"captures": []
|
||||
}
|
||||
},
|
||||
"behaviourEquals": false,
|
||||
|
|
@ -1003,7 +1023,8 @@
|
|||
"evt-2",
|
||||
"evt-1"
|
||||
],
|
||||
"publishedBoundary": "42"
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": []
|
||||
},
|
||||
"operational": {
|
||||
"wallTimeNs": "1234567890",
|
||||
|
|
@ -1036,7 +1057,8 @@
|
|||
"deliveryIds": [
|
||||
"dlv-7",
|
||||
"own-9"
|
||||
]
|
||||
],
|
||||
"captures": []
|
||||
}
|
||||
},
|
||||
"behaviourEquals": false,
|
||||
|
|
@ -1098,7 +1120,8 @@
|
|||
"evt-1",
|
||||
"evt-2"
|
||||
],
|
||||
"publishedBoundary": "42"
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": []
|
||||
},
|
||||
"operational": {
|
||||
"wallTimeNs": "1234567890",
|
||||
|
|
@ -1131,7 +1154,8 @@
|
|||
"deliveryIds": [
|
||||
"dlv-7",
|
||||
"own-9"
|
||||
]
|
||||
],
|
||||
"captures": []
|
||||
}
|
||||
},
|
||||
"behaviourEquals": false,
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
@ -2756,7 +2763,8 @@
|
|||
"eventIds": [
|
||||
"evt-1"
|
||||
],
|
||||
"publishedBoundary": "42"
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": []
|
||||
},
|
||||
"operational": {
|
||||
"wallTimeNs": "1234567890",
|
||||
|
|
@ -2789,12 +2797,13 @@
|
|||
"deliveryIds": [
|
||||
"dlv-7",
|
||||
"own-9"
|
||||
]
|
||||
],
|
||||
"captures": []
|
||||
}
|
||||
},
|
||||
"note": "",
|
||||
"canonical": "{\"behaviour\":{\"acknowledgedBoundary\":\"42\",\"agents\":[{\"agentId\":\"fly-a\",\"brainTicks\":\"2517\",\"committedStep\":\"42\",\"decisionDigest\":\"a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"3\",\"numerator\":\"1000000\"},\"ticksAdvanced\":\"17\"},{\"agentId\":\"fly-b\",\"brainTicks\":\"2550\",\"committedStep\":\"42\",\"decisionDigest\":\"ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"1\",\"numerator\":\"0\"},\"ticksAdvanced\":\"17\"}],\"batchId\":\"batch-41\",\"controlDigest\":\"1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6\",\"eventIds\":[\"evt-1\"],\"observationBoundaries\":[{\"producedStep\":\"42\",\"viewId\":\"screen\"}],\"outcomeIds\":[\"outcome-1\"],\"publishedBoundary\":\"42\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"41\"}},\"operational\":{\"advanceRequestId\":\"req-43\",\"busCallIds\":[\"call-100\",\"call-101\",\"call-102\"],\"commitRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-44\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-45\"}],\"deliveryIds\":[\"dlv-7\",\"own-9\"],\"prepareRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-41\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-42\"}],\"wallTimeNs\":\"1234567890\"}}",
|
||||
"digest": "707701d6dcf5529a67cc9b1e300d5e1a273b9a7b02d91154d4a2a1978cd8e549"
|
||||
"canonical": "{\"behaviour\":{\"acknowledgedBoundary\":\"42\",\"agents\":[{\"agentId\":\"fly-a\",\"brainTicks\":\"2517\",\"committedStep\":\"42\",\"decisionDigest\":\"a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"3\",\"numerator\":\"1000000\"},\"ticksAdvanced\":\"17\"},{\"agentId\":\"fly-b\",\"brainTicks\":\"2550\",\"committedStep\":\"42\",\"decisionDigest\":\"ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"1\",\"numerator\":\"0\"},\"ticksAdvanced\":\"17\"}],\"batchId\":\"batch-41\",\"boundaryActions\":[],\"controlDigest\":\"1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6\",\"eventIds\":[\"evt-1\"],\"observationBoundaries\":[{\"producedStep\":\"42\",\"viewId\":\"screen\"}],\"outcomeIds\":[\"outcome-1\"],\"publishedBoundary\":\"42\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"41\"}},\"operational\":{\"advanceRequestId\":\"req-43\",\"busCallIds\":[\"call-100\",\"call-101\",\"call-102\"],\"captures\":[],\"commitRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-44\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-45\"}],\"deliveryIds\":[\"dlv-7\",\"own-9\"],\"prepareRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-41\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-42\"}],\"wallTimeNs\":\"1234567890\"}}",
|
||||
"digest": "36ecd5d39f81863c30c0fd52a12d72eb3ba71f0f3b72b2349e2c7f02a1d8e3e5"
|
||||
},
|
||||
{
|
||||
"name": "trace behaviour on its own",
|
||||
|
|
@ -2846,11 +2855,12 @@
|
|||
"eventIds": [
|
||||
"evt-1"
|
||||
],
|
||||
"publishedBoundary": "42"
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": []
|
||||
},
|
||||
"note": "the half two runs must agree on",
|
||||
"canonical": "{\"acknowledgedBoundary\":\"42\",\"agents\":[{\"agentId\":\"fly-a\",\"brainTicks\":\"2517\",\"committedStep\":\"42\",\"decisionDigest\":\"a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"3\",\"numerator\":\"1000000\"},\"ticksAdvanced\":\"17\"},{\"agentId\":\"fly-b\",\"brainTicks\":\"2550\",\"committedStep\":\"42\",\"decisionDigest\":\"ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"1\",\"numerator\":\"0\"},\"ticksAdvanced\":\"17\"}],\"batchId\":\"batch-41\",\"controlDigest\":\"1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6\",\"eventIds\":[\"evt-1\"],\"observationBoundaries\":[{\"producedStep\":\"42\",\"viewId\":\"screen\"}],\"outcomeIds\":[\"outcome-1\"],\"publishedBoundary\":\"42\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"41\"}}",
|
||||
"digest": "d938188fe3006537dbc0038fabf2a05df584e64b361b1106d79f3f965dff414a"
|
||||
"canonical": "{\"acknowledgedBoundary\":\"42\",\"agents\":[{\"agentId\":\"fly-a\",\"brainTicks\":\"2517\",\"committedStep\":\"42\",\"decisionDigest\":\"a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"3\",\"numerator\":\"1000000\"},\"ticksAdvanced\":\"17\"},{\"agentId\":\"fly-b\",\"brainTicks\":\"2550\",\"committedStep\":\"42\",\"decisionDigest\":\"ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"1\",\"numerator\":\"0\"},\"ticksAdvanced\":\"17\"}],\"batchId\":\"batch-41\",\"boundaryActions\":[],\"controlDigest\":\"1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6\",\"eventIds\":[\"evt-1\"],\"observationBoundaries\":[{\"producedStep\":\"42\",\"viewId\":\"screen\"}],\"outcomeIds\":[\"outcome-1\"],\"publishedBoundary\":\"42\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"41\"}}",
|
||||
"digest": "59ed2ef812fe44a5808d8251b253c76a642cdc6828ee964a1c90ef302b66c9e4"
|
||||
},
|
||||
{
|
||||
"name": "trace operational metadata on its own",
|
||||
|
|
@ -2886,11 +2896,799 @@
|
|||
"deliveryIds": [
|
||||
"dlv-7",
|
||||
"own-9"
|
||||
]
|
||||
],
|
||||
"captures": []
|
||||
},
|
||||
"note": "recorded by step-v1 section 8, excluded from its comparison",
|
||||
"canonical": "{\"advanceRequestId\":\"req-43\",\"busCallIds\":[\"call-100\",\"call-101\",\"call-102\"],\"commitRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-44\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-45\"}],\"deliveryIds\":[\"dlv-7\",\"own-9\"],\"prepareRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-41\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-42\"}],\"wallTimeNs\":\"1234567890\"}",
|
||||
"digest": "07c3402ab0e48b186d56f667da980a15b2a48b21381235cff37523a24ff0dfff"
|
||||
"canonical": "{\"advanceRequestId\":\"req-43\",\"busCallIds\":[\"call-100\",\"call-101\",\"call-102\"],\"captures\":[],\"commitRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-44\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-45\"}],\"deliveryIds\":[\"dlv-7\",\"own-9\"],\"prepareRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-41\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-42\"}],\"wallTimeNs\":\"1234567890\"}",
|
||||
"digest": "d9fd215dbea34237559ff5a5826beca16d3646340f2ce76ee62115d21f7c27d6"
|
||||
},
|
||||
{
|
||||
"name": "agent telemetry reporting a running stimulation pulse",
|
||||
"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_go_out",
|
||||
"macro_go_warp",
|
||||
"macro_go_route",
|
||||
"macro_go_item",
|
||||
"macro_go_npc",
|
||||
"macro_go_frontier",
|
||||
"macro_go_shop",
|
||||
"macro_go_heal",
|
||||
"macro_talk",
|
||||
"macro_menu",
|
||||
"macro_next",
|
||||
"macro_yes",
|
||||
"macro_no",
|
||||
"macro_close",
|
||||
"macro_confirm",
|
||||
"macro_back",
|
||||
"macro_move_1",
|
||||
"macro_move_2",
|
||||
"macro_move_3",
|
||||
"macro_move_4",
|
||||
"macro_switch",
|
||||
"macro_item",
|
||||
"macro_throw_ball",
|
||||
"macro_run",
|
||||
"macro_buy_potion",
|
||||
"macro_buy_ball",
|
||||
"macro_buy_antidote",
|
||||
"macro_buy_repel",
|
||||
"macro_heal",
|
||||
"macro_leave"
|
||||
]
|
||||
},
|
||||
"decoderConfigDigest": "82b6601f0390b742a67eca44ef935b49e16a73315db70f5644f1cbd21f7c8a52",
|
||||
"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 digest; real macros-mode decoder digest and channels",
|
||||
"canonical": "{\"checkpointFormatOfRecord\":\"FLYSIM01\",\"compositionId\":\"pokered-live\",\"decoderConfigDigest\":\"82b6601f0390b742a67eca44ef935b49e16a73315db70f5644f1cbd21f7c8a52\",\"environment\":{\"audio\":{\"channels\":2,\"sampleRate\":48000},\"controllerSchema\":{\"digest\":\"1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e\",\"id\":\"gameboy-joypad-v1\",\"version\":1},\"extensions\":[\"gameboy-slots-v1\"],\"inspectionSchema\":{\"digest\":\"d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6\",\"id\":\"gameboy-memory-inspection-v1\",\"version\":1},\"setupFrames\":1,\"slots\":[\"best\"],\"stepDuration\":{\"denominator\":\"512\",\"numerator\":\"8572265625\"}},\"episodePolicy\":\"legacy-ratchet-rollback-v1\",\"executor\":{\"adapter\":\"pokered-unique8-v6\",\"id\":\"pokered-macros-v1\",\"macroChannels\":[\"macro_go_objective\",\"macro_go_out\",\"macro_go_warp\",\"macro_go_route\",\"macro_go_item\",\"macro_go_npc\",\"macro_go_frontier\",\"macro_go_shop\",\"macro_go_heal\",\"macro_talk\",\"macro_menu\",\"macro_next\",\"macro_yes\",\"macro_no\",\"macro_close\",\"macro_confirm\",\"macro_back\",\"macro_move_1\",\"macro_move_2\",\"macro_move_3\",\"macro_move_4\",\"macro_switch\",\"macro_item\",\"macro_throw_ball\",\"macro_run\",\"macro_buy_potion\",\"macro_buy_ball\",\"macro_buy_antidote\",\"macro_buy_repel\",\"macro_heal\",\"macro_leave\"],\"mode\":\"macros\",\"rom\":{\"byteLength\":\"1048576\",\"digest\":\"c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20\",\"format\":\"gb-rom\",\"id\":\"pokered-rom\"},\"symbolProvenance\":\"0cd19d3b877b7dc66d12c7050bed9a7f38154d4b\"},\"flysimCompatibility\":\"lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu\",\"profile\":{\"byteLength\":\"1137\",\"digest\":\"41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878\",\"format\":\"fly-profile-v1\",\"id\":\"gameboy-legacy-fafb-v783-v1\"},\"restore\":\"legacy-transient-reset\",\"scheduler\":\"lockstep-v1\"}",
|
||||
"digest": "77d8a88ff7fea51eb29b1ae75fc9cf8c17c184e6a1e5a585e86399f41fd9c9b0"
|
||||
},
|
||||
{
|
||||
"name": "an example legacy composition in raw mode",
|
||||
"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": "6234e4a0363cfe82b8d515943c4f645a3960eda8fa5bf9465009061f80d50812",
|
||||
"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\":\"6234e4a0363cfe82b8d515943c4f645a3960eda8fa5bf9465009061f80d50812\",\"environment\":{\"audio\":{\"channels\":2,\"sampleRate\":48000},\"controllerSchema\":{\"digest\":\"1bde5fa114b99824ad608fba4ea85706cb0cebc6123a778dc1e5f89791d2a05e\",\"id\":\"gameboy-joypad-v1\",\"version\":1},\"extensions\":[\"gameboy-slots-v1\"],\"inspectionSchema\":{\"digest\":\"d6cb62248bfdac2ffdf00290ffbebf9a101b1fe28f7be766d24db28ede8da3e6\",\"id\":\"gameboy-memory-inspection-v1\",\"version\":1},\"setupFrames\":1,\"slots\":[\"best\"],\"stepDuration\":{\"denominator\":\"512\",\"numerator\":\"8572265625\"}},\"episodePolicy\":\"legacy-ratchet-rollback-v1\",\"executor\":{\"adapter\":\"pokered-unique8-v6\",\"id\":\"pokered-macros-v1\",\"macroChannels\":[],\"mode\":\"raw\",\"rom\":{\"byteLength\":\"1048576\",\"digest\":\"c840ea493f9bf41505f26cf5b1db26815dd588e7ec91d5fd6f1ad4d363dc4f20\",\"format\":\"gb-rom\",\"id\":\"pokered-rom\"},\"symbolProvenance\":\"0cd19d3b877b7dc66d12c7050bed9a7f38154d4b\"},\"flysimCompatibility\":\"lif-1ms-f64-v2/pokered-unique8-v6/75ba5d3536a2862fdb9f4ef1a76b96fe099a7201ac737f0cf53fe4c5ad4183f3:1657ba7716494c9db95a13b1527bc129226363f99b395774de1f76ff28571f0e:63b1acb26272edccdcfacf3c2ff58069e0cefaa84451429c989258bafaeae1d5:f567d7f07227e71c0df2ab4e6510f3a3f79e51b675095792e16d216d74b7b5b7:ece0b5e76d1884dd2f3ff6e0362bc284febe205ac1ce07fdab5009942ddffb62:b8c33144d4cec31c3ac4b6091ef1f4207f567c4f710f7c13fd2dc47c44c2b634:dbbafc044cd50aad7b792615988ff6d9991c846cc3d8b2eafc86b7c357b5eefc/fly-kc-mbon-rstdp-v2/binjgb:c60e138da5a795ebb55e56b11b7e90024e41112c/pokered:0cd19d3b877b7dc66d12c7050bed9a7f38154d4b/statefmt:199616-x86_64-unknown-linux-gnu\",\"profile\":{\"byteLength\":\"1137\",\"digest\":\"41e5d1ac62ab23f1b2d7252d52faac08b269c85e6b4c9ed7a370c74032c60878\",\"format\":\"fly-profile-v1\",\"id\":\"gameboy-legacy-fafb-v783-v1\"},\"restore\":\"legacy-transient-reset\",\"scheduler\":\"lockstep-v1\"}",
|
||||
"digest": "ac42e702ec6c9b09482b1ee335743ef85fe599a72e2c34a4d32aea67d67d0201"
|
||||
},
|
||||
{
|
||||
"name": "a rank climb: slot saved, then the milestone capture",
|
||||
"type": "TransitionTrace",
|
||||
"value": {
|
||||
"behaviour": {
|
||||
"scope": {
|
||||
"sessionId": "demo",
|
||||
"epoch": "epoch-1",
|
||||
"step": "41"
|
||||
},
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "fly-a",
|
||||
"profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0",
|
||||
"ticksAdvanced": "17",
|
||||
"brainTicks": "2517",
|
||||
"remainder": {
|
||||
"numerator": "1000000",
|
||||
"denominator": "3"
|
||||
},
|
||||
"decisionDigest": "a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b",
|
||||
"committedStep": "42"
|
||||
},
|
||||
{
|
||||
"agentId": "fly-b",
|
||||
"profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0",
|
||||
"ticksAdvanced": "17",
|
||||
"brainTicks": "2550",
|
||||
"remainder": {
|
||||
"numerator": "0",
|
||||
"denominator": "1"
|
||||
},
|
||||
"decisionDigest": "ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e",
|
||||
"committedStep": "42"
|
||||
}
|
||||
],
|
||||
"batchId": "batch-41",
|
||||
"controlDigest": "1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6",
|
||||
"acknowledgedBoundary": "42",
|
||||
"observationBoundaries": [
|
||||
{
|
||||
"viewId": "screen",
|
||||
"producedStep": "42"
|
||||
}
|
||||
],
|
||||
"outcomeIds": [
|
||||
"outcome-1"
|
||||
],
|
||||
"eventIds": [
|
||||
"evt-1"
|
||||
],
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": [
|
||||
{
|
||||
"kind": "save-slot",
|
||||
"slotId": "best",
|
||||
"stateDigest": "5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d"
|
||||
}
|
||||
]
|
||||
},
|
||||
"operational": {
|
||||
"wallTimeNs": "1234567890",
|
||||
"prepareRequestIds": [
|
||||
{
|
||||
"agentId": "fly-a",
|
||||
"requestId": "req-41"
|
||||
},
|
||||
{
|
||||
"agentId": "fly-b",
|
||||
"requestId": "req-42"
|
||||
}
|
||||
],
|
||||
"advanceRequestId": "req-43",
|
||||
"commitRequestIds": [
|
||||
{
|
||||
"agentId": "fly-a",
|
||||
"requestId": "req-44"
|
||||
},
|
||||
{
|
||||
"agentId": "fly-b",
|
||||
"requestId": "req-45"
|
||||
}
|
||||
],
|
||||
"busCallIds": [
|
||||
"call-100",
|
||||
"call-101",
|
||||
"call-102"
|
||||
],
|
||||
"deliveryIds": [
|
||||
"dlv-7",
|
||||
"own-9"
|
||||
],
|
||||
"captures": [
|
||||
{
|
||||
"checkpointId": "milestone-11",
|
||||
"afterActions": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"note": "legacy-gameboy-v1 section 16: a slot save due at a boundary completes before any capture there",
|
||||
"canonical": "{\"behaviour\":{\"acknowledgedBoundary\":\"42\",\"agents\":[{\"agentId\":\"fly-a\",\"brainTicks\":\"2517\",\"committedStep\":\"42\",\"decisionDigest\":\"a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"3\",\"numerator\":\"1000000\"},\"ticksAdvanced\":\"17\"},{\"agentId\":\"fly-b\",\"brainTicks\":\"2550\",\"committedStep\":\"42\",\"decisionDigest\":\"ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"1\",\"numerator\":\"0\"},\"ticksAdvanced\":\"17\"}],\"batchId\":\"batch-41\",\"boundaryActions\":[{\"kind\":\"save-slot\",\"slotId\":\"best\",\"stateDigest\":\"5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d\"}],\"controlDigest\":\"1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6\",\"eventIds\":[\"evt-1\"],\"observationBoundaries\":[{\"producedStep\":\"42\",\"viewId\":\"screen\"}],\"outcomeIds\":[\"outcome-1\"],\"publishedBoundary\":\"42\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"41\"}},\"operational\":{\"advanceRequestId\":\"req-43\",\"busCallIds\":[\"call-100\",\"call-101\",\"call-102\"],\"captures\":[{\"afterActions\":1,\"checkpointId\":\"milestone-11\"}],\"commitRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-44\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-45\"}],\"deliveryIds\":[\"dlv-7\",\"own-9\"],\"prepareRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-41\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-42\"}],\"wallTimeNs\":\"1234567890\"}}",
|
||||
"digest": "68c6f1e21cc6f63c36e69b73dbd5c11313b2a76e57f1e5e8468984516a6f96d5"
|
||||
},
|
||||
{
|
||||
"name": "save, capture, rollback, capture at one boundary",
|
||||
"type": "TransitionTrace",
|
||||
"value": {
|
||||
"behaviour": {
|
||||
"scope": {
|
||||
"sessionId": "demo",
|
||||
"epoch": "epoch-1",
|
||||
"step": "41"
|
||||
},
|
||||
"agents": [
|
||||
{
|
||||
"agentId": "fly-a",
|
||||
"profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0",
|
||||
"ticksAdvanced": "17",
|
||||
"brainTicks": "2517",
|
||||
"remainder": {
|
||||
"numerator": "1000000",
|
||||
"denominator": "3"
|
||||
},
|
||||
"decisionDigest": "a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b",
|
||||
"committedStep": "42"
|
||||
},
|
||||
{
|
||||
"agentId": "fly-b",
|
||||
"profileDigest": "1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0",
|
||||
"ticksAdvanced": "17",
|
||||
"brainTicks": "2550",
|
||||
"remainder": {
|
||||
"numerator": "0",
|
||||
"denominator": "1"
|
||||
},
|
||||
"decisionDigest": "ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e",
|
||||
"committedStep": "42"
|
||||
}
|
||||
],
|
||||
"batchId": "batch-41",
|
||||
"controlDigest": "1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6",
|
||||
"acknowledgedBoundary": "42",
|
||||
"observationBoundaries": [
|
||||
{
|
||||
"viewId": "screen",
|
||||
"producedStep": "42"
|
||||
}
|
||||
],
|
||||
"outcomeIds": [
|
||||
"outcome-1"
|
||||
],
|
||||
"eventIds": [
|
||||
"evt-1"
|
||||
],
|
||||
"publishedBoundary": "42",
|
||||
"boundaryActions": [
|
||||
{
|
||||
"kind": "save-slot",
|
||||
"slotId": "best",
|
||||
"stateDigest": "5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d"
|
||||
},
|
||||
{
|
||||
"kind": "rollback",
|
||||
"slotId": "best",
|
||||
"stateDigest": null
|
||||
}
|
||||
]
|
||||
},
|
||||
"operational": {
|
||||
"wallTimeNs": "1234567890",
|
||||
"prepareRequestIds": [
|
||||
{
|
||||
"agentId": "fly-a",
|
||||
"requestId": "req-41"
|
||||
},
|
||||
{
|
||||
"agentId": "fly-b",
|
||||
"requestId": "req-42"
|
||||
}
|
||||
],
|
||||
"advanceRequestId": "req-43",
|
||||
"commitRequestIds": [
|
||||
{
|
||||
"agentId": "fly-a",
|
||||
"requestId": "req-44"
|
||||
},
|
||||
{
|
||||
"agentId": "fly-b",
|
||||
"requestId": "req-45"
|
||||
}
|
||||
],
|
||||
"busCallIds": [
|
||||
"call-100",
|
||||
"call-101",
|
||||
"call-102"
|
||||
],
|
||||
"deliveryIds": [
|
||||
"dlv-7",
|
||||
"own-9"
|
||||
],
|
||||
"captures": [
|
||||
{
|
||||
"checkpointId": "periodic-7",
|
||||
"afterActions": 1
|
||||
},
|
||||
{
|
||||
"checkpointId": "after-rollback-7",
|
||||
"afterActions": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"note": "a durable save follows the rollback, as the legacy loop checkpoints after a recovery",
|
||||
"canonical": "{\"behaviour\":{\"acknowledgedBoundary\":\"42\",\"agents\":[{\"agentId\":\"fly-a\",\"brainTicks\":\"2517\",\"committedStep\":\"42\",\"decisionDigest\":\"a62db30eb7427e2ea9ec58661e69a1eff38e67f21ec97df7d91b24015890d89b\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"3\",\"numerator\":\"1000000\"},\"ticksAdvanced\":\"17\"},{\"agentId\":\"fly-b\",\"brainTicks\":\"2550\",\"committedStep\":\"42\",\"decisionDigest\":\"ddc2e7b8b300cda681c3d91368050fcb95dd66d20fd295834c31307d3df2623e\",\"profileDigest\":\"1900eab6c028483d7126599ee6f50de0d27907b5c65fa90524580b4b0f9852b0\",\"remainder\":{\"denominator\":\"1\",\"numerator\":\"0\"},\"ticksAdvanced\":\"17\"}],\"batchId\":\"batch-41\",\"boundaryActions\":[{\"kind\":\"save-slot\",\"slotId\":\"best\",\"stateDigest\":\"5d41402abc4b2a76b9719d911017c592ae9d3a0c8a7a6f5d2e3d4c5b6a7f8e9d\"},{\"kind\":\"rollback\",\"slotId\":\"best\",\"stateDigest\":null}],\"controlDigest\":\"1e2135d1b50f14d3d3d6cbe24a4294bcb9e4d5d3c28eb1ce4980ff2a17c5d3b6\",\"eventIds\":[\"evt-1\"],\"observationBoundaries\":[{\"producedStep\":\"42\",\"viewId\":\"screen\"}],\"outcomeIds\":[\"outcome-1\"],\"publishedBoundary\":\"42\",\"scope\":{\"epoch\":\"epoch-1\",\"sessionId\":\"demo\",\"step\":\"41\"}},\"operational\":{\"advanceRequestId\":\"req-43\",\"busCallIds\":[\"call-100\",\"call-101\",\"call-102\"],\"captures\":[{\"afterActions\":1,\"checkpointId\":\"periodic-7\"},{\"afterActions\":2,\"checkpointId\":\"after-rollback-7\"}],\"commitRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-44\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-45\"}],\"deliveryIds\":[\"dlv-7\",\"own-9\"],\"prepareRequestIds\":[{\"agentId\":\"fly-a\",\"requestId\":\"req-41\"},{\"agentId\":\"fly-b\",\"requestId\":\"req-42\"}],\"wallTimeNs\":\"1234567890\"}}",
|
||||
"digest": "ae22a2c5e3eb5e65c6ca82c27b2f5242bd94983fca4b11978d7067dc4389d0bb"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
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,
|
||||
|
|
@ -126,10 +126,15 @@ pub const ENUMS: &[EnumSchema] = &[
|
|||
source: "publishing-v1 3",
|
||||
members: &["lockstep-v1"],
|
||||
},
|
||||
EnumSchema {
|
||||
name: "BoundaryActionKind",
|
||||
source: "step-v1 8",
|
||||
members: crate::trace::BoundaryActionKind::ALL,
|
||||
},
|
||||
EnumSchema {
|
||||
name: "EpisodeRequestKind",
|
||||
source: "workers-v1 4",
|
||||
members: &["terminal"],
|
||||
members: crate::workers::EpisodeRequestKind::ALL,
|
||||
},
|
||||
];
|
||||
|
||||
|
|
@ -259,6 +264,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 +423,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 +759,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 +968,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",
|
||||
|
|
@ -986,6 +1075,11 @@ pub const SCHEMAS: &[TypeSchema] = &[
|
|||
req("outcomeIds", "array<Id>", "task outcome ids in task order"),
|
||||
req("eventIds", "array<Id>", "task event ids in task order"),
|
||||
req("publishedBoundary", "U64", ""),
|
||||
req(
|
||||
"boundaryActions",
|
||||
"array<{kind:BoundaryActionKind,slotId:Id,stateDigest:Digest|null}>",
|
||||
"<= 5, application order: slot saves first (each slot once, digest set), then at most one rollback (no digest)",
|
||||
),
|
||||
],
|
||||
},
|
||||
TypeSchema {
|
||||
|
|
@ -1019,6 +1113,11 @@ pub const SCHEMAS: &[TypeSchema] = &[
|
|||
),
|
||||
req("busCallIds", "array<BusCallId>", "call-<U64>"),
|
||||
req("deliveryIds", "array<OwnerToken>", "dlv-<U64> or own-<U64>"),
|
||||
req(
|
||||
"captures",
|
||||
"array<{checkpointId:Id,afterActions:int}>",
|
||||
"taken order, unique checkpointId; afterActions >= the boundary's slot saves and <= its actions",
|
||||
),
|
||||
],
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -44,6 +44,42 @@ pub struct TraceObservation {
|
|||
pub produced_step: u64,
|
||||
}
|
||||
|
||||
/// The kind of an action taken at the boundary a transition reached, after all of its commits
|
||||
/// (amendment of 2026-09-23, RT-01a).
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum BoundaryActionKind {
|
||||
SaveSlot,
|
||||
Rollback,
|
||||
}
|
||||
|
||||
impl BoundaryActionKind {
|
||||
pub const ALL: &'static [&'static str] = &["save-slot", "rollback"];
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
BoundaryActionKind::SaveSlot => "save-slot",
|
||||
BoundaryActionKind::Rollback => "rollback",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse(s: &str) -> Result<BoundaryActionKind> {
|
||||
match s {
|
||||
"save-slot" => Ok(BoundaryActionKind::SaveSlot),
|
||||
"rollback" => Ok(BoundaryActionKind::Rollback),
|
||||
_ => err("boundary action kind must be save-slot or rollback"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// One boundary action, in the order the coordinator applied it: an `Environment.SaveSlot`
|
||||
/// (with the saved state's digest) or a rollback to a slot (no digest).
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BoundaryAction {
|
||||
pub kind: BoundaryActionKind,
|
||||
pub slot_id: String,
|
||||
pub state_digest: Option<String>,
|
||||
}
|
||||
|
||||
/// The fields two runs of the same transition must agree on.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TraceBehaviour {
|
||||
|
|
@ -60,6 +96,9 @@ pub struct TraceBehaviour {
|
|||
/// Task event ids in task order.
|
||||
pub event_ids: Vec<String>,
|
||||
pub published_boundary: u64,
|
||||
/// Slot saves and a rollback at the reached boundary, in application order. Empty for a
|
||||
/// composition without them.
|
||||
pub boundary_actions: Vec<BoundaryAction>,
|
||||
}
|
||||
|
||||
impl TraceBehaviour {
|
||||
|
|
@ -126,6 +165,22 @@ impl DomainType for TraceBehaviour {
|
|||
let outcome_ids = crate::scalar::id_list(&mut f, "outcomeIds", 0, MAX_RATE_ROLES)?;
|
||||
let event_ids = crate::scalar::id_list(&mut f, "eventIds", 0, MAX_RATE_ROLES)?;
|
||||
let published_boundary = f.u64_string("publishedBoundary")?;
|
||||
let boundary_actions = list(&mut f, "boundaryActions", 0, MAX_BOUNDARY_ACTIONS, |v| {
|
||||
let mut a = Fields::new(v, "TraceBehaviour.boundaryActions")?;
|
||||
let kind = BoundaryActionKind::parse(a.string("kind")?)?;
|
||||
let slot_id = a.id("slotId")?;
|
||||
let state_digest = match a.value("stateDigest")? {
|
||||
Value::Null => None,
|
||||
Value::String(d) => Some(d.clone()),
|
||||
_ => return err("stateDigest must be null or a digest"),
|
||||
};
|
||||
a.finish()?;
|
||||
Ok(BoundaryAction {
|
||||
kind,
|
||||
slot_id,
|
||||
state_digest,
|
||||
})
|
||||
})?;
|
||||
f.finish()?;
|
||||
let b = TraceBehaviour {
|
||||
scope,
|
||||
|
|
@ -137,6 +192,7 @@ impl DomainType for TraceBehaviour {
|
|||
outcome_ids,
|
||||
event_ids,
|
||||
published_boundary,
|
||||
boundary_actions,
|
||||
};
|
||||
b.validate()?;
|
||||
Ok(b)
|
||||
|
|
@ -190,6 +246,24 @@ impl DomainType for TraceBehaviour {
|
|||
Value::Array(self.event_ids.iter().map(|i| i.clone().into()).collect()),
|
||||
),
|
||||
("publishedBoundary", u64_json(self.published_boundary)),
|
||||
(
|
||||
"boundaryActions",
|
||||
Value::Array(
|
||||
self.boundary_actions
|
||||
.iter()
|
||||
.map(|a| {
|
||||
obj(vec![
|
||||
("kind", a.kind.as_str().into()),
|
||||
("slotId", a.slot_id.clone().into()),
|
||||
(
|
||||
"stateDigest",
|
||||
a.state_digest.clone().map_or(Value::Null, Value::String),
|
||||
),
|
||||
])
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -244,8 +318,55 @@ impl DomainType for TraceBehaviour {
|
|||
self.outcome_ids.iter().map(String::as_str),
|
||||
"TraceBehaviour.outcomeIds",
|
||||
)?;
|
||||
self.validate_boundary_actions()
|
||||
}
|
||||
}
|
||||
|
||||
/// Boundary actions per transition: every slot at most once, plus one rollback.
|
||||
pub const MAX_BOUNDARY_ACTIONS: usize = crate::extensions::MAX_SLOTS + 1;
|
||||
|
||||
impl TraceBehaviour {
|
||||
/// Slot saves come first, each slot at most once, with the saved state's digest; at most
|
||||
/// one rollback, last, naming a slot and no digest (step-v1 section 6 amendment).
|
||||
fn validate_boundary_actions(&self) -> Result<()> {
|
||||
let mut rolled_back = false;
|
||||
let mut saved: Vec<&str> = Vec::new();
|
||||
for action in &self.boundary_actions {
|
||||
if !is_id(&action.slot_id) {
|
||||
return err("TraceBehaviour: boundary action slotId is not a valid id");
|
||||
}
|
||||
if rolled_back {
|
||||
return err("TraceBehaviour: nothing follows a rollback at the same boundary");
|
||||
}
|
||||
match action.kind {
|
||||
BoundaryActionKind::SaveSlot => {
|
||||
match &action.state_digest {
|
||||
Some(d) if is_digest(d) => {}
|
||||
_ => return err("TraceBehaviour: a slot save records its state digest"),
|
||||
}
|
||||
if saved.contains(&action.slot_id.as_str()) {
|
||||
return err("TraceBehaviour: a slot is saved at most once per boundary");
|
||||
}
|
||||
saved.push(&action.slot_id);
|
||||
}
|
||||
BoundaryActionKind::Rollback => {
|
||||
if action.state_digest.is_some() {
|
||||
return err("TraceBehaviour: a rollback records no state digest");
|
||||
}
|
||||
rolled_back = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// How many leading boundary actions are slot saves.
|
||||
pub fn slot_saves(&self) -> usize {
|
||||
self.boundary_actions
|
||||
.iter()
|
||||
.take_while(|a| a.kind == BoundaryActionKind::SaveSlot)
|
||||
.count()
|
||||
}
|
||||
}
|
||||
|
||||
/// One agent's domain request id for one phase.
|
||||
|
|
@ -267,6 +388,18 @@ pub struct TraceOperational {
|
|||
/// these and nothing in [`TraceBehaviour`].
|
||||
pub bus_call_ids: Vec<BusCallId>,
|
||||
pub delivery_ids: Vec<OwnerToken>,
|
||||
/// Checkpoint captures (and FLYSIM01 exports) taken at the reached boundary, each with the
|
||||
/// number of boundary actions already applied when it was taken. Operational because a
|
||||
/// capture's schedule is wall-clock policy; the ordering rule against slot saves is checked
|
||||
/// by [`TransitionTrace`] (amendment of 2026-09-23, legacy-gameboy-v1 section 16).
|
||||
pub captures: Vec<TraceCapture>,
|
||||
}
|
||||
|
||||
/// One checkpoint capture at the reached boundary.
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct TraceCapture {
|
||||
pub checkpoint_id: String,
|
||||
pub after_actions: u64,
|
||||
}
|
||||
|
||||
impl DomainType for TraceOperational {
|
||||
|
|
@ -296,6 +429,16 @@ impl DomainType for TraceOperational {
|
|||
Some(s) => OwnerToken::parse(s),
|
||||
None => err("every deliveryId must be a string"),
|
||||
})?;
|
||||
let captures = list(&mut f, "captures", 0, MAX_BOUNDARY_ACTIONS + 1, |v| {
|
||||
let mut c = Fields::new(v, "TraceOperational.captures")?;
|
||||
let checkpoint_id = c.id("checkpointId")?;
|
||||
let after_actions = c.int("afterActions", 0, MAX_BOUNDARY_ACTIONS as u64)?;
|
||||
c.finish()?;
|
||||
Ok(TraceCapture {
|
||||
checkpoint_id,
|
||||
after_actions,
|
||||
})
|
||||
})?;
|
||||
f.finish()?;
|
||||
let o = TraceOperational {
|
||||
wall_time_ns,
|
||||
|
|
@ -304,6 +447,7 @@ impl DomainType for TraceOperational {
|
|||
commit_request_ids,
|
||||
bus_call_ids,
|
||||
delivery_ids,
|
||||
captures,
|
||||
};
|
||||
o.validate()?;
|
||||
Ok(o)
|
||||
|
|
@ -341,6 +485,20 @@ impl DomainType for TraceOperational {
|
|||
.collect(),
|
||||
),
|
||||
),
|
||||
(
|
||||
"captures",
|
||||
Value::Array(
|
||||
self.captures
|
||||
.iter()
|
||||
.map(|c| {
|
||||
obj(vec![
|
||||
("checkpointId", c.checkpoint_id.clone().into()),
|
||||
("afterActions", Value::from(c.after_actions)),
|
||||
])
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
),
|
||||
])
|
||||
}
|
||||
|
||||
|
|
@ -361,6 +519,17 @@ impl DomainType for TraceOperational {
|
|||
self.delivery_ids.iter().map(OwnerToken::as_str),
|
||||
"TraceOperational.deliveryIds",
|
||||
)?;
|
||||
require_unique(
|
||||
self.captures.iter().map(|c| c.checkpoint_id.as_str()),
|
||||
"TraceOperational.captures",
|
||||
)?;
|
||||
let mut last = 0;
|
||||
for capture in &self.captures {
|
||||
if capture.after_actions < last {
|
||||
return err("TraceOperational: captures are recorded in the order they were taken");
|
||||
}
|
||||
last = capture.after_actions;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
@ -412,6 +581,9 @@ impl TransitionTrace {
|
|||
if a.event_ids != b.event_ids {
|
||||
out.push("eventIds differ".to_owned());
|
||||
}
|
||||
if a.boundary_actions != b.boundary_actions {
|
||||
out.push("boundaryActions differ".to_owned());
|
||||
}
|
||||
let ids_a: Vec<&str> = a.agents.iter().map(|x| x.agent_id.as_str()).collect();
|
||||
let ids_b: Vec<&str> = b.agents.iter().map(|x| x.agent_id.as_str()).collect();
|
||||
if ids_a != ids_b {
|
||||
|
|
@ -481,6 +653,25 @@ impl DomainType for TransitionTrace {
|
|||
}
|
||||
}
|
||||
}
|
||||
// A slot save due at a boundary completes before any capture at that boundary, so a
|
||||
// checkpoint never pairs a task ledger that names the new slot with the old slot
|
||||
// contents (legacy-gameboy-v1 section 16, review round 1 of 2026-09-23).
|
||||
let saves = self.behaviour.slot_saves() as u64;
|
||||
let actions = self.behaviour.boundary_actions.len() as u64;
|
||||
for capture in &self.operational.captures {
|
||||
if capture.after_actions < saves {
|
||||
return err(format!(
|
||||
"TransitionTrace: capture {:?} was taken before this boundary's slot saves completed",
|
||||
capture.checkpoint_id
|
||||
));
|
||||
}
|
||||
if capture.after_actions > actions {
|
||||
return err(format!(
|
||||
"TransitionTrace: capture {:?} counts more boundary actions than were applied",
|
||||
capture.checkpoint_id
|
||||
));
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2659,6 +2659,8 @@ impl Coordinator {
|
|||
outcome_ids,
|
||||
event_ids: event_ids.to_vec(),
|
||||
published_boundary: k + 1,
|
||||
// The synthetic composition takes no boundary actions (step-v1 section 8 amendment).
|
||||
boundary_actions: Vec::new(),
|
||||
};
|
||||
let operational = TraceOperational {
|
||||
// Wall time is for pacing, health and presentation only.
|
||||
|
|
@ -2673,6 +2675,9 @@ impl Coordinator {
|
|||
// and nothing in the behaviour above.
|
||||
bus_call_ids: Vec::new(),
|
||||
delivery_ids: Vec::new(),
|
||||
// Captures are recorded by the store path, not by the transition that reached the
|
||||
// boundary; the synthetic trace records none.
|
||||
captures: Vec::new(),
|
||||
};
|
||||
self.trace.transition(TransitionTrace { behaviour, operational });
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
144
services/flysim/crates/flysim/tests/legacy_profile_identity.rs
Normal file
144
services/flysim/crates/flysim/tests/legacy_profile_identity.rs
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
//! 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 fly_session_types::{canonical, fixtures};
|
||||
use flybrain_core::agent::{AgentConfig, DEFAULT_WARMUP_MS, GAMEBOY_MS_PER_FRAME, NeuralAgent};
|
||||
use flybrain_core::dataset::load_brain_dataset_from_dir;
|
||||
use flybrain_core::decoder::gameboy::{GAMEBOY_BUTTON_BITS, gameboy_decoder_config_with_macros};
|
||||
use flybrain_core::decoder::{DecoderConfig, ExclusiveGroup};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
#[test]
|
||||
fn the_profile_fingerprint_and_versions_are_the_ones_this_build_computes() {
|
||||
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]");
|
||||
}
|
||||
}
|
||||
|
||||
fn group_form(group: Option<&ExclusiveGroup>) -> Value {
|
||||
match group {
|
||||
None => Value::Null,
|
||||
Some(g) => json!({
|
||||
"channels": g.channels.iter()
|
||||
.map(|(channel, role)| json!({"channel": channel, "role": role}))
|
||||
.collect::<Vec<_>>(),
|
||||
"decisionMs": g.decision_ms,
|
||||
"holdMs": g.hold_ms,
|
||||
"hysteresis": g.hysteresis,
|
||||
"fatigueGain": g.fatigue_gain,
|
||||
"fatigueDecay": g.fatigue_decay,
|
||||
"blockedFatigue": g.blocked_fatigue,
|
||||
"blockedMs": g.blocked_ms,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
/// The canonical decoder-configuration form of legacy-gameboy-v1 section 12, built from the
|
||||
/// Rust twin. `@flybrain/session-types` builds the same value from the TypeScript oracle's
|
||||
/// preset (`decoderConfigForm`), and both are held to `fixtures/gameboy-decoder-config.json`.
|
||||
fn decoder_config_form(config: &DecoderConfig) -> Value {
|
||||
json!({
|
||||
"form": "gameboy-decoder-config-v1",
|
||||
"exclusive": group_form(config.exclusive.as_ref()),
|
||||
"macros": group_form(config.macros.as_ref()),
|
||||
"pulses": config.pulses.iter().map(|p| json!({
|
||||
"channel": p.channel,
|
||||
"role": p.role,
|
||||
"holdMs": p.hold_ms,
|
||||
"cooldownMs": p.cooldown_ms,
|
||||
"threshold": p.threshold,
|
||||
"boot": p.boot.map_or(Value::Null, |b| json!({"cooldownMs": b.cooldown_ms, "threshold": b.threshold})),
|
||||
"throttleGroup": p.throttle_group.clone().map_or(Value::Null, Value::String),
|
||||
})).collect::<Vec<_>>(),
|
||||
"clearLockoutMs": config.clear_lockout_ms,
|
||||
})
|
||||
}
|
||||
|
||||
/// The shared `decoderConfigDigest` vectors: raw mode and the Pokemon Red macro group, from
|
||||
/// `gameboy_decoder_config_with_macros`. `FLY_UPDATE_FIXTURES=1` rewrites the file; otherwise
|
||||
/// the checked-in values must be exactly what this build computes.
|
||||
#[test]
|
||||
fn the_decoder_config_digest_vectors_are_what_the_rust_preset_computes() {
|
||||
let pokered: Vec<&str> = flybrain_gb::macro_channels("pokemon-red");
|
||||
let cases: Vec<Value> = [("raw", Vec::new()), ("macros", pokered)]
|
||||
.into_iter()
|
||||
.map(|(name, channels)| {
|
||||
let form = decoder_config_form(&gameboy_decoder_config_with_macros(&channels));
|
||||
json!({
|
||||
"name": name,
|
||||
"macroChannels": channels,
|
||||
"form": form,
|
||||
"canonical": canonical::canonicalize(&form).expect("canonical"),
|
||||
"digest": canonical::digest_of(&form).expect("digest"),
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let file = json!({
|
||||
"description": "decoderConfigDigest vectors (legacy-gameboy-v1 section 12): the canonical form of gameboy_decoder_config_with_macros / gameboyDecoderConfig for raw mode and the Pokemon Red macro group. Written by FLY_UPDATE_FIXTURES=1 cargo test -p flysim --test legacy_profile_identity; both languages must reproduce every form and digest.",
|
||||
"cases": cases,
|
||||
});
|
||||
let mut text = serde_json::to_string_pretty(&file).expect("json");
|
||||
text.push('\n');
|
||||
let path = fixtures::dir().join("gameboy-decoder-config.json");
|
||||
if std::env::var_os("FLY_UPDATE_FIXTURES").is_some() {
|
||||
std::fs::write(&path, &text).expect("write the fixture");
|
||||
}
|
||||
let found = std::fs::read_to_string(&path).expect("the checked-in fixture");
|
||||
assert_eq!(
|
||||
found, text,
|
||||
"gameboy-decoder-config.json is stale; rerun with FLY_UPDATE_FIXTURES=1"
|
||||
);
|
||||
// Channel order is identity: reversing the macro group moves the digest.
|
||||
let mut reversed: Vec<&str> = flybrain_gb::macro_channels("pokemon-red");
|
||||
reversed.reverse();
|
||||
let other = canonical::digest_of(&decoder_config_form(&gameboy_decoder_config_with_macros(
|
||||
&reversed,
|
||||
)))
|
||||
.expect("digest");
|
||||
assert_ne!(Some(other.as_str()), cases[1]["digest"].as_str());
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue