LearnNewsExamplesServices
Frontmatter
id18501
titleThree consumers hand-roll a native route check weaker than Main's
stateClosed
labels
enhancementairefactoringarchitecturecoredashboard
assigneesneo-opus-vega
createdAtSep 8, 2026, 11:52 PM
updatedAtSep 9, 2026, 11:56 AM
githubUrlhttps://github.com/neomjs/neo/issues/18501
authorneo-opus-vega
commentsCount0
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[x] 18460 Simplify Workstation vessel integration around existing Neo owners
closedAtSep 9, 2026, 11:56 AM

Three consumers hand-roll a native route check weaker than Main's

Closed v13.2.0/archive-v13-2-0-chunk-20 enhancementairefactoringarchitecturecoredashboard
neo-opus-vega
neo-opus-vega commented on Sep 8, 2026, 11:52 PM

Context

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:

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 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) ADR 0029 §2.8.5 App-Worker resolution step; src/manager/Window.mjs:151 fail-closed capability defaults 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 Refuse. Never return a route on partial knowledge Method JSDoc + @summary; ADR 0029 §2.8.5 cited as upstream contract New unit spec per refusal axis
Main.mjs:783 #getNativeWindowRoute src/Main.mjs:783, 8 call sites Unchanged. Stays private, stays the main-thread authority, stays the last word n/a n/a Existing native arms stay green
apps/workstation/view/Workspace.mjs 10 guard sites current source Replaced by the predicate Same refusal, one implementation — Workstation unit + NL tiers green unchanged
examples/dashboard/crossWindow/DemoBWorkspace.mjs 6 guard sites current source Replaced by the predicate as above — DemoB unit + matrix arms green unchanged
src/ai/client/RuntimeService.mjs 3 guard sites :316, :370, :412 Replaced by the predicate as above — RuntimeService specs green unchanged

Decision Record impact

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.
  • src/dashboard/dock/window/Placement.mjs's route-identity comparison.
  • 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.
  • Own-assignment sweep: #18460, #18027, #17918, #16212; none equivalent.
  • 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.

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