Parent: #482 Related: #474, #493, #874
Root cause
The lifecycle fuzzer accumulated Promise-reaction completions for Thread.asyncJoin(), Atomics.waitAsync(...), and Lock.asyncHold(fn) with shared ordinary numeric += operations. Those reactions may run concurrently on different no-GIL settling or task-pump threads after their jobs settle. The counters therefore contained JavaScript program races, not engine synchronization witnesses.
The initially observed failures were both in the async-hold total and each lost exactly one unique marker:
- seed 17:
923080 / 984084, delta61004(thread 2, iteration 4) - seed 26:
1804312 / 1845315, delta41003(thread 1, iteration 3)
Six immediate seed-17 reruns and a later instrumented 20-seed window did not reproduce the schedule collision. Source ordering nevertheless proves the assertion was invalid: runHoldJob releases the callback-form async hold before resolving the outer Promise, and its reaction performed the racy += afterward. A wider audit found the same invalid pattern in the async-join and wait-async reaction totals.
The no-function asyncHold() release total is different: each fulfillment reaction retains the live hold while updating its score and calls release() only afterward, so that ordinary total remains lock-protected.
Required correction
- Accumulate async-join, wait-async, and callback-form async-hold sum/count pairs in a shared integer TypedArray using
Atomics.add. - Keep validating each async-hold callback result against its unique expected marker.
- Check every exact sum and completion count after run-loop quiescence so missing, duplicate, rejected, or wrong-value delivery remains a hard failure.
- Preserve the ordinary release total and document why its live no-function hold provides synchronization.
- Do not serialize engine task execution, move assertions into unrelated locks, weaken expected totals, retry failed seeds, add sleeps, or treat non-reproduction as proof.
- Preserve the original failure evidence and link the test correction to #874, whose lifecycle gate exposed it.
Acceptance
- Post-settlement shared totals contain no ordinary read-modify-write outside synchronization.
- Exact atomic sum/count pairs detect missing, duplicate, and rejected async-join, wait-async, and async-hold completions; async-hold also validates every returned marker.
- Seeds 17–36 pass as one 1,120-program lifecycle window.
- Seeds 17 and 26 pass under suppression-free TSan.
- The full 22,400-program lifecycle profile passes with zero functional failures.
- The change is test-only and makes no runtime throughput or semantic claim.