ADR 0029 §2.8.5 already specifies this authority chain. Focus, position, resize and close
"resolve the topology entry inside the App Worker, route the opaque key to the exact owning main thread, and revalidate the live generation before touching the native handle."
The main-thread half is implemented and correct: src/Main.mjs:783#getNativeWindowRoute(data, capability), called at eight sites across all four capabilities. The App-Worker half — resolving the topology entry and deciding whether this caller may ask at all — has no public owner. So every caller that needs it wrote its own, and each one is weaker than Main's.
Surfaced while mapping #18460's vessel surface (AC-1 map part 2, effects layer). It is the named engine prerequisite blocking that leaf's native bucket; @neo-gpt-emmy, who stewards the consumer epic #18456, declined to fold it there and asked for its own engine authority after a duplicate sweep (that sweep is attested at the foot of this body).
The Problem
capabilities?.<capability> === true guards, enumerated across src (492 files), apps (317), examples (418), test (570), buildScripts (88):
file
capability-guard sites
realm
apps/workstation/view/Workspace.mjs
10
App Worker
examples/dashboard/crossWindow/DemoBWorkspace.mjs
6
App Worker
src/ai/client/RuntimeService.mjs
3
App Worker
total
19 across 3 files
src/dashboard/dock/window/Placement.mjs:306-311 also reads nativeRoute, but compares route identity (WindowManager.get(windowId)?.nativeRoute === route) rather than guarding a capability. Different kind; counted separately and deliberately not swept into this ticket.
Two of the three are dock consumers. The third is not.src/ai/client/RuntimeService.mjs is engine-side Brain client code, three realms away from docking, and it hand-rolls the same shape at :316-329 (close), :370-383 (position), :412-424 (focus) — destructure nativeRoute, test capabilities?.<cap>, rebuild {nativeHandleKey, targetWindowId, windowId: ownerWindowId}. A predicate re-derived independently inside src/ by a subsystem with no relationship to the dock is the evidence that this is engine mechanism, not consumer style.
Why the duplication is not merely redundant — it is asymmetrically weaker
Main.mjs:783 refuses on six conditions and repairs state as a side effect:
So a consumer can pass its own guard, dispatch, and be refused downstream by a condition it never checked — same-name reuse having replaced the entry, the window being closed, or the native method being absent. The refusal is real, arrives after dispatch, and looks like nothing. That is the same silent-refusal class this subsystem keeps producing (#18455, #18448).
⚠️ The fix is NOT to give consumers Main's function. Three of Main's six conditions read main-thread state (entry.win.closed, the live WindowProxy, method existence) that the App Worker cannot see and must not pretend to. The gap is that nothing owns the checks the App Worker can make authoritatively, so each caller invented a subset.
The Architectural Reality
src/manager/Window.mjs is already the App-Worker-side entry owner ADR 0029 §2.8.5 names — "manager.Window remains the runtime topology and geometry observer. Its private entry may carry the reconnect-bounded {targetWindowId, ownerWindowId, opaqueHandleKey} route plus generic capability facts."
It already holds the data and already fails closed: :151 stores capabilities: nativeRoute?.capabilities || {close: false, focus: false, position: false, resize: false}, :155 stores the nativeRoute itself, :207 nulls the route on disconnect.
Its public surface is getWindowAt, calculateGeometry, the three lifecycle handlers and toJSON — no route-authority accessor. The data is present; the question has no method.
ADR 0029 §2.8.5 also binds the failure mode: "Independently opened, cross-origin, stale, or uncorrelated windows remain inspectable where possible but fail closed for physical control."
The Fix
Give the App-Worker-side route-authority question one public owner, and route all three callers through it.
Evidenced candidate: Neo.manager.Window. It is the entry owner in the ADR, it already normalizes the capability facts fail-closed, and the predicate needs nothing it does not already hold. Falsifier that would move it: if the predicate turns out to need dock- or vessel-specific state to answer, manager.Window is the wrong home — §2.8.5 forbids dock semantics entering the manager — and it belongs in a src/dashboard/dock/window/ module instead, with RuntimeService then needing a separate answer.
Alternative considered — publish Main's predicate. Rejected: it answers a main-thread question. Exposing it would either force a round trip per pre-check or invite consumers to cache main-thread truth in the App Worker, which is the staleness ADR 0029 §2.8.5 exists to prevent.
Deliberately not prescribing a file, class name or superclass here. #18460's own history is the reason: its earlier prescription named a class and a file before establishing the responsibility's semantic owner and was withdrawn as ticket-prescription-off. The responsibility is named above; the implementer establishes the owner against it.
Contract Ledger Matrix
Target Surface
Source of Authority
Proposed Behavior
Fallback
Docs
Evidence
App-Worker route-authority predicate (new public method)
Given a target window id and a capability, resolve the entry and answer whether this caller may dispatch — refusing on absent entry, absent route, ownership mismatch, target mismatch, or capability not granted
aligned-with ADR 0029 (§2.8.5). This implements a step the accepted record already specifies and does not amend it: Main remains the physical-handle owner, close remains a separately owner-granted post-commit effect, and the fail-closed rule for stale or uncorrelated windows is what the predicate enforces rather than relaxes. No ADR change is sought; if the implementation finds it must relax any §2.8.5 clause, that is a stop-and-amend, not a judgement call at implementation time.
Acceptance Criteria
Each criterion names the world in which it goes red, because an AC that cannot fail is not a criterion.
A single public App-Worker predicate answers the route-authority question for all four capabilities. Red today: no such method exists — manager.Window's public surface is getWindowAt, calculateGeometry, three lifecycle handlers and toJSON.
It refuses on each axis independently, one spec per axis: no entry, entry without nativeRoute, ownerWindowId mismatch, targetWindowId mismatch, capability not granted. Red: an implementation that returns a route when the caller does not own the window — a single combined assertion would pass while four axes go unchecked, so the ACs are per-axis by construction.
All 19 guard sites across the three files route through it; the count of capabilities?.<cap> literals in those files reaches zero. Red today: 19. Placement.mjs:306-311 is out of scope and its identity compare must survive untouched.
A caller can distinguish "refused before dispatch" from "dispatched and refused by Main". Red: a bare boolean return, which reproduces the current silent-refusal class in one place instead of three.
No spec weakened, and no tier moved from green to red by this change — Workstation unit + NL, DemoB unit, RuntimeService. Arms already failing on the merge base are excepted only where the PR names them and a control run at that base reproduces them, and the PR must not claim they are green. Every behavioural delta is named in the PR body. Refusals may only get stricter: a well-formed route behaves identically, and a route missing a required identity refuses earlier — never the reverse. Red: a spec edited to accommodate the predicate; a delta the PR body did not name; any input the old guard refused and the new one admits; or a baseline failure asserted as green.
Author correction, 2026-09-09. This criterion originally read "Zero behavioural change in all three consumers". That was imprecise at filing and fought AC-1: routing three consumers through one predicate necessarily makes the two that checked fewer axes refuse cases they previously passed to Main, so a literal zero-delta bar could only be met by not delivering the ticket. The replacement is stricter, not looser — it keeps "no spec weakened" and adds an enumeration duty the original did not impose.
Out of Scope
Changing Main.mjs#getNativeWindowRoute, its privacy, or any of its eight call sites.
The multi-step compensating park/reshow transaction (resumeWindowDrag → orphan-recovery probe → resize → move with two-level rollback). That is the second half of #18460's bucket-2 prerequisite and is a separate, larger question about whether VesselPark should own a transaction skeleton. This ticket is the authority half only.
Any new native-window capability, or granting a capability any consumer does not already hold.
Consumer-side refactoring beyond replacing the guard call.
Avoided Traps
Exposing Main's predicate as the "obvious" fix — it answers a main-thread question; publishing it invites App-Worker callers to cache main-thread truth, the exact staleness §2.8.5 prevents.
A generic "window capability service" — the measured need is one predicate over data an existing owner already holds. A new service would be a second native authority, which ADR 0029 and #18460 both forbid.
Naming a class and file before the owner — #18460's withdrawn prescription is the local precedent for why this body names a responsibility and an evidenced candidate with its falsifier, not a path.
Treating this as consumer cleanup — two of the three sites are dock consumers and it would read as a dock-consumer problem, except the third is src/ai/client/. The third site is why this is an engine ticket.
Counting Placement.mjs to make the number bigger — it reads nativeRoute but guards nothing; folding it in would inflate the evidence and change the ticket's subject.
Prior decision this ticket must answer to
@neo-gpt-emmy measured the shared native-route responsibility on 2026-09-06 with a four-arm prototype probe across both dock consumers and recorded it on #18304 (IC_kwDODSospM8AAAABS41lxQ), explicitly as evidence to preserve substantial body ownership as unresolved, and explicitly not as grounds for a small symptom ticket. That disposition was correct on its evidence: two dock consumers sharing a shape is a dock-decomposition question, and #18304 owns dock decomposition.
What changed is the third site.src/ai/client/RuntimeService.mjs re-derives the same predicate with no relationship to the dock, which moves the question outside #18304's subject — the reason this is filed rather than folded onto that epic. The park/reshow transaction ownership she measured stays where she put it: unresolved on #18304, and out of scope here.
Related
Blocks: #18460 (its native bucket, plus resolveTearOutVessel and disposeParkedTearOutVessel)
Related: #18456 · #18304 · #15614 (whose Out of Scope defers exactly this: "Promoting any seam into src/dashboard/ (separate ticket if a seam proves framework-general).")
Decision record: ADR 0029 §2.8.5
Sweeps
Live latest-open sweep: checked the latest 20 open issues at 2026-09-08T21:47:56Z; no equivalent.
A2A in-flight claim sweep: 30 most recent messages, all read-states; no competing [lane-claim]. @neo-gpt-emmy's message the same evening states the native prerequisite is unclaimed and unfolded.
Memory Core rationale sweep: one query_raw_memories on the problem's nouns surfaced the #18304 prior decision above, which this body answers rather than ignores.
Agent OS Structure Map Gate:npm run ai:structure-map is Brain-hosted and does not resolve in this repository — recorded rather than skipped. Placement evidence is instead the ADR §2.8.5 owner citation plus manager.Window's existing custody of the data.
Context
ADR 0029 §2.8.5 already specifies this authority chain. Focus, position, resize and close
The main-thread half is implemented and correct:
src/Main.mjs:783#getNativeWindowRoute(data, capability), called at eight sites across all four capabilities. The App-Worker half — resolving the topology entry and deciding whether this caller may ask at all — has no public owner. So every caller that needs it wrote its own, and each one is weaker thanMain's.Surfaced while mapping #18460's vessel surface (AC-1 map part 2, effects layer). It is the named engine prerequisite blocking that leaf's native bucket; @neo-gpt-emmy, who stewards the consumer epic #18456, declined to fold it there and asked for its own engine authority after a duplicate sweep (that sweep is attested at the foot of this body).
The Problem
capabilities?.<capability> === trueguards, enumerated acrosssrc(492 files),apps(317),examples(418),test(570),buildScripts(88):apps/workstation/view/Workspace.mjsexamples/dashboard/crossWindow/DemoBWorkspace.mjssrc/ai/client/RuntimeService.mjssrc/dashboard/dock/window/Placement.mjs:306-311also readsnativeRoute, but compares route identity (WindowManager.get(windowId)?.nativeRoute === route) rather than guarding a capability. Different kind; counted separately and deliberately not swept into this ticket.Two of the three are dock consumers. The third is not.
src/ai/client/RuntimeService.mjsis engine-side Brain client code, three realms away from docking, and it hand-rolls the same shape at:316-329(close),:370-383(position),:412-424(focus) — destructurenativeRoute, testcapabilities?.<cap>, rebuild{nativeHandleKey, targetWindowId, windowId: ownerWindowId}. A predicate re-derived independently insidesrc/by a subsystem with no relationship to the dock is the evidence that this is engine mechanism, not consumer style.Why the duplication is not merely redundant — it is asymmetrically weaker
Main.mjs:783refuses on six conditions and repairs state as a side effect:if ( !route || route.targetWindowId !== targetWindowId || this.openWindows[entry.windowName] !== entry || entry.win.closed || entry.nativeCapabilities[capability] !== true || typeof entry.win[methods[capability]] !== 'function' ) { entry?.win?.closed && this.#invalidateNativeWindowEntry(entry); return null }A consumer's version tests two (
apps/workstation/view/Workspace.mjs:2414):route.targetWindowId !== entry.windowId || route.capabilities?.position !== true || …So a consumer can pass its own guard, dispatch, and be refused downstream by a condition it never checked — same-name reuse having replaced the entry, the window being closed, or the native method being absent. The refusal is real, arrives after dispatch, and looks like nothing. That is the same silent-refusal class this subsystem keeps producing (
#18455,#18448).⚠️ The fix is NOT to give consumers
Main's function. Three ofMain's six conditions read main-thread state (entry.win.closed, the liveWindowProxy, method existence) that the App Worker cannot see and must not pretend to. The gap is that nothing owns the checks the App Worker can make authoritatively, so each caller invented a subset.The Architectural Reality
src/manager/Window.mjsis already the App-Worker-side entry owner ADR 0029 §2.8.5 names — "manager.Windowremains the runtime topology and geometry observer. Its private entry may carry the reconnect-bounded{targetWindowId, ownerWindowId, opaqueHandleKey}route plus generic capability facts.":151storescapabilities: nativeRoute?.capabilities || {close: false, focus: false, position: false, resize: false},:155stores thenativeRouteitself,:207nulls the route on disconnect.getWindowAt,calculateGeometry, the three lifecycle handlers andtoJSON— no route-authority accessor. The data is present; the question has no method.The Fix
Give the App-Worker-side route-authority question one public owner, and route all three callers through it.
Evidenced candidate:
Neo.manager.Window. It is the entry owner in the ADR, it already normalizes the capability facts fail-closed, and the predicate needs nothing it does not already hold. Falsifier that would move it: if the predicate turns out to need dock- or vessel-specific state to answer,manager.Windowis the wrong home — §2.8.5 forbids dock semantics entering the manager — and it belongs in asrc/dashboard/dock/window/module instead, withRuntimeServicethen needing a separate answer.Alternative considered — publish
Main's predicate. Rejected: it answers a main-thread question. Exposing it would either force a round trip per pre-check or invite consumers to cache main-thread truth in the App Worker, which is the staleness ADR 0029 §2.8.5 exists to prevent.Deliberately not prescribing a file, class name or superclass here. #18460's own history is the reason: its earlier prescription named a class and a file before establishing the responsibility's semantic owner and was withdrawn as
ticket-prescription-off. The responsibility is named above; the implementer establishes the owner against it.Contract Ledger Matrix
src/manager/Window.mjs:151fail-closed capability defaults@summary; ADR 0029 §2.8.5 cited as upstream contractMain.mjs:783#getNativeWindowRoutesrc/Main.mjs:783, 8 call sitesapps/workstation/view/Workspace.mjs10 guard sitesexamples/dashboard/crossWindow/DemoBWorkspace.mjs6 guard sitessrc/ai/client/RuntimeService.mjs3 guard sites:316,:370,:412Decision Record impact
aligned-with ADR 0029(§2.8.5). This implements a step the accepted record already specifies and does not amend it:Mainremains the physical-handle owner, close remains a separately owner-granted post-commit effect, and the fail-closed rule for stale or uncorrelated windows is what the predicate enforces rather than relaxes. No ADR change is sought; if the implementation finds it must relax any §2.8.5 clause, that is a stop-and-amend, not a judgement call at implementation time.Acceptance Criteria
Each criterion names the world in which it goes red, because an AC that cannot fail is not a criterion.
A single public App-Worker predicate answers the route-authority question for all four capabilities. Red today: no such method exists —
manager.Window's public surface isgetWindowAt,calculateGeometry, three lifecycle handlers andtoJSON.It refuses on each axis independently, one spec per axis: no entry, entry without
nativeRoute,ownerWindowIdmismatch,targetWindowIdmismatch, capability not granted. Red: an implementation that returns a route when the caller does not own the window — a single combined assertion would pass while four axes go unchecked, so the ACs are per-axis by construction.All 19 guard sites across the three files route through it; the count of
capabilities?.<cap>literals in those files reaches zero. Red today: 19.Placement.mjs:306-311is out of scope and its identity compare must survive untouched.A caller can distinguish "refused before dispatch" from "dispatched and refused by
Main". Red: a bare boolean return, which reproduces the current silent-refusal class in one place instead of three.No spec weakened, and no tier moved from green to red by this change — Workstation unit + NL, DemoB unit,
RuntimeService. Arms already failing on the merge base are excepted only where the PR names them and a control run at that base reproduces them, and the PR must not claim they are green. Every behavioural delta is named in the PR body. Refusals may only get stricter: a well-formed route behaves identically, and a route missing a required identity refuses earlier — never the reverse. Red: a spec edited to accommodate the predicate; a delta the PR body did not name; any input the old guard refused and the new one admits; or a baseline failure asserted as green.Out of Scope
Main.mjs#getNativeWindowRoute, its privacy, or any of its eight call sites.resumeWindowDrag→ orphan-recovery probe → resize → move with two-level rollback). That is the second half of #18460's bucket-2 prerequisite and is a separate, larger question about whetherVesselParkshould own a transaction skeleton. This ticket is the authority half only.src/dashboard/dock/window/Placement.mjs's route-identity comparison.Avoided Traps
Main's predicate as the "obvious" fix — it answers a main-thread question; publishing it invites App-Worker callers to cache main-thread truth, the exact staleness §2.8.5 prevents.src/ai/client/. The third site is why this is an engine ticket.Placement.mjsto make the number bigger — it readsnativeRoutebut guards nothing; folding it in would inflate the evidence and change the ticket's subject.Prior decision this ticket must answer to
@neo-gpt-emmy measured the shared native-route responsibility on 2026-09-06 with a four-arm prototype probe across both dock consumers and recorded it on #18304 (IC_kwDODSospM8AAAABS41lxQ), explicitly as evidence to preserve substantial body ownership as unresolved, and explicitly not as grounds for a small symptom ticket. That disposition was correct on its evidence: two dock consumers sharing a shape is a dock-decomposition question, and #18304 owns dock decomposition.
What changed is the third site.
src/ai/client/RuntimeService.mjsre-derives the same predicate with no relationship to the dock, which moves the question outside #18304's subject — the reason this is filed rather than folded onto that epic. The park/reshow transaction ownership she measured stays where she put it: unresolved on #18304, and out of scope here.Related
Blocks: #18460 (its native bucket, plus
resolveTearOutVesselanddisposeParkedTearOutVessel) Related: #18456 · #18304 · #15614 (whose Out of Scope defers exactly this: "Promoting any seam intosrc/dashboard/(separate ticket if a seam proves framework-general).") Decision record: ADR 0029 §2.8.5Sweeps
[lane-claim]. @neo-gpt-emmy's message the same evening states the native prerequisite is unclaimed and unfolded.query_raw_memorieson the problem's nouns surfaced the #18304 prior decision above, which this body answers rather than ignores.npm run ai:structure-mapis Brain-hosted and does not resolve in this repository — recorded rather than skipped. Placement evidence is instead the ADR §2.8.5 owner citation plusmanager.Window's existing custody of the data.Origin Session ID: 02a567b8-3204-4840-a50b-259f03f7b6bb
Retrieval Hint:
"App Worker side native route authority predicate duplicated across three consumers"·nativeRoute+capabilities?.+#getNativeWindowRoute· ADR 0029 §2.8.5