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:
acamilo 2026-09-23 08:32:05 +00:00
parent bbf71bfead
commit 24ba933500
9 changed files with 702 additions and 1 deletions

View file

@ -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; - **Done:** empty required populations, malformed CSR and incorrect profile restores fail;
legacy FAFB artifacts and default numerical version strings remain unchanged. 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 ### DATA-01 — Acquire and normalize MaleCNS
- **Branch:** `feat/malecns-import` - **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 - **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. 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 ### RUNTIME-02 — Extract the single-agent session
- **Branch:** `refactor/session-runtime` - **Branch:** `refactor/session-runtime`

View file

@ -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 clocks start at zero. Rendering, physics and decision cadence may differ, but the backend
must define their relationship. 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 Start with sequential agent evaluation for reproducibility. Then compare parallel agent
evaluation against the same action trace. Cap total worker budget: `agents × brain_threads` 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 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. Exact replay requires action-executor and admission state, not just the neural envelope.
Legacy macros intentionally discard transient execution on restart; preserve that behavior 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. 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 Keep `FLYSIM01` readable through a legacy adapter. Never silently rewrite a checkpoint on

View file

@ -46,6 +46,15 @@ be built without them:
envelope and the durable commit sequence. `FLYSIM01` is unchanged and stays separately envelope and the durable commit sequence. `FLYSIM01` is unchanged and stays separately
readable. 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 For context: [modular-session analysis](../malecns-modular-sessions.md) and
[Melee audit](../melee-framework-audit.md). Each contract owns its named subject; step ordering [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 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 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. 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 ## 5. Reuse criterion
Adding a third environment/application requires a backend, task/profile, composition and Adding a third environment/application requires a backend, task/profile, composition and

View file

@ -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), (`feat/brain-profile-contract`) in the [MaleCNS backlog](../malecns-modular-implementation.md),
which has not been built. Not started. 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; **Implement:** adapter over existing LIF, plasticity, retina and fixed readout primitives;
reference-first composition/goldens; independently seeded agent state and shared immutable data. 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. 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 RUNTIME-01 (`refactor/environment-task-boundary`) in the same backlog, which has not been
built. Not started. 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 **Implement:** binjgb environment, task-local memory inspector and identity/existing action
adapter. Keep `legacy-gameboy-v1` separately routed with exact old ordering/hash semantics. adapter. Keep `legacy-gameboy-v1` separately routed with exact old ordering/hash semantics.

View 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.

View file

@ -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 browser delivery and streaming belong to the application/presentation layer. No bus or
generic session configuration assumes a 1080p show or Twitch output. 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 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 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 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 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. 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 **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 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 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. or reproducible reconstruction inputs, admission state and event watermarks.
- Payload names, lengths and hashes, including external-helper state required for exact resume. - 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 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 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, 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 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. 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 ## 6. Durable commit, router failure and recovery
Write payload/envelope temporary generation, fsync, rename, fsync directory, then atomically 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. 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 Legacy Pokémon ratchet behavior remains in the legacy composition. Shared competitive worlds
never restore one player's environment independently of the other players. 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.

View file

@ -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 implements the barrier. Read [architecture](README.md) and [session RPC](ipc-v1.md) first. Method payloads are in
[worker interfaces](workers-v1.md). [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 ## 1. Committed boundary
At `Ready(epoch, k)`: 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 to finish first, so the only boundary such a pause can land on is the one the
transition just committed. 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 ## 3. Transaction sequence
### Phase A: prepare all agents concurrently ### 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 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. game state, task progress/objectives and clock from this boundary.
Direct-control profiles use an identity executor. Macro profiles are explicit extensions. 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 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. ports. Uncontrolled ports are configured neutral before the epoch, not supplied ad hoc.
4. Send exactly one `Environment.Advance(scope=k, batchId, controls)`. 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 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. 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 ### Phase D: commit all agent outcomes concurrently
Send `Agent.Commit(scope=k)` with that agent's next sensory observation and routed outcomes. 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 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. 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 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 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. 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 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. 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 ## 7. Failure rules
| Failure point | Required response | | Failure point | Required response |

View file

@ -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.Capture` | Coordinator → agent/environment | Immutable snapshot of committed boundary |
| `State.StageRestore` | Coordinator → agent/environment | Validate replacement state under new epoch | | `State.StageRestore` | Coordinator → agent/environment | Validate replacement state under new epoch |
| `State.ActivateRestore` | Coordinator → agent/environment | Install staged state; remain quiescent | | `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 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. message consumption are bus operations managed by the SDK, not Worker/Coordinator methods.
@ -51,6 +54,8 @@ interface AgentTelemetry {
rates: { roleId: Id; hz: number }[]; rates: { roleId: Id; hz: number }[];
learning: { enabled: boolean; updates: U64; changed: U64; signal: number }; learning: { enabled: boolean; updates: U64; changed: U64; signal: number };
} }
// 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 `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; different numerical rule. `id`/`eventId` is unique within its outcome or command namespace;
the coordinator assigns stable IDs before sending a mutating request. 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 ## 2. Agent methods
### Agent.Initialize ### 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 neural policies. `taskConfig` resolves a declared setup configuration; the complete task
implementation and ledger stay in the coordinator. 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 ### Environment.Advance
```ts ```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 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. 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; 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 progressView supplies task history/objectives. It updates its selected action every step
(movement, path replanning, interaction, completion), not merely replaying a blind button (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 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. 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 ## 5. Admission and audience boundary
The first synthetic implementation has no audience input. Later integration maps permitted 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 paid interactions are enabled. Do not inherit a claim of durable exactly-once stimulation
from these in-memory worker request caches. 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 ## 6. Health, shutdown and extensions
All workers implement the common Hello/Status/Shutdown/Acknowledge methods. Capture/restore 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, New task-specific fields belong in registered TypedValue schemas. New worker capabilities,
variable-duration stepping, subscriptions or additional sensor modalities require a contract 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. 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.

View file

@ -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 the retina already sees, arriving through a much narrower channel, and it is disclosed on the
honesty panel with the rest of the readout. 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) ## Macro group (2026-09-16)
`DecoderConfig.macros` is a second `ExclusiveGroup` with the same fields and the same decision rules `DecoderConfig.macros` is a second `ExclusiveGroup` with the same fields and the same decision rules