Review round 1, B1. The p99 sleep-overshoot bound measured the OS scheduler
and the published*2 bound failed whenever a starved debug publisher
coalesced, which it is designed to do, so the workspace gate went red on a
loaded box. The three gated tests keep what the slice claims: pacer lag 0,
no watch send held by a consumer (50 ms bound), no refused publication,
something reaches the bus, a bounded store, and a healthy client that reaches
the newest snapshot. The overshoot and throughput bounds are in
the_three_scenarios_keep_their_rates, #[ignore]d.
The two pending decisions, taken for the feed with EDGE-01 and dated:
sizing from the measured 122,367-byte snapshot (the frame is 92,160 bytes,
not the 1.2 MB the list assumed) with the worst case of seven stuck latest
subscribers at about 3 MB inside a 32 MiB tmpfs store; and the lifecycle,
flysim owning the router under /run/fly/bus and starting first, the edge
After= and Requires= it and reconnecting by itself across a crash. Plus the
design as built, the tour's pointer, the example config and the flybus
README's no-longer-true line.
Shutting the router down first raced the last publish and logged a refusal
on every clean stop. The publisher ends on its own when the watch sender goes;
the edge sees the socket close either way.
flyedge.service runs /opt/fly/current/fly-edge After= and Requires=
flysim.service, with its metrics on loopback :9102 and a
ConditionPathExists so a release without the binary leaves it inactive. It is
in no target and 07-enable.sh does not enable it; the header has the switch
and the way back.
build-flysim.sh also builds fly-edge beside the flysim binary and
package-release.sh ships it when present. 05-deploy.sh writes
FLY_FEED_VIA (default direct) into fly.env, flysim.service names
FLY_BUS_DIR=/run/fly/bus and tmpfiles creates it. Watchdog check 2 reads the
feed counters from whoever serves the feed: flyedge when fly.env says bus.
lint.sh holds all of that, and drives check 2's choice against a fixture.
parity.rs replays the four committed stage fixtures whose headers the Rust
producer can read (macros, shop, center, bigpad; 400 snapshots each, all of
them with FLY_EDGE_PARITY_ALL=1) into one watch slot served both ways at once,
recorded by a stage, a bridge and a frame-only client per path: headers equal
without wall times, attachments byte-equal and equal to the fixture's, and
the whole messages byte-equal. FLY_EDGE_PARITY_OUT writes the recordings as
.flyfeed files. Also: a header past the envelope limit, and the edge dropping
its clients, unbinding and coming back across a router restart.
stall.rs runs flysim's Pacer at 60 Hz publishing full-size snapshots at 30 Hz
against three stages that stopped reading, a bus subscriber that hoards every
delivery, and no subscriber at all: pacer lag 0, no slow watch send, no
refused publication, a bounded store, and a healthy client that stays current.
A new workspace binary. It reads flysim's own configuration (same env file,
same FLY_FEED_BIND, FLY_BUS_DIR and idle cadence), subscribes to
fly.feed.snapshots as fly-edge with one latest slot and one delivery in
flight, turns each publication back into a Snapshot with feedbus::receive and
serves it through flysim's feed::router, so hello, wants, drop-oldest and the
2 Hz idle header are flysim's code and the bytes are flysim's bytes.
The port is bound only once the first snapshot has arrived, and when the bus
goes away every client is dropped and the port unbound, which is what a
stopped flysim looks like to the stage; then it reconnects every 500 ms.
FLY_EDGE_METRICS_ADDR serves /metrics (fly_frames_sent_total and
fly_feed_clients under their flysim names, plus fly_edge_*) and /healthz.
feed.via (FLY_FEED_VIA, default direct) and feed.bus_dir (FLY_BUS_DIR,
default /run/fly/bus). In bus mode flysim does not bind the feed port: it
starts a router on its own two-thread runtime with a closed policy (flysim
may publish fly.feed.snapshots, fly-edge may only subscribe to it), listens
for the edge on <bus_dir>/edge.sock, and a publisher task copies each snapshot
out of the watch slot onto the latest-retained topic: frame, audio and spikes
as sealed artifacts, the header as the envelope payload, or as a header
artifact past 48 KiB. The sim thread still only writes its watch slot, so a
slow bus costs snapshots on the bus and never a frame of the loop.
feedbus holds both halves of the encoding, publish and receive, and the
limits sized for the real 122,367-byte snapshot. Two counters are new:
fly_bus_published_total and fly_bus_publish_failures_total.
feed::router needed the whole AppState for three things: the watch slot, the
feed counters and the idle cadence. It now takes exactly those, and Shared's
Metrics sits behind an Arc so the counters can be handed over. Nothing about
what the feed writes changes.
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.
Review round 1, N2. The gameboy-decoder-config-v1 form (channels as ordered arrays), vectors for
raw mode and the Pokemon Red macro group computed by flysim's legacy_profile_identity test from
gameboy_decoder_config_with_macros and reproduced by @flybrain/session-types from the oracle's
gameboyDecoderConfig. The example composition now carries the real macros-mode digest and
channel set.
Review round 1, B1 and N4. TraceBehaviour.boundaryActions records the slot saves and the rollback
at the reached boundary in application order; TraceOperational.captures records each capture with
the number of boundary actions before it; TransitionTrace refuses a capture taken before the
boundary's slot saves. Both languages, fixtures for the rule, synthetic coordinator records both
lists empty. contractDigest moves to a56e25e6.
The operator decided on 2026-09-23 to port the live fly onto the session framework in full.
New contract legacy-gameboy-v1: the profile gameboy-legacy-fafb-v783-v1, the proof that the
legacy f64 frame clock equals the rational one, the step-by-step placement of step_frame in
lockstep-v1, the readout context (location allowed and declared), the channels decision, the
memory-image inspection and ROM AssetRef, the environment (one no-button setup frame, u8->f32
audio, DC blocker at the edge, gameboy-slots-v1), the pokered-macros-v1 executor as one object
with its task, the legacy-ratchet-rollback-v1 policy, the composition digest carrying decoder
and macro-channel configuration, legacy-transient-reset restore semantics, sugar admission with
a one-commit lag, and FLYSIM01 as format of record until RETIRE-01. PROF-02b is a stub.
Dated amendments, each citing the decision, where earlier text kept the legacy loop outside
lockstep or had no place for it: workers-v1 (telemetry, Initialize, executor, episode request
kind, admission, new section 7 extension methods), step-v1 (rollback edge, Phase B/C, clock,
episode policy, sugar lag), state-media-v1 (audio, memory-image retention, restore semantics,
format of record, section 7 ratchet), README section 4, implementation.md (AGENT-01 and ENV-01
unblocked), the MaleCNS backlog (FOUNDATION-02 split, RUNTIME-01 contract) and analysis (5.2,
5.4), and readout.md (where the location comes from).
PROF-02a and RT-01a, machine-readable half, per the operator's port decisions of 2026-09-23.
Generic (in the session schema set, so contractDigest moves):
- AgentTelemetry.stimulusRemainingMs (number|null): sugar admission reads the pulse from the
last commit.
- EpisodeRequest.kind is terminal | rollback.
- Environment.SaveSlot / Environment.RestoreSlot (capability gameboy-slots-v1) and
Agent.Rollback (capability legacy-ratchet-rollback-v1) payloads, with their scope checks;
maxSlots 4.
Legacy Game Boy (module gameboy, digested apart from contractDigest):
- registered payload schemas gameboy-readout-context-v1, gameboy-channels-v1,
gameboy-joypad-v1, gameboy-memory-inspection-v1, legacy-ratchet-rollback-v1, each SchemaRef
digest over its canonical declaration;
- the one legacy profile gameboy-legacy-fafb-v783-v1 embedding today's schema-1 fingerprint,
lif-1ms-f64-v2 and fly-kc-mbon-rstdp-v2, with its AssetRef digest;
- the composition declaration carrying the decoder and macro-channel configuration, the
executor pokered-macros-v1, gameboy-slots-v1, legacy-ratchet-rollback-v1,
legacy-transient-reset and FLYSIM01 as format of record, cross-checked against the FLYSIM01
compatibility string.
Fixtures regenerated by update_fixtures (new derived gameboy-legacy.json); valid/invalid cases
for every new type in both languages; the frame clock proven identical to the legacy f64
accumulator. flysim gains only a test (and a dev-dependency) that recomputes the pinned
fingerprint, versions, frame size, warm-up, clock and button order. The synthetic fly-session
agent reports stimulusRemainingMs null and its task names kind terminal; no runtime change.
Measured with the real brain on the seeded pocket: it pressed GO ROUTE
first, while the gym's door was still the second tier's answer. That
refusal wrote the door to the blocked ledger and also withheld GO ROUTE
on the tile, so the next deal -- the last resort, whose walk now goes
where it can -- was never made, and the fly waited out the window.
Only a last resort deals goals the ledger is already resting, so the
refusal worth remembering where the fly stands is the one that taught
the ledger nothing new. A refusal that writes a new exclusion changes
the next deal by itself.
Row 57's pocket had a way out: the road east, three tiles from the fly
and resting in the blocked window. The last resort ignores that window
but narrows to the ways toward the objective, and the only one was the
gym's door beyond the fence, so the route search refused and the road
was never tried. With the refusal withheld the pad went empty and the
fly waited out the road's window, ten brain minutes.
The narrowing is a preference and the dealer cannot search. When start's
route search cannot reach the preferred ways, it tries the rest of the
last resort, nearest reachable first, exactly as every walk chooses. The
pad is unchanged; only where the pressed macro walks.
The ROM proof now asserts the fly leaves the pocket inside a brain
minute: frame 517 (0.14 minutes), against 36,325 (10.1) on the base.
The seeding moves from scene_probe into examples/support/ledgers.rs and
trap_hunt reads it as FLY_TRAP_SEED_*, so both arms of a hunt can start
inside the state the live session was in: row 57's pad was dealt by
ledgers a restore starts empty, and a hunt from the bare checkpoint
never meets it. The report gains refusals by macro and the longest run
of one macro refused with the fly on one tile.
scene_probe's own choice seed is FLY_PROBE_RNG now, so it does not read
as one of the FLY_PROBE_SEED_* ledgers.
Row 57's pad was one button refused every hold for two hours: one start
and one name in ten brain minutes, so neither the sequence rule nor the
dominance rule could fire, and the coordinator's own watcher saw it
first. Check 10 now reads every macro outcome in the window. The fly's
decisions are its starts and its refusals; `stalled` flags 90% of 20+
decisions ending refused, blocked or timed out, and `zero-progress`
flags decisions with no `done` among them on two probes in a row. Both
sit behind the same no-new-ground gate as the old rules.
fly_loop_refused, fly_loop_blocked and fly_loop_done are exported, and
loop.json carries window.decisions and window.outcomes. It still never
acts: the lint fixture asserts no unit is restarted across six cases,
two of them new (the row-57 log, and a zero-progress log).
the_pewter_east_pad_is_never_one_dead_button_from_the_rung_ten_checkpoint,
gated on FLY_ROM and FLY_PEWTER_EAST_CHECKPOINT. Part one, from the
ratchet's rollback snapshot: GO ROUTE walks east, meets the youngster,
and the pushed ledger walls a tile he fires on, not the south entrance
the walk set out from. Part two, from the live frame with the pocket's
session ledgers rebuilt: twelve brain minutes, no button refused twice
running on one tile, and the fly leaves the pocket. On the base the same
run refuses GO ROUTE 746 holds running on one tile.
Row 57, live on rung 10 for two hours: the Pewter City pad was GO ROUTE
and nothing else, refused `no route` every 800 brain ms, no button
pressed. The dealer asks the cheap question and start asks the real
one; the blocked ledger closes the gap for every list except a last
resort, which ignores that ledger by design. So GO ROUTE's refusal,
which wrote the gym door to the ledger, could not take the button off
the pad, and re-stamped the door's window every hold, which kept GO
OBJECTIVE's only goal excluded for ever.
A `no route` or `precondition` refusal is now recorded with the tile
the fly stood on, and the dealer does not deal that button from that
tile for the blocked window. It is dealt again the moment the fly
stands anywhere else or the window closes. Nothing presses; a pad with
nothing runnable is empty and the fly waits.
Row 57. The pushed ledger (row 37, no window) recorded the tile a macro
set out from when the cartridge moved the fly. For a TALK that is the
tile the script fired on; for a walk it can be the far side of the map.
Pewter City's youngster takes the joypad on four tiles by the road east,
GO ROUTE aims east at Route 3 every time, and one walk from the town's
south entrance walled the south entrance, twenty-six tiles from the
script. Walks start wherever the last one ended, so the walls fenced the
fly into a pocket no route could leave.
A walk now records the tile it last stood the fly on, which is where the
cartridge took over. Every other macro keeps the tile it started on.
FLY_PROBE_CATCH=route drives the real PokemonPalette from a checkpoint,
one uniform choice per hold, prints every pad it deals and every refusal
with its reason, and reads the frame the pad comes down to one refusing
button on: which list emptied why, the no-window ledgers, and whether the
route search reaches each way out. FLY_PROBE_SEED_* rebuild the session
ledgers a restore starts empty; FLY_PROBE_RATCHET starts from the
ratchet's rollback snapshot; FLY_PROBE_HOLD presses raw directions first.
PokemonPalette gains inspect(), fences() and a doc(hidden) ledgers_mut()
for the survey. The loop never calls them.
Row 41 counted `YES` starts per map, which cannot tell one conversation from
another; the Pewter Gym stall was 64 `YES` and 62 `NO` in ten brain minutes at
one person. So the harness counts an answer to a box it can read as a choice,
charged to whatever the fly is facing -- `palette::facing_target`, which is
`TALK`'s own precondition and the same reading its ledger entry uses.
`the_fly_leaves_the_pewter_gym_guides_ring_from_the_rung_ten_checkpoint`: the
fly leaves map 54 on frame 1,337 against never in twenty brain minutes, spends
27 macros in the gym (one lap of the ring), answers the guide's prompt **once**
against a bound of four, re-opens the ring **never**, and deals `NEXT` on no pad
while a readable prompt is open.
The rung is printed and not asserted: neither this run nor either arm of the
trap hunt reaches rung 11 from this checkpoint, and why the fly does not walk
back through the gym door is row 12.5's ground and row 54's rather than this
row's. Named in the residuals.
The survey whole, both arms of the trap hunt, the ROM run, and the residuals --
including the two this row does not work: neither arm reaches rung 11 from this
checkpoint, and the watchdog's check 10 cannot see a loop of exactly four
distinct macros.
The other half of row 56. The Pewter Gym guide's conversation is fifty-two boxes
long and a third of the presses that walk it are `NO`, whose B advances a plain
box exactly as `NEXT`'s A does. Every one of them cleared the pending `TALK`, so
the talked ledger never learned the conversation had happened, `TALK` stayed on
the overworld pad, and an A press at him reopened the whole ring: thirty brain
minutes of scene `dialog` with no walk macro dealt.
12.4's rule -- "the fly said no, so the thing is still on offer" -- is about a
declined *offer*, and which of the two a `NO` was is decided where it can be
seen: by whether the box closes on it. So the decision moves to the frame the
text goes away, which is where the talked entry is written anyway, and the
reading is `pending_answer`: armed only by an answer to a prompt this crate can
read, alive for one hold. A declining `NO` still standing there is a `NO` the
box closed on; anything else is a conversation walked through to its end.
Row 41 read the border at (11, 6)-(19, 11) because that is where the Pokemon
Center's script puts it, and named the limit in its own residual: "Red places a
two-option menu where the script asking for it says, so a prompt drawn elsewhere
reads false and keeps the pad it had". The Pewter Gym guide draws the same menu
at (14, 7)-(19, 11) with the cursor at column 15. Surveyed over 260 presses from
the live checkpoint: the box is drawn on 10 frames, `yes_no_prompt` answered
false on all 260, and `wTextBoxID` read `TWO_OPTION_MENU` on exactly the 10.
So the pad was `NEXT, YES, NO` on a box that was a choice -- two channels for
one A press, which is 12.10's forbidden pair -- and the reopened-prompt
exclusion never armed, because it only judges an answer to a prompt this crate
can read.
The screen half is now the border drawn **around the cursor the game parked in
it**. One fact about `DisplayTwoOptionMenu` rather than about any script: the
cursor goes in the box's first interior column, so the left edge is one column
to its left, in both boxes surveyed. The top is not fixed -- the nurse's box
begins two rows above the first item and the guide's one -- so the top is found
and the figure is then read whole, as `waiting` and the move list are.
Row 56 is a gym's dialog for thirty brain minutes with YES, NO and NEXT dealt
in equal thirds, so the question is which box each press is answering and
whether the seam can see it at all. `state::drawn_boxes` asks the screen
instead of a pinned rectangle: every complete `TextBoxBorder` on the frame.
`FLY_PROBE_CATCH=dialog` walks the conversation one raw pulse at a time and
prints both readings of every frame side by side, with the pad the palette
deals for it, and tallies how the candidate readings separate the frames.
`FLY_PROBE_ANSWER` picks the button a frame with a box on it is answered with,
so the two arms of a choice can be walked separately.
Three conflicts, all of them two reviews appending in the same place, all
resolved by keeping both sides: `macros.md` (row 50 keeps section 12.18, row 55
becomes 12.19), `macros-traps.md` (both trap rows, both residual pairs, both arm
sections) and `scene_probe.rs` (both survey modes, `accept` and `shop`). Every
source file auto-merged.
The two rows compose on the cartridge: from the mart checkpoint the fly leaves
the counter, walks the town, and no `MOVE n` blocks any more, so the longest
chain of one macro blocked on an unchanged frame is 1 in any scene.
`macros.md` section 12.18 is the review; `macros-wram.md` section 7.1 corrects
the address table, where `wListMenuID` was said to be cleared by every text
display and an item's position in the stock was said to be its cursor index;
`macros-traps.md` carries row 55, its two arms and its ROM run, and two new
residuals -- `wListScrollOffset` is not a pinned address, and a mart that has
never drawn a buy list reads `Unknown` rather than `Shop`.
The press survey, twenty brain minutes before and after on both checkpoints,
and the ROM-gated forest run.
The ethos check's 'fewer flagged windows, more distinct tiles' holds on the
tiles on both arms (296 to 430, 163 to 184) and does NOT hold on the windows
(33/73 to 70/73, 68/73 to 73/73). After the fix the fly spends three quarters
of each run inside battles it is actually fighting, and the hunt's tile rule
flags a fighting fly exactly as hard as a stuck one. Reported rather than
smoothed.
take_events kept returning Option<EventBatchView> and defaulting through the
question-mark operator, so a batch missing a field read as end of stream and the
ConsumerEvents enum added in the previous round described nothing. It returns
Batch or Unreadable now, and a test publishes a batch with no droppedBefore,
asserts it is reported as unreadable naming the field, and asserts the next real
batch still reads.
The checkpoint-envelope-v1 amendment cites the rule that lets a required
manifest field land with envelopeVersion still 1 while no production file
exists.
Dropping the length check left nothing checking the reply against the
request at all: AcknowledgeResult::validate_against existed with no
caller, so a worker could acknowledge ids this session never asked
about. That is the other half of the rule the README states. A short
list is the worker reporting what it released and is accepted; an id
from outside the request is the worker reporting about someone else's
cache and is refused, named, before any mutation.
The bootstrap-path regression is now covered too. The earlier test calls
acknowledge_replies directly, which guards the check where it lives but
not where it lived, so a length check put back into acknowledge_lifecycle
left it green. The duplicate_lifecycle_acknowledge injection releases the
ids first, out of sight, so the call that method makes and checks is
already the second one -- the shape the section 6 resolution produces.
Verified by putting the old check back: three tests fail with it, none
without.
Also: last_resolution_attempts is cleared with last_resolution, so a
resolution ending before its first attempt no longer reports the
previous count; the guard half of the bound test asserts the fence like
the budget half; the attempts assertion checks a real bound rather than
u32::MAX; and two dead Instant bindings are gone.
The index an agent attested to at Agent.Initialize joins its compatibility
identity and its checkpoint manifest row, so a replacement fly that built
another graph -- the same dataset, the same neuron count, another index --
cannot install a checkpoint taken under the first one. It was accepted before,
because agent_compatibility digested a dataset digest recomputed from a free
function rather than what the worker attested to, and the restored composition
was then published under its predecessor's indexDigest.
The refusal names what the worker is, not just that two digests differ. Dated
amendment to checkpoint-envelope-v1's agents row; no wire type and no schema
text change, so the contract digest is unchanged.