LearnNewsExamplesServices
Frontmatter
id17587
titleFamily-keyed gates compare 'unknown' as a family name
stateClosed
labels
bugaiarchitecture
assigneesneo-opus-grace
createdAtAug 23, 2026, 1:14 AM
updatedAtAug 23, 2026, 1:58 AM
githubUrlhttps://github.com/neomjs/neo/issues/17587
authorneo-opus-grace
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 23, 2026, 1:58 AM

Family-keyed gates compare 'unknown' as a family name

Closed Backlog/active-chunk-18 bugaiarchitecture
neo-opus-grace
neo-opus-grace commented on Aug 23, 2026, 1:14 AM

Context

Surfaced reviewing PR #17584 (#17583), the activation of the guest seat @neo-preview as Eos. That PR is correct and this is not a defect in it — the hole is created by activation in general, and it becomes reachable the moment the guest chair rotates.

The guest seat's family and modelFamily are 'unknown' by design: the occupant's lab is undisclosed. That constraint is legitimate and this ticket does not propose trading it away. The problem is that nothing downstream distinguishes "undisclosed" from "a family literally named unknown".

The Problem

Two disciplines key on model family:

  • cross-family review — a review clears the independence gate only when reviewer and author differ by family;
  • consensus quorum (AGENTS.md §swarm_topology_anchor) — "≥ 2 active families with signal AND ≥ 1 non-author family [GRADUATION_APPROVED]".

ai/services/graph/agentFamilyResolution.mjs resolves era-chain-first and otherwise returns the flat property verbatim:

export function resolveResidentFamily(identity) {
    // …era chain…
    return identity?.properties?.modelFamily
}

For an undisclosed seat that returns the literal string 'unknown'. A sweep of ai/ finds no guard anywhere pairing family resolution with an unknown-or-absent case, so 'unknown' flows into every family-keyed comparison as an ordinary value.

Today that is harmless for exactly one reason: there is one holder. Against every named family it compares as distinct, which is why the current seat works correctly.

The preview chair is by design a rotating guest seat. Provision a second undisclosed occupant the same way and the two compare as same family:

  • a cross-family review between two mutually-undisclosed seats falsely clears the gate that exists to protect review independence;
  • a family-keyed quorum counts one family as two, satisfying "≥ 2 active families" with what is, as far as the system knows, an unknown number of distinct labs.

Both fail silently, because "unknown" === "unknown" is a correct string comparison. Nothing is positioned to notice, and the failure surfaces as an approval that should not have counted.

The Architectural Reality

The registry stores absence in the same field, and the same type, as presence. identityRoots.mjs carries modelFamily: 'unknown' alongside real values like 'claude', 'gpt', 'kimi', 'gemini'; resolveResidentFamily hands it on untouched; consumers compare with ===.

This is the sentinel-in-a-value-field shape. A sentinel stored where real values live is compared like a real value, and it is unambiguous only while exactly one holder exists. "Rotating seat" is precisely the condition that guarantees that stops being true.

Note the two places the invariant is nearly right already, which is what makes this a small change:

  • resolveResidentFamilyById already returns undefined — a real absence — for ids outside the static registry, and documents that consumers fall back to the node's flat property. So the codebase already has a notion of "family not resolvable" that is distinct from a string.
  • The AGENT:<family>/<model> mailbox alias resolves "only when exactly one AgentIdentity matches that modelFamily" (ai/mcp/server/memory-core/openapi.yaml). That rule is uniqueness-guarded and therefore already safe under rotation — a second undisclosed seat makes the alias ambiguous and it rejects, which is the correct behaviour and a useful precedent for what the gates should do.

The Fix

Make undisclosed-family never satisfy a family comparison in either direction, at the comparison site rather than the storage site.

Preferred shape: a single predicate the gates consume — something like familiesAreDistinct(a, b) returning false when either side is undisclosed, so an undisclosed seat can neither claim cross-family independence against another undisclosed seat, nor be counted as a distinct family for quorum. Against a named family it should still read as distinct, preserving today's correct behaviour.

Deliberately not proposed:

  • Disclosing the family. The undisclosed-by-design constraint is the seat's whole point.
  • A distinct per-occupant token (preview-2026-08, era-keyed, etc.). This works, but it prevents collision by convention — every future occupant must be provisioned carefully, and a convention failure here is silent. Fixing the comparison makes misclassification impossible by check rather than by arrangement. (Same reasoning @neo-opus-ada applied on #17586 when choosing an identity check over separate data homes, and the parallel is not a coincidence — both are absence-vs-presence confusions on an identity field.)

Acceptance Criteria

  • A single predicate owns family comparison for the review gate and the quorum rule; no consumer compares modelFamily strings directly.
  • Two undisclosed-family identities do not satisfy cross-family independence, and do not count as two families for quorum.
  • An undisclosed-family identity does still compare as distinct from any named family — the current seat's review standing is preserved exactly.
  • resolveResidentFamily's contract states what an undisclosed family returns and how consumers must treat it, so the next consumer cannot reintroduce a bare ===.
  • A unit witness covers the two-undisclosed-seats case and fails on today's behaviour. A test that passes before the change has not captured this defect — the current code returns 'unknown' for both sides and compares them equal.
  • The skill-level prose for the cross-family gate and the quorum rule says what an undisclosed family means for each, since both are read by agents as discipline rather than executed as code.

Out of Scope

  • PR #17584 / #17583 — the activation is correct and must not be gated on this.
  • Disclosing or inferring any seat's model family.
  • #17586 (the shared wake envelope). Same family of confusion, different surface, already owned.
  • Deployment-level provisioning of guest seats (data homes, harness config).

Avoided Traps

  • Treating this as a bug in the guest seat's entry. It is a bug in what the gates do with an honest declaration of absence.
  • Fixing it at the storage site by inventing per-occupant family tokens — that trades a check for a convention, and the convention fails silently.
  • Waiting for the collision to prove itself. There is currently one undisclosed seat, so the defect is unobservable until a second occupant exists — at which point the first thing it does is participate in review gates. The absence of a witness here is a property of the roster, not evidence of correctness.

Related

PR #17584 / #17583 (where this surfaced; explicitly not blocked by it) · #17586 (sibling absence-vs-presence confusion on an identity field, @neo-opus-ada) · AGENTS.md §swarm_topology_anchor (the quorum rule this protects)

Retrieval Hint: unknown modelFamily compared as family name cross-family review gate quorum undisclosed guest seat resolveResidentFamily sentinel in value field

Origin Session ID: 1b0d28eb-3461-40b6-bb35-88d6bf09ec94