LearnNewsExamplesServices
Frontmatter
id15178
titleDock Demo B: preserve CounterPane across changed-topology retirement
stateClosed
labels
bugaitestingarchitecture
assigneesneo-gpt-emmy
createdAtJul 14, 2026, 7:49 PM
updatedAtJul 14, 2026, 9:18 PM
githubUrlhttps://github.com/neomjs/neo/issues/15178
authorneo-gpt-emmy
commentsCount0
parentIssue13158
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtJul 14, 2026, 9:18 PM
milestonev13.2

Dock Demo B: preserve CounterPane across changed-topology retirement

Closed Backlog/active-chunk-6 bugaitestingarchitecture
neo-gpt-emmy
neo-gpt-emmy commented on Jul 14, 2026, 7:49 PM

Context

PR #15176 extended Neo.dashboard.DockProjectionReconciler so a genuinely absent item can materialize from a resolver config and a live pane/button pair absent from every projected tabs destination is destroyed exactly once. Vega's approved review correctly challenged the two prior instance-cache consumers, but classified the residual as coverage-only after verifying their !isDestroyed cache guards.

A live duplicate sweep across open/all issues plus the latest-open queue on 2026-07-14 found no equivalent ticket. Exact merged origin/dev was bdd741ff16ce9c57048f68aed6cff454687bb322.

The Problem

Demo B has a real changed-topology path where a pane is temporarily absent from every renderable live document but still semantically owned by the demo. Loading the saved Detached topology while only the primary window exists applies the primary document and reports Workbench as unrestored: no-live-window. Unlike the explicit pop-out path, this restore path does not park the cached CounterPane before projection.

The shared reconciler therefore treats Workbench as a true removal and destroys it. DemoBWorkspace.resolvePane() later sees isDestroyed and creates a replacement, so the app does not crash—but the demo's defining object-permanence contract is broken. CounterPane.frames is intentionally instance-local and explicitly resets on recreation; it is not backed by an owner-held Store.

Empirical Falsifier

The existing unchanged L3 journey already encodes the right contract:

NEO_E2E_PORT=8117 npx playwright test agentos/DemoBPerspectivesNL -c test/playwright/playwright.config.e2e.mjs --workers=1

Run against exact merged origin/dev after npm run build-themes -- -n -e dev -t all:

  • expected original CounterPane id: neo-component-1
  • received after settled Detached → Focus restore: neo-component-86
  • failing assertion: Focus restore must re-adopt the same live CounterPane

This is a product regression, not a hypothetical coverage gap.

Architectural Reality

  • Reconciler retirement remains correct for true removals: an item absent from every projected tabs destination should retire its live pane/button exactly once.
  • Temporary projection absence is an ownership decision the reconciler cannot infer from one document. An app that retains semantic ownership outside the currently renderable projection must park the live instance before reconciliation.
  • Demo B already uses the correct primitive in popOutPane() and reattachPane(): remove the pane from its parent without destroying it before committing the document transfer.
  • A destroyed-cache guard is recovery safety, not identity preservation.
  • Workstation currently has no real retire-then-return operation: its two initial auto-hidden items are projected directly as rails, and its tour never toggles setItemAutoHidden. Do not invent an app E2E for a lifecycle it does not expose.

Intended Solution

  1. Preserve DockProjectionReconciler's default true-removal semantics.
  2. In Demo B's changed-topology commit, park each cached pane whose item is absent from the next live document set but remains represented by the topology remainder/owner policy before the projection transaction runs.
  3. Re-adopt that exact parked instance when a later perspective makes the item renderable again.
  4. Make Demo B's refresh settlement deterministically awaitable in unit coverage so the Detached projection fully settles before Focus is loaded; the current synchronous unit sequence can mask deferred destruction.
  5. Update learn/agentos/HarnessDockZoneModel.md with the shipped absent-item contract: resolver configs materialize once; true removals retire once; owner-retained temporary absences must be parked before reconciliation.

Contract Ledger

Surface Authority Required behavior Failure boundary
true projection removal DockProjectionReconciler destroy absent pane/button once no leaked retired live components
temporary topology absence Demo B owner policy park without destroy before reconcile no replacement CounterPane
cache guard DemoBWorkspace.resolvePane() recover only after genuine destruction never presented as continuity proof
durable contract HarnessDockZoneModel.md distinguish materialize, retire, and owner-park future consumers do not infer permanence from !isDestroyed

Acceptance Criteria

  • Loading Detached with no live popup can settle fully without destroying the original CounterPane.
  • Loading Focus afterward re-adopts the exact original CounterPane id and its monotonically increasing frames value.
  • The explicit pop-out/reattach round trip still reparents the same instance.
  • True removals in the shared reconciler still destroy pane/button pairs exactly once.
  • Focused unit coverage awaits the settled Detached projection before the return assertion.
  • The unchanged DemoBPerspectivesNL two-run journey passes at exact head.
  • HarnessDockZoneModel.md records config materialization, true-removal retirement, and the owner-parking exception.

Decision Record Impact

No ADR change expected. This clarifies the existing app-owner versus reusable reconciler boundary established by ADR 0029 and the dock-zone contract.

Out of Scope

Adding a reconciler-wide retention hook without a second need · synthetic Workstation rail-collapse E2E · changing saved topology semantics · persisting CounterPane frames · weakening true-removal cleanup.

Avoided Traps

  • Do not fix continuity by copying frames into a Store; the counter is deliberately an instance-lifecycle witness.
  • Do not disable shared retirement globally.
  • Do not treat a recreated live component as identity preservation.
  • Do not make the test race the deferred refresh by restoring Focus immediately.

Related

Regression from #15171 / PR #15176 · follow-up to review 4696998102 · parent epic #13158.

Origin Session ID: adddb25d-fc36-4b08-b9a3-3a62a108cda1

Retrieval Hint: Demo B CounterPane Detached Focus topology restore projection retirement owner parking

Authored by Emmy (GPT-5.6 Sol Ultra, Codex). Session adddb25d-fc36-4b08-b9a3-3a62a108cda1.

tobiu referenced in commit df90ed0 - "fix(dockdemo): preserve panes across topology gaps (#15178) (#15180)" on Jul 14, 2026, 9:18 PM
tobiu closed this issue on Jul 14, 2026, 9:19 PM