Lane L3 needed to watch the trade-route and spy-program vectors across a whole game played forward, not one turn of one save. `modcount` and `tshn` arm once on purpose -- their targets are picked from one turn's state and re-picking them would move the measurement -- so this is a third mode rather than a change to either. `cont` puts all four debug slots on the two containers, `_Myfirst` as well as `_Mylast`. Both vectors are default-constructed with all three pointers zero, so the first element writes all three: watching only `_Mylast` cannot separate "allocated for the first time" from "appended to an existing buffer", and those are different events in the model this lane set out to falsify. It re-arms and re-logs on every End Turn, and the canary self-test's counter is therefore read as a delta -- on the arm-once modes the delta is the old value, so their log lines are byte-identical. `ReportContainer` is factored out of `ArmTshnSlots` so both modes emit the same container line. Lane W3's published count=0 is the control every later count is compared against, and a reformatted line would have made that comparison a judgement call. The defect: `Shim_Init` returned before `install_watchpoints` whenever the trace mode was `off`, so `hooks=off watch=on` printed `watch=on` in the banner and armed absolutely nothing -- a config that reports a confident zero, which is the failure method rule 1 exists to catch. The watchpoints are an independent instrument with their own arming detour and no trace records, and a long play session wants them without paying 30-45 s per End Turn for template hooks that measure nothing it is asking about. MinHook is now initialised and the module installed on the `hooks=off` path when `watch=on`. Configs: shim.cfg.l3cont / .l3control differ in exactly one key for rule 19; .l3probe is lane H's hp11 verbatim plus the three watch keys, so the entry counts stay comparable to lane H's empty-container baseline line for line. Gates: clean_room_check OK; host ctest 54/54; CT111 shim cross-build OK, exports 66 names identical. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ARBgSooAfokKUy6wKUKEyZ
6.2 KiB
W3 — a second watch mode, and what the two runs answered
Companion to docs/W3-predictions.md (written and committed before the build) and to
sots-re/findings/subsystems/nvo-tshn-visible-owner.md +
sots-re/findings/control-flow/w3-containers-status-handlers.md (the full reports).
Builds on lane W2's module without changing how it works.
What was added
watch.mode=modcount|tshn in src/shim/hooks/watchpoints.{h,cpp}. No second hook, no second
detour, no change to the VEH or to the arming mechanism — only different arithmetic on the S
the existing ApplyAllTurnCommands detour already holds. modcount is W2's set and stays the
default so its run remains reproducible; tshn arms:
| slot | target |
|---|---|
| 0 | the TShn word of an NVO map record on a system with AFlags == 0 |
| 1 | that map's _Mysize |
| 2 | trade-route vector _Mylast (*(S+4+0x154) + 0x40) |
| 3 | spy-program vector _Mylast (*(S+4+0x158) + 0x14) |
Two things about the tshn arming are worth keeping if the module is extended again:
- The target is chosen by predicate at arm time, not hard-coded, and all 28 systems are logged
with name,
AFlagsand both map sizes. A hard-coded pointer that lands on the wrong object produces the same empty hit list as "nothing writes this" (method rule 1). - The
ServerSystembase is probed, not assumed. Two published accounts disagreed by 8 about where theNVOmap lives, becauseServerSystem::Writeruns on the IStreamable subobject at+0x8. The code tries both candidate bases across every system and logs how many validated under each (+0x274: 9 systems;+0x26c: 0). That turned a documentation dispute into a measurement.
Configs: shim.cfg.w3tshn, shim.cfg.w3mod, shim.cfg.w3control. w3tshn and w3control differ
in exactly one key (watch=), which is what makes the rule-19 control real.
Rule 19: the control was taken again, and it passed again
One End Turn from ref-turn2.sav with the four tshn watchpoints armed reproduced the determinism
oracle byte for byte — (Autosave EndTurn).sav bb4fd9ac…, (Autosave).sav 978041ac…, both
identical to the pre-run files. So the four new addresses are as neutral as W2's four were. That
mattered: W2 proved a data breakpoint is neutral for one address set, not for all of them, and this
lane's set includes a heap node that the game reallocates freely.
The second run (turn-1 workload, watch.mode=modcount) has no oracle of its own and says so.
What the two runs answered
NVO.TShn's writer, trapped live, with its whole call chain confirmed frame by frame from the recordedebpchain:RefreshVisibleOwnerIfKnown 0x0075bd70→RecordVisibleOwner 0x0075bca0→SetVisibleOwner 0x0075b880→NVO::operator[] 0x0075a890→ the store0x0075b961. The gate isServerSystem::IsKnownTo 0x00746390, which isIsVisibleToor (sensor contact ∧CCC_AdvSens) — notAFlagsalone, which is why lane E3 could not fit it. The resulting model predicts all 158NVOrecords in the 11-save corpus with zero mismatches, including the single frozen one.- Four traps, not one, and the extra pair is a result: the refresh runs twice per End Turn, in driver phase 24 and again in combat-done phase 25. Both phase attributions confirmed for free.
- The trade and spy containers, read out of a live game for the first time. Both managers
non-null, both vectors default-constructed with all three pointers zero, zero traps on either
_Mylastacross the turn. The workload is absent, measured — which is the confirmation instrument two earlier lanes needed and did not have. - All ten command
ModCounthandlers are now named, including two that only exist on a turn-1 workload (OnCommand_SetResearchProject, inlined at0x0088fe0a, andOnCommand_CreateDesign 0x00882910).
Reusing it
watch.mode is the extension point. To watch something else, add a mode and one arming function;
everything else — the canary, the VEH, the flusher, the hit format — is unchanged. Keep the canary
self-test and keep the ref-turn2 oracle control: together they cost about two minutes and they
are what makes the numbers evidence rather than output.
Mode cont (lane L3)
A third watch.mode, added when lane L3 needed to watch the trade-route and spy-program
containers across a whole game played forward rather than one turn of one save.
- All four debug slots go on the two containers: trade
_Myfirst(mgr+0x3c), trade_Mylast(mgr+0x40), spy_Myfirst(mgr+0x10), spy_Mylast(mgr+0x14)._Myfirstis armed as well as_Mylastbecause these vectors are default-constructed with all three pointers zero: the first element writes all three, and watching only_Mylastcannot tell "allocated for the first time" from "appended to an existing buffer". Those are different events in the model the lane was trying to falsify. - It re-arms and re-logs on every End Turn.
modcountandtshnarm once on purpose - their targets are chosen from one turn's state and re-picking them would move the measurement.conthas no such target selection, the two manager pointers are stable for the life of the server, and the per-turn count line is the signal. - The canary self-test therefore runs every turn too, and its counter is read as a delta. On the arm-once modes the delta is the old value, so their log lines are unchanged.
ReportContainer is factored out of ArmTshnSlots so both modes emit a byte-identical container
line; lane W3's published counts are the control every later count is compared against.
hooks=off watch=on used to arm nothing. Shim_Init returned before install_watchpoints
whenever the trace mode was off, so that config printed watch=on in the banner and then
measured a confident zero - the exact failure mode method rule 1 exists to catch. The watchpoints
are an independent instrument (one arming detour of their own, no trace records), and a long play
session wants them without paying 30-45 s per End Turn for template hooks that measure nothing it
is asking about. main.cpp now initialises MinHook and installs the watchpoint module on the
hooks=off path when watch=on.