Repository navigation
RFC-013 draft: structured refusal for forwarded events - #31
Conversation
A refusal of a push/event or a channels/incoming message gets three optional machine-readable fields, the same on both lanes and in both forms (a refused result, or error.data): `refusal` (a small registry: invalid, not_permitted, unknown_channel, unavailable, too_large, duplicate), `retryable` (may the same occurrence be sent again), and `retryAfterMs`, plus `limitBytes` with too_large. Existing Appendix A codes map onto the registry; two codes are added for the error lane (-32030 Temporarily unavailable, -32031 Content too large), because -32000 can't mean "retry" and -32602 must not mean "too large". Producer rules: resend the same occurrence only when retryable, never sooner than retryAfterMs, within RFC-006 §3.1's window; register and resend once on unknown_channel; send smaller content as a new occurrence on too_large. No answer stays RFC-006 §3.1's. Host rules: classify refusals; never signal a registry condition only in prose or a bare -32000; retryable:true is a promise the resend won't repeat effects. A new top-level host capability member, `eventAdmission`, declares structured refusals and an optional `maxContentBytes` (JCS bytes of one occurrence's content). 12 conformance vectors; open questions listed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
There was a problem hiding this comment.
Automated review
Recommendation: merge and fast-follow. 8 medium severity findings can be fixed after the merge.
Reviewed commit e475f66 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This draft RFC belongs here. It extends the protocol's existing event lanes, takes up an idea SPEC §18.8 already parks, and follows the repo's habit of filing draft RFCs at the root ahead of any SPEC.md edits.
6 reasons with sources
- The project's stated way to change the protocol is through RFC files at the root, and this is a draft RFC filed the same way. The README says SPEC.md is the authoritative text and that RFC files keep the proposal and review history. Earlier drafts such as RFC-006, RFC-007 and RFC-008 sit at the root as Draft RFCs, like this one. (README.md (Status))
- The topic, machine-readable refusals, is already parked in the spec as future work. SPEC lists 'Structured refusal — server_full, retryAfter' as a future item for connection admission. This RFC applies the same idea to forwarded events. (SPEC.md §18.8 (line 2712))
- It extends push events and channels, both core protocol features, and adds no new feature area. Push Events is listed as a core feature. The RFC adds only optional fields to existing results and error data, plus two error codes. (README.md (Features))
- It follows RFC-006's established pattern for host information and its relation to capability grants. Like RFC-006, it adds no
usespath and leaves RFC-002 as the only source of authority. ItseventAdmissionmember is a top-level host member, like RFC-006'seventCoalescing. (RFC-006-event-coalescing.md (header)) - It follows the conformance-vector acceptance practice the project already uses. The RFC requires executable vectors under conformance/ before it moves from Draft to Accepted, which mirrors RFC-005's criterion. (README.md (Status, RFC-005 acceptance criterion))
- It is a single, coherent proposal with no unrelated changes bundled in. The diff adds one file. SPEC.md is left unchanged until the RFC is accepted. (RFC-013-structured-refusal.md (diff))
Findings
8 findings of medium or high severity, see the inline comments:
- Medium severity. -32003 "Unknown feature set" is mapped to not_permitted, but it describes a producer defect.
- Medium severity. unknown_channel resend contradicts the rules for retryable and non-retryable refusals.
- Medium severity. A JSON-RPC error answering a whole channels/incoming request is not covered.
- Medium severity. Occurrence identity is defined by eventId, which is optional on uncoalesced channels/incoming messages.
- Medium severity.
duplicaterefusal conflicts with RFC-006's rule that a coalesced retry returns its original result. - Medium severity. No bound on retries when retryAfterMs is given (including 0) or when an uncoalesced event has no window.
- Medium severity. Host rule 2 is an unconditional MUST that makes existing hosts non-conforming, contradicting the compatibility claims.
- Medium severity. The Appendix B schema closes the
refusalenum, breaking the forward-compatibility rule for unknown values.
8 more findings, less severe
- Low severity, documentation,
RFC-013-structured-refusal.md:91. retryAfterMs meaning is undefined with retryable: false or on kinds that need a new occurrence.retryAfterMsis defined only as a wait before resending the same occurrence. The RFC does not say whether it applies, or is meaningless, whenretryableis false. It also does not say whether a producer sending a reduced new occurrence aftertoo_large, or re-sending afterunknown_channel, must honour it. Implementations will differ on whether{ refusal: "too_large", retryAfterMs: 5000 }delays the smaller resend. The RFC should state its scope. - Low severity, test coverage,
RFC-013-structured-refusal.md:298-300. The size-limit vector skips the exact boundary. Vector 10 tests 1,999,999 and 2,000,001 bytes againstmaxContentBytes: 2000000, but not content of exactly 2,000,000 bytes. That is the one case where an off-by-one (>=versus>) in a host would show up. RFC §6 says the limit is "the largest content the host admits", so exactly 2,000,000 bytes must be admitted, and a vector should say so. - Low severity, test coverage,
RFC-013-structured-refusal.md:270-305. No vectors for several normative rules. The vectors leave several rules untested: a JSON-RPC-32023on a channel-scopedpush/eventmapped tounknown_channelwithoutdata.refusal; a-32030with nodata, which must default to retryable through the code mapping; RFC-006's per-messagereason: "coalesce_invalid", which must read asinvalidwhenrefusalis absent (line 121); and atoo_largerefusal onchannels/incoming. Each is a distinct producer behavior that RFC §5 specifies, and the acceptance criterion freezes only these vectors. - Low severity, documentation,
RFC-013-structured-refusal.md:127. Initial-policy rejection is cited as §6.7; the rule lives in §5.3. Line 127 (and vector 4) describes a host refusing pushes "until its initial policy is established (§6.7)". SPEC §5.3 is the section that says that until the initial policy exchange completes, "a host MUST reject inbound privileged methods". §6.7 coversfeatureSets/updatenegotiation in general. The citation should point to §5.3. - Low severity, documentation,
RFC-013-structured-refusal.md:11-12. Host capability members are cited as §5.1, which is Server Capabilities. The header says the RFC amends "§5.1's list of host capability members". In SPEC.md, §5.1 is "Server Capabilities", and the host'scapabilities.experimental.mcpladvertisement is in §5.2 "Host Support". The amendment target should be §5.2. The §5.1 reference at line 207, for the boolean-shorthand rule, is correct. - Low severity, documentation,
RFC-013-structured-refusal.md:29-30. Summary says "three optional fields" but the RFC defines four. The Summary (and the PR description) says the RFC adds three optional fields. RFC §3, §7 and B.5 define four:refusal,retryable,retryAfterMsandlimitBytes. §7 itself says "add the same four fields". The count should be corrected so readers don't misslimitBytes. - Low severity, documentation,
RFC-013-structured-refusal.md:237. Error-code numbering relies on an RFC-012 and drafts that are not in the repository. The rationale for-32030/-32031leaves-32026..-32029free for "drafts in progress next to RFC-012's proposed-32025". No RFC-012 exists in the checkout, and-32025does not appear in SPEC.md. Readers cannot check this reservation, and there is no record of it to stop collisions. The reserved range should be recorded in SPEC Appendix A, or the reference should be dropped. - Low severity, maintainability,
RFC-013-structured-refusal.md:80. Result-form not_permitted on push/event conflicts with §6.6's error-object requirement. RFC §3 item 1 allows apush/eventresult{ accepted: false, refusal: "not_permitted" }, and the registry places feature-set and capability denial undernot_permitted. SPEC §6.6 requires that when a host rejects for these reasons it "MUST use a JSON-RPC error object, not a result carrying a failure flag". The RFC should say that the result form ofnot_permittedis only for host admission policy, and that authority denials keep their §6.6 error codes.
- -32003 (undeclared feature set) maps to invalid, not not_permitted. - The unknown_channel resend is an explicit exception to the retryable rules (rule 5), so a strict reading no longer forbids it. - An error answering a whole channels/incoming request carries the fields for every message in it. - "Same occurrence" is RFC-006 §3.1's identity (eventId, else messageId); a new occurrence carries a fresh eventId, added when the message had none. A refusal that invites a resend leaves no identity behind (host rule 4). - duplicate is for uncoalesced events only; a coalesced retry keeps RFC-006's original result. - Retries are bounded in every case, with the producer's own floor when retryAfterMs is 0; retryAfterMs covers any further send of the event. - A refusal answered before the initial policy exchange (§5.3) is not a verdict on authority: one resend after it (rule 6). - Authority denials on push/event stay §6.6 error objects; the result form of not_permitted is for a host's own admission policy. - Host rule 2 (no meaning in prose alone) binds only hosts advertising eventAdmission; §8 says what binds a non-advertising host. - B.5/B.6 are JSON Schema; refusal is an open string, its known values in B.4. - Citations: §5.2 (host support), §5.3 (initial policy). "Four" fields. Dropped the unverifiable code-range reservation. - Vectors 13-20 added; vector 10 tests the exact boundary. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Revision 2 (b38758f) answers the review. The 8 medium findings are answered and resolved on their threads. All 8 lower findings held up too:
There's a new "Decisions" list for what revision 2 settles. The open questions are narrowed accordingly. 🤖 Generated with Claude Code |
There was a problem hiding this comment.
Automated review
Recommendation: fix, then merge. 2 high severity findings should be fixed first.
Reviewed commit b38758f with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This draft RFC adds structured, machine-readable refusals for forwarded events, and it follows the way this project changes its protocol: through a root-level RFC that is integrated into SPEC.md only once accepted. It extends features the project already lists (push events, channels) and takes up an idea SPEC.md already parks.
5 reasons with sources
- It is proposed the way the project changes its protocol: as a draft RFC file, with no edits to SPEC.md. The README calls SPEC.md the authoritative integrated text, with RFC files keeping proposal and review history. RFC-006, RFC-007 and RFC-008 are draft RFC files at the root, just like this one. The PR touches only RFC-013-structured-refusal.md and lists its SPEC amendments for after acceptance. (README.md (Status))
- It serves features the project states, push events and channels, by making refusals of them machine-readable. Push Events is the first listed feature. The RFC amends the push/event result (§9.3) and the channels/incoming result (§14.3), and builds on RFC-006's coalescing identity. It does not add a new feature area. (README.md (Features))
- SPEC.md already names structured refusal as deferred work, though for connection admission rather than forwarded events. §18.8 parks "Structured refusal — server_full, retryAfter, queued admission and ready" under endpoint URIs. The RFC keeps clear of that connection-admission case and applies the idea to event admission, so it does not reopen something the spec deferred in that section. (SPEC.md §18.8 Out of scope)
- It leaves authority unchanged: no new
usespath and no new grant, so RFC-002's model is not touched. TheeventAdmissionadvertisement is host information. It is a top-level member, as RFC-006's eventCoalescing is, not a server capability, and authority denials keep their §6.6 error codes. (RFC-002-capability-grants.md / SPEC.md §5.4) - Its acceptance criterion follows the precedent of executable vectors under conformance/. The RFC lists 20 vectors and makes Draft→Accepted wait until they are frozen under conformance/, as RFC-003 and RFC-005 did. The contradictions the code review found in the host rules are for the RFC's review to settle; they do not affect whether the proposal belongs. (README.md (Status, RFC-005 acceptance criterion); conformance/)
Findings
6 findings of medium or high severity, see the inline comments:
- High severity. Host rules 3 and 4 contradict each other on retryable: true.
- High severity. Host rule 4 forbids keeping a messageId that RFC-006 requires for edit detection.
- Medium severity. Pre-policy resend exception covers kinds where a resend is pointless or wrong.
- Medium severity. Pre-policy resend can hit a retained eventId and be recorded as delivered.
- Medium severity. B.6 schema rejects eventAdmission: false.
- Medium severity. Advertisement and rule 8 leave out the legacy coalesce_invalid reason that RFC §4 maps.
3 more findings, less severe
- Low severity, documentation,
RFC-013-structured-refusal.md:356-358. Vector 4 and rule 6 assume a push/event that §5.3 forbids the server to send. SPEC §5.3 says that until the initial policy exchange completes, a server MUST treat every capability-dependent behaviour as unavailable. A conforming producer therefore never sendspush/eventbefore it receivesfeatureSets/update. Rule 6 and vector 4 describe that send as a normal case and reward it with a resend, without saying it is a producer conformance violation. Either cite §5.3 and frame the case as recovery from a non-conforming send, or explain how a conforming producer could get there. - Low severity, test coverage,
RFC-013-structured-refusal.md:374-376. Vector 10 treats a SHOULD as a required outcome. RFC §6 says a host advertisingmaxContentBytes"SHOULD answer content over it withtoo_largeandlimitBytes". Only not refusing content within the limit is a MUST. Vector 10 requires 2,000,001 bytes to be refusedtoo_largewithlimitBytes: 2000000. Its advertisement{ maxContentBytes: 2000000 }also omitsstructuredRefusals, so the host has not declared classified refusals either. A conformant host that refuses differently, or admits oversized content, would fail a vector it does not have to pass. Split the MUST part from the SHOULD part, or mark the over-limit half as advisory. - Low severity, documentation,
RFC-013-structured-refusal.md:264. Claims to amend §14.6 but gives no §14.6 text. The header and the RFC §7 heading both list §14.6 as amended. RFC §7 only gives text for §9.3, §14.3, §9.4, Appendix A and Appendix B. §14.6 is the channel error-code table (-32002/-32017/-32023/-32024). It is unclear whether -32030/-32031 are meant to be added there as well, so an editor applying the RFC cannot tell what to change.
- Host rules 3 and 4 become one rule (host rule 3): with nothing admitted, the host keeps none of the identity the refused occurrence itself introduced, while a messageId known from an admitted occurrence stays, so a resent edit is still an edit; with effects applied, it promises a deduplicated resend or answers accepted. - The before-policy resend (producer rule 6) is framed as recovery for a send §5.3 forbids, and covers only not_permitted (-32001/-32002/-32017); a §5.3 rejection records no identity (new host rule 4: SHOULD, MUST for hosts advertising eventAdmission), so the resend can't meet duplicate. - One definition of a classified refusal (RFC §4) for the advertisement, host rule 2 and producer rule 8, including RFC-006's coalesce_invalid reason and -32600/-32700; a -32000/-32603 from a declaring host reads as no answer. - B.6 accepts eventAdmission: false (no statement). - Vector 10 separates the MUST from the SHOULD; vectors 21-23 added. - §14.6 no longer listed as amended. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Revision 3 (738fe2e) answers the second review. The 2 high and 4 medium findings are answered and resolved on their threads. All three lows held up:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
Automated review
Recommendation: merge and fast-follow. 8 medium severity findings can be fixed after the merge.
Reviewed commit 738fe2e with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This change fits. It is a draft RFC, filed where the project's other RFCs live.
5 reasons with sources
It extends the push-event and channel features the spec already has, and it leaves SPEC.md alone until the RFC is accepted, as the project's RFC process expects.
- It adds a draft RFC at the repository root next to RFCs 001–008, and SPEC.md stays untouched until acceptance. The README says SPEC.md is the authoritative integrated text and that RFC files keep the proposal and review history. RFC-013 is marked Draft and puts the amendments it would make in RFC §7 instead of editing SPEC.md. (README.md (Status))
- It works on features the project already lists: push events and channels. Push Events are a headline feature. The RFC defines refusal fields for the
push/eventresult (§9.3) and thechannels/incomingresult (§14.3), both of which already exist. (README.md (Features)) - The spec has already parked structured refusal as deferred work, though for a different lane. §18.8 lists 'Structured refusal — server_full, retryAfter, queued admission and ready' as deferred, so parked rather than lost. That entry covers connection admission. The RFC applies the same idea to forwarded events and says plainly that this is a separate case. (SPEC.md §18.8 Out of scope)
- It follows the project's way of accepting RFCs: conformance vectors first, then executable vectors before acceptance. RFC-013 §9 lists vectors and requires them to be frozen under conformance/ and run against a host and a producer before it moves from Draft to Accepted. RFC-005 and RFC-003 set this precedent. (README.md (Status, RFC-005 acceptance criterion); conformance/)
- New public surface is kept small and optional, and it follows the pattern the project already uses for host-capability members. The RFC adds two Appendix A codes and a top-level
eventAdmissionmember. It places that member at the top level for the same reason RFC-006 placeseventCoalescingthere, and it adds nousespath and nothing for RFC-002 to gate. Whether the added surface is worth keeping is a normal question to settle in RFC review, not a scope problem. The internal inconsistencies the code review found belong to that review. (RFC-006-event-coalescing.md (eventCoalescing); SPEC.md Appendix A)
Findings
8 findings of medium or high severity, see the inline comments:
- Medium severity. Pre-policy recovery depends on when the refusal arrives, not when the request was sent.
- Medium severity, documentation. The RFC disagrees on which advertisement binds host rule 4.
- Medium severity. §8 says producers keep today's behaviour with legacy hosts, but §5 binds them on mapped codes.
- Medium severity. A refused retry is assumed not admitted, so a later new occurrence can duplicate delivered content.
- Medium severity. The 'effects applied' promise for uncoalesced events has no retention period.
- Medium severity, test coverage. No vector for a
retryableoverride on a permanent kind. - Medium severity, test coverage. No vector for host rule 3's 'effects applied' branch.
- Medium severity, test coverage. No vector for rule 8's
-32000/-32603exception.
2 more findings, less severe
- Low severity, maintainability,
RFC-013-structured-refusal.md:331-332. The schema letslimitBytesbe 0 whilemaxContentBytesmust be at least 1. B.5 setslimitBytestominimum: 0, but B.6 setsmaxContentBytestominimum: 1. Both are measured the same way (RFC §6). AlimitBytes: 0can never be met by any content, including an empty array (its JCS serialization[]is 2 bytes). The two minimums should match, or the schema should say why 0 is meaningful. - Low severity, documentation,
RFC-013-structured-refusal.md:3. The PR description is out of date with the document. The file is "Draft (revision 3)" and has 23 conformance vectors. The PR description still says the RFC is "now revision 2" and that "12 conformance vectors are included". Reviewers working from the description will look at the wrong rule set. Update the description to match revision 3.
- One trigger, the declaring host (structuredRefusals: true or the shorthand), binds host rules 2 and 4 and producer rule 8; an eventAdmission object with only maxContentBytes doesn't. - Producer rule 4's MUST NOT binds a kind the host stated; a kind inferred from a mapped code or coalesce_invalid from a non-declaring host is advice, so today's recoveries stay open, and §8 says so. - The before-policy recovery keys on when the request was sent, not when the refusal arrives. - A refusal of a resend after no answer says nothing about the earlier attempt: no new occurrence follows, the outcome is unknown (rule 7). - An uncoalesced "effects applied" promise holds for at least an hour after the refusal, across restarts, matching producer rule 2's bound. - limitBytes minimum 1, like maxContentBytes. - Vectors 24-27: retryable override on a permanent kind, effects applied, -32000 from a declaring host, refused resend after no answer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Revision 4 (bcda034) answers the third review. The 8 medium findings are answered and resolved on their threads. Both lows held up:
🤖 Generated with Claude Code |
There was a problem hiding this comment.
Automated review
Recommendation: merge and fast-follow. 7 medium severity findings can be fixed after the merge.
Reviewed commit bcda034 with claude-opus-5-5, by reading the code. It did not run the code or look at the checks.
Does this change belong in the project?
It fits the project. This is a draft RFC that extends existing protocol features (push events, channels) in the same form as the project's other RFCs. It leaves SPEC.md alone until the RFC is accepted, which follows the project's stated process.
5 reasons with sources
- It is a new draft RFC file at the repository root, the same place and form the project uses for every other proposal. The README says SPEC.md is the authoritative integrated text and that RFC files keep the history of proposals and reviews. The existing RFC-001 to RFC-008 files sit at the root with a Status / Targets / Depends on header. RFC-013 copies that header and targets Spec 0.5. It says SPEC.md amendments come only after acceptance. (README.md (Status); RFC-006-event-coalescing.md header)
- It refines how hosts refuse push events and channel messages, both of which the project already lists as core features. The README lists Push Events, Capability Grants and channels (§14). The RFC adds optional fields to the existing
push/eventandchannels/incomingresults and to their error data. It does not add a new subsystem. (README.md (Features)) - It adds no authority path and leaves the capability grant model alone, matching how RFC-006 set its scope. RFC-006 says RFC-002 / §5.4 remains the only source of authority and adds no
usespath. RFC-013 uses the same wording. Its neweventAdmissionmember is a top-level host advertisement, like RFC-006'seventCoalescing. (RFC-006-event-coalescing.md (Depends on)) - SPEC §18.8's parked 'structured refusal' item is about connection admission for endpoint URIs, so it neither covers this nor rules it out. §18.8 defers
server_full,retryAfterand queued admission from the URI work. The RFC cites it only as a parallel and does not take on that deferred item, so it does not run against a stated exclusion. (SPEC.md §18.8) - The new error codes and the advertisement member are maintenance cost, but the RFC's acceptance step is where maintainers decide on them. The RFC proposes
-32030/-32031andeventAdmission, which no file uses yet. Like RFC-005, it makes acceptance depend on executable vectors underconformance/. That is the project's existing gate for adding new protocol surface. (README.md (Status); RFC-005 acceptance criterion)
Findings
7 findings of medium or high severity, see the inline comments:
- Medium severity. A whole-batch error applies a permanent kind to every message, including well-formed and permitted ones.
- Medium severity.
-32031/too_largeon a whole batch is undefined against a per-occurrence limit. - Medium severity. Host rule 3's MUST reaches existing hosts through the
-32023mapping. - Medium severity. Pre-policy recovery cutoff ('received featureSets/update') differs from §5.3's 'exchange completes'.
- Medium severity. Rule 4's
duplicatebranch is unreachable becauseduplicatehas noretryabledefault. - Medium severity. Rule 8's
-32000as no answer escapes rule 2's attempt bound for uncoalesced events. - Medium severity, test coverage. No vector for the 'advice' path of a host that does not declare (revision 4's main change).
3 more findings, less severe
- Low severity, documentation,
RFC-013-structured-refusal.md:265-268. The refusal kind for a §5.3 pre-policy rejection is not specified. SPEC §5.3 says a host MUST reject privileged methods before the exchange but names no code. Host rule 2 requires a declaring host to classify every refusal, and producer rule 6 assumes the rejection isnot_permitted. Host rule 4 never says which kind a declaring host should use. If a host picksinvalid, the producer stops for good. If it picksunavailable, the producer may retry before the exchange completes. Host rule 4 should name the kind, for examplenot_permittedor a §5.3-specific value. - Low severity, test coverage,
RFC-013-structured-refusal.md:123. No vector forretryAfterMsgating the channel-registration resend or the reduced new occurrence. RFC §3 definesretryAfterMsas gating any further send of the event: the same occurrence, the resend after registering a channel, or the reduced new occurrence aftertoo_large. Vectors 1, 2, 19 and 24 only exercise resends of the same occurrence. Vectors 5 and 6 (unknown_channel, too_large) carry noretryAfterMs. A vector should show that atoo_largeorunknown_channelrefusal withretryAfterMsdelays the new occurrence or the resend. - Low severity, documentation,
RFC-013-structured-refusal.md:340-351. B.5 schema does not express 'limitBytes only with too_large'. RFC §3 and §7 saylimitBytesappears only withrefusal: "too_large", but B.5 accepts it with anyrefusalor with none. A conformance validator built from Appendix B, the wayconformance/check-bulk-references.mjsextracts B.1, will not catch the misuse. Add adependentSchemas/if-thenconstraint, or say the restriction is not schema-enforced.
| 3. **JSON-RPC error answering `push/event`, or a whole `channels/incoming` request**: | ||
| members of `error.data`. A `data` that is not an object (or absent) carries none of | ||
| them. An error answering a whole `channels/incoming` request refuses every message in | ||
| it, and its fields apply to each message; a producer acting on them treats each message | ||
| by its own identity. |
There was a problem hiding this comment.
Medium severity. A whole-batch error applies a permanent kind to every message, including well-formed and permitted ones.
RFC §3 item 3 says an error answering a whole channels/incoming request refuses every message in it, and its fields apply to each message. Each SPEC §14.3 message carries its own channelId, so one batch can span channels and mix valid and invalid messages. A whole-request -32602, -32017 or -32003 from a declaring host is read as invalid or not_permitted for every message. Producer rule 4 then says MUST NOT resend any of them, so well-formed messages on permitted channels are dropped for good because of one bad neighbour. The RFC should either say that a permanent whole-batch kind does not condemn the other messages (the producer may resend them separately or as new occurrences), or require hosts to refuse per message when the cause belongs to one message.
| | `-32030` | Temporarily unavailable | The host cannot admit the request now; `data: { refusal: "unavailable", retryable: true, retryAfterMs? }` | | ||
| | `-32031` | Content too large | The content exceeds a size the host admits; `data: { refusal: "too_large", retryable: false, limitBytes? }` | | ||
|
|
||
| They exist because `-32000` cannot mean "retry" (RFC §2) and `-32602` must not mean "too | ||
| large" (RFC-006 §13). A host MAY instead answer either condition as a result with | ||
| `accepted: false`. |
There was a problem hiding this comment.
Medium severity. -32031/too_large on a whole batch is undefined against a per-occurrence limit.
limitBytes and maxContentBytes measure one occurrence's content (RFC §3, RFC §6). However, -32031 may answer a whole channels/incoming request, and its fields then apply to each message (RFC §3 item 3, §7). When a batch is refused for size because of its total or frame size, and each message is under the limit, rule 4 tells the producer to shrink every message and send each as a new occurrence. limitBytes would then describe a quantity the RFC never defines. Splitting the batch would have been the right recovery. The RFC should either forbid a whole-request too_large unless one occurrence exceeds the limit, or define a batch-level meaning.
| 3. **What a resend meets.** A host that answers `retryable: true`, `unknown_channel` or | ||
| `too_large` MUST make the resend (or, after `too_large`, the new occurrence) meet what it | ||
| would have met had the refused occurrence never arrived: |
There was a problem hiding this comment.
Medium severity. Host rule 3's MUST reaches existing hosts through the -32023 mapping.
Host rule 3 binds any host that answers unknown_channel, and §8 says a host that does not declare takes it on only 'where it chooses'. RFC §4 maps -32023 to unknown_channel, and today's hosts already send -32023 for unregistered channels (RFC-006 §3.2, SPEC §14.6). So an existing host that deduplicates by eventId before checking the channel becomes non-conforming without opting in. Producer rule 5's resend of the same occurrence would then meet a duplicate, or §9.4 deduplication, and be lost. §8 also calls these mapped codes only 'advice', which contradicts rule 3 binding the host. The RFC should limit rule 3's unknown_channel trigger to a refusal that states the kind, or to declaring hosts.
| A `not_permitted` refusal (including `-32001`, `-32002` and `-32017`) of a request the | ||
| producer **sent** before it had received the host's initial `featureSets/update` is | ||
| §5.3's rejection, not a verdict on authority, whenever the refusal arrives. As an exception to rules 1 and 4, the producer | ||
| MAY send the same occurrence once more after the exchange completes. Other kinds follow |
There was a problem hiding this comment.
Medium severity. Pre-policy recovery cutoff ('received featureSets/update') differs from §5.3's 'exchange completes'.
Producer rule 6 covers only requests sent before the producer 'had received the host's initial featureSets/update'. SPEC §5.3 says the host rejects privileged methods until the exchange completes, which needs the server's response. A producer that sends push/event after receiving the update but before answering it is still rejected under §5.3 (for example with -32002). That send falls outside rule 6, so rule 4 treats the refusal as a real authority verdict and the event is lost. The cutoff should be 'before the exchange completed', which matches the resend condition in the same sentence.
| 4. **Not retryable.** With `retryable: false` the producer MUST NOT send the same | ||
| occurrence unchanged, except as rules 5 and 6 allow, when the host stated the kind: the | ||
| refusal carries `refusal` or `retryable`, or comes from a declaring host (RFC §6). A | ||
| kind inferred from a mapped code or `coalesce_invalid` from any other host is advice: | ||
| the producer SHOULD follow it, and MAY keep a recovery it already uses for that host | ||
| (§8). By kind: | ||
| - `invalid`: stop; this is the producer's defect. | ||
| - `not_permitted`: stop. If authority later changes (§6.7), the producer MAY deliver the | ||
| same underlying content as a new occurrence; this occurrence is not revived. | ||
| - `too_large`: the producer MAY send reduced content (a bulk content reference, §19; a | ||
| shrunken or omitted attachment; truncated text) as a **new** occurrence (RFC §3) with | ||
| the same `coalesce` member. Nothing was admitted, so an `initial` claim stays true. | ||
| When `limitBytes` is given, the new content SHOULD fit under it. | ||
| - `duplicate`: the event was delivered; record it so and send nothing. |
There was a problem hiding this comment.
Medium severity. Rule 4's duplicate branch is unreachable because duplicate has no retryable default.
Producer rule 4 applies only 'with retryable: false', and its duplicate bullet ('record it so and send nothing') sits under that condition. The registry gives duplicate the default '—' (none), so a plain { refusal: "duplicate" } (vector 9) never meets rule 4's condition. Rule 1 does not apply either, since retryable is not true. No rule then tells the producer to record the event as delivered. The RFC also does not say what duplicate with retryable: true means. Either give duplicate a default, or move its handling out of the retryable: false branch.
| 8. **A declaring host's silence.** From a declaring host (RFC §6), an unclassified | ||
| refusal (RFC §4) is non-conforming; the | ||
| producer treats it as `retryable: false` and SHOULD log it. A `-32000` or `-32603` | ||
| error from such a host is the exception: it reads as no answer (rule 7), since a client | ||
| library may raise it on its own. |
There was a problem hiding this comment.
Medium severity. Rule 8's -32000 as no answer escapes rule 2's attempt bound for uncoalesced events.
Rule 8 reads a declaring host's -32000/-32603 as no answer (rule 7), and rule 7 hands that to RFC-006 §3.1 or §9.4. The §5 preamble applies rules 1–4, including rule 2's MUST bound on attempts, only to classified refusals. An uncoalesced event that a declaring host keeps answering with -32000 therefore has no RFC-006 window and no rule-2 bound, so a producer following the text may resend it forever. Rule 7/8 should state that rule 2's bound also applies to resends after no answer.
| refusal carries `refusal` or `retryable`, or comes from a declaring host (RFC §6). A | ||
| kind inferred from a mapped code or `coalesce_invalid` from any other host is advice: | ||
| the producer SHOULD follow it, and MAY keep a recovery it already uses for that host | ||
| (§8). By kind: |
There was a problem hiding this comment.
Medium severity, test coverage. No vector for the 'advice' path of a host that does not declare (revision 4's main change).
Revision 4 makes a kind inferred from a mapped code or coalesce_invalid, from a host that does not declare, only advice: the producer MAY keep an existing recovery, for example resending without inline media after an older host's -32602 for size (§8). None of vectors 1–27 covers this. Vectors 13, 14 and 16 only show the producer following the inferred kind, and vector 23 covers a declaring host. A vector should show that the same -32602/coalesce_invalid from a host that does not declare leaves the reduced resend open, while from a declaring host it does not.
What it proposes
When a host refuses a forwarded event (
push/event, or a message inchannels/incoming), the producer can resend the same occurrence, send something different, fix its own state and resend, or stop. Today the protocol doesn't say which.reasonis free text (§9.3), achannels/incomingresult defines onlyaccepted(§14.3), and the only machine-readable refusals are the Appendix A authority codes and RFC-006's "malformed" answer.RFC-013 (revision 4) adds four optional fields to every such refusal. They are the same on both lanes and in every form: a refused result, a per-message result, or the
dataof an error answeringpush/eventor a wholechannels/incomingrequest.refusal: one of an open registry:invalid,not_permitted,unknown_channel,unavailable,too_large,duplicate.duplicateapplies to uncoalesced events only.retryable: whether the same occurrence (RFC-006's identity:eventId, elsemessageId) can still be admitted unchanged.retryAfterMs: don't send this event again in any form sooner than this.limitBytes: only withtoo_large.Code mapping. Existing codes map onto the registry:
invalid;not_permitted;unknown_channel.Two codes are added for the error lane, because -32000 can't carry "retry" and -32602 must not mean "too large":
A refusal is classified when it carries
refusal, has a mapped code, or is RFC-006'scoalesce_invalidreason.Producer rules:
retryAfterMsor the producer's own backoff floor. Attempts are always bounded, within RFC-006 §3.1's window, or one hour for uncoalesced events.unknown_channel, register the channel and resend once.too_large, send smaller content as a new occurrence.not_permittedrefusal of a request sent before the initial policy (§5.3) gets one resend after it, as recovery.Host rules:
structuredRefusals.retryable: truepromises a deduplicated resend, held for an hour across restarts for uncoalesced events.push/eventstay §6.6 error objects.A new top-level host capability member,
eventAdmission(true,false, or{ structuredRefusals, maxContentBytes }), declares structured refusals and an optional content limit, measured as JCS bytes of one occurrence's content.Why
One producer, discord-mcpl, ran into three cases against hosts that follow the current text:
-32000with an English sentence. Onchannels/incomingit gives the same sentences as a per-messagereason. A producer can only act on them by matching that host's wording (fix(forwarding): send an occurrence the host took but couldn't keep once more, as the same occurrence discord-mcpl#111). Client libraries also use-32000for their own request timeouts.channels/incomingmessage with a bareaccepted: false. It does that both for an unregistered channel and for its own admission policy, which need opposite responses.-32602/coalesce_invalid(Coalesced channels/incoming rejects messages with inline images: 1 MiB content cap counts base64 image data agent-framework#266, fixed in #267). RFC-006 reserves those answers for malformed requests, so the producer had to guess (Read the host's verdict on a forwarded message; resend once without inline media when refused for its content discord-mcpl#105).SPEC §18.8 already parks "structured refusal" for connection admission; this does it for forwarded events.
What it doesn't do
usespath, and RFC-002 has nothing to gate.reason, which stays human-readable.27 conformance vectors are included. The open questions cover:
retryableshould be implied by the kind;accepted: true;too_largebelongs in the registry or only in the advertisement;maxContentBytesmeasures;🤖 Generated with Claude Code