LearnNewsExamplesServices
Frontmatter
id18276
titleA Workspace born headless arms the wrong window and never projects
stateClosed
labels
bugaiarchitecturecoredashboard
assigneesneo-gpt
createdAtSep 4, 2026, 3:41 PM
updatedAtSep 4, 2026, 7:31 PM
githubUrlhttps://github.com/neomjs/neo/issues/18276
authorneo-fable
commentsCount4
parentIssue18151
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 4, 2026, 7:30 PM

A Workspace born headless arms the wrong window and never projects

Closed v13.2.0/archive-v13-2-0-chunk-19 bugaiarchitecturecoredashboard
neo-fable
neo-fable commented on Sep 4, 2026, 3:41 PM

Context

Measured live on apps/workstation at dev@2ee23d7801 (blob 115a3f9b98) through the Neural Link while running the D#18224 popup-birth arm (receipt). Neo.create('Neo.dashboard.dock.Workspace', {dockModel}) with no parent and no window succeeds, does two wrong things, and one of them is silent. Every shipping host hides both by seeding a first shell in its own constructor — knowledge only the engine has, which is this epic's theme.

Filed at the operator's request to capture the arm's findings as engine work; the D#18224 Step-Back (DC_kwDODSospM4BFxQB, addendum) lists the same two requirements as acceptance criteria of the owner design, so this leaf is prerequisite work for it, not part of its vehicle decision.

The Problem

  1. Construct arms geometry on a window it does not have. construct calls observeWindowGeometry(this.windowId) unconditionally (src/dashboard/dock/Workspace.mjs:548). With windowId: null the remote call carries no routing key; src/worker/Base.mjs:478–484 delivers it to ports[0] and the worker console prints sendMessage destination "main" is deprecated … remoteMethod: setConfigs, data: {observeMovement: true, observeResize: true, windowId: null}. WindowPosition#setConfigs (src/main/addon/WindowPosition.mjs:330) deletes data.windowId, so nothing is poisoned — but an arbitrary window was armed by a workspace that has no render target.
  2. The class owns no first projection, and the failure is silent. After binding (Neo.worker.App#moveComponent into a viewport → container.Base#createItem NeoInstance branch, :413–437, adopts windowId/appName, mounted: true), onDockZoneDocumentChange(document) renders nothing: Reconciler.reconcileProjection throws Dock projection could not find a current shell at index 0 (src/dashboard/dock/projection/Reconciler.mjs:353–357), the error is untyped, onDockProjectionFailed re-throws it (Workspace.mjs:3923), and the rejected refreshPromise reaches no console. (A rejected refresh tail was left outside #18039's fixed scope on purpose; this leaf adds no generic logging either — it makes the no-shell state not an error.) examples/dashboard/dock/MainContainer.mjs:147, :310 and every host seed items = [projectDockModel()] in construct, which is why nobody sees it.

Seeded through a hot-patched beforeRefreshDockWorkspace (this.add(this.projectDockModel(null, null, document)) when no shell exists), the same instance projected its document, and with a registry-answered resolvePane adopted two LIVE panes of the main workspace by projection alone (same ids, verify_component_consistency 1/1/1).

The Architectural Reality

  • construct :516–549: preview producer, optional tear-out handlers + worker connect/disconnect subscription (:535), then the unconditional geometry arm (:548).
  • refreshDockWorkspace :3741 waits for the host to mount, then projects and reconciles; the reconciler requires host.items[shellIndex] to exist (:353–357).
  • onDockProjectionFailed :3918–3949 recovers only errors marked isDockProjectionFailure; everything else re-throws into the refresh tail.
  • ADR 0029 §2.1: the workspace container owns the committed document and re-projects it through LayoutAdapter.project() — the first projection is part of that contract, not the consumer's.

The Fix

Narrowed 2026-09-04 14:55Z per intake (IC_kwDODSospM8AAAABSlARkA), verified against Reconciler.mjs:353–399 and Workspace.mjs:2074: once this leaf lands, "no current shell" is the first projection's precondition, not a projection failure. Typing the no-shell throw as recoverable would retry against the same missing shell; with a seed present the typed error would be unreachable.

  1. Guard the geometry arm on a window: one idempotent window-binding hook arms only a real, newly bound windowId (afterSetWindowId from null to an id); construct calls the same guarded helper, so a headless instance arms nothing — beside component/Abstract#afterSetWindowId :289–304 (theme files) and component/Base#afterSetWindowId :959–962 (drag zones).
  2. Own the first projection as the normal path: Workspace#afterSetMounted (:2074) detects an absent shell at dockShellIndex and schedules the ordinary document refresh; Reconciler.reconcileProjection treats oldShell === null (:353) as an explicit first projection — mount exactly one shell at shellIndex, resolve its panes, make it visible, return a normal result with landedInPlace: false (:394–399 vocabulary) — never staging beside a shell that does not exist. Consumers that already supply a shell keep their static boot path.
  3. Only a failing initial mount is a failure: if the first projection's mount or reconciliation rejects, retire the failed initial shell back to zero shells and mark the real error as a typed projection failure with the named recovery retired-initial, so the existing exactly-one retry rule of onDockProjectionFailed operates on it. Generic refresh-tail logging stays out of this leaf.

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
Workspace#construct geometry arm (:548) ADR 0029 §2.8.5 (the host arms its own render target) arms only with a windowId; a headless instance arms nothing none needed class docblock worker console shows no deprecated-destination warning on Neo.create(Workspace, {dockModel})
Workspace#afterSetWindowId (inherited) + afterSetMounted (:2074) component/Abstract:289 cascade first bind arms geometry once (idempotent, real id only); the mount schedules the first projection — class docblock headless → bind renders the document with no consumer seeding
Reconciler.reconcileProjection with oldShell === null (:353–357) ADR 0029 §2.1 (the workspace owns projection) explicit first projection: one shell mounted at shellIndex, landedInPlace: false; a rejected initial mount retires to zero shells and surfaces as a typed failure with recovery retired-initial current untyped throw Reconciler docblock headless → bind renders; a forced initial-mount rejection surfaces once and retries once

Decision Record impact

aligned-with ADR 0029 §2.1 / §2.8.5 — no authority changes; the leaf makes the class honour a contract it already states.

Acceptance Criteria

  • Unit: Neo.create(Workspace, {dockModel}) with no window emits no remote geometry call (spy on Neo.main.addon.WindowPosition.setConfigs), and binding it into a container with a windowId arms exactly one.
  • Unit: after viewport.add(headlessWorkspace) and onDockZoneDocumentChange(document), the host holds one shell projecting the document's tabs nodes — no consumer seeding, no hot patch.
  • Unit, red-first at dev@2ee23d7801: the same sequence today leaves items empty and refreshPromise rejected with the untyped no-shell error.
  • The no-shell precondition never produces a failure; a first projection whose initial mount rejects (forced in the spec) retires the initial shell back to zero shells, fires dockProjectionFailed with recovery retired-initial, and retries exactly once — never a silent rejection.
  • examples/dashboard/dock/MainContainer.mjs keeps working with its constructor seeding removed (deletion, per the accretion rule), and the Workstation is unchanged.

Out of Scope

  • The owner-level item→instance registry (the D#18224 owner design; the arm faked it with Neo.getComponent(componentRef)).
  • Moving the tear-out lifecycle out of Workspace (D#18150 cluster 1).
  • Popup-birth latency (needs the Playwright rig; separate arm).

Avoided Traps

  • Projecting in construct: a headless instance must project nothing (Step-Back addendum: headless arms neither geometry nor visible projection); the first projection belongs to the first bind.
  • Typing the no-shell throw as a recoverable failure: the retry would run against the same missing shell, and with a seed present the typed error is unreachable (intake correction). Only a failing initial mount is a failure; other throws stay loud.
  • Reviving generic refresh-tail logging here: #18039 left it out explicitly; it needs its own evidence and owner.

Related

  • #18151 (parent: the dock asks consumers to re-derive knowledge only the engine has) · #18039 (the swallowed refresh rejection) · #18143 (a rejected update mid-swap) · D#18224 DC_kwDODSospM4BFxN1 (the arm) and DC_kwDODSospM4BFxQB (Step-Back addendum) · ADR 0029 §2.1, §2.8.5.

Live latest-open sweep: checked the latest 40 open issues (created-desc) at 2026-09-04 13:37Z; no equivalent found (#18272, #18274, #18275 are neighbours, not this). A2A claim sweep (last 60 min, all read-states): no claim on this scope. Memory Core sweep: only this session's own arm memories. Own-assignment sweep: none on this surface.

unowned-rationale: found while measuring for D#18224 in peer-role; claimable by any seat; prerequisite for the owner design's headless → bound lifecycle, independent of its vehicle decision.

Origin Session ID: ee061205-8271-4458-b73b-b132c1bec310

Retrieval Hint: query_raw_memories("headless dock Workspace geometry ports[0] first projection no shell silent reject moveComponent bind")

tobiu cross-referenced by PR #18298 on Sep 4, 2026, 6:28 PM