Skip to content

RFC-013 draft: structured refusal for forwarded events - #31

Merged
antra-tess merged 4 commits into
mainfrom
rfc-013-structured-refusal
Oct 23, 2026
Merged

antra-tess merged 4 commits into
mainfrom
rfc-013-structured-refusal

Conversation

@antra-tess

@antra-tess antra-tess commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

What it proposes

When a host refuses a forwarded event (push/event, or a message in channels/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. reason is free text (§9.3), a channels/incoming result defines only accepted (§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 data of an error answering push/event or a whole channels/incoming request.

  • refusal: one of an open registry: invalid, not_permitted, unknown_channel, unavailable, too_large, duplicate. duplicate applies to uncoalesced events only.
  • retryable: whether the same occurrence (RFC-006's identity: eventId, else messageId) can still be admitted unchanged.
  • retryAfterMs: don't send this event again in any form sooner than this.
  • limitBytes: only with too_large.

Code mapping. Existing codes map onto the registry:

  • -32602, -32600, -32700 and -32003 → invalid;
  • -32001, -32002 and -32017 → not_permitted;
  • -32023 → unknown_channel.

Two codes are added for the error lane, because -32000 can't carry "retry" and -32602 must not mean "too large":

  • -32030 Temporarily unavailable;
  • -32031 Content too large.

A refusal is classified when it carries refusal, has a mapped code, or is RFC-006's coalesce_invalid reason.

Producer rules:

  • Resend the same occurrence only when it's retryable, never sooner than retryAfterMs or the producer's own backoff floor. Attempts are always bounded, within RFC-006 §3.1's window, or one hour for uncoalesced events.
  • On unknown_channel, register the channel and resend once.
  • On too_large, send smaller content as a new occurrence.
  • A not_permitted refusal of a request sent before the initial policy (§5.3) gets one resend after it, as recovery.
  • A request with no answer is still governed by RFC-006 §3.1. A refusal of the resend that follows says nothing about the earlier attempt, so no new occurrence follows it.
  • A kind inferred from a mapped code from a host that doesn't declare is advice, so today's recoveries stay open.

Host rules:

  • Classify refusals. This is a MUST for a declaring host, one that advertises structuredRefusals.
  • A refusal that invites a resend keeps none of the identity the refused occurrence itself introduced, and identity from admitted occurrences stays, so a resent edit is still an edit.
  • If effects were applied, retryable: true promises a deduplicated resend, held for an hour across restarts for uncoalesced events.
  • A §5.3 rejection records nothing.
  • Authority denials on push/event stay §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:

SPEC §18.8 already parks "structured refusal" for connection admission; this does it for forwarded events.

What it doesn't do

  • No change to authority: there's no uses path, and RFC-002 has nothing to gate.
  • No change to how a request with no answer is handled: RFC-006 §3.1 still governs it.
  • No grammar for reason, which stays human-readable.
  • No MUST for a host that doesn't declare, beyond what it chooses to promise.
  • No SPEC.md edits yet. RFC §7 lists the amendments for when it's accepted.

27 conformance vectors are included. The open questions cover:

  • whether retryable should be implied by the kind;
  • whether an uncoalesced duplicate should be accepted: true;
  • whether too_large belongs in the registry or only in the advertisement;
  • what maxContentBytes measures;
  • whether to fix numbers for the retry bound.

🤖 Generated with Claude Code

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>

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 uses path and leaves RFC-002 as the only source of authority. Its eventAdmission member is a top-level host member, like RFC-006's eventCoalescing. (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:

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. retryAfterMs is defined only as a wait before resending the same occurrence. The RFC does not say whether it applies, or is meaningless, when retryable is false. It also does not say whether a producer sending a reduced new occurrence after too_large, or re-sending after unknown_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 against maxContentBytes: 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 -32023 on a channel-scoped push/event mapped to unknown_channel without data.refusal; a -32030 with no data, which must default to retryable through the code mapping; RFC-006's per-message reason: "coalesce_invalid", which must read as invalid when refusal is absent (line 121); and a too_large refusal on channels/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 covers featureSets/update negotiation 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's capabilities.experimental.mcpl advertisement 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, retryAfterMs and limitBytes. §7 itself says "add the same four fields". The count should be corrected so readers don't miss limitBytes.
  • 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/-32031 leaves -32026..-32029 free for "drafts in progress next to RFC-012's proposed -32025". No RFC-012 exists in the checkout, and -32025 does 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 a push/event result { accepted: false, refusal: "not_permitted" }, and the registry places feature-set and capability denial under not_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 of not_permitted is only for host admission policy, and that authority denials keep their §6.6 error codes.

Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
- -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>
@antra-tess

Copy link
Copy Markdown
Contributor Author

Revision 2 (b38758f) answers the review. The 8 medium findings are answered and resolved on their threads. All 8 lower findings held up too:

  • retryAfterMs scope: it's now the earliest time for any further send of the event: the same occurrence, the resend after registering a channel, or a reduced new occurrence after too_large. It has no use with invalid or duplicate.
  • Boundary: vector 10 now has content of exactly 2,000,000 bytes, which is admitted. §6 says maxContentBytes is included in what's admitted.
  • Missing vectors: added 13–20:
    • -32003;
    • -32023 without data;
    • -32030 without data;
    • legacy coalesce_invalid;
    • too_large on a message without an eventId;
    • a whole-batch error;
    • a zero-wait loop;
    • a coalesced retry, which never gets duplicate.
  • §6.7 → §5.3: corrected. The initial-policy case is now rule 6: a refusal received before the host's initial featureSets/update is resent once after it, whatever its kind except invalid, because §5.3 makes such rejections mandatory and says nothing about authority.
  • §5.1 → §5.2: the amendment target is now §5.2 (Host Support). The §5.1 boolean-shorthand reference stays.
  • "Three" → "four" fields: corrected in the Summary and the PR description.
  • Code-range reservation: dropped. The two codes stand on their own justification.
  • §6.6 vs the result form: authority denials on push/event stay §6.6 error objects (host rule 6, RFC §3 item 1). The result form of not_permitted is only for a host's own admission policy.

There's a new "Decisions" list for what revision 2 settles. The open questions are narrowed accordingly.

🤖 Generated with Claude Code

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 uses path and no new grant, so RFC-002's model is not touched. The eventAdmission advertisement 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:

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 sends push/event before it receives featureSets/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 advertising maxContentBytes "SHOULD answer content over it with too_large and limitBytes". Only not refusing content within the limit is a MUST. Vector 10 requires 2,000,001 bytes to be refused too_large with limitBytes: 2000000. Its advertisement { maxContentBytes: 2000000 } also omits structuredRefusals, 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.

Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
- 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>
@antra-tess

Copy link
Copy Markdown
Contributor Author

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:

  • Vector 4 / rule 6 vs §5.3: rule 6 is now framed as recovery. It cites §5.3, which says a conforming producer sends no push before the exchange completes, and gives an earlier-revision producer as the example of one that did. It now covers only not_permitted.
  • Vector 10 treated a SHOULD as required: split into what's required and what's advisory.
    • Required: 1,999,999 and exactly 2,000,000 bytes aren't refused for size. If 2,000,001 is refused for size, the refusal is too_large. The host now advertises structuredRefusals, so host rule 2's MUST applies.
    • Advisory: that refusal carries limitBytes.
  • §14.6 listed as amended without text: removed from the header and the §7 heading. The two codes go in Appendix A only, since they aren't channel codes.

🤖 Generated with Claude Code

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/event result (§9.3) and the channels/incoming result (§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 eventAdmission member. It places that member at the top level for the same reason RFC-006 places eventCoalescing there, and it adds no uses path 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:

2 more findings, less severe
  • Low severity, maintainability, RFC-013-structured-refusal.md:331-332. The schema lets limitBytes be 0 while maxContentBytes must be at least 1. B.5 sets limitBytes to minimum: 0, but B.6 sets maxContentBytes to minimum: 1. Both are measured the same way (RFC §6). A limitBytes: 0 can 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.

Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
Comment thread RFC-013-structured-refusal.md Resolved
- 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>
@antra-tess

Copy link
Copy Markdown
Contributor Author

Revision 4 (bcda034) answers the third review. The 8 medium findings are answered and resolved on their threads. Both lows held up:

  • limitBytes minimum: now 1, like maxContentBytes. A limit of 0 can't be met by any content, since the JCS form of an empty array is 2 bytes.
  • Out-of-date PR description: rewritten to match revision 4, with 27 vectors, the declaring-host trigger, the classified-refusal definition and the identity rule.

🤖 Generated with Claude Code

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/event and channels/incoming results 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 uses path. RFC-013 uses the same wording. Its new eventAdmission member is a top-level host advertisement, like RFC-006's eventCoalescing. (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, retryAfter and 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/-32031 and eventAdmission, which no file uses yet. Like RFC-005, it makes acceptance depend on executable vectors under conformance/. 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:

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 is not_permitted. Host rule 4 never says which kind a declaring host should use. If a host picks invalid, the producer stops for good. If it picks unavailable, the producer may retry before the exchange completes. Host rule 4 should name the kind, for example not_permitted or a §5.3-specific value.
  • Low severity, test coverage, RFC-013-structured-refusal.md:123. No vector for retryAfterMs gating the channel-registration resend or the reduced new occurrence. RFC §3 defines retryAfterMs as gating any further send of the event: the same occurrence, the resend after registering a channel, or the reduced new occurrence after too_large. Vectors 1, 2, 19 and 24 only exercise resends of the same occurrence. Vectors 5 and 6 (unknown_channel, too_large) carry no retryAfterMs. A vector should show that a too_large or unknown_channel refusal with retryAfterMs delays 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 say limitBytes appears only with refusal: "too_large", but B.5 accepts it with any refusal or with none. A conformance validator built from Appendix B, the way conformance/check-bulk-references.mjs extracts B.1, will not catch the misuse. Add a dependentSchemas/if-then constraint, or say the restriction is not schema-enforced.

Comment on lines +113 to +117
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +331 to +336
| `-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`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +251 to +253
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +223 to +226
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +199 to +212
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +236 to +240
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.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +201 to +204
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:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@anima-connectome-github

Copy link
Copy Markdown

The review of revision 4 (bcda034) recommends "merge and fast-follow", with no high findings. This merges as a Draft at revision 4. The seven open threads, and the three low findings in the review summary, are carried to #32 for revision 5.

@antra-tess
antra-tess merged commit 2df40e9 into main Oct 23, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant