# 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. A process whose natural seeds were `0x227235b9 / 0x762d125e / 0x9d77d4e6`, pinned to an earlier run's `0xe70a4703 / 0x0c63ca36 / 0x372be4df`, reproduced that run's **entire command block** — including the research pick that varies — and its autosave **byte for byte**. ## 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.