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) {
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
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
Context
Surfaced reviewing PR #17584 (#17583), the activation of the guest seat
@neo-previewas 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
familyandmodelFamilyare'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:
AGENTS.md§swarm_topology_anchor) — "≥ 2 active families with signal AND ≥ 1 non-author family[GRADUATION_APPROVED]".ai/services/graph/agentFamilyResolution.mjsresolves 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 ofai/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:
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.mjscarriesmodelFamily: 'unknown'alongside real values like'claude','gpt','kimi','gemini';resolveResidentFamilyhands 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:
resolveResidentFamilyByIdalready returnsundefined— 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.AGENT:<family>/<model>mailbox alias resolves "only when exactly one AgentIdentity matches thatmodelFamily" (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)returningfalsewhen 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:
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
modelFamilystrings directly.resolveResidentFamily's contract states what an undisclosed family returns and how consumers must treat it, so the next consumer cannot reintroduce a bare===.'unknown'for both sides and compares them equal.Out of Scope
Avoided Traps
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 fieldOrigin Session ID: 1b0d28eb-3461-40b6-bb35-88d6bf09ec94