# 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`](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.