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.
This commit is contained in:
parent
be1426630c
commit
e923d2928c
1 changed files with 202 additions and 0 deletions
202
findings/control-flow/pinned-seed-rich-turn.md
Normal file
202
findings/control-flow/pinned-seed-rich-turn.md
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
# 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.)*
|
||||
Loading…
Add table
Reference in a new issue