sots-re/findings/control-flow/seed-exposure-forward-test.md
alex c43f638bc0 BQ: cross-reference lane BP's probe 2 - both lanes landed on the AI fleet layer
BP has a per-PROCESS input (two pinned processes disagree); this lane has a
per-HISTORY input (two load processes agree with each other and disagree
with the continuation). One datum against the ASLR reading as stated: the
exe base was identical in all five VM145 processes (0x00f40000, delta
+11796480), so any image-base hypothesis has to name which allocation it
means. Probe 3's ledger taken twice on ar-oracle-A-pre - once by
continuation, once by load - now speaks to both.
2026-09-09 02:13:22 -04:00

47 KiB
Raw Blame History

The seed-exposure predicate's first forward test — probe 1 (parts 1b and 1)

  • Type: control-flow / determinism (two hooks=off measurements from saves already on disk; no build, no shim config change, no instrument)
  • Owner / date: lane BQ · 2026-09-09 · guest VM145 (sots-re-win10-145, 192.168.10.145)
  • Spec: findings/resolutions/2026-09-09-seed-exposure-is-a-predicate.md §6, probe 1
  • Tests: guides/method-rules.md rule 26 corollary (c) — the predicate written from lane AR's falsified P7, so far supported only by three states read after the fact
  • Depends on: findings/control-flow/raid-target-pick-verdict.md §6 (lane AR's pair)

1. Predictions, committed before either run (rule 2)

Everything in §1 and §2 was written and committed before the game was launched on VM145 and before any hash was read. It is not edited after the fact, only annotated with verdicts in §5.

1.1 What is actually at stake

The resolver ruled that the per-process AI client seed (row 326) is present in every process on every turn, and reaches the save only through a decision whose outcome set is larger than one given the state — and that whether such a decision is reached is a predicate on the pre-turn save. That ruling currently rests on three states read after their controls were run (lane AD's turn 28, lane AS's turn 16, lane AR's turns 38–39). It has never made a forward prediction.

This lane makes two, on states nobody has run, and both can fail.

1.2 Part 1b — the precondition

Procedure. ar-oracle-A-pre.sav (Frame 38, sha256 15b99255e1f03dab3e35ab8c1ac64f221cb5aa9321f113c7a6e3d1263c3f34ca), one fresh process, hooks=off, one End Turn, no other input.

P1b — committed. The resulting (Autosave).sav (Frame 39) hashes to 7a8b3d5eb3a60ebac9f40646d3e4b15768a24a0af047a439ce090c7cf38e8b38.

Why it matters. Lane AR's 15b99255… → 7a8b3d5e… step has only ever been reached by continuation — the second of two End Turns inside one process that started from ar-turn37-816raiders.sav. A calibration pair for the standalone must be a load → turn procedure. If P1b holds, the single-turn pair is certified in that form and is the first pair anywhere that exercises the trade-raid roll (four raid rolls, one word each, no encounter). If it fails, the pair remains valid only as the two-turn procedure from turn 37, and the resolution names the first suspect: the resave canonicalisation (determinism-oracle.md, round-trip section).

P1b-aux — committed, and it costs nothing. The same run also writes (Autosave EndTurn).sav, which is the state as End Turn is pressed — i.e. a resave of the file that was loaded. I predict it hashes to 15b99255…, the input's own hash. This is a direct test of the resave-canonicalisation suspect, taken in the same run whichever way P1b goes:

  • P1b holds and P1b-aux holds ⇒ load→resave is byte-exact here and the pair is clean.
  • P1b fails and P1b-aux fails ⇒ the divergence is present before the turn runs; it is the round trip, not the turn, and the resave canonicalisation is confirmed as the cause.
  • P1b fails and P1b-aux holds ⇒ the round trip is exact and the turn diverged from its continuation form. That is the interesting failure and it is not one the resolution anticipated; it would mean a load boundary changes turn 39's outcome, and it is a resolver case, not a lane result.
  • P1b holds and P1b-aux fails ⇒ the resave is not byte-exact but is behaviourally equivalent; the pair is certified and determinism-oracle.md's round-trip section needs a note.

1.3 Part 1 — the exposure test

Procedure. ar-oracle-A-post.sav (Frame 39, sha256 7a8b3d5e…), two fresh processes, hooks=off, one End Turn each, any encounter query resolved with Auto Resolve Peacefully in both.

P1 — committed. The two (Autosave).sav files differ, and the difference is confined to Player[32]'s TechTree/*, ResTNm, otch, Events, turnstats/…/tch and Summary/Checksum, while /Sim/RNG, /Sim/trdmgr, every Player[16] leaf and every player-0 fleet are identical.

The reasoning, and it is the predicate speaking: ar-oracle-A-post.sav carries ResTNm == '' for Player[32] (§2). By the AS precedent (as-turn15 → 16, 22 leaves, entered with ResTNm == '') and the row-326 precedent on turn1-state, that is a research-pick turn. So the predicate says, in advance, that lane AR's pair must not be extended by a turn — and this run is what makes that a measurement rather than an inference.

How P1 can be wrong, and the symptom of each way (rule 2):

way it could be wrong symptom
a producer supplied the target, so the pick had an outcome set of size 1 (as for player 32 on turn1-state) the two files agree byte-for-byte; the predicate survives but needs a producer gate, and lane L4's airesearch=on dump names which producer returned non-null
something else on turn 40 is exposed as well as the pick the files differ, but outside the named leaf set — e.g. in fleets, trdmgr, or Player[16]
the pick is exposed but its outcome-set is 1 for a different reason (only one affordable node) files agree; same follow-up as the producer case, and otch/TechTree at turn 40 says which node was taken
VM145 is not byte-faithful to the lineage P1b fails as well, and part 1b is the guard against reading part 1 off a guest that cannot reproduce anything

A negative here is a result, not a null. If the two files agree, the honest statement is "no exposed decision was reached on turn 40 either, and the pick is therefore producer-supplied on this state" — never "the AI is deterministic here" (rule 26 (c)).

1.4 A false-positive class in the predicate, named before the run

Read off the two saves (§2), and stated here because it is a pre-run correction to the predicate as the resolution words it:

ResTNm == '' at turn start is true of four players on ar-oracle-A-pre.sav — the turn whose control agreed byte-for-byte. Players 528 (Alien Menace), 544 (Peacekeeper Enforcer), 560 (Von Neumann) and 576 (Independent Colony) all carry ResTNm == '' on turn 38 and turn 39, and player 16 (the human) does too. They have NumOwn == 0: no colonies, no research income, no pick to make. So the raw predicate has a known false-positive class already sitting inside the very state that falsified AR's P7.

The sharp form the next lane should use is therefore:

ResTNm == '' at turn start, for a non-eliminated AI player with NumOwn > 0, names a research-pick turn.

On ar-oracle-A-post.sav exactly one player satisfies that: Player[32], NumOwn 18. That is the whole basis of P1, and it is why P1 names Player[32]'s leaves and no one else's.

(Note for honesty: players 496 and 512 — the two Spengler NPCs — hold non-empty ResTNm and also have NumOwn == 0. They are not evidence either way; they are listed in §2 for completeness.)

1.5 What this lane will not settle (rule 15)

  • Two processes, not three, for part 1 — rule 26's bar, matching AR's.
  • One state. Neither part says anything about ad-turn27, which is probe 2's job on VM146.
  • The producer gate is not run here unless part 1 comes out as agreement and time allows; if it does not, this document names it as the next step rather than guessing at it.
  • 0x0069dbb0's 3–40-turn fuse cannot fire in a one-turn-from-load run, by construction — so neither part can say anything about it, in either direction.
  • No instrument. hooks=off throughout, so this lane measures behaviour and never attributes it to a draw site. The consumer identification is probe 3's job.

2. The exposure facts, read off the saves before the runs (rule 26 (c))

uv run python3 verify/save-reader/save_reader.py --json, three saves on the AD/AR lineage. NumDes = the designs list length, NumOwn = the owners list length (both are the on-wire count prefixes; verified against the --dump tree at NumOwn/NumDes). "ships +" is the per-owner ShipID set delta from the previous save in the lineage.

2.1 ar-oracle-A-pre.sav — Frame 38, ModCount 2108, the input to part 1b

PID PlyrIdx name species NPC ResTNm NumDes NumOwn ships fleets
16 0 re (human) 0 no '' 6 3 5 5
32 1 The Eternal Empire 2 no BIO_EnvTail 47 18 243 59
496 2 Spengler 0 yes DRV_PlsFiss 0 0 0 0
512 3 Spengler 2 yes XNC_TrnsMorr2 0 0 0 0
528 4 Alien Menace 4 yes '' 19 0 0 0
544 5 Peacekeeper Enforcer 4 yes '' 1 0 0 0
560 6 Von Neumann 4 yes '' 6 0 0 0
576 7 Independent Colony 4 yes '' 1 0 0 0

Predicate reading for turn 38→39: the only player that can research holds a target (BIO_EnvTail), NumDes is constant at 47, NumOwn constant at 18. No exposed decision predicted — which is exactly what AR measured (three processes agreed). Part 1b is therefore a re-run of an agreeing turn from a load, and its only open question is the load boundary itself.

2.2 ar-oracle-A-post.sav — Frame 39, ModCount 2147, the input to part 1

PID PlyrIdx name species NPC ResTNm NumDes NumOwn ships fleets
16 0 re (human) 0 no '' 6 3 5 5
32 1 The Eternal Empire 2 no '' ← pick signature 47 18 245 59
496 2 Spengler 0 yes DRV_PlsFiss 0 0 0 0
512 3 Spengler 2 yes XNC_TrnsMorr2 0 0 0 0
528 4 Alien Menace 4 yes '' 19 0 0 0
544 5 Peacekeeper Enforcer 4 yes '' 1 0 0 0
560 6 Von Neumann 4 yes '' 6 0 0 0
576 7 Independent Colony 4 yes '' 1 0 0 0

Predicate reading for turn 39→40: EXPOSED. Player[32] enters with ResTNm == '' and NumOwn 18. otch moved 46 → 47 across turn 39 (BIO_EnvTail completed), which is the same shape AS recorded.

2.3 Ship completions on the lineage, for the fleet-assignment half of the predicate

transition ModCount PID 16 ships PID 32 ships reading
turn 37 → 38 2057 → 2108 −7408 (Zeta Fleet's destroyer, lost in the encounter) +8784, +8800 2 completions, into existing fleets — not a batch
turn 38 → 39 2108 → 2147 none +8816, +8832 2 completions, into existing fleets — not a batch

Two ships a turn into existing Freighters fleets is the quiet fleet consumer, not the thirteen-into-five-new-fleets batch that AD's turn 28 carried. designIds is constant at 95 and fleetIds at 64 across both transitions, corroborating "no design turn, no fleet creation".

So: on the input to part 1b the predicate says NOT exposed; on the input to part 1 it says EXPOSED, via the research pick, for exactly one player.


3. Procedure and configuration

hooks=off throughout, no build (rule 24 is satisfied trivially — nothing is compiled).

item value
guest VM145 sots-re-win10-145, 192.168.10.145
shim build C:\SOTS\shimdist-l4\binkw32.dll, BUILD_ID l4r-20260908T2208Z
config C:\SOTS\shimdist-l4\shim.cfg.l4off, unmodified — hooks=off, installs nothing
why this build lane L4 proved on this guest, with this exact DLL and this exact config that VM145 reproduces the campaign's published autosave oracle byte-identically (ai-order-capture.md §3: bb4fd9ac89f41e3b / 978041acd168b56e). It is the only hooks=off pair on VM145 with a published byte-fidelity result behind it
SavedGames reset to exactly one file — the run's input save — before every launch, so the Load dialog has one row and its position is re-derived from a screenshot anyway
main menu verified from a qm monitor screendump (tools/vmshot.py --one 145), never by sleeping
encounter query detected by sampling the Done pixel at (233, 673): ≈(183,18,0) query up, ≈(8,8,8) map. Auto Resolve Peacefully (667,641), Done (233,673)

VM145 as found, recorded so it can be put back (§8):

binkw32.dll  15,527,327 B  sha256 903527f4a698eea9fbe25f3a6236657c7d8348f994eef394696f187e5e554b97
             (= shimdist-recap, BUILD_ID recap-7584bad-20260908T0615Z)
shim.cfg     sha256 0ae410cc72fe155837d711c78543e14b1f5743310cd023298eb1e5bbd5457e71  (hooks=trace, lane R recapture)
SavedGames   9 files, saved to C:\SOTS\ui\preBQ\
game         not running

4. Results

(added after the runs; nothing above this line is edited)

4.0 What was run

Five fresh processes on VM145, all with C:\SOTS\shimdist-l4\binkw32.dll (BUILD_ID l4r-20260908T2208Z) and SavedGames reset to exactly one file before each launch. The Load dialog was screenshotted every time and the row re-derived: with one file it sits at (400, 262), and it sat there for all five runs. Main menu verified from a qm monitor screendump before every click path; no run was driven by sleeping.

Instrument armed and verified from shim.log, not assumed (rule 1). For all four hooks=off runs the log reads exactly config: hooks=off / hook: disabled by config — nothing installed, no ignoring unknown key lines.

run input config End Turn encounter (Autosave EndTurn).sav (Autosave).sav
1b-A ar-oracle-A-pre (F38) l4off 1 none 107,477 33e30092… 107,732 4c356f59…
1b-B ar-oracle-A-pre (F38) l4off 1 none 107,477 33e30092… 107,732 4c356f59…
1-A ar-oracle-A-post (F39) l4off 1 Venkman, peaceful 107,661 1a9d937b… 108,580 56980aa0…
1-B ar-oracle-A-post (F39) l4off 1 Venkman, peaceful 107,661 1a9d937b… 108,541 b36ac0ec…
1-R ar-oracle-A-post (F39) l4research 1 Venkman, peaceful 107,661 1a9d937b… 108,571 28bbd9b5…

Run 1-R is the producer-gate run the resolution names (§4.4); it is the only instrumented run and it was taken after the two hooks=off processes, not instead of them.

4.1 Part 1b — P1b is FALSIFIED, and the load→turn procedure is nonetheless a valid pair

ar-oracle-A-pre.sav + load + one End Turn does not reproduce 7a8b3d5e…. It produces 4c356f5916f0e6e4… — and it produces it in two fresh processes, byte for byte, at both snapshot points.

The load→turn procedure agrees with itself and disagrees with the continuation. That is not non-determinism; it is a different procedure with a different, reproducible answer.

state_checksum --floats bits --mask none against AR's ar-oracle-A-post.sav: 23 leaves, and they are not the research leaves at all.

leaves what
1 /CD[1]/NPrvVa 501.13 → 506.13 — the load-boundary counter (§4.3)
2 /Summary/Checksum (derived), /Sim/ModCount 2147 → 2145
17 Flt[1570 "Zeta Fleet V"] (PID 32) — FPlan only-in-A, HFPlan True→False, FtFlg 13→12, LocID 0→432, Pos/FtOrig/PrvPos all three components, both ships' Range 7.0 → 9.0
2 Sys[432 "Alpha Centauri"] NumFlts 0→1, Flt only-in-B, FFlags 0→2
1 Flt[914 "Nu Fleet III"] HLay False→True, Lay only-in-B

Read off ar-oracle-A-pre.sav: Zeta Fleet V enters turn 39 parked at Alpha Centauri (LocID 432, HFPlan False, both ships at full Range 9.0).

  • In AR's continuation the AI issued it a move order — a FPlan with one waypoint (Wpt 320, FPsp2 2.0, FPeta2 3, pnd 432), the fleet left the system, FtFlg went to 13 and the ships burned 2 ly of range.
  • In both of my loads the AI issued nothing: the fleet is still at Alpha Centauri with full range, and the system's NumFlts records it as present.

This is an AI fleet-task decision — the exact consumer class the resolution lists as unidentified — and it came out differently across the load boundary while coming out identically in two processes on each side of it. Two hooks=off processes from turn 37 agree (AR, plus AR's instrumented third); two hooks=off processes from a load of turn 38 agree (this lane). They do not agree with each other.

So the honest statement, and it is the one that matters for the standalone:

ar-oracle-A-pre.sav → 7a8b3d5e… is NOT a load→turn pair and must not enter determinism-hashes.txt as one. What is certified, by two fresh processes, is a different pair: ar-oracle-A-pre.sav → (load, one End Turn) → (Autosave EndTurn).sav 33e3009254f5acd1…, (Autosave).sav 4c356f5916f0e6e4….

The single-turn pair still exercises the trade-raid roll (four raiders parked, no encounter), so the standalone gets its first trade-raid calibration pair — with the load→turn hashes, not AR's.

4.2 The resave canonicalisation is not the cause — and the round-trip rule needs one addition

The resolution named the resave canonicalisation as the first suspect if P1b failed. It is exonerated, and the measurement that exonerates it is P1b-aux.

Pre-turn form (ar-oracle-A-pre.sav vs its own resave, my (Autosave EndTurn).sav): 1 leaf. Not Status, not Checksum — those are already 0/consistent in a pre-turn file, exactly as determinism-oracle.md says. The one leaf is /CD[1]/NPrvVa, +5.00.

Post-turn form (ar-oracle-A-post.sav vs my run 1-A (Autosave EndTurn).sav): 6 leaves — Summary/Checksum, the four Player.Status 4 → 0, and /CD[1]/NPrvVa +5.00. Applying the campaign's documented rule (--mask resave, "mask every Player.Status and Summary.Checksum; nothing else varies") leaves exactly one leaf standing:

policy: floats=bits mask=resave  [masked: Checksumx1, Statusx8]
DIVERGED: 1 leaf difference(s)
  /CD[1]/NPrvVa: 501.13360595703125 -> 506.13360595703125

Correction to determinism-oracle.md, "Round-trip stability" / "Recommended canonicalization rule" (rule 11): "Nothing else varies" is false on this save. /CD[1]/NPrvVa advances by exactly +5.00 across a load, in both the pre-turn and the post-turn form, and --mask resave does not cover it. The rule was derived on ref-turn2 / turn2-state, three-player early-game saves; this is a two-empire turn-38 save with a populated CD[1] diplomacy block, and the ratchet broke on new content exactly as rule 27 describes.

What NPrvVa is, measured rather than assumed: it sits in the CD[1] custom-data block next to NPrv (a count) and NPrvId (a player id, 16 here), so it reads as a per-player provocation / threat value. Its observed behaviour: +5.00 per turn boundary crossed, plus +5.00 across the load (496.13 at F38 → 501.13 at F39 in AR's process; 496.13 loaded → 501.13 at the End Turn press → 506.13 at F39 in mine). It is not a monotone counter: on turn 40 it fell, to 461.48 in run 1-A and 476.65 in run 1-B, tracking that turn's combat.

Because it advances on the load itself, no load→turn run can ever reproduce a continuation's hash on this leaf, independently of anything the AI does. P1b was unsatisfiable on NPrvVa alone; the other 22 leaves are the substantive result.

4.3 Part 1 — P1 is half right, and wrong about the mechanism

The two hooks=off processes differ: 56980aa086e1a145… (108,580 B) vs b36ac0ec2986f3a8… (108,541 B). state_checksum: 144 leaf differences.

But the (Autosave EndTurn).sav is identical in all three turn-39 loads (1a9d937b…, 107,661 B, runs 1-A, 1-B and 1-R). The load is perfectly reproducible; the divergence is entirely inside the turn.

And the divergence is not the research pick. Where the 144 leaves live:

leaves subtree
50 /Sim/crep[29]/* — the combat report (per-ship damage, weapon rows, cpk, cdt)
41 Flt[2274 "Sigma Fleet VII"] (PID 32) — the AI fleet in the fight: NShips 5 vs 6, three different ship ids, hull health, 16 TH/THM pairs on one ship
8 Sys[224 "Venkman"] — Pop, PopC, Infra, Res, Suit, RepCur, RepMax, pbon
6 /CD[1]/CmbR[2]/…/TacReports
4 /Sim/turnstats/history/…
20 Player[16 "re"]: BnkPr, BnkEl, otch×6, owep×4, odes×2, ShipRecs, preps, the turn-40 EvMsg
13 Player[32]: Maint, ShipRecs×8, odes, owep×2, otch×2, the turn-40 EvMsg
2 /Sim/ShipIDs[], /Summary/Checksum
1 /CD[1]/NPrvVa

The two EvMsg strings say what happened in one sentence each:

A: "...147,834,923 Imperial citizens were killed. ... 2 enemy ships were destroyed."
B: "... 78,818,284 Imperial citizens were killed. ... 1 enemy ship was destroyed."

The AI's Sigma Fleet VII attacked player 16's colony Venkman, the human committed no ships ("No ships available", Auto Resolve Peacefully in both runs), and the planetary defences killed two raiders in one process and one in the other. Ship deltas confirm it exactly: both runs completed the same two ships for PID 32 (+8848, +8864); run A lost 7744 and 7952, run B lost 8000. Nothing else about the turn moved.

Now the leaf-by-leaf verdict on what P1 named:

P1 clause verdict
the two files differ CONFIRMED
/Sim/RNG identical CONFIRMED — the 2,503-byte raw block is byte-equal (a689d7a3…) in both
/Sim/trdmgr identical CONFIRMED — no trdmgr leaf in the diff
every player-0 fleet identical CONFIRMED — all five PID-16 fleets (3744, 6544, 33, 49, 81) match on id, LocID, Pos and ship count
difference confined to Player[32]'s TechTree/*, ResTNm, otch, Events, turnstats, Checksum FALSIFIED — no TechTree leaf and no ResTNm leaf differs at all, and the difference reaches a colony's population, both players' banks and the combat reports
every Player[16] leaf identical FALSIFIED — twenty Player[16] leaves moved

The research pick did not vary. Both processes set Player[32]'s ResTNm to BIO_GrvAdpt, otch 47 in both, NumDes 47, NumOwn 18. The predicate's own signature — ResTNm == '' at turn start for the one player that can research — fired, a pick was made, and it was the same pick in both processes.

4.4 The producer gate, run — and the answer is better than "a producer supplied it"

The resolution's follow-up for exactly this outcome: "a producer supplied the target … and the predicate needs a producer gate. One run with lane L4's airesearch=on dump names which producer returned non-null." That config is on the guest already (shimdist-l4\shim.cfg.l4research), so it cost one run and no build. Run 1-R, same input, same procedure, same peaceful resolution.

verify/results/shim/bq/bq-res-aiorders.txt, the whole research record for the turn:

---- airesearch sel=1 agent=0x32446560 ... curTarget=0x00000000 species=2 ----
aicand   sel=1 idx=0 agent=0x32446560 slot=0x3244669c w0=0x00000002(2) w1=0x00000001(1)
airesult sel=1 candidatesTried=1 chose='BIO_GrvAdpt' (ptr=0x0463fd3c)
---- airesearch sel=2 ... curTarget=0x04707c50 species=0 ----      (PID 496, target held)
---- airesearch sel=3 ... curTarget=0x0d6c34a8 species=2 ----      (PID 512, target held)

aiprobe idx=16 AIResearch::ProducerA     turn=3 total=3
aiprobe idx=17 AIResearch::ProducerB     turn=3 total=3
aiprobe idx=18 AIResearch::FallbackIndex turn=0 total=0
aiprobe idx=19 AIResearch::FallbackArm   turn=0 total=0

It was not a producer short-circuit. Player[32] reached producer C — the candidate walk — and the candidate list had length ONE. Producers A and B were entered once per AI player and both returned null for the curTarget == 0 agent; SelectResearchTarget ran; TryResearchCandidate was offered exactly one candidate and accepted it; the 0x006b36e0 & 7 fallback rotation was never reached (FallbackIndex / FallbackArm both 0).

candidatesTried = 1 is the whole explanation. The outcome set of that pick is a singleton because the candidate vector holds one element, and no draw on any stream can change a one-element choice.

That is a different mechanism from the one the resolution guessed, and it is strictly more useful, because it is a counting criterion rather than a code-path one. It also reconciles the three lanes without special pleading:

state ResTNm == ''? path outcome set control
turn1-state, PID 32 (lane L4) yes producers A/B, no walk 1 (varies — for other reasons; see below)
turn1-state, PID 512 (lane L4) yes walk, 1 candidate 1 —
as-turn15 → 16, PID 32 (lane AS) yes not measured > 1 (A chose 64, B chose 62) varies, 22 leaves
ar-oracle-A-post → 40, PID 32 (this lane) yes walk, 1 candidate 1 pick agrees

So ResTNm == '' is a necessary condition for research exposure, not a sufficient one. The sufficient condition is |candidates accepted by TryResearchCandidate| > 1, and that is not readable from the pre-turn save — it needs the walk. §5 states the corrected predicate.

4.5 What actually varies on turn 40, and why it is the warning already on the books

The varying decision is combat resolution, and resolution 09-08 §6 already carries the warning: "combat turns are not yet known to be cross-process reproducible." AR added one datum against it — a turn-38 encounter auto-resolved peacefully that reproduced across three processes — and explicitly declined to treat that as discharging the warning, on the reasoning that "peacefully" probably bypasses RunCombatRound.

AR was right to decline, and the reason is now measured: Auto Resolve Peacefully does not bypass combat when the defender is a colony. The human committed no ships; the planet fought, RunCombatRound ran, and it is per-process. Three processes on the same input produced three different kill counts (2 / 1 / — run 1-R gives a third distinct file, 28bbd9b5…, whose 48-leaf diff against run 1-A is again wholly inside crep, Sigma Fleet VII, Venkman and the two EvMsgs).

Auto Resolve Peacefully is not a determinism-preserving resolution. It means "I commit no ships", not "no combat occurs". A procedure that contains a peaceful auto-resolve is reproducible only if the other side also has nothing to fight with — which is why AR's turn-38 encounter reproduced and this turn-40 one does not.

One consequence for rule 26 (c), and it is the sharp one. Rule 26 says a control must agree with itself before it exonerates anything. Here the control varies, so nothing on turn 40 can be measured off a single run — and the sub-tree discipline lane AD used is what saves the turn: /Sim/RNG, /Sim/trdmgr and every player-0 fleet are identical across the two processes, so a lane measuring the trade half on this turn has a valid sub-tree, and a lane measuring anything downstream of Venkman does not.

Lane BP's pinned-seed-rich-turn.md landed on VM146 while these runs were in flight, and its result and this one point at the same layer of the AI from opposite directions. Neither lane knew the other's answer when it committed its prediction.

lane BP (probe 2) lane BQ (probe 1b)
what was held constant the per-client seed (pinned), same process shape the save (identical input, identical hooks=off config)
what varied the process the route to the state — continuation from turn 37 vs load of turn 38
residue newly allocated fleet ids paired differently with newly formed fleets a fleet's move order issued in one route and not the other
BP's reading a per-process input that is not the seed; inferred to be a pointer-keyed container under ASLR —

One datum this lane can hand BP for free, and it is against the ASLR hypothesis as stated. shim.log records the image base on every launch, and on VM145 it was identical in every one of the five processes: exe base=0x00f40000 (link-time image base 0x00400000, ASLR delta +11796480). Different pids, different sessions, same base. That does not touch heap addresses — BP's residue could still be a heap-pointer-ordered container — but it does mean exe ASLR is not varying on these guests, so any hypothesis that reaches for image-base randomisation needs to say which allocation it means.

And the two results are not the same phenomenon: BP's two processes disagreed with each other, while this lane's two load processes agreed with each other and disagreed with the continuation. So BP has a per-process input and this lane has a per-history input. Both live in the fleet layer, and both are open. Probe 3's per-client draw ledger, taken on ar-oracle-A-pre → 39 twice — once by continuation from turn 37 and once from a load — is now the single measurement that would speak to both.


5. Verdicts on the committed predictions

# prediction verdict
P1b ar-oracle-A-pre.sav + load + one End Turn → 7a8b3d5e… FALSIFIED. It produces 4c356f59…, reproducibly, in two fresh processes. 23 leaves apart from AR's continuation; 17 of them one AI fleet that the continuation moved and the load did not
P1b-aux the same run's (Autosave EndTurn).sav = 15b99255… (the input's own hash) FALSIFIED by exactly one leaf — /CD[1]/NPrvVa, +5.00. Every other leaf of the pre-turn round trip is byte-exact, so this lands in the fourth branch P1b-aux enumerated: "the resave is not byte-exact but is behaviourally equivalent". The resave canonicalisation is exonerated as the cause of P1b's failure, and determinism-oracle.md's "nothing else varies" is corrected (§4.2)
P1 the two turn-40 files differ CONFIRMED
P1, clause: /Sim/RNG, /Sim/trdmgr, every player-0 fleet identical CONFIRMED, all three
P1, clause: difference confined to Player[32]'s TechTree/*, ResTNm, otch, Events, turnstats, Checksum FALSIFIED. Zero TechTree leaves and zero ResTNm leaves differ. The difference is a combat at Venkman and its consequences
P1, clause: every Player[16] leaf identical FALSIFIED — twenty of them moved

The prediction was right for the wrong reason, and that is the result. P1 said the files would differ because the research pick is exposed. The files differ, the research pick is not exposed, and the exposed decision is one the predicate does not mention at all.

5.1 The predicate, corrected

Rule 26 corollary (c) currently reads "ResTNm == '' names a pick turn". Measured here:

  1. ResTNm == '' at turn start is necessary, not sufficient. It names a turn on which a pick will be made. Whether that pick is exposed is |candidates| > 1, and on this state |candidates| = 1 (§4.4, candidatesTried=1). A pick with one candidate is a decision with outcome set 1 — the resolution's own criterion, satisfied.
  2. The predicate has a false-positive class that was visible before the run (§1.4): four NPC players carry ResTNm == '' on every turn of this lineage, including the turns whose controls agree. All four have NumOwn == 0. The predicate must be read only for a non-eliminated AI player with NumOwn > 0.
  3. The predicate is incomplete on the other side: it has no combat term. Turn 40 was exposed through RunCombatRound, and the pre-turn save does name the condition — an AI fleet (Sigma Fleet VII, PID 32) standing at an enemy colony (Venkman, PID 16). That is as readable from the save as ResTNm is, and it belongs in the predicate.
  4. A fourth term, and it is not a predicate on the save at all: the load boundary. Turn 39 ran one way in a process that had just run turn 38 and another way in a process that loaded turn 38's autosave — with both sides reproducible. So the pre-turn save does not fully determine the next turn's AI fleet-task decisions. Whatever Zeta Fleet V's move order depended on was not in the file. That is a bigger claim than exposure and it is the one this lane would send back to the resolver.

6. Coverage, and what was not done (rule 15)

  • Two processes per pair, not three. Rule 26's bar, matching AR's. Run 1-R is a third process on the part-1 input but it is instrumented, so it counts as corroboration of variability, not as a third control.
  • Rule 19 cannot be run on turn 40, and it is worth saying why. The hooks=off control varies here, so an instrumented run agreeing with either arm would be a 1/k coincidence with k ≥ 3 (rule 26). Run 1-R produced a third distinct file; that is consistent with neutrality and proves nothing about it. No neutrality claim is made for l4research on this state.
  • The 1b divergence is localised but not explained. This lane measured that the load boundary changes an AI fleet-task outcome; it did not identify the input that is missing from the save. That needs probe 3's per-client draw ledger with this recorded, on ar-oracle-A-pre → 39, taken twice — once by continuation from turn 37 and once from a load. The site that fires in one and not the other is the answer, and the resolution has already queued exactly that instrument.
  • |candidates| = 1 is one observation on one turn. It explains this agreement; it is not a claim that the candidate list is usually 1 (lane AS's state clearly had more). Rule 20: do not fit a constant to one observation.
  • The combat was measured, not decoded. Three kill counts (2, 1, and run 1-R's own) on one encounter. No draw site is attributed — hooks=off throughout for the two controls, and this lane makes no claim about which consumer inside RunCombatRound varies.
  • The 0x0069dbb0 fuse cannot have fired in any of these runs (earliest fire is 3 turns after the load that draws it), so nothing here bears on it in either direction.
  • /CD[1]/NPrvVa is named, not decoded. Its neighbours (NPrv, NPrvId = 16) read as a per-player provocation record and its behaviour is +5.00 per turn boundary and per load, falling on a combat turn. No writer was looked for. It is a one-line watchpoint for whoever wants it (rule 18).

7. Artefacts

what where
this document findings/control-flow/seed-exposure-forward-test.md
1b pair, two fresh hooks=off processes, byte-identical verify/results/saves/bq/bq-1b{A,B}-{endturn,post}.sav (33e30092… / 4c356f59…)
part 1, two fresh hooks=off processes, differ verify/results/saves/bq/bq-1{A,B}-post.sav (56980aa0… / b36ac0ec…), shared bq-1{A,B}-endturn.sav (1a9d937b…)
producer-gate run (instrumented, l4research) verify/results/saves/bq/bq-res-post.sav (28bbd9b5…)
the research record — candidatesTried=1 chose='BIO_GrvAdpt' verify/results/shim/bq/bq-res-aiorders.txt
shim log (install status for all 20 aiprobes + 3 airesearch detours) verify/results/shim/bq/bq-res-shim.log
inputs (unchanged) verify/results/saves/ar-oracle-A-{pre,post}.sav

verify/results/saves/bq/ is a subdirectory, so it is invisible to the corpus scan (find_saves() uses a non-recursive os.listdir on *.sav) and the coverage ratchet is not touched (rule 27). None of these files should be promoted into the corpus without deciding that question deliberately.


8. VM145 as left

Hold released. Guest restored and verified byte-for-byte against the as-found snapshot (§3):

SavedGames   the same 9 files, every sha256 identical to as-found
binkw32.dll  15,527,327 B  903527f4a698eea9…   (= shimdist-recap, recap-7584bad-20260908T0615Z)
shim.cfg     0ae410cc72fe1558…  hooks=trace     (lane R's recapture config)
game         stopped, as found

Left in place, all additive: C:\SOTS\ui\preBQ\ (the as-found SavedGames snapshot), C:\SOTS\ui\bq\ (the two input saves and this lane's outputs), and C:\SOTS\ui\bq{deploy,grab,click}.ps1. click_helper.ps1 is unmodified.

Four things for the next holder of VM145.

  • Startup is ~2 minutes if you drive it, confirming lane AR's correction on VM141: fg + key {ESC} ×3 spaced ~2.5 s, then ~90 s, and the main menu is up. Loading a turn-38/39 save is a further 3–4 minutes and is not compressible. Verify both from a screendump; tools/vmshot.py --one 145 is one call and does not touch the guest.
  • Chained clicks in one cmd.txt are unreliable through the Load dialog. A three-click chain (Load Game → Single Player → OK) landed only the first click, twice. One bqclick.ps1 call per click, with a screenshot between, is the only thing that worked reliably — independent confirmation of lane M's gotcha, on a different guest.
  • Reset SavedGames to exactly one file — the run's input. The Load dialog then has one row at (400, 262) and there is nothing to re-derive. bqdeploy.ps1 -Save <name> does it, prints the input's sha256, and re-verifies the config.
  • C:\Windows\system32\cmd.exe windows appear over the game as a side effect of the SOTSUI scheduled task and will cover the map. fg raises the game again; screenshot before believing a click landed.
  • The Done pixel test works exactly as documented and is worth keeping: (233, 673) read (183, 18, 0) with the Venkman query up and (0, 0, 0) on the map (AR's "≈(8,8,8)" — same thing, the map is near-black there). Auto Resolve Peacefully (667, 641) shows a tooltip and the dove fills the centre panel, so the selection is confirmable from a screenshot before Done.

Proposed board rows

New rows:

| PROBE 1: the load->turn pair is NOT the continuation's - AR's second hash does not survive a load, and the load form is its own reproducible pair | meta | verified | high | 100% | 2026-09-09 | **Lane BQ**, VM145, `findings/control-flow/seed-exposure-forward-test.md`, probe 1b of the 09-09 resolution. Prediction committed before the run (commit be14266): `ar-oracle-A-pre.sav` (Frame 38, `15b99255...`) + LOAD + one End Turn reproduces `7a8b3d5e...`. **FALSIFIED.** It produces `4c356f5916f0e6e4...` (107,732 B), and it produces it in TWO FRESH `hooks=off` PROCESSES BYTE FOR BYTE, at both snapshot points (`(Autosave EndTurn).sav` `33e3009254f5acd1...` in both). So the load->turn procedure AGREES WITH ITSELF and DISAGREES WITH THE CONTINUATION. 23 leaves apart from AR's file and **17 of them are ONE AI FLEET**: `Flt[1570 "Zeta Fleet V"]` (PID 32) enters turn 39 parked at Alpha Centauri with full range; in AR's continuation the AI issued it a move order (FPlan Wpt 320, FtFlg 12->13, LocID 432->0, both ships Range 9.0->7.0) and in BOTH loads it issued nothing. Plus `Sys[432]` NumFlts 0->1 and `Flt[914 "Nu Fleet III"]` HLay True/False. **RULING: `ar-oracle-A-pre.sav -> 7a8b3d5e...` MUST NOT enter determinism-hashes.txt as a load->turn pair.** What IS certified, two processes: `ar-oracle-A-pre.sav -> (load, one End Turn) -> 33e30092.../4c356f59...` - and that is still the standalone's first trade-raid pair (four raiders parked, no encounter). **THE RESAVE CANONICALISATION IS EXONERATED**: the pre-turn round trip is byte-exact but for ONE leaf, `/CD[1]/NPrvVa` (+5.00 across a load). CONSEQUENCE: **the pre-turn save does not fully determine the next turn's AI fleet-task decisions** - something `Zeta Fleet V`'s order depended on is not in the file. Resolver case; probe 3's ledger on `ar-oracle-A-pre -> 39` taken BOTH by continuation and by load names the site |
| PROBE 1: turn 40 varies - but NOT by the research pick. `Auto Resolve Peacefully` does NOT bypass combat, and a pick with ONE candidate is not exposed | control-flow | verified | high | 100% | 2026-09-09 | Lane BQ, VM145. `ar-oracle-A-post.sav` (`7a8b3d5e...`, Player[32] `ResTNm == ''`) in two fresh `hooks=off` processes, one End Turn, `Auto Resolve Peacefully` in both. **The files DIFFER** (`56980aa0...` vs `b36ac0ec...`, 144 leaves) - the committed prediction's first clause holds, and so do `/Sim/RNG` identical (2,503-B block byte-equal), `/Sim/trdmgr` identical and every player-0 fleet identical. **THE MECHANISM CLAUSE IS FALSIFIED: ZERO TechTree leaves and ZERO ResTNm leaves differ.** Both processes picked **BIO_GrvAdpt**. The 144 leaves are a COMBAT: the AI's `Sigma Fleet VII` (PID 32) hit player 16's colony Venkman, the human committed no ships, and the PLANET's defences killed 2 raiders in one process and 1 in the other (50 leaves `crep`, 41 the AI fleet, 8 `Sys[224 "Venkman"]`, 20 `Player[16]` - the prediction's "every Player[16] leaf identical" is also FALSIFIED). Both runs completed the SAME two ships (+8848,+8864); only the losses differ (7744+7952 vs 8000). **So `Auto Resolve Peacefully` means "I commit no ships", NOT "no combat occurs" - it is not a determinism-preserving resolution, and resolution 09-08 §6's combat warning is now measured, not just retained.** AR's turn-38 peaceful auto-resolve reproduced because the other side had nothing to fight with. **WHY THE PICK DID NOT VARY - the producer run, `shim.cfg.l4research` already on the guest, no build**: producers A and B were entered once per AI player and BOTH RETURNED NULL for Player[32]; it reached producer C, the candidate walk; `TryResearchCandidate` was offered **ONE** candidate and took it - `airesult sel=1 candidatesTried=1 chose='BIO_GrvAdpt'` - and the `0x006b36e0 & 7` fallback rotation was never reached (FallbackIndex/FallbackArm both 0). **The outcome set is a singleton because the CANDIDATE LIST HAS LENGTH ONE.** Not the producer gate the resolution guessed - a COUNTING criterion, and a better one |
| Rule 26 (c) needs four corrections, all measured on one lineage in one session | meta | verified | high | 100% | 2026-09-09 | Lane BQ, from probe 1. (1) **`ResTNm == ''` is NECESSARY, NOT SUFFICIENT** for research exposure: it names a turn on which a pick is MADE; exposure needs `|candidates accepted by TryResearchCandidate| > 1`, and on `ar-oracle-A-post -> 40` it is 1, so the pick agreed across two processes. Lane AS's varying pick (64 vs 62) had a longer list. The sufficient condition is NOT readable from the pre-turn save. (2) **The predicate has a false-positive class, and it was visible BEFORE the run**: four NPC players (528 Alien Menace, 544 Peacekeeper Enforcer, 560 Von Neumann, 576 Independent Colony) carry `ResTNm == ''` on EVERY turn of this lineage INCLUDING the turns whose controls agree; all four have `NumOwn == 0`. Read the predicate only for a non-eliminated AI player with NumOwn > 0. (3) **The predicate has no COMBAT term and needs one** - turn 40 was exposed through `RunCombatRound`, and the pre-turn save names the condition as plainly as it names ResTNm: an AI fleet standing at an enemy colony. (4) **A fourth term is not a predicate on the save at all**: turn 39 ran one way by continuation and another way after a load, BOTH reproducibly, so exposure is a property of (save, procedure, LOAD HISTORY), not of (save, turn). Rule 26 (c)'s instruction "record the pair as (save, procedure, hashes)" was already right and is now load-bearing for a reason it did not anticipate |

Edits to existing rows:

  • Row 426 (the resolution's row) — append: PROBE 1 RUN 2026-09-09 (lane BQ, VM145). 1b FALSIFIED: ar-oracle-A-pre + LOAD + one End Turn gives 4c356f59..., not 7a8b3d5e..., reproducibly in two processes - the AI moved Zeta Fleet V in the continuation and not after a load, so the pair may NOT enter determinism-hashes.txt in AR's form; the LOAD form (33e30092.../4c356f59...) is certified instead. The resave canonicalisation is EXONERATED (pre-turn round trip differs in one leaf, /CD[1]/NPrvVa +5.00 per load). Probe 1 CONFIRMED "the files differ" and CONFIRMED /Sim/RNG + /Sim/trdmgr + player-0 fleets identical, but FALSIFIED the mechanism: the research pick AGREED (both chose BIO_GrvAdpt) and the variation is a COMBAT at Venkman. Producer run: producers A and B both returned null, Player[32] reached the CANDIDATE WALK, and candidatesTried=1 - the outcome set is a singleton by COUNT, not by producer gate.
  • Row 424 (AR's oracle pair) — append: LOAD-BOUNDARY SCOPE 2026-09-09 (lane BQ): this pair is certified ONLY as the two-turn continuation from ar-turn37-816raiders.sav. Its second half does NOT survive being reached by load: ar-oracle-A-pre.sav + load + one End Turn gives 4c356f59..., twice, and differs from 7a8b3d5e... in 17 leaves of ONE AI fleet's movement (Zeta Fleet V moved in the continuation, stayed put after a load). Use the load->turn hashes for the standalone.
  • Row 62 — VM145 = FREE (lane BQ released 2026-09-09; guest RESTORED and verified byte-for-byte to as-found: SavedGames = the same 9 files with identical sha256, binkw32.dll = shimdist-recap 15,527,327 B 903527f4..., shim.cfg = 0ae410cc... hooks=trace, game stopped). Lane BQ left C:\SOTS\ui\preBQ\ + C:\SOTS\ui\bq\ + ui\bq{deploy,grab,click}.ps1; click_helper.ps1 unmodified. GOTCHA (confirms lane AR's startup correction on a second guest): fg + key {ESC} x3 spaced ~2.5 s then ~90 s puts the main menu up in about TWO minutes; loading a turn-38/39 save is a further 3-4 min and is NOT compressible. GOTCHA (confirms lane M on a second guest): chained clicks through the Load dialog DROP - a three-click chain landed only the first click, twice. ONE click per helper call with a screenshot between. TIP: reset SavedGames to EXACTLY ONE file (the run's input) and the Load dialog is one row at (400,262) with nothing to re-derive. GOTCHA: C:\Windows\system32\cmd.exe windows from the SOTSUI task appear OVER the game and cover the map - fg raises the game; screenshot before believing a click landed. CONFIRMED: the Done pixel at (233,673) reads (183,18,0) with a query up and (0,0,0) on the map; Auto Resolve Peacefully (667,641) shows a tooltip and fills the centre panel with the dove, so the selection is confirmable from a screenshot before Done.
  • determinism-oracle.md "Recommended canonicalization rule" / row for the oracle — append: CORRECTED 2026-09-09 (lane BQ, rule 11): "mask Player.Status and Summary.Checksum; NOTHING ELSE VARIES" is FALSE on ar-oracle-A-pre/post. --mask resaveleaves ONE leaf standing -/CD[1]/NPrvVa, which advances by exactly +5.00 across a load (and per turn boundary), and FALLS on a combat turn. The old rule was derived on three-player early-game saves with an empty CD[1] diplomacy block. Any load->turn comparison must mask it too, or no load->turn run can ever reproduce a continuation's hash.
  • Row 326 / the seed rows — append: SHARPENED 2026-09-09 (lane BQ): a research pick is exposed only when the CANDIDATE LIST has more than one entry. Measured on ar-oracle-A-post -> 40: Player[32] entered with ResTNm == '', producers A and B returned null, the candidate walk ran, candidatesTried=1, and the pick AGREED across two fresh processes. ResTNm == '' predicts a pick, not a divergence.