joule-moq-relay: a MOQT draft-22 relay over QUIC, WebTransport and a WebSocket fallback #293

Merged
dcharlot merged 19 commits from joule-moq/relay into main 2026-10-18 12:37:19 -04:00
Owner

joule-moq-relay: a MOQT draft-22 relay over QUIC, WebTransport and a WebSocket fallback

A new crate in the joule-code workspace, joule-code/joule-code-rs/crates/joule-moq-relay, library and binary. It is a relay for draft-ietf-moq-transport-22, built on joule-moq's codec. It serves:

  • native QUIC (ALPN moqt-22);
  • WebTransport over HTTP/3 (ALPN h3, with the draft chosen by WT-Available-Protocols) on the same UDP port;
  • a WebSocket fallback for paths that carry no UDP.

It fans objects out in §5.1.2's priority order and applies each subscriber's delivery timeouts at this hop. Every object's receipts reach the subscriber, including the receipts of objects it drops.

What is in it

Transports (src/transport/), behind one Connection/SendStream/RecvStream interface.

  • QUIC and WebTransport. One quinn endpoint offers moqt-22 and h3. A connection with ALPN moqt-22 runs MOQT directly. One with ALPN h3 is handed to the WebTransport layer, which:
    • checks the CONNECT path and WT-Available-Protocols;
    • answers with WT-Protocol: "moqt-22";
    • maps stream error codes into the HTTP/3 range, per draft-ietf-webtrans-http3-16 §4.4.
  • WebSocket binding. Each binary message carries exactly one capsule of draft-ietf-webtrans-http2-15 §6:
    • WT_STREAM, WT_RESET_STREAM, WT_STOP_SENDING, WT_MAX_STREAMS, DATAGRAM and WT_CLOSE_SESSION;
    • the subprotocol moqt-22 selects the draft;
    • the crate README documents it in full as a non-normative binding, with example bytes.

The WebTransport library is wtransport 0.7.2 (MIT OR Apache-2.0). I chose it for three reasons:

  • It runs on quinn 0.11, as joule-moq does. With its quinn feature, one UDP endpoint serves both ALPNs.
  • It gives the server the CONNECT request's headers and lets it answer with its own. That is what WT-Available-Protocols and WT-Protocol need.
  • Its client checks a certificate by hash under the rules a browser applies to serverCertificateHashes: ECDSA P-256, valid two weeks at most. So the tests connect the way a page does.

The other permissive candidate had three releases in the two weeks before this slice. wtransport 0.7.2 has been out since August.

Relay behaviour (src/relay.rs, src/session.rs):

  • SETUP. Each end opens a control stream with SETUP. A client's SETUP is held to §9.1's rules for a client. The relay echoes joule-moq's receipt Setup Option when the client declares it.
  • Namespaces.
    • PUBLISH_NAMESPACE stands until it is cancelled or its session ends.
    • SUBSCRIBE_NAMESPACE gets NAMESPACE and NAMESPACE_DONE, each carrying the suffix after the prefix. An overlapping prefix in the same session gets PREFIX_OVERLAP.
  • PUBLISH is accepted with REQUEST_OK. PUBLISH_DONE ends the track's downstream subscriptions after their queues drain, with an exact stream count.
  • SUBSCRIBE.
    • SUBSCRIBE_OK goes out only once an upstream subscription is Established (§7.4). That is either one the relay already holds, or one it opens to every publisher of a matching namespace.
    • Upstream, the relay asks at priority 128 with no timeout (§5.1.3).
    • RENDEZVOUS_TIMEOUT is honoured.
    • A publisher that publishes a matching namespace later is sent a SUBSCRIBE for every subscription the relay holds under it (§7.6).
    • When the last downstream subscriber leaves, the relay cancels its own upstream SUBSCRIBE.
  • REQUEST_UPDATE covers priority, FORWARD, the Location Filter and the delivery timeouts.
  • FETCH and fills.
    • A range that starts where the cache knows every object is served from the cache.
    • Otherwise the FETCH is forwarded to a publisher and its fetch stream piped through. Past the Fill Timeout, what is still missing is reported as a Timed-Out range.
    • A range past the Largest Object gets INVALID_RANGE.
    • A SUBSCRIBE with FILL_PARAMETERS gets a fill fetch stream named by its Request ID.
  • TRACK_STATUS is answered from what the relay holds.
  • Multiple publishers of one track are deduplicated per (group, subgroup).
  • GOAWAY on shutdown.
    • Every session is sent goaway_uri and the timeout.
    • New requests are refused with GOING_AWAY.
    • A session still open at the timeout is closed with GOAWAY_TIMEOUT.
  • Request IDs. A wrong parity or a repeated ID closes the session with INVALID_REQUEST_ID. MAX_REQUEST_UPDATES is enforced per stream.
  • Requests in flight are bounded by the stream limit, as -22 intends (§6.4.2): MAX_STREAMS on QUIC, WT_MAX_STREAMS on the WebSocket binding.
  • The authorization-token alias cache (§8.9, §9.1.3, §9.1.4) is implemented. Grants name the SHA-256 of a token, never the token itself.

Fan-out (src/sched.rs). Each downstream session has a scheduler.

  • Each data stream is a lane whose items go out in order, so no stream's objects are reordered (§7.4).
  • Among lanes, the scheduler picks by §5.1.2: subscriber priority, then publisher priority, then group order. Fills come before subscription delivery, then the lower Subgroup or Object ID, and a datagram before a subgroup object.
  • At most write_concurrency writes are in flight, and QUIC's send window is configurable. What waits therefore waits in the scheduler, where it can still be dropped (§5.2).
  • An object's OBJECT_DELIVERY_TIMEOUT runs from its arrival at the relay. It bounds both opening the object's stream and writing it. When it passes, the stream is reset with DELIVERY_TIMEOUT and the rest of that subgroup is dropped for that subscriber.
  • SUBGROUP_DELIVERY_TIMEOUT runs from the FIN until all the subgroup's data is acknowledged.

Receipts (src/door.rs):

  • A receipts track (WAI moq-streaming-format.md §4.1) gets no delivery timeout from the relay, even one a subscriber asks for. The receipts of every dropped object therefore still reach a subscriber of that receipts track (WAI jwp-receipts.md §4.1, §4.4).
  • In namespaces a door rule names, receipts are checked at the door with joule-moq's verifier:
    • each group receipt's signature is verified, under trusted keys or any key, before the group's first object is fanned out;
    • each object's content hash and Merkle proof are checked against its position in the group;
    • an object waits for its group's receipts up to hold_ms;
    • a group whose receipts fail is dropped and logged with a structured error.

Session summaries (src/summary.rs). When a session ends, the relay signs a summary with its own Ed25519 key. It counts, per subscriber and per track:

  • bytes and objects forwarded;
  • objects dropped for their delivery timeout, and how many of those were on a track whose receipts track the session also held;
  • objects fetched;
  • objects still queued at close;
  • for the publisher side: objects received, refused at the door, and deduplicated.

relay_energy is {"acquisition": "unmetered", "joules_micro": 0}: no meter backs the relay, and the 0 is WAI's unmetered marker, not a figure. Where the door verified an object's receipt, the summary also sums the publisher's signed figures under their class, labelled as the publisher's.

Configuration is TOML (relay.example.toml); --dev gives loopback defaults. Without certificate files, the relay makes a development certificate (ECDSA P-256, valid fourteen days) and logs its SHA-256 as hex, as base64 and as a ready serverCertificateHashes entry. No secret is in the repository: keys are files, and tokens are named by their hash.

Tests

All tests run on loopback, and every wait has a hard bound:

Test What it shows
relay_quic, relay_webtransport, relay_websocket Two publishers (one via PUBLISH_NAMESPACE and the relay's SUBSCRIBE, one via PUBLISH) and three subscribers, over each transport. Every object verifies against its publisher's receipts, which the door also checks. SUBSCRIBE_NAMESPACE is covered. Every summary verifies and labels the relay's energy unmetered. The WebTransport client trusts the relay by certificate hash.
slow_subscriber A base track at priority 0 and a layer track at priority 200 with a 100 ms delivery timeout. A subscriber with small receive windows reads nothing until the publisher has sent everything and 1.5 s more has passed, then reads all its streams concurrently. It loses layer objects (streams reset with DELIVERY_TIMEOUT) and keeps the whole base track. It receives every receipt of both tracks, although it asked for a 50 ms timeout on its receipts subscriptions. Each dropped object verifies by inclusion. The relay's summary counts each layer object as either forwarded or dropped, and both counts match what arrived.
late_joiner TRACK_STATUS; FETCH of a track and of its receipts from the cache, with every object verified; a range in descending order; INVALID_RANGE; a fill on SUBSCRIBE. A group that reaches the relay whole after a later one still has its FETCH served from the cache. A FETCH is forwarded upstream when the cache holds nothing.
goaway GOAWAY with its URI and timeout reaches every session. A new request gets GOING_AWAY. The two sessions that leave close with NO_ERROR; the one that stays is closed with GOAWAY_TIMEOUT.
auth_tokens A token registered in SETUP; one registered in a request and then used by alias; UNAUTHORIZED without a token or with a wrong one; UNKNOWN_AUTH_TOKEN_ALIAS.
request_ids INVALID_REQUEST_ID for the wrong parity and for a repeated ID. With a limit of two requests, a third waits for credit until one is cancelled.
priority_fan_out The load-shaped test. Two publishers, four tracks and three subscribers, each with its own subscriber priorities and group orders. The subscribers' pipes are held shut while the load arrives. When they open, each subscriber receives every object in §5.1.2's order.

There are also 20 unit tests: the capsule codec, the multiplexer (streams, resets, datagrams, close, stream credit), WebTransport code mapping and protocol selection, the development certificate, the door, the token cache and grants, the cache's coverage rules and its budget, the scheduler's order, and the signed summary.

Three falsification runs (each on a committed tree, then reverted):

  • With the scheduler made first-come-first-served, priority_fan_out fails: subscriber 0 receives in arrival order.
  • With the receipts-track exemption removed, slow_subscriber fails: "the receipts of every delivered group arrived" does not hold.
  • With the cache's old coverage latch restored, the reordering test in late_joiner fails: "the cache covers the track from group 0, though group 0 came after group 1" does not hold.

The cache's coverage: a defect the first CI run found, now fixed

The first CI run passed the dedicated relay job. But standards workspaces build + test / joule-code-rs (linux-arm64) (job 32029) failed in late_joiner, a_late_joiner_is_served_from_the_cache, with REQUEST_ERROR INTERNAL_ERROR: the publisher did not answer FETCH. The relay had sent a late joiner upstream for objects it held.

Cause. The cache latched its coverage on the first object it kept. The test publishes each group on its own subgroup stream. On a loaded host, group 1's stream can be handled before group 0's; the latch then sat on group 1, a FETCH from group 0 was never "covered", and the relay forwarded it to a publisher that answers no FETCH. This was a relay defect, not only a flaky test. I reproduced it with a new test that sends group 1 whole, waits until the relay has it, then sends group 0: against the latch it failed (rb exit 101).

Fix (src/cache.rs, with the relay counting streams in src/relay.rs and src/session.rs). The cache now keeps what it knows of each group's arrival. A group is whole when:

  • its start arrived: Object ID 0, or a Prior Object ID Gap saying the objects before do not exist (§10.9);
  • none of its subgroup streams is open or was reset or cut short;
  • its end is known: its streams ended with a FIN, or an End of Group or End of Track status, or a datagram with END_OF_GROUP, ended it.

A FETCH from start is covered when:

  • every group from start up to the Largest Object's group is whole, or known not to exist (a Prior Group ID Gap, §10.8);
  • that last group has its start and lost nothing;
  • nothing in between was evicted.

Coverage is worked out from the groups held each time it is asked. A group that arrives after a later one therefore extends coverage back once it is whole. A group ID never seen is still a gap the cache does not answer across. The relay counts each upstream subgroup stream from its first kept object and closes the count at its FIN; any other end leaves the group not whole. The relay cannot tell a subgroup it has not yet seen from one that does not exist, so a group whose subgroups have all closed counts as whole. The module documentation and README say so.

objects_in. I kept its meaning: objects received from upstream, counted on arrival, before the door, deduplication and the cache. Its documentation now says so. The late-joiner test no longer waits on it. It waits on Relay::cache_covers for the track and its receipts track from group 0, a query added for this, and on Relay::largest.

Stability. In one rb run, the late_joiner binary (its three tests, the reordering test among them) ran 32 times, as four concurrent loops of eight, on the build host under its CI load (load average about 43 to 73). It passed 32 of 32 times.

The slow subscriber's base track: not a relay defect, now tested as claimed

The second CI run, at 4a8a6e34, failed slow_subscriber once in standards workspaces build + test / joule-code-rs (linux-arm64) (job 33622): 39 of 40 base objects arrived, after 42.6 s. That time is the 1.5 s stall, then the first read loop running to its 40 s limit, then the drain.

Reproduction. I ran the test unchanged on the build host, with CI running there too. It failed 3 times in 64 runs (eight concurrent loops), then 8 times in 144 (24 concurrent loops). Before rerunning, I added a report that runs only on failure. It records:

  • which base groups the subscriber saw, and whether any arrived reset;
  • the times the subscriber's reader gave up waiting on a stream's header, and how many streams were queued behind it each time;
  • what arrived once those queued streams were read concurrently;
  • each QUIC session's flow-control counters;
  • the relay's own tally of the slow session's base track.

The evidence, the same in all eight failures of the second batch:

  • The relay's base tally was objects forwarded 40, dropped 0, undelivered at close 0. The relay handed every base object to the transport, and no base stream arrived reset. So it is not (a): the relay did not drop, reset or lose a base object.
  • The subscriber's one-at-a-time reader had given up waiting on a stream's header with 22 to 88 accepted streams unread behind it.
  • Once those queued streams were read concurrently, every missing base object arrived intact: groups 38–39 in two runs, and 4–39 through 8–39 in the others.

So it is (b). QUIC's connection-level flow control is shared by a session's streams. Bytes the subscriber had received but not read, in the streams queued behind the one it waited on, held the window. The stream it waited on could not get its bytes, and the reader read nothing else. quinn credits a reset stream's unread bytes back to the connection at once, so the layer streams the relay resets for their deadline were not what held the credit.

The test now waits on what it claims:

  • The slow subscriber reads its streams concurrently, as a subscriber does.
  • It reads until the base track and every receipt are whole and a second passes with nothing new, or the overall deadline passes.
  • Its assertions are unchanged: the whole base track, every receipt, at least one layer drop, inclusion for every dropped object, and the relay's exact accounting.

A first round of stress on that change found one more timing dependency. With concurrent reading, the drops rested on the stall alone. At a load average near 200, a slow publisher was still sending when the stall ended, and 3 of 144 runs dropped no layer object at all. The subscriber now reads nothing until the publisher has sent everything and the stall has passed after that. After that change, 144 of 144 runs passed at a load average of 204 to 244.

The test client no longer drops a stream it timed out waiting on. next_data keeps it and offers it again, records each such wait, and next_stream hands streams out unread.

For diagnosis: Connection::diagnostics gives a QUIC session's flow-control counters, and Relay::session_diagnostics gives each session's.

The README has a new section, "A subscriber that does not read". A subscriber that does not read stalls its own high-priority track through connection flow control, and one that reads its streams one at a time can stall itself. The relay cannot release credit the subscriber holds, and does not drop what has no deadline. What it does do (§5.2):

  • it keeps what waits in its scheduler, by write_concurrency and quic_send_window_bytes, so an object that expires there takes no credit;
  • it resets expired objects, which returns their credit;
  • it sends the highest-priority bytes first when credit frees.

It does not yet end such a subscription with PUBLISH_DONE TOO_FAR_BEHIND. That is listed under "Not yet implemented".

Commands run on the build host

Every build ran through rb. The final runs are on the branch head, 159d7ad9, after merging origin/main (6abb063d, with #303 and #308) in. Each exit status below is rb's, which is the command's own.

Command rb exit
rb <worktree> -- 'cd joule-code/joule-code-rs && cargo clippy --locked -p joule-moq-relay --all-targets > $RB_ROOT/tmp/moq-relay-b-clippy2.log 2>&1' 0. No warnings from this crate; the remaining warnings are joule-moq's and jwp's, which predate this slice.
rb <worktree> -- 'cd joule-code/joule-code-rs && cargo test --locked -p joule-moq-relay --all-targets > $RB_ROOT/tmp/moq-relay-b-test2.log 2>&1' 0. 11 test binaries, 31 tests passed, 0 failed.
Stability, in one rb run: the slow_subscriber and late_joiner binaries, 16 concurrent loops of four runs each, all 32 loops at once, at a load average of about 207 to 220; the command fails if any run did 0. slow_subscriber 64 of 64, late_joiner 64 of 64.
The slow-subscriber stress before the last change: 24 concurrent loops of six 0. 144 of 144, at a load average of 204 to 244.
The reproduction of the slow-subscriber failure (the test as it was, 8 and 24 concurrent loops) 1. 3 of 64 and 8 of 144 failed, each with the report above.
The late-joiner stress after the coverage fix: 32 runs in four concurrent loops 0. 32 of 32.
rb <worktree> -- './scripts/check-lockfiles.sh joule-code/joule-code-rs' (no dependency has changed since) 0. The lockfile is current; the change only adds packages.
The reproduction of the coverage defect (--test late_joiner against the latch) 101: the reordering test fails, as the defect predicts.
The three falsification runs (--test priority_fan_out, --test slow_subscriber, --test late_joiner, each with its property removed) 101, as intended.

Locally, these pass: check-tests-in-ci.py, check-workflow-concurrency.sh and its self-test, check-spec-refs.py, check-wai-section-refs.py, check-no-vanity-metrics.sh, check-comparative-prose.sh, check-orphan-sources.py, and test-public-text-checks.sh.

CI

joule-code-conformance.yml gains a moq-relay job on linux-arm64 that runs cargo test --locked -p joule-moq-relay --all-targets. The crate needs nothing x86-specific. The x86 workspace step now excludes the crate, so it does not run twice on the scarcer runner. The standards workspaces build + test / joule-code-rs (linux-arm64) leg also builds and tests the whole joule-code workspace, the relay included.

On the first CI run, at beb2a883, the dedicated job and the x86 conformance gate passed. The standards-build leg failed on the coverage defect described above, which is fixed at 4b8f08ba. On the second, at 4a8a6e34, every check but that leg passed. That leg failed on the slow subscriber's read order, also described above, and the test is fixed at 159d7ad9.

Not yet implemented

The crate README lists these too. They fall into four groups.

Left for later in this crate:

  • SUBSCRIBE_TRACKS: refused with NOT_SUPPORTED.
  • Range Filters: the relay declares MAX_FILTER_RANGES 0.
  • 0-RTT is not accepted.
  • Pausing upstream when every downstream subscriber is paused; forwarding NEW_GROUP_REQUEST upstream; EXPIRES.
  • Migration as a client: following a GOAWAY URI for the relay's own upstream subscriptions, and REDIRECT.
  • Detecting a cancellation that arrives only as STOP_SENDING after the subscriber's FIN. The relay notices on its next write or at session end.
  • Receipts tracks are recognised by a configured name suffix, not by reading the catalog's wai_receipts_track.
  • No delivery-gap claim is signed.
  • Energy: the relay has no meter, so its own figure is always the unmetered marker.

Library limits:

  • WebTransport over HTTP/2.
  • Some of draft-ietf-webtrans-http3-16 that the library does not speak: it negotiates with the settings deployed browsers send, not SETTINGS_WT_ENABLED; it has no session flow-control capsules; and it closes a session by closing its QUIC connection, not with WT_CLOSE_SESSION.
  • quinn 0.11 has no RESET_STREAM_AT.

Deferred to L5c:

  • The browser client (WebCodecs and WebTransport, falling back to the WebSocket binding) and the /room/ page, with end-to-end tests against real media.
  • A UDP endpoint in front of the relay, so that WebTransport reaches browsers on the public sites. This is an owner decision: today the sites' fronts carry HTTP over TCP only, so browsers there would use the WebSocket fallback.
  • Requesting a provisional registration for Setup Option 0x3A46, also an owner decision.

Deployment: none. Nothing was started outside the build host's tests, and no port, firewall, proxy or tunnel was touched.

🤖 Generated with Claude Code

## joule-moq-relay: a MOQT draft-22 relay over QUIC, WebTransport and a WebSocket fallback A new crate in the joule-code workspace, `joule-code/joule-code-rs/crates/joule-moq-relay`, library and binary. It is a relay for `draft-ietf-moq-transport-22`, built on `joule-moq`'s codec. It serves: - native QUIC (ALPN `moqt-22`); - WebTransport over HTTP/3 (ALPN `h3`, with the draft chosen by `WT-Available-Protocols`) on the same UDP port; - a WebSocket fallback for paths that carry no UDP. It fans objects out in §5.1.2's priority order and applies each subscriber's delivery timeouts at this hop. Every object's receipts reach the subscriber, including the receipts of objects it drops. ### What is in it **Transports** (`src/transport/`), behind one `Connection`/`SendStream`/`RecvStream` interface. - *QUIC and WebTransport.* One quinn endpoint offers `moqt-22` and `h3`. A connection with ALPN `moqt-22` runs MOQT directly. One with ALPN `h3` is handed to the WebTransport layer, which: - checks the CONNECT path and `WT-Available-Protocols`; - answers with `WT-Protocol: "moqt-22"`; - maps stream error codes into the HTTP/3 range, per `draft-ietf-webtrans-http3-16` §4.4. - *WebSocket binding.* Each binary message carries exactly one capsule of `draft-ietf-webtrans-http2-15` §6: - `WT_STREAM`, `WT_RESET_STREAM`, `WT_STOP_SENDING`, `WT_MAX_STREAMS`, `DATAGRAM` and `WT_CLOSE_SESSION`; - the subprotocol `moqt-22` selects the draft; - the crate README documents it in full as a non-normative binding, with example bytes. **The WebTransport library is `wtransport` 0.7.2 (MIT OR Apache-2.0).** I chose it for three reasons: - It runs on quinn 0.11, as `joule-moq` does. With its `quinn` feature, one UDP endpoint serves both ALPNs. - It gives the server the CONNECT request's headers and lets it answer with its own. That is what `WT-Available-Protocols` and `WT-Protocol` need. - Its client checks a certificate by hash under the rules a browser applies to `serverCertificateHashes`: ECDSA P-256, valid two weeks at most. So the tests connect the way a page does. The other permissive candidate had three releases in the two weeks before this slice. `wtransport` 0.7.2 has been out since August. **Relay behaviour** (`src/relay.rs`, `src/session.rs`): - *SETUP.* Each end opens a control stream with SETUP. A client's SETUP is held to §9.1's rules for a client. The relay echoes `joule-moq`'s receipt Setup Option when the client declares it. - *Namespaces.* - PUBLISH_NAMESPACE stands until it is cancelled or its session ends. - SUBSCRIBE_NAMESPACE gets NAMESPACE and NAMESPACE_DONE, each carrying the suffix after the prefix. An overlapping prefix in the same session gets PREFIX_OVERLAP. - *PUBLISH* is accepted with REQUEST_OK. PUBLISH_DONE ends the track's downstream subscriptions after their queues drain, with an exact stream count. - *SUBSCRIBE.* - SUBSCRIBE_OK goes out only once an upstream subscription is Established (§7.4). That is either one the relay already holds, or one it opens to every publisher of a matching namespace. - Upstream, the relay asks at priority 128 with no timeout (§5.1.3). - RENDEZVOUS_TIMEOUT is honoured. - A publisher that publishes a matching namespace later is sent a SUBSCRIBE for every subscription the relay holds under it (§7.6). - When the last downstream subscriber leaves, the relay cancels its own upstream SUBSCRIBE. - *REQUEST_UPDATE* covers priority, FORWARD, the Location Filter and the delivery timeouts. - *FETCH and fills.* - A range that starts where the cache knows every object is served from the cache. - Otherwise the FETCH is forwarded to a publisher and its fetch stream piped through. Past the Fill Timeout, what is still missing is reported as a Timed-Out range. - A range past the Largest Object gets INVALID_RANGE. - A SUBSCRIBE with FILL_PARAMETERS gets a fill fetch stream named by its Request ID. - *TRACK_STATUS* is answered from what the relay holds. - *Multiple publishers of one track* are deduplicated per (group, subgroup). - *GOAWAY on shutdown.* - Every session is sent `goaway_uri` and the timeout. - New requests are refused with GOING_AWAY. - A session still open at the timeout is closed with GOAWAY_TIMEOUT. - *Request IDs.* A wrong parity or a repeated ID closes the session with INVALID_REQUEST_ID. MAX_REQUEST_UPDATES is enforced per stream. - *Requests in flight* are bounded by the stream limit, as -22 intends (§6.4.2): MAX_STREAMS on QUIC, WT_MAX_STREAMS on the WebSocket binding. - *The authorization-token alias cache* (§8.9, §9.1.3, §9.1.4) is implemented. Grants name the SHA-256 of a token, never the token itself. **Fan-out** (`src/sched.rs`). Each downstream session has a scheduler. - Each data stream is a lane whose items go out in order, so no stream's objects are reordered (§7.4). - Among lanes, the scheduler picks by §5.1.2: subscriber priority, then publisher priority, then group order. Fills come before subscription delivery, then the lower Subgroup or Object ID, and a datagram before a subgroup object. - At most `write_concurrency` writes are in flight, and QUIC's send window is configurable. What waits therefore waits in the scheduler, where it can still be dropped (§5.2). - An object's OBJECT_DELIVERY_TIMEOUT runs from its arrival at the relay. It bounds both opening the object's stream and writing it. When it passes, the stream is reset with DELIVERY_TIMEOUT and the rest of that subgroup is dropped for that subscriber. - SUBGROUP_DELIVERY_TIMEOUT runs from the FIN until all the subgroup's data is acknowledged. **Receipts** (`src/door.rs`): - A receipts track (WAI `moq-streaming-format.md` §4.1) gets no delivery timeout from the relay, even one a subscriber asks for. The receipts of every dropped object therefore still reach a subscriber of that receipts track (WAI `jwp-receipts.md` §4.1, §4.4). - In namespaces a door rule names, receipts are checked at the door with `joule-moq`'s verifier: - each group receipt's signature is verified, under trusted keys or any key, before the group's first object is fanned out; - each object's content hash and Merkle proof are checked against its position in the group; - an object waits for its group's receipts up to `hold_ms`; - a group whose receipts fail is dropped and logged with a structured error. **Session summaries** (`src/summary.rs`). When a session ends, the relay signs a summary with its own Ed25519 key. It counts, per subscriber and per track: - bytes and objects forwarded; - objects dropped for their delivery timeout, and how many of those were on a track whose receipts track the session also held; - objects fetched; - objects still queued at close; - for the publisher side: objects received, refused at the door, and deduplicated. `relay_energy` is `{"acquisition": "unmetered", "joules_micro": 0}`: no meter backs the relay, and the 0 is WAI's unmetered marker, not a figure. Where the door verified an object's receipt, the summary also sums the publisher's signed figures under their class, labelled as the publisher's. **Configuration** is TOML (`relay.example.toml`); `--dev` gives loopback defaults. Without certificate files, the relay makes a development certificate (ECDSA P-256, valid fourteen days) and logs its SHA-256 as hex, as base64 and as a ready `serverCertificateHashes` entry. No secret is in the repository: keys are files, and tokens are named by their hash. ### Tests All tests run on loopback, and every wait has a hard bound: | Test | What it shows | |---|---| | `relay_quic`, `relay_webtransport`, `relay_websocket` | Two publishers (one via PUBLISH_NAMESPACE and the relay's SUBSCRIBE, one via PUBLISH) and three subscribers, over each transport. Every object verifies against its publisher's receipts, which the door also checks. SUBSCRIBE_NAMESPACE is covered. Every summary verifies and labels the relay's energy unmetered. The WebTransport client trusts the relay by certificate hash. | | `slow_subscriber` | A base track at priority 0 and a layer track at priority 200 with a 100 ms delivery timeout. A subscriber with small receive windows reads nothing until the publisher has sent everything and 1.5 s more has passed, then reads all its streams concurrently. It loses layer objects (streams reset with DELIVERY_TIMEOUT) and keeps the whole base track. It receives every receipt of both tracks, although it asked for a 50 ms timeout on its receipts subscriptions. Each dropped object verifies by inclusion. The relay's summary counts each layer object as either forwarded or dropped, and both counts match what arrived. | | `late_joiner` | TRACK_STATUS; FETCH of a track and of its receipts from the cache, with every object verified; a range in descending order; INVALID_RANGE; a fill on SUBSCRIBE. A group that reaches the relay whole after a later one still has its FETCH served from the cache. A FETCH is forwarded upstream when the cache holds nothing. | | `goaway` | GOAWAY with its URI and timeout reaches every session. A new request gets GOING_AWAY. The two sessions that leave close with NO_ERROR; the one that stays is closed with GOAWAY_TIMEOUT. | | `auth_tokens` | A token registered in SETUP; one registered in a request and then used by alias; UNAUTHORIZED without a token or with a wrong one; UNKNOWN_AUTH_TOKEN_ALIAS. | | `request_ids` | INVALID_REQUEST_ID for the wrong parity and for a repeated ID. With a limit of two requests, a third waits for credit until one is cancelled. | | `priority_fan_out` | The load-shaped test. Two publishers, four tracks and three subscribers, each with its own subscriber priorities and group orders. The subscribers' pipes are held shut while the load arrives. When they open, each subscriber receives every object in §5.1.2's order. | There are also 20 unit tests: the capsule codec, the multiplexer (streams, resets, datagrams, close, stream credit), WebTransport code mapping and protocol selection, the development certificate, the door, the token cache and grants, the cache's coverage rules and its budget, the scheduler's order, and the signed summary. **Three falsification runs** (each on a committed tree, then reverted): - With the scheduler made first-come-first-served, `priority_fan_out` fails: subscriber 0 receives in arrival order. - With the receipts-track exemption removed, `slow_subscriber` fails: "the receipts of every delivered group arrived" does not hold. - With the cache's old coverage latch restored, the reordering test in `late_joiner` fails: "the cache covers the track from group 0, though group 0 came after group 1" does not hold. ### The cache's coverage: a defect the first CI run found, now fixed The first CI run passed the dedicated relay job. But `standards workspaces build + test / joule-code-rs (linux-arm64)` (job 32029) failed in `late_joiner`, `a_late_joiner_is_served_from_the_cache`, with `REQUEST_ERROR INTERNAL_ERROR: the publisher did not answer FETCH`. The relay had sent a late joiner upstream for objects it held. **Cause.** The cache latched its coverage on the first object it kept. The test publishes each group on its own subgroup stream. On a loaded host, group 1's stream can be handled before group 0's; the latch then sat on group 1, a FETCH from group 0 was never "covered", and the relay forwarded it to a publisher that answers no FETCH. This was a relay defect, not only a flaky test. I reproduced it with a new test that sends group 1 whole, waits until the relay has it, then sends group 0: against the latch it failed (rb exit 101). **Fix** (`src/cache.rs`, with the relay counting streams in `src/relay.rs` and `src/session.rs`). The cache now keeps what it knows of each group's arrival. A group is *whole* when: - its start arrived: Object ID 0, or a Prior Object ID Gap saying the objects before do not exist (§10.9); - none of its subgroup streams is open or was reset or cut short; - its end is known: its streams ended with a FIN, or an End of Group or End of Track status, or a datagram with END_OF_GROUP, ended it. A FETCH from `start` is covered when: - every group from `start` up to the Largest Object's group is whole, or known not to exist (a Prior Group ID Gap, §10.8); - that last group has its start and lost nothing; - nothing in between was evicted. Coverage is worked out from the groups held each time it is asked. A group that arrives after a later one therefore extends coverage back once it is whole. A group ID never seen is still a gap the cache does not answer across. The relay counts each upstream subgroup stream from its first kept object and closes the count at its FIN; any other end leaves the group not whole. The relay cannot tell a subgroup it has not yet seen from one that does not exist, so a group whose subgroups have all closed counts as whole. The module documentation and README say so. **`objects_in`.** I kept its meaning: objects received from upstream, counted on arrival, before the door, deduplication and the cache. Its documentation now says so. The late-joiner test no longer waits on it. It waits on `Relay::cache_covers` for the track and its receipts track from group 0, a query added for this, and on `Relay::largest`. **Stability.** In one rb run, the `late_joiner` binary (its three tests, the reordering test among them) ran 32 times, as four concurrent loops of eight, on the build host under its CI load (load average about 43 to 73). It passed 32 of 32 times. ### The slow subscriber's base track: not a relay defect, now tested as claimed The second CI run, at 4a8a6e34, failed `slow_subscriber` once in `standards workspaces build + test / joule-code-rs (linux-arm64)` (job 33622): 39 of 40 base objects arrived, after 42.6 s. That time is the 1.5 s stall, then the first read loop running to its 40 s limit, then the drain. **Reproduction.** I ran the test unchanged on the build host, with CI running there too. It failed 3 times in 64 runs (eight concurrent loops), then 8 times in 144 (24 concurrent loops). Before rerunning, I added a report that runs only on failure. It records: - which base groups the subscriber saw, and whether any arrived reset; - the times the subscriber's reader gave up waiting on a stream's header, and how many streams were queued behind it each time; - what arrived once those queued streams were read concurrently; - each QUIC session's flow-control counters; - the relay's own tally of the slow session's base track. **The evidence, the same in all eight failures of the second batch:** - The relay's base tally was objects forwarded 40, dropped 0, undelivered at close 0. The relay handed every base object to the transport, and no base stream arrived reset. So it is not (a): the relay did not drop, reset or lose a base object. - The subscriber's one-at-a-time reader had given up waiting on a stream's header with 22 to 88 accepted streams unread behind it. - Once those queued streams were read concurrently, every missing base object arrived intact: groups 38–39 in two runs, and 4–39 through 8–39 in the others. So it is (b). QUIC's connection-level flow control is shared by a session's streams. Bytes the subscriber had received but not read, in the streams queued behind the one it waited on, held the window. The stream it waited on could not get its bytes, and the reader read nothing else. quinn credits a reset stream's unread bytes back to the connection at once, so the layer streams the relay resets for their deadline were not what held the credit. **The test now waits on what it claims**: - The slow subscriber reads its streams concurrently, as a subscriber does. - It reads until the base track and every receipt are whole and a second passes with nothing new, or the overall deadline passes. - Its assertions are unchanged: the whole base track, every receipt, at least one layer drop, inclusion for every dropped object, and the relay's exact accounting. A first round of stress on that change found one more timing dependency. With concurrent reading, the drops rested on the stall alone. At a load average near 200, a slow publisher was still sending when the stall ended, and 3 of 144 runs dropped no layer object at all. The subscriber now reads nothing until the publisher has sent everything and the stall has passed after that. After that change, 144 of 144 runs passed at a load average of 204 to 244. **The test client** no longer drops a stream it timed out waiting on. `next_data` keeps it and offers it again, records each such wait, and `next_stream` hands streams out unread. **For diagnosis:** `Connection::diagnostics` gives a QUIC session's flow-control counters, and `Relay::session_diagnostics` gives each session's. **The README** has a new section, "A subscriber that does not read". A subscriber that does not read stalls its own high-priority track through connection flow control, and one that reads its streams one at a time can stall itself. The relay cannot release credit the subscriber holds, and does not drop what has no deadline. What it does do (§5.2): - it keeps what waits in its scheduler, by `write_concurrency` and `quic_send_window_bytes`, so an object that expires there takes no credit; - it resets expired objects, which returns their credit; - it sends the highest-priority bytes first when credit frees. It does not yet end such a subscription with PUBLISH_DONE TOO_FAR_BEHIND. That is listed under "Not yet implemented". ### Commands run on the build host Every build ran through `rb`. The final runs are on the branch head, 159d7ad9, after merging `origin/main` (6abb063d, with #303 and #308) in. Each exit status below is `rb`'s, which is the command's own. | Command | rb exit | |---|---| | `rb <worktree> -- 'cd joule-code/joule-code-rs && cargo clippy --locked -p joule-moq-relay --all-targets > $RB_ROOT/tmp/moq-relay-b-clippy2.log 2>&1'` | 0. No warnings from this crate; the remaining warnings are `joule-moq`'s and `jwp`'s, which predate this slice. | | `rb <worktree> -- 'cd joule-code/joule-code-rs && cargo test --locked -p joule-moq-relay --all-targets > $RB_ROOT/tmp/moq-relay-b-test2.log 2>&1'` | 0. 11 test binaries, 31 tests passed, 0 failed. | | Stability, in one rb run: the `slow_subscriber` and `late_joiner` binaries, 16 concurrent loops of four runs each, all 32 loops at once, at a load average of about 207 to 220; the command fails if any run did | 0. slow_subscriber 64 of 64, late_joiner 64 of 64. | | The slow-subscriber stress before the last change: 24 concurrent loops of six | 0. 144 of 144, at a load average of 204 to 244. | | The reproduction of the slow-subscriber failure (the test as it was, 8 and 24 concurrent loops) | 1. 3 of 64 and 8 of 144 failed, each with the report above. | | The late-joiner stress after the coverage fix: 32 runs in four concurrent loops | 0. 32 of 32. | | `rb <worktree> -- './scripts/check-lockfiles.sh joule-code/joule-code-rs'` (no dependency has changed since) | 0. The lockfile is current; the change only adds packages. | | The reproduction of the coverage defect (`--test late_joiner` against the latch) | 101: the reordering test fails, as the defect predicts. | | The three falsification runs (`--test priority_fan_out`, `--test slow_subscriber`, `--test late_joiner`, each with its property removed) | 101, as intended. | Locally, these pass: `check-tests-in-ci.py`, `check-workflow-concurrency.sh` and its self-test, `check-spec-refs.py`, `check-wai-section-refs.py`, `check-no-vanity-metrics.sh`, `check-comparative-prose.sh`, `check-orphan-sources.py`, and `test-public-text-checks.sh`. ### CI `joule-code-conformance.yml` gains a `moq-relay` job on `linux-arm64` that runs `cargo test --locked -p joule-moq-relay --all-targets`. The crate needs nothing x86-specific. The x86 workspace step now excludes the crate, so it does not run twice on the scarcer runner. The `standards workspaces build + test / joule-code-rs (linux-arm64)` leg also builds and tests the whole joule-code workspace, the relay included. On the first CI run, at beb2a883, the dedicated job and the x86 conformance gate passed. The standards-build leg failed on the coverage defect described above, which is fixed at 4b8f08ba. On the second, at 4a8a6e34, every check but that leg passed. That leg failed on the slow subscriber's read order, also described above, and the test is fixed at 159d7ad9. ### Not yet implemented The crate README lists these too. They fall into four groups. **Left for later in this crate:** - SUBSCRIBE_TRACKS: refused with NOT_SUPPORTED. - Range Filters: the relay declares MAX_FILTER_RANGES 0. - 0-RTT is not accepted. - Pausing upstream when every downstream subscriber is paused; forwarding NEW_GROUP_REQUEST upstream; EXPIRES. - Migration as a client: following a GOAWAY URI for the relay's own upstream subscriptions, and REDIRECT. - Detecting a cancellation that arrives only as STOP_SENDING after the subscriber's FIN. The relay notices on its next write or at session end. - Receipts tracks are recognised by a configured name suffix, not by reading the catalog's `wai_receipts_track`. - No `delivery-gap` claim is signed. - Energy: the relay has no meter, so its own figure is always the unmetered marker. **Library limits:** - WebTransport over HTTP/2. - Some of `draft-ietf-webtrans-http3-16` that the library does not speak: it negotiates with the settings deployed browsers send, not `SETTINGS_WT_ENABLED`; it has no session flow-control capsules; and it closes a session by closing its QUIC connection, not with `WT_CLOSE_SESSION`. - quinn 0.11 has no RESET_STREAM_AT. **Deferred to L5c:** - The browser client (WebCodecs and WebTransport, falling back to the WebSocket binding) and the `/room/` page, with end-to-end tests against real media. - A UDP endpoint in front of the relay, so that WebTransport reaches browsers on the public sites. This is an owner decision: today the sites' fronts carry HTTP over TCP only, so browsers there would use the WebSocket fallback. - Requesting a provisional registration for Setup Option `0x3A46`, also an owner decision. **Deployment:** none. Nothing was started outside the build host's tests, and no port, firewall, proxy or tunnel was touched. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A new crate in the joule-code workspace, library and binary:

- transports behind one interface: native QUIC (ALPN moqt-22) and WebTransport
  over HTTP/3 (ALPN h3, WT-Available-Protocols) on one quinn endpoint, and a
  WebSocket binding whose messages are draft-ietf-webtrans-http2-15 capsules;
- the relay: SETUP, PUBLISH_NAMESPACE, SUBSCRIBE_NAMESPACE, PUBLISH, SUBSCRIBE
  from an upstream it holds or makes, RENDEZVOUS_TIMEOUT, FETCH and fills from
  the cache or upstream, TRACK_STATUS, REQUEST_UPDATE, GOAWAY, Request ID rules,
  MAX_REQUEST_UPDATES, the authorization-token alias cache;
- a per-session scheduler in section 5.1.2's order with delivery timeouts at
  this hop; receipts tracks carry none;
- receipts checked at the door per namespace, and signed session summaries.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
One scenario run over each transport on loopback: a publisher reached by the
namespace route and one by PUBLISH, three subscribers of both, one of them
discovering the first namespace by SUBSCRIBE_NAMESPACE. Every object reaches
every subscriber and verifies against its publisher's signed receipts, which
the relay checks at the door; every session's summary verifies under the
relay's key and labels the relay's energy unmetered.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The scheduler now bounds opening a stream, not only the write, by the
object's deadline; the relay's QUIC send window is configurable, so what
cannot be sent waits in the scheduler where delivery timeouts apply; the
test client can read lazily, holding flow control like a slow reader.

The test: a base track at priority 0 and a layer at 200 with a 100 ms
OBJECT_DELIVERY_TIMEOUT. A slow subscriber reads nothing for 1.5 s. Its base
track arrives whole; some layer objects pass their deadline and their
streams are reset with DELIVERY_TIMEOUT; every receipt of both tracks
arrives and each dropped object verifies by inclusion; the relay's signed
summary counts each layer object as forwarded or dropped, exactly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A late joiner over the WebSocket fallback reads TRACK_STATUS's Largest Object,
FETCHes a whole track and its receipts track from the cache and verifies
every object, FETCHes a range in descending group order, is refused a range
past the Largest Object with INVALID_RANGE, and joins at a past group by a
SUBSCRIBE whose fill arrives on a fetch stream named by its Request ID. The
publisher is asked for nothing. A FETCH for a track the relay holds nothing
of goes to the namespace's publisher, and its fetch stream is piped through.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
- goaway.rs: every session gets GOAWAY with the New Session URI and Timeout;
  a new request after it is refused with GOING_AWAY; the sessions that leave
  close with NO_ERROR, and the one that stays is closed with GOAWAY_TIMEOUT.
- auth_tokens.rs: a token registered in SETUP authorizes a publisher; one
  registered in a request is used by alias in the next; no token or a wrong
  one is UNAUTHORIZED; an alias never registered closes the session with
  UNKNOWN_AUTH_TOKEN_ALIAS.
- priority_fan_out.rs: two publishers, four tracks, three subscribers with
  their own subscriber priorities and group orders, the subscribers' pipes
  held shut while the load arrives; each receives every object in section
  5.1.2's order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
It does what moq-streaming-format 4.1 says a subscriber should not, and the
relay still applies no delivery timeout to a receipts track: every receipt
reaches it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A Request ID of the server's parity, or one used twice, closes the session
with INVALID_REQUEST_ID; with a limit of two requests a third waits for
WT_MAX_STREAMS credit, which returns when a request is cancelled.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Let-chains where it collapsed nested ifs, values() where it iterated a map
for its values; three allows where the shape is deliberate.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The README states the draft the relay speaks and how each transport selects
it, the WebTransport library and why, how a browser connects (a certificate
hash in development; the WebSocket fallback), the WebSocket binding in full
as a non-normative binding of webtrans-http2 capsules, what the relay does
section by section, its scheduling and timeouts, receipts and the door, the
session summary and how its energy is labelled, and what is not yet built.

CI: a job of its own on linux-arm64 runs every target of the crate; the x86
workspace step excludes it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Read one stream at a time until a second passes with none, bounded by the
number of streams, rather than for a fixed window a loaded runner could
outlast.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Merge remote-tracking branch 'origin/main' into joule-moq/relay
Some checks failed
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 2m1s
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 1m33s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 19s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 2m4s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 10m3s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Failing after 4m35s
beb2a8832b
The cache latched coverage on the first object it kept. When a group's
streams finished after a later group's, as they do on a loaded host, the
latch sat on the later group, and a FETCH from the earlier one went upstream
for objects the relay held: a late joiner on the arm64 runner was refused
when the publisher did not answer.

Coverage is now worked out from what is known of each group. A group is
whole when its start arrived (Object ID 0, or a Prior Object ID Gap), none of
its subgroup streams is open or was reset, and its end is known (its streams
ended with a FIN, or an End of Group or End of Track status, or a datagram
with END_OF_GROUP). A fetch is covered when every group from its start to the
Largest Object's is whole or known absent (a Prior Group ID Gap), that last
group has its start, and nothing between was evicted. A group never seen is
a gap the cache does not answer across.

The relay counts each upstream subgroup stream from its first kept object and
closes the count at its FIN (clean) or on any other end (the group is then
not whole). The late-joiner test now waits on the cache's coverage rather
than on objects_in, which counts arrivals before the cache; a new test sends
group 1 whole before group 0 and FETCHes from group 0, served from the cache.
It failed against the latch before this change.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-moq-relay: the cache's test-only import where its tests are
Some checks failed
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 2m3s
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 1m23s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 22s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m49s
standards workspaces build + test / macOS energy backends (pull_request) Failing after 3s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 3m53s
4b8f08ba70
Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-code CHANGELOG: joule-moq-relay 0.1.0
Some checks failed
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 2m11s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 18s
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 3m31s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 3m57s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 5m37s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Failing after 2m37s
4a8a6e340b
Co-Authored-By: Claude Opus 5.5 <[email protected]>
CI failed slow_subscriber once (39 of 40 base objects). Reproduced on the
build host under load: 3 failures in 64 runs, then 8 in 144. In every one the
relay's own tally for the base track was 40 forwarded, none dropped, none left
at close; no base stream arrived reset; the subscriber's one-at-a-time reader
had given up waiting on a stream's header with 22 to 88 accepted streams
unread behind it; and once those were read concurrently every missing base
object arrived. The relay lost nothing: the reader waited on a stream whose
bytes waited for connection credit the unread streams behind it held.

- The test now reads its streams concurrently, as a subscriber does, until
  the base track and every receipt are whole and a second passes with
  nothing new, or the overall deadline passes. Its assertions are unchanged.
  A failure report says where any missing base object was.
- The client keeps a stream next_data timed out on, rather than dropping it,
  records each such wait, and can hand out streams unread (next_stream).
- Connection::diagnostics gives a QUIC session's flow-control counters;
  Relay::session_diagnostics each session's.
- README: a subscriber that does not read stalls its own high-priority track
  through connection flow control; what the relay does about it (§5.2), and
  that it does not yet end such a subscription with TOO_FAR_BEHIND.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
With its streams read concurrently, the test's drops rested on the stall
alone; under a load average near 200 a slow publisher was still sending when
the stall ended, and three runs of 144 dropped no layer object at all. The
subscriber now reads nothing until the publisher has sent everything and the
stall has passed after that, so every layer object waiting then is past its
deadline. 144 of 144 runs passed at a load average of 204 to 244.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
joule-moq-relay: clippy's notes on the new test code
Some checks failed
wai formal verification (integer core) / kani (rans64-t2) (pull_request) Has been cancelled
wai formal verification (integer core) / kani (stage-long) (pull_request) Has been cancelled
wai formal verification (integer core) / kani (integer-core) (pull_request) Has been cancelled
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
wai cross-architecture byte-equality / does this change touch it (pull_request) Has been cancelled
wai cross-architecture byte-equality / linux-aarch64 (pull_request) Has been cancelled
wai cross-architecture byte-equality / linux-x86_64 (pull_request) Has been cancelled
wai cross-architecture byte-equality / macos-arm64 (reference) (pull_request) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 2m3s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 17s
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 1m31s
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 2m29s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 8m20s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 2m50s
159d7ad9e3
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!293
No description provided.