LearnNewsExamplesServices
Frontmatter
id19594
titleA stack returned home into a tabs node inside a split never settles
stateClosed
labels
bugaidashboard
assigneesneo-opus-ada
createdAtOct 11, 2026, 6:17 AM
updatedAtOct 11, 2026, 7:19 AM
githubUrlhttps://github.com/neomjs/neo/issues/19594
authorneo-opus-ada
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[x] 19583 A popup re-opened by Undo or a perspective has no road home but close
closedAtOct 11, 2026, 7:18 AM

A stack returned home into a tabs node inside a split never settles

neo-opus-ada
neo-opus-ada commented on Oct 11, 2026, 6:17 AM

Context

I found this while building #19583's witness. I ran it headless at dev f1fbfe5c08 on FiveBeat's ordinary stage (viewport 1280×800, screen 1920×1080), driving NativeGestureDriver#executeStackReturnStep, the executor FiveBeat scene 4 runs in CI:

Vessel Home tabs node Result
metrics alone right-top-tabs, in a split with right-bottom-tabs hangs, every run (≥ 6)
commits alone scale-tabs, in a split with heavy-tabs hangs, every run (≥ 4)
metrics + queues right-top-tabs, split intact hangs (1/1)
metrics + commits (scene 4's staging) right-top-tabs; the commits tear-out collapsed right-bottom-tabs settles in 0.6 s, every run

A hang has these symptoms:

  • The pane is adopted into main's document.
  • lastCrossWindowTransfer.phases stops at ["documents-adopted"].
  • The Neural Link call that drives the gesture times out at 10 s.
  • The emptied vessel stays open, and TourController#getCueLog reports main's refresh pending.

Main then stays frozen. A later setActiveItem commit lands in the document and calls projectDockZoneDocument, but no refreshDockWorkspace starts within 3 s.

The Problem

The mouseup commits transferNode. VesselWorkspace#commitCrossWindowTransfer awaits main's refreshPromise before it records projections-settled (apps/workstation/view/VesselWorkspace.mjs:650), so the close never comes. Every later main projection chains onto that promise (src/dashboard/dock/Workspace.mjs:2295), so main's dock stops rendering.

The predicate is the home node's place in a split, not the vessel's item count. Two earlier readings of the stall, Mnemosyne's single-item note and my corroboration on #19583, were proxies. FiveBeat's staging never meets the predicate.

The Architectural Reality

  • transferNode admits the stable-topology path: VesselWorkspace#getRefreshOptions sets retainTopology, then Reconciler.reconcileProjection calls reconcileStableTopology.
  • reconcileStableTopology writes flex and wrapperStyle with setSilent on every retained node whose projection carries flex (src/dashboard/dock/projection/Reconciler.mjs:276-281). A silent write leaves needsVdomUpdate: true and schedules no flight (src/mixin/VdomLifecycle.mjs:1318-1319).
  • The stable branch of reconcileProjection awaits commitAncestors and then commitBars (Reconciler.mjs:494-502), before the host commit that would publish those writes (:503-505).
  • A committed bar's promiseUpdate() goes through mergeIntoParentUpdate (VdomLifecycle.mjs:955). It finds an ancestor flagged, registers as merged, and waits for that ancestor's update. Only the host commit after the await starts that update, so neither side moves.
  • Measured with temporary trace markers, since reverted: the hanging refresh stops at the commitBars await. At the call and again 3 s later, the target bar, its tab container and the split container above it all hold needsVdomUpdate: true, and none is updating. The flags' writer is Reconciler.mjs:277. In the settling case, the bar's ancestors hold no flag.
  • The awaited bar commit publishes silent bar-membership changes. It existed as materializedBars; #18025 (2f8fc79792) renamed it commitBars and widened it to both sides of a cross-bar move. commitAncestors serves same-window native reparenting. Either one hangs under a flex-bearing retained ancestor.
  • Design authority: this is a hang that contradicts the surfaces' own JSDoc:
    • VdomLifecycle#promiseUpdate: "settles once that render has landed in the DOM";
    • executeStackReturnStep: it "waits through atomic transferNode, main projection, native close request, and physical topology exit".
    • The Dock guide also lists stack return among the "four roads home" (learn/guides/uibuildingblocks/DockLayoutsWindows.md:85).

The Fix

In Reconciler.mjs:

  • reconcileStableTopology returns its flex writes as geometry instead of applying them in mid-scan.
  • reconcileProjection applies them silently right before its depth -1 host commit, which publishes them in the same flight as today.

This adds no flight, and no descendant commit merges into an ancestor update that nothing has scheduled. width and height stay as they are: a non-silent set starts its own flight, which a descendant awaits through isParentUpdating, not through a merge.

Rejected alternatives:

  • Flushing the written ancestors before the bars adds flights to every stable projection.
  • A non-silent flex costs one flight per split child on every splitter drag.
  • Changing mergeIntoParentUpdate changes engine-wide semantics for every silent write.

Acceptance Criteria

  • Unit test with real in-process VDom flights: a retainTopology projection that adds a tab to a tabs node inside a split settles, and the new header button renders. Without the fix, the test hangs.
  • E2E: executeStackReturnStep brings a single-item vessel home into a split-held tabs node (metrics → right-top-tabs, commits → scale-tabs). Each return passes the four phases in order, the emptied vessel closes, and the pane is in its home node.
  • E2E: the same holds for a two-item vessel whose home node stays in its split (metrics + queues).
  • FiveBeat scene 4 and the dock unit suites stay green.

The evidence is in #19595, merged as 6188a7b049 with Emmy's approval:

  • DockStableSplitCommit.spec.mjs is red without the fix ("did not settle within 5000 ms").
  • WorkstationStackReturnSplitNL.spec.mjs is red 3/3 without the fix ("Request timed out").
  • The full unit suite and CI's e2e job are green.

Out of Scope

  • #19583: the close path and identity of re-opened windows. It gets re-measured on the fixed engine.
  • Native-return adoption of a single-item vessel (Mnemosyne's third note on #19583).
  • VdomLifecycle merge semantics.

Related

#19583 · #19569 · #15252 · #18025 · #19188

Sweeps:

  • Live latest-open sweep: the latest 20 open issues at 04:2xZ, plus open-issue searches for "stack return never settles", "reconcileStableTopology", "stack return hangs", "emptied vessel stays open", "commitBars" and "refreshPromise pending", and open PRs. No equivalent found.
  • A2A in-flight sweep: the last 30 messages, all states. No claim on the dock reconciler or stack return.
  • MC sweep: "stack return home never settles emptied vessel window stays open main refresh pending reconcileProjection commitBars flex", 6 results. No prior decision found; #19188 fixed ownership after a whole-stack return, always on the composed staging.
  • Own-assignment sweep: #19583 and #19569 are mine and on the same surface. Both are confounded by this hang; neither covers it.

Origin Session ID: a6673f82-f995-4e40-83a9-3093d4ecd357 Retrieval Hint: "stack return split tabs node hang reconcileStableTopology silent flex commitBars mergeIntoParentUpdate"

tobiu referenced in commit 6188a7b - "fix(dashboard): a stack returned into a split-held tabs node settles (#19594) (#19595) on Oct 11, 2026, 7:18 AM
tobiu closed this issue on Oct 11, 2026, 7:18 AM