Summary
conversation.md binds a host advertising multiPartyConversation to accept a roster, refuse a turn from outside it, and refuse an oversized roster. It never says where the roster comes from or what a refusal looks like, because v2 has no client route that opens a conversation. The rules are witnessed only by the v1 seam POST /v1/host/sample/conversation/multi-party/{open,exchange}. This RFC names the input (the core.conversationGate config) and registers the two refusal codes. A turn arrives as the resume value of a conversation.exchange interrupt, which is already a normative operation, so the suite can witness both rules with no seam.
Motivation
Three MUSTs with no input and no refusal shape. spec/v2/core/conversation.md §multiPartyConversation:
- "MUST accept an optional
participantsarray of agent references on conversation creation"; - "MUST refuse a turn from a principal absent from that roster";
- "MUST refuse, at creation, a roster that exceeds
multiPartyConversation.maxParticipants".
"Conversation creation" is not an operation (§Why this exists: "No client route opens a conversation"), and "refuse" names no code or status. RFC 0101 left the status open ("status-tolerant 400/422", §E).
The only witness is a v1 seam. multi-party-conversation-behavioral.test.ts drives /v1/host/sample/conversation/multi-party/{open,exchange}. v1 reached end-of-support on 2026-10-04 and v2 mounts no such seam, so conformance.md §Witness class requires a normative path or demotion. The family is declared witnessable-gated, which nothing currently makes true.
The input already exists in two halves. A conversation is opened by a core.conversationGate node, and every turn is the resume value of a conversation.exchange interrupt (interrupt.md §Payload). Both are reachable from outside: a workflow fixture carries the config, and resolveInterrupt carries the turn.
Proposal
§A. The roster input
core.conversationGate config gains an optional member:
"config": { "participants": [ { "agentId": "host:council-a" }, { "agentId": "host:council-b" } ] }
participantsis an array ofAgentRef(schemas/v2/agent-ref.schema.json), at least two entries, eachagentIdunique.- A host advertising
multiPartyConversationMUST carry the configured roster, unchanged, on that conversation'sconversation.opened.participants. - Absent
participants, the conversation has no roster and §B does not apply.
§B. A turn from outside the roster
- The turn is the resume value of the conversation's
conversation.exchangeinterrupt. A turn withrole: "agent"names its speaker inspeakerId. - When the conversation has a roster, a host MUST refuse a turn whose
speakerIdis not a rosteragentIdwith422 conversation_speaker_not_participant,details.speakerIdnaming it. - A refused turn MUST NOT be consumed: the interrupt stays open, no
conversation.exchangedis emitted, and a later valid resume still resolves it.
§C. An oversized roster
- A host advertising
multiPartyConversation.maxParticipantsMUST refuse a workflow whosecore.conversationGateparticipantsexceeds it with422 conversation_roster_exceeded,details.maxParticipantscarrying the ceiling. It refuses at registration or at run creation, likecapability_required(runs.md§conversationPrimitive). - It MUST NOT truncate the roster or open the conversation.
§D. Error codes
Two rows in spec/v2/errors.json, both 422, retriable: false:
| Code | Raised when | details |
|---|---|---|
conversation_speaker_not_participant | §B.2 | { speakerId } |
conversation_roster_exceeded | §C.1 | { maxParticipants } |
§E. Fixtures
conformance-multi-party-council: onecore.conversationGatewithmockAutoResume: falseand a three-member rosterhost:conformance-council-a,-b,-c. A host advertises it in discoveryfixtureswhen it can run it, its roster ids resolving as the host decides (RFC 0003).conformance-multi-party-council-oversize: the same gate with a 64-member roster. It is meaningful only for a host advertisingmaxParticipantsbelow 64.
§F. Conformance
A new scenario, v2-multi-party-council, gated on multiPartyConversation and the advertised fixture:
- roster-carried (
openwop.requirement.0239.council.roster-carried): run the council;conversation.opened.participantsequals the configured roster. - speaker-refused (
openwop.requirement.0239.council.speaker-refused): resume the exchange with an agent turn whosespeakerIdishost:conformance-intruder. Expect422 conversation_speaker_not_participant, the interrupt still open, and noconversation.exchanged. Then a turn fromhost:conformance-council-aresolves it (the control). - roster-exceeded (
openwop.requirement.0239.council.roster-exceeded): withmaxParticipantsbelow 64, creating a run of the oversize fixture is refused422 conversation_roster_exceeded, and no run opens a conversation.
Dispositions: no multiPartyConversation or fixture not advertised ⇒ inapplicable. Leg 3 is inapplicable when maxParticipants is absent or 64 or more, with the reason.
Falsifiability — one row per normative requirement
| Requirement | Observable — what an outside party sees | Who can cause the condition | Verdict |
|---|---|---|---|
§A.2 the configured roster is carried on conversation.opened — openwop.requirement.0239.council.roster-carried | the event's participants | the suite, running the advertised fixture | witnessable — executed-pass required on a host bundle |
§B.2–§B.3 a non-member turn is refused unconsumed — openwop.requirement.0239.council.speaker-refused | the 422 code, the open interrupt, no conversation.exchanged, then a member turn resolving it | the suite, unaided | witnessable — executed-pass required on a host bundle |
§C an oversized roster is refused unopened — openwop.requirement.0239.council.roster-exceeded | the 422 code at run creation | the suite, running the oversize fixture, when maxParticipants is below 64 | witnessable — gated (a host bounding at 64 or more cannot be shown an oversize fixture without workflow registration) |
Compatibility
additive.
participantsis optional, so a gate without it is unchanged.- The two codes refine refusals the MUSTs already required; RFC 0101 had left the status open between
400and422. - A host that refused with
400 validation_errornow returns a registered422code. That is a suite-visible change under COMPATIBILITY.md §2.3, not a wire break.
Alternatives considered
- Demote the three MUSTs. Rejected: speaking rights are the point of a council.
- A conversation-open operation. It would let the suite size the roster dynamically. Rejected: v2 opens conversations from workflows by design, and a new operation is a larger surface than a config field.
- Reuse
400 validation_error. Rejected: a resume value that validates againstresumeSchemais not malformed, and a client should be able to tell "you are not a member" from "your turn is malformed". - A per-host oversize fixture. It would cover every host. Rejected: a fixture cannot depend on the host reading it, and v2 has no workflow registration through which the suite could build one.
Decisions
An /architect review decided these on 2026-10-06.
- Roster in node config, not run input: the roster is part of the workflow's design, replays with it, and needs no new run field.
- Refusal status
422with two registered codes (§D). - A refused turn is not consumed (§B.3), so a client can retry, and replay never sees a refused value.
- The oversize leg is gated on
maxParticipantsbelow 64 (§F; the fixture size openwop-5e proposed, which covers far more hosts than nine). No schema ceiling exists to guarantee the fixture exceeds it. - Roster ids resolve as the host decides. The host advertises the fixture only when it can run it (RFC 0003), so the suite never requires particular agents to exist.
Acceptance criteria
- [x]
Active: the comment window was waived on the record (maintainer, 2026-10-06; MAINTAINERS.md) with §A–§D unchanged in substance. - [x] Both codes are in
spec/v2/errors.json(and so the generated error envelope); both fixtures are catalogued;conversation.mdcarries §A–§C;CHANGELOG.mdrecords it. v2 has no core-node config schema, soparticipantsis specified inconversation.mdand the fixtures. - [x]
v2-multi-party-councilships in 2.45.23 overlib/council-roster-witness.ts. Each leg fails on its sabotage in the self-test double (9 cases): a roster dropped fromconversation.opened, a non-member turn accepted, a refused turn consumed, a refusal withvalidation_error, an oversized roster truncated. NomaxParticipants, one of 64 or more, or unadvertised fixtures recordinapplicable. - [x]
Accepted: a host advertisingmultiPartyConversationand the fixture records the threeopenwop.requirement.0239.idsexecuted-pass, or roster-exceededinapplicablewith its reason, on a certified bundle. (2026-10-07: the v2 reference host's certified public cut on published suite 2.45.23, buildcommit:1492562f. All three areexecuted-pass; the host advertisesmaxParticipants8, so the oversize leg ran.)*
References
spec/v2/core/conversation.md§multiPartyConversation;spec/v2/core/interrupt.md§Payload;spec/v2/core/runs.md§conversationPrimitive;spec/v2/core/conformance.md§Witness class.- RFC 0005 (conversation primitive), RFC 0101 (multi-party council), RFC 0003 (fixture advertisement), RFC 0234 (v1 end-of-support), RFC 0238 (the observation-path pattern).
conformance/src/scenarios/multi-party-conversation-behavioral.test.ts;schemas/v2/conversation-event.schema.json.- Registers:
gaps,risks.