#140 receipts: name the corruption case in a FIELD log
Two ungated one-liners, because no bench here reached the failing path and
the next playtest is a better instrument than more automation:
[glasswin] destroy entry #N windows=M -- N=2,M=0 is the double-destroy
[glasswin] saved ... (live=L remembered=R) -- live=0 IS the corruption case
(pre-cache that wrote a file
holding only the plasma line)
Also lands the benches that did NOT reproduce it, with their failure modes
recorded in the headers so the next attempt does not repeat them:
layoutsave.sh (round trip -- passes on the fixed build), layoutteardown.sh
(graceful WM_CLOSE; still never reaches the dtor chain), layoutround.sh (MP
round boundary; the relay never started the mission inside the window).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NCJQkvq6G2JNrpVbA75tVZ
This commit is contained in:
co-authored by
Claude Opus 5
parent
c04bec0a52
commit
43d30ca7e4
@@ -1030,8 +1030,15 @@ static void
|
||||
++wrote;
|
||||
}
|
||||
fclose(f);
|
||||
// #140 receipt (ungated): `live=` is the diagnostic that matters. A save
|
||||
// that runs with live=0 is the corruption case -- before the remembered-
|
||||
// geometry cache it wrote a file containing ONLY the plasma line, which is
|
||||
// what testers reported. It stays in the log so the FIELD can tell us which
|
||||
// caller does that (teardown ordering, a drag after a round boundary, ...),
|
||||
// which no bench here managed to reach.
|
||||
DEBUG_STREAM << "[glasswin] saved " << wrote << " window position(s) to "
|
||||
<< layoutFileName << "\n" << std::flush;
|
||||
<< layoutFileName << " (live=" << gWinCount
|
||||
<< " remembered=" << gLastGeomCount << ")\n" << std::flush;
|
||||
}
|
||||
|
||||
// Public save trigger for registered external windows -- their WndProc calls
|
||||
@@ -1789,6 +1796,16 @@ void
|
||||
void
|
||||
BTGlassPanels_Destroy()
|
||||
{
|
||||
// #140 receipt (ungated, one line per teardown): this function has two
|
||||
// callers on the desktop path and the ORDER is what broke the layout file.
|
||||
// A run that shows `entry #2 windows=0` is the double-destroy, on the
|
||||
// record, without needing to catch the cfg mid-corruption.
|
||||
{
|
||||
static int s_destroyN = 0;
|
||||
DEBUG_STREAM << "[glasswin] destroy entry #" << ++s_destroyN
|
||||
<< " windows=" << gWinCount << "\n" << std::flush;
|
||||
}
|
||||
|
||||
// Backstop for a clean teardown (WM_EXITSIZEMOVE already caught every
|
||||
// finished drag); no-op unless mode==save. GUARDED on there being windows:
|
||||
// this function has TWO callers (~PadRIO and ~LBE4ControlsManager), so on
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
"""Night 13: report the #52 root cause on the tracker. ASCII only."""
|
||||
import sys
|
||||
sys.path.insert(0, r"C:\git\bt411\scratchpad\night7")
|
||||
import gitea
|
||||
|
||||
BODY = """**ROOT CAUSE FOUND AND FIXED (2026-08-07, commit `6a96fb6`)** -- but read the
|
||||
"what is NOT proven" section before treating this as closed.
|
||||
|
||||
## The defect
|
||||
|
||||
A replicant cannot start walking between gait-change records.
|
||||
|
||||
The port's body `case 4` (the task-#64 lockstep twin, mech2.cpp) is an INSERTION sitting between
|
||||
`case 0` and the advance group. In the binary it is a MEMBER of that group -- `FUN_004a5678`
|
||||
@004a5678 reads `case 2,3,`**`4`**`,5,8,...` with no turn block and no speed exit [T1] -- so case
|
||||
0's fallthrough is meant to land on `Advance()`. The insertion intercepted it.
|
||||
|
||||
On a replicant that is not a race, it is an identity:
|
||||
|
||||
* case 0 arms walk iff `standSpeed < bodyTargetSpeed`
|
||||
* the inserted block resets iff `standSpeed < bspd`, and `bspd` **IS** `bodyTargetSpeed` on a replicant
|
||||
|
||||
Same expression. Arm and reset fire on the same frame, every frame. A peer parked at Standing with a
|
||||
live replicated demand never cycles: `bodyCycleSpeed` stays 0 while position advances from dead
|
||||
reckoning. That is the skate. (Reverse is dead the same way -- both sides test `< ZeroSpeed`.)
|
||||
|
||||
## Why it appeared when it did
|
||||
|
||||
This is the sequel to `e91d447` (#82). Before that commit the replicant branch read the LOCAL
|
||||
mapper's `speedDemand` -- a dead cell on a peer, 0 forever -- so the exit never fired and the
|
||||
fallthrough worked BY ACCIDENT. Fixing the dead cell (correctly) closed the accidental escape
|
||||
hatch, and the trn-lock skate came back as a Standing-lock skate.
|
||||
|
||||
## Why masters were unaffected
|
||||
|
||||
Two reasons, either sufficient: their two tests read DIFFERENT cells (`bodyTargetSpeed` held at
|
||||
last-sent by the gait mirror, vs the live mapper `speedDemand`), so they only stall in the window
|
||||
where those disagree; and the master's body channel runs `mj=0` and writes no joints, so its stall
|
||||
is invisible -- the leg channel, whose case 0 falls through correctly, drives pose and travel.
|
||||
|
||||
## The load-bearing detail
|
||||
|
||||
`mech4.cpp`'s "stand; case 0 walk-begins next tick" is not an aside. A peer's body state is set from
|
||||
`record->legState` only on **type-3 edges**, and ENTERING Standing emits one while LEAVING it does
|
||||
not. Between gait-change records a replicant is REQUIRED to derive walking itself from the
|
||||
replicated demand. The insertion removed that ability.
|
||||
|
||||
This also explains the shape of the field data: the lock needs a mech holding a STEADY demand, so a
|
||||
mech whose gait keeps changing is continually rescued by records. Night 13's four episodes all carry
|
||||
`bodyTargetSpeed` 39-48 held across 100-400 frames, and each ended when that mech next changed gait.
|
||||
|
||||
## Fix
|
||||
|
||||
`case 0` -> `goto advance_body_normally`, the leg twin's own idiom, restoring the binary's structure
|
||||
without touching the #64/#82 turn logic. `BT_NO_BODY_FALLTHRU=1` reverts.
|
||||
|
||||
## Measured (2-node, `scratchpad/night13/skatelock.sh`)
|
||||
|
||||
| | legacy | fixed |
|
||||
|---|---|---|
|
||||
| STANDING-LOCK seconds | **336 consecutive**, `bspd=39.2324 bts=39.2324` identical every line | **0**, every pass |
|
||||
| master body-Standing samples | 52 | 21 |
|
||||
| turn-in-place | -- | body state 4 x9 / leg state 4 x8, armed in lockstep |
|
||||
|
||||
## What is NOT proven [T3]
|
||||
|
||||
That this accounts for the night-13 episodes. The lock is proven and proven removed; the link to the
|
||||
field symptom is INFERENCE -- a locked peer has `bodyCycleSpeed==0` and never advances its clip, so
|
||||
locked + translating IS the `[skate]` signature by construction -- but no bench caught the two
|
||||
together. Four rigs failed to reproduce the symptom end-to-end.
|
||||
|
||||
The `[skate]` line now carries `bstate=`, which is the diagnostic tonight's logs lacked. Next
|
||||
playtest settles it: episodes gone -> confirmed; any survivor names its own state.
|
||||
|
||||
**Correction for the record:** the night-12 `skatebench` "reproductions" (6 episodes, `sk_run.out`)
|
||||
were a DETECTOR ARTIFACT, not this bug. The first detector build tested only `legCycleSpeed==0`,
|
||||
which is NORMAL on a peer -- the body channel poses it -- so it fired on every healthy movement
|
||||
phase. It was corrected the same day to require both channels idle. Old-format lines
|
||||
(`legCycleSpeed=`, no `bodyCyc=`) are not evidence of anything.
|
||||
|
||||
Related: #130 (Vulture skating) is very likely the same defect -- re-test it against this build
|
||||
before spending separate effort."""
|
||||
|
||||
gitea.comment(52, BODY)
|
||||
gitea.comment(130,
|
||||
"Cross-ref: #52's root cause was found and fixed 2026-08-07 (`6a96fb6`) -- a replicant could not "
|
||||
"start walking between gait-change records, so a peer parked at Standing with a live replicated "
|
||||
"demand kept its position advancing with a dead animation channel. That is the same shape as the "
|
||||
"skating reported here. **Re-test this against a build newer than `6a96fb6` before investigating "
|
||||
"separately.** Full write-up in #52.")
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env bash
|
||||
# =========================================================================
|
||||
# #140 -- reach the teardown the way the FIELD does: a MISSION that ENDS.
|
||||
#
|
||||
# Three earlier attempts never exercised BTGlassPanels_Destroy at all:
|
||||
# bt_kill_ours uses `taskkill /F` (no dtors), and a graceful WM_CLOSE on a
|
||||
# solo -egg run did not reach it either. The path that matters is the one
|
||||
# testers hit constantly -- the controls manager is destroyed and rebuilt at
|
||||
# every ROUND boundary, which is exactly what the relay drives in MP.
|
||||
#
|
||||
# Panels ON for both nodes; the relay starts a round and the round ends.
|
||||
# The receipt to read is `[glasswin] destroy entry #N windows=M`:
|
||||
# #1 windows=7 then #2 windows=0 -> the double-destroy, confirmed
|
||||
# and then exactly ONE "saved N window position(s)" line, with the cfg intact.
|
||||
# =========================================================================
|
||||
set -x
|
||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
||||
cd /c/git/bt411/content || exit 1
|
||||
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
rm -f glass_layout.cfg lr_a.log lr_b.log lr_relay.log
|
||||
|
||||
bt_expert_egg MP.EGG LR.EGG
|
||||
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" LR.EGG
|
||||
|
||||
( export BT_GLASS_PANELS=1 BT_GLASS_LAYOUT=save BT_MP_LOG=1
|
||||
bt_launch lr_b.log LR.EGG 0x0C -net 1601 )
|
||||
sleep 2
|
||||
( export BT_GLASS_PANELS=1 BT_GLASS_LAYOUT=save BT_MP_LOG=1
|
||||
bt_launch lr_a.log LR.EGG 0x03 -net 1501 )
|
||||
sleep 5
|
||||
python ../tools/btconsole.py LR.EGG 127.0.0.1:1501 127.0.0.1:1601 > lr_relay.log 2>&1 &
|
||||
RELAY=$!
|
||||
sleep 180
|
||||
kill $RELAY 2>/dev/null
|
||||
sleep 5
|
||||
bt_kill_ours
|
||||
sleep 2
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 3
|
||||
|
||||
echo "=================== #140 TEARDOWN RECEIPTS ==================="
|
||||
for n in a b; do
|
||||
echo "--- node $n: destroy entries ---"
|
||||
grep -a "destroy entry" lr_${n}.log
|
||||
echo "--- node $n: SaveLayout receipts ---"
|
||||
grep -a "window position" lr_${n}.log
|
||||
done
|
||||
echo
|
||||
echo "--- glass_layout.cfg entries ---"
|
||||
grep -aE "^[^#]+=" glass_layout.cfg 2>/dev/null || echo "(no file)"
|
||||
echo -n "entry count: "; grep -acE "^[^#]+=" glass_layout.cfg 2>/dev/null
|
||||
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
# =========================================================================
|
||||
# #140 -- exercise the TEARDOWN path, which is where the bug lives.
|
||||
#
|
||||
# WHY THIS EXISTS: bench_common's bt_kill_ours uses `taskkill /F`. That is a
|
||||
# HARD kill -- no destructors, so ~LBE4ControlsManager / ~PadRIO never run and
|
||||
# BTGlassPanels_Destroy (the function that saves, and the one that ran twice)
|
||||
# is never reached. Both of the first attempts died before teardown and the
|
||||
# result looked like a difference between builds when it was really a
|
||||
# difference in how far each run got. A graceful `taskkill` (NO /F) posts
|
||||
# WM_CLOSE and lets the dtor chain run.
|
||||
#
|
||||
# Pass a label; run it once on the pre-fix build and once on the fixed one.
|
||||
# =========================================================================
|
||||
LABEL="${1:-run}"
|
||||
. /c/git/bt411/scratchpad/night6/bench_common.sh
|
||||
cd /c/git/bt411/content || exit 1
|
||||
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1
|
||||
sleep 2
|
||||
rm -f glass_layout.cfg lt_${LABEL}.log
|
||||
|
||||
( export BT_GLASS_PANELS=1 BT_GLASS_LAYOUT=save
|
||||
bt_launch lt_${LABEL}.log ARENA1.EGG 0x03 )
|
||||
|
||||
# wait for the panels to actually exist before asking for a shutdown
|
||||
for i in $(seq 1 60); do
|
||||
grep -aq "per-display cockpit up" lt_${LABEL}.log 2>/dev/null && break
|
||||
sleep 2
|
||||
done
|
||||
grep -aq "per-display cockpit up" lt_${LABEL}.log || { echo "PANELS NEVER CAME UP"; }
|
||||
sleep 20 # let it settle into the mission
|
||||
|
||||
PID=$(cat "$BT_PIDFILE" 2>/dev/null | head -1)
|
||||
echo "graceful close of pid $PID"
|
||||
taskkill //PID "$PID" > /dev/null 2>&1 # NO /F -- WM_CLOSE, dtors run
|
||||
for i in $(seq 1 30); do
|
||||
tasklist //FI "PID eq $PID" 2>/dev/null | grep -q btl4 || break
|
||||
sleep 1
|
||||
done
|
||||
taskkill //F //IM btl4.exe > /dev/null 2>&1 # backstop
|
||||
rm -f "$BT_PIDFILE"
|
||||
sleep 2
|
||||
|
||||
echo "=============== $LABEL ==============="
|
||||
echo "--- every SaveLayout receipt (TWO lines = the double-save) ---"
|
||||
grep -a "window position" lt_${LABEL}.log
|
||||
echo
|
||||
echo "--- glass_layout.cfg entries after teardown ---"
|
||||
grep -aE "^[^#]+=" glass_layout.cfg 2>/dev/null || echo "(no file)"
|
||||
echo
|
||||
echo -n "entry count: "; grep -acE "^[^#]+=" glass_layout.cfg 2>/dev/null || echo 0
|
||||
Reference in New Issue
Block a user