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.
- 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.
- 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
- Done — the reclaim isolated with the three reads both witnesses lacked (resolver branch,
takeReturningPane, destination insert). Receipts linked above.
- 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.
- 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.
- 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
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.
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:
#18014is 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
edc433bon engine7e8b32e421, the three reads plus a ~100 ms poll). In every run the release was committed:reintegrateItemreceived the live paneneo-dock-pane-2, placement and descriptor resolved, andreturning.streamwas written. In runs 1–5 the next projection took thereturningbranch 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,memoriesat +55/+72/+139 ms, fall-through) and never asked forstream— no resolver call, no insert — sotakeReturningPanenever fired and the pane stayedmounted: false,windowIdthe dead vessel's,parentId: neo-viewport-2for 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, aprojectionFailedrecorder row added): the projection that should re-treestreamfails 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 noprojectionFailedrow.What produces that failure — measured on this author's tree, four arms in the unit tier (
test/playwright/unit/dashboard/, scratch, at dev3044d7f86b; the shipped red-first arm is AC-1 below):Reconciler#reconcileTabChromebuilds its live-item map by position. For each current tabs node it zips the tab bar'ssortZoneConfig.dockItemIdsagainstbody.items[index], andresolve(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:resolveItem, soresolveProjectedPaneis never called for it, sotakeReturningPanenever fires and the held pane stays inreturning— the probe reports it still held;body.items.length !== plan.desiredItems.lengththrowsDock projection tab chrome "…" has an inexact item set;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:
windowId. Arm A returns a pane from a dead foreign-window vessel — unmounted,parentIdthe dead viewport,windowIdthe 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.reconcileTabChromehas moved the pane/button pairs and before the bar's ids are rewritten — the window every real failure lands in.settleFailedProjectionrecovers 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.Open … as window/Show … here. A recovery from that state rebuilds the pane (resolvePaneseesisDestroyedand callscreatePane), so the same-live-instance contract cannot hold on this sub-route. Promoted to #18632, repaired there.generation: 1) that never releases and never expires. Mechanism, measured 2026-09-13 on a rendered vessel (the control arm of #18646): a window'sdisconnectrides onmain/DomEvents.mjs#onBeforeUnload→worker/Base#fire('disconnect')→Transaction#onWindowDisconnect→release. Playwright'spage.close()tears the target down without dispatchingbeforeunload, 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 underpage.close({runBeforeUnload: true})stayed bound for 32 s and is not explained by this mechanism alone (that option does dispatchbeforeunload); it stays a single-run sighting.Withdrawn — the gesture-less reclaim (tear out, Reset, Undo). Measured at
fdca66095bwith the root resolver recorded: the document moves at +32 ms, the popup empties at +51 ms, the root'sresolveProjectedPaneruns once through the fall-through with the cached instance at +333–360 ms, and at +360 ms the pane'swindowIdis 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#reconcileTabChromepairs live panes to item ids positionally, frombar.sortZoneConfig.dockItemIds×body.items[index], and that map takes precedence over the host'sresolveItem. It is the only reader that can answer "which live component is this item" without consulting identity.dockItemId:projection.LayoutAdapterstamps it on the pane config, its header and its placeholder, andWorkspace#resolveLivePanealready depends on exactly that stamp (down({dockItemId})), with a documented category exclusion for buttons, rail tabs and placeholders.Neo.dashboard.dock.Workspace#resolveProjectedPanetakes a returning pane fromtearOutHandlers.takeReturningPane(itemId)and, when it gets one, doesreturning.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.returningmap 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#addsets{appName, parentId, parentIndex, windowId}on a moved item and, across a window boundary, forcesmounted = falsesoafterSetMountedre-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 = 20000bounds 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
takeReturningPane, destination insert). Receipts linked above.dockItemIdrather 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 reachesresolveItem,takeReturningPanefires, 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.page.close()); no engine change.Show … herere-tree withoutcreatePane, 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
projectionFailedtwice with the inexact-item-set message,resolveProjectedPanenever called for the drifted item, its held pane still inreturning— and green on the repair, with the red on the assertion that encodes it; the cockpit's Witness A arm (1 of 11 at7e8b32e421) no longer strands.NativeLifecycle#onLeaseExpired→expired→onBindingReleased→reintegrateItem→settlePaneand promoted to #18632; sighting 2 traced to Playwright'spage.close()skippingbeforeunload, the carrier of the worker'sdisconnect— a rig artifact with its mechanism, closed as a sighting.createPane) — split out as #18646, delivered by PR #18647.Out of Scope
WorkspaceSetmembership signal and the topology bar's warm re-sync (#18622).src/dashboard/dock/Workspace.mjs(#18304) and the size ceiling ofapps/workstation/view/Workspace.mjs(#18456).Avoided Traps
Container.Base#addsetswindowIdand forcesmounted = falseacross a window boundary, so a single projection carries the component home.windowIdcolumn said popup at that step; the root container was inferred, never measured.reparentDockPanearrives naked throughapp.mainView.add(pane); assert component attachment and embodiment, not chrome.projectionFailedrows, nothing lost behind the tail. #18642 was real and is fixed on its own ticket (PR #18644); it is not this strand.parentIdthe dead viewport, re-trees cleanly with and without a refresh in flight — resolver asked once, no rebuild, no failure. ThewindowIdplays no part; the pairing does. Written from Clio's receipt before the arm that would refute it was run.settleFailedProjectionholds.beforeunload; Playwright'spage.close()never dispatches it.window.close()from inside the page releases the slot within one poll interval.Related
#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_memorieson "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.