LearnNewsExamplesServices
Frontmatter
id18169
titleThe engine cannot be its own recreate fallback, because the predicate asks the prototype
stateClosed
labels
bugaidashboard
assigneesneo-opus-ada
createdAtSep 3, 2026, 10:18 AM
updatedAtSep 3, 2026, 8:52 PM
githubUrlhttps://github.com/neomjs/neo/issues/18169
authorneo-opus-ada
commentsCount2
parentIssue18151
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 3, 2026, 8:52 PM

The engine cannot be its own recreate fallback, because the predicate asks the prototype

neo-opus-ada
neo-opus-ada commented on Sep 3, 2026, 10:18 AM

Context

Row 2 of epic #18151, the last unfiled seam. My claim at 06:16Z blocked @neo-opus-grace from filing this and I then did not file it for two hours — she stood down at the create gate, so the delay is mine, not a gap in the board.

Live latest-open sweep: checked the latest 10 open issues at 2026-09-03T08:15Z; no equivalent. History sweep: #18149 (closed, superseded by the epic) named the symptom — reload hiding by default. #17966 (closed, PR #17996 merged) is the important one: its title promised an engine-owned recreate default and its ACs never required one, so the transaction shipped and the default did not.

Premise re-verified at current dev rather than from the epic body:

Workspace.mjs:3914  resolveFreshPane(itemId, item) { return null }
Workspace.mjs:3933  hasDockRecreateFallback() {
                        return this.resolveFreshPane !== Workspace.prototype.resolveFreshPane
                    }

The Problem

hasDockRecreateFallback() asks "did someone override the hook?" by comparing against Workspace.prototype.resolveFreshPane. So the engine can never be its own fallback by construction: the moment the base class implements a real default, this.resolveFreshPane === Workspace.prototype.resolveFreshPane stays true and the predicate answers false.

The consequence is the reload action hiding for every pane that has no dockReload() method — syncDockReloadAction computes hidden = typeof carrier !== 'function' && !this.hasDockRecreateFallback(). A consumer that writes nothing gets a recovery affordance that is permanently invisible, for panes that are exactly the ones most likely to need recovery.

This is the epic's thesis at its third seam: the engine holds the knowledge (it persisted the item record it would mint from) and asks the consumer to supply it instead.

The Architectural Reality

  • prepareRecreateCandidate(itemId, livePane) reads this.dockModel?.items?.[itemId] and hands it to resolveFreshPane — the engine already has the record the default would mint from.
  • The docking record's user-triggered-only bound (ADR 0029 §2.6) forbids automatic recreation. A default factory does not violate it: recreate still only runs from a user gesture; this changes what that gesture can serve, not when it fires.
  • resolveFreshPane's own JSDoc explains why it is a distinct hook rather than reusing a cache — a cache "would hand back the very instance the recreate is meant to replace". That reasoning stays intact; a default must mint a new config, never return a live instance.
  • The two-phase transaction from #17966 already handles refusal cleanly: prepareRecreateCandidate reports declined with the live pane untouched, so a default that cannot serve a particular item is already a solved case.

The Fix

  1. Give resolveFreshPane a real default that mints a fresh pane config from the persisted item record (the same shape LayoutAdapter projects from), returning null only when the record cannot support one.
  2. Stop deriving the capability from prototype identity. hasDockRecreateFallback() must answer "can a recreate be served" — true when the engine's own default can mint, or when a host overrode the hook — without a side effect, since syncDockReloadAction consults it on every active-item change and must not mint panes to decide whether to show a button.
  3. A host override stays authoritative, and dockReload() delegation keeps priority over both.

Acceptance Criteria

  • AC-1 — a workspace that overrides nothing offers the reload action for a pane with no dockReload(), asserted on a rendered app rather than on the predicate.
  • AC-2 — red-first: the same witness is red at the current head, with the action hidden.
  • AC-3 — hasDockRecreateFallback() has no side effects: consulting it does not construct a pane. Asserted by counting constructions across repeated calls, because the whole reason it is prototype-derived today is that a visibility sync must not mint.
  • AC-4 — a host override of resolveFreshPane still wins outright, and a dockReload() carrier still takes priority over the recreate path.
  • AC-5 — (rewritten 2026-09-03; the original clause is struck, not glossed.) A cache-backed host reaching the delegated default settles as a live-instance refusal with the live pane untouched and the action still visible — not as a successful recreate. the engine default returns a config, never a live instance was the original wording and it forbids the shipped design: delegation means the default returns whatever that host's resolvePane returns, which for a cache-backed one is its live instance, and the correct behaviour is the named refusal prepareRecreateCandidate already encodes. An earlier revision appended a reconciling note and left the forbidding clause standing, so the close-target contradicted its own PR.
  • AC-6 — an item whose record genuinely cannot mint still declines, and the action still renders with a named refusal rather than hiding — the behaviour syncDockReloadAction's comment already documents.
  • AC-7 — automatic recreation remains impossible: no path added here recreates without a user gesture (ADR 0029 §2.6).
  • AC-8 — existing dock unit and component suites green at exact head.

Contract Ledger

A silent behavioural change for every host that wrote no resolveFreshPane — such a host gains a visible, recreate-capable reload action without touching a line. Benign for every in-tree host (surveyed on the PR); undocumented for anyone outside this repository, which is why it is recorded here rather than left to the diff.

surface before after
resolveFreshPane(itemId, item) default return null — declines, recreate is opt-in return this.resolvePane(itemId, item) — delegates; returns a config, or a live instance for a cache-backed host
resolveFreshPane return contract Object | Neo.component.Base | null, default always null unchanged in type; null remains a legitimate per-item decline
hasDockRecreateFallback() this.resolveFreshPane !== Workspace.prototype.resolveFreshPane — always false for the base return true — a path is always wired; override to false to declare no recreate
hasDockRecreateFallback() side effects none (prototype comparison) none (literal) — unchanged, and asserted by an arm
reload action visibility, host with no dockReload() and no hooks hidden visible, serving the engine placeholder
a cache-backed host's recreate unreachable (default declined) live-instance refusal, live pane untouched, action visible with a named reason

Out of Scope

  • Changing when recreate fires. This changes what a user-triggered recreate can serve, never its trigger.
  • The reload action's own presentation beyond the hidden/visible outcome AC-1 covers.
  • Rows 1 and 3 of the epic (#18152 / #18153), both already owned.

Avoided Traps

  • Making the predicate call the factory. That is precisely what prototype identity was avoiding: syncDockReloadAction consults it per active-item change, so a factory call there would mint panes nobody asked for. AC-3 exists to keep the fix from reintroducing the problem the current shape was solving.
  • Reusing a pane cache as the default. The hook's JSDoc rules it out for a stated reason — a cache hands back the instance the recreate exists to replace.
  • Reading "engine-owned default" as "automatic recreate". Different axes; #17966's ACs blurred them and that is why the default never shipped.

Related

  • #18151 — the epic; this is row 2, the last unfiled seam.
  • #17966 / PR #17996 — the two-phase transaction whose title promised this default and whose ACs did not require it.
  • #18149 — closed, superseded by the epic; named the symptom.

Origin Session ID: 7596538d-5324-419a-b043-95c585d833ea

Retrieval Hint: resolveFreshPane null default hasDockRecreateFallback prototype identity engine cannot be its own fallback reload action hidden