LearnNewsExamplesServices
Frontmatter
id16824
titleScoped-empty roster: 0 agents shared with you is not a dead plane
stateOpen
labels
enhancementaitestingarchitecturesecurityagent-os
assignees[]
createdAtAug 9, 2026, 7:56 PM
updatedAtAug 24, 2026, 11:57 PM
githubUrlhttps://github.com/neomjs/neo/issues/16824
authorneo-kimi-phoebe
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]

Scoped-empty roster: 0 agents shared with you is not a dead plane

Open Backlog/active-chunk-14 enhancementaitestingarchitecturesecurityagent-os
neo-kimi-phoebe
neo-kimi-phoebe commented on Aug 9, 2026, 7:56 PM

Split from #16744 (its AC-2), the OQ8 empty-state trap — graduated scope of D#16720 (body v12 @ 2026-08-08T19:52:47Z). Split reason: every other #16744 AC is a render-layer deliverable; THIS one needs a wire projection that does not exist yet — the roster read result is {rows} only (apps/agentos/view/fleet/FleetCockpit.mjs:2264 destructures rows alone), so the viewer cannot know the plane-side count the honest line requires. Filing it separately keeps #16744's PR one-PR-resolvable.

Context

D#16720's OQ8: the default-private roster's empty state must be distinguishable from dead-plane emptiness (June RLS anchor). A plane that answers "0 agents shared with you" while N operators are present must render "plane alive · N operators present · 0 agents shared with you · request access" — the operator action (request access) differs categorically from both "start the server" and "define agents".

The Problem

Today the only answered-empty reason is 'server connected · fleet registry empty — define agents to go live' (FleetCockpit.mjs:2297). For an access-scoped empty that line is a LIE in the actionable direction: it tells the viewer to define agents on a plane that HAS agents they cannot see. The viewer literally cannot distinguish "empty plane" from "plane that shares nothing with me" — the wire does not carry the count.

The Architectural Reality

  • Wire schema: ai/services/fleet/fleetWireMethods.mjs with the browser twin apps/agentos/config/fleetWireMethods.mjs — parity-linted twin binding (husky pre-commit lint-fleet-vocabulary-parity).
  • Roster read path: FleetCockpit.mjs loadRoster (:2240–:2330) — the answered-empty branch at :2289–:2299 is where the two empty kinds must part.
  • Render: gridDegradedReasonderiveSpineBanner retained-reason discipline (shipped in PR #16721) — the new reason rides it, no fork.
  • Sibling precedent for the two-substrate shape: PR #16721 (merged).
  • BLOCKED ON #16737 (viewer-scoped roster projection, @neo-fable-clio). Not merely a producer dependency — the scoped-empty STATE is currently unreachable: FleetRegistryService.listAgents() returns all agent definitions, so nothing filters rows per viewer and rows: [] can only mean the registry is empty. This ticket becomes implementable when #16737 lands, and its wire row is the contract that projection should meet.

The Fix

  1. Wire: the roster read success result MAY carry the empty CAUSE, authored by the scoping authority — never a bare liveness count. Superseded design, kept for the record: presence.operatorsPresent: Number was prescribed here and falsified in PR #17736 review. Operator presence and roster emptiness are independent axes: rows: [] with operatorsPresent: 0 serialises both a plane with hidden definitions and nobody online AND a plane with no definitions at all, and the consumer must select opposite actions for them. A positive count proves operators are present, never that a definition exists the viewer cannot see. The privacy boundary from the original design SURVIVES — whatever the field carries is a cause or a count, never identities (a bare-private boundary this feature serves). Twin-bound both sides + parity-lint fixtures.
  2. Cockpit: loadRoster's answered-empty path distinguishes registry-truly-empty from scoped-empty and writes the matching reason; the banner renders it through the existing retained-reason channel.
  3. Tests: unit matrix rows in the spineBanner.spec.mjs idiom + the cockpit empty-path spec (fleetCockpit.spec.mjs).

Contract Ledger Matrix

Target Surface Source of Authority Proposed Behavior Fallback Docs Evidence
fleetRoster wire result shape #16737's scoping authority (result shape lives in createFleetCockpitStatus; the parity-linted wire twins bind method NAMES only, and fleetRoster is already in both) success result MAY carry the empty CAUSE — presence.operatorsPresent: Number struck: operator presence is a different axis from roster emptiness (PR #17736 review) field absent → current "registry empty" line, unchanged module JSDoc unit matrix + a production writer, which is the check PR #17736 lacked

Decision Record impact

none — inside D#16720's graduated scope; no ADR touched.

Decision Record

Optional: D#16720 (graduation authority anchor; signal ledger on #16744).

Acceptance Criteria

  • Roster read result can disclose the empty CAUSE, authored by #16737's scoping authority — never inferred from a sibling liveness axis, and never identities. (Note: the parity-linted wire twins bind method NAMES; fleetRoster is already in both. The result shape lives in createFleetCockpitStatus.)
  • Scoped-empty renders "plane alive · N operators present · 0 agents shared with you · request access" — distinguishable from BOTH dead-plane emptiness and registry-truly-empty.
  • Absent the field, the current answered-empty line is byte-identical (no regression for pre-projection planes).
  • Unit coverage in the spineBanner matrix idiom + the cockpit empty-path spec.

Out of Scope

  • The connection-axis banner vocabulary (connecting / refused / slow / unreachable / failed-upstream) — lands under #16744's PR.
  • The request-access FLOW itself (grant UX = #16745's sharing pane).
  • #16737's projection implementation itself — this ticket consumes it. It is now recorded as the BLOCKER above rather than as out-of-scope background, because without it the state this ticket renders cannot occur.

Avoided Traps

  • Rendering 0-shared as "registry empty — define agents" (the current line) is the lie this ticket exists to kill.
  • Disclosing identities instead of a count inverts the default-private boundary — count only.

Related

  • #16744 (parent scope; its AC-2 moves here)
  • #16745 (sharing pane — the "request access" destination)
  • PR #16721 (shipped sibling, same substrate pair)
  • D#16720 (graduated source, OQ8)

Live latest-open sweep: checked latest 20 open issues at 2026-08-09T17:53Z — no equivalent found. A2A in-flight sweep: 48-message mailbox scan over the 11:04Z–17:40Z window — no claim on this scope. KB semantic sweep: no duplicate (nearest: shipped ancestors #14560, #15284).

Retrieval Hint: "scoped-empty roster operatorsPresent default-private OQ8 D#16720"


Truth-synced 2026-08-24 after PR #17736 was dropped. That PR implemented the consumer half of the operatorsPresent design above; @neo-gpt-emmy's review established two blocking facts — no production composer writes the field, and operator-presence is the wrong axis for classifying roster emptiness. Verified both, then closed the PR rather than repairing it: repairing meant inventing a contract for a producer that does not exist and shipping a shape #16737 would have to work around.

The ticket stays OPEN and is now honestly blocked. Unassigned so whoever picks up #16737 can carry it, or take it after. The problem statement is unchanged and still correct — 0 agents shared with you is not a dead plane — only the mechanism was wrong.