# L4 — what the live AI will emit, written before the module was built Lane L4, 2026-09-08. Guest **VM145** (`sots-re-win10-145`, 192.168.10.145). Worktree `wip/l4` off `main` `0117495`. Rule 2: this file is committed **before** the shim module is written, and it names the falsifier for every claim. Nothing below is a summary of a run; there has been no run. The question this lane exists to answer: **does our emission model produce the same commands the game does?** `src/game/ai` models the *shape* of a `TurnCommands` block and the *cost* of each element, and decides nothing. Nobody has ever looked at a real AI block. --- ## 0. The instrument, and why it is one detour `Game::StrategySim::ApplyTurnCommandBatch` **0x0088f9b0** receives `(blocks, n)` as two stack arguments with `ecx = sim` — read straight off the prologue at `0x0088f9db`–`0x0088f9e7`, where `n` is multiplied by `0x1b4` and added to `blocks` to make the end pointer. **At that instant every player's submitted block is complete and in memory**, so one register-transparent entry stub dumps the entire turn's command traffic: six gates and twenty-seven list lengths per player, plus the element bytes. That is a strictly better instrument than counting `ModCount` bumps, which is what every previous lane has had. A bump count says *how many* commands; the block says *which lists, how many elements, and what values*. Second instrument: sixteen register-transparent entry counters (`probe_entry.cpp`'s pattern, its own table so lane H's set is untouched), on the task bodies and the emission gates. One of them — `StrategyAIAgent::RunTaskList` **0x006b3320** — additionally records its `pass` stack argument into a global, so every later probe hit is **attributed to a pass**. That is what turns AI3's P2 from a count into a measurement. **Rule 20 is the whole design.** `AITRaid`'s list-16 emit is the named open item, and a zero at `0x007635f0` means nothing unless `AITRaid::Execute 0x0068e670` is also probed: "never ran" and "ran and emitted nothing" are opposite answers. **Rule 19 control.** Both workloads have published oracle bytes. Every configuration is run against them and any configuration that moves a byte has invalidated its own numbers. --- ## 1. Workload W1 — `turn1-state.sav`, one End Turn (the turn 1→2 transition) Facts already in the corpus, from `turn1-state.sav` → `turn2-state.sav` (read this session with `verify/save-reader`, not taken from a report): | | | |---|---| | players | 8: ids 16, 32, 496, 512 (`Species` 0/2/0/2) and 528/544/560/576 (`Species == 4`) | | player 32 | `HomeSys` **288** = Ke'Dolarra, `PlyrIdx` 1 | | `ModCount` | 0 → 12 | | `ResRate` | 0.25 → **0.8** on 32, 496, 512; unchanged (0.25) on 16 | | `ResTNm` | `''` → `IND_Waldo` (32), `DRV_PlsFiss` (496), `BIO_GnMod` (512); `''` on 16 | | `Status` | 0 → 4 on 16/32/496/512; **0 on all four `Species == 4`** | | player 32 designs | 5 → 6; the new one is `DesID` **18**, `"Honor Lance"` | | `designIds` | gains **18** *and* **1712** — the second belongs to player 528, whose design count also goes 18 → 19 | | `NMnx` | 106 → 109 (ship **1728**, fleet **1744**, and one more) | | new fleet | **1744** `"Alpha Fleet"` at system 288, holding ship 1728 of design 18 | ### P1 — the block set **Prediction: `n == 4`, and the four `block+0x04` player ids are 16, 32, 496, 512 in that order.** *Falsified if* `n == 8` — then the `Species == 4` factions **do** submit, they just submit empty blocks, and AI4 §2's "no `StrategyClient` at all" is too strong (it would become "a client that never sets the always-set gate", which is a different and stranger claim). *Falsified differently if* the four ids are 0,1,2,3 — then `block+0x04` is a player **index**, not the save id, and every published reading of that field needs the same correction. ### P2 — the four blocks, list by list | block | gates set | lists non-empty | |---|---|---| | 16 (human) | rate only, value **0.25** | **none** | | 32 (AI) | rate **0.8**, target (`IND_Waldo`) | **1, 3, 5 — one element each** | | 496 (AI) | rate **0.8**, target (`DRV_PlsFiss`) | **none** | | 512 (AI) | rate **0.8**, target (`BIO_GnMod`) | **none** | Boost, group-4, the three-float Hiver gate and `CivilianRatios` **clear on all four**. That sums to AI4's twelve: 4 rate + 3 target + 3 (player 32's lists) + 2 drivers. *Falsified if:* any list outside {1, 3, 5} is non-empty on any block; or a list in {1, 3, 5} carries more than one element; or the human's block carries a list at all; or 496/512 carry a list (they own nothing, so a list element from them would mean a task fired against objects they do not have). **The sharpest falsifier is list 8/10/14 non-empty.** Player 32 gets a brand-new fleet (1744) this turn, and a fleet is exactly the thing lists 8/10/14 order. If the AI issues a fleet order on the same turn it creates the fleet, AI4's P2 attribution is wrong by exactly the fleet-order group and the twelve has to be re-derived. ### P3 — the element values * **list 3** (`{i32 ordinal, i32 designId, i32 systemId, i32 w}`): `designId == 18`, `systemId == 288`. The `w` word has never been observed and is not predicted. * **list 5** (`{i32 systemId, OutputRates}`): `systemId == 288`, i.e. **the same system the build order names**. * **list 1** (`ShipDesignDef` frame + `i32`): the element carries the id **18** somewhere in its first 48 bytes. *Why 18 matters.* 18 is not a multiple of 16 and does not come from the master `NMnx` counter — the *other* new design that turn, player 528's, got **1712** from that counter, and 528 submits no block. So there are two id spaces, and the prediction is that **a client-issued design carries a small client-local id in the command itself**, while a server-created object gets a master id. *Falsified if* the list-1 element's ids are all ≥ 512 / multiples of 16 — then 18 was assigned server-side when the command was applied and the design command carries no id at all, which is a materially different thing for a reimplementation to emit. ### P4 — the probes on W1 | probe | prediction | what a miss means | |---|---|---| | `RunTaskList 0x006b3320` | **6** = 3 AI players × 2 passes | 2 ⇒ only one player's task list runs and 496/512 reach `ResTNm` by some other path; 0 ⇒ the instrument is broken, not the game | | `BuildTurnCommands 0x00783780` | **4**, one per submitting block | ≠ 4 ⇒ P1 is wrong from the other side | | `RequestBuildForTask 0x006cea50` | **> 0 and even-ish** — entered on both passes, emits only on pass 1 | entries only in pass 1 ⇒ the caller gates before it, and AI3 §2.3's "the hub returns an empty list on pass 0" is not the whole mechanism | | `AssignFleetsAndIssueOrders 0x006c16c0`, `IssueRouteForFleets 0x006bbd50` | **entered, both passes, zero list-8/10/14 elements** | entered + elements ⇒ P2 falsified | | `AITRaid::Execute 0x0068e670` | **0** | — | | `ClientOrder 0x007635f0` (list 16) | **0** | — | | the six shared planner bodies (`AITColonize` 0x0068b400, `AITEscortGateInvade` 0x0068c7c0, `AITInvade` 0x0068d7a0, `AITNodeBore` 0x0068e590, `AITBuildPoliceShips` 0x00690380, `AITBuildDeepScanShips` 0x006901a0) | **at least one build-shaped body entered in both passes; at most one produces the single list-3 element** | none entered ⇒ the list-3 element comes from a task nobody has named, and AI3 §5's path table is not the emission path | | `AITAdvanceIdleShips::Execute 0x0068f230` | entered (priority 0, always last) | — | | `IsClaimedByAnotherTask 0x006a8d20` | **> 0** | — | **Stated in advance so it cannot be claimed afterwards: W1 almost certainly cannot settle AI3 §2.4.** A turn-1 AI with one colony and no fleet of its own has nothing to raid, so `AITRaid` is predicted not to run at all, and a zero at `0x007635f0` will then be a *non-answer* about pass 0 — exactly the "did not fire" vs "fired and found nothing" distinction rule 20 is about. The AITRaid question needs `AITRaid::Execute` to be entered, and W2 is the better chance. **Also stated in advance: this probe set cannot settle AI3's P3.** An entry counter on `IsClaimedByAnotherTask` measures the "called often" half only; the steal branch at `0x006a8ded` is *inside* the function and an entry probe cannot see it. P3 stays open. --- ## 2. Workload W2 — `ref-turn2.sav`, one End Turn (the turn 2→3 transition) `ref-turn2.sav` **is** `turn2-state.sav` (same sha256). Corpus facts for 2→3: * `ModCount` 12 → 24; `NMnx` 109 → 111 (ship **1760**, fleet **1776** `"Gamma Fleet"`); * fleet **1744** `"Alpha Fleet"` is **gone**, replaced by fleet **34** `"Beta Fleet"`, in transit (`FtTrans 1`), flight plan one waypoint `Wpt 272`, `pnd 288`, ETA 3, origin = Ke'Dolarra's position; * no `ResTNm` change on any player; `ResRate` already 0.8. AI4 §2.1 attributes the twelve as 4 rate + list 5 + list 3 + list 10 + **2 × list 14** + list 8 + 2 drivers. ### P5 — the block **Prediction:** `n == 4`; blocks 16, 496, 512 carry the rate gate and **nothing else** (no target gate this turn); block 32 carries the rate gate and lists **3 = 1, 5 = 1, 8 = 1, 10 = 1, 14 = 2**, everything else empty. ### P6 — list 14 is two elements against one fleet, and the second field is the discriminator Lane Q read the element as `{i32, i32, bool}`; `human-turn2-orders.sav` (a UI fleet move) carries exactly one, `{1456, 0, true}`. AI2's P1 says the AI's bridge calls the adder twice with mode 0 and mode 1 and the adder keys on `(fleet, mode)`. **Prediction: the two list-14 elements are `{F, 0, b}` and `{F, 1, b}` with the same `F`, and `F` is also the fleet id list 8's element names.** That converts AI2's P1 from an inference about bump counts into a read of the values. *Falsified if:* both elements carry the same second word (then the adder does not key on mode and the doubling has another cause); or the two fleet ids differ (then it is two orders against two fleets and the "one order costs two elements" rule is wrong); or list 14 has one element (then AI4's two list-14 bumps came from two different fleets and the reference turn moved two). ### P7 — which fleet id the order names **Prediction: `F == 1744`** — the fleet that exists at submit time — and **34 does not appear anywhere in the block.** 34 is created when the order is applied, from the same small-id space as design 18. *Falsified if* `F == 34`: then the client creates the fleet object *before* submitting and ships the new id in the command, which would make the small-id space **client-allocated and part of the wire protocol** — a much bigger constraint on a reimplementation than the alternative, because our engine would have to reproduce that counter exactly to get byte-identical saves. This is the one prediction I would most like to be wrong, because the falsifier is the more interesting world. ### P8 — list 10's first word Lane AI4 §4.5 refused to name list 10 (`{i32, i32, counted i32}`) on positional evidence alone. **Prediction: its first `i32` is `F`, and its counted vector holds ship ids** — on W2 that means the single ship **1728**. If so, "assign these ships to this fleet" is supported by values rather than by adjacency, and the list can be named. *Falsified if* the counted vector is empty or holds system ids (W2's route is a single hop to 272, so a `{…, 272}` tail would make it a route-shaped command instead). ### P9 — AITRaid, the second attempt Player 32 now owns a fleet, so `AITRaid` has a candidate. **Prediction: `AITRaid::Execute` is entered on both passes and `ClientOrder 0x007635f0` is entered zero times**, i.e. list 16 stays empty and AI3 §2.4's inference holds — but *for the reason the probe can see*, which is the point. *Falsified if* `0x007635f0` is entered with `pass == 0` recorded: then pass 0 **does** emit, AI3's P2 is falsified, and every `ModCount` arithmetic in `ai-order-emission.md` that assumes one emitting sweep is off by a factor. --- ## 3. Rule 19 — the controls, and what they are compared against Published oracle bytes (sha256 prefixes, `findings/subsystems/running-the-game.md`): | workload | `(Autosave EndTurn).sav` | `(Autosave).sav` | |---|---|---| | W1 from `turn1-state.sav` | `a3f9dc4b…` | `ab4ac2d7…` | | W2 from `ref-turn2.sav` | `bb4fd9ac…` | `978041ac…` | Three configurations per workload, each a separate launch: | run | `shim.cfg` | purpose | |---|---|---| | **C** | `hooks=off` | proves **this clone** reproduces the oracle at all. VM145 is a ZFS clone; nothing has ever checked that it is byte-faithful. If C fails, the lane reports that and stops — every later number would be measured against an unknown baseline. | | **A** | `aiorders=on`, `aiprobes=off` | the block dump alone: one detour | | **B** | `aiorders=on`, `aiprobes=all` | the dump plus sixteen entry probes | **Prediction:** C, A and B all reproduce both oracle hashes. **Falsified if** A or B moves a byte — in which case that configuration's numbers are reported as unmeasured, not as results, and the sixteen-probe set is bisected with `aiprobes=N`. Lane H's precedent is explicit that a MinHook detour has changed this game's behaviour once already, and that the probe module is the one it happened to. --- ## 4. What this lane will not do, said now 1. **It does not close AI3's P3** (§1, P4). An entry probe cannot see a branch inside the callee. 2. **It does not test the eleven "free" lists** (AI4 P1's untested half) unless one of them turns up populated, which P2/P5 predict will not happen. If one does, that is the result. 3. **It does not settle the Hiver gate** (AI4 P3). The corpus has no Hiver player and this lane is not manufacturing one. 4. **It reads element bytes, not element types.** The dump emits a fixed 48-byte window per element plus the same bytes as ints and floats; decoding to lane Q's records happens offline, in the report, where a wrong record is visible as a wrong value rather than being baked into the instrument. Where a list's in-memory element is larger than 48 bytes the dump says so rather than silently truncating. 5. **The dump's own coverage check**: for every list it walks the node chain *and* reads `_Mysize`, and logs a `MISMATCH` line if they disagree. A wrong list layout would otherwise print a confident, wrong zero (rule 1). --- # Addendum — the research-selection tie set Added after the first two runs, before the research instrument was written. The coordinator's question: if the one varying AI decision is a **tie** broken by something per-process, the original's possible outcomes form a small enumerable set, and a deterministic `game/ai` can pick canonically and claim *"our choice is one of exactly k the original can produce, and here are all k"*. That is a stronger claim than behavioural equivalence, and it needs the candidate set. ## What the selection site actually is (read before predicting) Process Turn phase 18 is `0x006caf70`. It tries three producers in order and takes the first non-null: ``` eax = 0x006a84f0(agent) ; producer A if (!eax) eax = 0x006c27c0(agent) ; producer B if (!eax) eax = 0x006c8890(agent, &agent+0x13c); producer C -- the candidate walk if (eax && eax != player->+0x294) cl_SetResearchTarget() ``` `player->+0x294` is the current target, and `cl_SetResearchTarget 0x00578f60` takes the tech's **name string**, not an id — the object's `+0x4` is a `std::string` and the caller resolves the short-string union before pushing it. Producer C, `0x006c8890`, has two halves: **C1, the candidate walk.** `if (player->+0x294 != 0) return 0`. Otherwise build a `std::vector` of 0x0c-stride candidates with `0x006c2490` — which constructs a working object, fills it (`0x006bcca0`), emits the vector (`0x006bc500`, a nested walk over groups, **no `std::sort` at that level**) and destroys it — then walk the vector **front to back** calling `0x006c8580(ecx = the target slot, edx = cand[1], stack: agent, cand[0])` and **return the first one that answers non-zero**. First-acceptable in arrival order. No score, no comparator, no tie resolution: **whatever the vector's order is, is the answer.** **C2, the fallback**, reached only when the walk accepts nothing: `0x006b36e0(agent) & 0x80000007` indexes one of two eight-entry `.data` tables, and the value there seeds a three-arm rotation `(i + seed) % 3` over `0x006c8670`. Both tables are in the image and both hold values in {0,1,2}: ``` 0x00a1a544 = 2 0 2 0 2 1 2 0 0x00a1a564 = 0 2 0 2 1 0 2 0 ``` `0x006b36e0` is not an RNG call — it reads `player->+0xf4`, calls `0x0080da80` and computes. So C2's "roll" is a **hash of player state**, and its outcome space is at most **three arms**. ## Predictions ### P10 — where the variation lives **Prediction: `0x006c8890` (producer C) is entered once per AI player on turn 1 and the variation is in C1's candidate vector, not in C2.** Specifically, across two runs of the same workload: * players 32 and 496 (stable across five runs between lanes L4 and L5) produce **identical candidate streams in identical order**, and the accepted candidate sits at the same position; * player 512 produces a candidate stream that is **the same set in a different order**, and the first-acceptable one is therefore a different tech. *Falsified if:* the streams are identical for 512 and the accepted position still differs ⇒ the decision is inside `0x006c8580`, i.e. state or a seed, and arrival order is not the mechanism. *Also falsified if:* `0x006b36e0` fires for 512 ⇒ the pick came from **C2**, which is not a tie at all but a three-arm rotation seeded by a state hash, and `k <= 3` by construction. ### P11 — k is enumerable, and small **Prediction: the tie set for player 512 is the set of candidates in its stream that `0x006c8580` would accept, and the stream is short — tens, not thousands.** A player with no colonies and no research history has few reachable techs. *Falsified if:* the stream is hundreds of entries ⇒ "name all k" is not a practical verification claim for this decision and the honest move is to mask the leaf, as lane L5 already does. ### P12 — the two stable players are the control **Prediction: 32 and 496 accept a candidate at position 0 or very near it**, because a player whose choice never varies is one whose first candidate is always acceptable. If instead they accept deep in the stream and are still stable, then order is stable for them and unstable for 512 — which would point at 512's *container*, not at the walk. ### P13 — what our engine should do If P10 holds, a deterministic `game/ai` sorts the candidate vector by a canonical key (the tech name, which is what the original ships in the command anyway) before the walk. The verification claim becomes: **our pick is the canonical member of the tie set; every observed original run picks some member of the same set; here is the set.** If P10 is falsified in the C2 direction instead, the claim is different and weaker in kind but stronger in size: three arms, both tables in the image, so k <= 3 and all three are nameable without any capture at all. *Either way the capture names k.* That is the point of running it.