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`,
zero leases at disposal). Full `airborne-transition True` confirmation is
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)
confirmed the #365 fix produces the correct behavior once past that unrelated
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`
**Status:** OPEN — 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.
**Status:** CLOSED 2026-08-10 — fixed in `b7f59923`. One dedicated update
thread (`acdream-headless-update`, spawned by `HeadlessProcessHost.RunAsync`)
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,
`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,
not a regression from a specific commit.
**NOT fixed here.** A proper fix needs its own investigation (e.g. a
dedicated single update thread for the scheduler's tick loop, or redesigning
the thread-affinity invariant for verified multi-thread-safe callers) and
must be verified against the GRAPHICAL host too — out of reach for the
session that found this (constrained not to launch the graphical client).
Do not "fix" this by loosening or removing `EnsureCollisionMutationThread`'s
check — it guards a real invariant elsewhere in the physics/collision
system, and a workaround shape here is exactly what CLAUDE.md's
no-workarounds rule forbids without explicit approval.
**Why the dedicated-thread shape (not an invariant redesign):** the guard
is only the ENFORCER — Runtime's single-update-thread contract is
documented all over (`RuntimeEntityDirectory` "single update-thread
authority", `RuntimeLocalPlayerPhysicsPublicationState` "this single
Runtime update thread", `RuntimePlacementProjectionChannel`), mostly
without enforcement. An async tick loop violates the whole contract, not
one check; weakening the check would have silenced the one place that
noticed while leaving every unenforced assumption exposed, and would have
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
collision generation spans more than a couple of scheduler ticks (the
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
**Status:** OPEN — filed 2026-08-10, Campaign CH slice CH6b (register row