From 2325e441d225ef02d8d773b1f4999650faf3cc3c Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 6 Oct 2026 14:30:17 -0700 Subject: [PATCH 1/3] quest: resolve the 2026-10-06 audit Apply the maintainer's /quest-audit decisions to the quest tree on main: sync the auth line with its branch, delete children already finished on the ffi-shape, cpp, and qos line branches, delete route trust and the m3 processor line, record the settled decisions (catalog rendition IDs, ladder controller ownership, stats-split first, flv/mkv --delay, shared-clock anchors, unordered qmux scope, tls:// mTLS through qmux), split a stats producer benchmark quest, and fix stale facts and Required ordering. Co-Authored-By: Claude Opus 5.5 --- quest/README.md | 3 +- quest/m0/wildcard/README.md | 7 +- quest/m1/README.md | 9 +- quest/m1/archive/README.md | 44 +++++- quest/m1/archive/backward-timestamps.md | 4 - quest/m1/archive/dvr.md | 10 +- quest/m1/archive/flush.md | 6 +- quest/m1/archive/hls-target.md | 4 - quest/m1/archive/replay-catalog.md | 9 +- quest/m1/archive/track-timeline.md | 71 --------- quest/m1/audio-quality-native.md | 21 ++- quest/m1/auth/README.md | 51 +++--- quest/m1/auth/auth-ok-preflight.md | 24 --- quest/m1/auth/bindings.md | 8 +- quest/m1/auth/error-codes.md | 36 ----- quest/m1/auth/expired-error.md | 6 +- quest/m1/auth/interop.md | 24 --- quest/m1/auth/js-fetch-watch.md | 14 ++ quest/m1/auth/lite.md | 147 ------------------ quest/m1/auth/moq-transport.md | 106 ------------- quest/m1/auth/narrowing.md | 69 -------- quest/m1/auth/not-supported.md | 54 +++++++ quest/m1/auth/relay-refresh.md | 39 ++--- quest/m1/auth/request-token.md | 2 +- quest/m1/auth/token-in-band.md | 7 +- quest/m1/auth/unauthorized.md | 25 --- quest/m1/auth/violations.md | 41 +++++ quest/m1/auth/wip-version.md | 37 +++++ quest/m1/binding-surface.md | 9 +- quest/m1/c/package.md | 1 + quest/m1/catalog-track-alias.md | 42 ++--- quest/m1/cli-packaging.md | 6 +- quest/m1/cluster-routing/README.md | 7 +- quest/m1/cluster-routing/routes.md | 4 +- quest/m1/cluster-routing/selection.md | 50 +++--- quest/m1/cpp/README.md | 13 +- quest/m1/cpp/cancel.md | 26 ---- quest/m1/cpp/cxx-standard.md | 29 ---- quest/m1/cpp/generator.md | 43 ----- quest/m1/cpp/macos-alias-check.md | 17 -- quest/m1/cpp/obs.md | 53 ------- quest/m1/cpp/package.md | 44 ------ quest/m1/ffi-shape/README.md | 3 - quest/m1/ffi-shape/codec.md | 9 +- quest/m1/ffi-shape/json.md | 33 ---- quest/m1/ffi-shape/media.md | 31 ---- quest/m1/ffi-shape/net.md | 61 -------- quest/m1/flv-catalog-stream.md | 5 +- quest/m1/flv-export-delay.md | 11 +- quest/m1/hls-bounded.md | 3 +- quest/m1/hop-aligned-import.md | 12 +- quest/m1/ietf-fetch-location.md | 9 +- quest/m1/libmoq-final-release.md | 10 +- quest/m1/mkv-export-delay.md | 11 +- ...relay-cpu-is-vdso-clock-reads-the-drive.md | 8 +- quest/m1/publish-timestamp.md | 2 +- quest/m1/qos/README.md | 16 +- quest/m1/qos/final-lag-sample.md | 38 ----- quest/m1/qos/lag-dashboard.md | 26 ++-- quest/m1/qos/lag-splice.md | 4 - quest/m1/qos/publisher-timeliness.md | 16 +- quest/m1/qos/starvation.md | 87 ----------- quest/m1/quic/README.md | 4 - quest/m1/quic/uring-close.md | 24 --- quest/m1/route-wakes.md | 14 +- quest/m1/rs2ts/translator.md | 7 +- quest/m1/shared-clock.md | 15 +- quest/m1/stats/README.md | 19 +-- quest/m1/stats/health.md | 2 +- quest/m1/stats/js.md | 4 +- quest/m1/stats/rust.md | 2 +- quest/m1/stats/schema.md | 8 +- quest/m1/subscribe-ranges/README.md | 2 +- quest/m1/subscribe-ranges/ietf.md | 10 ++ quest/m1/subscriber-max-delay.md | 6 +- quest/m1/track-priority-scope.md | 8 +- quest/m1/transport-upgrade/README.md | 22 +-- quest/m1/transport-upgrade/js-qmux-finish.md | 4 - quest/m1/transport-upgrade/js.md | 1 + quest/m2/README.md | 3 +- quest/m2/announce-shapes.md | 8 +- quest/m2/bench-coverage.md | 27 +--- quest/m2/catalog-tracks.md | 4 +- quest/m2/cpp-vcpkg.md | 2 +- quest/m2/flate.md | 2 +- quest/m2/ietf-malformed-close.md | 9 ++ quest/m2/nvenc-pool.md | 9 +- quest/m2/quic-ecn-measure.md | 7 +- quest/m2/relay-io-uring-package.md | 4 - quest/m2/starvation-frames.md | 2 - quest/m2/stats-delta.md | 2 +- quest/m2/stats-producer-bench.md | 37 +++++ quest/m2/tls-listener-mtls.md | 18 ++- quest/m2/ts-eac3.md | 2 +- quest/m2/tstd-controls.md | 2 +- quest/m2/vaapi-vulkan-import.md | 6 +- quest/m3/README.md | 2 - quest/m3/closure-counters.md | 45 +++--- quest/m3/ladder/README.md | 10 +- quest/m3/ladder/controller.md | 14 +- quest/m3/p2p/README.md | 10 +- quest/{m1/auth => m3/p2p}/peer-grant.md | 12 +- quest/m3/p2p/signal.md | 14 +- quest/m3/p2p/unordered.md | 18 ++- quest/m3/processor/README.md | 48 ------ quest/m3/processor/advertise-auth.md | 43 ----- quest/m3/processor/grant-lease.md | 39 ----- quest/m3/processor/media-contract.md | 37 ----- quest/m3/route-trust.md | 43 ----- quest/m3/stats-encoder-feedback.md | 20 ++- quest/m3/video-hardware-access.md | 5 +- quest/m3/video-hardware.md | 11 +- 112 files changed, 689 insertions(+), 1595 deletions(-) delete mode 100644 quest/m1/archive/track-timeline.md delete mode 100644 quest/m1/auth/auth-ok-preflight.md delete mode 100644 quest/m1/auth/error-codes.md delete mode 100644 quest/m1/auth/interop.md create mode 100644 quest/m1/auth/js-fetch-watch.md delete mode 100644 quest/m1/auth/lite.md delete mode 100644 quest/m1/auth/moq-transport.md delete mode 100644 quest/m1/auth/narrowing.md create mode 100644 quest/m1/auth/not-supported.md delete mode 100644 quest/m1/auth/unauthorized.md create mode 100644 quest/m1/auth/violations.md create mode 100644 quest/m1/auth/wip-version.md delete mode 100644 quest/m1/cpp/cancel.md delete mode 100644 quest/m1/cpp/cxx-standard.md delete mode 100644 quest/m1/cpp/generator.md delete mode 100644 quest/m1/cpp/macos-alias-check.md delete mode 100644 quest/m1/cpp/obs.md delete mode 100644 quest/m1/cpp/package.md delete mode 100644 quest/m1/ffi-shape/json.md delete mode 100644 quest/m1/ffi-shape/media.md delete mode 100644 quest/m1/ffi-shape/net.md delete mode 100644 quest/m1/qos/final-lag-sample.md delete mode 100644 quest/m1/qos/starvation.md delete mode 100644 quest/m1/quic/uring-close.md create mode 100644 quest/m2/stats-producer-bench.md rename quest/{m1/auth => m3/p2p}/peer-grant.md (84%) delete mode 100644 quest/m3/processor/README.md delete mode 100644 quest/m3/processor/advertise-auth.md delete mode 100644 quest/m3/processor/grant-lease.md delete mode 100644 quest/m3/processor/media-contract.md delete mode 100644 quest/m3/route-trust.md diff --git a/quest/README.md b/quest/README.md index 884c9a529a0..f1ed4436bfe 100644 --- a/quest/README.md +++ b/quest/README.md @@ -8,8 +8,7 @@ grouped into milestones ordered by priority. ## Plan m0 is everything in flight now: relay hardening and the last known IETF -interop fix ahead of Seattle, wildcard routing, broadcast epochs, and audio playout (jitter target -and quality harness). m1 is the next wave across reliability, features, performance, and +interop fix ahead of Seattle, wildcard routing, broadcast epochs, and audio playout (the jitter target). m1 is the next wave across reliability, features, performance, and planning. m2 holds later features, design studies, and experiments. m3 is gated on the outside world (hardware, a partner, a consumer, or a provider's offer) or is speculative work with no named consumer yet. m4 waits on an upstream release. diff --git a/quest/m0/wildcard/README.md b/quest/m0/wildcard/README.md index 70c62a8afb2..a901c85577a 100644 --- a/quest/m0/wildcard/README.md +++ b/quest/m0/wildcard/README.md @@ -128,10 +128,9 @@ path, not with a route identity or a generation field. advertised prefix MUST overlap the sender's granted patterns or it is refused. A prefix wider than the grant is accepted, but it only routes requests for paths the grant covers. Fleet-wide services use the cluster - identity; a customer service serves only what its own v1 grant contains. - Until [Advertise-only authorization](/quest/m3/processor/advertise-auth.md) - lands, the publish scope stands in for advertising; a credential with its own - advertise scope is checked against that instead. + identity; a customer service serves only what its own grant contains. + The publish scope authorizes advertising; there is no separate advertise + scope (dropped with the processor questline in the 2026-10-06 audit). - **Claims are visible to subscribers.** A claim is an ordinary prefix announcement, so it tells a client it may subscribe beneath it, and its withdrawal tells the client the capability is gone. The browser player's diff --git a/quest/m1/README.md b/quest/m1/README.md index 2d355bbc6d2..824249bd3d3 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -18,8 +18,7 @@ transport, benchmark tooling); worktrees isolate commits, not semantics. A line with no named consumer waits in m3 until one appears; the 2026-10-05 audit moved P2P, ladder, processor, and timed metadata there from m2. -Decided in the 2026-10-05 audit: the clock chain ranks first (import at the -first frame, the HLS first catalog, typed timedness, the untimed models, +Decided in the 2026-10-05 audit: the clock chain ranks first (the untimed models, CMAF frame timestamp, the shared clock, and the two publish-timestamp quests), with FFI shape pulled up inside it because publishing never invents a timestamp requires its JSON pilot. The hard fork follows, ahead of transport-seam and perf, and the rest @@ -47,9 +46,10 @@ blocks. The quests that gated m0 lines moved under them. - [main merges through a squash queue](/quest/m1/merge-queue-settings.md) - condition: once `release` carries the new back-merge script, the maintainer enables the squash merge queue and moq-bot's pull_request bypass together - [Binding audio delay](/quest/m1/binding-surface.md) - moq-ffi and every wrapper configure and observe audio playout delay - [moq-net owns its transport seam](/quest/m1/transport-seam.md) - moq-net names only its own transport traits, so a web-transport-trait or qmux major bump is a patch for it +- [Bump mp4-atom for avc3](/quest/m1/mp4-atom-avc3-bump.md) - this repository depends on mp4-atom 0.16.2, which reads and writes an `avc3` sample entry - [fMP4 init from the catalog](/quest/m1/fmp4-catalog-init.md) - fMP4 export writes avc3/hev1 entries from the catalog, so an Annex-B H.264 or H.265 init no longer waits for the first keyframe - [Delete the live marker](/quest/m1/announce-live-removal.md) - announce streams yield only route events; the `Live` marker and its hold machinery are gone -- [WebTransport close backends](/quest/m1/wt-close-backends.md) - web-transport-noq and -quinn keep the session alive while closing, and iroh's client reads a capsule close +- [WebTransport close backends](/quest/m1/wt-close-backends.md) - iroh's client reads a peer's capsule close, and moq-uring's HTTP/3 close task keeps the session alive until the capsule is sent - [Enabled flag](/quest/m1/catalog-enabled.md) - one `enabled` rendition flag replaces `stalled` and pausing, and the flapping encoder-lag detector is deleted - [Hang changelog](/quest/m1/hang-changelog-04.md) - the hang draft lists under -03 only what -03 published - [SUBSCRIBE_DROP](/quest/m1/subscribe-drop.md) - every stream group in a lite subscription arrives or is dropped by name, and lite-07 drops its stream count for it @@ -117,7 +117,6 @@ blocks. The quests that gated m0 lines moved under them. - [TS AAC PCE joins](/quest/m1/ts-aac-pce-join.md) - PCE-described AAC over TS plays after a mid-stream join or resume, and a missing PCE silences only its track - [FFI frame duration default](/quest/m1/ffi-frame-duration-default.md) - the binding audio encoder takes the codec's own frame by default, so `aac()` needs no explicit 0 - [mp4-atom dOps mapping](/quest/m1/mp4-atom-dops-mapping.md) - a released mp4-atom reads and writes any `dOps` channel mapping family and table -- [Bump mp4-atom for avc3](/quest/m1/mp4-atom-avc3-bump.md) - this repository depends on mp4-atom 0.16.2, which reads and writes an `avc3` sample entry - [CMAF surround Opus](/quest/m1/cmaf-opus-surround.md) - fMP4 import and export carry an Opus channel mapping table - [A self-hosted NVIDIA runner is registered](/quest/m1/gpu-runner.md) - the maintainer registers the host that runs the NVIDIA tests - [GPU CI](/quest/m1/gpu-ci.md) - NVIDIA tests run nightly on a self-hosted GPU runner, and `just rs nvidia` runs them locally instead of skipping @@ -129,7 +128,7 @@ blocks. The quests that gated m0 lines moved under them. - [io_uring flow control](/quest/m1/uring-flow-control-windows.md) - the relay's io_uring workers honor the QUIC flow-control windows instead of refusing them - [Own the QUIC stack](/quest/m1/quic/README.md) - quinn hard-forked in-tree as `moq-quic`, carrying BBR, reliable reset, hierarchical scheduling, peer limits, and endpoint sharding - [QoS](/quest/m1/qos/README.md) - broadcast health: relay starvation and timeliness histograms -- [Catalog track alias](/quest/m1/catalog-track-alias.md) - catalog rendition keys become aliases with an optional `track` name, so one catalog lists renditions from several broadcasts +- [Catalog rendition IDs](/quest/m1/catalog-track-alias.md) - catalog rendition keys become IDs unique across video and audio, with an optional `track` name, so one catalog lists renditions from several broadcasts - [Media stats](/quest/m1/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 - [JS track handover](/quest/m1/js-group-handover.md) - a JS track subscription resumes across a route swap from the first frame it lacks, so `test/drain` passes at zero latency budget - [JS GOAWAY requests](/quest/m1/js-goaway-requests.md) - after GOAWAY the JS client opens no new request on the old session, like Rust diff --git a/quest/m1/archive/README.md b/quest/m1/archive/README.md index 5dc959e1319..039eeac55bf 100644 --- a/quest/m1/archive/README.md +++ b/quest/m1/archive/README.md @@ -72,6 +72,47 @@ follows by listing each timeline's keys after its cursor; `Reader::finish` applies out-of-band finality. `rs/moq-archive/src/proof.rs` records one multi-rendition broadcast end to end. +### Decisions + +Moved from the deleted per-track timelines quest (2026-10-06 audit: its Rust +work landed in #4034 and its children are this line's own): + +- One timeline track per track, live and recorded. `moq-mux` publishes them for + every broadcast; an unsubscribed track costs nothing. +- The catalog's root `archive` entry maps each track to its timeline, including + the catalog track itself. `replay`, `store`, and `version` stay beside it. +- Each track cuts on its own by one rule: at a group boundary between a + minimum and maximum duration (a 2s minimum today; 4s and a multiple of the + declared duration once + [declared duration](/quest/m1/archive/declared-duration.md) lands; a zero + minimum only for sparse data such as the catalog), splitting a long-lived + group by frame at the maximum. Manual cuts stay as an optimization, such as + a video keyframe cutting audio so derived segments need fewer objects. +- A stored object may hold a frame range of a group, not only whole groups. +- HLS and DASH segments are derived at the edge from group timestamps, not + from storage objects. Fetching extra objects is fine when they land in the + reader's cache for the next request. +- `moq-hls` never retries a failed timeline, because the origin already reconnects + transient source failures. A replacement publisher that restarts group + numbering is a publisher bug: a broadcast, track, or group name always means + the same content, and MoQ has no ETag-style invalidation + ([#4556](https://github.com/moq-dev/moq/pull/4556)). A failed timeline + fails loud instead: the reference ends every playlist with `EXT-X-ENDLIST`, + and another rendition's playlist ends at its last covered segment rather than + listing gaps. +- HLS `EXT-X-TARGETDURATION` is fixed for the run, not the observed maximum + #4280 shipped. This reverses that PR's decision 4 (09-28 merged-PR audit). + The target comes from each timeline's declared duration, which replaces the + broadcast-wide `durationMax` (09-29 planning). + +For triage, not blocking: two Codex P2s arrived after #4280 merged and are +unanswered. A timeline record whose `sequence` differs from its window index +is passed through unvalidated +([review](https://github.com/moq-dev/moq/pull/4280#pullrequestreview-5332725290)), +and `Timelines::track` re-enrolling a name while its old `Recorder` is alive +leaves two handles on one segmenter +([r4117516035](https://github.com/moq-dev/moq/pull/4280#discussion_r4117516035)). + ### Format The format is the draft's @@ -118,8 +159,7 @@ owned by that prerequisite, not duplicated in archive storage. - [Timelines declare their segment duration](/quest/m1/archive/declared-duration.md) - each timeline entry declares its segment duration (reported or estimated by the publisher), replacing the root `durationMax` - [JS per-track timelines](/quest/m1/archive/js-timelines.md) - `@moq/hang` publishes the same per-track timelines as Rust (it already reads them) - [Fixed HLS target duration](/quest/m1/archive/hls-target.md) - one `EXT-X-TARGETDURATION` for the run, from the reference timeline's declared duration; an overrun is listed with a warning -- [Per-track timelines](/quest/m1/archive/track-timeline.md) - every track segments and expires on its own timeline, and HLS is derived from group timestamps at the edge -- [Replay catalog](/quest/m1/archive/replay-catalog.md) - `moq import archive` publishes the recorded catalog live with `store` set, so stock `moq export hls` serves the whole replay +- [Replay catalog](/quest/m1/archive/replay-catalog.md) - `moq_archive::Reader` republishes the recorded catalog live with `store` set, so stock `moq export hls` serves the whole replay of `moq import archive` - [Idle flush](/quest/m1/archive/flush.md) - idle tracks are recorded within a bounded wall-clock delay, and `flush()` forces a track's pending record out - [Backward timestamps](/quest/m1/archive/backward-timestamps.md) - a resumed recording refuses a restarted source instead of dropping its groups or overlapping media time - [HLS media sequence](/quest/m1/archive/hls-media-sequence.md) - `EXT-X-MEDIA-SEQUENCE` never decreases when the reference rendition switches diff --git a/quest/m1/archive/backward-timestamps.md b/quest/m1/archive/backward-timestamps.md index 348a4fa9a45..bd9e573c2f3 100644 --- a/quest/m1/archive/backward-timestamps.md +++ b/quest/m1/archive/backward-timestamps.md @@ -20,7 +20,3 @@ The 2026-09-30 audit folded this into the recording writer, which landed without it; the [#4034 review](https://github.com/moq-dev/moq/pull/4034#issuecomment-6005694031) found the silent group drop. The archive format may break in place since it is unreleased. - -## Related - -- [Per-track timelines](/quest/m1/archive/track-timeline.md) - reshapes the recovery this check reads diff --git a/quest/m1/archive/dvr.md b/quest/m1/archive/dvr.md index ef9003c0456..3dcdfc42cba 100644 --- a/quest/m1/archive/dvr.md +++ b/quest/m1/archive/dvr.md @@ -37,8 +37,8 @@ playlists keep serving for rewind and for players finishing the last segments. The fall-through needs the recording to publish its catalog live, since `moq-hls` subscribes to it rather than FETCHing it. Decided (09-29): that republishing moves into `moq-archive`, so any host of the archive -behind the root claim does it, not only `moq-cli`; update [Replay -catalog](/quest/m1/archive/replay-catalog.md) to match. +behind the root claim does it, not only `moq-cli`, as [Replay +catalog](/quest/m1/archive/replay-catalog.md) now plans. During live, rewind needs no handover: a recorded broadcast's live timeline is durable, and every group it lists is promised available, so a seek past @@ -51,10 +51,12 @@ numbers when the name moves from the live publisher to the archive. ## Required -- [Per-track timelines](/quest/m1/archive/track-timeline.md) - seeks through per-track timelines - [Replay catalog](/quest/m1/archive/replay-catalog.md) - the recording publishes its catalog live, so `moq-hls` finds it after the handover -- [Wildcard](/quest/m0/wildcard/README.md) - the archive's root claim serves the source path once the live announcement ends ## Closes - [#2275](https://github.com/moq-dev/moq/issues/2275) - close this issue when the quest finishes + +## Related + +- [Wildcard](/quest/m0/wildcard/README.md) - landed (#4403) the root claim that serves the source path once the live announcement ends diff --git a/quest/m1/archive/flush.md b/quest/m1/archive/flush.md index 895bf4283a2..7c795d91f48 100644 --- a/quest/m1/archive/flush.md +++ b/quest/m1/archive/flush.md @@ -23,8 +23,8 @@ Decided: - The segmenter stays pure: it takes `now` as an input and exposes the next wall-clock deadline at which a record must close. Callers arm the timer. The moq-archive writer uses tokio time; moq-mux's `Recorder` must stay wasm-safe - (moq-mux builds for wasm32), so it uses moq-net's runtime `Timer` or - `web_async` time, never `tokio::time`. + (moq-mux builds for wasm32), so it uses `web_async` time, never + `tokio::time` (moq-net's runtime `Timer` went away in #4437). - Manual flush is per track: `Control::flush(name)` on the writer, beside the broadcast-wide `cut`, and `Recorder::flush()` on a timeline. - JS mirrors it: `@moq/hang`'s `Timeline.Recorder` gets the same idle @@ -39,4 +39,4 @@ Decided: ## Required -- [Per-track timelines](/quest/m1/archive/track-timeline.md) - the per-track segmenter, writer, and JS recorder this extends, so the flush lands on the per-track shape rather than the aligned one it replaces +- [JS per-track timelines](/quest/m1/archive/js-timelines.md) - the JS `Timeline.Recorder` this extends; the Rust per-track segmenter and writer are already on `main` (#4034) diff --git a/quest/m1/archive/hls-target.md b/quest/m1/archive/hls-target.md index 5c76ce24672..bfb9423b1a4 100644 --- a/quest/m1/archive/hls-target.md +++ b/quest/m1/archive/hls-target.md @@ -46,7 +46,3 @@ keeps the original target. ## Required - [Timelines declare their segment duration](/quest/m1/archive/declared-duration.md) - the declared value the target is taken from - -## Related - -- [Per-track timelines](/quest/m1/archive/track-timeline.md) - the quest this blocks diff --git a/quest/m1/archive/replay-catalog.md b/quest/m1/archive/replay-catalog.md index 3435387cbf2..51faba1ca7c 100644 --- a/quest/m1/archive/replay-catalog.md +++ b/quest/m1/archive/replay-catalog.md @@ -26,9 +26,12 @@ this by hand-building a catalog. recording format. Refuse a URL carrying userinfo and strip its query so credentials never land in a catalog, and let the importer opt out of advertising the URL at all. `replay` stays unset: the timelines live on this broadcast. -- Keep the logic in `moq-cli`; `moq-archive` stays catalog-agnostic. Select the - catalog track with the CLI's catalog format as `export archive` does: hang - and hang.z are stamped, MSF is refused. +- The logic lives in `moq-archive` (`moq_archive::Reader`), so any host of + the archive behind the root claim republishes and stamps the catalog, not + only `moq-cli` (decided 09-29 in [DVR rewind](/quest/m1/archive/dvr.md)). + `moq-cli` only passes the URL and the opt-out. Select the catalog track by + catalog format as `export archive` does: hang and hang.z are stamped, MSF + is refused. - The recorded catalog's `archive` entry describes the source's live timelines, not the recording's, so replace it with the timelines the reader replays rather than trusting the recorded ones. diff --git a/quest/m1/archive/track-timeline.md b/quest/m1/archive/track-timeline.md deleted file mode 100644 index 937cad2cced..00000000000 --- a/quest/m1/archive/track-timeline.md +++ /dev/null @@ -1,71 +0,0 @@ -# [S] Per-track timelines - -## Goal - -Every track carries its own timeline, so tracks segment, commit, and expire -independently. A DVR keeps each track's newest group, such as a catalog that -never changes, and an append-only group stays addressable as frames arrive. An -edge like `moq-hls` derives HLS and DASH from group timestamps, so a publisher -never needs to know about HLS. - -Nothing on `main` has users yet: break the timeline, catalog `archive` entry, -and recording format in place, with no compatibility path, even though hang -0.21 released the current shape. The Rust side lands on `main` with the -[archive line](/quest/m1/archive/README.md) (#4034); the children follow as -their own PRs to `main`. - -## Plan - -The Rust side has landed: a `hang::timeline::Record` is one span of its own -track (`sequence`, `pts`, `duration`, `start`/`end` group and frame positions), -`moq_mux::timeline::Timelines` publishes one timeline per enrolled track, -`moq-archive` writes recording version 2 (`/segments/` beside its -timeline's `segments/`), and `moq-hls` derives segments from a reference -rendition's records (`rs/moq-hls/src/export/spans.rs`). The draft is updated -in moq-hang-04. - -Decisions: - -- One timeline track per track, live and recorded. `moq-mux` publishes them for - every broadcast; an unsubscribed track costs nothing. -- The catalog's root `archive` entry maps each track to its timeline, including - the catalog track itself. `replay`, `store`, and `version` stay beside it. -- Each track cuts on its own by one rule: at a group boundary between a - minimum and maximum duration (a 2s minimum today; 4s and a multiple of the declared - duration once its declared-duration child lands; a zero minimum only for sparse data such as the catalog), - splitting a long-lived group by frame at the maximum. Manual cuts stay as an optimization, such as a - video keyframe cutting audio so derived segments need fewer objects. -- A stored object may hold a frame range of a group, not only whole groups. -- HLS and DASH segments are derived at the edge from group timestamps, not - from storage objects. Fetching extra objects is fine when they land in the - reader's cache for the next request. -- `moq-hls` never retries a failed timeline, because the origin already reconnects - transient source failures. A replacement publisher that restarts group - numbering is a publisher bug: a broadcast, track, or group name always means - the same content, and MoQ has no ETag-style invalidation - ([#4556](https://github.com/moq-dev/moq/pull/4556)). A failed timeline - fails loud instead: the reference ends every playlist with `EXT-X-ENDLIST`, - and another rendition's playlist ends at its last covered segment rather than - listing gaps. -- HLS `EXT-X-TARGETDURATION` is fixed for the run, not the observed maximum - #4280 shipped. This reverses that PR's decision 4 (09-28 merged-PR audit). - The target comes from each timeline's declared duration, which replaces the - broadcast-wide `durationMax` (09-29 planning). - -For triage, not blocking: two Codex P2s arrived after #4280 merged and are -unanswered. A timeline record whose `sequence` differs from its window index -is passed through unvalidated -([review](https://github.com/moq-dev/moq/pull/4280#pullrequestreview-5332725290)), -and `Timelines::track` re-enrolling a name while its old `Recorder` is alive -leaves two handles on one segmenter -([r4117516035](https://github.com/moq-dev/moq/pull/4280#discussion_r4117516035)). - -## Required - -- [Timelines declare their segment duration](/quest/m1/archive/declared-duration.md) - each timeline entry declares its segment duration (reported or estimated by the publisher), replacing the root `durationMax` -- [JS per-track timelines](/quest/m1/archive/js-timelines.md) - `@moq/hang` publishes the same per-track timelines as Rust (it already reads them) -- [Fixed HLS target duration](/quest/m1/archive/hls-target.md) - one `EXT-X-TARGETDURATION` for the run, from the reference timeline's declared duration; an overrun is listed with a warning - -## Related - -- [Catalog track identity](/quest/m2/catalog-tracks.md) - the catalog's own timeline gives it timestamps, but which catalog applies to a group stays there diff --git a/quest/m1/audio-quality-native.md b/quest/m1/audio-quality-native.md index 800c599fe9a..d45f65276be 100644 --- a/quest/m1/audio-quality-native.md +++ b/quest/m1/audio-quality-native.md @@ -34,15 +34,20 @@ from whichever platform a user happened to be on. they do not get their own schema. - Add the lane to the nightly matrix beside the browser one. -Nothing here is a second estimator. Compare the two runtimes at the estimator -first: the target series each produces from the same arrival trace, which is -the conformance corpus's own comparison and carries no device timing in it. A -disagreement there is a finding against [Audio jitter -target](/quest/m0/audio-jitter-target/README.md) and never a reason to widen a -budget. Only then compare end-to-end totals, with the backend-dependent stages -isolated: this lane deliberately accepts real device callback noise, so a -difference in totals alone proves nothing about the estimator. +Nothing here is a second estimator, and this lane does not compare the two +runtimes' target series: the [Audio jitter +target](/quest/m0/audio-jitter-target/README.md) line replays its recorded +trace through the native decode path and asserts the browser's target series +(2026-10-06 audit). This lane relies on that result; a disagreement there is +a finding against that line and never a reason to widen a budget. It grades +only end-to-end budgets, with the backend-dependent stages isolated: this +lane deliberately accepts real device callback noise, so a difference in +totals alone proves nothing about the estimator. Standalone in m1 rather than a child of the m0 audio quality harness line (decided in the 2026-09-28 quest audit): nothing in m0 waits on it. The native jitter target it grades is done on the jitter target line. + +## Related + +- [Audio jitter target](/quest/m0/audio-jitter-target/README.md) - its native trace replay is the estimator comparison this lane relies on diff --git a/quest/m1/auth/README.md b/quest/m1/auth/README.md index 26822c7824e..d81105229d8 100644 --- a/quest/m1/auth/README.md +++ b/quest/m1/auth/README.md @@ -16,9 +16,8 @@ This questline adds an AUTH exchange to both wires: one stream per token, a grant per token, the union of every accepted token as the session's scope, and a loud failure when a publish can never be honored. It ends with the credential able to travel in band, while the URL keeps working for every peer -that predates the stream. Direct peer sessions need a second credential: a -relay-signed, hop-bound grant that a browser can verify without a signing -key, which HMAC relay keys cannot provide. +that predates the stream. Hop-bound peer grants for direct sessions belong to +[P2P](/quest/m3/p2p/peer-grant.md), their only consumer. ## Plan @@ -39,17 +38,18 @@ Decisions settled while planning, recorded so review does not relitigate them: the union and cancels publications and subscriptions that lose authorization. Other authorized work continues on the same session. An empty union leaves the session connected with no access, so it can accept a fresh token. - [Origin narrowing](/quest/m1/auth/narrowing.md) owns the common resize - operation; relay token handling requires it rather than shipping a temporary - close-on-shrink policy. + `auth::Handle::authorize` is the common resize operation; relay token handling + uses it rather than shipping a temporary close-on-shrink policy. - **A public grant contains publish patterns, subscribe patterns, and an expiry**, in the presenter's own root; the presenter never sees the relay-side root, and every token in a union shares the connection's root. Unscoped - permission is `**`; an empty union grants nothing. Legacy AUTH wire codecs - explicitly convert representable prefix unions, where `[""]` means all, - and refuse patterns they cannot represent. Pattern interest (#4277, on this - line) upgrades AUTH and ANNOUNCE_REQUEST wire fields together without changing - the public pattern-valued grant type. + permission is `**`; an empty union grants nothing. Lite AUTH_OK carries + those patterns, wildcards and literals alike, from the first release, with + no covering-prefix workaround. IETF AUTH_OK carries Track Namespace + prefixes, so an acceptor whose grant is not a union of subtrees answers + AUTH_ERROR NOT_SUPPORTED rather than widening it. Announce stays a prefix: + ANNOUNCE_REQUEST and SUBSCRIBE_NAMESPACE do not gain patterns. The public + grant type stays pattern-valued. - **Fail loud by aborting the session.** A publisher whose origin announces a broadcast outside the union aborts the session with `Unauthorized`, naming the path. The check runs against the grants in hand once the tokens the @@ -73,7 +73,9 @@ Decisions settled while planning, recorded so review does not relitigate them: must explicitly grant `**` for unrestricted access; AUTH does not widen a scoped grant because the caller is another relay. - **Client API.** Tokens live on `moq_tokio::connect::Config`, the - dial-side config, and `Connection` exposes the live session's auth handle. + dial-side config. `Connection::auth()` is a handle the connection owns: it + keeps every added token, presents them on each session as it reconnects, + and reports the live session's grant. - **Spec home.** The AUTH stream is core in the wip lite version (`moq-lite-07-wip` today) in `drafts/draft-lcurley-moq-lite.md`, the way routing is. moq-transport gets @@ -93,28 +95,18 @@ published version in place, so AUTH and its stream code land in ## Required - [WebSocket refusal](/quest/m1/auth/ws-unauthorized.md) - the relay serves WebSocket through moq-tokio, so a refused token closes the session as Unauthorized, as QUIC does -- [Lite stream](/quest/m1/auth/lite.md) - both sides of a wip-version lite session - exchange grants over AUTH streams, exposed as `Session::auth()`, and an - out-of-scope announce aborts the session -- [Interop grants](/quest/m1/auth/interop.md) - the interop matrix asserts - each AUTH-capable cell's grant and that a publish outside it fails loud -- [Unauthorized reset](/quest/m1/auth/unauthorized.md) - a subscription that - loses access resets with a dedicated UNAUTHORIZED stream code -- [AUTH_OK preflight](/quest/m1/auth/auth-ok-preflight.md) - an unencodable IETF grant answers NOT_SUPPORTED with nothing written, as JS already does -- [AUTH endings](/quest/m1/auth/error-codes.md) - an out-of-range AUTH_ERROR code is refused, and both sides settle and recompute grants when a stream ends +- [AUTH on the wip version](/quest/m1/auth/wip-version.md) - lite AUTH and UNAUTHORIZED move from lite-06 to `moq-lite-07-wip`, so no published version changes in place +- [Lite NOT_SUPPORTED](/quest/m1/auth/not-supported.md) - a lite acceptor answers AUTH_ERROR NOT_SUPPORTED after a grant too, with a lite session code for `Error::Unsupported` +- [AUTH violations](/quest/m1/auth/violations.md) - every AUTH protocol violation closes the session in Rust and JS, lite and IETF - [Malformed grant](/quest/m1/auth/malformed-grant.md) - a malformed or non-canonical grant pattern, or an out-of-range `Expires`, closes the session with PROTOCOL_VIOLATION in Rust and JS -- [Origin narrowing](/quest/m1/auth/narrowing.md) - a live grant narrows in - place: subscriptions outside it reset, publishes outside it abort, and relay - revalidation stops closing the session +- [JS fetch grant watch](/quest/m1/auth/js-fetch-watch.md) - a JS `fetchGroup` ends `Unauthorized` when its path leaves the grant - [Relay tokens](/quest/m1/auth/relay-refresh.md) - the relay verifies tokens sent in band, unions their grants, and cancels only work that loses access - [Request tokens](/quest/m1/auth/request-token.md) - an `AUTHORIZATION TOKEN` on a moq-transport request authorizes that request when the session grant does not, and REQUEST_UPDATE refreshes it -- [moq-transport](/quest/m1/auth/moq-transport.md) - the same exchange as a - setup-option extension on draft-17+, specified in a new draft - [Expired token error](/quest/m1/auth/expired-error.md) - an expired token reports `Error::Expired`, not `Unauthorized`, in Rust, JS, and the bindings - [Bindings](/quest/m1/auth/bindings.md) - grants and tokens reach every @@ -122,11 +114,8 @@ published version in place, so AUTH and its stream code land in - [Token in band](/quest/m1/auth/token-in-band.md) - the credential can leave the URL: a session starts on what the URL carried and its AUTH streams add the rest, with the URL kept for peers without the AUTH stream -- [Peer grants](/quest/m1/auth/peer-grant.md) - the relay issues a hop-bound, - asymmetrically signed grant a browser can verify; HS256 keys issue none ## Related -- [Expiring media grants](/quest/m3/processor/grant-lease.md) - a worker's - lease renewal is a new in-band token -- [P2P](/quest/m3/p2p/README.md) - the first consumer of hop-bound peer grants +- [Peer grants](/quest/m3/p2p/peer-grant.md) - P2P's hop-bound credential, + built on this line's relay tokens diff --git a/quest/m1/auth/auth-ok-preflight.md b/quest/m1/auth/auth-ok-preflight.md deleted file mode 100644 index eecead51acf..00000000000 --- a/quest/m1/auth/auth-ok-preflight.md +++ /dev/null @@ -1,24 +0,0 @@ -# [XS] An AUTH_OK that cannot be sent is refused, not half-written - -## Goal - -When the Rust IETF acceptor's grant cannot be encoded as one AUTH_OK for any -reason, it answers `AUTH_ERROR { NOT_SUPPORTED }` and nothing of the AUTH_OK -reaches the wire, the same as JS. Today the preflight in -`rs/moq-net/src/ietf/auth.rs` (`serve_issue`) catches only -`EncodeError::Unsupported`, so a prefix grant too large for the `u16` message -size falls through to `encode_message`, which fails mid-write and ends the -stream with a transport error instead of a refusal the presenter can read. -Found in review of [#4124](https://github.com/moq-dev/moq/pull/4124). - -## Plan - -- Refuse on any sizing error, not only `Unsupported`, including the message - size ceiling the writer enforces. Never trim or widen the grant to make it - fit: withholding it is the only safe answer. -- Check whether the lite AUTH_OK path has the same gap and fix both if so. -- Regression test mirroring JS's "a grant too large for one message is - refused before anything is written": the presenter sees `Unsupported` and - no AUTH_OK bytes were written. - -Public API: none. Wire: none. diff --git a/quest/m1/auth/bindings.md b/quest/m1/auth/bindings.md index c46c2540c8b..f582e890303 100644 --- a/quest/m1/auth/bindings.md +++ b/quest/m1/auth/bindings.md @@ -24,9 +24,9 @@ one without the plugin reconnecting. generated C and C++ bindings get this from moq-ffi, and OBS reaches it through [C++ through moq-ffi](/quest/m1/cpp/README.md); update `cpp/obs/src` only if the plugin surfaces a token field. -- Interop: the Python, Go, and C++ interop clients print their grant and join - the assertion [Interop grants](/quest/m1/auth/interop.md) adds for Rust - and JS. +- Interop: the Python, Go, and C++ interop clients print their grant as the + `auth granted publish=[...] subscribe=[...]` line and join `prints_grant` and + `enforces_grant` in `test/interop/interop.sh`, beside Rust and JS. - Wrappers: `py/moq-rs/moq/session.py`, `swift/Sources/Moq`, `kt/.../Flows.kt` (a `Flow` over `grant_changed`), `go/wrapper/moq/session.go` (context-cancellable like the rest), and `dart/moq/lib/moq.dart`. Kotlin @@ -43,6 +43,6 @@ surface comes from moq-ffi rather than new `moq_session_auth_*` calls. ## Required -- [Lite stream](/quest/m1/auth/lite.md) - supplies `Session::auth()` +- [FFI shape](/quest/m1/ffi-shape/README.md) - reshapes the moq-ffi session and wrappers this edits, so the wrappers break once - [Relay tokens](/quest/m1/auth/relay-refresh.md) - supplies the connection accessor and a relay that answers a real token, which the wrapper tests need diff --git a/quest/m1/auth/error-codes.md b/quest/m1/auth/error-codes.md deleted file mode 100644 index 76dfcbe1f5f..00000000000 --- a/quest/m1/auth/error-codes.md +++ /dev/null @@ -1,36 +0,0 @@ -# [S] AUTH endings are exact on both sides - -## Goal - -The loose ends Codex left on [#4062](https://github.com/moq-dev/moq/pull/4062) -are closed, so every way an AUTH stream ends reports what actually happened: - -- A lite AUTH_ERROR whose code does not fit a `u32` is a protocol violation. - Today `rs/moq-net/src/lite/session.rs` saturates it with - `u32::try_from(refused.code).unwrap_or(u32::MAX)`, so distinct peer codes - collapse into one and the app sees a code the peer never sent. -- JS `AuthSession.close()` in `js/net/src/auth_session.ts` clears its tokens - but never recomputes the union, so a retained `auth.grant` keeps reporting - a live grant after the session closed. Rust already clears it. -- A JS presenter whose acceptor ends the grant with a clean FIN exits the read - loop without closing its own write half, unlike the AUTH_ERROR branch, so - the acceptor's `Issued.closed` stays pending until the session ends. -- A Rust `Issued::closed()` never resolves if the session drops the task - serving that token (`AuthServe` in the lite publisher, `Serve` in - `ietf/auth.rs`): only the task's own completion records `issue.peer`. - -## Plan - -- Fail loud on the unrepresentable code rather than widen the error type: - the codes AUTH_ERROR carries are session codes, which are `u32` everywhere - else. -- Settle a dropped serve task from a drop path (a guard that records the - session's error on `Issue` and wakes waiters), so no exit path can forget - it. -- One regression test per item, each failing without its fix. - -The fifth deferred item, a grant recheck after async origin resolution, -belongs to [Origin narrowing](/quest/m1/auth/narrowing.md) with the other -watcher races. - -Public API: none. Wire: none. diff --git a/quest/m1/auth/expired-error.md b/quest/m1/auth/expired-error.md index ea31fd167db..e2b322f743f 100644 --- a/quest/m1/auth/expired-error.md +++ b/quest/m1/auth/expired-error.md @@ -12,6 +12,10 @@ refusal as final. - Add the variant to the `#[non_exhaustive]` error, so the change is additive on `main`. Map it from lite's `AUTH_ERROR { Expired }` and moq-transport's `EXPIRED_AUTH_TOKEN`, and back again when refusing. +- Lite session code 0x30 is NOT_SUPPORTED + ([Lite NOT_SUPPORTED](/quest/m1/auth/not-supported.md)). `Expired` takes a + shared moq-transport session value if one means the same thing, otherwise + 0x31. - Carry it through moq-ffi's error mapping and each wrapper. Moved from m2 into the auth line: relay tokens refuse an expired @@ -21,5 +25,3 @@ apart from `Unauthorized`. ## Required - [WebSocket refusal](/quest/m1/auth/ws-unauthorized.md) - both transports refuse a token at the session level -- [Lite stream](/quest/m1/auth/lite.md) - the lite AUTH streams that carry `Expired` -- [moq-transport](/quest/m1/auth/moq-transport.md) - the extension that carries `EXPIRED_AUTH_TOKEN` diff --git a/quest/m1/auth/interop.md b/quest/m1/auth/interop.md deleted file mode 100644 index 216be0e7890..00000000000 --- a/quest/m1/auth/interop.md +++ /dev/null @@ -1,24 +0,0 @@ -# [S] Interop asserts the AUTH grant - -## Goal - -`just test interop --all` fails an AUTH-capable (wip lite) cell whose client -did not receive the grant its relay token implies, and a new negative round passes only when -a client publishing outside its grant fails loud with `Unauthorized`. Today -the matrix checks media alone, so a malformed AUTH_OK that leaves a session -with no grant passes silently. - -## Plan - -The Rust and JS interop clients each print the grant they received in one -parseable line; `test/interop/interop.sh` -compares it against the token the cell minted. The negative round mints a -token that excludes the published path and expects the publisher's session -to close with `Unauthorized` naming the path, and the subscriber to see -nothing. Cells on versions without the AUTH stream skip both checks, as do -binding clients until [Bindings](/quest/m1/auth/bindings.md) gives them a grant to print; that -quest adds them to the same assertion. No public API or wire change. - -## Required - -- [Lite stream](/quest/m1/auth/lite.md) - the AUTH exchange being asserted diff --git a/quest/m1/auth/js-fetch-watch.md b/quest/m1/auth/js-fetch-watch.md new file mode 100644 index 00000000000..68015c86b54 --- /dev/null +++ b/quest/m1/auth/js-fetch-watch.md @@ -0,0 +1,14 @@ +# [XS] JS fetchGroup stops when its grant is revoked + +## Goal + +A `@moq/net` subscriber-side `fetchGroup` holds a grant watch from its first +check to its end, as subscriptions do, so a fetch whose path leaves the +session's grant ends with `Unauthorized` instead of finishing. + +## Plan + +Found by #4560, which gave +every other JS request a watch armed before its first await. Follow the same +pattern, and add a regression test where the grant changes during setup and +mid-fetch. diff --git a/quest/m1/auth/lite.md b/quest/m1/auth/lite.md deleted file mode 100644 index a2d4c08be95..00000000000 --- a/quest/m1/auth/lite.md +++ /dev/null @@ -1,147 +0,0 @@ -# [L] Lite stream - -## Goal - -Both sides of a session on the wip lite version (`moq-lite-07-wip` today) -learn what they may publish and subscribe -to. Right after SETUP each side opens an AUTH stream carrying an empty token -and receives its grant; `Session::auth()` exposes the union of every grant and -lets the app present more tokens, each on its own stream. A publisher whose -origin announces a broadcast outside that union aborts the session with -`Unauthorized` naming the path instead of waiting for a solicitation that -never comes. Rust and JavaScript both speak it, and the lite draft specifies -it. - -## Plan - -Use pattern unions in public grants from their first introduction, matching -dev origin scopes. Legacy AUTH wire codecs convert only representable prefix -unions explicitly and refuse every unrepresentable grant. Never widen an -exact or suffix pattern into a prefix. The pattern-interest quest changes wire -encoding without another public grant-type migration. - -### Wire - -Add stream type `0x7` AUTH, creator either, to the bidirectional stream table -in `drafts/draft-lcurley-moq-lite.md` and a section beside Probe modeled on -it. One stream carries one token for the life of that token. Messages, -encoded like PROBE with the `(i)`, `(b)`, `(s)` notation the draft uses: - -- `AUTH { Token (b) }`, written once by the opener as the stream's first - message. An empty token means the credential the connection already - presented, or nothing. -- `AUTH_OK { Publish Count (i), Publish Prefix (s)..., Subscribe Count (i), - Subscribe Prefix (s)..., Expires (i) }`, written by the acceptor. The - prefixes use the encoding and rules ANNOUNCE_REQUEST's prefix uses: a list - holding the empty prefix grants everything, an empty list grants nothing. - Expires is milliseconds until the grant lapses, zero for never. The acceptor - MAY write further AUTH_OK messages on the same stream to update that - token's grant, such as a lowered expiry; the latest one is the token's grant. -- `AUTH_ERROR { Code (i), Reason (s) }`, written by the acceptor. As the - first reply it refuses the token; after an AUTH_OK it revokes it. Either - way the acceptor then closes the stream. - -A session's scope is the union of the grants of its open AUTH streams. The -opener withdraws a token by closing or resetting its stream. A stream reset -before any reply, which is what a peer does with a stream type it does not -understand (the existing rule at the STREAM_TYPE section), leaves the opener -with no grant for that token, and the client reports it as `Unsupported` -rather than a refusal. Record it in the wip version's changelog. Run -`just drafts check`. - -### Model - -New module `rs/moq-net/src/auth.rs`, public as `moq_net::auth`: - -- `auth::Grant { publish: Patterns, subscribe: Patterns, expires: - Option }`, a plain value in the presenter's root. -- `auth::Handle`, cloneable, returned by `Session::auth()`. `grant()` is a - watchable union of every open token's grant (`None` until the first - AUTH_OK; the `kio` shared cell the session already uses for the peer SETUP - is the pattern). `add(token) -> Result` opens a stream - and resolves with the first reply. `auth::Token` exposes that token's own - watchable `grant()` and `closed()`, and dropping it closes the stream, - withdrawing the token. `requests()` yields `auth::Request { token, - accept(Grant) -> auth::Issued, reject(code, reason) }` for the acceptor - side, where `auth::Issued` can `update(Grant)` and `revoke(code, reason)` - and its drop ends the stream; a request left unanswered is a bug, so - dropping one rejects with `Unauthorized`. -- The default acceptor needs no app code, and whether it is in charge is - decided once, before the driver starts, never per message: the server-side - `moq_net::Request` builder exposes `auth()` before `.ok()`, and a caller - that takes `requests()` there owns every AUTH the session receives, the - initial empty one included. With no such consumer, the driver answers the - empty token from the session's own origin handles, publish from the - subscriber half's `origin::Producer::allowed()` and subscribe from the - publisher half's `origin::Consumer::allowed()`, with no expiry. A missing - half grants nothing. That is what makes the cluster case free: both relays - present mTLS, both learn the unrestricted grant. A non-empty token with no - consumer is rejected `Unsupported`; the relay is the consumer in - [Relay tokens](/quest/m1/auth/relay-refresh.md). - -`Session` is protocol-agnostic (`rs/moq-net/src/session.rs` holds a version, -bandwidth handles, and a boxed driver), so `lite::start` builds the handle and -hands it to `Session::new`; `ietf::start` hands one whose `grant()` stays -`None` and whose `add` fails `Unsupported` until -[moq-transport](/quest/m1/auth/moq-transport.md) fills it in. - -### Session - -Accepted bidi streams are dispatched by the publisher half alone -(`Publisher::handle` in `rs/moq-net/src/lite/publisher.rs`), which holds only -the publish-side `origin::Consumer`; the grant needs both halves' `allowed()`, -so the handle is built in `lite::start` with both handles and shared into the -publisher and subscriber, the way the peer-setup slot is. Opening: the -subscriber half opens the empty-token AUTH stream where it opens Probe, after -the peer SETUP resolves, and `add` opens further ones on demand. Accepting: a -new `recv_auth` beside `recv_probe` runs one stream for the life of its -token, on error aborting only that stream. - -Fail loud: the publisher half already scopes its origin per solicited prefix; -add a check that every path its origin announces is covered by the union's -publish prefixes at initial admission and when a new publication is -attempted. A later grant shrink withdraws previously authorized publications -and cancels subscriptions that lose access on this session, without aborting -the session or mutating the shared origin. Process this resize before -checking new attempts, so a revocation is not mistaken for a new unauthorized -publication. It waits only for the tokens the session presented itself at setup -(the empty one, and the configured ones once -[Token in band](/quest/m1/auth/token-in-band.md) lands), so a peer that never -answers an app-added token cannot suspend enforcement; an app awaits `add` -before publishing what that token unlocks. On a miss, abort the session with -`Error::Unauthorized` and put the path in the close reason (`lite::start`'s -teardown already formats one; log it at error level too, since `Error` -carries no payload). - -Gate everything on the wip version (`Version::Lite07` while lite-07 is wip); -published versions never open the stream and `auth().grant()` stays `None` -there. Decided 2026-10-05: wire work targets the wip release, never a -published version in place. - -### JavaScript - -Mirror in `js/net/src/lite/`: `stream.ts` gains `Auth: 7`, an `auth.ts` with -the three messages, `connection.ts` opens and answers the streams, and -`Established` in `js/net/src/connection/established.ts` gains `auth` with -`grant: Getter` (a `@moq/signals` signal like `probe`), -`add(token): Promise`, and `requests()`. The publisher in -`js/net/src/lite/publisher.ts` aborts the connection the same way. - -### Docs and tests - -Update `doc/concept` where sessions and authorization are described, and the -moq-net guide if it lists stream types. Tests: encode and decode round trips -and the version gate; both sides receive a grant from scoped origins, -including the empty-prefix and empty-list spellings; a publish-only session -grants no subscribe; an out-of-scope announce aborts with `Unauthorized` and -names the path in the reason, while an in-scope one does not; two tokens union -and closing one stream shrinks the union on both sides without disconnecting -or aborting work still covered; revoked publications are withdrawn even if -they remain in the shared origin; an empty union can later be authorized -again; an update AUTH_OK -replaces one token's grant without touching the other; an unanswered -app-added token does not suspend the check; a reset AUTH stream reports -`Unsupported`; Rust to JS and JS to Rust interop in the existing -cross-language harness. - -On main, additive. diff --git a/quest/m1/auth/moq-transport.md b/quest/m1/auth/moq-transport.md deleted file mode 100644 index d6d7c567241..00000000000 --- a/quest/m1/auth/moq-transport.md +++ /dev/null @@ -1,106 +0,0 @@ -# [M] moq-transport - -## Goal - -The grant exchange works on a moq-transport session between two moq-net -peers, with the same grant lifecycle through `Session::auth()`. This initial -IETF encoding supports prefix-representable grants and explicitly refuses -other pattern unions. An IETF publisher fails loud on an out-of-scope -PUBLISH_NAMESPACE before sending it. A -new `drafts/draft-lcurley-moq-auth.md` specifies it as an extension a -conforming peer can ignore. - -## Plan - -### Draft - -`drafts/draft-lcurley-moq-auth.md`, modeled on `draft-lcurley-moq-solicit.md` -for the setup option and on `draft-lcurley-moq-cluster.md` for the IANA -tables. It declares: - -- Setup Option `AUTH`, an even key in the `0x40B5x` series the cluster and - solicit options use, value `1`. Both endpoints send it; the extension is - negotiated only when both did, per the moqt extension rule that the set is - fixed once both SETUPs are seen. Draft-17 and later only, since that is the - first unified SETUP, the same gate `cluster::supported` applies. -- One AUTH request stream per token, living as long as the token, the way a - subscribe request stream outlives its SUBSCRIBE_OK. AUTH carries a Request - ID like every request plus the token; AUTH_OK and AUTH_ERROR carry the lite - fields and may repeat on the stream with the lite update and revoke - meanings; closing the stream withdraws the token. Track namespaces are - tuples on this wire, so a prefix is a namespace tuple, matching how - SUBSCRIBE_NAMESPACE spells one. Sent only after negotiation; an endpoint - that receives one without negotiating closes with PROTOCOL_VIOLATION, which - is what moq-net already does for an unknown request stream. -- Which existing codes AUTH_ERROR reuses: `UNAUTHORIZED`, `EXPIRED_AUTH_TOKEN`, - `MALFORMED_AUTH_TOKEN`, and `NOT_SUPPORTED` from the request error registry. - An unrepresentable pattern grant returns `NOT_SUPPORTED`, surfaced by the - public handle as `Unsupported`, rather than widening it into a namespace - prefix. The shared request error mapping already supports that code. -- A note relating it to the AUTHORIZATION TOKEN setup option: a token - presented there is the connection credential an empty AUTH refers to. - -Cite [moq-wg #1854](https://github.com/moq-wg/moq-transport/issues/1854) in -the introduction: the grant answers which role a peer will play. Run -`just drafts check`; `doc/.vitepress/drafts.ts` discovers the file by name. - -### Grant conversion - -Keep the public grant pattern-valued. Convert only unions that can be expressed -exactly as namespace-prefix tuples; a subtree and the all-path grant are -representable, while exact-path, suffix, and segment-wildcard grants are not. -Validate the whole publish/subscribe union before emitting AUTH_OK. Never -silently drop a member, broaden it to a literal head, or leave the request -waiting indefinitely. An unsupported initial grant refuses that token; an -unsupported update revokes its previous grant with AUTH_ERROR and closes the -stream. Other tokens and work still covered by them remain on the session. - -Full-pattern IETF encoding would require a separately negotiated extension -revision and is outside this initial prefix-tuple contract. Document this -capability limit in the draft and token-in-band guidance. - -### Code - -`rs/moq-net/src/ietf/auth.rs` beside `solicit.rs` and `cluster.rs`: the setup -option round trip with the tri-state `from_setup` shape solicit uses, the -three messages as `Message` impls with IDs from the draft, and negotiation -recorded on the peer state. `run_dispatch` in `ietf/session.rs` routes an AUTH -request stream to the shared `auth::Handle` from -[Lite stream](/quest/m1/auth/lite.md); `ietf::start` opens the empty-token -stream after SETUP when negotiated and answers the peer's from the origin -handles exactly as lite does, and `add` opens further ones. The fail-loud -check moves into the shared handle so the IETF subscriber half consults it -before a new PUBLISH_NAMESPACE, aborting with `Unauthorized` and the path. -A shrinking grant instead withdraws previously authorized publications and -cancels only affected subscriptions, preserving the session as lite does. - -`js/net/src/ietf/auth.ts` mirrors it, wired through `handshake.ts` like -`Ietf.Cluster.intoSetup` and `fromSetup`, and `ietf/connection.ts` dispatches -the request streams. - -Version-gate on draft-17+; earlier drafts leave `grant()` at `None` and `add` -at `Unsupported`. - -### Tests - -Cross-language fixtures cover prefix unions and the root grant, refusal of -exact/suffix/segment-wildcard and mixed unions, plus an update from a supported -grant to an unsupported one that revokes the old permission without changing -other tokens. Assert `Unsupported` completes promptly and no unauthorized -PUBLISH_NAMESPACE is sent. - -Setup option round trip on every supported draft and absence on 14 to 16; -negotiation requires both sides; grants from scoped origins over an IETF -session in Rust, JS, and across; two tokens union and closing one shrinks the -union without disconnecting, cancelling only work that loses authorization; -an out-of-scope new publication aborts before any PUBLISH_NAMESPACE is written; a -peer without the option (the relay built without it, and the interop runner's -reference relay) sees no AUTH stream and keeps working. Run -`just test interop --all`. - -On main, additive. - -## Required - -- [Lite stream](/quest/m1/auth/lite.md) - supplies `moq_net::auth` and the - shared handle this binds to the IETF wire diff --git a/quest/m1/auth/narrowing.md b/quest/m1/auth/narrowing.md deleted file mode 100644 index fdac84e4749..00000000000 --- a/quest/m1/auth/narrowing.md +++ /dev/null @@ -1,69 +0,0 @@ -# [M] A live origin grant narrows in place - -## Goal - -A `moq-net` session's grant can be replaced by a narrower pattern union -while the session is live: announce cursors retract the patterns that fell -outside, resolution refuses them, subscriptions under them reset with -`UNAUTHORIZED`, and broadcasts the session published under them abort. The -relay uses it on revalidation so a moderation decision (a deafened user's -audio path) takes effect on the session it targets, which a forked client -cannot bypass. This is the per-subscriber exclusion -[#2714](https://github.com/moq-dev/moq/issues/2714) asked for; the scope and -match half already landed in #3672 and #3746. - -A narrowing always succeeds. A changed root still closes the session. - -## Plan - -- Enforce per session, not per handout. The line's session gate for received - grants (`auth::Gate`, the lite publisher resetting subscriptions it denies, - `regrant` retracting announces no longer covered, and the IETF equivalent) - is the same mechanism pointed at the relay's own grant. Reuse it rather than - building a second one. Readers that bypass the session publisher (HTTP - `/fetch` holds its own lease and closes on it; moq-hls and WHEP enforce no - per-user grant today) are out of scope. -- #3971 built narrowing alone and was closed: it refused whenever anything - live sat in the removed part, which is exactly when a grant narrows, and it - kept the ceiling in a side map. Scopes today are copied by value into every - handle and cursor (`OriginScope` in `rs/moq-net/src/model/origin.rs`), so - narrowing needs shared state. If handles need a tree to share it, keep the - ceiling on the grant node itself (an `Arc` shared by clones and children). -- Close the revocation races Codex found on - [#4179](https://github.com/moq-dev/moq/pull/4179), which the same watcher - mechanism owns: an in-flight lite FETCH keeps serving after its path is - revoked (Rust and JS check only at accept, and Rust's dropped fetches reset - `CANCELLED`, not `UNAUTHORIZED`); a Rust lite subscription still in - `Establish` when the grant shrinks is dropped with `CANCELLED` instead of - aborted `UNAUTHORIZED`; and the JS lite publisher and subscriber arm their - grant watchers only after an await (`demand()`, `#openSubscribe`), and - `Getter.subscribe` does not replay, so a shrink during setup is missed for - good. Every request holds one watcher from its first check to its end, and - rechecks when the watcher is armed. -- Publish side: routes and broadcasts the session published outside the - narrowed grant abort, so consumers see `Unauthorized` just as the subscribe - side does. Draining is not an option: a group may stay open as long as its - track. -- Relay revalidation uses it: a re-checked grant with the same root narrows - the session instead of closing it (`Lease::ended` reporting - `Reason::Narrowed` in `rs/moq-relay/src/auth.rs`), on every accept path: native, - io_uring, and `moq --listen` share `connection::supervise`, while WebSocket - has its own copy of the loop. -- Prove the deafen case end to end: subscribe under a room prefix, narrow - with a grant that excludes that audio path, and assert the subscription - resets with `UNAUTHORIZED` and no further objects arrive, while a sibling - path under the same prefix keeps flowing. -- Benchmark the narrowing and per-session gate cost in - `rs/moq-net/benches/origin.rs`, swept over routes and subscribers, with a - session that never narrows as the baseline. - -Public API: additive on moq-net and on moq-relay's revalidate path. Wire: -none beyond the UNAUTHORIZED code the required quest adds. - -## Required - -- [Unauthorized reset](/quest/m1/auth/unauthorized.md) - the stream code a narrowed subscription resets with - -## Closes - -- [#2714](https://github.com/moq-dev/moq/issues/2714) - close this issue when the quest finishes diff --git a/quest/m1/auth/not-supported.md b/quest/m1/auth/not-supported.md new file mode 100644 index 00000000000..236e81d736c --- /dev/null +++ b/quest/m1/auth/not-supported.md @@ -0,0 +1,54 @@ +# [M] Lite NOT_SUPPORTED + +## Goal + +A lite presenter learns that its token is unsupported whenever that happens, +not only on the first reply. Today, if a grant update is too large for AUTH_OK +after a grant was issued, the acceptor resets the stream with INTERNAL_ERROR. +The grant is revoked, but the presenter sees a generic stream error, while the +moq-transport binding answers `AUTH_ERROR { NOT_SUPPORTED }`. After this +change, lite has a NOT_SUPPORTED session code, so `Error::Unsupported` +round-trips on both wires. + +## Plan + +Decided while planning the follow-ups of +[#4446](https://github.com/moq-dev/moq/pull/4446): + +- **The carrier is AUTH_ERROR, not a stream reset code.** AUTH_ERROR after + AUTH_OK already revokes a grant, so this mirrors the IETF binding exactly. + A lite acceptor answers `AUTH_ERROR { NOT_SUPPORTED }` for every + unsupported case: no in-band verification, a grant that cannot be + expressed, or a grant too large for AUTH_OK, whether as the first reply or + after one. A presenter still reads a reset before any reply as unsupported, + because older acceptors and peers without the Auth Stream reset. +- **The code is lite session code 0x30 NOT_SUPPORTED**, the first of + moq-lite's own range (48 to 63). moq-transport's session registry has no + NOT_SUPPORTED. A bridge maps it to REQUEST_ERROR NOT_SUPPORTED (0x3), which + `Error::Unsupported` already does on IETF. + [Expired token error](/quest/m1/auth/expired-error.md) allocates after it. +- **`Error::Unsupported` maps to 0x30 everywhere on lite**, not just in + AUTH_ERROR. Rust `SessionError` and JS `SessionCode` round-trip it, so a + session closed for an unsupported feature stops reading as INTERNAL_ERROR. +- **The message ceiling is not this quest's** (decided in the 2026-10-06 + audit): [Request caps](/quest/m0/request-caps.md) (#4820) caps lite + messages at 65,535 bytes, moq-transport's 16-bit control Message Length, so + a grant too large for AUTH_OK is too large on both wires. This quest only + answers that case with NOT_SUPPORTED. +- **Compatibility.** An old peer that receives 0x30 treats it as an + unspecified error, as the draft requires. That is no worse than today's + reset. +- Drafts: add the session code to `drafts/draft-lcurley-moq-lite.md`, and + replace the reset rule in the Auth Stream section. Update `drafts/draft-lcurley-moq-auth.md` only where it + refers to lite. `just drafts check`. +- Tests: a regression test per side (Rust and JS). An oversized update after + an AUTH_OK reports `Unsupported` and revokes the grant. Also add an interop + case, run with `just test interop --all`. + +Public API: none new (`Error::Unsupported` exists). Wire: a new lite session +code. + +## Related + +- [Request caps](/quest/m0/request-caps.md) - owns the 65,535-byte lite message ceiling that makes a grant too large for AUTH_OK +- [AUTH violations](/quest/m1/auth/violations.md) - AUTH protocol violations close the session on the same path (AUTH endings landed in #4550) diff --git a/quest/m1/auth/relay-refresh.md b/quest/m1/auth/relay-refresh.md index 65fa8ddc8b1..81768957683 100644 --- a/quest/m1/auth/relay-refresh.md +++ b/quest/m1/auth/relay-refresh.md @@ -13,8 +13,10 @@ expiry, an expiry that leaves the union intact ends only that token, and ## Plan - `Connection::run` in `rs/moq-relay/src/connection.rs` takes `requests()` - from the `moq_net::Request` builder before `.ok()`, so the relay owns the - initial empty AUTH too and the driver's fallback never races it. An empty + from `moq_net::server::Handshake::auth()` before `.ok()`, so the relay owns + the initial empty AUTH too and the driver's fallback never races it. + `moq_tokio::server::Request::ok` spawns the driver itself, so it needs the + same `auth()` passthrough first. An empty token is answered from the origin handles as the default does, plus the admitted grant's `expires`. A non-empty token is presented through `Client::attach(&connection_lease, request)`, a method this quest adds: @@ -28,27 +30,28 @@ expiry, an expiry that leaves the union intact ends only that token, and sessions reject a non-empty token as `Unsupported`. - Union: the connection holds the set of accepted leases. Every one must carry the admitted root, else `AUTH_ERROR { Unauthorized }` naming the - root. The session's origin handles are rebuilt through `Cluster::publisher` - and `Cluster::subscriber` from the union of the set's publish and subscribe - prefixes, intersected with the role the client declared at SETUP so a + root. The peer is authorized for the union of the set's publish and subscribe + patterns, intersected with the role the client declared at SETUP so a publish-only session never starts receiving announcements because a later - token happened to carry subscribe prefixes, and swapped in whenever the set - grows; keep it behind one function, and a shrinking union goes through - [Origin narrowing](/quest/m1/auth/narrowing.md) rather than a close. + token happened to carry subscribe prefixes, re-authorized through + `auth::Handle::authorize` whenever the union changes, grown or shrunk; keep it + behind one function. A grant never reaches past the session's origin handles, + so scope those to the admitted root and role rather than to the first + token's patterns, and nothing is rebuilt or swapped in. - Expiry: the deadline today is the admitted lease's `closed()` inside one `tokio::select!` arm. Select on one lease per token in the set instead, and on any firing recompute the union without it: unchanged means `AUTH_ERROR { Expired }` on that token's stream and the session - continues, shrunk narrows the origin and resets each stream that lost - access with `UNAUTHORIZED`. A token + continues, shrunk re-authorizes the smaller union, which resets each stream that + lost access with `UNAUTHORIZED`. A token withdrawn by the client (its stream closed or reset) recomputes the union the same way with nothing written back, since the stream is gone: unchanged - means the session continues, shrunk narrows the same way. Each lease revalidates on + means the session continues, shrunk re-authorizes the same way. Each lease revalidates on its own cadence, and every re-check that changes the grant, a lower `expires` or a different `publish` or `subscribe`, writes the complete replacement grant as an update AUTH_OK on that token's stream and recomputes the union from the latest grant of every token, so the client can present a replacement in time and a narrowed token never keeps its old - scope; a shrunk union narrows as above. + scope; a changed union re-authorizes as above. - Refusals map as connect-time ones do and never expose a status: every refusal `Client::attach` returns for a new token, a `403`, an empty or invalid grant, an unparseable body, or a `5xx`, is @@ -70,18 +73,10 @@ expiry, an expiry that leaves the union intact ends only that token, and - Tests: a second token with a later `exp` and the same scope keeps the session past the first token's expiry, with only the first stream ending; an expired or invalid token is refused and nothing changes; a token with a - different root is refused naming it; a token adding a prefix rebuilds the - origin scope and the prefix is now solicited; withdrawing the only token + different root is refused naming it; a token adding a prefix widens the + grant and the prefix is now solicited; withdrawing the only token covering a prefix resets that prefix's streams with `UNAUTHORIZED` and the session stays connected; an mTLS session refuses a token; a proxy-mode outage on re-check keeps the token. Additive. - -## Required - -- [Origin narrowing](/quest/m1/auth/narrowing.md) - the live re-scope a shrinking token union needs, so no temporary close-on-shrink policy ships -- [Lite stream](/quest/m1/auth/lite.md) - supplies the AUTH stream and - `auth::Request` this consumes -- [Unauthorized reset](/quest/m1/auth/unauthorized.md) - the code this - relay's revocations reset with diff --git a/quest/m1/auth/request-token.md b/quest/m1/auth/request-token.md index d1d4bc40553..01e86fdf740 100644 --- a/quest/m1/auth/request-token.md +++ b/quest/m1/auth/request-token.md @@ -62,7 +62,7 @@ no longer fails the session; this quest gives it meaning. until it lapses. When a request's grant expires or is revoked, that request alone ends with `EXPIRED_AUTH_TOKEN` or `UNAUTHORIZED`. A session grant that shrinks cancels the requests it covered, as for any request, through - [Origin narrowing](/quest/m1/auth/narrowing.md). + `auth::Handle::authorize`. - Relay: each such request gets its own lease from a per-request call on `moq_auth::Client`, not the `Client::attach` that [Relay tokens](/quest/m1/auth/relay-refresh.md) builds. `attach` connects with the diff --git a/quest/m1/auth/token-in-band.md b/quest/m1/auth/token-in-band.md index 511951b4f1f..5a6be0335d7 100644 --- a/quest/m1/auth/token-in-band.md +++ b/quest/m1/auth/token-in-band.md @@ -16,8 +16,8 @@ AUTH-capable. IETF AUTH initially carries only prefix-representable grants. Propagate its `Unsupported` result for an unrepresentable pattern union; do not retry the -token through the URL, widen it, or leave token admission pending. Lite pattern -AUTH can carry the full grant once the pattern-interest prerequisite lands. +token through the URL, widen it, or leave token admission pending. Lite AUTH +already carries the full pattern grant. - Client configuration separates the credential from the address: `moq_tokio::connect::Config` gains `tokens`, repeatable as `--connect-token` and `MOQ_CONNECT_TOKEN`, the default set for every dial @@ -82,9 +82,8 @@ Additive. ## Required +- [FFI shape](/quest/m1/ffi-shape/README.md) - puts the `MoqClientConfig` record in place that `tokens` extends, so the wrappers break once - [Relay tokens](/quest/m1/auth/relay-refresh.md) - supplies the verify and widen path the configured tokens reuse - [Bindings](/quest/m1/auth/bindings.md) - supplies the client surface the new token setters sit beside -- [moq-transport](/quest/m1/auth/moq-transport.md) - supplies the IETF AUTH - exchange the setup-option token pairs with diff --git a/quest/m1/auth/unauthorized.md b/quest/m1/auth/unauthorized.md deleted file mode 100644 index 17f9b28e6d9..00000000000 --- a/quest/m1/auth/unauthorized.md +++ /dev/null @@ -1,25 +0,0 @@ -# [S] A subscription that loses access resets with UNAUTHORIZED - -## Goal - -When a grant shrinks and a subscription, fetch, or announce loses access, -its stream resets with a dedicated `UNAUTHORIZED` stream code, so the peer -tells revocation apart from a session closing. Today it resets with -`0x3 SESSION_CLOSED` (`StreamError::Session(Unauthorized)`). - -## Plan - -Assign `0x3A UNAUTHORIZED` in the lite draft's stream error table, the next -code in the application block after `TIMESTAMP_MISMATCH`, in the wip lite -version that carries the AUTH stream (`moq-lite-07-wip` today), never a -published one in place (decided 2026-10-05). Add the matching `StreamError` -variant (the enum is non-exhaustive) in Rust and JS, send it from every -revocation path the lite stream added, and cover it in the Rust and JS -tests. Map it to the existing `Unauthorized` protocol kind in -`stream_kind` in `rs/moq-ffi/src/error.rs` and `rs/moq-c/src/error.rs`, -whose wildcard arms would otherwise report `Unknown`, with a test for each. -Update `drafts/draft-lcurley-moq-lite.md` and run `just drafts check`. - -## Required - -- [Lite stream](/quest/m1/auth/lite.md) - the revocation paths that send it diff --git a/quest/m1/auth/violations.md b/quest/m1/auth/violations.md new file mode 100644 index 00000000000..481af71115b --- /dev/null +++ b/quest/m1/auth/violations.md @@ -0,0 +1,41 @@ +# [M] AUTH protocol violations close the session everywhere + +## Goal + +Every AUTH stream message that the lite or IETF drafts call a +PROTOCOL_VIOLATION closes the session in Rust and `@moq/net` alike, instead +of ending only its token. Examples: an AUTH_ERROR code past `u32`, an +out-of-range AUTH_OK expiry, or an AUTH, AUTH_OK, or AUTH_ERROR that fails to +decode, such as a bad pattern or length. + +## Plan + +AUTH endings (#4550) closes the session only +on an explicit `ProtocolViolation` in Rust lite. These gaps remain: + +- JS reports the exact oversized code but doesn't close the session. Let the + JS connection close on an `AuthSession` protocol violation. +- An out-of-range IETF AUTH_OK expiry still ends only its token. +- An AUTH_OK or AUTH_ERROR that fails to decode ends only the token, like + every other lite stream, although the draft calls a bad pattern a + PROTOCOL_VIOLATION. +- On the acceptor side, a presenter's AUTH that fails to decode only aborts + its stream (Rust lite `AuthServe`, JS `#runBidis`), so a peer can repeat + malformed AUTH streams without closing the session. +- Both IETF AUTH acceptors (Rust `Serve::run`, JS `IetfAuthWire.accept`) + discard the decoded Request ID, so a reused ID is accepted. Validate it like + every other request, where a duplicate is session-fatal. +- An AUTH reply of an unknown type (neither AUTH_OK nor AUTH_ERROR) fails + the type dispatch before either decoder runs, and ends only the token. +- A second AUTH on an open IETF AUTH stream is never read: after the first, + Rust `Serve::run` and JS `AuthSession.serve` wait only for the stream to + close. Treat it like a repeated PUBLISH_NAMESPACE, a duplicate request. + +This list comes from review, not a full audit. Walk every AUTH read path on +both sides, both protocols, and both languages, and fold in anything missing. + +Close the session on each, in both languages and on both protocols, with one +regression test per case, and check the lite and IETF draft text agrees. Run +`just test interop --all`. + +Public API: none. Wire: none beyond draft wording. diff --git a/quest/m1/auth/wip-version.md b/quest/m1/auth/wip-version.md new file mode 100644 index 00000000000..24ec28ced14 --- /dev/null +++ b/quest/m1/auth/wip-version.md @@ -0,0 +1,37 @@ +# [M] AUTH on the wip lite version + +## Goal + +The lite Auth Stream (0x7) and the 0x3A UNAUTHORIZED stream code exist only +on the wip lite version (`moq-lite-07-wip` today), in Rust, JS, the draft, +and the interop matrix. A lite-06 session never opens or accepts an Auth +Stream, so no published version changes in place. + +## Plan + +The line built AUTH on lite-06 before the maintainer decided on 2026-10-05 +that wire work targets the wip version until it is cut, never a published +version (see the README and [Finalize moq-lite-07](/quest/m1/lite07-finalize.md)). +The gate is one predicate on each side: `Version::has_auth` +(`rs/moq-net/src/lite/version.rs`) and `hasAuth` (`js/net/src/lite/version.ts`). + +- Make both false for lite-06; the URL credential already covers peers + without the stream, so a lite-06 session keeps working on its URL token. +- Move the Auth Stream and UNAUTHORIZED entries in + `drafts/draft-lcurley-moq-lite.md` from the lite-06 changelog to lite-07's, + and run `just drafts check`. +- Tests that negotiate the default version for AUTH must ask for + `moq-lite-07-wip` explicitly, since the default version sets leave it out: + `rs/moq-net/tests/auth.rs` (`LITE_06`), `js/net/src/auth.test.ts`, + `rs/moq-relay/tests/auth_lifetime.rs`, and the AUTH rounds of + `test/interop/interop.sh`. Add one test per language that a lite-06 session + has no grant and opens no Auth Stream. +- Check the docs (`doc/concept`, `doc/lib/rs/moq-net.md`, `doc/lib/js/net.md`) + name the wip version, not lite-06. + +Public API: none. Wire: AUTH and UNAUTHORIZED leave lite-06; the line has not +landed, so no released peer speaks them there. + +## Related + +- [Finalize moq-lite-07](/quest/m1/lite07-finalize.md) - the cut that publishes AUTH with lite-07 diff --git a/quest/m1/binding-surface.md b/quest/m1/binding-surface.md index 1d080fded3a..3de1ba7110a 100644 --- a/quest/m1/binding-surface.md +++ b/quest/m1/binding-surface.md @@ -16,10 +16,17 @@ configure and observe audio playout delay. - Update `doc/lib/{py,swift,kt,go,dart}` in the same PR. - Test configuration and observed delay in every wrapper that has tests. +Decided in the 2026-10-06 audit: the native decode delay API is on `main` +(#4162, `rs/moq-audio/src/decode/consumer.rs`), so this no longer waits on +the m0 jitter-target line, whose remaining Watch proof bindings do not use. +It waits on Codecs instead, so each wrapper adds delay to the reshaped audio +decoder rather than to `BroadcastConsumer.decode_audio`, which Codecs removes. + ## Required -- [Audio jitter target](/quest/m0/audio-jitter-target/README.md) - lands the native decode delay API on `main`; #3967 merged into the questline only +- [Codecs](/quest/m1/ffi-shape/codec.md) - the reshaped audio decoder each wrapper adds delay to ## Related - [FFI shape](/quest/m1/ffi-shape/README.md) - reshapes the binding namespaces +- [Audio jitter target](/quest/m0/audio-jitter-target/README.md) - landed the native decode delay API this exposes diff --git a/quest/m1/c/package.md b/quest/m1/c/package.md index 800a128758d..aad95acde5b 100644 --- a/quest/m1/c/package.md +++ b/quest/m1/c/package.md @@ -25,4 +25,5 @@ under the same names. ## Required +- [FFI shape](/quest/m1/ffi-shape/README.md) - moq-c 0.8.0 ships the reshaped moq-ffi, so C breaks once - [C backend](/quest/m1/c/backend.md) - the generator output this packages diff --git a/quest/m1/catalog-track-alias.md b/quest/m1/catalog-track-alias.md index a41e1b59fe5..fcaa8188a83 100644 --- a/quest/m1/catalog-track-alias.md +++ b/quest/m1/catalog-track-alias.md @@ -1,52 +1,58 @@ -# [M] hang catalog rendition keys are aliases +# [M] hang catalog rendition keys are IDs ## Goal -A hang catalog's video and audio rendition keys are aliases, unique within -the catalog, rather than wire track names. A rendition config gains an -optional `track`, the wire track name, beside its existing `broadcast`; -absent, the track name is the alias, so every catalog published today parses -and plays unchanged. One catalog can then list renditions from several -broadcasts that share a track name. +A hang catalog's video and audio rendition keys are IDs, unique across video +and audio within the catalog, rather than wire track names. A rendition +config gains an optional `track`, the wire track name, beside its existing +`broadcast`; absent, the track name is the ID, so every catalog published +today parses and plays unchanged. One catalog can then list renditions from +several broadcasts that share a track name. A catalog that repeats an ID +across video and audio is refused. ## Plan Decided (2026-09-29) while planning [media stats](/quest/m1/stats/README.md), -which keys its snapshots by this alias: without it, a catalog referencing two +which keys its snapshots by this ID: without it, a catalog referencing two broadcasts that both publish `video` cannot list both, and a snapshot keyed by track name would collapse them. +Decided by the maintainer in the 2026-10-06 audit: "make the key be the ID, +adding in a dedicated `track` field". The key is an ID (this quest earlier +called it an alias), unique across video and audio; a cross-kind duplicate is +refused, a validation tightening on main, so stats and echo key by ID alone +with no kind qualifier; `track` stays optional and defaults to the ID. +Rejected: an ID plus a required `track`. + - The field is `track`, beside `broadcast` on `VideoConfig` and `AudioConfig` in `rs/hang` and their zod schemas in `js/hang`. Additive on main: an optional field on `#[non_exhaustive]` types. - Every reader that subscribes by the map key resolves the wire name through one helper instead, in Rust and JS, so no path keeps assuming key equals name. Publishers keep writing no `track` unless they need it. +- Video and audio are separate maps, so one key could appear in both; with + `broadcast` references it parses today. Parsing refuses it in Rust and JS. - Open: a released reader ignores `track` and subscribes by the key, so it - cannot play a rendition whose `track` differs from its alias. Candidates: + cannot play a rendition whose `track` differs from its ID. Candidates: accept that, since such a listing was not expressible before and a publisher sets `track` only when it must; or gate the reader change behind a catalog version. The maintainer settled it as additive on main; confirm the forward-compatibility cost before starting. - The MSF conversion in `rs/moq-mux` names MSF tracks by the key today. - It carries the wire name and keeps the alias through a round-trip test, or - refuses a catalog whose `track` differs from its alias. + It carries the wire name and keeps the ID through a round-trip test, or + refuses a catalog whose `track` differs from its ID. - Scope: `rs/hang`, `js/hang`, their readers, `drafts/draft-lcurley-moq-hang.md` (validate with `just drafts check`), and `doc/concept/hang.md`. - Tests: an old catalog resolves each track name to its key; a catalog with two renditions naming the same `track` in different broadcasts round-trips - in both languages and plays each. + in both languages and plays each; a catalog repeating an ID across video + and audio is refused. - Open: text, JSON, and binary sections also carry `broadcast` and key by track name. Candidates: keep this to video and audio, as decided, or give every section with `broadcast` a `track` through the same helper so resolution is uniform. -- Open: video and audio are separate maps, so one key can appear in both. - Within one broadcast that already means one track for two kinds, but with - `broadcast` references it parses today. Candidates: refuse a cross-kind - duplicate (a validation tightening on main), or accept it and kind-qualify - the stats and echo keys. ## Related - [Media stats](/quest/m1/stats/README.md) - keys stats and feedback by this - alias + ID diff --git a/quest/m1/cli-packaging.md b/quest/m1/cli-packaging.md index ef9dc1dc715..0166a2160b2 100644 --- a/quest/m1/cli-packaging.md +++ b/quest/m1/cli-packaging.md @@ -51,7 +51,11 @@ libpipewire-0.3 to load, the same load-time requirement Decided in the 2026-09-30 audit: the v4l2 encode quest folded in here, since its remaining work was one release feature flag and a doc note. +Decided in the 2026-10-06 audit: this does not wait on +[Capture by default](/quest/m1/capture-default.md). moq-cli's `capture` +feature enables `moq-video/capture` and `moq-audio/capture` explicitly, so +making it default-on does not depend on the library defaults. + ## Required -- [Capture by default](/quest/m1/capture-default.md) - also changes moq-cli's `capture` feature; land it first - [Audio capture without runtime system libraries](/quest/m1/capture-alsa-link.md) - the microphone path must start without system audio libraries before every distribution can ship it diff --git a/quest/m1/cluster-routing/README.md b/quest/m1/cluster-routing/README.md index 56c5d875437..9809bcdaede 100644 --- a/quest/m1/cluster-routing/README.md +++ b/quest/m1/cluster-routing/README.md @@ -63,9 +63,9 @@ vector the 2026-09-30 cache-tiers audit kept: current wip version, decided 2026-10-05); a plain client with one link advertises only itself. Node ids are opaque randoms, so routes reveal no backbone addresses. - **Trust.** Cluster-peer links may advertise any node; a client link's node - ids stay scoped to its session. Promoting them is - [Route trust](/quest/m3/route-trust.md), which moved beside P2P in the - 2026-10-05 audit; this line finishes without it. + ids stay scoped to its session. Promoting them to a shared identity was + dropped in the 2026-10-06 audit: session scoping already covers the + security goal, and promotion has no consumer. - **Multi-CDN by link preference.** A node may rank its links (moq.pro primary, Cloudflare secondary) ahead of the metric, so metrics from different operators are never compared; a CDN that speaks no ROUTE is just @@ -141,7 +141,6 @@ Once every child has landed: - [Wildcard](/quest/m0/wildcard/README.md) - the longest-prefix rule and pool spread Selection builds on - [P2P](/quest/m3/p2p/README.md) - browser and native peers that become routing nodes over this layer -- [Route trust](/quest/m3/route-trust.md) - a peer grant lets a client link advertise the nodes behind it as one identity - [Remove `--hop`](/quest/m0/broadcast-epoch/hop-removal.md) - redundant publishers share an explicit `@` - [One route cost](/quest/m1/route-cost.md) - Warm and Cold collapse to one static cost - [Same-epoch importers](/quest/m1/hop-aligned-import.md) - the importer half of a redundant pair under one explicit epoch diff --git a/quest/m1/cluster-routing/routes.md b/quest/m1/cluster-routing/routes.md index b05fc75a185..c59473872f6 100644 --- a/quest/m1/cluster-routing/routes.md +++ b/quest/m1/cluster-routing/routes.md @@ -70,8 +70,8 @@ Decided 2026-10-01 (see the [line's decisions](/quest/m1/cluster-routing/README. other links, and a route carrying it is never sent on an upstream link (extending the `upstream` link mark in `doc/bin/relay/cluster.md`). - A plain client with one link advertises a ROUTE for itself and the - ANNOUNCEs it publishes; its node id is scoped to its session (shared - identity across sessions is [Route trust](/quest/m3/route-trust.md)). + ANNOUNCEs it publishes; its node id is scoped to its session, and + nothing promotes it to an identity shared across sessions. A relay advertising routes to a client sends them as usual; node ids reveal nothing about the backbone. - Every hop re-selects; SUBSCRIBE names no origin, and the reply names none diff --git a/quest/m1/cluster-routing/selection.md b/quest/m1/cluster-routing/selection.md index 0ce37b18416..71100d7e836 100644 --- a/quest/m1/cluster-routing/selection.md +++ b/quest/m1/cluster-routing/selection.md @@ -1,4 +1,4 @@ -# [L] Deterministic origin selection +# [M] Deterministic origin selection ## Goal @@ -11,11 +11,17 @@ resumes on another when the incumbent ends or becomes unreachable. ## Plan +Most of this landed with [Wildcard](/quest/m0/wildcard/README.md) (#4403) and +#4741: `route_order` ranks the longest covering prefix first, keys its hash on +the requested path so equal-cost advertisers of one prefix split paths between +them, and a concrete path is one source with mid-group resume. Terminal +refusal landed too. What remains (cut 2026-10-06, after #4403 merged): the HRW +split test, the same-path failover test, and spreading paths over upstream +links. + Decided: -- The longest covering prefix ranks first, per - [Wildcard](/quest/m0/wildcard/README.md), and origin choice is - per broadcast, not per announced prefix. +- Origin choice is per broadcast, not per announced prefix. - A concrete path, with or without an epoch, is a source's identity: every origin announcing it is interchangeable, which is how a redundant pair works under one explicit epoch. There is no per-origin identity, for claim @@ -25,37 +31,29 @@ Decided: except where a publisher starts at a group boundary, as claim workers do per [Wildcard](/quest/m0/wildcard/README.md)'s double-claim rule. So a relay never needs to tell claim output from a redundant pair. - -Recorded in the 2026-10-05 audit: part of this is already built. The -wildcard line's `route_order` hash is keyed on the requested path, the HRW -spread below, and #4741 made a concrete path one source with mid-group -resume. Once [Wildcard](/quest/m0/wildcard/README.md) (#4403) lands, cut this -quest down to what remains: the HRW split test, the same-path failover -test, and upstream-link spread. Terminal refusal already landed. +- Every hop re-selects (decided 2026-10-01): SUBSCRIBE names no origin, since + a pin breaks subscription aggregation. +- A refusal is terminal, and an origin sheds load by withdrawing or + re-pricing its route instead. Candidate mechanics: -- Route selection stays `route_order` over the routes a relay holds: longest - prefix, then cost, then a rendezvous hash (HRW) of the requested path and - the route's origin. At an edge that spreads paths over the region's cores - and makes every edge pick the same core for a path; failover moves only the - paths the lost core won. -- Every hop re-selects (decided 2026-10-01): SUBSCRIBE names no origin, since - a pin breaks subscription aggregation. Once - [Routes and announces](/quest/m1/cluster-routing/routes.md) lands, the +- Upstream-link spread: at an edge, the rendezvous hash (HRW) of the + requested path and the route's origin spreads paths over the region's + cores and makes every edge pick the same core for a path; failover moves + only the paths the lost core won. +- Once [Routes and announces](/quest/m1/cluster-routing/routes.md) lands, the candidates are the origin nodes announcing the path, ranked by longest prefix, then the link's preference ([Multi-CDN endpoints](/quest/m1/cluster-routing/multi-cdn.md)), then route metric to the node, then HRW. Write the ranking so that change swaps its inputs, not its shape. -- A refusal follows Wildcard's refusal rule: every refusal is terminal, and - an origin sheds load by withdrawing or re-pricing its route instead. Wire: none expected; if one is needed it goes in lite-07 (the current wip -version, where the route layer also lands) with the draft. Tests cover an HRW split across an equal-cost -pool, a terminal refusal, and a same-path pair failing over mid-group with -no timestamp rewind. +version, where the route layer also lands) with the draft. Tests cover an HRW +split across an equal-cost pool and a same-path pair failing over mid-group +with no timestamp rewind. -## Required +## Related -- [Wildcard](/quest/m0/wildcard/README.md) - the longest-prefix rule and pool spread this builds on +- [Wildcard](/quest/m0/wildcard/README.md) - landed the longest-prefix rule and pool spread this builds on diff --git a/quest/m1/cpp/README.md b/quest/m1/cpp/README.md index 24bfaa05853..a84d440f9fe 100644 --- a/quest/m1/cpp/README.md +++ b/quest/m1/cpp/README.md @@ -1,4 +1,4 @@ -# C++ through moq-ffi +# [S] C++ through moq-ffi ## Goal @@ -69,15 +69,14 @@ line (#4519) lands first, and this line ports `cpp/moq`, `cpp/obs`, and the C++ interop client onto the reshaped moq-ffi, since its branch still calls APIs FFI shape deleted. +Recorded in the 2026-10-06 audit: every child (generator #4100, package +#4187, cancel, C++ standard, macOS alias check, OBS migration #4281) merged +into the line branch, which deletes their quest files, so main deletes them +too. What remains is landing [#4079](https://github.com/moq-dev/moq/pull/4079). + ## Required - [FFI shape](/quest/m1/ffi-shape/README.md) - lands first; this line ports onto its moq-ffi -- [Generator](/quest/m1/cpp/generator.md) - the uniffi 0.32 C++ generator with futures and expected-style errors, pinned and generating `cpp/ffi` in CI -- [Package](/quest/m1/cpp/package.md) - the `cpp/moq` wrapper, CMake package, release tarball, interop client, and docs -- [Cancel](/quest/m1/cpp/cancel.md) - a cancelled or consumed future reports `valid() == false`, like `std::future`, and a read of it aborts with a message naming the misuse -- [macOS alias check](/quest/m1/cpp/macos-alias-check.md) - `just cpp check` passes its alias step with the BSD `sed` macOS ships -- [C++ standard](/quest/m1/cpp/cxx-standard.md) - a consumer that sets C++23 only on its own target links the package -- [OBS migration](/quest/m1/cpp/obs.md) - the OBS plugin moves from moq-c handles and trampolines to the generated C++ ## Related diff --git a/quest/m1/cpp/cancel.md b/quest/m1/cpp/cancel.md deleted file mode 100644 index 8faad9eacfe..00000000000 --- a/quest/m1/cpp/cancel.md +++ /dev/null @@ -1,26 +0,0 @@ -# [XS] A cancelled future says so before it is read - -## Goal - -Callers are told to check `valid()` before reading a future that may be -cancelled, consumed, or moved from, and the tests prove it goes false in each -case. - -## Plan - -The fork half is done on the line: the `-kixelated.2` generator's -`uniffi::Future` has `valid()`, false once `get()`, `then()`, `cancel()`, or a -move took its state, like `std::future`, and a read of an invalid future aborts -with "it was consumed or cancelled". `cpp/moq/test/probe.cpp` checks `valid()` -after `cancel()`. Decided in the #4307 review; `&&`-qualifying `cancel()` and an -error-returning `get()` stay rejected. - -What remains: - -- `cpp/moq/README.md` (the Error and Cancellation sections) still only says a - misused future aborts; point callers at `valid()` instead. -- Extend the probe to check `valid()` after `then()` and after a move. -- Check the in-tree callers that can hold a cancelled future (`cpp/moq`, - `cpp/obs`) against `valid()`. - -Public API: none beyond the unreleased C++ package's `valid()`. Wire: none. diff --git a/quest/m1/cpp/cxx-standard.md b/quest/m1/cpp/cxx-standard.md deleted file mode 100644 index e051fadd9f9..00000000000 --- a/quest/m1/cpp/cxx-standard.md +++ /dev/null @@ -1,29 +0,0 @@ -# [XS] A C++23 consumer links the package - -## Goal - -A consumer that raises the standard only on its own target -(`target_compile_features(app PRIVATE cxx_std_23)`) either links against the -`moq-cpp` package or is told clearly how to configure it. Today it gets link -errors: that requirement does not flow backward into the separate `moq-cpp` -static library, which compiles `moq.cpp` at `cxx_std_17` and so picks -`tl::expected` while the app's `` picks `std::expected`. The ABI -guard turning that into a link error is correct; the surprise is not. Codex -found it on [#4187](https://github.com/moq-dev/moq/pull/4187). - -## Plan - -- Fix the misleading comment in `cpp/moq/cmake/moq-cpp-config.cmake.in` - (and the matching one in `cpp/moq/CMakeLists.txt`): the bindings compile - with the project's standard (`CMAKE_CXX_STANDARD`) or C++17, not with - whatever the including target asks for. -- Then choose the smallest fix that makes the C++23 path work: document - `CMAKE_CXX_STANDARD` (what the probe does) in `cpp/moq/README.md` and the - C++ docs page, or add a package option that sets the standard `moq-cpp` - compiles with. Prefer documentation unless a consumer (OBS, Unreal) needs - the option. -- Test the documented configuration: the package test already builds at - several standards; add one that builds the app at C++23 the documented - way. - -Public API: none, or one CMake option. Wire: none. diff --git a/quest/m1/cpp/generator.md b/quest/m1/cpp/generator.md deleted file mode 100644 index dfca3a0dab4..00000000000 --- a/quest/m1/cpp/generator.md +++ /dev/null @@ -1,43 +0,0 @@ -# [M] uniffi-bindgen-cpp: futures and expected-style errors on uniffi 0.32 - -## Goal - -`just cpp check` regenerates `cpp/ffi` from `rs/moq-ffi` with a pinned C++ -generator and compiles a small program that connects, subscribes to a track, -reads one frame through a future, cancels a pending read, and observes an -error as a returned `expected`, on gcc, clang, and MSVC at C++17. This is the -go/no-go gate for the questline: if the generator cannot be made to hold, -stop here and write down why. - -## Plan - -- Fork `livekit/uniffi-bindgen-cpp` at branch `livekit/uniffi-0.31-async` - (futures with `get`/`wait_for`/`cancel`/`then`, async callback interfaces, - a pluggable dispatcher; last commits 2026-09-08) into - `kixelated/uniffi-bindgen-cpp`. Port it to uniffi 0.32.x: the metadata - encoding changed in 0.32 without a contract bump, so a 0.31 generator fails - to read a 0.32 cdylib at all; the Go port (`kixelated/uniffi-bindgen-go` - `v0.8.0+v0.32.0`) is the worked example. Tag `vX.Y.Z+v0.32.0`. -- Add an `error_style = expected` generator option: methods return - `moq::expected` instead of throwing, `uniffi::Future::get()` - returns the same, `then` continuations already receive a result type. Ship - a bundled `tl::expected` and alias `std::expected` when `__cpp_lib_expected` - is defined. Callback interfaces implemented in C++ return errors the same - way. The exceptions style stays the default so the fork remains upstreamable. -- Verify the generated headers compile with `-fno-exceptions` under the - expected style (Unreal's default), and that the async dispatcher shuts down - cleanly when the consumer unloads (`uniffi::shutdown_async_dispatcher`). -- Pin the fork in `flake.nix` next to `uniffi-bindgen-go` and `uniffi-bindgen-dart` - with the same comment discipline: every place that names the generator - version is listed and bumped together (`rs/moq-ffi/build.sh`, the release - workflow, `cpp/ffi/README.md`, `doc/lib/cpp`). `just cpp check` regenerates - into `cpp/ffi` and compiles the probe; wire it into the check workflow the way - `just go check` is. -- Document the cancellation contract from what moq-ffi does: native - `Task::run` and `detached` hold an `AbortOnDrop` on the spawned task - (`rs/moq-ffi/src/ffi.rs`), so dropping the Rust future aborts the work, and - a mid-write abort is possible. The generated future's `cancel()` and its - destructor map onto that; the wrapper says so where it matters (finish a - group before dropping its write future). -- Offer both the 0.32 port and the expected flag upstream to LiveKit and - NordSecurity; the fork exists only until they tag. diff --git a/quest/m1/cpp/macos-alias-check.md b/quest/m1/cpp/macos-alias-check.md deleted file mode 100644 index 3bbca6e06ea..00000000000 --- a/quest/m1/cpp/macos-alias-check.md +++ /dev/null @@ -1,17 +0,0 @@ -# [XS] The alias check runs on macOS - -## Goal - -`just cpp check` passes its "every generated `moq::MoqFoo` has a `moq::Foo` -alias" step on macOS outside the dev shell, so the C++23 probe after it runs. -Today the second `sed -nE` in `cpp/justfile` matches with a backreference -(`= Moq\1;`), which BSD `sed` ignores, so the alias list comes back empty and -every type reports missing. CI runs Ubuntu with GNU `sed` and never sees it. - -## Plan - -Extract both names from each `using` line and compare them in `awk` (or -another portable tool) instead of a backreference in the pattern. Check it -with `/usr/bin/sed` first on PATH. - -Public API: none. Wire: none. diff --git a/quest/m1/cpp/obs.md b/quest/m1/cpp/obs.md deleted file mode 100644 index 6c9a4574bf6..00000000000 --- a/quest/m1/cpp/obs.md +++ /dev/null @@ -1,53 +0,0 @@ -# [L] OBS plugin on the generated C++ - -## Goal - -`cpp/obs` links `cpp/moq` instead of moq-c. The output and source hold -`moq::` objects instead of `int` handles, async work is a future with a -continuation on OBS's own executor, and the generation counters, condvars, -`SessionRef`/`callback_state` heap trampolines, and "never take signal_mutex -inside a moq-c call" lock-order rules that exist only to survive moq-c's -single callback thread are deleted. Behavior visible to OBS users is -unchanged: same settings, same dock, same reconnect and teardown timing. - -## Plan - -- Executor: install a `moq::Executor` that marshals continuations onto the - thread OBS expects for each site (graphics thread for source frames, the - output's own thread for status), so no continuation runs on the moq-ffi - runtime thread and none can stall other sessions. -- Output (`moq-output.cpp`): `moq::Client::connect` returns a future; the - `attempt` generation guard becomes future ownership (a superseded attempt - drops its future, which cancels). Reconnect stays in the plugin. Publish - through `moq::Broadcast` producers and the media importer with the same - catalog handling. -- Source (`moq-source.cpp`): `subscribe_catalog` / `subscribe_media` futures - replace `moq_consume_*` callbacks; frame lifetime is the returned frame - object, not a `free` call. FFmpeg decode stays as is; the native decode - replacement is the OBS native codecs questline and now starts from this - code. -- Tests: `cpp/obs/test/*-test.cpp` currently stub moq-c's C symbols to race - the OBS and runtime threads. Rewrite them against `moq::` interfaces with a - fake executor; keep the scenarios (stop during connect, late terminal, - superseded attempt). -- Build and release: `cpp/obs/CMakeLists.txt` consumes the package - (in-tree `cpp/` for `MOQ_LOCAL`, the release tarball otherwise); `obs.yml` - rides `release-cpp.yml` instead of `moq-c.yml`. `doc/bin/obs.md` says the - plugin is C++ over the generated bindings. -- Shutdown: define `obs_module_unload` and call `moq_ffi_shutdown` there, - after OBS has destroyed the outputs and sources, so the runtime thread is - joined before the module is unmapped; log it so a hung shutdown is visible. - A `cpp/obs/test` stub test asserts the unload calls it. Reproduce the crash - first (exit with an output running, right after stopping one, and with a - source whose destroy hit its backstop) and record which cases crashed. -- Verification: `just obs compile` and `just obs test` on all three - platforms; a manual publish and watch round trip against a relay with - reconnect and mid-stream source deletion. - -## Required - -- [Package](/quest/m1/cpp/package.md) - the wrapper and CMake package OBS links - -## Related - -- [OBS native codecs](/quest/m1/obs-moq-video/README.md) - its quests require this migration and build on moq-ffi's audio and video types diff --git a/quest/m1/cpp/package.md b/quest/m1/cpp/package.md deleted file mode 100644 index abad2c9221b..00000000000 --- a/quest/m1/cpp/package.md +++ /dev/null @@ -1,44 +0,0 @@ -# [M] cpp/moq: the wrapper, CMake package, release tarball, interop client, and docs - -## Goal - -`find_package(moq)` or the tarball's `moq.pc` gives a C++17 program -``, and the program publishes and subscribes through the full -moq-ffi surface with futures and `moq::expected`, `co_await`-able on C++20. -A `test/interop/clients/cpp` client joins `just test interop --all`, and -`doc/lib/cpp` documents the package the way `doc/lib/go` does Go. - -## Plan - -- `cpp/ffi` is generated only (committed like `go/ffi`, regenerated by `just - cpp check`). `cpp/moq` is the thin wrapper: `namespace moq` aliases that drop - the `Moq` prefix and mirror the names the other wrappers use (`moq::Client`, - `moq::Broadcast`, `moq::Track`), `std::chrono::microseconds` for timestamps, - `std::string_view` and `std::span`-like views at the edges, and - `moq::Executor` for `then`. Nothing in the wrapper re-implements a method; - a shape problem is fixed in moq-ffi. -- Coroutines: an awaiter over `uniffi::Future` that resumes on the future's - continuation, compiled only under `__cpp_impl_coroutine`. Cancellation on - awaiter destruction follows the future's own rule. -- Executor: the default dispatcher is fine for scripts; document how a host - installs its own (`moq::set_executor`) before the first call, and that - continuations must not block on the moq-ffi runtime thread. -- Build: `cpp/CMakeLists.txt` builds `libmoq_ffi` (staticlib, features - `video`+`audio`) with the same `cargo build` custom command and - `BUILD_RUST_LIB` switch `rs/moq-c/CMakeLists.txt` uses (no Corrosion, so - the two CMake entry points stay alike), compiles `cpp/ffi`, installs headers, `moqConfig.cmake`, and `moq.pc`. Windows MSVC, - macOS, and Linux, both architectures, matching `moq-c.yml`'s matrix. -- Release: `release-cpp.yml` mirrors `moq-c.yml` (tag `cpp-v*`, nightly - dry-run through the `workflow_call` chain), producing one tarball per target - containing the static library, generated sources, wrapper headers, and the - CMake package. The registries quest consumes these artifacts. -- Interop: `test/interop/clients/cpp` subscribes and publishes like the Go client; - the C client stays, since moq-c stays. -- Docs: `doc/lib/cpp/index.md` (install via tarball, then the registries once - they exist; the future, expected, executor, and coroutine rules) and a row in - `doc/lib/index.md`. The C row now says moq-c is the plain-C ABI and points - C++ readers at the new package. - -## Required - -- [Generator](/quest/m1/cpp/generator.md) - the pinned generator that emits `cpp/ffi` diff --git a/quest/m1/ffi-shape/README.md b/quest/m1/ffi-shape/README.md index 5e1b39ff0d4..a128aed7caa 100644 --- a/quest/m1/ffi-shape/README.md +++ b/quest/m1/ffi-shape/README.md @@ -63,7 +63,4 @@ work no child does: ## Required - [Bindings](/quest/m0/broadcast-epoch/bindings.md) - lands first; this line adopts its epoch surface and the `session.epoch()` rename -- [JSON](/quest/m1/ffi-shape/json.md) - the pilot: json and flate become their own namespaces wrapping a track in every binding and set the per-language pattern -- [Net](/quest/m1/ffi-shape/net.md) - client and server take config records, snapshots are records, and the verbs match moq-net -- [Media](/quest/m1/ffi-shape/media.md) - catalog, import, and container consume move under `media` - [Codecs](/quest/m1/ffi-shape/codec.md) - audio and video encoders and decoders move under their own namespaces with one constructor shape diff --git a/quest/m1/ffi-shape/codec.md b/quest/m1/ffi-shape/codec.md index 629f7b96264..513ce7c78ed 100644 --- a/quest/m1/ffi-shape/codec.md +++ b/quest/m1/ffi-shape/codec.md @@ -26,9 +26,8 @@ their callers construct, not one they are asked to match, so variant additively instead of breaking `gop` a second time. Go gets no uniffi default, so its zero value must read as keyframe mode. -Public API: breaking in every binding. Wire: none. - -## Required +The audio and video frame and decoder-output records carry microsecond fields +(`timestamp_us`, `max_age_us`, `frame_duration_us`); in Python and Go they +should become owned `timedelta` / `time.Duration` records like net's. -- [JSON](/quest/m1/ffi-shape/json.md) - sets the per-language namespace pattern -- [Media](/quest/m1/ffi-shape/media.md) - the catalog handle the encoders register into +Public API: breaking in every binding. Wire: none. diff --git a/quest/m1/ffi-shape/json.md b/quest/m1/ffi-shape/json.md deleted file mode 100644 index 1475f6217a2..00000000000 --- a/quest/m1/ffi-shape/json.md +++ /dev/null @@ -1,33 +0,0 @@ -# [M] JSON and flate get their own namespaces in every binding - -## Goal - -JSON tracks live under `json` and opaque tracks under `flate` in moq-ffi and -every wrapper, constructed from a track producer or consumer as in `moq-json` -and `moq-flate`, and `BroadcastProducer`/`BroadcastConsumer` lose -`publish_json_*`/`subscribe_json_*` and `publish_flate_*`. The per-language -namespace pattern this sets is what the other children copy. - -## Plan - -JSON is the smallest group, so it carries the setup cost: a Python submodule, -a Go subpackage, a Kotlin package, a Dart library, and a Swift namespace, each -wired into its package build, tests, and docs. Settle the pattern here and -write it down where the next child will find it (`rs/moq-ffi/AGENTS.md` if it -is a convention, after reading `PROMPTING.md`). - -Mirror `moq-json`'s names (`snapshot`, `stream`) and constructors, which -take a track: that also covers a track accepted from a request, which the -broadcast methods cannot reach. Snapshot and -stream producers keep `demand()`. Payloads stay `String` at the FFI; wrappers -that already type them (Swift's generic producer, Kotlin's reified `update`) -keep doing so, and the rest may follow. - -Watch for Go import cycles: a subpackage takes the root's broadcast handle, so -the root must not import it back. - -`flate` is the same shape over opaque bytes: moq-ffi's `flate.rs` -(`publish_flate_snapshot`, `publish_flate_stream`) moves under it, mirroring -`moq_flate::{snapshot, stream}`. - -Public API: breaking in every binding. Wire: none. diff --git a/quest/m1/ffi-shape/media.md b/quest/m1/ffi-shape/media.md deleted file mode 100644 index e2a0a537140..00000000000 --- a/quest/m1/ffi-shape/media.md +++ /dev/null @@ -1,31 +0,0 @@ -# [L] Catalog, import, and container consume move under media - -## Goal - -`media` owns what hang and moq-mux do for a binding: the catalog, the import -producers, and the container consumers, each constructed from a broadcast. -`BroadcastProducer` loses `publish_{audio,video,container}` and their -`_on_track`/`_stream` variants plus the catalog setters; -`BroadcastConsumer` loses `subscribe_catalog`, `subscribe_media`, and -`fetch_media_group`. - -## Plan - -Mirror moq-mux's import names and hang's catalog. The catalog setters -(`set_video_properties`, `set_catalog_section`, `remove_catalog_section`) -belong on a catalog type, not the broadcast. #3959 prototyped one -(`MoqCatalogProducer` with `set_video_properties`, `set_section`, and -`remove_section`, wrapped and tested in every language) behind a rejected -`BroadcastProducer::catalog()` accessor; crib the type, not the accessor. It -held the broadcast weakly so the handle keeps nothing open and writes fail -with `Closed` once the broadcast finishes. The `_on_track` variants take a -`TrackRequest`; decide whether that is a second constructor or an argument -once the shape is in front of you, and prefer one path. Go's -`FetchMediaGroup` takes an options struct. Media producers watch subscribers -through `demand()` only. - -Public API: breaking in every binding. Wire: none. - -## Required - -- [JSON](/quest/m1/ffi-shape/json.md) - sets the per-language namespace pattern diff --git a/quest/m1/ffi-shape/net.md b/quest/m1/ffi-shape/net.md deleted file mode 100644 index 350c7e6196d..00000000000 --- a/quest/m1/ffi-shape/net.md +++ /dev/null @@ -1,61 +0,0 @@ -# [L] The root namespace matches moq-net - -## Goal - -What stays at the root reads like moq-net: `Client` and `Server` take config -records, getter-only handles are records, producers watch subscribers only -through `demand()`, and the verbs no additive change could rename -are renamed. - -## Plan - -- `Client::new(config)` and `Server::new(config)` replace the fallible - setters, mirroring moq-tokio's `client::Config` and `server::Config` with - nested TLS, QUIC, and backoff records. Validate in `new`. Resolve defaults - in Rust, since Go gets none; Option fields keep additions additive. This - also retires Kotlin `Moq.connect`'s twelve named parameters. -- The client config carries the protocol versions to offer, as libmoq's - `moq_client_config.versions` already does (`rs/moq-c/src/client.rs:37`), so - every binding can pin or restrict versions. moq-ffi has no version setter - today. -- The cpp line's client-config quest (`quest/m1/cpp/client-config.md` on - branch `quest/m1/cpp/README`) ships this same `MoqClientConfig` record - additively. Decided in the 2026-09-30 audit: this quest then only - removes the fallible setters, rather than designing the record - twice. -- Objects that are only getters become records (`AnnounceUpdate` today). - Handles with verbs (`Request`, `TrackRequest`, `GroupRequest`) stay objects. -- An enum whose variants a wrapper must name spells each variant - `` (`AnnounceEventStart`, `AnnounceEventEnd`) in Go, - Kotlin, Dart, and Python, whatever the generated name. Swift keeps its - generated `.` cases, since it cannot alias a case. -- `TrackProducer` drops `name`/`is_used`/`used`/`unused` for `demand()`. -- `GroupRequest` gains `demand()` too, in moq-ffi, moq-c, and every wrapper, - so a binding's fetch handler can tell when every caller left and drop the - request (decided 2026-10-05, from #4708, which moved Rust's - `group::Request` onto `demand()` and withdraws an abandoned fetch on the - last caller's drop). -- The renames no additive change could make: - - `subscribe` to `consume` on Python `Client`/`connect`, Kotlin - `Moq.connect`/`Server.listen`, Dart `ConnectOptions`/`ListenOptions`, and Go - `WithSubscribeOrigin`/`WithServerSubscribeOrigin`. - - Go's `All`/`Requests`/`Updates`/`Frames`/`Values` iterator helpers to one - verb. - - Kotlin and Dart `announcements` versus `announced`: one name for the - stream, one for the cursor, without the collision. One option is - `announced(config).updates()`, dropping `announcements` (prototyped with - the Dart renames in #3959). - - Microsecond fields as `timedelta` in Python and `time.Duration` in Go, - which means owning those record types in the wrapper rather than aliasing - the generated ones. -- Open: `Moq` (Kotlin, Dart) and `Client` (Python, Go) re-expose - `createBroadcast`, `announced`, `announcedBroadcast`, and `requestBroadcast` - from `session.publish()`/`session.consume()`. That is the same flat spread - this line removes, but dropping it costs every quick-start a hop (raised in - #3959). - -Public API: breaking in every binding. Wire: none. - -## Required - -- [JSON](/quest/m1/ffi-shape/json.md) - sets the per-language namespace pattern diff --git a/quest/m1/flv-catalog-stream.md b/quest/m1/flv-catalog-stream.md index a0eec4cb763..0e6131853da 100644 --- a/quest/m1/flv-catalog-stream.md +++ b/quest/m1/flv-catalog-stream.md @@ -8,6 +8,7 @@ callers narrow renditions with `catalog::Stream::select` and the FLV-only ## Plan -This is a published API break. Consider whether the TS -and Matroska exports should take the same shape in the same pass. RTMP play +This is a published API break. The Matroska and fMP4 exports already take a +catalog stream (`mkv::Export::new(source, catalog)`), so only TS is open for +the same pass: consider whether it should take the same shape too. RTMP play passes its client-capability selection through the stream instead. diff --git a/quest/m1/flv-export-delay.md b/quest/m1/flv-export-delay.md index 4496cb2e952..ff3c79ed935 100644 --- a/quest/m1/flv-export-delay.md +++ b/quest/m1/flv-export-delay.md @@ -14,9 +14,16 @@ has arrived. Replace it with the release stage from [fixed-delay release](/quest/m1/tstd/delay.md), with the same `--delay` flag and the same late-frame drop. Update `doc/bin/cli.md`. -Public API: the exporter gains a delay setting; breaking only if it replaces -an existing one. Wire: none. +Decided in the 2026-10-06 audit: `--delay` replaces the staleness flag, as it +replaced `--max-age` on TS, so one knob sets both the release delay and the +sources' staleness budget. [Subscriber max-delay](/quest/m1/subscriber-max-delay.md) +(#4917) renames that flag to `--max-delay` first; this quest then replaces +`--max-delay` with `--delay`. Rejected: excluding flv from the #4917 rename. + +Public API: breaking, the exporter's staleness setting becomes the delay +setting. Wire: none. ## Required +- [Subscriber max-delay](/quest/m1/subscriber-max-delay.md) - renames the staleness flag first, so this replaces `--max-delay` - [Fixed-delay release](/quest/m1/tstd/delay.md) - builds the shared release stage diff --git a/quest/m1/hls-bounded.md b/quest/m1/hls-bounded.md index 81a4ece7132..a858c01c0fb 100644 --- a/quest/m1/hls-bounded.md +++ b/quest/m1/hls-bounded.md @@ -27,8 +27,7 @@ timelines need it first. The offline archive HLS that `quest/m1/archive/hls.md` used to describe landed on `main` with the archive line (#4034: #4115, #4155, #4169). So did -the per-track gap behaviour from -[Per-track timelines](/quest/m1/archive/track-timeline.md) (#4280, `spans.rs`), +the per-track gap behaviour from per-track timelines (#4280, `spans.rs`), which derives segments at the edge from group timestamps and emits `EXT-X-GAP` for a rendition with no group start in a span. The rules here apply to that shape. diff --git a/quest/m1/hop-aligned-import.md b/quest/m1/hop-aligned-import.md index b63352b87dd..3a1d425cf3c 100644 --- a/quest/m1/hop-aligned-import.md +++ b/quest/m1/hop-aligned-import.md @@ -23,8 +23,11 @@ fed one encoded stream. Capture is out: two encoders never align. not a per-process counter. Decide how both processes agree across a timestamp wrap (TS PTS wraps every 26.5 h) when they started on opposite sides of it. -- Frame timestamps derive from the input alone too. Importers publish the - stream's own timestamps and refuse a rewind, so nothing shifts them. +- Frame timestamps derive from the input alone too, and importers refuse a + rewind. Any offset [Shared import clock](/quest/m1/shared-clock.md) + applies is input-derived for a same-epoch importer, never from + `clock.now()` (decided in the 2026-10-06 audit), so two instances shift + identically. - The catalog's root `clock` must agree too. Today each importer anchors it to its own first-frame arrival (`Clock::arrival` in `rs/moq-mux/src/catalog/producer.rs`), which is also the first published @@ -32,7 +35,8 @@ fed one encoded stream. Capture is out: two encoders never align. offsets a joining importer by its arrival time. Decided in the 2026-10-05 audit: redundant importers derive the wall anchor from the input (its PTS or PCR) or from the shared epoch, never from arrival, so two catalogs of - one stream are identical. Rejected: narrowing the contract to exclude the + one stream are identical. They pass it through Shared import clock's + `Input`/offset API rather than a second anchoring path (2026-10-06 audit). Rejected: narrowing the contract to exclude the catalog clock. - An importer announces only once it knows its tracks, so it never refuses a track the incumbent serves. @@ -46,6 +50,7 @@ issues' 1+1 setup (one relay, two `import ts` sharing one `--epoch`, two ## Required +- [Shared import clock](/quest/m1/shared-clock.md) - the `Input`/offset API this supplies an input-derived anchor through - [Remove `--hop`](/quest/m0/broadcast-epoch/hop-removal.md) - the shared `--epoch` this keys a redundant pair on ## Closes @@ -56,4 +61,3 @@ issues' 1+1 setup (one relay, two `import ts` sharing one `--epoch`, two ## Related - [Broadcast epochs](/quest/m0/broadcast-epoch/README.md) - a redundant pair shares one epoch -- [Shared import clock](/quest/m1/shared-clock.md) - the clock and offset rules an importer joining an existing clock follows diff --git a/quest/m1/ietf-fetch-location.md b/quest/m1/ietf-fetch-location.md index daf1f8bf4e7..944d3f479a7 100644 --- a/quest/m1/ietf-fetch-location.md +++ b/quest/m1/ietf-fetch-location.md @@ -5,7 +5,10 @@ On drafts 20 and later, a FETCH whose LOCATION_FILTER stays within one group is answered the way drafts 14 to 19 answer a standalone FETCH: from cache, with a miss fetched upstream, and an upstream refusal passed through. A range -touching several groups is refused `NOT_SUPPORTED`, as on older drafts. +touching several groups follows the same group-span rule as drafts 14 to 19, +through the shared path: refused `NOT_SUPPORTED` until +[moq-transport ranges](/quest/m1/subscribe-ranges/ietf.md) lifts that +refusal for every draft. ## Plan @@ -17,3 +20,7 @@ The codec already decodes and encodes the draft-20 layout as FETCH carrying Range Filters stays refused. Update the draft-20 note in `doc/concept/standard.md`. + +## Related + +- [moq-transport ranges](/quest/m1/subscribe-ranges/ietf.md) - lifts the one-group refusal in `run_fetch_stream` for every draft, draft-20 included diff --git a/quest/m1/libmoq-final-release.md b/quest/m1/libmoq-final-release.md index 3378206c87f..e9118158428 100644 --- a/quest/m1/libmoq-final-release.md +++ b/quest/m1/libmoq-final-release.md @@ -5,14 +5,16 @@ 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`. +cut, which must take `main`'s stub for `rs/libmoq` at a version above +0.6.12. `main`'s stub is at 0.6.11, which crates.io already holds as a full +crate, 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-05 the newest release is libmoq 0.6.10 (2026-10-03), again cut -from `release`'s full crate, not the stub, and the open release PR #4763 -bumps `release`'s full libmoq once more. Check with +As of 2026-10-06 the newest release is libmoq 0.6.12 (2026-10-06), still cut +from `release`'s full crate, not the stub; release PR #4763 merged and +published 0.6.11 the same way. Check with `curl -s https://crates.io/api/v1/crates/libmoq` for a newer `max_version`. diff --git a/quest/m1/mkv-export-delay.md b/quest/m1/mkv-export-delay.md index 4fb5d5fcf67..4c36da4f164 100644 --- a/quest/m1/mkv-export-delay.md +++ b/quest/m1/mkv-export-delay.md @@ -14,9 +14,16 @@ has arrived. Replace it with the release stage from [fixed-delay release](/quest/m1/tstd/delay.md), with the same `--delay` flag and the same late-frame drop. Update `doc/bin/cli.md`. -Public API: the exporter gains a delay setting; breaking only if it replaces -an existing one. Wire: none. +Decided in the 2026-10-06 audit: `--delay` replaces the staleness flag, as it +replaced `--max-age` on TS, so one knob sets both the release delay and the +sources' staleness budget. [Subscriber max-delay](/quest/m1/subscriber-max-delay.md) +(#4917) renames that flag to `--max-delay` first; this quest then replaces +`--max-delay` with `--delay`. Rejected: excluding mkv from the #4917 rename. + +Public API: breaking, the exporter's staleness setting becomes the delay +setting. Wire: none. ## Required +- [Subscriber max-delay](/quest/m1/subscriber-max-delay.md) - renames the staleness flag first, so this replaces `--max-delay` - [Fixed-delay release](/quest/m1/tstd/delay.md) - builds the shared release stage diff --git a/quest/m1/perf/3122-moq-uring-2-5-of-relay-cpu-is-vdso-clock-reads-the-drive.md b/quest/m1/perf/3122-moq-uring-2-5-of-relay-cpu-is-vdso-clock-reads-the-drive.md index 86db0eff186..29f45b7e5be 100644 --- a/quest/m1/perf/3122-moq-uring-2-5-of-relay-cpu-is-vdso-clock-reads-the-drive.md +++ b/quest/m1/perf/3122-moq-uring-2-5-of-relay-cpu-is-vdso-clock-reads-the-drive.md @@ -45,10 +45,10 @@ every QUIC timeout re-arm is an O(log n) map removal and insertion with `Rc` traffic. The #2875 design note called for a timer wheel. Not urgent at these connection counts, but it is on the same hot path and grows with it. -moq-net's runtime is crate-private now, and its drivers already receive the -current instant from their owner (`Runtime::now`, "the latest instant -supplied by the owner", in `rs/moq-net/src/runtime.rs`); moq-uring's worker -passes `Instant::now()` to `driver.poll` once per poll +moq-net's drivers already receive the current instant from their owner +(`Clock::now`, "the latest instant supplied by the owning driver", in +`rs/moq-net/src/time.rs`); moq-uring's worker passes `Instant::now()` to its +single `driver.poll(Instant::now(), ...)` call once per poll (`rs/moq-uring/src/worker.rs`). Sample once per drive turn there and pass that instant through `fire`, the `handle_timeout`, and `poll_transmit`. diff --git a/quest/m1/publish-timestamp.md b/quest/m1/publish-timestamp.md index c29e27a01df..cc5fb137d1c 100644 --- a/quest/m1/publish-timestamp.md +++ b/quest/m1/publish-timestamp.md @@ -59,9 +59,9 @@ with the untimed implementation quests. ## Required -- [JSON and flate namespaces](/quest/m1/ffi-shape/json.md) - moves the data producers this changes, so the two breaks land in order rather than colliding - [moq-net carries untimed frames faithfully](/quest/m1/untimed-model.md) - the model must hold an untimed payload before producers stop filling in now; until lite-07 encodes absence, a lite encoder writes its send time, as producers effectively do today ## Related - [JS publishing never invents a timestamp](/quest/m1/js-publish-timestamp.md) - the same change in the JS packages +- [FFI shape](/quest/m1/ffi-shape/README.md) - #4519 moves the JSON and flate producers this changes into their namespaces; its JSON pilot (#4526) is done, so this waits only for #4519 to land, not for the whole line (2026-10-06 audit) diff --git a/quest/m1/qos/README.md b/quest/m1/qos/README.md index 1ef63ffeb80..0d1b291d8dd 100644 --- a/quest/m1/qos/README.md +++ b/quest/m1/qos/README.md @@ -37,6 +37,14 @@ per-broadcast verdict combining client reports, the relay's starvation, and publisher timeliness, and where it would live. This line only reports the counters. +Decided (2026-10-06 audit): [stats totals and prefix +tracks](/quest/m0/broadcast-epoch/stats-split.md) lands first, since it is m0 +and gates the release; #4133 rebases its histograms onto stats-split's totals +and prefix tracks rather than the per-path `publisher.json` and +`subscriber.json` map rows it writes today, which stats-split retires. +Rejected: #4133 first, which would make stats-split carry the lag histogram +and dropped counters across. + Decided (2026-09-28): the line's moq-stats changes break the published crate. Client stats left the line (2026-09-29) when media stats moved out of moq-stats onto hang tracks, which are additive @@ -47,11 +55,6 @@ on `main`. - [Lag across a splice](/quest/m1/qos/lag-splice.md) - a route switch neither loses pending lag weight nor keeps weighing a segment replaced before its first frame; the line does not land until it is fixed -- [Starvation](/quest/m1/qos/starvation.md) - per broadcast, how far behind - the acknowledged frontier of its subscriptions is, in media time, plus the - media dropped before it was acknowledged -- [Final lag sample](/quest/m1/qos/final-lag-sample.md) - a closing - subscription records its last partial interval instead of losing it - [Lag dashboard](/quest/m1/qos/lag-dashboard.md) - the demo stats dashboard shows viewer lag percentiles and dropped media - [Publisher timeliness](/quest/m1/qos/publisher-timeliness.md) - per @@ -60,6 +63,9 @@ on `main`. ## Related +- [Stats totals and prefix tracks](/quest/m0/broadcast-epoch/stats-split.md) - + lands first and replaces the map rows; this line's histograms move onto its + totals and prefix tracks - [Media stats](/quest/m1/stats/README.md) - publishers and viewers report their own media, transport, and playback health, the media half of a verdict - [Loss delay](/quest/m3/cut-through/loss-delay.md) - an ingress counter of diff --git a/quest/m1/qos/final-lag-sample.md b/quest/m1/qos/final-lag-sample.md deleted file mode 100644 index 6b4727e4d26..00000000000 --- a/quest/m1/qos/final-lag-sample.md +++ /dev/null @@ -1,38 +0,0 @@ -# [XS] A closing subscription takes its last lag sample - -## Goal - -When an egress subscription ends, the bytes its track produced since the last -stats tick still land in the `lag` histogram at its final lag, instead of -being lost. A subscription that opens and closes between two ticks is sampled -at least once. - -## Plan - -Lag is sampled only on `Registry::report` ticks: `Counters::sample` in -`rs/moq-net/src/stats.rs` walks weak `FrontierInner` references and prunes -the dead ones without sampling them, so a subscription's last partial -interval disappears with it. - -- The subscription guard and each in-flight group `Delivery` hold a strong - reference, so the frontier dies only once the last of them does. Its drop is - the natural place for one final `sample(now)` into the same counters. -- No double counting: `sample` advances each source's `sampled` bytes under the - frontier lock, so a tick racing the drop leaves nothing for it to count again. -- Test: a subscription that opens and closes between ticks while its track - produces lands those bytes in the histogram; one that closes mid-interval - adds exactly the bytes since its last sample. Note the behaviour in - `doc/concept/stats.md`. - -Public API: none. Wire: none, only the histogram's values change. - -Overlaps the lag-splice quest on the line branch -([#4381](https://github.com/moq-dev/moq/pull/4381)), which also changes how -`FrontierInner::sample` holds `unsampled` weight and when a spliced segment's -source stops being sampled. Land them in sequence on the same sampler, not in -parallel, and make the final drop-time sample keep any weight lag-splice -defers. - -## Required - -- [Starvation](/quest/m1/qos/starvation.md) - the sampler this extends (#4298) diff --git a/quest/m1/qos/lag-dashboard.md b/quest/m1/qos/lag-dashboard.md index 4a2d5f01ae7..f8bac97e2c7 100644 --- a/quest/m1/qos/lag-dashboard.md +++ b/quest/m1/qos/lag-dashboard.md @@ -9,24 +9,28 @@ and per node like the existing counters. ## Plan -- The relay writes both on `publisher.json` rows only. `lag` is a cumulative - byte count per bucket keyed by its upper edge (`"50ms"` to `"5s"`, then - `"inf"`, empty buckets omitted); `dropped` is `{ duration, bytes, groups }` - with the duration in fractional milliseconds. `doc/concept/stats.md` on the - QoS line documents them. Diff two samples for an interval's distribution, - as the dashboard already does to turn cumulative bytes into rates, and sum - nodes bucket by bucket. +- Read both from [stats totals and prefix + tracks](/quest/m0/broadcast-epoch/stats-split.md), which lands first and + retires the `publisher.json` rows they sit on today (decided in the + 2026-10-06 audit); #4133 rebases them onto the egress side of its per-group + totals and prefix tracks. `lag` is a cumulative byte count per bucket keyed + by its upper edge (`"50ms"` to `"5s"`, then `"inf"`, empty buckets + omitted); `dropped` is `{ duration, bytes, groups }` with the duration in + fractional milliseconds. `doc/concept/stats.md` documents them. Diff two + samples within one subscription for an interval's distribution, as the + dashboard already does to turn cumulative bytes into rates, and sum nodes + bucket by bucket. - A percentile read from buckets is a bucket edge, not a point value, and `inf` has no upper edge. Show it as "under X" or interpolate inside the bucket, and say which. - Skip `.`-prefixed system broadcasts, as the existing aggregate does. Lag is - per broadcast, so a per-broadcast view is worth adding if it stays cheap. + per broadcast, so a per-broadcast view reads that broadcast's prefix track + if it stays cheap. - Extend the dashboard's existing relay-stats interfaces; no `@moq/stats` package is planned. Public API: none. Wire: none. -## Required +## Related -- [Starvation](/quest/m1/qos/starvation.md) - the `lag` histogram and - `dropped` counters (#4298) +- [Stats totals and prefix tracks](/quest/m0/broadcast-epoch/stats-split.md) - the totals and prefix tracks the dashboard reads lag from diff --git a/quest/m1/qos/lag-splice.md b/quest/m1/qos/lag-splice.md index 3b506c5d128..f7c4f583a2c 100644 --- a/quest/m1/qos/lag-splice.md +++ b/quest/m1/qos/lag-splice.md @@ -46,7 +46,3 @@ Final lag sample (#4451) changed the same sampler; keep any weight this quest defers in its drop-time sample. Public API: none. Wire: none, only the histogram's values change. - -## Related - -- [Final lag sample](/quest/m1/qos/final-lag-sample.md) - the drop-time sample on the same sampler diff --git a/quest/m1/qos/publisher-timeliness.md b/quest/m1/qos/publisher-timeliness.md index 1a976155556..6d410edf397 100644 --- a/quest/m1/qos/publisher-timeliness.md +++ b/quest/m1/qos/publisher-timeliness.md @@ -10,8 +10,8 @@ cooperation. ## Plan -This is the ingress mirror of [starvation](/quest/m1/qos/starvation.md) and -needs no acknowledgments: the relay is the receiver, and a frame is measured +This is the ingress mirror of the egress `lag` histogram (#4298, on the QoS +line) and needs no acknowledgments: the relay is the receiver, and a frame is measured when its last byte arrives, because a partial frame is not useful. Frame timestamps are relative and jittered with no epoch, so measure each @@ -26,9 +26,11 @@ hour. The window bounds both directions to window length times skew, a few milliseconds, and the re-anchor happens on its own as old minima expire. Document the window length beside the bucket edges. -Aggregate as a byte-weighted cumulative histogram of drift on the -`Role::Subscriber` (ingress) rows, with the same bucket edges and the same -monotonic contract as the starvation histogram. Beside it keep two cumulative +Aggregate as a byte-weighted cumulative histogram of drift on the ingress +(`Role::Subscriber`) side of [stats totals and prefix +tracks](/quest/m0/broadcast-epoch/stats-split.md), which lands first and +retires the per-path rows (decided in the 2026-10-06 audit), with the same +bucket edges and the same monotonic contract as the egress `lag` histogram. Beside it keep two cumulative counters: `timestamp_regressions`, frames whose timestamp is below the previous group's newest timestamp, and `stalls`, gaps between complete frames longer than a documented threshold. Compare only across group boundaries for @@ -53,8 +55,6 @@ excluded. ## Related - [Untimed model](/quest/m1/untimed-model.md) - pre-lite-05 tracks arrive untimed instead of stamped on arrival - -- [Starvation](/quest/m1/qos/starvation.md) - the egress half, same - histogram shape +- [Stats totals and prefix tracks](/quest/m0/broadcast-epoch/stats-split.md) - the totals and prefix tracks the drift histogram lands on - [Media stats](/quest/m1/stats/schema.md) - the publisher's own view of the same uplink, in the transport section of its stats track diff --git a/quest/m1/qos/starvation.md b/quest/m1/qos/starvation.md deleted file mode 100644 index 9e6c21d5520..00000000000 --- a/quest/m1/qos/starvation.md +++ /dev/null @@ -1,87 +0,0 @@ -# [M] Starvation: how far behind viewers are, from the relay - -## Goal - -The relay's `moq-stats` egress rows report, per broadcast, how far behind the -acknowledged frontier of its subscriptions is in media time, and how much -media was abandoned before the peer acknowledged it. A dashboard can read the -relative health of a broadcast's viewers from the relay's point of view -without any per-viewer row on the wire. - -## Plan - -This is the DELIVERY half of a health verdict and closes the byte-backlog -design in #2733: media-time lag is the primary signal, bytes are the weights. - -Define the frontier and the sample separately. Each subscription has an -acknowledged frontier: the newest frame timestamp the peer is known to have -received. At activation, before any frame is chosen or acknowledged, it is -the track's newest produced timestamp at that moment, so a subscription -blocked before its first write is still sampled, its lag is exactly the -media produced since it started, and the backlog it deliberately starts -behind (a latency floor, an ordered start) never reads as starvation. In this slice the frontier advances when a group -stream's FIN is acknowledged, which `Writer::close()` already awaits in -`Subscription::serve_group` in `rs/moq-net/src/lite/publisher.rs` and in the -IETF publisher, and the group's last frame timestamp becomes the frontier. - -The sample is taken on the stats producer's interval tick, not on any ACK -and not in the write path. Each tick walks the active subscriptions and -records `lag = newest frame timestamp the track has produced - the -subscription's frontier`, weighted by the bytes the track produced during -the interval. Both are timestamps on the same track, so the missing epoch -does not matter. The write path cannot be the sampler: a flow-controlled -subscription's serve tasks block inside `write`, which is precisely the -stalled viewer the metric exists to show, and sampling only on ACK would let -that viewer emit nothing and vanish from the snapshot deltas. A periodic -sample keeps it climbing the buckets while its frontier stands still. While -the source is paused nothing is produced, so a tick carries no weight and -the histogram does not move; a viewer's lag stays whatever it was until its -frontier catches up, and reappears in the buckets once production resumes. This slice moves the frontier once per group; -the [frame-granularity quest](/quest/m2/starvation-frames.md) moves it -per frame without changing the wire shape or the sampler, so fix both here. - -Aggregate as a byte-weighted cumulative histogram on `Traffic`, on the -`Role::Publisher` (egress) rows of the existing `(tier, broadcast, role)` -key: fixed log-spaced buckets of media time, each a monotonic byte counter -(bytes produced for a subscription during intervals when its frontier lag -fell in that bucket). Propose buckets at 50 ms, -100 ms, 250 ms, 500 ms, 1 s, 2 s, 5 s, and above, and document the edges in -the stats section of `doc/bin/relay/config.md`. A consumer diffs two snapshots -for a byte-weighted distribution, percentiles, or mean over any interval. -This keeps the crate's cumulative-monotonic contract and the `.z` merge-patch -deltas intact; do not add gauges. - -Account for media that is never acknowledged. When a group stream is reset by -skip, expiry, subscriber stop, or session close before its FIN is -acknowledged, add the media duration the peer never got to a cumulative -`dropped_duration`, with `dropped_bytes` and `dropped_groups` beside it. The -duration is the group's newest written timestamp minus its newest acknowledged -timestamp; at group granularity nothing inside the group is known to be -acknowledged, so the whole written span counts, which over-reports until the -frame slice lands. Say so in the docs. Sum tracks per broadcast: the histogram -is in media time, so mixing audio and video is coherent, and byte weighting -already lets video dominate. - -Mechanics. The track's newest produced timestamp and interval byte count -live beside the existing per-frame bump points on the crate-private -`stats::Meter` in `rs/moq-net/src/stats.rs`; the frontier is a shared cell on -the `stats::Subscription` guard that the serve task advances on ACK and the -sampler reads. The serve task records the newest timestamp it has written to -each group so a drop can be sized. `moq-stats` sums histograms across nodes -in its aggregate consumer the same way it sums counters today. Update the stats -section of `doc/bin/relay/config.md` with the new fields and their meaning. - -Tests: a subscriber that reads at media rate lands in the lowest bucket; a -flow-controlled subscriber walks up the buckets as backlog grows, including -one that stops acknowledging entirely; a skipped group adds its span to -`dropped_duration` and nothing to the histogram beyond the interval samples -already taken while it was outstanding; a -session close mid-group counts as dropped; two subscriptions of one broadcast -sum into one row; the aggregate consumer sums two nodes bucket by bucket. - -## Related - -- [Publisher timeliness](/quest/m1/qos/publisher-timeliness.md) - the ingress - mirror on the `Role::Subscriber` rows -- [Media stats](/quest/m1/stats/README.md) - receiver-side evidence - for the same lag, reported by the viewers themselves diff --git a/quest/m1/quic/README.md b/quest/m1/quic/README.md index 802190d376c..7aa78732ed7 100644 --- a/quest/m1/quic/README.md +++ b/quest/m1/quic/README.md @@ -55,8 +55,6 @@ consumes them. - [Hard fork](/quest/m1/quic/fork/README.md) - quinn in-tree as `moq-quic`, with BBR3 and lazy stream slots, and MoQ switched onto it - [BBR idle burst](/quest/m1/quic/bbr-app-limited.md) - a fork regression proves a burst after a long idle is paced at the learned bandwidth, closing #4219 - [Mark BBR starvation wherever the source runs dry](/quest/m1/quic/bbr-app-limited-edges.md) - partial polls count, local send caps do not, receiver credit is pinned -- [Deliver the application close before io_uring teardown](/quest/m1/quic/uring-close.md) - - the peer receives the final close when the client immediately stops its worker - [Reliable stream reset](/quest/m1/quic/reliable-reset.md) - `RESET_STREAM_AT`, so a reset WebTransport stream still delivers its header - [Hierarchical stream scheduling](/quest/m1/quic/scheduler.md) - strict @@ -72,8 +70,6 @@ consumes them. - [Scope track priority](/quest/m1/track-priority-scope.md) - the per-broadcast fairness policy on cluster sessions -- [Starvation](/quest/m1/qos/starvation.md) - the first consumer of ACK - progress: how far behind viewers are, from the relay's point of view - [Discover media headroom](/quest/m2/quic-probe.md) - test useful-media pacing before adding redundant probe traffic - [L4S on the backbone](/quest/m2/quic-ecn.md) - an ECT(1) option in the fork, an `ecn` config knob, and a dualpi2 measurement - [Careful resume on reconnect](/quest/m2/quic-careful-resume.md) - a redial starts at the previous connection's rate diff --git a/quest/m1/quic/uring-close.md b/quest/m1/quic/uring-close.md deleted file mode 100644 index 68971a73f69..00000000000 --- a/quest/m1/quic/uring-close.md +++ /dev/null @@ -1,24 +0,0 @@ -# [M] Deliver the application close before io_uring teardown - -## Goal - -Make `moq-uring::teardown::a_published_close_has_already_left_the_client` -consistently deliver the application close code and reason when the client -stops its worker immediately after `poll_closed` completes. - -During local validation of [#3649](https://github.com/moq-dev/moq/pull/3649), -`just test` passed 4,082 tests but this test failed after 10.4 seconds: the -server saw `connection timed out` instead of application close code 42 and -reason `done`. The isolated run passed in 0.1 seconds. The PR did not change -`rs/moq-uring` or its teardown test. - -Reproduce on a Linux kernel supporting the io_uring worker, both in isolation -and alongside the full test suite. Determine where the final close is lost -between QUIC output, UDP submission, and worker teardown. Preserve the existing -close contract; do not mask the failure with a retry or a longer timeout. - -## Plan - -#4431 (2f4b78581) fixed the same root cause, a paced CONNECTION_CLOSE lost -at teardown. Re-run this test under the #4431 stress setup (8 stress-ng hogs -on one CPU); if it passes, delete this quest. diff --git a/quest/m1/route-wakes.md b/quest/m1/route-wakes.md index d4f1b455700..d73d2c25763 100644 --- a/quest/m1/route-wakes.md +++ b/quest/m1/route-wakes.md @@ -17,15 +17,15 @@ no `select`, track poll, or deadline scan for a front that keeps its route. ## Plan -Facts from the wildcard line (`rs/moq-net/src/model/origin.rs` there): +Facts from `main` (`rs/moq-net/src/model/origin.rs`, since Wildcard landed in +#4403): - Every route change calls `sync_route`, which calls `routes.poke_below(prefix)`. That bumps a counter-only `Watch` on every path below the prefix, so a woken front cannot tell what changed. Each re-runs `select` under the table read lock (`retain_routes`, then `best_route`, - which hashes every pool member unless a serving front's route still - serves; that `Pin::Stay` short-circuit goes with the follow-the-best-route - rule below), then the driver polls every track and rescans deadlines. + which hashes every pool member), then the driver polls every track and + rescans deadlines. `Pin` is gone (#4741). - `route_order` is rendezvous-style: FNV over the path and hop ids, lowest wins. A join moves only paths the newcomer wins, a leave only paths the leaver was winning. @@ -67,11 +67,9 @@ served root is withdrawn. Keep `pool_resolve` unchanged. Public API: none. Wire: none. -## Required - -- [Wildcard](/quest/m0/wildcard/README.md) - `sync_route`, `poke_below`, and the `origin/pool_churn` bench this reworks exist only on its line branch - ## Related +- [Wildcard](/quest/m0/wildcard/README.md) - landed (#4403) the `sync_route`, `poke_below`, and `origin/pool_churn` bench this reworks + - [Front deadlines](/quest/m1/front-deadline-index.md) - each spurious wake also pays that per-track poll and deadline scan - [Front parking](/quest/m1/origin-front-parks.md) - replaces the `routed_broadcast` watch loop this wakes diff --git a/quest/m1/rs2ts/translator.md b/quest/m1/rs2ts/translator.md index 21b5cd03ead..bd599f40f22 100644 --- a/quest/m1/rs2ts/translator.md +++ b/quest/m1/rs2ts/translator.md @@ -20,9 +20,10 @@ Mapping decided in planning: - Structs become classes, enums discriminated unions, traits interfaces. `Option` is `T | undefined`, `&[u8]` a `Uint8Array` view with no copy. That collapses a nested `Option`, whose states the source relies on: - `model/track.rs::first_start` returns `Option>` to tell - "no successor" from an unstamped one, and `reach` behaves differently for - each. Recommendation: the subset lint rejects nested `Option`, and the source + `model/resume.rs`'s `end()` returns `Option>>` to tell a + front that may still replace its copy (`None`) from one that concluded + (`Some(None)`), and its `until: Option>` does the same for a + replaced route. Recommendation: the subset lint rejects nested `Option`, and the source names those states with an enum; a tagged TypeScript form for the inner `Option` is the alternative. Either way nested states never merge silently. - `Drop` becomes an explicit `drop()` at each MIR drop point, exposed as diff --git a/quest/m1/shared-clock.md b/quest/m1/shared-clock.md index cad69a29ed2..7ce67c18231 100644 --- a/quest/m1/shared-clock.md +++ b/quest/m1/shared-clock.md @@ -96,6 +96,17 @@ Decided (2026-10-05): never share a clock), and deferring to #2279 (this quest is what makes the splice times stale). +Decided in the 2026-10-06 audit (with +[Same-epoch importers](/quest/m1/hop-aligned-import.md)): + +- A same-epoch (redundant) importer's offset or anchor comes from its input + (its PTS or PCR) or from the shared epoch, never from `clock.now()`. It + supplies that input-derived anchor through this quest's `Input`/offset API, + so Same-epoch importers only provides it, and two importers of one stream + publish identical timestamps even when the clock is already fixed. The + arrival-derived `clock.now() - first_pts` above stays the rule for every + other importer. Rejected: keeping the two quests' anchoring separate. + Guidance: - The offset is signed: a stream starting at 3600 s on a clock reading 10 s @@ -129,7 +140,8 @@ Guidance: decodes at the frame timestamp, in Rust and JS. Public API: `catalog::Input`, `catalog::Producer::input`, and -`Input::reserve` are new. `catalog::Producer::clock()` and the first publish +`Input::reserve` are new, and `Input` accepts an input-derived anchor in +place of the arrival one. `catalog::Producer::clock()` and the first publish now fix the mapping, and importers no longer publish verbatim PTS when the clock was already taken or set with `Config::with_clock`. Wire: none. @@ -140,4 +152,5 @@ Wire: none. ## Related +- [Same-epoch importers](/quest/m1/hop-aligned-import.md) - supplies the input-derived anchor through this quest's API - [Audio capture time](/quest/m2/audio-capture-time.md) - maps audio's capture timeline onto the broadcast clock once per open diff --git a/quest/m1/stats/README.md b/quest/m1/stats/README.md index 35c1e31e385..5eabf51bd80 100644 --- a/quest/m1/stats/README.md +++ b/quest/m1/stats/README.md @@ -31,16 +31,17 @@ 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::Track } }`, 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. -- **Keyed by rendition alias** (2026-09-29). Both snapshots key by the - catalog's rendition keys, which [catalog track - alias](/quest/m1/catalog-track-alias.md) makes aliases unique within a - catalog. Nothing repeats the catalog's `video`/`audio` nesting; the kind +- **Keyed by rendition ID** (2026-09-29). Both snapshots key by the + catalog's rendition keys, which [catalog rendition + IDs](/quest/m1/catalog-track-alias.md) make IDs unique across video and + audio within a catalog, refusing a cross-kind duplicate (2026-10-06 audit). + Nothing repeats the catalog's `video`/`audio` nesting; the kind comes from the catalog entry. A viewer reports a rendition that references - another broadcast to the catalog that lists it, under its alias there. The + another broadcast to the catalog that lists it, under its ID there. The publisher's own snapshot covers only renditions it writes and omits referenced ones, whose sender reports them in its own catalog. Reason: a track name alone collides once a catalog lists renditions from @@ -70,7 +71,7 @@ kind. - **Trust is the token prefix** (2026-09-29). Whoever the application's tokens let publish under the echo path may report, and no report is authenticated beyond that. -- **Feedback track: one snapshot**, `{ transport, renditions: { : +- **Feedback track: one snapshot**, `{ transport, renditions: { : echo::Track } }`, 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 @@ -108,8 +109,8 @@ connection view and the dashboard flow. ## Required -- [Catalog track alias](/quest/m1/catalog-track-alias.md) - rendition keys - become aliases, the key both snapshots use +- [Catalog rendition IDs](/quest/m1/catalog-track-alias.md) - rendition keys + become IDs unique across kinds, the key both snapshots use - [Schema](/quest/m1/stats/schema.md) - hang defines the `stats` and `echo` catalog sections, their snapshot types, and the draft text - [Rust reporters](/quest/m1/stats/rust.md) - the CLI, players, encoders, and diff --git a/quest/m1/stats/health.md b/quest/m1/stats/health.md index fd2e81a8967..ed71a25abc1 100644 --- a/quest/m1/stats/health.md +++ b/quest/m1/stats/health.md @@ -26,7 +26,7 @@ deferring the model. The model reads the shapes this line settled after [#4510](https://github.com/moq-dev/moq/pull/4510): the publisher's `stats` -track named in the catalog, keyed by rendition alias, and viewers' `.echo` +track named in the catalog, keyed by rendition ID, and viewers' `.echo` broadcasts. moq.pro's plan predates that and read client stats from a `.stats` broadcast, which #4510 replaced with these tracks (the relay's own `.stats/node/` is unchanged); do not revive the client one. diff --git a/quest/m1/stats/js.md b/quest/m1/stats/js.md index 01d5c23531b..ac6eaf2d4d1 100644 --- a/quest/m1/stats/js.md +++ b/quest/m1/stats/js.md @@ -11,7 +11,7 @@ echo path of a catalog soliciting feedback. Both use the `@moq/hang` schemas. - `js/publish`: - The encoder `Stats` signals are already per rendition (`frames`, `bytes`, `keyframes`). Add drops and the target bitrate. - - Fold them into the stats snapshot keyed by rendition alias, and write it + - Fold them into the stats snapshot keyed by rendition ID, and write it through the `moq-json` snapshot producer with its `.z` sibling. A `stats` attribute enables it. - `transport` carries PROBE rtt only, because `WebTransport.getStats()` @@ -23,7 +23,7 @@ echo path of a catalog soliciting feedback. Both use the `@moq/hang` schemas. - An `echo` attribute names the viewer, refused unless it is one path segment, as in Rust. Once a watched catalog carries an `echo` section, the element resolves the echo path against the broadcast, - appends `.echo`, and publishes there with the fixed feedback track, keyed by rendition alias, the same rule as + appends `.echo`, and publishes there with the fixed feedback track, keyed by rendition ID, the same rule as Rust, and reconciles it on each catalog update like Rust. - Watch elements on one connection watching the same catalog under one name share its producer, because a second `createBroadcast` at a path diff --git a/quest/m1/stats/rust.md b/quest/m1/stats/rust.md index 6afa565ed05..5b2df472a52 100644 --- a/quest/m1/stats/rust.md +++ b/quest/m1/stats/rust.md @@ -31,7 +31,7 @@ - Feedback: after reading a catalog with an `echo` section, the player resolves the echo path against the broadcast, appends `.echo`, and publishes there with the fixed feedback - track, keyed by the catalog's rendition aliases. Each catalog update + track, keyed by the catalog's rendition IDs. Each catalog update reconciles it: a removed `echo` section, a changed path, or no longer watching unannounces the old broadcast. A name that is not a single path segment is refused at parse time. diff --git a/quest/m1/stats/schema.md b/quest/m1/stats/schema.md index 31d56e4481e..49ca779efc6 100644 --- a/quest/m1/stats/schema.md +++ b/quest/m1/stats/schema.md @@ -28,8 +28,8 @@ and the hang draft specs both. Nothing produces them yet. - stalls, stalled duration, and underruns; - the newest media timestamp received, with the wall time it arrived; - the playout latency, as a gauge. -- Both snapshots key by the catalog's rendition alias. Feedback covers a - rendition that references another broadcast under its alias in the catalog +- Both snapshots key by the catalog's rendition ID. Feedback covers a + rendition that references another broadcast under its ID in the catalog that lists it; the publisher snapshot omits referenced renditions (decided in the [README](/quest/m1/stats/README.md)). - `Transport` is shared: rtt, estimated rate, bytes and packets lost, and @@ -58,8 +58,8 @@ and the hang draft specs both. Nothing produces them yet. ## Required -- [Catalog track alias](/quest/m1/catalog-track-alias.md) - the rendition - alias both snapshots key by +- [Catalog rendition IDs](/quest/m1/catalog-track-alias.md) - the rendition + ID both snapshots key by ## Related diff --git a/quest/m1/subscribe-ranges/README.md b/quest/m1/subscribe-ranges/README.md index 5ed8d2c2fb8..feac5d0355e 100644 --- a/quest/m1/subscribe-ranges/README.md +++ b/quest/m1/subscribe-ranges/README.md @@ -52,10 +52,10 @@ every sequence delivered or dropped. ## Required - [Model ranges](/quest/m1/subscribe-ranges/model.md) - `Subscription` carries ranges and order, and `Dynamic` fills misses by range +- [SUBSCRIBE_DROP](/quest/m1/subscribe-drop.md) - publishers name every group they won't deliver - [Lite-07 ranges](/quest/m1/subscribe-ranges/lite.md) - the lite-07 wire, Rust publisher and subscriber, and the draft - [moq-transport ranges](/quest/m1/subscribe-ranges/ietf.md) - range FETCH upstream per missing run, and non-blocking FETCH served downstream - [JS ranges](/quest/m1/subscribe-ranges/js.md) - `@moq/net` model and lite-07 parity -- [SUBSCRIBE_DROP](/quest/m1/subscribe-drop.md) - publishers name every group they won't deliver ## Related diff --git a/quest/m1/subscribe-ranges/ietf.md b/quest/m1/subscribe-ranges/ietf.md index 08dcd281fcb..4c3d9eed16f 100644 --- a/quest/m1/subscribe-ranges/ietf.md +++ b/quest/m1/subscribe-ranges/ietf.md @@ -16,6 +16,16 @@ one stream. Absent sequences between the FETCH's groups become drops, and a group already cached is discarded as a duplicate. Cap a downstream FETCH at the Largest Object, as the moq-transport drafts require. +Serving downstream lifts the one-group refusal ("FETCH spanning several +groups not supported") in `run_fetch_stream` +(`rs/moq-net/src/ietf/publisher.rs`) for every draft, including draft-20's +`FetchType::Filtered` once [Draft-20 FETCH](/quest/m1/ietf-fetch-location.md) +serves it. + ## Required - [Model ranges](/quest/m1/subscribe-ranges/model.md) - the range requests this answers + +## Related + +- [Draft-20 FETCH](/quest/m1/ietf-fetch-location.md) - serves draft-20 FETCH through the same group-span path this widens diff --git a/quest/m1/subscriber-max-delay.md b/quest/m1/subscriber-max-delay.md index cf0296afd70..2f06d05e961 100644 --- a/quest/m1/subscriber-max-delay.md +++ b/quest/m1/subscriber-max-delay.md @@ -22,7 +22,11 @@ Use different names for these different roles rather than rename both. - Rename subscriber/export staleness CLI flags from `--max-age` to `--max-delay`, except T-STD export, whose own quest unifies presentation and staleness under `--delay`. Keep `moq play --delay` unchanged. - Publisher/import retention remains `--max-age`. + Publisher/import retention remains `--max-age`. flv and mkv take the + rename here and move to `--delay` later, in + [FLV export delay](/quest/m1/flv-export-delay.md) and + [MKV export delay](/quest/m1/mkv-export-delay.md) (decided in the + 2026-10-06 audit). - Keep the wire field identifiers, encoding, and interpretation unchanged. Describe subscriber staleness consistently in the matching draft and docs; a terminology edit must not accidentally rename the publisher's field. diff --git a/quest/m1/track-priority-scope.md b/quest/m1/track-priority-scope.md index 48c57b375ad..5f67831fa0a 100644 --- a/quest/m1/track-priority-scope.md +++ b/quest/m1/track-priority-scope.md @@ -61,7 +61,11 @@ Direction to settle in the draft first, then the code: between equal subscriber priorities in `Priority::cmp` (`rs/moq-net/src/lite/priority.rs:48`). The [ladder controller](/quest/m3/ladder/controller.md), now in m3, wants that - tiebreak; this quest owns the answer so the controller only consumes it. + tiebreak; this quest owns the answer and the code change, so the controller + only consumes it (2026-10-06 audit). Either way, make the docs agree: + `Info::priority` in `rs/moq-net/src/model/track.rs` already claims the + tiebreak, and the allocator doc in `rs/moq-net/src/model/bandwidth.rs` + presents send order as unrelated. - A per-session cap on distinct ranks is a scheduling detail; whatever replaces the 255-entry sort must stay O(log n) per group under chat-shaped churn. @@ -80,7 +84,5 @@ change. ## Related -- [Starvation](/quest/m1/qos/starvation.md) - the relay-side signal that - shows a starved subscription - [Signed priority](/quest/m2/signed-priority.md) - changes the priority type, not which streams it competes with diff --git a/quest/m1/transport-upgrade/README.md b/quest/m1/transport-upgrade/README.md index 49b24267859..8322e071323 100644 --- a/quest/m1/transport-upgrade/README.md +++ b/quest/m1/transport-upgrade/README.md @@ -3,15 +3,19 @@ ## Goal A session that came up over the WebSocket fallback moves to QUIC once the -QUIC handshake completes, without dropping a group. Today `https://` races -QUIC against WebSocket, WebSocket wins whenever QUIC is slower than the head +QUIC handshake completes, without dropping a group. `https://` races QUIC +against WebSocket, and WebSocket wins whenever QUIC is slower than the head start plus a TCP+TLS+upgrade round trip (a lost Initial, a slow first -WebTransport dial), and the loser is closed: the session then spends its whole -life on a reliable transport that cannot shed load under congestion, and the -"WebSocket won" memo removes QUIC's head start from every later connect in -that process or page. The end state: when WebSocket wins, the QUIC dial keeps +WebTransport dial). The end state: when WebSocket wins, the QUIC dial keeps going; if it lands, the reconnect loop attaches the QUIC session, hands the -routes over, sends a GOAWAY on the WebSocket session, and drains it. When QUIC wins, WebSocket is closed immediately, as today. +routes over, sends a GOAWAY on the WebSocket session, drains it, and forgets +the "WebSocket won" memo. When QUIC wins, WebSocket is closed immediately. + +The Rust `moq_tokio::Connection` does this since #4180 +(`websocket_upgrades_to_quic`, `a_refused_upgrade_keeps_websocket` in +`rs/moq-tokio/src/connection.rs`). What remains is the `js/net` half, where +the loser is still closed and the memo never forgotten, plus the two +children below. Non-goals: migrating between IPv6 and IPv4. That race is inside the QUIC dial, before any MoQ session exists, the loser has no claim to being the better path @@ -38,7 +42,7 @@ GOAWAY handover landed with the drain line, but it does not resume tracks yet; the JS half requires [JS track handover](/quest/m1/js-group-handover.md) for that. -Shared decisions: +Shared decisions, which Rust implements and [JavaScript](/quest/m1/transport-upgrade/js.md) mirrors: - The race returns the winner plus the still-pending QUIC dial when WebSocket wins. The attempt's connect deadline bounds that dial; no extra deadline. @@ -64,6 +68,6 @@ Shared decisions: ## Required +- [JS qmux finish](/quest/m1/transport-upgrade/js-qmux-finish.md) - `@moq/qmux` reports a cleanly finished send stream as closed without error - [JavaScript](/quest/m1/transport-upgrade/js.md) - js/net keeps the WebTransport dial after WebSocket wins and migrates through the client-goaway handover - [Closed fallback](/quest/m1/transport-upgrade/closed-fallback.md) - a WebSocket session that closes right after connecting falls back to the pending QUIC dial instead of redialing -- [JS qmux finish](/quest/m1/transport-upgrade/js-qmux-finish.md) - `@moq/qmux` reports a cleanly finished send stream as closed without error diff --git a/quest/m1/transport-upgrade/js-qmux-finish.md b/quest/m1/transport-upgrade/js-qmux-finish.md index 1ce689a9bd4..4b14d933e52 100644 --- a/quest/m1/transport-upgrade/js-qmux-finish.md +++ b/quest/m1/transport-upgrade/js-qmux-finish.md @@ -16,7 +16,3 @@ there with a regression test, release, and bump `@moq/qmux` in `js/net`. If not, note that in the PR that deletes this quest. Public API: none. Wire: none. - -## Related - -- [JavaScript upgrade](/quest/m1/transport-upgrade/js.md) - sends a GOAWAY over the WebSocket session on every upgrade diff --git a/quest/m1/transport-upgrade/js.md b/quest/m1/transport-upgrade/js.md index 1b4a1df1f59..0038f64c679 100644 --- a/quest/m1/transport-upgrade/js.md +++ b/quest/m1/transport-upgrade/js.md @@ -52,5 +52,6 @@ session to close on its own or at the handover cap. See the ## Required +- [JS qmux finish](/quest/m1/transport-upgrade/js-qmux-finish.md) - the GOAWAY this sends over the WebSocket session must not read as a failure - [JS track handover](/quest/m1/js-group-handover.md) - tracks carry across the handover this upgrade reuses without a dropped group - [JS GOAWAY requests](/quest/m1/js-goaway-requests.md) - no new request opens on a session that is going away diff --git a/quest/m2/README.md b/quest/m2/README.md index 1d3d3740a3e..06af5ba204d 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -57,7 +57,7 @@ waiting on an upstream release waits in [m4](/quest/m4/README.md). - [Cache shard](/quest/m2/cache-shard.md) - stop hammering one process-global cache line from every worker - [Send depth](/quest/m2/send-depth.md) - moq-net futures prove Send at the default recursion limit, so downstream crates see no nightly lint - [Refusal reasons](/quest/m2/refusal-reasons.md) - refused-session metrics tell an expired token from an invalid one, and count gateway admissions -- [Bench coverage](/quest/m2/bench-coverage.md) - Criterion targets for moq-pattern matching first, then the stats producer, moq-mux containers, the hang catalog, and moq-auth +- [Bench coverage](/quest/m2/bench-coverage.md) - Criterion targets for moq-pattern matching first, then moq-mux containers, the hang catalog, and moq-auth - [Signed priority](/quest/m2/signed-priority.md) - every API priority is an `i8` with 0 as the unset midpoint, and hang's built-ins sit above it - [mTLS on tls://](/quest/m2/tls-listener-mtls.md) - a `tls://` listener can identify a cluster peer by its client certificate - [One transport adapter](/quest/m2/transport-adapter-dedup.md) - the poll transport adapter exists once, and the 64 KiB cap in moq-net's default `poll_read_buf` (not in the adapter) has a test @@ -85,6 +85,7 @@ waiting on an upstream release waits in [m4](/quest/m4/README.md). - [Capture multi-plane PipeWire cameras](/quest/m2/pipewire-camera-planes.md) - I420 and NV12 cameras that deliver one memory block per plane - [#2819](/quest/m2/2819-moq-video-carry-pipewire-dma-bufs-safely-into-the-vulkan.md) - moq-video: validate PipeWire DMA-BUFs into the Vulkan renderer on hardware - [vcpkg registry](/quest/m2/cpp-vcpkg.md) - a registry we own serves the prebuilt package to `vcpkg` manifests +- [Benchmark the moq-stats producer](/quest/m2/stats-producer-bench.md) - a Criterion target for one relay's per-tick stats drain and encode, swept over held paths - [Binary delta stats](/quest/m2/stats-delta.md) - an on-demand varint delta flavor of every stats track, if relay encode CPU still matters after the JSON fixes - [#3115](/quest/m2/3115-moqsink-the-publication-has-no-generation-so-a-flush.md) - moqsink: a flushing restart after EOS opens a new publication generation - [QUIC I/O boundary](/quest/m2/quic-io-boundary.md) - moq-uring receives from the buffer ring and transmits into registered buffers with no copy, once a profile says where diff --git a/quest/m2/announce-shapes.md b/quest/m2/announce-shapes.md index 5befc5175c2..4e1953d52dd 100644 --- a/quest/m2/announce-shapes.md +++ b/quest/m2/announce-shapes.md @@ -99,10 +99,10 @@ lookups, each swept over publishers and subscribers, and extend routes independently of Rust. The slopes decide between a reversed-segment index and dropping suffix shapes from the quest. -A non-prefix advertisement needs authorizing: -[advertise auth](/quest/m3/processor/advertise-auth.md) scopes are prefix-only -today, and this quest extends them to the new shapes. Token patterns -(`moq-pattern`, `moq_auth::Claims`) already match suffixes and do not change. +A shaped advertisement is authorized against the sender's publish claim, as +[Wildcard](/quest/m0/wildcard/README.md) does for prefix advertisements today +(decided in the 2026-10-06 audit). Token patterns (`moq-pattern`, +`moq_auth::Claims`) already match suffixes, so no auth change is needed. ## Related diff --git a/quest/m2/bench-coverage.md b/quest/m2/bench-coverage.md index 9e78d0cd71a..7fa14549dba 100644 --- a/quest/m2/bench-coverage.md +++ b/quest/m2/bench-coverage.md @@ -1,35 +1,22 @@ -# [M] Benchmarks for patterns, stats, containers, catalog, and auth +# [M] Benchmarks for patterns, containers, catalog, and auth ## Goal Hot paths that have no benchmark get Criterion targets, so CI tracks them: -`moq-pattern` path matching, the `moq-stats` producer per tick, `moq-mux` -container import and export per frame, `hang` catalog encode, decode, and +`moq-pattern` path matching, `moq-mux` container import and export per frame, `hang` catalog encode, decode, and update, and `moq-auth` token verification. ## Plan Deferred to m2 in the 2026-09-30 audit. The `moq-pattern` benchmark is the -one worth doing first, and could return to m1 as its own quest. +one worth doing first, and could return to m1 as its own quest. The +`moq-stats` producer benchmark split out into +[its own quest](/quest/m2/stats-producer-bench.md) in the 2026-10-06 audit, +since [Binary delta stats](/quest/m2/stats-delta.md) gates on it alone. - `moq-pattern`: matching swept over pattern count and path depth. Patterns shipped as `rs/moq-pattern` and `js/pattern`, so the matcher gets one bench here, not a second one elsewhere. -- `moq-stats` producer (folded in from the stats producer benchmark): measure - what one relay pays per stats tick to drain its registry and encode the - traffic tracks. #4299 keeps an idle path in every frame while the registry - holds its counters (about 430 B per idle path per plain frame), and nothing - measures that or the per-tick drain as held paths grow. Sweep held paths - (for example 100 to 50k) against tiers and the share of idle versus changed - paths. Report time and allocations per tick, and plain and compressed bytes - per frame the way `rs/moq-stats/benches/decode.rs` reports its table, - including the point where a plain frame nears its size cap. Drive the real - producer path (`process_slot`, the snapshot encoders) over a synthetic - `Registry`; a bench hook is fine if it stays out of the public API. This is - the moq-json snapshot encoder profile - [Binary delta stats](/quest/m2/stats-delta.md) gates on. `decode.rs` is not - in CI either; run both targets in the nightly benchmark smoke, as - `moq-net`'s are. - `moq-mux`: fMP4/CMAF and Annex-B import and export over checked-in or generated fixtures, with throughput in frames and bytes. Keep fixture generation out of the timed region. @@ -47,4 +34,4 @@ Public API: none. Wire: none. ## Related - [Benchmark regressions in CI](/quest/m1/bench-ci.md) - tracks these targets on PRs and nightly -- [Binary delta stats](/quest/m2/stats-delta.md) - its gate needs the stats encode baseline this measures +- [Benchmark the moq-stats producer](/quest/m2/stats-producer-bench.md) - the stats producer target, split out of this quest diff --git a/quest/m2/catalog-tracks.md b/quest/m2/catalog-tracks.md index f55df5fcb6f..9aaeaf8fdf1 100644 --- a/quest/m2/catalog-tracks.md +++ b/quest/m2/catalog-tracks.md @@ -18,7 +18,7 @@ means the same content, and MoQ has no ETag-style invalidation. Audit publishers in Rust and JS for the properties that change mid-track today (codec/config bytes, resolution, audio layout, rendition metadata) and make each one either refuse the change or mint a new identity: a new track -name through a [catalog alias](/quest/m1/catalog-track-alias.md) when the +name through a [catalog rendition ID](/quest/m1/catalog-track-alias.md) when the catalog can keep both, or a new [broadcast epoch](/quest/m0/broadcast-epoch/README.md) when the whole broadcast restarts. The catalog may still add and remove tracks; a removed name is never reused for different content. @@ -28,6 +28,6 @@ JS, and HLS/watch tests. ## Related -- [Catalog track alias](/quest/m1/catalog-track-alias.md) - lets a catalog list a new track name for a changed rendition +- [Catalog rendition IDs](/quest/m1/catalog-track-alias.md) - lets a catalog list a new track name for a changed rendition - [Broadcast epochs](/quest/m0/broadcast-epoch/README.md) - a restart is a new epoch rather than a changed track - [Archive](/quest/m1/archive/README.md) - storage and replay consume the identity contract diff --git a/quest/m2/cpp-vcpkg.md b/quest/m2/cpp-vcpkg.md index 646839fbbe3..2f2b2aa20ac 100644 --- a/quest/m2/cpp-vcpkg.md +++ b/quest/m2/cpp-vcpkg.md @@ -26,7 +26,7 @@ CI on Windows, macOS, and Linux. ## Required -- [Package](/quest/m1/cpp/package.md) - the release tarballs the port fetches +- [C++ through moq-ffi](/quest/m1/cpp/README.md) - the release tarballs the port fetches ## Related diff --git a/quest/m2/flate.md b/quest/m2/flate.md index c00a0124920..64beb25cc3b 100644 --- a/quest/m2/flate.md +++ b/quest/m2/flate.md @@ -37,4 +37,4 @@ routed, and cached like any other. ## Required -- [JSON](/quest/m1/ffi-shape/json.md) - sets the `json` and `flate` namespace pattern this follows +- [FFI shape](/quest/m1/ffi-shape/README.md) - lands the `json` namespace pattern and the `MoqFlate*Producer` constructors this follows diff --git a/quest/m2/ietf-malformed-close.md b/quest/m2/ietf-malformed-close.md index 202bdc7067c..be38fccdfeb 100644 --- a/quest/m2/ietf-malformed-close.md +++ b/quest/m2/ietf-malformed-close.md @@ -43,6 +43,15 @@ runner's scenario name, where it has one: ## Plan +Decided in the 2026-10-06 audit: external draft PR +[#4927](https://github.com/moq-dev/moq/pull/4927) (mondain) owns two cases in +Rust and JS, GROUP_ORDER outside 1..2 and repeated unknown or GREASE Setup +Options. Do not implement them here. When #4927 merges, delete those two +cases from the Goal and their mapping lines (`group.rs`, the `parameters.rs` +GREASE entry); this quest keeps the rest. If #4927 stalls, they come back +here. Rejected: asking the contributor to retarget #4927 at this quest's +branch. + Decided with the maintainer on 2026-10-04 and 2026-10-05: - **Add the missing session codes to the shared registry.** diff --git a/quest/m2/nvenc-pool.md b/quest/m2/nvenc-pool.md index bd6c851e7f8..9bb5ba90fb4 100644 --- a/quest/m2/nvenc-pool.md +++ b/quest/m2/nvenc-pool.md @@ -15,10 +15,5 @@ quest and report the numbers. frames) on every `encode`, and frees them all at the end of the call. Keep a small pool sized by the frames in flight, which is one today since B-frames are off. Re-register a CUDA resource only when its pointer changes. Measure -with the `encode-presets` example from #4099, which so far exists only on the -[OBS moq-video](/quest/m1/obs-moq-video/README.md) line; if that line has not -merged, run it from that branch or add an encode benchmark on main instead. - -## Related - -- [OBS moq-video](/quest/m1/obs-moq-video/README.md) - carries the `encode-presets` example this measures with +with `cargo run -p moq-video --example encode-presets` on `main` (#4080 +landed it). diff --git a/quest/m2/quic-ecn-measure.md b/quest/m2/quic-ecn-measure.md index 32657baf08d..335a9e68723 100644 --- a/quest/m2/quic-ecn-measure.md +++ b/quest/m2/quic-ecn-measure.md @@ -40,6 +40,7 @@ from 1.3.1 is a defective baseline, not evidence that classic ECN cannot help. Decided in the 2026-09-30 audit: moved to m2 with its only consumer, [L4S on the backbone](/quest/m2/quic-ecn.md). -## Required - -- [Hard fork](/quest/m1/quic/fork/README.md) - the change lands in `moq-quic`, not the frozen fork +Decided in the 2026-10-06 audit: no longer waits on the hard fork. No code +lands here, and the pinned moq-noq-proto 2.0.1 already carries the classic +CE response, so the study runs today. The fork stays Required on L4S on the +backbone, which does land code in `moq-quic`. diff --git a/quest/m2/relay-io-uring-package.md b/quest/m2/relay-io-uring-package.md index 7e82c06e82a..e0a3b23c87b 100644 --- a/quest/m2/relay-io-uring-package.md +++ b/quest/m2/relay-io-uring-package.md @@ -37,7 +37,3 @@ Public API: none. Wire: none. must not drop protocols when the ring is on - [Flow-control windows](/quest/m1/uring-flow-control-windows.md) - the `[quic]` section must not be refused at startup on the ring -- [Close before teardown](/quest/m1/quic/uring-close.md) - sessions on the - ring must end with their application close; probably fixed by - [#4431](https://github.com/moq-dev/moq/pull/4431), so re-run its repro - before starting and drop this blocker if it passes diff --git a/quest/m2/starvation-frames.md b/quest/m2/starvation-frames.md index c53282f4e8b..60e7638f5c5 100644 --- a/quest/m2/starvation-frames.md +++ b/quest/m2/starvation-frames.md @@ -54,7 +54,5 @@ unsupported. ## Required -- [Starvation](/quest/m1/qos/starvation.md) - fixes the wire shape and the - group-granularity fallback - [poll_acked on moq-net's send stream](/quest/m2/quic-ack-hook.md) - the hook this samples through diff --git a/quest/m2/stats-delta.md b/quest/m2/stats-delta.md index 9c97471f0eb..1d4967ae650 100644 --- a/quest/m2/stats-delta.md +++ b/quest/m2/stats-delta.md @@ -108,7 +108,7 @@ impact: new on-demand tracks; existing tracks unchanged. ## Required -- [Bench coverage](/quest/m2/bench-coverage.md) - its stats producer benchmark is the moq-json snapshot encoder profile the gate needs +- [Benchmark the moq-stats producer](/quest/m2/stats-producer-bench.md) - the moq-json snapshot encoder profile the gate needs ## Related diff --git a/quest/m2/stats-producer-bench.md b/quest/m2/stats-producer-bench.md new file mode 100644 index 00000000000..0b136476b8d --- /dev/null +++ b/quest/m2/stats-producer-bench.md @@ -0,0 +1,37 @@ +# [S] Benchmark the moq-stats producer + +## Goal + +A Criterion target measures what one relay pays per stats tick to drain its +registry and encode the traffic tracks, swept over held paths and tiers, and +runs in the nightly benchmark smoke. It is the moq-json snapshot encoder +profile [Binary delta stats](/quest/m2/stats-delta.md) gates on. + +## Plan + +Split out of [Bench coverage](/quest/m2/bench-coverage.md) in the 2026-10-06 +audit, so the stats-delta gate waits on this benchmark alone rather than on +four unrelated ones. + +- #4299 keeps an idle path in every frame while the registry holds its + counters (about 430 B per idle path per plain frame), and nothing measures + that or the per-tick drain as held paths grow. Sweep held paths (for + example 100 to 50k) against tiers and the share of idle versus changed + paths. +- Report time and allocations per tick, and plain and compressed bytes per + frame the way `rs/moq-stats/benches/decode.rs` reports its table, + including the point where a plain frame nears its size cap. +- Drive the real producer path (`process_slot`, the snapshot encoders) over + a synthetic `Registry`; a bench hook is fine if it stays out of the public + API. +- `decode.rs` is not in CI either; run both targets in the nightly benchmark + smoke, as `moq-net`'s are. + +Name the target after what it measures, so the CI comment reads without +opening the file. + +Public API: none. Wire: none. + +## Related + +- [Benchmark regressions in CI](/quest/m1/bench-ci.md) - tracks this target on PRs and nightly diff --git a/quest/m2/tls-listener-mtls.md b/quest/m2/tls-listener-mtls.md index 74c7a843c85..a550c06e176 100644 --- a/quest/m2/tls-listener-mtls.md +++ b/quest/m2/tls-listener-mtls.md @@ -13,8 +13,24 @@ 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. Either expose it upstream in qmux or own the TLS accept here. +without it. + +Decided by the maintainer (2026-10-06): the peer certificate comes from a +new accessor on qmux's TLS session upstream (qmux is in our org), then a +dependency bump here. Rejected: owning the TLS accept in this repository, or +leaving it open. + Keep the token path working on the same listener, and keep pinned peers and CA roots meaning the same as on QUIC. +[Relay client-CA validation](/quest/m1/relay-auth-client-ca.md) refuses a +listener TLS client CA on a stream-only relay (the `NoBackend` case in +`rs/moq-relay/src/relay.rs`), since nothing checks it there. Once a `tls://` +listener verifies the certificate, this quest lifts that refusal for a relay +with a `tls://` listener and updates the refusal's test. + Public API: relay config may gain a listener option. Wire: none. + +## Required + +- [Relay client-CA validation](/quest/m1/relay-auth-client-ca.md) - adds the stream-only client-CA refusal this lifts for `tls://` diff --git a/quest/m2/ts-eac3.md b/quest/m2/ts-eac3.md index 424fba20598..d870b9d2f96 100644 --- a/quest/m2/ts-eac3.md +++ b/quest/m2/ts-eac3.md @@ -30,4 +30,4 @@ with no buffer model, so a multi-frame PES can overflow B. ## Required -- [T-STD TS export](/quest/m1/tstd/README.md) - the per-PID schedule this joins +- [Fixed-delay muxing](/quest/m1/tstd/delay.md) - the per-PID admission this joins, which #4645 lands; the rest of the T-STD line is not needed (2026-10-06 audit) diff --git a/quest/m2/tstd-controls.md b/quest/m2/tstd-controls.md index 07ef58ed98d..15ea07c6920 100644 --- a/quest/m2/tstd-controls.md +++ b/quest/m2/tstd-controls.md @@ -25,4 +25,4 @@ The only positive reference carries MPEG audio, but the export emits AAC. ## Required -- [T-STD TS export](/quest/m1/tstd/README.md) - `just test ts-tstd` and `tstd-controls.py` land with it (#4645) +- [Fixed-delay muxing](/quest/m1/tstd/delay.md) - #4645 edits the same `tstd-controls.py`; the harness itself (`just test ts-tstd`) is already on `main` from #4640, so this does not wait on the whole T-STD line (2026-10-06 audit) diff --git a/quest/m2/vaapi-vulkan-import.md b/quest/m2/vaapi-vulkan-import.md index caec02d989d..baeae0a2e5e 100644 --- a/quest/m2/vaapi-vulkan-import.md +++ b/quest/m2/vaapi-vulkan-import.md @@ -53,7 +53,11 @@ sync_file import, so an explicit-sync producer races it. Test: an ignored hardware test in `just rs gpu`'s Intel branch renders on the iGPU with Vulkan, exports a DMA-BUF image and an explicit-sync timeline, and -encodes H.264 at two sizes; the output decodes, and the slots recycle. +encodes H.264 at two sizes; the output decodes, and the slots recycle. On +the two-node host, also check that a surface carrying `renderD129`'s +`dev_t`, or `MOQ_VAAPI_DEVICE` naming it, opens that node rather than the +first render node: the second-node check moved here from +[Video hardware validation](/quest/m3/video-hardware.md) (2026-10-06 audit). Public API: none beyond the surface quest's. Wire: none. diff --git a/quest/m3/README.md b/quest/m3/README.md index 49a557f6a9c..b2ea04416b2 100644 --- a/quest/m3/README.md +++ b/quest/m3/README.md @@ -50,9 +50,7 @@ deferred for having no named consumer moved here. - [V4L2 refresh](/quest/m3/intra-refresh-v4l2.md) - the V4L2 backend encodes refresh mode - [Bindings](/quest/m3/intra-refresh-bindings.md) - moq-ffi and every wrapper expose refresh mode, additive on the ffi-shape `Gop` enum - [P2P](/quest/m3/p2p/README.md) - opted-in clients serve each other over data channels and iroh while the relay stays the rendezvous and the fallback, under application policy -- [Route trust](/quest/m3/route-trust.md) - a peer grant lets a client link advertise the nodes behind it as one identity; P2P is its first consumer - [Ladder](/quest/m3/ladder/README.md) - a transcode ladder adapts to the uplink it publishes over, instead of encoding every live rung at its ceiling -- [Processor](/quest/m3/processor/README.md) - a customer-run worker publishes an on-demand contribution under its own service prefix with scoped access - [fMP4 emsg](/quest/m3/emsg.md) - settles the shared framing and missing-data semantics before this section adopts them - [ID3 catalog section](/quest/m3/id3.md) - timed ID3 as a first-class container-neutral catalog section - [FLV script tags](/quest/m3/flv-script.md) - onMetaData and AMF data messages survive RTMP and FLV import diff --git a/quest/m3/closure-counters.md b/quest/m3/closure-counters.md index 174f12f26d9..7039797360c 100644 --- a/quest/m3/closure-counters.md +++ b/quest/m3/closure-counters.md @@ -2,29 +2,32 @@ ## Goal -`moq_stats::aggregate` has one stated, tested rule for what a consumer reads -when a departed node returns: a same-lifetime rejoin never regresses a closure -counter, and a genuine restart is either floored to the last value or an -explicit fresh segment. The rule is documented as a consumer-facing contract. +`moq_stats::aggregate` has one stated, tested rule for whether a same-lifetime +rejoin within the grace may regress the `*_ended` counters that +`Traffic::retire` advanced on departure, documented beside the existing +restart contract in `rs/moq-stats/src/aggregate.rs`. ## Plan Deferred in the 2026-09-30 audit and moved to m3 in the 2026-10-05 audit: no named consumer. -The stickiness fix ([moq#3625](https://github.com/moq-dev/moq/pull/3625)) -keeps a departed node's last `Traffic` contribution and drops `Presence` -immediately. Its review raised an open P2: retiring the gauges advances the -open/closed pairs, so the next raw frame from the same node can show a lower -`*_closed` value and regress a monotonic reading. - -Two directions were discussed, without a decision: - -- Retire gauges on departure (current), accepting a closure-counter regression - on rejoin. -- Keep the last gauges as a floor, which suppresses a still-live node's gauges - after a transient reader failure. - -Settle which counters must be monotonic and over what lifetime, and whether the -fresh-segment contract already permits a per-node closure reset or only a -cumulative traffic regression. The answer is a consumer-facing contract, so -document it in `moq-stats` rather than leaving it as an aggregate detail. +Decided by [#4874](https://github.com/moq-dev/moq/pull/4874) (recorded in the +2026-10-06 audit): the restart and after-grace contract. A node returning +within `Config::grace` with lower counters regresses the merged counter, the +same fresh-segment rule a single node's restart follows; after the grace its +contribution folds into one retired total. `return_within_grace_resumes_counters` +and `return_after_grace_counts_twice` pin it. + +Still open: `Traffic::retire` raises `announces_ended`, `broadcasts_ended`, +and `subscriptions_ended` to their `*_started` on departure, so the next raw +frame from the same node, rejoining within the grace with its boot-lifetime +counters intact, can show a lower `*_ended` value. The stickiness fix +([moq#3625](https://github.com/moq-dev/moq/pull/3625)) review raised it as a +P2. Two directions were discussed: + +- Retire on departure (current), accepting an `*_ended` regression on rejoin. +- Keep the retired values as a floor until the node's own counters pass + them, which suppresses a still-live node's closures after a transient + reader failure. + +Pick one, test it, and document it as part of the consumer-facing contract. diff --git a/quest/m3/ladder/README.md b/quest/m3/ladder/README.md index bf2572440f7..47694c066c9 100644 --- a/quest/m3/ladder/README.md +++ b/quest/m3/ladder/README.md @@ -39,12 +39,10 @@ alone. Send order is a different number today: `Priority::cmp` (`rs/moq-net/src/lite/priority.rs`) ranks first by `Priority.track`, which is the subscriber's priority from SUBSCRIBE (`msg.priority` in `rs/moq-net/src/lite/publisher.rs`), not the publisher's `Info::priority`. -The controller quest makes the publisher's number the tiebreak after it, so -the same number decides what to produce and, among equal subscriber -priorities, what to send first; -[Scope track priority](/quest/m1/track-priority-scope.md) settles what that -ranking means on the first mile versus a cluster session before the -controller depends on it. +[Scope track priority](/quest/m1/track-priority-scope.md) owns making the +publisher's number the tiebreak after it, so the same number decides what to +produce and, among equal subscriber priorities, what to send first; the +controller only consumes that tiebreak. ### Adaptive bands diff --git a/quest/m3/ladder/controller.md b/quest/m3/ladder/controller.md index 92853267a95..3252e41ada6 100644 --- a/quest/m3/ladder/controller.md +++ b/quest/m3/ladder/controller.md @@ -32,16 +32,13 @@ stamped `PRIORITY.video` today (`rs/moq-transcode/src/rung.rs:133`, `rs/moq-transcode/src/lib.rs:352`, `:409`). The allocator fills a tier before the next sees a bit, so that alone protects lower rungs' allocation. -Honor the same number in `Priority::cmp` -(`rs/moq-net/src/lite/priority.rs:48-62`): subscriber priority stays first -(`:51-53`), then the publisher's `track::Info::priority` breaks the tie, -then the subscribe-id fallback (`:56-59`), then newest group. That is what -`Info::priority` already claims to do (`rs/moq-net/src/model/track.rs:100-102`), -and it is what the `BitrateUnsupported` fallback leans on: without it, an +For send order, consume the publisher-priority tiebreak in `Priority::cmp` +that [Scope track priority](/quest/m1/track-priority-scope.md) settles; that +quest owns the code change and the doc fixes (decided in the 2026-10-06 +audit). The `BitrateUnsupported` fallback leans on it: without it, an encoder that cannot retune degrades the whole ladder equally instead of protecting the bottom. A subscriber asking for a higher rendition ahead of a -lower one still gets what it asked for. Fix the allocator doc that presents -send order as unrelated (`rs/moq-net/src/model/bandwidth.rs:229-238`). +lower one still gets what it asked for. ### Keep honest @@ -77,5 +74,6 @@ priority where the subscriber's order wins, and a custom ladder order. ## Related +- [Encoder feedback](/quest/m3/stats-encoder-feedback.md) - feeds viewer stalls into this controller as an input for `moq transcode --echo` - [Hierarchical stream scheduling](/quest/m1/quic/scheduler.md) - supplies the fair subscription buckets beneath this rendition policy diff --git a/quest/m3/p2p/README.md b/quest/m3/p2p/README.md index 472328f69ba..944581c613e 100644 --- a/quest/m3/p2p/README.md +++ b/quest/m3/p2p/README.md @@ -79,8 +79,10 @@ negotiated `maxMessageSize` (256 KiB in Chrome). The cost is head-of-line blocking: one lost chunk stalls every stream until SCTP retransmits it. qmux frames already carry stream offsets, so the follow-up is a qmux transport parameter that permits reordering plus receiver -reassembly, which [qmux on the QUIC core](/quest/m2/quic-qmux.md) provides -for free. Both sides advertise that capability in the roster before the +reassembly. [qmux on the QUIC core](/quest/m2/quic-qmux.md) provides that +reassembly for the Rust qmux only, so [unordered qmux](/quest/m3/p2p/unordered.md) +covers native pairs, and a pair with a browser stays ordered (decided +2026-10-06). Both sides advertise that capability in the roster before the channel is created so it can run `ordered: false`; a loss then stalls only the stream it hit. `RTCDataChannel.ordered` cannot change after the channel exists, so the first qmux record is too late to choose. The @@ -126,15 +128,15 @@ WASM build, so the browser side stays TypeScript. ## Required - [Data channel transport](/quest/m3/p2p/transport.md) - `@moq/p2p` speaks qmux over one ordered RTCDataChannel behind the WebTransport shape `@moq/net` consumes +- [Peer grants](/quest/m3/p2p/peer-grant.md) - the relay issues a hop-bound, asymmetrically signed grant a browser can verify; HS256 keys issue none - [Signaling and policy](/quest/m3/p2p/signal.md) - opted-in peers find each other under the prefix, the application picks who to dial, and the roster-size gate decides whether STUN is used - [Native data channel transport](/quest/m3/p2p/webrtc.md) - `moq-tokio` holds a moq-net session with a browser over str0m with a full ICE agent - [moq-cli joins](/quest/m3/p2p/cli.md) - `--p2p` publishes a roster entry with its iroh endpoint, dials iroh between native peers, and serves browsers as a transit hop - [Direct peers win](/quest/m3/p2p/cost-scopes.md) - a direct link wins only when the peer is the origin or a routing node, by link costs the node sets itself - [Watch opts in](/quest/m3/p2p/watch.md) - one attribute turns it on in the demo and the watcher migrates to the cheapest route - [Harness](/quest/m3/p2p/harness.md) - the Playwright harness and the numbers behind every mapping decision -- [Unordered qmux](/quest/m3/p2p/unordered.md) - qmux tolerates reordering so the data channel runs unordered and a loss stalls one stream +- [Unordered qmux](/quest/m3/p2p/unordered.md) - between native peers, qmux tolerates reordering so the data channel runs unordered and a loss stalls one stream ## Related -- [Peer grants](/quest/m1/auth/peer-grant.md) - the hop-bound credential a direct session presents; HMAC keys issue none - [E2EE](/quest/m1/e2ee/README.md) - what a peer would need if the token scope stopped being the trust boundary diff --git a/quest/m1/auth/peer-grant.md b/quest/m3/p2p/peer-grant.md similarity index 84% rename from quest/m1/auth/peer-grant.md rename to quest/m3/p2p/peer-grant.md index 2861c0d0890..e10b0bf65bd 100644 --- a/quest/m1/auth/peer-grant.md +++ b/quest/m3/p2p/peer-grant.md @@ -45,9 +45,9 @@ against the roster, and verifies AUTH_POP with `cnf`. The origin then serves only the granted paths. A missing, expired, HMAC-signed, or unproven grant is not a grant. -P2P is the first consumer: [Signaling and policy](/quest/m3/p2p/signal.md) presents the grant in band -on each direct session. This quest does not depend on that line. +on each direct session. P2P is its only consumer, so it lives on this line +rather than the auth line (decided in the 2026-09-30 audit). Docs: `doc/bin/relay/auth.md` states that peer grants need an asymmetric key, that HS256 operators get none, and that the public JWKS is not a @@ -61,10 +61,4 @@ Additive. ## Required -- [Lite stream](/quest/m1/auth/lite.md) - the AUTH stream the grant rides -- [Relay tokens](/quest/m1/auth/relay-refresh.md) - the relay owns AUTH and knows the session's paths - -## Related - -- [Signaling and policy](/quest/m3/p2p/signal.md) - the first consumer -- [Route trust](/quest/m3/route-trust.md) - extends the grant with the node ids its holder may advertise routes for, a second purpose beside serving paths +- [In-band auth](/quest/m1/auth/README.md) - relay tokens reach `main` with this line, so the relay owns AUTH and knows the session's paths diff --git a/quest/m3/p2p/signal.md b/quest/m3/p2p/signal.md index 226f2141cf8..98211a01604 100644 --- a/quest/m3/p2p/signal.md +++ b/quest/m3/p2p/signal.md @@ -17,15 +17,15 @@ relay and the origin's announce stream is the roster. Tests inject a peer connection factory so `bun test` covers pairing, the roster gate, and `select` with a fake. -Identity: the roster id is the origin's hop id. The handshake already -declares a random hop per session; move ownership of that value to the -origin, one per tab, as `origin::Producer::empty(Hop::random())` does in -Rust, so the id in the roster is the id in every chain the tab forwards. +Identity: the roster id is the origin's route-layer node id, the id its +ROUTE_START carries ([routes](/quest/m1/cluster-routing/routes.md)), owned +once per tab by the origin rather than once per session. lite-07 drops the +hop list, so no hop chain carries it. Roster: each peer publishes `` with an `info.json` snapshot track: the moq ALPNs it accepts, `webrtc: true`, whether it can run qmux unordered, the presenter public key that -[peer grants](/quest/m1/auth/peer-grant.md) bind, the application's `meta`, +[peer grants](/quest/m3/p2p/peer-grant.md) bind, the application's `meta`, and for native peers an optional `webtransport: { url, fingerprint }` and `iroh` endpoint id. The schema is shared with [moq-cli](/quest/m3/p2p/cli.md). Unordered is advertised here so the dialer @@ -73,7 +73,7 @@ and short-lived, which the peer presents in band with a proof of possession; the other side verifies the relay's signature, the hop id and key against the roster, and AUTH_POP, then serves only the granted paths. Issuance, asymmetric keys, JWKS, PoP, and refresh live in -[Peer grants](/quest/m1/auth/peer-grant.md): HMAC keys cannot be given to +[Peer grants](/quest/m3/p2p/peer-grant.md): HMAC keys cannot be given to browsers without also letting them forge grants, so an HS256-only relay issues nothing. A peer session with no verifiable grant serves nothing; there is no equal-scope shortcut. @@ -81,4 +81,4 @@ there is no equal-scope shortcut. ## Required - [Data channel transport](/quest/m3/p2p/transport.md) -- [Peer grants](/quest/m1/auth/peer-grant.md) - the hop-bound, asymmetrically signed credential a direct session presents; HS256 keys issue none +- [Peer grants](/quest/m3/p2p/peer-grant.md) - the hop-bound, asymmetrically signed credential a direct session presents; HS256 keys issue none diff --git a/quest/m3/p2p/unordered.md b/quest/m3/p2p/unordered.md index 38f2bc518fd..696f6bac43d 100644 --- a/quest/m3/p2p/unordered.md +++ b/quest/m3/p2p/unordered.md @@ -4,18 +4,25 @@ qmux tolerates reordered records when both sides agree to it, so the data channel runs `ordered: false` and a lost SCTP chunk stalls only the stream -whose frame it carried. The browser and native transports flip one option; -the WebSocket and TCP bindings are untouched. +whose frame it carried. Native pairs only: both endpoints must run the Rust +qmux core, and a pair with a browser stays ordered. The native transports +flip one option; the WebSocket and TCP bindings are untouched. ## Plan +Decided in the 2026-10-06 audit: native pairs only. Receiver reassembly comes +from [qmux on the QUIC core](/quest/m2/quic-qmux.md), which ports only the +Rust qmux; the TypeScript `@moq/qmux` keeps its own in-order state machine, +and building offset reassembly there was rejected. So only an endpoint on +the Rust core advertises `unordered`, and a browser never does. + qmux frames already carry stream offsets; the draft's in-order STREAM requirement exists so a receiver can deliver without reassembly. Add a transport parameter, `unordered`, that both sides must send for the relaxed rule to apply, and receiver-side reassembly by offset for STREAM frames when -it does. Once [qmux on the QUIC core](/quest/m2/quic-qmux.md) lands, that -reassembly is QUIC's own receive buffer and this quest is the parameter plus -the binding flip; before it, do not build a second reassembly buffer. One +it does. That reassembly is QUIC's own receive buffer once qmux runs on the +QUIC core, so this quest is the parameter plus the binding flip; do not +build a second reassembly buffer. One record is one SCTP message, so the unordered writer emits frames for only one stream per record; mixing streams in a record would stall both on one lost chunk and is forbidden when unordered is on. @@ -51,5 +58,6 @@ throughput must not regress. ## Required +- [qmux on the QUIC core](/quest/m2/quic-qmux.md) - the Rust qmux whose QUIC receive buffer reassembles by offset - [Harness](/quest/m3/p2p/harness.md) - the loss row this quest is measured against - [Signaling and policy](/quest/m3/p2p/signal.md) - the roster advertisement that decides ordered before the channel exists diff --git a/quest/m3/processor/README.md b/quest/m3/processor/README.md deleted file mode 100644 index 1a127f53ce2..00000000000 --- a/quest/m3/processor/README.md +++ /dev/null @@ -1,48 +0,0 @@ -# External processors - -## Goal - -A customer runs a worker in its own environment, connects outbound to a MoQ -deployment, reads only eligible source media, and publishes an on-demand -contribution under the processor's own prefix, mirroring the source path -(for example `.pro//`, the -[wildcard](/quest/m0/wildcard/README.md) line's derived-output layout). The -platform supplies -registration, scoped credentials, routing, demand, status, and usage -visibility; it does not upload or execute customer code. - -This questline holds the protocol and authorization contracts that make the -seam possible. The hosted halves (processor registration, the contribution -service contract, and credential minting) stay downstream in moq.pro. The -contract is not vision-specific: captioning, moderation, telemetry extraction, -and custom transforms use the same worker lifecycle. - -## Plan - -Decided: derived output follows [Wildcard](/quest/m0/wildcard/README.md)'s -service-prefix layout, not `/.pro`. Processor output does -not use suffix routing ([announcement shapes](/quest/m2/announce-shapes.md) -may add it to moq-lite separately), and a prefix claim needs the variable part of the path -trailing, so the processor claims its prefix and mirrors the source path -beneath it. The source's catalog reaches the output through a -cross-broadcast reference ([media contract](/quest/m3/processor/media-contract.md)). - -Deferred in the 2026-09-30 audit and moved to m3 in the 2026-10-05 audit: no processor customer is committed, -and its end-to-end proof (processor-vision) was deleted. - -## Required - -- [Processor media contract](/quest/m3/processor/media-contract.md) - define - contribution references, source relations, and correlation in the Hang - catalog -- [Advertise-only authorization](/quest/m3/processor/advertise-auth.md) - a - worker may advertise its service prefix without receiving permission to - publish arbitrary paths under it -- [Expiring media grants](/quest/m3/processor/grant-lease.md) - enforce - short-lived exact grants on already-open consumer and producer handles - -## Related - -- [Wildcard advertisements](/quest/m0/wildcard/README.md) - lets a dormant - processor advertise what it could serve without enumerating live sources, - and sets the service-prefix layout diff --git a/quest/m3/processor/advertise-auth.md b/quest/m3/processor/advertise-auth.md deleted file mode 100644 index 3b9d5198700..00000000000 --- a/quest/m3/processor/advertise-auth.md +++ /dev/null @@ -1,43 +0,0 @@ -# [M] Advertise-only authorization - -## Goal - -A v1 worker credential can advertise an allowed prefix without receiving -permission to publish any path under it. Relays enforce advertise and publish -as independent capabilities before external processor credentials are -minted. - -## Plan - -Add an explicit advertise prefix scope to the v1 claims, token SDKs, origin -scope, and relay authorization model. [Wildcard](/quest/m0/wildcard/README.md) -checks advertisements against `moq_auth::Claims.publish` today; this quest -gives them their own scope instead of borrowing the publish one. A concrete -announcement or publish request still requires publish permission, so an -advertise-only worker cannot bypass the demand exchange. - -Decided: the advertise scope is prefix-only. Advertising is prefix-only on -every wire (Wildcard's decision) until [announcement shapes](/quest/m2/announce-shapes.md) -adds exact and suffix shapes to moq-lite, so leading-star and suffix advertise -patterns have nothing to authorize yet; that quest extends this scope. Token claim patterns -keep their suffix support for publish and subscribe. - -Decided: an advertised prefix must overlap the advertise scope, not sit -inside it, and a wider claim only routes the requests the scope covers. This -matches how the relay authorizes advertisements today. - -Preserve current customer credentials in the wire and authorization design: -existing claims retain their current publish-implies-advertise behavior, while -the new v1 claim separates the capabilities. Land the claims, SDK, -origin-scope, relay authorization, and tests without combining the release or -the moq.pro (downstream) pin rollout into this quest. - -Cover overlap and per-request filtering, rebasing, missing versus empty -advertise scope, v0 compatibility, token revalidation, concrete announce, -publish, FETCH, and a prefix demand that receives only an exact short-lived -publish grant. - -## Required - -- [Wildcard](/quest/m0/wildcard/README.md) - the prefix advertisements this scopes -- [Auth](/quest/m1/auth/README.md) - the v1 claims and relay authorization this extends diff --git a/quest/m3/processor/grant-lease.md b/quest/m3/processor/grant-lease.md deleted file mode 100644 index d7691c03b65..00000000000 --- a/quest/m3/processor/grant-lease.md +++ /dev/null @@ -1,39 +0,0 @@ -# [L] Expiring media grants - -## Goal - -An exact native-worker v1 grant deadline applies to media handles already -opened with that grant. A worker cannot keep reading or publishing after demand -ends merely by opening the source and output before its token expires, and its -deadline never attaches to a pooled HTTP or HLS consumer. - -## Plan - -Give processor grants a native-worker audience that HTTP and HLS gateways -reject before opening or reusing a consumer. On a native worker session, an -accepted unpooled `Consumer` or `Producer` retains its grant deadline and is -cancelled when that deadline passes. A refresh with a fresh relay demand -assertion may atomically extend the same exact source and output handles; a -missing, late, broader, or mismatched refresh closes them. Reconnecting with an -expired grant is denied as it is today. - -Keep deadline enforcement in the relay authorization owner rather than a -cooperative worker timer. Build on what exists: `Grant::deadline` -(`rs/moq-auth/src/grant.rs`, #4237) already pins an accepted grant to a fixed -deadline, and `moq_auth::lease` (#3943) re-checks a session on cadence -and reports why it ended. Extend those to the handles a grant opened rather -than adding a second timer. No clock-skew grace: #4368 (merged) made expiry -exact and dropped `CLOCK_SKEW`, so a deadline in the past is expired. - -Cover an idle open handle, active source reads, active publication, refresh -before expiry, refresh after demand ends, disconnect races, HTTP and HLS rejection of -the worker audience, and unrelated traffic continuing through an existing -pooled HLS consumer after a worker grant expires. - -Land the implementation and tests here. The release and the moq.pro -(downstream) pin rollout stay out of this quest. - -## Related - -- [Relay tokens](/quest/m1/auth/relay-refresh.md) - the in-band token a - lease renewal presents diff --git a/quest/m3/processor/media-contract.md b/quest/m3/processor/media-contract.md deleted file mode 100644 index 27c4f3d8cd8..00000000000 --- a/quest/m3/processor/media-contract.md +++ /dev/null @@ -1,37 +0,0 @@ -# [M] Processor media contract - -## Goal - -The Hang catalog defines an external processor contribution reference, output -source relation, and correlation contract that Rust and JavaScript consumers -can resolve without eagerly subscribing to processor output. - -## Plan - -Build on hang's existing per-rendition relative `broadcast` reference -(`broadcast` on the video rendition in `rs/hang/src/catalog/video/mod.rs`, -with its audio, text, JSON, and binary counterparts): it already names another -broadcast relative to the catalog's, and Rust rejects one that escapes. The -processor output lives under the processor's prefix, mirroring the source -path, so the source catalog reaches it through that relative reference. Add -only what it lacks: a catalog-level contribution entry that names a whole -contribution without opening it, which the processor contract specializes. - -The processor publishes a normal Hang contribution rather than an arbitrary -fragment for an edge to merge. Output renditions carry their own schema and an -explicit source relation. - -Frame-derived output identifies the source video rendition, group sequence, and -frame ordinal. Audio and text processors use the source timing contract for -their rendition while retaining their own group identity. Version the relation -for future cross-broadcast inputs; the first version relates only to tracks -carried by the named source broadcast. - -Land Rust and JavaScript catalog bindings, resolver behavior, and fixtures for -video, audio, text, missing output, malformed relations, lazy resolution, and -relative-path escape. The release and the moq.pro (downstream) pin rollout -stay out of this quest. - -## Required - -- [Catalog track alias](/quest/m1/catalog-track-alias.md) - renditions from a source and an output broadcast that share a track name would otherwise collide in one catalog diff --git a/quest/m3/route-trust.md b/quest/m3/route-trust.md deleted file mode 100644 index 571d0e6b854..00000000000 --- a/quest/m3/route-trust.md +++ /dev/null @@ -1,43 +0,0 @@ -# [M] Route trust - -## Goal - -A session may advertise routes only to nodes it is trusted for, so an -authorized publisher cannot claim a cheap route to a CDN relay's node and -attract its subscriptions. Cluster-peer links may advertise any node. A -client link's node ids stay scoped to that session, unless its peer grant -names the nodes it may speak for: then a Starlink drone carries its fleet's -nodes as their real identities, and two uplinks of one swarm reach one origin -instead of two unrelated ones. - -## Plan - -Decided in the 2026-10-05 audit: route trust moved out of the cluster-routing -line to sit beside [P2P](/quest/m3/p2p/README.md), its first consumer, so -cluster routing finishes without it. Session-scoped ids already ship with -routes. - -Decided 2026-10-01: - -- Session scoping is the default and ships with - [Routes and announces](/quest/m1/cluster-routing/routes.md): a scoped id - resolves only that session's own authorized announces and never shadows a - route learned on a trusted link. -- Promotion rides [Peer grants](/quest/m1/auth/peer-grant.md), the - hop-bound credential a direct session already presents; extend it to name - the node ids (or an id range or prefix of them) the holder may advertise. - Decide the shape with the auth line. -- A promoted link is still subject to its upstream attribute and the - down-only bit. - -Test: a client advertising a cheap route to a cluster node does not move any -other session's subscription; two uplinks of one mesh holding grants for the -same drone node advertise one origin node rather than two unrelated ones. - -Public API: the grant's node scope (`moq-token`, `js/token`, and the token -CLI docs). Wire: none beyond the grant. - -## Required - -- [Routes and announces](/quest/m1/cluster-routing/routes.md) - the node ids this scopes -- [Peer grants](/quest/m1/auth/peer-grant.md) - the credential this extends diff --git a/quest/m3/stats-encoder-feedback.md b/quest/m3/stats-encoder-feedback.md index 6d1c55283f2..5c359423ce6 100644 --- a/quest/m3/stats-encoder-feedback.md +++ b/quest/m3/stats-encoder-feedback.md @@ -18,14 +18,14 @@ reads land first in m1. - 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 - feedback track on each, and reads its own renditions by alias. + feedback track on each, and reads its own renditions by ID. - It folds the reports into one signal: the share of viewers stalled over the last interval and their late frame rate, weighted equally per viewer. - The counters are cumulative, so the handle keeps the previous snapshot per viewer and diffs it. A counter that goes backwards means a restarted viewer and resets that baseline. - A viewer counts toward a rendition while its latest snapshot has a row - for that alias, so viewers of other rungs, or ones that switched away, + for that ID, so viewers of other rungs, or ones that switched away, never dilute the share. The announcement, not the age of its last report, keeps that snapshot current: an unchanged snapshot sends no frame, so a quiet healthy viewer must not age out. Diffing already makes one stall long @@ -46,8 +46,13 @@ reads land first in m1. only). It follows this signal only once the grant quest lands; until then the loop drives video. - `moq import --echo ` and `moq transcode --echo ` set the - catalog's `echo` section and wire the handle. Each rung of the ladder - reads its own renditions. + catalog's `echo` section and wire the handle. The + [ladder controller](/quest/m3/ladder/controller.md) owns every + `moq-transcode` rung's target (decided in the 2026-10-06 audit), so for + `transcode` the folded stall signal is a controller input beside the + bandwidth estimate, read per rung from its own renditions, and never drives + a rung's `rate::Control` directly. Rejected: dropping `moq transcode --echo` + from this quest. - Test: the CLI publishes to a relay, and two `moq play --echo` viewers report under the echo path, one of them throttled through the impairment profile. The target bitrate drops within two intervals of the throttled @@ -71,13 +76,14 @@ Open, to settle before starting (moved from the - **Shared echo prefixes.** Two catalogs can resolve their echo paths to one prefix (`../viewers` from `room/a/live` and `room/b/live`). Then a viewer using one name for both closes one `.echo` with the other, and each - publisher reads the other's reports under a shared alias. Candidates: + publisher reads the other's reports under a shared ID. Candidates: require each catalog's echo prefix to be its own, as application policy like the token rights, or carry the catalog's broadcast in the snapshot and ignore reports for another. ## Required +- [Ladder controller](/quest/m3/ladder/controller.md) - owns the rung targets that viewer stalls feed into - [Schema](/quest/m1/stats/schema.md) - the `echo` section and feedback snapshot - [Rust reporters](/quest/m1/stats/rust.md) - the viewers that report and the CLI it wires @@ -86,5 +92,5 @@ Open, to settle before starting (moved from the - [Audio follows the grant](/quest/m1/2848-follow-the-bandwidth-grant-in-moq-audio-instead-of.md) - the audio rate follow this signal would feed -- [Ladder](/quest/m3/ladder/README.md) - the transcode ladder that adapts to - its uplink today +- [Ladder](/quest/m3/ladder/README.md) - the transcode ladder, which today + encodes every rung at a fixed rate with no bandwidth input diff --git a/quest/m3/video-hardware-access.md b/quest/m3/video-hardware-access.md index 081828a7dd4..3d6971a03e6 100644 --- a/quest/m3/video-hardware-access.md +++ b/quest/m3/video-hardware-access.md @@ -3,7 +3,7 @@ ## Goal Someone with the hardware can run the video validation: an Intel GPU -exposing the VAAPI low-power entrypoint, a second render node, a Windows +exposing the VAAPI low-power entrypoint, a Windows machine with MJPEG and YUY2 cameras, a live camera per platform, a KDE/Wayland desktop with an Intel or AMD GPU, a sandbox that can show the camera portal dialog, and a Raspberry Pi whose CSI camera appears as a PipeWire node. @@ -16,7 +16,8 @@ this quest and every `Required` entry that links it. Check by asking the maintainer which machines are available. As of 2026-10-05 only part is on hand. The maintainer's desktop has an Intel Arrow Lake iGPU (iHD 26.1.2, `renderD128`) and an AMD RX 9070 (RADV, Mesa 26.0.8, -`renderD129`), so it has a second render node. The +`renderD129`), so it has a second render node; that check moved to +[VA-API external images](/quest/m2/vaapi-vulkan-import.md). The [multi-vendor GPU quests](/quest/m2/gpu-release.md) test on it. Whether the iGPU exposes the low-power entrypoint is unchecked. diff --git a/quest/m3/video-hardware.md b/quest/m3/video-hardware.md index 960318bcae9..58134bb2830 100644 --- a/quest/m3/video-hardware.md +++ b/quest/m3/video-hardware.md @@ -13,11 +13,12 @@ the KDE DMA-BUF capture and PipeWire camera validations fold in here, and the V4L2 `VIDIOC_EXPBUF` source is dropped with the export itself (see [#2819](/quest/m2/2819-moq-video-carry-pipewire-dma-bufs-safely-into-the-vulkan.md)). -- **VAAPI low-power entrypoint and a second GPU.** H.264 encode, DMA-BUF - input, and VPP resize ran on Intel Meteor Lake with iHD (moq-vaapi 0.1.0). - Still unrun: the low-power encode entrypoint, which that device does not - expose, and `MOQ_VAAPI_DEVICE` naming a node other than the first render - node. +- **VAAPI low-power entrypoint.** H.264 encode, DMA-BUF input, and VPP + resize ran on Intel Meteor Lake with iHD (moq-vaapi 0.1.0). Still unrun: + the low-power encode entrypoint, which that device does not expose. A + render node other than the first moved to + [VA-API external images](/quest/m2/vaapi-vulkan-import.md) (2026-10-06 audit), + whose two-node test host has one. - **Windows Media Foundation capture**: on-demand open and close, so the camera LED is off when nobody is watching, and NV12 delivery from MJPEG and YUY2 cameras. From 2d7e5c97607ca9c7eefa685bd0112d78b0b3ad1a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 6 Oct 2026 14:33:02 -0700 Subject: [PATCH 2/3] quest: match publish-timestamp's FFI shape link to the line branch Co-Authored-By: Claude Opus 5.5 --- quest/m1/publish-timestamp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/quest/m1/publish-timestamp.md b/quest/m1/publish-timestamp.md index cc5fb137d1c..d7566583593 100644 --- a/quest/m1/publish-timestamp.md +++ b/quest/m1/publish-timestamp.md @@ -64,4 +64,4 @@ with the untimed implementation quests. ## Related - [JS publishing never invents a timestamp](/quest/m1/js-publish-timestamp.md) - the same change in the JS packages -- [FFI shape](/quest/m1/ffi-shape/README.md) - #4519 moves the JSON and flate producers this changes into their namespaces; its JSON pilot (#4526) is done, so this waits only for #4519 to land, not for the whole line (2026-10-06 audit) +- [FFI shape](/quest/m1/ffi-shape/README.md) - moves the data producers this changes into `json` and `flate` namespaces in the same merge, so this is ready once #4519 lands rather than waiting on the codec child (2026-10-06 audit) From f133f1d2f0d28b1c3dc9bdd6a4509243cf1c8e8b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 6 Oct 2026 14:43:22 -0700 Subject: [PATCH 3/3] quest(qos): stats-split is Required, so quest ready keeps the order Co-Authored-By: Claude Opus 5.5 --- quest/m1/qos/lag-dashboard.md | 2 +- quest/m1/qos/publisher-timeliness.md | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/quest/m1/qos/lag-dashboard.md b/quest/m1/qos/lag-dashboard.md index f8bac97e2c7..ebc6b92755a 100644 --- a/quest/m1/qos/lag-dashboard.md +++ b/quest/m1/qos/lag-dashboard.md @@ -31,6 +31,6 @@ and per node like the existing counters. Public API: none. Wire: none. -## Related +## Required - [Stats totals and prefix tracks](/quest/m0/broadcast-epoch/stats-split.md) - the totals and prefix tracks the dashboard reads lag from diff --git a/quest/m1/qos/publisher-timeliness.md b/quest/m1/qos/publisher-timeliness.md index 6d410edf397..e40c22e2bbe 100644 --- a/quest/m1/qos/publisher-timeliness.md +++ b/quest/m1/qos/publisher-timeliness.md @@ -52,9 +52,12 @@ stall and no drift for frames it never sent; a regression across groups is counted while intra-group reordering is not; a track without a timescale is excluded. +## Required + +- [Stats totals and prefix tracks](/quest/m0/broadcast-epoch/stats-split.md) - the totals and prefix tracks the drift histogram lands on + ## Related - [Untimed model](/quest/m1/untimed-model.md) - pre-lite-05 tracks arrive untimed instead of stamped on arrival -- [Stats totals and prefix tracks](/quest/m0/broadcast-epoch/stats-split.md) - the totals and prefix tracks the drift histogram lands on - [Media stats](/quest/m1/stats/schema.md) - the publisher's own view of the same uplink, in the transport section of its stats track