sots-re/findings/subsystems/trade-and-spy-workload.md
alex 7d51767e87 lane L3: both containers filled, and filling them does not make the tail draw
Three lanes could not build this workload. VM144 built both halves from a cold main
menu on a stock, unmodified install -- no data file touched, no save edited -- and the
answer to the question they were built to ask is no.

TRADE. The premise everyone carried, that trade needs station construction plus its
tech, is wrong about the station: StrategyVars.txt makes a trade station a +2-routes,
+25%-income bonus, and NumTradeRoutesSupported has a floor of 1 per owned system, so
the population thresholds scale the count and do not gate it. The gate is one tech,
CCC_FTLEcon, and Zuul cannot research it at all. 20 routes and 6 trade sectors by
turn 4; 21 by turn 13.

SPY. Lane W3: "no lane has identified which UI produces a spy-program entry." It is
the fleet panel's Special menu, and the reason nobody found it is that the ship a spy
docks with is the Cruiser REPAIR AND SALVAGE mission section -- nothing in the UI
calls it a tender or mentions spies. Prediction S1 holds: the container grows at Build
Spy while the craft is still docked (tdep=-1, atto=<tender>), so no enemy colony and no
asteroid belt are on the critical path, which is what every earlier cost model assumed.

THE MEASUREMENT. With 21 routes, 6 sectors and 1 spy craft live, lane H's eleven entry
probes report the four outer tail callees entered exactly once per turn, as always, and
SpyManager::Slot13RngCallee, TradeManager::Slot13RngCalleeA and CalleeB entered ZERO
times -- this turn and across all seven instrumented turns, every probe installed=true.
The RNG ledger and the standalone's generator model do NOT need extending on account of
either container being non-empty. The next condition is active contents, not present
contents: tsnumflt=0 on every sector and spyon=0 on the spy.

WHAT ELSE FELL OUT. The trade-route vector is torn down and rebuilt every turn inside
ServerTradeManagerImpl::vslot9 (lane V2's phase-23 call 5 of 8, previously "draw-free"
and unexplained) -- an agreement, dropped rather than re-minted, as is the AI2 growth
helper. Two addresses are minted, both trapped live and both `mapped` not `verified`
because ReVa was unreachable: ShipAction_BuildSpy 0x00789620, a sibling of lane B6's
stack-built-fptr-table entry 0x120 bytes away, which is why no call-graph sweep ever
found this writer; and ServerSpyManager_CreateSpyCraft 0x008383c0.

And a correction with teeth: `spies2` is 0 in all 28 systems of a save that DOES have a
spy. The natural reading of lane W's count-0-in-11-saves was "no save has ever had one".
That reading is dead; spies2 is not the spy list, or it only fills for a deployed spy.

The research click path is written down for the first time (§4): clicking a tech node is
a PREVIEW, the bottom-left button confirms, Esc leaves without confirming, and the tree
pans only on a right-drag of about 600 px. That, not the tech cost, is what stopped
three lanes.

Saves: human-turn{5,8}-traderoutes, human-turn11-spytechs, human-turn15-spyprogram --
all --strict clean, 0 errors, 0 warnings.
2026-09-08 19:46:16 -04:00

444 lines
24 KiB
Markdown

# The trade-route and spy-program workload
Lane L3, 2026-09-08. Guest **VM144** (`sots-re-win10-144`, `re@192.168.10.144`, a ZFS clone of
VM140). Shim builds `l3cont-0117495-20260908T2107Z` and `l3cont2-0117495-20260908T2124Z`.
Three lanes failed to build this workload. This one built **both halves**, from a cold main menu,
in one session on a stock unmodified install. The headline result is nevertheless a **negative**
one, and it retires lane W3's open risk rather than confirming it.
---
## 0. Summary
| question | answer |
|---|---|
| Can the trade-route vector be made non-empty? | **Yes.** 20 routes, 6 trade sectors, from turn 4 of a stock custom game; 21 by turn 13. |
| Can the spy-program vector be made non-empty? | **Yes.** `count=1` on turn 14, from the fleet panel's `Special` -> `Build Spy`. |
| Do saves now exist? | **Yes.** Four, all `--strict` clean: `human-turn5-traderoutes.sav`, `human-turn8-traderoutes.sav`, `human-turn11-spytechs.sav`, **`human-turn15-spyprogram.sav`** (both containers non-empty). |
| With 21 routes **and** a spy program live, do the four tail callees draw RNG? | **No.** All three inner RNG-bearing callees are entered **0** times, this turn and cumulatively across 7 instrumented turns. |
| Does the RNG ledger / standalone generator model need extending? | **No -- not for either container being non-empty.** The gate is deeper than "the container is empty", and §3.5 names the next condition. |
---
## 1. Why the previous lanes' cost estimate was wrong
`tail-probes.md` §3.2 and every brief since carried this premise: *trade income needs
trade-station construction plus its tech, which from `ref-turn2` is tens of 30-60 s End Turns.*
Both halves of that are wrong, and the second one is wrong for a reason worth stating.
**A trade station is not a prerequisite.** Read out of `Data/Strategy/StrategyVars.txt` (inside
`sots.gob`, which is a plain ZIP):
```
STATION_BONUS_TRADE_INCOME 0.25 // Increases value of system trade routes.
STATION_TRADE_NUMROUTES 2 // Routes possible with trade station.
```
A station *adds* two routes and a 25 % income multiplier. The gate is one tech, `CCC_FTLEcon`,
which sets `ServerPlayer+0xff` (`CnTrd`); `StrategyServer::RegisterTradeSystems 0x007adc80` then
registers `ServerSystem::NumTradeRoutesSupported 0x00819d20` slots for every owned system, and
that function has a **floor of 1**. The population thresholds
```
TRADE_ROUTE_REQ_CIVPOPULATION 200000000
TRADE_ROUTE_REQ_IMPPOPULATION 500000000
```
scale the count; they do not gate it. And they were never close to binding anyway: the
reference game's own homeworld at turn 2 already reads `Civilian 520,000,000 /
Imperial 2,000,000,000`, i.e. 3 + 4 = 7 route slots from the homeworld alone.
**The real cost was the tech, and the real obstacle was the research UI.** See §4.
---
## 2. The recipe that worked, end to end
Stock game, stock data files. **Nothing in `sots.gob` or any loose data file was modified**, and no
save was edited; the resulting saves are legitimate corpus members.
1. Main menu -> `Single Player` (512,449) -> **`Custom`** (445,357) -> `OK` (551,451).
2. On **Custom Game Setup** the sliders respond to a plain click on the track. The ones that
matter, and what previous lanes did not know existed:
* `Initial Colonies` **10** (max) - ten developed systems at turn 1;
* `Initial Treasury` **9,850,000** (max);
* `Initial Technologies` **15** (max) - random techs, and the roll included `CCC_FTLBrdB`,
which is the parent of both `CCC_FTLEcon` and `CCC_SpyBm`;
* `Research Efficiency` **146 %** (max), `Economic Efficiency` **148 %** (max);
* `Number of Stars` 28, `Distance` **5 ly** (min) - so systems fall inside one
`TRADE_SECTOR_SIZE 10.0` cell;
* species **Human** (`CCC_FTLEcon` is `Zuul:0` in `MasterTechList.tech` - a Zuul game
cannot do this at all), 2 players.
Player species defaults to **Random**: set it explicitly on the launch screen via the card's
`setup` button (359,486) -> `Select Species` dropdown (557,117).
3. Research `CCC_FTLEcon`. **One turn** at these settings.
4. End Turn. The routes appear on the *next* End Turn's arming.
Turn 4 -> 5 was the first End Turn with a non-empty container. Total elapsed from a cold main
menu: about 50 minutes, most of it spent on §4.
---
## 3. The measurement
Instrument: `watch.mode=cont`, a third mode added to lane W3's watchpoint module
(`src/shim/hooks/watchpoints.cpp`). Same single arming detour on
`StrategyServer::ApplyAllTurnCommands` (RVA `0x0038f6a0`), same VEH, same canary. Two changes:
all four debug slots go on the two ends of the two containers (`_Myfirst` **and** `_Mylast`, so an
allocation and an append are separable), and it **re-arms and re-logs on every End Turn** instead
of only the first.
### 3.1 The control (rule 19)
`shim.cfg.l3cont` and `shim.cfg.l3control` differ in exactly one key, `watch=`. Both were run on
`ref-turn2.sav`, one End Turn each, on VM144:
| run | `(Autosave EndTurn).sav` | `(Autosave).sav` |
|---|---|---|
| `watch=off` | `bb4fd9ac89f41e3b…` | `978041acd168b56e…` |
| `watch=on` | `bb4fd9ac89f41e3b…` | `978041acd168b56e…` |
| published oracle | `bb4fd9ac89f41e3b…` | `978041acd168b56e…` |
Byte-neutral, and the clone reproduces VM140's determinism oracle exactly - which is also the
adoption check for VM144.
The armed control run reproduced lane W3's numbers through a different arming path:
```
watch: trade routes -- manager=0x0d812270 (S+4+0x154) vector@mgr+0x3c first=0 last=0 end=0 count=0
watch: spy programs -- manager=0x0d82da80 (S+4+0x158) vector@mgr+0x10 first=0 last=0 end=0 count=0
watch: SELFTEST canary writes=1 traps=1 dr7=0xdddd0055 PASS
```
The canary passes on **every** turn, not just the first (the counter is now read as a delta).
### 3.2 The workload
```
turn 1->2 trade routes count=0 spy programs count=0
turn 2->3 trade routes count=0 spy programs count=0
turn 3->4 trade routes count=0 spy programs count=0
turn 4->5 trade routes first=0x33288560 last=0x332885b0 end=0x332885d0 count=20
spy programs count=0
turn 8->9 trade routes count=20 (reloaded from the save - the routes survive save/load)
```
`_Myfirst`, `_Mylast` and `_Myend` all moved off zero together on the same turn, which is the
first allocation of a default-constructed vector - exactly the transition lane W3 said it could
not distinguish from an emptied one.
### 3.3 Who fills it, measured not read
Debug-register traps, exe base `0x006b0000` (ASLR delta `0x2b0000`), 40 hits on the reload turn:
| slot | writer VA | owning function | x | returns to |
|---|---|---|---|---|
| trade `_Mylast` | `0x00841e57` | `FUN_00841cd0 +0x187` | 20 | `0x00868092` (inside `0x00868060`) |
| trade `_Mylast` | `0x00868328` | `FUN_00868060 +0x2c8` | 20 | `0x007d97df` |
`0x007d97df` is the return address of the call at **`0x007d97dd`**, and lane V2 already named that
site: `OnAllCombatDone_Tail` **phase 23, call 5 of 8**, target
`ServerTradeManagerImpl_vslot9 = 0x00868060` (vftable `0x00a31b74` slot 9). Lane V2 recorded that
slot as "closure 185 functions, draw-free" and did not know what it was for.
> **`ServerTradeManagerImpl::vslot9` is the trade-route builder, and it tears the vector down and
> rebuilds it from scratch every turn**: 20 `_Mylast` writes from a helper called at `+0x32`
> (the clear) and 20 from the body at `+0x2c8` (the refill), on a turn whose count is 20 both
> before and after. On the first turn the routes existed, the growth helper `0x00483410` (already
> named by lane AI2, "vector<T> too long") appears in the trap set as well, because that was the
> allocation.
This is an **agreement**, not a new address: `0x00868060` is already `lane-v2.json`'s
`ServerTradeManagerImpl_vslot9` and `0x00483410` is already `lane-ai2.json`'s growth helper, so
both are dropped rather than re-minted. ReVa/CT111 was unreachable this session
(`ReVa (CONNECTION_CLOSED)`), so no body was read in Ghidra and nothing beyond the trap is claimed.
---
### 3.4 The spy program
The spy half was built too, on the same game, four turns later. Full click path in §5. The
container transition, from the same instrument:
```
turn 12->13 spy programs count=0 (tender built, Build Spy ordered)
turn 13->14 spy programs count=0 (arming is at ApplyAllTurnCommands ENTRY, before the
order is applied -- the count lags the order by one turn)
turn 14->15 spy programs first=0x3f025ad0 last=0x3f025ad4 end=0x3f025ad4 count=1
```
`_Myfirst`, `_Mylast` and `_Myend` again moved off zero together: one element, first allocation.
> **Prediction S1 holds.** `ServerSpyManager`'s vector is the spy-**craft** list and it grows at
> **Build Spy**, while the craft is still docked to its tender - `tdep = -1`, `spyon = 0`,
> `atto = <tender ship id>` in the save. **No second empire, no enemy colony and no asteroid belt
> are needed to make this container non-empty.** Every earlier lane's cost model for the spy half
> assumed Deploy; Deploy is not on the critical path.
**Who writes it, trapped live.** The single allocation arrived on the turn thread as one chain:
```
0x00789620 +0x53 -> 0x008383c0 +0x9c -> 0x0059f1a0 +0x5f -> 0x00483410 +0x7c/+0x82
```
`0x00483410` is lane AI2's already-named vector growth helper and `0x0059f1a0` is the push_back
wrapper lane AI3 already uses by address. The two new ones are recorded in
`ghidra/addresses.d/lane-l3.json` as **`ShipAction_BuildSpy 0x00789620`** and
**`ServerSpyManager_CreateSpyCraft 0x008383c0`**, `status: mapped` rather than `verified` because
ReVa/CT111 was unreachable this session and no body was read.
`0x00789620` sits **0x120 bytes after** lane B6's `ShipBorneBuildQueue_ProcessTurn 0x00789500`,
which lane B6 flagged as a new indirection class: zero call sites, no vtable slot, its address
written into a **stack-built function-pointer table** by the ship-action dispatcher. `Build Spy`
is `SHIPACTION_BUILDSPY`, so this is that table's sibling entry - which is why no call-graph
sweep ever found the spy container's writer.
### 3.5 Do the tail callees draw now? **No, on either container.**
Lane H's eleven register-transparent entry probes, `shim.cfg.l3probe` (lane H's `shim.cfg.hp11`
verbatim plus the three `watch.*` keys, so the counts are comparable line for line). Seven
instrumented End Turns, the last of which had **21 trade routes, 6 trade sectors and 1 spy
program** live:
| probe | calls, last turn | since launch (7 turns) | lane H baseline (empty containers) |
|---|---|---|---|
| `ServerSpyManager::vslot13` | 1 | 7 | 1 |
| `ServerSpyManager::vslot14` | 1 | 7 | 1 |
| `ServerTradeManagerImpl::vslot13` | 1 | 7 | 1 |
| `ServerTradeManagerImpl::vslot15` | 1 | 7 | 1 |
| **`SpyManager::Slot13RngCallee`** | **0** | **0** | 0 |
| **`TradeManager::Slot13RngCalleeA`** | **0** | **0** | 0 |
| **`TradeManager::Slot13RngCalleeB`** | **0** | **0** | 0 |
| `ServerTradeManager::CreateRaidEncounter` | 0 | 5 | 1 |
| `ServerTradeManager::GenerateTradeRaidEncounters` | 1 | 7 | 1 |
| `ServerSpyManager::vslot15` [control] | 1 | 7 | 1 |
| `ServerTradeManagerImpl::vslot14` [control] | 1 | 7 | 1 |
Every probe reports `installed=true`, so these are measured zeros, not MinHook failures.
> **Filling either container is not sufficient to make the tail draw.** The RNG ledger and the
> standalone's generator model do **not** need extending on account of the trade-route or the
> spy-program vector being non-empty. Lane W3's open risk is retired, in the direction nobody
> expected - and it is retired by evidence, not by the absence of it.
**The next condition, and it is now cheap to test.** The containers are non-empty but their
*contents are inert*, and the saves say so precisely:
* every trade sector has `tsnumflt = 0` - **no freighters in service**. `TRADE_ROUTE_STARTUP_TURNS`
is 3, `TRADE_ROUTE_MAX_FREIGHTERS` 5, and `TRADE_RAID_ODDS_PLAYER` 0.07: a raid roll needs
something to raid. (`CreateRaidEncounter` was entered 5 times in 7 turns with, on lane H's
reading, an empty candidate list.)
* the one spy has `tdep = -1` and `spyon = 0` - **built but never deployed**. `vslot13`/`vslot14`
loop over the spy list; the per-element body almost certainly wants a spy that is *at* a system.
So the hypothesis for the next lane is **active contents, not present contents**: build freighters
from the sector summary's `Build Freighter` button, and Deploy the spy at an enemy system with an
asteroid belt (`STARMAP_ASTEROIDBELT_ODDS 25` %), then re-run `shim.cfg.l3probe`. Rule 20 applies
to this paragraph as much as to anything: the table above separates "vslot15 did not run" from
"vslot15 ran and found nothing", and it says the latter.
## 4. The research click path - the thing that actually cost the day
No lane had written this down, and it is why three lanes could not get a tech. Lane O saw the
bottom-left button on the Research screen and recorded it as *"`Cancel research`, NOT back"*. It
is both, depending on state, and when nothing is being researched it is the **confirm**:
```
map: Research (55,124)
navigate to the tech (see below)
click the tech node -> the bottom bar previews "<tech>, N turns to complete."
THIS DOES NOT ASSIGN ANYTHING
click the bottom-left button (91,715)
Confirm Research dialog -> OK (~(595,393); the dialog is centred, read it off a screenshot)
leave with the bottom-right circle (999,715) -- NOT Esc
```
Three traps, each of which cost a run:
* **A click on a tech node is a preview, not an assignment.** The bottom bar says
"N turns to complete" either way. The map's ticker keeps saying `No Research Project Assigned.`
and savings keep growing by the full income - that is the tell.
* **Esc leaves the screen but does not confirm**, and the previewed selection is lost. Use the
bottom-right circle.
* Once a project *is* running, the top-of-screen `Savings <-> Research` slider does something
visible (clicking its right end took `FTL Economics` from "2 turns" to "1 turn"), and the
in-progress tech shows as an extra icon next to the species portrait on the map.
**Navigating the tree.** The categories sit on a rotating sphere, the labels do not line up with
the branches drawn above them, and the campaign's click helper had no drag verb, which is why
earlier attempts read as "the screen ignores input".
* **Right-button drag pans; left-drag and edge-scroll do nothing.** A `drag`/`rdrag`/`sdrag`/
`wheel` verb was added to `C:\SOTS\ui\click_helper.ps1` on VM144 (backup at
`click_helper.ps1.preL3`); it is additive and existing verbs are untouched.
* A **~600 px horizontal right-drag is one category step**; the response is non-linear, so shorter
drags fling several categories and are unusable. One drag per `cmd.txt`, screenshot between.
Batched drags drop about half, exactly like batched clicks.
* Dragging **left** moves the camera **right**. The order wraps:
`… Ballistic, C3, Xenotech, Industrial, Biological, Drone, Star Drives, Warhead, Shield, Power,
Torpedo, Energy, C3 …`
* The wheel zooms but **re-centres the camera somewhere else**, so zoom is not usable for
targeting. Work at the zoom the screen opens at.
* **Double-clicking a node** opens a large readable detail card for it (title, description,
"N turns to complete") with its children drawn below - the one genuinely useful navigation aid.
* The tree only draws the researched frontier plus one step. A tech whose parent is unresearched
is not on screen at all, which is why the Industrial branch looked like two nodes.
---
## 5. The spy path, start to finish
Lane W3: *"no lane has identified which UI produces a spy-program entry."* It is the fleet panel's
**`Special`** button, and here is the whole thing. Every precondition was read out of the shipped
data files and the binary's own refusal strings first, then executed.
**The ability gate.** `Species/<race>/sections/_Spy.shipsection`:
```
entity_class "SpyShip" section_type mission section_class destroyer
nodesign 1 spy 1 cost 20000
requires CCC_SpyBm
requires IND_SlvgTech
```
`nodesign 1` means the spy craft is never designed and never appears in a build queue - it is
produced by a **ship action**, which is why looking for it in the Design or Build screens finds
nothing.
**The tender.** `Can't build spy: Fleet does not have a ship spy can dock with.` The ship needs a
section carrying `spytender`, and in this image there is exactly one per race:
> **`CRRepairandSalvage` - the Cruiser "Repair and Salvage" mission section** (`requires
> IND_SlvgTech`, cost 60,000). Nothing in the UI calls it a tender, and nothing links it to spies.
> This is the single fact that makes the whole path findable.
**The tech chain** (RP from `TechTree/MasterTechList.tech`); at this game's settings each of these
completed in **one turn**:
```
CCC_ROOT -> CCC_FTLBrdB (4,000) -> CCC_SpyBm (12,000) [spy craft half]
IND_OrbFound -> IND_CruisCon (16,000) -> IND_SlvgTech (12,000) [tender + spy craft half]
```
`CCC_FTLBrdB` is shared with the trade chain, and the `Initial Technologies 15` roll supplied it.
**The click path, executed on turns 11-14 at 1024x768:**
1. **Design** (142,124). Click **`Cruiser`** in the left list (104,267) to switch hull class. The
design has three slots - Command / Mission / Engine - each with `<` `>` arrows; click the
Mission slot's right arrow (710,483) until the title reads **`Repair and Salvage*`** (five
clicks from `Armor`, through `Medium Defense Platform`). Bottom-left **`Save Design`**
(135,715) -> name prompt -> `OK`.
2. **Build** (221,124) -> **`Cruiser`** (104,276) -> the `Repair and Salvage` row (74,130) ->
**`Add To Queue`** (125,715). ETA 2 turns at a developed homeworld.
3. Two End Turns. The fleet panel then lists `Alpha Fleet* / CR Repair and Salvage 9/9` and the
`Manage Fleets` / `Move` / **`Special`** buttons light up.
4. **`Special`** (229,464) -> the menu is `Repair / Scuttle / `**`Build Spy`**` / Auto-repair /
Flag As Guard`. `Build Spy` at (277,517).
5. Dialog **`Build Spy At <system>?` / "Select ships for spy craft to dock with:"** -> `Select All`
(361,566) -> `OK` (599,566). The ship's row gains a leading `*` and a spy glyph: the order is
queued, not yet executed.
6. End Turn - the order is applied. **End Turn again** and the container reads `count=1`; the
arming point is `ApplyAllTurnCommands` *entry*, so the count lags the order by one turn.
**Everything the engine will refuse, from its own strings** (none of these bit, but they are the
map):
* build - your own system, player has both techs, the fleet contains a tender, that ship is not
already carrying a spy and is not busy;
* deploy - **not your own system**, no enemy fleet present ("You gotta fight 'em."), no spy already
there, and `Can't deploy spy: Spy requires an asteroid belt to hide.`
(`SPYSHIP_NEARBY_ASTEROID_RANGE 500.0`; `STARMAP_ASTEROIDBELT_ODDS 25` %).
**Deploy was not done** - it needs a fleet move to an enemy system with an asteroid belt, and §3.5
says it is the most likely next gate for the spy RNG callees. It is the one step of this subsystem
still unexercised.
**What the save now proves about the wire format.** `spymgr` is populated for the first time in
the corpus:
```
spymgr { xsid 2, nspy 1,
spy { sid 1, sown 16, atto 3728, deat 0, tdep -1, cbh 0,
sdo 0, sdet -1, spyon 0, spyat 0, cm 0, cmo 0, ncp 0 } }
```
`sown 16` is the player id of `re`; `atto 3728` is the tender's ship id; `tdep -1` is "never
deployed". Every member `layouts.md` lists for `Game::SpyCraft` is present, which confirms that
record - but note that **the on-the-wire order is not the memory order**: `sdo` and `sdet`
(memory `+0x3c`, `+0x40`) are written *between* `cbh` and `spyon`, not last. And `ncp` (memory
`+0x2c`, typed as a `0x10` vector) serialises here as a bare count of 0, so an empty vector member
is indistinguishable on the wire from a scalar zero until a save has a non-empty one.
---
## 6. Artifacts
| what | where |
|---|---|
| turn-5 save, the first trade routes in the corpus | `verify/results/saves/human-turn5-traderoutes.sav` (72,204 B, `--strict` clean, 0 issues) |
| turn-8 save, routes matured, one sector secure | `verify/results/saves/human-turn8-traderoutes.sav` (76,554 B) |
| turn-11 save, all four spy techs researched, no tender yet | `verify/results/saves/human-turn11-spytechs.sav` (81,702 B) |
| **turn-15 save, BOTH containers non-empty** - 21 trade routes, 6 sectors, 1 spy craft | **`verify/results/saves/human-turn15-spyprogram.sav`** (87,736 B, `--strict` clean, 0 issues) |
| probe-run trace, 7 End Turns of entry counts + RNG ledger | `verify/traces/l3-probe-turn8-turn15.jsonl` |
| probe-run shim logs and watch hits | `verify/results/shim/l3/l3-probe-run-{shim.log,watch.txt}`, `l3-spy-run-{shim.log,watch.txt}` |
| the two addresses this lane mints | `ghidra/addresses.d/lane-l3.json` (`ShipAction_BuildSpy`, `ServerSpyManager_CreateSpyCraft`) |
| the instrument | `sots-engine` `src/shim/hooks/watchpoints.cpp` (`watch.mode=cont`), `src/shim/shim.cfg.l3cont`, `.l3control`, `.l3probe` |
| predictions written before the build (rule 2) | `sots-engine` `docs/L3-predictions.md` |
**What the trade saves contain that no earlier save does.** `trdmgr` is populated:
`NumTradeSectors 6`, `SctSize 8.0`, and six `TradeID`/`Trade` records each with `tsgridID`,
`tsctr`, `tssec`/`tsct`/`tscr`, `ptssec`/`ptsct`/`ptscr`, an `fwarn` vector, `tsnumsys` and its
`tssys` member list, and `tsnumflt`. In `turn2-state.sav` the same `trdmgr` block holds
`NumTradeSectors` and nothing else. `spymgr` likewise goes from `xsid`/`nspy 0` to a populated
`spy` record (§5).
**`spies2` is still 0 in all 28 systems even so.** Lane W flagged `spies2` / `SysMem` / `mts` as
hypotheses with count 0 in all 11 saves, and the natural reading was "no save has ever had a spy".
That reading is now falsified: this save *has* a spy, in `spymgr`, and `spies2` is still empty in
every system. **`spies2` is therefore not the spy list** - it is something else, or it only fills
for a *deployed* spy. Whoever picks that up should start from the turn-15 save rather than from
the assumption.
**An engine defect found and fixed.** `hooks=off` returned from `Shim_Init` before
`install_watchpoints`, so `hooks=off watch=on` printed `watch=on` in the banner and armed
absolutely nothing - a config that would have reported a confident zero. `src/shim/main.cpp` now
installs the watchpoint module (and MinHook) on the `hooks=off` path when `watch=on`. This
matters for any long play session: the template hooks cost 30-45 s per End Turn and measure
nothing a container watch is asking about.
---
## 7. VM144 as left
Main menu, profile `re`, screenshot-verified. Shim build `l3cont2-0117495-20260908T2124Z` from
`C:\SOTS\shimdist-l3`; `C:\SOTS\shim.cfg` is a copy of `shim.cfg.l3probe`. Original config saved
at `C:\SOTS\ui\preL3-shim.cfg`.
**`C:\SOTS\ui\click_helper.ps1` has four new verbs** and its pre-lane backup is
`click_helper.ps1.preL3`. The additions are purely additive - every existing verb is byte-for-byte
unchanged - and they are what made the research and design screens drivable at all:
```
drag x1 y1 x2 y2 left-button drag
rdrag x1 y1 x2 y2 RIGHT-button drag, 30 steps <- the only thing that pans the tech tree
sdrag x1 y1 x2 y2 slow left drag, 40 steps
wheel x y delta [n] n wheel notches (delta may be negative)
```
`SavedGames` gained four files (`MyGamel3tradeturn{5-traderoutes,8-tr,11-spytechs,15-spy}.sav`)
plus the game's three autosaves, so **every Load-dialog row position recorded by earlier lanes has
moved on this guest**. Screenshot the dialog; do not reuse a remembered row. The repo copies of
those four saves are the canonical ones.
Also left in place, all harmless: `C:\SOTS\shimdist-l3\`, `C:\SOTS\l3data\` (the config text files
extracted from `sots.gob`), and `C:\SOTS\ui\l3-*.ps1` (the gob extractor, the click-helper
patchers, a display-mode lister).
**Guest adoption notes for the next clone.** VM144 came up with no IPv4 in the host's ARP cache;
it had one all along (`192.168.10.144`, DHCP from 192.168.0.1, /16). Two things worth knowing:
`ssh re@<addr>` works directly from the dev box, and when the IPv4 address is genuinely unknown,
**`ssh -6 re@fe80::…%vmbr0` from the PVE host works** - the link-local address is in
`ip neigh` on `spicy` even when the v4 one is not, and Windows answers SSH on it while dropping
ICMP. That is a faster route in than a console screendump. The clone reproduces VM140's
determinism oracle byte for byte (§3.1), which is the adoption check worth running first.