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
5.3 KiB
19 — Symbol Properties dialog hangs: a stranded tool fiber (live investigation)
Status: DIAGNOSED, NOT FIXED (2026-08-05). Reproduced end-to-end in a real browser against the dev platform on the scheduler build. Regression-vs-pre-existing is not yet determined — see §5. Related:
17S4 (waits),16(the quarantine guard), the 8/4 three-UI-bugs triage (which blamed the interlock drain — that is the symptom layer, not the proximate cause).
1. Repro (100%, ~40 s)
Dev platform (npm run dev), editor at :3048, Arduino Leonardo schematic:
- Open the project URL, wait for load (~30 s).
- Double-click the USB receptacle (J1) → Symbol Properties opens.
- Click OK / Cancel / any control → nothing happens. Only the titlebar × closes it.
2. Frozen state (captured live)
Fibers.__internallyParked = [607191040] // tool fiber, quarantined
Fibers.__parkSleepBuf = {607191040 → 607977472}
Asyncify.__pendingSleepContexts= [{buf 607977472, rootOwned:false, cleaned:false},
{buf 205586432, rootOwned:true, cleaned:false}]
Asyncify.currData = 205586432, state = 0
Fibers.__validSuspensions ∌ 607191040 // no resumable suspension
scheduler: mailbox=3 enqueued=655 delivered=652 // FROZEN (0 progress in 4 s)
waits=1 waitsBegun=1 waitsResolved=0 // the quasi-modal wait, unresolved
Console beacons, in order:
[wx-asyncify] overlapped-wake × 10 (benign: restore over null)
[wx-asyncify] aliased-wake-live: restoring currData=205586432 over 607977472
[wx-asyncify] fiber-resume-refused: fiber=607191040 is asyncify-parked mid-body (sleep in flight)
3. What is NOT the cause
- Not clicks failing to reach wx. The OK click produced exactly one
wx_dom_event(domId 81, kind 1)ccall; the button is enabled and hit-testable. (Rules out theWINDOW_DISABLER/IsEnabled()hypothesis.) - Not slow I/O. The last library network resource completed at t=5 s; the hang was inspected at t=277 s with nothing in flight. The fiber's park is waiting on a promise that will never settle — a lost wake, not pending work.
- Not the mailbox/embind/wait bookkeeping.
strayWrites=0,mutQ=0, no deferred wakes, no stranded messages beyond the 3 blocked by the interlock.
4. Mechanism
The tool fiber running the dialog parks mid-body (the quasi-modal wait). The stale-fiber
guard correctly marks it __internallyParked — its slice ended with a sleep in flight.
Its resume then arrives and is refused (fiber-resume-refused), because a quarantined
fiber has no valid suspension; the guard's contract is "the parked body completes via its
own wake." Here that wake is the refused resume, so nothing ever completes:
- the fiber never resumes → the dispatch guard it holds is never released →
wxWasmDispatchParked()stays true forever →ProcessEventsis Paint-only andwxWasmMailboxDeliverbails → every subsequent click is deferred and never drained, and timer delivery stops (the frozen 652).- The × works because
wx_window_close(toplevel.cpp) is ungated and synchronous.
The 8/4 triage saw the outer ring of this (deferred clicks + a drain gated on the same predicate) and proposed flushing the queue at depth 0 — that cannot help: the depth never returns to 0 because the holder is stranded.
5. Open: regression or pre-existing?
Undetermined. The quarantine guard and the aliasing repair are pre-existing (ported
verbatim into the scheduler at S2), but S4 changed how a quasi-modal parks
(wxWasmRunNestedLoop pump → wxWasmBeginWait/wxWasmYieldUntil). Both shapes park the
fiber mid-body, so the quarantine interaction plausibly predates S4 — but that must be
proven, not assumed. A hand-edited legacy glue is not a valid comparison (attempted
2026-08-05: stripping the scheduler and re-injecting handlesleep.js into a C-lane build
booted to a fatal TypeError: … reading 'mode'). The differential needs a real
WX_SCHEDULER=0 docker build of kicad_editor, same flow.
6. Fix directions (ranked, none implemented)
- Don't drop a refused resume — defer it. When
__refuseFiberfires for a fiber in__internallyParked, record the pending resume and retry it when that fiber's park resolves (__parkSleepBufalready maps fiber → its sleep buffer, and the sleep's context leaving__pendingSleepContextsis the exact "now safe" signal). This is the same drop→deliver flip the whole mailbox migration is built on, applied one layer down. Caveat: doc 16 closed the deferral family for the root-hot case (a suspension broken at write time). This is a different case — a healthy fiber quarantined mid-body — so the closure does not automatically apply, but the round-6 evidence must be re-read first. - Make a permanently-held interlock loud (watchdog beacon after N seconds with a parked holder). Diagnostic only, but it turns this class from "UI mysteriously dead" into a one-line console verdict.
- Handler fibers (the ledgered Design-B step): if parkable handlers own scheduler contexts, "a parked chain holds the global interlock" stops being representable. This is the structural cure and the S5 ledger's unlock for deleting the interlock entirely.