# The AI client seed is fresh in every process — the `turn1-state → turn2` variation is a seed effect - **Type:** subsystem (live measurement) - **Status:** **verified** — the same save loaded in two freshly launched processes, with the seeding primitive itself hooked - **Confidence:** high on the fact (every AI seed differs, structure otherwise identical); the *source* of the seed is **not** identified — §4 - **Owner / date:** lane L1 · 2026-09-08 · VM140 - **Instrument:** `sots-engine` `wip/l1`, `Mars::RNG::Seed` + `Game::StrategyApp::RunAI` hooks, config `shim.cfg.l1seed`, build `l1seed-20260908T2206Z` - **Answers:** the probe requested against `findings/subsystems/turn1-to-turn2-nondeterminism.md` (lane L5) — *"is the varying research pick a seed effect or an ordering effect?"* - **Corrects:** `ghidra/addresses.d/lane-ai1.json` — `g_UnseededStaticRNG`'s "every draw returns 0" --- ## 0. The answer, and it is not the one that was predicted **Seed effect.** Every AI client's generator is seeded with a **different value in every process**. Two launches, same input (`l1-turn1.sav` = `verify/results/saves/turn1-state.sav`), loaded only — no End Turn, because `StrategyApp::RunAI` and the client constructor run on load: | `RunAI` call | player net id | process 1 `rng_seed` | process 2 `rng_seed` | |---|---|---|---| | 1 | 32 | `0x75F692C0` | `0x414F415E` | | 2 | 496 | `0xF2EDAC21` | `0x10B94E78` | | 3 | **512** (the empire lane L5 saw vary) | `0x165A2ADB` | `0xC218DBF8` | Each value reappears one record earlier as the `seed` argument of an `Mars::RNG::Seed` call on a fresh object, which is the `StrategyClient+0x134` generator being constructed. **Both my prediction and the coordinator's expectation were that these would be identical.** They are not. **The structure is identical, which is what makes the difference legible.** Both processes emit exactly 8 records in exactly the same order — four `Seed` calls interleaved with three `RunAI` calls, same net ids, same `personality = 0`, same `ai_data = "SavedGames/l1-turn1.sav"`. Nothing about *which* objects are seeded or *when* varies. Only the values do. **And one of the four seeds is deterministic**, which rules out "the hook perturbs it" and gives the comparison a built-in control: the first `Seed` call takes `seed = 0x00000000` in both processes and produces a byte-identical state (`mt0 = 2443250962`, `mt1 = 1093594115`, `mt2 = 1878467924`, `left = 624`). A run in which the instrument randomised things would not have left that one alone. ## 1. Why only one of three empires' picks moves Lane L5's puzzle was *"a time-seeded per-client RNG would move all three"*. That inference does not hold, and the data says why: **all three seeds do move.** A different seed only becomes visible in the save where the decision it feeds actually depends on the draw. Two of the three AI players evidently have a unique best research candidate — a different random stream picks the same one — and the third does not. So the observation "one of three varies" is not evidence for an ordering effect at all. It is evidence that **two of the three decisions are robust to the stream and one is not**, which is a statement about the candidate sets and not about the generator. ## 2. What this settles for the campaign * **The `turn1-state → turn2` pair is unfixable as a byte oracle by any amount of care with the instrument.** The input to the varying decision is not in the save, is not the strategic generator, and is fresh per process. `determinism-oracle.md` is now scoped to `ref-turn2 → turn 3`. * **A standalone can never match a per-process seed.** For anything downstream of an AI client's generator, the reachable target is *a* legal outcome, not *the* recorded one. Which leaves is exactly what lane PL put on Rung B for these shadow empires — the corroboration is now mechanical rather than statistical. * **The strategic generator is untouched by this.** Lane L1's hive run reproduced the historical word positions and `NextQ` values exactly (`hive-creation-rng.md` §2.2). Two generators, two different determinism stories, and only one of them is in the save. ## 3. Correction to `lane-ai1.json` `g_UnseededStaticRNG`'s entry reads: > *its only static initialiser writes the `Mars::IStreamable` vftable, NOT the `Mars::RNG` vftable > that `RNG_Seed` installs: none of the six `RNG_Seed` call sites in the image targets it, so its > `mt[624]` is the zero-initialised BSS array and `left` is 0. An all-zero MT19937 state is a fixed > point of the twist, so **EVERY draw from it returns 0**. Five consumers: SNMRunAI (the AI client > seed, OnMessage+0x955) …* The arithmetic is right and the conclusion is **falsified by measurement**: the seed `SNMRunAI` hands to `RunAI` is neither 0 nor constant. That entry was explicitly flagged by its own author as "the all-zero fixed point is arithmetic, not measurement", with a prediction attached — this is that prediction coming back negative, which is the best possible outcome for having written it down. Either the object `OnMessage+0x955` draws from is not the one AI1 identified, or that object is seeded by a path outside the six `RNG_Seed` call sites. §4 says how to tell. ## 4. What is NOT established, and the cheapest next step * **Where the seed comes from.** This lane hooked `RNG_Seed` and `RunAI`; neither says what fed `RunAI`'s fourth argument. The candidates are a time source (`timeGetTime`/`GetTickCount`, the usual suspects for a per-process value) and a generator seeded from one. **The next probe is one hook**: `StrategyNetworkClient::OnMessage` around `+0x955`, or simply a breakpoint-style read of whatever object that site draws from, in the same two-process shape. The seeds recorded here (`0x75F692C0` / `0x414F415E` …) are the values it must reproduce. * **Whether the seed varies within one process.** Both runs seed three clients with three different values, so *something* advances between them — consistent with successive draws from one generator and equally with three reads of a clock. * **Whether any of this reaches the strategic generator.** It does not on the measured turns (`hive-creation-rng.md` §6.2), but that is two turns of one save. * **The other four `Seed` calls' owners.** Only the three that immediately precede a `RunAI` are identified. The `seed = 0` one is unidentified and is the only deterministic one, which makes it the most interesting of the rest. * **`is_strategic_generator` reads `false` on every record** in these runs — correctly, but only because this config installs no turn-driver hook, so the shim never learned the server pointer. It is not evidence that the strategic generator is never seeded here. ## 5. Files - `verify/results/shim/l1/trace-seed-proc1.jsonl`, `trace-seed-proc2.jsonl` — the two traces - `verify/results/shim/l1/seed-two-processes.txt` — both rendered side by side - `verify/results/shim/l1/seedrep.py` — the reader - Prediction, committed before the build: `sots-engine/docs/L1-predictions.md` §P7