docs: PROF-02a and RT-01a, the legacy Game Boy composition on the session framework
The operator decided on 2026-09-23 to port the live fly onto the session framework in full. New contract legacy-gameboy-v1: the profile gameboy-legacy-fafb-v783-v1, the proof that the legacy f64 frame clock equals the rational one, the step-by-step placement of step_frame in lockstep-v1, the readout context (location allowed and declared), the channels decision, the memory-image inspection and ROM AssetRef, the environment (one no-button setup frame, u8->f32 audio, DC blocker at the edge, gameboy-slots-v1), the pokered-macros-v1 executor as one object with its task, the legacy-ratchet-rollback-v1 policy, the composition digest carrying decoder and macro-channel configuration, legacy-transient-reset restore semantics, sugar admission with a one-commit lag, and FLYSIM01 as format of record until RETIRE-01. PROF-02b is a stub. Dated amendments, each citing the decision, where earlier text kept the legacy loop outside lockstep or had no place for it: workers-v1 (telemetry, Initialize, executor, episode request kind, admission, new section 7 extension methods), step-v1 (rollback edge, Phase B/C, clock, episode policy, sugar lag), state-media-v1 (audio, memory-image retention, restore semantics, format of record, section 7 ratchet), README section 4, implementation.md (AGENT-01 and ENV-01 unblocked), the MaleCNS backlog (FOUNDATION-02 split, RUNTIME-01 contract) and analysis (5.2, 5.4), and readout.md (where the location comes from).
This commit is contained in:
parent
bbf71bfead
commit
24ba933500
9 changed files with 702 additions and 1 deletions
|
|
@ -68,6 +68,15 @@ already created. Builders use separate worktrees; the coordinator reviews contra
|
|||
- **Done:** empty required populations, malformed CSR and incorrect profile restores fail;
|
||||
legacy FAFB artifacts and default numerical version strings remain unchanged.
|
||||
|
||||
**2026-09-23: split by operator decision.** PROF-02a, the legacy profile, is done as a contract:
|
||||
[legacy Game Boy composition v1](session-framework/legacy-gameboy-v1.md) defines
|
||||
`gameboy-legacy-fafb-v783-v1` (today's schema-1 fingerprint embedded, `lif-1ms-f64-v2` and
|
||||
`fly-kc-mbon-rstdp-v2` unchanged, the macro-role exception declared), the readout context
|
||||
`gameboy-readout-context-v1`, the decision `gameboy-channels-v1`, and a composition declaration
|
||||
whose digest carries the decoder and macro-channel configuration instead of the legacy
|
||||
compatibility string. PROF-02b -- the bundle manifests, role mapping, strict graph validation and
|
||||
profile-mismatch fixtures above -- is later and gates DATA-01, not the session port.
|
||||
|
||||
### DATA-01 — Acquire and normalize MaleCNS
|
||||
|
||||
- **Branch:** `feat/malecns-import`
|
||||
|
|
@ -108,6 +117,16 @@ already created. Builders use separate worktrees; the coordinator reviews contra
|
|||
- **Done:** existing single-agent action/reward traces match and a fake environment can be
|
||||
driven through the same boundary without importing binjgb or task-specific addresses.
|
||||
|
||||
**2026-09-23: contract written (RT-01a), implementation pending.** The operator decided the
|
||||
boundary: macros run in the coordinator's action executor over a per-boundary 64-KiB memory
|
||||
image carried as an inspection artifact plus the ROM as an `AssetRef`; the emulator shim gains
|
||||
one read-only bulk read and the joypad stays its only write; the Pokémon task and executor are
|
||||
one object (`pokered-macros-v1`); the ratchet is the `legacy-ratchet-rollback-v1` episode policy
|
||||
over the environment extension `gameboy-slots-v1`. The dated amendments are in
|
||||
[workers-v1](session-framework/workers-v1.md), [step-v1](session-framework/step-v1.md) and
|
||||
[state-media-v1](session-framework/state-media-v1.md); the session implementation guide's
|
||||
ENV-01 carries the build.
|
||||
|
||||
### RUNTIME-02 — Extract the single-agent session
|
||||
|
||||
- **Branch:** `refactor/session-runtime`
|
||||
|
|
|
|||
|
|
@ -548,6 +548,14 @@ clock may lead environment time by warm-up; persist that offset instead of prete
|
|||
clocks start at zero. Rendering, physics and decision cadence may differ, but the backend
|
||||
must define their relationship.
|
||||
|
||||
**Amendment, 2026-09-23 (operator decision of 2026-09-23).** "Preserve the detailed legacy
|
||||
ordering inside the legacy single-agent composition" now means inside a legacy composition that
|
||||
runs on the session framework's `lockstep-v1`, not beside it: the operator decided on a full port.
|
||||
The detailed ordering is preserved because it is already the lockstep transaction order with one
|
||||
agent ([legacy-gameboy-v1](session-framework/legacy-gameboy-v1.md) section 4), and "keep the
|
||||
legacy floating remainder arithmetic" costs nothing, because the legacy `f64` frame constant is
|
||||
exactly the rational Game Boy frame and the two accumulators are identical (section 3 there).
|
||||
|
||||
Start with sequential agent evaluation for reproducibility. Then compare parallel agent
|
||||
evaluation against the same action trace. Cap total worker budget: `agents × brain_threads`
|
||||
can otherwise oversubscribe the machine. Use private pools for concurrent agents or serialize
|
||||
|
|
@ -605,7 +613,9 @@ jobs so repeated copies cannot exhaust memory under slow storage.
|
|||
|
||||
Exact replay requires action-executor and admission state, not just the neural envelope.
|
||||
Legacy macros intentionally discard transient execution on restart; preserve that behavior
|
||||
for v1 and label it as legacy continuation semantics, not exact session replay. New sessions
|
||||
for v1 and label it as legacy continuation semantics, not exact session replay.
|
||||
(*2026-09-23:* the label is `restore: legacy-transient-reset`, declared by the legacy
|
||||
composition, [legacy-gameboy-v1](session-framework/legacy-gameboy-v1.md) section 14.) New sessions
|
||||
persist all behavior-affecting state or explicitly restart an episode under a documented rule.
|
||||
|
||||
Keep `FLYSIM01` readable through a legacy adapter. Never silently rewrite a checkpoint on
|
||||
|
|
|
|||
|
|
@ -46,6 +46,15 @@ be built without them:
|
|||
envelope and the durable commit sequence. `FLYSIM01` is unchanged and stays separately
|
||||
readable.
|
||||
|
||||
One contract added by the 2026-09-23 amendments (PROF-02a, RT-01a), after the operator decided
|
||||
to port the live fly onto this framework:
|
||||
|
||||
- [Legacy Game Boy composition v1](legacy-gameboy-v1.md) — the legacy profile
|
||||
`gameboy-legacy-fafb-v783-v1`, its readout context and decision schemas, the memory-image
|
||||
inspection, the `pokered-macros-v1` executor, the `gameboy-slots-v1` environment extension,
|
||||
the `legacy-ratchet-rollback-v1` episode policy, `legacy-transient-reset` restore semantics and
|
||||
the composition digest. MaleCNS bundles (PROF-02b) come later.
|
||||
|
||||
For context: [modular-session analysis](../malecns-modular-sessions.md) and
|
||||
[Melee audit](../melee-framework-audit.md). Each contract owns its named subject; step ordering
|
||||
wins over an informal diagram, and Flybus owns transport/resource rules. Resolve contradictions
|
||||
|
|
@ -126,6 +135,19 @@ versions, historical arithmetic/fingerprints and FLYSIM01 reader. New identities
|
|||
readout/executor/task/scheduler semantics. New public feed v2 is an application/presentation
|
||||
gateway contract built on the same internal bus; it does not replace the bus or expose it raw.
|
||||
|
||||
**Amendment, 2026-09-23 (operator decision of 2026-09-23).** "Keep legacy-gameboy-v1 distinct
|
||||
from lockstep-v1" is superseded for scheduling. The operator decided on a **full port** of the
|
||||
live fly onto the session framework: the legacy composition runs under `lockstep-v1` with one
|
||||
agent, one port and one world, declared as [legacy-gameboy-v1](legacy-gameboy-v1.md). What the
|
||||
sentence protected is kept, and now written down rather than implied: the legacy frame order is
|
||||
this framework's transaction order (legacy-gameboy-v1 section 4); the historical fingerprint,
|
||||
the default version strings and the compatibility string are unchanged and embedded, not
|
||||
recomputed; the legacy clock is proven identical to the rational one (section 3); `FLYSIM01`
|
||||
stays the format of record and its reader untouched until RETIRE-01; and the legacy restore and
|
||||
rollback semantics are declared (`legacy-transient-reset`, `legacy-ratchet-rollback-v1`) rather
|
||||
than approximated. The new identity covers what the old string never did: readout context,
|
||||
decision, decoder configuration and macro channels, executor, slots and restore semantics.
|
||||
|
||||
## 5. Reuse criterion
|
||||
|
||||
Adding a third environment/application requires a backend, task/profile, composition and
|
||||
|
|
|
|||
|
|
@ -164,6 +164,18 @@ delayed rendering retains its handle. Distinguish AssetRef from transient Artifa
|
|||
(`feat/brain-profile-contract`) in the [MaleCNS backlog](../malecns-modular-implementation.md),
|
||||
which has not been built. Not started.
|
||||
|
||||
**2026-09-23:** unblocked. The operator split FOUNDATION-02 (decision of 2026-09-23): its legacy
|
||||
half, PROF-02a, is the [legacy Game Boy composition](legacy-gameboy-v1.md) contract with
|
||||
machine-readable profile, readout-context and decision schemas in `fly-session-types` and
|
||||
`@flybrain/session-types`; the MaleCNS half (PROF-02b) is later and does not gate this slice.
|
||||
AGENT-01 builds the legacy profile `gameboy-legacy-fafb-v783-v1` first. It must: report
|
||||
`brainTicks` equal to the legacy `network.ms` (legacy-gameboy-v1 section 3); consume
|
||||
`gameboy-readout-context-v1` and return `gameboy-channels-v1`; keep the held channel, blocked
|
||||
window and last location as private readout state; report `stimulusRemainingMs`; answer
|
||||
`Agent.Rollback` under capability `legacy-ratchet-rollback-v1`; and choose the mapping from the
|
||||
legacy rate-role names (`command_0`, `macro_*`, which are not `Id`s) to `AgentGraph.rateRoles`,
|
||||
which that contract leaves open.
|
||||
|
||||
**Implement:** adapter over existing LIF, plasticity, retina and fixed readout primitives;
|
||||
reference-first composition/goldens; independently seeded agent state and shared immutable data.
|
||||
Avoid using the old whole-frame `tick` wrapper if it changes the specified phase ordering.
|
||||
|
|
@ -180,6 +192,27 @@ dispatch order and varying worker count preserves results. Keep 64-role limits e
|
|||
RUNTIME-01 (`refactor/environment-task-boundary`) in the same backlog, which has not been
|
||||
built. Not started.
|
||||
|
||||
**2026-09-23:** unblocked. RUNTIME-01's contract is the RT-01a amendments of 2026-09-23 to
|
||||
[workers-v1](workers-v1.md) (sections 1, 3, 4, 5 and the new section 7),
|
||||
[step-v1](step-v1.md) (sections 2, 3, 5 and 6) and [state-media-v1](state-media-v1.md)
|
||||
(sections 2, 3, 4, 5 and 7), with the Game Boy specifics in
|
||||
[legacy-gameboy-v1](legacy-gameboy-v1.md), all implementing the operator's decisions of
|
||||
2026-09-23. ENV-01 and AGENT-01 may proceed in parallel against the schemas and fixtures; ENV-01
|
||||
needs FND-01's trace harness for its "legacy fixtures unchanged" acceptance. ENV-01 must also:
|
||||
add the one read-only bulk memory read to the shim and prove it mutates nothing; publish the
|
||||
memory image per boundary; declare the one-frame setup scaffold; convert audio to f32 and leave
|
||||
the DC blocker to the edge; implement `gameboy-slots-v1`; and make the coordinator refuse
|
||||
`episodeRequest.kind = "rollback"` in any composition that declares no rollback policy (the
|
||||
synthetic coordinator today pauses on every episode request, which is safe but not the rule).
|
||||
|
||||
**Amendment, 2026-09-23 (operator decision of 2026-09-23).** The "Implement" paragraph below said
|
||||
to keep `legacy-gameboy-v1` separately routed. The operator decided on a full port instead: the
|
||||
legacy composition runs on `lockstep-v1` as declared in legacy-gameboy-v1, and "exact old
|
||||
ordering/hash semantics" is kept by construction and by test rather than by a separate route --
|
||||
the frame order maps one to one (section 4), the legacy clock equals the rational one
|
||||
(section 3), and the fingerprint and compatibility string are embedded unchanged (sections 2
|
||||
and 12). The acceptance criteria below stand.
|
||||
|
||||
**Implement:** binjgb environment, task-local memory inspector and identity/existing action
|
||||
adapter. Keep `legacy-gameboy-v1` separately routed with exact old ordering/hash semantics.
|
||||
|
||||
|
|
|
|||
366
docs/design/session-framework/legacy-gameboy-v1.md
Normal file
366
docs/design/session-framework/legacy-gameboy-v1.md
Normal file
|
|
@ -0,0 +1,366 @@
|
|||
# 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` |
|
||||
| `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 (section 16) |
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
|
||||
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 canonical effective DecoderConfig
|
||||
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 taken over the canonical JSON of the effective `DecoderConfig` in the
|
||||
TypeScript oracle's shape (the Game Boy preset with its macro group). 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/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 is cleared: no held channel, and no last location (the first
|
||||
observed location starts the blocked window);
|
||||
- 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. In one direction a
|
||||
pulse that ended inside the in-flight transition reads as still running, and the request is refused and retried.
|
||||
In the other direction 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. Each case is bounded to one frame. After a restore, and before the first commit, the pulse is unknown and admission
|
||||
refuses with a retry. 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
|
||||
|
||||
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,15 @@ 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). 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 +206,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 +244,34 @@ 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.
|
||||
|
||||
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 |
|
||||
|
|
|
|||
|
|
@ -22,6 +22,9 @@ rpc.result. Large inputs/outputs use owned bus attachments, never another worker
|
|||
| `State.Capture` | Coordinator → agent/environment | Immutable snapshot of committed boundary |
|
||||
| `State.StageRestore` | Coordinator → agent/environment | Validate replacement state under new epoch |
|
||||
| `State.ActivateRestore` | Coordinator → agent/environment | Install staged state; remain quiescent |
|
||||
| `Environment.SaveSlot` | Coordinator → environment | Capability `gameboy-slots-v1`; record a slot at the committed boundary (section 7) |
|
||||
| `Environment.RestoreSlot` | Coordinator → environment | Capability `gameboy-slots-v1`; boundary k under a new epoch (section 7) |
|
||||
| `Agent.Rollback` | Coordinator → agent | Capability `legacy-ratchet-rollback-v1`; Ready(e,k) → Ready(e',k), no tick (section 7) |
|
||||
|
||||
State methods have payloads in [state and media](state-media-v1.md). Artifact lifetime and
|
||||
message consumption are bus operations managed by the SDK, not Worker/Coordinator methods.
|
||||
|
|
@ -51,6 +54,8 @@ interface AgentTelemetry {
|
|||
rates: { roleId: Id; hz: number }[];
|
||||
learning: { enabled: boolean; updates: U64; changed: U64; signal: number };
|
||||
}
|
||||
// Amendment 2026-09-23: AgentTelemetry also carries
|
||||
// stimulusRemainingMs: number | null; // pulse still running after the operation; null = reports none
|
||||
```
|
||||
|
||||
`AssetRef` names persistent content in a preprovisioned local registry; it is not an arbitrary path or
|
||||
|
|
@ -75,6 +80,15 @@ finite; shipped positive-only task profiles reject negatives. Empty rewards do n
|
|||
different numerical rule. `id`/`eventId` is unique within its outcome or command namespace;
|
||||
the coordinator assigns stable IDs before sending a mutating request.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** `AgentTelemetry` gains
|
||||
`stimulusRemainingMs: number | null`: the milliseconds of stimulation pulse still running when
|
||||
the operation that reports the telemetry completed, or `null` for an agent that has no pulse to
|
||||
report. The operator decided that sugar admission reads the legacy `reward_remaining` from the
|
||||
last commit's telemetry (section 5 amendment), and no field carried it. It is finite and
|
||||
nonnegative; it is a report, never an input. The field is required-and-nullable like every
|
||||
optional field in these contracts, so every existing producer now writes `null`. It changes
|
||||
`contractDigest`, which [session RPC](ipc-v1.md) section 4 already provides for.
|
||||
|
||||
## 2. Agent methods
|
||||
|
||||
### Agent.Initialize
|
||||
|
|
@ -283,6 +297,25 @@ The environment only needs backend-relevant portions of task setup, not reward r
|
|||
neural policies. `taskConfig` resolves a declared setup configuration; the complete task
|
||||
implementation and ledger stay in the coordinator.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** Three readings for the
|
||||
Game Boy environment of the legacy composition ([legacy-gameboy-v1](legacy-gameboy-v1.md)
|
||||
sections 8 and 9), stated here because they are about this method's shape:
|
||||
|
||||
- *Setup scaffold.* The backend configuration may declare setup frames the environment runs
|
||||
with every control neutral before it returns O[0]. The legacy composition declares exactly
|
||||
**one frame with no button down**, which is what its fresh start does; O[0] then has
|
||||
`engineFrame` `"1"`, `worldTime` `0/1` and no audio chunk. These frames are declared
|
||||
scaffold, attributed to no fly, and never a transition.
|
||||
- *Inspection may be artifact-backed.* `inspection` is a `TypedValue` under the descriptor's
|
||||
schema; the legacy schema `gameboy-memory-inspection-v1` carries a 64-KiB memory image as an
|
||||
`ArtifactRef` (listed in the bus attachments) plus the ROM's digest. This is the "explicit
|
||||
artifact-backed schema" section 1 requires for typed state over 32 KiB, and it is the one
|
||||
bulk transfer per boundary the section 4 rule against per-byte remote reads asks for. The
|
||||
environment's only write to a running game remains the controller batch.
|
||||
- *Audio.* The environment publishes native samples in the declared f32 format; converting a
|
||||
backend's integer samples to f32 is the environment's job (binjgb: `sample / 255`), and any
|
||||
filtering for listening -- the legacy DC blocker -- is presentation, applied by the edge.
|
||||
|
||||
### Environment.Advance
|
||||
|
||||
```ts
|
||||
|
|
@ -349,6 +382,18 @@ but not a port assignment. The coordinator supplies the port. Per-agent executor
|
|||
private; a running macro may emit controls according to its declared policy, but only after
|
||||
neural selection. The first implementation supports the stateless identity executor only.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** The legacy composition
|
||||
declares the extension **`executor: pokered-macros-v1`**: its task and its action executor are
|
||||
**one object** implementing both interfaces above, serving one agent on one port
|
||||
([legacy-gameboy-v1](legacy-gameboy-v1.md) section 10). They share state that neither could
|
||||
reach through a declared channel if split: the executor's scene observation is the `bound` set
|
||||
the task hands the decoder, the macros read the task's exploration ledgers, and the macro layer's
|
||||
progress signal feeds the ratchet. The executor reads the boundary's memory image and the ROM
|
||||
`AssetRef`, never the emulator. "Stateless identity executor only" remains true of the
|
||||
synthetic composition; a stateful executor is permitted exactly where a composition declares
|
||||
one by name, and its state is captured or declared transient by that composition's restore
|
||||
semantics (state-media-v1 section 4 amendment).
|
||||
|
||||
The executor's currentGameState is a coherent read-only inspector view at this boundary;
|
||||
progressView supplies task history/objectives. It updates its selected action every step
|
||||
(movement, path replanning, interaction, completion), not merely replaying a blind button
|
||||
|
|
@ -372,6 +417,15 @@ not arbitrary raw inspector memory or incoming chat.
|
|||
It requests a coordinator-owned policy transition after final reward commit; it cannot reset
|
||||
the environment directly. Generic progress is a TypedValue, not mandatory Pokémon ladder data.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** `episodeRequest.kind` is
|
||||
`"terminal"` or **`"rollback"`**. A rollback request asks for the composition's declared
|
||||
rollback policy; its `outcome` is that policy's registered schema. The only policy defined is
|
||||
`legacy-ratchet-rollback-v1` (outcome `{slotId, trigger}`), applied at the boundary the
|
||||
transition just committed without a pause ([step-v1](step-v1.md) section 6 amendment). A
|
||||
composition that declares no rollback policy treats the request as a task failure. It still
|
||||
"cannot reset the environment directly": the coordinator applies the policy through the
|
||||
section 7 methods.
|
||||
|
||||
## 5. Admission and audience boundary
|
||||
|
||||
The first synthetic implementation has no audience input. Later integration maps permitted
|
||||
|
|
@ -386,6 +440,19 @@ v2 contract must specify accepted/applied/rolled-back/aborted states and reconci
|
|||
paid interactions are enabled. Do not inherit a claim of durable exactly-once stimulation
|
||||
from these in-memory worker request caches.
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** Sugar in the legacy
|
||||
composition is admitted by the coordinator with the legacy rules -- the rate limiter and "no
|
||||
overlap with an active pulse" -- reading the pulse from `stimulusRemainingMs` of the **last
|
||||
completed commit**. The operator accepted that this value can be one commit old; both
|
||||
consequences are bounded to one frame and are listed in
|
||||
[legacy-gameboy-v1](legacy-gameboy-v1.md) section 15. Admitted sugar is a profile-supported
|
||||
`reward-pulse` stimulus in the next Prepare's `preStepStimulations`. Because admission and
|
||||
application are now separate, each admission record carries its interaction id and ends
|
||||
`applied` or, if the epoch fails before that Prepare commits, `aborted`; the edge fulfils the
|
||||
first and refunds the second, and the slice wiring the bridge tests both. This is the
|
||||
accepted/applied/aborted distinction the paragraph above asks for, for this one interaction
|
||||
kind; paid interactions in general still need the public v2 contract.
|
||||
|
||||
## 6. Health, shutdown and extensions
|
||||
|
||||
All workers implement the common Hello/Status/Shutdown/Acknowledge methods. Capture/restore
|
||||
|
|
@ -395,3 +462,56 @@ advertised. Unsupported methods return `UNSUPPORTED`, mutation none.
|
|||
New task-specific fields belong in registered TypedValue schemas. New worker capabilities,
|
||||
variable-duration stepping, subscriptions or additional sensor modalities require a contract
|
||||
change and shared fixtures. An unconstrained plugin dictionary is not a substitute for that.
|
||||
|
||||
## 7. Extension methods (amendment, 2026-09-23)
|
||||
|
||||
**Amendment, 2026-09-23 (RT-01a; operator decision of 2026-09-23).** The operator decided on a
|
||||
full port of the live fly, whose ratchet rolls the *game* back to a saved state while the brain
|
||||
continues. Neither half of that is expressible with sections 2 and 3: there is no method that
|
||||
saves or restores world state outside a coherent group checkpoint, and none that installs a new
|
||||
input in an agent without a transition. These three methods are that, generically shaped, each
|
||||
behind a capability a worker advertises in `Worker.Hello`; a worker without it answers
|
||||
`UNSUPPORTED`, mutation none. Payloads are in `fly-session-types` (`extensions`) and
|
||||
`@flybrain/session-types`, in the session schema set.
|
||||
|
||||
```ts
|
||||
// Environment.SaveSlot -- capability gameboy-slots-v1. Scope: the committed boundary (e, k).
|
||||
interface SaveSlotParams { slotId: Id } // a slot the composition declares
|
||||
interface SaveSlotResult { slotId: Id; boundary: U64; // == scope.step
|
||||
stateDigest: Digest; byteLength: U64 }
|
||||
|
||||
// Environment.RestoreSlot -- capability gameboy-slots-v1. Scope: (e', k), a NEW epoch.
|
||||
interface RestoreSlotParams { slotId: Id; priorEpoch: Id; // the environment is Ready(e, k)
|
||||
policy: "legacy-ratchet-rollback-v1" }
|
||||
interface RestoreSlotResult { slotId: Id; committedStep: U64; // == k: no transition ran
|
||||
observation: WorldObservation } // boundary k, no audio chunk
|
||||
|
||||
// Agent.Rollback -- capability legacy-ratchet-rollback-v1. Scope: (e', k), the same new epoch.
|
||||
interface AgentRollbackParams { agentId: Id; priorEpoch: Id; // the agent is Ready(e, k)
|
||||
policy: "legacy-ratchet-rollback-v1";
|
||||
input: SensoryInput; // boundary k: the restored world
|
||||
decisionContext: TypedValue } // for the next Prepare
|
||||
interface AgentRollbackResult { agentId: Id; committedStep: U64; // == k: no tick ran
|
||||
decisionContextDigest: Digest; telemetry: AgentTelemetry }
|
||||
```
|
||||
|
||||
- Both environment methods run only at a committed boundary with no Advance outstanding.
|
||||
`SaveSlot` replaces the slot's contents with the world state and the frame on screen at that
|
||||
boundary; slots are environment state and belong to its `State.Capture` payload. It is one
|
||||
operation per boundary under the section-5 operation key of [session RPC](ipc-v1.md).
|
||||
- `RestoreSlot` and `Agent.Rollback` move a participant from `(priorEpoch, k)` to
|
||||
`(scope.epoch, k)`; `priorEpoch` must differ from the scoped epoch, and after the move every
|
||||
request scoped to the prior epoch is `STALE_EPOCH`. The restored observation keeps the
|
||||
boundary number, `worldTime` and `engineFrame`, and returns fresh artifacts; it carries no
|
||||
audio chunk, and the next chunk marks a discontinuity (state-media-v1 section 2).
|
||||
- `Agent.Rollback` applies the policy's agent half and nothing else: for
|
||||
`legacy-ratchet-rollback-v1`, clear decoder holds and plastic eligibility, install `input`
|
||||
without a tick, reset the readout's transient (held channel, blocked window, location from
|
||||
the context), keep the brain clock, membrane, RNG, rates and gains. No reward, stimulation or
|
||||
calibration. It is the only way to install an input outside a Commit.
|
||||
- A failure of any of these methods mid-policy fails the epoch; recovery is the coherent group
|
||||
restore of [state-media-v1](state-media-v1.md) section 6. There is no partial rollback.
|
||||
- `maxSlots` is 4 (a crate-chosen bound, published in the schema set).
|
||||
|
||||
The sequence that uses them is [step-v1](step-v1.md) section 6's amendment and
|
||||
[legacy-gameboy-v1](legacy-gameboy-v1.md) section 11.
|
||||
|
|
|
|||
|
|
@ -99,6 +99,17 @@ argmax of the same normalized scores over the same rates. It is the same kind of
|
|||
the retina already sees, arriving through a much narrower channel, and it is disclosed on the
|
||||
honesty panel with the rest of the readout.
|
||||
|
||||
**Where the location comes from on the session framework (2026-09-23).** When the live fly runs
|
||||
on the session framework (the operator's port decision of 2026-09-23), the sim loop that owned
|
||||
the position is split: the task reads the location, the agent owns the decoder. The location
|
||||
then reaches the decoder as a **declared** field of its decision context,
|
||||
`gameboy-readout-context-v1 {boot, bound, location}`
|
||||
([legacy Game Boy composition](design/session-framework/legacy-gameboy-v1.md) section 5). The rule is
|
||||
unchanged: the location only restarts the blocked window, `null` is still no information, the
|
||||
held channel and the window stay the readout's own state, and the blocked channel is still
|
||||
computed here, never handed in. The profile allowlists the field, and the task cannot put
|
||||
anything else in it.
|
||||
|
||||
## Macro group (2026-09-16)
|
||||
|
||||
`DecoderConfig.macros` is a second `ExclusiveGroup` with the same fields and the same decision rules
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue