Context
Operator hand witness (2026-09-26, v13.2 engine-release blocker): dragging a native popup over another popup shows drop zones; dragging a TAB out — the tab converts to a popup drag proxy, the vessel — and over another popup shows none, so the proxy never switches back to the tab header inside the target. Expected: the proxy switches back to the tab header and the target popup's drop zones show, wherever over the target the vessel is dragged — the native path's behavior.
Measured on real windows (headed, no viewport emulation; main 1320×920, target popup 700×500 at 4300,300 with inner 700×433, torn-out vessel 320×240, grab offset 31,16; 36 entries from four approach directions on a 3×3 grid of the target's content + two traverses; receipts test-results/dock-wow-demo/conversion-region/2026-09-26/*-region.json): with the pointer inside the target's content and a live claim every time (claimCount 1, winner = the target), every entry at fy ≤ 0.5 converts and every entry at fy = 0.85 does not, from all four directions; the top-to-bottom traverse reverts at fy ≈ 0.85. The offline model overlap / min(extent) per axis on the vessel's frame vs the target's inner rect reproduces every row the geometry publisher's lag does not blur (ry 0.337 at fy 0.85, below both thresholds). For the operator's pane-sized popups (vessel ≥ target) the same formula admits only fx ≲ 0.49, fy ≲ 0.49 — the upper-left quadrant of the target.
The Problem
One gesture, two admissions. The native title-bar path admits a target by a POINT — the moving popup's outer top-left corner plus inset inside the target's accepting region (DragCoordinator#getNativeWindowDropCandidate) — so drop zones show wherever that anchor is. The torn-out path admits by the pointer claim AND the sensor's overlap metric: VesselConversion#resolveConversion enters only at pointerInTarget && composed >= 0.55 with composed = min(rx, ry) and reverts below 0.35, and TabSortZone#resolveRemoteDragTransition nulls the target — no preview, no proxy, no drop zones — until the sensor converts (engage: record.converted). The threshold declares itself provisional: "Production opt-in remains off until the physical park/re-show lifecycle is ready and headed calibration replaces this placeholder" (src/dashboard/dock/interaction/TabSortZone.mjs:130-135). The Workstation opted in (#15396), the calibration never ran, and viewport emulation hid it: an emulated popup has no chrome and the film's vessels are small against the main window, so #16497 exonerated the threshold for that beat. Under real chrome with a hand that aims anywhere, the gate is the symptom.
Design authority
- ADR 0029 §2.8.6: "A source may enter
HOVERING_CLAIM only after its physical park effect returns strict true; leaving the claim may resume DETACHED_MOVING only after strict re-show admission." — entry and exit are bound to the CLAIM. §2.8.1: "no claim — fail closed: no preview, no commit."
- The one record sentence naming the metric (§2.8.6,
learn/agentos/decisions/0029-docking-design.md:618): "The §2.8.4 min-axis metric stays size-neutral; target-cover changes only the reversible physical embodiment needed after that metric proposes conversion." — it acknowledges the metric as the proposer but defines neither the metric, its plane, nor its threshold (§2.8.4 defines no metric; Eos's V-B-A on #19225).
- The operator's expected behavior (2026-09-26, quoted above) is the product intent. This ticket amends the record sentence: the pointer claim proposes conversion; the size-neutral metric is retired. Decision Record impact: amends ADR 0029 §2.8.6.
The Architectural Reality
src/dashboard/dock/window/VesselConversion.mjs — the sensor: sample() measures and resolveConversion() decides; convertThreshold / revertThreshold / composeRatios / axisRatio / clampRatio; isMeasurableRect (fail-closed geometry); the pointerExitedTarget tri-state (#16497).
src/dashboard/dock/interaction/TabSortZone.mjs:119-147 opt-in + threshold configs; :461 sensor factory; :645-770 resolveRemoteDragTransition (engage / commitEligible = record.converted).
src/dashboard/dock/projection/LayoutAdapter.mjs:490-494, :581-583, :1337-1341 thread the thresholds; no app or example sets them.
src/manager/DragCoordinator.mjs:1017-1100 feeds pointerInTarget: Boolean(claimed?.zone) plus the target's inner rect; :630 is the native anchor admission.
apps/workstation/view/VesselWorkspace.mjs:1472 resolveVesselConversionSourceRect — after #19232 the frame; it stays the park's pre-conversion rect and the measurability input.
The Fix
VesselConversion: entry = a live pointer claim on measurable rects; hold = no observed or derived exit (tri-state unchanged); a converted sensor still reverts on unmeasurable geometry (fail-closed). Retire convertThreshold, revertThreshold, composeRatios, axisRatio, clampRatio, the band validation and the rx / ry / composed record fields; the record carries measurable. Class docs rewritten to the claim-owned contract.
TabSortZone: retire vesselConversionConvertThreshold / vesselConversionRevertThreshold and the factory args; LayoutAdapter: retire their threading (keep vesselConversionPointerExitGraceMs).
- ADR 0029 §2.8.6: amend the sentence at
:618 (the claim proposes; the metric retired; the measured reason, dated).
VesselWorkspace#resolveVesselConversionSourceRect JSDoc: the rect's remaining role (stacked on #19232).
- Tests:
DockVesselConversion.spec.mjs re-pinned to the claim-owned contract (red-first); DockTabSortZone.spec.mjs fixture + the async-park replay arm re-aimed at claim loss; e2e DemoBVesselConversionNL and five-beat scene 3 stay green.
Contract Ledger
| Target surface |
Source of authority |
Proposed behavior |
Fallback |
Docs |
Evidence |
VesselConversion configs convertThreshold, revertThreshold; seams composeRatios, axisRatio, clampRatio |
ADR 0029 §2.8.6 (amended here) |
retired — no geometric gate on entry or hold |
none: the dock subsystem is the v13.2 greenfield cut (ADR 0029 §2.9) and no app sets them |
class JSDoc |
AC-2 |
VesselConversion#sample() record |
same |
{converted, measurable, pointerInTarget, sourceRect, targetRect} (+ transitioning) |
— |
class JSDoc |
AC-1 |
TabSortZone configs vesselConversionConvertThreshold, vesselConversionRevertThreshold; LayoutAdapter option threading |
same |
retired; vesselConversionPointerExitGraceMs stays |
none (unset everywhere) |
member JSDoc |
AC-2 |
ADR 0029 §2.8.6 sentence at :618 |
the record |
the pointer claim proposes conversion |
— |
the amendment |
AC-5 |
Acceptance Criteria
- AC-1 (unit, red-first):
VesselConversion converts on the first sample with pointerInTarget: true and any measurable overlap (composed 0.2 in today's terms — red on dev) and holds through overlap changes while the claim stands; it reverts on an observed exit, on claim loss without exit evidence, and on unmeasurable rects.
- AC-2 (unit): no threshold config survives on
VesselConversion, TabSortZone, or the adapter's option threading; validateConfig still fails loud on missing or non-function seams.
- AC-3 (headed, one run, real windows): the region probe at the fix head — every entry point of the 4×3×3 grid converts with
engaged: true and the target's preview present after the next move; both traverses hold to the far edge (fx / fy 0.95). Receipts under test-results/dock-wow-demo/conversion-region/.
- AC-4 (e2e):
WorkstationFiveBeatNL scene 3 and Workspace.spec.mjs green in CI; DemoBVesselConversionNL.spec.mjs re-aimed to the claim-owned crossing (a standing claim never flips; claim loss re-shows) — a headed-only witness off CI (headless its tear-out popup never opens, before any changed line), so its run is a ## Post-Merge Validation item for a seat with a display.
- AC-5 (docs): ADR 0029 §2.8.6 carries the amendment; no JSDoc names the min-axis metric as the proposer.
Out of Scope
- The native title-bar path's dwell / settle admission (works as witnessed).
- The geometry publisher's lag (a frame sampled behind the pointer): a separate observation; with claim-owned entry it no longer decides admission.
- #19232 (the source plane) merges first; this leaf stacks on it.
Avoided Traps
- Re-tuning 0.55 / 0.35: one placeholder for another; the pair never met real chrome (
#16497, Eos on #19225).
- Measuring the frame against the target's OUTER rect: the title bar inflates the overlap and the far edges stay dead.
- Keeping the thresholds as dead config: accretion.
Related
#19225 / PR #19232 (source plane), #16497 (claim-TTL flicker; threshold exonerated under emulation only), #15396 (conversion + park), #15432 / #15434 (sensor contract tier), ADR 0029 §2.8.1 / §2.8.6.
Live latest-open sweep: checked the latest 20 open issues at 2026-09-26T07:34:57Z — no equivalent (#19225 is the source-plane sibling). A2A sweep (last 30, all read-states): Clio's morning board names this lane mine; no competing claim. Memory Core sweep: #16497's trail (threshold exonerated under emulation; claim-owned boundary proposed, not built). Own-assignment sweep: #19225, #19186, #19101, #15252 — none covers the admission policy. Structure-map gate: N/A (engine dock, no ai/ surface).
Origin Session ID: bfc0eb67-ebf7-4123-a23d-69021b6700a1
Retrieval Hint: "vessel conversion admits on the pointer claim, min-axis metric retired"
🪢 Mnemosyne (Claude Fable 5.1 · Claude Code) · session bfc0eb67-ebf7-4123-a23d-69021b6700a1
Context
Operator hand witness (2026-09-26, v13.2 engine-release blocker): dragging a native popup over another popup shows drop zones; dragging a TAB out — the tab converts to a popup drag proxy, the vessel — and over another popup shows none, so the proxy never switches back to the tab header inside the target. Expected: the proxy switches back to the tab header and the target popup's drop zones show, wherever over the target the vessel is dragged — the native path's behavior.
Measured on real windows (headed, no viewport emulation; main 1320×920, target popup 700×500 at 4300,300 with inner 700×433, torn-out vessel 320×240, grab offset 31,16; 36 entries from four approach directions on a 3×3 grid of the target's content + two traverses; receipts
test-results/dock-wow-demo/conversion-region/2026-09-26/*-region.json): with the pointer inside the target's content and a live claim every time (claimCount 1, winner = the target), every entry at fy ≤ 0.5 converts and every entry at fy = 0.85 does not, from all four directions; the top-to-bottom traverse reverts at fy ≈ 0.85. The offline modeloverlap / min(extent)per axis on the vessel's frame vs the target's inner rect reproduces every row the geometry publisher's lag does not blur (ry 0.337 at fy 0.85, below both thresholds). For the operator's pane-sized popups (vessel ≥ target) the same formula admits only fx ≲ 0.49, fy ≲ 0.49 — the upper-left quadrant of the target.The Problem
One gesture, two admissions. The native title-bar path admits a target by a POINT — the moving popup's outer top-left corner plus inset inside the target's accepting region (
DragCoordinator#getNativeWindowDropCandidate) — so drop zones show wherever that anchor is. The torn-out path admits by the pointer claim AND the sensor's overlap metric:VesselConversion#resolveConversionenters only atpointerInTarget && composed >= 0.55withcomposed = min(rx, ry)and reverts below 0.35, andTabSortZone#resolveRemoteDragTransitionnulls the target — no preview, no proxy, no drop zones — until the sensor converts (engage: record.converted). The threshold declares itself provisional: "Production opt-in remains off until the physical park/re-show lifecycle is ready and headed calibration replaces this placeholder" (src/dashboard/dock/interaction/TabSortZone.mjs:130-135). The Workstation opted in (#15396), the calibration never ran, and viewport emulation hid it: an emulated popup has no chrome and the film's vessels are small against the main window, so#16497exonerated the threshold for that beat. Under real chrome with a hand that aims anywhere, the gate is the symptom.Design authority
HOVERING_CLAIMonly after its physical park effect returns stricttrue; leaving the claim may resumeDETACHED_MOVINGonly after strict re-show admission." — entry and exit are bound to the CLAIM. §2.8.1: "no claim — fail closed: no preview, no commit."learn/agentos/decisions/0029-docking-design.md:618): "The §2.8.4 min-axis metric stays size-neutral; target-cover changes only the reversible physical embodiment needed after that metric proposes conversion." — it acknowledges the metric as the proposer but defines neither the metric, its plane, nor its threshold (§2.8.4 defines no metric; Eos's V-B-A on #19225).The Architectural Reality
src/dashboard/dock/window/VesselConversion.mjs— the sensor:sample()measures andresolveConversion()decides;convertThreshold/revertThreshold/composeRatios/axisRatio/clampRatio;isMeasurableRect(fail-closed geometry); thepointerExitedTargettri-state (#16497).src/dashboard/dock/interaction/TabSortZone.mjs:119-147opt-in + threshold configs;:461sensor factory;:645-770resolveRemoteDragTransition(engage/commitEligible=record.converted).src/dashboard/dock/projection/LayoutAdapter.mjs:490-494,:581-583,:1337-1341thread the thresholds; no app or example sets them.src/manager/DragCoordinator.mjs:1017-1100feedspointerInTarget: Boolean(claimed?.zone)plus the target's inner rect;:630is the native anchor admission.apps/workstation/view/VesselWorkspace.mjs:1472 resolveVesselConversionSourceRect— after #19232 the frame; it stays the park's pre-conversion rect and the measurability input.The Fix
VesselConversion: entry = a live pointer claim on measurable rects; hold = no observed or derived exit (tri-state unchanged); a converted sensor still reverts on unmeasurable geometry (fail-closed). RetireconvertThreshold,revertThreshold,composeRatios,axisRatio,clampRatio, the band validation and therx/ry/composedrecord fields; the record carriesmeasurable. Class docs rewritten to the claim-owned contract.TabSortZone: retirevesselConversionConvertThreshold/vesselConversionRevertThresholdand the factory args;LayoutAdapter: retire their threading (keepvesselConversionPointerExitGraceMs).:618(the claim proposes; the metric retired; the measured reason, dated).VesselWorkspace#resolveVesselConversionSourceRectJSDoc: the rect's remaining role (stacked on #19232).DockVesselConversion.spec.mjsre-pinned to the claim-owned contract (red-first);DockTabSortZone.spec.mjsfixture + the async-park replay arm re-aimed at claim loss; e2eDemoBVesselConversionNLand five-beat scene 3 stay green.Contract Ledger
VesselConversionconfigsconvertThreshold,revertThreshold; seamscomposeRatios,axisRatio,clampRatioVesselConversion#sample()record{converted, measurable, pointerInTarget, sourceRect, targetRect}(+transitioning)TabSortZoneconfigsvesselConversionConvertThreshold,vesselConversionRevertThreshold;LayoutAdapteroption threadingvesselConversionPointerExitGraceMsstays:618Acceptance Criteria
VesselConversionconverts on the first sample withpointerInTarget: trueand any measurable overlap (composed 0.2 in today's terms — red on dev) and holds through overlap changes while the claim stands; it reverts on an observed exit, on claim loss without exit evidence, and on unmeasurable rects.VesselConversion,TabSortZone, or the adapter's option threading;validateConfigstill fails loud on missing or non-function seams.engaged: trueand the target's preview present after the next move; both traverses hold to the far edge (fx / fy 0.95). Receipts undertest-results/dock-wow-demo/conversion-region/.WorkstationFiveBeatNLscene 3 andWorkspace.spec.mjsgreen in CI;DemoBVesselConversionNL.spec.mjsre-aimed to the claim-owned crossing (a standing claim never flips; claim loss re-shows) — a headed-only witness off CI (headless its tear-out popup never opens, before any changed line), so its run is a## Post-Merge Validationitem for a seat with a display.Out of Scope
Avoided Traps
#16497, Eos on #19225).Related
#19225 / PR #19232 (source plane),
#16497(claim-TTL flicker; threshold exonerated under emulation only),#15396(conversion + park),#15432/#15434(sensor contract tier), ADR 0029 §2.8.1 / §2.8.6.Live latest-open sweep: checked the latest 20 open issues at 2026-09-26T07:34:57Z — no equivalent (#19225 is the source-plane sibling). A2A sweep (last 30, all read-states): Clio's morning board names this lane mine; no competing claim. Memory Core sweep:
#16497's trail (threshold exonerated under emulation; claim-owned boundary proposed, not built). Own-assignment sweep: #19225, #19186, #19101, #15252 — none covers the admission policy. Structure-map gate: N/A (engine dock, noai/surface).Origin Session ID: bfc0eb67-ebf7-4123-a23d-69021b6700a1 Retrieval Hint: "vessel conversion admits on the pointer claim, min-axis metric retired"
🪢 Mnemosyne (Claude Fable 5.1 · Claude Code) · session bfc0eb67-ebf7-4123-a23d-69021b6700a1