#137: REPRODUCE the myomer freeze -- reset is innocent, the myomers RUN AWAY after it

First reproduction of #137 in a bench.  The reset path is exonerated and the
real defect is located, though not yet explained.

DECOMP RE-READ (what the reset actually does):
  Mech::Reset @0049fb74 walks the roster from index 2 calling vtable +0x28
    (slot 10 = ResetToInitialState), then @0049f788.
  Myomers::RTIS @004b8aa4 -> PoweredSubsystem::RTIS @004b0e6c -> ALWAYS
    HeatSink::RTIS @004ad760, whose first act is
        param_1[0x45] = param_1[0x4f]     // currentTemperature = startingTemperature
    (byte 0x114 = 0x13C).  Our port matches AND additionally resets heatEnergy,
    which it must, since HeatSinkSimulation derives
        currentTemperature = heatEnergy / thermalMass.
  The freeze itself is the derating curve @004b8ac0:
        temp >= degradation(@0x118) -> falls off
        temp >= FAILURE(@0x11C)     -> 0.0  -> chain MAX 0 -> speedDemand *= 0.
  Nothing in the reset chain touches Myomers::speedEffect (@0x31C).

MEASURED (scratchpad/night14/myofreeze.sh -- hot mech, repeated self-kill):

    at-reset   Myomers T=77      deg=1000 fail=2000  speedEffect=0   <- stale
    post-reset Myomers T=77.14                        speedEffect=1   <- 1 frame
    post-reset Myomers T=9297.8  fail=2000            speedEffect=0
    post-reset Myomers T=11620.4 fail=2000            speedEffect=0

So, in order:
  * THE RESET WORKS.  T is exactly startingTemperature at the reset.  My
    original bench was right about that much; it just stopped looking there.
  * THE STALE speedEffect IS REAL BUT HARMLESS -- it survives the reset (no
    RTIS writes it) yet self-heals on the very next tick.  Not the bug.
  * THE MYOMERS THEN RUN AWAY: 77 -> ~11,600 against a FAILURE point of 2,000,
    nearly 6x over, in seconds.  That is not heat earned by running; that is a
    runaway, and it is what pins speedDemand at 0 until it cools -- exactly
    Oracle's "maxed heat bar ... unable to move until it cools off".
  * Rate: 7 of 105 census samples over the failure temp (~6.7%), against the
    field's 5-of-61 respawns (~8%).  Same order, so the bench is reproducing
    the field condition and not a bench artifact.
  * The myomers link to Condenser5 (mass=250000 k=190000) -- literally Oracle's
    "loop 5 and generator D heating up as all the excess heat goes into the loop".

NOT YET ESTABLISHED, and the reason this is a checkpoint and not a fix: whether
the runaway is CAUSED by the respawn or is a heat-model calibration problem that
merely CORRELATES with it (you die when you overheat, so respawns cluster around
hot periods).  The bench drives at 95% throttle with continuous missile autofire,
which is abusive, and the [heat-t] census has no pre-first-reset samples to
compare against.  Next step is to instrument the heat INPUT and diff a
respawn-adjacent window against a steady-running window.

Probe added: [myofreeze] prints T / degradation / FAILURE / speedEffect per
myomers plus the chain MAX the mover multiplies by, AT the reset and for ~4 s
after (armed by Mech::Reset, sampled where the multiplier is formed).  The
post-reset window is what nothing was watching -- sampling only at the reset is
what made this look innocent and got the ticket wrongly closed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018SgmXGNMXavXiafKXf9MDC
This commit is contained in:
Joe DiPrima
2026-08-09 11:46:23 -05:00
co-authored by Claude Opus 5
parent 92783b9935
commit 5556329807
4 changed files with 175 additions and 0 deletions
+64
View File
@@ -1486,3 +1486,67 @@ void BTReportHeatAtReset(void *mech_v)
<< "\n" << std::flush;
}
}
//===========================================================================//
// BTReportMyomerFreeze -- #137 forensic (BT_HEAT_LOG), the POST-reset trace.
//
// BTReportHeatAtReset samples AT the reset and showed every subsystem at
// T == startingTemperature, which is what made the reset path look innocent and
// got #137 wrongly closed. The field then proved the freeze is real (5 of 61
// respawns: throttle up, speedDemand pinned at 0) -- so the interesting window
// is the frames immediately AFTER the reset, which nothing was sampling.
//
// Three explanations survive the decomp read and only data separates them:
// (a) RESET DIDN'T TAKE -> temp is high right after the reset
// (b) STALE CACHE -> temp is at start but speedEffect is still 0
// (HeatSink::RTIS @004ad760 writes only bytes
// 0x114/0x12C/0x130/0x134/0x138/0x158/0x15C --
// it does NOT touch Myomers::speedEffect @0x31C,
// and Myomers::RTIS @004b8aa4 only chains to the
// PoweredSubsystem one, so the pre-death value
// survives until the myomers next ticks)
// (c) INSTANT RE-HEAT -> temp starts at start and climbs back at once
//
// Prints per myomers: temperature, its own speedEffect, and the mech-level MAX
// the mover actually multiplies by. Freeze == that MAX at 0.
//===========================================================================//
void BTReportMyomerFreeze(void *mech_v, const char *when)
{
if (mech_v == 0 || getenv("BT_HEAT_LOG") == 0)
return;
Entity *mech = (Entity *)mech_v;
extern Scalar BTMyomersSpeedEffectOf(void *subsystem);
const int count = mech->GetSubsystemCount();
Scalar best = -1.0f;
for (int i = 0; i < count; ++i)
{
Subsystem *s = mech->GetSubsystem(i);
if (s == 0)
continue;
Scalar f = BTMyomersSpeedEffectOf(s);
if (f < -0.5f)
continue; // not a Myomers
if (f > best) best = f;
Scalar t = -1.0f, tdeg = -1.0f, tfail = -1.0f;
if (s->IsDerivedFrom(*HeatableSubsystem::GetClassDerivations()))
{
HeatableSubsystem *h = (HeatableSubsystem *)s;
t = h->currentTemperature; // @0x114
tdeg = h->degradationTemperature; // @0x118
tfail = h->failureTemperature; // @0x11C
}
// The derating curve (@004b8ac0) is exactly:
// temp >= degradation -> falls off; temp >= FAILURE -> 0.0 (frozen).
// So printing the thresholds beside the temperature says immediately
// whether a 0 effectiveness is JUSTIFIED by the temperature (reset did
// not take / re-heated) or is a STALE cache (temp fine, effect still 0).
DEBUG_STREAM << "[myofreeze] " << when << " " << (s->GetName() ? s->GetName() : "?")
<< " T=" << t << " deg=" << tdeg << " fail=" << tfail
<< " speedEffect=" << f
<< (f <= 1.0e-4f && t < tfail ? " <<<< STALE (cold but zero)" : "")
<< "\n" << std::flush;
}
if (best >= 0.0f)
DEBUG_STREAM << "[myofreeze] " << when << " CHAIN MAX=" << best
<< (best <= 1.0e-4f ? " <<<< FROZEN" : "") << "\n" << std::flush;
}
+9
View File
@@ -666,6 +666,7 @@ static int gBTPPCKey = 0;
static int gBTMissileKey = 0;
static int gBTPinkyKey = 0; // key '4' = the pod's 4th fire button (Pinky 0x45)
int gBTModeCycle = 0; // 'M' edge: cycle the control mode (mapper consumes)
int gBTMyoTrace = 0; // #137: frames of post-reset myomer tracing left (armed by Mech::Reset)
int gBTDisplayCycle = 0; // 'N' edge: cycle the secondary schematic (Gitea #6, mapper consumes)
int gBTPresetCycle[3] = {0,0,0}; // J/K/L edges: cycle an upper-MFD preset page (Gitea #9, L4 mapper consumes)
//
@@ -2379,6 +2380,14 @@ void
{
extern void BTReportHeatAtReset(void *mech_v);
BTReportHeatAtReset((void *)this);
// #137: sample the myomers AT the reset, and arm the POST-reset trace.
// Sampling only here is what got this bug wrongly closed -- every
// subsystem reads T == start at this instant, which looks innocent.
// The freeze shows up in the frames AFTER.
extern void BTReportMyomerFreeze(void *mech_v, const char *when);
extern int gBTMyoTrace;
BTReportMyomerFreeze((void *)this, "at-reset");
gBTMyoTrace = 240; // ~4 s of post-reset frames
}
// --- DESKTOP THROTTLE RELEASE (#146) -- PORT LAYER, desktop-only ---------
+14
View File
@@ -1090,6 +1090,20 @@ void
// reads the same live factor (dead/overheated myomers cannot squat
// or rise; the posture selector tests |factor| <= 1e-4).
mech->myomerEffectiveness = drive;
// #137 POST-RESET TRACE. Mech::Reset arms gBTMyoTrace; sample here,
// where the mover's actual multiplier is formed, for a few seconds
// after a respawn. This is the window nothing was watching -- the
// at-reset sample always looks clean.
{
extern int gBTMyoTrace;
extern void BTReportMyomerFreeze(void *mech_v, const char *when);
if (gBTMyoTrace > 0)
{
--gBTMyoTrace;
if ((gBTMyoTrace % 30) == 0)
BTReportMyomerFreeze((void *)mech, "post-reset");
}
}
if (fabsf(drive) <= 1.0e-4f) // @0x4a9d89 vs _DAT_004ab16c
turnDemand = 0.0f; // @0x4a9d9e: mapper+0x12C -- the FREEZE
}
+88
View File
@@ -0,0 +1,88 @@
#!/usr/bin/env bash
# =========================================================================
# #137 -- "respawns with the myomers overheated ... initially unable to move
# until it cools off" (Oracle, across several builds).
#
# WHY THE OLD BENCH PROVED NOTHING. heatrespawn.sh sampled AT the reset and
# found every heat-bearing subsystem at T == startingTemperature, so I closed
# #137 as not-a-bug. The field then measured the freeze at 5 of 61 respawns
# (~8%) across THREE machines -- throttle up, speedDemand pinned at 0. A
# bench that never reproduces the failure is not evidence the failure does not
# exist, which is the mistake that closed the ticket.
#
# WHAT THE DECOMP SAYS (re-read 2026-08-09):
# Mech::Reset @0049fb74 walks the roster from index 2 calling vtable +0x28
# (slot 10 = ResetToInitialState) on each subsystem, then @0049f788.
# Myomers::RTIS @004b8aa4 -> PoweredSubsystem::RTIS @004b0e6c -> ALWAYS
# HeatSink::RTIS @004ad760, which does `param_1[0x45] = param_1[0x4f]`
# i.e. currentTemperature(@0x114) = startingTemperature(@0x13C).
# The freeze itself is the derating curve @004b8ac0:
# temp >= degradation(@0x118) -> falls off; temp >= FAILURE(@0x11C) -> 0.0
# and 0.0 reaches the mover as the chain MAX -> speedDemand *= 0.
# CRUCIALLY: nothing in that reset chain touches Myomers::speedEffect
# (@0x31C). It keeps its pre-death value until the myomers next ticks.
#
# So three explanations survive, and only data separates them:
# (a) RESET DIDN'T TAKE -> post-reset T is high (>= fail)
# (b) STALE CACHE -> T is at start but speedEffect is still 0
# ("<<<< STALE (cold but zero)" in the receipt)
# (c) INSTANT RE-HEAT -> T starts at start and climbs back immediately
#
# THE MEASUREMENT. Mech::Reset now arms a ~4 s post-reset trace sampled where
# the mover's multiplier is actually formed:
# [myofreeze] at-reset Myomers T=.. deg=.. fail=.. speedEffect=..
# [myofreeze] post-reset Myomers T=.. deg=.. fail=.. speedEffect=..
# [myofreeze] post-reset CHAIN MAX=0 <<<< FROZEN
#
# A drives hard (heat) and self-damages to death repeatedly, so deaths land on
# a HOT mech -- the field composition. Long run: at ~8% we need many respawns.
# =========================================================================
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 mf137_a.log mf137_b.log mf137_relay.log
bt_expert_egg MP.EGG MF137.EGG
sed -i "s/^map=.*/map=grass/; s/^time=.*/time=day/; s/^vehicle=.*/vehicle=madcat/" MF137.EGG
( export BT_DEATH_LOG=1 BT_MP_LOG=1
bt_launch mf137_b.log MF137.EGG 0x0C -net 1601 )
sleep 2
# A: run hot (autodrive) + fire continuously (weapon heat) + die often.
( export BT_AUTODRIVE=0.95 BT_SELF_DAMAGE=7
export BT_AUTOFIRE=1 BT_AF_MISSILE=1 BT_AF_PERIOD=4
export BT_HEAT_LOG=1 BT_DEATH_LOG=1 BT_MP_LOG=1
bt_launch mf137_a.log MF137.EGG 0x03 -net 1501 )
sleep 5
python ../tools/btconsole.py MF137.EGG 127.0.0.1:1501 127.0.0.1:1601 > mf137_relay.log 2>&1 &
RELAY=$!
sleep 540
kill $RELAY 2>/dev/null
sleep 3
bt_kill_ours; sleep 2; taskkill //F //IM btl4.exe > /dev/null 2>&1; sleep 3
echo "=================== #137 MYOMER FREEZE ==================="
echo -n "respawns: "; grep -ac "Mech::Reset" mf137_a.log
echo -n "post-reset FROZEN samples: "; grep -ac "FROZEN" mf137_a.log
echo -n "STALE (cold but zero) samples: "; grep -ac "STALE (cold but zero)" mf137_a.log
echo
echo "--- any reset where the myomers came back AT or OVER the failure temp? ---"
python - <<'PY'
import re, io
bad = 0
for ln in io.open(r"C:\git\bt411\content\mf137_a.log", encoding="latin-1", errors="replace"):
m = re.search(r"\[myofreeze\] (\S+)\s+(\S+)\s+T=([-\d.e+]+) deg=([-\d.e+]+) fail=([-\d.e+]+)\s+speedEffect=([-\d.e+]+)", ln)
if not m:
continue
when, name, t, deg, fail, se = m.group(1), m.group(2), float(m.group(3)), float(m.group(4)), float(m.group(5)), float(m.group(6))
if se <= 1e-4:
bad += 1
if bad <= 12:
why = "TEMP >= fail (reset did not take / re-heated)" if t >= fail else "STALE CACHE (temp fine, effect 0)"
print(" %-10s %-12s T=%8.1f fail=%8.1f effect=%.4f -> %s" % (when, name, t, fail, se, why))
print(" zero-effect samples: %d" % bad)
PY
echo
echo "--- the first frozen episode in context ---"
grep -aE "Mech::Reset|myofreeze" mf137_a.log | grep -aB2 -A6 "FROZEN" | head -20