diff --git a/quest/m0/README.md b/quest/m0/README.md index 000707c559a..5ecad752d36 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -2,8 +2,8 @@ ## Goal -The work in flight now: fixes for the `release` line, CI cost, and the -recurring audits. +The work in flight now: a passing Nightly, fixes for the `release` line, the +quest merge driver, and CI cost and latency. ## Plan @@ -18,11 +18,6 @@ serve instead of enumerating broadcasts. Serving the relay's ingested-only view (`origin::Consumer::local()`) to localhost workers belongs to moq.pro's edge, which embeds moq-relay; it moved there on 2026-09-28. -Hygiene (decided 2026-10-19): the audits sit at the end of this list. -Each recurs once per major release: its PR adds the next run to the following -milestone, gated from m2 onward on the [release cut](/quest/m2/release/cut.md). -Each lands its fixes in its own PR and plans only what needs a decision. - ## Required - [Nightly passes on main](/quest/m0/nightly/README.md) - the scheduled Nightly passes end to end, so red means a new break diff --git a/quest/m0/ci/README.md b/quest/m0/ci/README.md index 459dd36ab9d..52ad8f8eb9e 100644 --- a/quest/m0/ci/README.md +++ b/quest/m0/ci/README.md @@ -84,7 +84,8 @@ feature permutations and release-profile builds. Expensive and optional checks run nightly on `main` and are required on `release`. Faster runners come last, chosen from measurements of the optimized pipeline. Each CI pass costs twice per merged PR today (PR and queue), at 13 minutes median and 26 minutes p90 of -wall time per PR run. +wall time per PR run. Superseded in part 2026-10-24: the PR gate checks +feature shapes for the crates a branch changes; Nightly keeps the full matrix. Decided 2026-10-25 (maintainer): across moq's many CI and release builds (targets, features, FFI languages, Nix packages), each crate configuration diff --git a/quest/m0/ci/nix-cache-verify.md b/quest/m0/ci/nix-cache-verify.md index 0935c10a1da..2607b069630 100644 --- a/quest/m0/ci/nix-cache-verify.md +++ b/quest/m0/ci/nix-cache-verify.md @@ -14,15 +14,12 @@ release tag ruleset, both buckets and the Worker, the `cache-write` Environment's tag policies and secrets, and the signing key). Check it with `curl https://nix.moq.dev/nix-cache-info`. -Decided 2026-10-22 (maintainer): -[#6040](https://github.com/moq-dev/moq/pull/6040) deleted `cache.yml`, so -nothing pushes the dev shell to `nix.moq.dev`; only `release-nix.yml` pushes, -on release tags. First measure one hosted Nightly job's dev-shell entry time, -no earlier than 2026-11-20: `cache.yml`'s last push to `nix.moq.dev` ran -2026-10-20, and those paths serve a warm entry until the 30-day expiry -removes them. If it still compiles `uniffi-bindgen-go`, `uniffi-bindgen-cpp` and `quest` -(the 12-14 minute baseline below), restore a dev-shell push. Otherwise this -narrows to release-tag substitution and Cachix retirement. +[#6588](https://github.com/moq-dev/moq/pull/6588) restored `cache.yml` +after [#6040](https://github.com/moq-dev/moq/pull/6040) deleted it: every +merge to `main` pushes the dev shell's build closure for x86 Linux, ARM Linux, +and ARM macOS to `nix-ci-moq-dev` (`sh/gh/cache.sh`), and `release-nix.yml` +pushes on release tags. So the dev-shell entry time can be measured on any +scheduled Nightly now, and the expiry check below applies. Then confirm it, and record the numbers in the PR that closes this: diff --git a/quest/m0/nightly/README.md b/quest/m0/nightly/README.md index 058fab6dc8c..edf2de6001e 100644 --- a/quest/m0/nightly/README.md +++ b/quest/m0/nightly/README.md @@ -25,14 +25,14 @@ This README's own work, after the children: [audio jitter target](/quest/m1/audio-jitter-target/README.md), the macOS `receive_credit` timeout to [test flakes](/quest/m1/test-flakes-2/README.md), and the paired bench - threshold to [bench CI](/quest/m1/bench-ci.md). The `cli-export` - bufferbloat failure is expected to clear with - [moq-dev/moq#6656](https://github.com/moq-dev/moq/pull/6656). + threshold to [bench CI](/quest/m1/bench-ci.md). Public API: none. Wire: none. ## Required +- [moq-net compiles in Fuzz and send-depth](/quest/m0/nightly/send-overflow.md) - the ietf publisher future stops overflowing the Send check +- [The Python sdist builds](/quest/m0/nightly/py-sdist.md) - the sdist carries every workspace crate moq-ffi needs - [OBS audio-candidate test](/quest/m0/nightly/obs-audio-candidate.md) - the failed audio candidate check passes under TSan and load, fixed at its reproduced cause - [moq-net builds without async](/quest/m0/nightly/net-async-free.md) - the no-default-features lane stops pulling `async` back in - [Feature shapes per crate](/quest/m0/nightly/hls-poll-closed-gate.md) - moq-hls `Broadcaster::poll_closed` is gated on `server`, and the gate checks each crate's feature shapes in isolation diff --git a/quest/m0/nightly/py-sdist.md b/quest/m0/nightly/py-sdist.md new file mode 100644 index 00000000000..657a82ace5e --- /dev/null +++ b/quest/m0/nightly/py-sdist.md @@ -0,0 +1,17 @@ +# [XS] The Python sdist builds + +## Goal + +Nightly's Release Py FFI "Build sdist" job builds and installs the moq-ffi +sdist. + +## Plan + +Nightly [40192013303](https://github.com/moq-dev/moq/actions/runs/40192013303) +fails in maturin's `cargo metadata`: the sdist lacks +`rs/moq-ice/Cargo.toml`, which `moq-tokio` depends on since moq-ice was split +out. `pyproject.toml` lists the extra sdist paths by hand. Fix it so a new +workspace dependency cannot drop out of the sdist again, and keep the sdist +build in a lane that runs before release. + +Public API: none. Wire: none. diff --git a/quest/m0/nightly/send-overflow.md b/quest/m0/nightly/send-overflow.md new file mode 100644 index 00000000000..a84d5191d24 --- /dev/null +++ b/quest/m0/nightly/send-overflow.md @@ -0,0 +1,18 @@ +# [S] moq-net compiles in Fuzz and send-depth + +## Goal + +Nightly's six Fuzz jobs and Tests (rs send-depth) compile `moq-net` again. + +## Plan + +Nightly [40192013303](https://github.com/moq-dev/moq/actions/runs/40192013303) +(2026-10-25, at 66af72315b) fails both with "overflow evaluating the +requirement ... Send" on the async block at +`rs/moq-net/src/ietf/publisher/tracks.rs:826` (`run_publish` into +`resume_publish`). The PR gate does not build these configurations, so the +nesting grew unnoticed. Reproduce with the send-depth recipe, then make the +future shallower at its source (box or split the nested publish future) +rather than raising the recursion limit. + +Public API: none. Wire: none. diff --git a/quest/m1/README.md b/quest/m1/README.md index 09a88cacb2b..3e620566850 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -41,9 +41,11 @@ inline. Keep generic proofs here and deployments and their operational acceptance with the downstream operator. Keep one owner per shared code area and order prerequisites before consumers -(re-ranked in the 2026-10-20 audit: auth before cluster routing, the C line -before the binding breaks, and every lite-07 prerequisite before those -quests). +(re-ranked in the 2026-10-20 audit: auth before cluster routing, and every +lite-07 prerequisite before those quests). +Decided 2026-10-25 (maintainer): the hand-written `moq-c` ships at this cut +with parity to moq-ffi for every m1 binding feature, so each binding quest +lands its C half there; the generated C line moves after the cut. The moq-transport passthrough fields remain crate-private: no public moq-net item exists only for moq-transport (maintainer, 2026-10-23). @@ -87,7 +89,6 @@ force another major (`announce="never"`, the unused OBS outputs, binding durations, and the remaining web-transport-trait adapters) land before the cut, as do the small hostile-input and container fixes from the 2026-10-22 security audit, and the stuck-window scan. -Binding durations wait on Duration support in the generated C. Added 2026-10-24 (maintainer, from a moq-hls audit): import and export hardening (URL redaction, ranged responses, redirect bases, sequence @@ -112,6 +113,7 @@ surface and zero-copy texture import, land before the cut. ## Required +- [Resume checked against a model](/quest/m1/resume-model-test.md) - a randomized test checks every resumed reader against a one-track reference model in Rust and JS, and passes before the cut - [Release audit findings](/quest/m1/release-audit/README.md) - resolve newly identified resource, parser, playback, and interoperability risks before the cut - [Pre-cut audit findings](/quest/m1/cut-audit/README.md) - fix the crashes, hostile-input costs, and correctness bugs the 2026-10-24 audit of main found - [Cancelled moq-transport requests stop](/quest/m1/ietf-request-cancel.md) - a cancelled request releases its state even when the peer withholds credit @@ -119,9 +121,11 @@ surface and zero-copy texture import, land before the cut. - [In-band auth](/quest/m1/auth/README.md) - a session tells its peer what it may publish and subscribe to, unions tokens presented in band, and fails loud on an out-of-scope publish - [Cluster routing](/quest/m1/cluster-routing/README.md) - prove shared routing contracts, implement per-origin routes, and connect outbound local clusters - [Routing guide](/quest/m1/routing-guide.md) - concepts and practical outbound federation setup -- [Generated C bindings](/quest/m1/c/README.md) - C generated from moq-ffi ships as `moq-c` 0.8.0 and replaces the hand-written libmoq +- [moq-c at parity](/quest/m1/c/README.md) - the hand-written `moq-c` ships at the cut with every m1 binding feature moq-ffi gains +- [Lite-05/06 timestamps pass through](/quest/m1/lite-timestamps-through.md) - a timed track received over lite-05/06 stays timed and is forwarded with its timestamps over every version - [Exact timestamps in every binding](/quest/m1/binding-timestamps.md) - bindings carry `{value, scale}` timestamps instead of lossy microseconds - [Announcement takeover](/quest/m1/announce-takeover.md) - a new independent route explicitly continues instances served through its predecessor, with shared Rust/JS semantics and lite-07 wire +- [The transport may start a group mid-way](/quest/m1/transport-frame-offsets.md) - a hop may request and deliver a group from any frame it needs, while applications still ask for whole groups - [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) - a lite-07 SUBSCRIBE asks for past and live ranges with absolute or relative ends, in either order, and replaces FETCH; merged ranges never starve a subscriber; relays fill misses by range, including over moq-transport - [Drop set](/quest/m1/drop-set.md) - each cursor judges missing groups against its own budget, so a narrow reader drops promptly and a widening re-offers groups already reported dropped on lite-07 - [Broadcast archives](/quest/m1/archive/README.md) - ANNOUNCE, TRACKS, and a SUBSCRIBE per track record every track under a prefix to any object_store, a host serves a stored broadcast back, and the catalog drops its archive pointer for an opt-in `hls` field @@ -149,7 +153,7 @@ surface and zero-copy texture import, land before the cut. - [Buffered playback subscribes in ascending order](/quest/m1/buffered-ascending.md) - a buffered `` asks for the oldest group first, so a voice reply's opening arrives first, and audio's head hold shrinks to the floor - [TS fidelity and recovery](/quest/m1/ts-reliability/README.md) - both modes have explicit profiles, automated validation, loss tests, and a dedicated guide - [WebGPU on Safari](/quest/m1/webgpu-safari.md) - the WebGPU renderer is verified on Safari 26 for macOS and iOS -- [CI housekeeping](/quest/m1/ci-housekeeping.md) - validate queue nextest at 24 threads and remove the actionlint queue ignore after its release +- [CI housekeeping](/quest/m1/ci-housekeeping.md) - remove the actionlint queue ignore once a release after 1.7.12 reaches the dev shell - [Main is no slower than release](/quest/m1/release-perf/README.md) - every benchmark where main is slower than release is fixed or accepted with a reason, checked by a final paired run before the cut - [API Extractor checks](/quest/m1/api-extractor-checks.md) - API Extractor fails a PR on a forgotten export, an unresolved declaration, or an undocumented export, replacing the hand-rolled `.d.ts` checkers - [Generated JS API reference](/quest/m1/js-api-reference.md) - the doc site renders an API reference for every `@moq/*` package from API Extractor's doc model diff --git a/quest/m1/api-audit/README.md b/quest/m1/api-audit/README.md index 70464eaa864..6728c5acecc 100644 --- a/quest/m1/api-audit/README.md +++ b/quest/m1/api-audit/README.md @@ -35,8 +35,9 @@ Rules every child follows: counts only when that quest is in m1. - Inherited debt is fixed here only when it is cheap or sits on a surface this release breaks anyway. Otherwise it joins the later quest that - reshapes it (the ordered track reader joins - [foreign poll](https://github.com/kixelated/rust2/blob/main/quest/m1/ffi-poll-protocol.md)). + reshapes it (the ordered track reader joins rust2's + [exporter](https://github.com/kixelated/rust2/blob/main/quest/m1/ffi-export.md), + which absorbed its foreign poll quest on 2026-10-25). - Each child lists its public API and wire impact in its PR, and its Impact bullet says how to migrate. @@ -80,6 +81,7 @@ Rejected in planning: - [One lifecycle vocabulary](/quest/m1/api-audit/lifecycle.md) - finish, abort, closed, and close mean one thing on every handle, and Rust and JS observers fire alike - [moq-net names say one thing](/quest/m1/api-audit/net-names.md) - demand says `wanted`, colliding stats and transport names are renamed, and Rust and JS info lookups mirror - [Request handlers are named requests](/quest/m1/api-audit/requests.md) - `dynamic()` and `Dynamic` become `requests()` and `Requests` at every layer +- [Demand is wanted, everywhere](/quest/m1/api-audit/wanted.md) - every `used`/`unused` pair becomes `wanted()` and `wanted_changed()`, and unused ones go - [One meaning for Credential](/quest/m1/api-audit/dial-credential.md) - the internal dial type gets its own name in JS and moq-tokio, and the `Presented` alias goes ## Related diff --git a/quest/m1/api-audit/binding-publication.md b/quest/m1/api-audit/binding-publication.md index 21d83165e8f..4d6f384a9b0 100644 --- a/quest/m1/api-audit/binding-publication.md +++ b/quest/m1/api-audit/binding-publication.md @@ -13,8 +13,9 @@ prepared broadcast no longer exposes announcement calls that can only fail. Decided 2026-10-23 (maintainer, on [moq-dev/moq#6464](https://github.com/moq-dev/moq/pull/6464)): ✅ land the announcement split in three PRs: native Rust in that PR, the -[JS mirror](https://github.com/moq-dev/moq/pull/6795), and this binding rollout -once the native PR merges. The native PR keeps private adapters that hold the +[JS mirror](https://github.com/moq-dev/moq/pull/6795), and this binding rollout. +Both merged on 2026-10-25, so this quest can start. The native PR kept +private adapters that hold the `Publication` beside each binding fill handle (`rs/moq-ffi/src/producer.rs` `from_publication`, `rs/moq-c/src/publish.rs` `create_publication` and `admit`); this quest replaces them with public handles. @@ -42,4 +43,13 @@ Test in each layer: clone/drop, explicit unannounce, prepared admission (including a mismatched fill), and a stale publication against a replacement. Run `just test interop --all`. +Decided 2026-10-25 (maintainer): the hand-written moq-c ships at this cut +with parity with moq-ffi for every m1 binding feature, so it takes this break +too; it is not frozen. The generated C line moves after the cut and waits for +rust2. + +[moq-dev/moq#6740](https://github.com/moq-dev/moq/pull/6740) already made +closing a binding broadcast end its tracks; the public handles keep that +`MoqBroadcastProducer` and `moq_publish_*` close path in every wrapper. + Public API: breaking in moq-ffi, the C ABI, and every wrapper. Wire: none. diff --git a/quest/m1/api-audit/requests.md b/quest/m1/api-audit/requests.md index de9cf1127bf..ec5e03df6e9 100644 --- a/quest/m1/api-audit/requests.md +++ b/quest/m1/api-audit/requests.md @@ -13,8 +13,9 @@ mechanism; `auth::Handle::requests() -> auth::Requests` is the precedent. - Rename `{origin,broadcast,track}::Producer::dynamic()` (and `track::Request::dynamic()`) to `requests()`, and each `Dynamic` type to - `Requests`. JS `dynamic()`/`Dynamic` mirror it. moq-ffi and its wrappers - follow; the frozen hand-written moq-c ABI stays as it is. + `Requests`. JS `dynamic()`/`Dynamic` mirror it. moq-ffi, its wrappers, + and moq-c (`moq_origin_dynamic*`) follow, since moq-c takes the same breaks + (decided 2026-10-25). - Rename the crate-private peer-route hatches (`dynamic_route`, `update_route`) with the rest. - Quests that say `Dynamic` (subscribe ranges, archive) switch @@ -22,4 +23,4 @@ mechanism; `auth::Handle::requests() -> auth::Requests` is the precedent. About 240 Rust, 30 binding, and 10 JS call sites; land it in one sweep. -Public API: moq-net, `@moq/net`, moq-ffi and wrappers renamed. Wire: none. +Public API: moq-net, `@moq/net`, moq-ffi, wrappers, and moq-c renamed. Wire: none. diff --git a/quest/m1/api-audit/wanted.md b/quest/m1/api-audit/wanted.md new file mode 100644 index 00000000000..e0c4d573bd2 --- /dev/null +++ b/quest/m1/api-audit/wanted.md @@ -0,0 +1,47 @@ +# [M] Demand is wanted, everywhere + +## Goal + +Every demand signal reads as `wanted`: a snapshot `wanted()` plus an async +`wanted_changed()` (and its poll), where "not wanted" is `false` or `None`. +No `used`/`unused`/`is_used` methods remain in kio, moq-net, `@moq/net`, +moq-ffi, moq-c, or the wrappers, and methods without a consumer are deleted +instead of renamed. + +## Plan + +Decided 2026-10-25 (maintainer), extending +[moq-net names](/quest/m1/api-audit/net-names.md)'s "Demand says `wanted`": + +- Yes/no demand returns `bool`; `track::Demand::wanted() -> Option` + keeps its meaning. +- Broadcast demand is deleted: Rust's `broadcast::Demand` went in + [#6744](https://github.com/moq-dev/moq/pull/6744), and the `@internal` JS + `Broadcast.Demand` (read only by tests and a bench) goes too. The origin + closes a superseded producer explicitly instead of a publisher watching + demand. +- `group::Demand` has no production consumer in Rust; delete it rather than + convert it, unless net-names lands the conversion first. The JS group + demand stays internal. +- `bandwidth::Producer` gets a `demand()` handle with `wanted() -> bool` and + `wanted_changed()`, like track. +- `track::Fill` merges `is_used()` and `next()` into + `wanted() -> Option` (the next group owed), with `wanted_changed()` + replacing `unused()`. Check that it returns `None` once no reader is left. +- kio `Producer`/`ProducerWeak` rename too: `wanted() -> bool` and + `wanted_changed()`; delete the test-only async variants. +- The Announce consumer count from + [lifecycle](/quest/m1/api-audit/lifecycle.md) is a `wanted`-shaped signal, + not a new `unused()`. +- moq-c's `MOQ_DEMAND_USED/UNUSED` become `WANTED/UNWANTED`; moq-c takes the + break with every binding. + +Fix the stale `is_used()` advice in `rs/moq-json/MIGRATE.md`, +`rs/moq-flate/MIGRATE.md`, and the Go wrapper README. + +Public API: breaking renames and deletions in every language. Wire: none. + +## Required + +- [moq-net names say one thing](/quest/m1/api-audit/net-names.md) - lands group demand's first `wanted` shape +- [One lifecycle vocabulary](/quest/m1/api-audit/lifecycle.md) - settles close and demand semantics this renames diff --git a/quest/m1/binding-timestamps.md b/quest/m1/binding-timestamps.md index c3323e887ad..2eb693c580e 100644 --- a/quest/m1/binding-timestamps.md +++ b/quest/m1/binding-timestamps.md @@ -33,8 +33,8 @@ Decided with the maintainer on 2026-10-18: - **#5677 shipped** the data-value timestamps with the interim microsecond fields; this quest converts every site at once, `MoqJsonValue` included. - **Shape mirrors the core:** one `{value, scale}` per value, `value` bounded - as `Timestamp::new` bounds it (`2^61 - 1` once - [#6168](https://github.com/moq-dev/moq/pull/6168) lands, decided 2026-10-22) + as `Timestamp::new` bounds it (`2^61 - 1` since + [#6168](https://github.com/moq-dev/moq/pull/6168), decided 2026-10-22) and `scale` non-zero and at most `2^62 - 1`, matching `Timescale::new`. The FFI boundary refuses anything else with an error. Rejected: raw ticks at the track scale beside `MoqTrackInfo.timescale`, because a producer argument needs its own scale @@ -57,13 +57,10 @@ Verification: round-trip a 90 kHz tick, a present zero, and untimed through moq-ffi, moq-c, C++ and every wrapper's tests; refuse scale 0, an out-of-range scale, and an out-of-range value; `just test interop --all`. -Decided 2026-10-20 (maintainer): the moq-c half lands on the -[generated C package](/quest/m1/c/README.md), not the frozen hand-written -ABI, so C users break once. +Decided 2026-10-25 (maintainer), reversing the 2026-10-20 decision to wait +on the generated C package: the hand-written `moq-c` ships at this cut with +parity to moq-ffi for every m1 binding feature, so the moq-c half lands +there, and the generated C line follows after the cut. Wall and monotonic times stay `moq_clock::Wall` and `moq_clock::Instant`, not media timestamps; keep the types distinct. - -## Required - -- [Generated C bindings](/quest/m1/c/README.md) - `moq_timestamp` reaches C through the generated package, so C users break once diff --git a/quest/m1/c/README.md b/quest/m1/c/README.md index 1722d4c5785..48f0e8efa0d 100644 --- a/quest/m1/c/README.md +++ b/quest/m1/c/README.md @@ -1,72 +1,32 @@ -# Generated C bindings +# [M] moq-c at parity for the cut ## Goal -C programs use moq through a C API generated from moq-ffi, shipped as the -`moq-c` package with CMake target `moq::c`, so C replaces the hand-written libmoq ABI and tracks every moq-ffi change the way Go, Swift, Kotlin, Python, -Dart, and C++ already do. The C API shape follows moq-ffi and breaks C users -once. The C++ package (`cpp/moq`) and the OBS plugin are out of scope; they -already sit on moq-ffi through the generated C++. +The hand-written `moq-c` crate (`rs/moq-c`, `moq.h`) ships at this cut as +`moq-c` 0.7 with every binding feature moq-ffi gains in m1, so a C user can do +whatever a Python, Swift, Kotlin, Go, Dart, or C++ user can. The C interop +client and every `doc/lib/c` sample build and run against the built release +archive before the cut. ## Plan -Decided: +Decided 2026-10-25 (maintainer): the hand-written crate ships at this cut and +the [generated C line](/quest/m2/c/README.md) moves after it, waiting on the +switch to rust2's UniFFI fork. The generated C is not built in CI today and +needs a generator fix first, so it must not block the release. C users break +twice (0.6 to 0.7, then to the generated 0.8); that is the accepted cost. -- The generator is a C backend in the `kixelated/uniffi-bindgen-cpp` fork, - beside the C++ one, so both share its parsing, async, and error plumbing and - one pinned tool covers both. No mature upstream C generator exists; uniffi - itself only emits the low-level scaffolding header. -- Async calls take a completion callback and user data and return a task; - freeing the task cancels the call. Callbacks run on moq's dispatcher thread - by default, or on the host's own loop through `moq_set_dispatcher` and - `moq_work_run`, the C form of `moq::set_executor`. Streams are repeated calls. - This removes the trampolines and lock-order rules libmoq's single callback - thread forced on hosts like OBS. -- Errors and records mirror the C++ package: calls return `MoqError *` - (NULL on success) with `moq_error_message()`, results come through - out-params, and records are owned structs with `moq__free`. Names - follow GLib: CamelCase types and snake_case functions. -- The header is C11: errors and data enums are a `tag` plus an anonymous union - (`err->protocol`). Infallible calls return their value directly, optional - scalars are `{has_value, value}`, each async function has its own callback - typedef, and unnamed variant fields are `v1`, as in C++. -- #4288 already renamed the hand-written crate to - `moq-c` (`rs/moq-c`), and the generated package takes over that name - and `moq::c` target, so C users migrate once. Its first release is 0.8.0, a - minor bump over the hand-written 0.7.x. -- Docs change inline: the consumer quest rewrites `doc/lib/c`, and its - Impact bullet carries the move for the package's `MIGRATE.md`. No separate - guide. +- moq-c is not frozen: it takes the same breaks as moq-ffi and the other + bindings. Every m1 quest or PR that changes the binding API changes + `rs/moq-c` in the same PR, with its `MIGRATE.md` entry and `doc/lib/c` + update; none waits on the generated package. +- Before the cut, diff the moq-ffi exports against `moq.h` and add any m1 + binding feature still missing, so parity is checked rather than assumed. +- `rs/moq-c` publishes through release-plz like the other crates; confirm the + release produces the C archive the samples are checked against. -The line owns the pre-cut check: every `doc/lib/c` sample and the C interop -client build and run against the built release archive, not only in-tree. -[Published archive proof](/quest/m2/release/c-publish.md) repeats it after -publication; [old-crate retirement](/quest/m2/release/c-retire.md) follows -that publication and is not a prerequisite of this preparation. - -The hand-written crate gets no more feature work: its shutdown, CMake library, -and fetch quests were abandoned for this line, and hidden is done. -Decided in the quest audit: [claim epochs](https://github.com/moq-dev/moq/pull/5678) -is a narrow correctness exception. Its existing claim API migrates with -Rust and JS so C cannot retain the unsafe accept path while this line waits. -That change owns its C tests and migration notes; other API reshaping stays here. - -Decided 2026-10-20 (maintainer): m1 quests that change the C API -([exact binding timestamps](/quest/m1/binding-timestamps.md) -and [Rust identity enforcement](/quest/m1/catalog-identity-rs.md)) Require this -line and land their C half on the generated package, so C users break once. -Signed priorities already reached the hand-written ABI as `int8_t` in -[moq-dev/moq#6142](https://github.com/moq-dev/moq/pull/6142). -Dynamic subscribe ranges ([moq-dev/moq#6171](https://github.com/moq-dev/moq/pull/6171)) -was the settled exception from its later plan in -[moq-dev/moq#6123](https://github.com/moq-dev/moq/pull/6123): generation -had not landed, so the frozen C crate splits fills into its existing per-group -requests internally and keeps its ABI. +Public API: moq-c gains each m1 binding feature. Wire: none. ## Required -- [One C name per moq-ffi item](/quest/m1/c/name-clash.md) - rust2's C generator names types in CamelCase and functions in snake_case, so the record `MoqAudioEncoderFinish` and the method `moq_audio_encoder_finish` never share a name -- [moq-c package](/quest/m1/c/package.md) - the generated header ships as `moq-c` 0.8.0 with `moq::c`, pkg-config, and a release workflow -- [C consumers](/quest/m1/c/consumers.md) - the C interop client and `doc/lib/c` samples move onto the generated API -- [Async failure contract](/quest/m1/c/async-failure-contract.md) - a generated async call that fails early fires no callback, and one that started always delivers its terminal callback - [moq-c decode start](/quest/m1/c/decode-start.md) - a moq-c decoder can read a finished track from its start diff --git a/quest/m1/catalog-identity-rs.md b/quest/m1/catalog-identity-rs.md index 0aa0f2e26e1..6b0e6f99492 100644 --- a/quest/m1/catalog-identity-rs.md +++ b/quest/m1/catalog-identity-rs.md @@ -5,7 +5,7 @@ No Rust publisher redefines a track it has published. A change to a rendition's identity, the fields a consumer cannot change without reinitializing its decoder or demuxer (`codec`, `description`, `container`, -`display_aspect_width`/`display_aspect_height`, `sample_rate`, +`display_aspect_width`/`display_aspect_height`, `interlaced`, `sample_rate`, `channel_count`), publishes a new rendition name, per the hang draft's [Track Identity](/drafts/draft-lcurley-moq-hang.md#identity) section. Every other field, including coded size, `bitrate`, and `framerate`, updates in @@ -50,14 +50,16 @@ minting a name where the identity changes and updating in place otherwise: when the codec string differs. - `moq-video` reopening onto another camera mode or backend (`source_resize_updates_the_published_rendition` asserts the rewrite). -- `moq-c` re-declaring a config under the same name, and `moq-gst` reusing an - explicit pad name after caps renegotiation. +- `moq-c` re-declaring a config under the same name. - TS AAC/MP2/AC-3 and FLV legacy audio fix the config from the first header and silently ignore a later change; refuse or mint instead. A PMT that changes a PID's stream type or descriptors already opens a new track. - `moq-audio`/`moq-ffi` `set_bitrate` should update the catalog's bitrate. -FLV, fMP4, MKV, HLS import, and `moq-transcode` rungs already comply; keep +FLV, fMP4, MKV, HLS import, `moq-transcode` rungs, and the `moq-gst` sink +(since [moq-dev/moq#6780](https://github.com/moq-dev/moq/pull/6780), which +keeps the live producer for caps that build the same importer and mints +`/` otherwise) already comply; keep their tests. Update the tests that assert in-place identity rewrites (`tracks.rs`, `moq-video`, `moq-gst`, `moq-c`); a same-name resize, such as the transcode fixture's, is now allowed. @@ -66,18 +68,15 @@ Later, at lower priority: watch and HLS export consumer coverage for in-band resolution changes, rendition switches, reconnects, and late joiners against the contract. -Decided 2026-10-20 (maintainer): the moq-c half lands on the -[generated C package](/quest/m1/c/README.md), not the frozen hand-written -ABI, so C users break once. +Decided 2026-10-25 (maintainer), reversing the 2026-10-20 decision to wait +on the generated C package: the hand-written `moq-c` ships at this cut with +parity to moq-ffi for every m1 binding feature, so the moq-c fix lands there, +and the generated C line follows after the cut. The branch checkpoint `origin/quest/m1/catalog-identity-rs` predates the 2026-10-15 decision to mint a new rendition name, so rework it against that decision rather than resuming it as is. -## Required - -- [Generated C bindings](/quest/m1/c/README.md) - the moq-c config re-declaration fix lands on the generated package, not the frozen hand-written ABI - ## Related - [Broadcast archives](/quest/m1/archive/README.md) - an archived catalog describes every stored group by the identity contract diff --git a/quest/m1/cut-audit/aac-description-less.md b/quest/m1/cut-audit/aac-description-less.md index 1ee0d458942..8f0bc3161c6 100644 --- a/quest/m1/cut-audit/aac-description-less.md +++ b/quest/m1/cut-audit/aac-description-less.md @@ -18,12 +18,9 @@ AudioSpecificConfig and decodes the frames as raw, and `rs/moq-mux/src/container/ts/export.rs:3823` treats them as raw and adds ADTS headers. Whichever form a publisher sends, half the consumers break. -Decision for the maintainer, with a recommendation: - -1. **ADTS (recommended).** Matches WebCodecs, the browser path most viewers - use. moq-audio parses ADTS headers and TS export passes the frames - through. -2. Raw access units. JS and moqsrc must synthesize a description. +Decided 2026-10-25 (maintainer): ADTS, matching WebCodecs, the browser +path most viewers use. moq-audio parses ADTS headers and TS export passes +the frames through. State the choice in `drafts/draft-lcurley-moq-hang.md` and add a test per consumer with a description-less fixture. Check which first-party diff --git a/quest/m1/cut-audit/catalog-parity.md b/quest/m1/cut-audit/catalog-parity.md index 4eb9cc5295a..6657975057b 100644 --- a/quest/m1/cut-audit/catalog-parity.md +++ b/quest/m1/cut-audit/catalog-parity.md @@ -24,13 +24,12 @@ in both languages, inherited. `js/hang/src/catalog/video.ts:129,146,229`, `audio.ts:110`, `container.ts:15`. -Decision for the maintainer, with a recommendation: **strict and per -rendition (recommended)**. Both languages validate codec strings and -dimension ranges the same way, refuse zero rates and channel counts, drop -the legacy array form in Rust's direction (or keep it in both), and skip a -malformed rendition while keeping the rest of the catalog. The alternative -is to keep each parser and only document the differences. Add a shared -fixture corpus both test suites load, so a future divergence fails CI. +Decided 2026-10-25 (maintainer): strict and per rendition. Both languages +validate codec strings and dimension ranges the same way, refuse zero rates +and channel counts, and skip a malformed rendition while keeping the rest of +the catalog. Strict means JS drops the legacy `video:[{track,config}]` array +form, as Rust already refuses it. Add a shared fixture corpus both test +suites load, so a future divergence fails CI. Also check the container consumer's skip measurement: Rust measures newest content minus the next group's start (`rs/moq-mux/src/container/consumer.rs:576`), diff --git a/quest/m1/cut-audit/datagram-size.md b/quest/m1/cut-audit/datagram-size.md index 87b10bbc211..1ed51de285b 100644 --- a/quest/m1/cut-audit/datagram-size.md +++ b/quest/m1/cut-audit/datagram-size.md @@ -20,12 +20,9 @@ larger QUIC limit we send and accept a 1300-byte body that a relay then cannot forward onto a 1200-byte hop, so it vanishes mid-path. [E2EE](/quest/m2/e2ee/README.md) sizes its plaintext on the 1200 cap. -Decision for the maintainer, with a recommendation: +Decided 2026-10-25 (maintainer): enforce 1200 end to end. Cap sends at the +smaller of 1200 and the transport limit, and drop larger bodies on receive, +in both languages. A per-hop limit was rejected: it loses datagrams silently +mid-path. -1. **Enforce 1200 end to end (recommended).** Cap sends at the smaller of - 1200 and the transport limit, and drop larger bodies on receive, in both - languages. Predictable across any path, matches the draft and E2EE. -2. Relax the draft to a per-hop limit and size E2EE per hop. Larger - datagrams on good paths, at the cost of silent mid-path loss. - -Public API: none. Wire: none (option 1 matches the draft as written). +Public API: none. Wire: none (matches the draft as written). diff --git a/quest/m1/cut-audit/lite-wire-nits.md b/quest/m1/cut-audit/lite-wire-nits.md index fde495704b9..d386e940003 100644 --- a/quest/m1/cut-audit/lite-wire-nits.md +++ b/quest/m1/cut-audit/lite-wire-nits.md @@ -28,14 +28,6 @@ Found by the 2026-10-24 pre-cut audit at 151a529670, all source-inferred. 64 KiB SETUP bound already limits the count. - **Draft text:** the Fetch section mentions an "implicit" Subscribe ID that FETCH does not have, and the `(s)`/`(b)` notations are never defined. -- **Partial groups (decision):** a lite subscription GROUP with - `frame_start > 0` is cached as a group tail (`lite/subscriber.rs:1033`) so a - front can continue a group across routes, while `rs/moq-net/AGENTS.md` - says never to emit or accept partial groups, except for moq-transport - passthrough. Recommendation: keep the code, since resume depends on it, - and ask the maintainer to add route continuation to that rule's - exceptions. Agents never edit `AGENTS.md` unprompted, so leave this under - `## Decisions needed` if the maintainer has not answered. Validate with `just drafts check`. diff --git a/quest/m1/cut-audit/mkv-import-errors.md b/quest/m1/cut-audit/mkv-import-errors.md index 0bf1210e80c..e78f21e638f 100644 --- a/quest/m1/cut-audit/mkv-import-errors.md +++ b/quest/m1/cut-audit/mkv-import-errors.md @@ -17,8 +17,9 @@ Found by the 2026-10-24 pre-cut audit at 151a529670, source-inferred. avcC/hvcC, or a missing AAC CodecPrivate drops the video track and the catalog goes out audio-only. Every validation fix has had to extend the allowlist by hand ([moq-dev/moq#6532](https://github.com/moq-dev/moq/pull/6532)). - Return `Ok` only for an unsupported role or codec, decided where the error - is raised, and propagate everything else; delete the allowlist. + Return `Ok` only for an unsupported role, decided where the error is + raised, and propagate everything else, an unsupported codec included; + delete the allowlist. - A duplicate `TrackNumber` (`:452`) replaces the earlier track. Refuse it. - `cluster_timestamp as i64 + block.timestamp` (`:533`) can overflow (a wrap in release builds, misreported as `NegativeBlockTimestamp`), and @@ -28,12 +29,9 @@ Found by the 2026-10-24 pre-cut audit at 151a529670, source-inferred. `(r.read(16)? + 1) as u16`, so a 65536 dimension becomes 0 in the catalog. Use `u32`, like the catalog field. -Decision for the maintainer: MKV and FLV (`flv/import.rs` about lines 215, -302, 359, 431) drop unsupported codecs by design, which contradicts -`rs/moq-mux/AGENTS.md` ("Reject an unsupported codec ... with a typed -Error"). Recommendation: keep dropping unsupported codecs in container -imports (a file with a subtitle or an exotic audio track should still -import its video) and ask the maintainer to scope that rule to codec -importers. Leave it under `## Decisions needed` if unanswered. +Decided 2026-10-25 (maintainer): reject an unsupported codec everywhere, as +`rs/moq-mux/AGENTS.md` says. MKV and FLV (`flv/import.rs` about lines 215, +302, 359, 431) stop dropping such tracks and fail the import with a typed +Error. Public API: none. Wire: none. diff --git a/quest/m1/ietf-request-cancel.md b/quest/m1/ietf-request-cancel.md index 41839501095..d7dbdc1c801 100644 --- a/quest/m1/ietf-request-cancel.md +++ b/quest/m1/ietf-request-cancel.md @@ -3,20 +3,33 @@ ## Goal Cancelling a moq-transport SUBSCRIBE, FETCH, TRACK_STATUS, or namespace -request releases its state at once, even when the peer withholds send -credit. +request releases its state at once, even when the peer's send window holds +back the request's first message. ## Plan Every request write in `rs/moq-net/src/ietf/subscriber.rs` awaits its first -message without watching for cancellation, so a peer that withholds credit -pins a cancelled request. -Lazy discovery ([moq-dev/moq#6530](https://github.com/moq-dev/moq/pull/6530)) -fixed only the stream-open race. On drafts 16 and up, reset a partially written request -stream. Drafts 14 and 15 share the ordered control stream, so a partial -message cannot be abandoned; decide there whether to finish the message and -cancel right after, and record the choice here. +`encode_message` without watching for cancellation, so a peer that withholds +flow-control credit pins a cancelled request. +[moq-dev/moq#6409](https://github.com/moq-dev/moq/pull/6409) already refuses +a request at once when stream credit is exhausted, but an admitted header +still waits for send capacity, and +[moq-dev/moq#6530](https://github.com/moq-dev/moq/pull/6530) landed the Rust +lazy discovery that rewrote these writes. What remains is the awaited first +encode under send-window pressure: watch for cancellation there and reset the +partially written request stream. -Test with a peer that never grants credit, on each draft family. +Decided 2026-10-25 (maintainer): drop support for the older drafts rather +than handle them here. Drafts 14 and 15 shared one ordered control stream, so +a partial message could not be abandoned; once +[Drop moq-transport 14-17](/quest/m1/ietf-drop/d14-17.md) lands, every +remaining draft gives each request its own stream, and only the reset path +is needed. + +Test with a peer that never grants send credit, on drafts 18 and 22. Public API: none. Wire: none. + +## Required + +- [Drop moq-transport 14-17](/quest/m1/ietf-drop/d14-17.md) - leaves only per-request streams, so a cancelled request is always a stream reset diff --git a/quest/m1/ietf-takeover.md b/quest/m1/ietf-takeover.md index 725a62b608b..69d98edf886 100644 --- a/quest/m1/ietf-takeover.md +++ b/quest/m1/ietf-takeover.md @@ -20,7 +20,7 @@ Give the new route its own ID and independent lifetime. Only matching served instances are adopted, with the shared `Takeover` event. Response identity alone never bypasses a requested epoch's matching-announcement requirement. -Negotiate support explicitly across the extension's drafts 17-22; an older +Negotiate support explicitly on the extension's drafts 18 and 22; an older implementation of the epoch extension must not receive an unknown operation. Keep epoch and cluster negotiation independent, with unambiguous framing for epoch-only and combined sessions. No changes to unnegotiated published drafts. diff --git a/quest/m1/interop-coverage/README.md b/quest/m1/interop-coverage/README.md index 28c7a4df5d6..9e3e2800e48 100644 --- a/quest/m1/interop-coverage/README.md +++ b/quest/m1/interop-coverage/README.md @@ -13,11 +13,10 @@ Boundaries: - Only what moq-net can do. Subgroups other than 0, rendezvous, and per-request authorization tokens are not exposed, so they are out of - scope, and runner cells that need them stay red by design. PUBLISH (push) - and outbound forward=0 join coverage now that SUBSCRIBE_TRACKS outbound - ([#6280](https://github.com/moq-dev/moq/pull/6280)) and IETF subscribe - pause ([#6299](https://github.com/moq-dev/moq/pull/6299)) have landed - (decided 2026-10-22, maintainer). + scope, and runner cells that need them stay red by design. +- PUBLISH (push) and outbound forward=0 are out of scope (decided + 2026-10-25, maintainer, reversing the 2026-10-22 decision): no child + quest owned them, and the cases above carry the interop bug history. - Media stays hang. No MSF catalog in our runner clients. - New cases live in-tree (`test/interop`), not in the runner's spec. - Not covered: mid-subscription updates (`Subscription::update` in Rust and diff --git a/quest/m1/js-fetch.md b/quest/m1/js-fetch.md index cd5c5024d02..8227d1cc1ea 100644 --- a/quest/m1/js-fetch.md +++ b/quest/m1/js-fetch.md @@ -1,4 +1,4 @@ -# [M] Serve IETF FETCH in JavaScript +# [L] Serve IETF FETCH in JavaScript ## Goal @@ -29,13 +29,53 @@ The JS publisher already answers the TRACK_STATUS a native fetch-only request asks before its first FETCH, so after that answer the FETCH is all that is refused today. +Decided 2026-10-25 (maintainer): the joining FETCH lands here, folded from +the m2 JS joining FETCH quest, whose remaining part (answering a FETCH after +the subscription ends) stays in +[JS join after a quick finish](/quest/m2/ietf-js-joining-fetch.md). Today +`js/net/src/ietf/publisher.ts` refuses every FETCH, ignores the filter before +draft 22, and serves the latest group whole from object 0, rounding +SUBSCRIBE_OK's Largest to `{G, 0}`. The two parts below only work together; +[moq-dev/moq#6407](https://github.com/moq-dev/moq/pull/6407) made a refused +joining FETCH cost the Rust subscriber only that group's head. + +1. **Honor Largest Object before draft 22.** `subscribeRange` serves the + `nextObject` filter from `edge.next`, as draft 22 already does, and + SUBSCRIBE_OK reports the true Largest on every draft. Otherwise the + FETCH's head duplicates the subscription's first objects, and the Rust + subscriber's "a whole group arrived for one the fill already headed" path + truncates the group to the head. JS subscribers send `unfiltered` before + draft 22 and are unaffected. A third-party Largest Object subscriber that + sends no joining FETCH then loses the head, as the draft says it should. +2. **Serve the head.** Keep a per-session registry of joinable SUBSCRIBEs: + their track, the advertised Largest, the SUBSCRIBE_OK properties and + timescale, the ranking, and the unsubscribe signal. `#runFetch` resolves a + joining FETCH against it: unknown subscription is + `invalid_joining_request_id`, no Largest or a start past it is + `invalid_range`, more than one group is `not_supported` (as in Rust and + the draft-22 fill), and a head evicted from the cache is refused before + FETCH_OK. Then it writes FETCH_OK (end `{G, k + 1}`, not end of track) and + a fetch stream named by the FETCH's own request ID, reusing + `#writeFillGroup`. It is not one of the subscription's counted streams. + Codec work: `Fetch` keeps the joining fields; a `FetchOk` encoder for + drafts 14 to 18 (request ID through 16, GROUP_ORDER on 14, empty + parameters, Track Properties from 15); `FetchFrame` writes draft-14's + explicit object layout and the status after an empty payload on 14 and 15. + +Joining tests: unit tests per draft for each refusal and for a served head +(bytes the Rust subscriber stitches), and the tail cells +`just test interop --tail --publishers js-native-node,js-native-bun --subscribers rust --versions ietf` +plus the browser publisher on drafts before 22, except a track that finishes +right after SUBSCRIBE_OK, which the m2 quest covers. + Verify with an in-memory application responder: a browser publisher serves a native IETF subscriber after a group is evicted or was never cached. Run the supported-draft matrix and `just test interop --all` through CI. Public API: none beyond JS ranges' surface. Wire: implement the existing -supported IETF FETCH formats; update relevant documentation and any MoQ draft -claims that change. +supported IETF FETCH formats, including the joining FETCH, and serve a +Largest Object subscription from past Largest; update relevant documentation +and any MoQ draft claims that change. ## Required @@ -44,3 +84,4 @@ claims that change. ## Related - [IETF fill timeout](/quest/m1/ietf-fill-timeout.md) - native timeout budgets and gap dispositions build on this responder +- [JS join after a quick finish](/quest/m2/ietf-js-joining-fetch.md) - answers a joining FETCH that arrives after its subscription ended diff --git a/quest/m1/lite-timestamps-through.md b/quest/m1/lite-timestamps-through.md new file mode 100644 index 00000000000..f9573e51c0b --- /dev/null +++ b/quest/m1/lite-timestamps-through.md @@ -0,0 +1,40 @@ +# [M] Lite-05/06 timestamps pass through + +## Goal + +A timed track received over moq-lite-05/06 keeps its frame and datagram +timestamps, and a relay forwards them over every version, so a lite-06 +reader of a timed feed (such as a stats feed) sees timed frames instead of +untimed ones. Partly reverses +[moq-dev/moq#6150](https://github.com/moq-dev/moq/pull/6150), which drops +every lite-05/06 timestamp a receiver reads. + +## Plan + +Decided 2026-10-25 (maintainer): "We dropped lite timestamps because they +defaulted to wall clock timestamps, but we should pass them through instead." + +- A track received over lite-05/06 is timed and marked crate-private + `unchecked`, like a moq-transport one: its groups skip the + [moq-dev/moq#5803](https://github.com/moq-dev/moq/pull/5803) rewind check + on ingress, because an untimed origin stamps send times at serve time and + an older group fetched later would look like a rewind. +- Unchecked tracks, lite-05/06 and moq-transport alike, go out with their own + timestamps over every version: lite-05/06, lite-07, and moq-transport. A + moq-transport track served to a lite-05/06 peer stops being stamped with + send times. +- A lite-07 receiver keeps checking every received group (no wire flag), so a + send-time origin's out-of-order group may be refused downstream; rewinds + are unsupported, and the cost was accepted. +- A lite-05/06 sender serving an untimed track keeps the published rule: + timescale 1000 and each payload's send time in milliseconds. Receivers now + pass that through as the track's time. + +Update the draft's lite-05/06 receiver text and changelog, +`doc/concept/moq-lite.md`, and the `MIGRATE.md` entries #6150 added, in Rust +and `@moq/net` alike. Test a lite-06 hop between two relays: a timed track +arrives timed with its values, and an out-of-order fetched group from an +untimed origin is accepted on lite-05/06. + +Public API: none (crate-private `unchecked`). Wire: none for lite-05/06; +receiver behavior changes, and the lite draft text says so. diff --git a/quest/m1/release-audit/README.md b/quest/m1/release-audit/README.md index a9e8a490375..5db229f9867 100644 --- a/quest/m1/release-audit/README.md +++ b/quest/m1/release-audit/README.md @@ -77,7 +77,9 @@ reordered-frame timestamps and missing EOF output. Each owning quest records the reproduction's limits. JS reader and publisher datagram paths now enforce subscription bounds, and the OBS source now stamps each decoded picture with its own timestamp and drains at clean EOF. -Go reservation disposal remains an investigation. +Go reservation disposal was settled by +[moq-dev/moq#6424](https://github.com/moq-dev/moq/pull/6424): `Update(0)` +releases a reservation deterministically. No platform-wide binding parity, full OBS runtime, fuzzing campaign, or network-impairment performance result is implied by these checks. diff --git a/quest/m1/resume-drop-final.md b/quest/m1/resume-drop-final.md index faf0374b3b2..fd84b134899 100644 --- a/quest/m1/resume-drop-final.md +++ b/quest/m1/resume-drop-final.md @@ -14,8 +14,9 @@ can still arrive and be served. Making Rust's drops final broke `rejoin::rejoined_quiet_catalog_survives_cache_expiry`: a late reader never got the catalog's group 0. -Decide (maintainer) whether a reported drop is final. Then change the side that -differs, the plain cursor and the resume path together, with tests in both -languages. +Decided 2026-10-25 (maintainer): a reported drop is not final, as in Rust. +JS stops recording a reported drop as delivered, so a group that arrives +later is still served, on the plain cursor and the resume path together, +with tests in both languages. Public API: none expected. Wire: none. diff --git a/quest/m1/resume-model-test.md b/quest/m1/resume-model-test.md new file mode 100644 index 00000000000..4b1adf05221 --- /dev/null +++ b/quest/m1/resume-model-test.md @@ -0,0 +1,34 @@ +# [L] Resume checked against a model + +## Goal + +A randomized test drives resumed fronts through route flips, route failures, +drops, ranges, cache expiry, and reader joins and leaves, and checks every +reader's observed groups, frames, gaps, and end against a simple reference +model of one logical track. It runs in Rust and `@moq/net`, and it passes +before the cut. + +## Plan + +Decided 2026-10-25 (maintainer): resume must be flawless at the cut. Resume +has had about 18 fixes in its last 31 commits (rs/moq-net/src/model/resume.rs, +2026-10-18 to 10-25), and nothing checks it beyond hand-written route-change +cases, so an oracle comes before any rewrite. + +- The model is what one plain cursor on the logical track would see: a + reader gets every group its range and budget allow, each exactly once, in + its order, with drops reported per the drop rules. A reported drop is not + final: a group that arrives later is still served. +- Include a newer publisher epoch winning the path while an older-epoch + subscription is open: that subscription never sees the new instance's + groups, and it ends with an error once its own instance has no route. +- Drive it on a mocked clock with seeded randomness, and shrink a failure to + a minimal sequence that becomes a regression test. Prefer an existing + property-testing crate. +- Run a fixed seed set in the merge gate and a longer sweep nightly. +- Fix what it finds at the cause. If the failures cluster on the per-reader + route-copy design rather than single missed checks, bring back the + [producer-replacement redesign](/quest/m2/resume-redesign.md) to the + maintainer for m1, with this test as its oracle. + +Public API: none. Wire: none. diff --git a/quest/m1/transport-frame-offsets.md b/quest/m1/transport-frame-offsets.md new file mode 100644 index 00000000000..e586f19b7e7 --- /dev/null +++ b/quest/m1/transport-frame-offsets.md @@ -0,0 +1,45 @@ +# [M] The transport may start a group mid-way + +## Goal + +A moq-lite hop may request and deliver a group starting at any frame k the +requester needs, whether or not it still holds the frames below k (route +continuation, hole fills, and front-eviction refills after the front was +evicted, with a frame-precise end), so a relay never refetches frames it does +not need. Applications still only ask for whole groups: no public API +starts at a frame. Rust and `@moq/net` both accept and cache such a tail. + +## Plan + +Decided 2026-10-25 (maintainer): remove frame-N starts from the API, as +[moq-dev/moq#6465](https://github.com/moq-dev/moq/pull/6465) did, but allow +them in the transport as an optimization. + +- Draft: amend lite-07-wip so a subscriber may send any nonzero frame start, + replacing "MUST send 0 unless it already holds every frame below this + index". A front-eviction refill needs frames k and up after the frames + below k were evicted, so holding the prefix cannot be the condition. Keep + the `Frame End` rule. +- Widening is relay policy, not a wire rule: a relay asked for frame k of a + group it holds none of widens the upstream request to frame 0 and cuts + locally, so the cache stays usable by + whole-group readers and a compressed group can inflate. +- A partial copy is the same group, never a new identity: `covering_group` + already serves only copies that cover the start. A copy cut short at the back + is marked incomplete so it never answers a whole-group fetch. +- JS accepts a GROUP with `frameStart > 0` (numbering from it, as Rust's + `group::Producer::start_at` does) instead of refusing it as a protocol + violation. +- A moq-transport stream that starts mid-group with no head is still dropped + (2026-10-22 decision, reaffirmed 2026-10-25). +- The rule in `rs/moq-net/AGENTS.md` that banned partial groups was deleted + 2026-10-25. + +Bound the cost a hostile peer can create: many distinct tail starts for one +group must not multiply its copies; coalesce to one copy per group. + +Public API: none. Wire: lite-07-wip draft text only; the fields exist. + +## Related + +- [Front eviction](/quest/m1/unbounded-groups/front-eviction.md) - refills a front hole with a frame-precise range diff --git a/quest/m1/unbounded-groups/README.md b/quest/m1/unbounded-groups/README.md index 592ee82063f..f28c3d294d7 100644 --- a/quest/m1/unbounded-groups/README.md +++ b/quest/m1/unbounded-groups/README.md @@ -90,8 +90,10 @@ Decided 2026-10-16 (maintainer interview): - `GroupTooLarge` and `FrameTooLarge` stay, raised only when an allocation can't be satisfied. - The byte offset goes on the next unpublished lite version after lite-07, - not lite-07. Frame-boundary holes already refill over lite-07's - frame-precise ranges. + not lite-07. Frame-boundary holes refill over lite-07's frame offsets once + [transport frame offsets](/quest/m1/transport-frame-offsets.md) lets a + refill start at a frame the relay no longer holds below (decided + 2026-10-25, maintainer: the application API stays whole-group). - JS is a separate quest. Docs change inline: the lite draft, `doc/concept`, the relay cache docs, the JSON and moq-net compression docs, and the e2ee draft's 32 MiB interop cap. The diff --git a/quest/m1/unbounded-groups/front-eviction.md b/quest/m1/unbounded-groups/front-eviction.md index 58ffa9d6f42..5b1050df2f6 100644 --- a/quest/m1/unbounded-groups/front-eviction.md +++ b/quest/m1/unbounded-groups/front-eviction.md @@ -36,13 +36,21 @@ See the [epic](/quest/m1/unbounded-groups/README.md) for the decisions. that is protected or being read. - A refill writes a run strictly below the base, so it never races the tail writer. Coalesce concurrent readers on one hole into a - single request, as `PendingFetch` does. Fold today's `Slot.head` tail copy + single request, as `FillState` does. Fold today's `Slot.head` tail copy into this. - A refill asks for exactly the hole: from the reader's missing frame to the first frame still cached, over a frame-precise range end. Today's fills always run to the group's end (`end_frame: None`), which on a live group would duplicate the live edge. If more is evicted while the refill runs, the reader hits the next hole and refills again, one RTT per hole. +- Decided 2026-10-25 (maintainer): the application API stays whole-group, + but the transport may start a group at a frame offset as an optimization, + and a front-eviction refill is one. The lite draft's rule that a + subscriber "MUST send 0 unless it already holds every frame below" forbids + this refill, since the relay evicted the frames below the hole, so + [transport frame offsets](/quest/m1/transport-frame-offsets.md) amends it + first. A relay asked for frame k of a group it holds nothing of widens to + frame 0 upstream. - Refills are unpaced (decided 2026-10-24, maintainer). Pacing a refill on its fastest reader, and shared "trains" of readers that merge into the run ahead, were rejected as not worth their complexity. `FillState` @@ -99,5 +107,8 @@ See the [epic](/quest/m1/unbounded-groups/README.md) for the decisions. Public API: removes `MAX_GROUP_FRAMES`, and `MAX_CACHE_BYTES` stops capping groups. A reader that used to see `GroupTooLarge` now refills or sees -`Evicted`. Wire: none; it uses the frame-precise ranges. +`Evicted`. Wire: none beyond the amended frame offset rule. +## Required + +- [Transport frame offsets](/quest/m1/transport-frame-offsets.md) - the lite draft lets a refill start at the missing frame without holding the frames below it diff --git a/quest/m2/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md b/quest/m2/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md index f63fd9a454d..820498bb7b4 100644 --- a/quest/m2/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md +++ b/quest/m2/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md @@ -42,7 +42,9 @@ grant; `Options::bandwidth` documents that `300_000 * channels` (`backend/libopus.rs`, `rs/moq-audio/src/opus.rs`). `Policy::min` defaults to a tenth of the ceiling (`rs/moq-mux/src/rate.rs`); for Opus it is the codec floor. A grant below it disables the rendition until bandwidth recovers (decided - 2026-10-04 with the enabled flag); the encoder never errors. Once viewers + 2026-10-04 with the enabled flag), through the disable and re-enable rule + [simulcast rung disable](/quest/m1/simulcast-rung-disable.md) puts beside + `Control` rather than a second one; the encoder never errors. Once viewers deselect it, its demand and so its grant go away, so recovery evaluates a hypothetical share against the current estimate, as the ladder controller does. The reservation's ceiling is @@ -65,3 +67,7 @@ one policy step, holds it on a `None` grant, and ramps back when the grant returns; a grant below the Opus floor disables the rendition, and recovered bandwidth enables it again even after the last viewer unsubscribed; a PCM Producer ignores every grant. + +## Required + +- [Simulcast rung disable](/quest/m1/simulcast-rung-disable.md) - the shared disable and re-enable rule in `moq_mux::rate` that this quest reuses diff --git a/quest/m2/README.md b/quest/m2/README.md index eedc5c81044..a24c3b25451 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -27,6 +27,7 @@ lite-07; bitrate enforcement is not part of this release. - [P2P](/quest/m2/p2p/README.md) - application-selected peer sessions use shared priorities, routing, and same-epoch handover - [rust2 publishes its UniFFI fork](/quest/m2/rust2-uniffi.md) - waiting on kixelated/rust2 to import UniFFI and moq's generators and publish them - [Switch to rust2's UniFFI](/quest/m2/uniffi-fork.md) - moq-ffi and every wrapper build from kixelated/rust2's fork under one pin, and moq's generator code is deleted +- [Generated C bindings](/quest/m2/c/README.md) - C generated from moq-ffi ships as `moq-c` 0.8.0 and replaces the hand-written crate - [Benchmark sources](/quest/m2/bench-sources.md) - every measuring nightly benchmark feeds the regression table, and none is a smoke run - [Poll-native async](/quest/m2/ffi-poll-migrate.md) - moq-ffi's pure-kio async exports poll their state directly instead of going through `ffi::Task` - [QoS](/quest/m2/qos/README.md) - the relay's egress `lag` histogram and `dropped` counters on the stats totals and prefix tracks @@ -66,7 +67,7 @@ lite-07; bitrate enforcement is not part of this release. - [V4L2 virtual camera](/quest/m2/v4l2-virtual-camera.md) - V4L2 capture runs nightly against the `vivid` virtual driver instead of only on a developer's camera - [Probe](/quest/m2/probe/README.md) - a session probes up to the bitrate it wants to send or receive, so a stopped simulcast rendition resumes and a viewer up-switches; the browser pads where native sessions retransmit - [Failed groups reach the publisher](/quest/m2/group-failed.md) - Rust and both JS publishers surface a group whose delivery a subscriber's stream gave up on, with one shape -- [Headless moq-transport groups](/quest/m2/ietf-headless-group.md) - a group whose head no fill or joining FETCH supplies keeps its tail instead of being dropped, and a relay forwards it to moq-transport subscribers +- [Headless moq-transport groups](/quest/m2/ietf-headless-group.md) - a mid-group stream whose head no fill or joining FETCH supplies is dropped in either arrival order, never kept as a whole group - [A finished PROBE stops reporting](/quest/m2/probe-end-stale.md) - the Rust receive estimate reads unknown once its PROBE stream ends - [Media stats](/quest/m2/stats/README.md) - publishers announce a stats track in the catalog, viewers answer a soliciting catalog through a per-catalog `.echo` broadcast, and one model turns both into a health verdict and a preflight report - [Perf](/quest/m2/perf/README.md) - eliminate measured hot-path costs across moq-uring, kio, and the moq-net model @@ -216,7 +217,7 @@ lite-07; bitrate enforcement is not part of this release. - [JS IETF request streams](/quest/m2/js-request-stream.md) - `RequestStream` owns deadlines and cancellation, and TRACK trailing bytes close the session like Rust - [moq-transport finish interop](/quest/m2/ietf-finish.md) - the interop `finish` case runs on every even moq-transport draft instead of being skipped - [Join after a quick finish](/quest/m2/ietf-join-after-finish.md) - a pre-draft-22 joining FETCH that arrives after its subscription finished is still answered, within a bound the peer already negotiates -- [JS joining FETCH](/quest/m2/ietf-js-joining-fetch.md) - the JS publisher honors Largest Object before draft 22 and serves the head on a joining FETCH, as the Rust publisher does +- [JS joining FETCH](/quest/m2/ietf-js-joining-fetch.md) - the JS publisher answers a joining FETCH that arrives after a quick finish, mirroring the Rust bound - [ashpd uses maintained D-Bus crates](/quest/m2/ashpd-maintained.md) - wait for a stable zbus 6 port, then remove the portal dependency exceptions - [Askama uses a maintained TOML parser](/quest/m2/askama-maintained.md) - wait for a stable parser migration, then remove the generator dependency exception - [Dependency and alternatives audit](/quest/m2/audit-deps.md) - dependencies are current, deduplicated, needed, and the best maintained option diff --git a/quest/m2/ashpd-maintained.md b/quest/m2/ashpd-maintained.md index 51438fc1b48..fa41f944fca 100644 --- a/quest/m2/ashpd-maintained.md +++ b/quest/m2/ashpd-maintained.md @@ -9,8 +9,9 @@ from `deny.toml`; `just rs audit` passes without those exceptions. ## Plan -Check the upstream [zbus 6 port](https://github.com/bilelmoussaoui/ashpd/pull/491) -and crates.io releases. Until a stable port is published, report that outside +The upstream [zbus 6 port](https://github.com/bilelmoussaoui/ashpd/pull/491) +merged on 2026-10-25, but ashpd 0.13.13 is still the latest stable release. +Check crates.io for one that carries it. Until a stable port is published, report that outside condition without disabling portal capture or adding a local fork. ## Related diff --git a/quest/m2/askama-maintained.md b/quest/m2/askama-maintained.md index 04b21096ff2..aba8060cfc7 100644 --- a/quest/m2/askama-maintained.md +++ b/quest/m2/askama-maintained.md @@ -17,3 +17,4 @@ its template syntax. Verify binding generation after the upgrade. ## Related - [Dependency audit](/quest/m2/audit-deps.md) - recheck the rest of the dependency graph +- [Switch to rust2's UniFFI](/quest/m2/uniffi-fork.md) - askama reaches the lock only through `uniffi_bindgen`, which that switch moves to rust2 diff --git a/quest/m2/audit-compile-time.md b/quest/m2/audit-compile-time.md index 7681bf67b77..67d51792e5f 100644 --- a/quest/m2/audit-compile-time.md +++ b/quest/m2/audit-compile-time.md @@ -39,8 +39,9 @@ What to check first: PR gate. - The last per-reader `Any::decode_atom` copy in moq-mux: `fmp4/text.rs` decodes from a bare `&[u8]` (13,307 lines, 1.3% of the lib) while every - other fMP4 path uses `Cursor<&[u8]>`. It was skipped for open PR - [moq-dev/moq#6379](https://github.com/moq-dev/moq/pull/6379). + other fMP4 path uses `Cursor<&[u8]>`. It was skipped for + [moq-dev/moq#6379](https://github.com/moq-dev/moq/pull/6379), which has + since merged, so it is ready to fix. - Whether `cargo-llvm-lines` joined the pinned shell. `flake.nix` had four open PRs at the m1 run, so it was not added. diff --git a/quest/m2/audit-deps.md b/quest/m2/audit-deps.md index 24a39d93725..3d3ed9faa22 100644 --- a/quest/m2/audit-deps.md +++ b/quest/m2/audit-deps.md @@ -63,7 +63,8 @@ Advisories waiting on upstream: - ashpd 0.13 pulls zbus 5 and the unmaintained zvariant, zvariant_derive, and zbus_names (RUSTSEC-2026-0370, 0371, 0372). Upstream's - [zbus 6 port](https://github.com/bilelmoussaoui/ashpd/pull/491) is open. + [zbus 6 port](https://github.com/bilelmoussaoui/ashpd/pull/491) merged on + 2026-10-25; wait for a stable ashpd release that carries it. - basic-toml (RUSTSEC-2026-0374) comes through askama_derive's default `config` feature in uniffi_bindgen 0.32, which reads its `askama.toml`. It only reaches the generator-only uniffi fork and never ships. diff --git a/quest/m2/audit-simplify.md b/quest/m2/audit-simplify.md index ab9066a514a..551a23d4ced 100644 --- a/quest/m2/audit-simplify.md +++ b/quest/m2/audit-simplify.md @@ -59,7 +59,7 @@ New from m1: Proposals from m0 are settled. The moq-video raw-frame publisher and the shared `Background` would each have had moq-ffi as their only user once the -[generated C bindings](/quest/m1/c/README.md) retire moq-c's hand-written +[generated C bindings](/quest/m2/c/README.md) retire moq-c's hand-written copies, so both were dropped. The Go wrapper's missing `bridge.CallHandle` is in the generated binding, not the wrapper: see [Go cancelled results](/quest/m2/go-cancelled-result.md). @@ -69,8 +69,8 @@ in the generated binding, not the wrapper: see One PR, private and behavior-preserving: moq-mux's `import::Track` and `TrackStream` expand their codec arms through one macro each, and nine importers lost a deleted method's doc left on `cut`. The H.264 and H.265 -exporters were already planned as [one Annex-B exporter](https://github.com/moq-dev/moq/pull/6575), -ordered after the rendition rebind, so this run left them alone. Search the +exporters were left alone for [one shared Annex-B export lifecycle](https://github.com/moq-dev/moq/pull/6575), +which has since landed. Search the quest tree before fixing a duplicate: the 2026-10-22 refactor audit filed several. diff --git a/quest/m2/broadcast-fairness.md b/quest/m2/broadcast-fairness.md index b96e6037718..31911518cd8 100644 --- a/quest/m2/broadcast-fairness.md +++ b/quest/m2/broadcast-fairness.md @@ -45,6 +45,7 @@ Direction carried over from 2026-09-19, not re-confirmed: - Decided 2026-10-20 (maintainer): the `transport::poll` trait breaks land in order, receive leases ([moq-dev/moq#5878](https://github.com/moq-dev/moq/pull/5878)), then [native request admission](https://github.com/moq-dev/moq/pull/6409), then the send domain. + Both earlier breaks have landed, so the send domain waits on nothing in that order. Receive ranks, folded in from [moq-dev/moq#5878](https://github.com/moq-dev/moq/pull/5878)'s follow-ups by diff --git a/quest/m2/c/README.md b/quest/m2/c/README.md new file mode 100644 index 00000000000..4f88a4ba044 --- /dev/null +++ b/quest/m2/c/README.md @@ -0,0 +1,53 @@ +# Generated C bindings + +## Goal + +C programs use moq through a C API generated from moq-ffi, shipped as the +`moq-c` package with CMake target `moq::c`, so C replaces the hand-written ABI +and tracks every moq-ffi change the way Go, Swift, Kotlin, Python, Dart, and +C++ already do. The C++ package (`cpp/moq`) and the OBS plugin are out of +scope; they already sit on moq-ffi through the generated C++. + +## Plan + +Decided 2026-10-25 (maintainer): this line ships after the cut, once moq builds +its bindings from rust2's UniFFI fork. The hand-written crate ships at the cut +with [parity](/quest/m1/c/README.md) instead. The generator fix for the C name +clash lands in rust2, not in a `kixelated/uniffi-bindgen-cpp` release. + +Decided earlier, still standing: + +- Async calls take a completion callback and user data and return a task; + freeing the task cancels the call. Callbacks run on moq's dispatcher thread + by default, or on the host's own loop through `moq_set_dispatcher` and + `moq_work_run`, the C form of `moq::set_executor`. Streams are repeated calls. +- Errors and records mirror the C++ package: calls return `MoqError *` + (NULL on success) with `moq_error_message()`, results come through + out-params, and records are owned structs with `moq__free`. Names + follow GLib: CamelCase types and snake_case functions. +- The header is C11: errors and data enums are a `tag` plus an anonymous union + (`err->protocol`). Infallible calls return their value directly, optional + scalars are `{has_value, value}`, each async function has its own callback + typedef, and unnamed variant fields are `v1`, as in C++. +- The generated package takes over the `moq-c` name and `moq::c` target at + 0.8.0, a minor bump over the hand-written 0.7.x. +- Docs change inline: the consumer quest rewrites `doc/lib/c`, and its Impact + bullet carries the move for the package's `MIGRATE.md`. + +Generation drifted while unbuilt: the backend refuses the +`#[uniffi(default = Some(0))]` that #6171 added in `rs/moq-ffi/src/consumer.rs`. +Build the generated header in CI as soon as this line starts so drift fails +loud. + +The line owns the pre-publication check: every `doc/lib/c` sample and the C +interop client build and run against the built release archive. +[Published archive proof](/quest/m2/release/c-publish.md) repeats it after +publication; [old-crate retirement](/quest/m2/release/c-retire.md) follows. + +## Required + +- [Switch to rust2's UniFFI](/quest/m2/uniffi-fork.md) - the C generator comes from rust2's fork +- [One C name per moq-ffi item](/quest/m2/c/name-clash.md) - rust2's C generator names types in CamelCase and functions in snake_case, so the record `MoqAudioEncoderFinish` and the method `moq_audio_encoder_finish` never share a name +- [moq-c package](/quest/m2/c/package.md) - the generated header ships as `moq-c` 0.8.0 with `moq::c`, pkg-config, and a release workflow +- [C consumers](/quest/m2/c/consumers.md) - the C interop client and `doc/lib/c` samples move onto the generated API +- [Async failure contract](/quest/m2/c/async-failure-contract.md) - a generated async call that fails early fires no callback, and one that started always delivers its terminal callback diff --git a/quest/m1/c/async-failure-contract.md b/quest/m2/c/async-failure-contract.md similarity index 96% rename from quest/m1/c/async-failure-contract.md rename to quest/m2/c/async-failure-contract.md index 94f3c00848c..61668dd1b5d 100644 --- a/quest/m1/c/async-failure-contract.md +++ b/quest/m2/c/async-failure-contract.md @@ -23,4 +23,4 @@ Public API: header documentation and a guaranteed terminal callback. Wire: none. ## Required -- [moq-c package](/quest/m1/c/package.md) - the generated C API this contract applies to +- [moq-c package](/quest/m2/c/package.md) - the generated C API this contract applies to diff --git a/quest/m1/c/consumers.md b/quest/m2/c/consumers.md similarity index 92% rename from quest/m1/c/consumers.md rename to quest/m2/c/consumers.md index 8a4d3e0f790..ac6343f3ef8 100644 --- a/quest/m1/c/consumers.md +++ b/quest/m2/c/consumers.md @@ -17,4 +17,4 @@ build. Nothing in the repository calls the hand-written ABI any more. ## Required -- [moq-c package](/quest/m1/c/package.md) - the package these consumers link +- [moq-c package](/quest/m2/c/package.md) - the package these consumers link diff --git a/quest/m1/c/name-clash.md b/quest/m2/c/name-clash.md similarity index 100% rename from quest/m1/c/name-clash.md rename to quest/m2/c/name-clash.md diff --git a/quest/m1/c/package.md b/quest/m2/c/package.md similarity index 100% rename from quest/m1/c/package.md rename to quest/m2/c/package.md diff --git a/quest/m2/e2ee/README.md b/quest/m2/e2ee/README.md index 6fa23b6d5ae..ffeb6a6c4f3 100644 --- a/quest/m2/e2ee/README.md +++ b/quest/m2/e2ee/README.md @@ -46,11 +46,11 @@ The Rust and TypeScript cores expose the same surface, and nothing else: ### Dynamic names -Physical names are reversible (decided with the maintainer on 2026-10-24 in [#6623](https://github.com/moq-dev/moq/pull/6623)). A physical track name is a 12-byte nonce, the first 12 bytes of an HMAC-SHA-256 of the semantic name padded to a multiple of 16 bytes, followed by the AES-128-GCM encryption of that padded name under that nonce, with both keys derived from the generation (the draft's Key Derivation section). It needs only HMAC-SHA-256 and AES-128-GCM, so it runs on aws-lc-rs, graviola, and WebCrypto with no new dependency (maintainer, 2026-10-24, over AES-GCM-SIV, which graviola lacks). A publisher opens a requested name and recomputes its nonce to recover its semantic name, so the e2ee `dynamic()` answers undeclared names through the [moq-net traits](/quest/m2/net-wrap/README.md) unchanged, and a name that does not open is answered as missing with no state kept. Equal names under one generation still share one physical name, so relays dedupe and cache them, and names stay unlinkable across epochs. AEAD keys bind the semantic name. The cost is that a relay learns each name's length rounded up to 16 bytes: every name up to 15 bytes looks alike, and a longer one leaks its length to within 16 bytes (maintainer, 2026-10-24, over power-of-two buckets that leak less but can double a long name). Broadcast paths stay one-way: e2ee wraps no origin, so nothing requests an undeclared protected broadcast. +Physical names are reversible (decided with the maintainer on 2026-10-24 in [#6623](https://github.com/moq-dev/moq/pull/6623)). A physical track name is a 12-byte nonce, the first 12 bytes of an HMAC-SHA-256 of the semantic name padded to a multiple of 16 bytes, followed by the AES-128-GCM encryption of that padded name under that nonce, with both keys derived from the generation (the draft's Key Derivation section). It needs only HMAC-SHA-256 and AES-128-GCM, so it runs on aws-lc-rs, graviola, and WebCrypto with no new dependency (maintainer, 2026-10-24, over AES-GCM-SIV, which graviola lacks). A publisher opens a requested name and recomputes its nonce to recover its semantic name, so the e2ee `requests()` handler (`dynamic()` until [named requests](/quest/m1/api-audit/requests.md)) answers undeclared names through the [moq-net traits](/quest/m2/net-wrap/README.md) unchanged, and a name that does not open is answered as missing with no state kept. Equal names under one generation still share one physical name, so relays dedupe and cache them, and names stay unlinkable across epochs. AEAD keys bind the semantic name. The cost is that a relay learns each name's length rounded up to 16 bytes: every name up to 15 bytes looks alike, and a longer one leaks its length to within 16 bytes (maintainer, 2026-10-24, over power-of-two buckets that leak less but can double a long name). Broadcast paths stay one-way: e2ee wraps no origin, so nothing requests an undeclared protected broadcast. Rejected: -- Declared names only (`reserve_track` over an enumerated universe): it cannot serve an open-ended name such as a moq-stats prefix or presence query, so the e2ee `dynamic()` could not mirror moq-net. +- Declared names only (`reserve_track` over an enumerated universe): it cannot serve an open-ended name such as a moq-stats prefix or presence query, so the e2ee `requests()` could not mirror moq-net. - A name-directory track: a directory the publisher writes is the catalog again and still needs every name in advance. One the subscriber writes requires viewers to publish, links each request to a visible subscriber path, and races the subscription it serves. - Requests that carry an encrypted semantic name beside a one-way name: it needs a moq-lite and moq-transport field that every relay forwards, and a moq-net `track(name)` signature change that breaks the trait mirror. The relay forwards one coalesced subscriber's field, so a relay-authorized party without the secret could race a bad one and get the name refused for everyone. Its field leaks the same rounded length. - One-way names for declared tracks and reversible ones for dynamic tracks: one semantic name would have two physical names, so it would not always mean the same content, and a subscriber cannot know which one the publisher serves. @@ -64,7 +64,7 @@ Extractor declaration checks, which analyze only published packages since ## Required - [Undeclared track names](/quest/m2/e2ee/undeclared-names.md) - an encrypted - broadcast's `dynamic()` serves names nobody created or reserved, under + broadcast's `requests()` serves names nobody created or reserved, under semantic names, in Rust - [JS undeclared track names](/quest/m2/e2ee/js-undeclared-names.md) - `@moq/net` exposes a broadcast request handler and `@moq/e2ee` serves diff --git a/quest/m2/e2ee/js-undeclared-names.md b/quest/m2/e2ee/js-undeclared-names.md index 90b63b6debe..795e6156f6c 100644 --- a/quest/m2/e2ee/js-undeclared-names.md +++ b/quest/m2/e2ee/js-undeclared-names.md @@ -22,9 +22,14 @@ Decided with the maintainer on 2026-10-24: - `@moq/e2ee` mirrors the Rust handler exactly: it opens each request's physical name, claims it, and yields a request whose `name` is semantic. A name that does not open is answered as missing and keeps no state. A - re-requested name follows the rule undeclared-names settles. + re-requested or re-created name resumes above its high-water marks, as in + Rust (decided 2026-10-25, maintainer: resume always, replacing the + 2026-10-17 refusal, so every identity stays unique and a handler can serve + a dropped name again). - The handler offers the lister, taking semantic names and listing their - physical names, as undeclared-names does. + physical names, as undeclared-names does. `@moq/net`'s broadcast lister is + internal today (tests reach it through `wireOf`), so export it with the + handler. - Port undeclared-names' tests: a relay-forged name, an older epoch's name, and a burst of forged names that leaves no state behind. diff --git a/quest/m2/e2ee/rust-publish.md b/quest/m2/e2ee/rust-publish.md index ca7e429ff33..94611e4770e 100644 --- a/quest/m2/e2ee/rust-publish.md +++ b/quest/m2/e2ee/rust-publish.md @@ -3,8 +3,7 @@ ## Goal Rust media and catalog publishers emit no plaintext semantic catalog in E2EE -mode, and a track name the e2ee producer already carried is refused loudly -rather than silently left out. +mode. The reusable crypto crate remains independent of Hang and codec-specific publisher policy. @@ -34,14 +33,14 @@ in advance, so it does not wait on encryption. Retain ordinary plaintext publication unchanged outside E2EE mode. - Cover every current Rust importer and capture path so a convenience API cannot emit a plaintext semantic catalog. -- Refuse a re-created track (decided 2026-10-17, maintainer): moq-e2ee - never releases a name's claim, because reopening would restart its nonce - counters, so a track created again under a name the producer already - carried fails with a new error instead of being dropped. A track name - always means the same content, so a restarted encoder publishes a new name - or a new broadcast. Test it through moq-mux. - -Public API: a new error from the e2ee producer. Wire: none. +- A re-created track resumes (decided 2026-10-25, maintainer, replacing the + 2026-10-17 refusal): moq-e2ee never releases a name's claim, and a track + created again under a claimed name continues above its high-water marks, + so nonces never repeat. [Undeclared names](/quest/m2/e2ee/undeclared-names.md) + owns that `create_track` change and its test, so this quest still does not + wait on it. + +Public API: none. Wire: none. ## Required diff --git a/quest/m2/e2ee/undeclared-names.md b/quest/m2/e2ee/undeclared-names.md index ea37e597816..6e7c0b2ac41 100644 --- a/quest/m2/e2ee/undeclared-names.md +++ b/quest/m2/e2ee/undeclared-names.md @@ -30,14 +30,21 @@ The design is the [E2EE epic](/quest/m2/e2ee/README.md)'s Dynamic names section. - A handler that drops an idle track must be able to serve the name again when it is requested later, as with moq-net. moq-net already keeps one sequence namespace per name for the broadcast's life and refuses a reused - sequence (`replacement_refuses_reused_sequences`). Recommended: the claim - keeps the name's high-water marks across every broadcast of the - generation, and a re-served name resumes above them instead of being - refused. That keeps every `(group, frame)` identity unique without - releasing the claim. Say in the PR whether `create_track` adopts the same - rule, which today refuses a re-created name. + sequence (`replacement_refuses_reused_sequences`). Decided 2026-10-25 + (maintainer): resume always. The claim keeps the name's high-water marks + across every broadcast of the generation, and a re-served or re-created + name resumes above them, `create_track` included. This replaces the + 2026-10-17 refusal, under which `create_track` returns `Error::Reuse` for + a name the generation already claimed (`generation.rs` `claim`). Reason: + resuming keeps every `(group, frame)` identity unique without releasing the + claim, so nonces never repeat, and an on-demand handler can drop an idle + track and serve it again. An explicit sequence at or below a high-water + mark is still refused. Test a dropped and re-served name and a re-created + track resuming above their marks. - Listing (maintainer, 2026-10-24): the wrapper's handler offers moq-net's - `lister`, taking semantic names and listing their physical names. An + `lister`, taking semantic names and listing their physical names. + `broadcast::Dynamic::lister` and `Lister` are crate-private since #6470, + so this quest exports them as their first consumer. An e2ee consumer's listing decrypts them back ([moq-e2ee implements the traits](/quest/m2/net-wrap/e2ee.md)), so a listed name round-trips. @@ -48,8 +55,10 @@ The design is the [E2EE epic](/quest/m2/e2ee/README.md)'s Dynamic names section. `generation::Producer::open` in Rust (drop its `allow(dead_code)` once the handler calls it) and `@internal` `Generation.Producer.open` in JS. -Public API: a request handler on `moq_e2ee::broadcast::Producer`, and -`track::Request::name()` returns the semantic name. Wire: none. +Public API: a request handler on `moq_e2ee::broadcast::Producer`, +`track::Request::name()` returns the semantic name, `create_track` resumes a +claimed name instead of returning `Error::Reuse`, and moq-net exports the +broadcast lister. Wire: none. ## Related diff --git a/quest/m2/h264-reader.md b/quest/m2/h264-reader.md index 2363b274a27..274f3ba81d7 100644 --- a/quest/m2/h264-reader.md +++ b/quest/m2/h264-reader.md @@ -2,8 +2,8 @@ ## Goal -moq-mux parses H.264 parameter sets with `h264-reader`, the crate moq-video -already uses, instead of `h264-parser` plus a hand-written VUI and HRD walker, +moq-mux parses H.264 parameter sets with `h264-reader`, which moq-video and +moq-mux's TS video timing (`container/ts/video_timing.rs`) already use, instead of `h264-parser` plus a hand-written VUI and HRD walker, and its codec config readers share one maintained bit reader. Catalog output is unchanged. diff --git a/quest/m2/ietf-finish.md b/quest/m2/ietf-finish.md index 5023435e549..c7a469a609d 100644 --- a/quest/m2/ietf-finish.md +++ b/quest/m2/ietf-finish.md @@ -31,6 +31,6 @@ every reader's acknowledgement, so judge each cell from its reader's log. Public API: none. Wire: none expected; any fix that changes framing updates its draft. -## Related +## Required -- [Join after a quick finish](/quest/m2/ietf-join-after-finish.md) - the pre-draft-22 half of this +- [Join after a quick finish](/quest/m2/ietf-join-after-finish.md) - the pre-draft-22 half of this, without which drafts 14 to 18 cannot pass diff --git a/quest/m2/ietf-headless-group.md b/quest/m2/ietf-headless-group.md index 6ceb5312e56..4843481dfac 100644 --- a/quest/m2/ietf-headless-group.md +++ b/quest/m2/ietf-headless-group.md @@ -1,72 +1,49 @@ -# [M] Keep a moq-transport group whose head never arrives +# [S] Drop a moq-transport group whose head never arrives, in either order ## Goal When a moq-transport subscription delivers a group starting partway through -and the fill or joining FETCH that should supply its head is refused or -ends empty, moq-net keeps the tail as a group that starts at that object. -Today, depending on a race, it either drops the whole group or keeps the -tail as if it were the whole group. A relay then forwards the tail to its -moq-transport subscribers instead of losing objects it received, which -draft-18 §9.4 forbids ("A relay MUST NOT reorder or drop objects received -on a multi-object stream"). +and the fill or joining FETCH that should supply its head is refused or ends +empty, moq-net drops that mid-group stream whichever arrives first, the +stream or the refusal. Today one order drops it and the other keeps the tail +as if it were the whole group. ## Plan -Decided with the maintainer on 2026-10-22, from the runner cases work -([moq-dev/moq#6275](https://github.com/moq-dev/moq/pull/6275)): keep the -tail, don't drop the group. This was ranked m2 because it blocks no m1 -interop goal: a publisher that answers SUBSCRIBE_OK before writing, or that -serves the joining FETCH, never triggers it. +Decided by the maintainer: a stream starting mid-group with no head is +dropped and the subscription goes on +([moq-dev/moq#6407](https://github.com/moq-dev/moq/pull/6407), 2026-10-22). +Confirmed 2026-10-25, which rejected the earlier plan to keep the tail and +forward it to moq-transport subscribers. Where it happens: `open_group` in `rs/moq-net/src/ietf/subscriber.rs`. -The outcome is a race between the mid-group stream and the joining FETCH's -refusal: -- If the refusal lands first, the fill is no longer outstanding, and the - stream takes the unfilled path. That keeps the tail, but only the wire - metadata (`group::Wire.first_id`) records where it starts: the model - group begins at frame 0, so it looks complete, and `position_group` - serves the tail to moq-lite subscribers as a whole group. - If the stream arrives while the fill is still pending, it waits in `claim_fill`. Once the refusal settles the fill, `claim` returns no head, and the stream fails with "no fill to stitch a mid-group stream onto", - which drops the whole group. - -Cloudflare's moq-test-client reproduces both: it writes object 0 before -SUBSCRIBE_OK and refuses the joining FETCH + which drops the group. This is the decided behavior. +- If the refusal lands first, the fill is no longer outstanding, and the + stream takes the unfilled path, which opens the group from its first + object. Only the wire metadata (`group::Wire.first_id`) records where it + starts: the model group begins at frame 0, so it looks complete, and + `position_group` serves the tail to moq-lite subscribers as a whole group. + `Fill::Done`'s doc already says such a stream "gets dropped"; the code + does not. + +Drop it in the second order too. The unfilled path also serves subscriptions +that never requested a head, so tell a refused or failed fill apart from no +fill at all rather than dropping every stream whose first Object ID is +nonzero. + +Cloudflare's moq-test-client reproduces both orders: it writes object 0 +before SUBSCRIBE_OK and refuses the joining FETCH ([cloudflare/moq-rs#281](https://github.com/cloudflare/moq-rs/issues/281)). -The ingest fix opens the same headless group in both orders, and the model -must know its head is missing so `position_group` refuses it to moq-lite -subscribers. `group::Producer::start_at` marks a missing front, but in -frame ordinals: relayed Object IDs need not match them (`group::Wire`), so -trimming to the first Object ID would invent frames and break FETCH and -partial-reader positioning. Mark the missing head without equating the -two, and check that the moq-transport egress forwards the group as a -subgroup stream starting at its first Object ID. - -Open design points, with recommendations: - -1. **Serving the tail to moq-transport subscribers.** Recommended: forward - it as a subgroup stream that starts at its first object, since §9.4 - applies. The alternative is to skip it, as moq-lite does. -2. **Serving the tail to moq-lite subscribers.** Recommended: keep the - current DROP (`position_group` in `rs/moq-net/src/lite/publisher.rs`) - for any subscriber that did not ask for a partial group. On moq-lite a - group is the unit of decodability, and only the subscriber knows whether - a partial one is any use. -3. **A later FETCH or joining FETCH for the missing head.** Recommended: - refuse the missing prefix, as the publisher already does - (`a_joining_fetch_refuses_a_missing_prefix_before_fetch_ok`), rather - than ask upstream again. - Tests: a subscriber unit test for each order, with the fill refused before -the mid-group stream arrives and after it. The two orders must end the same -way. Add a relay-level test that a moq-transport -subscriber receives the tail while a moq-lite subscriber gets the DROP. +the mid-group stream arrives and after it. Both drop the group, and the +subscription keeps delivering whole groups after it. -Public API: none expected. Wire: none. +Public API: none. Wire: none. ## Related diff --git a/quest/m2/ietf-js-joining-fetch.md b/quest/m2/ietf-js-joining-fetch.md index ed936998440..7b2a5c3bb42 100644 --- a/quest/m2/ietf-js-joining-fetch.md +++ b/quest/m2/ietf-js-joining-fetch.md @@ -1,66 +1,37 @@ -# [M] The JS publisher serves the joining FETCH +# [S] The JS publisher answers a joining FETCH after a quick finish ## Goal -On drafts 14 to 18, the JS publisher answers a single-group joining FETCH -with the head of the group its SUBSCRIBE_OK named as Largest, the way the -Rust publisher does. A Largest Object subscription then gets exactly the -objects it asked for: the head on the fetch stream and the rest on the -subscription, with no gap or overlap. The `tail` interop cells with a -native JS or browser publisher keep passing on every draft, including a -track that finishes right after SUBSCRIBE_OK. +Before draft 22, the JS publisher answers a joining FETCH that arrives after +its subscription finished cleanly, from the subscription's saved Largest +group, as the Rust publisher will. The `tail` interop cells with a native JS +or browser publisher pass on a track that finishes right after +SUBSCRIBE_OK. ## Plan -Decided with the maintainer on 2026-10-22, after -[moq-dev/moq#6407](https://github.com/moq-dev/moq/pull/6407) made a refused -joining FETCH cost the Rust subscriber only that group's head. Until this -lands, `js/net/src/ietf/publisher.ts` refuses every FETCH, ignores the -filter before draft 22, and serves the latest group whole from object 0, -rounding SUBSCRIBE_OK's Largest to `{G, 0}`. +Decided 2026-10-25 (maintainer): honoring Largest Object and serving the +head moved into [JavaScript FETCH](/quest/m1/js-fetch.md) in m1; only this +part stays in m2. -Three parts, which only work together: +In the tail test the track is finished before SUBSCRIBE_OK goes out, so +PUBLISH_DONE follows at once and the FETCH arrives to no subscription. Keep a +finished subscription's join answerable under the bound +[Join after a quick finish](/quest/m2/ietf-join-after-finish.md) settles for +Rust, and mirror it on the joinable-SUBSCRIBE registry js-fetch adds. -1. **Honor Largest Object before draft 22.** `subscribeRange` serves the - `nextObject` filter from `edge.next`, as draft 22 already does, and - SUBSCRIBE_OK reports the true Largest on every draft. Otherwise the - FETCH's head duplicates the subscription's first objects, and the Rust - subscriber's "a whole group arrived for one the fill already headed" path - truncates the group to the head. JS subscribers send `unfiltered` before - draft 22 and are unaffected. A third-party Largest Object subscriber that - sends no joining FETCH then loses the head, as the draft says it should. -2. **Serve the head.** Keep a per-session registry of joinable SUBSCRIBEs: - their track, the advertised Largest, the SUBSCRIBE_OK properties and - timescale, the ranking, and the unsubscribe signal. `#runFetch` resolves a - joining FETCH against it: unknown subscription is - `invalid_joining_request_id`, no Largest or a start past it is - `invalid_range`, more than one group is `not_supported` (as in Rust and - the draft-22 fill), and a head evicted from the cache is refused before - FETCH_OK. Then it writes FETCH_OK (end `{G, k + 1}`, not end of track) and - a fetch stream named by the FETCH's own request ID, reusing - `#writeFillGroup`. It is not one of the subscription's counted streams. - Codec work: `Fetch` keeps the joining fields; a `FetchOk` encoder for - drafts 14 to 18 (request ID through 16, GROUP_ORDER on 14, empty - parameters, Track Properties from 15); `FetchFrame` writes draft-14's - explicit object layout and the status after an empty payload on 14 and 15. -3. **Answer a FETCH after the subscription ends.** In the tail test the track - is finished before SUBSCRIBE_OK goes out, so PUBLISH_DONE follows at once - and the FETCH arrives to no subscription. Keep a finished subscription's - join answerable under the bound - [Join after a quick finish](/quest/m2/ietf-join-after-finish.md) settles - for Rust, and mirror it here. - -Tests: unit tests per draft for each refusal and for a served head (bytes the -Rust subscriber stitches), and the tail cells +Tests: a unit test for a FETCH that arrives after PUBLISH_DONE, the bound's +refusals mirrored from Rust, and the tail cells `just test interop --tail --publishers js-native-node,js-native-bun --subscribers rust --versions ietf` -plus the browser publisher on drafts 14 to 18. +plus the browser publisher. -Public API: none. Wire: the JS publisher answers a joining FETCH it refused, -and serves a Largest Object subscription from past Largest. +Public API: none. Wire: none; a late joining FETCH is answered where it was +refused. ## Required -- [Join after a quick finish](/quest/m2/ietf-join-after-finish.md) - settles the bound part 3 reuses +- [JavaScript FETCH](/quest/m1/js-fetch.md) - the joining FETCH and the registry this extends +- [Join after a quick finish](/quest/m2/ietf-join-after-finish.md) - settles the bound this reuses ## Related diff --git a/quest/m2/media-qa-audio.md b/quest/m2/media-qa-audio.md index 03d12b3510c..985296d407a 100644 --- a/quest/m2/media-qa-audio.md +++ b/quest/m2/media-qa-audio.md @@ -26,8 +26,6 @@ Decided in planning (2026-10-20): emitter's private volume `GainNode` (`js/watch/src/audio/emitter.ts`), so these cases need an observation point after that gain, where samples reach the speakers. -- `js/watch/src/audio/render-worklet.ts` gets a unit test; today only the - browser lanes reach it. Recommendations: diff --git a/quest/m2/moqsrc-opus-surround.md b/quest/m2/moqsrc-opus-surround.md index 275e451108a..197e1e7d127 100644 --- a/quest/m2/moqsrc-opus-surround.md +++ b/quest/m2/moqsrc-opus-surround.md @@ -1,10 +1,9 @@ -# [S] moqsrc plays surround Opus +# [XS] moqsrc plays surround Opus, under test ## Goal -`moqsrc` outputs `audio/x-opus` caps that `opusdec` accepts for a 3 to 8 -channel Opus rendition, so surround Opus published through `moqsink` plays -back through GStreamer. +A test proves that a 3 to 8 channel Opus rendition published through +`moqsink` plays back through `moqsrc` and `opusdec` with every channel. ## Plan @@ -13,17 +12,19 @@ worked, so this is not a regression. [moq-dev/moq#4991](https://github.com/moq-dev/moq/pull/4991) fixed publishing and named this follow-up. -`rs/moq-gst/src/source/imp.rs` builds the caps from rate and channels, plus -`codec_data` and `stream-format=ogg` when a description exists. `opusdec` -ignores `codec_data` and reads `streamheader` or the mapping fields, so with -more than two channels it assumes family 0 and refuses. - -Parse the catalog's OpusHead description with the existing `opus::Mapping` -and set `channel-mapping-family`, `stream-count`, `coupled-count`, and -`channel-mapping`. This needs no synthesized OpusTags, unlike a -`streamheader`. Test `opusenc` 5.1 through `moqsink`, a relay, `moqsrc`, and -`opusdec`, checking six decoded channels. The only moqsrc test rendition -today is stereo. +The caps side landed in +[moq-dev/moq#6284](https://github.com/moq-dev/moq/pull/6284): `opus_caps` in +`rs/moq-gst/src/source/imp.rs` parses the catalog's OpusHead with +`opus::Mapping`, sets `channel-mapping-family`, `stream-count`, +`coupled-count`, and `channel-mapping`, and sends the OpusHead plus an empty +OpusTags as the `streamheader`, since `opusdec` reads the head (and its output +gain) only from a two-buffer `streamheader`. Its unit test, +`opus_caps_carry_the_stream_layout`, covers only mono and stereo. + +What remains is the end-to-end test: `opusenc` 5.1 through `moqsink`, a +relay, `moqsrc`, and `opusdec`, checking six decoded channels, plus each +channel count from 3 to 8 in the caps unit test, since every one maps +through the same family-1 table. Public API: none. Wire: none. diff --git a/quest/m2/net-wrap/README.md b/quest/m2/net-wrap/README.md index d04cdaa41d0..3fc95ca25ff 100644 --- a/quest/m2/net-wrap/README.md +++ b/quest/m2/net-wrap/README.md @@ -32,7 +32,8 @@ and its trait-versus-bridge comparison: that spawns adds the bound at its spawn site. - **Mirror moq-net's current API exactly.** The trait methods are the methods writers and readers call today, with the same names, arguments, - and errors: `create_track`, `reserve_track`, `dynamic`, `demand`, + and errors: `create_track`, `reserve_track`, `requests` (renamed from + `dynamic` by [named requests](/quest/m1/api-audit/requests.md)), `demand`, `create_group(group::Info)`, `append_group`, `append_datagram`, `insert_datagram`, `write_frame`, `create_frame`, `write`, `finish`, `abort(self, Error)`, `closed`, `track(name)` then `.subscribe(..)`, diff --git a/quest/m2/net-wrap/e2ee.md b/quest/m2/net-wrap/e2ee.md index b4e87fe946f..09b98f27417 100644 --- a/quest/m2/net-wrap/e2ee.md +++ b/quest/m2/net-wrap/e2ee.md @@ -77,8 +77,9 @@ the reverse (decided 2026-10-20, maintainer): - Demand and preference cross the wrapper: an encrypted track's demand is its physical track's, and preference updates reach the physical track. - Keep the nonce rules the epic specifies: a group producer still owns its - frame numbering, and a re-created track name is refused, not dropped (see - [rust-publish](/quest/m2/e2ee/rust-publish.md)). + frame numbering, and a re-created or re-served track name resumes above + its high-water marks (decided 2026-10-25, maintainer, replacing the + refusal; see [undeclared names](/quest/m2/e2ee/undeclared-names.md)). - Test the wrappers against the questline's pass-through test cases. The moq-json round trip through an encrypted track waits for [moq-json to take the traits](/quest/m2/net-wrap/json.md). diff --git a/quest/m2/net-wrap/traits.md b/quest/m2/net-wrap/traits.md index c0ce52f7a14..087ddfb46d3 100644 --- a/quest/m2/net-wrap/traits.md +++ b/quest/m2/net-wrap/traits.md @@ -26,11 +26,13 @@ the methods exactly as moq-net has them today. the frame level they stay concrete or buffer whole frames. `frame::Producer<'_>` borrows its group, so its associated type is generic over that lifetime. - - Requests: a broadcast producer's `dynamic` returns its own - `broadcast::Dynamic`, whose `requested_track` yields its own + - Requests: a broadcast producer's `requests` returns its own + `broadcast::Requests` (`dynamic` and `Dynamic` until + [named requests](/quest/m1/api-audit/requests.md) renames them before + the cut this waits on), whose `requested_track` yields its own `track::Request`, so generic code serves undeclared names (as [E2EE undeclared names](/quest/m2/e2ee/undeclared-names.md) needs). A track - dynamic's `requested_fill` yields its own `track::Fill`, whose + `Requests`'s `requested_fill` yields its own `track::Fill`, whose `accept` returns its own group producer and whose `drop_groups` names the rest dropped. - Pending operations: `track::Consumer::subscribe` resolves to the diff --git a/quest/m2/obs-qlog.md b/quest/m2/obs-qlog.md index 3dac0c293ca..5b2f98c94d9 100644 --- a/quest/m2/obs-qlog.md +++ b/quest/m2/obs-qlog.md @@ -18,10 +18,11 @@ Decided with the maintainer on 2026-10-17: `cpp/moq/CMakeLists.txt` runs the `cargo build` for moq-ffi, but only on a source build. `cpp/obs/CMakeLists.txt` builds `cpp/moq` in-tree only when `MOQ_LOCAL` contains it; otherwise it fetches a published `moq-cpp` archive -for `MOQ_VERSION` with `FetchContent`. Both that archive and a prebuilt -`MOQ_FFI_DIR` package carry default features only, so the option requires -the in-tree source build and fails configuration with `FATAL_ERROR` on the -fetched archive or `MOQ_FFI_DIR` path instead of silently dropping the +for `MOQ_VERSION` with `FetchContent`, or uses a local one named by +`MOQ_ARCHIVE_FILE`. That archive and a prebuilt `MOQ_FFI_DIR` package carry +default features only, so the option requires the in-tree source build and +fails configuration with `FATAL_ERROR` on the fetched archive, +`MOQ_ARCHIVE_FILE`, or `MOQ_FFI_DIR` path instead of silently dropping the feature. Test: a CI configure of `cpp/obs` with the option on and `MOQ_LOCAL` diff --git a/quest/m2/obs-source-renditions.md b/quest/m2/obs-source-renditions.md index 785d5a1659f..016efa201ef 100644 --- a/quest/m2/obs-source-renditions.md +++ b/quest/m2/obs-source-renditions.md @@ -39,11 +39,9 @@ Decided with the maintainer on 2026-10-22: to moq-ffi until another binding consumer needs one. - Make `epoch` part of rendition identity for both the active and the pending track. -- moq-ffi's `MoqVideo` record drops hang's `preference` today, so OBS - cannot rank by it until the required m1 quest adds it to every binding. - Decided in review of - [moq-dev/moq#6366](https://github.com/moq-dev/moq/pull/6366): that field - rides m1's breaking binding cut, and this quest only consumes it. +- moq-ffi's `MoqVideo` record carries hang's `preference` in every binding + since [moq-dev/moq#6410](https://github.com/moq-dev/moq/pull/6410); this + quest only consumes it. Extend `cpp/obs/test/moq-source-test.cpp` with these cases: diff --git a/quest/m2/one-port/rtc-feed.md b/quest/m2/one-port/rtc-feed.md index 3708dca649d..2f846076181 100644 --- a/quest/m2/one-port/rtc-feed.md +++ b/quest/m2/one-port/rtc-feed.md @@ -37,15 +37,23 @@ pub enum Socket { } ``` -The mux and each session hold `Arc` instead of -`Arc`, and `UdpSocket` implements `Transmit`, so the bound case -behaves as today. Candidates come from `advertised` instead of the bound -socket's local address; it is a list, like `ice_candidates`, so a dual-stack -peer still gets a same-family candidate. Address-family mapping follows the -real socket, not the advertisement: a dual-stack socket bound to `[::]` may -advertise a public IPv4 address, so the `Transmit` impl owns the IPv4-mapped -conversion the session applies today. Received and pinned addresses use the -same canonical form the demux's flow table keys on, or no pin ever matches. +Build on `moq-ice` (split out in #5702) rather than beside it. +`moq_ice::Socket { udp, locals, inbound }` already separates the advertised +host candidates (`locals`, from `ice_candidates`) from the bound address and +takes routed datagrams on its `Inbound` queue, and the server mux +(`rs/moq-rtc/src/server/mux.rs`) already routes its own `recv_from` loop +into per-session `Inbound`s. What is left is the send side and the mux's +read loop: `moq_ice::Socket`'s `udp: Arc` becomes +`Arc` (`Transmit` lives in `moq-ice`, and `UdpSocket` +implements it, so the bound case behaves as today), and a fed mux routes +datagrams from the demux hook instead of reading a bound socket. Adapt the +sketch above to that: `advertised` is `ice_candidates`, and `recv` feeds +the mux's existing router. Address-family mapping follows the real socket, +not the advertisement: a dual-stack socket bound to `[::]` may advertise a +public IPv4 address, so the `Transmit` impl owns the IPv4-mapped conversion +`moq-ice` applies today. Received and pinned addresses use the same +`moq_ice::canonical` form the demux's flow table keys on, or no pin ever +matches. When ICE selects a pair, the remote address goes out on `pinned`, so the demux routes that tuple to WebRTC before any SRT test (see the [questline README](/quest/m2/one-port/README.md)). A pin also has to be diff --git a/quest/m2/ordered-owed-wait.md b/quest/m2/ordered-owed-wait.md index d11f79cd762..064a7530655 100644 --- a/quest/m2/ordered-owed-wait.md +++ b/quest/m2/ordered-owed-wait.md @@ -27,20 +27,23 @@ sequence until the budget elapsed. The model cannot tell "not yet" from waiting reader (new public API no consumer needs, and opted-in readers hit the same holes); fixing #5475 forward (discontinuity markers still stall, and unlimited-budget readers still hang on a never-produced group). -- m2, because no reader needs it yet. The media path (`moq-mux`'s container - consumer) already waits for reordered groups within `max_delay`, and the - archive writer reads media in arrival order. +- m2. The media path (`moq-mux`'s container consumer) already waits for + reordered groups within `max_delay`. The archive writer keeps its own + reader (`rs/moq-archive/src/producer/ordered.rs`, from + [moq-dev/moq#6160](https://github.com/moq-dev/moq/pull/6160)) that waits + within `max_delay` for a late group or datagram, since `Ordered` skips gaps. Open: what "owed" means on a locally produced track, which gets no drops or -dispositions. The motivating case is the archive's local timeline, read in -order with an unlimited budget. +dispositions. The motivating case was the archive's local timeline, read in +order with an unlimited budget; [moq-dev/moq#6262](https://github.com/moq-dev/moq/pull/6262) +removed that timeline, so no current reader motivates it. - Only a subscription owes sequences, so local tracks keep today's ordered - reads (recommended: no local reader needs to wait, and the archive - timeline can never hang on a hole). + reads (recommended: no local reader needs to wait, and an unlimited-budget + local reader can never hang on a hole). - A local producer's unreleased sequences also count as owed, until it - writes, skips, or aborts them. Every local skip must then be marked, or - the archive timeline hangs. + writes, skips, or aborts them. Every local skip must then be marked, or an + unlimited-budget local reader hangs. Watch for: @@ -51,10 +54,9 @@ Watch for: `max_delay`. - Eviction, expiry, and an aborted group whose slot was reclaimed leave no record today; the wait needs the disposition to survive them. -- History reads with unlimited budgets (`moq-hls` `history::read`, the - archive's local timeline): an evicted or never-produced group must not hang - them. `history_mode_recovers_evicted_timeline_groups` and - `history_mode_refuses_a_retained_recording` failed on #5475. +- Readers with unlimited budgets: an evicted or never-produced group must not + hang them. On #5475 this failed in `moq-hls` history mode, which + [moq-dev/moq#6517](https://github.com/moq-dev/moq/pull/6517) removed. Reuse from #5475: its reorder regressions (exact deadline and wakeup, explicit starts, capped reads, a changed budget), and its snapshot-reader change: diff --git a/quest/m2/p2p/watch.md b/quest/m2/p2p/watch.md index a87e4f79244..9399469c70d 100644 --- a/quest/m2/p2p/watch.md +++ b/quest/m2/p2p/watch.md @@ -11,7 +11,10 @@ the peer goes away, with no visible interruption. Publish `@moq/p2p` with its first application consumer in this quest. It is currently private and has no release script; the `test/p2p` harness uses its -workspace source. +workspace source. Before its first workflow publish, a maintainer claims +`@moq/p2p` on npm and JSR with a placeholder and attaches the `moq-dev/moq` +trusted publisher, as [Claim new npm names](/quest/m2/release/npm-first-publish.md) +does for the cut's packages. `moq-watch` and `moq-publish` gain a `p2p` attribute that constructs `Peers` on the shared connection's origin and its relay auth handle (see diff --git a/quest/m2/perf/relay-allocs.md b/quest/m2/perf/relay-allocs.md index cc3e5e20611..b2bd42b89b9 100644 --- a/quest/m2/perf/relay-allocs.md +++ b/quest/m2/perf/relay-allocs.md @@ -29,13 +29,15 @@ that grows with the table shows as a slope. Decided 2026-10-17 (maintainer): m2, under perf. -Decided 2026-10-22 (maintainer): this waits on -[dropping web-transport-trait](/quest/m1/drop-web-transport-trait.md), which -replaces the session clones and boxed opens at the top of this profile; -re-profile after it lands before fixing what remains. +Decided 2026-10-22 (maintainer): this waits on the work that replaces the +session clones and boxed opens at the top of this profile; re-profile after +it lands before fixing what remains. Since 2026-10-23 the QUIC half of that +removal is [native WebTransport](/quest/m1/quic-api/native-web.md), so the +relay's QUIC profile waits on it rather than on all of +[dropping web-transport-trait](/quest/m1/drop-web-transport-trait.md). Public API: none expected. Wire: none. ## Required -- [Drop web-transport-trait](/quest/m1/drop-web-transport-trait.md) - removes the per-stream session clone and boxed future, so the profile is retaken after it +- [Native WebTransport](/quest/m1/quic-api/native-web.md) - removes the QUIC path's per-stream session clone and boxed future, so the profile is retaken after it diff --git a/quest/m2/perf/uring-quiescence.md b/quest/m2/perf/uring-quiescence.md index 0cb975542b9..8341b15accf 100644 --- a/quest/m2/perf/uring-quiescence.md +++ b/quest/m2/perf/uring-quiescence.md @@ -5,7 +5,7 @@ A received packet's QUIC processing and the sends it produces are staged in the same drive-loop turn, so one enter carries the reply. Today `Tasks::poll` runs one pass over the ready set per turn and a mid-pass wake -lands in the next turn by design (rs/kio/src/task.rs:246-257, test +lands in the next turn by design (`Tasks::poll` in rs/kio/src/task.rs, test `a_mid_pass_wake_lands_in_the_next_pass`), so a datagram costs three turns: dispatch sets the demux bit, the demux drains packets and kicks each connection driver (`Endpoint::demux` in @@ -27,8 +27,9 @@ must not starve the socket. (today), 2, 4, and 8. - Dispatch runs before the task pass, not after: the loop becomes reap and dispatch, then tasks to quiescence, then submit or park. That removes the - stale first pass a park-returning turn runs today (worker.rs:180 polls on - readiness the previous turn already consumed). + stale first pass a park-returning turn runs today (the worker loop in + rs/moq-uring/src/worker.rs polls tasks on readiness the previous turn + already consumed). - The egress driver stages one GSO train of `TRAIN_SEGMENTS = 63` segments per turn and then wakes itself (`Driver::flush` in rs/moq-uring/src/quic/driver/connection.rs), so a deep backlog pays a @@ -41,6 +42,9 @@ must not starve the socket. Decided in the 2026-09-30 audit: the egress requeue quest merged here, since both budgets need the same sweep. - Add `passes` per turn to the metrics beside `turns`. +- [Task capacity](/quest/m2/performance-survey/task-capacity.md) also + reshapes the `Tasks::poll` wake-word snapshot. Whichever of the two lands + second rebases its benchmark on the other. - Ingress fairness: a pass budget does not bound work inside one poll. The endpoint's `Inner::poll_run` calls `poll_recv` until `Pending`, so one poll drains every queued receive completion, and `Endpoint::demux` drains diff --git a/quest/m2/performance-survey/README.md b/quest/m2/performance-survey/README.md index ec9126c6d8a..8217e441ee2 100644 --- a/quest/m2/performance-survey/README.md +++ b/quest/m2/performance-survey/README.md @@ -70,7 +70,7 @@ benchmark result is claimed. For settings sweeps, record effective values, not only requested ones. [Resolved client defaults](/rs/moq-tokio/src/quic.rs) currently allow 10,000 -streams per direction, 64 MiB connection receive credit, 4 MiB per-stream +streams per direction, 128 MiB connection receive credit, 4 MiB per-stream credit and 16 MiB send credit. [Relay peer overrides](/rs/moq-relay/src/limits.rs) allow 100,000 streams and 640 MiB each of connection receive and send credit. Standalone backend defaults differ again; benchmark the effective settings @@ -143,9 +143,10 @@ No duplicate quest or workflow edit belongs here. ### Existing ownership and deliberately deferred work -- [Release performance](/quest/m1/release-perf/README.md) owns sparse gaps, +- [Release performance](/quest/m1/release-perf/README.md) fixed sparse gaps, resume duplicates, idle GC, stats drain, FETCH fan-out, JS frame regressions, - and paired comparison correctness. Active PRs already address these. + and paired comparison correctness; its remaining work is the final + main-against-release comparison. - [Perf](/quest/m2/perf/README.md) owns measured channel contention, relay allocations, ingest batching, worker quiescence and runtime selection. - The [release audit](/quest/m1/release-audit/README.md) bounded the media diff --git a/quest/m2/performance-survey/announced-snapshots.md b/quest/m2/performance-survey/announced-snapshots.md index 3c9157fd07e..98b65e34254 100644 --- a/quest/m2/performance-survey/announced-snapshots.md +++ b/quest/m2/performance-survey/announced-snapshots.md @@ -8,8 +8,8 @@ results, with avoidable copying or unrelated-route work removed if worthwhile. ## Plan [serve_announced](/rs/moq-relay/src/web.rs) creates an announcement consumer, -collects prefixes, creates another Vec of strings and joins them per request -(lines 1011-1044). Some output-size cost is necessary; repeated snapshot and +collects prefixes, creates another Vec of strings and joins them per request. +Some output-size cost is necessary; repeated snapshot and formatting work is the hypothesis, not proof of a bottleneck. Sweep registered routes × HTTP pollers, independently varying matched prefix diff --git a/quest/m2/performance-survey/audio-batches.md b/quest/m2/performance-survey/audio-batches.md index eb91dfc7794..0dd7c00361e 100644 --- a/quest/m2/performance-survey/audio-batches.md +++ b/quest/m2/performance-survey/audio-batches.md @@ -8,9 +8,9 @@ same remaining samples, while preserving audio and timestamp behavior. ## Plan [Producer::publish_full_frames](/rs/moq-audio/src/encode/producer.rs) -front-drains into a new Vec for each packet after appending the entire input -(lines 379-389). [Resampler::convert](/rs/moq-audio/src/resample.rs) likewise -front-drains after every chunk (line 291). N chunks in a call can move a +front-drains into a new Vec for each packet after appending the entire input. +[Resampler::convert](/rs/moq-audio/src/resample.rs) likewise +front-drains after every chunk. N chunks in a call can move a triangular number of trailing samples. Capture and binding producers use these paths; small normal capture periods may hide the cost. diff --git a/quest/m2/performance-survey/browser-media-copies.md b/quest/m2/performance-survey/browser-media-copies.md index 1bf171afcc6..66825c2f03a 100644 --- a/quest/m2/performance-survey/browser-media-copies.md +++ b/quest/m2/performance-survey/browser-media-copies.md @@ -2,18 +2,20 @@ ## Goal -CMAF decode and LOC encode retain only copies that earn their lifetime or +CMAF decode and legacy frame encode retain only copies that earn their lifetime or API benefits under representative browser media workloads. ## Plan -[CMAF decode](/js/hang/src/cmaf/decode.ts) slices each sample from mdat -(line 437). [LOC encoding](/js/loc/src/index.ts) allocates contiguous framed -output and copies the payload (lines 181-195). These are concrete copy sites, +[CMAF decode](/js/hang/src/cmaf/decode.ts) slices each sample from mdat. +[Legacy `encodeFrame`](/js/hang/src/container/legacy.ts), the publish path's +framing since the JS LOC encoder was removed in +[moq-dev/moq#6517](https://github.com/moq-dev/moq/pull/6517), allocates +contiguous framed output and copies the payload. These are concrete copy sites, not proof that views are cheaper: a small sample view may pin a large fragment, and a WebCodecs copyTo may be necessary to obtain bytes at all. -Extend the existing browser CMAF case and add LOC framing measurements. +Extend the existing browser CMAF case and add legacy framing measurements. Sweep tracks × consumers, sample size × samples per fragment, and held-sample lifetime. Report JS-visible allocation/copy volume, CPU, GC and retained heap for immediate consumption and a slow reader. Separate format cost from codec @@ -23,8 +25,8 @@ Check bytes, codec configuration, timestamps and ownership through close. A view/owned-buffer change must preserve mutation isolation and release large backing allocations promptly. Keep current copies if that tradeoff wins. This extracts the recurring audit's sample-copy item; missing generic -`js/msf`/`js/flate` coverage remains with that audit. CMAF malformed-count -handling stays with the release audit. +`js/msf`/`js/flate` coverage remains with that audit. CMAF parsing is already +bounded by [moq-dev/moq#6378](https://github.com/moq-dev/moq/pull/6378). Follow the [survey's benchmark contract](/quest/m2/performance-survey/README.md): measure the baseline before optimizing, retain paired evidence, verify equivalent diff --git a/quest/m2/performance-survey/cache-backing.md b/quest/m2/performance-survey/cache-backing.md index 0afb507802d..82eb01f58bc 100644 --- a/quest/m2/performance-survey/cache-backing.md +++ b/quest/m2/performance-survey/cache-backing.md @@ -23,10 +23,11 @@ backing or assume each slice owns a fresh allocation. Compare continued sharing with copying/compaction only where the workload warrants it. This covers completed reliable groups. Coordinate with -receive leases ([moq-dev/moq#5878](https://github.com/moq-dev/moq/pull/5878)), -[browser media copies](/quest/m2/performance-survey/browser-media-copies.md), -and the [release audit](https://github.com/moq-dev/moq/pull/6311), which owns -decoded datagrams and incomplete/reordered media storage. +receive leases ([moq-dev/moq#5878](https://github.com/moq-dev/moq/pull/5878)) +and [browser media copies](/quest/m2/performance-survey/browser-media-copies.md). +Decoded datagrams are already billed to the cache pool +([moq-dev/moq#6413](https://github.com/moq-dev/moq/pull/6413)) and the media +reorder queues are bounded ([moq-dev/moq#6453](https://github.com/moq-dev/moq/pull/6453)). Follow the [survey benchmark contract](/quest/m2/performance-survey/README.md): baseline first, paired evidence, equivalent work and nightly coverage. A diff --git a/quest/m2/performance-survey/cache-metadata.md b/quest/m2/performance-survey/cache-metadata.md index 8da660418b1..aa71b435355 100644 --- a/quest/m2/performance-survey/cache-metadata.md +++ b/quest/m2/performance-survey/cache-metadata.md @@ -20,11 +20,19 @@ grow/drain cycles to expose reclamation CPU, copies and allocation churn. Preserve arrival cursors, live-edge protection and useful cache retention. Correct the accounting explanation with the chosen implementation. -Coordinate with [Front eviction](/quest/m1/unbounded-groups/front-eviction.md) and -[log adoption](/quest/m2/perf/kio-channel-contention/log-adoption.md) if their -representation has changed. Idle GC clock and lock work was settled by -[moq-dev/moq#6291](https://github.com/moq-dev/moq/pull/6291); this owns retained queue backing. +Coordinate with [Front eviction](/quest/m1/unbounded-groups/front-eviction.md) if +its representation has changed. Land the benchmark now, but any shrink of the +`arrival`/`evict` deques waits on +[log adoption](/quest/m2/perf/kio-channel-contention/log-adoption.md)'s verdict, +since that quest may move `TrackState` arrival order onto the kio log. +Idle GC clock and lock work was settled by +[moq-dev/moq#6291](https://github.com/moq-dev/moq/pull/6291); this owns +retained queue backing. Follow the [survey benchmark contract](/quest/m2/performance-survey/README.md): baseline first, paired evidence, equivalent work and nightly coverage. A measured no-win is valid. No API or wire change is expected; report actual impact. + +## Related + +- [Log adoption](/quest/m2/perf/kio-channel-contention/log-adoption.md) - may replace the `arrival` deque this quest would shrink diff --git a/quest/m2/performance-survey/gst-buffers.md b/quest/m2/performance-survey/gst-buffers.md index 7e096d0f781..d9c8e951e69 100644 --- a/quest/m2/performance-survey/gst-buffers.md +++ b/quest/m2/performance-survey/gst-buffers.md @@ -8,8 +8,8 @@ proven with both fast and slow consumers. ## Plan [Sink::forward_buffer](/rs/moq-gst/src/sink/imp.rs) receives an owned -GStreamer buffer but maps and copies its full encoded payload into Bytes -(lines 714-737). Egress already wraps Bytes in a GStreamer buffer. The ingest +GStreamer buffer but maps and copies its full encoded payload into Bytes. +Egress already wraps Bytes in a GStreamer buffer. The ingest copy could be avoidable, but retaining a mapped upstream buffer can exhaust a finite producer pool or pin more memory than copying. diff --git a/quest/m2/performance-survey/hls-serving.md b/quest/m2/performance-survey/hls-serving.md index 7c576e9397d..ed7b9bb8f2d 100644 --- a/quest/m2/performance-survey/hls-serving.md +++ b/quest/m2/performance-survey/hls-serving.md @@ -9,7 +9,7 @@ measurements justify sharing, without retaining stale or unauthorized output. [Rendition::fetch](/rs/moq-hls/src/export/rendition.rs) creates a muxer, fetches group ranges, gathers frames and constructs a fragment on each segment -request (lines 1147-1217). Init construction already has caching; do not count +request. Init construction already has caching; do not count it as missing. Media playlists snapshot/render on each call, and DASH builds representations from the same rendition state. @@ -23,8 +23,7 @@ handover, concurrent first requests and authorization differences. Shared in-flight work or bounded immutable output caching are candidates only after profiling. Keys and ownership must include the actual content instance; never let a reused row number cross epochs or cache one viewer's credentials -in another's playlist. The archive/HLS contract quests and the -[release audit](/quest/m1/release-audit/README.md) HLS quests retain policy decisions. +in another's playlist. The archive/HLS contract quests retain policy decisions. This takes ownership of the recurring audit's HLS serving coverage item. Follow the [survey's benchmark contract](/quest/m2/performance-survey/README.md): diff --git a/quest/m2/performance-survey/js-cached-groups.md b/quest/m2/performance-survey/js-cached-groups.md index e7a9bf4e256..92571d76085 100644 --- a/quest/m2/performance-survey/js-cached-groups.md +++ b/quest/m2/performance-survey/js-cached-groups.md @@ -7,9 +7,9 @@ does no unnecessary whole-backlog sorting. ## Plan -[Track replay](/js/net/src/track.ts) mirrors every cached group on join -(line 1061); each mirror pushes and sorts the entire unread group array -(lines 1096-1098). Even ordered replay sorts arrays of sizes 1 through G. +[Track replay](/js/net/src/track.ts) mirrors every cached group on join; +each mirror pushes and sorts the entire unread group array. Even ordered +replay sorts arrays of sizes 1 through G. The separate timeline index already has an append fast path and ordered insertion. Existing track timing drains immediately and misses growing queues. @@ -22,7 +22,7 @@ mirror lifetimes remain identical. Reuse existing ordering machinery if it makes the result simpler. This is distinct from subscription update traffic in -[moq-dev/moq#6305](https://github.com/moq-dev/moq/pull/6305) and the storage +[moq-dev/moq#6391](https://github.com/moq-dev/moq/pull/6391) and the storage contract in [unbounded JS groups](/quest/m1/unbounded-groups/js.md). Follow the [survey's benchmark contract](/quest/m2/performance-survey/README.md): diff --git a/quest/m2/performance-survey/jwt-verification.md b/quest/m2/performance-survey/jwt-verification.md index 295b36a9b3a..23be33be36e 100644 --- a/quest/m2/performance-survey/jwt-verification.md +++ b/quest/m2/performance-survey/jwt-verification.md @@ -8,7 +8,7 @@ and avoidable work is reduced without weakening key rotation or validation. ## Plan [Serve::verify](/rs/moq-auth/src/serve.rs) rereads a key or whole key-set -file for each request, then imports it (lines 249-271). A Key's cached crypto +file for each request, then imports it. A Key's cached crypto material is therefore not shared across these requests. The [KeySet lookup](/rs/moq-auth/src/set.rs) is linear, while header parsing also occurs in [Key::decode](/rs/moq-auth/src/key.rs). Existing session admission diff --git a/quest/m2/performance-survey/quic-schedule-churn.md b/quest/m2/performance-survey/quic-schedule-churn.md index ded9b12e813..2f05c9d4b4e 100644 --- a/quest/m2/performance-survey/quic-schedule-churn.md +++ b/quest/m2/performance-survey/quic-schedule-churn.md @@ -8,8 +8,8 @@ work on unrelated groups sharing a connection. ## Plan [The QUIC scheduler](/rs/moq-quic/src/connection/streams/schedule.rs) -linearly searches round-robin VecDeques in `unring_group` and retirement -(lines 786-821). Priority changes call that path. On shared relay sessions, +linearly searches round-robin VecDeques in `unring_group` and retirement. +Priority changes call that path. On shared relay sessions, repeated updates or unfavorable retirement order can multiply changed groups by unrelated queued groups. Existing dispatch/stream-churn benches do not isolate scheduling-group reprioritization and retirement. diff --git a/quest/m2/performance-survey/socket-queues.md b/quest/m2/performance-survey/socket-queues.md index 7a6d5814d4e..0f37378857c 100644 --- a/quest/m2/performance-survey/socket-queues.md +++ b/quest/m2/performance-survey/socket-queues.md @@ -26,8 +26,7 @@ Report useful goodput, latency, CPU, memory and local versus path loss. Check both shared and io_uring receive paths. Retain fixed defaults if measurements do not justify a new configuration surface; larger queues can worsen delay. -Coordinate with the UDP pool memory quest planned in -[moq-dev/moq#6350](https://github.com/moq-dev/moq/pull/6350) and +Coordinate with [io_uring buffer pool memory](/quest/m2/perf/uring-pool-memory.md) and [runtime selection](/quest/m2/perf/shard-default.md), sharing fixtures and reports without making independent experiments block each other. diff --git a/quest/m2/performance-survey/task-capacity.md b/quest/m2/performance-survey/task-capacity.md index 0637bc528f2..df8043b542a 100644 --- a/quest/m2/performance-survey/task-capacity.md +++ b/quest/m2/performance-survey/task-capacity.md @@ -8,8 +8,8 @@ cost for a burst that has already drained. ## Plan [kio::Tasks](/rs/kio/src/task.rs) grows slots and per-slot wakers, retains -them after completion, and snapshots every allocated wake-bit word each poll -(lines 270-298, 364-370). Dispatch scans those words again. Work can depend on +them after completion, and snapshots every allocated wake-bit word in each +`Tasks::poll`. Dispatch scans those words again. Work can depend on historical peak/64 even when only one task remains. The io_uring worker and session serving loops are current consumers. @@ -32,6 +32,8 @@ much is historical capacity. Related: [worker quiescence](/quest/m2/perf/uring-quiescence.md) and [relay allocations](/quest/m2/perf/relay-allocs.md) own other costs. +Worker quiescence also reshapes the `Tasks::poll` snapshot with a pass +budget; whichever of the two lands second rebases its benchmark on the other. Follow the [survey's benchmark contract](/quest/m2/performance-survey/README.md): measure the baseline before optimizing, retain paired evidence, verify equivalent diff --git a/quest/m2/performance-survey/transport-history.md b/quest/m2/performance-survey/transport-history.md index 0630eb0579c..581f8addded 100644 --- a/quest/m2/performance-survey/transport-history.md +++ b/quest/m2/performance-survey/transport-history.md @@ -22,8 +22,6 @@ fallback URLs and disabled fallback. Never put real credentials in reports. Establish the useful ownership lifetime before changing it. Do not silently collapse endpoint-specific behavior into origin-only keys or pick an arbitrary TTL; bring a necessary racing-policy decision to the maintainer. -Recheck the [release audit](https://github.com/moq-dev/moq/pull/6311) for overlap -before implementation; its current QMux queue scope does not own this set. Follow the [survey benchmark contract](/quest/m2/performance-survey/README.md): baseline first, paired evidence, equivalent work and nightly coverage. A diff --git a/quest/m2/probe/README.md b/quest/m2/probe/README.md index f888a5ce549..4ca7b087780 100644 --- a/quest/m2/probe/README.md +++ b/quest/m2/probe/README.md @@ -94,4 +94,4 @@ This README's own work, once the children land: - [A finished PROBE stops reporting](/quest/m2/probe-end-stale.md) - the receive estimate this epic raises reads unknown once its stream ends - [BBR media study](/quest/m2/quic-bbr-study.md) - preserving an estimate and discovering spare capacity are separate problems -- [Realtime uplink congestion control](/quest/m2/quic-realtime/README.md) - a delay-based alternative to BBR3 on native uplinks, with no ProbeBW_UP phase to ride +- [Realtime uplink congestion control](/quest/m2/quic-realtime/README.md) - a delay-based alternative to BBR3 on native uplinks; it must implement native probing to ship (decided 2026-10-25) diff --git a/quest/m2/quic-egress-profile.md b/quest/m2/quic-egress-profile.md index f220404c893..77ce2dc6a46 100644 --- a/quest/m2/quic-egress-profile.md +++ b/quest/m2/quic-egress-profile.md @@ -24,7 +24,7 @@ workers versus io_uring workers, with the existing profiling captures: `Bytes`, with no coalescing copy. Count allocations and bytes copied per frame above it, split by frame size, so any copy left is located. - Bursts: the QUIC connection paces inside `poll_transmit` on both runtimes - (#4400; `moq-quic`'s `connection/pacing.rs`), so the only open question + (#4400; `moq-congestion`'s pacer), so the only open question is whether a released GSO train leaves the NIC as a burst the bottleneck cannot absorb. On a netem bottleneck, compare inter-packet gaps and queue occupancy for a 64-segment train against the same bytes paced at the @@ -40,7 +40,7 @@ Then, only where the profile shows a cost: with [the I/O boundary redesign](/quest/m2/quic-io-boundary.md), which reworks the same transmit buffers. - `SCM_TXTIME` stamps, one per train, at the time the pacer would have - released it, tested under both `etf` and `fq`. The socket is shared across + released it (the next-send time `moq-congestion`'s trait exposes), tested under both `etf` and `fq`. The socket is shared across connections per worker, so `SO_MAX_PACING_RATE` cannot express per-connection pacing. Measure on a real NIC as well as loopback, since `etf` needs hardware offload to be exact. diff --git a/quest/m2/quic-realtime/README.md b/quest/m2/quic-realtime/README.md index 77c351f27d2..214973e1d17 100644 --- a/quest/m2/quic-realtime/README.md +++ b/quest/m2/quic-realtime/README.md @@ -33,6 +33,15 @@ spike assumed no consumer; teleoperation uplinks are one): in `moq-congestion`. The spike's moq-shaper uplink presets decide it; the report states the shaper's timing jitter as the noise floor of the delay signal. +- Native probing is required (decided 2026-10-25, maintainer). Every + candidate implements `moq_congestion::Controller::probes` and reports + `probe_inflight`, so moq-quic's native probing + ([moq-dev/moq#6635](https://github.com/moq-dev/moq/pull/6635)) still + raises its estimate, and the bake-off measures probing alongside queuing + delay. Reason: the [native rung disable](/quest/m1/simulcast-rung-disable.md) + only stops a rung on a session with a sender, so a `Realtime` that cannot + probe would silently leave moq-cli, OBS, and FFI simulcast publishers with + no rung disable or recovery. A candidate that cannot probe does not ship. - Naming: add `Realtime` to `moq_tokio::quic::CongestionControl` (`realtime` on the CLI and in TOML), which is `non_exhaustive`, so it is additive. `Delay` keeps meaning BBR3; reword its doc line to say model-based, since diff --git a/quest/m2/quic-realtime/bbr-delay-bound.md b/quest/m2/quic-realtime/bbr-delay-bound.md index dcf558c9f4a..bf0d05e52f3 100644 --- a/quest/m2/quic-realtime/bbr-delay-bound.md +++ b/quest/m2/quic-realtime/bbr-delay-bound.md @@ -15,6 +15,10 @@ against it. - Build it as a configuration of `moq-congestion`'s BBR3, not a fork of the module, so a loss leaves nothing to delete but the option. +- Keep BBR3's `Controller::probes` and `probe_inflight` working under the + brake, so moq-quic's native probing still raises the estimate (decided 2026-10-25, maintainer; + see the [epic](/quest/m2/quic-realtime/README.md)). Measure that a stopped + rung recovers under it. - Unit tests drive the controller with mocked time, not wall clock. ## Required diff --git a/quest/m2/quic-realtime/gcc.md b/quest/m2/quic-realtime/gcc.md index 5ef578b2ee0..b962eeee63e 100644 --- a/quest/m2/quic-realtime/gcc.md +++ b/quest/m2/quic-realtime/gcc.md @@ -24,6 +24,10 @@ fork. window and `ControllerMetrics::{pacing_rate, bandwidth_estimate}`, keeping its congestion-window pushback as the inflight bound. - Keep the BSD license and PATENTS file with the vendored code. +- Implement `Controller::probes` and report `probe_inflight` so moq-quic's + native probing still raises the estimate (decided 2026-10-25, maintainer; + see the [epic](/quest/m2/quic-realtime/README.md)). Measure that a stopped + rung recovers under it. - Unit tests drive the controller with mocked time, not wall clock. ## Required diff --git a/quest/m2/quic-realtime/scream.md b/quest/m2/quic-realtime/scream.md index 219c53ad8a1..3e881082316 100644 --- a/quest/m2/quic-realtime/scream.md +++ b/quest/m2/quic-realtime/scream.md @@ -18,6 +18,10 @@ BSD-licensed reference (EricssonResearch/scream) to check behavior. - SCReAM's queue delay comes from one-way delay samples; use the receive-ts forward delay with its basis offset removed (the spike's estimator), not absolute time. +- Implement `Controller::probes` and report `probe_inflight` so moq-quic's + native probing still raises the estimate (decided 2026-10-25, maintainer; + see the [epic](/quest/m2/quic-realtime/README.md)). Measure that a stopped + rung recovers under it. - Unit tests drive the controller with mocked time, not wall clock. ## Required diff --git a/quest/m2/rate-grant.md b/quest/m2/rate-grant.md index 69550ca13a5..f89611d4031 100644 --- a/quest/m2/rate-grant.md +++ b/quest/m2/rate-grant.md @@ -18,12 +18,13 @@ Decided in the quest audit: this quest owns net's in-band grants, their lite codecs, relay propagation, and publisher clamps. Token claims and HTTP grants carry path patterns only; object roles, including -bitrate caps, are refused during parsing. The first transport-enforcement quest -reintroduces optional caps with a type moq-auth owns (or a lower shared units -crate), converted at the relay boundary so moq-auth never depends on moq-net. -Its parsers and refusal rules must prevent any transport without enforcement -from silently discarding a cap. This quest reuses only caps that admission -actually enforces. +bitrate caps, are refused during parsing. Decided 2026-10-25 (maintainer): +[QUIC caps](/quest/m2/rate-quic.md) reintroduces optional caps with a type +moq-auth owns (or a lower shared units crate), converted at the relay boundary +so moq-auth never depends on moq-net, and this quest Requires it. Its parsers +and refusal rules must prevent any transport without enforcement from silently +discarding a cap. This quest reuses only caps that admission actually +enforces. Update the AUTH grant in `drafts/draft-lcurley-moq-lite.md` and run `just drafts check`. Rates target the next unpublished lite version after finalized lite-07, @@ -42,3 +43,4 @@ rates, on the next unpublished lite version after lite-07 only. ## Required - [In-band auth](/quest/m1/auth/README.md) - the AUTH grant this extends +- [QUIC caps](/quest/m2/rate-quic.md) - reintroduces the moq-auth cap type the grant carries diff --git a/quest/m2/rate-quic.md b/quest/m2/rate-quic.md index 5b85283b0e8..3d48b64c9d3 100644 --- a/quest/m2/rate-quic.md +++ b/quest/m2/rate-quic.md @@ -10,12 +10,14 @@ refused. ## Plan Token claims and HTTP grants carry path patterns only; object roles, including -bitrate caps, are refused during parsing. The first transport-enforcement quest -reintroduces optional caps with a type moq-auth owns (or a lower shared units -crate), converted at the relay boundary so moq-auth never depends on moq-net. -Its parsers and refusal rules must prevent any transport without enforcement -from silently discarding a cap. This quest reuses only caps that admission -actually enforces. +bitrate caps, are refused during parsing. Decided 2026-10-25 (maintainer): +this quest reintroduces optional caps with a type moq-auth owns (or a lower +shared units crate), converted at the relay boundary so moq-auth never depends +on moq-net. [WebSocket caps](/quest/m2/rate-websocket.md) reuses that type, +and [the grant](/quest/m2/rate-grant.md) Requires this quest for it. Its +parsers and refusal rules must prevent any transport without enforcement from +silently discarding a cap: admission accepts a cap only on a transport that +enforces it. Decided in the 2026-10-04 plan: enforce in the QUIC stack, through backpressure the peer cannot ignore, rather than metering and closing. diff --git a/quest/m2/rate-websocket.md b/quest/m2/rate-websocket.md index e2b25dcb531..1f792155cbb 100644 --- a/quest/m2/rate-websocket.md +++ b/quest/m2/rate-websocket.md @@ -10,12 +10,13 @@ a request has no connection to meter. ## Plan Token claims and HTTP grants carry path patterns only; object roles, including -bitrate caps, are refused during parsing. The first transport-enforcement quest -reintroduces optional caps with a type moq-auth owns (or a lower shared units -crate), converted at the relay boundary so moq-auth never depends on moq-net. -Its parsers and refusal rules must prevent any transport without enforcement -from silently discarding a cap. This quest reuses only caps that admission -actually enforces. +bitrate caps, are refused during parsing. Decided 2026-10-25 (maintainer): +[QUIC caps](/quest/m2/rate-quic.md) reintroduces optional caps with a type +moq-auth owns (or a lower shared units crate), converted at the relay boundary +so moq-auth never depends on moq-net, and [the grant](/quest/m2/rate-grant.md) +Requires it. This quest reuses that type. Its parsers and refusal rules must +prevent any transport without enforcement from silently discarding a cap, so +WebSocket admission keeps refusing caps until this wrapper enforces them. TCP carries the backpressure, but only if the kernel cannot absorb the excess: @@ -44,6 +45,10 @@ Tests: a client writing flat out is held to the cap plus the bounded buffers; delivery to a capped subscriber never exceeds it; a cap change on a live session applies. +## Required + +- [QUIC caps](/quest/m2/rate-quic.md) - introduces the moq-auth cap type this reuses + ## Related - [TCP acceptor](/quest/m2/one-port/tcp-demux.md) - boxes the accepted stream, so it must keep the socket handle these buffer sizes need diff --git a/quest/m2/readers-range-live.md b/quest/m2/readers-range-live.md index cf1f025585e..a2ed3605058 100644 --- a/quest/m2/readers-range-live.md +++ b/quest/m2/readers-range-live.md @@ -2,10 +2,7 @@ ## Goal -Condition: [moq-dev/moq#6278](https://github.com/moq-dev/moq/pull/6278) has -merged, so `Range::live()` starts at the live edge on a warm relay copy too. - -Then the readers that wait for the largest group and raise their floor to +The readers that wait for the largest group and raise their floor to start near it (moq-audio, moq-video, `moq play`, and both moq-room and `@moq/room` chat) subscribe with a relative range instead, with no change in what they play: `Range::live()` for a start at the newest group, and a @@ -15,6 +12,9 @@ decodes. ## Plan +Since [moq-dev/moq#6278](https://github.com/moq-dev/moq/pull/6278), +`Range::live()` starts at the live edge on a warm relay copy too. + Inventory with `rg 'largest\(|poll_largest' rs/ js/` first, since Rust chat polls rather than awaits; keep any caller that needs the position itself rather than a start. Each reader keeps its existing tests, diff --git a/quest/m2/release/README.md b/quest/m2/release/README.md index 6468ecf84b2..b88913b24ad 100644 --- a/quest/m2/release/README.md +++ b/quest/m2/release/README.md @@ -31,7 +31,7 @@ production stability proven merely because upstream artifacts published. - [API shape review](/quest/m2/release/api-review.md) - every new or changed public API reviewed for a better shape before the cut - [Feature review](/quest/m2/release/feature-review.md) - every major work line since the last release judged end to end before the cut - [Cargo feature review](/quest/m2/release/feature-flags.md) - defaults stay lean and every remaining feature is scoped to the crate that owns it, before the cut -- [Claim new npm names](/quest/m2/release/npm-first-publish.md) - a maintainer claims `@moq/clock`, `libav`, `video`, and `p2p` on npm and JSR with trusted publishing, before the cut publishes them +- [Claim new npm names](/quest/m2/release/npm-first-publish.md) - a maintainer claims `@moq/clock`, `libav`, and `video` on npm and JSR with trusted publishing, before the cut publishes them - [Cut and publish](/quest/m2/release/cut.md) - independently executable after m1; release-triggered publication - [Published assets](/quest/m2/release/assets-release.md) - npm carries hostable worklets - [C++ publication](/quest/m2/release/cpp-release.md) - archives and matching OBS plugin publish and install diff --git a/quest/m2/release/assets-release.md b/quest/m2/release/assets-release.md index 07d09c89b25..20b1eb47d91 100644 --- a/quest/m2/release/assets-release.md +++ b/quest/m2/release/assets-release.md @@ -10,9 +10,9 @@ this quest and every `Required` entry that links it. ## Plan -As of 2026-10-20 `main` carries `assets()` (#4518) but `release` does not, so -the newest releases (`@moq/watch` 0.6.3, `@moq/publish` 0.5.3) still lack it; -`@moq/watch` 0.6.3's `exports` has no `./assets/*`. moq.pro's dashboard +As of 2026-10-25 `main` carries `assets()` (#4518) but `release` does not, so +the newest releases (`@moq/watch` 0.6.6, `@moq/publish` 0.5.3) still lack it; +`@moq/watch` 0.6.6's `exports` has no `./assets/*`. moq.pro's dashboard adoption of the hosted files lives in moq.pro's package adoption, not here. Check with `npm view @moq/watch version` and `npm view @moq/publish version` for a newer version, then confirm its `exports` lists `./assets/*`. diff --git a/quest/m2/release/c-publish.md b/quest/m2/release/c-publish.md index a106eb2dc58..0e0755de89a 100644 --- a/quest/m2/release/c-publish.md +++ b/quest/m2/release/c-publish.md @@ -7,15 +7,15 @@ the C interop client build and run against it from a clean consumer checkout. ## Plan -The m1 C line proves the same cases with the built archive before the cut. -This quest proves registry/release delivery after publication and records the -artifact versions and results. It cannot block the cut that creates them. -The hand-written crate never published, so there is no old package to -supersede; its source stays until +The generated C line proves the same cases with the built archive before +publication. This quest proves registry/release delivery after publication +and records the artifact versions and results. The hand-written `moq-c` 0.7 +ships at the m1 cut (decided 2026-10-25), so the generated 0.8 supersedes a +published package; its source stays until [c-retire](/quest/m2/release/c-retire.md). ## Required - [Breaking release cut](/quest/m2/release/cut.md) - prepared code reaches release before artifact publication and installed proof -- [Generated C preparation](/quest/m1/c/README.md) - package and consumers pass before publication +- [Generated C bindings](/quest/m2/c/README.md) - package and consumers pass before publication diff --git a/quest/m2/release/c-retire.md b/quest/m2/release/c-retire.md index e1c4ae19573..ecec2c44dd6 100644 --- a/quest/m2/release/c-retire.md +++ b/quest/m2/release/c-retire.md @@ -10,9 +10,10 @@ served it are deleted once the generated `moq-c` publishes. The generated - #4288 renamed libmoq to `moq-c` (`rs/moq-c`); this quest deletes that hand-written crate. -- Decided 2026-10-20 (maintainer): no final hand-written notice release, - because the hand-written `moq-c` never published to a registry; the - generated package is the first `moq-c` users install. +- Decided 2026-10-25 (maintainer): the hand-written `moq-c` 0.7 ships at the + m1 cut, so it is a published package when the generated 0.8 replaces it. + Its `MIGRATE.md` history carries into the generated package. This replaces + the 2026-10-20 premise that it never published. - `rs/moq-c/native-libs/` serves the generated package too, and `cpp/moq/cmake/moq-ffi.cmake` serves both C++ and C. Move them to a neutral home shared by both packages before deleting the crate, update every @@ -25,7 +26,7 @@ served it are deleted once the generated `moq-c` publishes. The generated - [Generated C publication](/quest/m2/release/c-publish.md) - the replacement is available and verified -- [C consumers](/quest/m1/c/consumers.md) - nothing in the repository still calls the hand-written ABI +- [C consumers](/quest/m2/c/consumers.md) - nothing in the repository still calls the hand-written ABI ## Related diff --git a/quest/m2/release/cpp-release.md b/quest/m2/release/cpp-release.md index 4fc68bfb173..23fe95f1a4a 100644 --- a/quest/m2/release/cpp-release.md +++ b/quest/m2/release/cpp-release.md @@ -14,8 +14,8 @@ once `main` reaches `release`; check with The version is `cpp/moq/VERSION`; the plugin's is `cpp/obs/VERSION`. Delete this quest once the release exists. -As of 2026-10-20 no `cpp-v*` release exists, and `obs-moq` still publishes -from the release line's C build (`obs-moq-v0.6.17`, 2026-10-17); the first +As of 2026-10-25 no `cpp-v*` release exists, and `obs-moq` still publishes +from the release line's C build (`obs-moq-v0.6.18`, 2026-10-23); the first plugin built on the C++ package replaces it at this tag. Decided when the C++ line landed on `main` (#4079): the first plugin built on the C++ package must not drop the Advanced settings or the dock's protocol and reconnect reason that the moq-c diff --git a/quest/m2/release/dart-publish.md b/quest/m2/release/dart-publish.md index 4a3f306743a..23fb5f21536 100644 --- a/quest/m2/release/dart-publish.md +++ b/quest/m2/release/dart-publish.md @@ -7,8 +7,8 @@ A `moq-dart-v*` tag publishes `moq` to pub.dev with no manual step, as ## Plan -Both packages are on pub.dev (checked 2026-10-20). `moq_ffi` publishes from -every `moq-ffi-v*` tag (0.4.3 through 0.4.17, the latest 2026-10-17), so its +Both packages are on pub.dev (checked 2026-10-25). `moq_ffi` publishes from +every `moq-ffi-v*` tag (0.4.3 through 0.4.18, the latest 2026-10-23), so its tag-driven publish works. `moq` has only 0.1.0 (2026-09-25), the one `moq-dart-v*` tag so far, so whether its tag publishes unattended through trusted publishing is still unproven. diff --git a/quest/m2/release/libmoq-final-release.md b/quest/m2/release/libmoq-final-release.md index 23749330cbe..bd48837587f 100644 --- a/quest/m2/release/libmoq-final-release.md +++ b/quest/m2/release/libmoq-final-release.md @@ -6,14 +6,15 @@ The newest `libmoq` on crates.io is the code-free stub that `main`'s `rs/libmoq` holds, whose README points at `moq-c`. Since #4738 publishing runs only from `release`, so the condition clears at the next `main`-to-`release` cut, which must take `main`'s stub for `rs/libmoq` at a version above -0.6.17. `main`'s stub is at 0.6.12, below versions crates.io already holds as -full crates, so the cut bumps it or the stub never publishes. +crates.io's current `max_version`. `main`'s stub is at 0.6.12, below versions +crates.io already holds as full crates, so the cut bumps it or the stub never +publishes. This quest tracks a condition outside the repository. When it holds, delete this quest and every `Required` entry that links it. ## Plan -As of 2026-10-20 the newest release is libmoq 0.6.17 (2026-10-17), still cut -from `release`'s full crate, not the stub. Check with +As of 2026-10-25 the newest release is libmoq 0.6.18 (2026-10-23), still cut +from `release`'s full crate, not the stub; patch releases keep raising it. Check with `curl -s https://crates.io/api/v1/crates/libmoq` for a newer `max_version`. diff --git a/quest/m2/release/npm-first-publish.md b/quest/m2/release/npm-first-publish.md index a4e39a4cb23..48eb8261aaf 100644 --- a/quest/m2/release/npm-first-publish.md +++ b/quest/m2/release/npm-first-publish.md @@ -2,22 +2,22 @@ ## Goal -`@moq/clock`, `@moq/libav`, `@moq/video`, and `@moq/p2p` exist on npm, and -the three with JSR packages exist on JSR, each with trusted publishing +`@moq/clock`, `@moq/libav`, and `@moq/video` exist on npm, and +the two with JSR packages exist on JSR, each with trusted publishing attached to `moq-dev/moq`. The cut's release workflow can then publish their real versions. A maintainer does this by hand. Delete this quest once the checks pass: -- `npm view @moq/` resolves for all four, and the package settings show +- `npm view @moq/` resolves for all three, and the package settings show the `moq-dev/moq` trusted publisher. -- `https://api.jsr.io/scopes/moq/packages/` returns 200 for `clock`, - `video`, and `p2p`, each linked to the repository. `@moq/libav` sets +- `https://api.jsr.io/scopes/moq/packages/` returns 200 for `clock` + and `video`, each linked to the repository. `@moq/libav` sets `"jsr": false`. ## Plan -Found 2026-10-22 while planning the release audit's follow-ups: all four -names return 404, and nearly every published package (net, hang, watch, +Found 2026-10-22 while planning the release audit's follow-ups: these three +names returned 404, and nearly every published package (net, hang, watch, publish, auth, signals, and more) now depends on `@moq/clock`. npm trusted publishing attaches only to an existing package, so without this the cut's publish fails on these names and still publishes dependents that can't @@ -34,6 +34,10 @@ Until the cut publishes `@moq/libav` 0.1.0, Windows `bun install` keeps failing, since `js/libav/build.ts` there unpacks the published tarball of the workspace version. The placeholder does not change that. +`@moq/p2p` is out of scope: it is private with no release script, so the cut +does not publish it. [Watch opts in](/quest/m2/p2p/watch.md) owns its first +publication, including claiming its names. + Public API: none. Wire: none. ## Related diff --git a/quest/m2/resume-redesign.md b/quest/m2/resume-redesign.md index e6a0bb53069..764fe40808a 100644 --- a/quest/m2/resume-redesign.md +++ b/quest/m2/resume-redesign.md @@ -14,9 +14,9 @@ cost no longer grows with readers times copies. From the 2026-10-24 pre-cut audit at 151a529670. Internal only: no public API or wire change, so it can ship after the cut. The audit recommended after the cut, not before: a rewrite now would discard the large resume test -corpus right before release. Ask the maintainer whether to move it into m1 -instead, since release still ships the older splice design and nearly all -of `resume.rs` is new at this cut. +corpus right before release. Decided 2026-10-25 (maintainer): the +[model test](/quest/m1/resume-model-test.md) comes first and gates the cut; +this rewrite moves to m1 only if that test shows design-level bugs. **Why.** Main replaced release's splice design with per-reader route copies in 0382d309f4 (2026-10-04). Since then about 30 of 58 commits to @@ -46,11 +46,15 @@ paths diverged. **Sketch.** 1. Route copies become successive writers into the logical `TrackState`, - the only cache. Same-epoch routes carry identical content, so a later - writer's duplicate sequence is dropped or kept as the slot's backup. The - old writer keeps feeding below the new writer's first group and is - released once the logical cache covers that span or the aggregate budget - passes, judged once per track by `drifted`, not per reader. + the only cache. Every writer is of the instance the subscription started + on: a newer [publisher epoch](/doc/concept/moq-lite.md#publisher-epochs) + never feeds it (the route rule in `rs/moq-net/AGENTS.md`), so its readers + end with an error once their instance's routes are gone. Same-epoch routes + carry identical content, so a later writer's duplicate sequence is + dropped or kept as the slot's backup. The old writer keeps feeding below + the new writer's first group and is released once the logical cache + covers that span or the aggregate budget passes, judged once per track by + `drifted`, not per reader. 2. The upstream request is already the union of readers' resume points (`Producer::subscription`), so per-reader copy cursors are local overhead. 3. Readers become plain cursors: delete `Inner::Resume`, `Reader`, `Copy`, diff --git a/quest/m2/starvation-frames.md b/quest/m2/starvation-frames.md index 6663375ceb8..753a13974e0 100644 --- a/quest/m2/starvation-frames.md +++ b/quest/m2/starvation-frames.md @@ -47,8 +47,9 @@ first after the next release; frame-granularity sampling then adds the ACK hook without making either resolution a release gate. Decided 2026-10-08: the group-granularity sampler is the -[viewer lag histogram](/quest/m2/qos/lag-histogram.md), not yet on main (its -code is on `wip/4133-lag-histogram`), so that quest is Required. +[viewer lag histogram](/quest/m2/qos/lag-histogram.md), not yet on main (in +progress as [moq-dev/moq#6791](https://github.com/moq-dev/moq/pull/6791)), so +that quest is Required. Tests: the frontier tracking frame ends under a peer that acknowledges in bursts, with interval samples landing one bucket lower than group-granularity diff --git a/quest/m2/stats/README.md b/quest/m2/stats/README.md index dba62cd8c94..81d4fc0110b 100644 --- a/quest/m2/stats/README.md +++ b/quest/m2/stats/README.md @@ -32,7 +32,7 @@ kind. extension, `Merge` wrapper, or flattened generic is needed. One layout for relay and clients is given up on purpose. - **Publisher: `stats: { track }` in the catalog.** A root section naming one - snapshot track: `{ transport, renditions: { : stats::Track } }`, + snapshot track: `{ transport, renditions: { : stats::Rendition } }`, plus container sections flattened in the way `Catalog` flattens `ts::Ext`. The stats stay off the catalog track, which would otherwise churn for every viewer on each interval. @@ -72,11 +72,11 @@ kind. tokens let publish under the echo path may report, and no report is authenticated beyond that. - **Feedback track: one snapshot**, `{ transport, renditions: { : - echo::Track } }`, so the publisher looks up its own renditions directly. + echo::Rendition } }`, so the publisher looks up its own renditions directly. - **One type per role, shared across kinds.** - - `stats::Track`: sent frames and bytes, keyframes, skipped frames, target + - `stats::Rendition`: sent frames and bytes, keyframes, skipped frames, target bitrate. - - `echo::Track`: received, decoded, late, decode errors, stalls, + - `echo::Rendition`: received, decoded, late, decode errors, stalls, stalled duration, underruns, newest arrival, latency. - A kind's unused fields are omitted. - `transport` is one type at both roles: rtt and rate gauges, cumulative diff --git a/quest/m2/uniffi-fork.md b/quest/m2/uniffi-fork.md index 50de6b6bc0f..f5b22b3dca9 100644 --- a/quest/m2/uniffi-fork.md +++ b/quest/m2/uniffi-fork.md @@ -7,7 +7,9 @@ generator moq uses (Kotlin, Swift, Python, Go, Dart, C++, and C) comes from [kixelated/rust2](https://github.com/kixelated/rust2) through one pinned flake input. `rs/uniffi-bindgen`, `nix/uniffi-dart-*`, and the per-generator pins are deleted, and the three `kixelated/uniffi-*` generator forks are archived. -Generated bindings are unchanged by the switch itself. +The browser's TypeScript, generated through ubrn's `wasm2` flavour, still +generates against the fork. Generated bindings are unchanged by the switch +itself. ## Plan @@ -16,8 +18,9 @@ UniFFI: - **Base**: UniFFI. [The BoltFFI spike](https://github.com/kixelated/rust2/blob/main/bench/ffi/README.md#boltffi-comparison) recommended it. Decided later in #5838 (2026-10-17, maintainer): - UniFFI-generated moq-ffi serves the browser too, over BoltFFI, so one generator serves every - language. + UniFFI-generated moq-ffi serves the browser too, through ubrn's `wasm2` + flavour (chosen over BoltFFI and hand-written moq-wasm), so one binding + surface serves every language. - **Why fork**: every UniFFI minor release breaks external generators (0.31 changed `module_path`, 0.32 changed the metadata encoding without bumping the contract version, and `main` removes the old library-mode API). No @@ -51,13 +54,19 @@ which publishes `rust2-uniffi`, `rust2-uniffi-core`, `rust2-uniffi-macros`, and Delete `rs/uniffi-bindgen`, the "five places" comments in `flake.nix`, and the `nix/uniffi-dart-*` lockfile and patch. The `sh/{kt,swift,dart,cpp}` packaging scripts stay: they package moq. +- **Browser TypeScript**: ubrn's `wasm2` generator is upstream and reads + UniFFI metadata, which the fork may change. Either show upstream ubrn still + generates from the renamed crates, or carry ubrn's TypeScript generator in + rust2 with the others, as [ubrn 0.32](/quest/m2/rs2ts/ubrn-032.md) already + plans if ubrn has not moved. Include the TypeScript in the regenerated + bindings diff. - Negative durations (2026-10-25, maintainer): the Dart negative-duration patch moq carries goes upstream into the fork's Dart generator, and the Go generator stops panicking on a negative `time.Duration` (the Go wrapper guards it today; delete the guard in the PR that pins a generator with the fix, with a test passing a negative duration through the Go binding). - Update the quests that name a `kixelated/uniffi-*` fork - ([Generated C bindings](/quest/m1/c/README.md)) to point at rust2, then + ([Generated C bindings](/quest/m2/c/README.md)) to point at rust2, then archive the three forks. Public API: none; moq-ffi's dependency names change. Wire: none. @@ -68,6 +77,7 @@ Public API: none; moq-ffi's dependency names change. Wire: none. ## Related +- [ubrn generates UniFFI 0.32 TypeScript](/quest/m2/rs2ts/ubrn-032.md) - the browser generator this switch must keep working - [Payload bytes](https://github.com/kixelated/rust2/blob/main/quest/m1/ffi-bytes.md) - the first patch the fork carries - [Kotlin calls](https://github.com/kixelated/rust2/blob/main/quest/m1/kt-ffi-calls.md) - a Kotlin generator patch the fork carries - [Foreign poll for hot reads](https://github.com/kixelated/rust2/blob/main/quest/m1/ffi-poll-protocol.md) - a generated poll protocol the fork carries; dropping a future at cancel is optional here, since no moq quest waits on it diff --git a/quest/m2/uring-lifecycle.md b/quest/m2/uring-lifecycle.md index c2172e6e458..3f3b35576c0 100644 --- a/quest/m2/uring-lifecycle.md +++ b/quest/m2/uring-lifecycle.md @@ -25,14 +25,16 @@ reachable by an embedder: instead of leaking until idle timeout, and `web::Session` counts live streams rather than closing on its last clone. Update the `Connection` doc that states the opposite. -- Receive close at the read-ahead cap: `stream.rs` parks once the backlog - reaches `READ_AHEAD` before checking `closed()`, so a reset or connection - close (idle timeout included) behind 64 KiB of unread data never - surfaces. `moq-tokio/src/transport.rs` has the same cap and returns - Pending without registering a waker. Fix both: check terminal state - before the cap, keeping held credit, and register the closed watcher so - a read that drains below the cap wakes it. Test with `poll_closed` and - the draining read in separate tasks. +- Receive close at the read-ahead cap: moq-tokio's adapter + (`RecvStream::poll_closed` in `rs/moq-tokio/src/transport.rs`) returns + Pending without registering a waker once its read-ahead reaches + `READ_AHEAD_CAP` (64 KiB), so a reset or connection close (idle timeout + included) behind that much unread data never surfaces. Check terminal + state before the cap and register the closed watcher so a read that + drains below the cap wakes it. Test with `poll_closed` and the draining + read in separate tasks. moq-uring's own `READ_AHEAD` backlog is gone: + [moq-dev/moq#5878](https://github.com/moq-dev/moq/pull/5878) replaced it + with receive leases, whose `poll_lease` checks `closed()` when blocked. Use the existing teardown and credit integration tests' shapes; drive time with the manual clock for the idle-timeout case. diff --git a/quest/m2/wire-stats/README.md b/quest/m2/wire-stats/README.md index 28362bcf87b..4857a763351 100644 --- a/quest/m2/wire-stats/README.md +++ b/quest/m2/wire-stats/README.md @@ -16,8 +16,9 @@ excluded: ACKs, and retransmits. - **WebSocket:** the TCP stream's bytes, which include TLS, WebSocket, and qmux framing and retransmits. -- **Gateways an embedder terminates:** SRT and WebRTC through moq-srt's and - moq-rtc's sources. An embedder terminating RTMP on its own socket +- **Gateways an embedder terminates:** SRT through moq-srt's source. moq-rtc + exposes none since #6454 removed its unused one; it returns only with an + adapter a consumer registers. An embedder terminating RTMP on its own socket registers that socket's `TCP_INFO` counts; moq-rtmp's own `Server` and client expose no source until a consumer needs one. - **TCP receive:** bytes TCP delivered, so a peer's duplicate retransmits @@ -45,7 +46,8 @@ A provider bills the wire, so a relay operator needs per-root wire bytes to see cost per tenant. The shared core landed in #5773 (`778263c50`): `Presence` carries both -counters, `moq_net::stats::Handle::wire` registers embedder sources, and +counters, `stats::Handle::session(label)` then `stats::Session::wire` +registers embedder sources (`Handle::wire` was removed in #6470), and the stats producer samples and preserves their final values. Native tokio and io_uring QUIC sources and their relay integration tests are already on `main`. Reuse that registry and accounting; do not create another one. @@ -66,9 +68,8 @@ receive-side limitations. - **Sampled on the stats tick.** Rejected: a once-only count at session end, which loses a crashed session and lags a long one. - **One source registry for sessions and embedders.** An embedder registers - a gateway connection's wire source against a (tier, root) on its stats - handle, the same path the relay's own sessions use, like the existing - `stats::Handle::refuse` API. + a gateway connection's wire source on a `stats::Session` from its stats + handle's `session(label)`, the same path the relay's own sessions use. ## Required diff --git a/quest/m2/wire-stats/qmux.md b/quest/m2/wire-stats/qmux.md index 4e9bbe6c9ef..e5ded37a9ae 100644 --- a/quest/m2/wire-stats/qmux.md +++ b/quest/m2/wire-stats/qmux.md @@ -24,7 +24,7 @@ beside the field. decide how qmux's I/O path refreshes the `Wire` (for example, after each socket write batch) without a syscall per write, in place of the qmux record bytes `qmux::Session::wire` counts today, including the relay's - `SocketStats` wiring (`rs/moq-relay/src/websocket.rs`). + `SocketStats` wiring (`rs/moq-relay/src/web.rs`). - Test: a loopback WebSocket session's `sent_bytes` covers at least the payload it wrote. diff --git a/quest/m2/wire-stats/srt.md b/quest/m2/wire-stats/srt.md index d733b1dd0a2..66949797d98 100644 --- a/quest/m2/wire-stats/srt.md +++ b/quest/m2/wire-stats/srt.md @@ -17,7 +17,7 @@ the same definition as QUIC's `udp_tx`. - `serve_subscribe` and the publish path own the socket today and expose nothing. Update a `moq_net::transport::Wire` per connection from each statistics update and hand it to the embedder, which registers it - with `moq_net::stats::Handle::wire`. + with `stats::Session::wire` on a session from `stats::Handle::session`. - Read `srt-tokio`'s statistics now rather than waiting on one port (decided 2026-10-20, maintainer, because the source needs no socket ownership change). When [SRT on the shared socket](/quest/m2/one-port/srt-demux.md) diff --git a/quest/m3/README.md b/quest/m3/README.md index 6518973b547..99ea2faa7e1 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -35,7 +35,7 @@ m4's upstream waits in here and moved m2 work with no consumer. - [L4S on the backbone](/quest/m3/quic-ecn.md) - an ECT(1) option in `moq-quic`, an `ecn` config knob, and a dualpi2 measurement, if the ECN measurement says marks survive - [Careful resume on reconnect](/quest/m3/quic-careful-resume.md) - a Rust redial starts at the previous connection's rate - [Cut-through](/quest/m3/cut-through/README.md) - a bufferbloat bench decides whether a relay should forward bytes behind a QUIC stream hole; the build is re-planned if it says go -- [Middle eviction](/quest/m3/middle-eviction.md) - a group too large to keep whole keeps its head for joiners and its tail for live readers, evicting between; speculative, after the GroupState storage benchmark +- [Middle eviction](/quest/m3/middle-eviction.md) - a group too large to keep whole keeps its head for joiners and its tail for live readers, evicting between; speculative, after front eviction - [Unreal prototype](/quest/m3/unreal.md) - a UE5 module on the C++ package with exceptions disabled, rendering a subscribed broadcast to a texture - [vcpkg registry](/quest/m3/cpp-vcpkg.md) - a registry we own serves the prebuilt package to `vcpkg` manifests, once a consumer asks - [Conan remote](/quest/m3/cpp-conan.md) - a remote we own serves the same tarball to `conan install` diff --git a/quest/m3/ac3-low-rate.md b/quest/m3/ac3-low-rate.md index 1b9363403df..2a0ce14b73c 100644 --- a/quest/m3/ac3-low-rate.md +++ b/quest/m3/ac3-low-rate.md @@ -10,7 +10,7 @@ variants, through TS and FLV import and TS export, instead of refusing them. Decided 2026-10-22 (maintainer, from the release audit): support them rather than only documenting the refusal. It is cheap and removes an inconsistency noted in [moq-dev/moq#5758](https://github.com/moq-dev/moq/pull/5758): -`ac3_core` in `rs/moq-mux/src/ts/export.rs` counts bsid up to 10 as an AC-3 +`ac3_core` in `rs/moq-mux/src/container/ts/export.rs` counts bsid up to 10 as an AC-3 core, then hands it to `ac3::parse_header`, which refuses anything above 8. In `rs/moq-mux/src/codec/ac3.rs`, keep the frame-size tables and shift the diff --git a/quest/m3/auth-token-fetch.md b/quest/m3/auth-token-fetch.md index f04f67213f0..c0c2268cef0 100644 --- a/quest/m3/auth-token-fetch.md +++ b/quest/m3/auth-token-fetch.md @@ -37,6 +37,7 @@ the session grant, refused when the verifier refuses, and ended when the grant ends while the response waits on credit. Outgoing requests carry the token. Run cross-language interop for the wire change. -Public API: none expected beyond widening what `auth::RequestKind` names. +Public API: none; `auth::RequestKind`, which gains the two request types, is +crate-private. Wire: the existing AUTHORIZATION TOKEN parameter (0x03), now sent and honored on TRACK_STATUS and FETCH. diff --git a/quest/m3/hidden-exemption.md b/quest/m3/hidden-exemption.md index f5edfae95ef..39d91f15ef1 100644 --- a/quest/m3/hidden-exemption.md +++ b/quest/m3/hidden-exemption.md @@ -12,10 +12,11 @@ for peers that predate it. In m3 because it waits on a deployment, not on code; it comes back once the mesh runs lite-07. -- Remove the `cluster_peer` exemption, now a local inside `Cluster::scope` - (`rs/moq-relay/src/cluster.rs`, with its TODO), so an inbound peer discovers - hidden routes only when it asks, and the forced `with_hidden(true)` on the - outbound dial in the same file. `connection::authorize` no longer exists on +- Remove the exemption, the `with_hidden(token.peer)` in `Cluster::scope` + (`rs/moq-relay/src/cluster.rs`, with its TODO) and the `with_hidden(bound.peer)` + in `Cluster::scope_session`, so an inbound peer discovers hidden routes only + when it asks, and the forced `with_hidden(true)` on the outbound dial in the + same file. `connection::authorize` no longer exists on `main`; only `release` still has that shape. - Replace `rs/moq-relay/tests/integration/hidden_cluster.rs` with a lite-07 mesh test that still sees a `.`-prefixed hidden broadcast. diff --git a/quest/m3/ladder/fetch.md b/quest/m3/ladder/fetch.md index 36a1faf8b0e..3b0db2ad70f 100644 --- a/quest/m3/ladder/fetch.md +++ b/quest/m3/ladder/fetch.md @@ -12,7 +12,7 @@ Two independent leaks of controller state, both cheap to close once the controller owns it. The FETCH path opens a new encoder per requested group at -`Rung::bitrate`: `spawn_fetch` and `fetch` +`Rung::bitrate`: `fetches` and `fetch` (`rs/moq-transcode/src/rung.rs`) call `encode`, which sets `config.bitrate = Some(self.info.bitrate)`, so a constrained ladder still burns full-rate encodes on demand. Make it read the controller's diff --git a/quest/m3/stats-encoder-feedback.md b/quest/m3/stats-encoder-feedback.md index 079e1ada45a..4915201e015 100644 --- a/quest/m3/stats-encoder-feedback.md +++ b/quest/m3/stats-encoder-feedback.md @@ -20,8 +20,9 @@ stall signal to that controller rather than to `rate::Control`; split it into its own quest requiring the controller if this is revived. This quest covers the encode producers and `moq import --echo-path`, which need no ladder. -- `encode::Options` in `rs/moq-video` and `rs/moq-audio` (the producer - options, beside `bandwidth`) gains `echo: Option`. +- `encode::Capture` in `rs/moq-video` and `encode::Options` in + `rs/moq-audio` (the producer options, beside `bandwidth`) gain + `echo: Option`. - The handle is built from an `origin::Consumer` and the echo path, resolved against the publisher's broadcast. - It consumes `.echo` announcements under that path, subscribes to the diff --git a/rs/moq-net/AGENTS.md b/rs/moq-net/AGENTS.md index 7b945329c64..633d6ea0f17 100644 --- a/rs/moq-net/AGENTS.md +++ b/rs/moq-net/AGENTS.md @@ -10,7 +10,7 @@ The wire layer. Generic over the transport and media-agnostic: the relay never i - A moq-transport feature with no moq-lite counterpart (e.g. FORWARD=0) gets the least code inside the IETF layer and nothing in the model. - Sessions are caller-driven: `connect`/`accept` return `(Session, Driver)`. The `Driver` holds no `Session` clone, so spawning it never keeps the session alive. - Version matching: outside the crate branch on `is_lite()` and `alpn()`; inside it match the draft enums. List old drafts explicitly and let the newest behavior be the fallthrough so future drafts fall forward. -- Interop rule of thumb: respond to every feature a peer may use, but don't request new ones. Never emit or accept partial groups; a group starts at frame 0. Exception: moq-transport passthrough forwards a mid-group start as the publisher sent it. +- Interop rule of thumb: respond to every feature a peer may use, but don't request new ones. - `Timestamp` is an instant with a scale, not a scalar. `checked_add`/`checked_sub` error on mismatched scales; `.convert()` first. `ZERO` is second-scale, so seed a max with `Option`, not `ZERO`. A track's timestamps never rewind. A local write refuses one, but some received tracks skip that check: a peer's rewind must never panic or cost unbounded work, yet results on that track may be wrong, so spend nothing on staying correct after one. # Testing