diff --git a/quest/m0/README.md b/quest/m0/README.md index 5257031009a..4bfa9c81cd3 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -45,6 +45,7 @@ Each lands its fixes in its own PR and plans only what needs a decision. - [qmux 0.5.3](/quest/m0/qmux-0.5.3.md) - crates.io publishes qmux 0.5.3 with the builder session config the release backport of #5609 needs - [Release shutdown backport](/quest/m0/ffi-shutdown-backport.md) - `release` publishes a non-breaking moq-ffi with an async `drain()` and qmux sessions on the QUIC stream limit - [Release fixed delay](/quest/m0/release-fixed-delay.md) - a fixed latency on `release`, which moq.dev runs, ignores the catalog's jitter and delay as `main` already does +- [HLS admission backport](/quest/m0/hls-admission-backport.md) - `release` refuses the hostile HLS playlists `main` does, without breaking `import::Config::client` - [Adopt quest resolve](/quest/m0/quest-resolve.md) - local merges resolve concurrent quest link-list edits once kixelated/quest ships its merge driver - [CI cost and latency](/quest/m0/ci/README.md) - cloud CI runs only in the merge queue as one `just ci` pass, expensive checks run nightly and gate `release`, caches move to R2, and runners are chosen last - [MIGRATE.md at the cut](/quest/m0/migrate-at-cut.md) - PRs stop conflicting on `MIGRATE.md`: breaking PRs carry their migration note in the PR, and the breaking cut writes the guides diff --git a/quest/m0/ci/README.md b/quest/m0/ci/README.md index d8401a348f6..a2eb6ade570 100644 --- a/quest/m0/ci/README.md +++ b/quest/m0/ci/README.md @@ -85,4 +85,5 @@ The historical samples locate candidates; they are not controlled benchmarks. - [Bump to the manifest fix](/quest/m0/ci/mbx-bump.md) - condition: an mbx release contains jdx/mr-boxington#798; then bump the Rust cache's pin - [Verify the R2 Rust cache](/quest/m0/ci/mbx-r2-verify.md) - measure the hosted Nightly jobs' hits, restore time, and expiry once the bump lands - [Verify the Nix cache](/quest/m0/ci/nix-cache-verify.md) - confirm CI and releases use `nix.moq.dev` and retire Cachix +- [The gate builds a Nix package](/quest/m0/ci/nix-package-gate.md) - a change that breaks `nix build .#moq-relay` or the Dockerfile fails before `main` - [First shared FFI release](/quest/m0/ci/nightly-ffi-reuse.md) - condition: the first `moq-ffi-v*` tag that runs `Release FFI`, and a `Shipping sizes` run; verify the wrappers and size keys diff --git a/quest/m0/ci/nix-package-gate.md b/quest/m0/ci/nix-package-gate.md new file mode 100644 index 00000000000..a956eff0fe8 --- /dev/null +++ b/quest/m0/ci/nix-package-gate.md @@ -0,0 +1,25 @@ +# [S] The gate builds a Nix package + +## Goal + +A change that breaks `nix build .#` or the root `Dockerfile` fails +before it reaches `main`. + +## Plan + +[moq-dev/moq#6159](https://github.com/moq-dev/moq/pull/6159) broke every +crane package build (kio's real manifest inherited `workspace.lints`, which +crane's dummy root drops) and nothing noticed: +the merge queue's `nix flake check` builds no packages, Docker builds only on +tags, and Nightly had not run since. [moq-dev/moq#6586](https://github.com/moq-dev/moq/pull/6586) +fixed the build. + +Build one crane package, `moq-relay`, where a change to `nix/`, `Cargo.toml`, +`Cargo.lock`, or a crate's manifest can break it. Measure its cost on the +cache first: if it fits the merge gate, run it there only for those paths; +otherwise run it in Nightly next to the Docker dry-run, so a break is caught +within a day. + +Decided 2026-10-23 (maintainer): track this as m0 CI work. + +Public API: none. Wire: none. diff --git a/quest/m0/hls-admission-backport.md b/quest/m0/hls-admission-backport.md new file mode 100644 index 00000000000..d17d6a1a939 --- /dev/null +++ b/quest/m0/hls-admission-backport.md @@ -0,0 +1,26 @@ +# [S] Backport HLS import admission to release + +## Goal + +`release`, which moq.dev runs, refuses the same hostile HLS playlists `main` +does: the origin-admission and redirect-hop checks from +[HLS admission](/quest/m1/release-audit/hls-admission.md) ship in a +non-breaking `moq-hls` patch release. + +## Plan + +`main` drops `import::Config::client` while adding the checks. `release` +still has `pub client: Option` (`rs/moq-hls/src/import.rs`), so the +backport keeps that field and documents that a caller-supplied client owns +its own redirect handling; the checks apply to the client moq-hls builds. + +Decided 2026-10-23 (maintainer): backport once the checks have been validated +on `main`, following the shape of +[the shutdown backport](/quest/m0/ffi-shutdown-backport.md): cut from +`release`, no breaking change, a patch release. + +Public API: none on `release`. Wire: none. + +## Required + +- [HLS admission](/quest/m1/release-audit/hls-admission.md) - the checks land and are validated on `main` first diff --git a/quest/m1/README.md b/quest/m1/README.md index dd0645232a7..2e0126409dd 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -73,13 +73,22 @@ tracks, so per-group stats stop costing cluster announces. - [Epoch correctness](/quest/m0/broadcast-epoch/README.md) - publisher identity, duplicate suppression, and catalog pinning gate the breaking cut - [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 - [Release audit findings](/quest/m1/release-audit/README.md) - resolve newly identified resource, parser, playback, and interoperability risks before the cut +- [SUBSCRIBE_TRACKS follows credit](/quest/m1/listing-credit.md) - a listing holds state in proportion to what the peer reads, not to the catalog +- [Cancelled moq-transport requests stop](/quest/m1/ietf-request-cancel.md) - a cancelled request releases its state even when the peer withholds credit +- [SUBSCRIBE setup sees a cancel](/quest/m1/ietf-setup-cancel.md) - a subscriber that cancels mid-setup releases upstream demand and its slot at once +- [JS qmux stream credit](/quest/m1/js-qmux-credit.md) - credit returns when both halves close, so `@moq/net` drops its caps +- [Every close records its cause](/quest/m1/close-cause.md) - protocol-violation closes and abandoned SETUP replies record why +- [Empty segments refused](/quest/m1/empty-segments.md) - `room//cam` fails loud instead of naming `room/cam` - [Readers follow restarts](/quest/m1/sink-restart-follow.md) - every moq-cli and moq-gst reader, moqsrc and `moq play` included, follows instances through `announced_broadcast` and an epochless `Restart` where its output can mark it; otherwise it stays on the old instance and fails once that route dies +- [Readers follow a moved rendition](/quest/m1/rendition-move-follow.md) - moq-rtc, moq-ffi, moq-gst and `moq play` follow a rendition the catalog moves to a new track - [In-band auth](/quest/m1/auth/README.md) - a session tells its peer what it may publish and subscribe to, unions tokens presented in band, and fails loud on an out-of-scope publish - [Cluster routing](/quest/m1/cluster-routing/README.md) - prove shared routing contracts, implement per-origin routes, and connect outbound local clusters - [Stats groups as tracks](/quest/m1/stats-group-tracks.md) - one stable node broadcast per stats producer, with each group a produced track listed by TRACKS +- [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 - [Controlled time](/quest/m1/time/README.md) - every crate and package reads time through moq-clock or @moq/clock, with regression tests for the changed contracts - [io_uring MTU matches tokio](/quest/m1/uring-mtu.md) - a 1200-byte floor and `quic.mtu_discovery` honored, so the ring stops black-holing smaller paths +- [io_uring listen extensions](/quest/m1/uring-listen-extensions.md) - ring listeners offer the configured `[listen] extensions` - [Rust request credit](/quest/m1/rs-request-credit.md) - a moq-net request fails at once without stream credit instead of waiting - [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 - [Exact timestamps in every binding](/quest/m1/binding-timestamps.md) - bindings carry `{value, scale}` timestamps instead of lossy microseconds @@ -88,6 +97,7 @@ tracks, so per-group stats stop costing cluster announces. - [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 - [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, or order 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 - [Drop set](/quest/m1/drop-set.md) - each cursor judges missing groups against its own budget, so a narrow reader drops promptly and a widening re-offers groups already reported dropped on lite-07 - [Broadcast archives](/quest/m1/archive/README.md) - ANNOUNCE, TRACKS, and a SUBSCRIBE per track record every track under a prefix to any object_store, a host serves a stored broadcast back, and the catalog drops its archive pointer for an opt-in `hls` field @@ -97,6 +107,7 @@ tracks, so per-group stats stop costing cluster announces. - [Finalize moq-lite-07](/quest/m1/lite07-finalize.md) - when the maintainer cuts it, lite-07 negotiates as `moq-lite-07` and the next release ships it - [Moxygen minimal interop](/quest/m1/smoke-moxygen.md) - waiting on Moxygen to send a registered filter type, then the smoke lane stops hiding its failure - [More tests under load](/quest/m1/test-flakes-2/README.md) - the second round of load-only failures, one quest per flake, fixed at the cause +- [JS interop tone](/quest/m1/interop-js-tone.md) - the `js ->` cells pass the audio check on every draft again, after the 2026-10-23 regression - [A smaller, safer API than release](/quest/m1/api-audit/README.md) - every surface added since release has a consumer or is gone, overlapping operations are collapsed, names mirror across Rust, JS, and the bindings, and every break has its MIGRATE entry - [JavaScript FETCH](/quest/m1/js-fetch.md) - browser publishers answer IETF FETCH through the JS ranges request surface - [IETF fill timeout](/quest/m1/ietf-fill-timeout.md) - native FILL_TIMEOUT bounds upstream waiting on drafts 18-22, alongside local content-age enforcement @@ -115,15 +126,18 @@ tracks, so per-group stats stop costing cluster announces. - [Buffered playback subscribes in ascending order](/quest/m1/buffered-ascending.md) - a buffered `` asks for the oldest group first, so a voice reply's opening arrives first, and audio's head hold shrinks to the floor - [Buffered video poster](/quest/m1/buffered-video-poster.md) - buffered startup paints the first video frame at once and starts A/V together at a shared floor - [qmux local lifecycle](/quest/m1/qmux-local-lifecycle.md) - an abort blocked on send credit resets at once, and a close behind a backed-up socket still sends APPLICATION_CLOSE +- [JS reload config](/quest/m1/js-reload-config.md) - `@moq/net` reload takes Rust's `healthy` backoff and no ignored `extensions` - [JS release preflight](/quest/m1/js-publish-preflight.md) - `release.ts` refuses a package whose `@moq/*` dependency is not on npm, so no dependent publishes uninstallable - [Stream sockets disable Nagle](/quest/m1/tcp-nodelay.md) - every qmux TCP, TLS, and WebSocket socket, accepted or dialed, sets `TCP_NODELAY` - [TS fidelity and recovery](/quest/m1/ts-reliability/README.md) - both modes have explicit profiles, automated validation, loss tests, and a dedicated guide +- [Encoders declare no reordering](/quest/m1/encoder-reorder.md) - moq-video encoders publish `reorder: 0` - [Rust resume checks compression](/quest/m1/resume-compress.md) - a same-epoch route copy with a different `compress` is refused instead of corrupting framing - [WebGPU on Safari](/quest/m1/webgpu-safari.md) - the WebGPU renderer is verified on Safari 26 for macOS and iOS - [Track live signal](/quest/m1/track-live.md) - a reader can wait for its track's feed to be live, so a player skips old cached groups while a fresh upstream subscription starts - [Producers drop the phantom catalog type](/quest/m1/catalog-phantom.md) - moq-video, moq-audio and moq-mux producers lose an `E: CatalogExt` parameter that only `PhantomData` holds - [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 +- [Python docs stubs](/quest/m1/py-docs-stubs.md) - `just py docs` rebuilds its autosummary stubs, so a renamed symbol cannot break it - [Interop AAC](/quest/m1/interop-aac.md) - the Rust interop publisher adds an AAC tone and every subscriber checks it beside Opus - [Interop pre-skip](/quest/m1/interop-preskip.md) - the interop tone check anchors phase to timestamps, so a misapplied pre-skip fails - [moq-net internals](/quest/m1/net-internals.md) - no per-group session clones, no transport type parameter in lite and IETF internals, and the `Local` check measured diff --git a/quest/m1/api-audit/README.md b/quest/m1/api-audit/README.md index 33766fe4a58..c2ff4761432 100644 --- a/quest/m1/api-audit/README.md +++ b/quest/m1/api-audit/README.md @@ -80,7 +80,9 @@ Rejected in planning: - [Derived origins borrow](/quest/m1/api-audit/derived-origin-close.md) - closing a JS `scope()` or `withPreference()` handle releases only that handle - [Wrapper config names](/quest/m1/api-audit/wrapper-config.md) - Python, Go, and Dart client options use the FFI's names and nesting - [One decode read](/quest/m1/api-audit/decode-read.md) - audio and video decoders yield the same `Frame`, `Gap`, `End` read as moq-mux containers +- [Bindings read gaps and ends](/quest/m1/api-audit/decode-read-bindings.md) - moq-ffi and every wrapper surface `Gap` and `End` instead of discarding them - [Media surface trim](/quest/m1/api-audit/media-trim.md) - media crate items with no consumer go private, and Rust/JS media names agree +- [One frame rate name](/quest/m1/api-audit/frame-rate.md) - core `moq_video::Rate` becomes `FrameRate`, as the bindings name it - [CLI and gst flags](/quest/m1/api-audit/cli-flags.md) - one flag name per meaning across CLI subcommands and gst elements, and `linger` means one thing - [qmux shape](/quest/m1/api-audit/qmux-shape.md) - qmux binds framing at connect, shares flow-control names with moq-tokio, and JS drops the legacy wire - [Transport config](/quest/m1/api-audit/transport-config.md) - moq-tokio and moq-quic configs have one meaning for unset, enums for modes, no order-dependent setters, and no leftover features diff --git a/quest/m1/api-audit/decode-read-bindings.md b/quest/m1/api-audit/decode-read-bindings.md new file mode 100644 index 00000000000..005482d2edb --- /dev/null +++ b/quest/m1/api-audit/decode-read-bindings.md @@ -0,0 +1,31 @@ +# [L] Bindings read decoder gaps and ends + +## Goal + +Every binding reads the same audio and video decode stream as Rust: +frames, a gap where groups were skipped, and the end. moq-ffi, the generated +C and C++, Python, Swift, Kotlin, Dart, and Go stop discarding `Gap` and +`End`. + +## Plan + +[One decode read](/quest/m1/api-audit/decode-read.md) gives moq-audio and +moq-video `decode::Read { Frame, Gap, End }`, but moq-ffi (`audio.rs`, +`video.rs`), the hand-written moq-c, and the interop audio client still +hand over frames only. + +Open for this quest: the binding shape. Options are a tagged record per read +(mirrors Rust, one call), or frames plus a separate gap and end signal +(smaller change for wrappers). Recommend the tagged record, mirroring Rust +names, and ask the maintainer before implementing. + +Breaking in moq-ffi and every wrapper: `MIGRATE.md` entries per package, and +the Cross-Package Sync table's docs (`doc/lib/*`). Run `just test interop --all`. + +Decided 2026-10-23 (maintainer): tracked under the API audit. + +Public API: breaking, every binding. Wire: none. + +## Required + +- [One decode read](/quest/m1/api-audit/decode-read.md) - the Rust `decode::Read` this exposes diff --git a/quest/m1/api-audit/frame-rate.md b/quest/m1/api-audit/frame-rate.md new file mode 100644 index 00000000000..910c981f835 --- /dev/null +++ b/quest/m1/api-audit/frame-rate.md @@ -0,0 +1,23 @@ +# [S] One frame rate name + +## Goal + +Rust's `moq_video::Rate` is `moq_video::FrameRate`, the name the bindings +already use (`moq_video_frame_rate` in C, `VideoFrameRate` in C++), so the +type has one name across Rust, JS, and the bindings. + +## Plan + +[Binding encoder config](/quest/m1/binding-encoder-config.md) names the +binding type `FrameRate` and leaves the core `Rate` +(`rs/moq-video/src/rate.rs`) as is. Rename the core type and its uses in +moq-video, moq-transcode, moq-cli, moq-boy, moq-c, and moq-ffi (about 137 +references), with `MIGRATE.md` entries for each published crate. + +Decided 2026-10-23 (maintainer): rename the core type, not the bindings. + +Public API: breaking rename in moq-video. Wire: none. + +## Required + +- [Binding encoder config](/quest/m1/binding-encoder-config.md) - introduces the binding `FrameRate` diff --git a/quest/m1/auth/README.md b/quest/m1/auth/README.md index cf9ff0086de..60f3ef64903 100644 --- a/quest/m1/auth/README.md +++ b/quest/m1/auth/README.md @@ -149,6 +149,7 @@ because `main` assigned 0x3A to NOT_FETCHABLE first. AUTH streams: a session starts on the token its SETUP or URL carried and its AUTH streams add the rest - [Binding auth polish](/quest/m1/auth/bindings-polish.md) - OBS reconnects with the presented token, one FFI auth handle, Swift grant sequences +- [Re-present a lost token](/quest/m1/auth/token-represent.md) - a token whose AUTH stream ended without a verdict is presented again on the live session ## Related diff --git a/quest/m1/auth/token-represent.md b/quest/m1/auth/token-represent.md new file mode 100644 index 00000000000..991c8be1f4d --- /dev/null +++ b/quest/m1/auth/token-represent.md @@ -0,0 +1,21 @@ +# [M] A lost AUTH stream re-presents its token + +## Goal + +When a session's AUTH stream ends without a verdict, the shared auth handle +presents its token again on the same session, with backoff, on lite and +moq-transport. The session never runs on without a token it was given. + +## Plan + +After [Auth outlives its session](/quest/m1/auth/issuer.md), a token whose +AUTH stream is lost waits for the next session. The moq-transport presenter +has no retry loop at all. Retry on the live session with the connection's +backoff, stop on a verdict or session end, and test both wires with mocked +time. + +Public API: none expected. Wire: none. + +## Required + +- [Auth outlives its session](/quest/m1/auth/issuer.md) - the shared handle that presents the token diff --git a/quest/m1/c/consumers.md b/quest/m1/c/consumers.md index 79ec9006599..8a4d3e0f790 100644 --- a/quest/m1/c/consumers.md +++ b/quest/m1/c/consumers.md @@ -9,7 +9,9 @@ build. Nothing in the repository calls the hand-written ABI any more. ## Plan - Port the interop client first; it is the smallest real consumer and proves - the callback and dispatcher shape end to end. + the callback and dispatcher shape end to end. It skips a disabled audio + rendition through the generated record's `enabled`, as js-native does; the + hand-written ABI has only `moq_consume_video_enabled` (decided 2026-10-23). - Rewrite `doc/lib/c` around the generated API, including a sample that runs callbacks on the host's own loop through `moq_set_dispatcher`. diff --git a/quest/m1/close-cause.md b/quest/m1/close-cause.md new file mode 100644 index 00000000000..103f0864837 --- /dev/null +++ b/quest/m1/close-cause.md @@ -0,0 +1,27 @@ +# [S] Every session close records its cause + +## Goal + +A session closed for a protocol violation, or abandoned at shutdown before +its SETUP reply, records why, over every transport, instead of a clean end +or nothing. + +## Plan + +[One clean close](/quest/m1/api-audit/clean-close.md) adds the session's close +cause, but some closes bypass it and close the transport directly: +`lite::error::control` (`rs/moq-net/src/lite/error.rs`), +`request_update::too_many` (`rs/moq-net/src/ietf/request_update.rs`), and the +IETF dispatcher's protocol-violation close. Over QUIC those still record a +clean end. Route them through the close cause. + +The relay's shutdown drain drops `request.ok()` at its deadline +(`until_deadline` in `rs/moq-relay/src/connection.rs`, the `select` in +`rs/moq-relay/src/uring.rs`), so an abandoned SETUP reply records nothing. +Record it as the handshake deadline already does. + +Public API: none. Wire: none. + +## Required + +- [One clean close](/quest/m1/api-audit/clean-close.md) - introduces the close cause diff --git a/quest/m1/drop-set.md b/quest/m1/drop-set.md index 957622b3b20..ab78fc5ce47 100644 --- a/quest/m1/drop-set.md +++ b/quest/m1/drop-set.md @@ -206,6 +206,13 @@ Test on mock time through a relay: - A single run spanning billions of sequences is settled by each cursor in one step, whether dropped, skipped by a wider reader, or evicted and judged again. +- Per-reader misses are bounded the same way. [Dynamic range + requests](/quest/m1/subscribe-ranges/dynamic.md) adds a third unbounded + record: each reader's given-up misses, one per gap, which a reader whose + floor never moves cannot forget. Fold them into the cursor's runs, billed + to the pool (from + [moq-dev/moq#6171](https://github.com/moq-dev/moq/pull/6171), decided + 2026-10-23). Mirror each case in `js/net`. diff --git a/quest/m1/empty-segments.md b/quest/m1/empty-segments.md new file mode 100644 index 00000000000..ff1f3bc4b37 --- /dev/null +++ b/quest/m1/empty-segments.md @@ -0,0 +1,19 @@ +# [S] Publication paths refuse empty segments + +## Goal + +A publication path with an empty segment, such as `room//cam`, is refused +in Rust and JS instead of being silently collapsed to `room/cam`. + +## Plan + +[moq-dev/moq#6441](https://github.com/moq-dev/moq/pull/6441) refuses `.` and +`..` segments. `Path` normalization (`rs/moq-net/src/path/mod.rs`) still +collapses empty ones, so two different inputs name the same broadcast. + +Decided 2026-10-23 (maintainer): fail loud. Refuse empty publication segments +before normalization, at the same point #6441 refuses navigation segments, +in Rust and `@moq/net`, with tests for leading, trailing, and doubled +slashes. Start after #6441 merges. + +Public API: inputs that were accepted now fail. Wire: none. diff --git a/quest/m1/encoder-reorder.md b/quest/m1/encoder-reorder.md new file mode 100644 index 00000000000..b58b32546b6 --- /dev/null +++ b/quest/m1/encoder-reorder.md @@ -0,0 +1,20 @@ +# [XS] moq-video encoders declare no reordering + +## Goal + +moq-video's encoders, which emit no B-frames, declare `reorder: 0` in the +`VideoHint` they publish, so readers take decode time from it instead of +guessing. + +## Plan + +[Decode order](/quest/m1/ts-reliability/decode-order.md) adds +`VideoHint.reorder` and lists the moq-video encoders as a producer that +declares it, but leaves them out. Set it in `rendition_hint` +(`rs/moq-video/src/encode/producer.rs`) and assert it in an encoder test. + +Public API: none. Wire: none. + +## Required + +- [Decode order](/quest/m1/ts-reliability/decode-order.md) - adds the `reorder` field diff --git a/quest/m1/fill-updates.md b/quest/m1/fill-updates.md new file mode 100644 index 00000000000..87c847dd409 --- /dev/null +++ b/quest/m1/fill-updates.md @@ -0,0 +1,21 @@ +# [M] Subscription updates reach queued and taken fills + +## Goal + +When a reader changes its priority, delay, or order, fills already queued or +taken by a request handler serve under the new values. + +## Plan + +After [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md), a +`track::Fill` keeps the priority, delay, and order its joins gave it. Once a +handler takes it, an update cannot reach it, and a queued one is not +refreshed. A reader that switches to descending still has filled groups +served in the old order. Cover queued and taken fills with one mechanism, +and test an order switch mid-fill. + +Public API: possibly a handler-facing update. Wire: none. + +## Required + +- [Dynamic range requests](/quest/m1/subscribe-ranges/dynamic.md) - introduces the fills diff --git a/quest/m1/ietf-request-cancel.md b/quest/m1/ietf-request-cancel.md new file mode 100644 index 00000000000..37cab3dd181 --- /dev/null +++ b/quest/m1/ietf-request-cancel.md @@ -0,0 +1,26 @@ +# [M] A cancelled moq-transport request stops writing + +## Goal + +Cancelling a moq-transport SUBSCRIBE, FETCH, TRACK_STATUS, or namespace +request releases its state at once, even when the peer withholds send +credit. + +## 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. + +Public API: none. Wire: none. + +## Required + +- [IETF lazy discovery](/quest/m1/ietf-lazy-discovery.md) - changes the same request writes diff --git a/quest/m1/ietf-setup-cancel.md b/quest/m1/ietf-setup-cancel.md new file mode 100644 index 00000000000..09bc07e0beb --- /dev/null +++ b/quest/m1/ietf-setup-cancel.md @@ -0,0 +1,22 @@ +# [M] SUBSCRIBE setup stops when the subscriber cancels + +## Goal + +A subscriber that cancels a SUBSCRIBE while the publisher is still setting it +up releases the upstream demand and the request slot at once, on every +moq-transport draft, in Rust and JS. + +## Plan + +In `rs/moq-net/src/ietf/publisher.rs`, a SUBSCRIBE's setup +(`request_broadcast`, `subscribe_track`, and the `poll_live` wait) races only +the auth gate, not the subscriber withdrawing it (UNSUBSCRIBE, or a FIN or +reset on its request stream). Until the producer answers, a cancelled +request keeps upstream demand and its slot. Resume uses the same setup. +Raised on [moq-dev/moq#6299](https://github.com/moq-dev/moq/pull/6299), where +the maintainer placed it in SUBSCRIBE setup as a whole. + +Race setup against withdrawal, mirror it in `@moq/net`, and test a cancel +mid-setup with a producer that never answers. + +Public API: none. Wire: none. diff --git a/quest/m1/interop-js-tone.md b/quest/m1/interop-js-tone.md new file mode 100644 index 00000000000..691201d6450 --- /dev/null +++ b/quest/m1/interop-js-tone.md @@ -0,0 +1,31 @@ +# [M] The JS interop publisher's tone survives every draft + +## Goal + +Interop's `js ->` cells pass the audio check on every moq-transport draft +again, with the cause of the 2026-10-23 regression fixed at its source. + +## Plan + +The scheduled Interop run on 2026-10-23 +([run 39958710886](https://github.com/moq-dev/moq/actions/runs/39958710886), +`--all --versions ietf`) failed every `js ->` cell on drafts 18 and 22, and +`js -> rust` and `js -> c` on draft 20, with "wrong tone: channel 0 carries +~440 Hz, not 440 Hz" at 0.10 to 0.77 of the energy at 440 Hz. `rust -> js` +also failed on drafts 20 and 22. The same job passed all of these on +2026-10-22, and drafts 14 and 16 pass on both days, so it is not a load +flake. It started after +[moq-dev/moq#6299](https://github.com/moq-dev/moq/pull/6299) merged (pause +and resume as a new subscriber), which is a guess from timing only. + +Reproduce one cell locally, bisect between the two runs, and fix the cause. +Separate from [tone grace](/quest/m1/test-flakes-2/tone-grace.md), which +covers a false "wrong tone" under CPU starvation. + +Decided 2026-10-23 (maintainer): its own quest, ranked above the load flakes. + +Public API: depends on the cause. Wire: depends on the cause. + +## Related + +- [Interop timestamp gap](/quest/m1/test-flakes-2/interop-timestamp-gap.md) - the load-only audio flake, a different symptom diff --git a/quest/m1/js-qmux-credit.md b/quest/m1/js-qmux-credit.md new file mode 100644 index 00000000000..c9448579696 --- /dev/null +++ b/quest/m1/js-qmux-credit.md @@ -0,0 +1,23 @@ +# [S] JS qmux returns stream credit when both halves close + +## Goal + +`@moq/qmux` returns MAX_STREAMS credit for a peer stream only once both of +its halves are terminal, as QUIC and Rust qmux do, so stream credit bounds +open streams. `@moq/net` then drops its own subscription and FETCH caps to +mirror Rust. + +## Plan + +`maybeReplenishStreamCredit` in `js/qmux/src/session.ts` fires once a stream +is accepted and its receive half is terminal. A bidirectional subscription +whose receive half ends while its send half stays open frees credit early. +Because of this, [Session limits](/quest/m1/api-audit/session-limits.md) +kept the `@moq/net` caps. Fix the credit, then remove those caps, with a test +that a half-closed stream still holds its credit. + +Public API: `@moq/net` loses its subscription and FETCH caps. Wire: none. + +## Related + +- [Session limits](/quest/m1/api-audit/session-limits.md) - kept the JS caps because of this diff --git a/quest/m1/js-reload-config.md b/quest/m1/js-reload-config.md new file mode 100644 index 00000000000..2d6dddea60b --- /dev/null +++ b/quest/m1/js-reload-config.md @@ -0,0 +1,26 @@ +# [S] JS reload takes the Rust backoff config + +## Goal + +`@moq/net`'s reload loop takes the same backoff settings as Rust (`initial`, +`max`, and `healthy`, the time a session must stay up to count as healthy) +and accepts no option it ignores. + +## Plan + +[Transport config](/quest/m1/api-audit/transport-config.md) splits Rust's +healthy bar out of `backoff.initial` into `backoff.healthy`. `ReloadDelay` +(`js/net/src/connection/reload.ts`) still uses `initial` as the healthy bar. +Add `healthy` with Rust's default and a mocked-time test. + +`ReloadProps` is `Omit`, so it accepts `extensions`, but +the reload loop never passes them to `connect()`. Decided 2026-10-23 +(maintainer): one quest for both. Omit `extensions` from `ReloadProps`, since +[moq-net exports](/quest/m1/api-audit/net-exports.md) makes it `@internal`, +unless that quest lands keeping it public, in which case forward it. + +Public API: breaking JS option change, with a `MIGRATE.md` entry. Wire: none. + +## Required + +- [Transport config](/quest/m1/api-audit/transport-config.md) - the Rust `backoff.healthy` this mirrors diff --git a/quest/m1/listing-credit.md b/quest/m1/listing-credit.md new file mode 100644 index 00000000000..254c36d0a3c --- /dev/null +++ b/quest/m1/listing-credit.md @@ -0,0 +1,28 @@ +# [M] SUBSCRIBE_TRACKS listings follow stream credit + +## Goal + +A draft-17+ SUBSCRIBE_TRACKS listing holds state in proportion to what the +peer reads, not to the catalog. Each listing PUBLISH opens only as the +peer's stream credit admits it, and a peer that stops reading stops the +enumeration. + +## Plan + +In `rs/moq-net/src/ietf/publisher/tracks.rs`, every FORWARD=1 or unfittable +track goes into `request.skips` as the listing is enumerated, and +`poll_write_skips` paces only the writes. A peer that stops reading makes the +session hold state proportional to the whole catalog, multiplied across +peers. After [Session limits](/quest/m1/api-audit/session-limits.md), the +listing PUBLISHes are bounded only by `Limits::requests`, a runtime cap. + +Decided 2026-10-23 (maintainer): replace the cap with flow control. Couple +enumeration to write backpressure, open each PUBLISH as stream credit admits +it, and drop the listing's use of `Limits::requests`. Test with a peer that +withholds credit and reads nothing. + +Public API: none expected. Wire: none. + +## Required + +- [Session limits](/quest/m1/api-audit/session-limits.md) - rewrites the same listing code diff --git a/quest/m1/name-reuse.md b/quest/m1/name-reuse.md new file mode 100644 index 00000000000..bb8a1e7609a --- /dev/null +++ b/quest/m1/name-reuse.md @@ -0,0 +1,23 @@ +# [M] Publishers never reuse a finished track's name + +## Goal + +No first-party publisher finishes a track and then publishes different +content under the same name. Each either aborts the track or numbers its +groups past its predecessor's. + +## Plan + +[Stats groups as tracks](/quest/m1/stats-group-tracks.md) documents that a +name always means the same content and fixes moq-transcode. Others still +finish and re-create: moq-gst's sink on caps renegotiation and `release_pad` +(`rs/moq-gst/src/sink/imp.rs`). Also audit moq-mux's timeline error path, +moq-cli echo retire, moq-rtsp, moq-rtmp, `cpp/obs`, `js/publish`, and the IETF +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. + +## Required + +- [Stats groups as tracks](/quest/m1/stats-group-tracks.md) - states the rule and fixes moq-transcode diff --git a/quest/m1/py-docs-stubs.md b/quest/m1/py-docs-stubs.md new file mode 100644 index 00000000000..ae19fbbf496 --- /dev/null +++ b/quest/m1/py-docs-stubs.md @@ -0,0 +1,15 @@ +# [XS] Python docs rebuild their API stubs + +## Goal + +A renamed or removed binding symbol never breaks `just py docs` through +stale generated stubs. + +## Plan + +`py/justfile`'s `docs` recipe clears only `dist/docs`. The gitignored +autosummary stubs in `py/moq-rs/docs/api/` survive between builds, so +`sphinx-build -W` fails on a symbol that no longer exists. This broke +`just check` for several agents. Clear that directory in the recipe. + +Public API: none. Wire: none. diff --git a/quest/m1/release-perf/README.md b/quest/m1/release-perf/README.md index 1205f1818a5..b473e0e9b4d 100644 --- a/quest/m1/release-perf/README.md +++ b/quest/m1/release-perf/README.md @@ -162,6 +162,10 @@ The raw report lived in a scratch directory that has since been removed. `just bench ` now pairs full Criterion IDs; point `MOQ_BENCH_ARTIFACTS` outside the worktree to keep the next one. +## Required + +- [Landing skips the parked lock](/quest/m1/release-perf/expiry-landed.md) - `expiry::Wakes::landed` takes no lock when no reader is parked + ## Related - [Performance audit](/quest/m2/audit-perf.md) - compares nightly to nightly; this epic compares main to release diff --git a/quest/m1/release-perf/expiry-landed.md b/quest/m1/release-perf/expiry-landed.md new file mode 100644 index 00000000000..dea31a4f491 --- /dev/null +++ b/quest/m1/release-perf/expiry-landed.md @@ -0,0 +1,20 @@ +# [S] Landing a group skips the parked lock when nothing waits + +## Goal + +`expiry::Wakes::landed` costs no lock when no reader is parked, cutting about +600 instructions from every group's arrival. + +## Plan + +`landed` (`rs/moq-net/src/model/expiry.rs`) takes the `parked` mutex on every +group, even when `landings` is empty. Add a fast path that skips it, with a +written memory-ordering argument that a reader parking concurrently is never +missed, and a loom model for that race. Measure on `track_stamp_groups` and +`session_delivery_paced`. + +Decided 2026-10-23 (maintainer): only this fast path. The rest of +`session_delivery_*` stays accepted as recorded in +[the epic](/quest/m1/release-perf/README.md). + +Public API: none. Wire: none. diff --git a/quest/m1/rendition-move-follow.md b/quest/m1/rendition-move-follow.md new file mode 100644 index 00000000000..77723f728f7 --- /dev/null +++ b/quest/m1/rendition-move-follow.md @@ -0,0 +1,24 @@ +# [M] Catalog readers follow a moved rendition + +## Goal + +When a catalog moves a rendition to a new track, every reader that picked +it keeps playing from the new track, not only the exporters. + +## Plan + +[moq-dev/moq#6276](https://github.com/moq-dev/moq/pull/6276) made the +exporters rebind. Other readers resolve a rendition's track once: +`rs/moq-rtc/src/egress.rs` subscribes per pick, and moq-ffi's audio and video +consumers, moq-gst's source, and `moq play` are candidates to check. Audit +each, make the ones that resolve once follow catalog updates, and test a move +mid-playback for each. + +[Readers follow restarts](/quest/m1/sink-restart-follow.md) covers broadcast +restarts, not track moves inside one broadcast. + +Public API: none expected. Wire: none. + +## Related + +- [One Annex-B exporter](/quest/m1/annexb-export.md) - its shared lifecycle also follows a rebound rendition diff --git a/quest/m1/test-flakes-2/README.md b/quest/m1/test-flakes-2/README.md index 451e07f078d..9630dbbd052 100644 --- a/quest/m1/test-flakes-2/README.md +++ b/quest/m1/test-flakes-2/README.md @@ -43,6 +43,8 @@ Public API: none. Wire: none. - [Media resource instrumentation](/quest/m1/test-flakes-2/resource-instrumentation.md) - the media test stops seeing a playing player report nothing to release - [Tone check grace](/quest/m1/test-flakes-2/tone-grace.md) - the interop tone check charges its grace only for audible playback, so a refilling ring under CPU starvation is not a failure - [Relay tests in one process](/quest/m1/test-flakes-2/relay-one-process.md) - moq-relay's integration binary passes under plain `cargo test`, not only nextest +- [Interop timestamp gap](/quest/m1/test-flakes-2/interop-timestamp-gap.md) - `rust -> js-native-node` audio has no gap under load +- [Go reservation grant](/quest/m1/test-flakes-2/go-reservation-grant.md) - the Go reservation test waits for its grant ## Related diff --git a/quest/m1/test-flakes-2/go-reservation-grant.md b/quest/m1/test-flakes-2/go-reservation-grant.md new file mode 100644 index 00000000000..7771e740351 --- /dev/null +++ b/quest/m1/test-flakes-2/go-reservation-grant.md @@ -0,0 +1,16 @@ +# [XS] The Go reservation test waits for a grant + +## Goal + +`TestReservationUpdateReleasesSubscribedTrack` (`go/wrapper/bandwidth_test.go`) +never fails with "grant unavailable". + +## Plan + +`Reservation.Grant()` returns nothing until the QUIC estimate exists, and the +test asserts right after `Dial`. Seen under load in +[moq-dev/moq#6476](https://github.com/moq-dev/moq/pull/6476) and +[moq-dev/moq#6517](https://github.com/moq-dev/moq/pull/6517). Wait on the +grant's own signal rather than a sleep. + +Public API: none. Wire: none. diff --git a/quest/m1/test-flakes-2/interop-timestamp-gap.md b/quest/m1/test-flakes-2/interop-timestamp-gap.md new file mode 100644 index 00000000000..5bf5c6fac23 --- /dev/null +++ b/quest/m1/test-flakes-2/interop-timestamp-gap.md @@ -0,0 +1,17 @@ +# [S] Interop audio has no timestamp gap under load + +## Goal + +`rust -> js-native-node` (and `rust-swapped`) never fail the audio check with +a timestamp gap under load. + +## Plan + +Seen in the interop runs of +[moq-dev/moq#6447](https://github.com/moq-dev/moq/pull/6447), +[moq-dev/moq#6530](https://github.com/moq-dev/moq/pull/6530), and +[moq-dev/moq#6517](https://github.com/moq-dev/moq/pull/6517): a gap up to the +next 1 s group, passing on rerun. Reproduce under CPU load, find whether the +publisher, relay, or the Node subscriber drops the audio, and fix that layer. + +Public API: none. Wire: none. diff --git a/quest/m1/uring-listen-extensions.md b/quest/m1/uring-listen-extensions.md new file mode 100644 index 00000000000..840cf5499f0 --- /dev/null +++ b/quest/m1/uring-listen-extensions.md @@ -0,0 +1,17 @@ +# [XS] io_uring listeners honor listen.extensions + +## Goal + +A relay listening on io_uring offers the `[listen] extensions` its operator +configured, as the tokio listeners already do. + +## Plan + +`rs/moq-relay/src/uring.rs` builds `moq_net::Server::new().with_versions(...)` +and never applies `listen.extensions`; the tokio path +(`rs/moq-relay/src/relay.rs`, `websocket.rs`) does. Apply them and add a uring +test that a configured extension is negotiated. +[Relay config](/quest/m1/api-audit/relay-config.md) keeps extensions as the +operator's interop knob, so every listener must honor it. + +Public API: none. Wire: none. diff --git a/quest/m2/README.md b/quest/m2/README.md index 6ec072389a6..c459a1ca979 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -51,6 +51,7 @@ ranked just below deterministic TS legs, which it requires. - [Native browser codecs](/quest/m2/native-compression-adopt.md) - adopt native operations when timing, frame and resource contracts hold - [Time test sweep](/quest/m2/time-test-sweep.md) - unrelated test conversion and the workspace lint ratchet +- [Drop test-only cut requests](/quest/m2/video-cut-test-only.md) - moq-video's encode producer loses code only its tests use - [Pipelined requests](/quest/m2/pipeline-requests/README.md) - SUBSCRIBE and the first FETCH go out with the track-info request at every hop, so first data arrives a round trip sooner per hop - [OBS native codecs](/quest/m2/obs-moq-video/README.md) - replace FFmpeg video and audio decoding with moq-video and moq-audio, deliver GPU frames, and use native audio/video encoders @@ -79,6 +80,8 @@ ranked just below deterministic TS legs, which it requires. - [Nightly fuzzing keeps its corpus](/quest/m2/fuzz-corpus.md) - each fuzz target resumes from the previous Nightly's minimized corpus instead of starting from seeds - [Relay session bench](/quest/m2/bench-relay.md) - the same scenario through moq-relay's own connection handling - [Transport x version benches](/quest/m2/bench-transport-sweep.md) - the session benches sweep transports as well as versions +- [Paired bench BASE](/quest/m2/bench-base-paired.md) - `just bench ` uses the nightly's paired comparison and the old one is deleted +- [Relay NixOS module](/quest/m2/nix-relay-module.md) - module edits run moq-relay's tests, and `cluster.rootUrl` takes several peers - [Read-only lookup](/quest/m2/read-only-lookup.md) - a live track lookup stops waking the front and every demand watcher - [Ordered owed wait](/quest/m2/ordered-owed-wait.md) - an ordered reader waits within max_delay for a missing group only while the track still owes it - [lite-05/06 gap judgement](/quest/m2/lite-legacy-gap.md) - a resumed copy on lite-05/06 never reads a descending track's newest group as its start diff --git a/quest/m2/bench-base-paired.md b/quest/m2/bench-base-paired.md new file mode 100644 index 00000000000..eeb36707b3f --- /dev/null +++ b/quest/m2/bench-base-paired.md @@ -0,0 +1,21 @@ +# [S] just bench BASE uses the paired comparison + +## Goal + +`just bench ` compares through the same paired screen-and-confirm path +the nightly uses, and the old per-case comparison is deleted. + +## Plan + +[moq-dev/moq#6511](https://github.com/moq-dev/moq/pull/6511) added the paired +path (`--criterion` with `MOQ_BENCH_BASE`, writing `paired.json`). The BASE +mode in `bench/run.sh` still goes through `compare_criterion`. Route it +through the paired path, and delete `criterion_cases`, `compare_criterion`, +`report_set_changes`, and `--criterion-baseline`. That also deletes the +`criterion_cases ... >file` call that hangs under errexit when it fails. + +Public API: none. Wire: none. + +## Related + +- [Calibrate the paired nightly benchmarks](/quest/m1/bench-ci.md) - tunes the thresholds this path uses diff --git a/quest/m2/nix-relay-module.md b/quest/m2/nix-relay-module.md new file mode 100644 index 00000000000..9c71865c3f9 --- /dev/null +++ b/quest/m2/nix-relay-module.md @@ -0,0 +1,19 @@ +# [XS] The relay's NixOS module is tested and takes several cluster peers + +## Goal + +A change to `nix/modules/moq-relay.nix` runs moq-relay's tests, and +`cluster.rootUrl` accepts several peers. + +## Plan + +moq-relay's `the_nixos_module_sets_only_known_variables` test reads the +module, but a module-only change selects only the flake scope. Map +`nix/modules/moq-relay.nix` to moq-relay in `sh/dispatch.sh` and +`sh/rs/select.sh`. + +`cluster.rootUrl` is a single string mapped to `MOQ_CLUSTER_CONNECT`, which +accepts a comma-separated list (`rs/moq-relay/src/cluster.rs`). Make it a +non-empty list joined with commas. That breaks the module option; say so in the PR. + +Public API: NixOS module option type changes. Wire: none. diff --git a/quest/m2/tls-listener-mtls.md b/quest/m2/tls-listener-mtls.md index b7e950ce975..04022c2d1da 100644 --- a/quest/m2/tls-listener-mtls.md +++ b/quest/m2/tls-listener-mtls.md @@ -13,7 +13,10 @@ token. `moq_tokio::tcp::Listener::with_tls` (`rs/moq-tokio/src/tcp.rs`) already takes an `Arc`, so requesting a certificate is config. The real gap is getting the peer certificate out afterwards: `qmux::tls::Server::accept` finishes the handshake and returns a `Session` -without it. +without it. The same accessor also returns the SNI the client sent, so a +`tls://` session reports it as QUIC sessions do (from +[moq-dev/moq#6509](https://github.com/moq-dev/moq/pull/6509), decided +2026-10-23). Decided by the maintainer (2026-10-06): the peer certificate comes from a new accessor on qmux's TLS session. Rejected: owning the TLS accept outside diff --git a/quest/m2/video-cut-test-only.md b/quest/m2/video-cut-test-only.md new file mode 100644 index 00000000000..31f3beec76b --- /dev/null +++ b/quest/m2/video-cut-test-only.md @@ -0,0 +1,20 @@ +# [S] Drop moq-video's test-only cut requests + +## Goal + +moq-video's encode producer carries no code only its tests use: the keyframe +`Requests` and cut counting behind the private capture `Control` are gone. + +## Plan + +After [Media surface trim](/quest/m1/api-audit/media-trim.md) makes capture +`Control` private, `Requests` and the cut counting in +`rs/moq-video/src/encode/producer.rs` serve only tests. Delete them, fold the +driver into `publish_capture`, and rewrite the tests against the public +path. + +Public API: none. Wire: none. + +## Required + +- [Media surface trim](/quest/m1/api-audit/media-trim.md) - makes `Control` private