Lane RB consumed the capture and found the record was wrong: the element is dumped in
memory order and its memory field order is not its wire order. Memory member 1 is wire
member SRsc; six members are unread. RB's first replay regressed two leaves on exactly
that.
The values now ship as rates_memory_order_{u32,f32} with wire: null, and the .tcb emits
the seven '?' tokens RB's own converter emits, so a replayer refuses rather than guesses.
A missing field is honest; a mislabelled one propagates -- and a capture is meant to
outlive whatever adapter happened to read it.
Also records the outcomes against every prediction, including the one the consumer
falsified.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
17 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.
Outcomes recorded against the predictions above
P1 held. C0, hooks=off, reproduced the oracle exactly
bb4fd9ac89f41e3b… / 66,732 B and 978041acd168b56e… / 67,219 B. VM146 is a faithful reference
guest and the control is real.
P2 held. C1's autosaves are byte-identical to C0's, so the capture binds
One detour (the block dump) plus one (the seed reader), both read-only, and both oracle hashes came back. The stream and the autosave are from the same run.
P3 held, with P3b half-falsified
272 for the route (P3a, exactly as predicted from the output save's waypoint). 1728 for list
10's counted vector — a master-counter id, not a client-allocated one (P3b's falsifier). The
Population body is 24 bytes and turn-dependent (P3c).
P7's false-positive warning was right, and it cost nothing
At words=32 the generic vector follower fires on neighbouring heap: 18 aivec lines on the
canonical block, of which three sit at a word index a record names. The decoder reads only those
three, so the noise is visible and inert — which is the outcome the warning was written for.
A defect found by the consumer, not by me — list 5
Lane RB consumed this capture and reported that my list-5 record was wrong: the element is
dumped in memory order and its memory field order is not its wire order. Memory member 1 is
wire member SRsc; six members are unread. RB predicted zero regressions on its first replay and
got two, the AI's one non-zero slider landing in SRt where the oracle holds SRsc = 1.0.
The record is now dropped, per RB's recommendation and against my own first instinct to
relabel it positionally: the values ship as rates_memory_order_{u32,f32} with wire: null, and
the .tcb emits the seven ? tokens lane RB's own converter emits. A replayer that needs them
must refuse rather than guess. A missing field is honest; a mislabelled one propagates — and it
would have propagated inside the artefact that is supposed to outlive the adapter that read it.
What the capture bought, reported by the consumer
sots_turn --turn-commands puts /Sim/ModCount on 24 exactly, zero residual: 2 drivers + 4
research-rate gates + list 3 + list 5 + list 10 + 2 × list 14 + list 8, with the list-23 population
element free. That leaf has been "not derivable from a save" since lane A2. The 1–16 / 17–27
cost boundary in src/game/ai/orders.h held on the first workload that ever populated the free
half.