diff --git a/findings/control-flow/ai-visit-order-probe.md b/findings/control-flow/ai-visit-order-probe.md new file mode 100644 index 0000000..2ef9c07 --- /dev/null +++ b/findings/control-flow/ai-visit-order-probe.md @@ -0,0 +1,114 @@ +# Is the residual fleet visit order an RNG draw or heap order? — the per-client draw ledger on `ad-turn27` + +- **Owner / date:** lane BR · 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. VM145 free and untouched. +- **Trigger:** `findings/resolutions/2026-09-09-fleet-id-order-residue.md` §4 / §8 — the single probe + the resolver chose over two alternatives, because it is simultaneously **run C** (a third pinned + sample, sizing the residual outcome set) and **probe 3** (the per-client draw ledger). +- **Build:** `bp-989c692-20260909T0505Z`, lane BP's dist, still deployed at `C:\SOTS\shimdist-bp` + (`binkw32.dll` sha256 `9de1c57a38e66800a9487f46f7e7abc4fea6efc05cd72319d911733e9d971f47`). + **No build for this lane** — the resolution costed it at "one fresh process, no build". +- **Input:** `C:\SOTS\SavedGames\ad-turn27-two-raiders.sav`, sha256 + `1c8baa27680809d585ab1728391e82729642d32aaedffd5bf95b616f0a11a436`, 100,809 B — **re-verified on + this guest before the run**, equal to lane AD's and lane BP's published value. +- **Route:** **load** (`ad-turn27-two-raiders.sav` → one End Turn), the same route as lane BP's three + runs. This lane holds the route fixed, so it is unconfounded by lane BQ's per-*history* input + (resolution §9.1). + +--- + +## 0. The question, in one paragraph + +Lane BP pinned all three AI client seeds on the campaign's richest AI turn and two fresh processes +still disagreed — but only in **35 leaves of 61,147**, and all 35 are a single transposition: two +newly formed fleets exchange contents. The resolver decoded the ids and changed the frame: the ids +are not the variable (1970/1986/2002 are Player[32]'s own client, counters 123/124/125, minted in +that order in every process), the **visit order over the ship groups needing a new fleet** is. Two +mechanisms remain, and only two. **H2:** a draw on the per-process global generator at +`0x00af6e58`, which the seed pin does not touch and which already has two unnamed AI-band consumers +in the record (`0x0079ea90+0x73`, `0x005b9f00+0xc0`). **H1/H1b:** an address-keyed container (or an +unstable comparator) walked in heap order — for which BP's own `aielem` node addresses supply a +*measured precondition* (same-size allocations landing in different relative orders in two processes +with identical decisions), the image base being constant on both guests (lane BQ). This probe +separates them by measurement: `airng=on` brackets each AI client's whole turn at +`StrategyClient::OnResumePlaying` and attributes **every draw on every generator** inside it to a +return address and a generator address. Lane I's inlined-draw inventory places **no** inlined draw +site between `0x680000` and `0x6e0000`, so the observer is complete in the AI band. + +--- + +## 1. Predictions, committed before the run + +These are the resolution's §4 **P1–P4**, restated verbatim in substance, with the falsifier of each +named. They are committed in their own commit, with no result in the file. + +> **P1 (run C — the outcome set).** `(Autosave).sav` ∈ {`e913ff41188211c02d536da616337544e64133996cd7cc7a6521a8df1960226f`, +> `724528ffde9a7514b69ab9e82ed3b8c8ade63d622c0720de30503a73a92df11e`} — lane BP's A/L file or its B +> file. **A third file does not immediately mean either thing**: it is ambiguous between the order +> outcome set exceeding 2 (`k ≥ 3`) and the `airng` detour perturbing the run. The resolution names +> the disambiguation and this lane will run it if the guest allows: one more run of +> **`shim.cfg.bppin`** (`airng=off`, plain run C). Under the `k ≥ 3` reading that plain 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. +> +> **P2 (the headline — probe 3).** In Player[32]'s `OnResumePlaying` bracket: +> `foreign_words=0`, `foreign_calls=0`, `residual=0`, and **no `airngcensus` row whose `rng` is +> anything but Player[32]'s own client generator at `client+0x134`** — in particular none on +> `0x00af6e58` and none on `Seed` record #2's object. The same for clients 496 and 512. +> +> **P3 (the ledger's shape — not load-bearing, recorded so it can be disagreed with).** Player[32]'s +> own generator spends **single-digit words** inside its bracket, at return addresses in the AI band +> (`0x0068xxxx`–`0x006exxxx` RVA-wise, i.e. `va` in `0x0068_0000`–`0x006e_0000` at +> `exe base = 0x005e0000`); clients 496 and 512 spend **fewer** than Player[32]. +> +> **P4 (the block).** The `aiorders` dump differs from lane BP's runs A and B **only inside the +> proven noise set** (the L/A mask: list 1 → words 1, 5, 8, 11; list 3 → 4–11; list 5 → 8–11; +> list 7 → 2–11; list 8 → 1–11; list 10 → 2–11; list 12 → 5–11; list 14 → 2–11; list 23 → 2–11) — +> plus, **at most**, the three-word transposition (`list 8 idx=1 word0`, `list 10 idx=0 word0`, +> `list 10 idx=1 word0`). Structure identical: 9 blocks, 81 elements, the same `(pid, list, idx)` +> sequence, 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}`. + +### 1.1 How each prediction could be wrong, and the symptom + +| way | symptom | what it would mean | +|---|---|---| +| **H2 is right** — some AI-band site draws on the global generator or on another object | ≥ 1 `airngcensus` row with `rng ≠ client+0x134` inside a bracket | **P2 falsified, and that is a success.** The row's `ret_rva`/`va` *names the consumer*. Follow-up is a global pin (`RNG_Seed(0xaf6e58, K)` after `Application::Initialize`), two fresh processes, predict byte-identical. **Stop and post the address.** | +| **H1/H1b is right** | `foreign_words=0` everywhere; the census carries only the client's own generator | **P2 holds.** RNG is excluded *by measurement* for the AI turn, and stage 2 becomes one bounded static read to place a hook on the group walk. **Stop. Do not read the binary. Do not start stage 2.** | +| an **inlined** draw on the AI path that the seven entry-point detours cannot see | `residual > 0` on a bracket (`left_delta > observed`) | P2 falsified in its `residual=0` clause. The residual is a per-generator number measured off the object at `+0x9c8`, so it reports the invisible draw's *existence* without its address; the follow-up would be an image-wide immediate scan restricted to the AI band, which lane I already ran and which found nothing there — so this outcome would also contradict lane I. | +| the **`airng` detour perturbs the turn** (rule 19) | a third autosave file | Not separable from `k ≥ 3` by this run alone — see P1. **This is the caution the resolver put on the record and it will be reported as ambiguity, not resolved by preference.** The disambiguation is one plain `shim.cfg.bppin` run. | +| the pin is **half-applied** | any `aiseed call=… pinned=0`, or a `PIN MODE WITH NO PINS` line, or a `used=deadbeef` wildcard hit | The run is **void** and nothing is read from it. Checked in `shim.log` *before* any output file is opened. | +| a **stale or wrong binary** (rule 24) | `BUILD_ID` ≠ `bp-989c692-20260909T0505Z` in `shim.log` | Void. No build is done by this lane, so the only failure mode is deploying the wrong dist. | +| the **config** carries an inherited defect | a live line inside what was meant to be a comment; an unnamed registered hook silently installed | Defended by the check that caught it in BP's own first draft: **the non-comment diff of `shim.cfg.bppin` against `shim.cfg.brprobe`, quoted in §2.1**, must be exactly three lines. | +| the **click path** lands somewhere else | wrong screen in a screenshot | Every step screenshotted and verified; never sleep-and-click. And the wall's `/shot/.png` is a **cached frame up to ~5 s stale** (BP's gotcha, four wasted clicks) — wait 12–16 s or take two shots and use the second. | + +### 1.2 What this probe cannot decide, whatever it says + +It is one save, one procedure, one build, one route (**load**), and — for the ledger half — the +turn's *own* AI band. A `foreign_words=0` result excludes a draw **inside the bracket**; it says +nothing about a draw taken before `OnResumePlaying` is entered whose value is carried into the +visit order (nothing in the record proposes such a path, and the seed pin is applied earlier still, +at `RunAI`, so a pre-bracket draw would have to be on a generator the pin does not touch and be +consumed later — named here so nobody reads P2 holding as more than it is). It also cannot identify +the container: that is stage 2's hook, and the resolver was explicit that the read must not begin +before this probe returns. The 3–40-turn fuse at `0x0069dbb0` cannot fire inside one turn of a load +and is untested here, exactly as in lane BP. + +--- + +## 2. The procedure + +*(filled in with the run)* + +--- + +## 3. The result + +*(filled in with the run)* + +--- + +## 4. Verdict + +*(filled in with the run)*