Leaf of #18303 (parent linked natively). Promoted from #18621 item 3 — the never-rendered-vessel sighting, now a claim with its receipt. Decision Record impact: aligned-with ADR 0029.
Corrected 2026-09-12 21:45Z, before any code: the first body said the dead window's binding was released and the host's retention answer "computed and discarded". A third run with a recorder on the host's onNativeWindowRelease showed it never ran — a vessel that never binds has no binding to release, and its lease runs from the reservation. The claim and the destroyer's stack are unchanged; the repair moved from "remember the answer" to "ask it once".
Context
A tear-out vessel window can die before it ever connects: a popup blocked or closed within the first ~200 ms, before its app registers. The Workstation keeps such a vessel's workspace headless — the item stays in the vessel's own document, the participant stays registered (committed: true), and the topology bar offers Open … as window / Show … here (#18596, #18622). Twenty seconds after the tear-out the reconnect lease of the vessel's reservation runs out and the engine destroys the live pane, while every other owner still believes the item exists.
Measured at dev@7e8b32e421 (headed Chromium, Neural Link reads only, the destroyer's own call stack recorded): three arms — the popup closed at the popup event with page.close() (twice) and with page.close({runBeforeUnload: true}) — identical result. Receipt: the #18621 isolation comments and the rig in that ticket's session scratchpad (IsolationLeaseEnd18621.scratch.spec.mjs).
The Problem
| when |
fact |
| 0 |
the tear-out reserves the vessel's slot; the 20 s reconnect lease starts at the reservation (Transaction#reserve → startLease, Transaction.mjs:854) |
| +111…151 ms |
the popup is closed before its app binds: the slot still reads {generation: 0, windowId: null}, findByWindow has nothing for it, no release fires, and the Workstation's onNativeWindowRelease is never called (recorder silent for 32 s) |
| ≈ 20.1 s after the tear-out |
settleDockPane(neo-grid-container-2) — the live pane is destroyed |
| same tick |
onDockPaneReturn({phase: 'after', returned: false}), then afterTearOutWindowDisconnect({itemId: 'feed', recovered: true, committed: true, expired: true}); the vessel document still lists feed; getPopupStates() still reports the vessel committed: true with its host alive |
The destroy's stack, identical in all three arms:
settleDockPane src/dashboard/dock/Workspace.mjs:598 (the settlePane seam)
Object.reintegrateItem src/dashboard/dock/window/TearOut.mjs:499
Object.onBindingReleased src/dashboard/dock/window/TearOut.mjs:608
Object.expired src/dashboard/dock/Workspace.mjs:789
NativeLifecycle.onLeaseExpired src/manager/transaction/NativeLifecycle.mjs:340
reintegrateItem reads the ROOT's committed document and finds no feed in it — the Workstation moved the item into workstation-vessel:feed's document at the tear-out terminal — so it takes the no-home branch and settles the pane it was handed (TearOut.mjs:496–500). That pane is the one capturePane stored at the terminal (dock/Workspace.mjs:976, TearOut.mjs:249–256) and nothing released, because a vessel that never connects never adopts.
The control — a vessel closed after it rendered, same rig, same held handle (heldPaneIds() = ['feed'] before the close in every arm): the binding releases at +29 ms, onNativeWindowRelease answers false, the pane parks in the dead window's container, and nothing destroys it — no settleDockPane, no afterTearOutWindowDisconnect — for the full 32 s. Same handle, different route.
The Architectural Reality
Neo.manager.transaction.NativeLifecycle#onRelease (:302–325) asks the source unbind(data) first and, when the answer is false (retained), skips the released effect: "Unbinds projection; false retains semantic ownership" / "Handles a released resource which was not retained" (:62–63). The dock Workspace supplies unbind from its onNativeWindowRelease hook (src/dashboard/dock/Workspace.mjs:783, :805–809); the Workstation answers false whenever the vessel's headless host exists (apps/workstation/view/Workspace.mjs:2578–2596).
- A never-bound reservation has no binding, so
Transaction#release finds nothing and onRelease never runs for it. #onLeaseExpired (:333–341) matches the never-connected admission (!entry.connected), calls retire() and fires the expired effect — the one route on which the host is never asked whether it retains the vessel. The route was written for "acknowledged cleanup of a provisional resource" (:64); a vessel whose document was committed and whose participant is registered is not provisional to its host.
- The dock Workspace's
expired effect (:788–790) hands the context to TearOut#onBindingReleased with committed: !!entry; for a committed owner that means releasePane + reintegrateItem (TearOut.mjs:603–612) — the return-home flow of a single-document host, which on a multi-document host destroys the pane.
Workstation.view.Workspace#afterTearOutWindowDisconnect({itemId, recovered=true}) (:2604–2614) documents recovered: false — "when the vessel's workspace stays registered headless" — and nothing in the engine writes it: one reader, no writer, since #18309.
- Recovery from the destroyed state rebuilds the pane:
resolvePane sees isDestroyed and creates again (:1801–1808), so the vessel lifecycle's same-live-instance contract fails silently on this route.
The Fix
Ask the host the question the release route asks, on the one route that never did — and give the documented recovered: false its writer.
src/dashboard/dock/Workspace.mjs, the expired effect (:788–790): before handing the context to onBindingReleased, call me.onNativeWindowRelease({groupId, workspaceKey, windowId: null, expired: true}) — the same hook unbind routes to, with the slot the lease freed and no window, since none ever bound. On false (retained): skip onBindingReleased — no releasePane, no reintegrateItem, no settle — and call afterTearOutWindowDisconnect({...context, committed: !!entry, expired: true, recovered: false, pane: null}), so the host runs its park-geometry and park-handler cleanup without retiring the retained target. Otherwise unchanged.
- Docblocks:
onNativeWindowRelease gains the expiry call in its contract (windowId: null, expired: true); afterTearOutWindowDisconnect documents recovered and expired.
- No change to
NativeLifecycle, TearOut.mjs or the Workstation, whose hook has read recovered since #18309.
Alternative weighed: a new lifecycle effect, or a retained flag computed by NativeLifecycle and carried in the expired payload. Both move the host's retention question into the manager, which owns no projection and would have to ask the source anyway; the host already answers it for the release route, and asking there keeps one answerer.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
Docs |
Evidence |
Neo.dashboard.dock.Workspace#onNativeWindowRelease(data) (:805–809) |
this leaf; #18314 (the release route that already calls it) |
also called by the host's own expired effect, once, with {groupId, workspaceKey, windowId: null, expired: true} when a never-bound reservation's lease ends; false retains |
hosts that do not override it answer true and take the existing reintegration path (the dock example, Demo B — neither overrides it) |
JSDoc |
test/playwright/unit/dashboard/DockWorkspace.spec.mjs: the expiry call arrives once with windowId: null |
the expired effect (:788–790) |
this leaf |
retained → no onBindingReleased; afterTearOutWindowDisconnect({…, recovered: false, expired: true, pane: null}); otherwise unchanged |
as above |
JSDoc on bindNativeWindowSource and afterTearOutWindowDisconnect |
DockWorkspace.spec.mjs: a retaining host keeps its held pane alive past the lease and its hook sees recovered: false; the non-retaining control returns the item as today |
Workstation.view.Workspace#afterTearOutWindowDisconnect({recovered}) (:2604–2614) |
#18309 — the documented contract |
unchanged; its recovered: false branch gains its writer |
— |
none |
the Workspace arm above |
Acceptance Criteria
Out of Scope
- The
released route (connected vessels) — already retention-aware.
- A window that dies without any disconnect being delivered (#18621 sighting 2, one run in four, a Playwright
page.close() artifact as far as measured).
- Closing vessels when their root leaves (D#18445) and the
headless-dirty interlock.
- Whether a never-adopted pane should stay in
paneHandles after a committed terminal — capturePane's handle-first design is the engine's, and the control shows the held handle is harmless on the release route.
Avoided Traps
| Rejected |
Why |
Fix it in the Workstation's afterTearOutWindowDisconnect |
The pane is already destroyed when the hook runs (stack order: settle → onDockPaneReturn → the hook). |
Teach reintegrateItem about other documents |
It is the single-document return flow by contract; the host's answer exists one layer up. |
| Remember the release route's answer for the expiry route |
Nothing to remember: a never-bound reservation never releases, so onRelease never runs for it — measured with a recorder on the hook, silent for 32 s. |
| Read the destroy as "the vessel was never really opened" |
getPopupStates() reports committed: true and the vessel document lists the item for the full 32 s; with #18622 the topology bar offers recovery for it — recovery that would rebuild the pane. |
Related
#18621 (item 3 promoted here; Witness A stays there) · #18596 (headless retention) · #18622 / PR #18623 (the warm recovery affordance) · #18309 (the recovered contract) · #18314 (NativeLifecycle and the expired route) · #18311 (TearOut.mjs) · #15648 (closed: a cockpit vessel whose boot exceeded the connect window — the older shape of the same trigger) · D#18445 (a vessel outlives its root).
Live latest-open sweep: checked the latest 20 open issues at 2026-09-12T21:23Z; no equivalent found. A2A in-flight claim sweep: 30 most recent messages, all read-states, ~90 min window — claims on #18610 / #18613 / #18037 / Institution #133, none on this route. Memory Core rationale sweep on the symptom returned the 2026-09-07 exploration locating the !entry.connected guard as the provisional-vessel reaper (design context, no decision against this fix) and D#18445's hung-close analysis. Own-assignment sweep: #18621 (source), #18532, #18460, #18622 — none covers it. Epic-layer sweep: n/a (leaf).
Origin Session ID: 6f95d318-61df-4f43-b77c-d20d20224299
Retrieval Hint: query_raw_memories on "never-connected vessel lease expiry settlePane reintegrateItem retained recovered false". Source anchors: NativeLifecycle#onLeaseExpired, dock/Workspace.mjs bindNativeWindowSource expired, TearOut.mjs reintegrateItem.
Leaf of #18303 (parent linked natively). Promoted from #18621 item 3 — the never-rendered-vessel sighting, now a claim with its receipt.
Decision Record impact: aligned-with ADR 0029.Context
A tear-out vessel window can die before it ever connects: a popup blocked or closed within the first ~200 ms, before its app registers. The Workstation keeps such a vessel's workspace headless — the item stays in the vessel's own document, the participant stays registered (
committed: true), and the topology bar offersOpen … as window/Show … here(#18596, #18622). Twenty seconds after the tear-out the reconnect lease of the vessel's reservation runs out and the engine destroys the live pane, while every other owner still believes the item exists.Measured at
dev@7e8b32e421(headed Chromium, Neural Link reads only, the destroyer's own call stack recorded): three arms — the popup closed at thepopupevent withpage.close()(twice) and withpage.close({runBeforeUnload: true})— identical result. Receipt: the #18621 isolation comments and the rig in that ticket's session scratchpad (IsolationLeaseEnd18621.scratch.spec.mjs).The Problem
Transaction#reserve→startLease,Transaction.mjs:854){generation: 0, windowId: null},findByWindowhas nothing for it, noreleasefires, and the Workstation'sonNativeWindowReleaseis never called (recorder silent for 32 s)settleDockPane(neo-grid-container-2)— the live pane is destroyedonDockPaneReturn({phase: 'after', returned: false}), thenafterTearOutWindowDisconnect({itemId: 'feed', recovered: true, committed: true, expired: true}); the vessel document still listsfeed;getPopupStates()still reports the vesselcommitted: truewith its host aliveThe destroy's stack, identical in all three arms:
reintegrateItemreads the ROOT's committed document and finds nofeedin it — the Workstation moved the item intoworkstation-vessel:feed's document at the tear-out terminal — so it takes the no-home branch and settles the pane it was handed (TearOut.mjs:496–500). That pane is the onecapturePanestored at the terminal (dock/Workspace.mjs:976,TearOut.mjs:249–256) and nothing released, because a vessel that never connects never adopts.The control — a vessel closed after it rendered, same rig, same held handle (
heldPaneIds()=['feed']before the close in every arm): the binding releases at +29 ms,onNativeWindowReleaseanswersfalse, the pane parks in the dead window's container, and nothing destroys it — nosettleDockPane, noafterTearOutWindowDisconnect— for the full 32 s. Same handle, different route.The Architectural Reality
Neo.manager.transaction.NativeLifecycle#onRelease(:302–325) asks the sourceunbind(data)first and, when the answer isfalse(retained), skips thereleasedeffect: "Unbinds projection; false retains semantic ownership" / "Handles a released resource which was not retained" (:62–63). The dock Workspace suppliesunbindfrom itsonNativeWindowReleasehook (src/dashboard/dock/Workspace.mjs:783,:805–809); the Workstation answersfalsewhenever the vessel's headless host exists (apps/workstation/view/Workspace.mjs:2578–2596).Transaction#releasefinds nothing andonReleasenever runs for it.#onLeaseExpired(:333–341) matches the never-connected admission (!entry.connected), callsretire()and fires theexpiredeffect — the one route on which the host is never asked whether it retains the vessel. The route was written for "acknowledged cleanup of a provisional resource" (:64); a vessel whose document was committed and whose participant is registered is not provisional to its host.expiredeffect (:788–790) hands the context toTearOut#onBindingReleasedwithcommitted: !!entry; for a committed owner that meansreleasePane+reintegrateItem(TearOut.mjs:603–612) — the return-home flow of a single-document host, which on a multi-document host destroys the pane.Workstation.view.Workspace#afterTearOutWindowDisconnect({itemId, recovered=true})(:2604–2614) documentsrecovered: false— "when the vessel's workspace stays registered headless" — and nothing in the engine writes it: one reader, no writer, since #18309.resolvePaneseesisDestroyedand creates again (:1801–1808), so the vessel lifecycle's same-live-instance contract fails silently on this route.The Fix
Ask the host the question the release route asks, on the one route that never did — and give the documented
recovered: falseits writer.src/dashboard/dock/Workspace.mjs, theexpiredeffect (:788–790): before handing the context toonBindingReleased, callme.onNativeWindowRelease({groupId, workspaceKey, windowId: null, expired: true})— the same hookunbindroutes to, with the slot the lease freed and no window, since none ever bound. Onfalse(retained): skiponBindingReleased— noreleasePane, noreintegrateItem, no settle — and callafterTearOutWindowDisconnect({...context, committed: !!entry, expired: true, recovered: false, pane: null}), so the host runs its park-geometry and park-handler cleanup without retiring the retained target. Otherwise unchanged.onNativeWindowReleasegains the expiry call in its contract (windowId: null,expired: true);afterTearOutWindowDisconnectdocumentsrecoveredandexpired.NativeLifecycle,TearOut.mjsor the Workstation, whose hook has readrecoveredsince #18309.Alternative weighed: a new lifecycle effect, or a
retainedflag computed byNativeLifecycleand carried in theexpiredpayload. Both move the host's retention question into the manager, which owns no projection and would have to ask the source anyway; the host already answers it for the release route, and asking there keeps one answerer.Contract Ledger Matrix
Neo.dashboard.dock.Workspace#onNativeWindowRelease(data)(:805–809)expiredeffect, once, with{groupId, workspaceKey, windowId: null, expired: true}when a never-bound reservation's lease ends;falseretainstrueand take the existing reintegration path (the dock example, Demo B — neither overrides it)test/playwright/unit/dashboard/DockWorkspace.spec.mjs: the expiry call arrives once withwindowId: nullexpiredeffect (:788–790)onBindingReleased;afterTearOutWindowDisconnect({…, recovered: false, expired: true, pane: null}); otherwise unchangedbindNativeWindowSourceandafterTearOutWindowDisconnectDockWorkspace.spec.mjs: a retaining host keeps its held pane alive past the lease and its hook seesrecovered: false; the non-retaining control returns the item as todayWorkstation.view.Workspace#afterTearOutWindowDisconnect({recovered})(:2604–2614)recovered: falsebranch gains its writerAcceptance Criteria
reconnectLeaseMsplus margin the same component instance is alive and unmounted — asserted on component identity (unit tier, PR #18635). Its second clause — a later re-tree resolving that instance from the cache withoutcreatePane— is a real-window effect on the flagship, deferred to #18621 item 4 (its headed rig; owner @neo-opus-vega) and listed as the PR's residual.expiredeffect asksonNativeWindowReleaseonce, withwindowId: nullandexpired: true; a host answeringfalsekeeps its pane and its hook receivesrecovered: false. Unit arms for both answers.settleDockPaneis not called on the retained route. Mutation-verified: reverting the branch reds the identity assertion, not boot or fixture.popupevent) records nosettleDockPaneand reads a live pane at +32 s on the flagship.Out of Scope
releasedroute (connected vessels) — already retention-aware.page.close()artifact as far as measured).headless-dirtyinterlock.paneHandlesafter a committed terminal —capturePane's handle-first design is the engine's, and the control shows the held handle is harmless on the release route.Avoided Traps
afterTearOutWindowDisconnectonDockPaneReturn→ the hook).reintegrateItemabout other documentsonReleasenever runs for it — measured with a recorder on the hook, silent for 32 s.getPopupStates()reportscommitted: trueand the vessel document lists the item for the full 32 s; with #18622 the topology bar offers recovery for it — recovery that would rebuild the pane.Related
#18621 (item 3 promoted here; Witness A stays there) · #18596 (headless retention) · #18622 / PR #18623 (the warm recovery affordance) · #18309 (the
recoveredcontract) · #18314 (NativeLifecycleand theexpiredroute) · #18311 (TearOut.mjs) · #15648 (closed: a cockpit vessel whose boot exceeded the connect window — the older shape of the same trigger) · D#18445 (a vessel outlives its root).Live latest-open sweep: checked the latest 20 open issues at 2026-09-12T21:23Z; no equivalent found. A2A in-flight claim sweep: 30 most recent messages, all read-states, ~90 min window — claims on #18610 / #18613 / #18037 / Institution #133, none on this route. Memory Core rationale sweep on the symptom returned the 2026-09-07 exploration locating the
!entry.connectedguard as the provisional-vessel reaper (design context, no decision against this fix) and D#18445's hung-close analysis. Own-assignment sweep: #18621 (source), #18532, #18460, #18622 — none covers it. Epic-layer sweep: n/a (leaf).Origin Session ID: 6f95d318-61df-4f43-b77c-d20d20224299
Retrieval Hint:
query_raw_memorieson "never-connected vessel lease expiry settlePane reintegrateItem retained recovered false". Source anchors:NativeLifecycle#onLeaseExpired,dock/Workspace.mjs bindNativeWindowSource expired,TearOut.mjs reintegrateItem.