fix(runtime): same-value LocalEntityId re-assertion is a no-op; file #365 headless hydration regression

The Campaign CH jump-probe headless reproduction quarantined on its first
advance tick: AdvanceBeforeNetwork re-asserts the resolved local entity id
every tick, which the C3c configuration seal treats as a mutation on a
dormant controller. A same-value write is now a no-op; a DIFFERENT id
while sealed still throws. One layer deeper the probe exposed #365: the
headless world never hydrates (entities stay 0, the movement controller
never publishes), so headless bots cannot move at head — filed with the
full evidence chain. HeadlessDiagnosticWriter.Failure now emits the full
exception detail (type-only cost a whole diagnosis round-trip).

Runtime tests 1,322/0, Headless tests 89/0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-10 08:48:37 +02:00
parent c1f1582576
commit ab82347d42
3 changed files with 56 additions and 0 deletions

View file

@ -58,6 +58,9 @@ internal sealed class HeadlessDiagnosticWriter
sessionId,
phase,
errorType = error.GetType().FullName,
// Full detail: a quarantine that reports only the exception TYPE
// costs a whole diagnosis round-trip (jump-probe, 2026-08-10).
errorDetail = error.ToString(),
});
}

View file

@ -381,6 +381,14 @@ public sealed class PlayerMovementController
get => _localEntityId;
set
{
// A same-value re-assertion is not a mutation:
// RuntimeLocalPlayerFrameController.AdvanceBeforeNetwork
// re-asserts the resolved id every advance tick, which on the
// headless host reached a still-dormant/sealed controller and
// quarantined the whole session (jump-probe repro, 2026-08-10).
// A DIFFERENT id while sealed remains the error it always was.
if (value == _localEntityId)
return;
EnsureConfigurationMutable();
_localEntityId = value;
}