resolution: byte-match survives as C-exact/C-set; tie set is a draw support not equal cost; rule 26 on self-consistent controls
This commit is contained in:
parent
9d722815a5
commit
af67d31394
4 changed files with 453 additions and 11 deletions
|
|
@ -52,18 +52,31 @@ So the rungs are:
|
|||
replays the AI's (and human's) orders captured from a real turn, and the output must match that
|
||||
turn's autosave byte-for-byte. This is what "the engine is the game" means when the game's own
|
||||
decision layer is not reproducible from a save. Lane AI1's fallback becomes the primary path.
|
||||
- **Rung C — a deterministic `game/ai` whose choice is provably inside the original's tie set.**
|
||||
Our engine need not inherit the original's tiebreak. If the variation is a tie broken per-process
|
||||
(the leading hypothesis), the original's possible outcomes are a small enumerable set; we pick
|
||||
canonically and verify that **our choice is one of exactly k outcomes the original can produce,
|
||||
we can name all k, and every observed original run lands inside that set.** That is stronger
|
||||
than behavioural equivalence and keeps the byte-match live on every run where the tiebreaks
|
||||
agree. (User's framing, 2026-09-08: make the AI deterministic and the old approach still pays.)
|
||||
- **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 from a per-process global generator — and that generator is one we already own bit
|
||||
for bit. Two strengths:
|
||||
- **C-exact.** Given the seeds captured from an original run (`sots_turn --ai-seed <netid>=<hex>`),
|
||||
our AI's block equals that run's block element for element and the autosave byte-matches, on
|
||||
**every** captured run. This is the user's framing made literal: make the AI deterministic and
|
||||
the old approach still pays. **Precondition:** `game/ai` must consume the client stream in the
|
||||
original's order and count, *including draws whose result never reaches the save* — a per-client
|
||||
draw ledger 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 must be a member of the
|
||||
outcome set of the one seed-sensitive decision. Lane L4 has the XNC arm (k = 6, four observed,
|
||||
all inside) and **one arm unaccounted for (`BIO_GnMod`, 2 of 6)**, so "we can name all k" is
|
||||
**to prove**, not proved. The set is the support of a random draw, not a set of equal-score
|
||||
candidates.
|
||||
|
||||
Mechanism, stated as inference: only **one** of three AI players varies, which looks like a tie
|
||||
broken by something per-process — most likely iteration order over a pointer-keyed container under
|
||||
ASLR — rather than a time seed (which would move all three). One probe separates them: read the AI
|
||||
client's generator state after construction in two processes. Routed to lane L1.
|
||||
Mechanism, **measured** (lane L1, not inferred): 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 in no save. Only one of three empires *shows* the effect because only one reaches the
|
||||
candidate walk (lane L4) — the other two are stream-insensitive **by code path**. My earlier
|
||||
"pointer-ordered tie under ASLR" inference was wrong and is **withdrawn**.
|
||||
|
||||
Rung B is unaffected: `--turn-commands` replay stays the no-AI path, and the canonical pair stays
|
||||
`turn2-state → turn3-state` until the pin probe promotes the creation turn.
|
||||
|
||||
## 3. Engine work to Rung A (ranked by leaves closed per lane)
|
||||
|
||||
|
|
|
|||
|
|
@ -344,3 +344,8 @@ Status flow: `backlog → in-progress → mapped → verified` (or `blocked`).
|
|||
| LAB HAZARDS from L2 | meta | verified | high | 100% | 2026-09-08 | (1) **`pgrep -f "tap141i0"` matches QEMU's OWN command line** (`ifname=tap141i0`), so stopping a tcpdump SIGTERMed the VM. Use `pgrep -x tcpdump`. The guest came back clean (fsutil not dirty, same IP). (2) Driving the starmap needs **REAL MOUSE MOTION** - SetCursorPos alone does not move the game's cursor - plus wheel zoom; L2's driver is in verify/harness/l2-ui/. (3) From lane L4: **`move X Y` then `click X Y`** - a bare click is delivered at the PREVIOUS cursor position about half the time, which reads as "the click did nothing" then "the next click did the previous thing" |
|
||||
| FOURTH cross-lane commit sweep - now with plain git commit | meta | verified | high | 100% | 2026-09-08 | Lane L1's board commit swallowed all 22 of lane L2's staged files. Content intact and verified in HEAD; L2 correctly did NOT rewrite history with other lanes live. **This is the fourth instance of the hazard addresses.d/README documents, and it now happens with plain `git commit` too, not just addresses.json** - because a lane stages by path but another lane's commit picks up whatever is in the index. The index is shared per-repo; staging by path does not protect against someone else's commit. Needs a real fix, not another rule |
|
||||
| STRUCTURAL FIX for the commit-sweep hazard (rule 25) | meta | verified | high | 100% | 2026-09-08 | Four instances in one session, the last with plain `git commit` and NO `-A` anywhere - **every lane had staged by path exactly as instructed and it still happened**, because `git add <path>` puts a file in THE REPO'S ONE INDEX and the next commit takes all of it. Staging by path was never the protection. **THE FIX IS A FORM OF THE COMMAND**: `git commit -m "msg" -- path/one path/two` commits only those paths and leaves the rest of the index untouched - verified in a scratch repo (two files staged, pathspec commit took one, the other stayed staged). Lanes now commit with a pathspec, always. Deeper fix if it recurs: a per-lane CLONE of sots-re, mirroring the per-lane worktree of sots-engine - which is precisely why sots-engine has never had this problem |
|
||||
| RESOLUTION: the byte-match SURVIVES, in a stronger form | meta | verified | high | 100% | 2026-09-08 | First re-resolver run, `findings/resolutions/2026-09-08-ai-seed-per-process.md`. **The AI is not non-deterministic - it is MT19937 from ONE 32-bit word per client, drawn once at construction from a per-process global, and we own that generator bit for bit.** So game/ai is a deterministic function of (save, per-client seed). Rung C splits: **C-exact** - capture the three seeds and our block matches element for element and the autosave byte-matches ON EVERY RUN (the user's framing made literal), precondition being a per-client DRAW LEDGER so we consume the stream in the original's order and count INCLUDING draws whose result never reaches the save; **C-set** - the fallback for references whose seeds were never logged, like turn2-state.sav, which **no process including the original can reproduce**. Instrument exonerated: three unrelated 32-bit words per process are successive generator outputs, not clock reads, so AI1's call-site reading STANDS and only its "never seeded" reading falls |
|
||||
| the tie-set claim: rescued as a STATEMENT, open as a RESULT | verify | mapped | high | 80% | 2026-09-08 | And **not on "equal cost"** - the resolver's sharpest correction. The 2000-RP tier-1s are not the picks; the picks are tier-2s at 13k-30k. S(D) is **the support of a random draw**, per save, not a set of equal-score candidates. `BIO_GnMod` (2 of 6 observations) proves a SECOND ARM whose producer is unidentified - likeliest producer A or B gated by a draw, never entering the walk - so **"we can name all k" is TO PROVE**. The four-part statement is in the resolution 3.1: derive S before any sweep; canonical pick = lowest tech id; containment; **and a pinned-seed sweep produces nothing outside S** - part (iv) is what makes it non-tautological |
|
||||
| THE ONE PROBE: pinned seeds THROUGH the resolver | verify | backlog | — | 0% | 2026-09-08 | The seed SOURCE is not the probe. The resolver probe is only an experiment **if the seed is pinned** - otherwise two runs with two unknown seeds cannot separate "draws from the stream" from "reads something else per-process". Implementation named: L1's `l1seed` Seed detour (return address 0x0078304b, three constants) + L4's instrument + a `left` region over client+0x134 at TryResearchCandidate 0x006c8580; ref-turn2 once as the pin's OWN control; turn1-state twice; then sweep by quit-to-menu/reload (the pin table advances per load, **no relaunch needed**), logging which producer returned non-null for player 512. Predictions committed in the resolution 4. If the four leaves still differ, extend the pin to the unidentified fourth seeded object |
|
||||
| NEW OPEN RISK: g_GlobalRNG has four other consumers | verify | backlog | — | 0% | 2026-09-08 | Including **RunCombatRound+0x60f** - and **no battle turn has ever been compared across two processes**. Add "twice, two processes" to the battle workload in backlog 5. Lane L2 verified combat lockstep between two clients in ONE game; this is the different question of whether a battle is reproducible across separate runs |
|
||||
| CORRECTION: neither hooks=off run was a control | verify | verified | high | 100% | 2026-09-08 | Rule 26. Three lanes ran hooks=off on turn1-state and got **three different files**, so no single hooks=off run on that workload ever exonerated anything. And lane L4's "run R2 is byte-identical to the hooks=off control" is a **~1/k COINCIDENCE, not a control** - with an outcome set of size k, agreement by chance is expected. Rule 19 said take the measurement with the instrument removed; it did not say what to do when THE CONTROL ITSELF VARIES |
|
||||
|
|
|
|||
403
findings/resolutions/2026-09-08-ai-seed-per-process.md
Normal file
403
findings/resolutions/2026-09-08-ai-seed-per-process.md
Normal file
|
|
@ -0,0 +1,403 @@
|
|||
# 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.
|
||||
|
|
@ -331,3 +331,24 @@ diff, but it is not the protection — the pathspec on `commit` is.
|
|||
The deeper fix, if this recurs: give each lane its own clone of `sots-re`, the way each already gets
|
||||
its own `git worktree` of `sots-engine` (which is why that repo has never had this problem). Rule 21
|
||||
covers the worktree side; this is its counterpart for the shared evidence repo.
|
||||
|
||||
## 26. A control must agree with itself before it exonerates anything
|
||||
|
||||
Rule 19 says take the measurement with the instrument removed. It did not say what to do when **the
|
||||
control itself varies**, and this session that gap produced two wrong readings.
|
||||
|
||||
Three lanes ran `hooks=off` on `turn1-state` and got **three different files**. So no single
|
||||
`hooks=off` run on that workload was ever a control — it could not exonerate or indict anything.
|
||||
Separately, one lane observed its instrumented run coming out byte-identical to a `hooks=off` run
|
||||
and read that as the instrument being clean; with an outcome set of size k that agreement is a
|
||||
**~1/k coincidence**, not evidence.
|
||||
|
||||
So: **a control must be reproduced in two fresh processes and agree with itself before it is used as
|
||||
a control.** If it does not agree, the workload is non-deterministic and the honest move is to pin
|
||||
the source of variation (here: the per-client AI seed) and re-run, not to pick the run that suits.
|
||||
|
||||
Corollary, from the same episode: **"one of N moved" says nothing about mechanism.** Only one of
|
||||
three AI empires visibly varied, which looked like a tiebreak among near-equal candidates. In fact
|
||||
all three streams differed every run; two empires never reach the seed-sensitive code path at all.
|
||||
Before reasoning from *which* items varied, establish whether the others were even exposed to the
|
||||
mechanism.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue