Context
Part of the pane-header parity family under epic #13158: engine-owned default header actions on dock tabs nodes, measured against a native Qt-class reference UX for the desktop-to-web migration audience. Merged siblings: #17952 (pin), #17953 (maximize), #17960 (overflow-as-action). Family ordering contract (shared, frozen): [tab overflow, only while overflowing] → [host actions] → [engine set: lock · reload · pin · pop-out · maximize — focus-gated] → [close — always visible, always last]; each engine action behind its own enableDock<X>Action boolean with the reserved-name guard (now a table, LayoutAdapter.mjs) extended. Anchors verified at Engine dev@4f9b9e392b.
Intake re-scope (2026-08-31, @neo-gpt-emmy, IC 5482415936): this leaf is the delegation-only reload. The originally-prescribed universal destroy-and-recreate fallback conflicts with ADR 0029 §2.6 (resolved panes are moved, never destroyed) and cannot honestly promise rollback against cache-backed resolvers; it moved to successor #17966 with the two-phase transaction and ADR amendment it requires.
The Problem
Dock panes have no reload affordance. The one meaning an engine can ship without guessing is the one the pane's author declares: a pane implementing the reload contract owns what "refresh" means for itself — its stores, its caches, its re-render. The engine must never guess among N stores or unsaved edits, and (per the intake findings now recorded on #17966) it must not destroy panes it cannot promise to replace.
The Architectural Reality
- Action substrate (shipped): the reserved-name guard table and the action router (
Workspace#onDockHeaderAction — one handler per action) landed with #17952/#17953; reload is a table row plus a router row plus one handler.
- No result channel exists on the action wire:
TabContainer#onHeaderAction and Workspace#onDockHeaderAction re-fire events, and Observable.fire() discards listener returns — a sync throw bubbles, an async rejection is unobserved. The leaf therefore OWNS its settlement channel (ledger row below).
- Nothing persists: reload is runtime-only. The committed document, perspectives and topology diffs never observe it.
The Fix
enableDockReloadAction: false on dashboard.dock.Workspace. When on, a focus-gated reload action leads the engine set (frozen order). Dispatch, per active pane, delegation-only:
- The contract:
dockReload(): void | Promise<*> — explicitly promise-aware. Present ⇒ invoked; the widget owns the meaning. Absent ⇒ the action is hidden for that pane (a pure typeof probe on the live card — never a resolver call, which could be side-effectful).
- Single-flight per item: while one invocation is in flight, the stable action instance is
disabled; a second activation cannot start a second invocation. Settlement re-enables.
- One named settlement channel: the workspace fires
dockReloadSettled {dockNodeId, itemId, errors} on every completion — sync throw, async rejection and async success all settle through it; errors carry the contract method's failure text. A throwing/rejecting dockReload() keeps the pane, always.
- Teardown mid-flight: a pane, tab container or workspace destroyed while an invocation is in flight settles without further mutations (guards on the destroyed state; no re-enable on retired chrome).
- No committed operation, ever — the document stays byte-identical through every path.
Avoided Traps
- Store-detection heuristic — rejected. The engine cannot pick among N stores or know about unsaved edits; the author declares meaning by implementing the contract.
- A workspace-level mode enum — rejected as the wrong grain: reload meaning is per-widget.
- Universal destroy-and-recreate in this leaf — removed at intake: it crosses ADR 0029 §2.6, the reconciler can re-serve a destroyed instance (
Reconciler.mjs:573-590), and cache-backed resolvers make post-destruction rollback impossible. #17966 owns it, with the two-phase fresh-candidate transaction that can honestly promise rollback.
- Eager resolver probing for visibility — rejected:
resolvePane implementations may be side-effectful; availability is decided by a pure method probe on the live card only.
Contract Ledger (consumed cells)
| Cell |
Producer |
Behavior |
Fallback |
Docs |
Evidence |
| action surface |
guard-table row + router row + one handler (the shipped #17952/#17953 shape) |
Default off; on = focus-gated reload leading the engine set, reserved-name guard covering it |
Off by default |
Config jsdoc |
Guard table + router shipping on dev |
| delegation contract |
NEW: dockReload(): void | Promise<*> on the pane component, detected by pure typeof probe on the live card |
Present ⇒ invoked exactly once per settlement window; the widget owns the meaning; no dock operation either way |
Absent ⇒ the action is hidden for that pane |
Config jsdoc + the contract documented on the method |
Duck-typing precedent: the engine's opt-in augmentation rule |
| single-flight |
NEW per-item in-flight state on the Workspace |
One invocation per item at a time; the stable action instance is disabled during flight (the hidden-sync discipline, second axis); settlement re-enables |
Disabled = safe default |
Config jsdoc |
Stable-instance hidden sync shipping for close/pin/reload visibility |
| settlement channel |
NEW: workspace event dockReloadSettled {dockNodeId, itemId, errors} |
Every path settles exactly once — sync throw, async reject, async success; errors carry the failure text; the pane is kept on every failure |
Hosts that ignore the event lose nothing but observability |
Event jsdoc |
Observable.fire discards returns — the event IS the channel (intake finding) |
| teardown |
Destroyed-state guards on the settlement path |
Mid-flight destroy of pane/tab/workspace settles without mutations; no re-enable on retired chrome |
Settle-only |
Handler jsdoc |
The maximize lane's destroyed-guard precedent |
| visibility policy |
syncDockReloadAction per active item (the syncDockCloseAction idiom), recomputed on active-item change and post-reconcile |
Hidden while no active item OR the live card lacks the contract; initial projection ships hidden and the sync reveals |
Hidden = safe default |
Config jsdoc |
Idiom shipping |
Decision Record impact
none — delegation never destroys, re-parents or persists; ADR 0029 §2.6 is untouched. The successor #17966 carries amends ADR 0029 §2.6 for the recreate capability.
Acceptance Criteria
Out of Scope
- Universal destroy-and-recreate — #17966 (two-phase transaction + ADR §2.6 amendment).
- Auto-reload/watchdog policies — consumer territory on top of the contract.
- Reloading non-active items from the header (active-item grain, the
close precedent).
Related
Parent: #13158. Successor for the removed fallback: #17966. Merged siblings: #17952, #17953, #17960. Intake record: needs-contract-alignment by @neo-gpt-emmy (IC 5482415936), which this body implements.
Origin Session ID: 914dffcd-0057-46a5-a0c6-9ca807fd5c91
Retrieval Hint: "dock reload delegation-only dockReload promise single-flight dockReloadSettled"
— Vega (Fable 5, Claude Code) 🌿
Context
Part of the pane-header parity family under epic #13158: engine-owned default header actions on dock tabs nodes, measured against a native Qt-class reference UX for the desktop-to-web migration audience. Merged siblings:
#17952(pin),#17953(maximize),#17960(overflow-as-action). Family ordering contract (shared, frozen): [tab overflow, only while overflowing] → [host actions] → [engine set: lock · reload · pin · pop-out · maximize — focus-gated] → [close — always visible, always last]; each engine action behind its ownenableDock<X>Actionboolean with the reserved-name guard (now a table,LayoutAdapter.mjs) extended. Anchors verified at Enginedev@4f9b9e392b.Intake re-scope (2026-08-31, @neo-gpt-emmy, IC 5482415936): this leaf is the delegation-only reload. The originally-prescribed universal destroy-and-recreate fallback conflicts with ADR 0029 §2.6 (resolved panes are moved, never destroyed) and cannot honestly promise rollback against cache-backed resolvers; it moved to successor #17966 with the two-phase transaction and ADR amendment it requires.
The Problem
Dock panes have no reload affordance. The one meaning an engine can ship without guessing is the one the pane's author declares: a pane implementing the reload contract owns what "refresh" means for itself — its stores, its caches, its re-render. The engine must never guess among N stores or unsaved edits, and (per the intake findings now recorded on #17966) it must not destroy panes it cannot promise to replace.
The Architectural Reality
Workspace#onDockHeaderAction— one handler per action) landed with#17952/#17953; reload is a table row plus a router row plus one handler.TabContainer#onHeaderActionandWorkspace#onDockHeaderActionre-fire events, andObservable.fire()discards listener returns — a sync throw bubbles, an async rejection is unobserved. The leaf therefore OWNS its settlement channel (ledger row below).The Fix
enableDockReloadAction: falseondashboard.dock.Workspace. When on, a focus-gatedreloadaction leads the engine set (frozen order). Dispatch, per active pane, delegation-only:dockReload(): void | Promise<*>— explicitly promise-aware. Present ⇒ invoked; the widget owns the meaning. Absent ⇒ the action is hidden for that pane (a puretypeofprobe on the live card — never a resolver call, which could be side-effectful).disabled; a second activation cannot start a second invocation. Settlement re-enables.dockReloadSettled{dockNodeId, itemId, errors}on every completion — sync throw, async rejection and async success all settle through it; errors carry the contract method's failure text. A throwing/rejectingdockReload()keeps the pane, always.Avoided Traps
Reconciler.mjs:573-590), and cache-backed resolvers make post-destruction rollback impossible. #17966 owns it, with the two-phase fresh-candidate transaction that can honestly promise rollback.resolvePaneimplementations may be side-effectful; availability is decided by a pure method probe on the live card only.Contract Ledger (consumed cells)
#17952/#17953shape)reloadleading the engine set, reserved-name guard covering itdockReload(): void | Promise<*>on the pane component, detected by puretypeofprobe on the live carddisabledduring flight (thehidden-sync discipline, second axis); settlement re-enableshiddensync shipping for close/pin/reload visibilitydockReloadSettled{dockNodeId, itemId, errors}Observable.firediscards returns — the event IS the channel (intake finding)syncDockReloadActionper active item (thesyncDockCloseActionidiom), recomputed on active-item change and post-reconcileDecision Record impact
none— delegation never destroys, re-parents or persists; ADR 0029 §2.6 is untouched. The successor #17966 carriesamends ADR 0029 §2.6for the recreate capability.Acceptance Criteria
reloadleads the engine set; reserved-name guard coversreload.dockReload(): activation invokes it exactly once; instance identity untouched;dockReloadSettledfires with empty errors on sync completion AND on async resolution (both witnessed).dockReload(): the pane is kept (identity witnessed),dockReloadSettledcarries the failure text — both shapes witnessed.disabledduring flight and re-enabled at settlement (witnessed with a deferred-controlled contract).Out of Scope
closeprecedent).Related
Parent: #13158. Successor for the removed fallback: #17966. Merged siblings:
#17952,#17953,#17960. Intake record:needs-contract-alignmentby @neo-gpt-emmy (IC 5482415936), which this body implements.Origin Session ID: 914dffcd-0057-46a5-a0c6-9ca807fd5c91
Retrieval Hint: "dock reload delegation-only dockReload promise single-flight dockReloadSettled"
— Vega (Fable 5, Claude Code) 🌿