From ab82347d42c3fd2164d100a3cf2fad4c4dff1960 Mon Sep 17 00:00:00 2001 From: Erik Date: Mon, 10 Aug 2026 08:48:37 +0200 Subject: [PATCH] fix(runtime): same-value LocalEntityId re-assertion is a no-op; file #365 headless hydration regression MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- docs/ISSUES.md | 45 +++++++++++++++++++ .../Diagnostics/HeadlessDiagnosticWriter.cs | 3 ++ .../Gameplay/PlayerMovementController.cs | 8 ++++ 3 files changed, 56 insertions(+) diff --git a/docs/ISSUES.md b/docs/ISSUES.md index f69864bf..49d2212d 100644 --- a/docs/ISSUES.md +++ b/docs/ISSUES.md @@ -121,6 +121,51 @@ than a REJECT-review fix batch; CH5-or-later. **Campaign:** `docs/plans/2026-08-09-chat-parity-campaign.md` (Campaign CH, CH4 REJECT-review). +## #365 — Headless host cannot move at head: session quarantines on the first advance tick; world never hydrates + +**Status:** OPEN — filed 2026-08-10 during the Campaign CH jump-probe +reproduction. Distinct from #330 (no collision) and #332 (no remote DR): +this is the LOCAL player. + +**Symptom:** a headless session (`jump-probe` policy, real ACE connect, +`testaccount`/`+Acdream`) reaches `InWorld`, `live: in world — CreateObject +stream active` prints, but `entityCount` stays **0** for the whole run and +the local movement controller is never published. The first +`SetIntent(Jump: true)` → next `AdvanceBeforeNetwork` tick quarantined the +session. + +**Evidence chain (2026-08-10):** +1. First quarantine: `RuntimeLocalPlayerFrameController.AdvanceBeforeNetwork:93` + unconditionally re-assigned `controller.LocalEntityId` — a sealed + configuration property — on a still-dormant controller. FIXED in the + probing session: a same-value re-assertion is now a no-op (a different + id while sealed still throws). +2. Second quarantine (one layer deeper): the frame controller takes the + `ObjectClockDisposition == Suspend` branch and calls + `SuspendObjectUpdate` → `EnsurePublishedForRuntimeOperation` throws — + `_host.CanAdvancePlayer` is true while the controller is unpublished. +3. Root condition: the session's world never hydrates (`entities: 0` + despite the active CreateObject stream), so dormant local activation + never completes and the controller never publishes. K3/K4 ran full + connected multi-session gates with movement — this regressed somewhere + in the many Runtime/placement/CH landings since 2026-07-27. + +**Also observed in the same run:** `[weenie-error] unmapped code=0x051D` — +an ACE-only id outside retail's 344-case `HandleFailureEvent` switch; +CH2's silent-toward-player + diagnostics-line fallback handled it as +designed (no action needed, noted for completeness). + +**Repro:** `dotnet run --project src/AcDream.Headless -c Release -- run +--config ` with a `jump-probe` policy session against local ACE +(config shape: version 1, endpoint 127.0.0.1:9000, credential provider +Environment). `HeadlessDiagnosticWriter.Failure` now emits `errorDetail` +(full exception) — added during this diagnosis. + +**Next step:** bisect the headless hydration path (why zero entities admit +headless when the graphical host hydrates fine) BEFORE touching +`CanAdvancePlayer` — the #357 closeout warns that dormant-activation +classification changes have a strict test matrix. + ## #364 — Three `/help` group topics still partial: HelpStupidChannelHack unresolved **Status:** OPEN — filed 2026-08-09, Campaign CH user-gate round 2, item 3. diff --git a/src/AcDream.Headless/Diagnostics/HeadlessDiagnosticWriter.cs b/src/AcDream.Headless/Diagnostics/HeadlessDiagnosticWriter.cs index 8290a2d2..4efc41f3 100644 --- a/src/AcDream.Headless/Diagnostics/HeadlessDiagnosticWriter.cs +++ b/src/AcDream.Headless/Diagnostics/HeadlessDiagnosticWriter.cs @@ -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(), }); } diff --git a/src/AcDream.Runtime/Gameplay/PlayerMovementController.cs b/src/AcDream.Runtime/Gameplay/PlayerMovementController.cs index b18ab208..b4daa174 100644 --- a/src/AcDream.Runtime/Gameplay/PlayerMovementController.cs +++ b/src/AcDream.Runtime/Gameplay/PlayerMovementController.cs @@ -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; }