- 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
438 lines
27 KiB
Markdown
438 lines
27 KiB
Markdown
# 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.**
|
||
|
||
1. **The determinism oracle reproduces exactly.** `bb4fd9ac…` and `978041ac…`, byte for byte, after
|
||
everything this session changed. §1.
|
||
2. **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.
|
||
3. 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.
|
||
4. `CreateRaidEncounter` **is 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.
|
||
5. The Zuul prediction holds exactly: **7 calls and 7 words per site, 14 not 16.** The raid loop
|
||
iterates the player vector. §5.
|
||
6. `EncounterDetect::AssignContacts` cannot fire on our corpus for a reason nobody had measured: its
|
||
caller is never called, because `EncounterDetect_Run` receives 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 `GenerateTradeRaidEncounters` at 0x00893290 with a MinHook detour changes the turn.**
|
||
Specifically it *suppresses* the `ServerPlayer::RollResearchEvent` draw 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`, report
|
||
`verify/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 from `z2-endturn.sav` and confirm
|
||
`b2124798470d85ea…` 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+0x10` vector, 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:
|
||
|
||
* **`AssignContacts` cannot fire on our corpus, and the reason is one level higher than anyone
|
||
thought.** It is not that the `+0xfc` gate 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 `records` vector (`entry[0]->+0xf9 == 0` sets bit
|
||
`entry[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 `+0xf9` is non-zero, builds a per-player candidate vector (0x00795870), and for each
|
||
non-empty one calls 0x0078cc70 — **that is the `NextFloat`** — 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-turn2` turn 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.
|
||
* **`ServerSpyManager` slot 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 `Guard` region 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 = 0` in
|
||
these traces is vacuous and must be read that way.
|
||
* **`probes=8` is 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 for `CreateRaidEncounter` is 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** the
|
||
`AssignContacts` family and is not blocked on the `+0xfc` gate; `EncounterDetect_Run` receives 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.md` tier 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`;
|
||
tool `tools/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, all `mapped`).
|
||
* Shim: `sots-engine/src/shim/hooks/probe_entry.{h,cpp}`, the `probes=` config key, and
|
||
`EncounterDetectProcessTeamRecordHook` in `tail_rng.{h,cpp}`; configs `shim.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.sav` loaded in **two separate process
|
||
launches** (pids 3400 and 2028, killed and relaunched between), shim at `hooks=off` so nothing is
|
||
installed, one End Turn each: both autosaves byte-identical to each other and to the 2026-09-07
|
||
oracle. Byte-identity subsumes `ModCount` and `Summary.Checksum`. **The AI's turn is reproducible
|
||
from a save alone**, so `backlog.md` §2's Rung B stays as written. Seven fresh processes across two
|
||
saves agreed today in total, counting the `z2-endturn.sav` bisect runs of §2.
|
||
* **P1 could not be answered as specified.** `session.log` after a full load + End Turn of a game with
|
||
an AI player is 22 lines and contains no `RunAI:`, no `random seed` and no `AI Process Turn` line;
|
||
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 the `this` pointer beside the return address in `draw_sites.cpp`).
|
||
* **P3 not run** — no `ModCount` watchpoint 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.
|