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

403 lines
33 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.