Compare commits
19 commits
d5d9249ea9
...
174dc7eabd
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
174dc7eabd | ||
|
|
6d5827716f | ||
|
|
6c7ee5476c | ||
|
|
bbb25e1afc | ||
|
|
4b6624757b | ||
|
|
3c4bb30bed | ||
|
|
2f8ad4914a | ||
|
|
a6e1623698 | ||
|
|
24ba933500 | ||
|
|
bbf71bfead | ||
|
|
73b160c2f8 | ||
|
|
cc6f06ed2a | ||
|
|
ef98b04547 | ||
|
|
2d04abaf52 | ||
|
|
b8a5c8fc52 | ||
|
|
6f05b96334 | ||
|
|
acbc655a23 | ||
|
|
ab3d4cb6fb | ||
|
|
c9212e5498 |
63 changed files with 9840 additions and 108 deletions
|
|
@ -1374,6 +1374,77 @@ answers at one person in the twenty-minute reproduction.
|
|||
The decoder, the reward catalog, the adapter version, the roles and the compatibility string are
|
||||
untouched.
|
||||
|
||||
### 12.21 A button refused from here is not dealt again from here, a last resort walks where it can, and an escort walls the tile it fired on (2026-09-23, row 57)
|
||||
|
||||
Live on v0.5.3, rank 10, map 2 (**Pewter City**), scene `overworld`: the pad was **`GO ROUTE` and
|
||||
nothing else**, and it was refused about **740 times per ten brain minutes for more than two
|
||||
hours**, with one `GO ROUTE start` / `blocked` pair every ten brain minutes, no button pressed and
|
||||
the exploration count frozen. The watchdog read one start and one name, and never flagged it. The
|
||||
`refused` event's `value 3.0` is the slot, not a reason (row 55); the feed carries no reason.
|
||||
|
||||
The ledgers that dealt that pad are session state and a restore starts them empty, so the trap
|
||||
does not come back from the checkpoint by itself: a twenty-brain-minute hunt with the real brain
|
||||
from the live frame covers 356 tiles. It was reproduced by **earning** them -- a new probe mode
|
||||
(`examples/scene_probe.rs`, `FLY_PROBE_CATCH=route`) drives the real palette from the checkpoint and
|
||||
can seed each ledger -- and three facts came out, each measured on the cartridge.
|
||||
|
||||
- **The pushed ledger walled the tile a walk set out from, not the tile the script fired on.**
|
||||
Pewter City's youngster takes the joypad on four tiles by the road east
|
||||
(`PewterCityPlayerLeavingEastCoords`) and walks the fly to the gym until Brock is beaten. `GO
|
||||
ROUTE` aims east every time, because Route 3 is the one connection the run has not crossed, so it
|
||||
is escorted every time -- and row 37's ledger, which has **no window**, recorded the macro's
|
||||
starting tile. Measured from the ratchet's rollback snapshot: one `GO ROUTE` from the town's
|
||||
south entrance walked 26 tiles to (37, 18), was escorted, and walled **(18, 35)**, the south
|
||||
entrance. Row 37's rule is right for a press (the fly is standing on the tile the script fires
|
||||
on) and wrong for a walk. Walks start wherever the last one ended, so the walls accumulate
|
||||
until the fly stands in a pocket no route leaves.
|
||||
- **In the pocket every walk refuses `no route`**, and each refusal is recorded where it belongs:
|
||||
`GO FRONTIER`'s marks the map exhausted (12.14, no window, cleared only by new ground, and there
|
||||
was none for hours), and `GO OBJECTIVE`'s only goal, the gym's door, goes to the blocked ledger.
|
||||
With every person and sign already talked to and the errands paid, nothing else is left.
|
||||
- **The last resort is the one list that ignores the blocked ledger**, by design ("a target the
|
||||
ledger is resting is still the only place to go", 13.1). So `ways` dealt `GO ROUTE` at the gym's
|
||||
door, the route search refused it, the refusal wrote the door to a ledger the dealer does not
|
||||
read, and the button was dealt again on the next hold. The same refusal **re-stamped the door's
|
||||
window every hold**, which is why `GO OBJECTIVE` never came back either. Once per window the
|
||||
road east lapsed, `GO ROUTE` walked it, and it was excluded again.
|
||||
|
||||
Seeded with that pocket -- three pushed tiles sealing the strip by the road from the town, the
|
||||
frontier mark, everything talked to, the road east resting -- the cartridge deals exactly the live
|
||||
pad: `GO ROUTE`, refused `no route` **746 holds running** on one tile over ten brain minutes.
|
||||
|
||||
**The fix, all three parts inside the macros:**
|
||||
|
||||
- **A walk walls the tile it last stood the fly on**, which is where the cartridge took over; every
|
||||
other macro keeps the tile it started on. The same walk now walls (37, 18), one of the four
|
||||
tiles the youngster fires on.
|
||||
- **A refusal that taught the blocked ledger nothing is recorded with the tile the fly stood on,
|
||||
and the dealer does not deal that button from that tile for the blocked window.** The dealer
|
||||
asks the cheap question and `start` the real one, and the blocked ledger closes that gap for
|
||||
every list but a last resort; this closes it for all of them without a route search in the
|
||||
dealer, which runs every frame. Only a last resort deals goals the ledger is already resting, so
|
||||
a `no route` whose every goal was resting already -- and any `precondition` refusal, which writes
|
||||
nothing -- is the one remembered; a refusal that writes a new exclusion changes the next deal
|
||||
by itself. (Measured: holding *every* refusal against the tile kept the real brain, which
|
||||
pressed `GO ROUTE` first while the door was still the second tier's answer, from ever reaching
|
||||
the last resort below.) It is a fact about *here*: the button is dealt again the moment the fly stands
|
||||
on any other tile, or when the window closes. A pad with nothing left that can run is empty and
|
||||
the fly waits, which is section 13.1's honest answer.
|
||||
- **A last resort that cannot reach the objective's door takes a way out it can reach.** The
|
||||
narrowing to "the ways toward the objective" is a preference the dealer cannot check, and in the
|
||||
pocket it chose the gym's door beyond the fence while the road east was three tiles away,
|
||||
resting in its window (which a last resort ignores). When `start`'s route search cannot reach
|
||||
the preferred ways it tries the rest of the last resort, nearest reachable first, as every walk
|
||||
chooses. The pad does not change; only where the pressed macro walks.
|
||||
|
||||
In the rebuilt pocket the fly now leaves on **frame 517** (0.14 brain minutes) -- walked east, met
|
||||
by the youngster, carried to the gym -- against frame 36,325 on the base, when the road's window
|
||||
lapsed; `GO ROUTE` is refused **once** there against 746 holds running.
|
||||
|
||||
Nothing presses for the fly and nothing is ranked: one button leaves a pad it could not run from,
|
||||
one wall moves to the tile that earned it, and one walk goes where it can. The decoder, the reward catalog, the adapter
|
||||
version, the roles and the compatibility string are untouched.
|
||||
|
||||
## 13. Shops and Pokémon Centers (the operator, 2026-09-17: "refactor the shop macros. make it a
|
||||
## priority to visit the shop at least once per area; make shop macros item purchases. same
|
||||
## for the Pokécenter. heal should be a macro.")
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
437
docs/design/session-framework/legacy-gameboy-v1.md
Normal file
437
docs/design/session-framework/legacy-gameboy-v1.md
Normal file
|
|
@ -0,0 +1,437 @@
|
|||
# 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. In practice the
|
||||
two converge: after `fly-reset-to-milestone` onto a legacy archive, the ratchet captures rung
|
||||
`r` again on the first safe frame (the rung is above the recorded best) and resets the attempt
|
||||
counter. The difference is only where the rung `r` slot sits -- on the exact frame of the climb
|
||||
(ported) or on the first safe frame after the reset (legacy) -- and it is visible only if the
|
||||
fly stalls or hits game over before any safe frame, when legacy falls back to rung `r-1`'s save
|
||||
and the ported loop to rung `r`'s (amended 2026-09-23, review round 2). 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.
|
||||
|
|
|
|||
|
|
@ -19,6 +19,16 @@ the project."
|
|||
on the encoder output and `/status`, and records the release in `docs/stream-mvp-plan.md`.
|
||||
No approval round trip with the operator; he sees the record.
|
||||
|
||||
## Unstick while a fix cooks (the operator, 2026-09-23)
|
||||
|
||||
"lets unstick the fly so stream stays interesting. lets make that a rule. while a fix cooks,
|
||||
revert." When the watcher confirms a trap and its review agent is dispatched, the coordinator
|
||||
restarts flysim only (claimed in the host log): a restore starts with empty macro ledgers and
|
||||
keeps the rung. A recurrence gets another restart; only a fly still trapped about ten brain
|
||||
minutes after a restart is rolled back with `fly-reset-to-milestone`. This is an action outside
|
||||
the fly: it presses no button and changes no ROM, readout, catalog or adapter, and it never
|
||||
replaces the fix, which still ships through the loop above.
|
||||
|
||||
## The ethos check (every one must hold, or the fix is not shipped)
|
||||
|
||||
- The ROM is never modified. Game memory is read, never written. The only write into the
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -802,3 +802,16 @@ median; a two-fly transition near 10 to 12 ms at the median in every execution m
|
|||
32,496 -> 2,849. Rung 11 is not reached on either hunt arm: GO OBJECTIVE is on the Pewter pad
|
||||
and the fly picks the frontier instead, which is the fly's choice, not a trap. The watchdog's
|
||||
distinct-macro rule cannot see a four-macro ring; the watcher's zero-progress check covers it.
|
||||
- 2026-09-23 (v0.5.4, loop review, auto): row 57, Pewter City. The pad was GO ROUTE alone,
|
||||
refused `no route` every 0.8 s for over two hours with no button pressed; the watchdog counted
|
||||
starts only and saw one. Mechanism: the youngster's escort walls the tile each escorted walk
|
||||
set out from, up to 26 tiles away, until the fly stands in a pocket no route leaves; every
|
||||
refusal re-stamped the gym door's blocked window, so GO OBJECTIVE never came back. Fix: an
|
||||
escort walls the tile where the script took over; a refusal that teaches the ledger nothing is
|
||||
not dealt again from that tile for the window; a last resort that cannot reach the objective's
|
||||
door takes a way out it can. ROM test: refusals in a row 746 -> 1, out of the pocket on frame
|
||||
517 instead of 36,325; seeded hunt tiles 303 -> 439, flagged windows 70 -> 66 of 73 counting
|
||||
refusals as decisions. Check 10 now counts outcomes (refused, blocked, done) and flags
|
||||
`stalled` and `zero-progress`, still never acting. The GO OBJECTIVE / GO OUT ring at the gym
|
||||
door is row 58. Ethos check held. While it cooked the operator's new unstick rule kept the
|
||||
stream moving with flysim restarts (loop-review.md).
|
||||
|
|
|
|||
|
|
@ -67,6 +67,7 @@ log_info() {
|
|||
: "${WD_LOOP_MIN_REPEATS:=20}" # ... repeating at least this many times
|
||||
: "${WD_LOOP_DOMINANCE_PCT:=95}" # or one macro being this share of the window
|
||||
: "${WD_LOOP_MIN_EVENTS:=20}" # floor under the dominance rule (see check 10)
|
||||
: "${WD_LOOP_STALL_PCT:=90}" # decisions that ended refused/blocked/timeout: this share is a stall (row 57)
|
||||
: "${WD_LOOP_REPORT:=${WD_RUN_DIR}/loop.json}"
|
||||
|
||||
mkdir -p "$WD_RUN_DIR" "$WD_STATE_DIR"
|
||||
|
|
@ -187,7 +188,7 @@ write_textfile_metrics() {
|
|||
# convention as fly_watchdog_encoder_degraded), while the flag itself
|
||||
# starts at 0, because a 0/1 flag carrying -1 reads as a loop to every
|
||||
# alert expression that would ever use it.
|
||||
echo "# HELP fly_loop_suspected 1 when the last check-10 probe found a short macro cycle repeating with no growth in the exploration count. Report only: the watchdog never restarts or presses anything for this."
|
||||
echo "# HELP fly_loop_suspected 1 when the last check-10 probe found a short macro cycle repeating, one macro dominating, or the decisions stalled (refused/blocked) or completing nothing, with no growth in the exploration count. Report only: the watchdog never restarts or presses anything for this."
|
||||
echo "# TYPE fly_loop_suspected gauge"
|
||||
echo "fly_loop_suspected $(cat "${WD_RUN_DIR}/loop.suspected" 2>/dev/null || echo 0)"
|
||||
echo "# HELP fly_loop_period Length in macro labels of the shortest repeating block found at the end of the window (0 when nothing repeats, -1 before the first probe)."
|
||||
|
|
@ -199,6 +200,18 @@ write_textfile_metrics() {
|
|||
echo "# HELP fly_loop_distinct_macros Distinct macro names started in the window (-1 before the first probe)."
|
||||
echo "# TYPE fly_loop_distinct_macros gauge"
|
||||
echo "fly_loop_distinct_macros $(cat "${WD_RUN_DIR}/loop.distinct" 2>/dev/null || echo -1)"
|
||||
# Row 57: outcomes, not only starts. A pad whose one button refuses
|
||||
# every hold reads as one start and one name; these say what the
|
||||
# decisions came to (-1 before the first probe).
|
||||
echo "# HELP fly_loop_refused Macro presses refused (nothing pressed) in the last check-10 window (-1 before the first probe)."
|
||||
echo "# TYPE fly_loop_refused gauge"
|
||||
echo "fly_loop_refused $(cat "${WD_RUN_DIR}/loop.refused" 2>/dev/null || echo -1)"
|
||||
echo "# HELP fly_loop_blocked Macros that ended blocked or timed out in the last check-10 window (-1 before the first probe)."
|
||||
echo "# TYPE fly_loop_blocked gauge"
|
||||
echo "fly_loop_blocked $(cat "${WD_RUN_DIR}/loop.blocked" 2>/dev/null || echo -1)"
|
||||
echo "# HELP fly_loop_done Macros that ended done in the last check-10 window (-1 before the first probe)."
|
||||
echo "# TYPE fly_loop_done gauge"
|
||||
echo "fly_loop_done $(cat "${WD_RUN_DIR}/loop.done" 2>/dev/null || echo -1)"
|
||||
echo "# HELP fly_places_delta Growth in game.uniqueLocations (the exploration count) since the previous check-10 probe. -1 when there is no previous probe to compare against."
|
||||
echo "# TYPE fly_places_delta gauge"
|
||||
echo "fly_places_delta $(cat "${WD_RUN_DIR}/loop.places_delta" 2>/dev/null || echo -1)"
|
||||
|
|
@ -736,10 +749,22 @@ check_capture_freeze() {
|
|||
# time, not wall time — a box running under 1.0x realtime would otherwise get
|
||||
# a shorter window than the thresholds were measured over), the distinct macro
|
||||
# names in it, and the shortest block up to WD_LOOP_MAX_PERIOD that repeats at
|
||||
# the end of the sequence. Start events only: a start is one decision, and
|
||||
# counting the `done` beside it would double every sequence and halve every
|
||||
# period. They are the same names trap_hunt prints, so its numbers and these
|
||||
# are comparable.
|
||||
# the end of the sequence. The sequence is the fly's DECISIONS: a `start`, or a
|
||||
# `refused` (a bound button that was pressed and did nothing, with no start
|
||||
# beside it). Counting the `done` beside a start would double every sequence
|
||||
# and halve every period. They are the same names trap_hunt prints, so its
|
||||
# numbers and these are comparable.
|
||||
#
|
||||
# Row 57 (macros-traps.md): counting starts alone was blind to a pad whose
|
||||
# one button refused. `GO ROUTE refused` ~740 times per 10 brain minutes for
|
||||
# two hours read as 1 start, 1 distinct name, nothing flagged. So the window
|
||||
# also counts every OUTCOME (done/blocked/timeout/refused), and two rules read
|
||||
# them, both behind the same "no new ground" gate as the others:
|
||||
# stalled — WD_LOOP_MIN_EVENTS+ decisions and WD_LOOP_STALL_PCT% of
|
||||
# them ended refused, blocked or timed out;
|
||||
# zero-progress — decisions in the window, not one `done` among them, on
|
||||
# this probe AND the previous one (two probes, so a single
|
||||
# unlucky window never flags).
|
||||
#
|
||||
# The tile rule is what separates a loop from a legitimately repeating
|
||||
# explorer (macros-traps.md: `GO FRONTIER` x19 over 93 tiles is a walk longer
|
||||
|
|
@ -798,9 +823,12 @@ loop_macro_stream() {
|
|||
# loop_analyze — reads that stream on stdin and echoes one tab-separated line:
|
||||
#
|
||||
# total US distinct US topCount US period US repeats US windowFrom US
|
||||
# windowTo US topName US block US tail
|
||||
# windowTo US topName US block US tail US decisions US refused US blocked US
|
||||
# timeout US done
|
||||
#
|
||||
# `total` counts start events inside the window, `period`/`repeats` describe
|
||||
# `total` counts start events inside the window, `decisions` starts plus
|
||||
# refusals (the sequence, `distinct` and `topCount` are over decisions), and
|
||||
# the last four count each outcome in the window. `period`/`repeats` describe
|
||||
# the shortest repeating block at the END of the sequence (0/0 when nothing
|
||||
# repeats), `block` is that block comma-joined, and `tail` is the last 12
|
||||
# names for context. The window ends at the newest macro event's own brain
|
||||
|
|
@ -809,13 +837,19 @@ loop_analyze() {
|
|||
awk -F'\t' -v win="$WD_LOOP_WINDOW_MS" -v maxp="$WD_LOOP_MAX_PERIOD" '
|
||||
{ ms[NR] = $1 + 0; lbl[NR] = $2; n = NR }
|
||||
END {
|
||||
if (n == 0) { printf "0\0370\0370\0370\0370\0370\0370\037\037\037\n"; exit }
|
||||
if (n == 0) { printf "0\0370\0370\0370\0370\0370\0370\037\037\037\0370\0370\0370\0370\0370\n"; exit }
|
||||
to = ms[n]; from = to - win
|
||||
k = 0; distinct = 0; topn = 0; top = ""
|
||||
starts = 0; refused = 0; blocked = 0; timeout = 0; done = 0
|
||||
for (i = 1; i <= n; i++) {
|
||||
if (ms[i] < from) continue
|
||||
if (lbl[i] !~ / start$/) continue
|
||||
name = lbl[i]; sub(/ start$/, "", name)
|
||||
if (lbl[i] ~ / blocked$/) { blocked++; continue }
|
||||
if (lbl[i] ~ / timeout$/) { timeout++; continue }
|
||||
if (lbl[i] ~ / done$/) { done++; continue }
|
||||
if (lbl[i] ~ / refused$/) refused++
|
||||
else if (lbl[i] ~ / start$/) starts++
|
||||
else continue
|
||||
name = lbl[i]; sub(/ (start|refused)$/, "", name)
|
||||
seq[++k] = name
|
||||
if (!(name in cnt)) { cnt[name] = 0; distinct++ }
|
||||
cnt[name]++
|
||||
|
|
@ -839,8 +873,9 @@ loop_analyze() {
|
|||
tailstr = ""
|
||||
start = k - 11; if (start < 1) start = 1
|
||||
for (j = start; j <= k; j++) tailstr = tailstr (tailstr == "" ? "" : ", ") seq[j]
|
||||
printf "%d\037%d\037%d\037%d\037%d\037%d\037%d\037%s\037%s\037%s\n", \
|
||||
k, distinct, topn, period, repeats, from, to, top, block, tailstr
|
||||
printf "%d\037%d\037%d\037%d\037%d\037%d\037%d\037%s\037%s\037%s\037%d\037%d\037%d\037%d\037%d\n", \
|
||||
starts, distinct, topn, period, repeats, from, to, top, block, tailstr, \
|
||||
k, refused, blocked, timeout, done
|
||||
}'
|
||||
}
|
||||
|
||||
|
|
@ -898,13 +933,20 @@ check_loop() {
|
|||
echo "$delta" > "${WD_RUN_DIR}/loop.places_delta"
|
||||
|
||||
local analysis total distinct topn period repeats win_from win_to top block tailstr
|
||||
local decisions refused blocked timeouts completed
|
||||
analysis="$(loop_macro_stream | loop_analyze)"
|
||||
IFS=$'\037' read -r total distinct topn period repeats win_from win_to top block tailstr <<< "$analysis"
|
||||
IFS=$'\037' read -r total distinct topn period repeats win_from win_to top block tailstr \
|
||||
decisions refused blocked timeouts completed <<< "$analysis"
|
||||
total="${total:-0}"; distinct="${distinct:-0}"; topn="${topn:-0}"
|
||||
period="${period:-0}"; repeats="${repeats:-0}"
|
||||
decisions="${decisions:-0}"; refused="${refused:-0}"; blocked="${blocked:-0}"
|
||||
timeouts="${timeouts:-0}"; completed="${completed:-0}"
|
||||
echo "$distinct" > "${WD_RUN_DIR}/loop.distinct"
|
||||
echo "$period" > "${WD_RUN_DIR}/loop.period"
|
||||
echo "$repeats" > "${WD_RUN_DIR}/loop.repeats"
|
||||
echo "$refused" > "${WD_RUN_DIR}/loop.refused"
|
||||
echo "$(( blocked + timeouts ))" > "${WD_RUN_DIR}/loop.blocked"
|
||||
echo "$completed" > "${WD_RUN_DIR}/loop.done"
|
||||
|
||||
local prev_suspected=0
|
||||
[ -f "${WD_RUN_DIR}/loop.suspected" ] && prev_suspected="$(cat "${WD_RUN_DIR}/loop.suspected" 2>/dev/null || echo 0)"
|
||||
|
|
@ -917,13 +959,32 @@ check_loop() {
|
|||
local grown=1
|
||||
[ "$delta_known" -eq 1 ] && [ "$delta" -le 0 ] && grown=0
|
||||
|
||||
# Zero progress is judged over two probes: a window with decisions and no
|
||||
# `done` among them is remembered, and only a second one in a row flags.
|
||||
local idle_file="${WD_RUN_DIR}/loop.prev_idle" prev_idle=0 idle=0
|
||||
[ -f "$idle_file" ] && prev_idle="$(cat "$idle_file" 2>/dev/null || echo 0)"
|
||||
case "${prev_idle:-}" in ''|*[!0-9]*) prev_idle=0 ;; esac
|
||||
[ "$decisions" -gt 0 ] && [ "$completed" -eq 0 ] && [ "$grown" -eq 0 ] && idle=1
|
||||
echo "$idle" > "$idle_file"
|
||||
|
||||
local failed=$(( refused + blocked + timeouts ))
|
||||
local suspected=0 reason=""
|
||||
if [ "$total" -gt 0 ] && [ "$grown" -eq 0 ]; then
|
||||
if [ "$distinct" -le "$WD_LOOP_MAX_DISTINCT" ] && [ "$repeats" -ge "$WD_LOOP_MIN_REPEATS" ]; then
|
||||
if [ "$decisions" -gt 0 ] && [ "$grown" -eq 0 ]; then
|
||||
if [ "$decisions" -ge "$WD_LOOP_MIN_EVENTS" ] \
|
||||
&& [ $(( failed * 100 )) -ge $(( decisions * WD_LOOP_STALL_PCT )) ]; then
|
||||
# Row 57: the fly keeps deciding and nothing it decides runs. A
|
||||
# refused press is a decision with no start, which is why the
|
||||
# rules below were blind to a pad of one button that refused.
|
||||
suspected=1
|
||||
reason="stalled"
|
||||
elif [ "$idle" -eq 1 ] && [ "$prev_idle" -eq 1 ]; then
|
||||
suspected=1
|
||||
reason="zero-progress"
|
||||
elif [ "$distinct" -le "$WD_LOOP_MAX_DISTINCT" ] && [ "$repeats" -ge "$WD_LOOP_MIN_REPEATS" ]; then
|
||||
suspected=1
|
||||
reason="sequence"
|
||||
elif [ "$total" -ge "$WD_LOOP_MIN_EVENTS" ] \
|
||||
&& [ $(( topn * 100 )) -ge $(( total * WD_LOOP_DOMINANCE_PCT )) ]; then
|
||||
elif [ "$decisions" -ge "$WD_LOOP_MIN_EVENTS" ] \
|
||||
&& [ $(( topn * 100 )) -ge $(( decisions * WD_LOOP_DOMINANCE_PCT )) ]; then
|
||||
# One macro and almost nothing else. The floor under it is not in
|
||||
# the spec but is load-bearing: two starts in ten brain minutes are
|
||||
# 100% of a window and mean the fly is barely deciding at all,
|
||||
|
|
@ -935,7 +996,7 @@ check_loop() {
|
|||
echo "$suspected" > "${WD_RUN_DIR}/loop.suspected"
|
||||
|
||||
local dom_pct=0
|
||||
[ "$total" -gt 0 ] && dom_pct=$(( topn * 100 / total ))
|
||||
[ "$decisions" -gt 0 ] && dom_pct=$(( topn * 100 / decisions ))
|
||||
# The sequence a reader wants: the repeating block when there is one, the
|
||||
# single dominant name when the dominance rule is what fired.
|
||||
local shown="$block"
|
||||
|
|
@ -956,6 +1017,11 @@ check_loop() {
|
|||
--argjson repeats "$repeats" \
|
||||
--argjson distinct "$distinct" \
|
||||
--argjson total "$total" \
|
||||
--argjson decisions "$decisions" \
|
||||
--argjson refused "$refused" \
|
||||
--argjson blocked "$blocked" \
|
||||
--argjson timeouts "$timeouts" \
|
||||
--argjson completed "$completed" \
|
||||
--argjson windowFrom "${win_from:-0}" \
|
||||
--argjson windowTo "${win_to:-0}" \
|
||||
--argjson windowMs "$WD_LOOP_WINDOW_MS" \
|
||||
|
|
@ -982,6 +1048,8 @@ check_loop() {
|
|||
window: {
|
||||
brainMsFrom: $windowFrom, brainMsTo: $windowTo, brainMs: $windowMs,
|
||||
macroStarts: $total,
|
||||
decisions: $decisions,
|
||||
outcomes: { done: $completed, blocked: $blocked, timeout: $timeouts, refused: $refused },
|
||||
tail: (if $tail == "" then [] else ($tail | split(", ")) end)
|
||||
},
|
||||
dominant: { name: (if $dominant == "" then null else $dominant end),
|
||||
|
|
@ -1005,13 +1073,15 @@ check_loop() {
|
|||
|
||||
local mins=$(( WD_LOOP_WINDOW_MS / 60000 ))
|
||||
if [ "$suspected" -eq 1 ]; then
|
||||
if [ "$reason" = "sequence" ]; then
|
||||
if [ "$reason" = "stalled" ] || [ "$reason" = "zero-progress" ]; then
|
||||
log_err "loop suspected (${reason}): [${shown}] — ${decisions} decisions in the last ${mins} brain minutes, ${total} started, ${completed} done, ${refused} refused, $(( blocked + timeouts )) blocked or timed out, exploration count ${places:-?} unchanged (delta ${delta}), rung ${rank:-?} '${mlabel:-?}' for ${since:-?}s. NOT acting: nothing restarted, nothing pressed, the game untouched — a pad whose buttons cannot run is a macro bug. Report: ${WD_LOOP_REPORT}; see infra/docs/runbook.md 'loop suspected'."
|
||||
elif [ "$reason" = "sequence" ]; then
|
||||
log_err "loop suspected: [${shown}] x${repeats} (period ${period}) in the last ${mins} brain minutes — ${total} macro starts, ${distinct} distinct name(s), exploration count ${places:-?} unchanged (delta ${delta}), rung ${rank:-?} '${mlabel:-?}' for ${since:-?}s. NOT acting: nothing restarted, nothing pressed, the game untouched — a loop is a macro target-choice bug and a bounce would only restore the same loop. Report: ${WD_LOOP_REPORT}; see infra/docs/runbook.md 'loop suspected'."
|
||||
else
|
||||
log_err "loop suspected: [${shown}] is ${dom_pct}% of ${total} macro starts in the last ${mins} brain minutes (${distinct} distinct name(s)), exploration count ${places:-?} unchanged (delta ${delta}), rung ${rank:-?} '${mlabel:-?}' for ${since:-?}s. NOT acting: nothing restarted, nothing pressed, the game untouched. Report: ${WD_LOOP_REPORT}; see infra/docs/runbook.md 'loop suspected'."
|
||||
fi
|
||||
elif [ "$prev_suspected" -eq 1 ]; then
|
||||
log_info "loop cleared: ${total} macro starts over ${distinct} distinct name(s) in the last ${mins} brain minutes, longest repeat x${repeats} (period ${period}), exploration count ${places:-?} delta ${delta}. Nothing was ever done about the loop; if a fix went in, this is it landing."
|
||||
log_info "loop cleared: ${total} macro starts (${decisions} decisions, ${completed} done, ${refused} refused) over ${distinct} distinct name(s) in the last ${mins} brain minutes, longest repeat x${repeats} (period ${period}), exploration count ${places:-?} delta ${delta}. Nothing was ever done about the loop; if a fix went in, this is it landing."
|
||||
fi
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2531,3 +2531,127 @@ own measurement and the fourth branch in a row to run into it. The merge is Fabl
|
|||
- `flysim --print-compatibility`: **648 bytes, sha256 `4929f340...9ebd9`** -- byte-identical to this
|
||||
branch's base `2de2dce`. Decoder, reward catalog, adapter version and roles untouched.
|
||||
|
||||
|
||||
## 2026-09-23, row 57: one dead button, a last resort that could not see it, and walls the walks built
|
||||
|
||||
### What was live
|
||||
|
||||
Map 2 (Pewter City), scene `overworld`, rank 10, v0.5.3: the pad was **`GO ROUTE` and nothing
|
||||
else**, `GO ROUTE refused` about **740 times per ten brain minutes for more than two hours**, one
|
||||
`GO ROUTE start` / `blocked` pair every ten brain minutes, no button pressed, `uniqueLocations`
|
||||
frozen at 1,846. The live log: `GO FRONTIER` blocked and timed out through 02:36-02:38, three
|
||||
"Stuck" rollbacks to the rung's snapshot (the town's south entrance), `GO FRONTIER refused` at
|
||||
02:38:44 and `GO ROUTE refused` every hold from 02:38:45. The refusal's `value 3.0` is the slot
|
||||
(row 55). Check 10 read one start and one name and never flagged.
|
||||
|
||||
### The survey: earning the ledgers a restore throws away
|
||||
|
||||
The pad was dealt by session ledgers, and a restore starts them empty: from the live checkpoint
|
||||
the pad is `GO OBJECTIVE, GO ROUTE, GO ITEM, GO NPC, GO FRONTIER`, and a twenty-brain-minute hunt
|
||||
with the real brain covers 356 tiles; sixty brain minutes from the pre-deploy gym checkpoint
|
||||
covers 1,064 without meeting it. `FLY_PROBE_CATCH=route` (`examples/scene_probe.rs`) drives the
|
||||
real palette from the checkpoint and seeds each ledger (`examples/support/ledgers.rs`, shared
|
||||
with the hunt as `FLY_TRAP_SEED_*`); `FLY_PROBE_RATCHET=1` starts from the rollback snapshot.
|
||||
|
||||
- **One escorted walk, one wall in the wrong place.** From the rollback snapshot at (18, 35),
|
||||
`GO ROUTE` walked 26 tiles to (37, 18) -- one of `PewterCityPlayerLeavingEastCoords`, where the
|
||||
youngster takes the joypad and walks the fly to the gym -- and the pushed ledger (row 37, no
|
||||
window) walled **(18, 35)**: the tile the walk set out from.
|
||||
- **The pocket.** Seeded with three pushed tiles sealing the strip by the road east from the town,
|
||||
every tile stood on, every sign and person talked to, the frontier mark and the road east
|
||||
resting: the pad is `GO OBJECTIVE, GO ROUTE`, `GO OBJECTIVE` refuses `no route` once, and then
|
||||
`GO ROUTE` alone, refused `no route` every hold -- **746 holds running on one tile** in ten
|
||||
brain minutes. Read at the frame: `stranded` true, `ways(Route)` = the gym's door (16, 17),
|
||||
blocked and unreachable; the road east reachable and blocked. At the window's lapse `GO ROUTE`
|
||||
walks east and the youngster carries the fly out.
|
||||
|
||||
| # | trap | trigger | test | fix, or why it is left |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 57 | a last resort is dealt by a dealer that ignores the blocked ledger, so its `no route` refusal cannot take it off the pad, and the refusal re-stamps the objective's door every hold | any map where the fly is walled off from the way toward the objective with nothing else left; Pewter City, two hours, `GO ROUTE` refused ~740 times per ten brain minutes | `a_way_out_refused_from_here_is_not_dealt_again_from_here`, part two of `the_pewter_east_pad_is_never_one_dead_button_from_the_rung_ten_checkpoint` (ROM-gated) | **fixed**: a refusal that teaches the blocked ledger nothing -- a `no route` whose every goal was already resting there, which only a last resort deals, or a `precondition` -- is recorded with the fly's tile, and the dealer does not deal that button from that tile for the blocked window. Dealt again the moment the fly stands elsewhere or the window closes. `docs/design/macros.md` 12.21 |
|
||||
| 57b | the last resort narrows to the ways toward the objective, so a reachable way out resting in its window is never tried while the preferred door is unreachable | the same pocket: the gym's door beyond the fence, the road east three tiles away | `a_last_resort_that_cannot_reach_the_objectives_door_takes_a_way_out_it_can_reach`, part two of the ROM test | **fixed**: when `start`'s route search cannot reach the preferred ways it tries the rest of the last resort, nearest reachable first. The pad is unchanged |
|
||||
| 57c | an escorted walk walls the tile it **set out from**, with no window, so walks toward a scripted road fence the fly in | Pewter City's youngster: every `GO ROUTE` toward Route 3 without the Boulder Badge | `an_escorted_walk_walls_the_tile_it_reached_not_the_one_it_set_out_from`, part one of the ROM test | **fixed**: a walk records the tile it last stood the fly on, which is where the cartridge took over; every other macro keeps its starting tile |
|
||||
| 57d | check 10 counts macro starts only, so a pad whose one button is refused every hold reads as one start and one name | the whole of row 57 | `infra/tests/lint.sh` check-10 cases 5 and 6 | **fixed**: the window counts every outcome; `stalled` (90% of 20+ decisions refused, blocked or timed out) and `zero-progress` (decisions and no `done`, two probes running), both behind the no-new-ground gate; `fly_loop_refused`, `fly_loop_blocked`, `fly_loop_done` exported. It still never acts |
|
||||
|
||||
### The ROM-gated run, from the live checkpoint
|
||||
|
||||
`the_pewter_east_pad_is_never_one_dead_button_from_the_rung_ten_checkpoint`
|
||||
(`FLY_PEWTER_EAST_CHECKPOINT`):
|
||||
|
||||
| measure | base `d5d9249` | this branch |
|
||||
| --- | ---: | ---: |
|
||||
| part one: tile the escorted walk walls | **(18, 35)**, where it set out | **(37, 18)**, where the script fired |
|
||||
| part two: `GO ROUTE` refused in the pocket | **746 holds running** | **1** |
|
||||
| part two: the fly leaves the pocket | frame 36,325 (10.1 brain min) | **frame 517 (0.14)** |
|
||||
|
||||
### The trap hunt, before and after
|
||||
|
||||
Same seed and checkpoint, twenty brain minutes, the real brain; base `d5d9249` (plus the survey
|
||||
seam, no behaviour) against this branch.
|
||||
|
||||
**From the bare checkpoint** the two arms are **identical** -- 356 distinct tiles, 73 of 73
|
||||
windows flagged, 1,272 macros, 3 refusals -- because the ledgers that make this row are built over
|
||||
tens of minutes of a live session and a restore starts them empty. Sixty brain minutes from the
|
||||
pre-deploy gym checkpoint on the base (1,064 tiles, 47 scattered refusals) does not meet it either.
|
||||
|
||||
**From the rebuilt pocket** (`FLY_TRAP_SEED_*`: the three-tile seal, every tile stood on, the
|
||||
frontier mark, everything talked to, the road east resting):
|
||||
|
||||
| measure | before | after |
|
||||
| --- | ---: | ---: |
|
||||
| refused | **748** (`GO ROUTE` 747) | **6** |
|
||||
| longest run of one macro refused on one tile | **747** | **1** |
|
||||
| the first ten brain minutes | in the pocket: one overworld run of 35,874 frames | out at once; longest overworld run 1,948 |
|
||||
| distinct (map, tile) | 303 | **439** |
|
||||
| macros started | 592 | 1,168 |
|
||||
| windows flagged | **31** / 73 | 66 / 73 |
|
||||
|
||||
**The flagged-window count rises, and this says why rather than smoothing it.** The before arm's
|
||||
ten stuck minutes are not flagged at all: the hunt, like check 10 before this row, counts macro
|
||||
*starts*, and a refused press starts nothing, so a window of 740 refusals on one tile reads as
|
||||
silence. The after arm is flagged for battles (`NEXT` on one tile, row 50's rule) and, from 14.75
|
||||
brain minutes, for **`GO OUT, GO OUT, GO OBJECTIVE` x16 to x35 on five tiles at the gym's door**.
|
||||
That ring is **not this branch's**. The base, run 35 brain minutes from the same pocket with the
|
||||
real brain, reaches the same ring -- `GO OUT, GO OBJECTIVE, GO OUT` x14 to x20 from 23.75 brain
|
||||
minutes, about fourteen after it leaves the pocket at ten, `GO OUT` done 435 -- and driven with
|
||||
uniform choices for 42 brain minutes from the pocket it runs it harder (`GO OUT` done **853**, `GO OBJECTIVE` 235, `GO ROUTE` refused
|
||||
749) than this branch (`GO OUT` 243, `GO OBJECTIVE` 91, `GO ROUTE` refused 4). It is row 56's
|
||||
named next brief -- `GO OBJECTIVE`'s aim at a person inside a building -- reached sooner because
|
||||
the fly is no longer held in the pocket.
|
||||
|
||||
### Residuals, named rather than worked around
|
||||
|
||||
- **The live pocket is reconstructed, not recovered.** The pushed tiles the session held are
|
||||
session state; the three-tile seal is the smallest one that reproduces the live pad and its
|
||||
refusal rate. Live, the once-per-window `GO ROUTE` ended `blocked` in 3.8 s with no escort;
|
||||
in the reconstruction it ends in the escort. The mechanism is the same; the exact walls are not
|
||||
known.
|
||||
- **A press answered by a script still walls its own tile** (row 37's rule, unchanged): the
|
||||
`NEXT` that advanced the youngster's last box walled (11, 18), where the escort ends. Not a trap
|
||||
measured here; named.
|
||||
- **The hunt from the bare checkpoint cannot see this row**: the ledgers that make it are built
|
||||
over tens of minutes of a live session. The seeded arm is the measurement.
|
||||
- **The hunt's window rule still counts starts only.** It now prints refusals by macro and the
|
||||
longest run on one tile, but a window of refusals is still not flagged; changing what the hunt
|
||||
flags moves every earlier row's before/after and is left for its own change.
|
||||
- **A trainer's sight line is a push-back too.** `scripted` is any joypad takeover, so a walk that
|
||||
steps into a trainer's view walls the tile it reached -- and, before this row, the tile it set
|
||||
out from -- with no window, though the trainer beaten opens it again. Moved, not introduced;
|
||||
named.
|
||||
- **Rung 11 is not reached** on any arm, as in row 56: `GO OBJECTIVE`'s aim at a person behind a
|
||||
door is still the next brief.
|
||||
- The runbook's "at most 3 distinct macro names" and `WD_LOOP_MAX_DISTINCT=4` in the watchdog
|
||||
disagree; older than this row and left.
|
||||
|
||||
### Gates
|
||||
|
||||
- `cargo test --workspace --no-fail-fast` with `FLY_ROM` and `FLY_DATASET`: **1,245 passed, 1
|
||||
failed** -- `flysim::integration::the_service_streams_takes_sugar_checkpoints_and_resumes_after_being_killed`,
|
||||
the debug-build boot failure row 56 recorded (`/status` still the booting header, `total` 1
|
||||
against 38), **failing identically on the base `d5d9249`**. Nothing under `crates/flysim/src`
|
||||
changes on this branch.
|
||||
- ROM-gated in release with the live checkpoint: `the_pewter_east_pad_is_never_one_dead_button_from_the_rung_ten_checkpoint` passes.
|
||||
- `cargo clippy --all-targets`: **0 warnings**.
|
||||
- `npm test`: 655 passed, 0 failed; `npm run typecheck`: clean.
|
||||
- `infra/tests/lint.sh`: ALL CHECKS PASSED, the two new check-10 cases and the de-PII guard included.
|
||||
- `flysim --print-compatibility`: **648 bytes, sha256 `4929f340...9ebd9`** -- byte-identical to the
|
||||
base `d5d9249`. Decoder, reward catalog, adapter version and roles untouched.
|
||||
|
|
|
|||
|
|
@ -599,9 +599,17 @@ pct exec <ctid> -- cat /run/fly/wd/loop.json | jq .
|
|||
| `fly_loop_repeats` | how many times that block repeats at the end of the 10-brain-minute window |
|
||||
| `fly_loop_distinct_macros` | distinct macro names started in the window |
|
||||
| `fly_places_delta` | growth in `game.uniqueLocations` since the previous probe (`-1` = no previous probe) |
|
||||
| `fly_loop_refused` | macro presses refused in the window: a bound button pressed, nothing run |
|
||||
| `fly_loop_blocked` | macros that ended `blocked` or `timeout` in the window |
|
||||
| `fly_loop_done` | macros that ended `done` in the window |
|
||||
|
||||
The flag needs **both** halves: at most 3 distinct macro names with the block repeating 20+
|
||||
times, or one macro at 95%+ of the window — **and** no growth in the exploration count. A
|
||||
times, one macro at 95%+ of the window's decisions, 90%+ of 20+ decisions ending refused,
|
||||
blocked or timed out (`stalled`), or decisions with no `done` among them on two probes in a row
|
||||
(`zero-progress`) — **and** no growth in the exploration count. A decision is a `start` or a
|
||||
`refused`: a refused press starts nothing, which is why counting starts alone read row 57's
|
||||
pad (`GO ROUTE refused` ~740 times in ten brain minutes, `macros-traps.md`) as one start and
|
||||
one name. A
|
||||
repeating macro over ground that keeps growing is a walk longer than the 600-frame cap, not a
|
||||
trap (`macros-traps.md`: `GO FRONTIER` x19 across 93 tiles), and the watchdog is deliberately
|
||||
quiet about it. The thresholds are `WD_LOOP_*` in `infra/bin/fly-watchdog`; the 3-name ceiling
|
||||
|
|
|
|||
|
|
@ -889,6 +889,26 @@ LPCAT
|
|||
printf '{"id":999999,"wallMs":1758'
|
||||
} > "$out"
|
||||
}
|
||||
# lp_outcomes FILE OUTCOME — like lp_events, but each name on stdin is one
|
||||
# decision that ended OUTCOME: `refused` writes the refusal alone (nothing
|
||||
# started, which is what a refused press is), anything else a start and
|
||||
# that outcome. Row 57's shape is `GO ROUTE refused` every 800 brain ms.
|
||||
lp_outcomes() {
|
||||
local out="$1" outcome="$2" id=0 ms=0 nm
|
||||
{
|
||||
while IFS= read -r nm; do
|
||||
if [ "$outcome" != "refused" ]; then
|
||||
id=$(( id + 1 ))
|
||||
printf '{"id":%d,"wallMs":%d,"brainMs":%d,"kind":"macro","label":"%s start","value":3}\n' \
|
||||
"$id" "$(( 1758000000000 + id ))" "$ms" "$nm"
|
||||
fi
|
||||
id=$(( id + 1 ))
|
||||
printf '{"id":%d,"wallMs":%d,"brainMs":%d,"kind":"macro","label":"%s %s","value":3}\n' \
|
||||
"$id" "$(( 1758000000000 + id ))" "$ms" "$nm" "$outcome"
|
||||
ms=$(( ms + 800 ))
|
||||
done
|
||||
} > "$out"
|
||||
}
|
||||
# lp_status FILE PLACES — the /status.json fields check 10 reads. `places`
|
||||
# is `game.uniqueLocations`; there is no `places` field in the contract.
|
||||
lp_status() {
|
||||
|
|
@ -1001,10 +1021,54 @@ LPCAT
|
|||
fail "check 10: expected the flag to clear on places growth, got flagged=${lp_flagged} then suspected=$(lp_metric fly_loop_suspected) places_delta=$(lp_metric fly_places_delta), journal: $(cat "$lp_fixture/journal.log")"
|
||||
fi
|
||||
|
||||
# (5) Row 57: a pad of one button that refuses every hold. One start in the
|
||||
# window and one name, so the sequence and dominance rules over starts
|
||||
# alone never fired; the outcomes say it is a stall.
|
||||
lp_reset
|
||||
{ lp_cycle 700 "GO ROUTE" | lp_outcomes "$lp_fixture/refused.jsonl" refused
|
||||
echo "GO ROUTE" | lp_outcomes "$lp_fixture/blocked.jsonl" blocked
|
||||
cat "$lp_fixture/refused.jsonl" "$lp_fixture/blocked.jsonl"; } > "$lp_fixture/events.jsonl"
|
||||
lp_status "$lp_fixture/status.json" 1846
|
||||
lp_pass
|
||||
lp_first="$(lp_metric fly_loop_suspected)"
|
||||
lp_pass
|
||||
if [ "$lp_first" = "0" ] \
|
||||
&& [ "$(lp_metric fly_loop_suspected)" = "1" ] \
|
||||
&& [ "$(lp_metric fly_loop_refused)" = "700" ] \
|
||||
&& [ "$(lp_metric fly_loop_blocked)" = "1" ] \
|
||||
&& [ "$(lp_metric fly_loop_done)" = "0" ] \
|
||||
&& grep -q 'loop suspected (stalled): \[GO ROUTE\]' "$lp_fixture/journal.log"; then
|
||||
pass "check 10: a pad whose one button is refused every hold flags as stalled (700 refused, 1 blocked, 0 done)"
|
||||
else
|
||||
fail "check 10: the row-57 refusal log gave first=${lp_first} suspected=$(lp_metric fly_loop_suspected) refused=$(lp_metric fly_loop_refused) blocked=$(lp_metric fly_loop_blocked) done=$(lp_metric fly_loop_done), journal: $(cat "$lp_fixture/journal.log")"
|
||||
fi
|
||||
if lp_report="$(jq -e -r '[.reason, (.window.macroStarts|tostring), (.window.decisions|tostring), (.window.outcomes.refused|tostring), (.window.outcomes.done|tostring), .action] | join(" ")' "$lp_fixture/run/loop.json" 2>/dev/null)" \
|
||||
&& [ "$lp_report" = "stalled 1 701 700 0 none" ]; then
|
||||
pass "check 10: loop.json carries the decisions and every outcome, not only the starts"
|
||||
else
|
||||
fail "check 10: loop.json read back as '${lp_report:-UNREADABLE}' — expected 'stalled 1 701 700 0 none'"
|
||||
fi
|
||||
|
||||
# (6) Zero progress: a handful of decisions, every one blocked, too few for
|
||||
# the stall rule's floor. One probe of it is not enough; two in a row are.
|
||||
lp_reset
|
||||
lp_cycle 3 "GO OBJECTIVE" "GO FRONTIER" | lp_outcomes "$lp_fixture/events.jsonl" blocked
|
||||
lp_status "$lp_fixture/status.json" 1846
|
||||
lp_pass
|
||||
lp_pass
|
||||
lp_first="$(lp_metric fly_loop_suspected)"
|
||||
lp_pass
|
||||
if [ "$lp_first" = "0" ] && [ "$(lp_metric fly_loop_suspected)" = "1" ] \
|
||||
&& grep -q 'loop suspected (zero-progress)' "$lp_fixture/journal.log"; then
|
||||
pass "check 10: decisions that complete nothing over two probes with no new ground flag as zero-progress"
|
||||
else
|
||||
fail "check 10: the zero-progress case gave first=${lp_first} then suspected=$(lp_metric fly_loop_suspected), journal: $(cat "$lp_fixture/journal.log")"
|
||||
fi
|
||||
|
||||
# The ethos, asserted rather than reviewed: over every case above, check 10
|
||||
# restarted nothing. It reports; a human or a review agent decides.
|
||||
if [ ! -s "$lp_fixture/systemctl.log" ]; then
|
||||
pass "check 10: never acts — no unit was restarted across any of the four cases"
|
||||
pass "check 10: never acts — no unit was restarted across any of the six cases"
|
||||
else
|
||||
fail "check 10 ACTED, which it must never do: $(cat "$lp_fixture/systemctl.log")"
|
||||
fi
|
||||
|
|
|
|||
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,
|
||||
|
|
|
|||
|
|
@ -552,6 +552,22 @@ pub trait MacroState: GameState {
|
|||
false
|
||||
}
|
||||
|
||||
/// Whether the macro in `slot` was refused, for want of a route or of its precondition, on
|
||||
/// the tile the fly is standing on now, inside the blocked window (`infra/docs/macros-traps.md`
|
||||
/// row 57).
|
||||
///
|
||||
/// The dealer asks the cheap question and `start` asks the real one, so a button can be dealt
|
||||
/// that its own route search refuses. The blocked ledger usually closes that gap -- the refusal
|
||||
/// writes what it could not reach and the dealer stops offering it -- but not for a list that
|
||||
/// deliberately ignores that ledger: a last resort. Live on rung 10, Pewter City, two hours:
|
||||
/// the pad was `GO ROUTE` and nothing else, refused `no route` every 800 brain ms, because
|
||||
/// `ways`'s last resort ignores the window its own refusal writes. A refusal from *here* is a
|
||||
/// fact about here; the fly standing anywhere else, or the window closing, is what can change
|
||||
/// it. Session state, never checkpointed.
|
||||
fn refused_here(&mut self, _slot: u8) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// Whether `GO ITEM` or `GO NPC` has already arrived at `target` and faced it this session.
|
||||
///
|
||||
/// The other half of the same stall: `GO NPC` walked to the same villager again and again,
|
||||
|
|
@ -907,6 +923,13 @@ pub trait TargetLedger {
|
|||
|
||||
/// Whether `target` on `map` has been arrived at and faced this session.
|
||||
fn reached(&self, map: u8, target: TargetKey) -> bool;
|
||||
|
||||
/// Whether the macro in `slot` was refused standing on `tile` of `map`, inside the window
|
||||
/// ([`MacroState::refused_here`]). Defaulted, so a ledger that never records one refuses
|
||||
/// nothing.
|
||||
fn refused(&self, _map: u8, _slot: u8, _tile: Tile) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Ledgers that have recorded nothing: every target still a candidate.
|
||||
|
|
@ -948,6 +971,9 @@ pub struct Targets {
|
|||
/// stayed shut. The window keeps the loop bounded at one walk per target per window while
|
||||
/// leaving the conversation available.
|
||||
reached: std::collections::BTreeMap<(u8, TargetKey), f64>,
|
||||
/// (map, slot, tile) -> the brain millisecond a macro was refused standing there
|
||||
/// ([`MacroState::refused_here`], row 57). The same window as `blocked`.
|
||||
refused: std::collections::BTreeMap<(u8, u8, Tile), f64>,
|
||||
/// The brain clock of the frame being decided, from [`super::driver::PokemonPalette`].
|
||||
now_ms: f64,
|
||||
/// How long an entry excludes its target, in brain milliseconds.
|
||||
|
|
@ -989,6 +1015,7 @@ impl Targets {
|
|||
blocked: std::collections::BTreeMap::new(),
|
||||
strikes: std::collections::BTreeMap::new(),
|
||||
reached: std::collections::BTreeMap::new(),
|
||||
refused: std::collections::BTreeMap::new(),
|
||||
now_ms: 0.0,
|
||||
window_ms: minutes * MINUTE_MS,
|
||||
}
|
||||
|
|
@ -1006,6 +1033,15 @@ impl Targets {
|
|||
self.window_ms / MINUTE_MS
|
||||
}
|
||||
|
||||
/// Record that the macro in `slot` was refused -- no route, or no precondition -- with the fly
|
||||
/// standing on `tile` of `map` (row 57). Re-recording restarts the window, as `blocked` does.
|
||||
pub fn record_refused(&mut self, map: u8, slot: u8, tile: Tile) {
|
||||
let now = self.now_ms;
|
||||
let window = self.window_ms;
|
||||
self.refused.retain(|_, at| now - *at < window);
|
||||
self.refused.insert((map, slot, tile), now);
|
||||
}
|
||||
|
||||
/// Record a `Blocked` or `Timeout` abort against the target it was aimed at.
|
||||
///
|
||||
/// Re-recording restarts the window, which is the honest reading: the walk failed *again*.
|
||||
|
|
@ -1080,4 +1116,8 @@ impl TargetLedger for Targets {
|
|||
fn reached(&self, map: u8, target: TargetKey) -> bool {
|
||||
self.reached.get(&(map, target)).is_some_and(|at| self.now_ms - *at < self.window_ms)
|
||||
}
|
||||
|
||||
fn refused(&self, map: u8, slot: u8, tile: Tile) -> bool {
|
||||
self.refused.get(&(map, slot, tile)).is_some_and(|at| self.now_ms - *at < self.window_ms)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -165,6 +165,44 @@ impl PokemonPalette {
|
|||
self.frontiers.len()
|
||||
}
|
||||
|
||||
/// Read the frame exactly as the macros read it -- this session's ledgers included -- and hand
|
||||
/// the state to `visit`. A survey seam (`examples/scene_probe.rs`), not a decision path: it
|
||||
/// writes nothing, and what it sees is what the next `observe` would deal from.
|
||||
pub fn inspect<R>(
|
||||
&mut self,
|
||||
memory: &mut dyn MemoryReader,
|
||||
ledger: &dyn RunLedger,
|
||||
visit: impl FnOnce(&mut dyn MacroState) -> R,
|
||||
) -> R {
|
||||
let mut state = PokeState::with_ledgers(
|
||||
memory,
|
||||
ledger,
|
||||
&self.talked,
|
||||
&self.targets,
|
||||
&self.stood,
|
||||
&self.areas,
|
||||
&self.pushed,
|
||||
)
|
||||
.caching_grid(&mut self.grids)
|
||||
.with_frontiers(&self.frontiers);
|
||||
visit(&mut state)
|
||||
}
|
||||
|
||||
/// The session's no-window ledgers, for a survey line: the tiles the cartridge has pushed the
|
||||
/// fly off and the maps whose frontier is marked unreachable.
|
||||
pub fn fences(&self) -> (&Pushed, &Frontiers) {
|
||||
(&self.pushed, &self.frontiers)
|
||||
}
|
||||
|
||||
/// The session's ledgers, writable, for a survey that has to rebuild a live session's state
|
||||
/// from a checkpoint (a restore starts them empty by design). Never called by the loop.
|
||||
#[doc(hidden)]
|
||||
pub fn ledgers_mut(
|
||||
&mut self,
|
||||
) -> (&mut Talked, &mut Targets, &mut Stood, &mut Pushed, &mut Frontiers) {
|
||||
(&mut self.talked, &mut self.targets, &mut self.stood, &mut self.pushed, &mut self.frontiers)
|
||||
}
|
||||
|
||||
/// Take whatever the machine's last finished macro earned into the session's ledgers.
|
||||
fn record_talk(&mut self) {
|
||||
if let Some((map, target)) = self.machine.take_talked() {
|
||||
|
|
@ -182,6 +220,11 @@ impl PokemonPalette {
|
|||
if let Some((map, tile)) = self.machine.take_pushed() {
|
||||
self.pushed.record(map, tile);
|
||||
}
|
||||
// A refusal from where the fly is standing: that button is not dealt again from this tile
|
||||
// for the window (row 57). The fly moving, or the window closing, deals it again.
|
||||
if let Some((map, slot, tile)) = self.machine.take_refused() {
|
||||
self.targets.record_refused(map, slot, tile);
|
||||
}
|
||||
// A frontier the walk could not reach any of: a fact about this map's ground, with no
|
||||
// window on it (section 12.14).
|
||||
if let Some(map) = self.machine.take_exhausted() {
|
||||
|
|
@ -414,6 +457,7 @@ impl MacroPalette for PokemonPalette {
|
|||
// reach: the fly is about to be standing somewhere else.
|
||||
let _ = self.machine.take_pushed();
|
||||
let _ = self.machine.take_exhausted();
|
||||
let _ = self.machine.take_refused();
|
||||
// The cached palette was dealt for a frame that is being thrown away. Dropping it makes
|
||||
// the next `start` before the next `observe` a nameless refusal, which presses nothing
|
||||
// and reports nothing, rather than a named refusal against a scene that no longer exists.
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ use super::palette::{
|
|||
precondition,
|
||||
shop_screen, stock_index, throw_slot, untalked_objects, untalked_people, ways,
|
||||
};
|
||||
use super::path::{self, Route, Way};
|
||||
use super::path::{self, Exit, Route, Way};
|
||||
use super::state::{Facing, Scene, ShopScreen};
|
||||
|
||||
/// Hard cap on one macro, in game frames. `docs/design/macros.md` section 4: "hard cap 600 frames
|
||||
|
|
@ -590,6 +590,10 @@ pub struct MacroMachine {
|
|||
/// from the frame the push is seen, because by the time the next macro starts the fly has been
|
||||
/// walked somewhere else.
|
||||
pushed_tile: Option<(u8, Tile)>,
|
||||
/// A refusal the route search or the precondition made, and where the fly stood for it --
|
||||
/// `(map, slot, tile)`, waiting to be taken into the session's ledger
|
||||
/// ([`super::cartridge::Targets::record_refused`], row 57 of `infra/docs/macros-traps.md`).
|
||||
refused_at: Option<(u8, u8, Tile)>,
|
||||
/// The target a frame-cap `Timeout` spent itself on, and whether the walk ended nearer a goal
|
||||
/// than it began: [`super::cartridge::Targets::record_timeout`]'s two arguments.
|
||||
///
|
||||
|
|
@ -661,6 +665,7 @@ impl MacroMachine {
|
|||
reached: None,
|
||||
exhausted: None,
|
||||
pushed_tile: None,
|
||||
refused_at: None,
|
||||
timed_out: None,
|
||||
resume: VecDeque::new(),
|
||||
pending_talk: None,
|
||||
|
|
@ -689,10 +694,19 @@ impl MacroMachine {
|
|||
machine.outcome = Some((spec.name, MacroAbort::Refused));
|
||||
Err(MacroRefused { slot, reason })
|
||||
};
|
||||
// Where the fly stands for a refusal that is a fact about *here* (row 57): no route from
|
||||
// this tile, or a precondition the dealer and the starter answered differently on it.
|
||||
let here = state.player().map(|player| (player.map, Tile::new(player.x, player.y)));
|
||||
let refused_here = |machine: &mut Self| {
|
||||
if let Some((map, tile)) = here {
|
||||
machine.refused_at = Some((map, slot.0, tile));
|
||||
}
|
||||
};
|
||||
if class(scene) != class(palette.scene) {
|
||||
return refuse(self, Refusal::WrongScene);
|
||||
}
|
||||
if !precondition(spec.kind, state) {
|
||||
refused_here(self);
|
||||
return refuse(self, Refusal::Precondition);
|
||||
}
|
||||
let mut unreachable: Vec<TargetKey> = Vec::new();
|
||||
|
|
@ -702,6 +716,13 @@ impl MacroMachine {
|
|||
// all" -- and the route search is the real one, so a bound macro can refuse `no route`
|
||||
// once per hold for ever while the candidate list never changes. Recording what it
|
||||
// could not reach is what empties the list and takes the button off the pad.
|
||||
// Row 57: a refusal that teaches the blocked ledger nothing new -- every goal it could
|
||||
// not reach is already resting there -- is one the dealer will deal again unchanged,
|
||||
// because only a last resort deals goals the ledger is resting. That refusal is the
|
||||
// one remembered where the fly stands. A refusal that writes a new exclusion changes
|
||||
// the next deal by itself and is not held against the tile: the next deal may be the
|
||||
// last resort, whose own walk can go where this one could not.
|
||||
let taught = unreachable.iter().any(|key| !state.blocked(*key));
|
||||
if let Some(map) = state.player().map(|player| player.map) {
|
||||
self.blocked.extend(unreachable.into_iter().map(|key| (map, key)));
|
||||
// And for the frontier, the same fact one level up: every tile of this map the
|
||||
|
|
@ -712,6 +733,9 @@ impl MacroMachine {
|
|||
self.exhausted = Some(map);
|
||||
}
|
||||
}
|
||||
if !taught {
|
||||
refused_here(self);
|
||||
}
|
||||
return refuse(self, Refusal::NoRoute);
|
||||
};
|
||||
// The map the target was chosen on, so an entry cannot be read back on another map.
|
||||
|
|
@ -883,6 +907,12 @@ impl MacroMachine {
|
|||
self.pushed_tile.take()
|
||||
}
|
||||
|
||||
/// Where the last `no route` or `precondition` refusal happened, taken rather than read
|
||||
/// (row 57).
|
||||
pub fn take_refused(&mut self) -> Option<(u8, u8, Tile)> {
|
||||
self.refused_at.take()
|
||||
}
|
||||
|
||||
/// The frame-cap timeout a walk earned, with whether it ended nearer its goal, taken rather
|
||||
/// than read. The ledger decides what it means.
|
||||
pub fn take_timeout(&mut self) -> Option<(u8, TargetKey, bool)> {
|
||||
|
|
@ -908,6 +938,7 @@ impl MacroMachine {
|
|||
// out of reach either: the fly is about to be somewhere else entirely.
|
||||
self.pushed_tile = None;
|
||||
self.exhausted = None;
|
||||
self.refused_at = None;
|
||||
self.timed_out = None;
|
||||
// A rollback puts the fly somewhere else on the map, so every suspended route is a route
|
||||
// from a tile it is no longer standing on. `take_resume` would refuse them one at a time;
|
||||
|
|
@ -1088,8 +1119,20 @@ impl MacroMachine {
|
|||
// say (row 37 of `infra/docs/macros-traps.md`). The fly may already have been
|
||||
// walked a tile by the script, so the tile the macro set out from is the honest
|
||||
// answer when there is one and the current tile otherwise.
|
||||
//
|
||||
// **Except for a walk** (row 57). A walk set out from wherever the last one left
|
||||
// the fly, and the script fired on the tile the walk had *reached*: Pewter City's
|
||||
// youngster takes the joypad on four tiles by the road east, and a `GO ROUTE` that
|
||||
// set out from the town's south entrance twenty-six tiles away walled the south
|
||||
// entrance -- with no window, in the middle of the town. A dozen of those fenced
|
||||
// the fly into a pocket no walk could leave. The tile a walk last stood the fly on
|
||||
// is its own record of where the cartridge took over.
|
||||
if let Some(player) = at {
|
||||
let tile = active.from.unwrap_or(Tile::new(player.x, player.y));
|
||||
let current = Tile::new(player.x, player.y);
|
||||
let tile = match active.plan.front() {
|
||||
Some(Step::Walk(walk)) => walk.expect.unwrap_or(current),
|
||||
_ => active.from.unwrap_or(current),
|
||||
};
|
||||
self.pushed_tile = Some((player.map, tile));
|
||||
}
|
||||
}
|
||||
|
|
@ -1652,7 +1695,17 @@ fn script(
|
|||
};
|
||||
let goals = exit_goals(state, way);
|
||||
unreachable.extend(goal_keys(&goals));
|
||||
let (walk, target) = walk_to(state, goals)?;
|
||||
// A last resort that preferred the way toward the objective, and the route search
|
||||
// cannot reach it: the rest of the last resort, nearest reachable first (row 57).
|
||||
let walked = match walk_to(state, goals) {
|
||||
Some(walked) => Some(walked),
|
||||
None => {
|
||||
let rest = super::palette::last_resort_wide(state, way);
|
||||
let wide = goals_of(state, rest);
|
||||
if wide.is_empty() { None } else { walk_to(state, wide) }
|
||||
}
|
||||
};
|
||||
let (walk, target) = walked?;
|
||||
aimed = target;
|
||||
vec![Step::Walk(walk)]
|
||||
}
|
||||
|
|
@ -2041,7 +2094,13 @@ fn one_target(goals: &[Goal]) -> Option<TargetKey> {
|
|||
/// is in the moment it comes down a staircase and lands on the warp tile. A doormat underfoot is
|
||||
/// different: its press is the point, so it stays.
|
||||
fn exit_goals(state: &mut dyn MacroState, way: Way) -> Vec<Goal> {
|
||||
let mut goals: Vec<Goal> = ways(state, way)
|
||||
let exits = ways(state, way);
|
||||
goals_of(state, exits)
|
||||
}
|
||||
|
||||
/// [`exit_goals`] over a list of exits the caller already has.
|
||||
fn goals_of(state: &mut dyn MacroState, exits: Vec<Exit>) -> Vec<Goal> {
|
||||
let mut goals: Vec<Goal> = exits
|
||||
.into_iter()
|
||||
.map(|exit| Goal {
|
||||
tile: exit.tile,
|
||||
|
|
|
|||
|
|
@ -440,7 +440,10 @@ impl Palette {
|
|||
pub fn for_scene(scene: Scene, state: &mut dyn MacroState) -> Self {
|
||||
let mut slots: [Option<MacroSpec>; SLOTS] = [None; SLOTS];
|
||||
for kind in scene_set(scene, state) {
|
||||
if precondition(kind, state) {
|
||||
// A button refused from this very tile inside the window is not dealt again from it
|
||||
// (row 57): the dealer's question is the cheap one, and `start`'s answer to the real
|
||||
// one outranks it until the fly stands somewhere else or the window closes.
|
||||
if precondition(kind, state) && !state.refused_here(kind.slot()) {
|
||||
slots[usize::from(kind.slot())] = Some(MacroSpec::of(kind));
|
||||
}
|
||||
}
|
||||
|
|
@ -1256,6 +1259,49 @@ fn last_resort(state: &mut dyn MacroState, way: Way) -> Vec<Exit> {
|
|||
if toward.is_empty() { every } else { toward }
|
||||
}
|
||||
|
||||
/// Every way out of this kind, when [`ways`] answered with its last resort and that answer was
|
||||
/// narrowed to the ways toward the objective; empty otherwise.
|
||||
///
|
||||
/// Row 57 (`infra/docs/macros-traps.md`). The narrowing is a preference -- "a map the fly has
|
||||
/// already seen is still the way to the next rung" -- and the dealer cannot search, so it can
|
||||
/// prefer a door the fly is walled off from over one it can walk to. Live in Pewter City the last
|
||||
/// resort was the gym's door on the far side of a fence while the road east, resting in the
|
||||
/// blocked window, was three tiles away. This is the rest of the last resort, for `start` to try
|
||||
/// when its route search cannot reach the preferred ones; the choice of *which* reachable way is
|
||||
/// the route search's, nearest first, exactly as it is for every walk.
|
||||
pub fn last_resort_wide(state: &mut dyn MacroState, way: Way) -> Vec<Exit> {
|
||||
// Only where `ways` fell through to the last resort: a tier that has anything in it is
|
||||
// already the answer, and so is a room's or a floor's own unexcluded list.
|
||||
if !exit_tiers(state, way).is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
match way {
|
||||
Way::Exit => {
|
||||
if !unexcluded_exits(state, way).is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
}
|
||||
Way::Passage => {
|
||||
if path::exits(state).iter().any(|exit| exit.way == Way::Exit)
|
||||
|| !unexcluded_exits(state, way).is_empty()
|
||||
{
|
||||
return Vec::new();
|
||||
}
|
||||
}
|
||||
Way::Route => {}
|
||||
}
|
||||
if !stranded(state) {
|
||||
return Vec::new();
|
||||
}
|
||||
let every: Vec<Exit> =
|
||||
path::exits(state).into_iter().filter(|exit| exit.way == way).collect();
|
||||
if toward_objective(state, &every).is_empty() {
|
||||
// Not narrowed: the last resort was every one of them already.
|
||||
return Vec::new();
|
||||
}
|
||||
every
|
||||
}
|
||||
|
||||
/// The exits of the current map of one kind that no ledger excludes.
|
||||
fn unexcluded_exits(state: &mut dyn MacroState, way: Way) -> Vec<Exit> {
|
||||
if !objective_targets(state).is_empty() {
|
||||
|
|
|
|||
|
|
@ -729,6 +729,10 @@ impl MacroState for World {
|
|||
self.targets.reached(self.map, target)
|
||||
}
|
||||
|
||||
fn refused_here(&mut self, slot: u8) -> bool {
|
||||
self.targets.refused(self.map, slot, self.player)
|
||||
}
|
||||
|
||||
fn objective(&mut self) -> Option<Objective> {
|
||||
self.objective
|
||||
}
|
||||
|
|
@ -786,6 +790,9 @@ fn drive(
|
|||
if let Some(map) = machine.take_exhausted() {
|
||||
world.exhausted.insert(map);
|
||||
}
|
||||
if let Some((map, slot, tile)) = machine.take_refused() {
|
||||
world.targets.record_refused(map, slot, tile);
|
||||
}
|
||||
return Err(refused);
|
||||
}
|
||||
// A walk's cap is its plan's, so the bound here is the ceiling on any macro plus slack.
|
||||
|
|
@ -4873,6 +4880,155 @@ fn a_tile_the_cartridge_pushes_the_fly_off_is_not_a_tile_to_walk_to() {
|
|||
);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
// Row 57: a last resort refused from here is not dealt again from here
|
||||
// ---------------------------------------------------------------------------------------------
|
||||
|
||||
/// Pewter City as the live pad found it: the fly fenced into a corner of the town, the
|
||||
/// frontier marked, every person and sign accounted for, the errands paid -- and the only way
|
||||
/// out anything aims at, the gym's door, on the far side of the fence.
|
||||
fn fenced_in_pewter() -> World {
|
||||
let mut world = World::room().at(3, 3);
|
||||
world.map = maps::PEWTER_CITY;
|
||||
world.size = MapSize { width: 20, height: 12 };
|
||||
// The fence: a wall the height of the map, with the fly on the near side of it.
|
||||
for y in 0..12 {
|
||||
world.walls.insert(Tile::new(10, y));
|
||||
}
|
||||
world.warps = vec![Warp { x: 15, y: 3, destination_warp: 0, destination_map: maps::PEWTER_GYM }];
|
||||
world.seen_maps.insert(maps::PEWTER_GYM);
|
||||
world.objective = Some(Objective {
|
||||
map: maps::PEWTER_GYM,
|
||||
tile: None,
|
||||
warp: None,
|
||||
edge: None,
|
||||
target: Some(PlaceKind::Person),
|
||||
});
|
||||
world.areas.insert((Amenity::Mart, maps::PEWTER_CITY));
|
||||
world.areas.insert((Amenity::Center, maps::PEWTER_CITY));
|
||||
world.exhausted.insert(maps::PEWTER_CITY);
|
||||
world
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_way_out_refused_from_here_is_not_dealt_again_from_here() {
|
||||
// Row 57, live on rung 10 for two hours: the pad was `GO ROUTE` alone, `refused` every 800
|
||||
// brain ms, nothing pressed. `ways`'s last resort deliberately ignores the blocked ledger, so
|
||||
// the `no route` refusal -- which writes what it could not reach to that ledger -- could not
|
||||
// take the button off the pad, and the same refusal re-stamped the gym door every hold, which
|
||||
// kept `GO OBJECTIVE`'s only goal excluded for ever.
|
||||
let mut world = fenced_in_pewter();
|
||||
let door = TargetKey::Exit(ExitId::Warp(0));
|
||||
let names = |world: &mut World| -> Vec<&'static str> {
|
||||
let scene = world.scene();
|
||||
plan::plan_for(scene, world).slots.iter().flatten().map(|spec| spec.name).collect()
|
||||
};
|
||||
assert_eq!(names(&mut world), ["GO OBJECTIVE", "GO ROUTE"], "the door, two ways");
|
||||
|
||||
assert_eq!(
|
||||
run(&mut world, MacroKind::GoObjective).map_err(|refused| refused.reason),
|
||||
Err(Refusal::NoRoute)
|
||||
);
|
||||
assert!(world.targets.blocked(world.map, door), "the door is excluded for the window");
|
||||
assert_eq!(names(&mut world), ["GO ROUTE"], "and the last resort still deals it");
|
||||
assert_eq!(
|
||||
run(&mut world, MacroKind::GoRoute).map_err(|refused| refused.reason),
|
||||
Err(Refusal::NoRoute),
|
||||
"the route search agrees with the fence"
|
||||
);
|
||||
|
||||
// The trap: before row 57 the same button was dealt again, from the same tile, on the next
|
||||
// hold, for ever.
|
||||
assert!(
|
||||
names(&mut world).is_empty(),
|
||||
"a button refused from this tile is not dealt again from it: {:?}",
|
||||
names(&mut world)
|
||||
);
|
||||
|
||||
// It is a fact about *here*, not a retirement: standing anywhere else deals it again ...
|
||||
world.player = Tile::new(3, 4);
|
||||
assert_eq!(names(&mut world), ["GO ROUTE"]);
|
||||
// ... and so does the window closing on the same tile.
|
||||
world.player = Tile::new(3, 3);
|
||||
assert!(names(&mut world).is_empty());
|
||||
world.targets.clock(BLOCKED_MINUTES_DEFAULT * 60_000.0 + 1.0);
|
||||
assert!(names(&mut world).contains(&"GO ROUTE"), "{:?}", names(&mut world));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_last_resort_that_cannot_reach_the_objectives_door_takes_a_way_out_it_can_reach() {
|
||||
// Row 57's pocket had a way out: the road east, three tiles from the fly and resting in the
|
||||
// blocked window. The last resort ignores that window but *narrows* to the ways toward the
|
||||
// objective, and the only one was the gym's door on the far side of the fence -- so the
|
||||
// route search refused, and the reachable road was never tried. The narrowing is a
|
||||
// preference; when the route search cannot honour it, the rest of the last resort is the
|
||||
// walk's to take, nearest reachable first.
|
||||
let mut world = fenced_in_pewter();
|
||||
world.connections = Connections { north: false, south: true, east: false, west: false };
|
||||
world.seen_maps.insert(maps::ROUTE_2);
|
||||
let south = TargetKey::Exit(ExitId::Edge(Edge::South));
|
||||
world.targets.record_blocked(world.map, south);
|
||||
world.targets.record_blocked(world.map, TargetKey::Exit(ExitId::Warp(0)));
|
||||
assert!(on_the_pad(&mut world, MacroKind::GoRoute), "the last resort deals it");
|
||||
|
||||
let started_at = world.player;
|
||||
let outcome = run(&mut world, MacroKind::GoRoute);
|
||||
assert!(outcome.is_ok(), "the road it can reach is walked, not refused: {outcome:?}");
|
||||
assert!(
|
||||
world.player.y > started_at.y,
|
||||
"south, toward the way out on this side of the fence: {:?}",
|
||||
world.player
|
||||
);
|
||||
|
||||
// The order the real brain pressed them in, measured on the seeded pocket: `GO ROUTE` first,
|
||||
// while the door is still the second tier's answer rather than the last resort's. That refusal
|
||||
// teaches the ledger the door, so it is not held against the tile -- the next deal is the last
|
||||
// resort, and its walk goes where the first could not.
|
||||
let mut world = fenced_in_pewter();
|
||||
world.connections = Connections { north: false, south: true, east: false, west: false };
|
||||
world.seen_maps.insert(maps::ROUTE_2);
|
||||
world.targets.record_blocked(world.map, south);
|
||||
world.exhausted.insert(world.map);
|
||||
world.objective = Some(Objective {
|
||||
map: maps::PEWTER_GYM,
|
||||
tile: None,
|
||||
warp: None,
|
||||
edge: None,
|
||||
target: Some(PlaceKind::Person),
|
||||
});
|
||||
assert_eq!(
|
||||
run(&mut world, MacroKind::GoRoute).map_err(|refused| refused.reason),
|
||||
Err(Refusal::NoRoute),
|
||||
"the door, through the second tier, beyond the fence"
|
||||
);
|
||||
assert!(on_the_pad(&mut world, MacroKind::GoRoute), "a refusal that taught the ledger is not held");
|
||||
let started_at = world.player;
|
||||
assert!(run(&mut world, MacroKind::GoRoute).is_ok(), "the last resort walks where it can");
|
||||
assert!(world.player.y > started_at.y, "{:?}", world.player);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_escorted_walk_walls_the_tile_it_reached_not_the_one_it_set_out_from() {
|
||||
// Row 57's other half. Pewter City's youngster takes the joypad on four tiles by the road
|
||||
// east and walks the fly to the gym. A `GO ROUTE` that set out from the town's south entrance
|
||||
// and reached one of those tiles wrote the *south entrance* into the pushed ledger -- a wall
|
||||
// with no window, twenty-six tiles from where the script fired -- and a dozen of those fenced
|
||||
// the fly into a pocket. The tile the walk had reached is where the cartridge took over.
|
||||
let mut world = World::room().at(3, 6);
|
||||
world.map = maps::PEWTER_CITY;
|
||||
world.connections = Connections { north: true, south: false, east: false, west: false };
|
||||
// Three tiles walked north, then the script: the scene changes with the game driving the fly.
|
||||
world.scripted_at = Some(52);
|
||||
world.switch = Some((52, Scene::Dialog));
|
||||
|
||||
let mut machine = MacroMachine::new(1);
|
||||
let _ = run_with(&mut machine, &mut world, MacroKind::GoRoute);
|
||||
let (map, tile) = machine.take_pushed().expect("the script moved the fly: a push-back");
|
||||
assert_eq!(map, maps::PEWTER_CITY);
|
||||
assert_ne!(tile, Tile::new(3, 6), "not the tile the walk set out from");
|
||||
assert_eq!(tile, world.player, "the tile the walk had reached when the script took over");
|
||||
}
|
||||
|
||||
/// The push-back writes the ledger, and it writes the *tile* rather than the target.
|
||||
#[test]
|
||||
fn a_scripted_push_back_records_the_tile_it_happened_on() {
|
||||
|
|
|
|||
|
|
@ -1804,6 +1804,12 @@ impl MacroState for PokeState<'_> {
|
|||
self.targets.blocked(map, target)
|
||||
}
|
||||
|
||||
/// Whether the macro in `slot` was refused on the tile the fly is standing on (row 57).
|
||||
fn refused_here(&mut self, slot: u8) -> bool {
|
||||
let Some(player) = player(self.memory) else { return false };
|
||||
self.targets.refused(player.map, slot, Tile::new(player.x, player.y))
|
||||
}
|
||||
|
||||
/// Whether `GO ITEM` or `GO NPC` has already reached `target` on the map that is loaded.
|
||||
fn reached(&mut self, target: TargetKey) -> bool {
|
||||
let Some(map) = player(self.memory).map(|player| player.map) else { return false };
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -46,6 +46,9 @@ use flysim::config::Config;
|
|||
use flysim::macros::macro_layer;
|
||||
use flysim::snapshot::MacroMode;
|
||||
|
||||
#[path = "support/ledgers.rs"]
|
||||
mod ledgers;
|
||||
|
||||
const MS_PER_FRAME: f64 = 1000.0 / 59.7275;
|
||||
const BURST_MS: f64 = 100.0;
|
||||
const HOLDS_PER_SLOT: usize = 3;
|
||||
|
|
@ -1337,6 +1340,238 @@ fn dialog_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64
|
|||
println!("\n{}", separator_table(&classes));
|
||||
}
|
||||
|
||||
/// `FLY_PROBE_CATCH=route`. The live trap was map 2, scene `overworld`, a pad of `GO ROUTE` alone,
|
||||
/// refused about 740 times per ten brain minutes for hours with no button pressed. The ledgers
|
||||
/// that dealt that pad are session state and a restore starts them empty, so this *earns* them:
|
||||
/// it drives the real [`PokemonPalette`] from the checkpoint, choosing uniformly among whatever the
|
||||
/// scene binds once per hold (the brain's hold, not a ranking), and prints every pad it deals and
|
||||
/// every refusal with its reason. When the pad is down to one button that keeps refusing, it reads
|
||||
/// the frame the way the macros do -- ledgers included -- and says which list emptied why, and
|
||||
/// whether the route search can reach any of the one button's goals.
|
||||
///
|
||||
/// `FLY_PROBE_FRAMES` bounds the drive; `FLY_PROBE_RNG` changes the choices; `FLY_PROBE_PREFER`
|
||||
/// (comma-separated names) presses those buttons whenever they are dealt.
|
||||
///
|
||||
/// [`PokemonPalette`]: flybrain_gb::pokemon_red::macros::PokemonPalette
|
||||
fn route_survey(gb: &mut Emulator, adapter: &mut PokemonRedReward, ms: &mut f64) {
|
||||
use flybrain_gb::MacroPalette;
|
||||
use flybrain_gb::pokemon_red::macros::cartridge::{MacroState, TargetKey};
|
||||
use flybrain_gb::pokemon_red::macros::path::Way;
|
||||
use flybrain_gb::pokemon_red::macros::{PokemonPalette, palette, path};
|
||||
|
||||
let budget = env_usize("FLY_PROBE_FRAMES", 240_000);
|
||||
let mut rng = env_usize("FLY_PROBE_RNG", 20_260_923) as u32 | 1;
|
||||
let hold_frames = 48usize;
|
||||
let trace_frames = env_usize("FLY_PROBE_TRACE_FRAMES", 0);
|
||||
// `FLY_PROBE_CATCH_AFTER=0` reads the frame at once, before any choice.
|
||||
let catch_after = env_usize("FLY_PROBE_CATCH_AFTER", 20) as u32;
|
||||
let prefer: Vec<String> = std::env::var("FLY_PROBE_PREFER")
|
||||
.map(|value| value.split(',').map(|name| name.trim().to_string()).collect())
|
||||
.unwrap_or_default();
|
||||
let mut macros = PokemonPalette::new(SEED);
|
||||
if let Some(seeded) = ledgers::seed(&mut macros, gb, adapter, *ms, "FLY_PROBE_SEED") {
|
||||
println!("\n- seeded: {seeded}");
|
||||
}
|
||||
let mut running = false;
|
||||
let mut since_decision = hold_frames;
|
||||
let mut last_pad = String::new();
|
||||
let mut refusals: BTreeMap<String, u64> = BTreeMap::new();
|
||||
let mut outcomes: BTreeMap<String, u64> = BTreeMap::new();
|
||||
let mut single_refusals = 0u32;
|
||||
let mut caught_at: Option<usize> = None;
|
||||
|
||||
// `FLY_PROBE_HOLD=right:96,up:32` holds raw directions first and prints where the fly is
|
||||
// every eight frames: what the cartridge does with a press, before any macro is asked.
|
||||
if let Ok(holds) = std::env::var("FLY_PROBE_HOLD") {
|
||||
println!("\n## Raw holds before the drive\n\n```");
|
||||
for hold in holds.split(',') {
|
||||
let (name, frames) = hold.split_once(':').unwrap_or((hold, "32"));
|
||||
let mask = match name {
|
||||
"right" => flybrain_gb::buttons::RIGHT,
|
||||
"left" => flybrain_gb::buttons::LEFT,
|
||||
"up" => flybrain_gb::buttons::UP,
|
||||
"down" => flybrain_gb::buttons::DOWN,
|
||||
"a" => flybrain_gb::buttons::A,
|
||||
"b" => flybrain_gb::buttons::B,
|
||||
_ => 0,
|
||||
};
|
||||
for index in 0..frames.parse::<usize>().unwrap_or(32) {
|
||||
gb.set_buttons(mask);
|
||||
gb.run_frame().expect("a frame should complete");
|
||||
*ms += MS_PER_FRAME;
|
||||
adapter.sample(gb, *ms);
|
||||
if index % 8 == 7 {
|
||||
println!(
|
||||
"{name:>5} +{index:<3} {:?} scene {:?} sim={} flags5={:#04x} joyignore={:#04x}",
|
||||
state::player(gb).map(|p| (p.map, p.x, p.y, p.facing)),
|
||||
scene::detect(gb),
|
||||
gb.read8(ram::wSimulatedJoypadStatesIndex),
|
||||
gb.read8(ram::wStatusFlags5),
|
||||
gb.read8(ram::wJoyIgnore),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
println!("```");
|
||||
}
|
||||
println!("\n## The drive: the real palette, one uniform choice per hold\n\n```");
|
||||
for frame in 0..budget {
|
||||
macros.clock(*ms);
|
||||
let observed = {
|
||||
let ledger = AdapterLedger(&*adapter);
|
||||
macros.observe(gb, &ledger)
|
||||
};
|
||||
let names: Vec<&str> = observed.bindings.iter().map(|binding| binding.name).collect();
|
||||
let player = state::player(gb);
|
||||
let pad = format!("{:?} {names:?}", observed.scene);
|
||||
if pad != last_pad {
|
||||
println!("f{frame:<6} {:?} pad {pad}", player.map(|p| (p.map, p.x, p.y)));
|
||||
last_pad = pad;
|
||||
}
|
||||
let mut mask = 0u8;
|
||||
if running {
|
||||
let ledger = AdapterLedger(&*adapter);
|
||||
match macros.step(gb, &ledger) {
|
||||
Some(held) => mask = held,
|
||||
None => running = false,
|
||||
}
|
||||
} else if since_decision >= hold_frames && !observed.bindings.is_empty() {
|
||||
since_decision = 0;
|
||||
rng ^= rng << 13;
|
||||
rng ^= rng >> 17;
|
||||
rng ^= rng << 5;
|
||||
// `FLY_PROBE_PREFER` names buttons to press whenever they are dealt, first one first:
|
||||
// the live brain's measured favourites, so the survey can earn the ledgers the live
|
||||
// session earned. A survey's driver, never the fly's: nothing in the crate reads it.
|
||||
let preferred = prefer
|
||||
.iter()
|
||||
.find_map(|want| observed.bindings.iter().find(|binding| binding.name == want));
|
||||
let binding = preferred
|
||||
.unwrap_or(&observed.bindings[rng as usize % observed.bindings.len()]);
|
||||
let ledger = AdapterLedger(&*adapter);
|
||||
match macros.start(binding.slot, gb, &ledger) {
|
||||
flybrain_gb::Started::Running(_) => {
|
||||
running = true;
|
||||
if let Some(held) = macros.step(gb, &ledger) {
|
||||
mask = held;
|
||||
} else {
|
||||
running = false;
|
||||
}
|
||||
}
|
||||
flybrain_gb::Started::Refused { name, reason } => {
|
||||
let key = format!("{} {reason}", name.unwrap_or("-"));
|
||||
*refusals.entry(key.clone()).or_default() += 1;
|
||||
if names.len() == 1 && name.is_some() {
|
||||
single_refusals += 1;
|
||||
} else {
|
||||
single_refusals = 0;
|
||||
}
|
||||
if refusals[&key] <= 3 || single_refusals == 1 {
|
||||
println!(
|
||||
"f{frame:<6} {:?} refused {key} (pad {names:?})",
|
||||
player.map(|p| (p.map, p.x, p.y))
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((name, outcome)) = macros.take_finished() {
|
||||
*outcomes.entry(format!("{name} {outcome:?}")).or_default() += 1;
|
||||
if !matches!(outcome, flybrain_gb::Outcome::Done) {
|
||||
println!(
|
||||
"f{frame:<6} {:?} {name} {outcome:?}",
|
||||
state::player(gb).map(|p| (p.map, p.x, p.y))
|
||||
);
|
||||
}
|
||||
}
|
||||
since_decision += 1;
|
||||
if frame < trace_frames {
|
||||
println!(
|
||||
" t{frame:<5} {:?} mask {mask:#04x} running {:?} marks {:?} stood {}",
|
||||
state::player(gb).map(|p| (p.map, p.x, p.y, p.facing)),
|
||||
macros.running(),
|
||||
macros.fences().1,
|
||||
macros.stood()
|
||||
);
|
||||
}
|
||||
gb.set_buttons(mask);
|
||||
gb.run_frame().expect("a frame should complete");
|
||||
*ms += MS_PER_FRAME;
|
||||
adapter.sample(gb, *ms);
|
||||
if single_refusals >= catch_after {
|
||||
caught_at = Some(frame);
|
||||
break;
|
||||
}
|
||||
}
|
||||
println!("```\n");
|
||||
println!("- refusals: {refusals:?}");
|
||||
println!("- outcomes: {outcomes:?}");
|
||||
let Some(frame) = caught_at else {
|
||||
println!("- pushed tiles at the end (no window): {:?}", macros.fences().0);
|
||||
println!("\nThe pad never came down to one refusing button in {budget} frames.");
|
||||
return;
|
||||
};
|
||||
println!(
|
||||
"\n## Caught on frame {frame} ({:.1} brain minutes): one button, refused twenty holds running\n",
|
||||
frame as f64 * MS_PER_FRAME / 60_000.0
|
||||
);
|
||||
let (pushed, frontiers) = macros.fences();
|
||||
println!("- pushed tiles (no window): {pushed:?}");
|
||||
println!("- frontier marks (no window): {frontiers:?}");
|
||||
let ledger = AdapterLedger(&*adapter);
|
||||
macros.inspect(gb, &ledger, |state: &mut dyn MacroState| {
|
||||
let player = state.player().expect("a loaded map");
|
||||
println!("- player: {player:?}");
|
||||
println!("- objective: {:?}", state.objective());
|
||||
println!("- `objective_goals`: {:?}", palette::objective_goals(state));
|
||||
println!("- `objective_targets`: {:?}", palette::objective_targets(state));
|
||||
println!("- `untalked_people`: {:?}", palette::untalked_people(state));
|
||||
println!("- `untalked_objects`: {:?}", palette::untalked_objects(state));
|
||||
println!("- `facing_untalked`: {}", palette::facing_untalked(state));
|
||||
println!("- `frontier_aims`: {} tiles", palette::frontier_aims(state).len());
|
||||
println!("- `frontier_exhausted`: {}", state.frontier_exhausted());
|
||||
println!("- `stranded`: {}", palette::stranded(state));
|
||||
for way in [Way::Exit, Way::Passage, Way::Route] {
|
||||
println!("- `ways({way:?})`: {:?}", palette::ways(state, way).iter().map(|exit| (exit.id, exit.tile)).collect::<Vec<_>>());
|
||||
}
|
||||
println!("\n### Every way out, and whether the route search reaches it from here\n");
|
||||
for exit in path::exits(state) {
|
||||
let reach = path::route(state, &[exit.tile]).map(|route| route.goal.is_some());
|
||||
println!(
|
||||
"- {:?} at ({:2},{:2}) way {:?} -> {:?}: map_visited {:?}, blocked {}, reachable {:?}",
|
||||
exit.id,
|
||||
exit.tile.x,
|
||||
exit.tile.y,
|
||||
exit.way,
|
||||
exit.destination(player.map),
|
||||
exit.destination(player.map).map(|map| state.map_visited(map)),
|
||||
state.blocked(TargetKey::Exit(exit.id)),
|
||||
reach
|
||||
);
|
||||
}
|
||||
println!("\n### The fly's own neighbourhood (pushed = `P`, player = `@`)\n\n```");
|
||||
for y in player.y.saturating_sub(3)..=player.y.saturating_add(3) {
|
||||
let row: String = (player.x.saturating_sub(6)..=player.x.saturating_add(6))
|
||||
.map(|x| {
|
||||
if x == player.x && y == player.y {
|
||||
'@'
|
||||
} else if state.pushed_tile(x, y) {
|
||||
'P'
|
||||
} else {
|
||||
match state.walkable(x, y) {
|
||||
flybrain_gb::pokemon_red::macros::state::Walkable::Yes => '.',
|
||||
flybrain_gb::pokemon_red::macros::state::Walkable::No => '#',
|
||||
flybrain_gb::pokemon_red::macros::state::Walkable::Unknown => '?',
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
println!("y{y:2} x{:2}.. {row}", player.x.saturating_sub(6));
|
||||
}
|
||||
println!("```");
|
||||
});
|
||||
}
|
||||
|
||||
/// How the candidate readings of "a two-option box is up" separate the frames of a survey.
|
||||
///
|
||||
/// Three columns, because three things could say it and only a measurement says which: the
|
||||
|
|
@ -1372,7 +1607,13 @@ fn main() {
|
|||
let mut gb = Emulator::new(&rom, DEFAULT_AUDIO_FREQUENCY, DEFAULT_AUDIO_FRAMES)
|
||||
.expect("binjgb should accept the cartridge");
|
||||
let mut adapter = PokemonRedReward::new();
|
||||
gb.import_state(&checkpoint.runtime.emulator).expect("the checkpoint's emulator state");
|
||||
// `FLY_PROBE_RATCHET=1` starts from the ratchet's best snapshot instead: where a "Stuck"
|
||||
// rollback puts the fly, which is where a live session that spent its rollbacks resumed from.
|
||||
if std::env::var("FLY_PROBE_RATCHET").is_ok_and(|value| value == "1") {
|
||||
gb.import_state(&checkpoint.runtime.ratchet_game).expect("the ratchet's snapshot");
|
||||
} else {
|
||||
gb.import_state(&checkpoint.runtime.emulator).expect("the checkpoint's emulator state");
|
||||
}
|
||||
adapter.import_state(&checkpoint.runtime.reward).expect("the checkpoint's reward ledger");
|
||||
|
||||
let channels = flybrain_gb::macro_channels("pokemon-red");
|
||||
|
|
@ -1436,6 +1677,13 @@ fn main() {
|
|||
return;
|
||||
}
|
||||
|
||||
// Row 57's pad survey: earn the session's ledgers from the checkpoint with the real palette,
|
||||
// and read the frame the pad comes down to one refusing button on.
|
||||
if std::env::var("FLY_PROBE_CATCH").is_ok_and(|value| value == "route") {
|
||||
route_survey(&mut gb, &mut adapter, &mut ms);
|
||||
return;
|
||||
}
|
||||
|
||||
let budget = env_usize("FLY_PROBE_FRAMES", 200_000);
|
||||
let stuck_after = env_usize("FLY_PROBE_STUCK", 600);
|
||||
let mut next_burst = ms;
|
||||
|
|
|
|||
95
services/flysim/crates/flysim/examples/support/ledgers.rs
Normal file
95
services/flysim/crates/flysim/examples/support/ledgers.rs
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
//! Rebuild a live session's macro ledgers from the environment, because a restore starts them
|
||||
//! empty (`docs/design/macros.md` section 12: the ledgers are session state and never reach the
|
||||
//! checkpoint). Shared by `scene_probe` and `trap_hunt`; `infra/docs/macros-traps.md` row 57 is
|
||||
//! the trap that needed it: the pad that stalled was dealt by ledgers no checkpoint carries.
|
||||
//!
|
||||
//! Every variable is `<PREFIX>_<NAME>`, so the two examples keep their own namespaces:
|
||||
//!
|
||||
//! | name | meaning |
|
||||
//! | --- | --- |
|
||||
//! | `PUSHED` | `"x,y;x,y"`: tiles of the loaded map walled the way a scripted push-back walls them |
|
||||
//! | `EXHAUSTED` | `1`: the loaded map's frontier marked unreachable (section 12.14) |
|
||||
//! | `TALKED` | `1`: every person and sign on the loaded map written into the talked ledger |
|
||||
//! | `BLOCKED` | `"warp2;east"`: those exits rested for the blocked window |
|
||||
//! | `STOOD` | `1`: the adapter's covered ground; `all`: every tile of the map (no new ground at all) |
|
||||
//!
|
||||
//! The tile underfoot is always recorded as stood on, or the first observe would read it as new
|
||||
//! ground and clear the frontier mark being rebuilt. Nothing is seeded when no variable is set.
|
||||
|
||||
use flybrain_gb::pokemon_red::macros::PokemonPalette;
|
||||
use flybrain_gb::pokemon_red::macros::cartridge::{Edge, ExitId, MacroState, TargetKey};
|
||||
use flybrain_gb::pokemon_red::macros::{Tile, path};
|
||||
use flybrain_gb::pokemon_red::{PokemonRedReward, state};
|
||||
use flybrain_gb::{AdapterLedger, Emulator, MacroPalette};
|
||||
|
||||
/// Seed `macros` from `<prefix>_*` at brain millisecond `ms`, and say what was seeded.
|
||||
pub fn seed(
|
||||
macros: &mut PokemonPalette,
|
||||
gb: &mut Emulator,
|
||||
adapter: &PokemonRedReward,
|
||||
ms: f64,
|
||||
prefix: &str,
|
||||
) -> Option<String> {
|
||||
let var = |name: &str| std::env::var(format!("{prefix}_{name}")).ok();
|
||||
let names = ["PUSHED", "EXHAUSTED", "TALKED", "BLOCKED", "STOOD"];
|
||||
if names.iter().all(|name| var(name).is_none()) {
|
||||
return None;
|
||||
}
|
||||
let player = state::player(gb)?;
|
||||
let map = player.map;
|
||||
let stood_mode = var("STOOD").unwrap_or_default();
|
||||
macros.clock(ms);
|
||||
let (things, covered): (Vec<_>, Vec<Tile>) =
|
||||
macros.inspect(gb, &AdapterLedger(adapter), |state: &mut dyn MacroState| {
|
||||
let mut all = path::person_targets(state);
|
||||
all.extend(path::interactable_targets(state));
|
||||
let mut covered = Vec::new();
|
||||
if let Some(size) = state.map_size() {
|
||||
for y in 0..size.height {
|
||||
for x in 0..size.width {
|
||||
if stood_mode == "all" || state.tile_visited(x, y) {
|
||||
covered.push(Tile::new(x, y));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(all, covered)
|
||||
});
|
||||
let (talked, targets, stood, pushed, frontiers) = macros.ledgers_mut();
|
||||
targets.clock(ms);
|
||||
stood.record(map, Tile::new(player.x, player.y));
|
||||
if stood_mode == "1" || stood_mode == "all" {
|
||||
for tile in &covered {
|
||||
stood.record(map, *tile);
|
||||
}
|
||||
}
|
||||
for pair in var("PUSHED").unwrap_or_default().split(';').filter(|pair| !pair.is_empty()) {
|
||||
let mut xy = pair.split(',').map(|n| n.trim().parse::<u8>().expect("PUSHED is x,y;x,y"));
|
||||
let (x, y) = (xy.next().expect("an x"), xy.next().expect("a y"));
|
||||
pushed.record(map, Tile::new(x, y));
|
||||
}
|
||||
if var("EXHAUSTED").as_deref() == Some("1") {
|
||||
frontiers.record(map);
|
||||
}
|
||||
if var("TALKED").as_deref() == Some("1") {
|
||||
for (_, target) in &things {
|
||||
talked.record(map, *target);
|
||||
}
|
||||
}
|
||||
for key in var("BLOCKED").unwrap_or_default().split(';').filter(|key| !key.is_empty()) {
|
||||
let id = match key {
|
||||
"north" => ExitId::Edge(Edge::North),
|
||||
"south" => ExitId::Edge(Edge::South),
|
||||
"east" => ExitId::Edge(Edge::East),
|
||||
"west" => ExitId::Edge(Edge::West),
|
||||
warp => ExitId::Warp(warp.trim_start_matches("warp").parse().expect("BLOCKED warpN")),
|
||||
};
|
||||
targets.record_blocked(map, TargetKey::Exit(id));
|
||||
}
|
||||
Some(format!(
|
||||
"map {map:#04x}: pushed {pushed:?}, frontier marks {frontiers:?}, {} things talked to, \
|
||||
{} tiles stood on",
|
||||
talked.len(),
|
||||
stood.len()
|
||||
))
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@
|
|||
//! | `FLY_MACRO_BRAIN` | `FLY_DATASET`, else `data/fafb-v783` | the connectome |
|
||||
//! | `FLY_TRAP_THREADS` | 4 | sweep threads |
|
||||
//! | `FLY_TRAP_SEED` | 20260917 | seeds the palette |
|
||||
//! | `FLY_TRAP_SEED_*` | unset | rebuilds session ledgers a restore starts empty: `PUSHED`, `EXHAUSTED`, `TALKED`, `BLOCKED`, `STOOD` (`examples/support/ledgers.rs`, row 57) |
|
||||
//!
|
||||
//! The frame order is `simloop.rs`'s, as `examples/palette_bench.rs` expresses it, so what this
|
||||
//! measures is the loop that ships rather than a second implementation of it. Without a
|
||||
|
|
@ -58,6 +59,9 @@ use flysim::config::Config;
|
|||
use flysim::macros::{MacroLayer, macro_layer};
|
||||
use flysim::snapshot::MacroMode;
|
||||
|
||||
#[path = "support/ledgers.rs"]
|
||||
mod ledgers;
|
||||
|
||||
/// One brain minute in milliseconds.
|
||||
const MINUTE_MS: f64 = 60_000.0;
|
||||
|
||||
|
|
@ -173,6 +177,9 @@ impl NeuralRecovery for AgentRecovery<'_> {
|
|||
}
|
||||
|
||||
/// Where the fly stood on one frame, and what it started on it.
|
||||
/// A refused macro and the `(map, x, y)` it was refused on.
|
||||
type RefusedAt = (&'static str, Option<(u32, u32, u32)>);
|
||||
|
||||
struct Trace {
|
||||
/// `(brain ms, map, x, y)` for every frame the adapter could place the player on.
|
||||
steps: Vec<(f64, u32, u32, u32)>,
|
||||
|
|
@ -186,6 +193,13 @@ struct Trace {
|
|||
recoveries: u64,
|
||||
rungs: Vec<(u32, &'static str, f64)>,
|
||||
outcomes: BTreeMap<&'static str, u64>,
|
||||
/// What `FLY_TRAP_SEED_*` rebuilt, when anything (row 57).
|
||||
seeded: Option<String>,
|
||||
/// `refused` outcomes by macro, and the run of one macro refused with the fly on one tile:
|
||||
/// row 57's pad was one button refused 740 times running.
|
||||
refusals: BTreeMap<&'static str, u64>,
|
||||
refusal_run: (Option<RefusedAt>, u64),
|
||||
longest_refusal_run: (u64, &'static str),
|
||||
/// Frames spent in each scene, so a window full of macros can be read back to the scene that
|
||||
/// dealt them.
|
||||
scenes: BTreeMap<&'static str, u64>,
|
||||
|
|
@ -352,6 +366,22 @@ fn run(
|
|||
config.loop_.game = "pokemon-red".to_string();
|
||||
config.macros.mode = mode;
|
||||
let mut macros: Option<MacroLayer> = macro_layer(&config, hold_ms, seed);
|
||||
let mut seeded_note: Option<String> = None;
|
||||
// Row 57: a trap dealt by session ledgers does not come back from a checkpoint, because a
|
||||
// restore starts them empty. `FLY_TRAP_SEED_*` rebuilds them on the real palette, so both arms
|
||||
// of a hunt start inside the state the live session was in.
|
||||
if let Some(flavour) = config.macros.mode.palette_mode() {
|
||||
let mut palette =
|
||||
flybrain_gb::pokemon_red::macros::PokemonPalette::with_mode(seed, flavour);
|
||||
let ms = agent.network.ms;
|
||||
if let Some(seeded) =
|
||||
ledgers::seed(&mut palette, &mut emulator, &adapter, ms, "FLY_TRAP_SEED")
|
||||
{
|
||||
eprintln!("seeded the session ledgers: {seeded}");
|
||||
seeded_note = Some(seeded);
|
||||
macros = Some(MacroLayer::new(Box::new(palette), hold_ms));
|
||||
}
|
||||
}
|
||||
|
||||
let began_ms = agent.network.ms;
|
||||
let until = began_ms + minutes * MINUTE_MS;
|
||||
|
|
@ -394,6 +424,10 @@ fn run(
|
|||
battle_starts: BTreeMap::new(),
|
||||
battle_pads: BTreeMap::new(),
|
||||
wall_seconds: 0.0,
|
||||
seeded: seeded_note,
|
||||
refusals: BTreeMap::new(),
|
||||
refusal_run: (None, 0),
|
||||
longest_refusal_run: (0, ""),
|
||||
};
|
||||
|
||||
if let Some(layer) = macros.as_mut() {
|
||||
|
|
@ -483,6 +517,18 @@ fn run(
|
|||
}
|
||||
Some(outcome) => {
|
||||
*trace.outcomes.entry(outcome.as_str()).or_insert(0) += 1;
|
||||
if outcome.as_str() == "refused" {
|
||||
*trace.refusals.entry(event.name).or_insert(0) += 1;
|
||||
let key = Some((event.name, location));
|
||||
trace.refusal_run = if trace.refusal_run.0 == key {
|
||||
(key, trace.refusal_run.1 + 1)
|
||||
} else {
|
||||
(key, 1)
|
||||
};
|
||||
if trace.refusal_run.1 > trace.longest_refusal_run.0 {
|
||||
trace.longest_refusal_run = (trace.refusal_run.1, event.name);
|
||||
}
|
||||
}
|
||||
if let Some(run) = running.take() {
|
||||
let net = match (run.from, location) {
|
||||
(Some((map, x, y)), Some((at, ax, ay))) if map == at => {
|
||||
|
|
@ -847,6 +893,9 @@ fn main() {
|
|||
trace.wall_seconds,
|
||||
Path::new(&checkpoint_path).display()
|
||||
);
|
||||
if let Some(seeded) = &trace.seeded {
|
||||
println!("Session ledgers rebuilt before the first frame (`FLY_TRAP_SEED_*`): {seeded}.\n");
|
||||
}
|
||||
println!("| measure | value |");
|
||||
println!("| --- | ---: |");
|
||||
println!("| rung reached | {} |", trace.rungs.iter().map(|(rank, ..)| *rank).max().unwrap_or(0));
|
||||
|
|
@ -856,6 +905,15 @@ fn main() {
|
|||
for (outcome, count) in &trace.outcomes {
|
||||
println!("| {outcome} | {count} |");
|
||||
}
|
||||
if !trace.refusals.is_empty() {
|
||||
let by: Vec<String> =
|
||||
trace.refusals.iter().map(|(name, count)| format!("`{name}` {count}")).collect();
|
||||
println!("| refused, by macro | {} |", by.join(", "));
|
||||
println!(
|
||||
"| longest run of one macro refused on one tile | {} (`{}`) |",
|
||||
trace.longest_refusal_run.0, trace.longest_refusal_run.1
|
||||
);
|
||||
}
|
||||
println!("| recoveries | {} |", trace.recoveries);
|
||||
println!("| windows examined | {windows} |");
|
||||
println!("| windows flagged | {} |", traps.len());
|
||||
|
|
|
|||
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());
|
||||
}
|
||||
|
|
@ -2921,3 +2921,264 @@ fn the_fly_leaves_the_pewter_gym_guides_ring_from_the_rung_ten_checkpoint() {
|
|||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn pewter_east_checkpoint() -> Option<flysim::store::Checkpoint> {
|
||||
std::env::var_os("FLY_PEWTER_EAST_CHECKPOINT").map(|path| {
|
||||
flysim::store::load(std::path::Path::new(&path))
|
||||
.expect("the checkpoint should be a FLYSIM01 envelope")
|
||||
})
|
||||
}
|
||||
|
||||
/// The real palette over the real cartridge, driven one uniform choice per hold -- the brain's
|
||||
/// hold, not a ranking -- so a test can seed the session ledgers a restore starts empty.
|
||||
struct Palette57 {
|
||||
gb: Emulator,
|
||||
adapter: PokemonRedReward,
|
||||
macros: flybrain_gb::pokemon_red::macros::PokemonPalette,
|
||||
ms: f64,
|
||||
rng: u32,
|
||||
}
|
||||
|
||||
impl Palette57 {
|
||||
const HOLD_FRAMES: u32 = 48;
|
||||
|
||||
fn new(rom: &[u8], checkpoint: &flysim::store::Checkpoint, from_rollback: bool) -> Self {
|
||||
let mut gb = Emulator::new(rom, DEFAULT_AUDIO_FREQUENCY, DEFAULT_AUDIO_FRAMES)
|
||||
.expect("binjgb should accept the cartridge");
|
||||
let state = if from_rollback {
|
||||
&checkpoint.runtime.ratchet_game
|
||||
} else {
|
||||
&checkpoint.runtime.emulator
|
||||
};
|
||||
gb.import_state(state).expect("the checkpoint's emulator state");
|
||||
let mut adapter = PokemonRedReward::new();
|
||||
adapter.import_state(&checkpoint.runtime.reward).expect("the checkpoint's reward ledger");
|
||||
Self {
|
||||
gb,
|
||||
adapter,
|
||||
macros: flybrain_gb::pokemon_red::macros::PokemonPalette::new(SEED),
|
||||
ms: 0.0,
|
||||
rng: 0x5eed_0057,
|
||||
}
|
||||
}
|
||||
|
||||
fn player(&mut self) -> (u8, u8, u8) {
|
||||
let player = flybrain_gb::pokemon_red::state::player(&mut self.gb).expect("a loaded map");
|
||||
(player.map, player.x, player.y)
|
||||
}
|
||||
|
||||
/// One frame: observe, then step the running macro or start one of the pad's buttons.
|
||||
/// Returns the pad and what a start did, when one was tried this frame.
|
||||
fn frame(
|
||||
&mut self,
|
||||
running: &mut bool,
|
||||
since: &mut u32,
|
||||
want: Option<flybrain_gb::pokemon_red::macros::MacroKind>,
|
||||
) -> (Vec<&'static str>, Option<flybrain_gb::Started>) {
|
||||
use flybrain_gb::MacroPalette;
|
||||
self.macros.clock(self.ms);
|
||||
let observed = self.macros.observe(&mut self.gb, &AdapterLedger(&self.adapter));
|
||||
let pad: Vec<&'static str> = observed.bindings.iter().map(|b| b.name).collect();
|
||||
let mut mask = 0u8;
|
||||
let mut tried = None;
|
||||
if *running {
|
||||
match self.macros.step(&mut self.gb, &AdapterLedger(&self.adapter)) {
|
||||
Some(held) => mask = held,
|
||||
None => *running = false,
|
||||
}
|
||||
} else if *since >= Self::HOLD_FRAMES && !observed.bindings.is_empty() {
|
||||
*since = 0;
|
||||
let slot = match want.and_then(|kind| {
|
||||
observed.bindings.iter().find(|b| b.slot == kind.slot()).map(|b| b.slot)
|
||||
}) {
|
||||
Some(slot) => slot,
|
||||
None => {
|
||||
self.rng ^= self.rng << 13;
|
||||
self.rng ^= self.rng >> 17;
|
||||
self.rng ^= self.rng << 5;
|
||||
observed.bindings[self.rng as usize % observed.bindings.len()].slot
|
||||
}
|
||||
};
|
||||
let started = self.macros.start(slot, &mut self.gb, &AdapterLedger(&self.adapter));
|
||||
if let flybrain_gb::Started::Running(_) = started {
|
||||
*running = true;
|
||||
match self.macros.step(&mut self.gb, &AdapterLedger(&self.adapter)) {
|
||||
Some(held) => mask = held,
|
||||
None => *running = false,
|
||||
}
|
||||
}
|
||||
tried = Some(started);
|
||||
}
|
||||
let _ = self.macros.take_finished();
|
||||
*since += 1;
|
||||
self.gb.set_buttons(mask);
|
||||
self.gb.run_frame().expect("a frame should complete");
|
||||
self.ms += MS_PER_FRAME;
|
||||
self.adapter.sample(&mut self.gb, self.ms);
|
||||
(pad, tried)
|
||||
}
|
||||
}
|
||||
|
||||
/// From the live rung-10 checkpoint in Pewter City: the pad is never one dead button.
|
||||
///
|
||||
/// **What was live** (2026-09-23, v0.5.3, `infra/docs/macros-traps.md` row 57): map 2, scene
|
||||
/// `overworld`, the pad `GO ROUTE` and nothing else, `GO ROUTE refused` about 740 times per ten
|
||||
/// brain minutes for more than two hours, `GO ROUTE start` / `blocked` once per ten brain minutes,
|
||||
/// no button pressed, the exploration count frozen. The watchdog read one start and one name.
|
||||
///
|
||||
/// **The mechanism, measured with `examples/scene_probe.rs`'s `FLY_PROBE_CATCH=route`:**
|
||||
///
|
||||
/// 1. Pewter City's youngster takes the joypad on four tiles by the road east and walks the fly to
|
||||
/// the gym. `GO ROUTE` aims east (Route 3 is the one connection the run has not crossed), so it
|
||||
/// is escorted every time -- and the pushed ledger, which has no window, walled the tile each
|
||||
/// escorted walk **set out from**: one walk from the south entrance walled the south entrance,
|
||||
/// twenty-six tiles from the script. Walks start wherever the last one ended, so the walls
|
||||
/// accumulate until the fly stands in a pocket no route leaves.
|
||||
/// 2. In the pocket every walk refuses `no route`. `GO FRONTIER`'s refusal marks the map
|
||||
/// exhausted (12.14, no window, cleared only by new ground); `GO OBJECTIVE`'s only goal is the
|
||||
/// gym's door, now excluded for the window.
|
||||
/// 3. With nothing else on the map, `ways`'s **last resort** deals `GO ROUTE` at that door, and a
|
||||
/// last resort ignores the blocked ledger by design -- so its `no route` refusal, which writes
|
||||
/// the door to that ledger, cannot take the button off the pad, and re-stamps the door's window
|
||||
/// every hold, so `GO OBJECTIVE` never returns. Once per window the road east lapses, `GO ROUTE`
|
||||
/// walks it, and the road is excluded again.
|
||||
///
|
||||
/// The session ledgers are what dealt that pad and a restore starts them empty, so part two seeds
|
||||
/// the pocket: the tiles every walk has stood on, the map's frontier mark, every sign and person
|
||||
/// talked to, the road east resting, and three pushed tiles sealing the strip by the road from the
|
||||
/// town. That is a reconstruction of state the checkpoint cannot carry, said as one. In it the last
|
||||
/// resort's preferred way out, the gym's door, is beyond the fence, while the road east is three
|
||||
/// tiles away and resting in its window: `start` now takes the way it can reach.
|
||||
///
|
||||
/// ```sh
|
||||
/// FLY_ROM=/path/to/pokemon-red.gb \
|
||||
/// FLY_PEWTER_EAST_CHECKPOINT=.local/checkpoints/release-rank10-pewter-east.checkpoint \
|
||||
/// cargo test --release -p flysim --test rom_macros_mode -- pewter_east --nocapture
|
||||
/// ```
|
||||
#[test]
|
||||
fn the_pewter_east_pad_is_never_one_dead_button_from_the_rung_ten_checkpoint() {
|
||||
use flybrain_gb::pokemon_red::macros::cartridge::{Edge, ExitId, PushedLedger, TargetKey};
|
||||
use flybrain_gb::pokemon_red::macros::{MacroKind, Tile};
|
||||
|
||||
let rom = skip_without_rom!();
|
||||
let Some(checkpoint) = pewter_east_checkpoint() else {
|
||||
eprintln!("skipped: no FLY_PEWTER_EAST_CHECKPOINT");
|
||||
return;
|
||||
};
|
||||
let pewter = PEWTER_CITY as u8;
|
||||
// `PewterCityPlayerLeavingEastCoords`.
|
||||
let escort = [Tile::new(35, 17), Tile::new(36, 17), Tile::new(37, 18), Tile::new(37, 19)];
|
||||
|
||||
// Part one: an escorted walk walls where the script took over, not where the walk began.
|
||||
// From the ratchet's snapshot -- where a "Stuck" rollback puts the fly, the town's south
|
||||
// entrance -- `GO ROUTE` whenever it is dealt, `NEXT` through the youngster's text.
|
||||
let mut run = Palette57::new(&rom, &checkpoint, true);
|
||||
let start = run.player();
|
||||
assert_eq!(start.0, pewter, "the rollback snapshot is in Pewter City: {start:?}");
|
||||
let (mut running, mut since) = (false, Palette57::HOLD_FRAMES);
|
||||
let mut escorted = false;
|
||||
for _ in 0..4_000 {
|
||||
let want = if escorted { MacroKind::Next } else { MacroKind::GoRoute };
|
||||
let (pad, _) = run.frame(&mut running, &mut since, Some(want));
|
||||
escorted |= pad.contains(&"NEXT");
|
||||
if escorted && !run.macros.fences().0.is_empty() && !running && pad.contains(&"GO ROUTE") {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert!(escorted, "the walk east met the youngster");
|
||||
let pushed = run.macros.fences().0;
|
||||
eprintln!("part one: from {start:?}, pushed {pushed:?}");
|
||||
assert!(!pushed.is_empty(), "the escort is a push-back and is recorded");
|
||||
assert!(
|
||||
!pushed.pushed(pewter, Tile::new(start.1, start.2)),
|
||||
"the tile the walk set out from is not walled: {pushed:?}"
|
||||
);
|
||||
assert!(
|
||||
escort.iter().any(|tile| pushed.pushed(pewter, *tile)),
|
||||
"the tile the script fired on is: {pushed:?}"
|
||||
);
|
||||
|
||||
// Part two: the pocket, rebuilt, and twelve brain minutes in it.
|
||||
let mut run = Palette57::new(&rom, &checkpoint, false);
|
||||
let here = run.player();
|
||||
assert_eq!(here, (pewter, 36, 18), "the live checkpoint stands by the road east");
|
||||
{
|
||||
use flybrain_gb::MacroPalette;
|
||||
run.macros.clock(0.0);
|
||||
let (_, targets, stood, pushed, frontiers) = run.macros.ledgers_mut();
|
||||
targets.clock(0.0);
|
||||
for y in 0..36 {
|
||||
for x in 0..40 {
|
||||
stood.record(pewter, Tile::new(x, y));
|
||||
}
|
||||
}
|
||||
frontiers.record(pewter);
|
||||
for tile in [Tile::new(33, 16), Tile::new(33, 17), Tile::new(33, 18)] {
|
||||
pushed.record(pewter, tile);
|
||||
}
|
||||
targets.record_blocked(pewter, TargetKey::Exit(ExitId::Edge(Edge::East)));
|
||||
}
|
||||
// Every sign and person on the map, talked to.
|
||||
let things = run.macros.inspect(
|
||||
&mut run.gb,
|
||||
&AdapterLedger(&run.adapter),
|
||||
|state: &mut dyn flybrain_gb::pokemon_red::macros::MacroState| {
|
||||
let mut all = flybrain_gb::pokemon_red::macros::path::person_targets(state);
|
||||
all.extend(flybrain_gb::pokemon_red::macros::path::interactable_targets(state));
|
||||
all
|
||||
},
|
||||
);
|
||||
for (_, target) in things {
|
||||
run.macros.ledgers_mut().0.record(pewter, target);
|
||||
}
|
||||
|
||||
let (mut running, mut since) = (false, Palette57::HOLD_FRAMES);
|
||||
let mut refusals = 0u32;
|
||||
let mut in_the_pocket = 0u32;
|
||||
let mut run_of_refusals = 0u32;
|
||||
let mut longest = 0u32;
|
||||
let mut last_refused: Option<(&'static str, (u8, u8, u8))> = None;
|
||||
let mut left: Option<u32> = None;
|
||||
let mut first_pad: Option<Vec<&'static str>> = None;
|
||||
let budget = (12.0 * 60_000.0 / MS_PER_FRAME) as u32;
|
||||
for frame in 0..budget {
|
||||
let at = run.player();
|
||||
let (pad, tried) = run.frame(&mut running, &mut since, None);
|
||||
first_pad.get_or_insert(pad.clone());
|
||||
if let Some(flybrain_gb::Started::Refused { name: Some(name), reason }) = tried {
|
||||
refusals += 1;
|
||||
if at.0 == pewter && at.1 >= 34 {
|
||||
in_the_pocket += 1;
|
||||
}
|
||||
let key = (name, at);
|
||||
run_of_refusals = if last_refused == Some(key) { run_of_refusals + 1 } else { 1 };
|
||||
last_refused = Some(key);
|
||||
longest = longest.max(run_of_refusals);
|
||||
eprintln!("frame {frame}: {name} refused ({reason}) at {at:?}, pad {pad:?}");
|
||||
} else if tried.is_some() {
|
||||
last_refused = None;
|
||||
run_of_refusals = 0;
|
||||
}
|
||||
let now = run.player();
|
||||
if left.is_none() && (now.0 != pewter || now.1 < 33) {
|
||||
left = Some(frame);
|
||||
}
|
||||
}
|
||||
eprintln!(
|
||||
"part two: first pad {first_pad:?}, {refusals} refusals ({in_the_pocket} in the pocket), \
|
||||
longest run on one tile {longest}, left the pocket at {left:?}"
|
||||
);
|
||||
// The live pad: `GO ROUTE` refused every hold, 740 times in ten brain minutes.
|
||||
assert!(
|
||||
longest <= 1,
|
||||
"a button was refused {longest} holds running on one tile: the pad kept dealing it"
|
||||
);
|
||||
assert!(in_the_pocket <= 2, "{in_the_pocket} refusals in the pocket");
|
||||
let Some(left) = left else { panic!("the fly never left the pocket in twelve brain minutes") };
|
||||
let minutes = f64::from(left) * MS_PER_FRAME / 60_000.0;
|
||||
eprintln!("left the pocket on frame {left} ({minutes:.2} brain minutes)");
|
||||
// The road east is three tiles away and resting in its window; the last resort preferred the
|
||||
// gym's door beyond the fence. On the base the fly leaves only when the road's window lapses,
|
||||
// ten brain minutes in.
|
||||
assert!(minutes < 1.0, "the fly waited {minutes:.2} brain minutes for a window to lapse");
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue