Context
Found while measuring #18885, where a grid lock change left App Worker: vdom update wedged: "neo-grid-body-4" has been in-flight for over 5000ms. It reproduces without a grid, at the registry, in a unit probe on dev@b6740acf61:
registerMerged('probe-owner', 'probe-child', 1, 2) // probe-owner > probe-mid > probe-child
getMergedChildIds('probe-owner') → ["probe-child", "probe-mid"]
probe-child.parentId = 'probe-other' // probe-root > probe-other
getMergedChildIds('probe-owner') → ["probe-child", "probe-other", "probe-root"]The Problem
For each merged child registered at a distance above 1, VDomUpdate#getMergedChildIds() adds the bridge path: every component between the child and the owner, found by walking parentId upward until the owner. The walk has no other stop. A child re-parented after it merged, to a place outside the owner's subtree, never meets the owner, so the walk adds every ancestor up to the root.
The owner's flight then collects those ancestors as payloads (VdomLifecycle#executeVdomUpdate()). An ancestor whose depth covers the owner makes the collision filter delete the owner's own payload. The reply carries no vnode for the owner, so its resolveVdomUpdate() never runs, its in-flight registration never clears, and the watchdog reports it wedged (the #12946 class).
In #18885's trace, the center body's flight went out keyed to neo-viewport-1 alone and was answered for the viewport alone. The merged child was the cell editor: it had merged while its update was in flight, and grid.Row#applyRendererOutput re-parented it to the end body's row in the same tick.
The Architectural Reality
src/manager/VDomUpdate.mjs: registerMerged() records {childUpdateDepth, distance}. getMergedChildIds() builds the bridge from the child's parentId chain.
src/mixin/VdomLifecycle.mjs: mergeIntoParentUpdate() merges along the same parentId chain. executeVdomUpdate() collects payloads recursively, then filters the ones an ancestor's depth covers.
- Re-parenting while a merge is pending is ordinary:
grid.Row#applyRendererOutput sets the editor's parentId on every render, and container.Base#insert moves an item to another parent.
The Fix
getMergedChildIds() adds a bridge path only when the walk reaches the owner. A child that has left the owner's subtree keeps its place in the set and goes out as its own disjoint payload, as a merged child at distance 1 does. It is never a bridge through components the owner does not contain.
Acceptance Criteria
Out of Scope
- The grid trigger itself, which #18885 removes by holding the editor out of every cell during a lock change.
vdom.Helper's ordering of an innerHTML switch against a move, the other engine defect #18885 found (its own ticket).
Related
#18885 · #18897 · #12946 · #17589
Decision Record impact: none.
Live latest-open sweep: the latest 20 open issues at 2026-09-18T14:44Z, and an all-state search for getMergedChildIds and bridge path: no equivalent (#8869 and #17589 are closed neighbours). A2A: no claim on this scope.
Origin Session ID: 6ed6621f-e2e4-4b9d-9a95-744679cb3c59
Retrieval Hint: "VDomUpdate getMergedChildIds bridge path re-parented merged child escapes to root collision filter owner wedged"
Context
Found while measuring #18885, where a grid lock change left
App Worker: vdom update wedged: "neo-grid-body-4" has been in-flight for over 5000ms. It reproduces without a grid, at the registry, in a unit probe ondev@b6740acf61:registerMerged('probe-owner', 'probe-child', 1, 2) // probe-owner > probe-mid > probe-child getMergedChildIds('probe-owner') → ["probe-child", "probe-mid"] probe-child.parentId = 'probe-other' // probe-root > probe-other getMergedChildIds('probe-owner') → ["probe-child", "probe-other", "probe-root"]The Problem
For each merged child registered at a distance above 1,
VDomUpdate#getMergedChildIds()adds the bridge path: every component between the child and the owner, found by walkingparentIdupward until the owner. The walk has no other stop. A child re-parented after it merged, to a place outside the owner's subtree, never meets the owner, so the walk adds every ancestor up to the root.The owner's flight then collects those ancestors as payloads (
VdomLifecycle#executeVdomUpdate()). An ancestor whose depth covers the owner makes the collision filter delete the owner's own payload. The reply carries no vnode for the owner, so itsresolveVdomUpdate()never runs, its in-flight registration never clears, and the watchdog reports it wedged (the #12946 class).In #18885's trace, the center body's flight went out keyed to
neo-viewport-1alone and was answered for the viewport alone. The merged child was the cell editor: it had merged while its update was in flight, andgrid.Row#applyRendererOutputre-parented it to the end body's row in the same tick.The Architectural Reality
src/manager/VDomUpdate.mjs:registerMerged()records{childUpdateDepth, distance}.getMergedChildIds()builds the bridge from the child'sparentIdchain.src/mixin/VdomLifecycle.mjs:mergeIntoParentUpdate()merges along the sameparentIdchain.executeVdomUpdate()collects payloads recursively, then filters the ones an ancestor's depth covers.grid.Row#applyRendererOutputsets the editor'sparentIdon every render, andcontainer.Base#insertmoves an item to another parent.The Fix
getMergedChildIds()adds a bridge path only when the walk reaches the owner. A child that has left the owner's subtree keeps its place in the set and goes out as its own disjoint payload, as a merged child at distance 1 does. It is never a bridge through components the owner does not contain.Acceptance Criteria
getMergedChildIds(owner)holds the child and nothing outside the owner's subtree.resolveVdomUpdate()runs, and its in-flight registration clears.unit/vdom/VdomMerging,SparseUpdates,AsymmetricMerging) stay green.Out of Scope
vdom.Helper's ordering of aninnerHTMLswitch against a move, the other engine defect #18885 found (its own ticket).Related
#18885 · #18897 · #12946 · #17589
Decision Record impact: none.
Live latest-open sweep: the latest 20 open issues at 2026-09-18T14:44Z, and an all-state search for
getMergedChildIdsandbridge path: no equivalent (#8869 and #17589 are closed neighbours). A2A: no claim on this scope.Origin Session ID: 6ed6621f-e2e4-4b9d-9a95-744679cb3c59 Retrieval Hint: "VDomUpdate getMergedChildIds bridge path re-parented merged child escapes to root collision filter owner wedged"