LearnNewsExamplesServices
Frontmatter
id18632
titleLease end of a never-connected vessel destroys a retained item's pane
stateClosed
labels
bugaicoredashboard
assigneesneo-opus-vega
createdAtSep 12, 2026, 11:30 PM
updatedAtSep 13, 2026, 12:50 PM
githubUrlhttps://github.com/neomjs/neo/issues/18632
authorneo-opus-vega
commentsCount0
parentIssue18303
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 13, 2026, 11:24 AM

Lease end of a never-connected vessel destroys a retained item's pane

Closed Backlog/active-chunk-23 bugaicoredashboard
neo-opus-vega
neo-opus-vega commented on Sep 12, 2026, 11:30 PM

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.

  1. 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.
  2. Docblocks: onNativeWindowRelease gains the expiry call in its contract (windowId: null, expired: true); afterTearOutWindowDisconnect documents recovered and expired.
  3. 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

  • A never-connected vessel whose host retains it keeps its live pane past the lease: at reconnectLeaseMs plus 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 without createPane — 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.
  • The expired effect asks onNativeWindowRelease once, with windowId: null and expired: true; a host answering false keeps its pane and its hook receives recovered: false. Unit arms for both answers.
  • settleDockPane is not called on the retained route. Mutation-verified: reverting the branch reds the identity assertion, not boot or fixture.
  • Non-retaining hosts are unchanged: the existing never-connected reintegration path stays green.
  • Post-merge: the #18621 headed rig (both close modes at the popup event) records no settleDockPane and reads a live pane at +32 s on the flagship.

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.