docs(issues): close #368 (fixed in b7f59923); file #370 — the headless jump-airborne timeout survives single-threading, so it was never a threading artifact

#368's entry now records the fix mechanism (dedicated
acdream-headless-update thread owning Start + every scheduler turn;
synchronous TimeProvider-timer scheduler loop; guard untouched, zero
shared Runtime changes) and the 3/3 live-ACE verification vs the 3/3
pre-fix quarantines. The #365 entry and diagnosis doc get dated
pointers: their open question is answered — the airborne residual
persists with threads provably single, refuting the
unsynchronized-thread hypothesis — and is split off as #370 with the
evidence and starting points.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-08-10 18:24:01 +02:00
parent b7f59923ad
commit 326a186897
2 changed files with 93 additions and 14 deletions

View file

@ -209,7 +209,8 @@ reached — no `seal-refused` spam, no crash from the original bug) and the
session exits gracefully every time (`[session] graceful logout confirmed`, session exits gracefully every time (`[session] graceful logout confirmed`,
zero leases at disposal). Full `airborne-transition True` confirmation is zero leases at disposal). Full `airborne-transition True` confirmation is
blocked by a SEPARATE, newly-discovered, pre-existing defect — see #368 below blocked by a SEPARATE, newly-discovered, pre-existing defect — see #368 below
— not by anything in this issue's scope. A diagnostic-only run with #368's — not by anything in this issue's scope. (2026-08-10 update: #368 is CLOSED
at `b7f59923`; the airborne residual survived that fix and is now #370.) A diagnostic-only run with #368's
guard temporarily neutralized (never shipped, reverted before commit) guard temporarily neutralized (never shipped, reverted before commit)
confirmed the #365 fix produces the correct behavior once past that unrelated confirmed the #365 fix produces the correct behavior once past that unrelated
blocker: full hydration, the jump-probe policy running to completion, exit blocker: full hydration, the jump-probe policy running to completion, exit
@ -241,9 +242,33 @@ Environment). `HeadlessDiagnosticWriter.Failure` now emits `errorDetail`
## #368 — Headless scheduler's async tick loop can run collision-generation calls on different threads, tripping `EnsureCollisionMutationThread` ## #368 — Headless scheduler's async tick loop can run collision-generation calls on different threads, tripping `EnsureCollisionMutationThread`
**Status:** OPEN — filed 2026-08-10 during #365's end-to-end verification. **Status:** CLOSED 2026-08-10 — fixed in `b7f59923`. One dedicated update
Explicitly OUT OF SCOPE for #365 — orthogonal mechanism, not mentioned thread (`acdream-headless-update`, spawned by `HeadlessProcessHost.RunAsync`)
anywhere in that diagnosis. now owns the whole session-side lifecycle: `Start()` (the live connect
transaction, where the first collision-mutating call can already happen),
every scheduler turn, and the post-loop resource captures.
`HeadlessProcessScheduler.Run(CancellationToken)` replaced `RunAsync` — the
same deadline math and counters, but fully synchronous on the calling
thread, with waits going through one rearmed `TimeProvider` timer
signalling an event instead of `await Task.Delay`, so the loop never leaves
its thread. `EnsureCollisionMutationThread` is untouched — the invariant it
guards is real, and the headless host now satisfies it the same way the
graphical host's game-loop thread does. Zero shared Runtime changes, so the
graphical host is unaffected by construction. Evidence: new
`ProcessHostRunsStartAndEveryTickOnOneDedicatedUpdateThread` test (RED
pre-fix — Start ran on the caller's thread, ticks migrated to pool
workers), Headless suite 97/97, full Release suite 12,554 / 4 skips / 0
failures, and three live jump-probe runs against local ACE that each
crossed `[wake] begin gen=1` — the exact point all three pre-fix runs
quarantined — with zero faults, 204205 hydrated entities, policy
completion, ACE-confirmed graceful logout, converged disposed samples, and
exit 0. The jump-airborne timeout this issue carried as an open question
persists 3/3 on the fixed tree — the threading-artifact hypothesis is
refuted; split off as #370.
Filed 2026-08-10 during #365's end-to-end verification. Explicitly OUT OF
SCOPE for #365 — orthogonal mechanism, not mentioned anywhere in that
diagnosis.
**Symptom:** a real headless run against live ACE (`jump-probe` policy, **Symptom:** a real headless run against live ACE (`jump-probe` policy,
`ACDREAM_PROBE_PARK=1`) that survives long enough for the local player's own `ACDREAM_PROBE_PARK=1`) that survives long enough for the local player's own
@ -292,21 +317,62 @@ directly, never through the real `HeadlessProcessScheduler.RunAsync` await
loop — so none of them exercise this path either. A genuine coverage gap, loop — so none of them exercise this path either. A genuine coverage gap,
not a regression from a specific commit. not a regression from a specific commit.
**NOT fixed here.** A proper fix needs its own investigation (e.g. a **Why the dedicated-thread shape (not an invariant redesign):** the guard
dedicated single update thread for the scheduler's tick loop, or redesigning is only the ENFORCER — Runtime's single-update-thread contract is
the thread-affinity invariant for verified multi-thread-safe callers) and documented all over (`RuntimeEntityDirectory` "single update-thread
must be verified against the GRAPHICAL host too — out of reach for the authority", `RuntimeLocalPlayerPhysicsPublicationState` "this single
session that found this (constrained not to launch the graphical client). Runtime update thread", `RuntimePlacementProjectionChannel`), mostly
Do not "fix" this by loosening or removing `EnsureCollisionMutationThread`'s without enforcement. An async tick loop violates the whole contract, not
check — it guards a real invariant elsewhere in the physics/collision one check; weakening the check would have silenced the one place that
system, and a workaround shape here is exactly what CLAUDE.md's noticed while leaving every unenforced assumption exposed, and would have
no-workarounds rule forbids without explicit approval. degraded the guard for the graphical host where migration is always a bug.
The dedicated thread fixes the entire class. Start() had to move onto the
same thread too: the first collision-mutating call can land during
connect, and a caller-thread Start would bind the guard there and trip the
very first dedicated tick. Disposal legitimately stays on the lifecycle
thread — `ResetSessionPhysics`'s doc comment designs for exactly that, and
every prior graceful-teardown run (including the quarantined ones)
exercised it.
**Repro:** run the `jump-probe` policy against local ACE with **Repro (historical):** run the `jump-probe` policy against local ACE with
`ACDREAM_PROBE_PARK=1` for long enough that the local player's own landblock `ACDREAM_PROBE_PARK=1` for long enough that the local player's own landblock
collision generation spans more than a couple of scheduler ticks (the collision generation spans more than a couple of scheduler ticks (the
default case against a real DAT-loaded landblock). default case against a real DAT-loaded landblock).
## #370 — Headless jump-probe: the released jump never registers as airborne (proven NOT a threading artifact)
**Status:** OPEN — filed 2026-08-10 during #368's fix verification.
**Symptom:** with #368 fixed (one dedicated update thread, thread
migration provably gone — the new affinity test pins it), the `jump-probe`
policy reaches `[jump-probe] releasing jump (fire)` and then reports
`TIMEOUT waiting for airborne after jump fire -- the released jump never
registered as airborne.` Reproduced 3/3 on the fixed tree at
`lb=0x1134FFFF`; the #365 session saw the identical timeout in its
guard-neutralized diagnostic run at `lb=0x0904FFFF`
(`docs/research/2026-08-10-365-headless-hydration-diagnosis.md` §8), so it
is location-independent and pre-existing. The run still exits 0 with
ACE-confirmed graceful logout — the probe treats its timeout as
completion, so nothing quarantines.
**What this refutes:** the #365 OUTCOME's hypothesis that the timeout was
"plausibly a downstream artifact of the same unsynchronized-thread
condition" — threads are now single and the timeout persists unchanged.
This is a distinct defect (or probe-expectation gap) in the headless jump
path: either the policy's charge/release never becomes a real jump on the
wire/local motion, or the airborne signal the policy polls is never set on
the headless projection. Everything before the fire provably works
(hydration 204205 entities, `local player present`, movement owner
publication per #365's fix).
**Where to start:** `JumpProbeHeadlessBotPolicy` (what it reads as
"airborne"), the J5.4 `RuntimeLocalPlayerMovementState` jump intent/outbound
cadence path, and whether the graphical host's airborne transition has a
presentation-side dependency the headless projection lacks.
**Repro:** #368's recipe (jump-probe vs local ACE, `ACDREAM_PROBE_PARK=1`);
the timeout fires seconds after `releasing jump (fire)`.
## #369 — Unconfirmed whether retail's floating chat windows share the main window's currently-selected talk-focus channel ## #369 — Unconfirmed whether retail's floating chat windows share the main window's currently-selected talk-focus channel
**Status:** OPEN — filed 2026-08-10, Campaign CH slice CH6b (register row **Status:** OPEN — filed 2026-08-10, Campaign CH slice CH6b (register row

View file

@ -447,3 +447,16 @@ Every real (unmodified) run's session tore down gracefully
(`[session] graceful logout confirmed`, zero entities/leases at the final (`[session] graceful logout confirmed`, zero entities/leases at the final
`disposed` sample) regardless of which way it exited — `testaccount` was `disposed` sample) regardless of which way it exited — `testaccount` was
never left in a stuck state by this work. never left in a stuck state by this work.
## 9. ADDENDUM (2026-08-10, #368 fix session)
#368 is CLOSED at `b7f59923`: one dedicated headless update thread now owns
Start, every scheduler turn, and the post-loop captures; the scheduler loop
is synchronous with TimeProvider-timer event waits (no `Task.Delay`
resumption migration; zero shared Runtime changes). Three live jump-probe
runs on the fixed tree each crossed `[wake] begin gen=1` cleanly with
204205 hydrated entities and graceful exits. The §8 open question is now
answered: the `airborne-transition True` timeout PERSISTS 3/3 with threading
provably single, so the "downstream artifact of the unsynchronized-thread
condition" hypothesis is refuted — the residual is a distinct pre-existing
defect, filed as #370.