# CB — the capture instrument, and what it measured Lane CB, 2026-09-08. Guest VM146, build `cb2-20260908T2251Z`. Predictions and their outcomes are in `docs/CB-predictions.md`; the RE-side write-up, the captures and the raw logs are in `sots-re` (`findings/subsystems/turncommands-capture.md`, `verify/results/turncommands/`). This document is the engine-side record: what changed in the shim, and the two facts the change bought that a replayer needs. --- ## 1. What changed Three additions to lane L4's `src/shim/hooks/ai_orders.cpp`, and nothing else. L4's design point is kept unchanged: **the instrument reads element bytes, not element types**, and the typing happens offline in `sots-re/tools/turncommands_capture.py`, so a corrected record costs a re-parse instead of a rebuild, a redeploy and a turn. That paid for itself twice in one session — see §4. | key | default | what | |---|---|---| | `aiorders.words=` | 12 | the element window, ≤ 64. List 1's `ShipDesignDef` puts its name past 48 bytes. | | `aiorders.deep=on\|off` | off | follow the heap: `{_Myfirst,_Mylast,_Myend}` and MSVC `std::string` shapes inside the window. | | `aiseed=off\|log\|pin` + `aiseed.values==,…` | off | read — or **replace** — the fourth stack argument of `Game::StrategyApp::RunAI`. | `aiorders=on aiprobes=off` is still one detour; `aiseed=` adds a second. Configs: `shim.cfg.cboff` (the control, installs nothing), `shim.cfg.cbcapture`, `shim.cfg.cbtrap`, `shim.cfg.cbpin`. **Three payloads in the canonical block are behind pointers** — list 8's route, list 10's counted vector, list 23's `Population` body. Without `deep` they are **absent**, not empty, and a fleet move with no route reads exactly like a fleet move that goes nowhere. That is why `aicfg words= deep=` is printed into every capture: a reader must never have to guess which of the two it is looking at. ## 2. `aiseed=pin` is an intervention, not an instrument, and is logged as one Every call logs `observed`, `used` and `pinned`, so a pinned run says so in its own capture and can never be read as a natural one. It exists because of a control problem, not a curiosity. `turn1-state → turn2` has an outcome set of size k > 1: three lanes ran it with `hooks=off` and got three different files. On such a workload **no single un-instrumented run is a control**, and an instrumented run agreeing with one of them is a 1/k coincidence — at lane L4's k = 6 that is a 17 % chance of a clean bill of health that means nothing. Pinning removes the only per-process input and makes a control possible. It worked, three times over. Three processes on that save, each drawing its own three natural seeds (nine distinct words in all), two of them pinned to the first's, produced **one** autosave (`d59bb9f2fd0eb535…`) with identical gates — including player 512's research target `282`, the value that varies — and identical element records. Two runs agreeing on a `k > 1` workload is a `1/k` coincidence; three is `1/k²`. ## 3. What a replayer needs from this, in one place * **The seeds are part of the stream.** `game/ai` is a deterministic function of (save, per-client seed). A capture without them records the AI's answer with no record of its input. * **Element sizes, measured** (not read): list 3 = 4 words; list 5 = 8; list 8 = 1 + vector; list 10 = 2 + vector; list 14 = 2 words **+ one byte**; list 23 = 1 + vptr + vector. Established by comparing two runs that wrote byte-identical autosaves — a word that differs between them is not a word the applier reads — and agreeing with the writer's own instruction stream on all six. * **List 14's bool is one byte.** The other three bytes of its word are heap garbage that differs run to run. Compare the byte, not the word. * **List 3's element is stored backwards** relative to its wire order. Per-list, not a rule. ## 4. Two records corrected, one of them mine **Lane L4's list-23 reading, twice.** `Population{vptr, vector(24 B), −1}` has no trailing int — the word after the vector differs between two runs whose autosaves are byte-identical, so it is an allocator cookie. And the body is **not** turn-dependent: five of its six words are the same on turn 1 and turn 2, and the sixth is a per-process pointer. **My own list-5 record, found by lane RB while consuming the capture.** 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 unread. RB's first replay predicted zero regressions and got two. The record is now **dropped** rather than relabelled: the values 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 it would have propagated inside the artefact that is meant to outlive the adapter that read it. That is the capture/replay pairing doing its job in both directions, which is the argument for having run the two lanes together. ## 5. Gates Run as separate commands (rule 13). * `tools/clean_room_check.sh` — **OK** * host `ctest --preset host` — **55/55** * CT111 shim cross-build (`/srv/re-lab/build/sots-engine-cb`, `DIST=/srv/re-lab/shim/dist-cb`), on a **freshly extracted tree with `build-shim` removed** (rule 24) — **exit 0**, exports 66 names identical to the real `binkw32.dll` The generated header was **regenerated** from `sots-re/ghidra/addresses.json` plus every `ghidra/addresses.d/*.json` fragment (rule 14), never hand-resolved — the merge of lane L4's instrument conflicted there and regeneration is what resolved it. **This lane adds no addresses**: it used `StrategySim_ApplyTurnCommandBatch` and `StrategyApp_RunAI`, both already carried by lanes A2 and AI1, so there is no `addresses.d/lane-cb.json` and there should not be one.