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.
13 KiB
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-bpafterssh spicy 'rm -rf …'of the whole tree (rule 24), from a lane-privategit worktreeat~/sots-engine-bp(branchwip/bp) with no localbuild*shipped (rule 21). Deployed toC:\SOTS\shimdist-bp. - Input:
verify/results/saves/ad-turn27-two-raiders.sav, sha2561c8baa27680809d585ab1728391e82729642d32aaedffd5bf95b616f0a11a436(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.savwith the AI client seeds pinned by the constructor-argument overwrite, one End Turn each, write byte-identical(Autosave).savfiles, andstate_checksum.py --floats bits --mask noneprints 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=logrun 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-turn27is 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
aiordersare 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::Initializedetour (the shim's own entry point); - the seven draw-site detours — installed unconditionally whenever
hooks != off; - one
aiordersdetour onStrategySim::ApplyTurnCommandBatch(aiprobes=off, so none of the entry probes; lane H's non-neutral probe index 8 is not installed); - one
aiseeddetour onStrategyApp::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 threeGame::SVSOSwarmQueen::*hooks,Game::SVSOSlaversRefuel::UpdateDifficultyTierandMars::RNG::Seed. Underhooks=tracethose 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.bppinnames 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.cbpinreadsaiseed.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:
- 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
StrategyClients. 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. - 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.
- Two runs agreeing on a
k > 1workload is a1/kcoincidence; three is1/k². That is exactly the argument CB used to justify its own third run, and it applies here with more force because nobody knowskon 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.)