pcbjam/scripts/common/shims/handlesleep.js

398 lines
20 KiB
JavaScript
Raw Normal View History

// === 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 = [];
// 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
+ " 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) || ""));
});
}
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);
};
})();
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) {
// 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));
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);
}
fix(asyncify): ownership-scoped root deferral — the recorded nested self-rewind, cured The v0.1.23 flight recorder caught the kill live (console-export-2026-8-1_19-16-8): dozens of benign fiber round-trips at w=0, the yield cycling healthily on its buffer — then "fcs … ROOT w=1" and the trap, state frozen at Rewinding with currData=root+20. The fatal condition, observed rather than inferred: a fiber round-trip inside the ROOT's OWN sleep-wake continuation re-suspends and re-rewinds the root nested inside its live wake rewind. Consume-once passed correctly — it guards a different corruption and stays. The round-3 deferral was aimed right but unscoped (taxed fiber-owned wakes, flaked S4). Final form: every fresh sleep is tagged root- or fiber-owned (fiber ⇔ started inside a finishContextSwitch fiber slice or a fiber-owned wake; root entries don't count as slices); finishContextSwitch(root) defers one macrotask ONLY while a root-owned wake is live (Asyncify.__wakingRoot). Beacon: root-entry-deferred. Verified inert where it must be: zero beacons across all 13 drift-trio-scenarios logs (26/26 + 25/26-then-26/26 stress — the single miss carried no beacons, i.e. the pre-existing under-load flake). Also: resume re-entries no longer push sleep contexts (the v0.1.23 dump carried ~380 leaked zero-linked entries), and wake events in the recorder are tagged R/f for ownership. .ci-cache-epoch 5→6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
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))
};
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.
fix(asyncify): ownership-scoped root deferral — the recorded nested self-rewind, cured The v0.1.23 flight recorder caught the kill live (console-export-2026-8-1_19-16-8): dozens of benign fiber round-trips at w=0, the yield cycling healthily on its buffer — then "fcs … ROOT w=1" and the trap, state frozen at Rewinding with currData=root+20. The fatal condition, observed rather than inferred: a fiber round-trip inside the ROOT's OWN sleep-wake continuation re-suspends and re-rewinds the root nested inside its live wake rewind. Consume-once passed correctly — it guards a different corruption and stays. The round-3 deferral was aimed right but unscoped (taxed fiber-owned wakes, flaked S4). Final form: every fresh sleep is tagged root- or fiber-owned (fiber ⇔ started inside a finishContextSwitch fiber slice or a fiber-owned wake; root entries don't count as slices); finishContextSwitch(root) defers one macrotask ONLY while a root-owned wake is live (Asyncify.__wakingRoot). Beacon: root-entry-deferred. Verified inert where it must be: zero beacons across all 13 drift-trio-scenarios logs (26/26 + 25/26-then-26/26 stress — the single miss carried no beacons, i.e. the pre-existing under-load flake). Also: resume re-entries no longer push sleep contexts (the v0.1.23 dump carried ~380 leaked zero-linked entries), and wake events in the recorder are tagged R/f for ownership. .ci-cache-epoch 5→6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 19:49:38 +02:00
__rec("wake buf=" + (sleepCtx.capturedData || 0) + " cdWas=" + (Asyncify.currData || 0)
+ (sleepCtx.rootOwned ? " R" : " f"));
if (sleepCtx.capturedData) {
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);
}
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;
fix(asyncify): ownership-scoped root deferral — the recorded nested self-rewind, cured The v0.1.23 flight recorder caught the kill live (console-export-2026-8-1_19-16-8): dozens of benign fiber round-trips at w=0, the yield cycling healthily on its buffer — then "fcs … ROOT w=1" and the trap, state frozen at Rewinding with currData=root+20. The fatal condition, observed rather than inferred: a fiber round-trip inside the ROOT's OWN sleep-wake continuation re-suspends and re-rewinds the root nested inside its live wake rewind. Consume-once passed correctly — it guards a different corruption and stays. The round-3 deferral was aimed right but unscoped (taxed fiber-owned wakes, flaked S4). Final form: every fresh sleep is tagged root- or fiber-owned (fiber ⇔ started inside a finishContextSwitch fiber slice or a fiber-owned wake; root entries don't count as slices); finishContextSwitch(root) defers one macrotask ONLY while a root-owned wake is live (Asyncify.__wakingRoot). Beacon: root-entry-deferred. Verified inert where it must be: zero beacons across all 13 drift-trio-scenarios logs (26/26 + 25/26-then-26/26 stress — the single miss carried no beacons, i.e. the pre-existing under-load flake). Also: resume re-entries no longer push sleep contexts (the v0.1.23 dump carried ~380 leaked zero-linked entries), and wake events in the recorder are tagged R/f for ownership. .ci-cache-epoch 5→6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
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;
test(asyncify): red-green race harness + ablation flags, unwind-catch shim, spec tightening, decisions docs The asyncify single-slot work, executed red-green (full ledger: docs/features/asyncify-arbiter/redgreen.md; decisions record: docs/features/async/07-decisions-and-outcome.md): - tests/apps/standalone/asyncify-races/ + tests/asyncify/ + dedicated playwright config: 8 scenarios reproducing the KiCad asyncify failure family with the kicad-faithful startup topology (pre-park fiber swap → park throw through the live trampoline). Built in 3 variants; the SHIM_DISABLE_TRAMPOLINE_HEAL / SHIM_DISABLE_HANDLESLEEP ablation builds keep the historical hang and index-out-of-bounds crash reproducible forever (mutation-style pins for the existing shims). - scripts/common/shims/handlesleep.js: catch the "unwind" park sentinel in the wakeUp path — when main's last pre-park suspension was a sleep, the main-loop park throw escaped through that sleep's promise reaction as an uncaught rejection (the calculator/gerbview console errors). - scripts/common/inject-dyncall-shims.sh: SHIM_DISABLE_* ablation knobs. - Spec tightening (the acceptance bar): 'uncaught exception: unwind' tolerance DELETED from pcbnew/eeschema specs; load-pcb gained a hard clean-console gate over 5 asyncify corruption signatures. - wxwidgets pointer bump: modal LIFO resolvers, pump resolve-on-error, sync clipboard IsSupported (014f67e6c1). Final state: asyncify suite 7/7, wx e2e 291/292 (1 skip), KiCad e2e 40 passed / 2 skipped with ZERO corruption signatures in any log across all six apps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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;
fix(asyncify): ownership-scoped root deferral — the recorded nested self-rewind, cured The v0.1.23 flight recorder caught the kill live (console-export-2026-8-1_19-16-8): dozens of benign fiber round-trips at w=0, the yield cycling healthily on its buffer — then "fcs … ROOT w=1" and the trap, state frozen at Rewinding with currData=root+20. The fatal condition, observed rather than inferred: a fiber round-trip inside the ROOT's OWN sleep-wake continuation re-suspends and re-rewinds the root nested inside its live wake rewind. Consume-once passed correctly — it guards a different corruption and stays. The round-3 deferral was aimed right but unscoped (taxed fiber-owned wakes, flaked S4). Final form: every fresh sleep is tagged root- or fiber-owned (fiber ⇔ started inside a finishContextSwitch fiber slice or a fiber-owned wake; root entries don't count as slices); finishContextSwitch(root) defers one macrotask ONLY while a root-owned wake is live (Asyncify.__wakingRoot). Beacon: root-entry-deferred. Verified inert where it must be: zero beacons across all 13 drift-trio-scenarios logs (26/26 + 25/26-then-26/26 stress — the single miss carried no beacons, i.e. the pre-existing under-load flake). Also: resume re-entries no longer push sleep contexts (the v0.1.23 dump carried ~380 leaked zero-linked entries), and wake events in the recorder are tagged R/f for ownership. .ci-cache-epoch 5→6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 19:49:38 +02:00
Asyncify.__wakingOwnerFiber = prevWakingOwnerFiber;
if (sleepCtx.rootOwned) Asyncify.__wakingRoot = prevWakingRoot;
test(asyncify): red-green race harness + ablation flags, unwind-catch shim, spec tightening, decisions docs The asyncify single-slot work, executed red-green (full ledger: docs/features/asyncify-arbiter/redgreen.md; decisions record: docs/features/async/07-decisions-and-outcome.md): - tests/apps/standalone/asyncify-races/ + tests/asyncify/ + dedicated playwright config: 8 scenarios reproducing the KiCad asyncify failure family with the kicad-faithful startup topology (pre-park fiber swap → park throw through the live trampoline). Built in 3 variants; the SHIM_DISABLE_TRAMPOLINE_HEAL / SHIM_DISABLE_HANDLESLEEP ablation builds keep the historical hang and index-out-of-bounds crash reproducible forever (mutation-style pins for the existing shims). - scripts/common/shims/handlesleep.js: catch the "unwind" park sentinel in the wakeUp path — when main's last pre-park suspension was a sleep, the main-loop park throw escaped through that sleep's promise reaction as an uncaught rejection (the calculator/gerbview console errors). - scripts/common/inject-dyncall-shims.sh: SHIM_DISABLE_* ablation knobs. - Spec tightening (the acceptance bar): 'uncaught exception: unwind' tolerance DELETED from pcbnew/eeschema specs; load-pcb gained a hard clean-console gate over 5 asyncify corruption signatures. - wxwidgets pointer bump: modal LIFO resolvers, pump resolve-on-error, sync clipboard IsSupported (014f67e6c1). Final state: asyncify suite 7/7, wx e2e 291/292 (1 skip), KiCad e2e 40 passed / 2 skipped with ZERO corruption signatures in any log across all six apps. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-12 16:59:07 +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) {
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;
fix(asyncify): ownership-scoped root deferral — the recorded nested self-rewind, cured The v0.1.23 flight recorder caught the kill live (console-export-2026-8-1_19-16-8): dozens of benign fiber round-trips at w=0, the yield cycling healthily on its buffer — then "fcs … ROOT w=1" and the trap, state frozen at Rewinding with currData=root+20. The fatal condition, observed rather than inferred: a fiber round-trip inside the ROOT's OWN sleep-wake continuation re-suspends and re-rewinds the root nested inside its live wake rewind. Consume-once passed correctly — it guards a different corruption and stays. The round-3 deferral was aimed right but unscoped (taxed fiber-owned wakes, flaked S4). Final form: every fresh sleep is tagged root- or fiber-owned (fiber ⇔ started inside a finishContextSwitch fiber slice or a fiber-owned wake; root entries don't count as slices); finishContextSwitch(root) defers one macrotask ONLY while a root-owned wake is live (Asyncify.__wakingRoot). Beacon: root-entry-deferred. Verified inert where it must be: zero beacons across all 13 drift-trio-scenarios logs (26/26 + 25/26-then-26/26 stress — the single miss carried no beacons, i.e. the pre-existing under-load flake). Also: resume re-entries no longer push sleep contexts (the v0.1.23 dump carried ~380 leaked zero-linked entries), and wake events in the recorder are tagged R/f for ownership. .ci-cache-epoch 5→6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
2026-08-01 19:49:38 +02:00
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.
fix(asyncify): ownership-scoped root deferral — the recorded nested self-rewind, cured The v0.1.23 flight recorder caught the kill live (console-export-2026-8-1_19-16-8): dozens of benign fiber round-trips at w=0, the yield cycling healthily on its buffer — then "fcs … ROOT w=1" and the trap, state frozen at Rewinding with currData=root+20. The fatal condition, observed rather than inferred: a fiber round-trip inside the ROOT's OWN sleep-wake continuation re-suspends and re-rewinds the root nested inside its live wake rewind. Consume-once passed correctly — it guards a different corruption and stays. The round-3 deferral was aimed right but unscoped (taxed fiber-owned wakes, flaked S4). Final form: every fresh sleep is tagged root- or fiber-owned (fiber ⇔ started inside a finishContextSwitch fiber slice or a fiber-owned wake; root entries don't count as slices); finishContextSwitch(root) defers one macrotask ONLY while a root-owned wake is live (Asyncify.__wakingRoot). Beacon: root-entry-deferred. Verified inert where it must be: zero beacons across all 13 drift-trio-scenarios logs (26/26 + 25/26-then-26/26 stress — the single miss carried no beacons, i.e. the pre-existing under-load flake). Also: resume re-entries no longer push sleep contexts (the v0.1.23 dump carried ~380 leaked zero-linked entries), and wake events in the recorder are tagged R/f for ownership. .ci-cache-epoch 5→6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
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)) {
fix(asyncify): consume-once root suspensions — replaces the wake-window deferral The deferral (9ca2ac1) modeled the wrong condition and taxed every parked fiber completion with a macrotask hop: under CI load that stretched three-client apply chains and flaked drift-trio S4 twice consecutively (26/26 green locally under stress) — retracted. The actual fatal state, readable in all four prod stacks once seen: a SECOND rewind of the same root suspension. Root suspends once per fiber_swap out of it; two parked fibers completing against one root suspension epoch (a tool fiber + a collab fiber both waking around open:settled) each drive finishContextSwitch(root) — the second rewinds already-consumed data → "unreachable executed" → poisoned runtime, with the wake-side "index out of bounds" as the sibling symptom. Cure: stop exempting root from the validity check the shim already keeps. First consumption proceeds synchronously — zero added latency anywhere; the second is refused ([wx-asyncify] "root suspension already consumed") — the yielded fiber stays properly suspended and resumable, root continues via its real pending resume, libcontext's ghost-epoch contract enforced one layer lower. Root remains exempt only from the internally-parked quarantine (its yield park is routine). .ci-cache-epoch 4→5 (the epoch-4 cache holds the retracted deferral shim). Local: fiber 2/2 + timer 1/1, firefox sweep 20 passed, drift-trio-scenarios kicad-chromium 26/26 under 3-worker stress, 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 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;
}
fix(asyncify): consume-once root suspensions — replaces the wake-window deferral The deferral (9ca2ac1) modeled the wrong condition and taxed every parked fiber completion with a macrotask hop: under CI load that stretched three-client apply chains and flaked drift-trio S4 twice consecutively (26/26 green locally under stress) — retracted. The actual fatal state, readable in all four prod stacks once seen: a SECOND rewind of the same root suspension. Root suspends once per fiber_swap out of it; two parked fibers completing against one root suspension epoch (a tool fiber + a collab fiber both waking around open:settled) each drive finishContextSwitch(root) — the second rewinds already-consumed data → "unreachable executed" → poisoned runtime, with the wake-side "index out of bounds" as the sibling symptom. Cure: stop exempting root from the validity check the shim already keeps. First consumption proceeds synchronously — zero added latency anywhere; the second is refused ([wx-asyncify] "root suspension already consumed") — the yielded fiber stays properly suspended and resumable, root continues via its real pending resume, libcontext's ghost-epoch contract enforced one layer lower. Root remains exempt only from the internally-parked quarantine (its yield park is routine). .ci-cache-epoch 4→5 (the epoch-4 cache holds the retracted deferral shim). Local: fiber 2/2 + timer 1/1, firefox sweep 20 passed, drift-trio-scenarios kicad-chromium 26/26 under 3-worker stress, 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 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.1922): 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)) {
fix(asyncify): consume-once root suspensions — replaces the wake-window deferral The deferral (9ca2ac1) modeled the wrong condition and taxed every parked fiber completion with a macrotask hop: under CI load that stretched three-client apply chains and flaked drift-trio S4 twice consecutively (26/26 green locally under stress) — retracted. The actual fatal state, readable in all four prod stacks once seen: a SECOND rewind of the same root suspension. Root suspends once per fiber_swap out of it; two parked fibers completing against one root suspension epoch (a tool fiber + a collab fiber both waking around open:settled) each drive finishContextSwitch(root) — the second rewinds already-consumed data → "unreachable executed" → poisoned runtime, with the wake-side "index out of bounds" as the sibling symptom. Cure: stop exempting root from the validity check the shim already keeps. First consumption proceeds synchronously — zero added latency anywhere; the second is refused ([wx-asyncify] "root suspension already consumed") — the yielded fiber stays properly suspended and resumable, root continues via its real pending resume, libcontext's ghost-epoch contract enforced one layer lower. Root remains exempt only from the internally-parked quarantine (its yield park is routine). .ci-cache-epoch 4→5 (the epoch-4 cache holds the retracted deferral shim). Local: fiber 2/2 + timer 1/1, firefox sweep 20 passed, drift-trio-scenarios kicad-chromium 26/26 under 3-worker stress, 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 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);
}
fix(asyncify): ownership-scoped root deferral — the recorded nested self-rewind, cured The v0.1.23 flight recorder caught the kill live (console-export-2026-8-1_19-16-8): dozens of benign fiber round-trips at w=0, the yield cycling healthily on its buffer — then "fcs … ROOT w=1" and the trap, state frozen at Rewinding with currData=root+20. The fatal condition, observed rather than inferred: a fiber round-trip inside the ROOT's OWN sleep-wake continuation re-suspends and re-rewinds the root nested inside its live wake rewind. Consume-once passed correctly — it guards a different corruption and stays. The round-3 deferral was aimed right but unscoped (taxed fiber-owned wakes, flaked S4). Final form: every fresh sleep is tagged root- or fiber-owned (fiber ⇔ started inside a finishContextSwitch fiber slice or a fiber-owned wake; root entries don't count as slices); finishContextSwitch(root) defers one macrotask ONLY while a root-owned wake is live (Asyncify.__wakingRoot). Beacon: root-entry-deferred. Verified inert where it must be: zero beacons across all 13 drift-trio-scenarios logs (26/26 + 25/26-then-26/26 stress — the single miss carried no beacons, i.e. the pre-existing under-load flake). Also: resume re-entries no longer push sleep contexts (the v0.1.23 dump carried ~380 leaked zero-linked entries), and wake events in the recorder are tagged R/f for ownership. .ci-cache-epoch 5→6. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
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 ===