31 KiB
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, step protocol and
session media/state, 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 section 4, implementation guide ENV-01,
state-media-v1 section 7 and the
modular-session analysis section 5.2. Each now carries a
dated amendment that cites this decision. None was silently rewritten. The reason for the change is in
section 4: the legacy frame order already is the lockstep order, with one agent, one port
and one world. Moving it onto the framework therefore keeps every ordering fact those
statements protected.
2. The profile gameboy-legacy-fafb-v783-v1
Exactly one document exists. Every field of it is fixed, so both the document and its digest are constants of
this contract. Any other value is another profile and needs another id. The document is canonical
JSON (RFC 8785), its AssetRef is {id: "gameboy-legacy-fafb-v783-v1", format: "fly-profile-v1", digest, byteLength}, and the digest and length are taken over the canonical
bytes. The current values are in fixtures/gameboy-legacy.json: digest 41e5d1ac…c60878, length 1137.
| Field | Value | Why it is fixed |
|---|---|---|
profileId |
gameboy-legacy-fafb-v783-v1 |
|
datasetId, fingerprintSchema |
fafb-v783, 1 |
The schema-1 fingerprint is the seven SHA-256 digests joined with : |
datasetFingerprint |
Today's value, byte for byte the compatibility string's segment 2 | This is the "embeds today's schema-1 fingerprint" of the decision. flysim's legacy_profile_identity test recomputes it from data/fafb-v783 |
kernelVersion, plasticityVersion |
lif-1ms-f64-v2, fly-kc-mbon-rstdp-v2 |
Pinned defaults (CLAUDE.md). The same test compares them with the built network |
tickDuration |
1000000/1 ns |
One model tick |
warmupMs |
2500 |
Fresh-start warm-up with learning disabled, DEFAULT_WARMUP_MS. A service configured with another warm-up (loop.warmup_ms, FLYSIM_LOOP_WARMUP_MS) is another profile and needs its own id; the legacy composition refuses to start this profile with any other value. It matters only on a fresh start, because a restore never warms up, but it is identity all the same |
view |
lcd, 160 x 144 |
The retina's native frame |
supportedStimuli |
["reward-pulse"] |
Sugar and task reward events both drive stimulate(durationMs) |
readoutContextSchema, decisionSchema |
The registered references of sections 5 and 6 | |
legacyExceptions |
["macro-roles-outside-fingerprint"] |
The macro_* roles are merged after the fingerprint is taken (modular analysis 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 section 5 produce identical tick counts and remainders.
Both languages assert this over the first 100,000 (Rust) and 20,000 (TypeScript) frames, and
the fixture records the first twelve frames: 16, 17, 17, 16, and so on. No separate legacy
arithmetic is needed, and step-v1's rule that the clock must not accumulate rounded time holds unchanged. A
FLYSIM01 remainder (f64 ms) converts exactly to a RationalNs.
The task's clock is the agent's brain time. PreparedDecision.brainTicks times 1 ms is the
legacy network.ms, and it counts warm-up. AGENT-01 must make brainTicks equal to that value,
because the ratchet windows and the reward adapter's timing read it.
4. Placement in lockstep-v1
The legacy Sim::step_frame order maps onto the transaction phases one to one:
| Legacy step | Lockstep phase |
|---|---|
| Drain commands (sugar) | Admission cut at Ready(k). The sugar enters Prepare.preStepStimulations (section 15) |
network.step(ticks) |
Phase A: Agent.Prepare advances the ticks |
decode_bound(rates, ms, boot, blocked, bound) |
Phase A: readout with the context of section 5. The blocked rule stays inside the agent |
MacroLayer::decide |
Phase B: the pokered-macros-v1 executor reads O[k]'s memory image (section 10) |
set_buttons, run_frame |
Phase B: one Environment.Advance with the complete joypad batch |
Framebuffer, take_audio_u8 |
The environment returns O[k+1]: view, audio chunk, memory image |
adapter.sample |
Phase C: the task, which is the same object, evaluates old/new inspection once |
stimulate per event, reinforce(sum) |
Phase D: Agent.Commit installs the input, then the stimulations in event order, then one reinforcement |
MacroLayer::observe, location() |
Phase C: this produces the next context's bound and location |
| Ratchet observe, capture, recover | Phase C decides. Environment.SaveSlot and the rollback run at Ready(k+1) (section 11) |
| Milestone archive | A durable save at Ready(k+1), exported as FLYSIM01, after that boundary's slot save (section 16) |
Amended 2026-09-23, review round 1. One order differs from the legacy loop and is declared.
Legacy track_rank archives the milestone before the ratchet captures, in the same frame, so a
legacy archive holds the pre-capture ratchet (best = the previous rung) with the previous
snapshot. Here the ratchet ledger commits best = r in Phase C, and the slot is only filled by
Environment.SaveSlot at Ready(k+1). A capture ordered before that save would pair best = r
with the previous slot's contents -- or an empty slot on the first climb -- on every rank climb.
Section 16 therefore orders the save first, and a ported archive holds the post-capture
ratchet and slot. Both are internally consistent; they are not the same bytes.
The input installed at Commit and ticked at the next Prepare is the frame the legacy loop
hands set_visual_frame before it samples rewards. Rewards are sampled from the frame just
produced. This is the ordering that modular analysis 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
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 Ids. 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, "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 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
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
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 typeapplication/octet-stream. Its digest is optional, because it is a transient live artifact (state-media-v1 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_memin 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_buttonsstays the only write, and there is no memory-write path.read_uncachedis 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 persistentAssetRefin the composition (section 12), and it is refused unless its digest is the environment'scontentDigest. 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"andworldTime0/1. That frame's audio is not published: O[0] carries no chunk (state-media-v1 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
lcd160 x 144rgba8(row stride 640) withobservationDelaySteps0.engineFrameis 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 isf32le-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.
stepDurationis8572265625/512,inspectionSchemais section 8,recoveryisexact-checkpoint, anddeterminismisfixed-build. - Slots,
gameboy-slots-v1. This is an environment capability forEnvironment.SaveSlotandEnvironment.RestoreSlot(workers-v1 section 7). A slot holds the emulator's exported state and the framebuffer that was on screen, as the ratchet'sSnapshotdoes. 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'sState.Capturepayload, as FLYSIM01'sratchet_gameandratchet_frameare today. A slot save due at a boundary completes before anyState.Captureor FLYSIM01 export at that boundary (section 16).
10. Executor pokered-macros-v1
The Pokémon Red task (reward adapter, ladder, ratchet ledger) and its action executor (the
macro layer) are one object. It implements both workers-v1 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 oneReward(its value) and oneStimulusof kindreward-pulse(itsstimulation_ms), in event order. The macro layer observes O[k+1]. The location is read. The ratchet observes, and may requestSaveSlotatReady(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
rewardchunk) 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 byobserveon 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:
- If this boundary also has a slot save due,
Environment.SaveSlotruns first. - 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.worldTimecontinues,engineFramecontinues, the view is the slot's archived frame, and there is no audio chunk. - Coordinator-local: the task clears the adapter's transient reward observations. The
executor runs
cancel, thenobserve(O'[k+1]), which gives the next context. 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.- 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. - A durable save follows, as the legacy loop checkpoints after a recovery. Any capture at this boundary -- before or after the rollback -- is taken after step 1's slot save.
Worker status and lost replies (amended 2026-09-23, review round 1). While it executes
Environment.SaveSlot a worker's Worker.Status reports capturing; while it executes
Environment.RestoreSlot or Agent.Rollback it reports restoring, with currentScope still
the prior (e, k+1). After the reply it reports ready at (e', k+1). These are mutations
under the session RPC section 5 operation key (session, e', k+1, method, worker)
(SaveSlot: (session, e, k+1, …)), so a lost reply goes through ipc-v1 section 6 first: stop
dispatch, query Worker.Status or retransmit the same request id and body to the same
incarnation, and resolve only the matching cached result. Only an unresolvable outcome -- a
changed incarnation, lost routes or ownership, RESULT_EXPIRED -- fails the epoch, and then the
group restores from the last durable checkpoint. No participant is ever left in e' while
another continues in e: the coordinator issues no Prepare until every rollback reply is
resolved.
The following continue through the rollback: brain clock, membrane, RNG, learned gains, rates and
reward history, the ratchet ledger (its attempt and lifetime budgets were spent in Phase C), and
the adapter's persistent state. The first audio chunk after the rollback marks a
discontinuity. This is the legacy recover_game sequence with the neural half moved into the agent.
Each half touches only its own state, so the order between the halves does not matter.
12. Composition declaration and digest
interface LegacyGameboyComposition {
compositionId: Id;
scheduler: "lockstep-v1";
profile: AssetRef; // the section 2 document
executor: { id: "pokered-macros-v1"; rom: AssetRef; adapter: Id; symbolProvenance: string;
mode: "raw" | "macros"; macroChannels: ChannelName[] }; // [] iff raw
decoderConfigDigest: Digest; // SHA-256 of the gameboy-decoder-config-v1 form
environment: { extensions: ["gameboy-slots-v1"]; slots: Id[]; stepDuration: RationalNs;
inspectionSchema: SchemaRef; controllerSchema: SchemaRef; setupFrames: 1;
audio: { sampleRate: number; channels: 2 } };
episodePolicy: "legacy-ratchet-rollback-v1";
restore: "legacy-transient-reset";
checkpointFormatOfRecord: "FLYSIM01";
flysimCompatibility: string; // the FLYSIM01 string, recorded, not reinterpreted
}
decoderConfigDigestis the SHA-256 of the canonical JSON of the formgameboy-decoder-config-v1of the effective decoder configuration (amended 2026-09-23, review round 1):{form, exclusive, macros, pulses, clearLockoutMs}, each group{channels: [{channel, role}], decisionMs, holdMs, hysteresis, fatigueGain, fatigueDecay, blockedFatigue, blockedMs}ornull, each pulse{channel, role, holdMs, cooldownMs, threshold, boot: {cooldownMs, threshold} | null, throttleGroup: string | null}. Channels are an array because their order breaks argmax ties and canonical JSON sorts object keys. The shared vectors arefixtures/gameboy-decoder-config.json(raw mode and the 31-channel Pokémon Red group):flysim'slegacy_profile_identitytest computes them fromgameboy_decoder_config_with_macros(and rewrites them underFLY_UPDATE_FIXTURES=1), and@flybrain/session-typesreproduces them from the oracle'sgameboyDecoderConfig. The example composition carries the real macros-mode digest. A change to a decoder timing, a threshold or the macro channel set therefore changes the composition digest. None of those changes touches the legacy compatibility string, which never covered them.- The declaration's digest is the SHA-256 of its canonical JSON. The coordinator's
compositionDigestrecipe (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. flysimCompatibilitymust agree with the declaration in its kernel, adapter, fingerprint, plasticity andpokered:segments, so the two cannot describe two different flies. Until RETIRE-01 the restore gate is still that string and the legacy decision rules (flybrain_gb::compatibility::decide,FLY_ACCEPT_ADAPTERS). The composition digest is the identity for publication, traces and descriptor revisions, not a restore gate. This matches today's behaviour, where a decoder change does not refuse a checkpoint.
13. Machine-readable parts
| Where | What |
|---|---|
fly-session-types/src/gameboy.rs, packages/session-types/src/gameboy.ts |
The five registered payload schemas, the profile, the composition declaration and their readers and cross-checks |
fly-session-types/src/extensions.rs, packages/session-types/src/extensions.ts |
SaveSlot*, RestoreSlot* and AgentRollback* payloads, which are in the session schema set |
fixtures/gameboy-legacy.json (derived) |
The extension set and its digest, every SchemaRef, the profile document with its canonical bytes and AssetRef, the frame clock, and an example composition with its digest and the digest recipe |
fixtures/gameboy-decoder-config.json |
The decoderConfigDigest vectors (raw and Pokémon Red macros), written and checked by flysim's legacy_profile_identity test, reproduced by the TypeScript oracle |
TraceBehaviour.boundaryActions, TraceOperational.captures |
The section 16 order rule, refused by TransitionTrace validation in both languages |
fixtures/valid.json, invalid.json |
Accepted and refused cases for every new type, held to both languages |
flysim/tests/legacy_profile_identity.rs |
Recomputes the fingerprint, versions, frame size, warm-up, clock and button order from the committed dataset and the service defaults |
A payload schema's SchemaRef.digest is the SHA-256 of its canonical declaration
{registry, id, version, source, fields}. The legacy schemas are digested by their own
extension set, not by contractDigest: the session contract stays free of console state.
The generic changes of RT-01a (stimulusRemainingMs, EpisodeRequestKind.rollback, the six
extension payloads, maxSlots) are in the session schema set, and they moved
contractDigest to the value in fixtures/contract-digest.json. Regenerate with
cargo run -p fly-session-types --example update_fixtures. tests/schema_set.rs refuses
stale files.
Open for AGENT-01: the legacy rate roles (command_0, macro_go_item, and so on) are not
valid Ids, while AgentGraph.rateRoles and AgentTelemetry.rates[].roleId are Ids. The
mapping belongs to the agent adapter. This contract does not choose it.
14. Restore legacy-transient-reset
The legacy composition declares that a restore (a FLYSIM01 load today, and any group restore under this composition) is not an exact replay. The restored parts are those FLYSIM01 carries: agent state, emulator, framebuffer, adapter state, ratchet state and slots, frame counter, remainder, buttons and event watermark. The rest starts cleared, as it does in a fresh process:
- the executor's ledgers (blocked, reached, talked, errand) are empty, with no macro running;
- the agent's readout transient starts exactly as a fresh legacy process has it (amended
2026-09-23, review round 1): no held channel, no last location, and the blocked window
starting at brain time 0 ms, not at the restored clock. The decoder state itself
(
DecoderState: holds, winners, fatigue) is restored. The consequence AGENT-01 must reproduce: on the first decode after a restore,now - 0 >= blockedMs, so the restored direction winner, if any, is passed asblockedand its fatigue is raised toblockedFatigue. Only after that decode does the window restart, because the held channel changed from none to the winner, and again when the first location is observed. A rollback (section 11) differs: it clears the holds and winners and restarts the window at the current brain time; - the adapter's transient observations are cleared, as they are on a rollback.
Resume tests compare against the legacy restore outcome, not against an uninterrupted trace (state-media-v1 section 4 amendment). This is the property the operator's unstick procedure depends on: restarting the service clears the ledger-shaped traps and keeps the rung.
15. Sugar admission
The coordinator owns admission, with the legacy rules: the per-minute limiter, and "no overlap
with an active pulse". The pulse is read from AgentTelemetry.stimulusRemainingMs of the last
completed commit (an Agent.Rollback reply counts as one). The value can therefore be one
commit old, and the operator accepted this lag. The consequences, each bounded to one frame:
- a pulse that ended inside the in-flight transition still reads as running, so the request is refused and retried;
- a reward pulse added by the in-flight transition is not yet visible, so a sugar can be admitted over it where the legacy loop would have refused;
- after a restore, and until the first commit of the new epoch, the pulse is unknown and every request is refused with a retry, where the legacy loop admits against the restored pulse at once (amended 2026-09-23, review round 1).
The duration is clamped to [1, sugar_max_ms]. An admitted sugar is a reward-pulse
Stimulus in the next Prepare's preStepStimulations, which is the position of the legacy
drain at the top of a frame. Legacy admission is application. Here, the epoch can fail
between the two, so each admission record carries its interaction id. If the Prepare that
applies it never commits, the admission is reported aborted and the edge refunds it. The
bridge's fulfil path and refund path both get tests in the slice that wires them
(workers-v1 section 5 amendment).
16. Checkpoint format of record
Order at a boundary (amended 2026-09-23, review round 1). A slot save due at Ready(k)
completes -- its Environment.SaveSlot reply in hand -- before any State.Capture or FLYSIM01
export at Ready(k), whether that export is periodic, a milestone archive or the post-rollback
save. So a checkpoint whose ratchet ledger names best = r always carries the slot saved for
rung r. This is a declared difference from the legacy loop, which archives a milestone
before capturing the ratchet snapshot. A legacy milestone archive holds best = r-1 and the
rung r-1 snapshot; a ported one holds best = r and the rung r snapshot. In practice the
two converge: after fly-reset-to-milestone onto a legacy archive, the ratchet captures rung
r again on the first safe frame (the rung is above the recorded best) and resets the attempt
counter. The difference is only where the rung r slot sits -- on the exact frame of the climb
(ported) or on the first safe frame after the reset (legacy) -- and it is visible only if the
fly stalls or hits game over before any safe frame, when legacy falls back to rung r-1's save
and the ported loop to rung r's (amended 2026-09-23, review round 2). The operator's confirmation of this difference is requested with the
CUT-01 shadow run. The rule is machine-checked in the step trace: TraceBehaviour.boundaryActions
records the saves and the rollback in order, TraceOperational.captures records each capture
with the number of boundary actions before it, and a TransitionTrace in which a capture
precedes a slot save is refused (step-v1 section 8 amendment; fixtures in
valid.json and invalid.json).
FLYSIM01 stays the format of record until RETIRE-01. Every durable save of this composition
(periodic, milestone archive, after a rollback) exports a FLYSIM01 envelope that the
current flysim reads, under the unchanged compatibility string. The deploy gate
(--print-compatibility) and fly-reset-to-milestone keep working on those files. A FLYSESS1
checkpoint may be written beside it, but it is not what a restore selects until RETIRE-01
says so.
17. PROF-02b: MaleCNS bundles (later)
Dataset manifests, original-ID mapping, anatomical roles, sensory and readout bindings, strict graph validation for new bundles, and the composite behaviour identity of FOUNDATION-02 are not in this document. They ship with PROF-02b, before DATA-01, as their own contract. Nothing here constrains them, except that a new profile never reuses this profile's id or its legacy exception.