The per-frame park is how a synchronous C++ frame waits; a frame that must not
park can only RETURN. So D5 = loop body on a context + DoRun returns + main()
returns without tearing wx down (EXIT_RUNTIME=0). Step 3 is the real cost and
needs its own gate - teardown, ~wxTopLevelWindowWasm, the S6 shutdown latch
and the v0.1.28 quit-notify fix all assume DoRun returning means the app ends.
Checked rather than assumed that no smaller fix exists: the scheduler stack IS
the main stack, so while it is parked in the rAF yield any pump entry re-enters
a logically suspended stack, and the loop parks every frame.
Recommends doing D5 first and alone (wx-only, cheap to iterate) before
re-enabling the already-written D/C/B/E on top.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
NOT GREEN. resolveWait marks a context-parked waiter ready and arms a pump
instead of resolving a promise nobody awaits.
The doc records the measured correction: DoRun parks the MAIN stack every
frame in wxWasmYieldToBrowser, which doc 21 called safe-by-construction only
because dispatch also ran there. With the scheduler swapping contexts from the
tick, those interleave over one currData - overlapped-wake, the exact class
this work exists to remove. So the main loop must become a context (D5) and
the bridges (E) join the same flip: D5+D+C+B+E land together.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
The scheduler fiber and libcontext's root would both adopt the main stack -
two emscripten_fiber_t describing one stack, mutual corruption on first entry.
Harmless in Phase A (KiCad never called drain), fatal at the flip. So dispatch
must move onto a context FIRST, the libcontext root then adopts the running
context rather than the main stack, and only then can jump_fcontext become a
star transfer.
Also records that the synchronous-Call question is answered and pinned.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
star_transfer_call_is_synchronous stages exactly the TOOL_MANAGER shape -
Call() then read Running() on the next line - and pins that a parked caller
resumed by the scheduler is indistinguishable from a synchronous return.
star_transfer_chain pins a three-deep nested call (root -> A -> B and back) in
strict order, with a parked star context proved undisturbed throughout.
A rule learned the hard way here: a fiber entry must NEVER return (emscripten
ends the program if it does), so these bodies park forever once their script
is done, like libcontext's trampoline loops. Getting it wrong wedges the
battery with no output at all.
The fiberRunning assertion flips to the stronger Phase B invariant: once the
pump is quiescent NO fiber is on the CPU, the scheduler is.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
The synchronous TOOL_MANAGER contract vs the star: Call() must run to first
yield before returning, drain() refuses re-entry, and dispatch is not yet a
context - the same knot from the other side, and why B+C+D are one commit.
CALL_CONTEXT::Continue is a hand-rolled scheduler that should disappear rather
than be ported. Phase A's grace-ring-over-capacity: 33 says B must fix
coroutine LIFETIME, not just topology. Records the upstream-divergence
decision B has to take first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
Bumps wxwidgets (fiber lane in sched_context.h) and kicad (libcontext's wasm
backend becomes an adapter over it), so there is now ONE party performing
every emscripten_fiber_swap and recording who is on the CPU. Behaviour is
preserved throughout - libcontext still decides, the registry observes and
beacons any disagreement - which is the de-risking step doc 20's D2 never had.
Harness + spec gain three fiber-lane scenarios: a fresh fiber enters at its
entry and a swap suspends the swapper (fiber_roundtrip), releasing a suspended
fiber is legal while a stale id refuses instead of use-after-free
(fiber_release_suspended), and symmetric swaps leave a parked star context
undisturbed (fiber_and_star_coexist). The spec asserts the lane's counters,
including the tripwire that must stay zero: fiberNonEnterableSwaps.
Doc 22 gains the Phase A work log: the gate (kicad suite 139/1, the 1 being the
pre-existing occ-probe glb matrix; wx 346/1/3; batteries 48/48; zero tripwires
anywhere), the four bugs the tripwires caught and the single rule behind three
of them, the grace ring and the 33-coroutine measurement Phase B needs from it,
and the process traps this run paid for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EarUW9DS1c1sSW4ZNrkGQS
Doc 20 gets a status table instead of a narrative: D-1/D0/D1 done, D2
reverted, D3's goal met but its MEANS skipped (waits still park in
place — only the stack they park on changed), D4/D5/D6 not started. Plus
an explicit "what was skipped and is still owed" list, so nothing quietly
reads as delivered: dispatch contexts, waits-as-yields, bridges,
real-park-site buffer sizing, the both-EH CI matrix.
Doc 22 is the handoff plan a fresh session can implement from:
- the bug it targets is the BLUE SCREEN (a context recovered twice or by
the wrong fiber), not doc 19's hang, which is fixed and was only one
ingredient;
- the diagnosis that matters: bookkeeping is already centralised
(currData is single-writer, the shim sees every swap) but the DECISION
is not — libcontext decides swaps and never tells anyone, so three
layers each invented heuristics. Centralising means moving the
decision;
- why D2/D3 knotted, with the measurements, so nobody retries that order;
- phases A-F with estimates (~4-6 wk): A absorbs libcontext behaviour-
preservingly and is landable ALONE (the de-risking step D2 never had),
B+C+D must land as one commit, E bridges, F deletes the guards only
once they are provably silent;
- gates including the crash's own repro, since the batteries never
reproduced it and therefore cannot certify it;
- the traps this run paid for: 16-byte fiber stack alignment, the
per-fiber stack-limits gotcha, host-side asyncify buffers, the
stack-ownership rule, partial migration, and the gitignored-artifact
triage trick;
- measured build/test cycle costs, so the estimates are grounded.
README: index rows for 21 and 22, doc 19 marked FIXED, and the stale
2026-06 "single decisive next step" folded away behind the current one.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
Doc 19: status DIAGNOSED+PINNED RED -> FIXED, with the mechanism and the
explicit note that waits still park in place; the cure was to stop
parking on a COROUTINE stack, not to stop parking.
Doc 20: D3 work log (layering table, gate numbers) and a status that no
longer claims a phase queue. The bug that motivated the whole core
rewrite is closed WITHOUT contexts, so D2/D4/D5/D6 have lost their
forcing function and must be re-justified rather than continued by
default — including the honest option of deleting D1's unused context
layer.
Trap recorded: detection must capture the main stack's bounds at
top-level DoRun, not query them live — finishContextSwitch resets the
limits to the incoming fiber's, so a live query reports "main stack"
from everywhere and detects nothing.
Gate: kicad 139 passed / 1 failed (pre-existing occ-probe glb); wx +
asyncify + coroutine 394 passed / 1 failed (pre-existing
environment-sensitive modal:125). coroutine-nested — the battery D2
regressed — is green.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
THE BUG IS FIXED. tests/kicad/quasimodal-strand.spec.ts flips from a
test.fail() pin to a plain green regression test: 3/3 runs
closed=true dialogs=0 refused-resumes=0 (was closed=false dialogs=1
refused-resumes=1 on every run).
Mechanism: a quasi-modal's nested event loop parked on the TOOL
COROUTINE's stack, which suspends the fiber's body where the fiber layer
cannot see it — so the stale-fiber guard quarantined the fiber and then
refused its own resume, the dispatch guard was never released, and every
click after that was deferred forever. Bouncing the nested loop onto the
main stack leaves the coroutine suspended the legitimate way (a recorded
fiber swap), so nothing is quarantined and nothing is refused.
Layering, so this is not a pile of WASM ifdefs in KiCad:
- wx (3d37db3bf1) owns the POLICY and the hook; it must not know what a
coroutine is.
- wasm/bindings/main_stack_runner.h is the only place that may know both
sides: it finds the frame's TOOL_MANAGER and bounces via RunMainStack.
Header-only and self-installing, so no build-script change; included by
every editor's binding TU.
- KiCad gets ONE ifdef-free method (2c777efede), needed only because
TOOL_STATE is opaque outside TOOL_MANAGER. libcontext and dialog_shim
are untouched — an earlier draft edited both and was reverted.
This also reframes the remaining plan: the doc-19 class is closed WITHOUT
migrating tool coroutines onto scheduler contexts. Note it does not make
the wait a context yield — waits still park in place, just never on a
coroutine stack.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
Bumps wxwidgets fc762bed (yield_park refuses a yield from a foreign
stack).
D3's phase text assumed the wait's caller runs on the context it yields.
The doc-19 path does not: the dialog is opened from a KiCad tool
coroutine — a libcontext fiber above whatever dispatched it — so a yield
would save the tool fiber's stack into the host context's fiber struct.
Guarded now, and the rule is explicit: a wait yields the context that
OWNS its stack.
That makes D3 three things, not one: tool coroutines become scheduler
contexts, waits yield the owning context, and dispatch moves onto a
context (the old D2). They are atomic — D2 alone regressed the nested
battery, and D3 alone has no context to yield. §5's architecture already
listed "tool contexts" among the three kinds the scheduler drains; only
the phase text was narrower.
Also pinned green: fiber-on-context nesting works. An earlier buggy
scenario trapped and briefly suggested otherwise, which would have sent
the plan somewhere wrong.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
Two scenarios, deliberately separate because they answer different
questions that D3 depends on:
- fiber_nests_in_context: can a libcontext-style fiber run nested inside
a scheduler context at all? YES. Worth pinning explicitly — an earlier
version of this scenario trapped `unreachable` and briefly looked like
proof that nesting was impossible, which would have been a serious
wrong turn for the plan. The real cause was the test's own bug: the
host-side emscripten_fiber_t was zero-initialised, so the swap unwound
into a null asyncify buffer. Nesting works; the scenario now proves it.
- foreign_stack_refused: a yield from that nested fiber is REFUSED, and
the host context and scheduler both survive it.
The spec asserts foreignStackRefusals == 1, so the refusal path cannot
silently stop being exercised.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
Bumps wxwidgets d220aae5 (D2a: sched_context.h moved into wx's port,
header-only so evtloop.cpp can see it) and 5ee60a81 (D1 fix: 16-align
context stacks — EM_ASM's arg buffer lives on the running stack and the
glue asserts buf % 16 == 0, so misaligned contexts trapped in
readEmAsmArgs; std::vector<char> only gives malloc's 8-byte alignment).
The dispatch switch itself is NOT landed. Running the tick's
ProcessEvents on a context took the battery from 363 green to 388/7, six
of them the coroutine-nested harness wedging at
fiber_create_run_destroy_inside_modal via aliased-wake-live ->
fiber-resume-refused — doc 19's mechanism. A quasi-modal opened from a
tick handler suspends the dispatch context INSIDE the still-in-place
wait, putting one more Asyncify layer under every libcontext fiber.
Pooling contexts (8 burned in 30 ms) and falling back to entry-stack
dispatch both failed to avoid it, because the layer exists as soon as
the context is suspended.
That is doc 20's own risk 2 arriving on schedule, so the plan is
corrected rather than the symptom patched: D3 (waits become context
yields) must come first, after which the dispatch context is released at
its tick boundary instead of suspended and the failure class is
structurally absent. Verified the revert: the nested battery is green
again at this baseline (5 passed / 1 failed, the 1 being the
environment-sensitive modal:125 that also fails without any of this).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
Doc 20: D1 entry (star topology as the load-bearing decision, the
9-scenario gate, measured memory) and the §3 asset table flipped —
registry/park/resume/drain are no longer "stubs that throw".
Doc 21: what D1 built, plus the sizing instruction for D4 — take each
bridge's deep-park high-water from its own beacon; do NOT carry the
harness's ~34 B/frame figure (synthetic frames, three locals each = a
floor) and do not inherit libcontext's 512 K by default either.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
Doc 20 §6 D1, second slice: the dedicated test app D1 calls for, plus
the doc 20 §7 risk-1 gate.
tests/apps/standalone/sched-context/ drives the primitives with no wx
linked (a failure can only be the contexts layer) and assertions ON —
unlike the races harness, an emscripten "cannot start an async
operation" state here IS the bug, not a tolerated state. Nine
scenarios, each pinning one invariant; the load-bearing ones:
- parked_does_not_block: a parked context does not stop others running.
This is doc 19's freeze made unrepresentable — there a parked activity
held a global interlock and the UI died; here "parked" simply means
"not runnable", and three workers run past it before it resumes.
- one_transition_in_flight: a context calling drain() gets a no-op, with
a second ready context queued so a buggy nested drain would actually
run something and be caught.
- async_wake: a real macrotask hop (setTimeout → mark_ready → drain),
the shape every production bridge has.
- deep_park_sizing: parks 64 live frames deep to measure what a park
actually costs.
The memory gate asserts the ceiling (peak live contexts, peak bytes),
that nothing leaked (live=0, bytes=0, created==finished), that nothing
was left mid-transition, and that refusals occurred (zero would mean the
illegal-operation scenarios stopped provoking).
Measured here: ~34 B/frame, 2200 B for a 64-frame park, 1 MB peak for 4
concurrent contexts. Recorded with the caveat that the harness's frames
carry three locals each, so this is a FLOOR — real bridges save far more
per frame (libcontext runs 512 K after a 64 K buffer silently
overflowed). The apparatus and its units are validated; the sizing
DECISION needs deep-park numbers from real bridges at D3/D4.
playwright.config: asyncify-firefox now matches every spec in ./asyncify
instead of only asyncify-races* — a new harness there is covered by
construction rather than by remembering to widen the pattern.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
Doc 20 §6 D1, first slice: wasm/sched/context.{h,cpp} implements
create / yield_park / mark_ready / drain with the registry as truth.
The shape that matters — a STAR, not libcontext's symmetric swap:
contexts only ever swap OUT to the scheduler, and only the scheduler
swaps IN. So "is this target safe to enter?" stops being a guess
(libcontext's swap_suspended / parked / hot-main refusals) and becomes a
lookup: the registry says Parked/Ready and holds the buffer. Doc 19's
refused-resume is unrepresentable here because resume is not a decision
made at the swap site.
Enforced, not hoped for:
- at most one transition in flight (drain() refuses re-entry, so a
context calling drain() cannot turn the star into a cycle);
- mark_ready() never resumes inline — it queues, and drain() resumes
from a clean stack (doc 13 §1.4's deferred-wake law, per context);
- yield_park() off a context is REFUSED, which is the "nothing parks in
place" rule made mechanical;
- destroy() on a non-Finished context is refused (freeing a parked
stack strands whatever is on it);
- FIFO ready queue (no starvation);
- main-thread-only, per doc 21 §2's pthread finding.
Memory is accounted from the start (doc 20 risk 1): live/peak contexts,
bytes/peak bytes, and per-context asyncify high-water measured from
asyncify_data.stack_ptr — the same quantity the shim reports as `rem=`,
from the other end. Sizes deliberately start at 128 KB C stack + 128 KB
asyncify buffer rather than inheriting libcontext's 512 K, so the number
gets derived from evidence; a >75% buffer use beacons BUFFER-PRESSURE
because that overflow is silent corruption, not a crash.
No production path runs on this yet — dispatch moves at D2.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
Doc 19: status DIAGNOSED -> DIAGNOSED + PINNED RED; the repro section
leads with the automated deterministic spec (the manual Leonardo flow
stays as the field repro).
Doc 20: D0 work-log entry; status D-1 + D0 done; next = D1.
Doc 21: red-spec section updated to "landed", with the staging test's
anti-vacuity role.
Carried finding: the strand reproduces on a 2-OBJECT fixture schematic,
so warm-load byte volume (the 68/1 dice-loader) is not an ingredient —
two concurrent parks suffice. That is why D3 closes this class and D5 is
not required for it.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
Doc 20 D0 second deliverable (doc 21 §4). tests/kicad/quasimodal-strand.spec.ts
reproduces the Symbol Properties hang on demand, in two parts:
- "staging" (GREEN): double-click the fixture symbol → Symbol Properties
opens → arm the parking timer (wasm/bindings/timer_park.h) WHILE the
dialog is up. Because the opener's fiber park is open-ended, a timer
firing now necessarily parks on top of it — the overlap is structural,
not a won race. Asserts the dialog opened, the timer fired, its OK
button is hittable, and the shim beaconed concurrent contexts. Keeps
the red pin from rotting into vacuity, and fails loudly on its own.
- "doc-19 red" (test.fail()): clicks OK and asserts the desired end state
— dialog closes, zero fiber-resume-refused beacons, wait books balanced
(no unresolved nested/modal wait). Goes green at D3, when Playwright
will report "expected to fail but passed" and the marker comes off.
Verified 6/6 consecutive full-file runs, identical outcome each time:
closed=false dialogs=1 refused-resumes=1 — the doc-19 mechanism exactly
(quarantined fiber's legitimate resume refused, dialog never closes).
Deliberately NOT asserted in staging: the timer park COMPLETING (whether
a park survives the aliasing is the disease under test) and sawParked (a
100ms sampler can miss a short park) — both are reported, not gated.
Retires tests/kicad/dialog-deadlock-probe.spec.ts: the 8/4 throwaway probe
that established the mechanism. This spec supersedes it and, unlike it, is
deterministic (the probe's 3 blind waitForTimeouts were the only
determinism-lint violations in the tree; the guard is now clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
Doc 20 D0 first deliverable: every Asyncify park site post-D-1,
classified by whose stack it suspends (tool fiber / entry stack / main
loop) with a routing decision per site — 14 production sites (8 wx, 9
KiCad/bridge counting quartets/pairs) + 3 deliberate test levers.
W1 (wxWasmYieldUntilJs) and W3 (popup) route to D3 context yields;
the clipboard/font/lib/3D/occ/ngspice/nanosleep bridges route to D4;
W2 (per-frame yield) stays safe-by-construction unless D5 is taken.
Also settles doc 20 risk 4 (pthreads): all parks are main-thread only;
the lib bridge's pthread path is blocking-proxy, not Asyncify, and is
out of migration scope. Defines what D4's "no handleSleep on a fiber
stack" assertion must cover.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
Gate results (single battery, scheduler-only): wx app battery +
asyncify + coroutine 363 passed / 3 skipped / 0 failed; full kicad
suite 138 passed / 30 skipped / 1 failed — the one failure is the
pre-existing local occ-probe glb case (predates D-1, unrelated).
Post-gate scrubs: vestigial 'startModal' removed from ASYNCIFY_IMPORTS
(tests/apps/Makefile.wasm) and env.startModal from
scripts/common/asyncify-imports.txt (the import no longer exists in any
wasm; both lists are boundary supersets so behavior is identical).
Stale comment pointers to the deleted legacy modal machinery updated.
Doc 20: D-1 work log added, status flipped to IN PROGRESS.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
Doc 20 D-1, second slice: the injector now injects asyncify-scheduler.js
unconditionally — the WX_SCHEDULER=0 legacy opt-out, the
SHIM_DISABLE_HANDLESLEEP ablation skip, and the
SHIM_DISABLE_TRAMPOLINE_HEAL ablation skip are gone, and
scripts/common/shims/handlesleep.js is deleted (the scheduler subsumed
its capture/restore, fiber guard, and trampoline-heal duties in S2).
Comment-only scrubs point the remaining references at the scheduler
shim. .ci-cache-epoch bumped (shim/injector behavior changed).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
Doc 20 D-1 (legacy runtime deletion), first slice: races_test_noheal /
races_test_nosleepfix pinned behavior of the legacy handlesleep shim,
which is being deleted — the pins now assert properties of a runtime
that no longer exists. Drops the two Makefile.wasm link+inject variants
(SHIM_DISABLE_TRAMPOLINE_HEAL / SHIM_DISABLE_HANDLESLEEP), the
shim-redundancy pin specs in asyncify-races.spec.ts, and resolves the
tests/README.md open task. The green battery still runs every scenario
against the scheduler glue.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
Carrying the WX_SCHEDULER=0 runtime through the core rewrite would make
every phase dual-path (two code paths per park site, dual-glue builds,
two batteries per gate) for a runtime we intend to delete anyway; git
on the feature branch already provides the rollback the fallback was
for. D-1 now deletes it up front — injector branch, handlesleep.js,
the wxWasmMailboxEnabled gating, the wx legacy twins, and the ablation
builds that pin the old shim — executing doc 17's S5 ledger item 1
early. Interlock and busy gates unaffected (ledger items 2-3, D2/D6).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
Doc 20: the remaining core of Design B, scoped against what S0-S6 built
and motivated by the doc-19 hang. Core rule: nothing parks in place —
every suspendable activity yields its own context, so a context's state
is authoritative and the guessing layer (quarantine, consume-once,
libcontext refusals, dispatch interlock) gets deleted rather than
tuned. Phases D0-D6 with gates, 6-10 wk; memory/partial-migration/
pthread risks called out; index updated.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
Reproduced live on the dev platform: the tool fiber running the
quasi-modal parks mid-body, is quarantined by the stale-fiber guard,
and its resume is REFUSED — so it never releases the dispatch guard.
Interlock held forever => clicks deferred and never drained, timer
delivery frozen; the titlebar X works because it is ungated.
Includes the captured frozen state, what is ruled out (clicks do reach
wx; no I/O in flight), and ranked fix directions. Regression vs
pre-existing still undetermined — needs a real WX_SCHEDULER=0 build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
Injector defaults to asyncify-scheduler.js (WX_SCHEDULER=0 = explicit
legacy opt-out); .ci-cache-epoch 9->10. Doc 17 S5 corrected: the
interlock/busy-gate deletions assumed handler-fibers that S1-S4 never
built — they stay as load-bearing second lines; each real deletion is
ledgered with its unlock condition. Flip gate: full kicad suite 136
passed on BOTH variants (occ-probe glb fails identically on both =
pre-existing; ngspice bg_run = rerun-passes flake).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
asyncify-scheduler.js REPLACES handlesleep.js on WX_SCHEDULER=1 builds
(injector either-or): ports capture/restore, fiber consume-once/
quarantine guard, wake-window flags, recorder, trampoline heal — and
adds deferred wakes (a wake mid-transition queues and drains from a
clean macrotask) plus the N1 currData accessor (pure-JS writes need
scheduler authorization; strict mode throws; meta-tested). Gates:
races 9/9 with NO legacy shim (subsumption), coroutine 39/39,
wx-chromium 30/30, kicad trio 3/3 on the C-lane build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
Shim embind lane wraps the doc-18 production mutators at the Module
boundary (busy-window calls queue + deliver post-settle; time-boxed
unkillable pump). N2 un-fixme'd and green; collab-load-fuzz carries the
variant contract (drop on legacy, deliver-in-order on scheduler, capped
hammer on the scheduler lane); timer-park's timerRetry silence tripwire
arms on shim+export and is green on the C-lane kicad build. Bump
wxwidgets for the wheel lane. CI both-EH matrix deliberately deferred.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
Doc 17 step S0 scaffolding: WX_SCHEDULER=1 injector path with an
observation-only asyncify-scheduler.js skeleton (legacy shim stays
authoritative until S2), guard-beacon extraction with occurrence
recovery for rate-limited beacons, and the fixme'd N2 ordering spec
(add-then-move probe; un-fixme at S1).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
Doc 17: Design B phasing revised with the July-August guard record.
Test inventory with per-test fate (keep / rewrite / retire / new),
steps S0-S6 with gates and rollback, ~5-7 wk.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
Picks up the wxwidgets correction for the CI failure: the loop-depth gate is
replaced by releasing the parked nested DoRun from the scheduled tick's own
error path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
Picks up the wxwidgets follow-up CI caught: the top-level loop must not
schedule dispatches while a quasi-modal's nested pump owns event delivery.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
The editor reported nothing when a session died. Evidence lived only in-tab —
an 800-line React array behind a "Show console" button — so diagnosis meant
asking a user to paste a screenshot.
Better Stack's Error Tracking ingests the Sentry wire protocol, so this runs
the stock @sentry/browser against a Better Stack DSN. Sentry.init installs its
own window error/unhandledrejection handlers, so uncaught main-thread errors
and the wasm traps that escape emscripten's DOM event handlers are captured
with no instrumentation at the throw sites. Not their JS tag: it has no
beforeSend or fingerprint hooks, its runtime spawns workers from cross-origin
CDN hosts (this page is COEP: require-corp), and it ships session replay on by
default — which on a CAD canvas records customers' board geometry.
@sentry/browser is imported in exactly one file so the vendor stays swappable,
mirroring how lib/analytics.ts isolates Plausible.
Also replaces the terminal-signature regex with a shared, unit-tested predicate
(wasm/terminal-error.ts) used by BOTH the fatal overlay and the reporter, so
they cannot disagree. The regex was a type check written as a string match and
had three live holes: `RuntimeError` was listed but never appears IN
`.message`; Chrome's bare "unreachable" and "null function" matched nothing
(the v0.1.20 prod log is exactly those); and narrowing "table index is out of
bounds" to `\bindex out of bounds` for Firefox in 197f317 silently stopped
matching Chrome's spelling. Checking the TYPE — every trap in this family is a
WebAssembly.RuntimeError — covers all engines and ends the spelling chase; the
message patterns remain as a fallback for paths that lose the Error object,
such as a worker ErrorEvent crossing the realm boundary with error: null.
197f317's pthread-worker tap, promote() and Firefox findings are kept as-is.
Notes:
- Off unless VITE_ERRORS_DSN is set AND VITE_ALLOW_USER_OVERRIDE !== "1" (dev
servers and every Playwright harness set the latter, and production builds
never do), so a production DSN in a local .env still cannot report. With no
DSN the whole SDK is const-folded out: 1,193,080 vs 1,282,463 bytes of JS.
- browserApiErrors integration removed. It wraps setTimeout/rAF/addEventListener
in try/catch, which is exactly how KiCad-on-Emscripten drives its main loop.
- Console breadcrumbs off (collab/debug.ts's clog fires per Yjs update and would
evict the ring before any crash); dom/fetch/navigation breadcrumbs kept.
- beforeSend redacts token/apiKey/Bearer — collab/provider.ts puts the collab
token in the y-partyserver URL, so a connection-failure string carries a live
credential — and guards the cascade: one wedge produced 8 errors in prod, and
after the first terminal event the rest are dropped into cascade_count.
Verified end to end against the real EU host from a cross-origin-isolated page:
POST /api/<id>/envelope/ -> 200, and 4 terminal throws produce 1 event
(control: 1 throw, same count).
Privacy policy 9, cookie policy 6 and the licenses page are updated: Better
Stack is disclosed as an EU processor, and the licenses page now describes the
browser app's own JS dependencies, which it never did.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every repo moved from emergence-engineering/ to PCBJam/, but the published
source pointers still named the old org. GitHub's transfer redirect resolves
them (all six checked, 301 -> 200), so nothing is broken today — but it stops
working the moment anyone creates a repo at an old path, and a GPLv3
corresponding-source pointer is a poor thing to leave depending on a redirect.
CI never passes --repo, so the hardcoded default is what actually ships: the
editor's version badge on editor.pcbjam.com has been linking users to the old
org for their source.
Covers the source pointers (licenses.md, terms.md 12.4, REPO_URL and its doc
comment, the three build-script --repo defaults, the site footer's build-commit
link) and the two "our GitHub" org links. Bumps the pcbjam-shared pointer for
the same fix there.
Deliberately untouched: emergence-engineering.com, the company domain behind
contact@ and the EE credit block — the trailing slash in the substitution keeps
it out. And docs/security-audit-glm/, which describes a finding rather than
linking anywhere.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Picks up the wxwidgets fix (main loop schedules its events into a fresh JS
task instead of dispatching them inside its own Asyncify wake continuation)
and documents the whole round in docs/features/async/16.
Local verification on the warm-load repro built yesterday — the case that
failed every warm load on every build since v0.1.12: 3/3 loads settle with a
fully rendered Leonardo board, rootHotTotal=0 (it was exactly 1 at every
death), fcsTotal=72, no traps. Full kicad e2e: 136 passed, 1 failed, and that
one (occ-probe's GLB format) fails identically on a build without this change
— a pre-existing OCC build-flag issue, tracked separately.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
The prod differential ladder finished: staged byte VOLUME on a warm load is
the only trigger left (V1a siblings-without-lib-tables dies, V1b +120 files
survives, V1c sibling KiCad files renamed byte-for-byte dies, V1d Leonardo +
123MB of inert markdown dies on loads 3-4; 14MB never dies). 3D models,
collab/ydoc/presence, lib tables, sibling KiCad handling and file count are
all exonerated — volume only loads the dice on the underlying race.
That made the crash reproducible locally for the first time in six campaigns:
a persistent browser profile + a 110MB project fails every warm load with the
exact prod signature. Iteration is now ~12 minutes instead of a release cycle.
Shim: every fiber switch now records the departing side's remaining asyncify
buffer and its recorded rewind entry (rem=/rf=), which is what identified the
unrewindable capture and disproved buffer overflow. The deferral family is
closed for good — a microtask-deferred retry on a clean empty stack died
identically to the nested rewind, because the suspension is broken at write
time, not by nesting.
Shell: log the origin stack when wx reports the top window destroyed. That
notification fires from ~wxTopLevelWindowWasm for ANY top-level window, so a
transient frame dying mid-load navigates the user out of the editor — a real
bug in its own right, found while chasing the empty flight-recorder dumps.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
The 8/2 crash-hunt bisection attempt with ?collab=0 was silently invalid: the
flag only gated the attach, while the boot fan-out joined the doc room and
materialized the target file from the ydoc regardless (the "collab=0" prod
log shows both, plus an attach). The flag now also skips the doc-room join —
the file falls back to the plain fetch path — making it a real lever for the
warm-siblings crash bisection (ydoc-vs-sexpr file source, the next suspect
after sibling restage and collab attach were exonerated) and an honest
user-facing escape hatch.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
The differential-repro ladder (2026-08-02, five prod runs + local counter
measurements) narrowed the crash trigger empirically: warm loads of
sibling-heavy projects die at settle (V1/V4 fail 2/2 warm; V2/V3 without
siblings never fail, warm or cold), while the flight-recorder counters show
the settle-time collision windows themselves are universal (fcsTotal=72,
rootHotTotal=3 on V1 AND V3, every load, cold and warm — so the windows are
the shared fan-out, not sibling-made). The sibling restage's room connects +
restage fetches are the only sibling-specific traffic contending with those
windows, and warm IDB compresses it into exactly that moment.
Nothing in the restage is needed for first paint — the boot snapshot staged
every sibling seconds earlier — so it now starts on requestIdleCallback
(5s timeout; setTimeout(3s) fallback), well clear of the settle storm.
Unmount-safe via disposedRef (armed per mount, checked in the deferred
starter and on handle resolution).
Validation is empirical by design: the counters won't move (windows are not
sibling-made); the test is warm V1/V4 prod loads no longer dying. If they
still die, the sibling lever is exonerated too and the remaining suspects
narrow to the ydoc-materialization path差 (second-load file source) — the
next probe either way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
The 96-event recorder ring holds under a second of history at idle tick rate
(~110Hz), so settle-time collision pressure scrolls out before any poll can
read it — the prod dumps only caught the kill because the trap froze the
moment. Scroll-proof totals since boot: every finishContextSwitch increments
fcsTotal; every root entry inside a sleep-wake window increments rootHotTotal
(the fatal precondition). Both appear in the [wx-asyncify] STATE line, i.e.
in every trap auto-dump, every __wxAsyncifyDump() call, and every blue-screen
console — turning ANY prod load (crashing or clean) into a dose measurement
for the differential-repro experiment (which project ingredient generates
collision windows: siblings, 3D models, libs).
.ci-cache-epoch 7→8.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019SE4o46Lnq3hF574FFq8x4
Crash-hunt sessions kept re-asking "was that even the new build?". Every boot
now logs two self-identifying lines into the in-app log (and thus the fatal
ring + blue screens): the app's own bundled chunk name + manifest base + UA,
and the wasm's CDN ETag + SHA-256 of the first 128KiB + length — teed from
the same stream the progress counter reads (no second download, no
buffering), emitted early so the line exists even when the load dies later.
Verify against the CDN with: curl -r 0-131071 <wasm-url> | shasum -a 256.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ac/code/session_019SE4o46Lnq3hF574FFq8x4
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