From 9017e788a71b00fb3bde47b057e221b3bb144f29 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 25 Oct 2026 10:11:44 -0700 Subject: [PATCH 1/5] quest: audit the tree and record the 2026-10-25 decisions Seven parallel audits checked every quest against main and open PRs. Stale plans, broken references, and missing links are corrected, and the maintainer's answers to the open planning decisions are recorded. New quests: resume checked against a model, transport frame offsets, demand is wanted everywhere, and two Nightly fixes. The generated C line moves to m2; the hand-written moq-c ships at the cut with parity. rs/moq-net/AGENTS.md drops the partial-groups rule, as the maintainer asked. Co-Authored-By: Claude Opus 5.5 --- quest/m0/README.md | 9 +-- quest/m0/ci/README.md | 5 +- quest/m0/ci/nix-cache-verify.md | 15 ++-- quest/m0/nightly/README.md | 6 +- quest/m0/nightly/py-sdist.md | 17 ++++ quest/m0/nightly/send-overflow.md | 18 +++++ quest/m1/README.md | 19 +++-- quest/m1/api-audit/README.md | 6 +- quest/m1/api-audit/binding-publication.md | 14 +++- quest/m1/api-audit/wanted.md | 45 +++++++++++ quest/m1/binding-timestamps.md | 15 ++-- quest/m1/c/README.md | 80 +++++------------- quest/m1/catalog-identity-rs.md | 20 ++--- quest/m1/ietf-request-cancel.md | 33 +++++--- quest/m1/ietf-takeover.md | 2 +- quest/m1/interop-coverage/README.md | 9 +-- quest/m1/js-fetch.md | 47 ++++++++++- quest/m1/release-audit/README.md | 6 +- quest/m1/resume-model-test.md | 32 ++++++++ quest/m1/transport-frame-offsets.md | 42 ++++++++++ quest/m1/unbounded-groups/README.md | 6 +- quest/m1/unbounded-groups/front-eviction.md | 13 ++- ...bandwidth-grant-in-moq-audio-instead-of.md | 8 +- quest/m2/README.md | 5 +- quest/m2/ashpd-maintained.md | 5 +- quest/m2/askama-maintained.md | 1 + quest/m2/audit-compile-time.md | 5 +- quest/m2/audit-deps.md | 3 +- quest/m2/audit-simplify.md | 6 +- quest/m2/broadcast-fairness.md | 1 + quest/m2/c/README.md | 53 ++++++++++++ quest/{m1 => m2}/c/async-failure-contract.md | 2 +- quest/{m1 => m2}/c/consumers.md | 2 +- quest/{m1 => m2}/c/name-clash.md | 0 quest/{m1 => m2}/c/package.md | 0 quest/m2/e2ee/README.md | 6 +- quest/m2/e2ee/js-undeclared-names.md | 9 ++- quest/m2/e2ee/rust-publish.md | 16 ++-- quest/m2/e2ee/undeclared-names.md | 27 ++++--- quest/m2/h264-reader.md | 4 +- quest/m2/ietf-finish.md | 4 +- quest/m2/ietf-headless-group.md | 81 +++++++------------ quest/m2/ietf-js-joining-fetch.md | 71 +++++----------- quest/m2/media-qa-audio.md | 2 - quest/m2/moqsrc-opus-surround.md | 30 +++---- quest/m2/net-wrap/README.md | 3 +- quest/m2/net-wrap/e2ee.md | 5 +- quest/m2/net-wrap/traits.md | 8 +- quest/m2/obs-qlog.md | 9 ++- quest/m2/obs-source-renditions.md | 8 +- quest/m2/one-port/rtc-feed.md | 26 +++--- quest/m2/ordered-owed-wait.md | 28 ++++--- quest/m2/p2p/watch.md | 5 +- quest/m2/perf/relay-allocs.md | 12 +-- quest/m2/perf/uring-quiescence.md | 10 ++- quest/m2/performance-survey/README.md | 7 +- .../performance-survey/announced-snapshots.md | 4 +- quest/m2/performance-survey/audio-batches.md | 6 +- .../browser-media-copies.md | 16 ++-- quest/m2/performance-survey/cache-backing.md | 9 ++- quest/m2/performance-survey/cache-metadata.md | 16 +++- quest/m2/performance-survey/gst-buffers.md | 4 +- quest/m2/performance-survey/hls-serving.md | 5 +- .../m2/performance-survey/js-cached-groups.md | 8 +- .../m2/performance-survey/jwt-verification.md | 2 +- .../performance-survey/quic-schedule-churn.md | 4 +- quest/m2/performance-survey/socket-queues.md | 3 +- quest/m2/performance-survey/task-capacity.md | 6 +- .../performance-survey/transport-history.md | 2 - quest/m2/probe/README.md | 2 +- quest/m2/quic-egress-profile.md | 6 +- quest/m2/quic-realtime/README.md | 9 +++ quest/m2/quic-realtime/bbr-delay-bound.md | 4 + quest/m2/quic-realtime/gcc.md | 4 + quest/m2/quic-realtime/scream.md | 4 + quest/m2/rate-grant.md | 14 ++-- quest/m2/rate-quic.md | 14 ++-- quest/m2/rate-websocket.md | 17 ++-- quest/m2/readers-range-live.md | 8 +- quest/m2/release/README.md | 2 +- quest/m2/release/assets-release.md | 6 +- quest/m2/release/c-publish.md | 12 +-- quest/m2/release/c-retire.md | 9 ++- quest/m2/release/cpp-release.md | 4 +- quest/m2/release/dart-publish.md | 4 +- quest/m2/release/libmoq-final-release.md | 9 ++- quest/m2/release/npm-first-publish.md | 18 +++-- quest/m2/starvation-frames.md | 5 +- quest/m2/stats/README.md | 8 +- quest/m2/uniffi-fork.md | 18 ++++- quest/m2/uring-lifecycle.md | 18 +++-- quest/m2/wire-stats/README.md | 13 +-- quest/m2/wire-stats/qmux.md | 2 +- quest/m2/wire-stats/srt.md | 2 +- quest/m3/README.md | 2 +- quest/m3/ac3-low-rate.md | 2 +- quest/m3/auth-token-fetch.md | 3 +- quest/m3/hidden-exemption.md | 9 ++- quest/m3/ladder/fetch.md | 2 +- quest/m3/stats-encoder-feedback.md | 5 +- rs/moq-net/AGENTS.md | 2 +- 101 files changed, 768 insertions(+), 475 deletions(-) create mode 100644 quest/m0/nightly/py-sdist.md create mode 100644 quest/m0/nightly/send-overflow.md create mode 100644 quest/m1/api-audit/wanted.md create mode 100644 quest/m1/resume-model-test.md create mode 100644 quest/m1/transport-frame-offsets.md create mode 100644 quest/m2/c/README.md rename quest/{m1 => m2}/c/async-failure-contract.md (96%) rename quest/{m1 => m2}/c/consumers.md (92%) rename quest/{m1 => m2}/c/name-clash.md (100%) rename quest/{m1 => m2}/c/package.md (100%) diff --git a/quest/m0/README.md b/quest/m0/README.md index eedf7199724..40ce82e7197 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 2f0da3af34e..44219846000 100644 --- a/quest/m0/ci/README.md +++ b/quest/m0/ci/README.md @@ -81,7 +81,10 @@ 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 +([feature-shapes](/quest/m0/nightly/feature-shapes.md)); Nightly keeps the +full matrix. Measure each change against a current baseline and record the result in its PR. The historical samples locate candidates; they are not controlled benchmarks. diff --git a/quest/m0/ci/nix-cache-verify.md b/quest/m0/ci/nix-cache-verify.md index 16818bfe36a..a5290ec1746 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 330a865a317..f52ddd82101 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 - [Feature shapes in the gate](/quest/m0/nightly/feature-shapes.md) - feature-gated dead code fails the PR that adds it, not the next Nightly - [ts_schedule regression](/quest/m0/nightly/ts-schedule.md) - the moq-mux `ts_schedule` benches return to their old cost - [Nightly's base in a shallow checkout](/quest/m0/nightly/shallow-base.md) - `sh/gh/nightly.test.py` passes in Nightly's depth-1 checkout 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 9c55c18b5ae..c4d853a6bed 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 @@ -98,6 +99,7 @@ Catalog HLS field. ## Required - [moq-congestion](/quest/m1/congestion/README.md) - one QUIC congestion-control crate with a transport-independent trait, run by `moq-quic` and published for noq +- [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 - [Cancelled moq-transport requests stop](/quest/m1/ietf-request-cancel.md) - a cancelled request releases its state even when the peer withholds credit - [Empty segments refused](/quest/m1/empty-segments.md) - `room//cam` fails loud instead of naming `room/cam` @@ -106,11 +108,12 @@ Catalog HLS field. - [No reused track names](/quest/m1/name-reuse.md) - no first-party publisher re-creates a finished track under its old name - [Routing guide](/quest/m1/routing-guide.md) - concepts and practical outbound federation setup - [One stuck check per packet](/quest/m1/quic-stuck-scan.md) - a RESET_STREAM storm into a shrunk window costs one stream scan per packet -- [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 - [Exact timestamps in every binding](/quest/m1/binding-timestamps.md) - bindings carry `{value, scale}` timestamps instead of lossy microseconds - [Binding durations](/quest/m1/ffi-durations.md) - moq-ffi takes and returns durations as a typed duration instead of `_us` integers, so wrappers stop converting by hand - [moq-transport passthrough](/quest/m1/ietf-extension/README.md) - object IDs, properties, end status, datagram objects, and unknown track properties survive moq-transport -> moq-relay (and a relay cluster) -> moq-transport; moxygen's moq-test sections 1-7 pass nightly under the relay conformance profile, except cases that use a subgroup other than 0 - [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 deliver a group from frame k when the receiver holds the frames below, 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 - [Updates reach fills](/quest/m1/fill-updates.md) - a reader's new priority, delay, order, or earlier start reaches fills already queued or taken - [A widened max age re-offers cached groups](/quest/m1/resolved-demand.md) - a warm relay serves a reader with a wider max age the cached groups a cold relay would, with no wire change @@ -143,7 +146,7 @@ Catalog HLS field. - [Lowered resume floor](/quest/m1/resume-floor.md) - lowering a resumed reader's floor re-offers groups only a released copy cached - [WebGPU on Safari](/quest/m1/webgpu-safari.md) - the WebGPU renderer is verified on Safari 26 for macOS and iOS - [JSON encoder options without a downcast](/quest/m1/json-encoder-options.md) - moq-mux's `json_snapshot` takes its `deltas` switch through a typed path any custom catalog entry can use, not an `Any` downcast -- [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 - [Drop announce="never"](/quest/m1/publish-announce-never.md) - `@moq/publish` offers only announce modes a watcher can reach - [Drop unused OBS outputs](/quest/m1/obs-output-variants.md) - the plugin registers only `moq_output`, so the broken audio-only variant is gone @@ -165,7 +168,3 @@ Catalog HLS field. - [Unbounded groups](/quest/m1/unbounded-groups/README.md) - groups and frames lose their size caps; the cache sheds parts of a group under pressure and refills the holes in place - [Clean close reason](/quest/m1/clean-close-reason.md) - a session that closes on its own is never recorded as `Unauthorized` because its in-band credentials closed with it - [Metric-safe tiers](/quest/m1/metric-tier.md) - tiers are `[A-Za-z0-9._-]` paths refused where they are set, and every metric label value is escaped - -## Related - -- [A busy js/net serve yields](https://github.com/moq-dev/moq/pull/5289) - js/net's serve loop leaves the browser its event loop under a fast publisher, if it does not already diff --git a/quest/m1/api-audit/README.md b/quest/m1/api-audit/README.md index bd82f27adf2..364f26f606b 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); binding durations get + 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; binding durations get [their own quest](/quest/m1/ffi-durations.md)). - Each child lists its public API and wire impact in its PR, and its Impact bullet says how to migrate. @@ -74,6 +75,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 ## Related diff --git a/quest/m1/api-audit/binding-publication.md b/quest/m1/api-audit/binding-publication.md index f4692196473..1b310f56de7 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](/quest/m1/api-audit/js-publication.md), and this binding rollout -once the native PR merges. The native PR keeps private adapters that hold the +[JS mirror](/quest/m1/api-audit/js-publication.md), and this binding rollout. +The native PR merged on 2026-10-25, so this quest can start. It 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. + Public API: breaking in moq-ffi, the C ABI, and every wrapper. Wire: none. + +## Related + +- [Closing a binding broadcast ends its media](/quest/m1/broadcast-lifecycle/ffi-close.md) - reshapes the same `MoqBroadcastProducer` and `moq_publish_*` close path in every wrapper; whichever lands second merges them diff --git a/quest/m1/api-audit/wanted.md b/quest/m1/api-audit/wanted.md new file mode 100644 index 00000000000..ab8222414bb --- /dev/null +++ b/quest/m1/api-audit/wanted.md @@ -0,0 +1,45 @@ +# [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 (no production consumer), as + [net-exports](/quest/m1/api-audit/net-exports.md) plans. 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 00986136419..e1b41deeff0 100644 --- a/quest/m1/c/README.md +++ b/quest/m1/c/README.md @@ -1,72 +1,32 @@ -# Generated C bindings +# 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, records, and names mirror the C++ package: calls return `moq_error *` - (NULL on success) with `moq_error_message()`, results come through - out-params, records are owned structs with `moq__free`, and names are - the uniffi names in snake case. -- 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 derives collision-free names, so `MoqAudioEncoder::finish` and `MoqAudioEncoderFinish` stop sharing `moq_audio_encoder_finish` -- [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 - [Refuse only waiting readers](/quest/m1/c/request-refuse.md) - freeing or aborting a group request refuses its readers instead of dropping the group for everyone diff --git a/quest/m1/catalog-identity-rs.md b/quest/m1/catalog-identity-rs.md index 0aa0f2e26e1..d8c1dc29bfe 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 @@ -51,7 +51,10 @@ minting a name where the identity changes and updating in place otherwise: - `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. + explicit pad name after caps renegotiation. [No reused track + names](/quest/m1/name-reuse.md) continues a re-created track past its + predecessor's groups; that continuation applies only when the decoder + identity is unchanged, and an identity change mints a new name here. - 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. @@ -66,18 +69,17 @@ 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 +- [No reused track names](/quest/m1/name-reuse.md) - continues a re-created track's groups when its identity is unchanged + - [Broadcast archives](/quest/m1/archive/README.md) - an archived catalog describes every stored group by the identity contract diff --git a/quest/m1/ietf-request-cancel.md b/quest/m1/ietf-request-cancel.md index 37cab3dd181..d7dbdc1c801 100644 --- a/quest/m1/ietf-request-cancel.md +++ b/quest/m1/ietf-request-cancel.md @@ -3,24 +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. -[IETF lazy discovery](/quest/m1/ietf-lazy-discovery.md) fixes 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. - -Test with a peer that never grants credit, on each draft family. +`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. + +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 -- [IETF lazy discovery](/quest/m1/ietf-lazy-discovery.md) - changes the same request writes +- [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 287c479fc0c..c33f0135f49 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/release-audit/README.md b/quest/m1/release-audit/README.md index fbd79026a82..36185975da1 100644 --- a/quest/m1/release-audit/README.md +++ b/quest/m1/release-audit/README.md @@ -1,4 +1,4 @@ -# [S] Resolve the release audit findings +# Resolve the release audit findings ## Goal @@ -76,7 +76,9 @@ indexes, JS IETF timestamp changes, JS datagram range violations, and OBS 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; the OBS defects moved to an m1 child (2026-10-22, maintainer). -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-model-test.md b/quest/m1/resume-model-test.md new file mode 100644 index 00000000000..14f0d29737e --- /dev/null +++ b/quest/m1/resume-model-test.md @@ -0,0 +1,32 @@ +# [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. +- 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 (planned for m2 in + [moq-dev/moq#6707](https://github.com/moq-dev/moq/pull/6707)) 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..4aabb49551b --- /dev/null +++ b/quest/m1/transport-frame-offsets.md @@ -0,0 +1,42 @@ +# [M] The transport may start a group mid-way + +## Goal + +A moq-lite hop may deliver a group starting at frame k when the receiver +already holds the frames below k (route continuation, hole fills, and +front-eviction refills with a frame-precise end), so a relay never refetches +frames it has. 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 a nonzero frame start for + a group it already holds below that frame, instead of "MUST send 0 unless + it already holds every frame below this index" read as a near ban. Keep the + `Frame End` 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 176a42bb376..2976bec6769 100644 --- a/quest/m1/unbounded-groups/README.md +++ b/quest/m1/unbounded-groups/README.md @@ -87,8 +87,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 91fe89b1837..2f8c7c02e70 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` @@ -98,9 +106,10 @@ 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 - [Segmented frames](/quest/m1/unbounded-groups/segmented-frames.md) - frame storage the cache can account for without whole-frame preallocation - [Append-only log](/quest/m1/unbounded-groups/log.md) - `GroupState`'s frames in a lock-free kio log with an advancing base, the tail run this builds on +- [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 980e0a6e1f6..4a8823acc87 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -25,6 +25,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 - [Pure-Rust crypto for moq-relay](/quest/m2/pure-crypto/README.md) - moq-relay's release builds use graviola and RustCrypto and compile no C - [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` @@ -62,7 +63,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 @@ -201,7 +202,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 1f5c29ca2a2..cbdd52516a9 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 da99771b20c..ecebc24b06c 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 e8ca4d60634..73ade58e09a 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..ff9b3bbb113 --- /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, records, and names mirror the C++ package: calls return `moq_error *` + (NULL on success) with `moq_error_message()`, results come through + out-params, records are owned structs with `moq__free`, and names are + the uniffi names in snake case. +- 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) - the generator gives methods an explicit namespace, so `MoqAudioEncoder::finish` and `MoqAudioEncoderFinish` stop sharing `moq_audio_encoder_finish` +- [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 0ca06bc4194..a5ec771c82a 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. @@ -60,7 +60,7 @@ The broadcast wrappers merged in [#5876](https://github.com/moq-dev/moq/pull/587 ## 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..33b51e1c69a 100644 --- a/quest/m2/e2ee/rust-publish.md +++ b/quest/m2/e2ee/rust-publish.md @@ -34,14 +34,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..1298e6550c4 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,18 @@ 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 a +surround case in the caps unit test. Public API: none. Wire: none. diff --git a/quest/m2/net-wrap/README.md b/quest/m2/net-wrap/README.md index 75d6ad48942..6429290db95 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 71c46f67c44..8a795eae809 100644 --- a/quest/m2/probe/README.md +++ b/quest/m2/probe/README.md @@ -93,4 +93,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..c55622dedb1 100644 --- a/quest/m2/quic-egress-profile.md +++ b/quest/m2/quic-egress-profile.md @@ -24,7 +24,9 @@ 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; today `moq-quic`'s `connection/pacing.rs`, which + [the portable controller trait](/quest/m1/congestion/trait.md) replaces + with `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 +42,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 33c7b4205fe..90357128416 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 0f37d79d3b6..8fe892bd774 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 207a336df4a..505072b19a5 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 4ec6ee3e384..4e08f48862f 100644 --- a/quest/m2/release/README.md +++ b/quest/m2/release/README.md @@ -30,7 +30,7 @@ production stability proven merely because upstream artifacts published. - [Migration audit](/quest/m2/release/migrate-audit.md) - the cut writes a MIGRATE.md entry for every break, stating the net published-to-final change - [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 -- [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 1c02587a92f..efe23aa4e82 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. - The code-free `rs/libmoq` stub left under the old name is [Retire the libmoq stub](/quest/m2/release/libmoq-retire.md)'s, separately. @@ -20,7 +21,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/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 b4e6141adc2..2ed643ea5a0 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 b8b39e8c64e..8ae807b6d76 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](/bench/ffi-gen/README.md) 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,8 +54,14 @@ 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. - 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. @@ -63,6 +72,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 c7e7d5db45f..4ae12ff096e 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 1b00fcc5dbc..a4de0e4cd59 100644 --- a/rs/moq-net/AGENTS.md +++ b/rs/moq-net/AGENTS.md @@ -8,7 +8,7 @@ The wire layer. Generic over the transport and media-agnostic: the relay never i - The cache is a performance optimization. A relay behaves the same warm or cold: report upstream state (e.g. the largest group), not what the cache happens to hold. - 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()` / `is_ietf()` 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 From b0b50c5b9a5509fe30f5d953705c8b8d9da3c0dc Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 25 Oct 2026 10:20:10 -0700 Subject: [PATCH 2/5] quest: plan lite-05/06 timestamp pass-through; any frame start in the transport Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 1 + quest/m1/lite-timestamps-through.md | 40 +++++++++++++++++++++++++++++ quest/m1/transport-frame-offsets.md | 23 +++++++++-------- 3 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 quest/m1/lite-timestamps-through.md diff --git a/quest/m1/README.md b/quest/m1/README.md index c4d853a6bed..f235306912a 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -109,6 +109,7 @@ Catalog HLS field. - [Routing guide](/quest/m1/routing-guide.md) - concepts and practical outbound federation setup - [One stuck check per packet](/quest/m1/quic-stuck-scan.md) - a RESET_STREAM storm into a shrunk window costs one stream scan per packet - [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 - [Binding durations](/quest/m1/ffi-durations.md) - moq-ffi takes and returns durations as a typed duration instead of `_us` integers, so wrappers stop converting by hand - [moq-transport passthrough](/quest/m1/ietf-extension/README.md) - object IDs, properties, end status, datagram objects, and unknown track properties survive moq-transport -> moq-relay (and a relay cluster) -> moq-transport; moxygen's moq-test sections 1-7 pass nightly under the relay conformance profile, except cases that use a subgroup other than 0 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/transport-frame-offsets.md b/quest/m1/transport-frame-offsets.md index 4aabb49551b..e586f19b7e7 100644 --- a/quest/m1/transport-frame-offsets.md +++ b/quest/m1/transport-frame-offsets.md @@ -2,10 +2,11 @@ ## Goal -A moq-lite hop may deliver a group starting at frame k when the receiver -already holds the frames below k (route continuation, hole fills, and -front-eviction refills with a frame-precise end), so a relay never refetches -frames it has. Applications still only ask for whole groups: no public API +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 @@ -14,12 +15,14 @@ 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 a nonzero frame start for - a group it already holds below that frame, instead of "MUST send 0 unless - it already holds every frame below this index" read as a near ban. Keep the - `Frame End` 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 +- 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 From abf04375892559f81cb12555dd2f4a9e5625a02a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 25 Oct 2026 11:34:59 -0700 Subject: [PATCH 3/5] quest: plan addressing groups by timestamp Add an m1 epic for a spike that replaces group sequence numbers with group start timestamps in the moq-net API and the lite-07 wire, plus a guide. The spike gates the lite-07 cut. Co-Authored-By: Claude Opus 5.5 --- quest/m1/README.md | 1 + quest/m1/lite07-finalize.md | 4 ++ quest/m1/name-reuse.md | 4 ++ quest/m1/subscribe-ranges/README.md | 1 + quest/m1/time-addressing/README.md | 15 ++++ quest/m1/time-addressing/guide.md | 22 ++++++ quest/m1/time-addressing/spike.md | 107 ++++++++++++++++++++++++++++ 7 files changed, 154 insertions(+) create mode 100644 quest/m1/time-addressing/README.md create mode 100644 quest/m1/time-addressing/guide.md create mode 100644 quest/m1/time-addressing/spike.md diff --git a/quest/m1/README.md b/quest/m1/README.md index e68eba12e71..6d498ffa7db 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -100,6 +100,7 @@ Catalog HLS field. - [moq-congestion](/quest/m1/congestion/README.md) - one QUIC congestion-control crate with a transport-independent trait, run by `moq-quic` and published for noq - [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 +- [Address groups by timestamp](/quest/m1/time-addressing/README.md) - group start timestamps replace sequence numbers in the moq-net API and lite-07 wire, while old subscribers, old publishers, and published JS clients keep working - [Release audit findings](/quest/m1/release-audit/README.md) - resolve newly identified resource, parser, playback, and interoperability risks before the cut - [Cancelled moq-transport requests stop](/quest/m1/ietf-request-cancel.md) - a cancelled request releases its state even when the peer withholds credit - [Empty segments refused](/quest/m1/empty-segments.md) - `room//cam` fails loud instead of naming `room/cam` diff --git a/quest/m1/lite07-finalize.md b/quest/m1/lite07-finalize.md index ca7a49779f2..f3e916d1baf 100644 --- a/quest/m1/lite07-finalize.md +++ b/quest/m1/lite07-finalize.md @@ -79,6 +79,9 @@ type for the m2 browser [padding probes](/quest/m2/probe/padding.md) landed in [#6233](https://github.com/moq-dev/moq/pull/6233). The `Time(pts)` range end rides in Subscribe ranges, already Required. +Decided 2026-10-25 (maintainer): addressing groups by timestamp gates the +cut, since it removes group sequence numbers from the lite-07 wire. + Rust's lite-07 varints already carry the full 64 bits the draft specifies, so no codec work waits on the cut. @@ -100,6 +103,7 @@ timestamp bound in [#6168](https://github.com/moq-dev/moq/pull/6168). - [Routes and announces](/quest/m1/cluster-routing/routes.md) - lite-07 carries the route layer: ROUTE per origin node and path-less ANNOUNCE, with the hop list gone - [Drop set](/quest/m1/drop-set.md) - lite-07's SUBSCRIBE_DROP carries `Max Age`, so a widening re-offers groups reported dropped - [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) - SUBSCRIBE carries ranges and an order and lite FETCH is gone, in Rust and JS +- [Spike: timestamps replace group sequence numbers](/quest/m1/time-addressing/spike.md) - decides the lite-07 wire without group sequence numbers and adds the implementation quests here - [Announcement takeover](/quest/m1/announce-takeover.md) - ANNOUNCE_TAKEOVER preserves a proven instance under a new independent route ## Related diff --git a/quest/m1/name-reuse.md b/quest/m1/name-reuse.md index 5f029ea6b49..bbb63e01cac 100644 --- a/quest/m1/name-reuse.md +++ b/quest/m1/name-reuse.md @@ -16,3 +16,7 @@ SUBSCRIPTION_ENDED path in moq-net. Fix each with a test that a restart never repeats a group sequence. Public API: none expected. Wire: none. + +## Related + +- [Timestamp spike](/quest/m1/time-addressing/spike.md) - the cut removes the sequence half of this fix; a re-created track then only keeps its PTS moving forward diff --git a/quest/m1/subscribe-ranges/README.md b/quest/m1/subscribe-ranges/README.md index fcd21b4bbc1..b7f0ea962d8 100644 --- a/quest/m1/subscribe-ranges/README.md +++ b/quest/m1/subscribe-ranges/README.md @@ -103,3 +103,4 @@ every sequence delivered or dropped. ## Related - [IETF fill timeout](/quest/m1/ietf-fill-timeout.md) - bounds upstream waiting independently of content age +- [Timestamp spike](/quest/m1/time-addressing/spike.md) - the cut reworks `Absolute(group)` ends and sequence-range drops onto times diff --git a/quest/m1/time-addressing/README.md b/quest/m1/time-addressing/README.md new file mode 100644 index 00000000000..a4030866ad8 --- /dev/null +++ b/quest/m1/time-addressing/README.md @@ -0,0 +1,15 @@ +# Address groups by timestamp + +## Goal + +Groups are addressed by their start timestamp instead of a sequence number, +in the moq-net API (Rust and `@moq/net`) and the lite-07 wire. A reader seeks +or starts from a time, nothing maps between group numbers and time, and no +producer has to remember group numbers. Old subscribers, old publishers, and +published JS clients keep working. VOD, FETCH, and archives need no backwards +compatibility. + +## Required + +- [Spike](/quest/m1/time-addressing/spike.md) - a Rust and JS prototype measures what breaks, and the spike writes the design and the implementation quests for the cut +- [Guide](/quest/m1/time-addressing/guide.md) - a page explaining seek by time, group ends, restarts, strictly increasing starts, and how legacy peers see group IDs diff --git a/quest/m1/time-addressing/guide.md b/quest/m1/time-addressing/guide.md new file mode 100644 index 00000000000..5479cfac736 --- /dev/null +++ b/quest/m1/time-addressing/guide.md @@ -0,0 +1,22 @@ +# [S] Addressing by time guide + +## Goal + +A documentation page explains how applications address groups after the +timestamp cut: seeking and starting from a time, why group starts strictly +increase, how a group ends (whole, incomplete, or aborted), how a restart +mints a new epoch instead of continuing numbers, and what group IDs +lite-01..06 and moq-transport peers see. Publishers and player +authors can follow it without reading the drafts. + +## Plan + +Decided 2026-10-25 (maintainer): a separate page, not only inline doc +updates. Write it against the design the spike records. Link it from +`doc/concept/moq-lite.md` and from the +[Identity and recovery guide](/quest/m1/identity-guide.md), which covers +epochs and restarts. + +## Required + +- [Spike](/quest/m1/time-addressing/spike.md) - the design this page describes diff --git a/quest/m1/time-addressing/spike.md b/quest/m1/time-addressing/spike.md new file mode 100644 index 00000000000..f2f981b2e6d --- /dev/null +++ b/quest/m1/time-addressing/spike.md @@ -0,0 +1,107 @@ +# [L] Spike: timestamps replace group sequence numbers + +## Goal + +A throwaway prototype in Rust moq-net and `@moq/net` drops group sequence +numbers from the API and the lite-07 wire, addressing each group by its start +timestamp, and measures what breaks. The spike ends by recording the design +in new implementation quests, adding them to +[Finalize moq-lite-07](/quest/m1/lite07-finalize.md)'s Required list and the +[guide](/quest/m1/time-addressing/guide.md)'s, and deleting itself. The +prototype is not merged. + +## Plan + +Decided 2026-10-25 (maintainer). Why: group numbers need timelines just to +map to and from time, and reusing a group number poisons caches, so resuming +a producer would need per-track memory of its last number. + +- **API and wire.** Sequence numbers leave the moq-net API and the lite-07 + wire, not just the API. This gates the lite-07 cut, since lite-07 is still + wip. +- **A group's address is its first frame's PTS**, known when the group opens, + so the GROUP header and legacy IDs can carry it. Frames inside a group stay + in decode order, and their PTS may go backwards (B-frames). Check that a + group's end and reach use its highest PTS, not its newest frame's + (`rs/moq-net/src/model/group.rs:2022`). +- **Starts strictly increase.** An equal start is refused loudly like a + rewind; today the rewind check allows ties (`track.rs:491`). +- **Three ways a group ends**, mirrored in the API and lite-07: + `finish(end)` sends GROUP_END with an exclusive end timestamp, meaning the + group is whole; `close()` is a bare FIN, meaning an incomplete group whose + frames all arrive (better than a reset, which drops data in flight); and + `abort(err)` resets. So a relay closes an expired or partial group instead of + resetting it (lite draft line 760 changes). GROUP_END maps to + moq-transport's END_OF_GROUP. It replaces hang's empty-frame duration + marker (`rs/moq-mux/src/container/producer.rs:523`), so moq-mux stops + writing empty frames. Evaluate whether a finished group's own end can set + its reach, so expiry stops waiting for a successor. +- **Discontinuity marker groups go away** + (`rs/moq-mux/src/container/producer.rs:650`). A gap is a previous end below + the next start; a missing group is a previous-start link that doesn't match. + Legacy egress synthesizes neither the empty frame nor marker groups: old + players lose end durations and pause markers, which they already tolerate + being absent. +- **Restarts keep minting a new epoch** with fresh media PTS, as today. + Replicas sharing an epoch agree by PTS without coordinating numbers. +- **Every track is timed.** Untimed goes away: the producer stamps an untimed + write with wall-clock time. +- **Gaps.** Each GROUP carries the previous group's start, so a cache or a + reader sees a hole without a round trip. The first group of an epoch has + none, and the gap before it is unknown. +- **Legacy egress** (lite-01..06 and moq-transport): the group ID is the + group's start in the track's timescale, so every relay agrees without a + table. Verify that lite-05/06 receivers and published `@moq/net` and + `@moq/watch` builds accept sparse, large IDs (JS numbers are exact to + 2^53). +- **Legacy ingest.** A timed source keeps its timestamps. An untimed source + (moq-transport without TIMESCALE, lite-01..04) uses the group ID as the + timestamp on a synthetic scale, so relays agree and legacy egress round-trips + the exact ID. Max age means nothing on those tracks. A timed moq-transport + group that starts at or before its predecessor ends the track with an + error. +- **Extensions are optional.** moq-transport may carry the new information + (end times, previous-start links) through a negotiated extension, but + moq-transport without it, and lite-01..06, must still work for live + playback. Only end precision and gap detection degrade. +- **Ranges.** [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) + proceeds in parallel; the cut reworks its `Absolute(group)` ends and + sequence-range SUBSCRIBE_DROP onto times. `Relative(n)` can stay, since it + counts groups rather than naming them. +- **[Name reuse](/quest/m1/name-reuse.md)** lands as planned. The cut removes + its sequence half, leaving a re-created track to keep its PTS moving + forward. + +The prototype covers the model, the lite-07 codec, and the legacy mapping in +Rust and JS. Every current role of the sequence needs a replacement or a +reason to drop it: + +- The cache key, the duplicate check, and the hand-out-once record + (`track.rs:318`, `delivered.rs`), plus slot stamps. +- Successor, reach, expiry, the arrival ledger, tail accounting, fills and + misses, and SUBSCRIBE_DROP accounting, which all need a total order and a + visible gap. +- Resume across route flips (`resume.rs`), mid-group `Position` and + `frame_start`, priority order within a subscription, `Demand::wanted`, and + refresh. +- Wire fields: GROUP, SUBSCRIBE and SUBSCRIBE_UPDATE bounds, SUBSCRIBE_START + and its largest, SUBSCRIBE_END, SUBSCRIBE_DROP, datagrams (which share the + group space), and every IETF Location, Filter, FETCH, Largest, and + NEW_GROUP_REQUEST through the legacy mapping. +- Consumers that number groups: hang and moq-mux timelines, HLS/DASH export, + `seek` for GOP-aligned failover in moq-mux and the bindings, HLS import's + discontinuity packing, transcode's 1:1 numbering, archive keys and resume, + relay and CLI `fetch --group`, and player continuity checks in the + Rust and JS container consumers and `js/watch`. +- The E2EE nonce, which binds the group sequence + (`drafts/draft-lcurley-moq-e2ee.md`, `rs/moq-e2ee`). + +The implementation quests also update `doc/concept`, the lite, hang, and E2EE +drafts, every binding per the cross-package sync table, and `MIGRATE.md`. + +Public API: none (prototype). Wire: none (prototype). + +## Related + +- [Lite-05/06 timestamps pass through](/quest/m1/lite-timestamps-through.md) - lite-05/06 tracks become timed, which this builds on +- [Resume checked against a model](/quest/m1/resume-model-test.md) - its reference model is the oracle for resume after the cut From 5755e4022a5975a9a2c34d053cab92b69185f1dc Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Sun, 25 Oct 2026 11:38:27 -0700 Subject: [PATCH 4/5] quest: map timestamp addressing onto moq-transport properties Co-Authored-By: Claude Opus 5.5 --- quest/m1/time-addressing/spike.md | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/quest/m1/time-addressing/spike.md b/quest/m1/time-addressing/spike.md index f2f981b2e6d..68ab0701c0a 100644 --- a/quest/m1/time-addressing/spike.md +++ b/quest/m1/time-addressing/spike.md @@ -60,10 +60,19 @@ a producer would need per-track memory of its last number. the exact ID. Max age means nothing on those tracks. A timed moq-transport group that starts at or before its predecessor ends the track with an error. +- **moq-transport mapping.** The Group ID is the start ticks, and the + previous-start link rides in the Prior Group ID Gap property. Object IDs + stay +1 per frame, so no Prior Object ID Gap is sent; each object's PTS + rides in the TIMESTAMP property, and GROUP_END maps to END_OF_GROUP with + the end in its TIMESTAMP. Keep it LOC compatible: TIMESTAMP (0x10) and + TIMESCALE (0x08) are the MOQ Properties shared with draft-ietf-moq-loc. + Rejected: Object ID as a PTS delta, because + B-frames run backwards in decode order and open-GOP leading frames present + before the group's start. - **Extensions are optional.** moq-transport may carry the new information - (end times, previous-start links) through a negotiated extension, but - moq-transport without it, and lite-01..06, must still work for live - playback. Only end precision and gap detection degrade. + through properties or a negotiated extension, but moq-transport without + them, and lite-01..06, must still work for live playback. Only end + precision and gap detection degrade. - **Ranges.** [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) proceeds in parallel; the cut reworks its `Absolute(group)` ends and sequence-range SUBSCRIBE_DROP onto times. `Relative(n)` can stay, since it From 20d7367d8400d60fb3fb31acd030848e5df201b9 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 26 Oct 2026 09:46:17 -0700 Subject: [PATCH 5/5] quest: keep END_OF_GROUP bare past draft-14 Co-Authored-By: Claude Opus 5.5 --- quest/m1/time-addressing/spike.md | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/quest/m1/time-addressing/spike.md b/quest/m1/time-addressing/spike.md index 68ab0701c0a..5347c08bdfb 100644 --- a/quest/m1/time-addressing/spike.md +++ b/quest/m1/time-addressing/spike.md @@ -63,12 +63,15 @@ a producer would need per-track memory of its last number. - **moq-transport mapping.** The Group ID is the start ticks, and the previous-start link rides in the Prior Group ID Gap property. Object IDs stay +1 per frame, so no Prior Object ID Gap is sent; each object's PTS - rides in the TIMESTAMP property, and GROUP_END maps to END_OF_GROUP with - the end in its TIMESTAMP. Keep it LOC compatible: TIMESTAMP (0x10) and - TIMESCALE (0x08) are the MOQ Properties shared with draft-ietf-moq-loc. - Rejected: Object ID as a PTS delta, because - B-frames run backwards in decode order and open-GOP leading frames present - before the group's start. + rides in the TIMESTAMP property, and GROUP_END maps to END_OF_GROUP. + Only draft-14 lets a status object carry properties, so only there does + END_OF_GROUP carry the end in its TIMESTAMP; later drafts refuse them + (`rs/moq-net/src/ietf/object.rs:57`), so their END_OF_GROUP stays bare and + the end is lost unless an extension carries it. Keep it LOC compatible: + TIMESTAMP (0x10) and TIMESCALE (0x08) are the MOQ Properties shared with + draft-ietf-moq-loc. Rejected: Object ID as a PTS delta, because B-frames + run backwards in decode order and open-GOP leading frames present before + the group's start. - **Extensions are optional.** moq-transport may carry the new information through properties or a negotiated extension, but moq-transport without them, and lite-01..06, must still work for live playback. Only end