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
- Preserve
DockProjectionReconciler's default true-removal semantics.
- 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.
- Re-adopt that exact parked instance when a later perspective makes the item renderable again.
- 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.
- 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
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.
Context
PR #15176 extended
Neo.dashboard.DockProjectionReconcilerso 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!isDestroyedcache 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/devwasbdd741ff16ce9c57048f68aed6cff454687bb322.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
Detachedtopology while only the primary window exists applies the primary document and reports Workbench asunrestored: no-live-window. Unlike the explicit pop-out path, this restore path does not park the cachedCounterPanebefore projection.The shared reconciler therefore treats Workbench as a true removal and destroys it.
DemoBWorkspace.resolvePane()later seesisDestroyedand creates a replacement, so the app does not crash—but the demo's defining object-permanence contract is broken.CounterPane.framesis 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=1Run against exact merged
origin/devafternpm run build-themes -- -n -e dev -t all:neo-component-1neo-component-86Focus restore must re-adopt the same live CounterPaneThis is a product regression, not a hypothetical coverage gap.
Architectural Reality
popOutPane()andreattachPane(): remove the pane from its parent without destroying it before committing the document transfer.setItemAutoHidden. Do not invent an app E2E for a lifecycle it does not expose.Intended Solution
DockProjectionReconciler's default true-removal semantics.learn/agentos/HarnessDockZoneModel.mdwith the shipped absent-item contract: resolver configs materialize once; true removals retire once; owner-retained temporary absences must be parked before reconciliation.Contract Ledger
DockProjectionReconcilerDemoBWorkspace.resolvePane()HarnessDockZoneModel.md!isDestroyedAcceptance Criteria
Detachedwith no live popup can settle fully without destroying the original CounterPane.Focusafterward re-adopts the exact original CounterPane id and its monotonically increasingframesvalue.DemoBPerspectivesNLtwo-run journey passes at exact head.HarnessDockZoneModel.mdrecords 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
framesinto a Store; the counter is deliberately an instance-lifecycle witness.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 parkingAuthored by Emmy (GPT-5.6 Sol Ultra, Codex). Session adddb25d-fc36-4b08-b9a3-3a62a108cda1.