Ran this file's own transition filter on a guarded Stage 02 run (stage asserted), reading the HUD from a crop taken at the same instant as each memory sample: scan at HUD 004 -> 41537 candidates; filter at HUD 008 -> 8; verify across the 008 -> 012 transition, which was NOT selected on -> exactly ONE survivor. That survivor, 0xbdb69668, tracked 4 -> 8 -> 12 against the HUD's 004 -> 008 -> 012. The other seven collapsed into noise at the first unselected transition, which is precisely what that rule exists to catch. The file's "try 0xbdb59668 first, re-scan when it reads 0" rule worked verbatim: it read a hard 0 here, and the re-scan cost about the predicted five minutes. The ...9668 page-offset pattern is REFINED, not reinstated: the three located addresses (0xbdb49668, 0xbdb59668, 0xbdb69668) are three ADJACENT 64 KB pages at one offset, and in this run exactly one of 8192 probed pages held 12 -- the counter -- making it a one-step lookup. But the 2026-08-26 refutation stands as measured (zero ...9668 VAs held the HUD value in that run), so this is a fast heuristic to be HUD-checked, not a law.
18 KiB
REMAINING OB — the mission's own objective counter, in RAM
Status: ✅ CONFIRMED as a big-endian u32 whose value is exactly the
HUD's REMAINING OB. 🟡 the ADDRESS recurs but is not universal — 0xbdb59668
carried it in 3 of 5 measured runs, read a hard 0 in 2, and read garbage in a
6th (2026-08-26). ❔ what it counts, and whether objective-marked entities carry
a flag.
🟡 A sixth run, and a new failure mode (2026-08-26)
❌ First, a correction to this very section
As originally written this section claimed to be testing an untested 🟡, and concluded "the address is run-dependent — always re-derive". Both halves were wrong, and the evidence was already further down this same file:
- Cross-run stability was not untested. The status line at the top said so, but the body had tested it repeatedly —
0xbdb59668carries the counter in 3 of the 5 runs measured, and reads a hard 0 in the other two. I refuted a stale status line and presented it as news.- "Always re-derive" is worse advice than the rule this file already gives: try
0xbdb59668first, check it against the HUD, and re-scan only when it reads 0. A scan costs ~5 minutes; the check costs seconds.What this run actually adds
A sixth data point, and a failure mode not previously seen: the address read neither the counter nor 0 but 95748078, constant over four samples. A fresh Stage 02 run (guarded route, stage asserted via
assert_stage.py UN_f101_TCAF_Acropolis):
HUD Remaining OB : 004(capture)RAM 0xbdb5966895748078, constant over four samples 12 s apart Not 4, not near 4, and not moving while the mission ran. So
0xbdb59668is a property of that run's heap, exactly as the corpus's own warning about the heap re-allocating between runs predicts.❔ No cheap constant shift either. Looking for a BE u32 equal to the HUD's
4near the old address gives 1654 candidates within ±1 MB and 11202 within ±16 MB — far too many to isolate without the transition filter. So there is no "old address + delta" shortcut to be had.Practical consequence: treat the address as derived per run, never as a constant. The method below (
ob_scan.py: scan at one value, filter against live memory at a different value) is the durable result here — the number0xbdb59668is not.
Why it matters
autopilot-memory-driven.md ranks this its
problem #2: a pilot that flies well but ignores the objective cannot finish a
mission. Its 300 s run took no damage, killed one fighter, and watched
REMAINING OB rise from 004 to 011 as waves spawned. Reading the counter is
what turns "shoot whatever is nearest" into "shoot what closes the mission".
The find
Guest VA 0xbdb59668, big-endian u32, in a Stage 02 run on the
upstream baseline.
| time | RAM 0xbdb59668 |
HUD |
|---|---|---|
| selected on | 18 → 24 | 018 → 024 |
| t+40 s | 24 | 024 |
| t+75 s | 23 | 023 |
| t+110 s | 22 | 022 |
| t+145 s | 22 | 022 |
| t+180 s | 22 | 022 |
The two middle rows are the ones that matter: the candidate was filtered on the 18→24 transition, and it then tracked 24→23→22 on its own.
Method, and the two traps in it
tools/re-capture/ob_scan.py. Scan one snapshot for the current value, then
filter that candidate set against live /dev/shm/xenia_memory_* at the next
distinct value. Only the first pass needs the 4.8 GB copy.
🔴 Filter on a change, not a repeat. The first attempt scanned at 19, filtered at 19 again, then at 18 — and left zero survivors. The value moves between the memory copy and the screenshot that reads it, so "still 19" is not reliable. Scanning 18 and filtering on 24 gave exactly one candidate on the first try.
🔴 Verify across a transition you did not select on. An earlier differential
over 19→18 also gave exactly one candidate, 0xbc22e83c — and it was wrong:
read live it held 26 while the HUD showed 017. It was an unrelated counter that
happened to step 19→18 in the same window. One matching transition is not
evidence; the same offset tracking a later, unselected change is.
What is not settled
- 🟡 Cross-run stability.
0xbdb59668is from a single run. The corpus's other runtime finds are re-scanned per run, so the durable result here is the method, not the number. Testing whether the address repeats costs one boot. - ❔ What it counts. It rose 18 → 24 while waves spawned and fell as things
died, so it is objective targets remaining, not kills. Whether every
OB-badged entity is one of them, and whether that badge is a flag in the entity object, is the natural follow-on — and it is what the pilot actually needs to choose targets rather than merely know how many are left. - ❔ Whether the same address holds for other stages.
🔴 2026-08-23 — cross-run stability: REFUTED
0xbdb59668 does not repeat. A second Stage 02 run on the same baseline
(rebuilt per ../dynamic-re-state-restore.md)
reads 0 at that address for the whole mission while the HUD counts up:
| HUD | RAM 0xbdb59668 |
|---|---|
004 (t+18 s, flight entry) |
0 |
008 |
0 |
012 (held for ~5 min) |
0 |
A second, independent run of the same mission repeats it: at TIME 01:08.96
with the HUD on REMAINING OB 004, the word is again 0
(../captures/stage02-inflight-run2-ob-address-zero.png).
Read three times back-to-back at 004 — all zero — and ±64 bytes around it are
zero too. The page is not a sparse hole (SEEK_DATA at the offset returns the
offset itself; the next hole is 5 MB later), so this is an allocated,
zero-filled word, not an unmapped read. The number was a per-run artefact, as
this file already suspected; the method is the durable part, and every future
session must re-scan.
And re-finding it in the new run failed too — recorded because both failures are instructive. Two candidates were produced and both were killed by the "verify across a transition you did not select on" rule:
0xbc2377dc— held 12 at selection, then went 12 → 18 while the HUD stayed012, and was reading 3 two minutes later at HUD012.0xbd295b04— read 12 once, then 0, 21, 54, 46, 4 in the next 50 s. Noise.
🔴 A correction to this file's own method note. The trap is not that the
scan is slow: ob_scan.py scan over the live /dev/shm image takes 0.9 s
(9 555 hits at value 12; the SEEK_DATA extent walk skips the sparse 4.6 GB). The
real trap is that REMAINING OB climbs fast early in Stage 02 — 004 → 008
→ 012 inside about four minutes as waves spawn — so a scan is only valid if the
HUD is confirmed to hold the same value immediately before and after it. The
earlier 4 ∩ 8 intersection here came back with no survivor for exactly that
reason: the value-8 scan was actually taken at 12.
What blocked finishing it: with the pilot in RETIRE (hull 340/1500) nothing
was killing objectives, and the counter sat at 012 for five minutes — no
transition to filter on. A run that intends to find this address needs a pilot
that is still engaging, which is the same constraint
../autopilot-memory-driven.md records.
Unchanged and still open: what the counter counts, and whether an OB-badged
entity carries a flag in its entity object. Nothing in this pass touched that.
🟡 2026-08-23 (later) — a third run, a HUD-clean scan, and one candidate that is very hard to explain away
With the launcher repaired the scan could finally be run the way the method
asks. HUD confirmed 004 immediately before and after the scan (0.9 s), so
the 39 596-entry candidate set is valid at that value; then filtered when the
HUD was confirmed at 012.
| step | HUD | candidates |
|---|---|---|
| scan | 004 (confirmed both sides) |
39 596 |
| filter | 012 (confirmed) |
17 |
| later sample | — (see caveat) | 7 still 12 |
✅ 2026-08-27 — the counter LOCATED again, at
0xbdb69668, and the…9668pattern is refinedA sixth located run, by this file's own transition filter, on a guarded Stage 02 run (stage asserted). Every step's HUD value was read from a crop taken at the same instant as the memory sample.
step HUD candidates scan 00441 537 filter 0088 verify (not selected on) 008 → 0121 The single survivor is
0xbdb69668, which tracked4 → 8 → 12against the HUD's004 → 008 → 012(capture). The other seven collapsed into noise (1040511,3211282537, …) at the first transition they were not selected on — which is exactly why that rule is in this file.✅ The "try it first, re-scan when it reads 0" rule worked verbatim
0xbdb59668read a hard 0 here — the documented failure mode, not garbage — so the re-scan was the right move and cost the five minutes the file predicts.🟡 The
…9668page offset: a fast HEURISTIC, not a lawThe three located addresses are three adjacent 64 KB pages at one offset:
run address run 1 0xbdb5``9668run 2 0xbdb4``9668this run 0xbdb6``9668And in this run the offset probe is decisive on its own: of 8192 pages, exactly one VA at offset
0x9668held12, and it was the counter. That is a one-step lookup where the full scan costs five minutes.⚠️ This refines rather than reverses the refutation immediately below. That refutation stands as measured: in that run, zero
…9668VAs held the HUD value. So the pattern recurs but is not universal — the same shape as the address itself. Practical order: try0xbdb59668; if it is 0 or garbage, probe the…9668pages (seconds); only then run the full scan. Check every candidate against the HUD before believing it.
❌ REFUTED 2026-08-26 — the page-offset
…9668prediction does not holdThe reading below — "the counter lives at a fixed offset inside an allocation whose base moves by whole 64 KB pages" — came with its own test: "a third scan should again land on
…9668." That test has now been run, on a fresh guarded Stage 02 run (stage asserted), and it fails.Probing 8192 pages, every VA of the form
0x????9668across0xa0000000–0xbfffffff:
HUD VAs at offset 0x9668holding that value0042 — 0xbc3f9668,0xbe3f96680120 Both candidates then failed the file's own transition rule. Watched for 252 s:
0xbc3f9668held a flat 4 and0xbe3f9668flickered 4/0, while the HUD went 004 → 012 (capture). A value that does not move when the counter moves is not the counter, and at012nothing at that page offset holds12at all.Both arms were read at the same instant (cropped HUD digits beside each memory sample), because comparing a stale screenshot against a later memory read is exactly how the earlier
13-vs-004confusion in this session arose.⚠️ Scope, stated so this is not over-read: this refutes the page-offset prediction for this run. It does not touch the confirmed finding that
0xbdb59668carries the counter in some runs — that address is simply not at a reachable…9668page here. Nor was the counter located in this run: no transition filter was run, so where it actually lives this time is ❔.
The survivor of interest is 0xbdb49668 — the run-1 address 0xbdb59668
minus exactly 0x10000, at the identical page offset 0x9668.
That is not a cheap coincidence, and it was checked rather than admired. Of
all 39 596 words holding 4 at scan time, exactly 2 sit at page offset
0x9668; one of those two is in the 17, and it is this one. A survivor landing
there by chance is a ~5 × 10⁻⁵ event. The reading it suggests — the counter lives
at a fixed offset inside an allocation whose base moves by whole 64 KB pages
between runs — is a testable claim, not a story: a third scan should again land
on …9668.
Why this is 🟡 and not ✅, stated plainly. The corpus's own rule is that a candidate is believable once it tracks a transition it was not selected on, against the HUD. This one has not done that yet:
- The "later sample" that killed 10 of the 17 was taken during the GAME OVER flash — the escorted ACROPOLIS was lost at ~13 min — so the HUD counter was not on screen. The ten that moved are genuinely refuted; the seven that did not move are merely unrefuted, which is a much weaker thing, and the counter may simply be frozen once the mission ends.
- Between the filter and the mission end,
REMAINING OBsat at012for about ten minutes:pilot.pyspent the window in EVADE/RETIRE at hull 822/1500 and killed no objectives, so there was no transition to use.
The one experiment that settles it: a run where the pilot is still engaging,
scanning and filtering as above, then watching 0xbdb4·9668-equivalent across a
HUD-verified change. The blocker is the pilot's survival, not the method — and
the same run would answer whether the page offset repeats, which is the stronger
result of the two.
✅ 2026-08-23 (third pass) — the counter is at 0xbdb59668 again, and the refutation above is refined, not reversed
Run 4, with the hunt automated end to end
(ob_hunt.py + the HUD reader), produced
exactly one surviving address:
scanned at 004: 35897 candidates (HUD confirmed 004 on BOTH sides)
004 -> 008 [SELECTED ON] 7 survive
008 -> 012 [verification (unselected)] 1 survive
va 0xbdb59668
(captures/ob-hunt-stage02-run4.log,
captures/ob-hunt-stage02-run4.json)
That is the corpus's own standard met: one filter to select, a second on a
transition it was not selected by to verify. Then three live paired readings —
RAM read, screenshot, RAM read again — all ram 12 -> 12 / hud 012.
And it is the same number as run 1.
What this does and does not overturn
| claim | status |
|---|---|
the counter sits at 0xbdb59668 in every run |
🔴 still refuted. Runs 2 and 3 read a hard 0 there, on an allocated page, while the HUD counted 004 → 012. Nothing about run 4 makes those readings wrong. |
| the run-1 number was a meaningless artefact | 🔴 withdrawn. It recurs exactly: 2 of the 4 runs measured put the counter at 0xbdb59668. |
| the address moves by whole 64 KB pages | 🟡 consistent, twice. Run 3's best candidate was 0xbdb49668 — one 64 KB page below — and both known-good addresses share the page offset 0x9668. Run 3's candidate was only ever amber, so this rests on one confirmed address plus one plausible one. |
So the practical rule is: try 0xbdb59668 first and check it against the
HUD, and re-scan when it reads 0. A scan costs about five minutes now
(ob_session.sh), most of that being the boot.
Why run 3 failed and run 4 did not
Nothing about the game changed; the procedure did. Run 3 filtered once, on
004 → 012, because the intermediate 008 step went by unnoticed between two
hand readings — and its 17 survivors then had no second transition to be tested
against, since REMAINING OB held at 012 for ten minutes while the pilot sat
in EVADE. Run 4 caught both early steps, five seconds apart from polling,
because reading the HUD stopped costing a human round trip. The evidence needed
was always in the first four minutes of the stage; the earlier runs were simply
not able to look often enough.
Still open, and untouched: ❔ what the counter counts, and whether an
OB-badged entity carries a flag in its entity object. That is the part the
autopilot actually needs, and knowing the address is only its precondition.
✅ 2026-08-24 — what is AROUND the counter: its own rendered digits
With the per-entity searches refuted at both word and bit level
(../mission-freeze-and-ob-flag.md), the next question was what object owns the
counter. It sits at 0xbdb59668, inside the entity-heap window, so the answer is
readable directly: sample ±0x200 around it across one transition and keep the
words that move with it
(ob_neighbourhood.py,
captures/ob-counter-neighbourhood-stage02.json).
Control first: over an 8-second interval while the counter sat still, 0 of the 256 words in the window changed. So the window is quiet, and anything that moves on the step is a real neighbour rather than noise.
On 4 → 8, nine words moved — the counter and eight others, in two kinds:
| offset from the counter | before → after | what it is |
|---|---|---|
-0x12c, -0x11c, +0x64, +0x74 |
0x34000000 → 0x38000000 |
ASCII '4' → '8', NUL-padded |
-0x10c, -0xec, +0x84, +0xa4 |
0xbcad258c ↔ 0xbcad280c, 0xbcad2d0c ↔ 0xbcad2f8c |
guest pointers into 0xbcad2xxx |
Read live a moment later with the HUD showing 008, all four character slots
hold '8'. So the neighbourhood is the HUD's rendered text for this counter —
the digit as a character in four places, each with a pointer that swaps as the
digit changes (a glyph or sprite record).
🟡 Which reframes what the address IS
This file has called 0xbdb59668 "the mission's own objective counter". The
evidence now says something narrower: it is the HUD counter widget's value,
sitting beside the characters it renders.
🔴 But there is no separate mission-side copy that moves with it, and that was
already measured: ob_hunt.py scans all of guest memory and requires a
candidate to match across two transitions, and it left exactly one address.
Whatever the mission script keeps internally either does not change on the same
step or is not a plain big-endian word.
Next: follow one of the 0xbcad2xxx pointers — if they resolve to glyph or
sprite records, the widget is confirmed and the search for a mission-side count
moves to the objects those digits are fed from.