From cd92076b391478b6422be349c980397c61a0a4ee Mon Sep 17 00:00:00 2001 From: alex Date: Tue, 8 Sep 2026 19:22:16 -0400 Subject: [PATCH] CB: the engine-side record of the capture instrument and what it measured Co-Authored-By: Claude Opus 5 (1M context) Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ --- docs/CB-capture.md | 94 +++++++++++++++++++++++++++++++++++++++++ src/shim/shim.cfg.cbpin | 8 +++- 2 files changed, 101 insertions(+), 1 deletion(-) create mode 100644 docs/CB-capture.md diff --git a/docs/CB-capture.md b/docs/CB-capture.md new file mode 100644 index 0000000..ef01f11 --- /dev/null +++ b/docs/CB-capture.md @@ -0,0 +1,94 @@ +# 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. diff --git a/src/shim/shim.cfg.cbpin b/src/shim/shim.cfg.cbpin index 5467a80..042542a 100644 --- a/src/shim/shim.cfg.cbpin +++ b/src/shim/shim.cfg.cbpin @@ -1,4 +1,7 @@ -# Lane CB -- the pinned creation-turn capture. `shim.cfg.cbcapture` with `aiseed=pin`. +# Lane CB -- the pinned creation-turn capture. `shim.cfg.cbcapture` with `aiseed=pin +# The seeds run C3 (unpinned, same save, same guest, same build) observed for itself. Pinning +# them reproduces a turn that actually happened rather than inventing one. +aiseed.values=32=e70a4703,496=0c63ca36,512=372be4df`. # # WHY PINNING IS NOT CHEATING. Lane L1 showed every AI client's generator is seeded with a # fresh per-process word, so `turn1-state -> turn2` has an outcome set of size k > 1 and NO @@ -47,3 +50,6 @@ airesearch=off aiorders.words=32 aiorders.deep=on aiseed=pin +# The seeds run C3 (unpinned, same save, same guest, same build) observed for itself. Pinning +# them reproduces a turn that actually happened rather than inventing one. +aiseed.values=32=e70a4703,496=0c63ca36,512=372be4df