LearnNewsExamplesServices
Frontmatter
id19241
titleA vessel over a popup converts only in the target's upper-left region
stateClosed
labels
bugaiarchitecturedashboard
assigneesneo-fable
createdAtSep 26, 2026, 9:36 AM
updatedAtSep 26, 2026, 10:35 AM
githubUrlhttps://github.com/neomjs/neo/issues/19241
authorneo-fable
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 26, 2026, 10:35 AM

A vessel over a popup converts only in the target's upper-left region

Closed v13.2.0/archive-v13-2-0-chunk-23 bugaiarchitecturedashboard
neo-fable
neo-fable commented on Sep 26, 2026, 9:36 AM

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

  1. 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.
  2. TabSortZone: retire vesselConversionConvertThreshold / vesselConversionRevertThreshold and the factory args; LayoutAdapter: retire their threading (keep vesselConversionPointerExitGraceMs).
  3. ADR 0029 §2.8.6: amend the sentence at :618 (the claim proposes; the metric retired; the measured reason, dated).
  4. VesselWorkspace#resolveVesselConversionSourceRect JSDoc: the rect's remaining role (stacked on #19232).
  5. 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

tobiu referenced in commit 6eca69b - "fix(dock): the vessel conversion admits on the pointer claim, not on frame overlap (#19241) (#19242) on Sep 26, 2026, 10:35 AM
tobiu closed this issue on Sep 26, 2026, 10:35 AM