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

13 KiB
Raw Blame History

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 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.
  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.)