docs: review round 1 on the port contracts

B1: a slot save due at a boundary completes before any State.Capture or FLYSIM01 export there;
the ported milestone archive holds the post-capture ratchet and slot where legacy holds the
pre-capture ones, declared in legacy-gameboy-v1 sections 4 and 16 and step-v1 sections 3 and 6.
N1 the exact restore transient (blocked window from 0 ms, restored winner reported blocked on the
first decode); N2 the decoder-config form; N3 worker status and lost-reply resolution for the
extension methods; N4 boundary actions and captures in the step-v1 section 8 trace; N5 a warm-up
override is another profile; N6 sugar refused until the first commit after a restore.
This commit is contained in:
acamilo 2026-09-23 09:00:12 +00:00
parent 2f8ad4914a
commit 3c4bb30bed
2 changed files with 110 additions and 22 deletions

View file

@ -54,7 +54,7 @@ bytes. The current values are in `fixtures/gameboy-legacy.json`: digest `41e5d1a
| `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` | | `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 | | `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 | | `tickDuration` | `1000000/1` ns | One model tick |
| `warmupMs` | `2500` | Fresh-start warm-up with learning disabled, `DEFAULT_WARMUP_MS` | | `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 | | `view` | `lcd`, 160 x 144 | The retina's native frame |
| `supportedStimuli` | `["reward-pulse"]` | Sugar and task reward events both drive `stimulate(durationMs)` | | `supportedStimuli` | `["reward-pulse"]` | Sugar and task reward events both drive `stimulate(durationMs)` |
| `readoutContextSchema`, `decisionSchema` | The registered references of sections 5 and 6 | | | `readoutContextSchema`, `decisionSchema` | The registered references of sections 5 and 6 | |
@ -97,7 +97,16 @@ The legacy `Sim::step_frame` order maps onto the transaction phases one to one:
| `stimulate` per event, `reinforce(sum)` | Phase D: `Agent.Commit` installs the input, then the stimulations in event order, then one reinforcement | | `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` | | `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) | | 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) | | 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 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 hands `set_visual_frame` before it samples rewards. Rewards are sampled from the frame just
@ -202,7 +211,8 @@ interface GameboyMemoryInspection {
replaces the slot. A restore imports the state, releases the buttons and returns the 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. 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 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. FLYSIM01's `ratchet_game` and `ratchet_frame` are today. A slot save due at a boundary
completes before any `State.Capture` or FLYSIM01 export at that boundary (section 16).
## 10. Executor `pokered-macros-v1` ## 10. Executor `pokered-macros-v1`
@ -249,7 +259,21 @@ before the next Prepare, without pausing**:
tick**, no reinforcement, no stimulation and no calibration. 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 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. group restores from the last durable checkpoint. No participant resets alone.
6. A durable save follows, as the legacy loop checkpoints after a recovery. 6. A durable save follows, as the legacy loop checkpoints after a recovery. Any capture at this
boundary -- before or after the rollback -- is taken after step 1's slot save.
**Worker status and lost replies (amended 2026-09-23, review round 1).** While it executes
`Environment.SaveSlot` a worker's `Worker.Status` reports `capturing`; while it executes
`Environment.RestoreSlot` or `Agent.Rollback` it reports `restoring`, with `currentScope` still
the prior `(e, k+1)`. After the reply it reports `ready` at `(e', k+1)`. These are mutations
under the [session RPC](ipc-v1.md) section 5 operation key `(session, e', k+1, method, worker)`
(`SaveSlot`: `(session, e, k+1, …)`), so a lost reply goes through ipc-v1 section 6 first: stop
dispatch, query `Worker.Status` or retransmit the same request id and body to the same
incarnation, and resolve only the matching cached result. Only an unresolvable outcome -- a
changed incarnation, lost routes or ownership, `RESULT_EXPIRED` -- fails the epoch, and then the
group restores from the last durable checkpoint. No participant is ever left in `e'` while
another continues in `e`: the coordinator issues no Prepare until every rollback reply is
resolved.
The following continue through the rollback: brain clock, membrane, RNG, learned gains, rates and 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 reward history, the ratchet ledger (its attempt and lifetime budgets were spent in Phase C), and
@ -266,7 +290,7 @@ interface LegacyGameboyComposition {
profile: AssetRef; // the section 2 document profile: AssetRef; // the section 2 document
executor: { id: "pokered-macros-v1"; rom: AssetRef; adapter: Id; symbolProvenance: string; executor: { id: "pokered-macros-v1"; rom: AssetRef; adapter: Id; symbolProvenance: string;
mode: "raw" | "macros"; macroChannels: ChannelName[] }; // [] iff raw mode: "raw" | "macros"; macroChannels: ChannelName[] }; // [] iff raw
decoderConfigDigest: Digest; // SHA-256 of the canonical effective DecoderConfig decoderConfigDigest: Digest; // SHA-256 of the gameboy-decoder-config-v1 form
environment: { extensions: ["gameboy-slots-v1"]; slots: Id[]; stepDuration: RationalNs; environment: { extensions: ["gameboy-slots-v1"]; slots: Id[]; stepDuration: RationalNs;
inspectionSchema: SchemaRef; controllerSchema: SchemaRef; setupFrames: 1; inspectionSchema: SchemaRef; controllerSchema: SchemaRef; setupFrames: 1;
audio: { sampleRate: number; channels: 2 } }; audio: { sampleRate: number; channels: 2 } };
@ -277,8 +301,18 @@ interface LegacyGameboyComposition {
} }
``` ```
- `decoderConfigDigest` is taken over the canonical JSON of the effective `DecoderConfig` in the - `decoderConfigDigest` is the SHA-256 of the canonical JSON of the form
TypeScript oracle's shape (the Game Boy preset with its macro group). A change to a decoder `gameboy-decoder-config-v1` of the effective decoder configuration (amended 2026-09-23,
review round 1): `{form, exclusive, macros, pulses, clearLockoutMs}`, each group
`{channels: [{channel, role}], decisionMs, holdMs, hysteresis, fatigueGain, fatigueDecay,
blockedFatigue, blockedMs}` or `null`, each pulse `{channel, role, holdMs, cooldownMs,
threshold, boot: {cooldownMs, threshold} | null, throttleGroup: string | null}`. Channels are
an array because their order breaks argmax ties and canonical JSON sorts object keys. The
shared vectors are `fixtures/gameboy-decoder-config.json` (raw mode and the 31-channel
Pokémon Red group): `flysim`'s `legacy_profile_identity` test computes them from
`gameboy_decoder_config_with_macros` (and rewrites them under `FLY_UPDATE_FIXTURES=1`), and
`@flybrain/session-types` reproduces them from the oracle's `gameboyDecoderConfig`. The
example composition carries the real macros-mode digest. A change to a decoder
timing, a threshold or the macro channel set therefore changes the composition digest. 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. 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 - The declaration's digest is the SHA-256 of its canonical JSON. The coordinator's
@ -299,6 +333,8 @@ interface LegacyGameboyComposition {
| `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/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 | | `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-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 | | `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 | | `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 |
@ -324,8 +360,16 @@ counter, remainder, buttons and event watermark. The rest starts cleared, as it
process: process:
- the executor's ledgers (blocked, reached, talked, errand) are empty, with no macro running; - 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 - the agent's readout transient starts exactly as a fresh legacy process has it (amended
observed location starts the blocked window); 2026-09-23, review round 1): no held channel, no last location, and the blocked window
starting at brain time **0 ms**, not at the restored clock. The decoder state itself
(`DecoderState`: holds, winners, fatigue) *is* restored. The consequence AGENT-01 must
reproduce: on the first decode after a restore, `now - 0 >= blockedMs`, so the restored
direction winner, if any, is passed as `blocked` and its fatigue is raised to
`blockedFatigue`. Only after that decode does the window restart, because the held channel
changed from none to the winner, and again when the first location is observed. A rollback
(section 11) differs: it clears the holds and winners and restarts the window at the
current brain time;
- the adapter's transient observations are cleared, as they are on a rollback. - 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 Resume tests compare against the legacy restore outcome, not against an uninterrupted trace
@ -336,20 +380,44 @@ the rung.
## 15. Sugar admission ## 15. Sugar admission
The coordinator owns admission, with the legacy rules: the per-minute limiter, and "no overlap 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 with an active pulse". The pulse is read from `AgentTelemetry.stimulusRemainingMs` of the **last
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 completed commit** (an `Agent.Rollback` reply counts as one). The value can therefore be one
pulse that ended inside the in-flight transition reads as still running, and the request is refused and retried. commit old, and the operator accepted this lag. The consequences, each bounded to one frame:
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 - a pulse that ended inside the in-flight transition still reads as running, so the request is
refuses with a retry. The duration is clamped to `[1, sugar_max_ms]`. An admitted sugar is a refused and retried;
`reward-pulse` `Stimulus` in the next Prepare's `preStepStimulations`, which is the position of the legacy - a reward pulse added by the in-flight transition is not yet visible, so a sugar can be
drain at the top of a frame. Legacy admission *is* application. Here, the epoch can fail between the two, so each admitted over it where the legacy loop would have refused;
admission record carries its interaction id. If the Prepare that applies it never commits, the - after a restore, and until the first commit of the new epoch, the pulse is unknown and every
admission is reported aborted and the edge refunds it. The bridge's fulfil path and refund path both get tests request is refused with a retry, where the legacy loop admits against the restored pulse at
in the slice that wires them ([workers-v1](workers-v1.md) section 5 amendment). once (amended 2026-09-23, review round 1).
The duration is clamped to `[1, sugar_max_ms]`. An admitted sugar is a `reward-pulse`
`Stimulus` in the next Prepare's `preStepStimulations`, which is the position of the legacy
drain at the top of a frame. Legacy admission *is* application. Here, the epoch can fail
between the two, so each admission record carries its interaction id. If the Prepare that
applies it never commits, the admission is reported aborted and the edge refunds it. The
bridge's fulfil path and refund path both get tests in the slice that wires them
([workers-v1](workers-v1.md) section 5 amendment).
## 16. Checkpoint format of record ## 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. After
`fly-reset-to-milestone` onto a ported archive, a stall rollback therefore returns to the
milestone boundary itself rather than to the previous rung's save, and the attempt counter
starts at the new rung. The operator's confirmation of this difference is requested with the
CUT-01 shadow run. The rule is machine-checked in the step trace: `TraceBehaviour.boundaryActions`
records the saves and the rollback in order, `TraceOperational.captures` records each capture
with the number of boundary actions before it, and a `TransitionTrace` in which a capture
precedes a slot save is refused ([step-v1](step-v1.md) section 8 amendment; fixtures in
`valid.json` and `invalid.json`).
FLYSIM01 stays the format of record until RETIRE-01. Every durable save of this composition 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 (periodic, milestone archive, after a rollback) **exports a FLYSIM01 envelope** that the
current `flysim` reads, under the unchanged compatibility string. The deploy gate current `flysim` reads, under the unchanged compatibility string. The deploy gate

View file

@ -126,7 +126,9 @@ this transition. No task output directly writes controllers or neural state.
boundary this transition reaches, after Phase D, never inside it: a slot save boundary this transition reaches, after Phase D, never inside it: a slot save
(`Environment.SaveSlot`, composition capability `gameboy-slots-v1`) and a rollback (`Environment.SaveSlot`, composition capability `gameboy-slots-v1`) and a rollback
(`episodeRequest.kind = "rollback"`). Both are recorded with the transition's result and (`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 applied by the coordinator in the order *save, then rollback* (section 6 amendment). A slot
save due at a boundary completes before any `State.Capture` or FLYSIM01 export at that boundary
(amended 2026-09-23, review round 1; [legacy-gameboy-v1](legacy-gameboy-v1.md) section 16). The
retained old inspection is what makes "evaluate once against old/new inspection" possible when 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 the inspection is artifact-backed: the coordinator keeps `O[k]`'s image until this evaluation
finishes. finishes.
@ -256,7 +258,12 @@ every Commit of the transition that reached `k` has succeeded:
running action and observes `O'[k]`, which yields the next decision contexts. 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 4. `Agent.Rollback(scope e',k; priorEpoch e)` on every agent concurrently: holds and
eligibility cleared, `O'[k]`'s view installed, no tick. eligibility cleared, `O'[k]`'s view installed, no tick.
5. With every reply in hand: `Ready(e', k)`, then the usual durable save. 5. With every reply in hand: `Ready(e', k)`, then the usual durable save. Every capture at
this boundary, before or after the rollback, follows step 1.
A worker reports `capturing` during `SaveSlot` and `restoring` during `RestoreSlot` or
`Agent.Rollback`. A lost reply is resolved by [session RPC](ipc-v1.md) section 6 against the
same operation key before anything fails the epoch (legacy-gameboy-v1 section 11).
Exactly what is retained, cleared and installed is named by the policy, as this section already 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 requires; nothing is reset by a worker on its own initiative, and a failure at any step fails
@ -299,6 +306,19 @@ The synthetic integration test must record, for every transition:
- Observation producing boundaries and task event/outcome IDs in order. - Observation producing boundaries and task event/outcome IDs in order.
- All Commit acknowledgments and published committed boundary. - All Commit acknowledgments and published committed boundary.
**Amendment, 2026-09-23 (RT-01a, review round 1).** For a composition with boundary actions the
record also carries, for the boundary the transition reached:
- in behaviour, `boundaryActions`: every `Environment.SaveSlot` (slot id and saved state
digest) and a rollback (slot id), in the order applied -- saves first, each slot once, at most
one rollback, last. FND-01's harness compares it like any other behaviour field;
- in operational metadata, `captures`: every checkpoint capture or FLYSIM01 export at that
boundary, in the order taken, each with the number of boundary actions already applied.
Captures are operational because their schedule is wall-clock policy.
A trace whose capture precedes one of the boundary's slot saves, or counts more actions than
were applied, is refused. The synthetic composition records both lists empty.
Evaluate agents sequentially, concurrently, and in reversed dispatch/completion order. All Evaluate agents sequentially, concurrently, and in reversed dispatch/completion order. All
committed state/action/reward results must match, excluding wall time, request IDs and other committed state/action/reward results must match, excluding wall time, request IDs and other
explicitly operational metadata. Delayed/lost/duplicate messages must not add a neural tick, explicitly operational metadata. Delayed/lost/duplicate messages must not add a neural tick,