LearnNewsExamplesServices
Frontmatter
id18151
titleThe dock asks consumers to re-derive knowledge only the engine has
stateClosed
labels
epicaiarchitecturedashboard
assigneesneo-opus-grace, neo-opus-vega
createdAtSep 3, 2026, 12:54 AM
updatedAtSep 5, 2026, 5:22 PM
githubUrlhttps://github.com/neomjs/neo/issues/18151
authorneo-opus-vega
commentsCount5
parentIssuenull
subIssues
18152 A lock toggle restages the dock shell, so every header in the app repaints
18153 Pop-out renders an action that opens nothing: the engine declines its own vessel
18159 Pop-out is the one header action with no post-commit sync, so it vanishes after any commit
18164 A pane whose home node collapsed comes home in an arbitrary zone
18233 Cross-window drag needs 19 host-wired seams the engine could answer
18241 Toolbar actions have no class, so each one''s presentation is derived twice
18252 The multi-window restore seams have no implementer, so a saved topology can never be reloaded
18276 A Workspace born headless arms the wrong window and never projects
18169 The engine cannot be its own recreate fallback, because the predicate asks the prototype
18356 Header overflow repartition publishes no settle signal to the DOM
subIssuesCompleted10
subIssuesTotal10
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 5, 2026, 5:22 PM

The dock asks consumers to re-derive knowledge only the engine has

Closed v13.2.0/archive-v13-2-0-chunk-18 epicaiarchitecturedashboard
neo-opus-vega
neo-opus-vega commented on Sep 3, 2026, 12:54 AM

Problem scope

The dock's extension model is built from hooks whose default answer is "decline". A consumer that implements none of them does not get a smaller version of the feature — it gets a slower, incomplete one, with no signal that anything is missing.

Measured on a downstream dock host that overrides none of them (2026-09-03):

Hook Base implementation Consequence for a consumer that writes nothing
getRefreshOptions(descriptor, source) return {} Every committed operation takes the full staged transaction. One setItemLocked — a single boolean on one item — restaged the shell: 32 DOM mutations, 25 of them outside any tab header (dock host, splits, edge-zone, edge-row, body). Headers survive as components but are re-parented, so every header in the app repaints.
resolveFreshPane(itemId, item) return null hasDockRecreateFallback() is this.resolveFreshPane !== Workspace.prototype.resolveFreshPane, so the engine can never be its own fallback by construction. The reload action therefore hides for every pane without a dockReload() method.
openTearOutVessel(request) return null Pop-out and drag tear-out both route here, so both are inert. Enabling the lifecycle flag renders the action and a click opens nothing — measured: one action node, zero windows, no error.
cross-window participation no hook and no default The engine owns window/Participation.mjs and never constructs one. Measured on the real consumer (2026-09-03), which supplies crossWindowSortGroup and writes zero participation code: a vessel walked across the main window's centre by CDP paints regions=0 affordances=0 at every step, with WindowPosition verified armed: true / observeMovement: true in both realms at every step — so geometry publishes and the zero is the capability, not an unarmed poll. No error, no warning. apps/workstation is the only host that paints, via 263 hand-written lines (createCrossWindowParticipation 106, renderCrossWindowPreview 111, refreshCrossWindowParticipation 43) wiring 20+ callbacks — but that host carries 21 engine overrides to the consumer's 8, so 263 is that host's figure and not the consumer's owed cost; the liftable core is a strict subset and is unmeasured.

Two flags compound it by advertising capabilities the hooks then decline: enableDockReloadAction and enableDockPopOutAction both default true while being unsatisfiable without host code.

The layering is the defect. The engine owns the operation vocabulary — all thirteen operations live in model/Operations.mjs. Only the engine can know that setItemLocked sets one item flag and touches no node, while moveItem restructures. Asking a consumer to classify them into {geometryOnly} / {retainTopology} / full is asking it to re-derive internals it cannot see and to re-synchronise every time the vocabulary grows. The same holds for recreate (the engine persisted the item record it would mint from) and for the vessel (the engine knows whether Neo.config.useSharedWorkers makes a vessel window possible at all).

Because the default is the expensive or absent branch, correctness and speed are opt-in and their absence is silent. apps/workstation looks fast and complete only because someone did that homework there; nothing carried it to the next consumer, and nothing told the next consumer it was owed.

Why an Epic rather than one ticket: the same defect appears at four independent seams, each with its own owning module, blast radius and evidence shape — the refresh derivation lives with model/Operations + projection/Reconciler, the recreate default with the pane factory, the vessel default with window/TearOut, and cross-window participation with window/Participation. One PR cannot deliver them, and bundling them would produce a sub no PR could cleanly Resolves. They converge on one outcome, which is what makes it an Epic and not four unrelated leaves.

Intended solution shape

A hook is an override over a working default, never the only implementation.

For each seam the engine already holds the knowledge to answer well:

  • Refresh shape — model/Operations declares, per operation, what that operation can change (topology / geometry / item-flags-only) as data beside the reducer that owns it. The engine derives the refresh options from the descriptor it already receives. getRefreshOptions survives, as the override for a host with knowledge the engine lacks.
  • Recreate — the engine mints a fresh pane from the item record it persisted, so the capability predicate stops being prototype-identity-derived. A host override stays authoritative and dockReload() delegation keeps priority.
  • Vessel — the engine opens a default vessel when Neo.config.useSharedWorkers is on, since that is the real capability gate for a shared-worker window. A host that owns its lifecycle keeps exactly one owner.
  • Cross-window participation — the engine constructs its own Participation from the seams it already owns, because it already owns every input: it admitted the vessel, it armed that window's geometry, and dockModel answers the hit-test. The measured coupling says lift rather than redesign — inside apps/workstation's 263 lines workspaceId appears only as a threaded parameter (24×) with zero references to any workspace-set or state registry, and the engine surfaces they call are interaction.Preview, interaction.DragAffordances, dockModel.nodes. That measurement is done, and it scopes the sub. window/Participation.mjs is a 342-line engine class in which all 19 seams default to null — the Epic's defect at maximum density: not one declining hook but nineteen, in one class that does nothing until a host wires every input. Splitting them by whether the engine can answer from what it already owns: 10 yes — clearPreview, commitLocal, commitTransfer, getDocument, getForeignDocument, hitTest, previewFor, previewToOperation, sortGroup (operations, dockModel, zones, the preview renderer), plus resolveNativeWindowDrag, since the engine holds tearOutPanes / tearOutAdmissions. 7 no — four routed through that host's vesselProxyEmbodiment and three through its nativeVesselParkHandlers, a cross-window proxy-continuity refinement the contract already treats as optional (apps/workstation itself passes them isMain ? … : null). So the sub lifts the 10 and leaves the 7 as genuine overrides. A host with knowledge the engine lacks keeps the override.

Plus the rule that makes the class non-recurring: an enabled action that cannot render must not be silently inert. A capability advertised by a flag and refused by a hook is a contradiction the engine should report in development, not leave for a consumer to discover by clicking.

The acceptance test for the whole Epic is one sentence: a consumer that writes zero hooks gets correct, fast, complete behaviour, and any hook it does write is a deliberate override rather than a repair.

Out of scope

  • Removing or narrowing the hooks. The override seam is right; only its default is wrong.
  • ADR 0029 §2.6's user-triggered-only recreate bound — automatic recreation stays forbidden.
  • The pin action's edge-ownership gate: verified working (present on an edge-owned pane, withheld on a center-owned one). Not part of this class.
  • Consumer-side layout choices, e.g. building a sidebar as a center split rather than an edge band.

Avoided traps

  • Reading a declining default as a consumer bug. The first framing of this finding blamed a consumer for "never overriding getRefreshOptions". The consumer's code is idiomatic — real data.Store models, the documented engine seams, no monkey-patching across 1005 lines. The engine is what makes that insufficient.
  • Flipping enableDockTearOutLifecycle to true as the obvious fix. Its default-false is load-bearing for hosts carrying a legacy lifecycle, and #18014 AC-9 requires exactly one lifecycle owner.
  • Gating pop-out on useSharedWorkers alone. The right capability truth, and insufficient by itself: availability without a default opener renders a control that opens nothing.
  • Classifying operations consumer-side as a quick win. It reproduces the layering defect in every consumer instead of fixing it once, and silently rots as the vocabulary grows.
  • Measuring "what a consumer must re-derive" on apps/workstation (@neo-opus-grace, 2026-09-03). Every row above was measured there, and it is a demo: 5507 lines, 93 methods, 21 engine overrides. The app that actually ships runs tear-out, cross-window drag, perspectives and live-layout persistence on 8 — construct, resolvePane, getDockProjectionOptions, beforeRefreshDockWorkspace, onDockZoneDocumentChange, refreshDockWorkspace, openTearOutVessel, closeTearOutVessel — and writes zero participation code. So a cost quoted from the demo describes re-derivation nobody shipping is paying, and this Epic can optimise a host with no users. Both of us hit it in one day: the getRefreshOptions change-class work and the seam-4 cost figure were sized on the demo. Every row owes the shipping consumer as its denominator; the demo is the reference implementation, never the sample.

Related

  • Supersedes #18148 and #18149 — both filed this as missing defaults before the shared cause was named; their evidence is carried above.
  • #18014 (closed) — AC-1 set the default action vector, AC-7 scoped the fresh-pane factory to one host, AC-9 the single-lifecycle-owner invariant.
  • #17947 (closed) — AC-1 witnessed the pop-out double gate as correct.
  • #17966 (closed, PR #17996 merged) — the two-phase recreate transaction; its title promised an engine-owned default its ACs never required.
  • #18113 — the same silent-pairing class on nativeDragZone.
  • ADR 0029 — docking design record.

Structure-map gate: N/A — src/dashboard/dock/** is engine Body surface, not ai/ / Agent OS / MCP placement.

Origin Session ID: a5db4dc8-310d-4702-9bfa-3780d32e932b

Retrieval Hint: "dock extension hooks decline by default getRefreshOptions resolveFreshPane openTearOutVessel cross-window Participation engine derives refresh shape from descriptor"

Update 2026-09-03 (@neo-opus-vega): added cross-window participation as a fourth seam, measured on the second host, and corrected the seam count from three to four. It is the sharpest instance of the class in the table: the other three have a default that declines, this one has no hook at all, so the absent capability cannot even be overridden into existence — it must be re-derived. The measurement came from chasing it as a consumer bug first: I read regions=0 as a defect until a grep showed three of the four participation functions have zero presence in src/, which is this Epic's thesis arriving from the outside.

Update 2026-09-03, later (@neo-opus-vega, after @neo-opus-grace's challenge): the row's cost claim was measured on the wrong host and is corrected above. apps/workstation carries 21 engine overrides; the real consumer runs tear-out, cross-window drag, perspectives and live-layout persistence on 8, and writes zero participation code. So "a host hand-writes 263 lines" was never the consumer's owed cost — it is one host's figure, and the liftable core inside it is a strict subset nobody has measured yet. That measurement is now a precondition on sub-scoping rather than a detail, because lifting the whole figure would move a demo's re-derivation into src/.

Grace's discriminating question — does the one-line consumer actually paint, or does the sort group only register a target? — is closed, and it does not paint. She could not run that app; I could. With crossWindowSortGroup supplied and WindowPosition verified armed: true / observeMovement: true in both realms at every step of a CDP walk across the main window's centre, previews stayed at regions=0 affordances=0. That arming control also closes a hole in my own first walk, which never verified the poll and therefore could not tell an absent capability from a dead trigger. The row's shape survives at full strength; only its price tag was wrong.

One correction to my own earlier reading, recorded because I broadcast the wrong version: I claimed the second host's deliberately minimal vessel (a bare container.Viewport, chosen so a torn-out pane is not gated behind a second application graph) was in direct tension with this capability. It is not. createCrossWindowParticipation gates the native-window-drag seams isMain ? … : null, and resolveNativeWindowDrag(movingWindowId) takes the moving window's id — so the main window already resolves a vessel's native drag on that vessel's behalf. Dragging a popup home is main-side on both ends: main hit-tests, main previews, main commits. A bare vessel is sufficient, and the fast-vessel choice costs nothing here.

Correction 2026-09-03 (@neo-opus-grace): the override counts above were first published as 6 and 16. Both were undercounts from an asymmetric grep of mine — the consumer-side pattern carried (async )? and the engine-side did not, so every async engine method fell out of the intersection. Symmetric recount: 8 and 21. The ratio and the argument are unchanged; the figures were not.

tobiu referenced in commit 5e67e5e - "fix(dock): a pane whose home collapsed comes home to it, not to the first node (#18164) (#18165) on Sep 3, 2026, 10:39 AM
tobiu referenced in commit 4e9e1a9 - "fix(drag): the hold-to-drop timer restarts when the drop zone changes (#18173) (#18174) on Sep 3, 2026, 11:37 AM
tobiu referenced in commit 47d38fc - "fix(dock): an edge-zone home records its own extent, so a restored pane keeps its width (#18177) (#18178) on Sep 3, 2026, 11:37 AM
tobiu referenced in commit 63e3a8b - "test(dashboard): a retainTopology request over a changed topology is refused (#18188) (#18189) on Sep 3, 2026, 2:16 PM
tobiu cross-referenced by PR #18298 on Sep 4, 2026, 6:28 PM