Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions quest/m0/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,7 @@ edge, which embeds moq-relay; it moved there on 2026-09-28.
- [Backport IETF close codes](/quest/m0/backport-ietf-close-codes.md) - `@moq/net` on `release` closes moq-transport sessions with the right error for three hostile cases
- [Backport @moq/flate bounds](/quest/m0/backport-flate-budget.md) - `@moq/flate` on `release` stops inflating past its cap and stops pinning pako scratch
- [Backport MKV, FLV, and VP9 import fixes](/quest/m0/backport-mux-import.md) - moq-mux on `release` refuses malformed imports instead of wrapping, panicking, or dropping tracks
- [Backport the frame-start refusal](/quest/m0/backport-frame-start.md) - `@moq/net` on `release` refuses a nonzero lite frame start instead of misnumbering frames
- [Smoke tests the released packages](/quest/m0/smoke-released.md) - smoke clients call only released APIs and every red cell is fixed, so Smoke passes on main
- [CI cost and latency](/quest/m0/ci/README.md) - cloud CI runs only in a batched merge gate as one `just ci` pass per batch, expensive checks run nightly and gate `release`, caches move to R2, and runners are chosen last
- [More tests under load](/quest/m0/test-flakes-2/README.md) - load-only and queue failures, one quest per flake, fixed at the cause, since each one fails a whole batch
25 changes: 25 additions & 0 deletions quest/m0/backport-frame-start.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# [S] Backport the frame-start refusal to release

## Goal

`@moq/net` on `release` refuses a lite group stream that starts at a nonzero
frame instead of silently numbering its frames from zero, and publishes a
patch release.

## Plan

Decided 2026-10-27 (maintainer): one PR against `release` per backport, in
m0, JS only. From
[moq-dev/moq#6922](https://github.com/moq-dev/moq/pull/6922), which makes
main honour a frame start. Release's Rust half does not apply (it lacks the
fill and group content model), and the #6903 and #6919 fixes are not
backported: #6919's `--stitch` is main-only, and #6903 would be a rewrite
against release's older watch API for a playback glitch.

As of 6ce67943f7, release's `js/net/src/lite/subscriber.ts` creates
`new netGroup.Producer(group.sequence)` and never reads the frame start, so
a mid-group start misnumbers frames, while release's Rust publisher can
send one. Refuse a nonzero start as main did before #6922 (the refusal
added in [moq-dev/moq#6395](https://github.com/moq-dev/moq/pull/6395)).

Public API: none. Wire: none; refuses a stream it misread before.
32 changes: 32 additions & 0 deletions quest/m0/smoke-released.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# [S] Smoke tests the released packages

## Goal

The Smoke workflow passes on main. Its clients call only the API of the
latest published release, so a main-only API never breaks it, and every
red cell is fixed at its cause.

## Plan

Decided 2026-10-27 (maintainer), from
[moq-dev/moq#6879](https://github.com/moq-dev/moq/pull/6879): smoke clients
use only released APIs, and a release cut moves them onto new ones. Smoke
has failed on main on every run since at least 2026-10-24 (for example
[run 40591351260](https://github.com/moq-dev/moq/actions/runs/40591351260)).

- Go (`test/smoke/clients/go/smoke.go`) calls `moq.AnnounceRoute`, and C
(`test/smoke/clients/c/subscribe.c`) calls `moq_connection_state` and
`moq_media_read`/`MOQ_DECODE_READ_FRAME`. None are on `release`. Revert
them to the released API.
- Record in the release-cut process (CONTRIBUTING or the release skill,
wherever the cut's checklist lives) that the cut updates smoke clients to
the new API.
- Cloudflare fails with "missing fields `end_of_group` and `status`" in
`moq_transport::serve::Datagram`. Find the cause and fix it.
- Upstream Docker fails because the nix build lacks the `rs/moq-net-tests`
workspace member. [moq-dev/moq#6931](https://github.com/moq-dev/moq/pull/6931)
may fix it; confirm after it lands.
- `freshness.sh` already enforces Go `@latest` and the newest `libmoq`;
keep it.

Public API: none. Wire: none.
1 change: 1 addition & 0 deletions quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ surface and zero-copy texture import, land before the cut.
- [fMP4 boundary sync](/quest/m1/fmp4-boundary-sync.md) - a declared fMP4 boundary opens a passthrough group only on a sync sample
- [Segment cuts hold bounded state](/quest/m1/segmenter-cuts.md) - the HLS segmenter keeps one pending cut, so `styp` input without sync samples cannot grow it
- [Next Object on a cold Rust copy](/quest/m1/next-object-cold-copy.md) - a join after an upstream group report starts at the first frame received, never skipping
- [Container consumers buffer by backpressure](/quest/m1/container-backpressure.md) - Rust and JS container consumers decode ahead only a byte budget, so a hostile publisher cannot grow them
- [One moq-ffi build definition](/quest/m1/ffi-build-flags.md) - every binding takes moq-ffi's default features from one place
- [A reported drop means the same everywhere](/quest/m1/resume-drop-final.md) - Rust and JS agree whether a group reported dropped can still be served
- [moq-clock moves into kio::clock](/quest/m1/kio-clock.md) - moq-clock and its test macro become `kio::clock` and `#[kio::test]` before the cut publishes them
Expand Down
2 changes: 1 addition & 1 deletion quest/m1/api-audit/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ Rejected in planning:
- [Binding publications](/quest/m1/api-audit/binding-publication.md) - moq-ffi, the C ABI, and every wrapper hand out a publication handle that announces, replacing the private adapters
- [Demand is wanted, everywhere](/quest/m1/api-audit/wanted.md) - every `used`/`unused` pair becomes `wanted()` and `wanted_changed()`, and unused ones go
- [One meaning for Credential](/quest/m1/api-audit/dial-credential.md) - the internal dial type gets its own name in JS and moq-tokio, and the `Presented` alias goes
- [One strict path rule](/quest/m1/api-audit/strict-paths.md) - `Relative` refuses empty segments, the error is `path::Error`, and auth mints only strict paths
- [One strict path rule](/quest/m1/api-audit/strict-paths.md) - `Relative` refuses empty segments, `Path` refuses non-canonical escapes, the error is `path::Error`, and auth mints only strict paths
- [Unix sockets accept through the allowlist](/quest/m1/api-audit/unix-accept.md) - the public pre-allowlist `unix::Listener::accept` becomes crate-private

## Related
Expand Down
24 changes: 20 additions & 4 deletions quest/m1/api-audit/strict-paths.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# [S] One strict path rule, minted and parsed
# [M] One strict path rule, minted and parsed

## Goal

`path::Relative` refuses empty segments as `Path::new` does, the path error is
`path::Error`, and moq-auth and @moq/auth mint only roots and paths that parse
`path::Relative` refuses empty segments as `Path::new` does, `Path` refuses a
non-canonical escape, the path error is `path::Error`, and moq-auth and @moq/auth mint only roots and paths that parse
strictly, so a token can never grant a path no peer can spell.

## Plan
Expand All @@ -22,8 +22,24 @@ Decided 2026-10-27 (maintainer):
- moq-auth and @moq/auth refuse to mint a root or path that would not parse
strictly, in Rust and JS alike.

Decided 2026-10-27 (maintainer), from
[moq-dev/moq#6927](https://github.com/moq-dev/moq/pull/6927): the moq-transport
namespace mapping must be symmetric, so `Path` refuses a non-canonical escape on
every wire rather than only at the IETF boundary.

- Today `to_tuple` (`rs/moq-net/src/ietf/namespace.rs`, and `toTuple` in
`js/net/src/ietf/namespace.ts`) keeps a lone `\x` as `\x`, while `from_tuple`
escapes every backslash, so the path returns as `\\x`. Both spellings are
valid paths today, and one tuple field can decode back to only one of them.
- `Path` (and `Relative`) accept a backslash only before `\` or `/`, a new
`path::Error` variant otherwise; `from_tuple`'s escape-every-backslash form
stays canonical.
- Add a round-trip property test in Rust and JS: `from_tuple(to_tuple(p)) == p`
for every valid path.

Public API: breaking for moq-net (`path::Invalid` becomes `path::Error`,
`Relative::new` fails on empty segments), moq-auth, and @moq/auth. Mirror the
`Relative::new` fails on empty segments, and both refuse a non-canonical
escape), moq-auth, and @moq/auth. Mirror the
names in JS and the bindings. Wire: none for moq-lite, but the hang catalog
carries `Relative` in JSON (`broadcast`, `echo.path`), so a catalog spelling one
with a leading, trailing, or doubled `/` stops parsing. Our publishers never
Expand Down
43 changes: 43 additions & 0 deletions quest/m1/container-backpressure.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# [M] Container consumers buffer by backpressure

## Goal

A hostile publisher cannot make a container consumer hold unbounded decoded
frames. `@moq/hang`'s `container/consumer.ts` and moq-mux's
`container/consumer.rs` decode ahead at most a byte budget of frames; past
it, reads wait, and the undecoded data stays in the net cache, which the
pool already bounds. Many frames packed into one timestamp, or many groups
in flight, no longer grow the consumer.

## Plan

Decided 2026-10-27 (maintainer), from
[moq-dev/moq#6903](https://github.com/moq-dev/moq/pull/6903): bound by
backpressure in Rust and JS together, not a frame-count cap that refuses a
group. Release keeps the gap until the next cut; no backport.

Today both consumers bound only by timestamp span:

- JS pushes every decoded sample (`group.frames.push` in
`js/hang/src/container/consumer.ts`), and `#checkMaxDelay` measures the
span across at least two groups, so frames that share a timestamp have
zero span and are never skipped.
- Rust buffers a `VecDeque<(Frame, Instant)>` per group
(`rs/moq-mux/src/container/consumer.rs`) bounded by span and elapsed
time only.
- The net layers cap a group (32 MiB, 8192 frames) but not how many groups
a consumer holds, and one CMAF wire frame can decode to many samples.

Steps:

- Charge decoded frames to a per-consumer byte budget and stop pulling from
net groups while it is full, so pressure reaches the net cache and QUIC
flow control instead of a refusal. Keep the existing span skip for
latency.
- Mirror names across Rust and JS.
- Test a publisher that packs many frames into one timestamp, and one that
opens many groups: memory stays near the budget and playback still skips
to the live edge.

Public API: the consumer config may gain a budget field; mirror it in JS.
Wire: none.
1 change: 1 addition & 0 deletions quest/m1/quic-api/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ side by side and fix any name or signature that still differs, and check
- [Streams](/quest/m1/quic-api/streams.md) - `send::`/`recv::` namespaces, one error per direction, `finish(self) -> send::Finished`, consuming `reset`/`stop`, `Buf` writes, both drivers mirrored
- [Crypto](/quest/m1/quic-api/crypto.md) - crypto traits sealed to rustls, concrete peer identity and handshake data
- [Config](/quest/m1/quic-api/config.md) - plain config structs, `Limits` as the one spelling, one endpoint constructor and `connect` that own the socket, v1-only `Version`, a local-close event
- [Incoming is not a future](/quest/m1/quic-api/incoming-future.md) - moq-tokio's `Incoming` drops `IntoFuture`, so no `futures` type is public
- [Waits](/quest/m1/quic-api/waits.md) - moq-quic owns every wait as a `poll_*` on `&kio::Waiter`, deleting `StreamEvent` and the drivers' and qmux's waiter tables
- [Poll forms](/quest/m1/quic-api/poll.md) - both drivers' waits poll as `&self, &kio::Waiter` forwards to moq-quic, the async forms wrap them, and `send::Finished` is a future
- [moq-net polls on kio waiters](/quest/m1/quic-api/net-poll.md) - moq-net's `transport::poll` traits take `&kio::Waiter`, `Session` ops take `&self`, and `Session` drops `Clone`
Expand Down
26 changes: 26 additions & 0 deletions quest/m1/quic-api/incoming-future.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# [XS] Incoming is not a future

## Goal

moq-tokio's `Incoming` no longer implements `IntoFuture`, so its public API
names no `futures` type. A server accepts with `incoming.accept()?.await`,
which awaits the named `Connecting` future.

## Plan

Decided 2026-10-27 (maintainer), from
[moq-dev/moq#6841](https://github.com/moq-dev/moq/pull/6841): delete the impl
rather than name a new future or fold an immediate refusal into
`Connecting`. The impl came from quinn and glues `accept()`'s two outcomes
with `futures::future::Either` (`rs/moq-tokio/src/quic/driver/incoming.rs`).
The only direct `.await` on an `Incoming` is one test
(`rs/moq-tokio/tests/integration/receive_credit.rs`); every other caller
already calls `accept()`.

- Delete `impl IntoFuture for Incoming` and the `Either`/`Ready` imports.
- Rewrite the test to `accept()?.await`.
- Check moq-uring's driver for the same impl and drop it too if present.
- Fix any docs or examples that await an `Incoming` directly.

Public API: breaking for moq-tokio (`Incoming` is no longer `IntoFuture`);
migrate with `incoming.accept()?.await`. Wire: none.
1 change: 1 addition & 0 deletions quest/m1/ts-reliability/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ catalog linger (#5305).
- [PAT changes](/quest/m1/ts-reliability/pat-lifecycle.md) - a revised PAT never leaves a broadcast meaning another program
- [TS workflow passes](/quest/m1/ts-reliability/validation-green.md) - the scheduled TS lanes pass on main again, with no lane loosened
- [Export SI order](/quest/m1/ts-reliability/export-si-order.md) - a TS export sends SI and media after its PMT and a PCR interval, so a hop keeps first emissions
- [A stitched switch returns to --delay](/quest/m1/ts-reliability/stitch-catch-up.md) - `--stitch` drops the old program's tail once the replacement acquires, so latency settles back to `--delay`

## Related

Expand Down
33 changes: 33 additions & 0 deletions quest/m1/ts-reliability/stitch-catch-up.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# [M] A stitched switch returns to --delay

## Goal

After `moq export ts --stitch` switches to a replacement publisher instance,
end-to-end latency returns to `--delay` once the replacement acquires,
instead of carrying the old program's tail as added latency.

## Plan

Decided 2026-10-27 (maintainer), from
[moq-dev/moq#6919](https://github.com/moq-dev/moq/pull/6919): drop the old
tail, not time-compress. The switch is already a TS program change, so the
skip costs nothing extra.

- Today `commit` in `rs/moq-mux/src/jitter.rs` starts the replacement past
the old `horizon`, and the anchor only moves forward. The only catch-up is
clock steering capped at about 30 ppm (`MAX_DRIFT`, `MAX_SLEW`), so the
3.9 s, 3.6 s, and 7.5 s measured in #6919's run would take about a day
and a half each to remove.
- Once the replacement acquires, cut the old program's queued tail and
start the new program at `--delay`. Keep output monotonic across the
program change.
- Test repeated switches: latency settles back to `--delay` after each,
rather than accumulating.
- Also reproduce #6919's priority observation: a mid-stream
`Subscriber::update` demotion of the old leg still showed priority 0 on
the receive side. Fix it if update propagation is wrong; otherwise
explain it in the PR.
- Update `doc/bin/cli.md` and `doc/setup/redundancy.md` inline if they
describe switch latency.

Public API: none expected. Wire: none.
Loading