Context
LegacyPopupReturnNL.spec.mjs:12 is red in every ten-shard dev run of the Brain-gated burn-in, and on both attempts of the retry (run 38313618683). It fails with "Request timed out". Hosted probes, from an unmerged branch:
- Run 38313590362: after the drag's popup connects, the shared App worker answers neither the Neural Link nor its own main thread (
Neo.worker.App.getWorkerId(), 3 s). The same call answers before the drag.
- Run 38314276213: Chrome's debugger paused the App worker 12 times. Every stack runs
updateVdom ← afterSetVdom ← update ← the mountedPromise.then() callback in src/mixin/VdomLifecycle.mjs (line 1337). That is a microtask loop, so no message is ever handled again.
- Run 38315013009:
this in the paused frame is legacy-return-widget, the dragged widget, in all six samples. It reads mounted: false, vnodeInitialized: true, has a vnode, hasBeenMounted: true, holds a _mountedPromise, and has no mountedPromiseResolve, so that promise is already resolved. Its parentId is the popup's viewport.
The Problem
updateVdom() on an unmounted component waits on mountedPromise.then(() => me.update()). Given a promise that an earlier mount resolved, the callback runs at once and finds the component still unmounted. It chains another then, and the loop never ends. The worker starves, and every later request to it times out.
The Architectural Reality
src/container/Base.mjs insert(), on a NeoInstance from a parent in another window, with removeFromPreviousParent: false: item._mounted = false. This raw write skips afterSetMounted() on purpose, so the copy's later mount still triggers it. It also skips the unmount branch, whose first act is delete me._mountedPromise.
src/dashboard/Container.mjs onWindowConnect() takes that branch during a window drag: mainView.add(detachedItem.widget, false, !me.#isWindowDragging).
src/component/Abstract.mjs get mountedPromise() returns any cached promise, whatever mounted reads now.
Design authority: the getter's JSDoc — "It also handles unmounting by resetting the promise, so it can be safely awaited again if the component is remounted". A raw unmount gets no reset.
The Fix
The getter owns the cache, so it checks it. A cached promise with no pending resolver is resolved. While the component is unmounted, the getter discards it and creates a pending one. This covers every path that clears mounted without the hook. The container's raw write stays, because skipping the unmount hooks for a copy is intended.
Acceptance Criteria
Out of Scope
- Euclid's defect note on a staged value surviving a throwing
beforeSet. It is a separate mechanism in Neo.createConfig.
- The other popup intermittents in #19657, for example #19684.
Related
Live latest-open sweep: the latest open issues at 2026-10-12T22:15Z, plus gh search issues for "mountedPromise". No equivalent found, and no competing claim in A2A.
Origin Session ID: 02040656-932a-48c8-8894-6f43d0368663
Context
LegacyPopupReturnNL.spec.mjs:12is red in every ten-sharddevrun of the Brain-gated burn-in, and on both attempts of the retry (run 38313618683). It fails with "Request timed out". Hosted probes, from an unmerged branch:Neo.worker.App.getWorkerId(), 3 s). The same call answers before the drag.updateVdom←afterSetVdom←update← themountedPromise.then()callback insrc/mixin/VdomLifecycle.mjs(line 1337). That is a microtask loop, so no message is ever handled again.thisin the paused frame islegacy-return-widget, the dragged widget, in all six samples. It readsmounted: false,vnodeInitialized: true, has a vnode,hasBeenMounted: true, holds a_mountedPromise, and has nomountedPromiseResolve, so that promise is already resolved. ItsparentIdis the popup's viewport.The Problem
updateVdom()on an unmounted component waits onmountedPromise.then(() => me.update()). Given a promise that an earlier mount resolved, the callback runs at once and finds the component still unmounted. It chains anotherthen, and the loop never ends. The worker starves, and every later request to it times out.The Architectural Reality
src/container/Base.mjsinsert(), on aNeoInstancefrom a parent in another window, withremoveFromPreviousParent: false:item._mounted = false. This raw write skipsafterSetMounted()on purpose, so the copy's later mount still triggers it. It also skips the unmount branch, whose first act isdelete me._mountedPromise.src/dashboard/Container.mjsonWindowConnect()takes that branch during a window drag:mainView.add(detachedItem.widget, false, !me.#isWindowDragging).src/component/Abstract.mjsget mountedPromise()returns any cached promise, whatevermountedreads now.Design authority: the getter's JSDoc — "It also handles unmounting by resetting the promise, so it can be safely awaited again if the component is remounted". A raw unmount gets no reset.
The Fix
The getter owns the cache, so it checks it. A cached promise with no pending resolver is resolved. While the component is unmounted, the getter discards it and creates a pending one. This covers every path that clears
mountedwithout the hook. The container's raw write stays, because skipping the unmount hooks for a copy is intended.Acceptance Criteria
mountedPromisemust stay pending until the next mount, and settle on it.update()on an unmounted component holding a promise from an earlier mount runsupdate()once. Red before the fix (with the count capped), green after.LegacyPopupReturnNL:12passes in the next ten-sharddevrun.Out of Scope
beforeSet. It is a separate mechanism inNeo.createConfig.Related
Live latest-open sweep: the latest open issues at 2026-10-12T22:15Z, plus
gh search issuesfor "mountedPromise". No equivalent found, and no competing claim in A2A.Origin Session ID: 02040656-932a-48c8-8894-6f43d0368663