sots-re/findings/subsystems/ai-client-seed-is-per-process.md
alex 4f805e381a L1: CDiff crosses at frame 50, and the AI client seed is fresh in every process
Two results from the same VM session, both of which needed a workload the
corpus cannot supply.

CDiff, played forward. ref-turn2 loaded and 49 End Turns driven through the UI
helper to frame 51, with the entry hook emitting one record per turn:

  50 calls, frames 2..51, EXACTLY TWO STORES
    frame  2  -1 -> 0
    frame 50   0 -> 1     <- the modelled tier transition, at the modelled frame
  and 47 turns between them on which the writer ran and wrote NOTHING

predict_path was computed at entry from the transcribed threshold table on all
fifty turns and agreed with the cdiff region on every one. Frame >= 100 is still
a code read and is not claimed as a measurement.

The AI seed probe (asked for by the coordinator, ranked above CDiff). Hooks on
Mars::RNG::Seed and StrategyApp::RunAI, two launches from turn1-state, load
only. Every AI client seed differs between processes -- net 32, 496 and 512 all
move -- while the record structure is identical and one Seed call with seed=0
produces a byte-identical state in both runs.

So the turn1-state -> turn2 nondeterminism is a SEED effect, not the ordering
effect predicted, and the 'one of three varies' observation is explained by two
of the three empires having a research pick that is robust to the stream. This
falsifies lane AI1's 'every draw from the static generator returns 0', which
that lane had explicitly flagged as arithmetic rather than measurement.

Where the seed comes from is NOT established; the finding names the one hook
that would settle it and the six values it must reproduce.

VM140 left as found: the 8-file save set with its oracle bytes intact.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
2026-09-08 18:23:19 -04:00

7 KiB

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