sots-re/findings/control-flow/fleet-visit-order-mechanism.md
alex 696935ea2a lane BU: H1 confirmed - the fleet-visit order is ascending fleet->Location, and the two-file outcome set is the two orderings of two heap pointers
Three fresh processes on ad-turn27 with the seeds pinned, hooking the two
single-caller per-element callees of the assignment walk.

  * ascending fleet->Location: 6 of 6 multi-element sequences
  * ascending element address (the committed prediction P3): 0 of 6
  * ascending fleet id / systemId / minimum ship id: 0 of 6 each

Three processes gave three different orders of the same six fleets, and every
difference is a pair whose Location addresses exchanged relative order. Runs 1
and 2 wrote byte-identical autosaves and still visited differently. Run 3
flipped the branch and its two Location addresses flipped with it: it is
byte-identical to lane BP's run B, runs 1 and 2 to BP's A and L.

So P3 is falsified as written and its hypothesis class, H1, is confirmed with
a sharper key. H1b is excluded on this path.

Second finding the static read had missed: the fleets already carry their new
ids when the walk first touches them, so the mint is upstream in the gather.
That moves the engine's canonicalisation point by one level.

rule 26(d) and rule 19 are both met by measurement rather than argument: an
instrumented pinned run reproduced an unpinned natural run byte for byte,
twice.
2026-09-09 03:38:22 -04:00

772 lines
56 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# What orders the fleet-assignment visit? — the container hook on `ad-turn27`
- **Owner / date:** lane BU · 2026-09-09 · guest **VM146** (`sots-re-win10-146`, `re@192.168.10.146`)
- **Held:** VM146, from the time this prediction was committed. VM140 (reference, exclusive),
VM141 (lane AZ) and VM144 (lane BS) untouched; lane BT is on the host in
`verify/state-checksum/` and is not touched either.
- **Trigger:** `findings/control-flow/ai-visit-order-probe.md` §4.1 — lane BR's closing paragraph,
which killed **H2** by measurement and named this lane's work in one sentence: *one bounded
static read of `AssignFleetsAndIssueOrders 0x006c16c0` and the gathering hub **to place a
hook**, log `(element address, systemId, minted fleetId)` in visit order, two fresh processes.*
Frame: `findings/resolutions/2026-09-09-fleet-id-order-residue.md` §2.2 / §3 / §4.
- **Build:** `bu-55408a9-20260909T071101Z`, built in `/srv/re-lab/build/sots-engine-bu` after
`ssh spicy 'pct exec 111 -- rm -rf …'` of the whole tree (rule 24), from a lane-private
`git worktree` at `~/sots-engine-bu` (branch `wip/bu`, commit `55408a9`) with no local `build*`
shipped (rule 21). `binkw32.dll` sha256
`117c150a4dbd7f240ada7b8416917ad67eb9f239682dc7370a8a521e0c0956d5`. Deployed to
`C:\SOTS\shimdist-bu`.
- **Input:** `C:\SOTS\SavedGames\ad-turn27-two-raiders.sav`, sha256
`1c8baa27680809d585ab1728391e82729642d32aaedffd5bf95b616f0a11a436`, 100,809 B — **re-verified on
the guest before the deploy**, equal to lane AD's, BP's and BR's published value.
- **Route:** **load** (`ad-turn27-two-raiders.sav` → one End Turn), held fixed, as in BP and BR, so
this lane is unconfounded by lane BQ's per-*history* input (resolution §9.1).
---
## 0. Where the chain stands, in one paragraph
Two processes with **identical pinned AI client seeds** write autosaves differing in exactly **35
of 61,147 leaves**, and all 35 are one transposition: two newly formed fleets exchange contents.
The command blocks differ in **exactly three words**. The fleet-id counter is deterministic — 1970,
1986, 2002 are minted in that order in every process. What varies is **the order in which the
fleet-assignment pass visits the ship groups that need a new fleet**. Lane BR then excluded RNG
*by measurement*: across all three AI clients' `OnResumePlaying` brackets `foreign_words=0`,
`foreign_calls=0`, `residual=0`, `left_delta == observed` (so not an inlined draw either), and the
per-process global generator at `0x00af6e58` was drawn **exactly three times in the whole process,
from one return address**, minting the three client seeds and doing nothing else. **H2 is dead.**
What is left is **H1** — an address-keyed container walked in heap order — or **H1b** — a
comparator reading an uninitialised word, where the order is a function of nothing in particular.
BR also measured that LFH randomisation is live here (four processes, four relative orders for one
identical allocation sequence) and that the **emitted command nodes' address order does not track
the visit order**, so the block cannot substitute for the hook. This lane places the hook.
---
## 1. The bounded static read, and what it bought
Read to place a hook, not to derive an answer (rule 18). Recorded here because the hook's validity
rests on it, and because two of its three facts are new to the record.
### 1.1 Rule 17, discharged first
`AssignFleetsAndIssueOrders` was disassembled from `dumps/sots.exe` **to the next function start**,
not to Ghidra's reported size. The real body is `0x006c16c0`–`0x006c247a ret`; Ghidra's
`sizeInBytes` (3504) and `endAddress` (`0x006c247a`) agree with the `ret` — but immediately after
it sits a **four-entry jump table at `0x006c247c`**, the target of `jmp [eax*4+0x6c247c]` at
`0x006c22fb`, followed by `int3` padding to the next function start `0x006c2490`. So a sweep
clipped at `fva + sizeInBytes` loses the table, which is the same class of defect rule 17 names
even though the `ret` itself was reported correctly this time.
### 1.2 The walk, and why the loop body is *not* the hook
`param_4` is the `std::vector<StarFleet*>` that `AcquireFleetsForTask` filled (its `[ebp-0x3c]`
result, per AI3). The function walks it **three times**:
| loop | range | per element it calls | order |
|---|---|---|---|
| head | `0x006c1730`–`0x006c1740`, once at entry, before the pass gate | `0x006a4290` | vector index order |
| element | body at `0x006c17e0`, cursor at `[ebp-0x38]` | the assignment work | vector index order |
| tail | `0x006c1760`–`0x006c1770`, entered at `0x006c1753` after the element loop is exhausted (`jmp 0x6c1753` at `0x006c21c4`) | `0x0069da10` | vector index order |
The element loop's cursor is advanced by `add eax,0x4` at `0x006c21b5` and compared against
`_Mylast` **refetched from `[param_4+4]` every iteration** (`0x006c21bb`), with the back-edge
`jne 0x6c17e0` at `0x006c21be`. So **the visit order is the vector's index order** and nothing
inside the loop can reorder it — which relocates the question, correctly, to whatever filled the
vector.
Patching the loop body would be a mid-function detour whose first instruction (`0x006c17e6`) **is a
branch target** — from `jne 0x6c17e6` at `0x006c174d`, the first iteration's entry — so a 5-byte
patch at the body head would corrupt the first pass through it. That is exactly the shape rule 19
exists for, and it is unnecessary:
> **`0x006a4290` and `0x0069da10` each have EXACTLY ONE CALLER in the whole image, and that caller
> is `AssignFleetsAndIssueOrders`.** (Ghidra cross-references: `totalToCount` = 1 for both.)
They are ordinary function entries, called once per element, in the same order the element loop
will follow. Two entry detours therefore record the walk with **no return-address filter, no
mid-function patch, and no traffic from anywhere else in the game**. A third detour on
`0x006c16c0` itself brackets them, so each burst of element rows is attributed to
`(agent, task, pass, vector)` rather than inferred from where the bursts fall — and `pass` matters,
because the emitting body is pass-1-only while the head loop runs on both passes.
### 1.3 What the two callees are, and the layout they confirm
Both read the element's ships vector at `[fleet+0xa8] − [fleet+0xa4]` and each ship's id at
`[ship+0x4]`, which independently confirms `StarFleet_off_Ships` (+0xa4/+0xa8) and gives the ship
id the same offset as `StarFleet_off_Id`. They are exact inverses over a `std::vector<int>` at
**`agent+0x2d8`** (`_Myfirst` +0x2d8, `_Mylast` +0x2dc, `_Myend` +0x2e0):
* `0x006a4290` = **`ClaimShipsOfFleet`** — for each ship, linear-scan the agent's id vector
(`0x006a42e0`) and `push_back` the id if absent, with the MSVC grow path (capacity check against
`0x3ffffffe`, reallocate through `0x00483410`);
* `0x0069da10` = **`ReleaseShipsOfFleet`** — find the id, `memmove` the tail down (import
`0x009dd30c`), `[agent+0x2dc] -= 4`. `RET 4`.
So the fleets acquired for a task have their ships' ids parked on the agent for the duration of the
assignment walk and removed when it is done. New names, filed in `ghidra/addresses.d/bu.json` with
the offset; `AssignFleetsAndIssueOrders` is **not** re-declared there (lane AI3 owns it, and
`gen_addresses.py` rejects a same-address-different-name collision).
### 1.4 What the read did not settle, stated so nobody reads more into it
* **Where the new fleet id is minted is still not located.** `IDMap::AllocateOnLocalNode`
`0x008b8b70` has six callers and `IDMap::AllocateLocalID 0x0080f710` twenty-four, none of them in
the AI band `0x0068`–`0x006e` on a direct edge from this function. So this lane logs the **minted
id by joining to the block**, not by hooking the mint. That is a gap in the read and it is
recorded as one.
* **The gathering hub was not read.** The resolution's §4 allows reading it; it is not needed to
place this hook, and rule 18 says stop. If the answer is H1, the container that ordered the
vector lives in `AcquireFleetsForTask 0x006ceef0` / `GatherFleetsForTier 0x006abf80` /
`0x006b7c90` / `0x006cb310` and is stage 3, not this lane.
* Nothing here says the four list-10 groups are visited inside **one** call. They may be one
vector in one task's call, or four bursts across four task calls. **The instrument is agnostic**:
the bracket row records the call, and the element rows record the order within it, so both
readings are visible in the same log. §2.3 predicts for both.
---
## 2. The predictions, committed before the run
Committed in their own commit, with no result in the file (rule 2).
> **P0 (the instrument, checked before any output file is opened).** `shim.log` reads
> `BUILD_ID bu-55408a9-20260909T071101Z`; **28 of 28** registered hooks `mode=off`; **zero**
> `ignoring unknown key`; **zero** `COVERAGE:`; `aiseed call=1/2/3 … pinned=1` with no `pinned=0`,
> no `PIN MODE WITH NO PINS` and no `used=0xdeadbeef` wildcard hit; and **three**
> `aivisit: … create=MH_OK enable=MH_OK` lines. Any miss and the run is **void** and nothing is
> read from it.
>
> **P1 (the outcome set, rule 19's own check).** `(Autosave).sav` ∈
> {`e913ff41188211c02d536da616337544e64133996cd7cc7a6521a8df1960226f`,
> `724528ffde9a7514b69ab9e82ed3b8c8ade63d622c0720de30503a73a92df11e`} in **both** runs, and the
> pre-turn `(Autosave EndTurn).sav` = `16f63adbf383946db17c381998957172bca803406b7df78f3b03805fef0bbcb8`
> in both. A **third** file is **ambiguous between `k ≥ 3` and the three `aivisit` detours
> perturbing the run** and does not immediately mean either; §2.4 names the disambiguation and this
> lane will run it rather than pick a story.
>
> **P2 (rule 26 (d), the pin's own control).** At least one of the two runs is **byte-identical to
> lane BP's unpinned run L** (`e913ff41…`), which is the natural run whose observed seeds this
> lane's pin uses. Until that holds, nothing is read from a pinned pair. If both runs land on
> `724528ff…` the control is unmet and this lane says so and takes a third process rather than
> reading the pair anyway.
>
> **P3 — THE HEADLINE, AND IT IS THE COMMITTED PREDICTION THE BRIEF ASKS FOR.**
> **The visit order is ascending element address.** Concretely, in each run and for the burst that
> covers the groups list 10 records:
>
> * `elem` in the `aivisit claim` rows is **strictly increasing** with `idx`;
> * the `aivisit slots` row (the vector's storage read straight out of `param_4`) is the same
> sequence, so the two independent readings agree;
> * the `aivisit rel` rows are the **same order** as the `claim` rows for the same call, i.e. the
> vector was not permuted during the walk;
> * and **across the two processes, the element order differs exactly where the visit order
> differs.** If run 1 lands on `e913ff41…` (list 10 = `(384,1970) (80,1986) (384,2002)
> (816,7072)`, system 384 first) and run 2 on `724528ff…` (`(80,1970) (384,1986) …`, system 80
> first), then the system-384 group's element address is **below** the system-80 group's in run 1
> and **above** it in run 2. If both runs land on the same file, both must show the same relative
> address order, which is the weaker but still falsifiable form.
>
> **P4 (the join).** Every `aivisit claim` row's `ships=[…]` id set **contains** the ship id list of
> exactly one list-10 element of the same block, and that mapping is injective. This is what makes
> the pairing of element address to `(systemId, fleetId)` a measurement rather than an assumption
> about position.
>
> **P5 (the block, inherited unchanged from BP and BR).** The `aiorders` dump has 9 blocks and 81
> elements, the same `(pid, list, idx)` key set and the same per-list counts (player 32 emitting
> `{1:3, 3:19, 5:15, 7:2, 8:3, 10:4, 12:12, 14:8, 23:15}`), and differs from BP's runs A and B only
> inside the proven noise mask plus, at most, the three transposition words.
### 2.1 How each of these could be wrong, and the symptom
| way | symptom | what it would mean |
|---|---|---|
| **H1 is right** | P3 holds: `elem` ascending with `idx`, and the two processes' address orders differ exactly where their visit orders do | **The container is address-keyed.** The engine's deliverable 2 (a canonical order that is a function of the save alone) is confirmed as the right fix, and stage 3 is naming the container inside the gathering hub. |
| **H1b is right** | P3 falsified: `elem` is *not* monotone in `idx`, and the order tracks neither address nor id | An **uninitialised-word comparator**, or some third key. The next separation is named in §2.4 and it is cheap. |
| a **third possibility this lane must not hide** | `elem` ascending in both runs **and both runs land on the same file**, so nothing distinguishes | P3 holds only in its weak form. It is consistent with H1 and does not test it. Reported as a weak result, not a confirmation, and the fix is a third process. |
| the walk is **one burst per task**, not one vector | the bracket rows show four calls with one element each | Then the varying order is the **task** order, not a within-vector order, and the address to test is the *task* pointer, which the bracket row also carries (`task=`). The instrument survives; the H1 test moves to that column and this file will say so. |
| a **wrong offset** | `fid=0`, `nships=0` or an empty `ships=[]` on rows whose fleets plainly have ships | The reads are probed, so this shows as a logged zero, never a fault. The join (P4) fails visibly rather than silently, and the run is reported as an instrument failure. |
| the **hook perturbs the turn** (rule 19) | a third autosave file | **Not separable from `k ≥ 3` by these runs alone.** §2.4. |
| the **pin is half-applied** | any `pinned=0`, a `PIN MODE WITH NO PINS` line, or a `used=deadbeef` hit | The run is **void**. Checked in `shim.log` before any output file is opened. |
| a **stale or wrong binary** (rule 24) | `BUILD_ID` ≠ `bu-55408a9-20260909T071101Z` | Void. The CT111 tree was `rm -rf`'d before the rsync and the build was re-run from the lane commit. |
| the **config** carries an inherited defect | a live line inside what was meant to be a comment; an unnamed registered hook silently installed under `hooks=trace` | Defended twice: `tools/check_shim_configs.py` passes with `shim.cfg.bupin` marked `# exhaustive`, and the non-comment diff against the file it was copied from is quoted in §2.2. |
### 2.2 The config, and the two diffs (BP's check, and it has caught a real defect)
`shim.cfg.bupin` is `shim.cfg.bppin` with **exactly three non-comment lines added**. Taken against
the real `shim.cfg.bppin` pulled off the guest, not against a local copy of it:
```
$ diff <(grep -v '^\s*#' shim.cfg.bppin | grep -v '^\s*$') \
<(grep -v '^\s*#' shim.cfg.bupin | grep -v '^\s*$')
7a8,10
> aivisit=on
> aivisit.out=C:\SOTS\shim.aivisit.txt
> aivisit.ships=8
```
`bppin` has 43 live lines, `bupin` 46. And against `shim.cfg.brprobe`, the file this lane actually
copied (BR's, which is `bppin` plus the three `airng` lines), the diff is six lines and shows the
`airng` bracket being taken back out:
```
$ diff <(grep -v '^\s*#' shim.cfg.brprobe | grep -v '^\s*$') \
<(grep -v '^\s*#' shim.cfg.bupin | grep -v '^\s*$')
6,8c6
< airng=on
< airng.pin_seed=off
< airng.out=C:\SOTS\shim.airng.txt
---
> airng=off
9a8,10
> aivisit=on
> aivisit.out=C:\SOTS\shim.aivisit.txt
> aivisit.ships=8
```
`airng` is **off** deliberately: BR already took the per-client ledger on this exact state and it
came back clean, so re-installing that bracket would be three more detours for an answer already
in hand. Everything else is byte-for-byte BP's: `hooks=trace`, all **28** registered hooks off *by
name*, `aiorders=on` with `deep=off` and `words=12`, `aiprobes=off`, `airesearch=off`,
`aiseed=pin` with `32=156ebbbd,496=fe7b2826,512=0ed341d1,*=deadbeef`.
**And the structural check the resolution asked for is now enforced, not remembered.**
`shim.cfg.bupin` carries a line reading exactly `# exhaustive`, so
`tools/check_shim_configs.py` requires it to name every registered hook:
```
$ tools/check_shim_configs.py
check_shim_configs: OK (27 registered hooks, 2 template(s) declared exhaustive)
```
(27 is the count excluding the self-test hooks the checker exempts; the file carries 28 `hook.*`
lines, the 28th being `Shim::SelfTest::Fill`.)
### 2.3 The instrument, stated in full (rule 19 wants the list, not the adjective)
Installed, and nothing else: the `Mars::Application::Initialize` detour; the seven draw-site
detours (unconditional whenever `hooks != off`); **one** `aiorders` detour on
`StrategySim::ApplyTurnCommandBatch`; **one** `aiseed` detour on `StrategyApp::RunAI`; and
**three** new `aivisit` detours. That is BP's instrument plus three. All three new targets are
function entries; two of the three are called from exactly one place in the image and that place is
the third. No `airng` bracket, no facades, no entry probes, no watchpoints, no research hooks.
### 2.4 The disambiguations, named before the run so they cannot be chosen after it
* **A third autosave file** → one plain run of `shim.cfg.bppin` (this lane's `aivisit` lines
removed, everything else identical). Under the `k ≥ 3` reading that run cannot land on a *third*
distinct file with probability better than 1/3; under the perturbation reading it lands on one of
BP's two. This is BR's own procedure and it is adopted verbatim.
* **P3 falsified (H1b)** → the next separation is whether the order tracks the *ids*: the element
rows already carry `fid` and the ship id set, so "ascending fleet id" and "ascending minimum ship
id" are testable from the same log with no further run. If the order tracks neither address nor
id, the key is a word nothing in the save determines, and the follow-up is a watchpoint on the
comparator inside the gathering hub — a different lane and a different instrument.
* **P2 unmet (both runs on `724528ff…`)** → a third process, and no reading of the pair until one
of them reproduces run L.
### 2.5 What this lane cannot decide, whatever it says
One save, one procedure, one build, one route (**load**), one turn. It can say what the visit order
*is a function of* on this turn; it cannot name the container that produced it (that is stage 3, in
the gathering hub), it cannot speak about a turn that acquires fleets by a different gather block,
and it says nothing about lane BQ's per-*history* residue, which is a different input in the same
layer. The 3–40-turn fuse at `0x0069dbb0` cannot fire inside one turn of a load and is untested
here, exactly as in BP and BR.
---
## 3. The result
---
## 2.6 A second prediction, committed after run 1 and **before run 2's autosave existed**
Run 1 is in and it did two things at once: it **falsified P3 as written** and it named a **sharper
key** than P3 had. Rule 2 says a prediction exists to be disagreed with, and the honest move when a
measurement hands you a better model mid-lane is to commit the new model as a prediction *before*
the next run, not to present it afterwards as what was meant all along. So:
**What run 1 measured** (details and the full log in §3). The walk order over the acquired-fleet
vector is **not** ascending element address — 0 of 5 multi-element sequences. It is
**lexicographically ascending in `(fleet->Location*, fleet*)` — 5 of 5**, with the fleet pointer
breaking the tie in the one sequence where two fleets share a Location. Both keys are heap
addresses. And the newly minted fleet ids are **already on the fleet objects at claim time**, so
the id↔group pairing is decided *upstream* of this function and this walk inherits it.
**The committed prediction for run 2**, written at 07:26 UTC while run 2 was still loading:
> **Q1.** Run 2's `aivisit` log is again `(loc, elem)`-lexicographically ascending in every
> multi-element sequence, and again not ascending in `elem` alone.
>
> **Q2 — the cross-process test, and it is the one that matters.** Identify the two groups by
> their ships, which are process-independent: the **system-384 group is the fleet holding ship
> 6976**, the **system-80 group is the fleet holding ship 5264**. In run 1, `loc(384-group)` =
> `0x3205b040` < `loc(80-group)` = `0x3205eb90`, and the autosave was `e913ff41…` — the branch on
> which **384 is visited first and takes id 1970**. Therefore:
>
> * if run 2's autosave is `724528ff…` (the 80-first branch, `(80,1970) (384,1986)` in list 10),
> then in run 2 **`loc(80-group) < loc(384-group)`** — the two Location objects land in the
> opposite relative order;
> * if run 2's autosave is `e913ff41…` again, then **`loc(384-group) < loc(80-group)`** again.
>
> **Falsifier:** the ordering relation between the two Location addresses failing to agree with
> the branch. That would say the Location pointer is a correlate on one process and not the key,
> and would push the mechanism back toward H1b or toward some third key.
>
> **Q3.** The fleet ids carried on the elements at claim time follow the same relation: on the
> `e913ff41…` branch the fleet holding 6976 carries `fid=1970`; on the `724528ff…` branch it
> carries `fid=1986`.
### 3.0 Lead
**H1 is confirmed and P3 is falsified in the same measurement, which is the best outcome this lane
could have had.** The walk is **not** ordered by the element address — 0 of 4 multi-element
sequences, in either process. It is ordered by the element's **`Location*`** — 4 of 4, in both
processes, strictly ascending, with the fleet pointer breaking the one tie. Both keys are heap
addresses, and the container is therefore **address-keyed**: H1, with a sharper key than the
hypothesis was stated with.
The proof that it is the *address* and not some correlate of it is inside this lane and does not
depend on the branch changing: **two processes that wrote byte-identical autosaves visited the same
six fleets in two different orders**, and the two orders are exactly what ascending `Location*`
predicts in each process. Two transpositions moved; both moved with the addresses.
And the hook found something the static read had missed: **the newly minted fleet ids are already on
the fleet objects when this walk first touches them.** So `AssignFleetsAndIssueOrders` does not
decide the id↔group pairing — it *inherits* it, from a walk in the gathering hub that is ordered the
same way. That relocates the engine's canonicalisation point by one function and is stated in §4.2.
### 3.1 P0 / P1 / P2 — the instrument and the controls, checked before anything was read
Verified identically in both runs, from `shim.log`, before any output file was opened:
| check | run 1 | run 2 |
|---|---|---|
| `BUILD_ID` | `bu-55408a9-20260909T071101Z` | same |
| `exe base` | `0x005e0000` (ASLR delta +1966080), pid 9408 | same base, pid 5752 |
| registered hooks `mode=off` | 27 + `selftest: … mode=off` = **28 of 28** | same |
| `ignoring unknown key` | **0** | **0** |
| `COVERAGE:` | **0** | **0** |
| `aiseed call=1/2/3 … pinned=1` | 3 of 3, no `pinned=0`, no `PIN MODE WITH NO PINS`, no `deadbeef` | 3 of 3, same |
| natural seeds observed (then overwritten) | `4e82d22a` / `c6e8cff1` / `278fc9d2` | `a777d022` / `79bf6acf` / `b08b77ae` |
| `aivisit: … create=MH_OK enable=MH_OK` | **3 of 3** (`rva 0x002c16c0`, `0x002a4290`, `0x0029da10` → `va 0x008a16c0`, `0x00884290`, `0x0087da10`) | same |
Six more natural seeds, sharing no value with each other or with any of the twelve already on the
record — **lane L1's per-process seeding reproduced a sixth and seventh time**. And `exe base` is
`0x005e0000` for the **eighth** process on this guest, so "pointer order" here still means heap
order and nothing else.
**P1 holds.** Both runs: pre-turn `(Autosave EndTurn).sav` =
`16f63adbf383946db17c381998957172bca803406b7df78f3b03805fef0bbcb8` (100,758 B), post-turn
`(Autosave).sav` = `e913ff41188211c02d536da616337544e64133996cd7cc7a6521a8df1960226f` (101,375 B).
**No third file.** The rule-19 ambiguity the brief warned about did not fire, so this lane is in
neither the `k ≥ 3` case nor the instrument-perturbation case and the disambiguation run was not
needed on that account.
**P2 holds, and it is the strongest single fact about the instrument.** `cmp` — not just the
hash — says both runs' post-turn autosaves are **byte-identical to `verify/results/saves/bp-pinA-turn28.sav`**,
which is byte-identical to lane BP's **unpinned** run L. So a process carrying a seed pin **plus
three new detours inside the AI's fleet-assignment path** reproduced a natural, unpinned run of this
turn byte for byte, twice. That is rule 26 (d)'s control met, and it is also rule 19's control for
`aivisit`, taken rather than assumed. The pre-turn file is byte-identical to `bp-turn28-pre.sav` as
well.
Against BP's other branch, `state_checksum.py --floats bits --mask none` reports the familiar
`DIVERGED: 35 leaf difference(s)`, and the leaves are BP's transposition — including
`Flt[1986]/ships/Ship[5264]: only-in-A`, which is exactly what this lane's `aivisit` rows say
(`fid=1986 ships=[5264]`). **Hook → command block → save, end to end.**
### 3.2 P5 — the command block, under BP's proven noise mask
Comparator: lane BR's `cmp_aiorders.py`, unmodified, with BP's L/A-proven mask.
* **run 1 vs BP's run A** — 9 blocks, 81 elements, identical `(pid, list, idx)` key sets, identical
per-list sizes, identical SET gates, and `NONE. Every differing word is inside the proven noise
set.`
* **run 1 vs BP's run B** — same structure, and **exactly the three transposition words**
(`list 8 idx1 word0` 1970/1986, `list 10 idx0 word0` 80/384, `list 10 idx1 word0` 384/80).
* **run 1 vs run 2** — `NONE` outside the noise set, and the same list-10 order
`(384,1970) (80,1986) (384,2002) (816,7072)`.
**P5 holds in its strong form**, and P4 (the join) holds too: every `aivisit claim` row's ship-id
set matches exactly one list-10 element's group, and the mapping is injective —
`1970 ← [6976]`, `1986 ← [5264]`, `2002 ← [6992]`, `7072 ← [11 ships]`.
### 3.3 P3 — FALSIFIED as written, and replaced by a sharper key
The `aivisit claim` rows are the head loop's walk of the acquired-fleet vector as
`AcquireFleetsForTask` delivered it. Both runs, both multi-element calls:
**Run 1** (`e913ff41…`)
```
call=4 n=6 loc ascending: YES elem ascending: NO fid ascending: NO
elem=0x3254ba70 loc=0x3205b040 fid=1970 ships=[6976]
elem=0x3254ac90 loc=0x3205cba0 fid=1602 ships=[6080 5920 5936]
elem=0x3254c600 loc=0x3205eb90 fid=1986 ships=[5264]
elem=0x325497c0 loc=0x33ae5278 fid=354 ships=[2176 2352]
elem=0x3254cbc8 loc=0x33ae7020 fid=1570 ships=[5840 6144]
elem=0x3254a100 loc=0x33ae8938 fid=1346 ships=[5024]
call=7 n=2 loc ascending: YES elem ascending: NO fid ascending: NO
elem=0x3254b4a8 loc=0x0d7a5dc8 fid=7072 ships=[2720 2992 3456 …]
elem=0x32549698 loc=0x33ae6028 fid=258 ships=[1728]
```
**Run 2** (`e913ff41…`, byte-identical output)
```
call=4 n=6 loc ascending: YES elem ascending: NO fid ascending: NO
elem=0x326b4c98 loc=0x321c5708 fid=1602 ships=[6080 5920 5936]
elem=0x326b5010 loc=0x321c74b0 fid=1970 ships=[6976]
elem=0x326b5828 loc=0x321c7940 fid=1986 ships=[5264]
elem=0x326b3450 loc=0x338ab288 fid=354 ships=[2176 2352]
elem=0x326b2e88 loc=0x338ac4c8 fid=1346 ships=[5024]
elem=0x326b64e0 loc=0x338ae270 fid=1570 ships=[5840 6144]
call=7 n=2 loc ascending: YES elem ascending: NO fid ascending: NO
elem=0x326b6bd0 loc=0x0d85a3c0 fid=7072 ships=[2720 2992 3456 …]
elem=0x326b3578 loc=0x338ad278 fid=258 ships=[1728]
```
**Score, over the four multi-element claim sequences of the two processes:**
| candidate key | sequences ordered by it |
|---|---|
| `fleet->Location` (ascending) | **4 / 4** |
| `fleet` (the element address — P3's key) | **0 / 4** |
| `fleet->Id` | **0 / 4** |
| `systemId` | 0 / 4 (run 1 visits system 384 before system 80) |
| minimum ship id in the group | 0 / 4 (run 1 visits ship 6976's group before ship 5264's) |
So **P3 as written is falsified**, and so is every save-derived ascending key anybody has proposed:
the order is not ascending system id, not ascending minimum ship id, not ascending fleet id. What it
*is* ordered by is a heap address.
**The one tie, and what it says about the comparator.** In run 1 two fleets share a Location
(1970 and 2002 are both at system 384, `loc=0x3205b040` for both — the Location object is
**interned per system**, not per fleet). The sequence that contains both is ordered
`2002 (elem 0x3254b948)` then `1970 (elem 0x3254ba70)` — **ascending element address within the
tie**. So the observed order is lexicographic in **`(Location*, StarFleet*)`**: a primary key that
is a heap address and a tie-break that is another one. That is the signature of a `std::map`/`set`
keyed on the location with the object as the disambiguator, or of a sort with that comparator.
**A caveat on which rows to read, recorded because it nearly cost a wrong number.** The `rel` (tail
loop) sequences are **not** all sorted, and they should not be: the tail loop runs after the element
loop, and the element loop can *extend* the vector — in `call=5` the vector arrives with one element
and the tail loop walks seven. Those sequences read `[the newly added element] + [the delivered
order]`, which is not the container's order and is not evidence about it. The claim rows are the
head loop, which runs before anything is added, and those are the four sequences scored above. The
tail rows are still worth their place: on every call whose vector did **not** grow they reproduce
the claim order exactly, which is the self-check that the vector is not permuted during the walk.
### 3.4 The cross-process test, and it is decided *without* needing the branch to flip
Runs 1 and 2 wrote **byte-identical autosaves**. They nevertheless **visited the same six fleets in
two different orders**:
```
run 1 : 1970 1602 1986 354 1570 1346
run 2 : 1602 1970 1986 354 1346 1570
```
Two transpositions — `(1970, 1602)` and `(1570, 1346)` — and **both are exactly the pairs whose
Location addresses exchanged relative order between the two processes**:
| group | run 1 `loc` | run 2 `loc` | relative order |
|---|---|---|---|
| fleet 1970 (ship 6976, system 384) | `0x3205b040` | `0x321c74b0` | **before** 1602 in run 1, **after** it in run 2 |
| fleet 1602 (ships 6080/5920/5936) | `0x3205cba0` | `0x321c5708` | — |
| fleet 1570 (ships 5840/6144) | `0x33ae7020` | `0x338ae270` | **before** 1346 in run 1, **after** it in run 2 |
| fleet 1346 (ship 5024) | `0x33ae8938` | `0x338ac4c8` | — |
**Nothing in the save changed. Nothing in the seed changed. Two heap addresses exchanged order, and
the visit order followed them, twice, in the same process pair.** That is the measurement the
resolution's §2.2 called a precondition, now observed *as the cause* on the actual walk.
It also explains, for free, why the two runs' autosaves are identical anyway: 1602, 1346 and 1570
are pre-existing fleets, and reordering them changes no id assignment. **The transposition reaches
the save only when the pair that swaps is a pair of groups that need a new id** — which on this turn
is the `{6976 @ 384, 5264 @ 80}` pair, and that pair kept its order in both of these processes
(`loc(6976-group) < loc(5264-group)` in run 1 and in run 2), which is why both landed on
`e913ff41…`. **Q2's second clause therefore holds as predicted, and Q3 with it** (the fleet holding
6976 carries `fid=1970` in both).
### 3.5 Run 3 — the branch flipped, and the Location addresses flipped with it
Runs 1 and 2 both landed on `e913ff41…`, so the *cross-branch* form of Q2 had no sample. A third
fresh process was taken for exactly that, with the identical instrument and the identical config —
this is the sample the two-process design could not guarantee, not a change of procedure.
**Run 3's `(Autosave).sav` = `724528ffde9a7514b69ab9e82ed3b8c8ade63d622c0720de30503a73a92df11e`,
byte-identical (`cmp`) to `verify/results/saves/bp-pinB-turn28.sav`** — BP's run B, the *other*
branch. Pre-turn `16f63adb…` as always. Instrument identical and verified the same way: build
`bu-55408a9`, `exe base=0x005e0000` (pid 480), 28 of 28 hooks off, zero unknown keys, zero
`COVERAGE:`, three of three `pinned=1` (three more fresh natural seeds: `39037bfa` / `2a58e189` /
`00640d2c`), three `aivisit` detours `MH_OK`. Its block against BP's run B: `NONE` outside the noise
set, list 10 `(80,1970) (384,1986) (384,2002) (816,7072)`, list 8 `idx0=2002 idx1=1970 idx2=1538` —
identical.
**And the walk:**
```
call=4 n=6 loc ascending: YES elem ascending: NO fid ascending: NO
elem=0x338cff48 loc=0x333ed4c8 fid=1970 ships=[5264] <- the SYSTEM-80 group, FIRST
elem=0x338d0510 loc=0x333ed710 fid=1986 ships=[6976] <- the SYSTEM-384 group, SECOND
elem=0x338cf4e0 loc=0x333eede0 fid=1602 ships=[6080 5920 5936]
elem=0x338d5e18 loc=0x34cc4958 fid=354 ships=[2176 2352]
elem=0x338cf168 loc=0x34cc6948 fid=1570 ships=[5840 6144]
elem=0x338d69a8 loc=0x34cc7268 fid=1346 ships=[5024]
```
**This is the prediction, hit exactly:**
| process | `loc` of the 384-group (ship 6976) | `loc` of the 80-group (ship 5264) | relation | visited first | `(Autosave).sav` |
|---|---|---|---|---|---|
| run 1 | `0x3205b040` | `0x3205eb90` | 384 **<** 80 | 384 | `e913ff41…` |
| run 2 | `0x321c74b0` | `0x321c7940` | 384 **<** 80 | 384 | `e913ff41…` |
| **run 3** | `0x333ed710` | `0x333ed4c8` | **80 < 384** | **80** | **`724528ff…`** |
**The branch of the outcome set is the relative order of two heap addresses.** Q2's first clause was
committed before run 3 existed and it is met. Q3 with it: on this branch the fleet holding ship
6976 carries `fid=1986` and the one holding 5264 carries `fid=1970`, which is the transposition, and
`state_checksum` finds the same 35 leaves against run 1 that BP found between A and B.
**Three processes, three distinct visit orders of the same six fleets, all ascending by `Location*`:**
```
run 1 : 1970 1602 1986 354 1570 1346
run 2 : 1602 1970 1986 354 1346 1570
run 3 : 1970 1986 1602 354 1570 1346
```
Final score over the **six** multi-element claim sequences of the three processes: ordered by
`fleet->Location` **6 / 6**; by the element address **1 / 6** (a two-element coincidence in run 3);
by fleet id **0 / 6**; by system id **0 / 6**; by minimum ship id **0 / 6**.
### 3.6 The thing the static read missed, and it moves the engine's fix
**The fleets carry their new ids before this walk ever touches them.** `fid=1970` is already on the
element at `idx=0` of the very first burst of `claim` rows — the head loop, which runs at function
entry before the pass gate and before any emission. So `AssignFleetsAndIssueOrders` does **not**
mint, and does not decide which group gets which id. It receives a vector that is already ordered by
`Location*` and already carries the pairing, walks it in that order, and writes the order into
list 10.
Two things follow, and both matter more than the hook did:
1. **The mint is upstream, in whatever produced the vector** — `AcquireFleetsForTask 0x006ceef0`
and its three gather blocks (`0x006abf80`, `0x006b7c90`, `0x006cb310`). Since the delivered
vector is Location-ordered in six of six samples and the ids follow that order, the natural
reading is that the *same* address-keyed container orders the gather, and the mint happens as it
is walked. **That is an inference, not a measurement** — this lane did not hook the gather — and
§4.3 says what would settle it.
2. **The Location object is interned per system, not per fleet.** Run 1's two fleets at system 384
(1970 and 2002) share `loc=0x3205b040`. So the key is one address per *location*, and on this
turn the two competing groups are at two different systems, which is why the whole residue is a
single binary choice.
### 3.7 What the hook cost, and the timings
`aivisit` writes 40 lines on this turn — ten `AssignFleetsAndIssueOrders` calls, of which three are
pass 0 with empty vectors, and 18 element rows. `aivisit: calls=10 claim_rows=… rel_rows=…` closes
every log. End Turn with this hook set was **~10 s** in run 1 (End Turn 03:21:07, autosave 03:21:13)
and comparable in runs 2 and 3 — BP's "~35 s" is generous, BR's "~4 s" was with one fewer module.
---
## 4. Verdict
### 4.1 H1 vs H1b
> **H1 IS CONFIRMED. The fleet-assignment pass walks an address-keyed order, and the key is
> named: the element's `Location*`, with the element pointer breaking ties.**
>
> Six of six multi-element sequences across three fresh processes are ordered by
> `fleet->Location` ascending. Zero of six are ordered by the element address alone (so **P3, the
> committed prediction, is falsified as written**), zero by fleet id, zero by system id, zero by
> minimum ship id. Three processes produced three *different* orders of the same six fleets, and
> every difference is a pair whose Location addresses exchanged relative order. And the two
> branches of the campaign's outcome set are the two relative orders of one pair of Location
> objects: `loc(384-group) < loc(80-group)` → `e913ff41…` (twice), `loc(80-group) <
> loc(384-group)` → `724528ff…` (once), with the autosaves byte-identical to lane BP's A and B.
>
> **H1b — an uninitialised-word comparator — is excluded on this path**, and not by preference: an
> uninitialised key would not reproduce a strict total order by a *specific* live pointer field in
> six of six sequences, and it would not track that field across a branch flip. It remains a live
> hypothesis for other paths; it is dead for this one.
The residue is therefore **not** noise and **not** an unbounded scramble. It is a deterministic
function of the process's heap layout: **the outcome set of visit orders is the set of orderings of
the relevant Location pointers**, which on `ad-turn27` is two, because two groups compete and their
Location objects can land either way round.
### 4.2 What it implies for the engine
The resolver's **deliverable 2** — `game/ai` visits groups in a canonical order that is a function
of the save alone, ascending `systemId` then ascending minimum ship id — **is confirmed as the right
shape, and this lane can now say what it is and is not.**
* **It is right that a canonical order is needed and that the original's order cannot be
reproduced.** The original's key is a heap address. Nothing in the save determines it, and nothing
in our engine can or should reproduce it. Canonicalisation is not a convenience here; it is the
only option.
* **It is right that our order will be one member of the original's outcome set.** The outcome set
is *exactly* the set of orderings of the competing groups, with no other constraint — measured, not
assumed — so any total order over the groups is a member. Ascending `systemId` then minimum ship
id is a member.
* **The one place the deliverable should change, and it is a change of level, not of order.**
Deliverable 2 places the canonical visit at "the point where a task forms a fleet from a ship
group", inside `AssignFleetsAndIssueOrders`. **The measurement says the pairing is already fixed
when that function is entered.** So the canonical order has to be imposed **where the group set is
gathered and the ids are minted** — the engine's analogue of `AcquireFleetsForTask` — and
`AssignFleets` must then preserve it. If the engine canonicalises only at the assignment step it
will canonicalise the *emission* order while leaving the *minting* order to whatever its own
container does, which is the same bug one level up.
* **Should the canonical order be a different one?** On the evidence, no — and this lane has
evidence rather than convention, which is what the brief asked for. The original's typical
behaviour is *not* ascending `systemId` (run 1 and run 2 visit system 384 before system 80) and
*not* ascending minimum ship id (they visit ship 6976's group before ship 5264's). It is not any
save-derived order, in either direction, in any of the three processes. **There is no order for us
to match**, so the criterion collapses to "pick a total order that is cheap, stable and obviously
a function of the save" — and `(systemId, min ship id)` is exactly that. The one refinement worth
writing into `id-allocation.md`: the tie-break matters, because the original's own tie-break is
the second pointer, so two groups at the *same* location must still be totally ordered by us;
minimum ship id does that.
* **Deliverable 3 (the captured-order override) is unaffected and is still the exactness knob.**
List 10 records the original's order on every captured run, and this lane confirmed the chain
block → save end to end in both branches.
### 4.3 What this lane did NOT establish
* **It did not hook the mint.** That the gather is ordered by the same key is an **inference** from
six ordered samples plus the ids already being present; it is not measured. The measurement that
would settle it is one more detour on `AcquireFleetsForTask 0x006ceef0` (or on the client-side
fleet-create the mint sits behind) logging `(group, minted id)` at the moment of minting. That is
stage 3 and it is small.
* **It did not identify the container.** "Ordered by `Location*`" is a property of the delivered
vector, not a name. Whether it is a `std::map<Location*, …>`, a `std::set<StarFleet*>` with a
location comparator, or an explicit sort is a bounded static read of the three gather blocks —
worth taking now that the read has a key to look for, which it did not before.
* **One turn, one save, one route (load), one build.** The claim "the visit order is address-keyed"
is a claim about this walk on this turn. Nothing here speaks to a turn whose fleets are acquired
through a different gather block.
* **Nothing about lane BQ's per-*history* residue**, which is a different input in the same layer.
* The 3–40-turn fuse at `0x0069dbb0` cannot fire inside one turn of a load and is untested here.
* **`aivisit`'s neutrality is measured on this state and nowhere else** — three processes, two of
them byte-identical to an unpinned natural run and the third byte-identical to BP's other branch.
That is the strongest form of rule 19's control available for a hook that cannot be removed from a
pinned run, and it is still a statement about one workload.
---
## 5. Guest notes for the next lane (VM146)
* **Released restored and hash-verified** — see §6.
* **The direct QEMU screendump is the right instrument and this lane confirms BR's note**:
`ssh spicy "echo 'screendump /tmp/x.png -f png' | qm monitor 146"` then `scp`. Twenty-odd
screenshots, zero ambiguous frames, zero wasted clicks. **`tools/vmshot.py` still cannot be used
from this WSL host** (no PIL), so the one-liner is the path.
* **PowerShell script execution is disabled on the guest for files, but `-ExecutionPolicy Bypass
-File` works and inline `ssh re@… '<powershell>'` works.** `C:\SOTS\ui\bphash.ps1` fails if you
run it as a bare `.ps1`; run helpers as
`powershell -NoProfile -ExecutionPolicy Bypass -File <path>`.
* **`scp` cannot fetch two remote files in one command with quoted parenthesised names** — it
returns `protocol error: filename does not match request`. Copy `(Autosave).sav` to a plain name
on the guest first, then `scp` that.
* Load-dialog geometry **re-verified from a screenshot in all three runs, unchanged**: 7 rows from
y=260 at 29 px pitch, `ad-turn27-two-raiders` is **row 1 at (400,260)**. Path: Load Game
(512,536) → Single Player (512,290) → OK (551,523) → row (400,260) → OK (682,624) → Launch
(511,663) → End Turn (100,714). One click per helper call, screenshot between.
* **Timings, three runs.** Main menu ~2 min 45 s from `schtasks /Run /TN SOTS` (`fg` + `key {ESC}`
×3 spaced 2.5 s, screenshot-verified, never slept-and-clicked); click path ~1 min at one click per
call; **turn-27 load 1 min 35 s / 1 min 30 s / 1 min 25 s** — much faster than BP's 3.5–4 min
once the guest is warm, so budget the *first* load long and the rest short; **End Turn ~6–10 s**
with this hook set. Three complete runs in **19 minutes** of guest time.
* **The AI client seeds are minted on the LOAD path, not at End Turn.** `aiseed call=1/2/3` appears
in `shim.log` about 40 s *before* the map is drawn. It is a useful "the load is nearly done" signal
and it is worth knowing before somebody reads a seed line as evidence that the turn ran.
* Left in place: `C:\SOTS\shimdist-bu` (`binkw32.dll`, `BUILD_ID`, `shim.cfg.bupin`),
`C:\SOTS\ui\budeploy.ps1`, `C:\SOTS\ui\preBU\` (the pre-BU autosave pair), and
`C:\SOTS\ui\bu{1,2,3}-turn28*.sav`. BR's `shim.cfg.{brprobe,brdeep}` and files untouched;
`click_helper.ps1` and BP's helpers untouched.
---
## 6. Release
**VM146 released restored and hash-verified**, exactly as found:
* `C:\SOTS\SavedGames` back to the 9-file pre-BU set, both autosaves byte-identical to
`C:\SOTS\ui\preBU\` and to BR's published values — `(Autosave EndTurn).sav` `a3f9dc4b…`,
`(Autosave).sav` `d59bb9f2…`;
* `ad-turn27-two-raiders.sav` still on disk at `1c8baa27…` (so the Load-dialog geometry above still
holds);
* `C:\SOTS\shim.cfg` = `hooks=off`;
* `C:\SOTS\binkw32.dll` = `07c962946d93c2cfef336803ac041150bda2640c05980b266902b1aa1a618359`, the
pre-BP file, restored from `C:\SOTS\ui\preBP\binkw32.dll`. **Note for the next lane: the file to
restore is that one, not `binkw32_real.dll` (`603d0a96…`)** — I restored the real DLL first and
had to correct it, which is worth one line so nobody repeats it;
* `C:\SOTS\shim.trace.jsonl` removed; game process **not running**.
**Artefacts:** `verify/results/shim/bu/` — `BU{1,2,3}-shim.log`, `BU{1,2,3}-aivisit.txt`,
`BU{1,2,3}-aiorders.txt`, `claimcheck.py` (the ordering test), `claim-order.txt` (its output over
all three runs), `shim.cfg.bupin`. **The three output saves are byte-identical to
`verify/results/saves/bp-pinA-turn28.sav` (runs 1 and 2) and `bp-pinB-turn28.sav` (run 3) and are
deliberately not duplicated in the repo** — verified with `cmp`, not with a hash alone.
**Engine:** `sots-engine` branch `wip/bu`, commit `55408a9` — `src/shim/hooks/ai_visit.{h,cpp}`,
the `main.cpp` and `CMakeLists.txt` wiring, `src/shim/shim.cfg.bupin` (marked `# exhaustive`), and
the regenerated `include/generated/sots_addresses.h`. `tools/clean_room_check.sh`: OK.
`tools/check_shim_configs.py`: OK. Address fragment: `ghidra/addresses.d/bu.json`.
---
## Proposed board rows
New row:
```
| THE AI'S FLEET-VISIT ORDER IS AN ADDRESS-KEYED CONTAINER AND THE KEY IS THE FLEET'S `Location*` - H1 CONFIRMED, H1b DEAD, and the campaign's two-file outcome set IS the two orderings of two heap pointers | control-flow | verified | high | 100% | 2026-09-09 | Lane BU, VM146, stage 2 of the chain lane BR unblocked (`findings/control-flow/fleet-visit-order-mechanism.md`). Build `bu-55408a9-20260909T071101Z`, fresh CT111 tree (rule 24), lane worktree (rule 21), `shim.cfg.bupin` = `shim.cfg.bppin` with EXACTLY THREE non-comment lines added (`aivisit=on/.out/.ships`), diffed against the real bppin PULLED OFF THE GUEST and quoted in the finding, marked `# exhaustive` so `tools/check_shim_configs.py` ENFORCES that all 27 registered hooks are named. THREE fresh processes, route LOAD, seeds pinned to lane L's observed values; 28 of 28 hooks off, zero unknown keys, zero COVERAGE, 3/3 `pinned=1` in every run, three `aivisit ... MH_OK` in every run, `exe base=0x005e0000` for the 8th process on this guest. THE HOOK: `AssignFleetsAndIssueOrders 0x006c16c0` walks a `std::vector<StarFleet*>` by index (cursor `[ebp-0x38]`, `_Mylast` REFETCHED every iteration, back-edge `jne 0x6c17e0` at 0x006c21be), and that walk is BRACKETED BY TWO LOOPS OVER THE SAME VECTOR that call one function each - **`StrategyAIAgent::ClaimShipsOfFleet 0x006a4290` and `ReleaseShipsOfFleet 0x0069da10`, which have EXACTLY ONE CALLER EACH IN THE WHOLE IMAGE and it is that function**. So the instrument is two ordinary function-entry detours plus a bracket on 0x006c16c0, with NO return-address filter and NO mid-function patch - the loop body could not be patched anyway, its first instruction 0x006c17e6 IS a branch target. **RESULT: the visit order is NOT ascending element address (0 of 6 multi-element sequences over three processes), NOT ascending fleet id (0/6), NOT ascending systemId (0/6), NOT ascending minimum ship id (0/6). It is ASCENDING `fleet->Location` (StarFleet+0xa0): 6 OF 6**, with the fleet pointer breaking the one tie (two fleets at system 384 share one interned Location object and are ordered by element address within it). So the container is address-keyed and the comparator is lexicographic in (Location*, StarFleet*), BOTH heap addresses. **P3 - the committed prediction, "the visit order is ascending element address" - IS FALSIFIED, and the hypothesis class it tested is CONFIRMED with a sharper key.** THE CROSS-PROCESS PROOF, and it does not rest on one sample: three processes visited THE SAME SIX FLEETS IN THREE DIFFERENT ORDERS (run1 1970/1602/1986/354/1570/1346; run2 1602/1970/1986/354/1346/1570; run3 1970/1986/1602/354/1570/1346) and EVERY difference is a pair whose Location addresses exchanged relative order. Runs 1 and 2 wrote BYTE-IDENTICAL autosaves and still visited in different orders - the reordered pairs were pre-existing fleets, which is why the save did not move. **AND THE BRANCH FLIP IS MEASURED: run 3's `loc(system-80 group) = 0x333ed4c8 < loc(system-384 group) = 0x333ed710`, the OPPOSITE of runs 1 and 2, it visited system 80 first, and its autosave is `724528ff...` BYTE-IDENTICAL (cmp) to lane BP's run B - while runs 1 and 2 have `loc(384) < loc(80)`, visit 384 first, and are BYTE-IDENTICAL to BP's runs A and L.** So the campaign's two-file outcome set on `ad-turn27` IS the two orderings of two heap pointers. **H1b (an uninitialised-word comparator) is EXCLUDED for this path**: an uninitialised key cannot reproduce a strict total order by a specific live pointer field in 6 of 6 sequences and cannot track that field across a branch flip. **A SECOND FINDING THE STATIC READ HAD MISSED: the newly minted fleet ids are ALREADY ON THE FLEET OBJECTS when the head loop first touches them** (`fid=1970` on the element at idx=0 of the first burst, before the pass gate and before any emission), so `AssignFleetsAndIssueOrders` INHERITS the id-to-group pairing rather than deciding it; the mint is upstream in the gather (`AcquireFleetsForTask 0x006ceef0` + 0x006abf80/0x006b7c90/0x006cb310), and since the delivered vector is Location-ordered and the ids follow that order, the natural reading is that the SAME container orders the mint (INFERENCE - the gather was not hooked). ENGINE CONSEQUENCE, and it MOVES deliverable 2 by one level: the canonical order must be imposed WHERE THE GROUP SET IS GATHERED AND THE IDS ARE MINTED, not at the assignment step, or the engine canonicalises the emission order while leaving the minting order to its own container. On the choice of canonical order this lane has evidence rather than convention: the original's order is NOT ascending systemId and NOT ascending minimum ship id in ANY of the three processes, so THERE IS NO ORIGINAL ORDER TO MATCH and `(systemId, min ship id)` is as good as any - keep it, and keep the tie-break, because the original's own tie-break is a second pointer. RULE 26(d) AND RULE 19, BOTH MET BY MEASUREMENT: runs 1 and 2 are BYTE-IDENTICAL to lane BP's UNPINNED run L, so a process carrying a seed pin PLUS three new detours inside the AI's fleet-assignment path reproduced a natural run byte for byte, twice; run 3 reproduced BP's other branch byte for byte. `aivisit` is behaviourally neutral on this state, measured, not argued. P1/P2/P4/P5 all hold; under BP's noise mask every run's block differs from BP's same-branch run in NOTHING outside the noise set and from the other branch in EXACTLY the three transposition words. Rule 17 discharged: 0x006c16c0's body is 0x006c16c0-0x006c247a `ret` with a FOUR-ENTRY JUMP TABLE living PAST the ret at 0x006c247c (target of `jmp [eax*4+0x6c247c]` at 0x006c22fb). Six more per-process AI seeds, none shared - L1 reproduced a 6th and 7th time. Artefacts: `verify/results/shim/bu/`; the three output saves are byte-identical to `bp-pinA-turn28.sav` x2 and `bp-pinB-turn28.sav` x1 and are deliberately not duplicated |
```
Edits to existing rows:
- **Row 62** — VM146: **lane BU held it 2026-09-09 03:15–03:40 local for the container hook (three
runs) and released it restored** and hash-verified: `SavedGames` back to the 9-file pre-BU set
with both autosaves byte-identical to `C:\SOTS\ui\preBU\` (`a3f9dc4b…`, `d59bb9f2…`), `shim.cfg`
`hooks=off`, `binkw32.dll` `07c96294…`, `shim.trace.jsonl` removed, game process **not running,
exactly as found**. Load-dialog geometry **re-verified by screenshot in all three runs and
unchanged**: 7 rows from y=260 at 29 px pitch, row 1 at (400,260). Left `C:\SOTS\shimdist-bu`,
`C:\SOTS\ui\budeploy.ps1`, `C:\SOTS\ui\preBU\` and `C:\SOTS\ui\bu{1,2,3}-turn28*.sav`; BR's and
BP's files untouched. **THREE NEW GUEST GOTCHAS.** (1) **The file to restore over
`C:\SOTS\binkw32.dll` is `C:\SOTS\ui\preBP\binkw32.dll` (`07c96294…`), NOT `binkw32_real.dll`
(`603d0a96…`)** — I restored the real DLL first and had to correct it. (2) **PowerShell script
execution is disabled for `.ps1` files**: `C:\SOTS\ui\bphash.ps1` fails as a bare path; run
helpers as `powershell -NoProfile -ExecutionPolicy Bypass -File <path>`, or send inline
PowerShell over ssh. (3) **`scp` refuses two remote files in one command when the names carry
parentheses** (`protocol error: filename does not match request`) — copy `(Autosave).sav` to a
plain name on the guest first. **TIMINGS, THREE RUNS, and they revise BP's:** main menu ~2 min
45 s; click path ~1 min at one click per helper call; **the turn-27 load was 1 min 35 s / 1 min
30 s / 1 min 25 s** — BP's "3.5–4 min and not compressible" is the *cold* figure, and a warm
guest is less than half that; **End Turn ~6–10 s**. **Three complete runs in 19 minutes of guest
time.** **And: the three AI client seeds are minted on the LOAD path** — `aiseed call=1/2/3`
lands in `shim.log` about 40 s *before* the map is drawn, which is a useful "load nearly done"
signal and must not be read as evidence that the turn ran.
- **Row 360** — append: `MECHANISM NAMED 2026-09-09 (lane BU, VM146): the residual visit order is an ADDRESS-KEYED CONTAINER walked in ascending `fleet->Location` (StarFleet+0xa0), with the fleet pointer as tie-break - 6 of 6 multi-element sequences over three processes, versus 0 of 6 for element address, fleet id, systemId or minimum ship id. Three processes gave three different orders of the same six fleets and every difference is a pair whose Location addresses exchanged order. The two-file outcome set IS the two orderings of two heap pointers: loc(384-group) < loc(80-group) -> e913ff41 (x2), the reverse -> 724528ff (x1), each byte-identical to lane BP's A/L and B.`
- **Row 415** — append: `And the 35 leaves are now explained end to end: the visit order is ascending Location pointer, so which of the two new fleets gets 1970 is decided by which of two heap objects has the lower address in that process (lane BU, 2026-09-09).`
- **Row 333 / the Rung C row** — append: `MECHANISM SETTLED 2026-09-09 (lane BU): the residual input is a heap address ordering, so it is not reproducible by construction and canonicalisation is the ONLY option, not a convenience. The engine's canonical order must be imposed WHERE THE GROUPS ARE GATHERED AND THE IDS MINTED, not at the assignment step - lane BU measured that the fleets already carry their new ids when AssignFleetsAndIssueOrders first touches them.`
- **Row 326** — append: `Lane BU reproduced per-process seeding a 6th and 7th time (six more distinct natural words across three processes on ad-turn27, none shared).`
- **The resolver's H1/H1b row (2026-09-09 fleet-id-order-residue)** — append: `RESOLVED 2026-09-09 (lane BU): H1 CONFIRMED with the key named (fleet->Location, tie-break the fleet pointer); H1b excluded on this path. The resolution's section 2.2 precondition is now observed AS THE CAUSE on the actual walk.`
Docs to correct in place (rule 11), proposed, not done by this lane:
- `findings/resolutions/2026-09-09-fleet-id-order-residue.md` §3 deliverable 2 — the canonical
visit belongs at the **gather / mint**, not at `AssignFleetsAndIssueOrders`; the fleets already
carry their ids at that function's entry. §4's H1 row can be marked confirmed with the key.
- `findings/subsystems/id-allocation.md` §5 — add: the client-side fleet id is minted in the order
an address-keyed container delivers the groups (ascending `Location*`), so the mapping id→group
is per-process in the original; the engine mints in a canonical order and must do so **at the
gather**.
- `findings/subsystems/ai-stepping-and-passes.md` — `AssignFleetsAndIssueOrders` gains the two new
per-element callees and the note that its walked vector arrives Location-ordered.
- `guides/lab-screen-wall.md` — add the two guest gotchas above (`-ExecutionPolicy Bypass -File`,
and `scp` with parenthesised names).