Context
Attribution correction (2026-08-07, review cycle 1): the filmed chip is NOT this defect's product. An instrumented film run at the fixed head shows every proxy removal dispatching AND settling (transport-confirmed), while a DOM identity probe names the on-camera survivor as neo-dock-stack-handle-metrics — the toolbar's STATIC stack grip (16x20 layout box carrying the large pseudo-element visual), chrome that legitimately persists until the vessel window closes. The filmed symptom's remedy is the #16626 departing overlay (frame receipt on PR #16627 shows it buried). THIS ticket stands on the engine defect below, which is real, in-tree, and unit-red-proven independent of any film frame.
Original context
Split-leaf of #16499's S-3a (full probe evidence in its r4 comment). Filmed symptom: after a cross-window stack-return commits, the grouped-drag proxy chip persists at full opacity on the emptied source vessel for ~1s until the OS window closes — 2/2 runs at film pace. An instrumented run proved the source-side dragEnd fires and completes within ~2ms of the drop, falsifying "end never fires" and "end fires late": the teardown call runs; its DOM effect never lands.
The Problem
destroyDragProxy() (src/draggable/DragZone.mjs:329-339) splits the teardown: the worker-side component is destroyed synchronously, but the main-thread DOM removal is deferred —
me.timeout(me.moveInMainThread ? 0 : 30).then(() => {
Neo.applyDeltas(me.windowId, [{action: 'removeNode', id}])
});core.Base#timeout() registers in the instance's async-reject set, and core.Base#destroy() clears the timer and rejects the promise (Neo.isDestroyed). When the zone (or its owner toolbar chain) is destroyed inside that 0–30ms window, the .then never runs and the removeNode delta is never dispatched — the proxy's DOM node is orphaned in the source window with no owner left to remove it.
The stack-return hits this deterministically: the probe timeline shows documents-adopted (which un-projects the source vessel's chrome, destroying the toolbar + its sort zone) landing in the SAME millisecond as dragEnd-done — the zone dies well inside the 30ms deferral every time.
The Architectural Reality
- The deferral exists to let custom animations settle ("Override for using custom animations"); the cancellation is an accident of riding the instance-scoped timeout, not a design intent — a cleanup delta is exactly the kind of dispatch that must survive its dispatcher.
- Every
DragZone consumer in every closing-window scenario inherits the class: dock vessels, dialog drags in popups, any drag whose zone is torn down within the deferral window.
- The unhandled rejection is invisible by design (
Neo.isDestroyed rejections are globally suppressed) — nothing fails loudly, matching the field evidence.
The Fix
Guarantee the dispatch outlives the zone: keep the deferred path, and dispatch on the rejection path too —
me.timeout(me.moveInMainThread ? 0 : 30)
.catch(() => null)
.then(() => Neo.applyDeltas(windowId, [{action: 'removeNode', id}]))
.catch(reason => {
reason !== undefined && console.error('DragZone: proxy removal delta failed', {id, reason, windowId})
});(binding windowId/id locally so the dispatch does not read the destroyed instance). A window that closed in the interim makes applyDeltas reject with bare undefined; the detached cleanup chain owns that expected terminal outcome silently, while a reasoned failure remains visible.
Acceptance Criteria
Out of Scope
- The closing vessel's terminal presentation (#16499 S-3b — app-side departing state, separate leaf).
- Phase ordering of the transfer (scene-4-pinned contract).
Related
#16499 (parent — S-3a probe r4 with the timeline), #16621 / PR #16622 (sibling leaf: the fast-path fix that already compresses the corpse window this defect lives in).
Origin Session ID: 84d669f4-2271-4d6a-8878-45e8754be6b3
Retrieval Hint: query_raw_memories("drag proxy removeNode timeout rejected destroy orphan dying window")
Context
Attribution correction (2026-08-07, review cycle 1): the filmed chip is NOT this defect's product. An instrumented film run at the fixed head shows every proxy removal dispatching AND settling (transport-confirmed), while a DOM identity probe names the on-camera survivor as
neo-dock-stack-handle-metrics— the toolbar's STATIC stack grip (16x20 layout box carrying the large pseudo-element visual), chrome that legitimately persists until the vessel window closes. The filmed symptom's remedy is the #16626 departing overlay (frame receipt on PR #16627 shows it buried). THIS ticket stands on the engine defect below, which is real, in-tree, and unit-red-proven independent of any film frame.Original context
Split-leaf of #16499's S-3a (full probe evidence in its r4 comment). Filmed symptom: after a cross-window stack-return commits, the grouped-drag proxy chip persists at full opacity on the emptied source vessel for ~1s until the OS window closes — 2/2 runs at film pace. An instrumented run proved the source-side
dragEndfires and completes within ~2ms of the drop, falsifying "end never fires" and "end fires late": the teardown call runs; its DOM effect never lands.The Problem
destroyDragProxy()(src/draggable/DragZone.mjs:329-339) splits the teardown: the worker-side component is destroyed synchronously, but the main-thread DOM removal is deferred —me.timeout(me.moveInMainThread ? 0 : 30).then(() => { Neo.applyDeltas(me.windowId, [{action: 'removeNode', id}]) });core.Base#timeout()registers in the instance's async-reject set, andcore.Base#destroy()clears the timer and rejects the promise (Neo.isDestroyed). When the zone (or its owner toolbar chain) is destroyed inside that 0–30ms window, the.thennever runs and theremoveNodedelta is never dispatched — the proxy's DOM node is orphaned in the source window with no owner left to remove it.The stack-return hits this deterministically: the probe timeline shows
documents-adopted(which un-projects the source vessel's chrome, destroying the toolbar + its sort zone) landing in the SAME millisecond asdragEnd-done— the zone dies well inside the 30ms deferral every time.The Architectural Reality
DragZoneconsumer in every closing-window scenario inherits the class: dock vessels, dialog drags in popups, any drag whose zone is torn down within the deferral window.Neo.isDestroyedrejections are globally suppressed) — nothing fails loudly, matching the field evidence.The Fix
Guarantee the dispatch outlives the zone: keep the deferred path, and dispatch on the rejection path too —
me.timeout(me.moveInMainThread ? 0 : 30) .catch(() => null) .then(() => Neo.applyDeltas(windowId, [{action: 'removeNode', id}])) .catch(reason => { reason !== undefined && console.error('DragZone: proxy removal delta failed', {id, reason, windowId}) });(binding
windowId/idlocally so the dispatch does not read the destroyed instance). A window that closed in the interim makesapplyDeltasreject with bareundefined; the detached cleanup chain owns that expected terminal outcome silently, while a reasoned failure remains visible.Acceptance Criteria
dragEnd()followed bydestroy()within the deferral window still dispatches exactly oneremoveNodedelta for the proxy id (spy on the delta surface; deterministic, no wall-clock races).The filmed receiptRETIRED by the attribution correction above: the filmed chip is the static stack grip, not this proxy — its presentation remedy is #16626 (receipt already on PR #16627). This ticket's evidence is complete at the unit layer (the race and both terminal outcomes are fully reproducible in-process).Out of Scope
Related
#16499 (parent — S-3a probe r4 with the timeline), #16621 / PR #16622 (sibling leaf: the fast-path fix that already compresses the corpse window this defect lives in).
Origin Session ID: 84d669f4-2271-4d6a-8878-45e8754be6b3
Retrieval Hint:
query_raw_memories("drag proxy removeNode timeout rejected destroy orphan dying window")