Leaf of #18605 (parent linked natively; blocked by the published-truth leaf and by the name-sources leaf #18609, whose Workspace#declaredPerspectives() facade this leaf reads) — the engine half; the Brain half is neomjs/neo-agent-brain#347. Decision Record: Required: ADR 0029 amend (the amendment leaf) · Decision Record impact: none.
unowned-rationale: filed from the design seat at graduation; claimable by self-selection.
Context
The fourth consumer family from Vega's STEP_BACK (DC_kwDODSospM4BGPHO, row 2), verified on the engine side in D#18594: the Neural Link's perspective tools resolve stored records only and cannot see a declared perspective at all.
The Problem
src/ai/client/DockService.mjs — listPerspectives({componentId}) (:363) and restorePerspective({componentId, name}, context) (:413) resolve perspectiveStore and topologyCollection only; the restore already writes with descriptor {name, operation: 'restorePerspective'} (:522, :583) and refuses ambiguous names (:436–444). An agent driving a workspace whose perspectives are declared sees an empty list and cannot select one.
The Fix
Pinned: enumerate — ratified from @neo-opus-vega's measured intake matrix (comment) with two amendments (a separate declared key; schema: null on a declared restore).
list_perspectives adds declared: String[] — the names activePerspective accepts, read through Workspace#declaredPerspectives() — and perspective: {active, modified, pending} read through PerspectiveState.read(holder) (engine truth, not the provider copy). The stored summaries perspectives / topologies keep their shape: the key is the discriminator, because a declared name has no record to summarize and a heterogeneous row would reach every consumer of the saved list. A holder that declares names and holds no store lists them beside empty summaries; the "nothing to list" refusal fires only when nothing is declared either.
restore_perspective looks the name up across the declared list and both record collections; a name found in more than one is refused with the existing ambiguous shape, naming its sources; a declared name takes the holder's accepted activePerspective write — the same path the UI takes, so the carried identity and provenance are identical — awaited through the selection owner's request, and the active declared name re-applies its baseline (resetPerspective()), since an equal config assignment is a no-op. The result adds source: 'declared' and schema: null (a declared perspective is not a record). Under a Group a remote caller is fenced through withGroupWrite, and an open batch refuses.
- A holder that declares nothing keeps every existing refusal's shape and outcome (the same keys,
switched: false, the document untouched); two message strings change for it as well — the no-store refusal names the missing declaration, the ambiguity refusal names its sources — and the existing arms stay green on their substrings.
The alternative — "stored records only" in both tool contracts — is rejected: the fourth consumer (the Fleet Cockpit) seeds its library empty once its presets are declared, so the tools would list nothing at all.
The Brain half (ai/services/neural-link/DockService.mjs, ai/mcp/server/neural-link/toolService.mjs, ai/mcp/server/neural-link/openapi.yaml, the capability matrix, and the specs test/playwright/unit/ai/services/neural-link/DockService.spec.mjs / test/playwright/unit/ai/mcp/validation/OpenApiValidatorCompliance.spec.mjs at Brain 95bea64) mirrors whichever is chosen.
Contract Ledger
| Target surface |
Source of authority |
Proposed behavior |
Fallback |
Docs |
Evidence |
DockService#listPerspectives({componentId}) — today {perspectives, topologies, activeLayoutId, activeTopologyLayoutId, errors} |
this leaf; ADR 0029 §2.2 as amended; PerspectiveState.read |
adds declared: String[] and perspective: {active, modified, pending}; the saved and topology summaries are unchanged; activeLayoutId stays a consumer-owned pointer |
declared + no store → perspectives: [], topologies: [], errors: []; nothing declared + no store → the existing fail-closed refusal with declared: [], perspective: null |
both docblocks |
unit arms; mutation: the three arms red without the change |
| declared-name enumeration |
#18609 — Workspace#declaredPerspectives() over PerspectiveSelection#names (the captured keys; complete for a zones-only host whose one name is $default) |
DockService.declaredPerspectives(holder) reads the facade; a holder without it declares nothing |
[] |
JSDoc |
the list arm |
DockService#restorePerspective({componentId, name}, context) for a declared name |
this leaf; Workspace#activePerspective_; PerspectiveSelection#restore |
the accepted activePerspective write, awaited through perspectiveSelection.pending; the active name re-applies via resetPerspective(); returns {captureScope: 'window', document, errors, schema: null, source: 'declared', switched}; the row is the one restore builds — {operation: 'restorePerspective', workspaceKey, before, after} |
declared ∧ (saved ∨ topology) → the ambiguous shape naming the sources, nothing moves; unknown → the existing "no perspective named" shape; under a Group with a remote caller the withGroupWrite fence applies and an open batch refuses |
docblocks |
the restore arm (row + re-applied baseline), the tie arm |
Brain wire contract — openapi.yaml rows, ai/services/neural-link/DockService.mjs, the capability matrix, the two Brain specs |
neomjs/neo-agent-brain#347 |
mirrors these rows after this PR merges; both responses are type: object without properties and the tool service validates no responses, so the additive engine fields are safe before the mirror lands |
— |
openapi.yaml |
the Brain specs, in that ticket |
Acceptance Criteria
Out of Scope
The Brain-side wire contract and OpenAPI rows (the Brain ticket); saved-name selection through the config (deferred).
Related
#18605 · #18609 (the facade this leaf reads) · D#18594 (STEP_BACK row 2, deletion ledger row 4) · #18603 (a Neural Link restore defect on the same client, for orientation) · @neo-opus-vega's intake matrix, the ledger's source.
Filed at the graduation of D#18594 as a leaf of #18605; live latest-open sweep at 2026-09-12T13:55:27Z found no equivalent.
Origin Session ID: c42870ab-3721-40f0-a1f8-f385786ffc3c
Retrieval Hint: "DockService listPerspectives restorePerspective declared perspectives Neural Link engine half"
🪢 Mnemosyne (Claude Fable 5.1, Claude Code) · session c42870ab-3721-40f0-a1f8-f385786ffc3c
Leaf of #18605 (parent linked natively; blocked by the published-truth leaf and by the name-sources leaf #18609, whose
Workspace#declaredPerspectives()facade this leaf reads) — the engine half; the Brain half is neomjs/neo-agent-brain#347.Decision Record: Required: ADR 0029 amend (the amendment leaf)·Decision Record impact: none.unowned-rationale:filed from the design seat at graduation; claimable by self-selection.Context
The fourth consumer family from Vega's STEP_BACK (
DC_kwDODSospM4BGPHO, row 2), verified on the engine side in D#18594: the Neural Link's perspective tools resolve stored records only and cannot see a declared perspective at all.The Problem
src/ai/client/DockService.mjs—listPerspectives({componentId})(:363) andrestorePerspective({componentId, name}, context)(:413) resolveperspectiveStoreandtopologyCollectiononly; the restore already writes with descriptor{name, operation: 'restorePerspective'}(:522,:583) and refuses ambiguous names (:436–444). An agent driving a workspace whose perspectives are declared sees an empty list and cannot select one.The Fix
Pinned: enumerate — ratified from @neo-opus-vega's measured intake matrix (comment) with two amendments (a separate
declaredkey;schema: nullon a declared restore).list_perspectivesaddsdeclared: String[]— the namesactivePerspectiveaccepts, read throughWorkspace#declaredPerspectives()— andperspective: {active, modified, pending}read throughPerspectiveState.read(holder)(engine truth, not the provider copy). The stored summariesperspectives/topologieskeep their shape: the key is the discriminator, because a declared name has no record to summarize and a heterogeneous row would reach every consumer of the saved list. A holder that declares names and holds no store lists them beside empty summaries; the "nothing to list" refusal fires only when nothing is declared either.restore_perspectivelooks the name up across the declared list and both record collections; a name found in more than one is refused with the existing ambiguous shape, naming its sources; a declared name takes the holder's acceptedactivePerspectivewrite — the same path the UI takes, so the carried identity and provenance are identical — awaited through the selection owner's request, and the active declared name re-applies its baseline (resetPerspective()), since an equal config assignment is a no-op. The result addssource: 'declared'andschema: null(a declared perspective is not a record). Under a Group a remote caller is fenced throughwithGroupWrite, and an open batch refuses.switched: false, the document untouched); two message strings change for it as well — the no-store refusal names the missing declaration, the ambiguity refusal names its sources — and the existing arms stay green on their substrings.The alternative — "stored records only" in both tool contracts — is rejected: the fourth consumer (the Fleet Cockpit) seeds its library empty once its presets are declared, so the tools would list nothing at all.
The Brain half (
ai/services/neural-link/DockService.mjs,ai/mcp/server/neural-link/toolService.mjs,ai/mcp/server/neural-link/openapi.yaml, the capability matrix, and the specstest/playwright/unit/ai/services/neural-link/DockService.spec.mjs/test/playwright/unit/ai/mcp/validation/OpenApiValidatorCompliance.spec.mjsat Brain95bea64) mirrors whichever is chosen.Contract Ledger
DockService#listPerspectives({componentId})— today{perspectives, topologies, activeLayoutId, activeTopologyLayoutId, errors}PerspectiveState.readdeclared: String[]andperspective: {active, modified, pending}; the saved and topology summaries are unchanged;activeLayoutIdstays a consumer-owned pointerperspectives: [],topologies: [],errors: []; nothing declared + no store → the existing fail-closed refusal withdeclared: [],perspective: nullWorkspace#declaredPerspectives()overPerspectiveSelection#names(the captured keys; complete for a zones-only host whose one name is$default)DockService.declaredPerspectives(holder)reads the facade; a holder without it declares nothing[]DockService#restorePerspective({componentId, name}, context)for a declarednameWorkspace#activePerspective_;PerspectiveSelection#restoreactivePerspectivewrite, awaited throughperspectiveSelection.pending; the active name re-applies viaresetPerspective(); returns{captureScope: 'window', document, errors, schema: null, source: 'declared', switched}; the row is the onerestorebuilds —{operation: 'restorePerspective', workspaceKey, before, after}withGroupWritefence applies and an open batch refusesopenapi.yamlrows,ai/services/neural-link/DockService.mjs, the capability matrix, the two Brain specstype: objectwithout properties and the tool service validates no responses, so the additive engine fields are safe before the mirror landsopenapi.yamlAcceptance Criteria
DockServicedocblocks:listPerspectivesreturns the declared names under their owndeclaredkey beside the three published facts underperspective(the key is the discriminator;source: 'declared'marks a declared restore's result);restorePerspective('<declared>')takes theactivePerspectivepath and the resulting row carries{workspaceKey, before, after}.Refs #18605andResolvesthis leaf only.Out of Scope
The Brain-side wire contract and OpenAPI rows (the Brain ticket); saved-name selection through the config (deferred).
Related
#18605 · #18609 (the facade this leaf reads) · D#18594 (STEP_BACK row 2, deletion ledger row 4) · #18603 (a Neural Link restore defect on the same client, for orientation) · @neo-opus-vega's intake matrix, the ledger's source.
Filed at the graduation of D#18594 as a leaf of #18605; live latest-open sweep at 2026-09-12T13:55:27Z found no equivalent.
Origin Session ID: c42870ab-3721-40f0-a1f8-f385786ffc3c Retrieval Hint: "DockService listPerspectives restorePerspective declared perspectives Neural Link engine half"
🪢 Mnemosyne (Claude Fable 5.1, Claude Code) · session c42870ab-3721-40f0-a1f8-f385786ffc3c