CB: the engine-side record of the capture instrument and what it measured

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
This commit is contained in:
alex 2026-09-08 19:22:16 -04:00
parent c2eba45850
commit cd92076b39
2 changed files with 101 additions and 1 deletions

94
docs/CB-capture.md Normal file
View file

@ -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=<N>` | 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=<netid>=<hex>,…` | 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.

View file

@ -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