sots-re/findings/control-flow/pinned-seed-rich-turn.md
alex e923d2928c BP: probe 2 prediction, committed before the run
Probe 2 of the 2026-09-09 resolution: pin the AI client seeds on
ad-turn27-two-raiders.sav -- the richest AI turn the campaign owns, and the one
whose hooks=off control varies in 94 leaves -- and run two fresh processes.

Committed prediction: the two autosaves are byte-identical and state_checksum
prints IDENTICAL over ~67k leaves. If they differ, reading 3 is alive on rich
states, C-exact is false on the turns that matter, and that is a resolution
trigger rather than a lane result.

Also records, before any run: the exposure facts for all eight players read off
the input save, and the observation that four AI players enter this turn with
ResTNm == '' while contributing none of AD's 94 leaves -- all four have
NumOwn == 0, so 'ResTNm == emptystring names a pick turn' is necessary and not
sufficient.
2026-09-09 01:10:55 -04:00

202 lines
13 KiB
Markdown
Raw 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.

# The pinned-seed probe on a rich AI turn — is the client seed the *only* per-process input?
- **Owner / date:** lane BP · 2026-09-09 · guest **VM146** (`sots-re-win10-146`, `re@192.168.10.146`)
- **Held:** VM146, from the time this prediction was committed. VM140 (reference), VM141 (lane AZ)
and VM144 (lane AP) untouched.
- **Trigger:** `findings/resolutions/2026-09-09-seed-exposure-is-a-predicate.md` §6, **probe 2** —
the first of the three probes, because it can invalidate C-exact.
- **Build:** `bp-989c692-20260909T0505Z`, built in `/srv/re-lab/build/sots-engine-bp` after
`ssh spicy 'rm -rf …'` of the whole tree (rule 24), from a lane-private `git worktree` at
`~/sots-engine-bp` (branch `wip/bp`) with no local `build*` shipped (rule 21). Deployed to
`C:\SOTS\shimdist-bp`.
- **Input:** `verify/results/saves/ad-turn27-two-raiders.sav`, sha256
`1c8baa27680809d585ab1728391e82729642d32aaedffd5bf95b616f0a11a436` (lane AD's published value;
re-verified on this guest before the first run — §3.1).
---
## 0. The question, in one paragraph
The campaign holds that the original's AI is a deterministic function of **(save, per-client
seed)** — "C-exact" — and that its apparent non-determinism is one 32-bit word drawn per AI client
at construction from a per-process global (resolution 2026-09-08, board row 326). Row 360 proved
that *pinning* those seeds collapses three processes to one autosave. **It proved it on
`turn1-state`, an early-game turn, and nowhere else.** `ad-turn27-two-raiders.sav` is the richest
AI turn this campaign owns — thirteen ships complete into five new fleets, a design is created, a
system is colonised — and two `hooks=off` processes on it differ in **94 leaves**
(`raid-gate-multiplicity.md` §5). The resolver established that that variation is **not** the
design content (the two `Bravestar Mk 3` bodies are byte-identical apart from `DesID`) and **not**
a research pick (`ResTNm` held for Player[32]); it is the build / fleet-assignment / colony layer
and the id allocation downstream of it, **and its consumer is unidentified**. So reading 3 — *a
non-seed per-process input* — is not excluded on rich states. This probe decides it.
---
## 1. The prediction, committed before the run
> **P1 (the headline).** Two fresh processes loading `ad-turn27-two-raiders.sav` with the AI client
> seeds pinned by the constructor-argument overwrite, one End Turn each, write **byte-identical**
> `(Autosave).sav` files, and `state_checksum.py --floats bits --mask none` prints **IDENTICAL**
> over roughly 67,000 leaves.
>
> **P2.** The `(Autosave EndTurn).sav` (pre-turn snapshot, written before the AI runs) is
> byte-identical between the two processes as well. This is the weaker half and it is expected to
> hold whatever P1 does: lane AD saw the pre-turn autosave agree bit-for-bit across three processes
> and two configurations, so the load path is already known to be deterministic.
>
> **P3.** The pinned pair also reproduces run L — the unpinned `aiseed=log` run whose observed
> seeds the pin uses (§2.3). This is CB's C3/C5a/C5b shape applied to a rich turn: three processes,
> three sets of natural seeds, one autosave. Weaker than P1 (it can fail for a reason that leaves
> P1 intact — see the falsifiers) and worth much more if it holds, because it says the pinned run
> reproduces **a turn that actually happened** rather than a synthetic one.
>
> **P4 (the exposure predicate, tested in passing).** `ad-turn27` is exposed on all three of the
> resolution's counts for Player[32] — a design is created, thirteen ships complete, a system is
> colonised. If P1 holds, then **an exposed turn is reproducible once the seed is pinned**, which
> is the whole content of C-exact on the turns that matter.
>
> **P5.** The AI command blocks dumped by `aiorders` are identical between the two processes,
> element for element, on every list of every block. If P1 fails, P5 is the localiser: blocks
> identical + autosaves different puts the non-seed input **downstream of the AI's decision**;
> blocks different puts it **inside** it.
### 1.1 How this could be wrong, and the symptom of each way
| way | symptom |
|---|---|
| **Reading 3 is alive** — some per-process input other than the seed feeds this turn | P1 fails. The leaf diff localises it. **This is a resolution trigger, not a lane result:** the resolution says so in as many words, so this lane localises by sub-tree, names the leaves, and stops. |
| The pin is **half-applied** — a client with a net id not in the list passes through unpinned | The two processes differ, for a reason that has nothing to do with reading 3. Defended by construction: the wildcard `*=<hex>` is set *as well as* every observed id, so no client can pass through, and `shim.log` prints `aiseed call=… observed=… used=… pinned=1` for every single call. **Every call must read `pinned=1`.** A run with even one `pinned=0` is void. |
| The pin is **not applied at all** — `aiseed=pin` with no values | `shim.log` prints `aiseed: PIN MODE WITH NO PINS -- every seed passes through unchanged, so this run is NOT pinned and must not be reported as one`. Checked before either autosave is read. |
| The **wrong mechanism** was used | `airng.pin_seed` re-seeds the generator at bracket entry and is a *declared perturbation*; it would test a different thing. This lane does not set it. `airng=off` is explicit in both configs. |
| The **instrument** perturbs the turn (rule 19) | Not separable here, and stated rather than hidden: a pin *cannot* be applied by a process with no hooks, because `hooks=off` returns from `Shim_Init` before the aiseed module is installed. §2.2 lists exactly what is installed. Both processes carry the identical instrument, so the A/B comparison is internally valid; what it cannot do is speak about the un-instrumented game. |
| A stale binary (rule 24) | The build tree was `rm -rf`'d on CT111 before the rsync; `BUILD_ID` is checked in `shim.log` on every run and quoted in §3. |
| The **click path** lands somewhere else | Every dialog step is screenshotted and verified; no sleep-and-click. Row positions are re-derived from a screenshot of the Load dialog, not carried over from another lane. |
### 1.2 What this probe cannot decide, whatever it says
It is a statement about **one save, one procedure, one build**. Rule 26 corollary (c): a control
that agrees certifies a *procedure*, not a state. If P1 holds it says "on this turn, with these
seven clients pinned, nothing else per-process reached the save" — it does **not** say the AI is
deterministic, and it does not transfer to a turn that reaches a consumer this one does not. In
particular the 3–40-turn fuse at `0x0069dbb0` cannot fire inside one turn of a load and is
therefore **untested by this probe**, exactly as it was untested by AR's two-turn pair.
---
## 2. The procedure
### 2.1 Why the constructor-argument overwrite and not `airng.pin_seed`
`aiseed=pin` detours `Game::StrategyApp::RunAI` and replaces its **fourth stack argument** — the
word the `StrategyClient` constructor is about to hand to `RNG_Seed` — before the callee reads it.
The client's generator is therefore *constructed* from the pinned word and its whole stream, from
the first draw, is the stream that word implies. `airng.pin_seed` (lane PAR) re-seeds an already
constructed generator at bracket entry; it discards whatever the client drew before that point and
is a **declared perturbation**. The resolution names the first and forbids the second. Getting this
wrong would invalidate the probe, so it is stated here rather than left to the config.
### 2.2 The hook set, stated in full — and a defect in the mechanism this lane inherited
`aiseed` lives in the `ai_orders` module, and `install_ai_orders()` is only reached when
`hooks != off`. So **there is no such thing as a `hooks=off` pinned run.** The minimum instrument
that can carry a pin, and the one both processes carry, is:
* the `Mars::Application::Initialize` detour (the shim's own entry point);
* the **seven draw-site detours** — installed unconditionally whenever `hooks != off`;
* **one** `aiorders` detour on `StrategySim::ApplyTurnCommandBatch` (`aiprobes=off`, so none of the
entry probes; lane H's non-neutral probe index 8 is not installed);
* **one** `aiseed` detour on `StrategyApp::RunAI`.
Every template hook is off **by name**. That matters more than it looks:
`trace::Config::mode_for` falls through to `default_mode` for any hook **not** named, and
`default_mode` under `hooks=trace` is `trace` — so **a hook omitted from the list is silently left
installed.**
> **Inherited defect, reported not worked around.** `src/shim/shim.cfg.cbpin` — the file the
> resolution points at — names 20 template hooks and **omits six that existed when it was
> written**: `Game::StrategyServer::BeginProcessTurn`, the three `Game::SVSOSwarmQueen::*` hooks,
> `Game::SVSOSlaversRefuel::UpdateDifficultyTier` and `Mars::RNG::Seed`. Under `hooks=trace` those
> six default to trace mode, so lane CB's pinned runs C5a/C5b carried a larger instrument than
> their config comment implies. It does not damage CB's result — both processes carried the same
> six, and CB's conclusion rests on an A/B agreement — but the next lane to copy that file inherits
> them. `shim.cfg.bppin` names **all 28** registered hooks.
>
> **Second, smaller defect in the same file.** Its header comment wraps onto a line that is *not*
> commented: line 4 of `shim.cfg.cbpin` reads `aiseed.values=32=e70a4703,496=0c63ca36,512=372be4df\`.`
> — a live config line ending in a stray backtick and a full stop, four lines above the real one.
> Harmless if the parser is last-wins, and nobody has checked which it is. Reported so it can be
> fixed rather than rediscovered.
### 2.3 Three runs, not two
The resolution costs the probe at two loads. This lane takes three, and the extra one is run **L**:
| run | config | seeds | End Turns | purpose |
|---|---|---|---|---|
| **L** | `shim.cfg.bplog` | `aiseed=log` — observed, **not** changed | 1 | enumerate the net ids and their *natural* seeds; produce a natural reference autosave |
| **A** | `shim.cfg.bppin` | pinned to L's observed values, every id **plus** the wildcard | 1 | the probe |
| **B** | `shim.cfg.bppin` | identical | 1 | the probe, second fresh process |
Why the third run is worth its nine minutes:
1. **Nobody has ever listed this save's AI net ids.** The save has seven non-human players (§2.4)
but the number of players is not the number of `StrategyClient`s. The alternative the resolution
allows — the bare wildcard — pins every client to the *same* word, which is a valid intervention
but not a natural one.
2. **It makes the pin reproduce a turn that happened.** Lane CB's rule: pin to values an unpinned
run of the same save observed for itself. P3 is then a real prediction rather than a definition.
3. **Two runs agreeing on a `k > 1` workload is a `1/k` coincidence; three is `1/k²`.** That is
exactly the argument CB used to justify its own third run, and it applies here with more force
because nobody knows `k` on a build turn.
`shim.cfg.bplog` and `shim.cfg.bppin` are **byte-identical except the `aiseed=` line and the
values**, so run L is comparable with A and B.
### 2.4 The exposure facts, read off the input save before any run
Rule 26 corollary (c) says to write the exposure facts next to the hashes. Read from
`ad-turn27-two-raiders.sav` with `verify/save-reader/save_reader.py --dump --json`:
| PlyrIdx | PlayerID | name | `ResTNm` at turn start | `NumOwn` | `NumDes` |
|---:|---:|---|---|---:|---:|
| 0 | 16 | `re` (**human**) | `''` | 3 | 6 |
| 1 | **32** | **The Eternal Empire** (Tarka) | **`BIO_TerBac`** (held) | **15** | **46** |
| 2 | 496 | Spengler (RebelAI) | `DRV_PlsFiss` | 0 | 0 |
| 3 | 512 | Spengler (RebelAI) | `XNC_TrnsMorr2` | 0 | 0 |
| 4 | 528 | Alien Menace | `''` | 0 | 19 |
| 5 | 544 | Peacekeeper Enforcer | `''` | 0 | 1 |
| 6 | 560 | Von Neumann | `''` | 0 | 6 |
| 7 | 576 | Independent Colony | `''` | 0 | 1 |
Ships completing this turn: **13**, into five new fleets (`ShipIDs` +13 −1), with `NumDes` 46→47
(`Bravestar Mk 3`) and `NumOwn` 15→16 — measured by the resolver from the 27→28 transition, quoted
here as the prior it is, and re-derivable from this lane's own output saves.
**One thing this table says that the resolution's §3 does not.** The resolution reports "AD's
variation was not a research pick — Player[32] entered turn 28 with `ResTNm = 'BIO_TerBac'`", which
is correct. But **four other AI players enter this turn with `ResTNm == ''`** (528/544/560/576) and
**none of them contributed a leaf to AD's 94** — every one of the 94 is Player[32]'s or the global
id counters'. All four have `NumOwn == 0`. So the predicate as written in rule 26 corollary (c) —
"`ResTNm == ''` names a pick turn" — is **necessary and not sufficient**: a player with no colonies
has no research income and does not reach the pick, whatever its `ResTNm` says. That is a
measurement on the input save, independent of how the runs come out, and it is offered as a
refinement of the predicate rather than a correction of the resolution's Player[32] claim, which
stands.
---
## 3. The result
*(Filled after the runs. This section is empty in the commit that carries the prediction.)*
---
## 4. Verdict
*(Filled after the runs.)*
---
## Proposed board rows
*(Filled after the runs.)*