challenge-mission-gate.md §5.6 attributes the 128 MB heap failure to a careless
cleared-stage mask poke, concludes that poking only real story ids does not blow
the heap, and ends by asking for the control: repeat without the poke.
Ran it. nav_to_flight.sh gains SYLPH_NO_POKE=1, which skips the write; only
Stage 1 is selectable without it, so the control changes stage too, which makes
the agreement stronger rather than weaker.
poked 0x0001FFFE Stage 02 frozen, 128 MB request refused
control untouched Stage 01 frozen, 128 MB request refused
Both logs carry not merely the same error but the same numbers:
requested 134217728 bytes, parent free 28969/131072 pages
28969 in both, across two stages and two boots. So the poke does not cause it
and neither does the stage; the guest reproducibly arrives at a 128 MB request
with ~113 MB free. An identical free-page count across independent runs also
says the allocation pattern is deterministic -- not a race, not host pressure.
The control was verified three ways, because the first attempt was confounded:
two emulators were alive at once (the previous one survived a pkill). The mask
was read back as 0x0 from the live mapping, the log was confirmed to be this
run's, and the liveness test was repeated after killing the stale process so
exactly one emulator was running -- three frames at rmse 0.00, and an 8 MB slab
of guest RAM with 0 bytes changed over 3 s.
4.6 KiB
The mission freeze is a failed 128 MB guest allocation
✅ Root-caused 2026-08-26 on a Stage 02 run. The freeze this corpus lists as "THE blocker" is not a hang in the emulator: it is the guest asking for 128 MB of physical memory, being refused, throwing, and stopping.
The freeze, measured rather than assumed
Two independent witnesses, both taken while screen_id.py was calling the
screen flight:
| three screenshots over 8 s | rmse 0.00, 0.00 % of pixels changed |
| a 4 MB slab of guest RAM re-read after 2 s | 0 bytes changed |
The emulator process is not deadlocked — 399 % CPU across 15 running threads. It is spinning while the guest does nothing.
The cause is in the emulator log, and it is specific
The log stops dead mid-stage-load, last four lines:
BaseHeap::Alloc failed to find contiguous range
PhysicalHeap::AllocRange unable to alloc physical memory in parent heap
(requested 134217728 bytes, parent free 28969/131072 pages)
MmAllocatePhysicalMemoryEx: Allocation failed: 00000000 Size: 08000000
Guest attempted to throw a C++ exception!
0x08000000 is 128 MB. The parent heap holds 131 072 pages and had 28 969
free — about 113 MB. So this is not only fragmentation, as
failed to find contiguous range suggests on its own: there was less free
memory in that heap than the request needed, contiguous or not.
Immediately before it, repeatedly:
BaseHeap::Release failed because address is not a region start
PhysicalHeap::Release failed due to parent heap failure
challenge-mission-gate.md already noted that this
failed release "leaks the range". A leak that repeats through a session gives the
mechanism the freeze's variable onset (27, 45, 83, 183 s across runs) needs:
free pages drain until some 128 MB request loses, and when that happens depends
on how much has leaked, not on the clock.
🔴 This refutes "poking only real story ids does not blow the heap"
challenge-mission-gate.md §5.6 records that poking 0xFFFFFFFF into the
cleared-stage mask blows the heap on a 128 MB request, and concludes:
Poking only real story ids (
0x0001FFFE= stages 1–16) does not blow the heap.
This run poked 0x0001FFFE — the value called safe — and hit the same 128 MB
failure, with parent free 28969/131072 against that page's 30633/131072.
✅ And the control run settles it: the poke is irrelevant
That page ends with the right experiment — "repeat without the poke to see whether
it fails the same way regardless" — so I ran it. nav_to_flight.sh gained
SYLPH_NO_POKE=1, which skips the write; only Stage 1 is selectable without it,
so the control also changes stage, which makes the agreement stronger rather than
weaker.
| run | mask | stage | outcome |
|---|---|---|---|
| poked | 0x0001FFFE |
Stage 02 | frozen; 128 MB request refused |
| control | untouched, reads 0x0 |
Stage 01 | frozen; 128 MB request refused |
Both logs carry the identical line — not merely the same error, the same numbers:
requested 134217728 bytes, parent free 28969/131072 pages
28969 in both, across two stages and two boots. The control was verified three
ways: the mask read back 0x0 from the live mapping, the log was this run's, and
the liveness test was repeated with the stale process killed so exactly one
emulator was running (three frames, rmse 0.00; an 8 MB slab, 0 bytes changed in
3 s).
🔴 So the poke does not cause it, and neither does the stage. The guest reproducibly arrives at a 128 MB physical request with ~113 MB free and throws. The identical free-page count across independent runs says the allocation pattern up to that point is deterministic — this is not a race and not memory pressure from the host.
⚠️ A frozen game passes the screen classifier
AGENT.md says to classify screens by whole-image statistics rather than named
pixels, and that is right. But a single-frame statistic cannot tell flight
from frozen in flight — screen_id.py called every frozen frame flight,
which is exactly why the entity probes returned 0 definitions, 0 movers and 0
vtable hits with no indication anything was wrong.
A second frame costs nothing and is decisive. Any probe that assumes the game is live should diff two frames a few seconds apart, or re-read a slab of guest RAM, before believing an empty result means "the thing is not there".
❔ Still open: why the guest needs a single 128 MB block, whether the heap can be given more, and whether the failure is reachable without the mask poke at all.