Skip to content

chore: merge main into dev - #2951

Merged
kixelated merged 32 commits into
devfrom
claude/merge-main-into-dev-b9f056
Aug 20, 2026
Merged

chore: merge main into dev#2951
kixelated merged 32 commits into
devfrom
claude/merge-main-into-dev-b9f056

Conversation

@kixelated

@kixelated kixelated commented Aug 20, 2026

Copy link
Copy Markdown
Collaborator

Merges main into dev. 29 files conflicted textually; several more conflicted semantically and merged clean. Both branches moved during the work, so this also carries a second round of integration against the newer tips.

Summary

Textual conflicts worth review

Silent semantic conflicts (merged clean because neither side touched the other's lines)

wasm32

web-transport-wasm 0.6 implements the poll traits moq-net requires (moq-dev/web-transport#369), so the hand-written adapters in moq-wasm and moq-ffi are deleted; both files are now just the dial (794 lines to 38, and 163 to 25). That is what lets main's moq-ffi wasm32 build (#2911) coexist with dev's poll-only transport (#2736). Pin bumped to "0.6"; web-transport-quinn picks up 0.12.1 under the existing "0.12" pin and web-transport-quiche is unchanged.

Three pre-existing dev bugs, each in its own commit

All three were invisible on dev because the CI that catches them is path-filtered, and this merge touches the paths.

  • cpp/obs did not compile against moq.h. The codec enums cross the ABI as uint32_t so that an unknown discriminant from C is an error rather than UB, which leaves no signature reaching them; the ENUMS allowlist in rs/libmoq/build.rs is what forces cbindgen to emit them anyway. moq_video_format and moq_container_format were never added to it, so the header declared neither and every C caller of moq_publish_video / moq_publish_container had to hardcode the discriminants. Nothing compiled a C consumer of libmoq in PR CI until obs.yml (feat(obs): compile the plugin in CI, and type-check it without obs-deps #2867) arrived from main.
  • Every python -> * smoke case failed. refactor(bindings)!: split media publishing by kind #2927 split publish_media_stream into publish_video_stream / publish_container_stream and updated py/moq-rs, but not the smoke client calling it, so the publisher died with 'BroadcastProducer' object has no attribute 'publish_media_stream'. smoke.yml runs on a PR only when it touches test/smoke, test/ts, test/justfile, package.json, or itself; refactor(bindings)!: split media publishing by kind #2927 touched none of those.
  • The wasm harness failed ietf: reads a published broadcast with old. It subscribes with the default Latency::REAL_TIME budget, which drops a group the live edge has moved past rather than delivering it late. feat(moq-net): enforce the subscriber latency budget #2890 made that enforcement real, so a publisher writing every 50ms can retire a group the reader is still on. Demanding consecutive sequences and treating old as a failure asserted a guarantee real-time delivery does not make. Skips are now accepted and bounded by an attempt cap, so a subscription that never delivers a whole group still fails; order and byte-exactness of a delivered group are unchanged. (Contrast test(mux): read the SI history with a latency budget that keeps it #2948, which hit the same budget in a Rust test that wanted history, where a wider budget was the right answer.)

One test changed deliberately

moq_net::model::resume::consecutive_updates_wake asserted an absolute wake count of 2 and saw 3. The extra wake happens during the poll, not at the update: main's kio::Park now reuses a still-registered waiter (#2905), so applying the subscription change notifies a list the poll is itself parked on and self-wakes once. That costs a redundant poll and nothing else, while a lost wakeup parks the task forever, so the assertion measures the delta now. Reason is recorded in the test.

Public API changes

moq.h gains the moq_video_format and moq_container_format declarations described above. That is additive: the discriminants are unchanged, so an existing caller that hardcoded them keeps working.

Otherwise no new or changed public surface beyond what each parent already shipped. Internal only: js/net/src/hop.ts is a new internal module (not re-exported from the entrypoint); moq_wasm::transport::Session is now a re-export of web_transport_wasm::Session rather than a local newtype (moq-wasm is publish = false); moq_ffi::transport's newtypes are gone in favour of the upstream types.

Wire behavior changes

None. No encoding, message, or framing changed in either direction, so no drafts/ update applies.

Testing

  • just rs check, just rs wasm -- pass
  • cargo nextest run --workspace -- 3295/3295 (dev's test(mux): read the SI history with a latency budget that keeps it #2948 fixed the moq-mux TS clock test that was failing on dev before this merge)
  • bun run --filter='*' check (type-check + docs build) -- pass
  • just obs ci -- all 11 targets build and link
  • ./smoke/smoke.sh --publishers python --subscribers rust,python -- pass
  • ruff + pyright on the changed Python -- clean
  • just test wasm -- 9/9 browser cases across all three protocol flavours, which is the real check on the transport swap: compiling says nothing about whether the bindings still open a session
  • bun biome check -- 3 errors, all pre-existing and identical on a pristine main checkout (cpp/obs/{CMakePresets,buildspec}.json formatting, doc/.vitepress/theme/index.js import order). Left alone rather than mixed into a merge.

🤖 Generated with Claude Code

https://claude.ai/code/session_017wFQ5wqKbvWwET3G5MJXY9

(Written by Claude Opus 5 (1M context))

kixelated and others added 27 commits August 15, 2026 16:45
Co-authored-by: GPT-5 <noreply@openai.com>
…sampler (M0) (#2877)

Co-authored-by: Claude <noreply@anthropic.com>
…ps (#2867)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…d0641fb99eee32 to 258712b0b7b1ddf8bddc9fc3b0faca682b2736c3 in the github-actions group (#2887)

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
## Summary

- Encode draft 17+ JS message parameters using each parameter's defined value type instead of the legacy key-parity framing.
- Preserve draft 14 through draft 16 behavior and add matching JS/Rust wire vectors for raw uint8 and Location values.
- Preserve exact Location identifiers without JavaScript `Number` rounding, including full `uint64` values in drafts 17+.
- Root cause: the JS codec continued inferring value framing from parameter ID parity after draft 17 introduced definition-specific value encodings. Its legacy Location cache also used number-only QUIC varint helpers despite exposing bigint identifiers.

Closes #2864.

## Public API changes

- Add `Varint.decodeBigInt` for exact QUIC varint decoding.
- Allow `Varint.encode` to accept `bigint` as well as `number`.
- `Parameters.largest` now preserves full `uint64` identifiers when using drafts 17+.
- Bump `@moq/net` from 0.3.0 to 0.3.1.

## Test plan

- [x] Original head: `just check`
- [x] Original head: `bun test js/net/src/ietf/ietf.test.ts` (88 passed)
- [x] Original head: `just rs test -p moq-net test_param_` (13 passed)
- [x] Intermediate head `859a765`: GitHub Actions `Check` and `Test`
- [x] Latest head `4890cbd`: GitHub Actions `Check` and `Test`
- [ ] Draft 17 JS-to-Rust browser smoke test. Playwright Chromium crashed with SIGTRAP before loading the publisher, so no protocol session formed.

## Cross-package sync

- Updated both `js/net` and `rs/moq-net` golden vectors.
- No draft update is needed because this corrects the implementations to the existing draft 17 framing.
- No other language package exposes the JS `Varint` API.

Co-authored-by: GPT-5 <noreply@openai.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: GPT-5 <noreply@openai.com>
Co-authored-by: GPT-5 <noreply@openai.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…le (#2828)

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Luke Curley <kixelated@gmail.com>
Co-authored-by: GPT-5 <noreply@openai.com>
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Luke Curley <kixelated@gmail.com>
Co-authored-by: OpenAI Codex <noreply@openai.com>
Two things kept moq-mux off wasm32, and the first is a latent bug on every
target.

The av1/h264/h265 splitters hold a `tokio::time::Instant`, but moq-mux declares
tokio with only the `macros` feature. That compiles today purely because another
workspace member enables tokio's `time`, and feature unification hands it over.
Build moq-mux in a resolver context that doesn't, and it fails to find
`tokio::time` at all. Swap it for `web_async::time::Instant`, which natively
re-exports the very same tokio type (so nothing changes on native) and maps to
wasmtimer on wasm. This is the migration moq-net already did.

The catalog `Stream` trait then required `Send + 'static`. moq-net's handles are
`Rc`-backed on wasm, so nothing there can satisfy it, and the bound exists to
allow moving exporters between native threads rather than to express anything
about the type. Add `kio::MaybeSend`, which is `Send` natively and vanishes on
wasm, and bound `Stream` on that instead. Natively `MaybeSend` is implied by
`Send` and implies it back, so every existing caller and impl is unaffected.

kio is the right home rather than a private copy: moq-net already keeps its own
`MaybeSend`-shaped helpers in `util.rs`, moq-mux is the second crate to need the
bound, and moq-ffi is the third once it grows a wasm target. Both crates already
depend on kio, and it carries no runtime.

Extend `just rs wasm` to cover moq-mux so this cannot regress silently, since a
host-target build will never notice. moq-mux has to share moq-wasm's cargo
invocation to inherit `getrandom`'s `wasm_js` backend, and the recipe explains
why splitting them fails.

Verified: `just check` clean (workspace clippy plus the extended wasm gate), and
582 tests pass across kio and moq-mux.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
#2905)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Claude <noreply@anthropic.com>
… update (#2908)

Co-authored-by: Claude <noreply@anthropic.com>
…ference (#2906)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
…l clamps (#2940)

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Write down a policy the iroh page was silent on, and correct two things it
got wrong.

P2P is a local-network feature; across the internet, run a MoQ relay. The
page presented hole punching and the iroh relay as one feature, but they are
not the same trade. A direct path between two peers costs nothing and is the
point. The relay forwards opaque UDP through an n0 server, which is WebRTC's
TURN with none of what a MoQ relay buys for that same hop: it cannot cache a
group or serve a second viewer from the first one's fetch, and it adds
whatever latency its location costs.

Discovery is not local either. Both n0 presets install a PkarrPublisher, a
PkarrResolver, and (outside browsers) a DnsAddressLookup against n0's
iroh.link, and N0DisableRelay is N0 plus relay_mode(Disabled) and nothing
else. So enabling iroh publishes this endpoint's addressing record to a third
party whatever the relay setting; disabling the relay only changes what the
record holds. Resolution is the half a caller can avoid, via
Client::with_iroh_addrs; publishing has no equivalent escape hatch.

Disabling the relay costs more than the fallback. A relay is the rendezvous
both sides reach first, so it is also how they coordinate the punch, and its
probes are how an endpoint learns the public address it advertises. Turning
it off leaves direct addresses only. That is still right for two peers on one
link, who need none of the three, but it is unsafe as a blanket
recommendation: on a cloud VM behind 1:1 NAT it leaves a relay advertising a
private address nobody can route to. The relay config sample therefore stays
at the default with the trade written out.

Also: iroh:// and https:// are separate MoQ connections to a relay, with no
automatic fallback between them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Semantic conflicts resolved beyond the textual ones:

- js/net origin.ts: dev put the broadcast routing table there (#2705), main
  moved the origin *id* module there from lite/ (#2910). The table keeps
  origin.ts; the id module is now the internal js/net/src/hop.ts, shared by
  both wire protocols as main intended.
- js/watch broadcast.ts: dev rejects a whole catalog when a rendition's
  broadcast reference escapes the root (#2630); main hides renditions whose
  broadcast is not announced (#2918). Both kept. The announcement gate moved
  into #relativeTarget so playback and rendition selection cannot disagree
  about what is reachable, and filterCatalog now covers text renditions.
- js/net ietf publisher: main's options-object constructor plus dev's
  origin-backed broadcasts and main's cluster advert.
- moq-ffi session: main's wasm32 browser client alongside dev's reconnecting
  moq_tokio::Connection, with Inner::Connection, MoqBackoff, and the
  moq_tokio::Status conversion gated to native.
- moq-net ietf subscriber: main's Arrival parameter plus dev's GOAWAY drain
  cost.

web-transport-wasm 0.6 implements the poll traits moq-net requires
(moq-dev/web-transport#369), so the hand-written adapters in moq-wasm and
moq-ffi are gone; both files are now just the dial. This is what unblocks
moq-ffi's wasm32 build (#2911) under dev's poll-only transport (#2736).

Two of main's additions were written against APIs dev had already changed,
and merged cleanly because neither side touched the other's lines:

- test/wasm harness published through Established.publish, removed by #2705.
  It now publishes into an Origin and passes publish: origin.consume().
- test/wasm, rs/justfile, moq-bench's hd.toml, and the new iroh doc invoked
  --server-bind / --server-version / --client-connect, which #2915 refuses.

moq_net::model::resume::consecutive_updates_wake asserted an absolute wake
count. main's kio Park now reuses a still-registered waiter (#2905), so
applying a subscription change notifies a list the poll is parked on and
self-wakes once. That costs a redundant poll and nothing else, while a lost
wakeup parks the task forever, so the test measures the delta instead.

moq-mux's tdt_round_trips_as_latest_value fails here and on dev alike; it is
not a merge regression.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wFQ5wqKbvWwET3G5MJXY9
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, add credits to your account and enable them for code reviews in your settings.

…-dev-b9f056

# Conflicts:
#	rs/moq-ffi/Cargo.toml
#	rs/moq-ffi/src/ffi.rs
…o-dev-b9f056

# Conflicts:
#	doc/concept/layer/iroh.md
kixelated and others added 3 commits August 19, 2026 20:02
They cross the ABI as `uint32_t` so no signature reaches them, which is what
the ENUMS allowlist in build.rs exists to compensate for. `moq_video_format`
and `moq_container_format` were never added to it, so moq.h declared neither
and a C caller of `moq_publish_video` or `moq_publish_container` had to
hardcode the discriminants. cpp/obs did not, hence the build failure.

Nothing caught it because PR CI never compiled a C consumer until obs.yml
arrived from main.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wFQ5wqKbvWwET3G5MJXY9
#2927 split `publish_media_stream` into `publish_video_stream` and
`publish_container_stream`, and updated `py/moq-rs`, but not the smoke
client that calls it. Every `python -> *` case failed on the publisher with
`'BroadcastProducer' object has no attribute 'publish_media_stream'`.

smoke.yml only runs on a PR touching test/smoke, test/ts, test/justfile,
package.json, or itself, so #2927 never triggered it and the break has been
sitting on dev waiting for the nightly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wFQ5wqKbvWwET3G5MJXY9
The harness subscribes with the default `Latency::REAL_TIME` budget, which
drops a group the edge has moved past rather than delivering it late. #2890
made that enforcement real, so the publisher writing a group every 50ms can
now retire one the reader is still on and the read fails with `old`.

Requiring `previous + 1` and treating `old` as a case failure asserts a
guarantee real-time delivery does not make. Skips are accepted and bounded by
an attempt cap instead, so a subscription that never delivers a whole group
still fails; what a delivered group has to be, in order and byte-exact, is
unchanged.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017wFQ5wqKbvWwET3G5MJXY9
@kixelated
kixelated merged commit e11b4a1 into dev Aug 20, 2026
17 checks passed
@kixelated
kixelated deleted the claude/merge-main-into-dev-b9f056 branch August 20, 2026 06:59
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.

3 participants