Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
181 lines
11 KiB
Markdown
181 lines
11 KiB
Markdown
# CB — predictions for the command-stream capture, written before the module exists
|
||
|
||
Lane CB, 2026-09-08. Worktree `wip/cb` off `main` `4f25f1e` (the brief said `7444c3b`; the
|
||
integrator merged L1 in the meantime, so this branch is based on the newer main and says so).
|
||
Guest **VM146** (`192.168.10.146`). Committed **before** `ai_orders.cpp` is touched and before
|
||
anything is built.
|
||
|
||
Lane RB is building `sots_turn --turn-commands <blocks>`. This lane supplies its input. A replay
|
||
with no real stream is untested and a stream with no autosave to check it against is a log file, so
|
||
every prediction below is either *about the stream* or *about the binding between the stream and
|
||
the autosave of the run that produced it*.
|
||
|
||
---
|
||
|
||
## 0. What is being changed, and why it is small
|
||
|
||
Lane L4 already dumps the block set at `StrategySim::ApplyTurnCommandBatch` — one
|
||
register-transparent entry stub, read-only, measured behaviour-neutral on the canonical pair. This
|
||
lane does **not** rebuild that. It adds three things and nothing else:
|
||
|
||
1. **`aiorders.words=<N>`** — the element window, today a fixed 12 words. List 1 carries a
|
||
polymorphic `ShipDesignDef` whose id sits past 48 bytes, so the turn-1 workload cannot be
|
||
captured completely at 12.
|
||
2. **`aiorders.deep=on`** — follow the heap. Three payloads in the canonical block are behind
|
||
pointers and are therefore *absent* from L4's capture: list 8's route vector, list 10's counted
|
||
vector, and list 23's `Population` body. A replayer cannot reconstruct a fleet move without the
|
||
route. The follower is deliberately **generic** — it scans the element window for anything
|
||
shaped like `{_Myfirst,_Mylast,_Myend}` and for anything shaped like an MSVC `std::string`, and
|
||
dumps the bytes. It does **not** know which list it is looking at.
|
||
3. Nothing in the instrument types an element. L4's design point — *the dump reads element bytes,
|
||
not element types; decoding happens offline so a wrong record shows up as a wrong value instead
|
||
of being baked into the instrument* — is kept, and the typed decode moves to a host-side Python
|
||
step that can be corrected without another VM run.
|
||
|
||
So `aiorders=on aiprobes=off` remains **exactly one MinHook detour**, and the added work inside it
|
||
is guarded reads of memory the game already owns.
|
||
|
||
---
|
||
|
||
## 1. The capture format (defined here because RB may not have landed first)
|
||
|
||
One JSON file per run under `verify/results/turncommands/`. Mechanical, and the raw words are the
|
||
ground truth:
|
||
|
||
* `binding` — the input save and both output autosaves, each with size and sha256, plus whether
|
||
they match the published determinism oracle. **A capture whose `binding.outputs` is absent or
|
||
whose hashes do not match the run is worthless for a byte-match and must be treated as such.**
|
||
* `batches[].blocks[]` — one entry per block slot, `playerId`, the six gates with their payloads,
|
||
and all 27 lists.
|
||
* Each element carries `raw_words` (the window, verbatim), `vectors` (each followed heap vector,
|
||
with its element words), `strings` (each decoded `std::string`), and `decoded` — a named record
|
||
**only where the campaign has one**, with a `record` field naming it and a `wire` array giving
|
||
the values in the order the writer emits them.
|
||
* `decoded` is regenerable from `raw_words` by `tools/turncommands_capture.py`. RB should prefer
|
||
`decoded.wire` and fall back to `raw_words`; if the two ever disagree, `raw_words` wins.
|
||
|
||
The one thing the decoder must get right and that is easy to get wrong: **list 3's in-memory
|
||
element is in the opposite order from its wire record.** Lane L4 read that off the writer
|
||
(`0x00822870` emits `+0x14, +0x10, +0x0c, +0x08`, descending); lists 5, 8, 10, 14 and 23 all write
|
||
ascending. That is per-list, not a rule, and the decoder encodes it as such.
|
||
|
||
---
|
||
|
||
## 2. Runs, and what each is for
|
||
|
||
| run | save | `hooks` | `aiorders` | `watch` | purpose |
|
||
|---|---|---|---|---|---|
|
||
| **C0** | `ref-turn2.sav` | `off` | — | `off` | rule-19 control: the oracle bytes with no instrument at all |
|
||
| **C1** | `ref-turn2.sav` | `trace` | `on`, words=32, deep | `off` | **the deliverable** — canonical pair capture |
|
||
| **C2** | `ref-turn2.sav` | `trace` | `on`, words=32, deep | `on` (modcount) | the trap multiset, as RB's second check |
|
||
| **C3** | `turn1-state.sav` | `trace` | `on`, words=32, deep | `off` | the non-deterministic pair, block + *its own* autosave |
|
||
| **C4** | `turn1-state.sav` | `trace` | `on`, words=32, deep | `off` | the same turn twice |
|
||
|
||
---
|
||
|
||
## 3. Predictions
|
||
|
||
### P1 — C0 reproduces the oracle. (control)
|
||
|
||
`(Autosave EndTurn).sav` = 66,732 B, `bb4fd9ac89f41e3b`; `(Autosave).sav` = 67,219 B,
|
||
`978041acd168b56e`. If this fails, **the guest is not the reference guest** and nothing else in
|
||
this lane means anything; lane L4 already reproduced these on VM145 and lane L5 held VM146, so a
|
||
failure here is a lab fact, not a finding.
|
||
|
||
### P2 — C1's autosaves are byte-identical to C0's, so the capture binds.
|
||
|
||
The deliverable stands or falls on this. Lane L4 ran **seventeen** detours on this same workload
|
||
and got the oracle bytes; this run has **one**, doing strictly more reads inside it. If C1's
|
||
autosaves differ from C0's, the capture is a stream that changed the turn it recorded, it is
|
||
unusable for a byte-match, and the report must lead with that (rule 19).
|
||
|
||
### P3 — the block set on the canonical pair is exactly lane L4's, plus the three payloads.
|
||
|
||
Two batches: `seq=1` at load (`n=1`, pid 16, all 27 lists empty) and `seq=2` at End Turn (`n=8`).
|
||
Slots 0–3 are pids **16, 32, 496, 512**; slots 4–7 are pid **0** with uninitialised gate payloads.
|
||
Only pid 32 is non-empty, with list sizes `3:1, 5:1, 8:1, 10:1, 14:2, 23:1`. Every rate gate on the
|
||
four real players is **set**; no research-target gate is set on any player this turn.
|
||
|
||
New, and the reason for the run:
|
||
|
||
* **P3a — list 8's route vector has count 1.** Its single value is the destination system id.
|
||
L4 could not read it and noted that `turn3-state.sav`'s waypoint says **272**. Predict **272**.
|
||
*Falsifier:* any other id, in which case the waypoint in the output save is not the route in the
|
||
command and the replayer has to carry both.
|
||
* **P3b — list 10's counted vector has count 1.** L4's reading is "at system 288, fleet 34, [one
|
||
object]". Predict the value is a **ship id that already exists in `ref-turn2.sav`** — the
|
||
pre-existing ship being folded into the newly allocated fleet 34. *Falsifier:* a small
|
||
client-allocated id in the 18/34 space, which would mean the client allocates the ship too and
|
||
the id-allocation hole L4 left open is wider than one counter.
|
||
* **P3c — list 23's `Population` vector is 24 bytes, i.e. six words**, at a stride the dump does
|
||
not name. Predict the six words are **not all zero** and that they **differ between the turn-2
|
||
and turn-1 captures** (L4 saw the element's trailing int go `+1` on turn 1 and `−1` on turn 2, so
|
||
the body is turn-dependent and not a constant the replayer can ignore).
|
||
|
||
### P4 — C2's trap multiset is exactly ten bumps, and they attribute.
|
||
|
||
The paying half of pid 32's block is `3:1 + 5:1 + 8:1 + 10:1 + 14:2` = **6 elements**, list 23
|
||
being free; plus **4** research-rate gates, one per submitted block, set unconditionally. Total
|
||
**10** — which is the number lane W2 measured on this turn from the other end, before anyone had
|
||
seen the block. Predicted per-EIP:
|
||
|
||
| trap EIP | count | what |
|
||
|---|---:|---|
|
||
| `0x00821a87` | 4 | prologue gate `+0x0c`, research rate, one per submitted block |
|
||
| `0x0084946e` | 1 | list 3, build |
|
||
| `0x0086c3e9` | 1 | list 5, system rates |
|
||
| `0x0088bf01` | 1 | list 10 |
|
||
| inlined at `ApplyTurnCommandBatch+0xabc` | 2 | list 14, the AI's two `(fleet, mode)` elements |
|
||
| list 8's site | 1 | fleet move |
|
||
|
||
*Falsifier:* any total other than 10, or any bump attributed to a list this capture reports empty.
|
||
That would mean the counter is not the length of the command stream and the whole
|
||
`ModCount`-from-the-block claim in `src/game/ai/orders.h` is wrong.
|
||
|
||
*Rule 19 for C2 specifically:* the watchpoint module has never been run in the same process as the
|
||
block dump. If C2's autosaves move off the oracle, C2 is reported as perturbing and **C1 remains
|
||
the deliverable** — the trap set is a bonus check, not the capture.
|
||
|
||
### P5 — C3 and C4 differ in exactly one word of the stream.
|
||
|
||
Both from `turn1-state.sav`. Predicted block: pid 32 with lists `1:1, 3:1, 5:1, 23:1`, three
|
||
research-**target** gates set (pids 32, 496, 512) and the human's clear, four rate gates set.
|
||
|
||
The prediction that matters: **C3's and C4's block sets are byte-identical except for player 512's
|
||
research-target payload at `block+0x10`**, and each run's autosave differs from the other's only in
|
||
`p512.ResTNm` and the derived checksum. That is Rung B's claim in its sharpest form — if the two
|
||
streams differ *only* where the two autosaves differ, then the sim is a function of the stream and
|
||
the decision layer is the only thing that is not.
|
||
|
||
*Falsifiers, and each is worth more than the prediction:*
|
||
* *The two blocks are identical and the two autosaves differ.* Then the stream does **not**
|
||
determine the save, Rung B is not reachable by replay alone, and the divergence is downstream of
|
||
the command — which contradicts L4 §3.1, which saw it in the block.
|
||
* *The two blocks differ somewhere else as well* (an id, an ordinal, a route). Then the
|
||
client-allocated id space is also per-process and a replay has to carry ids it cannot derive.
|
||
* *Player 512's target lands on a tech outside the six-member `XNC` set L4 named.* Then k is larger
|
||
than six and Rung C's tie-set claim has to start one level up. `BIO_GnMod` has already been
|
||
observed once, so this is not unlikely; a second `BIO` value would settle that the resolver
|
||
leaves the family.
|
||
|
||
### P6 — list 1's design element yields its id and its name at words=32.
|
||
|
||
`"Honor Lance"` is a short-string-optimised `std::string` inside the element, and design id **18**
|
||
is named by list 3 in the same block. Predict the widened window shows the string *and* a word
|
||
equal to **18** inside the list-1 element. *Falsifier:* no 18 anywhere in 128 bytes, in which case
|
||
L4 §1 P3 stands as it is — the design command does not carry its own id and the id is inferred.
|
||
|
||
### P7 — what this lane will **not** be able to say.
|
||
|
||
Stated in advance so a green capture is not read as more than it is (rule 15):
|
||
|
||
* Two turns, one AI empire with anything to do, a board with no contact. Lists 2, 4, 6, 7, 9,
|
||
11–22, 24–27 will be **empty in every block of every run**, exactly as they have been in all
|
||
eleven corpus saves and both of L4's runs. The capture exercises **six** of twenty-seven lists
|
||
and **one** of six gates on the canonical pair, two of six on turn 1. Everything the replayer
|
||
does with the other twenty-one lists is untested by this lane and must be labelled as such.
|
||
* The generic vector follower can produce a **false positive** — three consecutive words that
|
||
happen to look like a vector. The count bound and the readability probe make that unlikely, not
|
||
impossible. Any `aivec` line on a list whose record has no vector is to be read as noise until a
|
||
second run reproduces it.
|
||
* Nothing here says what list 10 or list 23 *mean*. It says what bytes they carry.
|