Lane SV recovered the script-object subsystem statically and predicted that SVSOSwarmQueen::RegisterHives takes one strategic-generator word per new hive inside StrategyServer::BeginProcessTurn, which runs inside lane Z's autosave bracket and outside both turn drivers. Nothing had ever been hooked in that interval. Measured on VM140, and it is right. turn 1 -> 2 (hives created) BeginProcessTurn 2 ProcessTurn 20 residual 2 turn 2 -> 3 (hives exist) BeginProcessTurn 0 ProcessTurn 19 residual 0 Two instruments that share no code path agree: the region ledger reads 170 -> 172 across RegisterHives, and the return-address draw-site table gains a row at 0x00527714 (NextInt, 2 calls / 2 words) which is absent on the next turn. The draws produce NextQ 30/28, TickHives' slip takes them to 31/29 -- exactly turn2-state.sav -- and the next turn's slip gives 32/30, exactly turn3-state.sav. So tail-rng-ledger.md's "all of it inside ProcessTurn, residual exactly zero" is correct for every turn it measured and false as a statement about the code. The interval a standalone must reproduce starts at BeginProcessTurn. Also closed or corrected: * LO/HI read live rather than fitted: NextQ = frame + 20 + NextInt(10), inclusive; the TickHives gates are 10 (a frame floor), 5 and 3, so no queen can spawn before frame 11 on any save. * CDiff's threshold scan re-read independently: the tier can only ever be 0 or 1, and "entered and stored nothing" was observed live and distinguished from "did not run" (method rule 20). * SnLv measured for the first time: Spica reads AFlags 0 with SnLv 0x200, so the sensor branch is what refreshes it -- nvo-tshn-visible-owner.md §6 goes [H] to [V]. ComputeContactLevel's documented "else 1" names one of four return tails; the observed non-visible level is 2. * Rule 19: the oracle reproduced byte for byte with all five new detours live, and again with four watchpoints armed on top. * turn1-state -> turn2 is not deterministic (lane L5 owns this). Two more runs here make it four distinct outcomes over six runs, including two DIFFERENT hooks=off results, and the strategic generator is provably not what varies. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
23 KiB
Hive creation draws, inside BeginProcessTurn — lane Z's zero was the workload, not the code
- Type: control-flow (live measurement)
- Status: verified — five nested boundary hooks on the running game, reconciled against a return-address draw-site ledger that shares no code path with them
- Confidence: high on the RNG result (two instruments agree word for word on two consecutive turns, and the values the draws produce match two corpus saves exactly); see §7 for what is not settled
- Owner / date: lane L1 · 2026-09-08 · VM140
- Instrument:
sots-enginewip/l1,src/shim/hooks/tail_rng.{h,cpp}(five new hooks), build0117495-dirty-20260908T2113Z, configshim.cfg.l1hive - Predictions, committed before the build:
sots-engine/docs/L1-predictions.md - Corrects:
findings/control-flow/tail-rng-ledger.md§0/§2 (qualified, not overturned) andfindings/objects/svsctob-writers.md§4.3 (one address)
0. The answer
SVSOSwarmQueen::RegisterHives draws one strategic-generator word per new hive, inside
StrategyServer::BeginProcessTurn, which runs between the pre-turn autosave and
StrategyServer::ProcessTurn — outside both turn drivers and before either. Measured on the End
Turn taken from turn1-state.sav, where the swarm's two hives are created:
| turn | BeginProcessTurn |
ProcessTurn |
tail | turn total | residual outside the drivers |
|---|---|---|---|---|---|
| 1 → 2 (hives created) | 2 | 20 | 0 | 22 | 2 |
| 2 → 3 (hives exist) | 0 | 19 | 0 | 19 | 0 |
Lane SV predicted this from the instruction stream and could not run it. It is right.
Read the two rows' evidence differently, because they are not equally strong. Row 2 is a closed
bracket: Autosave(endTurn=1) at word 192, Autosave(endTurn=0) at 211, subtotals 0 + 19 + 0,
residual 0 by subtraction. Row 1's bracket is incomplete by construction — it is the first End
Turn after a load, so the pre-turn autosave hook has no server pointer yet and records
words: null, the same limitation tail-rng-ledger.md reports for its turn 3. Its total of 22 comes
from the independent per-call-site ledger (§2.1), which sums to 22 with nothing unattributed and
carries the hive draws as their own row. So row 1's residual is attributed, not subtracted — and
row 2, which is subtracted, is the one that shows the residual returning to 0.
What this does to tail-rng-ledger.md. Its headline — "a strategic turn advances the strategic
generator by 18–22 words, all of it inside StrategyServer::ProcessTurn, and the residual outside
the two turn drivers is exactly zero" — is correct for every turn it measured and false as a
statement about the code. All eight of its turns, and the twelve more it added later, were turns on
which the hives already existed. On the one turn in this campaign's corpus where they do not, the
residual is 2 and it is outside ProcessTurn.
That is the same shape as "the tail draws nothing", which was true of eight turns and false on turn 64. Rule 20's distinction, one level up: a ledger that closes to zero is a statement about the states that occurred.
What it means for the standalone. A reimplementation that models ProcessTurn and the tail
perfectly writes a correct autosave on every turn of the corpus and is two generator words out of
step on the turn the hives appear, after which every later turn diverges. The interval it must
reproduce starts at BeginProcessTurn, not at ProcessTurn.
1. The instrument, and why it could see this when lane Z's could not
Lane Z's six hooks bracket Autosave(endTurn=1) → ProcessTurn → combat → tail → Autosave(endTurn=0).
BeginProcessTurn runs inside that bracket and outside every subtotal, so a draw there was
always visible as residual — and lane Z measured the residual as 0, correctly, because on its turns
there was nothing to see. Nothing had ever been hooked in that interval.
This lane adds five nested hooks, each declaring the live Mars::RNG at S+0x16c as a region whose
describe reports an absolute word position (lane Z's RngLedger):
Autosave(endTurn=1) ....................... 192 <- lane Z
StrategyServer::BeginProcessTurn ........ NEW <- the interval nobody had hooked
SVSOSwarmQueen::OnTurnBegin ........... NEW (evt 0x13, vtable slot +0x60)
SVSOSwarmQueen::RegisterHives ....... NEW THE DRAW
SVSOSwarmQueen::TickHives ........... NEW the NextQ slip; 0 words
StrategyServer::ProcessTurn ............. 19 <- lane Z
StrategyServer::OnAllCombatDone_Tail .... 0 <- lane Z
SVSOSlaversRefuel::UpdateDifficultyTier NEW (evt 0x14, tail phase 20)
Autosave(endTurn=0) ....................... 211
Each new hook carries a model evaluated at entry — the hive count and the two config constants,
the per-hive NextQ values and the three gate constants, the whole CDiff threshold-table scan — so
the record can disagree with the model instead of merely agreeing with itself.
2. The hive-creation turn, measured
Workload: turn1-state.sav (Frame 1, SvSctOb / EncID 10 / Hives count 0, two systems with
EggScio == 3), pushed to VM140 as l1-turn1.sav, loaded, End Turn.
[d2] SVSOSwarmQueen::RegisterHives words 170 -> 172 DELTA 2
scenario_tag = 3 enc_id = 10 hives_in = -1 (empty) systems = 28
systems_tagged = 2 predict_new_hives = 2 predict_min_words = 2
hive_nextq_lo = {ptr 0x0158e0a0, value 20}
hive_nextq_hi = {ptr 0x0158e0a4, value 30}
[d2] SVSOSwarmQueen::TickHives words 172 -> 172 DELTA 0
hives_in = 2
hives = [{sys 0x0e46e788, egg_scio 3, next_q 30, queen 0},
{sys 0x0e46dbe8, egg_scio 3, next_q 28, queen 0}]
gate_frame_floor = 10 gate_queen_age = 5 spawn_list_cap = 3
predict_gate1_fails = true predict_words = 0
[d1] SVSOSwarmQueen::OnTurnBegin words 170 -> 172 DELTA 2
[d0] StrategyServer::BeginProcessTurn words 170 -> 172 DELTA 2 side[frame] 1 -> 2
[d0] StrategyServer::ProcessTurn words 172 -> 192 DELTA 20
[d0] OnAllCombatDone_Tail words 192 -> 192 DELTA 0
predict_new_hives is computed at entry by transcribing the original's own two predicates over the
live Systems vector — sys->EggScio(+0x184) == queen->scenarioTag(+0x4) and "no existing hive
references this system". It said 2; the generator moved 2. The prediction and the measurement
were produced by different code paths and neither was fitted to the other.
2.1 A third instrument agrees, and it is keyed on the return address
The per-call-site ledger (lane Z's draw_sites, seven detoured generator entry points recording
__builtin_return_address(0)) reported, on the same turn:
| return VA | entry point | calls | words | site |
|---|---|---|---|---|
| 0x00527714 | RNG_NextInt |
2 | 2 | RegisterHives — a site no previous run has ever recorded |
| 0x0058788d | NextFloat |
1 | 1 | TechTree::ProcessResearch+0x1c8 |
| 0x0088df54 | NextFloat |
1 | 1 | ServerPlayer::RollResearchEvent+0x2f |
| 0x0089342b | Chance |
8 | 8 | GenerateTradeRaidEncounters+0x196 |
| 0x00893518 | Chance |
8 | 8 | GenerateTradeRaidEncounters+0x283 |
| 0x005032a2 | NextFloat |
1 | 1 | DetectEncounters subtree (lane H) |
| 0x007929a9 | NextInt |
1 | 1 | DetectEncounters subtree (lane H) |
| 0x008e6e09 | NextFloat |
16 | 16 | inside Chance's own body — the helper-internal double count |
38 raw, minus the 16 helper-internal, = 22 = 2 (BeginProcessTurn) + 20 (ProcessTurn), with
nothing left over. On the next turn the same table reads 35 − 16 = 19 and the 0x00527714 row is
absent entirely.
0x00527714 is the return address of the call 0x004271c0 at 0x0052770f. Lane SV's document puts
that call at 0x0052770c; 0x0052770c is the mov [ebp-0x14],esi that stores the bound. Corrected in
ghidra/addresses.d/lane-l1.json.
2.2 The draws produced the historical values, which is the strongest check available
NextQ = frame + LO + NextInt(HI − LO). Live: LO = 20, HI = 30, frame = 2.
| hive on system A | hive on system B | |
|---|---|---|
after RegisterHives (measured) |
next_q = 30 ⇒ draw 8 |
next_q = 28 ⇒ draw 6 |
after TickHives' slip, same turn |
31 | 29 |
turn2-state.sav (historical) |
31 | 29 |
| after the next turn's slip (measured) | 32 | 30 |
turn3-state.sav (historical) |
32 | 30 |
Two draws, two systems, four numbers, all matching saves captured in a different process on a
different day. The generator trajectory is identical to the historical game's, which is a much
stronger statement about instrument neutrality than a word count could be — and it is corroborated by
the absolute positions: this run reaches word 192 after turn 2 and 211 after turn 3, and
tail-rng-ledger.md §2.1 records 211 as the ProcessTurn entry position of turn 4 on the same
game.
3. LO and HI — the leaf lane SV declared blocked
"blocked on one MT draw and two data-file constants … behind pointers at 0x00ae0204/0x00ae0208 that no
.textor.datareference initialises in a form this lane could follow. FittingLOandHIfrom a single two-hive observation would have been fitting, not derivation."
Refusing to fit was right, and reading them live costs one LogF-shaped line:
| pointer | value | meaning |
|---|---|---|
*(int*)0x00ae0204 |
20 | LO — the low end of the queen-countdown window |
*(int*)0x00ae0208 |
30 | HI — the high end; RNG_NextInt's bound is HI − LO = 10, inclusive |
*(int*)0x00ae0210 |
10 | TickHives gate 1: no queen may spawn while Frame <= 10 |
*(int*)0x00ae0228 |
5 | TickHives gate 3: a minimum age in turns |
*(int*)0x00ae0220 |
3 | a cap applied inside the (never-executed) spawn arm |
So a new hive's countdown is frame + 20 + U{0..10}, and it then slips +1 per turn for as long as
any gate fails. The leaf is closed: a standalone needs these five integers and the two predicates,
not the data files.
4. The NextQ slip, live
Lane SV recovered the slip statically — a single inc [hive+8] at 0x0052785a that four
gate-failure edges converge on — and reproduced 31/29 → 32/30 with no draw and no fitting. Confirmed:
TickHiveswas entered on both turns and cost 0 words both times.- On turn 1→2 it received
next_q30 / 28 and the save carries 31 / 29. - On turn 2→3 it received 31 / 29 and the save carries 32 / 30.
predict_gate1_failswas true on both turns, and now has a number behind it:Frame <= 10, so on this save no queen can spawn before turn 11 whatever the countdown says. Every hive slips for at least the first ten turns of its life.
The spawn arm still has never executed (rule 6). No hive in any corpus save has a queen, and
queens_in was empty on both turns. What the gate constants now let anyone state is why, and how
far away it is, rather than "we never saw it".
5. CDiff — both edges, and one of them settled without a workload
SVSOSlaversRefuel::UpdateDifficultyTier 0x00515820 was entered once per End Turn, at depth 1
inside the tail (event 0x14, through the class's seven-instruction generic handler 0x0051a800), and
cost 0 words both times.
| turn | frame |
cdiff_in |
predict_path |
region cdiff before → after |
|---|---|---|---|---|
| 1 → 2 | 2 | −1 | store |
−1 → 0 |
| 2 → 3 | 3 | 0 | no_store_unchanged |
0 → 0 |
The second row is the point. "Entered and stored nothing" is now distinguishable from "did not
run" — which is exactly what a save-file comparison cannot do, and why every corpus pair after
frame 2 shows CDiff motionless without saying whether the writer ran.
The table itself was re-read from the instruction stream, independently of lane SV, at real instruction boundaries:
515868 [ebp-0x34] = 1 [ebp-0x30] = 1 [ebp-0x2c] = 1
515877 [ebp-0x28] = 0x32 [ebp-0x24] = 2 [ebp-0x20] = 3
515885 [ebp-0x1c] = 0x64 [ebp-0x18] = 2 [ebp-0x14] = 5
515893 xor eax,eax ; lea ecx,[ebp-0x34]
515898 cmp [ecx],edx ; jg 0x5158c2 ; first threshold GREATER than the frame
51589c add eax,ebx ; add ecx,0xc ; cmp eax,3 ; jl 0x515898
<falls through to 0x5158a6, which IS THE EPILOGUE -- no store>
5158c2 test eax,eax ; jle 0x5158a6 ; index 0 -> no store
5158cc cmp [edi+0x38],eax ; je 0x5158a6 ; unchanged -> no store
5158d1 mov [edi+0x38],eax ; CDiff = index - 1
Only the threshold column is ever loaded; the (1,1) (2,3) (2,5) payload columns are dead in
this function. So:
| frame | scan index | stored |
|---|---|---|
| ≤ 0 | 0 | nothing |
| 1 … 49 | 1 | CDiff = 0 |
| 50 … 99 | 2 | CDiff = 1 |
| ≥ 100 | walks off the end | nothing |
CDiff can only ever hold 0 or 1. Tier 2 is unreachable at any frame, and lane SV's reading of
that off-by-one is confirmed by a second independent disassembly. The class ctor 0x0051a820 stores
[eax+0x38] = -1, which is precisely the CDiff = −1 that turn1-state.sav carries and the reason
frame ≤ 0 never matters: Frame starts at 1 and the tail does not run before the first End Turn.
What is not yet measured is the 50 and 100 crossings live. §7 says what that costs and why it was
not bought with the VM time available; the entry hook is in the tree and prints predict_path on
every turn, so whoever plays a game past frame 50 gets both edges for free.
6. Instrument neutrality (rule 19) — checked, and it needed checking twice
Five new MinHook detours were installed for this lane, on top of lane Z's six template hooks, seven draw-site detours and twelve entry probes. Lane H's finding is that one detour on a clean prologue boundary changed the game's output by four bytes, so this was a real prediction, not a formality.
The oracle reproduces, exactly. Same build, same shim.cfg.l1hive, load ref-turn2.sav, one
End Turn:
| file | size | sha256 prefix | historical |
|---|---|---|---|
(Autosave EndTurn).sav |
66,732 | bb4fd9ac89f41e3b |
identical |
(Autosave).sav |
67,219 | 978041acd168b56e |
identical |
That run's trace is the second half of the same story: RegisterHives entered with the two hives
already present and NextQ 31 / 29, drew 0, BeginProcessTurn cost 0, ProcessTurn cost
19, position 192 → 211. So the hooks that measured 2 words on the creation turn measure 0 on a
turn that produces a byte-identical historical autosave.
6.1 The creation turn is NOT a byte oracle — and the reason is a live nondeterminism, not the hooks
Loading turn1-state.sav and ending one turn does not reproduce turn2-state.sav
(ab4ac2d7…); it produces a 66,739-byte save with a different hash. Reported here rather than
buried, because it is the kind of thing that looks like instrument perturbation and is not:
-
The generator trajectory is identical to the historical game's — same word positions (192 after turn 2, 211 after turn 3), and the two hive draws produce
NextQ31 / 29 and then 32 / 30, exactly the valuesturn2-state.savandturn3-state.savcarry. -
The pre-turn autosave round-trips byte-identically (
a3f9dc4b…in,a3f9dc4b…out). -
The whole difference, at schema level, is one field on one player:
player historical turn2-state.savthis run 3 ResTNm = BIO_GnModResTNm = XNC_TrnsLir2Players 0, 1 and 2 agree exactly (
"",IND_Waldo,DRV_PlsFiss), as doStatus,EvNxID,NumOwnand every other field checked. The twoTechTree/Stbytes that also move are that choice's consequence, andSummary/Checksumfollows.
The hooks=off control was then run from the same input, and it produced a third answer.
Lane L5 found the same thing independently on VM146 the same day, from the other direction — as a
by-product of its own rule-19 control — and owns the write-up:
findings/subsystems/turn1-to-turn2-nondeterminism.md. This lane's two runs are corroboration and
they widen its spread, so the union is recorded here and the analysis is not repeated.
| run | instrument | (Autosave).sav |
that player's ResTNm |
|---|---|---|---|
| the original 2026-09-07 session (live game, never loaded) | phase-2 shim, hooks=trace |
ab4ac2d7e2977260 |
BIO_GnMod |
| lane L5 run B | ComputeBudget=compare |
ab4ac2d7e2977260 |
BIO_GnMod |
| lane L5 run A | same hooks, different constant in ours only |
7fc9e6ab46b47794 |
XNC_TrnsHvr2 |
| lane L5 control | hooks=off |
62061918176e3441 |
XNC_TrnsLir2 |
| L1 | full L1 hook set | 62061918176e3441 |
XNC_TrnsLir2 |
| L1 control | hooks=off |
e43ec1d2b443c101 |
XNC_TrnsMorr2 |
The two rows in bold are what this lane adds, and the second one sharpens lane L5's conclusion.
L5 argued from "the hooks=off control is a third value, not one of the other two". This lane ran
hooks=off again and got a fourth value — so hooks=off is not self-consistent with itself
across processes, which is a stronger statement than "no hook is responsible" and rules out the
whole class of explanations in which some particular instrument configuration selects the pick.
Four distinct outcomes over six runs, and this lane's instrumented run happens to coincide with
L5's uninstrumented one, which is the clearest possible demonstration that the hooks are not the
variable.
Players 0, 1 and 2 pick identically in every run ("", IND_Waldo, DRV_PlsFiss). Only the
shadow-empire player moves. In this lane's instrumented run the strategic generator's trajectory was
identical to the historical game's — same word positions, and the hive draws produced the
historical NextQ values — so whatever chooses here does not draw from it.
6.2 It is bounded, and the strategic generator is not what varies
No claim in §0–§5 rests on an autosave diff. The hive result is a generator word position
measured by two instruments that share no code path — a region ledger reading left around a phase
boundary, and a set of detours keyed on return address — plus the entry-side prediction those
numbers were checked against. Diffing turn1-state → turn2 bytes is not part of it, and would not
have been able to answer the question either way.
The variation is small and exactly located. Every pairwise leaf comparison over the three post-turn saves this lane holds gives 4 differing leaves out of 35,032, with the leaf count and the whole tree structure identical:
/Summary/Checksum (derived)
/Sim/Player/TechTree/St 2 <-> 3 (the node it stopped researching)
/Sim/Player/TechTree/St 3 <-> 2 (the node it started researching)
/Sim/Player/ResTNm BIO_GnMod | XNC_TrnsLir2 | XNC_TrnsMorr2 | XNC_TrnsHvr2
so the reference pair remains usable for everything except one shadow empire's research selection. This matches lane L5's characterisation exactly, on a fourth sample.
And it is not the strategic generator. This is the part this lane can add that a save diff
cannot: in the instrumented run the strategic generator's trajectory was identical to the
historical game's — the two hive draws produced the historical NextQ 31 / 29, the next turn's
slip produced 32 / 30, and the absolute word positions 192 and 211 are the ones
tail-rng-ledger.md §2.1 records for the same game. A pick drawn from S+0x16c would have moved
that trajectory. It did not.
There is an obvious next place to look, and this lane's own draw-site table points at it: 8 words
per turn are drawn from a different Mars::RNG in the same process — the StrategyClient's at
client+0x134, from 0x00578cf0, 0x005798e0 and 0x0069dbb0 (tail-rng-ledger.md §11.2 first
identified these and correctly excluded them). That object is not serialised, so if the AI's
pick draws from it, its seed is not in the save and a per-process variation is exactly what one
would expect — which is lane AI1's "seeded from an unseeded global" reading, arriving from a
different direction. Two rows of that table appear on every turn of both saves measured here:
draw_site_words_other_rng = 8, draw_site_calls_other_rng = 8.
The experiment that would settle it is a boundary hook on the AI research-selection site with a
region over client+0x134 rather than S+0x16c, on two fresh processes: if the two runs enter it
with different generator state, the cause is found. That is one hook and one afternoon, and it is
the difference between "the reference pair has four unstable leaves" and "we know why".
7. What this does NOT settle (rule 15)
- One hive-creation event, on one save.
turn1-state.savis the only save in the corpus whose swarm queen has zero hives and whose next turn creates some; the six Zuul saves sit at zero hives for eighteen turns and never transition. So "one word per new hive" is confirmed for two hives created in one call and extrapolated beyond that. RNG_NextInt's rejection loop never fired. Bound 10 ⇒ mask 15, so a draw is rejected with probability 5/16 and the expected cost is 16/11 ≈ 1.45 words per hive, not 1. Two hives costing exactly 2 is a ~47% outcome. A reimplementation must model the rejection loop, not the constant. This run cannot tell the two apart and does not claim to.- The spawn arm, and everything past it. No queen has ever spawned under any instrument. Its callees 0x0050dfc0 and 0x004fe810 are unread and may draw.
CDiffat frame 50 and frame 100 — modelled and predicted, not observed (§5).BeginProcessTurn's other work is unmodelled. Its two container walks (three calls per system, one per fleet) and its event post are not declared by anything. On these two turns their cost is zero by subtraction —BeginProcessTurnandOnTurnBeginagree to the word on both turns — but that is a measurement on 28 systems and one player's fleets, not a proof.- No game-state region is declared by any of these hooks. They measure the generator and nothing else. A clean run here says the RNG accounting is right and says nothing about whether the turn was computed correctly.
- Event 0x15 and the alliance events remain unreachable from any save we hold — unchanged from lane SV.
8. Files
- Traces:
verify/results/shim/l1/trace-et1.jsonl(End Turn 1, hive creation),trace-et2.jsonl(both End Turns; the second is the control turn) - Report tool used to read them:
verify/results/shim/l1/rep.py - Saves produced:
verify/results/shim/l1/ - Addresses:
ghidra/addresses.d/lane-l1.json(10 entries) - Instrument + predictions:
sots-enginewip/l1,docs/L1-predictions.md