2026-05-25 15:07:10 +02:00
// === Nested-Asyncify handleSleep currData save/restore (Emscripten #9153) ===
//
// Asyncify.currData is a single-slot global. When a fiber swap runs inside an
// EM_ASYNC_JS Promise await (e.g., wxDialog::ShowModal via startModal), the
// fiber swap overwrites currData with the fiber's asyncify_data, losing the
// sleep's own buffer. On Promise resolution, handleSleep's doRewind then uses
// the wrong buffer and crashes with "index out of bounds" or "unreachable".
//
// Workaround: intercept Asyncify.allocateData to record which pointer belongs to
// the active handleSleep; restore it to Asyncify.currData inside the wakeUp
// callback before handleSleep proceeds to _asyncify_start_rewind + doRewind.
if ( typeof Asyncify !== "undefined" ) {
if ( typeof Asyncify . handleSleep === "function"
&& typeof Asyncify . allocateData === "function"
&& ! Asyncify . _ _nestedHandleSleepInstalled ) {
// Stack of handleSleep contexts awaiting their allocateData association.
Asyncify . _ _pendingSleepContexts = [ ] ;
2026-07-31 20:34:14 +02:00
// Anomaly reporting (diagnostics only — behavior unchanged). This shim has
// been SILENTLY repairing currData aliasing between concurrent parks since
// it was written; production traps in exactly this family ("index out of
// bounds" / "unreachable executed" during doRewind) keep arriving with no
// way to tell whether the shim fired, mislinked, or was bypassed (fiber
// swaps don't allocate through allocateData). Make every repair and every
// concurrent-park window loud, so a saved console dump answers that.
// Rate-limited per kind: first 10 in full, then every 100th.
fix(asyncify): layer 3 — serialize root re-entry out of sleep-wake windows + flight recorder
v0.1.22 still trapped with BOTH guards silent: the fatal rewind's target is
the ROOT context, which layer 2 exempted. All four prod stacks are the same
collision — a fiber completes its yield-back to main while main's sleep-wake
rewind is still on the stack (maybeStopUnwind → trampoline →
finishContextSwitch → doRewind(root) → unreachable), two "resume main" paths
interleaved in one tick; the 8ms-earlier "index out of bounds" is the wake
side of the same event.
Root entry is legal and constant in healthy flow; only the wake-window
overlap is fatal. So: serialize, don't refuse. The shim marks the
synchronous wake window (Asyncify.__inSleepWake around wakeUp) and DEFERS a
root finishContextSwitch landing inside it by one macrotask
([wx-asyncify] root-entry-deferred beacon, trampoline retry) — an ordering
change only, nothing dropped. Suspension recording happens before the
deferral branch, so the yielding fiber's validity survives the wake chain
nulling currData.
Plus a flight recorder: a 96-entry ring of asyncify/fiber events (sleeps,
wakes, every context switch with ROOT/wake-depth, refusals, deferrals),
silent in normal operation, auto-dumped with full machine state next to the
first trap signature in the console; window.__wxAsyncifyDump() on demand.
The next prod export reads like a black box, not a stack-shape puzzle.
.ci-cache-epoch 3→4 (wasm cache key omits scripts/**).
Local: fiber-resume-park 2/2 (one refusal beacon), timer-park 1/1, sweep 20
passed, web fatal+follow 2/2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 14:06:53 +02:00
// Flight recorder: a capped ring of asyncify/fiber events (never printed
// during normal operation), dumped to the console ONCE when a trap
// signature surfaces — so a prod console export carries the exact event
// sequence and machine state at death instead of just stack shapes.
// window.__wxAsyncifyDump() returns it on demand.
var _ _recMax = 96 ;
Asyncify . _ _rec = [ ] ;
var _ _rec = function ( ev ) {
var r = Asyncify . _ _rec ;
r . push ( ( ( typeof performance !== "undefined" ? performance . now ( ) : 0 ) | 0 ) + " " + ev ) ;
if ( r . length > _ _recMax ) r . shift ( ) ;
} ;
Asyncify . _ _recPush = _ _rec ;
var _ _dumpState = function ( ) {
var F = ( typeof Fibers !== "undefined" ) ? Fibers : null ;
var pend = Array . isArray ( Asyncify . _ _pendingSleepContexts )
? Asyncify . _ _pendingSleepContexts . map ( function ( c ) { return c . capturedData || 0 ; } ) . join ( "," )
: "n/a" ;
var head = "[wx-asyncify] STATE"
+ " state=" + Asyncify . state
+ " currData=" + ( Asyncify . currData || 0 )
+ " inSleepWake=" + ( Asyncify . _ _inSleepWake || 0 )
+ " exportStack=" + ( Asyncify . exportCallStack ? Asyncify . exportCallStack . length : - 1 )
+ " pendingSleeps=[" + pend + "]"
+ ( F ? ( " nextFiber=" + F . nextFiber
+ " trampolining=" + F . trampolineRunning
+ " root=" + F . _ _rootFiber
2026-08-02 08:55:41 +02:00
+ " fcsTotal=" + ( F . _ _fcsTotal || 0 )
+ " rootHotTotal=" + ( F . _ _rootHotTotal || 0 )
fix(asyncify): layer 3 — serialize root re-entry out of sleep-wake windows + flight recorder
v0.1.22 still trapped with BOTH guards silent: the fatal rewind's target is
the ROOT context, which layer 2 exempted. All four prod stacks are the same
collision — a fiber completes its yield-back to main while main's sleep-wake
rewind is still on the stack (maybeStopUnwind → trampoline →
finishContextSwitch → doRewind(root) → unreachable), two "resume main" paths
interleaved in one tick; the 8ms-earlier "index out of bounds" is the wake
side of the same event.
Root entry is legal and constant in healthy flow; only the wake-window
overlap is fatal. So: serialize, don't refuse. The shim marks the
synchronous wake window (Asyncify.__inSleepWake around wakeUp) and DEFERS a
root finishContextSwitch landing inside it by one macrotask
([wx-asyncify] root-entry-deferred beacon, trampoline retry) — an ordering
change only, nothing dropped. Suspension recording happens before the
deferral branch, so the yielding fiber's validity survives the wake chain
nulling currData.
Plus a flight recorder: a 96-entry ring of asyncify/fiber events (sleeps,
wakes, every context switch with ROOT/wake-depth, refusals, deferrals),
silent in normal operation, auto-dumped with full machine state next to the
first trap signature in the console; window.__wxAsyncifyDump() on demand.
The next prod export reads like a black box, not a stack-shape puzzle.
.ci-cache-epoch 3→4 (wasm cache key omits scripts/**).
Local: fiber-resume-park 2/2 (one refusal beacon), timer-park 1/1, sweep 20
passed, web fatal+follow 2/2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 14:06:53 +02:00
+ " valid=[" + ( F . _ _validSuspensions ? Array . from ( F . _ _validSuspensions ) . join ( "," ) : "" ) + "]"
+ " parked=[" + ( F . _ _internallyParked ? Array . from ( F . _ _internallyParked ) . join ( "," ) : "" ) + "]"
+ " deferrals=" + ( F . _ _rootDeferrals || 0 ) )
: " (no Fibers)" ) ;
return head + "\n[wx-asyncify] RECORDER (oldest first):\n " + Asyncify . _ _rec . join ( "\n " ) ;
} ;
if ( typeof window !== "undefined" ) {
window . _ _wxAsyncifyDump = _ _dumpState ;
// Auto-dump beside the first trap signatures in the console — the one
// artifact prod reports reliably contain.
var _ _dumps = 0 ;
var _ _onTrap = function ( msg ) {
if ( _ _dumps >= 2 ) return ;
if ( ! /index out of bounds|unreachable executed|table index|indirect call signature|null function or function signature|memory access out of bounds/i . test ( msg ) ) return ;
++ _ _dumps ;
try { console . error ( _ _dumpState ( ) ) ; } catch ( e ) { }
} ;
window . addEventListener ( "error" , function ( e ) {
_ _onTrap ( e && e . error instanceof Error ? e . error . message : String ( ( e && e . message ) || "" ) ) ;
} ) ;
window . addEventListener ( "unhandledrejection" , function ( e ) {
_ _onTrap ( e && e . reason instanceof Error ? e . reason . message : String ( ( e && e . reason ) || "" ) ) ;
} ) ;
}
2026-07-31 20:34:14 +02:00
var _ _wxAsyncifyReport = ( function ( ) {
var counts = { } ;
return function ( kind , msg , withStack ) {
var n = ( counts [ kind ] = ( counts [ kind ] || 0 ) + 1 ) ;
if ( n > 10 && n % 100 !== 0 ) return ;
var line = "[wx-asyncify] " + kind + ": " + msg + " (occurrence " + n + ")" ;
if ( withStack ) {
// The stack names WHICH EM_ASYNC_JS parked (__asyncjs__wxWasmYieldToBrowser,
// startModal, js_enumerateFonts, ...) — the missing actor in every prod dump.
try { line += "\n" + String ( new Error ( ) . stack ) . split ( "\n" ) . slice ( 1 , 8 ) . join ( "\n" ) ; } catch ( e ) { }
}
console . warn ( line ) ;
} ;
} ) ( ) ;
2026-05-25 15:07:10 +02:00
var _ _originalAllocateData = Asyncify . allocateData . bind ( Asyncify ) ;
Asyncify . allocateData = function ( ) {
var ptr = _ _originalAllocateData ( ) ;
// Associate with the innermost pending handleSleep not yet linked.
for ( var i = Asyncify . _ _pendingSleepContexts . length - 1 ; i >= 0 ; -- i ) {
var ctx = Asyncify . _ _pendingSleepContexts [ i ] ;
if ( ! ctx . capturedData ) {
ctx . capturedData = ptr ;
break ;
}
}
return ptr ;
} ;
var _ _originalHandleSleep = Asyncify . handleSleep . bind ( Asyncify ) ;
Asyncify . handleSleep = function ( startAsync ) {
2026-07-31 20:34:14 +02:00
// A FRESH park (state 0 = Normal) starting while another chain's park is
// still live: the single-slot currData is about to be overwritten. The
// shim's restore below makes the POINTER survive, but nothing protects
// deeper state (fiber swaps, freed buffers, out-of-order wakes) — this
// window is where the trap family lives, and until now it was invisible.
// state 2 (Rewinding) entries are NOT reported: every resume legally
// re-enters handleSleep while rewinding with currData set (verified
// empirically 2026-07-31 — the timer-park e2e produced ~100/s of them
// on a healthy run).
fix(asyncify): layer 3 — serialize root re-entry out of sleep-wake windows + flight recorder
v0.1.22 still trapped with BOTH guards silent: the fatal rewind's target is
the ROOT context, which layer 2 exempted. All four prod stacks are the same
collision — a fiber completes its yield-back to main while main's sleep-wake
rewind is still on the stack (maybeStopUnwind → trampoline →
finishContextSwitch → doRewind(root) → unreachable), two "resume main" paths
interleaved in one tick; the 8ms-earlier "index out of bounds" is the wake
side of the same event.
Root entry is legal and constant in healthy flow; only the wake-window
overlap is fatal. So: serialize, don't refuse. The shim marks the
synchronous wake window (Asyncify.__inSleepWake around wakeUp) and DEFERS a
root finishContextSwitch landing inside it by one macrotask
([wx-asyncify] root-entry-deferred beacon, trampoline retry) — an ordering
change only, nothing dropped. Suspension recording happens before the
deferral branch, so the yielding fiber's validity survives the wake chain
nulling currData.
Plus a flight recorder: a 96-entry ring of asyncify/fiber events (sleeps,
wakes, every context switch with ROOT/wake-depth, refusals, deferrals),
silent in normal operation, auto-dumped with full machine state next to the
first trap signature in the console; window.__wxAsyncifyDump() on demand.
The next prod export reads like a black box, not a stack-shape puzzle.
.ci-cache-epoch 3→4 (wasm cache key omits scripts/**).
Local: fiber-resume-park 2/2 (one refusal beacon), timer-park 1/1, sweep 20
passed, web fatal+follow 2/2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 14:06:53 +02:00
_ _rec ( "sleep s=" + Asyncify . state + " cd=" + ( Asyncify . currData || 0 )
+ " w=" + ( Asyncify . _ _inSleepWake || 0 ) ) ;
2026-07-31 20:34:14 +02:00
if ( Asyncify . state === 0 && Asyncify . currData ) {
_ _wxAsyncifyReport (
"concurrent-park" ,
"handleSleep entered while currData=" + Asyncify . currData ,
true ) ;
}
if ( Asyncify . state === 1 ) {
// Parking while an UNWIND is literally in progress is never legal —
// if this ever fires it IS the bug.
_ _wxAsyncifyReport (
"reentrant-state" ,
"handleSleep entered mid-unwind (state=1) currData=" + Asyncify . currData ,
true ) ;
}
2026-08-01 19:49:38 +02:00
// Only a FRESH park (state 0) allocates data and needs tracking. The
// state-2 resume re-entry returns synchronously through the rewind
// branch — pushing a context for it leaks one per resume (the v0.1.23
// prod dump carried ~380 zero-linked pending contexts).
if ( Asyncify . state !== 0 ) {
return _ _originalHandleSleep ( startAsync ) ;
}
var sleepCtx = {
capturedData : null ,
cleanedUp : false ,
// Ownership: does this park belong to the ROOT chain (the main
// loop's yield, an embind entry) or to a fiber body's slice? Root
// re-entry during a ROOT-owned wake is the nested-self-rewind that
// kills prod; fiber-owned wakes completing into root are the benign
// bulk (see the fiber guard below).
rootOwned : ( typeof Fibers === "undefined" )
|| ( ! Fibers . _ _inFiberEntry
&& ! ( Asyncify . _ _wakingOwnerFiber || false ) )
} ;
2026-05-25 15:07:10 +02:00
Asyncify . _ _pendingSleepContexts . push ( sleepCtx ) ;
var cleanup = function ( ) {
if ( sleepCtx . cleanedUp ) return ;
sleepCtx . cleanedUp = true ;
var idx = Asyncify . _ _pendingSleepContexts . indexOf ( sleepCtx ) ;
if ( idx !== - 1 ) Asyncify . _ _pendingSleepContexts . splice ( idx , 1 ) ;
} ;
try {
return _ _originalHandleSleep ( function ( wakeUp ) {
return startAsync ( function ( result ) {
// wakeUp runs from pure JS on Promise resolution. Fiber swaps during
// the await may have overwritten Asyncify.currData. Restore OUR buffer
// so handleSleep's _asyncify_start_rewind and doRewind use it.
2026-08-01 19:49:38 +02:00
_ _rec ( "wake buf=" + ( sleepCtx . capturedData || 0 ) + " cdWas=" + ( Asyncify . currData || 0 )
+ ( sleepCtx . rootOwned ? " R" : " f" ) ) ;
2026-05-25 15:07:10 +02:00
if ( sleepCtx . capturedData ) {
2026-07-31 20:34:14 +02:00
if ( Asyncify . currData !== sleepCtx . capturedData ) {
// The repair firing. currData=null → the overlapping chain
// already completed (benign overlap, but COUNT it: it proves
// concurrent parks happen on this load). currData=<other> → a
// DIFFERENT chain is parked right now and we are rewinding
// around it — the dangerous interleave.
_ _wxAsyncifyReport (
Asyncify . currData ? "aliased-wake-live" : "overlapped-wake" ,
"restoring currData=" + sleepCtx . capturedData +
" over " + ( Asyncify . currData || "null" ) +
" state=" + Asyncify . state ,
! ! Asyncify . currData ) ;
}
2026-05-25 15:07:10 +02:00
Asyncify . currData = sleepCtx . capturedData ;
}
cleanup ( ) ;
fix(asyncify): layer 3 — serialize root re-entry out of sleep-wake windows + flight recorder
v0.1.22 still trapped with BOTH guards silent: the fatal rewind's target is
the ROOT context, which layer 2 exempted. All four prod stacks are the same
collision — a fiber completes its yield-back to main while main's sleep-wake
rewind is still on the stack (maybeStopUnwind → trampoline →
finishContextSwitch → doRewind(root) → unreachable), two "resume main" paths
interleaved in one tick; the 8ms-earlier "index out of bounds" is the wake
side of the same event.
Root entry is legal and constant in healthy flow; only the wake-window
overlap is fatal. So: serialize, don't refuse. The shim marks the
synchronous wake window (Asyncify.__inSleepWake around wakeUp) and DEFERS a
root finishContextSwitch landing inside it by one macrotask
([wx-asyncify] root-entry-deferred beacon, trampoline retry) — an ordering
change only, nothing dropped. Suspension recording happens before the
deferral branch, so the yielding fiber's validity survives the wake chain
nulling currData.
Plus a flight recorder: a 96-entry ring of asyncify/fiber events (sleeps,
wakes, every context switch with ROOT/wake-depth, refusals, deferrals),
silent in normal operation, auto-dumped with full machine state next to the
first trap signature in the console; window.__wxAsyncifyDump() on demand.
The next prod export reads like a black box, not a stack-shape puzzle.
.ci-cache-epoch 3→4 (wasm cache key omits scripts/**).
Local: fiber-resume-park 2/2 (one refusal beacon), timer-park 1/1, sweep 20
passed, web fatal+follow 2/2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 14:06:53 +02:00
// Mark the synchronous wake window: everything below wakeUp() —
// the rewind, the resumed code running forward, its next unwind —
// executes inside it. A fiber completion whose root-entry lands
// in this window rewinds the root WHILE the wake's own rewind is
// in flight (the four identical prod trap stacks:
// maybeStopUnwind → trampoline → finishContextSwitch →
// doRewind(root) → unreachable). The stale-fiber guard below
// defers such root entries by one macrotask.
Asyncify . _ _inSleepWake = ( Asyncify . _ _inSleepWake || 0 ) + 1 ;
2026-08-01 19:49:38 +02:00
var prevWakingOwnerFiber = Asyncify . _ _wakingOwnerFiber || false ;
Asyncify . _ _wakingOwnerFiber = ! sleepCtx . rootOwned ;
var prevWakingRoot = Asyncify . _ _wakingRoot || 0 ;
if ( sleepCtx . rootOwned ) Asyncify . _ _wakingRoot = ( Asyncify . _ _wakingRoot || 0 ) + 1 ;
2026-06-12 16:59:07 +02:00
try {
return wakeUp ( result ) ;
} catch ( e ) {
// emscripten_set_main_loop(...,1) parks main() by throwing the
// "unwind" sentinel. When main's LAST pre-park suspension was a
// sleep, main is resumed from THIS wakeUp, so the sentinel
// propagates here instead of into callMain's catch — surfacing as
// an uncaught "unwind" promise rejection. Swallow it exactly like
// callMain/handleException do on the direct path.
if ( e === "unwind" ) {
return ;
}
throw e ;
fix(asyncify): layer 3 — serialize root re-entry out of sleep-wake windows + flight recorder
v0.1.22 still trapped with BOTH guards silent: the fatal rewind's target is
the ROOT context, which layer 2 exempted. All four prod stacks are the same
collision — a fiber completes its yield-back to main while main's sleep-wake
rewind is still on the stack (maybeStopUnwind → trampoline →
finishContextSwitch → doRewind(root) → unreachable), two "resume main" paths
interleaved in one tick; the 8ms-earlier "index out of bounds" is the wake
side of the same event.
Root entry is legal and constant in healthy flow; only the wake-window
overlap is fatal. So: serialize, don't refuse. The shim marks the
synchronous wake window (Asyncify.__inSleepWake around wakeUp) and DEFERS a
root finishContextSwitch landing inside it by one macrotask
([wx-asyncify] root-entry-deferred beacon, trampoline retry) — an ordering
change only, nothing dropped. Suspension recording happens before the
deferral branch, so the yielding fiber's validity survives the wake chain
nulling currData.
Plus a flight recorder: a 96-entry ring of asyncify/fiber events (sleeps,
wakes, every context switch with ROOT/wake-depth, refusals, deferrals),
silent in normal operation, auto-dumped with full machine state next to the
first trap signature in the console; window.__wxAsyncifyDump() on demand.
The next prod export reads like a black box, not a stack-shape puzzle.
.ci-cache-epoch 3→4 (wasm cache key omits scripts/**).
Local: fiber-resume-park 2/2 (one refusal beacon), timer-park 1/1, sweep 20
passed, web fatal+follow 2/2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 14:06:53 +02:00
} finally {
Asyncify . _ _inSleepWake -= 1 ;
2026-08-01 19:49:38 +02:00
Asyncify . _ _wakingOwnerFiber = prevWakingOwnerFiber ;
if ( sleepCtx . rootOwned ) Asyncify . _ _wakingRoot = prevWakingRoot ;
2026-06-12 16:59:07 +02:00
}
2026-05-25 15:07:10 +02:00
} ) ;
} ) ;
} catch ( e ) {
cleanup ( ) ;
throw e ;
}
} ;
Asyncify . _ _nestedHandleSleepInstalled = true ;
}
}
// === End nested-Asyncify handleSleep fix ===
fix(asyncify): stale-fiber-rewind guard — layer 2, attribution-proof
v0.1.21 still trapped with ZERO jump-refused beacons: the fatal swap PASSED
the C++ swap_suspended guard. Mechanism (async/16 round 2): a fresh JS entry
executing while g_current_context still points at a parked fiber gets
attributed to that fiber — fiber_swap writes a fresh, valid-LOOKING foreign
suspension into the parked fiber's struct and re-marks the flag. The flag
lies; the resume rewinds garbage.
This guard tracks truth at the emscripten-fiber layer (handlesleep.js wraps
Fibers.finishContextSwitch):
- valid suspensions = real swap-outs (currData == oldFiber+20 when the
trampoline runs), consumed on rewind;
- internally-parked = an entered slice that ended in a handleSleep park
(currData set, no nextFiber) — quarantined until a GENUINE swap-out,
where genuine means the fiber's pending sleep has resolved
(__pendingSleepContexts), so a laundering write cannot lift it;
- entering a quarantined or suspension-less fiber is REFUSED
([wx-asyncify] fiber-resume-refused, ghost contract).
The ROOT context is exempt from quarantine and refusal: its rewound
continuation runs the whole main loop, whose routine yield park says nothing
about a fiber body — the first build of this guard quarantined main off that
signal and starved every coroutine return (19 collab e2e reds, empty
results). Root = the old side of the first switch ever.
.ci-cache-epoch 2→3: the wasm output cache key omits scripts/**.
Red/green: fiber-resume-park.spec.ts scenario 2 (laundered resume → exactly
one refusal beacon, both coroutines complete); full fiber-heavy sweep green
(21 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 10:05:27 +02:00
// === Stale-fiber-rewind guard (the decoded 2026-07/08 prod board-load trap) ===
//
// A fiber whose body asyncify-parks inside handleSleep is suspended in a way
// the fiber machinery cannot see: its struct still holds the CONSUMED data of
// its last real swap-out. The C++ libcontext guard (swap_suspended) closes the
// simple case, but caller attribution can be poisoned — a fresh JS entry that
// jumps while g_current_context still points at a parked fiber writes a fresh
// suspension INTO that parked fiber's struct, so the flag lies. This guard is
// attribution-proof: it tracks validity at the emscripten-fiber layer itself.
//
// A fiber becomes safely resumable ONLY when a real swap-out writes its
// suspension — observable here because fiber_swap sets Asyncify.currData to
// oldFiber's asyncify data (fiber+20) and finishContextSwitch runs before
// anything else touches it. Consuming a suspension (the rewind path) removes
// it. A suspended-path entry for a fiber with NO live suspension is exactly
// the stale rewind that produced "unreachable executed" + a poisoned runtime
// (docs/features/async/16) — REFUSE it: the dropped dispatch ghost-resolves
// (the jump-ghost contract), the parked body completes via its own wake.
if ( typeof Fibers !== "undefined"
&& typeof Fibers . finishContextSwitch === "function"
&& ! Fibers . _ _staleRewindGuardInstalled ) {
// Fibers whose last swap-out wrote a live (unconsumed) suspension.
Fibers . _ _validSuspensions = new Set ( ) ;
// Fibers whose last slice ended in a handleSleep park instead of a swap-out:
// their body is mid-sleep, so entering them is unsafe no matter what their
// struct holds (a misattributed jump may have written a valid-LOOKING
// foreign suspension into it).
Fibers . _ _internallyParked = new Set ( ) ;
// fiber → the sleep buffer its internal park is waiting on. A LATER
// "swap-out" of that fiber is genuine only if this sleep has resolved
// (its context left __pendingSleepContexts) — a misattributed jump from a
// fresh JS entry writes the fiber's struct while the sleep is still
// pending, and must not launder the fiber back into the valid set.
Fibers . _ _parkSleepBuf = new Map ( ) ;
var _ _origFinishContextSwitch = Fibers . finishContextSwitch . bind ( Fibers ) ;
var _ _fiberRefusals = 0 ;
var _ _refuseFiber = function ( newFiber , why ) {
fix(asyncify): layer 3 — serialize root re-entry out of sleep-wake windows + flight recorder
v0.1.22 still trapped with BOTH guards silent: the fatal rewind's target is
the ROOT context, which layer 2 exempted. All four prod stacks are the same
collision — a fiber completes its yield-back to main while main's sleep-wake
rewind is still on the stack (maybeStopUnwind → trampoline →
finishContextSwitch → doRewind(root) → unreachable), two "resume main" paths
interleaved in one tick; the 8ms-earlier "index out of bounds" is the wake
side of the same event.
Root entry is legal and constant in healthy flow; only the wake-window
overlap is fatal. So: serialize, don't refuse. The shim marks the
synchronous wake window (Asyncify.__inSleepWake around wakeUp) and DEFERS a
root finishContextSwitch landing inside it by one macrotask
([wx-asyncify] root-entry-deferred beacon, trampoline retry) — an ordering
change only, nothing dropped. Suspension recording happens before the
deferral branch, so the yielding fiber's validity survives the wake chain
nulling currData.
Plus a flight recorder: a 96-entry ring of asyncify/fiber events (sleeps,
wakes, every context switch with ROOT/wake-depth, refusals, deferrals),
silent in normal operation, auto-dumped with full machine state next to the
first trap signature in the console; window.__wxAsyncifyDump() on demand.
The next prod export reads like a black box, not a stack-shape puzzle.
.ci-cache-epoch 3→4 (wasm cache key omits scripts/**).
Local: fiber-resume-park 2/2 (one refusal beacon), timer-park 1/1, sweep 20
passed, web fatal+follow 2/2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 14:06:53 +02:00
_ _fcsRec ( "refuse new=" + newFiber ) ;
fix(asyncify): stale-fiber-rewind guard — layer 2, attribution-proof
v0.1.21 still trapped with ZERO jump-refused beacons: the fatal swap PASSED
the C++ swap_suspended guard. Mechanism (async/16 round 2): a fresh JS entry
executing while g_current_context still points at a parked fiber gets
attributed to that fiber — fiber_swap writes a fresh, valid-LOOKING foreign
suspension into the parked fiber's struct and re-marks the flag. The flag
lies; the resume rewinds garbage.
This guard tracks truth at the emscripten-fiber layer (handlesleep.js wraps
Fibers.finishContextSwitch):
- valid suspensions = real swap-outs (currData == oldFiber+20 when the
trampoline runs), consumed on rewind;
- internally-parked = an entered slice that ended in a handleSleep park
(currData set, no nextFiber) — quarantined until a GENUINE swap-out,
where genuine means the fiber's pending sleep has resolved
(__pendingSleepContexts), so a laundering write cannot lift it;
- entering a quarantined or suspension-less fiber is REFUSED
([wx-asyncify] fiber-resume-refused, ghost contract).
The ROOT context is exempt from quarantine and refusal: its rewound
continuation runs the whole main loop, whose routine yield park says nothing
about a fiber body — the first build of this guard quarantined main off that
signal and starved every coroutine return (19 collab e2e reds, empty
results). Root = the old side of the first switch ever.
.ci-cache-epoch 2→3: the wasm output cache key omits scripts/**.
Red/green: fiber-resume-park.spec.ts scenario 2 (laundered resume → exactly
one refusal beacon, both coroutines complete); full fiber-heavy sweep green
(21 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 10:05:27 +02:00
++ _ _fiberRefusals ;
if ( _ _fiberRefusals <= 10 || _ _fiberRefusals % 100 === 0 ) {
console . warn ( "[wx-asyncify] fiber-resume-refused: fiber=" + newFiber + " " + why
+ " (occurrence " + _ _fiberRefusals + ")" ) ;
}
// No context is entered. The unwind that got us here already completed
// (state Normal); clear the dangling currData so the next fresh park
// does not read a foreign pointer.
Asyncify . currData = null ;
} ;
fix(asyncify): layer 3 — serialize root re-entry out of sleep-wake windows + flight recorder
v0.1.22 still trapped with BOTH guards silent: the fatal rewind's target is
the ROOT context, which layer 2 exempted. All four prod stacks are the same
collision — a fiber completes its yield-back to main while main's sleep-wake
rewind is still on the stack (maybeStopUnwind → trampoline →
finishContextSwitch → doRewind(root) → unreachable), two "resume main" paths
interleaved in one tick; the 8ms-earlier "index out of bounds" is the wake
side of the same event.
Root entry is legal and constant in healthy flow; only the wake-window
overlap is fatal. So: serialize, don't refuse. The shim marks the
synchronous wake window (Asyncify.__inSleepWake around wakeUp) and DEFERS a
root finishContextSwitch landing inside it by one macrotask
([wx-asyncify] root-entry-deferred beacon, trampoline retry) — an ordering
change only, nothing dropped. Suspension recording happens before the
deferral branch, so the yielding fiber's validity survives the wake chain
nulling currData.
Plus a flight recorder: a 96-entry ring of asyncify/fiber events (sleeps,
wakes, every context switch with ROOT/wake-depth, refusals, deferrals),
silent in normal operation, auto-dumped with full machine state next to the
first trap signature in the console; window.__wxAsyncifyDump() on demand.
The next prod export reads like a black box, not a stack-shape puzzle.
.ci-cache-epoch 3→4 (wasm cache key omits scripts/**).
Local: fiber-resume-park 2/2 (one refusal beacon), timer-park 1/1, sweep 20
passed, web fatal+follow 2/2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 14:06:53 +02:00
var _ _fcsRec = ( typeof Asyncify !== "undefined" && Asyncify . _ _recPush )
? Asyncify . _ _recPush
: function ( ) { } ;
fix(asyncify): stale-fiber-rewind guard — layer 2, attribution-proof
v0.1.21 still trapped with ZERO jump-refused beacons: the fatal swap PASSED
the C++ swap_suspended guard. Mechanism (async/16 round 2): a fresh JS entry
executing while g_current_context still points at a parked fiber gets
attributed to that fiber — fiber_swap writes a fresh, valid-LOOKING foreign
suspension into the parked fiber's struct and re-marks the flag. The flag
lies; the resume rewinds garbage.
This guard tracks truth at the emscripten-fiber layer (handlesleep.js wraps
Fibers.finishContextSwitch):
- valid suspensions = real swap-outs (currData == oldFiber+20 when the
trampoline runs), consumed on rewind;
- internally-parked = an entered slice that ended in a handleSleep park
(currData set, no nextFiber) — quarantined until a GENUINE swap-out,
where genuine means the fiber's pending sleep has resolved
(__pendingSleepContexts), so a laundering write cannot lift it;
- entering a quarantined or suspension-less fiber is REFUSED
([wx-asyncify] fiber-resume-refused, ghost contract).
The ROOT context is exempt from quarantine and refusal: its rewound
continuation runs the whole main loop, whose routine yield park says nothing
about a fiber body — the first build of this guard quarantined main off that
signal and starved every coroutine return (19 collab e2e reds, empty
results). Root = the old side of the first switch ever.
.ci-cache-epoch 2→3: the wasm output cache key omits scripts/**.
Red/green: fiber-resume-park.spec.ts scenario 2 (laundered resume → exactly
one refusal beacon, both coroutines complete); full fiber-heavy sweep green
(21 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 10:05:27 +02:00
Fibers . finishContextSwitch = function ( newFiber ) {
2026-08-02 08:55:41 +02:00
// Cumulative, scroll-proof counters (the 96-event ring holds <1s at idle
// tick rate — differential-repro dose measurements need totals).
Fibers . _ _fcsTotal = ( Fibers . _ _fcsTotal || 0 ) + 1 ;
if ( newFiber === Fibers . _ _rootFiber && ( Asyncify . _ _inSleepWake || 0 ) > 0 ) {
Fibers . _ _rootHotTotal = ( Fibers . _ _rootHotTotal || 0 ) + 1 ;
}
fix(asyncify): layer 3 — serialize root re-entry out of sleep-wake windows + flight recorder
v0.1.22 still trapped with BOTH guards silent: the fatal rewind's target is
the ROOT context, which layer 2 exempted. All four prod stacks are the same
collision — a fiber completes its yield-back to main while main's sleep-wake
rewind is still on the stack (maybeStopUnwind → trampoline →
finishContextSwitch → doRewind(root) → unreachable), two "resume main" paths
interleaved in one tick; the 8ms-earlier "index out of bounds" is the wake
side of the same event.
Root entry is legal and constant in healthy flow; only the wake-window
overlap is fatal. So: serialize, don't refuse. The shim marks the
synchronous wake window (Asyncify.__inSleepWake around wakeUp) and DEFERS a
root finishContextSwitch landing inside it by one macrotask
([wx-asyncify] root-entry-deferred beacon, trampoline retry) — an ordering
change only, nothing dropped. Suspension recording happens before the
deferral branch, so the yielding fiber's validity survives the wake chain
nulling currData.
Plus a flight recorder: a 96-entry ring of asyncify/fiber events (sleeps,
wakes, every context switch with ROOT/wake-depth, refusals, deferrals),
silent in normal operation, auto-dumped with full machine state next to the
first trap signature in the console; window.__wxAsyncifyDump() on demand.
The next prod export reads like a black box, not a stack-shape puzzle.
.ci-cache-epoch 3→4 (wasm cache key omits scripts/**).
Local: fiber-resume-park 2/2 (one refusal beacon), timer-park 1/1, sweep 20
passed, web fatal+follow 2/2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 14:06:53 +02:00
_ _fcsRec ( "fcs old=" + ( Asyncify . currData ? Asyncify . currData - 20 : 0 )
+ " new=" + newFiber
+ ( newFiber === Fibers . _ _rootFiber ? " ROOT" : "" )
+ " w=" + ( Asyncify . _ _inSleepWake || 0 ) ) ;
fix(asyncify): stale-fiber-rewind guard — layer 2, attribution-proof
v0.1.21 still trapped with ZERO jump-refused beacons: the fatal swap PASSED
the C++ swap_suspended guard. Mechanism (async/16 round 2): a fresh JS entry
executing while g_current_context still points at a parked fiber gets
attributed to that fiber — fiber_swap writes a fresh, valid-LOOKING foreign
suspension into the parked fiber's struct and re-marks the flag. The flag
lies; the resume rewinds garbage.
This guard tracks truth at the emscripten-fiber layer (handlesleep.js wraps
Fibers.finishContextSwitch):
- valid suspensions = real swap-outs (currData == oldFiber+20 when the
trampoline runs), consumed on rewind;
- internally-parked = an entered slice that ended in a handleSleep park
(currData set, no nextFiber) — quarantined until a GENUINE swap-out,
where genuine means the fiber's pending sleep has resolved
(__pendingSleepContexts), so a laundering write cannot lift it;
- entering a quarantined or suspension-less fiber is REFUSED
([wx-asyncify] fiber-resume-refused, ghost contract).
The ROOT context is exempt from quarantine and refusal: its rewound
continuation runs the whole main loop, whose routine yield park says nothing
about a fiber body — the first build of this guard quarantined main off that
signal and starved every coroutine return (19 collab e2e reds, empty
results). Root = the old side of the first switch ever.
.ci-cache-epoch 2→3: the wasm output cache key omits scripts/**.
Red/green: fiber-resume-park.spec.ts scenario 2 (laundered resume → exactly
one refusal beacon, both coroutines complete); full fiber-heavy sweep green
(21 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 10:05:27 +02:00
// The swap that scheduled this switch just suspended its old fiber and
// left currData = oldFiber+20 (fiber_swap's unwind path); record that
// suspension as live — and a GENUINE swap-out also ends any internal
// park. Genuine means the fiber's pending sleep (if any) has resolved;
// otherwise this is a misattributed fresh-entry jump writing into a
// parked fiber's struct, and the fiber must stay quarantined.
// finishContextSwitch only runs for genuine fiber switches, so currData
// here is never a handleSleep buffer.
if ( Asyncify . currData ) {
var oldFiber = Asyncify . currData - 20 ;
// The very first switch is always main → coroutine: remember the ROOT
// context. The root is exempt from quarantine below — after a rewind
// into it, execution continues into the whole main loop (which parks in
// its yield as a matter of course); reading that park as "the entered
// fiber is mid-body" quarantined MAIN and starved every coroutine
// return (empty collab results across the board on the first build of
// this guard).
if ( Fibers . _ _rootFiber === undefined ) {
Fibers . _ _rootFiber = oldFiber ;
}
var parkBuf = Fibers . _ _parkSleepBuf . get ( oldFiber ) ;
var stillParked = parkBuf !== undefined
&& Array . isArray ( Asyncify . _ _pendingSleepContexts )
&& Asyncify . _ _pendingSleepContexts . some ( function ( c ) { return c . capturedData === parkBuf ; } ) ;
if ( ! stillParked ) {
Fibers . _ _validSuspensions . add ( oldFiber ) ;
Fibers . _ _internallyParked . delete ( oldFiber ) ;
Fibers . _ _parkSleepBuf . delete ( oldFiber ) ;
}
}
var isRoot = newFiber === Fibers . _ _rootFiber ;
2026-08-01 19:49:38 +02:00
fix(asyncify): retire the deferral family — guard-layer road closed
v0.1.24 in prod, doubly convicted the same morning: the Leonardo open
crawled/hung (open:settled result=failed at the 60s escape, heap never past
256MB — every main-loop iteration runs INSIDE its yield-wake extent, so the
"root-owned wake" scope matched thousands of legitimate nested coroutine
Call/returns per open, each paying a deferred macrotask, throttled to ≥1s in
a background tab), AND the Nano crashed 22ms after deferrals=1 fired.
Harmful and insufficient: the fatal nested-rewind interleave and the benign
bulk are observationally identical at this layer — no discriminator exists.
Retired (second and final retraction, async/16 round 5). What stays shipped
and clean: consume-once root suspensions, the internally-parked quarantine +
laundering check, the flight recorder + beacons, the WSOD floor, the
pendingSleeps leak fix (confirmed by pendingSleeps=[] in the Nano dump). The
rare nested-rewind crash is ACCEPTED and fully observable until the
structural fix — the design-B fiber-first runtime (async/06,12,13), where
one scheduler owns every suspension and this interleave cannot exist.
.ci-cache-epoch 6→7.
Local: fiber 2/2 (one refusal beacon) + timer + firefox sweep 21 passed,
chromium scenarios 11 passed/4 quarantine-skips, web fatal+follow 2/2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-02 08:18:08 +02:00
// DEFERRAL RETIRED (2026-08-02, second retraction — see async/16 round 5).
// Both deferral variants are unsound: the main loop's every iteration runs
// INSIDE its yield-wake's synchronous extent, so "root re-entry during a
// root-owned wake" also matches every legit nested coroutine Call/return
// in a board open — v0.1.24 deferred thousands of them per load and the
// open crawled/hung (open:settled result=failed at the 60s escape,
// "hung forever" with a throttled background tab). The fatal interleave
// and the benign bulk share the same observable signature at this layer;
// the discriminator does not exist here. The rare nested-rewind crash is
// accepted until the fiber-first runtime (design B) removes the dual
// suspension protocols altogether; the recorder keeps every occurrence
// fully observable.
2026-08-01 19:49:38 +02:00
fix(asyncify): stale-fiber-rewind guard — layer 2, attribution-proof
v0.1.21 still trapped with ZERO jump-refused beacons: the fatal swap PASSED
the C++ swap_suspended guard. Mechanism (async/16 round 2): a fresh JS entry
executing while g_current_context still points at a parked fiber gets
attributed to that fiber — fiber_swap writes a fresh, valid-LOOKING foreign
suspension into the parked fiber's struct and re-marks the flag. The flag
lies; the resume rewinds garbage.
This guard tracks truth at the emscripten-fiber layer (handlesleep.js wraps
Fibers.finishContextSwitch):
- valid suspensions = real swap-outs (currData == oldFiber+20 when the
trampoline runs), consumed on rewind;
- internally-parked = an entered slice that ended in a handleSleep park
(currData set, no nextFiber) — quarantined until a GENUINE swap-out,
where genuine means the fiber's pending sleep has resolved
(__pendingSleepContexts), so a laundering write cannot lift it;
- entering a quarantined or suspension-less fiber is REFUSED
([wx-asyncify] fiber-resume-refused, ghost contract).
The ROOT context is exempt from quarantine and refusal: its rewound
continuation runs the whole main loop, whose routine yield park says nothing
about a fiber body — the first build of this guard quarantined main off that
signal and starved every coroutine return (19 collab e2e reds, empty
results). Root = the old side of the first switch ever.
.ci-cache-epoch 2→3: the wasm output cache key omits scripts/**.
Red/green: fiber-resume-park.spec.ts scenario 2 (laundered resume → exactly
one refusal beacon, both coroutines complete); full fiber-heavy sweep green
(21 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 10:05:27 +02:00
var HEAPU32v = ( typeof GROWABLE _HEAP _U32 === "function" ) ? GROWABLE _HEAP _U32 ( ) : HEAPU32 ;
var entryPoint = HEAPU32v [ ( ( newFiber + 12 ) >>> 2 ) >>> 0 ] ;
if ( ! isRoot && Fibers . _ _internallyParked . has ( newFiber ) ) {
2026-08-01 15:53:18 +02:00
// Root is exempt from THIS check only: it "parks" in the main loop's
// yield as a matter of course (quarantining it starved every coroutine
// return — 19 collab e2e reds on the first guard build).
fix(asyncify): stale-fiber-rewind guard — layer 2, attribution-proof
v0.1.21 still trapped with ZERO jump-refused beacons: the fatal swap PASSED
the C++ swap_suspended guard. Mechanism (async/16 round 2): a fresh JS entry
executing while g_current_context still points at a parked fiber gets
attributed to that fiber — fiber_swap writes a fresh, valid-LOOKING foreign
suspension into the parked fiber's struct and re-marks the flag. The flag
lies; the resume rewinds garbage.
This guard tracks truth at the emscripten-fiber layer (handlesleep.js wraps
Fibers.finishContextSwitch):
- valid suspensions = real swap-outs (currData == oldFiber+20 when the
trampoline runs), consumed on rewind;
- internally-parked = an entered slice that ended in a handleSleep park
(currData set, no nextFiber) — quarantined until a GENUINE swap-out,
where genuine means the fiber's pending sleep has resolved
(__pendingSleepContexts), so a laundering write cannot lift it;
- entering a quarantined or suspension-less fiber is REFUSED
([wx-asyncify] fiber-resume-refused, ghost contract).
The ROOT context is exempt from quarantine and refusal: its rewound
continuation runs the whole main loop, whose routine yield park says nothing
about a fiber body — the first build of this guard quarantined main off that
signal and starved every coroutine return (19 collab e2e reds, empty
results). Root = the old side of the first switch ever.
.ci-cache-epoch 2→3: the wasm output cache key omits scripts/**.
Red/green: fiber-resume-park.spec.ts scenario 2 (laundered resume → exactly
one refusal beacon, both coroutines complete); full fiber-heavy sweep green
(21 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 10:05:27 +02:00
_ _refuseFiber ( newFiber , "is asyncify-parked mid-body (sleep in flight)" ) ;
return ;
}
2026-08-01 15:53:18 +02:00
if ( entryPoint === 0 ) {
// Suspended-fiber path: about to rewind newFiber+20 — root INCLUDED.
// Consume-once semantics are the actual prod killer's cure (all four
// trap stacks, v0.1.19– 22): each fiber_swap suspension is rewindable
// exactly once. Two fibers completing against ONE root suspension
// epoch (a tool fiber and a collab fiber both waking around
// open:settled) makes the second finishContextSwitch(root) rewind
// already-consumed data → "unreachable executed" → poisoned runtime.
// Refusing the second consumption loses nothing: the fiber that
// yielded stays properly suspended (recorded above), and the root
// continues via its real pending resume (its own sleep wake or the
// next fresh JS entry) — the same contract as libcontext's
// ghost-resume epochs, enforced one layer lower.
fix(asyncify): stale-fiber-rewind guard — layer 2, attribution-proof
v0.1.21 still trapped with ZERO jump-refused beacons: the fatal swap PASSED
the C++ swap_suspended guard. Mechanism (async/16 round 2): a fresh JS entry
executing while g_current_context still points at a parked fiber gets
attributed to that fiber — fiber_swap writes a fresh, valid-LOOKING foreign
suspension into the parked fiber's struct and re-marks the flag. The flag
lies; the resume rewinds garbage.
This guard tracks truth at the emscripten-fiber layer (handlesleep.js wraps
Fibers.finishContextSwitch):
- valid suspensions = real swap-outs (currData == oldFiber+20 when the
trampoline runs), consumed on rewind;
- internally-parked = an entered slice that ended in a handleSleep park
(currData set, no nextFiber) — quarantined until a GENUINE swap-out,
where genuine means the fiber's pending sleep has resolved
(__pendingSleepContexts), so a laundering write cannot lift it;
- entering a quarantined or suspension-less fiber is REFUSED
([wx-asyncify] fiber-resume-refused, ghost contract).
The ROOT context is exempt from quarantine and refusal: its rewound
continuation runs the whole main loop, whose routine yield park says nothing
about a fiber body — the first build of this guard quarantined main off that
signal and starved every coroutine return (19 collab e2e reds, empty
results). Root = the old side of the first switch ever.
.ci-cache-epoch 2→3: the wasm output cache key omits scripts/**.
Red/green: fiber-resume-park.spec.ts scenario 2 (laundered resume → exactly
one refusal beacon, both coroutines complete); full fiber-heavy sweep green
(21 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 10:05:27 +02:00
if ( ! Fibers . _ _validSuspensions . has ( newFiber ) ) {
2026-08-01 15:53:18 +02:00
_ _refuseFiber ( newFiber , isRoot
? "root suspension already consumed - a second rewind would replay stale frames"
: "has no live suspension - rewinding would replay stale data" ) ;
fix(asyncify): stale-fiber-rewind guard — layer 2, attribution-proof
v0.1.21 still trapped with ZERO jump-refused beacons: the fatal swap PASSED
the C++ swap_suspended guard. Mechanism (async/16 round 2): a fresh JS entry
executing while g_current_context still points at a parked fiber gets
attributed to that fiber — fiber_swap writes a fresh, valid-LOOKING foreign
suspension into the parked fiber's struct and re-marks the flag. The flag
lies; the resume rewinds garbage.
This guard tracks truth at the emscripten-fiber layer (handlesleep.js wraps
Fibers.finishContextSwitch):
- valid suspensions = real swap-outs (currData == oldFiber+20 when the
trampoline runs), consumed on rewind;
- internally-parked = an entered slice that ended in a handleSleep park
(currData set, no nextFiber) — quarantined until a GENUINE swap-out,
where genuine means the fiber's pending sleep has resolved
(__pendingSleepContexts), so a laundering write cannot lift it;
- entering a quarantined or suspension-less fiber is REFUSED
([wx-asyncify] fiber-resume-refused, ghost contract).
The ROOT context is exempt from quarantine and refusal: its rewound
continuation runs the whole main loop, whose routine yield park says nothing
about a fiber body — the first build of this guard quarantined main off that
signal and starved every coroutine return (19 collab e2e reds, empty
results). Root = the old side of the first switch ever.
.ci-cache-epoch 2→3: the wasm output cache key omits scripts/**.
Red/green: fiber-resume-park.spec.ts scenario 2 (laundered resume → exactly
one refusal beacon, both coroutines complete); full fiber-heavy sweep green
(21 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 10:05:27 +02:00
return ;
}
Fibers . _ _validSuspensions . delete ( newFiber ) ;
}
2026-08-01 19:49:38 +02:00
// Sleeps started inside an entered FIBER's slice are fiber-owned (see the
// handleSleep wrapper's rootOwned tag). Root entries don't count: the
// main loop's continuation after a root rewind is root-owned by
// definition — that's exactly the chain whose wake must not be re-entered.
if ( ! isRoot ) Fibers . _ _inFiberEntry = ( Fibers . _ _inFiberEntry || 0 ) + 1 ;
var ret ;
try {
ret = _ _origFinishContextSwitch ( newFiber ) ;
} finally {
if ( ! isRoot ) Fibers . _ _inFiberEntry -= 1 ;
}
fix(asyncify): stale-fiber-rewind guard — layer 2, attribution-proof
v0.1.21 still trapped with ZERO jump-refused beacons: the fatal swap PASSED
the C++ swap_suspended guard. Mechanism (async/16 round 2): a fresh JS entry
executing while g_current_context still points at a parked fiber gets
attributed to that fiber — fiber_swap writes a fresh, valid-LOOKING foreign
suspension into the parked fiber's struct and re-marks the flag. The flag
lies; the resume rewinds garbage.
This guard tracks truth at the emscripten-fiber layer (handlesleep.js wraps
Fibers.finishContextSwitch):
- valid suspensions = real swap-outs (currData == oldFiber+20 when the
trampoline runs), consumed on rewind;
- internally-parked = an entered slice that ended in a handleSleep park
(currData set, no nextFiber) — quarantined until a GENUINE swap-out,
where genuine means the fiber's pending sleep has resolved
(__pendingSleepContexts), so a laundering write cannot lift it;
- entering a quarantined or suspension-less fiber is REFUSED
([wx-asyncify] fiber-resume-refused, ghost contract).
The ROOT context is exempt from quarantine and refusal: its rewound
continuation runs the whole main loop, whose routine yield park says nothing
about a fiber body — the first build of this guard quarantined main off that
signal and starved every coroutine return (19 collab e2e reds, empty
results). Root = the old side of the first switch ever.
.ci-cache-epoch 2→3: the wasm output cache key omits scripts/**.
Red/green: fiber-resume-park.spec.ts scenario 2 (laundered resume → exactly
one refusal beacon, both coroutines complete); full fiber-heavy sweep green
(21 passed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 10:05:27 +02:00
// How did the entered fiber's synchronous slice end? Another fiber swap
// (nextFiber set — the trampoline loop continues, proper suspension) or a
// handleSleep park (currData holds a sleep buffer — the body is mid-sleep
// and must not be entered until it properly swaps out). Never applied to
// the root: its rewound continuation runs the whole main loop, whose
// routine yield park says nothing about a fiber body.
if ( ! isRoot && ! Fibers . nextFiber && Asyncify . currData ) {
Fibers . _ _internallyParked . add ( newFiber ) ;
Fibers . _ _parkSleepBuf . set ( newFiber , Asyncify . currData ) ;
}
return ret ;
} ;
Fibers . _ _staleRewindGuardInstalled = true ;
}
// === End stale-fiber-rewind guard ===