403 lines
33 KiB
Markdown
403 lines
33 KiB
Markdown
# 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.
|