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
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.
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#18664suspect down on that basis. @neo-gpt-emmy then matched the operator's screenshot host to a temporary export of @neo-gpt's#18674header work built on7bd4db9f74— 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.
#18621is 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:That stand-in is created as
Neo.create(VesselPlaceholder)with no config, anddockItemIdappears nowhere inVesselPlaceholder. So an unstamped component legitimately occupiesbody.items[index]— the exact arrayReconciler#reconcileTabChromezips against the tab bar'sdockItemIdsto decide which live component is which item.#18667made 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.mjsswapped:#186673044d7f86b4add999757Two 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);.itemIdis already in scope:stage()is declared({itemId, windowId} = {}).src/dashboard/dock/window/VesselPlaceholder.mjs— declaresntype,baseCls,isLoadingandrole. Zero occurrences ofdockItemId.src/dashboard/dock/projection/Reconciler.mjs#reconcileTabChrome— builds its live-item map frombar.sortZoneConfig.dockItemIds×body.items[index], and that map outranks the caller'sresolveItem.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 twentyDockVesselEmbodimentunit 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.
itemIdis 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:
#18621's shipped arm — a drifted bar must not hand one item another item's paneNo 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.
Neo.dashboard.dock.Workspace#resolveLivePanesrc/dashboard/dock/Workspace.mjs, the method's own docblock, which states that stand-in exclusion is by CATEGORY and never one instance at a timenullVesselPlaceholderinstance created byVesselEmbodiment.stage()src/dashboard/dock/window/VesselEmbodiment.mjs, the creation site, whereitemIdis already in scopedockItemId, so a projection can recognize it in the slot it occupies rather than inferring from positionRetirement: 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
4add999757, then greens on the fix. Presence assertions do not satisfy this; the arm counts.#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.DockVesselEmbodimentarms and the workstation unit folder stay green.Out of Scope
#18667. The pairing repair it delivered is the fix for a silent mis-binding and is not traded away for this; if @tobiu wantsdevclean 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.#18414.Avoided Traps
#18664).Reconciler.mjsswapped, and the duplication follows the file.#18621's arm requires the opposite outcome at identical inputs. One predicate cannot serve both.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.
VesselPlaceholdersearch: 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_memorieson "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.