Context
Found while running the NL whitebox battery for #17844 — the first time that set has executed since the repo split, because every spec in it is config-ignored unless NEO_AGENTOS_RUNTIME_ROOT is set and nothing sets it.
Two of the four dock reds are one cause, and it is not the reducer.
Measured at dev ca83102346, N=2 (reproduced in two separate census runs)
DockAutoHideRevealNL.spec.mjs:90 and :293 both fail after a pin gesture on an auto-hidden pane.
The document is correct. The reducer commits exactly what it should:
[auto-hide] inspector: {…"autoHidden":true} -> {…"autoHidden":false,"pinned":true}The rail does not retire. Worker truth still holds the affordance:
Error: no rail instance may survive the post-pin re-projection
- Expected Array []
+ Received Array [ { className: "Neo.dashboard.dock.interaction.Rail",
+ id: "neo-dashboard-dock-rail-1" } ]and so does the DOM:
Error: the retired rail tab must leave the DOM
Locator: locator('.neo-dashboard-dock-rail-tab')
Expected: 0 Received: 1 (14 × resolved to 1 element)Nine util.VNode.getVnode: Component not found for id: neo-component-29 errors accompany the run — a vnode lookup against a component that no longer exists, which is the shape of a surviving affordance pointing at a torn-down pane.
Why this is a contract violation and not a stale expectation
I checked this before calling it a defect, because the other red in the same census is a stale expectation (DockLockActionNL:31 asserts ['alpha','beta'] against a fixture that gained a third item).
Rail.mjs:12 states the contract itself:
The rail is pure render projection (per-window, derived, never persisted)
So a Rail instance surviving a re-projection that rails nothing contradicts the class's own stated nature. The spec's expectation is right.
setItemPinned is classed topology in Operations.operationChangeClass, so this commit takes the full staged transaction — the slow path. This is not a fast-path escape: the rail fails to retire even when everything is restaged.
The engine already names the hazard, one guard over. Workspace#isDockRailedItem's JSDoc:
a railed pane is projected OUTSIDE the shell. reconcileStableTopology admits the fast path on "every structural dock node retains identity", which stays true while the rail — a separate surface it does not reconcile — still holds the old pane. The result is a stale rail copy beside a fresh one.
That guard stops the item-flag path from producing a stale rail. This ticket is the same stale rail arriving through the path the guard does not cover.
What I am NOT claiming
I cannot date the onset and I am not calling it a regression. #14911 ("Wholesale container refresh leaves destroyed child component DOM in place") closed 2026-07-10 and :293 cites it, so this is that symptom class returning — but the battery carrying the guard has been unrunnable since the split, so there is no CI history to bisect against. Dating it is cheap for whoever takes this: the spec exists, and NEO_AGENTOS_RUNTIME_ROOT plus a bisect over Rail.mjs / projection/ will band it.
I have not diagnosed the mechanism. Whether the projection fails to destroy the Rail, destroys it while leaving its DOM, or rebuilds it from stale railItems is unmeasured. The three candidates are distinguishable — worker-truth instance vs DOM node vs adapter input — and guessing between them is what this ticket exists to prevent.
Acceptance Criteria
Non-goals
DockLockActionNL:31 (a stale assertion) and DockSplitterProxyPaintNL:29 — separate reds from the same census, tracked on #17844.
- The invocation gap that hid this (nothing sets
NEO_AGENTOS_RUNTIME_ROOT, no workflow runs e2e) — #17844.
- Any change to
setItemPinned's change class. It is topology and correctly so; the fast path is not implicated.
Related
#17844 (the census that surfaced it) · #14911 (closed; :293 is its guard) · #18039, #17969 (closed, adjacent rail-return defects) · #18151 (the dock epic)
Sweep before filing: latest-20 open (no equivalent), open full-text rail in:title (none), closed-queue for the same symptom (#18039, #17969, #14911 — all closed, none this shape).
Authored by Grace (Claude Opus 5, Claude Code). Session 8d936ec9-b816-4ded-a91e-6e91ef6a3325.
Context
Found while running the NL whitebox battery for #17844 — the first time that set has executed since the repo split, because every spec in it is config-ignored unless
NEO_AGENTOS_RUNTIME_ROOTis set and nothing sets it.Two of the four dock reds are one cause, and it is not the reducer.
Measured at dev
ca83102346, N=2 (reproduced in two separate census runs)DockAutoHideRevealNL.spec.mjs:90and:293both fail after a pin gesture on an auto-hidden pane.The document is correct. The reducer commits exactly what it should:
[auto-hide] inspector: {…"autoHidden":true} -> {…"autoHidden":false,"pinned":true}The rail does not retire. Worker truth still holds the affordance:
Error: no rail instance may survive the post-pin re-projection - Expected Array [] + Received Array [ { className: "Neo.dashboard.dock.interaction.Rail", + id: "neo-dashboard-dock-rail-1" } ]and so does the DOM:
Error: the retired rail tab must leave the DOM Locator: locator('.neo-dashboard-dock-rail-tab') Expected: 0 Received: 1 (14 × resolved to 1 element)Nine
util.VNode.getVnode: Component not found for id: neo-component-29errors accompany the run — a vnode lookup against a component that no longer exists, which is the shape of a surviving affordance pointing at a torn-down pane.Why this is a contract violation and not a stale expectation
I checked this before calling it a defect, because the other red in the same census is a stale expectation (
DockLockActionNL:31asserts['alpha','beta']against a fixture that gained a third item).Rail.mjs:12states the contract itself:So a
Railinstance surviving a re-projection that rails nothing contradicts the class's own stated nature. The spec's expectation is right.setItemPinnedis classedtopologyinOperations.operationChangeClass, so this commit takes the full staged transaction — the slow path. This is not a fast-path escape: the rail fails to retire even when everything is restaged.The engine already names the hazard, one guard over.
Workspace#isDockRailedItem's JSDoc:That guard stops the item-flag path from producing a stale rail. This ticket is the same stale rail arriving through the path the guard does not cover.
What I am NOT claiming
I cannot date the onset and I am not calling it a regression. #14911 ("Wholesale container refresh leaves destroyed child component DOM in place") closed 2026-07-10 and
:293cites it, so this is that symptom class returning — but the battery carrying the guard has been unrunnable since the split, so there is no CI history to bisect against. Dating it is cheap for whoever takes this: the spec exists, andNEO_AGENTOS_RUNTIME_ROOTplus a bisect overRail.mjs/projection/will band it.I have not diagnosed the mechanism. Whether the projection fails to destroy the Rail, destroys it while leaving its DOM, or rebuilds it from stale
railItemsis unmeasured. The three candidates are distinguishable — worker-truth instance vs DOM node vs adapter input — and guessing between them is what this ticket exists to prevent.Acceptance Criteria
Railinstance in worker truth and no.neo-dashboard-dock-rail-tabin the DOM. Red-first: both arms ofDockAutoHideRevealNLfail atca83102346.getVnode: Component not founderrors either disappear or are shown to be an independent condition; a fix that leaves nine console errors has not finished.Non-goals
DockLockActionNL:31(a stale assertion) andDockSplitterProxyPaintNL:29— separate reds from the same census, tracked on #17844.NEO_AGENTOS_RUNTIME_ROOT, no workflow runs e2e) — #17844.setItemPinned's change class. It istopologyand correctly so; the fast path is not implicated.Related
#17844 (the census that surfaced it) · #14911 (closed;
:293is its guard) · #18039, #17969 (closed, adjacent rail-return defects) · #18151 (the dock epic)Sweep before filing: latest-20 open (no equivalent), open full-text
rail in:title(none), closed-queue for the same symptom (#18039, #17969, #14911 — all closed, none this shape).Authored by Grace (Claude Opus 5, Claude Code). Session 8d936ec9-b816-4ded-a91e-6e91ef6a3325.