sots-re/findings/resolutions/2026-09-08-ai-seed-per-process.md

33 KiB
Raw Blame History

Resolution — the AI client seed is fresh per process; the tie hypothesis is dead, the byte-match is not

  • Type: resolution (course-setting; no measurement taken here)
  • Trigger: lane L1's P7 falsified — findings/subsystems/ai-client-seed-is-per-process.md
  • Date / author: 2026-09-08 · re-resolver
  • Touches: backlog.md §2 (Rung C and the mechanism paragraph), board rows 240 / 255 / 331 / 333 / 335, determinism-oracle.md (closing paragraph), ai-turn-logic.md §5.1 + P1, ai-order-capture.md §3.1–§3.2, lane-ai1.json::g_GlobalRNG, lane L4 and CB briefs, method-rules.md (new rule 25)

0. Decision

  1. The variation on turn1-state → turn2 is a seed effect. Each AI StrategyClient's generator is seeded at construction with a 32-bit word that differs in every process. The coordinator's tiebreak-by-ASLR inference, and lane AI1's all-zero-seed derivation, are both overturned. The instrument is exonerated (§2).
  2. The byte-match ambition survives, and in a stronger form than the tie-set claim. The original's decision layer is MT19937 from that one word — a generator we already own bit for bit — so the AI is a deterministic function of (save, per-client seed). A deterministic game/ai that takes the seed as an input can byte-match every run whose seeds were captured, not only the runs where a tiebreak happens to agree. That answers the user's standing question — make it deterministic and the old approach still pays — with a yes, on the condition that the seed is treated as part of the input. Rung C is restated accordingly (§5).
  3. The tie-set claim is rescued as a statement and open as a result — and not on "equal cost". Where seeds were never captured (the historical turn2-state.sav) no one can reproduce the run — the original included. There the claim is membership in the outcome set of a random pick, S(D), with a four-part verification statement (§3.1). Lane L4 has one arm (k = 6, XNC) and BIO_GnMod proves a second arm exists whose producer is unidentified, so "we can name all k" is to prove, not proved. That is the fallback, "C-set".
  4. One probe, before anything else is redirected: pin the three client seeds and run the creation turn twice — through L4's resolver instrument, then sweep seeds. Prediction: byte-identical autosaves, same pick under the same seed, a measured word cost inside TryResearchCandidate. If it holds, the seed is the only per-process input on that turn, turn1-state → turn2 returns to the oracle set (with seeds), game/ai has a well-posed exact target, and the sweep names the BIO_GnMod arm. The seed's source is not the probe. §3.1, §4.
  5. Rule 19 gets a companion, rule 25: a control must agree with itself across two fresh processes before it can exonerate or indict anything, and "one of N moves" says nothing about mechanism. Draft in §7.

1. The surprise, stated precisely

Loading turn1-state.sav in two freshly launched processes, with Mars::RNG::Seed and StrategyApp::RunAI detoured, produced the same eight records in the same order with the same net ids — and three different seeds for the three AI clients in each process, none shared between processes (0x75F692C0 / 0xF2EDAC21 / 0x165A2ADB vs 0x414F415E / 0x10B94E78 / 0xC218DBF8). The prediction committed before the build was "identical, and zero". It was wrong on its first falsifier row.

The evidence is as strong as it looks. The Seed hook records the post-state of the object it seeded; the one call that takes seed = 0 gives mt0..mt2 = 2443250962 / 1093594115 / 1878467924 in both processes, so the hook reproduces a known input to a known state and did not randomise anything. The build was fresh (l1seed-20260908T2206Z, lane L1's own dist). The workload is load-only, so no End Turn logic is involved — this is the construction path and nothing else.

Two further things the raw trace says that the report does not spell out:

  • The three seeds within one process are unrelated words (high bits 0x75, 0xF2, 0x16), not three reads of a clock microseconds apart. They are successive outputs of a generator. That is exactly what AI1 read at OnMessage+0x955 — mov ecx,0xaf6e58 ; call NextUInt — so the call site reading stands and the "never seeded" reading falls: g_GlobalRNG is live and per-process. Something initialises it that a scan of the six direct RNG_Seed sites cannot see — rule 16's shape (an inlined MT init, or a state copy), or the object at 0xaf6e58 is not the one being drawn. Inference; §4.2 names the cheap check.
  • A fourth per-process seed exists before any AI client — record #2, seed = 0xf22aac3a / 0x16612cc3, on a heap object nobody has identified. It is either the global's source, a sibling consumer of it, or an unrelated per-process generator. On ref-turn2 it changes nothing save-visible (seven fresh processes agree there); on other workloads it is unbounded.

2. The instrument — exonerated, and the reason is worth keeping

Three lanes' instruments cannot be the cause and the seed probe shows why:

  • L5's pair A/B carried identical hooks and disagreed; L1's instrumented run coincided with L5's un-instrumented one; L4's runs differed with and without hooks. No hook configuration selects the pick.
  • ref-turn2 → turn3 reproduces byte-for-byte under every hook set tried (H, L1, L4, F, V) and under none. The instruments are behaviour-neutral on a workload where the game is self-consistent.
  • The seed probe's built-in control (seed = 0 → identical state) shows the Seed detour itself does not perturb the object it observes.

But no hooks=off run on turn1-state was a control of anything — L5's, L1's and L4's hooks=off runs gave three different files (62061918…, e43ec1d2…, d59bb9f2…). Each lane cited its single hooks=off run as "the un-instrumented value"; together they show the un-instrumented game has no single value there. The exoneration above rests on the identical-hooks pair and on ref-turn2, not on those runs. That is the gap in rule 19 — §7.

3. Standing claims, one by one

claim where verdict scope / correction
Per-client Mars::RNG at StrategyClient+0x134; all six AI-band NextInt sites plus cl_Chance/cl_RandRange reach it; zero AI draws on the strategic generator AI1 §5, board 240 (first half) survives L1's hive run has the strategic trajectory identical to the historical game while the pick varies — the strongest confirmation available
"g_GlobalRNG is never seeded; every draw returns 0; every AI client is seeded with 0" AI1 §5.1, board 240 (second half), lane-ai1.json::g_GlobalRNG overturned by measurement The author flagged it as arithmetic and wrote the falsifier that fired ("run-to-run different values mean it is time- or address-seeded"). Rule 2 working as designed. Lane H's counter-indication (rejection loop ran on that generator, impossible for an all-zero state) is now confirmed and should be credited
AI1's five consumers of g_GlobalRNG: SNMRunAI, RunCombatRound+0x60f, 0x007c2fa0+0xc84, 0x0079ea90+0x73 (Chance), 0x005b9f00+0xc0 lane-ai1.json survives as a list; its meaning flips Each is now a per-process input to whatever it feeds. ref-turn2 bounds them to "no save-visible effect on that turn" (seven processes). The combat consumer has never been cross-process tested on a battle turn — §6
"P2 HELD: the AI is fully reproducible from a save alone" AI1 §8.1, board 255 qualified with scope True of ref-turn2 → turn3, where every AI already holds a target and no decision depended on a draw. False as a general statement, as L5/L1/L4 showed. Already superseded by row 331; the row should say so
turn1-to-turn2-nondeterminism.md — three runs, four leaves, instrument exonerated L5 survives; cause now known Its "what was not established: the cause" is answered: the per-client seed. Its hooks=off row is a sample, not a control (§2)
Determinism oracle on ref-turn2 → turn3 determinism-oracle.md, board 50/253 survives unchanged The closing "Mechanism (inference…)" paragraph must be replaced with the seed finding
Coordinator's mechanism: "only one of three varies ⇒ pointer-ordered tie under ASLR, not a time seed which would move all three" backlog §2 last para, board 331 (5), determinism-oracle.md last bullet overturned All three streams move. A seed is visible only where a decision depends on a draw; 32 and 496 never reach the candidate walk (L4 §3.2), so their picks are stream-insensitive by code path, not by luck
Rung C = "deterministic game/ai whose choice is provably inside the original's tie set; k enumerable" backlog §2, board 333 restated (§5) The conclusion that the outcome space of the one varying decision is small and nameable survives; the mechanism is a draw, so it is the support of a random pick, not a tie. And the exact form — byte-match given the seed — is now available and is stronger
L4 §3.2: three producers; only 512 reaches SelectResearchTarget; candidate vector length 1 = a category {2,12}; fallback not taken; k = 6 for the XNC family from data; 4 of 6 observed inside L4 survives (measured parts) Its interpretation — "taking whichever member it reaches first … the per-process quantity is order" — is overturned: the per-process quantity is the client stream inside TryResearchCandidate 0x006c8580. Inference until the draw is seen; the L4 correction in §8 says how
L4's engine recommendation (canonical by tech id; claim membership in a nameable set) L4 §3.2 survives as C-set Needs the BIO_GnMod arm accounted for (2 of 6 observed picks are outside the XNC set), i.e. the category itself is probably drawn
Lane Z: "residual outside the two drivers is exactly zero" tail-rng-ledger.md §0/§2 qualified with scope (L1 did it in place) True of every turn with hives already present; on the creation turn BeginProcessTurn draws one NextInt per new hive, with the rejection loop live. The standalone's replay interval starts at BeginProcessTurn (§6)
Rule 19's hooks=off control method-rules.md qualified Valid only on a workload where the un-instrumented game agrees with itself across two fresh processes. Rule 25 (§7)
"The decision layer is not deterministic, and does not need to be" backlog §2 qualified It is deterministic given the seed. "Does not need to be" holds for lockstep (decisions travel as commands); for us the seed is an input, not noise
Lockstep MP is consistent with a per-machine AI seed board 331 (2) survives Only the machine hosting an AI client draws its seed; peers see commands. Prediction for L2 when convenient: the combat seed (the RunCombatRound consumer) must be host-distributed in the SNM stream, or MP battles would desync

3.1 The coordinator's follow-up, ruled on

Is the tie-set claim rescued? In form, yes; in content, not yet — and not on the grounds proposed. Three rulings:

  1. Do not re-ground it on "an identical-cost family". The six tier-1 XNC techs cost 2000 RP each, but the observed picks are their tier-2 successors at 13,000–30,000 RP — L4's own load-bearing detail is that the costs differ and the resolver still varies. Equal cost is not what makes the outcome set a set. What makes it a set is that the resolver's output is a function of the client stream over a finite candidate collection: S(D) = the support of a random pick, and k is the size of that support. Ground the claim on the draw and its bound, which the probe reads; never on cost.

  2. "We can name all k" is not claimable today, and BIO_GnMod is why. Two of six observed picks (L5 run B, and the original session that wrote turn2-state.sav) are outside the XNC six. L4 captured the candidate stream once, in a run that went to producer C. The honest reading of the six observations is: S(D) ⊇ XNC-six ∪ {BIO_GnMod}, with the producer of BIO_GnMod unidentified. The likeliest shape, stated as inference: in the BIO_GnMod runs producer A or B returned non-null for player 512 — gated by a draw on the same client stream — and the walk was never entered; then S(D) = A's outputs ∪ B's outputs ∪ C's arm, and naming k means reading three producers' gates, not one resolver. Finite, cheap, and not yet done. Carry k = 6 as L4 says: to prove.

  3. The verification statement, exactly. For a turn T from save X, let D be a decision whose output depends on the client stream (on turn1-state, exactly one: player 512's research target). Then:

    C-set. (i) Derivation: S(D) is listed before any sweep, from the code of every producer that can set D and from shipped data plus the save (the available-tech set is a function of X, so S is per-save, not a constant). (ii) Canonical pick: game/ai emits a fixed member of S(D) — lowest tech id, the quantity the command carries. (iii) Containment: every observed original run's D is in S(D). (iv) Falsifiable content: a sweep of pinned seeds — the seed is now a knob — produces no D outside S(D), and reaches every member of S(D) whose derived probability is not negligible.

    (iii) alone is a tautology if S is just the observed set; (iv) is what makes it a claim. Today (i) is done for one arm, (iii) holds 6/6 only by admitting BIO_GnMod on observation, and (iv) has not been run. C-set is therefore rescued as a statement and open as a result. C-exact (§5) does not depend on any of this and is the primary form.

On "a clean control" (R2 = hooks=off bytes). Not a control — rule 25. Two runs landing on the same one-of-k pick is a ~1/k event per pair, and with pairs compared across L5, L1 and L4 one coincidence is expected (L1's instrumented run also coincided with L5's hooks=off). It is consistent with neutrality; the exoneration rests on L5's identical-hooks pair and on ref-turn2 reproducing under every hook set, as §2 says.

Is the next probe inside the resolver rather than at the seed source? Yes on the source — it is not the probe — but the resolver probe is only an experiment if the seed is pinned. Without the pin, two runs of L4's instrument with two unknown seeds cannot separate "draws from the stream" from "reads something else per-process". With it, the same run answers both "what is per-process about it" (same seed ⇒ same pick settles that it is the stream) and "what does it iterate" (the left delta and the draw's bound name the arm's k). So the one probe is the pin through the resolver, one run, both instruments — §4.

4. The one probe — pinned seeds through the resolver

Pin the three AI client seeds to constants; run L4's research instrument with a left region over client+0x134 at TryResearchCandidate 0x006c8580; load turn1-state.sav in two fresh processes; End Turn; compare autosaves and the aicand/pick/word-cost lines. Then sweep seeds.

Why this and not the alternatives the coordinator listed:

  • Where the seed comes from only matters for reproducing files whose seeds were never logged — which is impossible whatever the source turns out to be. For every future capture the RunAI hook already logs the seed.
  • Whether the outcome space is enumerable is answered "yes, per decision, from the producers' gates and the draw's bound" the moment the mechanism is a draw — and it becomes irrelevant to exactness once the seed is an input. It stays relevant only for C-set, which is the fallback, and there it needs (i)–(iv) of §3.1, which the sweep supplies.
  • Whether the varying empire's candidate set is tied is already answered by L4: the vector has one element. There is nothing to tie.

The pin probe separates the two hypotheses that are actually live:

  • H1: the per-client seed is the only per-process input to this turn. Pinned seeds ⇒ byte-identical (Autosave).sav across processes, (Autosave EndTurn).sav still a3f9dc4b…, strategic word positions still 192 (hive draws unaffected).
  • H2: something else per-process feeds the pick or the turn — the fourth seeded object (record #2), a clock read inside the AI, an address-keyed container. Pinned seeds ⇒ the autosaves still differ. Where they differ localises it: the same four leaves ⇒ H2 is inside the research pick; anything else ⇒ a per-process input outside the AI, which nobody has predicted.

Implementation, for whoever runs it (minutes, not a lane): lane L1's l1seed shim already detours RNG_Seed and sees the seed argument. Add seed.pin=<hex>,<hex>,<hex>: when the detour's return address is 0x0078304b (the instruction after call RNG_Seed at 0x00783046 in the StrategyClient ctor), replace the seed with the next pinned value, in call order (the order is deterministic — eight records, same order, both runs). Log the replaced and original values. The existing post-state fields verify the pin: the three after states must be identical across processes, which is the built-in control. Use process 1's own values (0x75F692C0, 0xF2EDAC21, 0x165A2ADB) so a match against any file L1 already holds is possible in principle.

Then: (i) ref-turn2 once with the pin active — expect 978041acd168b56e…; that turn's decisions are stream-insensitive and the pin must not change that (rule 19 for the pin itself). (ii) turn1-state twice, fresh processes. (iii) If (ii) differs in the four known leaves only, extend the pin to record #2 (return address of whichever site seeds it — the trace has its this) and rerun. That is the whole bisection.

(iv) The sweep, same build: quit to menu and reload advances the pin table (RunAI runs again on every load, so each reload is a fresh sample without a relaunch — ~30 s per sample on L4's path). Ten to twenty pinned seeds, each logging: which of the three producers returned non-null for player 512, the aicand line, the word cost on client+0x134 inside 0x006c8580, and the pick. That is §3.1's (iv) — the first BIO_GnMod sample names its producer, and the word cost and bound name k for the arm that ran.

Predictions to commit before the build: H1 holds; same pinned seeds ⇒ same producer, same aicand, same word cost, same pick, byte-identical autosave; different seeds ⇒ same {2,12}, same cost, different pick; TryResearchCandidate costs ≥ 1 word on client+0x134; the BIO_GnMod samples come from producer A or B, not from the walk; word positions 192 / 211 unchanged throughout.

4.1 What the probe buys beyond the answer

  • If H1 holds, turn1-state → turn2 with pinned seeds is a byte oracle again — and it is the only corpus pair that exercises BeginProcessTurn's hive draw. The standalone gets its creation-turn reference back.
  • The same run, with L4's research instrument and a left region over client+0x134 around TryResearchCandidate, turns L4's inference into a measurement in one shot (§8).
  • CB's C3/C4 runs on turn1-state become reproducible captures: seeds + stream + autosave, all from one process, is the record RB needs.

4.2 Secondary probes, in order, none of them a week

  1. The seed's source: one hardware write watchpoint on 0xaf6e58+4 (mt[0]) armed at Application::Initialize entry, dropped at first SNMRunAI. The hit's return address is the seeder. Static cross-check for free: scan the image for the MT init multiplier 0x6C078965 at instruction boundaries — an inlined init_genrand that the six-site RNG_Seed scan could not see (rule 16, exactly).
  2. The other four consumers of g_GlobalRNG: entry probes (rule 20), cheapest on a battle save — §6.
  3. Record #2's owner: the Seed detour already has its this; one NextInt/NextFloat detour recording this (lane H's own suggestion for the client generators) names its consumers.

5. Rung C, restated

Replace backlog §2's Rung C bullet and the mechanism paragraph with:

  • Rung C — game/ai as a deterministic function of (save, per-client seed). The original's AI is not non-deterministic; it is MT19937 from one 32-bit word per AI client, drawn once at construction (on load, or at CreateGame) from a per-process global generator, and the generator is one we already own bit for bit. Two verification strengths:
    • C-exact. Given the seeds captured from an original run (three words, logged by the RunAI/Seed hook; sots_turn --ai-seed <netid>=<hex>,…), our AI's command block equals that run's block element for element and the autosave byte-matches. This holds on every captured run, because nothing else per-process reaches the turn — that "nothing else" is what the pin probe establishes, and it is the user's framing made literal: make the AI deterministic and the old approach still pays. Precondition: game/ai consumes the client stream in the original's order and count, including draws whose result never reaches the save (the surrender Chance at 0 %, the one-shot schedule at agent+0x36c, the NextInt rejection loops). A per-client draw ledger, lane Z's instrument with this recorded, is the prerequisite; the site table already shows 8 words/turn on these boards.
    • C-set. Where seeds were never captured — the historical turn2-state.sav, which no process including the original can reproduce — our canonical pick is a member of the outcome set of the one seed-sensitive decision: the image of the client stream under the resolver, named from the candidate set and the draw's bound. Lane L4 has the XNC arm (k = 6, four observed, all inside) and one arm unaccounted for (BIO_GnMod, 2 of 6 observations). The claim is "a member of a set we can name", as before; the mechanism is a draw, so the set is the support of a random pick, not a set of equal-score candidates.

Mechanism, measured (lane L1): the per-client generator at StrategyClient+0x134 is seeded from NextUInt on the global generator at 0x00af6e58, which is live and per-process; the seed is not in any save. Only one of three empires' picks shows the effect because only one reaches the candidate walk (lane L4); the other two are stream-insensitive by code path. The earlier "pointer-ordered tie under ASLR" inference was wrong and is withdrawn.

Two things Rung B keeps regardless: --turn-commands replay is still the no-AI path and still the right thing for RB to build; and the canonical pair stays turn2-state → turn3-state until the pin probe promotes the creation turn.

6. The other two items in the report

BeginProcessTurn and the standalone. Yes, the replay interval starts at BeginProcessTurn, not ProcessTurn. What else assumed the drivers bound the turn: sots_turn's "walk all three drivers' phase order" (backlog §1 app row) has no slot for it — add a fourth interval with RegisterHives (the five constants L1 read live, the two predicates, NextInt(10) with its rejection loop) and the TickHives slip. Lane Z's calibration numbers for the standalone generator (16 vs 20 / 18) were taken on turns where the interval costs 0 and are unaffected. The pre-turn autosave precedes BeginProcessTurn, so any standalone that starts from a pre-turn save must create the hives itself; only the creation turn shows it, and that turn is the pair the pin probe restores.

g_GlobalRNG's combat consumer. RunCombatRound+0x60f draws on the same per-process generator — most plausibly the seed for Mars::CombatSim+0x108. Lane Z's one instrumented battle (turn 55, auto-resolved) cost the strategic generator 0 words, in one process. No battle turn has ever been compared across two processes; lane H's z2-endturn seven-process agreement was on a turn with no encounter. So combat turns are not yet known to be cross-process reproducible, and this is the single most likely place the seed effect reappears. It does not touch Rung A (no battle on the canonical pair). Backlog §5's "a battle with res->+4 clear" workload should add: run it twice in two processes, and if it differs, pin the global (RNG_Seed(0xaf6e58, K) from the shim after Initialize) and rerun.

hooks=off and rule 19 — §7.

7. Draft rule 25 for method-rules.md

25. A control that disagrees with itself is not a control

Lane L5 ran hooks=off once on turn1-state → turn2, got a third value, and read it as "the instrument is not the cause". Lane L1 ran hooks=off again and got a fourth; lane L4 a fifth. All three were right about the instrument — by luck. None of those runs was a control, because the un-instrumented game does not reproduce itself on that workload: the cause was a per-process seed on the AI clients, present with no hooks and invisible to all of them.

Two consequences. (a) Rule 19 says hooks=off in two fresh processes. This is why: cite the agreement, not the run. If the two disagree, report "the workload is non-deterministic un-instrumented" as its own result, and take the control on a workload that does agree with itself (ref-turn2, z2-endturn) or pin the per-process input first. A control workload has to be chosen, and its self-consistency stated next to the number.

(b) "One of N moved" is not evidence about the mechanism. A seed moves every stream; it is visible only where a decision depends on a draw. The coordinator inferred "tie, not seed" from two stable empires and one moving one; the two stable empires were on a code path that never reaches the draw. Count the decisions that depend on draws, not the objects that changed — and when the inference has been written into the roadmap, write the probe that would falsify it on the same line.

Ranked by damage: the wrong inference reached backlog §2, determinism-oracle.md, two board rows and two lane briefs for about a day, and was caught by a written prediction before any engine work followed it. Below rule 24, above nothing yet — append as 25.

8. Course — lane by lane

L4 (VM145, live). Redirect mid-flight. Send:

L1's seed probe came back: every AI client's generator is seeded with a fresh per-process word (three different seeds per process, none shared across processes), so the varying pick is a seed effect, and the tie/arrival-order framing in your brief and in §3.2's interpretation is withdrawn — your own P10 falsification already pointed there. Your measured facts stand: three producers, only 512 reaches the walk, one candidate {2,12} that is a category, fallback not taken, k = 6 for the XNC arm. What changes is what the second capture is for. Do not chase order. Add a region over the client generator at client+0x134 (left / next_index — lane Z's RngLedger shape) at entry and exit of TryResearchCandidate 0x006c8580, and if the aicand dump can carry it, the bound of any NextInt it makes. Then run the creation turn twice with the seeds pinned — L1's l1seed Seed detour, return address 0x0078304b, three constants — first both processes on the same seeds (prediction: same aicand, same word cost, same pick, byte-identical autosave), then once on different seeds (prediction: same {2,12}, same cost, different pick). Then sweep: quit-to-menu and reload advances the pin table, so ten to twenty seeds cost ten to twenty loads, no relaunch; log which producer returned non-null for 512 on each — the BIO_GnMod arm is almost certainly producer A or B gated by a draw, and its first appearance under a known seed names it. That turns "the resolution varies" into "the resolution draws n words with bound b, and k = b+1 for this arm; the other arm is producer X behind gate G". Carry k = 6 as to prove until the sweep has produced BIO_GnMod under a known seed. Your C-set recommendation survives as the fallback for references whose seeds were never logged; the exact form — game/ai given the seed — is now the primary Rung C, and the per-client draw ledger you would be starting here is its prerequisite.

CB (VM146). Not a redirect; one addition to the capture format and one to the run list. The capture must carry the three AI seeds (RunAI's rng_seed per net id) next to the block set — they are three words and they are the difference between a log file and a reproducible pair. Add C5 = C3 with pinned seeds, twice; if the pin probe holds, C5's block + autosave + seeds is the first fully reproducible creation-turn record, and RB byte-matching it is the real test of "deterministic given the stream". P5 as written (C3/C4 differ in exactly one word) stands and is still worth running unpinned once.

RB (engine). No redirect. Add a --ai-seed <netid>=<hex>,… input alongside --turn-commands, unused until game/ai exists, so the capture format and the CLI agree from the start; nothing else changes.

L2 (VM141, multiplayer combat). Untouched. One prediction to note for later, not now: the combat-sim seed must travel host → peer in the SNM stream, because RunCombatRound draws it from a per-machine generator. If L2's capture already has the message, it names the consumer for free.

L3 (VM144, trade/spy tail draws). Untouched — its subject is the strategic generator, which this finding leaves exactly where lane Z put it.

VM140 is free. The pin probe goes there, on the l1seed build, before L4's second capture depends on it. Whoever runs it owns rule 25's first application: two fresh processes, agreement cited.

Docs to correct in place (rule 11): determinism-oracle.md last bullet; ai-turn-logic.md §5.1 (annotate, keep) and P1 (mark falsified, credit the falsifier row); ai-order-capture.md §3.1 last two paragraphs and §3.2's "reaches first" interpretation; lane-ai1.json::g_GlobalRNG prototype (drop "never seeded / every draw returns 0", keep the consumer list, set status mapped); turn1-to-turn2-nondeterminism.md "what was not established" (answered).


Proposed campaign/board.md rows

New row:

| THE AI SEED IS PER-PROCESS — tie hypothesis withdrawn, Rung C restated as (save, seed) | meta | verified | high | 100% | 2026-09-08 | Resolver, on lane L1's falsified P7 (`findings/resolutions/2026-09-08-ai-seed-per-process.md`). Each AI StrategyClient's generator is seeded at construction from NextUInt on the global generator at 0x00af6e58, which is LIVE and per-process (three unrelated words per process, none shared across processes; the seed=0 record reproduces byte-identically as the built-in control). OVERTURNED: AI1's "every client seeded with 0" (row 240 second half — its own falsifier row fired; lane H's rejection-loop counter-indication was right) and the coordinator's "one-of-three varies => pointer-ordered tie under ASLR" (rows 331(5)/333, backlog §2) — all three streams move; only 512 reaches the candidate walk (L4). SURVIVES: per-client RNG, zero AI draws on the strategic generator, the ref-turn2 oracle, L5's instrument exoneration (on the identical-hooks pair, NOT on hooks=off), L4's measured facts and k=6 XNC arm. **RUNG C RESTATED: game/ai as a deterministic function of (save, per-client seed)** — C-exact (seeds captured => block and autosave byte-match on EVERY run; the user's "make it deterministic and the old approach pays", literally) and C-set (seeds lost => canonical pick is a member of the nameable outcome set of a random draw, not a tie). ONE PROBE, VM140, minutes: pin the three client seeds in the l1seed Seed detour (ret addr 0x0078304b), turn1-state twice in fresh processes; predict byte-identical, positions 192/211 unchanged; if the four leaves still differ, extend the pin to Seed record #2. Holding => turn1->turn2 with seeds is an oracle again and the ONLY corpus pair exercising BeginProcessTurn's hive draw. OPEN: the global generator has four other consumers incl. RunCombatRound+0x60f — no battle turn has ever been compared across two processes. hooks=off on turn1-state gave THREE different files across L5/L1/L4 and was never a control: rule 25 drafted (a control must agree with itself in two fresh processes; "one of N moved" says nothing about mechanism). L4 corrected mid-flight (draw ledger on client+0x134 at 0x006c8580, pinned-seed runs); CB adds seeds to the capture and a pinned C5; RB adds --ai-seed |

Edits to existing rows (the integrator's SUPERSEDED convention):

  • Row 240 — prefix the notes with: PARTLY OVERTURNED by lane L1 (resolver 2026-09-08): the per-client RNG and "zero AI draws on the strategic generator" STAND; "never seeded / seeded with 0" is FALSE — the global at 0x00af6e58 is live and per-process. Status back to mapped for the seed half.
  • Row 255 — prefix: SCOPED: true of ref-turn2 -> turn3 only (no draw-dependent decision that turn); superseded by rows 331 and the resolver row.
  • Row 331 — append: (5) WITHDRAWN 2026-09-08 — it is a seed, not a tie; see the resolver row.
  • Row 333 — prefix: RESTATED by the resolver 2026-09-08: the mechanism is a per-process seed, not a tie; the tie-set claim survives only as C-set (fallback for uncaptured seeds); primary Rung C is C-exact given the seed.
  • Row 335 — append: CB: capture format carries the three AI seeds; add C5 = C3 with pinned seeds, twice. RB: add --ai-seed.
  • Row 62 — VM140 holder: the pin probe, before L4's second capture.