Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
96 lines
5.8 KiB
Markdown
96 lines
5.8 KiB
Markdown
# 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, 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.
|