# Hunting REMAINING OB by correlation — method works, run did not finish Status: ✅ the correlation method is sound and demonstrated; 🔴 the hunt is **unfinished**; 🔴 two self-inflicted defects, one a repeat. ## The method [mission-phase-objectives.md](mission-phase-objectives.md) settles what the counter is: Stage 02 phase 1 asks to *"shoot down all invading enemy fighters"* and the hints say red **[OB]** markers indicate the targets. So `REMAINING OB` must fall when a marked fighter dies — and the per-record craft strength already says exactly when that happens, for a *named* unit. So instead of scanning for a value, intersect: keep every word in the 32 MB heap that fell by the same amount, in the same interval, as an `e010` loss. Each kill event should cut the survivor set hard. `tools/re-capture/ob_probe2.py`. ## ✅ It works — one event cut 8 million words to 1056 ``` t= 219s e010 losses=2 (all=4) words falling by 2: 1056 -> candidates 1056 ``` From ~8 M candidate words to **1056** on a single event. Two or three more should leave a handful. ## 🔴 The run did not finish, and the candidates were lost The turn's timeout fired at t = 219 s, and the probe saved its candidate set **only at the end** — so the 1056 were discarded. The follow-up attach then started from nothing. **This is the same mistake already recorded in [guest-stalls.md](guest-stalls.md)**: "the first attempt deferred all analysis to the end of the run, and the turn's timeout killed it with 240 s of collected data still in memory and nothing written." I wrote that lesson down and then repeated it in a new script four iterations later. Fixed: candidates are now written to `/tmp/ob_candidates.json` **after every event**, and `SYLPH_OB_RESUME=1` loads them so a chained attach keeps intersecting on the same mission. ## 🔴 The attach could not tell a quiet mission from a frozen one The follow-up attach logged **535 s with zero losses of any kind**. That is exactly what a freeze looks like, and `ob_probe2` had no stall witness, so the run cannot say which it was. Also fixed — the witness from `wave7_probe` is now carried here. Two defects in one iteration, both of them things this corpus had already learned. The pattern is that each new probe starts from scratch and re-earns the same lessons; the fix that would actually stick is a shared probe harness rather than a family of one-off scripts. ## What is still open The hunt itself. The method is demonstrated but no address is identified. What it needs is a run that catches **two or three** `e010` kill events, which is the same combat-effectiveness limit recorded in [mission-objectives-text.md](mission-objectives-text.md) — the preference knob gets about two marked-fighter kills per five minutes against a dozen turrets. --- # Second attempt: saving works, the filter was wrong (2026-08-24) ## ✅ Incremental saving works A fresh mission caught one `e010` event at t = 241 s and **wrote 1187 candidates to disk immediately**. The turn timeout then fired, exactly as before — but this time the data survived it. The fix from the previous iteration is verified. The session also now clears `/tmp/ob_candidates.json` at launch: candidate *offsets* are only meaningful within one emulator instance, so resuming across launches would intersect unrelated addresses. ## 🔴 The correlation had no value filter, and the survivors were floats The 1187 survivors are not counters: ``` va 0xbd0a42b8 value 1044450858 (~0.1f) va 0xbd140bc0 value 3200164558 (a negative float) va 0xbd14a120 value 3212461993 ``` The filter matched on the **delta** alone, so any two float bit patterns whose integer representations differ by exactly `lost` qualified. In a 32 MB heap full of positions and velocities that is thousands of words. Fixed: candidates must also *look like a counter* — a small non-negative integer (`0 ≤ v < 1000`) in both samples. That removes float noise by construction rather than hoping the intersection washes it out. ## 🔴 The attach was frozen, and the witness said so The follow-up attach logged **0 events across 520 s**, which reads like the combat-effectiveness limit again. It was not: **25 of its 26 samples were flagged `GUEST STALLED`**. The guest was frozen for essentially the whole window. The witness added last iteration did its job. The lesson is about reading it — the run summary quoted "0 events" first and the stall count only turned up on a deliberate check. **A run's witness result should be the first thing looked at, before any interpretation of what the run "showed".** ## Still unfinished No address identified. What is needed is unchanged — two or three `e010` kill events in *non-stalled* samples — and the two obstacles are now clearly separate: the freeze rate, and the pilot managing roughly two marked-fighter kills per five minutes. --- # ✅ The method narrows hard — and refutes turret tracking (2026-08-24) With the value filter in place, an attach watching **turret** losses (which are frequent, unlike marked-fighter kills): ``` t= 45s e007 losses=2 words falling by 2: 374 -> candidates 374 t=134s e007 losses=4 words falling by 4: 1001 -> candidates 2 t=156s e007 losses=2 words falling by 2: 526 -> candidates 0 ``` **374 → 2 → 0.** That is exactly the behaviour a correlation search should show, and it ends in a refutation rather than fizzling out: > **No plain `u32` in `0xBD000000–0xBE000000` decrements consistently with turret > kills.** Witness first, as the rule now says: 12 of 25 samples stalled, but **all four kill events fall in the early non-stalled stretch** (t = 45–177 s), so the events themselves are sound. ## 🟡 The negative fits the objective text This is what the objective text predicts. Stage 02 phase 1 asks to *"shoot down all invading enemy **fighters**"*, and turrets are not fighters — so a counter of remaining marked targets **should not** move when a turret dies. The refutation is evidence *for* the reading in [mission-phase-objectives.md](mission-phase-objectives.md), not against it. It also rules out the cheaper alternative reading, that `REMAINING OB` is a general kill or enemy-remaining tally. It is not: it ignored ten turret deaths. ## What this leaves The method is proven and the search region is right — `0xbdb59668`, where the counter was originally seen, is inside the scanned range. What is missing is still **two or three `e010` kill events in non-stalled samples**, and the same two obstacles: the freeze rate, and the pilot's ~2 marked-fighter kills per five minutes. One clean 220 s run this iteration produced **zero** `e010` kills, which is the limit stated plainly. --- # ✅ A kill-free route via the HUD — and it refutes the u32 assumption (2026-08-24) The correlation route is gated on marked-fighter kills, which the pilot gets at about two per five minutes. But `ob_read.py` already reads the counter off the screen, so the value can be matched against memory directly — no kills needed. `tools/re-capture/ob_by_hud.py`: screenshot → read the digits → keep heap words equal to that value → intersect across readings. ``` t= 7s HUD=4 words==4: 6156 -> candidates 6156 t= 42s HUD=4 words==4: 6256 -> candidates 5153 t= 75s HUD=4 words==4: 6327 -> candidates 4620 t=108s HUD=4 words==4: 6451 -> candidates 4312 t=142s .. t=312s HUD unreadable ('00?', '??1', '???') t=347s HUD=11 words==11: 1052 -> candidates 0 ``` ## 🔴 Refuted: the counter is not a plain big-endian u32 in the entity heap Four readings at value 4 narrowed 6156 → 4312 — the expected slow drift. Then the HUD read **11**, and the intersection collapsed to **zero**. A word that genuinely holds this counter must equal 4 at the first four samples *and* 11 at the last. None does. So within `0xBD000000–0xBE000000`, read as big-endian `u32`, **the counter does not exist**. It may be `u16`, `u8`, little-endian, or simply outside that region. That is worth having: both hunts so far assumed BE-`u32` in the entity heap, and that assumption is now eliminated rather than merely unproductive. ## 🟡 The displayed value went UP, 4 → 11 Over ~340 s the counter *increased*. A pure countdown of remaining marked targets should not do that — unless targets were added, which the deployment work says does not happen for phase 1 ([mission-phase-deployment.md](mission-phase-deployment.md)). Possible readings, none tested: the cell being read is not `REMAINING OB`; the digits are misread (the template strip only covers **0 1 2 4 8**, so 3/5/6/7/9 come back as `?` — the many unreadable samples above); or the counter genuinely counts something that can rise. The two clean readings scored 0.95–0.98 against their templates, so a misread of those specific frames is unlikely — but "4" and "11" use only digits that *are* in the strip, which is exactly the selection effect that would hide a wrong reading. ## Next Widen the encoding: search `u16` and `u8`, little-endian as well as big, and beyond the entity heap. That is a change to one scan function, and unlike the kill-driven route it costs no combat. Also worth extending `ob_digits.png` with the missing digits — most samples were unreadable, which is why only two data points survived a 480 s run.