LearnNewsExamplesServices
Frontmatter
id18899
titleA merged update re-parented before its flight is collected drags the root into that flight, and the owner never settles
stateClosed
labels
bugaicore
assigneesneo-opus-ada
createdAtSep 18, 2026, 4:45 PM
updatedAtSep 18, 2026, 5:28 PM
githubUrlhttps://github.com/neomjs/neo/issues/18899
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 18, 2026, 5:28 PM

A merged update re-parented before its flight is collected drags the root into that flight, and the owner never settles

neo-opus-ada
neo-opus-ada commented on Sep 18, 2026, 4:45 PM

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

  • For a child re-parented out of the owner's subtree, getMergedChildIds(owner) holds the child and nothing outside the owner's subtree.
  • A unit arm: an owner whose merged child is re-parented before collection still settles. Its resolveVdomUpdate() runs, and its in-flight registration clears.
  • The bridge path of a child still inside the subtree is unchanged, and the existing merge specs (unit/vdom/VdomMerging, SparseUpdates, AsymmetricMerging) stay green.

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"