diff --git a/docs/design/malecns-modular-implementation.md b/docs/design/malecns-modular-implementation.md index 7b66b88..5f0c7b8 100644 --- a/docs/design/malecns-modular-implementation.md +++ b/docs/design/malecns-modular-implementation.md @@ -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` diff --git a/docs/design/malecns-modular-sessions.md b/docs/design/malecns-modular-sessions.md index 9be1214..095ad4b 100644 --- a/docs/design/malecns-modular-sessions.md +++ b/docs/design/malecns-modular-sessions.md @@ -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 diff --git a/docs/design/session-framework/README.md b/docs/design/session-framework/README.md index af0796c..f4cbc1b 100644 --- a/docs/design/session-framework/README.md +++ b/docs/design/session-framework/README.md @@ -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 diff --git a/docs/design/session-framework/implementation.md b/docs/design/session-framework/implementation.md index bfb408a..a11fa4c 100644 --- a/docs/design/session-framework/implementation.md +++ b/docs/design/session-framework/implementation.md @@ -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. diff --git a/docs/design/session-framework/legacy-gameboy-v1.md b/docs/design/session-framework/legacy-gameboy-v1.md new file mode 100644 index 0000000..a41d36f --- /dev/null +++ b/docs/design/session-framework/legacy-gameboy-v1.md @@ -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=`, 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. diff --git a/docs/design/session-framework/state-media-v1.md b/docs/design/session-framework/state-media-v1.md index 0285287..ac52f6c 100644 --- a/docs/design/session-framework/state-media-v1.md +++ b/docs/design/session-framework/state-media-v1.md @@ -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. diff --git a/docs/design/session-framework/step-v1.md b/docs/design/session-framework/step-v1.md index ab8554b..d9edf2f 100644 --- a/docs/design/session-framework/step-v1.md +++ b/docs/design/session-framework/step-v1.md @@ -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 | diff --git a/docs/design/session-framework/workers-v1.md b/docs/design/session-framework/workers-v1.md index 9692dc6..9769270 100644 --- a/docs/design/session-framework/workers-v1.md +++ b/docs/design/session-framework/workers-v1.md @@ -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. diff --git a/docs/readout.md b/docs/readout.md index 1b4ef52..e8a4a6d 100644 --- a/docs/readout.md +++ b/docs/readout.md @@ -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