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.
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):
getRefreshOptions(descriptor, source)return {}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 nullhasDockRecreateFallback()isthis.resolveFreshPane !== Workspace.prototype.resolveFreshPane, so the engine can never be its own fallback by construction. The reload action therefore hides for every pane without adockReload()method.openTearOutVessel(request)return nullwindow/Participation.mjsand never constructs one. Measured on the real consumer (2026-09-03), which suppliescrossWindowSortGroupand writes zero participation code: a vessel walked across the main window's centre by CDP paintsregions=0 affordances=0at every step, withWindowPositionverifiedarmed: true/observeMovement: truein both realms at every step — so geometry publishes and the zero is the capability, not an unarmed poll. No error, no warning.apps/workstationis the only host that paints, via 263 hand-written lines (createCrossWindowParticipation106,renderCrossWindowPreview111,refreshCrossWindowParticipation43) 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:
enableDockReloadActionandenableDockPopOutActionboth defaulttruewhile 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 thatsetItemLockedsets one item flag and touches no node, whilemoveItemrestructures. 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 whetherNeo.config.useSharedWorkersmakes 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/workstationlooks 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 withwindow/TearOut, and cross-window participation withwindow/Participation. One PR cannot deliver them, and bundling them would produce a sub no PR could cleanlyResolves. 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:
model/Operationsdeclares, 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.getRefreshOptionssurvives, as the override for a host with knowledge the engine lacks.dockReload()delegation keeps priority.Neo.config.useSharedWorkersis on, since that is the real capability gate for a shared-worker window. A host that owns its lifecycle keeps exactly one owner.Participationfrom the seams it already owns, because it already owns every input: it admitted the vessel, it armed that window's geometry, anddockModelanswers the hit-test. The measured coupling says lift rather than redesign — insideapps/workstation's 263 linesworkspaceIdappears only as a threaded parameter (24×) with zero references to any workspace-set or state registry, and the engine surfaces they call areinteraction.Preview,interaction.DragAffordances,dockModel.nodes. That measurement is done, and it scopes the sub.window/Participation.mjsis a 342-line engine class in which all 19 seams default tonull— 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), plusresolveNativeWindowDrag, since the engine holdstearOutPanes/tearOutAdmissions. 7 no — four routed through that host'svesselProxyEmbodimentand three through itsnativeVesselParkHandlers, a cross-window proxy-continuity refinement the contract already treats as optional (apps/workstationitself passes themisMain ? … : 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
ADR 0029 §2.6's user-triggered-only recreate bound — automatic recreation stays forbidden.Avoided traps
getRefreshOptions". The consumer's code is idiomatic — realdata.Storemodels, the documented engine seams, no monkey-patching across 1005 lines. The engine is what makes that insufficient.enableDockTearOutLifecycletotrueas the obvious fix. Its default-false is load-bearing for hosts carrying a legacy lifecycle, and #18014 AC-9 requires exactly one lifecycle owner.useSharedWorkersalone. The right capability truth, and insufficient by itself: availability without a default opener renders a control that opens nothing.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: thegetRefreshOptionschange-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
nativeDragZone.Structure-map gate: N/A —
src/dashboard/dock/**is engine Body surface, notai// 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"