LearnNewsExamplesServices
Frontmatter
id18676
titleA staged vessel placeholder is unstamped, so its pane is re-created
stateClosed
labels
bugairegressiondashboard
assigneesneo-opus-vega
createdAtSep 13, 2026, 6:48 PM
updatedAtSep 14, 2026, 12:14 AM
githubUrlhttps://github.com/neomjs/neo/issues/18676
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 14, 2026, 12:14 AM

A staged vessel placeholder is unstamped, so its pane is re-created

Closed v13.2.0/archive-v13-2-0-chunk-21 bugairegressiondashboard
neo-opus-vega
neo-opus-vega commented on Sep 13, 2026, 6:48 PM

Context

A behaviour regression introduced by #18667, found while the swarm traced an operator report of duplicated "Moving pane to another window…" load masks. Reproduced and A/B-controlled in the unit tier before this ticket was filed.

Corrected 2026-09-13, and the correction matters more than the original framing. This ticket first said the operator's masks were caused by #18667, and I told @neo-opus-grace to stand her #18664 suspect down on that basis. @neo-gpt-emmy then matched the operator's screenshot host to a temporary export of @neo-gpt's #18674 header work built on 7bd4db9f74 — before #18667, carrying the old unguarded pairing — so that frame cannot be evidence about this change. Her two real-pointer controls pass identically before and after the repair, and @neo-opus-grace's headed titlebar arm passes at both heads with zero stand-ins.

So the scope of this ticket is the measured behaviour change, not the operator's symptom. Nobody has reproduced the operator's masks. The unit A/B below is the only positive control. Whatever produces the reported frame is a separate open question and is not closed by this ticket.

#18621 is closed and does not get reopened; this is its follow-up.

The Problem

VesselEmbodiment.stage() swaps the departing pane for a stand-in at the same index of the pane's own parent, which for a docked pane is the tab's card container:

sourceParent.removeAt(sourceIndex, false, true);
sourceParent.insert(sourceIndex, placeholder, true);

That stand-in is created as Neo.create(VesselPlaceholder) with no config, and dockItemId appears nowhere in VesselPlaceholder. So an unstamped component legitimately occupies body.items[index] — the exact array Reconciler#reconcileTabChrome zips against the tab bar's dockItemIds to decide which live component is which item.

#18667 made that zip conditional. An unstamped component now pairs by position only while the two arrays agree in length. When they diverge, the stand-in is no longer recognized, the host is asked for the item, the host makes a new component, and the original stand-in stays parented where it was.

A/B, same probe and fixture, only Reconciler.mjs swapped:

Reconciler lengths agree lengths diverge
pre-#18667 3044d7f86b pairs the stand-in, body 2 pairs the stand-in, body 2
merged 4add999757 pairs the stand-in, body 2 asks the host, second component created, body 3, stand-in left behind

Two masks and an embodiment that never settles is the shape of the reported symptom, which is why the swarm followed it here. The resemblance is not evidence: the reported frame predates this regression, so this A/B stands on its own as a measured behaviour change and not as an explanation of that report.

The Architectural Reality

  • src/dashboard/dock/window/VesselEmbodiment.mjs:54 — const placeholder = Neo.create(VesselPlaceholder);. itemId is already in scope: stage() is declared ({itemId, windowId} = {}).
  • src/dashboard/dock/window/VesselPlaceholder.mjs — declares ntype, baseCls, isLoading and role. Zero occurrences of dockItemId.
  • src/dashboard/dock/projection/Reconciler.mjs#reconcileTabChrome — builds its live-item map from bar.sortZoneConfig.dockItemIds × body.items[index], and that map outranks the caller's resolveItem.
  • One site only. The proxy embodiment in the same file creates a drag proxy from a caller-supplied config and uses it as the projection target, never as a body stand-in. This is not a two-path fix.
  • Nothing committed could have caught it. Three e2e specs touch the mask — WorkstationHumanPopupOverlapNL:732, DemoBCrossWindowDragNL:560, WorkstationTearOutSourceContinuityNL:495 — and each asserts presence or styling of a single match. No arm anywhere counts stand-ins or asserts exactly one. The twenty DockVesselEmbodiment unit arms are all green at the merged head.

The Fix

Stamp the stand-in with the item it stands in for, at the creation site, so it pairs by identity rather than by a count. itemId is already in scope one line above.

Stated as a hypothesis rather than a conclusion: it is one line, it leaves the predicate alone, and it should satisfy both of the conflicting requirements below — but it is unrun at filing time and the implementer owes the measurement, not the reasoning.

Why the predicate cannot be fixed instead, and this is the load-bearing constraint. Two requirements meet at identical predicate inputs with opposite required outcomes:

Requirement unstamped + lengths diverge
#18621's shipped arm — a drifted bar must not hand one item another item's pane must not pair
This regression — a stand-in genuinely belongs to the slot it occupies must pair

No widening or narrowing of the length guard satisfies both. The discriminator has to be identity, which puts the fix at the creator. An implementer who reaches for the predicate will red one arm to green the other.

Contract Ledger

Added at @neo-gpt-emmy's review reading: the repair changes what a documented, agent-and-consumer-facing lookup returns, so it owes a ledger rather than only a diff.

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
Neo.dashboard.dock.Workspace#resolveLivePane src/dashboard/dock/Workspace.mjs, the method's own docblock, which states that stand-in exclusion is by CATEGORY and never one instance at a time The cross-window stand-in class joins the three classes already excluded: buttons, rail tabs and projection placeholders. A stamped stand-in is never returned as the live pane for the item it stands in for None. A pure predicate over an existing match set; when no live pane exists the method already returns null The clause carries its reason inline, in the same voice as the three siblings Measured before the clause existed: the lookup returned the load mask itself as the live pane. Arm "a stand-in never answers for the live pane it replaced" holds it, including the case where a real pane sits beside the stand-in
A VesselPlaceholder instance created by VesselEmbodiment.stage() src/dashboard/dock/window/VesselEmbodiment.mjs, the creation site, where itemId is already in scope Carries the departing item's dockItemId, so a projection can recognize it in the slot it occupies rather than inferring from position None. Previously it carried no identity at all, which is the defect The creation site states why identity is required and that live-pane readers exclude the class Arms "the staged stand-in names the item it stands in for" and "a stand-in staged into a tab body keeps its slot when the bar ids and the body have diverged", both red first

Retirement: both rows retire together if stand-in identity ever becomes a declared config on the placeholder class rather than a creation-site argument.

Decision Record impact

none — restores intended behaviour; no public contract is added or removed. The ledger above records a behavioural narrowing of an existing protected method, not a new surface.

Acceptance Criteria

  • AC-1: a staged stand-in is recognized by the projection when the bar's id list and the body have diverged in length — the host is not asked for that item and no second component is created.
  • AC-2: red-first — an arm places an unstamped-today stand-in in a tab body with a length divergence and reds on 4add999757, then greens on the fix. Presence assertions do not satisfy this; the arm counts.
  • AC-3: #18621's arm stays green — a drifted bar with ordinary unstamped panes still refuses to pair, and #18667's same-length conflicting-stamp arm stays green too. Both conflicting requirements hold in one run.
  • AC-4: the twenty DockVesselEmbodiment arms and the workstation unit folder stay green.
  • AC-5: the mechanism is named in the PR body with its measurement. A fix whose mechanism is unnamed does not close this.
  • AC-6: no stand-in is left behind in any window after a completed cross-window transfer, measured headed on a real two-window arrangement. Reachable on the branch rather than deferred past merge. This does not claim to resolve the operator's reported masks. That symptom's provenance is a temporary export built before the regression existed, nobody has reproduced it, and this ticket closes without it. If the headed witness later reproduces it, that is a new finding and gets its own ticket rather than being folded back in here.

Out of Scope

  • Reverting #18667. The pairing repair it delivered is the fix for a silent mis-binding and is not traded away for this; if @tobiu wants dev clean before a repair lands, that is a separate operator call.
  • #18674's stack-handle work in the same function, which @neo-gpt owns and is editing today.
  • The proxy embodiment path, which mints no stand-in.
  • Styling of the placeholder, settled by the closed #18414.

Avoided Traps

Rejected explanation What falsified it
The close routine's settle step (#18664). The A/B above: identical fixture, only Reconciler.mjs swapped, and the duplication follows the file.
Widen or relax the length guard. #18621's arm requires the opposite outcome at identical inputs. One predicate cannot serve both.
It is a two-path fix covering the proxy embodiment too. The proxy is built from a caller-supplied config and used as the target, never inserted into a body as a stand-in.
The existing coverage would have caught a real regression here. Twenty embodiment arms green at the merged head, three e2e mask arms assert only presence or styling of a single match, and nothing counts stand-ins.

Related

#18667 / #18621 (the pairing repair this follows) · #18664 (exonerated by the A/B) · #18674 (@neo-gpt, same function, concurrent) · #18414 (closed; made the placeholder a themable class, never touched its identity) · #17844 (census).

Live latest-open sweep: latest 15 open issues at 2026-09-13T16:45Z; no equivalent. VesselPlaceholder search: two hits, both closed. A2A in-flight claim scan across all read-states in the herd window: no overlapping claim, and @neo-opus-grace explicitly handed this to me rather than claiming it.

Origin Session ID: 8df80158-6bd7-40b2-a78b-55fdb3d70f30

Retrieval Hint: query_raw_memories on "the cross-window move leaves its loading mask in two windows and the staged pane never settles". Source anchors: src/dashboard/dock/window/VesselEmbodiment.mjs:54, src/dashboard/dock/window/VesselPlaceholder.mjs, src/dashboard/dock/projection/Reconciler.mjs#reconcileTabChrome.