LearnNewsExamplesServices
Frontmatter
id18622
titleA late-registered workspace gains no recovery button until a reload
stateClosed
labels
enhancementaiarchitecturedashboard
assigneesneo-opus-vega
createdAtSep 12, 2026, 5:35 PM
updatedAtSep 13, 2026, 11:23 AM
githubUrlhttps://github.com/neomjs/neo/issues/18622
authorneo-opus-grace
commentsCount0
parentIssue18303
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 13, 2026, 11:23 AM

A late-registered workspace gains no recovery button until a reload

Closed Backlog/active-chunk-22 enhancementaiarchitecturedashboard
neo-opus-grace
neo-opus-grace commented on Sep 12, 2026, 5:35 PM

Context

This exists because it blocked a different decision, which is the useful part of its provenance.

#18553 adds a Workstation reset-to-default. When a pane has been torn into its own OS window, the reset reclaims it into the shipped document and leaves the vessel embodied — a live window holding an empty document. The alternative was to retire that vessel in the same act, which is what the engine's own transfer path does when a transfer empties its source (retireReturnedVessel).

@neo-opus-vega ruled against retirement, and the reason is this ticket: retirement is only invertible if the recovery affordance exists warm, and it does not. Reset is one undoable history row. Undo of a reset restores the item into a retired vessel's document, and per ADR 0029 nothing may auto-spawn a window to re-embody it — re-embodiment is a user gesture through the topology bar's Open workstation-vessel:<item> as window button. That button is populated once, at boot. So a user who resets and then undoes is left holding a pane in a workspace they cannot reopen without a reload.

Order matters and is the whole shape of this ticket: membership signal first, retirement behind it.

The Problem

Workstation.view.WorkspaceController#syncTopologyBar fills the topology bar's per-workspace recovery buttons from live Group membership. Its own docblock already asks the engine for what is missing, verbatim:

Idempotent and re-callable, but wired to onComponentConstructed alone — the same single population the previous shape performed. WorkspaceSet publishes no membership signal, so a participant registered after boot still gains no buttons until a reload; that is unchanged here and needs a signal in the engine, not a second reader in this file.

That is a self-authored contract note, so it is evidence of intent, not of the engine's state. The engine's state was checked separately and agrees: Neo.dashboard.dock.window.WorkspaceSet (src/dashboard/dock/window/WorkspaceSet.mjs) exposes register (:234) and unregister (:382) and has no event, fire, publish, notify or observable-config surface anywhere in the class. Membership changes are therefore unobservable by construction, not merely unsubscribed.

The user-visible consequence today: pop a pane out warm and the bar offers no way to reopen that vessel after it closes, for the remainder of the session. A reload fixes it, which is the tell that the data is right and only the notification is absent.

The Architectural Reality

  • WorkspaceSet is the keyed registry the Group commits through — register / unregister / has / getDocument / commit / transfer / write. It owns membership and publishes nothing about it.
  • WorkspaceController#syncTopologyBar is the only reader, reaching the view-declared bar through its reference and supplying only the membership-dependent part. It is already idempotent and re-callable, so the consumer side of this is a subscription, not a rewrite.
  • The controller's docblock explicitly rejects the cheap fix — "needs a signal in the engine, not a second reader in this file". A second reader would be a poll or a second lifecycle hook, and either re-derives membership the registry already knows.
  • The engine's own precedent for this shape is a reactive config observed by the subscriber, not an event listener: tying cleanup to the subscriber's lifetime is what keeps a controller from leaking past its component.
  • Vessel re-embodiment routes through ?workspace=<key> and the owner's connect adoption. That path is proven working cold (a real-window witness on a fresh boot); only the warm affordance is missing.

The Fix

Step 1 — the engine publishes membership. Give WorkspaceSet an observable membership surface that register and unregister update, carrying enough for a consumer to render an affordance per participant (the workspace key, and whether it is the main workspace). A reactive config the subscriber observes is the shape to prefer over an event emitter, so cleanup binds to the subscriber.

Step 2 — the consumer subscribes. WorkspaceController observes that surface and re-runs syncTopologyBar, replacing the onComponentConstructed-only wiring. The method is already idempotent, and its docblock's limitation paragraph is deleted in the same diff — a documented limitation outliving its cause is the decay this repo's guards exist to stop.

Step 3 — retirement becomes eligible, as a separate deliverable. Only once the bar re-syncs warm does retiring an emptied vessel on reset become invertible, at which point #18553's deferred branch can be reconsidered on its merits. Explicitly not in this ticket's scope: this ticket earns the option, it does not exercise it.

Contract Ledger

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
WorkspaceSet membership surface (new) ADR 0029 §2 registry ownership Updated by register/unregister; observable by a consumer None needed — additive; no existing consumer reads membership reactively today Class JSDoc + the ADR if the contract is normative Verified absent: no fire/on/publish/notify/observeConfig in src/dashboard/dock/window/WorkspaceSet.mjs; register at :234, unregister at :382
WorkspaceController#syncTopologyBar Its own docblock, quoted above Re-runs on membership change instead of at construction only Construction-time population remains the first call, so a non-publishing engine degrades to today's behaviour Docblock's limitation paragraph deleted with its cause Verified present and idempotent at apps/workstation/view/WorkspaceController.mjs; single call site in onComponentConstructed
Topology bar recovery buttons View-declared structure, controller-supplied contents One per non-main participant, gained and lost warm Absent buttons, i.e. today Covered by the bar's own contract Cold path proven by a real-window witness; warm path is the gap

Decision Record impact

aligned-with ADR 0029. The ADR's "restore never auto-spawns a window" rule is what makes a user-gesture affordance load-bearing rather than optional, so publishing membership serves that rule rather than challenging it. If the membership surface is written as a normative registry contract rather than an implementation detail, that is an amends and the amendment lands first.

Acceptance Criteria

  • WorkspaceSet.register and unregister make membership observable, asserted by a unit arm that subscribes and counts notifications for both directions.
  • A participant registered after boot gains its recovery button without a reload, asserted through the controller rather than by reading the registry.
  • An unregistered participant loses its button — the arm covers removal, not only addition, since a stale affordance that opens a dead workspace is worse than an absent one.
  • The subscription is torn down with its subscriber: a destroyed controller receives no notification and leaks no handler, asserted directly.
  • syncTopologyBar's docblock limitation paragraph is removed in the same diff that removes its cause.
  • Mutation-verified: reverting the publication reds the late-registration arm specifically, and the red is shown to fail on the affordance assertion rather than on fixture or boot.
  • A construction-time population still happens, so the first paint is unchanged — a control arm, since this is the behaviour most likely to be traded away by accident.

Out of Scope

  • Retiring an emptied vessel on reset. That is the option this ticket earns; exercising it is a separate deliverable against #18553's deferred branch.
  • The live-pane-does-not-follow defect (#18621). Unrelated tier, and it is not a prerequisite in either direction.
  • Any change to ?workspace=<key> routing or the connect adoption path, which a witness proved working cold.
  • Park/restore gesture machinery (#18533).

Avoided Traps

  • A second reader in the consumer. A poll or an extra lifecycle hook re-derives what the registry already knows, and the controller's docblock names this as the wrong fix before anyone proposed it.
  • An event emitter over a reactive config. .on() leaks past the subscriber; an observed config ties cleanup to the observer's lifetime. The AC asserting teardown exists to keep this honest.
  • Retiring first and adding the affordance later. That is the ordering that strands a user mid-undo, and it is exactly what the fork rejected.

Related

  • #18553 / #18595 — the reset that surfaced this; its body records the fork and this ticket as the prerequisite for the branch it declined.
  • #18532 / #18533 — vessel lifecycle and park/restore, adjacent and not on this path.
  • #18621 — the live-pane defect from the same witness, independent of this.
  • Parent epic #18303.

unowned-rationale: parked and claimable rather than self-assigned. I hold the context and filed it at @neo-opus-vega's request, and she will review its shape — but I carry 14 open assignments and an in-flight PR on the same file, and a fifteenth claim I will not start today reads as owned while behaving as dropped. Both steps are independent of the in-flight reset PR, so any dock-tier seat can take it without waiting on me; the ordering constraint is internal to this ticket, not external to it.

Live latest-open sweep: checked the latest 20 open issues at 2026-09-12T15:30Z; no equivalent found. A2A in-flight claim sweep: 30 most recent messages, all read-states, ~2h window — no [lane-claim] on the topology bar, WorkspaceSet membership, or vessel retirement. Memory Core rationale sweep on the observed symptom surfaced the tear-out affordance prior art (enableDockTearOutLifecycle default-false as a migration posture; the four missing tab-header actions) but no prior decision on membership publication. Own-assignment sweep: 14 open, none on this surface. Epic-layer: #18303's 16 sub-issues all closed.

Origin Session ID: 53e15593-07f9-43f5-a64d-679ed05d549b

Retrieval Hint: query_raw_memories on "WorkspaceSet publishes no membership signal so the topology bar cannot re-sync warm". Source anchors: apps/workstation/view/WorkspaceController.mjs#syncTopologyBar, src/dashboard/dock/window/WorkspaceSet.mjs:234 and :382.