LearnNewsExamplesServices
Frontmatter
id18252
titleThe multi-window restore seams have no implementer, so a saved topology can never be reloaded
stateClosed
labels
enhancementwindowsaiarchitecturedashboard
assigneesneo-opus-grace
createdAtSep 4, 2026, 4:01 AM
updatedAtSep 4, 2026, 2:06 PM
githubUrlhttps://github.com/neomjs/neo/issues/18252
authorneo-opus-grace
commentsCount0
parentIssue18151
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtSep 4, 2026, 2:06 PM

The multi-window restore seams have no implementer, so a saved topology can never be reloaded

Closed v13.2.0/archive-v13-2-0-chunk-18 enhancementwindowsaiarchitecturedashboard
neo-opus-grace
neo-opus-grace commented on Sep 4, 2026, 4:01 AM

Context

The operator's stated prio-zero for dock layouts is: save the layout, reload the multi-window app with the saved layout, get the same multi-window setup back — and "i am 99% sure this is not the case."

He is right, and the reason is not a missing feature. Every piece is built. The gap is that nothing implements the two seams that connect them, so the whole path is unreachable from any component.

Measured at dev@274b63beac

The engine ships a complete multi-window save/restore chain:

piece file state
capture N documents as one v2 record model/Persistence.mjs:133 captureTopologyPerspective() built, unit-tested
shape-only topology term model/Persistence.mjs windowFingerprint built, validated (never ids or coordinates)
slot-indexed restore reconciliation model/TopologyReconciler.mjs (712 lines) built, unit-tested, fails the ENTIRE restore closed on any invalid slot
plan toward a captured layout persistence/RestorePlanner.mjs built
the routing that drives all of it ai/client/DockService.mjs:184, :379 built

DockService reaches a live app through exactly two holder seams — getDockTopologyDocuments() (ordered committed documents, primary first) and commitDockTopologyDocuments().

Neither has an implementation anywhere.

$ grep -rnE "^\s+(async )?getDockTopologyDocuments\s*\(" src/ apps/ examples/ test/ --include="*.mjs"
(none)

$ grep -rnE "^\s+(async )?commitDockTopologyDocuments\s*\(" src/ apps/ examples/ test/ --include="*.mjs"
test/playwright/unit/ai/client/DockService.spec.mjs:544
test/playwright/unit/ai/client/DockService.spec.mjs:631

Both definitions are stub objects inside DockService's own unit spec. Every reference in the repo lives in exactly two files: the service that requires them (9 hits) and the spec that stubs them (9 hits). dashboard/dock/Workspace.mjs implements neither; apps/workstation implements neither.

So the multi-window restore path is green, validated, and has been exercised only against test doubles. DockService:171 even handles the absence gracefully — it reports that the component "exposes no getDockTopologyDocuments() seam" — which is why this has never surfaced as an error.

Why this is structural, not one missing method

This is the second instance of one shape found tonight. On #18233 the finding was that interaction/Preview and interaction/DragAffordances each describe themselves as "the app-neutral piece every docking workspace composes" and no engine workspace composes either. This is the same defect in the persistence tier: the engine builds the machinery, documents the seam, tests it against stubs, and assembles nothing.

The pattern matters more than either instance. A capability can be fully implemented, fully covered, and still deliver nothing, because coverage against a stub proves the machinery and never the reach. Whatever the fix here, the epic should treat "is there an implementer, or only a requirer?" as a standing question.

Landing-site constraint

There is a standing operator directive on src/dashboard/dock/Workspace.mjs — stop adding to this layer (PR #18213, recorded in its closure comment; see #18208). Adding two methods there is the idiomatic-looking choice and is what the directive forbids.

window/WorkspaceSet.mjs already holds what the read seam needs: ids(), getDocument(workspaceId), and adoptTransfer() for the write half. Whether the seams are answered from the set, from a thin binding, or elsewhere is the implementing PR's decision — but it needs to be made deliberately, and dock/Workspace.mjs is not it.

Acceptance Criteria

RESTATED 2026-09-04 after cross-lab review of PR #18253 (@neo-gpt-emmy). Two ACs were unachievable as written and one was measuring the wrong thing. Both restatements are narrowing, not weakening — each names what it was protecting and what now witnesses it.

  • AC-1 — RESTATED. The MODEL half closes here; the VIEW half moves to #18272. A dock host captures a multi-window topology and an N-slot commit restores every slot's document atomically — all or none. Witnessed, not inferred.
    • ⚠️ What this ticket can no longer claim: that the restored composition is visible in the second window. TopologySeams converges the primary projection only, and that is structural: all seven workspaceSet.register sites in the repository supply plain-assignment writers, dockModel is not a reactive config, and a vessel's targetState.document is written in five places and read only by its own getDocument. There is no sibling projection to trigger. Building one is the popup/document-ownership question live on D#18224, so it is owned by #18272 rather than decided here by implementation accident.
    • The original wording ("restore it after a reload, with the same composition") reads as an operator-visible outcome. It was never achievable from a seams PR, and closing #18252 on it would have recorded a screen-level guarantee the code does not make.
  • AC-2 — RESTATED. Measure concern growth, not line count. The seams' implementation lives outside src/dashboard/dock/Workspace.mjs (window/TopologySeams.mjs, 105 lines), and Workspace.mjs gains only the composition cost — one import plus one mixin config, 7 lines, owning no state, no methods and no configs for the concern.
    • The original "line count unchanged" forbids the very vehicle the architecture review endorsed: a mixin cannot be composed without a composition line. It was protecting against the god object absorbing another concern, and 7 lines of composition against 105 lines of concern placed elsewhere is that protection satisfied, not evaded. Graded by the Stage-0-gated Ownership test (D#18150): TopologySeams owns no state or lifecycle, so it is judged as a protocol surface — minimal, mandatory, resolved by a named external consumer.
  • AC-3 — Restore does not replay history. The captured record is a composition state and is applied as one, per the operator's "reloading an app must not walk through the entire history."
  • AC-4 — A host that implements neither seam behaves exactly as today. Verified: DockService.restoreTopologyPerspective filters for missing seams and returns a structured refusal(...); nothing throws. The capability is additive.
  • AC-5 — RESTATED to follow AC-1. Mutation-verified: removing the implementation reds the N-slot atomicity arm. A green arm over a capability that resolves to a stub is the exact failure this ticket documents — and that bar now applies to the model half, with the view half's mutation bar carried by #18272.
  • AC-6 — The windowFingerprint stays shape-only. No window ids, no coordinates — ADR 0029's rule. Verified: model/Persistence.mjs:72 validates it as a JSON record and nothing in the restore path reads geometry.

Contract Ledger

Backfilled 2026-09-04 (@neo-gpt-emmy, RA-2). Three methods enter a contract with this ticket; a future consumer or reviewer detects drift against this table rather than against the diff.

method direction contract
getDockTopologyDocuments() exposed — resolved by DockService Returns the ordered live committed documents, primary first. A set-less workspace returns [dockModel]; an empty set returns []. The read seam that topology CAPTURE and topology RESTORE share, so both see one ordering.
commitDockTopologyDocuments(documents) exposed — resolved by DockService Atomic multi-document write: every registered slot adopts or none does. Returns {errors}; empty means committed. Returns a refusal rather than throwing — restoreTopologyPerspective branches on commit?.errors?.length and does not wrap the call, so a throw escapes the refusal shape as an RPC crash. Converges the primary projection only (see AC-1 / #18272).
workspaceSet.adoptAll(documents) consumed — from window/WorkspaceSet.mjs Slot-ordered, both-or-neither, with two-sided compensation on a writer failure. Refuses on slot-count mismatch, a null slot, or a read-only workspace. Absence is a valid state: a single-document workspace has no set, and commitDockTopologyDocuments falls back to the one-document path rather than treating it as an error.

Non-goals

  • Pixel geometry as durable state. Whether window position/size should persist is D#18224's open question to the operator and is not settled here. This ticket restores composition — which windows exist and what is in them.
  • Undo/redo across windows. The transaction/history half of the operator's ask is a separate axis; this one is the reload path, which explicitly must not replay history.
  • Changing TopologyReconciler, RestorePlanner or the capture format. They are built and tested; this is about reaching them.

Decision Record impact

aligned-with ADR 0029 — the docking design record. No new docking concept; this connects seams the record already defines.

Related

  • #18151 (epic — the same "engine holds it, consumer cannot reach it" thesis) · #18233 / PR #18251 (the same assemble-nothing shape in the drag tier) · #18208 (the layer directive that constrains the landing site) · D#18150, D#18224 (the multi-window architecture discussion and its open operator question)

Sweep before filing

Live latest-20 open at 2026-09-04T01:5xZ — no equivalent. Full-text perspective restore multi-window over open (nothing; #15614 and #13158 are adjacent, neither this). Closed-queue RestorePlanner OR capturePerspective — #14652 (single-window capture, closed), #16315, #16316 (demo-scoped, closed); none covers the missing implementer. A2A claim sweep over the last 14 messages: the only active lane claim is @neo-opus-vega's #18241 (toolbar actions), no overlap. Memory Core rationale sweep on the same nouns surfaced the DockLayouts v13.2 arc, no prior decision on this seam gap. Own-assignment sweep: none of my open tickets covers it.

Authored by Grace (Claude Opus 5, Claude Code). Session 118d8435-8d23-4745-a28a-8d22da918aac.