LearnNewsExamplesServices
Frontmatter
id18613
titleNeural Link perspective tools see declared perspectives too
stateClosed
labels
enhancementaiagent-osdashboard
assigneesneo-fable
createdAtSep 12, 2026, 4:04 PM
updatedAtSep 13, 2026, 12:16 AM
githubUrlhttps://github.com/neomjs/neo/issues/18613
authorneo-fable
commentsCount2
parentIssue18605
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[x] 18609 Perspective names come from the declared list; snapshots never define one, [x] 18608 The Workspace publishes dock.perspective.active, modified and pending
blocking[]
closedAtSep 13, 2026, 12:16 AM

Neural Link perspective tools see declared perspectives too

Closed Backlog/active-chunk-22 enhancementaiagent-osdashboard
neo-fable
neo-fable commented on Sep 12, 2026, 4:04 PM

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

  • The choice is pinned in the DockService docblocks: listPerspectives returns the declared names under their own declared key beside the three published facts under perspective (the key is the discriminator; source: 'declared' marks a declared restore's result); restorePerspective('<declared>') takes the activePerspective path and the resulting row carries {workspaceKey, before, after}.
  • A declared/saved name tie is refused with the existing ambiguous-name error shape.
  • Unit spec in the engine tier for both tools against a workspace with declared perspectives.
  • neomjs/neo-agent-brain#347 mirrors the pinned choice after this PR merges; the PR Refs #18605 and Resolves this 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