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
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.
Context
This exists because it blocked a different decision, which is the useful part of its provenance.
#18553adds 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 windowbutton. 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#syncTopologyBarfills the topology bar's per-workspace recovery buttons from live Group membership. Its own docblock already asks the engine for what is missing, verbatim: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) exposesregister(:234) andunregister(: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
WorkspaceSetis the keyed registry the Group commits through —register/unregister/has/getDocument/commit/transfer/write. It owns membership and publishes nothing about it.WorkspaceController#syncTopologyBaris 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.?workspace=<key>and the owner'sconnectadoption. 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
WorkspaceSetan observable membership surface thatregisterandunregisterupdate, 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.
WorkspaceControllerobserves that surface and re-runssyncTopologyBar, replacing theonComponentConstructed-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
WorkspaceSetmembership surface (new)register/unregister; observable by a consumerfire/on/publish/notify/observeConfiginsrc/dashboard/dock/window/WorkspaceSet.mjs;registerat:234,unregisterat:382WorkspaceController#syncTopologyBarapps/workstation/view/WorkspaceController.mjs; single call site inonComponentConstructedDecision 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 anamendsand the amendment lands first.Acceptance Criteria
WorkspaceSet.registerandunregistermake membership observable, asserted by a unit arm that subscribes and counts notifications for both directions.syncTopologyBar's docblock limitation paragraph is removed in the same diff that removes its cause.Out of Scope
#18553's deferred branch.?workspace=<key>routing or theconnectadoption path, which a witness proved working cold.#18533).Avoided Traps
.on()leaks past the subscriber; an observed config ties cleanup to the observer's lifetime. The AC asserting teardown exists to keep this honest.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.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,WorkspaceSetmembership, or vessel retirement. Memory Core rationale sweep on the observed symptom surfaced the tear-out affordance prior art (enableDockTearOutLifecycledefault-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_memorieson "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:234and:382.