- determinism oracle regenerated and byte-identical (bb4fd9ac / 978041ac) - the four phase-23/33 draw-bearing tail callees run EVERY turn; the three inner functions holding the draws run zero times -- the gate is inside each outer body - CreateRaidEncounter is called (2 on one turn) and draws nothing: candidate list empty - Zuul: 7 calls / 7 words per trade-raid Chance site, 14 not 16, as predicted - EncounterDetect_Run receives an EMPTY record vector, so ProcessTeamRecord and AssignContacts never run; the 2-word detection residual is in 0x007d5150's subtree - a MinHook detour on 0x00893290 changes the game's output; bisected over six runs. The un-instrumented game and lane Z's instrument agree, so lane Z's numbers stand - lane AI1 insert: P2 held across two fresh processes, Rung B stays as written
27 KiB
Five probes on the running game — and a harness defect the probes found in themselves
Lane H, 2026-09-08. Program sots / "Sword of the Stars.exe", ImageBase 0x00400000, all addresses VAs.
Engine worktree wip/probes, predictions in sots-engine/docs/H-probes.md, committed before the build
(method rule 2). VM140 held for the duration; released at the end.
The headline, in order of what depends on what.
- The determinism oracle reproduces exactly.
bb4fd9ac…and978041ac…, byte for byte, after everything this session changed. §1. - A hook in this lane's own instrument changed the game's output, and it was caught by comparing against a save lane Z had already produced. Bisected to one address. Every measurement below was then re-taken on a configuration proved byte-neutral against that save. §2.
- The four tail callees that can draw are entered on every single turn and gated inside; the three inner functions that hold the draws are entered zero times. "Never observed firing" was about a gate, not about reachability. §3.
CreateRaidEncounteris called — twice on one measured turn — and draws nothing. The two hypotheses a word count could not separate are separated: the candidate list is empty. §4.- The Zuul prediction holds exactly: 7 calls and 7 words per site, 14 not 16. The raid loop iterates the player vector. §5.
EncounterDetect::AssignContactscannot fire on our corpus for a reason nobody had measured: its caller is never called, becauseEncounterDetect_Runreceives an empty record vector. The 2-word detection residual is somewhere else entirely, and §6 names the function it is in.
1. The oracle, regenerated — and it reproduces
Lane N's End Turns overwrote the three autosave contents on VM140 and said so. The file set was
never disturbed, ref-turn2.sav still hashed ab4ac2d7e2977260… (checked before the run), and all 11
curated saves were in the repo, so nothing was lost — but the reference pair had to be remade.
Load ref-turn2.sav, one End Turn, build recap-7584bad-20260908T0615Z, hooks=trace:
| file | size | sha256 | historical value |
|---|---|---|---|
(Autosave EndTurn).sav |
66,732 | bb4fd9ac89f41e3bc0db2af08b18ce83417521ac4bcee695fc9fa6ce16e30948 |
identical |
(Autosave).sav |
67,219 | 978041acd168b56ed8eb3f5e42e78d5e70eae6e6517d75e659a5eb7ca3d60921 |
identical |
Both reproduce. The turn-2 state on screen (Imperial Savings 289,688) and the turn-3 state
(532,369) match determinism-oracle.md as well.
This is worth more than a repeat. The process the oracle now runs in is not the one it was measured in: nineteen template hooks and seven entry-point detours installed instead of one asm stub, an x87 control-word module, eight save files where there were four, and a Zuul game played to turn 64 in the same directory in between. The oracle is revalidated across all of that, which is what every Phase-2 comparison downstream of it needed.
Files: verify/results/shim/probes/oracle-restore/h-oracle-{endturn,autosave}.sav.
And the same two files were put back on the VM at the end, so the three autosaves once again carry the documented oracle bytes. That is a restore of bytes this lane produced by an actual run today, not a substitution of an older snapshot — the distinction lane N was right to insist on.
2. The instrument perturbed the game. Bisected to one address.
This section is first among the probes because it invalidates a run and because nothing in this campaign has ever tested it.
2.1 How it was caught
Three End Turns from ref-turn2 under the full lane-H build gave per-turn word counts of
19, 18, 19. Lane Z's per-site table for the same three turns of the same save reads 19, 18, 20.
One word apart on the third turn — the kind of difference that is easy to explain away as workload.
It was not explainable, because the input was byte-identical. The run's (Autosave EndTurn).sav
(the turn-4 pre-turn state) hashed 8648957fecf5041233f9add4c8173f78981f026f133ecdbecdb89ddd10917048,
which is exactly lane Z's z2-endturn.sav. Same bytes in; different bytes out:
(Autosave).sav after the turn-4 → turn-5 End Turn |
size | |
|---|---|---|
lane Z (z2-autosave.sav, and z-t5-autosave.sav from a second lane-Z run) |
b2124798470d85ea… |
68,267 |
| lane H, full build | 058656787877cbee… |
68,263 |
Both sides are reproducible: lane Z's value appears in two of its runs, and lane H's appeared in two
of its own (three consecutive End Turns, and a fresh load of z2-endturn.sav followed by one End
Turn). So this is not noise in either direction.
2.2 The bisect
z2-endturn.sav was pushed to the VM and the same single End Turn run five more times, changing one
thing each time. A probes= key was added to shim.cfg for exactly this purpose — an instrument that
can change what it measures has to be switchable, or the question cannot be asked at all.
| configuration | (Autosave).sav |
verdict |
|---|---|---|
hooks=off — the shim installs nothing |
b2124798… |
the un-instrumented game |
all template hooks + 7 draw-site detours, probes=0 |
b2124798… |
neutral |
| + probes 0–3 (the four phase-23/33 vtable callees) | b2124798… |
neutral |
+ probes 4–7 (three inner callees, CreateRaidEncounter) |
b2124798… |
neutral |
+ probe 8 (ServerTradeManager_GenerateTradeRaidEncounters 0x00893290) |
058656… |
perturbs |
Two things fall out, and the first is the more important:
- The un-instrumented game agrees with lane Z, not with lane H. So lane Z's ledger — 7 draw-site detours and 6 nested template hooks — is behaviour-neutral on this pair, and its published numbers stand. This is the first time any lane has checked that, and it came out the right way.
- Hooking
GenerateTradeRaidEncountersat 0x00893290 with a MinHook detour changes the turn. Specifically it suppresses theServerPlayer::RollResearchEventdraw at 0x0088df4f: the clean run spends 20 words with that site firing once, the perturbed run spends 19 with it not firing at all.
2.3 What the mechanism is not
The obvious relocation hazard is absent. The bytes at 0x00893290 are
55 push ebp
8b ec mov ebp,esp
6a ff push -1 <- the 5-byte patch boundary falls exactly here
68 37 c3 99 00 push 0x99c337 ; SEH scope table
64 a1 00 00 00 00 mov eax,fs:[0] ; ... standard MSVC __try + /GS prologue
so MinHook's five bytes land on a clean instruction boundary, the relocated prologue is three position-independent instructions, and there is no branch target inside the patched range. A mangled prologue is therefore not the explanation, which makes this more interesting rather than less.
The mechanism is undetermined and is recorded as an open item, not guessed at. What is odd, and is
the thing to chase: RollResearchEvent runs earlier in the turn than trade-raid generation, so
hooking 0x00893290 changed a decision taken before the hooked function was reached. The cheapest next
probe is a compare-mode hook on RollResearchEvent in both configurations, reading the threshold and
the progress value it tests, on the same input save.
2.4 What this costs, and what it does not
- Every number in §§3–6 was re-taken at
probes=8, the largest configuration proved byte-identical to the un-instrumented game on this save. Nothing below rests on the perturbed run. - The perturbed run's trace is kept —
verify/traces/h-probes-human-perturbed-t3-5.jsonl.gz, reportverify/results/shim/probes/human-t3-5-perturbed.txt— labelled as such, because it is the evidence for §2 and because its probe entry counts agree with the clean runs on every row. - The general lesson is a method rule waiting to be written. A behavioural harness has to be
checked against the oracle, not just against itself. It cost one save comparison to find this; it
would have cost a published wrong number to find it later. The
probes=key exists so the check is cheap to repeat: any lane adding detours should run one End Turn fromz2-endturn.savand confirmb2124798470d85ea…before trusting its own measurements.
3. The four tail callees — reached on every turn, gated inside
Lane V2 closed lane K's tier-4 blind spot: four of the eleven phase-23/33 vtable targets on
StrategyServer+0x158/+0x15c reach a draw on the strategic generator, at eight instructions. Lane Z
measured 0 tail words on eight turns. Between those two facts sat a question a word count cannot
answer — not reached, or reached and gated?
Register-transparent asm stubs (pushfl; pushal; push idx; call; popal; popfl; jmp trampoline) count
entries without assuming a calling convention. Two saves, three measured turns:
| address | what | Human t5 | Zuul t24 | Zuul t25 |
|---|---|---|---|---|
| 0x008877b0 | ServerSpyManager slot 13 |
1 | 1 | 1 |
| 0x0088db80 | ServerSpyManager slot 14 |
1 | 1 | 1 |
| 0x0088ef80 | ServerTradeManagerImpl slot 13 |
1 | 1 | 1 |
| 0x0082cca0 | ServerTradeManagerImpl slot 15 |
1 | 1 | 1 |
| 0x008408e0 | SpyManager_Slot13RngCallee |
0 | 0 | 0 |
| 0x00820ca0 | TradeManager_Slot13RngCalleeA |
0 | 0 | 0 |
| 0x0088b440 | TradeManager_Slot13RngCalleeB |
0 | 0 | 0 |
P2a and P2c both hold. The phase-23/33 dispatch block is unconditional, exactly as lane V2 read it; and the gate that keeps the draws from firing is inside each outer body, above the inner call. No draw-site row appeared at any of the eight return addresses on any turn, and the tail's bracket cost stayed 0 — reproducing lane Z's number under an instrument that can now say why.
So the correct statement is no longer "the tail draws nothing" and not even "the tail draws nothing on these turns". It is: the tail's four draw-bearing callees run on every turn and return before reaching their draws. That is a much stronger negative, and it converts the open question from "which turns reach these functions" to "what does the gate in each body test".
3.1 Three of the four gates are an empty container; the fourth is not, and it was wrong
Read from the bodies (each confirmed a real function start with exactly one caller):
| callee | the loop it drives | empty when |
|---|---|---|
ServerTradeManagerImpl slot 15 |
vector at tradeManager+0x3c .. +0x40 |
no trade routes |
ServerSpyManager slot 14 |
vector at spyManager+0x10 .. +0x14 |
no spy programs |
ServerSpyManager slot 13 |
1,869 bytes, same manager, not fully read | — |
ServerTradeManagerImpl slot 13 |
GetServer()+0x64 .. +0x68 — the FLEETS vector |
no fleets |
That last row produced a competing prediction, P2b-alt, committed after the bodies were read and
before any run: on a save with fleets, slot 13's loop body runs, TradeManager_Slot13RngCalleeA
fires, and the tail cost is non-zero.
P2b-alt is falsified, and cleanly. zuul-turn23-fleet23.sav is named for its 23 fleets; slot 13
was entered on both its turns and 0x00820ca0 was entered zero times. So the fleet loop is not the
gate — the per-fleet test chain that precedes the call at 0x0088f036 is, and it rejects every fleet on
a 23-fleet save. That is a narrower and more useful place to look than "the loop never runs", and it is
the kind of wrong prediction that pays for itself: had P2b been the only one written down, its
confirmation would have been read as "no trade routes, no spies" and the fleet loop would have gone
unexamined.
3.2 What is still a hypothesis (rule 6)
Whether any of the eight draw sites ever fires is still unexercised. The workload that would settle it — a game with live trade routes and a live spy program — was not built, and it is worth saying exactly why rather than vaguely:
- SotS 1 trade income requires trade-station construction and the tech behind it; from
ref-turn2(turn 2, 1 colony, 0 techs) that is tens of End Turns at 30–60 s each plus a Build/Research click path per turn. The newest Zuul save in the corpus (turn 23) still has no trade routes: slot 15's loop body did not execute on it. - The corresponding spy state is worse — no lane has yet identified which UI produces an entry in the
spyManager+0x10vector, so there is no click path to write down, only a container to watch. - What this lane did instead was cheaper and, for the map, better: it established that the containers are the gate for two of the four and that the fourth is gated per-fleet. A future lane that builds the workload now knows which two vectors to watch fill, and can confirm the workload took before spending a turn measuring.
4. CreateRaidEncounter — the two hypotheses, separated
A successful trade-raid roll calls ServerTradeManagerImpl slot 17, CreateRaidEncounter 0x008938a0,
which draws a NextInt at 0x008939ee to pick a target but returns at 0x0089391c without drawing
when the candidate vector is empty. It cost 0 words on all three of lane Z's turns, which lane Z read
as consistent with either "no roll succeeded" or "the candidate list is empty".
A correction to the prior first (rule 11). Lane Z wrote that no roll succeeding is "≈11% on the
defaults, so three quiet turns in a row is unremarkable". 11% is the probability of a quiet turn:
0.8^8 × 0.95^8 = 0.1113. Three consecutive is 0.00138, about 1 in 720. The two hypotheses were
never close to equally likely, and the expected number of successes per turn is 8 × 0.2 + 8 × 0.05 = 2.0.
Measured:
| turn | CreateRaidEncounter entries |
words drawn |
|---|---|---|
| Human t4 → t5 | 0 | 0 |
| Zuul t23 → t24 | 2 | 0 |
| Zuul t24 → t25 | 1 | 0 |
| Human t3/t4/t5, perturbed run | 0 / 1 / 0 | 0 |
Settled: rolls succeed at about the predicted rate, and the candidate list is empty every time.
Four successful rolls were observed across five measured turns and not one of them reached the
NextInt at 0x008939ee — no draw-site row at return address 0x008939f3 on any turn.
For the standalone this is the useful form: on a corpus like ours the cost of a successful raid roll is 0 further words, and the row lane Y flagged as "the one that will bite" does not bite yet. It will bite the first turn a raid candidate exists, and the candidate list is built by 0x0083b110 — a function no lane has read.
One caveat this lane found and must state. CreateRaidEncounter is slot 17 and is reached by
virtual dispatch, and GenerateTradeRaidEncounters is not its only caller: 0x007d5150, the
other subtree of DetectEncounters (see §6), also dispatches slot 17 —
(**(code **)(**(int **)(S + 0x158) + 0x44))(out, player, 1, records) — on the branch where its own
per-player roll returns 0x12. So an entry count of 2 does not prove two trade-raid successes; it
proves two slot-17 calls from one of two known sites. Separating them needs the return address, which
an entry counter deliberately does not capture. tail-rng-ledger.md §11.1's "called from
GenerateTradeRaidEncounters at 0x0089345e / 0x00893548 / 0x00893603" is incomplete as a list of
callers.
5. The Zuul word count — 14, exactly as predicted
Lane Z's free prediction, restated by lane Y as a consequence of its trade_raid.cpp model: the two
Chance sites in GenerateTradeRaidEncounters cost one word per entry of StrategyServer::Players,
which holds 8 on the Human saves and 7 on the Zuul ones. Lane N ran a Zuul End Turn with the RNG hooks
off, so it could not answer.
zuul-turn23-fleet23.sav, two End Turns, probes=8:
| site | Human t5 | Zuul t24 | Zuul t25 |
|---|---|---|---|
0x00893426 Chance (TRADE_RAID_ODDS_PLAYER) |
8 calls / 8 words | 7 / 7 | 7 / 7 |
0x00893513 Chance (TRADE_RAID_ODDS_NPC) |
8 calls / 8 words | 7 / 7 | 7 / 7 |
| trade-raid block total | 16 | 14 | 14 |
| turn total (attributed = measured) | 20 | 18 | 18 |
P5 holds in full, and it is a prediction that survived a changed workload rather than a repeat of a
recording: 7 is a number no earlier run produced. The loop iterates the server's player vector and
nothing else — not the TeamRecord vector it is handed, not a species list, not a fixed NPC count.
Lane Y's trade_raid.cpp, which counts NumPlrs, is right.
The Zuul turn's remaining 4 words are 2 detection (§6) and 2 research — both ProcessResearch
sites (0x00587888 and 0x005878bb) fired on both turns, which is the second research site's first
appearance in a site table.
6. DetectEncounters — the residual is not the AssignContacts family, and here is where it is
Lane Y's standalone leaves a 2-word residual per turn, attributed by lane Z to 0x0050329d
(NextFloat, depth 4) and 0x007929a4 (NextInt, depth 3), and refused to model it: "fitting 2/turn
to three observations is not a model." Lane I bounds the containing family at
|contacts| × |detectors| and showed EncounterDetect_AssignContacts draws one inlined NextFloat
per (contact, detector) trial. Lane Z found AssignContacts never ran.
EncounterDetect_ProcessTeamRecord 0x007ca640 — its only caller — was hooked, with lane I's gate
predicate, contact count and detector count recomputed from the record at hook entry.
It never ran either. On any turn, on either save. The hook installed (MH_OK for create and
enable, logged), was registered in the trace's meta line, and emitted zero records across five
End Turns, while the entry probe on EncounterDetect_Run recorded one call per turn, every turn.
EncounterDetect_Run calls ProcessTeamRecord once per 0x74-byte record. One call, zero records
means the vector it receives is empty. So:
AssignContactscannot fire on our corpus, and the reason is one level higher than anyone thought. It is not that the+0xfcgate fails on the records — there are no records. Lane I's|contacts| × |detectors|bound is correct and simply has nothing to bound yet.- P3b is falsified, exactly as its falsification clause anticipated, and the depth reasoning that produced it was wrong about the path, not the depth.
6.1 Where the two words actually are
A call-graph walk from both residual sites upward lands in the same place, and it is not
EncounterDetect_Run:
StrategyServer::DetectEncounters 0x007d7f70 (the last phase of ProcessTurn)
├─ 0x007d8469 GenerateTradeRaidEncounters [virtual] 16 words (14 on Zuul) §5
├─ 0x007d8470 EncounterDetect_Run 0x007cb080 0 words -- empty record vector
└─ 0x007d860b 0x007d5150 ** the 2 words **
├─ 0x007d52b3 0x0078cc70 -> 0x0078ccb1 0x00503200 -> NextFloat 0x0050329d 1 word
└─ 0x007d5311 / 0x007d5338 0x00792750 -> NextInt 0x007929a4 1 word
Every one of those edges is a direct call with a single incoming reference, so the attribution
is not a guess: 0x00503200 has exactly one caller (0x0078cc70), which has exactly one caller
(0x007d5150); 0x00792750 has two call sites, both inside 0x007d5150; and 0x007d5150 has exactly one
caller, DetectEncounters at 0x007d860b.
The whole detection residual is inside one 744-byte function with one call site. That is a far better handle than a family bound over two counts nobody can observe, and it makes the residual a tractable modelling target rather than a fitted constant. From its body:
- it first builds a bitmask over the same
recordsvector (entry[0]->+0xf9 == 0sets bitentry[0]->+0x28) and returns immediately if the mask is 0; - it then loops the player vector at
+0x54/+0x58, skipping players whose mask bit is set or whose+0xf9is non-zero, builds a per-player candidate vector (0x00795870), and for each non-empty one calls 0x0078cc70 — that is theNextFloat— whose non-zero return leads on, and whose return value of 0x12 dispatches slot 17,CreateRaidEncounter(§4's caveat); - the measured cost is 1 call and 1 word per site per turn on 8-player and 7-player saves alike, so only one player reaches the draw per turn on both — the per-player gate, not the loop bound, is what sets the count.
That last observation is the one to hand lane Y: the 2 words are not 2 × |players| and not a
constant by luck — they are one per player that passes a gate, and on both corpus saves exactly one
player passes. A model that assumes 2/turn will be right on our saves and wrong on a busier one, and
now there is a named function to read to find out when.
6.2 The hook that measured nothing, reported as loudly as the ones that did (rule 15)
EncounterDetectProcessTeamRecordHook compared nothing, because it never ran. Its coverage notes, its
gate predicate, its contact/detector classifier and its byte-vs-dword cross-check on +0xfc are all
unexercised. They are kept in the tree because the moment a save produces a non-empty record
vector they become the check that matters — but nothing in this report rests on them, and the
fc_byte_vs_dword_disagreements counter has never returned a value. Lane I's classification rules are
therefore still exactly as verified as they were: from the instruction stream, and from nowhere else.
7. Coverage — what these runs did not touch
- Five End Turns on two saves. Human
ref-turn2turn 4 → 5 (once clean, three times in the perturbed run) and Zuul turn 23 → 25. That is a thin workload measured precisely. - No trade routes, no spy programs, no manufactured workload for either. §3.2 says what it would take. Until then, every statement about the eight tail draw sites is "they are not reached on a game with none of either", and the four callees' gates are located but not read.
ServerSpyManagerslot 13 (1,869 bytes) was not read. Its loop and its gate are unexamined; it is grouped with slot 14 above only by manager, not by evidence.- The probe counters cannot say which caller. They record entries, not return addresses — a deliberate cost of being calling-convention-agnostic — and §4 shows that costing something real.
- The perturbation's mechanism is unknown, and it is the largest open item this lane produced.
A second, independent instance may already exist and be undetected in some other lane's build; the
z2-endturn.sav→b2124798470d85ea…check is cheap enough that every future VM lane should run it. - No
Guardregion is declared by any hook in this family, the same deliberate gap lane Z reported: these hooks measure the generator and claim nothing about game state.undeclared = 0in these traces is vacuous and must be read that way. probes=8is proved neutral on one save and one turn. It is not proved neutral in general, and the Zuul numbers in §3 and §5 inherit that limit — they were taken on a configuration validated against a Human turn, not a Zuul one.
8. Corrections to earlier findings
tail-rng-ledger.md§11.1: the caller list forCreateRaidEncounteris incomplete — 0x007d5150 dispatches slot 17 as well (§4).tail-rng-ledger.md§11.1: "three quiet turns in a row is unremarkable" — it is a 1-in-720 event, and the measurement shows the rolls do succeed (§4).tail-rng-ledger.md§11.3 and lane Y's residual table: the 2-word detection residual is not theAssignContactsfamily and is not blocked on the+0xfcgate;EncounterDetect_Runreceives an empty vector, and the words are in 0x007d5150's subtree (§6).- Lane V2's per-slot notes: "never observed firing" is confirmed for the draws and wrong as a statement about the callees — all four run on every turn (§3).
combat-done-tail.mdtier 4 / lane V2: the trade slot-13 loop is over the fleets vector, and a 23-fleet save still reaches no draw (§3.1).
9. Files
- Predictions (pre-registered):
sots-engine/docs/H-probes.md. - Traces:
verify/traces/h-probes-human-t5.jsonl.gz,h-probes-zuul-t24-25.jsonl.gz,h-probes-human-perturbed-t3-5.jsonl.gz. - Reports:
verify/results/shim/probes/{human-t5,zuul-t24-25,human-t3-5-perturbed}.txt; tooltools/probe_report.py. - Oracle pair:
verify/results/shim/probes/oracle-restore/h-oracle-{endturn,autosave}.sav. - Addresses:
ghidra/addresses.d/lane-h.json(3 entries, allmapped). - Shim:
sots-engine/src/shim/hooks/probe_entry.{h,cpp}, theprobes=config key, andEncounterDetectProcessTeamRecordHookintail_rng.{h,cpp}; configsshim.cfg.hprobes*,shim.cfg.hp{0,4,8,9,10,11},shim.cfg.hoff.
10. Priority insert from lane AI1 — run before the VM was released
Lane AI1 asked for its P1–P3 on the same VM session. Results are written up where they belong, in
findings/subsystems/ai-turn-logic.md §8.1; in brief:
- P2 held — and it is the roadmap-level one.
ref-turn2.savloaded in two separate process launches (pids 3400 and 2028, killed and relaunched between), shim athooks=offso nothing is installed, one End Turn each: both autosaves byte-identical to each other and to the 2026-09-07 oracle. Byte-identity subsumesModCountandSummary.Checksum. The AI's turn is reproducible from a save alone, sobacklog.md§2's Rung B stays as written. Seven fresh processes across two saves agreed today in total, counting thez2-endturn.savbisect runs of §2. - P1 could not be answered as specified.
session.logafter a full load + End Turn of a game with an AI player is 22 lines and contains noRunAI:, norandom seedand noAI Process Turnline; the game's stdout carries the same 22 and nothing more. The level-2 messages are not emitted by this build at its default log level, so the "one log read, no build" route does not exist. A suggestive counter-indication to the all-zero-seed prediction is recorded there as a hypothesis, with the decisive test named (record thethispointer beside the return address indraw_sites.cpp). - P3 not run — no
ModCountwatchpoint was armed and no debugger session was held.
11. VM140 released
Left at the main menu, verified by screenshot, build recap-7584bad-20260908T0615Z,
shim.cfg = the recap trace config, SavedGames\ back to the documented 8-file set
(ref-turn2 still at (400,436), zuul-turn5 at (400,465)) with the three autosaves carrying the
oracle bytes bb4fd9ac… / 978041ac… / 978041ac…. Lane H left C:\SOTS\shimdist-h and
C:\SOTS\ui\h{deploy,release}.ps1 in place, plus C:\SOTS\ui\h-oracle-{endturn,autosave}.sav and
ai-run{1,2}.log.
One VM note worth carrying forward. The board's advice to screenshot before clicking is right but
under-specified: the intro screens are bright white, so a brightness threshold fires on them and the
click batch lands nowhere. The reliable detector is redness in the main menu's Exit button —
crop 100x14+462+674, require mean.r > 0.2 and mean.g < 0.15 (the menu reads 0.327 / 0.036; the
white intro reads 1.0 / 1.0; a black screen reads 0 / 0). With that, every one of this lane's ten
launches reached the menu on the first attempt, at t+48…54 s.