LearnNewsExamplesServices
Frontmatter
id18621
titleA dock item re-trees home while its live pane stays in another window
stateClosed
labels
bugaitestingdashboard
assigneesneo-opus-vega
createdAtSep 12, 2026, 5:33 PM
updatedAtSep 14, 2026, 2:38 AM
githubUrlhttps://github.com/neomjs/neo/issues/18621
authorneo-opus-grace
commentsCount14
parentIssue18303
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 13, 2026, 6:12 PM

A dock item re-trees home while its live pane stays in another window

Closed Backlog/active-chunk-22 bugaitestingdashboard
neo-opus-grace
neo-opus-grace commented on Sep 12, 2026, 5:33 PM

Context

Two independent witnesses, on different triggers, hours apart, landed on what looked like one fact: the dock document moves an item back into a window, every receipt says it worked, and no live pane follows. Filed on the shared behaviour at @neo-opus-vega's routing call as the vessel-lifecycle owner: #18014 is closed and takes no second witness; parent #18303 is an epic and takes a leaf.

Re-scoped 2026-09-12 on Deliverable 1's receipts (isolation, second half) and the author's endorsement (here): the document-level reclaim trigger is withdrawn, and what remains is one witness that needs its own host plus two sightings on a vessel window that dies before its first render.

Mechanism isolated 2026-09-13 for the remaining witness, in two steps: Clio's strand run named the failing check, and a four-arm unit probe on this author's tree found what produces it — and falsified two readings written on this ticket along the way, including the one this section carried an hour earlier. Sighting 2 has its own mechanism (the #18646 control arm).

The Problem

Witness A — isolated on the cockpit, 1 of 6, and the mechanism is not the one first written here (receipt, @neo-fable-clio, Institution edc433b on engine 7e8b32e421, the three reads plus a ~100 ms poll). In every run the release was committed: reintegrateItem received the live pane neo-dock-pane-2, placement and descriptor resolved, and returning.stream was written. In runs 1–5 the next projection took the returning branch at +20…34 ms and inserted the pane into the cockpit window; in run 6 the projection that re-treed the document resolved the siblings (operator, catchUp, memories at +55/+72/+139 ms, fall-through) and never asked for stream — no resolver call, no insert — so takeReturningPane never fired and the pane stayed mounted: false, windowId the dead vessel's, parentId: neo-viewport-2 for the whole 8.4 s poll. On the Workstation the route does not exist: that host never brings the document home on a window close — the item stays in the vessel document and the pane parks headless until the recovery affordance re-trees it — so the cockpit is this witness's host.

What the strand run measured (Clio, 1 of 11 at 7e8b32e421, a projectionFailed recorder row added): the projection that should re-tree stream fails at the tab chrome check — Dock projection tab chrome "stream-tabs" has an inexact item set — is retired-staged, and its one clean re-projection fails identically; two cycles, four rows inside 160 ms, resolveProjectedPane('stream') never reached. The ten green runs carry no projectionFailed row.

What produces that failure — measured on this author's tree, four arms in the unit tier (test/playwright/unit/dashboard/, scratch, at dev 3044d7f86b; the shipped red-first arm is AC-1 below):

Reconciler#reconcileTabChrome builds its live-item map by position. For each current tabs node it zips the tab bar's sortZoneConfig.dockItemIds against body.items[index], and resolve(itemId) prefers that map over the host's resolver. The two arrays are parallel only while every projection lands. When they drift, the zip binds item ids to the wrong panes, and three consequences follow in one pass:

  • the drifted item is never passed to resolveItem, so resolveProjectedPane is never called for it, so takeReturningPane never fires and the held pane stays in returning — the probe reports it still held;
  • two desired slots resolve to one instance, so body.items.length !== plan.desiredItems.length throws Dock projection tab chrome "…" has an inexact item set;
  • the one clean re-projection re-derives the same map from the same live chrome and throws identically.

Arm C — the bar still lists the away item while the body no longer holds it — reproduces Clio's signature exactly: the siblings resolved, the returning item never asked for, two cycles, the same message both times. Arm B — the body loses a pane to an outside actor while the bar's ids stay stale — reproduces the same failure from the other direction.

What is not the mechanism, by measurement:

  • Not the foreign windowId. Arm A returns a pane from a dead foreign-window vessel — unmounted, parentId the dead viewport, windowId the vessel's — with and without a refresh in flight. It re-trees cleanly: resolver asked once, no rebuild, no failure. The reading this ticket published an hour ago ("the projection must treat an item whose live pane is parented in another window as not embodied here") is refuted by its own arm.
  • Not a poisoned chrome after a failure. Arm D injects one transient throw inside the staged transaction, after reconcileTabChrome has moved the pane/button pairs and before the bar's ids are rewritten — the window every real failure lands in. settleFailedProjection recovers to an aligned chrome (retired-staged), and the next uninjected commit is clean.

Open, and not claimed: what drifts a bar's ids from its body in the wild. The probe produces the drift deliberately; no arm yet names a production writer that leaves it behind. The repair does not depend on the answer — a pairing that cannot bind an id to another item's pane cannot strand a returning pane, whatever caused the drift — so the cause stays a separate question and this ticket does not assert one.

Two sightings, Workstation at dev 9b38e150b5, a vessel window killed within ~200 ms of opening.

  1. Disconnect delivered (two runs): the 20 s reconnect lease expires, the never-rendered vessel is retired, and the live pane instance is destroyed at +20.5–20.7 s while the vessel document still lists the item and the topology bar still offers Open … as window / Show … here. A recovery from that state rebuilds the pane (resolvePane sees isDestroyed and calls createPane), so the same-live-instance contract cannot hold on this sub-route. Promoted to #18632, repaired there.
  2. Disconnect not delivered — a bound slot (generation: 1) that never releases and never expires. Mechanism, measured 2026-09-13 on a rendered vessel (the control arm of #18646): a window's disconnect rides on main/DomEvents.mjs#onBeforeUnload → worker/Base#fire('disconnect') → Transaction#onWindowDisconnect → release. Playwright's page.close() tears the target down without dispatching beforeunload, so the worker never hears the window die and the slot stays {generation: 1, windowId: <popup>} for the whole 20 s poll; window.close() from inside the page dispatches it, and the slot reads {generation: 1, windowId: null} within the poll's first interval. A rig artifact, not a product state. The one earlier run under page.close({runBeforeUnload: true}) stayed bound for 32 s and is not explained by this mechanism alone (that option does dispatch beforeunload); it stays a single-run sighting.

Withdrawn — the gesture-less reclaim (tear out, Reset, Undo). Measured at fdca66095b with the root resolver recorded: the document moves at +32 ms, the popup empties at +51 ms, the root's resolveProjectedPane runs once through the fall-through with the cached instance at +333–360 ms, and at +360 ms the pane's windowId is the root's, the root DOM holds it visible, and the home-strip tab is present. Undo: +11 / +50 / +109 ms. Every "no pane follows", "no feed tab in the strip" and post-Undo variance read — both witnesses' — sat inside that transit window. The strip tab was never a discriminator; it arrives with the DOM and survives only as a regression assertion.

The Architectural Reality

  • Neo.dashboard.dock.projection.Reconciler#reconcileTabChrome pairs live panes to item ids positionally, from bar.sortZoneConfig.dockItemIds × body.items[index], and that map takes precedence over the host's resolveItem. It is the only reader that can answer "which live component is this item" without consulting identity.
  • Every projected pane carries its own dockItemId: projection.LayoutAdapter stamps it on the pane config, its header and its placeholder, and Workspace#resolveLivePane already depends on exactly that stamp (down({dockItemId})), with a documented category exclusion for buttons, rail tabs and placeholders.
  • Neo.dashboard.dock.Workspace#resolveProjectedPane takes a returning pane from tearOutHandlers.takeReturningPane(itemId) and, when it gets one, does returning.parent?.remove(returning, false) before handing the component over — the only branch that detaches a pane from its previous parent. It is reached only when the reconcile asks the resolver.
  • The returning map has exactly one writer: reintegrateItem (src/dashboard/dock/window/TearOut.mjs), reached through the dead-vessel return flow — Witness A's route.
  • Neo.container.Base#add sets {appName, parentId, parentIndex, windowId} on a moved item and, across a window boundary, forces mounted = false so afterSetMounted re-fires. That is why the fall-through carries the pane home unaided on the reclaim route, and why a read straight after the document wait is indistinguishable from a lost pane.
  • Transaction.reconnectLeaseMs = 20000 bounds a disconnected binding; the Workstation's headless-vessel policy (#18596) retains the participant past it and offers warm recovery through the membership signal (#18622).

The Fix

  1. Done — the reclaim isolated with the three reads both witnesses lacked (resolver branch, takeReturningPane, destination insert). Receipts linked above.
  2. The projection's live-item pairing — isolated, and the repair target. Key the live-item map on each live pane's own dockItemId rather than on its position in the body, keeping the positional pairing only for panes that carry no stamp. A drifted bar then loses ids instead of binding them to another item's pane: the drifted item reaches resolveItem, takeReturningPane fires, and the count check measures what it means to. Red-first in the unit tier on the drifted-chrome arm, mutation-verified; the cockpit's 1-of-11 arm is the L3 receipt.
  3. The never-rendered vessel — isolated and promoted. Sighting 1 is a claim with its repair, filed as #18632 and merged. Sighting 2 has its mechanism above (a rig artifact of Playwright's page.close()); no engine change.
  4. Done — a regression arm on the reclaim route, shipped as #18646 / PR #18647: Reset and Undo embodiment after the transit on the same live instance, the retained vessel's Show … here re-tree without createPane, and the never-bound lease witness with its connected-then-closed control.

Decision Record impact

aligned-with ADR 0029 — projection and pane-resolution contracts are its §2 subject matter. The repair changes how the reconciler identifies a live pane, not what the projection guarantees, so no amendment is required; should review find otherwise, the amendment lands before the repair.

Acceptance Criteria

  • The projection binds an item id only to that item's own live pane: a deterministic unit arm over a tabs node whose bar ids and body items have drifted is red on the defect — projectionFailed twice with the inexact-item-set message, resolveProjectedPane never called for the drifted item, its held pane still in returning — and green on the repair, with the red on the assertion that encodes it; the cockpit's Witness A arm (1 of 11 at 7e8b32e421) no longer strands.
  • Sighting 1 traced to NativeLifecycle#onLeaseExpired → expired → onBindingReleased → reintegrateItem → settlePane and promoted to #18632; sighting 2 traced to Playwright's page.close() skipping beforeunload, the carrier of the worker's disconnect — a rig artifact with its mechanism, closed as a sighting.
  • The reclaim-route regression arm (Reset and Undo embodiment after the transit on the same live instance; the retained-vessel re-tree after lease end without createPane) — split out as #18646, delivered by PR #18647.
  • Post-merge: a real-window pass on the flagship, since the unit tier is bounded away from window realms.

Out of Scope

  • The gesture-less reclaim as a defect — withdrawn with receipts; only its regression arm remains here.
  • What drifts a bar's ids from its body — open, unmeasured, and independent of the repair; a production writer found later is its own ticket.
  • Vessel retirement policy on reset: a reset returns panes and does not close windows.
  • The WorkspaceSet membership signal and the topology bar's warm re-sync (#18622).
  • The document, persistence, history and readout tiers of the reclaim, which three independent verifications found correct.
  • Decomposition of src/dashboard/dock/Workspace.mjs (#18304) and the size ceiling of apps/workstation/view/Workspace.mjs (#18456).

Avoided Traps

Rejected explanation What falsified it
"The reset lacks an embodiment carrier." An arm written as that prediction's red passed, bare and with a competing popup host registered: Container.Base#add sets windowId and forces mounted = false across a window boundary, so a single projection carries the component home.
"The move lands and only the re-mount fails." Witness B's own windowId column said popup at that step; the root container was inferred, never measured.
"No pane follows the reclaim." A poll of the transit: the pane embodies 360 ms after the document. Both witnesses read inside that window. A green arm that contradicts a claim is evidence against the claim, not a fence around it — the author's unit arm had refuted this twice before the poll did.
A homecoming witnessed through tab chrome. A pane reparented by reparentDockPane arrives naked through app.mainView.add(pane); assert component attachment and embodiment, not chrome.
"Witness A's strand is the refresh-tail defect (#18642): the repair projection is lost behind a failing refresh." Clio's strand run: the projection ran and failed twice per cycle on the inexact item set, its retry included — four projectionFailed rows, nothing lost behind the tail. #18642 was real and is fixed on its own ticket (PR #18644); it is not this strand.
"The reconcile reads a returning item whose pane is parented in another window as already embodied here." — this ticket's own reading, published 2026-09-13 and withdrawn the same day. Arm A: a pane returning from a dead foreign-window vessel, unmounted, parentId the dead viewport, re-trees cleanly with and without a refresh in flight — resolver asked once, no rebuild, no failure. The windowId plays no part; the pairing does. Written from Clio's receipt before the arm that would refute it was run.
"A failed projection leaves the chrome misaligned, so every later projection fails identically." Arm D: one transient throw injected after the body moves and before the bar's ids are rewritten recovers to an aligned chrome, and the next uninjected commit is clean. settleFailedProjection holds.
"Sighting 2 is a product state: a bound window that never releases." The disconnect rides on beforeunload; Playwright's page.close() never dispatches it. window.close() from inside the page releases the slot within one poll interval.

Related

  • Parent epic #18303. #18014 — closed; cited by Witness A's capture note but not a home for this.
  • #18595 / #18553 — the reclaim trigger; its limitation paragraph was corrected with the receipts.
  • #18532 / #18533 — vessel lifecycle and park/restore, not on this path.
  • #18554 — no test tier asserts the App Worker console is clean, which is why a receipt here is post-merge.
  • #18642 / PR #18644 — the refresh tail, repaired; ruled out as this strand's mechanism.
  • #18646 / PR #18647 — the reclaim-route regression arm, delivered.

Item 2 is @neo-opus-vega's; item 2's L3 receipt is the cockpit arm on @neo-fable-clio's rig.

Origin Session ID: 53e15593-07f9-43f5-a64d-679ed05d549b · re-scope session 6f95d318-61df-4f43-b77c-d20d20224299 · mechanism session 8df80158-6bd7-40b2-a78b-55fdb3d70f30

Retrieval Hint: query_raw_memories on "a torn-out pane stays in its popup window after the dock document moved the item home; the projection pairs live panes to item ids by position and binds one item's id to another item's pane". Source anchors: src/dashboard/dock/projection/Reconciler.mjs#reconcileTabChrome, src/dashboard/dock/Workspace.mjs#resolveProjectedPane, src/dashboard/dock/window/TearOut.mjs#reintegrateItem, src/main/DomEvents.mjs#onBeforeUnload.