Lane L1 found each AI client's generator takes a fresh per-process word, so the AI is MT19937 from one word per client and game/ai is a function of (save, seed). A capture without the seeds records the answer without the input. - aiseed=log|pin on StrategyApp::RunAI: one detour that reads the fourth stack argument where the callee reads it, and in pin mode replaces it. Both modes log observed AND used, so a pinned run says so in its own capture. - aiorders.words= widens the element window past list 1's 48-byte horizon. - aiorders.deep= follows the heap: the route vector, the counted vector and the Population body were ABSENT from every capture so far, not empty. - tools/turncommands_capture.py (in sots-re) does the typing offline, so a corrected element record costs a re-parse rather than a VM run. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
15 KiB
CB — predictions for the command-stream capture, written before the module exists
Lane CB, 2026-09-08. Worktree wip/cb off main 4f25f1e (the brief said 7444c3b; the
integrator merged L1 in the meantime, so this branch is based on the newer main and says so).
Guest VM146 (192.168.10.146). Committed before ai_orders.cpp is touched and before
anything is built.
Lane RB is building sots_turn --turn-commands <blocks>. This lane supplies its input. A replay
with no real stream is untested and a stream with no autosave to check it against is a log file, so
every prediction below is either about the stream or about the binding between the stream and
the autosave of the run that produced it.
0. What is being changed, and why it is small
Lane L4 already dumps the block set at StrategySim::ApplyTurnCommandBatch — one
register-transparent entry stub, read-only, measured behaviour-neutral on the canonical pair. This
lane does not rebuild that. It adds three things and nothing else:
aiorders.words=<N>— the element window, today a fixed 12 words. List 1 carries a polymorphicShipDesignDefwhose id sits past 48 bytes, so the turn-1 workload cannot be captured completely at 12.aiorders.deep=on— follow the heap. Three payloads in the canonical block are behind pointers and are therefore absent from L4's capture: list 8's route vector, list 10's counted vector, and list 23'sPopulationbody. A replayer cannot reconstruct a fleet move without the route. The follower is deliberately generic — it scans the element window for anything shaped like{_Myfirst,_Mylast,_Myend}and for anything shaped like an MSVCstd::string, and dumps the bytes. It does not know which list it is looking at.- Nothing in the instrument types an element. L4's design point — the dump reads element bytes, not element types; decoding happens offline so a wrong record shows up as a wrong value instead of being baked into the instrument — is kept, and the typed decode moves to a host-side Python step that can be corrected without another VM run.
So aiorders=on aiprobes=off remains exactly one MinHook detour, and the added work inside it
is guarded reads of memory the game already owns.
1. The capture format (defined here because RB may not have landed first)
One JSON file per run under verify/results/turncommands/. Mechanical, and the raw words are the
ground truth:
binding— the input save and both output autosaves, each with size and sha256, plus whether they match the published determinism oracle. A capture whosebinding.outputsis absent or whose hashes do not match the run is worthless for a byte-match and must be treated as such.batches[].blocks[]— one entry per block slot,playerId, the six gates with their payloads, and all 27 lists.- Each element carries
raw_words(the window, verbatim),vectors(each followed heap vector, with its element words),strings(each decodedstd::string), anddecoded— a named record only where the campaign has one, with arecordfield naming it and awirearray giving the values in the order the writer emits them. decodedis regenerable fromraw_wordsbytools/turncommands_capture.py. RB should preferdecoded.wireand fall back toraw_words; if the two ever disagree,raw_wordswins.
The one thing the decoder must get right and that is easy to get wrong: list 3's in-memory
element is in the opposite order from its wire record. Lane L4 read that off the writer
(0x00822870 emits +0x14, +0x10, +0x0c, +0x08, descending); lists 5, 8, 10, 14 and 23 all write
ascending. That is per-list, not a rule, and the decoder encodes it as such.
2. Runs, and what each is for
| run | save | hooks |
aiorders |
watch |
purpose |
|---|---|---|---|---|---|
| C0 | ref-turn2.sav |
off |
— | off |
rule-19 control: the oracle bytes with no instrument at all |
| C1 | ref-turn2.sav |
trace |
on, words=32, deep |
off |
the deliverable — canonical pair capture |
| C2 | ref-turn2.sav |
trace |
on, words=32, deep |
on (modcount) |
the trap multiset, as RB's second check |
| C3 | turn1-state.sav |
trace |
on, words=32, deep |
off |
the non-deterministic pair, block + its own autosave |
| C4 | turn1-state.sav |
trace |
on, words=32, deep |
off |
the same turn twice |
3. Predictions
P1 — C0 reproduces the oracle. (control)
(Autosave EndTurn).sav = 66,732 B, bb4fd9ac89f41e3b; (Autosave).sav = 67,219 B,
978041acd168b56e. If this fails, the guest is not the reference guest and nothing else in
this lane means anything; lane L4 already reproduced these on VM145 and lane L5 held VM146, so a
failure here is a lab fact, not a finding.
P2 — C1's autosaves are byte-identical to C0's, so the capture binds.
The deliverable stands or falls on this. Lane L4 ran seventeen detours on this same workload and got the oracle bytes; this run has one, doing strictly more reads inside it. If C1's autosaves differ from C0's, the capture is a stream that changed the turn it recorded, it is unusable for a byte-match, and the report must lead with that (rule 19).
P3 — the block set on the canonical pair is exactly lane L4's, plus the three payloads.
Two batches: seq=1 at load (n=1, pid 16, all 27 lists empty) and seq=2 at End Turn (n=8).
Slots 0–3 are pids 16, 32, 496, 512; slots 4–7 are pid 0 with uninitialised gate payloads.
Only pid 32 is non-empty, with list sizes 3:1, 5:1, 8:1, 10:1, 14:2, 23:1. Every rate gate on the
four real players is set; no research-target gate is set on any player this turn.
New, and the reason for the run:
- P3a — list 8's route vector has count 1. Its single value is the destination system id.
L4 could not read it and noted that
turn3-state.sav's waypoint says 272. Predict 272. Falsifier: any other id, in which case the waypoint in the output save is not the route in the command and the replayer has to carry both. - P3b — list 10's counted vector has count 1. L4's reading is "at system 288, fleet 34, [one
object]". Predict the value is a ship id that already exists in
ref-turn2.sav— the pre-existing ship being folded into the newly allocated fleet 34. Falsifier: a small client-allocated id in the 18/34 space, which would mean the client allocates the ship too and the id-allocation hole L4 left open is wider than one counter. - P3c — list 23's
Populationvector is 24 bytes, i.e. six words, at a stride the dump does not name. Predict the six words are not all zero and that they differ between the turn-2 and turn-1 captures (L4 saw the element's trailing int go+1on turn 1 and−1on turn 2, so the body is turn-dependent and not a constant the replayer can ignore).
P4 — C2's trap multiset is exactly ten bumps, and they attribute.
The paying half of pid 32's block is 3:1 + 5:1 + 8:1 + 10:1 + 14:2 = 6 elements, list 23
being free; plus 4 research-rate gates, one per submitted block, set unconditionally. Total
10 — which is the number lane W2 measured on this turn from the other end, before anyone had
seen the block. Predicted per-EIP:
| trap EIP | count | what |
|---|---|---|
0x00821a87 |
4 | prologue gate +0x0c, research rate, one per submitted block |
0x0084946e |
1 | list 3, build |
0x0086c3e9 |
1 | list 5, system rates |
0x0088bf01 |
1 | list 10 |
inlined at ApplyTurnCommandBatch+0xabc |
2 | list 14, the AI's two (fleet, mode) elements |
| list 8's site | 1 | fleet move |
Falsifier: any total other than 10, or any bump attributed to a list this capture reports empty.
That would mean the counter is not the length of the command stream and the whole
ModCount-from-the-block claim in src/game/ai/orders.h is wrong.
Rule 19 for C2 specifically: the watchpoint module has never been run in the same process as the block dump. If C2's autosaves move off the oracle, C2 is reported as perturbing and C1 remains the deliverable — the trap set is a bonus check, not the capture.
P5 — C3 and C4 differ in exactly one word of the stream.
Both from turn1-state.sav. Predicted block: pid 32 with lists 1:1, 3:1, 5:1, 23:1, three
research-target gates set (pids 32, 496, 512) and the human's clear, four rate gates set.
The prediction that matters: C3's and C4's block sets are byte-identical except for player 512's
research-target payload at block+0x10, and each run's autosave differs from the other's only in
p512.ResTNm and the derived checksum. That is Rung B's claim in its sharpest form — if the two
streams differ only where the two autosaves differ, then the sim is a function of the stream and
the decision layer is the only thing that is not.
Falsifiers, and each is worth more than the prediction:
- The two blocks are identical and the two autosaves differ. Then the stream does not determine the save, Rung B is not reachable by replay alone, and the divergence is downstream of the command — which contradicts L4 §3.1, which saw it in the block.
- The two blocks differ somewhere else as well (an id, an ordinal, a route). Then the client-allocated id space is also per-process and a replay has to carry ids it cannot derive.
- Player 512's target lands on a tech outside the six-member
XNCset L4 named. Then k is larger than six and Rung C's tie-set claim has to start one level up.BIO_GnModhas already been observed once, so this is not unlikely; a secondBIOvalue would settle that the resolver leaves the family.
P6 — list 1's design element yields its id and its name at words=32.
"Honor Lance" is a short-string-optimised std::string inside the element, and design id 18
is named by list 3 in the same block. Predict the widened window shows the string and a word
equal to 18 inside the list-1 element. Falsifier: no 18 anywhere in 128 bytes, in which case
L4 §1 P3 stands as it is — the design command does not carry its own id and the id is inferred.
P7 — what this lane will not be able to say.
Stated in advance so a green capture is not read as more than it is (rule 15):
- Two turns, one AI empire with anything to do, a board with no contact. Lists 2, 4, 6, 7, 9, 11–22, 24–27 will be empty in every block of every run, exactly as they have been in all eleven corpus saves and both of L4's runs. The capture exercises six of twenty-seven lists and one of six gates on the canonical pair, two of six on turn 1. Everything the replayer does with the other twenty-one lists is untested by this lane and must be labelled as such.
- The generic vector follower can produce a false positive — three consecutive words that
happen to look like a vector. The count bound and the readability probe make that unlikely, not
impossible. Any
aivecline on a list whose record has no vector is to be read as noise until a second run reproduces it. - Nothing here says what list 10 or list 23 mean. It says what bytes they carry.
Addendum — the seeds are part of the stream (written before C3/C4/C5, after C0/C1 were driven)
The coordinator relayed lane L1's seed probe mid-lane: every AI client's generator is seeded with
a fresh per-process 32-bit word, three per process, none shared between processes. So the AI is
not non-deterministic — it is MT19937 from one word per client, and MT19937 is a generator this
campaign owns bit for bit. game/ai is a deterministic function of (save, per-client seed).
That changes what a capture is. A block set without its seeds records the AI's answer without its input: replayable, but not re-derivable and not re-runnable. Three words turn a log file into a reproducible pair. The capture format therefore grows one field, and the instrument grows one detour:
"ai_seeds": [ {"call": 1, "netId": 32, "observed": "0x…", "used": "0x…", "pinned": false}, … ]
observed is what the game chose; used is what the client actually got; pinned says the run
forced it. Every call logs all three, so a pinned run can never be mistaken for a natural one —
which matters because pinning is an intervention, not an instrument.
aiseed=log reads the fourth stack argument of Game::StrategyApp::RunAI where the callee will
read it and changes nothing. aiseed=pin overwrites it before the callee sees it.
P8 — the new rule-26 control, stated as a prediction about method
Three lanes ran hooks=off on turn1-state and got three different files. So no single
un-instrumented run of that workload is a control, and an instrumented run agreeing with one of
them is a ~1/k coincidence rather than evidence — with k = 6 by lane L4's naming of the XNC
set, that is a 17% chance of a false clean bill of health. C0 is a valid control for
ref-turn2 → turn3 and for nothing else. Any rule-19 claim this lane makes about the turn-1
workload must come from a pinned pair, or it must not be made.
P9 — C5: pinned, the same seeds, twice.
C5 is C3 re-run with aiseed=pin set to the seeds C3 itself observed. Predicted: C5a and C5b
produce byte-identical autosaves, and both are byte-identical to C3's, because the seed is the
only per-process input and it has been removed. If that holds, it is the first fully reproducible
creation-turn record this campaign has: block set, seeds and autosave, all from runs that agree.
Falsifiers, each worth more than the prediction:
- C5a and C5b still differ. Then the seed is not the only per-process input, L1's mechanism is incomplete, and Rung B needs whatever else varies captured too. This is the single most valuable outcome available in this lane.
- C5 is reproducible but differs from C3. Then pinning perturbs beyond the seed value — most likely because the detour changes when the word is consumed, not only what it is.
RunAIis never entered. Then the seed does not reach the client by this path on this workload and the pin is a no-op that would look like a successful control. The instrument prints its own entry count precisely so this is visible; a silent empty seed list would be exactly the rule-1 failure of a confident zero.
P10 — the target gate's resolved name (lane RB's request)
RB's §163 asks the format to carry the observed tech name next to the target id, because the
target-gate handler cannot be modelled from the id alone. airesearch=on resolves it — phase 18
pushes the tech's std::string as a char*, which is why it is loggable at all. Predict the three
turn-1 target gates (ids 144, 90, 288) resolve to three names, of which player 512's is the one that
moves between runs. Adding those three detours is a rule-19 risk on a workload that has no valid
control, so it goes on the turn-1 runs only, never on the canonical pair.