sots-re/findings/control-flow/ai-visit-order-probe.md

49 KiB
Raw Blame History

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/<id>.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

2.1 The config, and the non-comment diff (the check this lane was briefed to run)

shim.cfg.brprobe is shim.cfg.bppin with exactly three non-comment lines changed. Diffed twice, once on the host before the file was pushed and once on the guest, from the two files that were actually there:

$ diff <(grep -v '^\s*#' shim.cfg.bppin  | grep -v '^\s*$') \
       <(grep -v '^\s*#' shim.cfg.brprobe | grep -v '^\s*$')
6c6,8
< airng=off
---
> airng=on
> airng.pin_seed=off
> airng.out=C:\SOTS\shim.airng.txt

bppin has 43 live lines, brprobe 45. Everything else — 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 — is byte-for-byte BP's. airng.pin_seed=off is stated explicitly even though it is the default: a pinned generator is a declared perturbation and the resolution forbids it here, so it is written down rather than assumed.

The same diff, taken from the runtime config: lines of the two shim.logs rather than from the files, is identical — an independent confirmation that the three lines are the three lines that reached the process:

$ diff <(grep -o 'config: .*' bp/A-shim.log) <(grep -o 'config: .*' br/BR-shim.log)
6c6,8
< config: airng=off
---
> config: airng=on
> config: airng.pin_seed=off
> config: airng.out=C:\SOTS\shim.airng.txt

BP's report says its own first draft of a config reproduced an inherited defect (a wrapped header comment leaving a live line in the body) and that this diff is what caught it. This lane's inserted header is seven lines, every one #-prefixed; the 45 live lines were printed and read before the run.

2.2 The instrument, verified from shim.log before anything was read

  • build bp-989c692-20260909T0505Z — BP's dist, unmodified, no build by this lane; binkw32.dll sha256 9de1c57a…d971f47.

  • exe base=0x005e0000 (link-time image base 0x00400000, ASLR delta +1966080) pid=7064 — the same base lane BQ measured in three of BP's processes on this guest, so image base is constant here for a fifth time and "pointer order" can still only mean heap order.

  • 27 hook: … mode=off (not installed) lines plus selftest: … mode=off = 28 of 28 registered hooks off. Zero ignoring unknown key. Zero COVERAGE:.

  • seven drawsite: detours create=MH_OK enable=MH_OK; aiorders: … probes=0 of 20; aicfg words=12 deep=0 research=0 seed=pin pins=4 detours=2; one batch detour MH_OK; one aiseed detour MH_OK.

  • the two lines the brief names:

    airng: bracket StrategyClient::OnResumePlaying rva=0x00377480 va=00957480 create=MH_OK enable=MH_OK
    airng: out=C:\SOTS\shim.airng.txt pin_seed=off:00000000
    airng: facade cl_Chance    rva=0x00178cf0 va=00758cf0 create=MH_OK enable=MH_OK
    airng: facade cl_RandRange rva=0x001798e0 va=007598e0 create=MH_OK enable=MH_OK
    

    No airng: PINNING IS ON line, as required.

  • the pin, three of three, and no PIN MODE WITH NO PINS, no pinned=0, no wildcard hit:

    aiseed call=1 netId=32  observed=0xd0598a7d used=0x156ebbbd pinned=1
    aiseed call=2 netId=496 observed=0x91193aec used=0xfe7b2826 pinned=1
    aiseed call=3 netId=512 observed=0x9733ed32 used=0x0ed341d1 pinned=1
    

    Three new natural seeds, shared with none of BP's nine — lane L1's per-process seeding reproduced a fifth time, now on a turn-27 state.

2.3 Standing instruction: who actually runs, and by which route

  • RunAI net-id list: 32 / 496 / 512 — three AI clients, in that order, exactly as lane BP measured. The save's other four non-human players (528 Alien Menace, 544 Peacekeeper Enforcer, 560 Von Neumann, 576 Independent Colony) get no client, no call and no seed. Independently confirmed a second way this run: the airng bracket fired five times — pid=16 (human, agent=0x00000000) at load, then pid=32, pid=496, pid=512 each with a non-null agent, then pid=16 again — and never for 528–576.
  • Route: load. ad-turn27-two-raiders.sav → Load Game → Single Player → OK → row 1 (400,260) → OK → Launch → one End Turn. No continuation. Every dialog step verified from a fresh QEMU screendump (screendump -f png over the guest's QMP socket, which bypasses the lab wall's ~5 s frame cache entirely — see §5).
  • Exposure (rule 26 (c), unchanged from BP and re-derived from this run's own outputs): Player[32] enters with ResTNm = BIO_TerBac held, NumDes 46, NumOwn 15, 13 ships completing into new fleets; 496 and 512 hold targets with NumOwn = 0; the four NPC factions have ResTNm == '' and no client. Exposed on the design, fleet-assignment and colony counts, not on the research pick. No encounter query — End Turn ran straight to the post-turn autosave.

Timings on this guest, for the next lane: schtasks /Run /TN SOTS → fg + key {ESC} ×3 spaced 2.5 s → main menu at ~2 min 45 s (screenshot-verified, never slept-and-clicked); the click path through the Load dialog ~2 min at one click per helper call; the turn-27 load 3 min 20 s the first time and 1 min 55 s the second; End Turn ~4 s with this hook set — BP's "~35 s" is generous, and the autosave mtime is the reliable completion signal.


3. The result

3.0 Lead

P1 holds, P2 holds, P4 holds, P3 is falsified in its stated form. There is no foreign draw — not one word, in any of the three AI clients' brackets. And the ledger says something sharper than "none": over the entire process lifetime, the per-process global generator at 0x00af6e58 was drawn exactly three times, three words, from exactly one return address, and that address sits 16 bytes before the RunAI call site — it is the AI client seed mint itself, one draw per client, the thing the pin overwrites. RNG is excluded by measurement. H2 is dead.

A second run followed (§4.2, the Rung B record) and landed on the other branch, which makes the outcome set two-for-two: five processes on this state, three on e913ff41…, two on 724528ff…, no third file.

3.1 P1 — the autosave, and which file it is

file sha256 bytes
pre-turn (Autosave EndTurn).sav 16f63adbf383946db17c381998957172bca803406b7df78f3b03805fef0bbcb8 100,758
post-turn (Autosave).sav e913ff41188211c02d536da616337544e64133996cd7cc7a6521a8df1960226f 101,375

It equals e913ff41… — lane BP's run A, which is also lane BP's run L. Not a third file, so the resolver's ambiguity caution does not fire and no disambiguation run was needed on that account. state_checksum.py --floats bits --mask none:

  • post-turn BR vs bp-pinA-turn28.sav: IDENTICAL, root 8100b28960f729ef9a24a22f13397b4a — the same root BP recorded for L and A;
  • post-turn BR vs bp-pinB-turn28.sav: DIVERGED: 35 leaf difference(s), and the 35 are BP's transposition, leaf for leaf (Flt[1970]/Flt[1986] exchanging Pos, PrvPos, FtOrig, FPlan, LocID 384↔0, HFPlan, FtTrans, FtFlg 12↔13, Ship[6976]↔Ship[5264], plus Sys[384]/Flt[1], ShipIDs[] list-reordered and Summary/Checksum). Nothing else moved;
  • pre-turn BR vs bp-turn28-pre.sav: IDENTICAL, root 6bbaf382c9e570ae882b599f566ae62e.

This is also the instrument's own control, in rule 26 (d)'s shape. The airng detour cannot be removed and cannot be argued away; what it can do is reproduce a natural run. It did: a process carrying the OnResumePlaying bracket and two facade detours wrote the same bytes as lane BP's unpinned run L. airng's behavioural neutrality was previously an argument from roll-parity.md; on this state it is now a measurement.

3.2 P2 — the per-client draw ledger. The bracket rows, all five

airng seq=1 pid=16  agent=0x00000000 rng=0x0d820d18 pin=off left=624->624 idx=0->0     left_delta=0  observed=0  calls=0  residual=0 foreign_words=0 foreign_calls=0 sites=0 overflow=0 life_in=0   life_out=0
airng seq=2 pid=32  agent=0x327811e0 rng=0x33a90010 pin=off left=612->533 idx=12->91   left_delta=79 observed=79 calls=75 residual=0 foreign_words=0 foreign_calls=0 sites=4 overflow=0 life_in=12  life_out=91
airng seq=3 pid=496 agent=0x32780038 rng=0x1f324540 pin=off left=219->219 idx=405->405 left_delta=0  observed=0  calls=0  residual=0 foreign_words=0 foreign_calls=0 sites=0 overflow=0 life_in=405 life_out=405
airng seq=4 pid=512 agent=0x32780ad0 rng=0x33968010 pin=off left=206->206 idx=418->418 left_delta=0  observed=0  calls=0  residual=0 foreign_words=0 foreign_calls=0 sites=0 overflow=0 life_in=418 life_out=418
airng seq=5 pid=16  agent=0x00000000 rng=0x0d820d18 pin=off left=624->624 idx=0->0     left_delta=0  observed=0  calls=0  residual=0 foreign_words=0 foreign_calls=0 sites=0 overflow=0 life_in=0   life_out=0

foreign_words=0 foreign_calls=0 residual=0 in every bracket, including the one that matters. left_delta == observed on Player[32] (79 = 79), so the two independent measurements agree and there is no inlined draw on the AI path either — which is what lane I's inventory predicted and what a call-graph-shaped observer could not have asserted on its own.

The four in-bracket sites for Player[32], by return address (va in these rows is the Ghidra VA, 0x00400000 + rva, not the runtime VA the install lines print):

ret_rva Ghidra va entry calls words
0x004e6e09 0x008e6e09 NextFloat 10 0
0x00178d15 0x00578d15 Chance 10 10
0x002ad878 0x006ad878 NextFloat 22 22
0x00179915 0x00579915 NextInt 43 47

and the facade call sites, all six in the AI band 0x0068–0x006e: cl_Chance from 0x0069086f (2 calls), 0x006adf35 (2), 0x006adfca (3, 2 true), 0x006ae418 (3); cl_RandRange from 0x006ad94c (22) and 0x00691ea0 (21).

3.3 The census rows whose generator is not that client's own — and the one that matters

The airngcensus table is a whole-process lifetime ledger (its per-generator totals are the airngen life_words, and rows for clients 496/512 are already present at seq=2, before those brackets ran). So "foreign rows" here means rows on a generator other than the bracket's own, and the bracket row's foreign_words=0 is what says none of them moved inside the bracket. Five generators were seen in the whole run:

runtime rng what it is lifetime words
0x00cd6e58 the per-process global generator — static 0x00af6e58 (runtime = static + the 0x1e0000 ASLR delta) 3
0x33a90010 Player[32]'s client generator (the word at client+0x134) 91 → 94
0x1f324540 client 496's 405 → 407
0x33968010 client 512's 418 → 420
0x0d820d18 human client 16's 0
0x0d83b170 a server-side generator, first seen at seq=5 (post-AI) 20

The whole of the global generator's activity, every row it has in the file, at every seq from 2 to 5:

airngen    seq=2..5 rng=0x00cd6e58 life_words=3 life_calls=3
airngcensus seq=2   rng=0x00cd6e58 ret_rva=0x00384f9e va=0x00784f9e entry=NextUInt calls=3 words=3
airngcensus seq=5   rng=0x00cd6e58 ret_rva=0x00384f9e va=0x00784f9e entry=NextUInt calls=3 words=3

Three calls, three words, one return address, and the count does not move between the first AI bracket and the end of the turn. The two AI-band consumers the resolution named as candidates — 0x0079ea90+0x73 (RNG_Chance) and 0x005b9f00+0xc0 — appear nowhere in the census: they did not fire in this process at all.

What 0x00784f9e is — inference from the record, not from the binary, which this lane was told not to read. Lane AI1 records that StrategyApp::RunAI is "reached only from StrategyNetworkClient::OnMessage 0x00784640 + 0x96e", i.e. the call is at 0x00784fae. The return address of the only global-generator draw in the process is 0x00784f9e, sixteen bytes earlier, in the same function, and it fires exactly three times for exactly three AI clients, at 02:30:34 — the same log timestamps as the three aiseed call=… lines. That is the seed mint: board row 326's "one 32-bit word drawn per AI client at construction from a per-process global", now measured live, with its consumer's address, its generator's address and its call count. It is also the only thing that generator does. A one-line static read would confirm the instruction; this lane did not take it.

3.4 P3 — falsified, as the weak prediction it was labelled

P3 said Player[32] would spend single-digit words in its bracket. It spent 79 (75 calls, four sites). The second half — "496 and 512 spend fewer" — holds degenerately: they spend zero. Recording it because rule 2 says a prediction exists to be disagreed with, and this is exactly rule 23's corollary again: a bracket total is a sum over every subsystem inside it, and this one was guessed rather than modelled.

Two things in the ledger are worth more than the number:

  • Clients 496 and 512 run a full AI turn and draw nothing. left does not move, idx does not move, agent is non-null. Rule 20's design working: "ran and drew nothing" is visibly different from "never ran". Their 405 / 418 lifetime words were all spent before the bracket, at client construction.
  • The 3–40-turn fuse consumer 0x0069dbb0 DID draw — before the bracket, on each client's own (pinned) generator. Census rows 0x0069dc02 (= +0x52) and 0x0069dc2e (= +0x7e): 2+1 words for client 32, 1+1 for 496, 1+1 for 512, all inside life_in. Both lane BP and lane BQ recorded that this fuse "cannot fire inside one turn of a load and is therefore untested"; that is right about the turn, and this run shows the site is nevertheless entered on the load path (RunAI → RaiseAIPrepareTurn), on the seeded generator. Rule 28 practice 4 exactly: an entry is not a cost, and a zero in a bracket is not a statement about the subtree.

3.5 P4 — the aiorders diff against BP's runs A and B, under BP's noise mask

Mask = the (list, word) positions BP's byte-identical L/A pair proves cannot be read by the applier (list 1 → 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 — named by BP in the same section and separated out explicitly here — aiblk gate payloads whose SET flag is 0, which are lane L4's uninitialised-when-unset words.

The comparator was self-tested against BP's own published result before being pointed at this run: on A vs B it reproduces §3.4's three words and nothing else; on L vs A (the pair that defines the mask) it reports NONE. Full output in verify/results/shim/br/aiorders-diff.txt.

BR vs run A — 9 blocks, 81 elements, identical (pid, list, idx) key sets, identical per-list sizes ({1:3, 3:19, 5:15, 7:2, 8:3, 10:4, 12:12, 14:8, 23:15} for pid 32), identical set gates (rate=1:1 for pid 16, rate=1:0.8 for 32/496/512):

== word diffs, masked ==
  NONE. Every differing word is inside the proven noise set.

BR vs run B — same structure, and exactly the three transposition words:

  blk=1 pid=32 list= 8 idx=1 word0  B=0x000007b2 (1970)  BR=0x000007c2 (1986)
  blk=1 pid=32 list=10 idx=0 word0  B=0x00000050 (80)    BR=0x00000180 (384)
  blk=1 pid=32 list=10 idx=1 word0  B=0x00000180 (384)   BR=0x00000050 (80)

The visit order, read straight off list 10 as the resolution says it can be:

  L : (384,1970) (80,1986) (384,2002) (816,7072)
  A : (384,1970) (80,1986) (384,2002) (816,7072)
  BR: (384,1970) (80,1986) (384,2002) (816,7072)      <- system 384 visited first
  B : (80,1970)  (384,1986) (384,2002) (816,7072)     <- system 80 visited first

P4 holds in its strong form: the difference from A is empty, and the difference from B is the transposition and nothing else.

3.6 A free measurement the dumps already contained: heap order does not track visit order

The resolution's §2.2 read node heap addresses out of BP's aielem rows to show that same-size allocations land in different relative orders in two processes. The same rows in this lane's two runs extend that, and add one negative result that matters for stage 2.

Player[32]'s list 3 — nineteen nodes allocated in one pass — low 16 bits of the first eight, in emission order, across four processes:

run visit order first eight list-3 node addresses
L 384 first 6338 6238 6358 6378 6418 64b8 63b8 6458
A 384 first f838 f538 f898 f4f8 f518 fa38 fc98 0378
BR 384 first 2f78 2df8 3038 3018 3078 2cd8 3a78 c458
B 80 first e1c0 e320 e200 e380 e460 e500 e260 e4e0

Four processes, four different relative orders, for an allocation sequence that is identical (identical decisions, identical block structure). §2.2's precondition is reproduced on four processes, and L vs A shows it holds even between two processes whose autosaves are byte-identical.

And the negative: the emitted command nodes' relative address order does not track the visit order. For list 10's four elements, idx1 < idx0 in BR and in B (different visit orders) and idx1 > idx0 in A and in L (same visit order as BR). So you cannot read the visit order off the block's own node addresses — which is expected, since those nodes are allocated after the visit decision, but it is worth stating because it closes the cheapest imaginable shortcut. Stage 2 must hook the walked container; the dump cannot substitute for it.


4. Verdict

4.1 Which hypothesis survives

H2 — "a foreign RNG draw sets the visit order" — is DEAD, by measurement.

Zero foreign words and zero foreign calls in all three AI clients' brackets; residual = 0, so not an inlined draw either; and the per-process global generator at 0x00af6e58 was touched three times in the entire process, all three at one return address 0x00784f9e inside StrategyNetworkClient::OnMessage, sixteen bytes before the RunAI call — one seed per AI client, which is precisely the word the pin overwrites. Neither of the two unnamed AI-band consumers the resolution named fired at all.

H1 / H1b — an address-keyed container, or a comparator reading an uninitialised word, on the fleet-assignment path — is the only hypothesis left standing, and this run neither confirms nor distinguishes them. It does strengthen their shared precondition to four processes (§3.6) and it rules out reading the answer off the block (§3.6).

Per the brief, this lane stops here. The binary was not read and stage 2 was not started. The follow-up the resolution names is now unblocked and unchanged: one bounded static read of AssignFleetsAndIssueOrders 0x006c16c0 and the gathering hub to place a hook on the loop that visits the ship groups, logging (element address, systemId, minted fleetId) in visit order, two fresh processes. Under H1 the visit order equals ascending element address in both and the two processes' address orders differ exactly where their visit orders differ; under H1b it tracks neither.

On the caution the resolver put on the record: it does not apply. The probe run produced BP's e913ff41…, not a third file, so this lane is in neither the k ≥ 3 case nor the instrument-perturbation case; and the second run (§4.2) produced BP's 724528ff…. The disambiguation run was therefore not needed and was not performed. No pinned file was compared with any of lane AD's unpinned files.

4.2 The Rung B record — run 2, taken while the guest was warm

Config shim.cfg.brdeep = shim.cfg.bppin with one non-comment line changed (aiorders.deep=off → on); airng stays off, which is bppin's own value. Verified in the log: aicfg words=12 deep=1 research=0 seed=pin pins=4 detours=2, three pinned=1, no airng: lines, no unknown keys.

Hash: (Autosave).sav = 724528ffde9a7514b69ab9e82ed3b8c8ade63d622c0720de30503a73a92df11e, 101,370 B — byte-identical to lane BP's run B, verified with cmp. Pre-turn autosave 16f63adb… as always. So the resolution's stated target ("if it lands on e913ff41…") was not hit; it landed on the other branch. That does not cost the record anything, and §6's own logic is why: the residual input is written into the block, so the record is complete as (ad-turn27-two-raiders.sav, load → one End Turn, seeds 32=156ebbbd / 496=fe7b2826 / 512=0ed341d1, visit order = list 10's order in the captured deep block, deep block, → 724528ff…). It is the campaign's first replayable rich-turn record — thirteen ships completing, a design created, a system colonised — and it is on the branch that lane BP's bp-pinB-turn28.sav already holds.

Does the deep dump follow list 10's ship vectors and list 8's routes? Yes, both, and list 1's strings as well:

aistr blk=1 pid=32 list=1  idx=0 at=w2 sso=1 len=14 text="Egg Thief Mk 4"
aistr blk=1 pid=32 list=1  idx=1 at=w2 sso=1 len=14 text="Egg Thief Mk 5"
aistr blk=1 pid=32 list=1  idx=2 at=w2 sso=1 len=14 text="Bravestar Mk 3"

ailist    blk=1 pid=32 list=8  size=3
aielem    ... list=8  idx=0 ints=[ 2002 ... ]   aivec ... at=w1 cap=1 count=1 ints=[ 256 ]
aielem    ... list=8  idx=1 ints=[ 1970 ... ]   aivec ... at=w1 cap=1 count=1 ints=[ 384 ]
aielem    ... list=8  idx=2 ints=[ 1538 ... ]   aivec ... at=w1 cap=1 count=1 ints=[ 240 ]

ailist    blk=1 pid=32 list=10 size=4
aielem    ... list=10 idx=0 ints=[  80 1970 ... ]  aivec ... at=w2 cap=1  count=1  ints=[ 5264 ]
aielem    ... list=10 idx=1 ints=[ 384 1986 ... ]  aivec ... at=w2 cap=1  count=1  ints=[ 6976 ]
aielem    ... list=10 idx=2 ints=[ 384 2002 ... ]  aivec ... at=w2 cap=1  count=1  ints=[ 6992 ]
aielem    ... list=10 idx=3 ints=[ 816 7072 ... ]  aivec ... at=w2 cap=11 count=11 ints=[ 2992 3456 4304 4912 5680 3184 5856 6000 6256 6464 2720 ]

35 aivec rows and 3 aistr rows across the block; list 3 and list 12 carry their vectors too (list 12's twelve elements resolve 3, 15, 1, 5, 4, 6, 20, 22, 11, 7, 1 and 7 ship ids). Under the noise mask the deep run's shallow words are element-for-element identical to BP's run B, so deep=on is confirmed to be additive: it adds followed rows, it does not disturb the block.

Two cross-checks the deep dump makes possible for the first time, both pass. (1) The block's ship↔fleet pairing matches the save it produced: in this run list 10 gives fleet 1970 ← ship 5264 at system 80 and fleet 1986 ← ship 6976 at system 384, and bp-pinB-turn28.sav is exactly the save in which Flt[1970] holds Ship[5264]. The resolution's "the visit order is written into the block" is now verified end to end, block → save. (2) The move goes to the group at system 80 in both orders: list 8 idx=1 is fleet 1970 with route [384] here, and fleet 1986 with the same route in run A — and 1970-here and 1986-there are both the system-80 group. That is the resolution's §3 qualification ("on ad-turn27 the move in list 8 went to the group at system 80 in both orders") confirmed from the command stream rather than inferred.

4.3 The outcome set, after five processes

file processes visit order
e913ff41… 3 — BP's L (unpinned), BP's A (pinned), BR's probe (pinned + airng) 384 first
724528ff… 2 — BP's B (pinned), BR's deep (pinned + deep) 80 first

No third file in five processes. The outcome set is still observed at 2, now with both members reproduced at least twice and across three different instrument configurations. That is not a proof that k = 2 — a binary order over two groups makes 2 the natural size, but nothing here rules out a turn where more groups make it larger — and it is enough to say the residue is a small, enumerable order rather than an unbounded scramble, which is what C-set's shape needs.

4.4 What this lane did NOT establish

  • Nothing about a draw taken before OnResumePlaying is entered whose value is carried into the visit order. The bracket is an exact bracket around the turn, not around the client's life. The global generator's constancy (3 words, lifetime, one site) is what actually closes that door, and it closes it for that generator only.
  • Nothing about the container's identity. §3.6 shows the block cannot tell you.
  • Nothing about a turn other than this one, a route other than load, or a build other than bp-989c692.
  • The 0x0069dbb0 fuse's turn-time behaviour remains untested (it fired on the load path here, which is a different statement).
  • The 0x00784f9e attribution is an inference from lane AI1's recorded call-site offset plus a count that matches the client count exactly. It is one instruction away from being a measurement and this lane did not take that step, by instruction.

5. Guest notes for the next lane (VM146)

  • The lab wall's cached frame was avoided entirely, and there is a better instrument. A direct QEMU screendump — echo 'screendump /tmp/x.png -f png' | qm monitor 146 on spicy, then scp — returns the live framebuffer with no poll cycle in it. Twelve screenshots this session, zero ambiguous frames, zero wasted clicks. BP's "wait 12–16 s" is the right advice for /shot/<id>.png and is unnecessary if you take the screendump yourself. (tools/vmshot.py cannot be used from this host: it imports PIL and PIL is not installed here.)
  • bpshot.ps1 reads C:\SOTS\ui\log.txt after a 3-second sleep, and the click-helper task writes that file at the END of its batch. So for any batch longer than ~3 s the log you get back is the previous batch's — it printed a timestamp 37 minutes stale on my first call. Harmless once you know; read the log in a separate call, or ignore it and screenshot.
  • Load-dialog geometry confirmed unchanged: with the two autosaves deleted the list is 7 rows from y=260 at 29 px pitch and 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.
  • Timings, both runs (§2.3): main menu ~2 min 45 s; turn-27 load 3 min 20 s then 1 min 55 s; End Turn ~4 s.
  • Released restored, hash-verified: SavedGames back to the 9-file pre-BR set with both autosaves byte-identical to C:\SOTS\ui\preBP\ (a3f9dc4b…, d59bb9f2…), shim.cfg = hooks=off, binkw32.dll = 07c962946d93c2cfef336803ac041150bda2640c05980b266902b1aa1a618359 (the pre-BP file), shim.trace.jsonl removed, game process not running — exactly as found. Left in place: C:\SOTS\shimdist-bp\shim.cfg.brprobe and shim.cfg.brdeep, and C:\SOTS\ui\br-probe-turn28{,-pre}.sav, br-deep-turn28.sav, br1-{airng,aiorders,shim.log}, br2-{aiorders,shim.log}. click_helper.ps1, bpdeploy.ps1, bpshot.ps1, bphash.ps1 untouched.

Artefacts: verify/results/shim/br/ — BR-shim.log, BR-airng.txt, BR-aiorders.txt, BR2-shim.log, BR2-deep-aiorders.txt, aiorders-diff.txt (all four comparisons including the two self-tests), cmp_aiorders.py (the masked comparator), shim.cfg.brprobe, shim.cfg.brdeep. The two output saves are byte-identical to verify/results/saves/bp-pinA-turn28.sav and bp-pinB-turn28.sav and are deliberately not duplicated in the repo.


Proposed board rows

New row:

| RNG IS EXCLUDED BY MEASUREMENT FOR THE AI VISIT ORDER - the global generator is drawn THREE TIMES IN A PROCESS, all three to mint the AI client seeds | control-flow | verified | high | 100% | 2026-09-09 | Lane BR, VM146, the single probe of `findings/resolutions/2026-09-09-fleet-id-order-residue.md` section 4 (`findings/control-flow/ai-visit-order-probe.md`). No build: lane BP's dist `bp-989c692-20260909T0505Z`, `shim.cfg.brprobe` = `shim.cfg.bppin` with EXACTLY THREE non-comment lines changed (`airng=on`, `airng.pin_seed=off`, `airng.out=`), diffed twice - once file-to-file on the guest, once from the two runs' runtime `config:` lines, both giving the same three lines. 28 of 28 registered hooks off BY NAME, zero unknown keys, zero COVERAGE lines, `aiseed call=1/2/3 pinned=1`, no wildcard hit, `airng: bracket ... MH_OK` + `pin_seed=off`. Route: LOAD. RunAI net-id list: 32/496/512 (three clients; the four NPC factions get none) - confirmed a second way by the bracket firing 5x (pid 16 human agent=0, then 32/496/512 with agents, then 16). **P1 HOLDS: `(Autosave).sav` = `e913ff41...`, BYTE-IDENTICAL to lane BP's runs A and L** (state_checksum IDENTICAL, root 8100b289..., 61147 leaves; vs BP's B, the same 35-leaf transposition, leaf for leaf; pre-turn `16f63adb...` IDENTICAL). Not a third file, so the resolver's ambiguity caution does not fire - AND this is the airng detour's own rule-26(d) control: an instrumented process reproduced an unpinned natural run byte for byte, so `airng`'s behavioural neutrality is now MEASURED on this state, not argued from roll-parity.md. **P2 HOLDS, AND HARDER THAN PREDICTED. `foreign_words=0 foreign_calls=0 residual=0` in ALL THREE AI brackets** (pid 32: left_delta=79 observed=79 calls=75 sites=4; pid 496 and pid 512: left_delta=0, observed=0 - they run a full AI turn and DRAW NOTHING, rule 20's design working). left_delta == observed on pid 32 so there is NO INLINED DRAW on the AI path either, confirming lane I's inventory from the other side. **AND THE LIFETIME CENSUS CLOSES IT: the per-process global generator 0x00af6e58 (runtime 0x00cd6e58) was drawn EXACTLY 3 TIMES, 3 WORDS, FROM ONE RETURN ADDRESS, `va=0x00784f9e`, entry NextUInt - and the count is 3 at every seq from 2 to 5, i.e. it does not move during the turn at all.** `0x00784f9e` is INSIDE `StrategyNetworkClient::OnMessage 0x00784640` and sits 16 BYTES BEFORE the `RunAI` call site that lane AI1 recorded at OnMessage+0x96e = 0x00784fae, and it fires exactly three times for exactly three AI clients at the same log timestamps as the three `aiseed call=` lines: **it is the AI client seed mint - board row 326's mechanism, measured live with its consumer address, generator address and call count, and it is the ONLY thing that generator does in the whole process.** (Inference on the identification, one instruction from a measurement; this lane was instructed not to read the binary.) The two unnamed AI-band consumers the resolution named as H2 candidates (0x0079ea90+0x73, 0x005b9f00+0xc0) DO NOT APPEAR IN THE CENSUS AT ALL. **VERDICT: H2 IS DEAD. H1/H1b (an address-keyed container or an uninitialised-word comparator on the fleet-assignment path) IS THE ONLY HYPOTHESIS LEFT.** **P4 HOLDS in its strong form**: under BP's L/A-proven noise mask (plus unset `aiblk` gate payloads, lane L4's class), BR vs run A = 9 blocks / 81 elements / identical key sets / identical per-list sizes / identical SET gates and **NOT ONE differing word outside the noise set**; BR vs run B = **EXACTLY the three transposition words** (list 8 idx1 w0 1986/1970, list 10 idx0 w0 384/80, list 10 idx1 w0 80/384). The comparator was self-tested against BP's published A/B and L/A results first. **P3 FALSIFIED** (it was labelled not load-bearing): pid 32 spent 79 words, not single digits - rule 23's corollary again, a bracket total is a sum over every subsystem in it. INCIDENTAL, and it is a rule-28 practice-4 case: **the 3-40-turn fuse consumer 0x0069dbb0 DOES draw - on the LOAD path, before the bracket, on each client's own (pinned) generator** (census 0x0069dc02 and 0x0069dc2e: 2+1 words for client 32, 1+1 for 496 and 512, all inside life_in). Its turn-time behaviour is still untested. FREE MEASUREMENT FROM THE DUMPS: section 2.2's LFH precondition now holds on FOUR processes (list-3 node addresses in four different relative orders for an identical allocation sequence - including between L and A, whose autosaves are byte-identical), AND a negative that matters for stage 2 - **the emitted command nodes' relative address order does NOT track the visit order** (BR and A share a visit order and differ in node order; BR and B share a node order and differ in visit order), so the block cannot substitute for the container hook. NEXT (unchanged, now unblocked): one bounded static read of AssignFleetsAndIssueOrders 0x006c16c0 + the gathering hub TO PLACE A HOOK on the group walk, log (element addr, systemId, minted id) in visit order, x2 processes. Artefacts: `verify/results/shim/br/` |
| THE FIRST REPLAYABLE RICH-TURN RECORD - a deep AI command block for ad-turn27, on the 80-first branch | control-flow | verified | high | 100% | 2026-09-09 | Lane BR run 2, VM146, the Rung B record of `findings/resolutions/2026-09-09-fleet-id-order-residue.md` section 6 (`findings/control-flow/ai-visit-order-probe.md` section 4.2). `shim.cfg.brdeep` = `shim.cfg.bppin` with ONE non-comment line changed (`aiorders.deep=off` -> `on`); `airng` stays off. Verified: `aicfg words=12 deep=1 research=0 seed=pin pins=4 detours=2`, three `pinned=1`, zero unknown keys. **The record: (`ad-turn27-two-raiders.sav` 1c8baa27..., LOAD -> one End Turn, seeds 32=156ebbbd/496=fe7b2826/512=0ed341d1, build bp-989c692-20260909T0505Z, visit order = list 10's order in the captured block) -> pre `16f63adb...`, post `724528ffde9a7514b69ab9e82ed3b8c8ade63d622c0720de30503a73a92df11e`** - BYTE-IDENTICAL to lane BP's run B (`cmp`, not just the hash). The resolution's stated target was `e913ff41...`; it landed on the OTHER branch and **that costs the record nothing**, because the residual input is written into the block (section 6's own logic): list 10 records the visit order on every captured run. THE DEEP DUMP FOLLOWS EVERYTHING IT WAS SUPPOSED TO: 35 `aivec` rows and 3 `aistr` rows. **List 10's ship vectors** - idx0 (80,1970)<-[5264], idx1 (384,1986)<-[6976], idx2 (384,2002)<-[6992], idx3 (816,7072)<-[11 ship ids]. **List 8's routes** - (2002)->[256], (1970)->[384], (1538)->[240]. Plus list 1's names as strings ("Egg Thief Mk 4", "Egg Thief Mk 5", "Bravestar Mk 3"), list 3's and list 12's vectors (12 elements resolving 3/15/1/5/4/6/20/22/11/7/1/7 ship ids). Under BP's noise mask the deep run's shallow words are ELEMENT-FOR-ELEMENT IDENTICAL to BP's run B, so `deep=on` is additive and does not disturb the block. **TWO CROSS-CHECKS NOW POSSIBLE FOR THE FIRST TIME, BOTH PASS: (1) the block's ship<->fleet pairing matches the save it produced** - list 10 gives fleet 1970 <- ship 5264 at system 80, and `bp-pinB-turn28.sav` is exactly the save where Flt[1970] holds Ship[5264]; the resolution's "the visit order is written into the block" is now verified END TO END, block -> save. **(2) The list-8 move goes to the group at system 80 in BOTH orders** (fleet 1970 with route [384] here; fleet 1986 with the same route in run A - both are the system-80 group), which confirms the resolution's section 3 qualification from the command stream instead of by inference. OUTCOME SET AFTER FIVE PROCESSES ON THIS STATE: `e913ff41...` x3 (BP's L unpinned, BP's A pinned, BR's probe pinned+airng, all 384-first), `724528ff...` x2 (BP's B, BR's deep, both 80-first). **NO THIRD FILE IN FIVE PROCESSES, ACROSS THREE INSTRUMENT CONFIGURATIONS**, with both members reproduced at least twice - the residue is a small enumerable order, which is what C-set's shape needs. Not a proof that k=2. Artefacts: `verify/results/shim/br/BR2-deep-aiorders.txt`, `BR2-shim.log`, `shim.cfg.brdeep`; the save is byte-identical to `verify/results/saves/bp-pinB-turn28.sav` and is deliberately not duplicated |

Edits to existing rows:

  • Row 62 — VM146: lane BR held it 2026-09-09 02:21–02:47 local for the visit-order probe and the Rung-B deep record, and released it restored and hash-verified: SavedGames back to the 9-file pre-BR set with both autosaves byte-identical to C:\SOTS\ui\preBP\ (a3f9dc4b…, d59bb9f2…), shim.cfg hooks=off, binkw32.dll 07c96294…, shim.trace.jsonl removed, game process not running, exactly as found. ad-turn27-two-raiders.sav still on disk, so BP's Load-dialog geometry still holds and was re-verified from a screenshot: 7 rows from y=260 at 29 px pitch, row 1 at (400,260). Left C:\SOTS\shimdist-bp\shim.cfg.{brprobe,brdeep} and C:\SOTS\ui\br-probe-turn28{,-pre}.sav, br-deep-turn28.sav, br1-*, br2-*; click_helper.ps1 and BP's three helpers untouched. BETTER INSTRUMENT THAN THE WALL, AND IT RETIRES BP'S FOUR-WASTED-CLICKS GOTCHA: take the screendump yourself — echo 'screendump /tmp/x.png -f png' | qm monitor 146 on spicy, then scp — that is the live framebuffer with no poll cycle in it. Twelve screenshots, zero ambiguous frames, zero wasted clicks; BP's "wait 12–16 s" is only needed for the wall's /shot/<id>.png. Note tools/vmshot.py cannot be used from the WSL host: it imports PIL and PIL is not installed. GOTCHA: bpshot.ps1 sleeps 3 s then prints ui\log.txt, but the click-helper task writes that file at the END of its batch — for any batch longer than ~3 s you get the previous batch's log (mine printed a 37-minute-stale timestamp on the first call). Read the log in a separate call or ignore it and screenshot. TIMINGS, TWO RUNS: main menu ~2 min 45 s (fg + key {ESC} ×3 spaced 2.5 s, screenshot-verified, never slept-and-clicked); the click path ~2 min at one click per helper call; turn-27 load 3 min 20 s then 1 min 55 s; End Turn ~4 s with this hook set — BP's "~35 s" is generous and the autosave mtime is the reliable completion signal.
  • Row 326 (the per-process AI client seed) — append: MECHANISM MEASURED LIVE 2026-09-09 (lane BR, VM146): the per-process global generator at 0x00af6e58 is drawn EXACTLY THREE TIMES IN THE WHOLE PROCESS, three words, from ONE return address va=0x00784f9e - inside StrategyNetworkClient::OnMessage 0x00784640 and 16 bytes before the RunAI call site lane AI1 recorded at OnMessage+0x96e - once per AI client, at the same log timestamps as the three aiseed calls. That is this row's mechanism, with a consumer address and a call count instead of an inference. It is also the ONLY draw that generator takes: its count is 3 at every airng bracket from the first AI client to the end of the turn.
  • Row 360 — append to the resolver's queued note: AND THE RESIDUE IS NOT A DRAW: lane BR (2026-09-09, VM146) measured zero foreign words and zero foreign calls in all three AI clients' OnResumePlaying brackets, residual=0 (so not an inlined draw either), and the global generator motionless through the turn. RNG is excluded by measurement; an address-keyed container is the only hypothesis left.
  • Row 415 — append: Lane BR reproduced the 384-first branch a third time (e913ff41..., byte-identical to BP's A and L) and the 80-first branch a second time (724528ff..., byte-identical to BP's B). Five processes, two files, no third.
  • Row 333 / the Rung C row — append: PRECONDITION MET 2026-09-09 (lane BR): the per-client draw ledger is taken and it is clean - zero foreign draws in every AI bracket on ad-turn27, so the residual visit order is not an RNG input and (save, seeds, visit order) is the complete input tuple as far as any generator is concerned.
  • The Rung B row / backlog section 2 Rung B — append: DONE 2026-09-09 (lane BR run 2): the first replayable rich-turn record exists. It is on the 80-first branch (724528ff..., byte-identical to bp-pinB-turn28.sav), not the e913ff41 branch section 6 named, and that is immaterial because list 10 records the visit order. The deep block follows list 10's ship vectors and list 8's routes, and its ship<->fleet pairing matches the save it produced - "the visit order is written into the block" is now verified end to end.
  • method-rules.md rule 26 (d) — no change proposed to the wording; note in passing that lane BR is its second application and its first successful one: the pinned+instrumented run reproduced lane BP's natural run L byte for byte, which is what licenses reading the ledger.

Docs to correct in place (rule 11), proposed, not done by this lane:

  • findings/subsystems/roll-parity.md — airng's behavioural neutrality can be upgraded from "two fresh processes on the published oracle" to "and it reproduced a natural unpinned run of ad-turn27 byte for byte on a rich AI turn (lane BR)".
  • guides/lab-screen-wall.md — add the direct-screendump one-liner as the recommended path for a lane that is driving a guest (the wall is for watching), and record that tools/vmshot.py needs PIL, which the WSL host does not have.
  • Any doc that still says the 0x0069dbb0 fuse "has never been observed firing" — it fired on the load path in this run, on each client's own generator, three sites' worth of words inside life_in. Its turn-time behaviour is still untested and that distinction should be written down.