Skip to content

Relay: act on Prior Group/Object ID Gaps across Objects (§2.1, §9.1, §12.8) - #104

Merged
floatdrop merged 1 commit into
draft-20from
fix/gap-property-malformed
Sep 26, 2026
Merged

floatdrop merged 1 commit into
draft-20from
fix/gap-property-malformed

Conversation

@floatdrop

Copy link
Copy Markdown
Owner

Summary

Until now the relay checked only the Prior Group ID Gap (§12.8) and Prior Object ID Gap (§12.9) rules that a single Object can show on its own. It now records every gap it sees, in the dedup ledger TrackEntry already keeps (same 32-Group window, same lock). On live subgroup and datagram Objects it:

  • Drops an Object inside a gap announced earlier, or in a Group a later gap covered: the Object is known not to exist, and that is permanent (§2.1). It is neither forwarded nor cached (§9.1: a caching relay "SHOULD NOT cache or forward" it), and the track continues.
  • Accepts a gap covering an Object or Group already received: §2.1 lets an Object go from existing to not existing. Any cached copy is kept, since §9.1 makes updating the cache a MAY.
  • Ends the track when one Group carries two different Prior Group ID Gap values (§12.8): every subscriber gets PUBLISH_DONE MALFORMED_TRACK (§2.4.2), and the Object is not cached.

A rejected Object leaves no state in the ledger.

Interpretation (marked per CLAUDE.md; chosen with you)

The draft conflicts with itself here:

  • §12.8 and §12.9: list "an Object … within a previously communicated gap" and "a gap covering an Object it previously received" as making the track malformed.
  • §2.1: says the first "is not a protocol error and the Track is not malformed", and lets an Object go from existing to not existing.

The relay follows §2.1 and §9.1. It also takes §9.1's specific SHOULD NOT over §9.4's general "MUST NOT reorder or drop objects received on a multi-object stream". The reasoning is in the ClaimDelivered doc and a new STATUS.md Limitations entry.

If that's wrong: the two drop paths would end the track instead, and the forwarded/dropped outcomes in TestTrackEntry_ClaimDeliveredGapProperties would become malformed.

API

  • New: message.ObjectPriorGaps(raw) PriorGaps reads both gaps in one pass, without allocating. PriorObjectIDGap now calls it.
  • Internal: registry.TrackEntry.ClaimDelivered(group, object, gaps) (fresh bool, err error).

Not covered (listed in STATUS.md; §12.8/§12.9 stay PARTIAL)

  • Where it isn't checked: upstream FETCH responses, sessions that aren't a relay's, and Groups older than the 32-Group window.
  • Downstream reaction: gap properties are forwarded unchanged, so a subscriber reading §12.9 literally may still end the track itself.
  • Accepted cost: checking a Group's announced Object-ID gaps takes longer the more distinct gaps that Group has (bounded by the window); a note in the code says so.

Tests (each verified red with its behaviour disabled)

  • Registry:
    • TestTrackEntry_ClaimDeliveredGapProperties covers forwarded, dropped and malformed outcomes, including a Group covered after it arrived.
    • TestTrackEntry_ClaimDeliveredMalformedLeavesNoTrace checks that a malformed Object records nothing.
  • Message: TestObjectPriorGaps, including a zero-allocation check.
  • Relay, drops: TestRelay_ObjectInsideAnnouncedGapDropped covers a subgroup Object gap, a datagram Object gap and a datagram Group gap. The Object inside the gap is never forwarded, the next Object arrives, and a FETCH afterwards serves the next Object but not the dropped one.
  • Relay, malformed: TestRelay_MalformedObjectEndsTrack has two cases where one Group carries two gap values, on subgroups and on datagrams.
  • Earlier test kept: the next-Object case "gap covering the previous Object" (Relay: use §11.4.3's same-upstream and Prior Object ID Gap next-Object rules #102) still holds; that Object is forwarded on a new stream.
  • Checks: lint, the full suite, -race and make bench-quick all pass, with no allocs/op increase. The benchmark Objects carry no properties.

🤖 Generated with Claude Code

…9.1, §12.8)

The relay checked only the gap rules decidable from one Object. It now
records every Prior Group and Object ID Gap in the dedup ledger (same
32-Group window and lock) and, on live subgroup and datagram Objects:

- drops an Object inside a gap announced earlier, or in a Group a gap
  covered: it is known not to exist, permanently (§2.1), so it is
  neither forwarded nor cached (§9.1 SHOULD NOT);
- accepts a gap covering an Object or Group already received: an
  Object may go from existing to not existing (§2.1);
- ends the track when one Group carries two Prior Group ID Gap values
  (§12.8): PUBLISH_DONE MALFORMED_TRACK, the Object not cached.

Interpretation, chosen with the user and marked in ClaimDelivered and
STATUS.md: §12.8/§12.9 list the first two cases as malformed, but §2.1
says the first "is not a protocol error and the Track is not malformed".
§9.1's specific rule is also taken over §9.4's general MUST NOT drop.

API: message.ObjectPriorGaps (one zero-alloc walk; PriorObjectIDGap
delegates to it). Internal: TrackEntry.ClaimDelivered takes the gaps and
returns an error for a malformed track; a rejected Object leaves no
state. runFanout's three malformed paths share one closure.

Tests, each verified red with its behaviour disabled:
TestTrackEntry_ClaimDeliveredGapProperties (forwarded / dropped /
malformed), _MalformedLeavesNoTrace, TestObjectPriorGaps,
TestRelay_ObjectInsideAnnouncedGapDropped (subgroup and datagram; the
FETCH afterwards serves the next Object, not the dropped one), and
TestRelay_MalformedObjectEndsTrack's two different-values cases.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@floatdrop
floatdrop merged commit 6453ac4 into draft-20 Sep 26, 2026
11 checks passed
@floatdrop
floatdrop deleted the fix/gap-property-malformed branch September 26, 2026 08:09
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