Skip to content
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ The API is the most important thing to get right.
A bad shape costs a breaking change in every language, and the surface is huge.

- Report the public API and wire impact of every change, in the PR description and whenever asked.
- A breaking change marks its title with `!` and its Impact bullet says how to migrate. Only the release cut writes `MIGRATE.md` sections; leave existing Unreleased notes for that cut to consume.
- A breaking change marks its title with `!` and its Impact bullet says how to migrate. Feature PRs add their migration notes under `## Unreleased`; only the release cut writes version sections.

@pullfrog pullfrog Bot Oct 31, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This new rule reverses the wording landed in bdc47852ad (this branch's base) and contradicts three files on main that record a 2026-10-30 decision to stop writing per-PR Unreleased notes: quest/m0/ci/README.md:78-80 ("MIGRATE.md Unreleased sections, a top source of conflicts, stop being written per PR"), quest/m0/ci/land.md:40-41 ("migration notes stay in PR Impact sections"), and quest/m2/release/migrate-audit.md:15-16 ("PRs into main no longer write MIGRATE.md entries"). As written, the rule (AGENTS.md:62) directs contributors to recreate the exact conflict source that quest land batching just eliminated.

Technical details
# AGENTS.md migration-notes rule contradicts the quest-land decision

## Affected sites
- `AGENTS.md:62` — new rule: "Feature PRs add their migration notes under `## Unreleased`"
- `quest/m0/ci/README.md:78-80` — decided 2026-10-30: Unreleased sections "stop being written per PR"
- `quest/m0/ci/land.md:40-41` — "migration notes stay in PR Impact sections until the release migration audit"
- `quest/m2/release/migrate-audit.md:15-16` — "PRs into `main` no longer write `MIGRATE.md` entries (decided 2026-10-23)"

## What's wrong
The three quest lines were written in `bdc47852ad` (2026-10-30 08:59); the AGENTS edit is `8c7669e7f0` (2026-10-30 16:47) and its removed predecessor matched them. The PR description records the AGENTS wording as an intentional pick but does not mention reversing the quest-land decision, so either the rule reverted by mistake or the quests are now stale.

## Required outcome
Reconcile the two: either restore the prior AGENTS wording ("Only the release cut writes MIGRATE.md sections; leave existing Unreleased notes for that cut to consume."), or, if per-PR Unreleased notes are intended again, update `quest/m0/ci/README.md`, `quest/m0/ci/land.md`, and `quest/m2/release/migrate-audit.md` in the same PR.

## Open questions for the human
- Is the 2026-10-30 "stop writing per-PR Unreleased sections" decision still current, or has it been reversed?

- Keep things private until a current or planned consumer needs them. Scrutinize every new exported item.
- Never add `foo_with_x`, `foo_checked`, or a compatibility shim. Make the breaking change to `foo` instead.
- Let the type system make misuse unrepresentable
Expand Down
2 changes: 2 additions & 0 deletions quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ removed).
- [Subscribe ranges](/quest/m1/subscribe-ranges/README.md) - a lite-07 SUBSCRIBE asks for past and live ranges with absolute or relative ends, in either order, and replaces FETCH; merged ranges never starve a subscriber; relays fill misses by range, including over moq-transport
- [Drop set](/quest/m1/drop-set.md) - each cursor judges missing groups against its own budget, so a narrow reader drops promptly and a widening re-offers groups already reported dropped on lite-07
- [Broadcast archives](/quest/m1/archive/README.md) - ANNOUNCE, TRACKS, and a SUBSCRIBE per track record every track under a prefix to any object_store, a host serves a stored broadcast back, and the catalog drops its archive pointer for a per-rendition `hls` field
- [Subscriptions take a Delay { min, max }](/quest/m1/subscription-delay.md) - one Delay type across every subscription API, and omitted max means 0 in every binding
- [Finalize moq-lite-07](/quest/m1/lite07-finalize.md) - when the maintainer cuts it, lite-07 negotiates as `moq-lite-07` and the next release ships it
- [Smoke token subject](/quest/m1/smoke-token-subject.md) - condition: grant subject ships; then the release smoke test signs with `--subject`
- [Moxygen pin bump](/quest/m1/moxygen-pin-bump.md) - waiting on moxygen#264, then the TRACK_STATUS patch is dropped
Expand Down Expand Up @@ -171,6 +172,7 @@ removed).
- [JS join tests cover draft 20](/quest/m1/join-tests-draft20.md) - join.test.ts runs on every supported draft, not just 18 and 22
- [Poll-first async](/quest/m1/poll-first/README.md) - library crates compile no coroutines by default: async wrappers are generated from `poll_*` and gated behind an `async` feature
- [Audit retention defaults](/quest/m1/max-age-defaults.md) - every max-age default listed, with a proposed finite default for the maintainer
- [JS ordered reads wait only on owed groups](/quest/m1/js-ordered-owed.md) - @moq/net matches Rust's owed-group wait
- [Binding announcements always carry captures](/quest/m1/ffi-required-captures.md) - moq-ffi and its generated bindings make `captures` a required list, as #7266 always sets it
- [HLS export follows a resolution change](/quest/m1/hls-reinit-same-codec.md) - a decoder-identity change under the same codec string starts a new init and segment tag
- [A JS reader finishes after a clean close](/quest/m1/js-close-range-finish.md) - a range ending below the cache delivers what's held and then ends
Expand Down
21 changes: 21 additions & 0 deletions quest/m1/js-ordered-owed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# [S] JS ordered reads wait only on owed groups

## Goal

`@moq/net`'s ordered reader on a subscribed track waits at a hole while the feed may still deliver that group, bounded by its max delay, as Rust's does since [moq-dev/moq#7403](https://github.com/moq-dev/moq/pull/7403). It no longer skips the gap, and local tracks still skip.

## Plan

Decided 2026-10-30 (maintainer, follow-up planning): m1, for parity with Rust before the cut. Port #7403's rule rather than invent a JS one:

- Only subscription-fed copies owe groups.
- An unlimited budget waits until the hole resolves, with no grace period.
- A finite budget gives up on the same media-time and wall-clock rule.

Port #7403's Rust tests, using JS's manual clock.

Public API: none. Wire: none.

## Related

- [Ordered reads wait only on owed groups](https://github.com/moq-dev/moq/pull/7403) - its PR, #7403, is the Rust rule this ports
1 change: 1 addition & 0 deletions quest/m1/lite07-finalize.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ timestamp bound in [#6168](https://github.com/moq-dev/moq/pull/6168).
- [Lite-07 by start](/quest/m1/time-addressing/lite07.md) - lite-07 carries group starts, previous-start links, GROUP_END, and time bounds instead of sequence numbers
- [@moq/net by start](/quest/m1/time-addressing/js.md) - JS speaks the same lite-07 wire
- [A job's exact route continues its claim's readers](/quest/m1/announce-takeover.md) - the job's route inherits the claim's identity, with no new wire message
- [Subscriptions take a Delay { min, max }](/quest/m1/subscription-delay.md) - lite-07 SUBSCRIBE carries the delay's min as well as its max

## Related

Expand Down
27 changes: 27 additions & 0 deletions quest/m1/subscription-delay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# [L] Subscriptions take a Delay { min, max }

## Goal

Every subscription API takes the same `Delay { min, max }` the consumer APIs got in [moq-dev/moq#7311](https://github.com/moq-dev/moq/pull/7311), in place of a bare max delay. That covers moq-net `Subscription`, `@moq/net`'s `Subscription`, FFI `MoqSubscription`, and moq-c `moq_subscription`. The `Delay` type moves to moq-net / `@moq/net`, and an omitted max means 0 in every binding, including Dart, Go, and C.

## Plan

Decided 2026-10-30 (maintainer, follow-up planning): m1, because it breaks the public API and doing it before the cut avoids a second break.

- **Move:** move `Delay` from `moq_mux::container` / `Container.Delay` into `moq_net::track` / `Track.Delay`. Update moq-mux's callers directly and leave no re-export, since `AGENTS.md` forbids compatibility shims; the MIGRATE note names the new path.
- **Rename:** rename `max_delay` to `delay.max` throughout, about 1,050 Rust and 600 JS references. `max` travels on the wire as today.
- **`min` on the wire:** decided 2026-10-30 (maintainer): because `Delay` lives in moq-net, `min` goes on the lite-07 SUBSCRIBE (and SUBSCRIBE_UPDATE) too, following the rule that everything in moq-net survives the wire. Had `Delay` stayed in moq-mux, it would have stayed local. A relay forwards a subscriber's `min` upstream, and merged subscriptions take the smallest. Nothing reads it on the publisher side yet; later uses could include starting a cold join server-side, delay-tolerant batching, and priority under congestion. Versions that can't carry `min` (lite-01..06, moq-transport) follow the `carries_max_delay` precedent (`rs/moq-net/src/lite/subscribe.rs:113`): encoding to them drops `min`, and decoding from them yields "not stated", not 0. A relay forwards a not-stated `min` as not stated, and it doesn't count toward a merged subscription's smallest, so a legacy peer doesn't pull a merged `min` to 0.
- **Semantics:** they're settled in [moq-dev/moq#7403](https://github.com/moq-dev/moq/pull/7403): every default is 0, unlimited is explicit (Rust `None`, JS `null`, FFI null), and an omitted JS field means 0.
- **Binding defaults:** this change also fixes the bindings where an omitted max is still unlimited.
- Dart: an optional nullable parameter.
- Go: a nil `MaxDelay` in a zero `Subscription{}`.
- C: `max_delay_present` is false in a zeroed struct.

Each one's omitted or zeroed form becomes 0, and unlimited stays expressible. Dart defaults the parameter to `Duration.zero`, so an explicit `null` stays unlimited. Go's nil and C's unset `present` can't mean both, so each gains an explicit unlimited flag in the delay; the PR names it. The breaking binding changes get MIGRATE notes under `## Unreleased`.

Public API: breaking in every binding. Wire: lite-07 SUBSCRIBE and SUBSCRIBE_UPDATE gain `min`.

## Related

- [A max-delay decoder joins an earlier group](https://github.com/moq-dev/moq/pull/7311) - its PR, #7311, adds `Delay`
- [Ordered reads wait only on owed groups](https://github.com/moq-dev/moq/pull/7403) - the Rust rule; its PR, #7403, settles the max-delay defaults and touches the same fields
3 changes: 3 additions & 0 deletions quest/m2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ lite-07; bitrate enforcement is not part of this release.
- [The HLS server follows each path once](/quest/m2/hls-follow-once.md) - `Following` reports synced absence, so moq-hls resolves with one follow
- [moq-gst playback shares one session context](/quest/m2/gst-session-context.md) - the playback helpers take one context instead of six arguments
- [moq-bench pins the instances it measures](/quest/m2/bench-epoch-pin.md) - pooled requests pin their announced epoch, so a takeover is not counted as the same broadcast
- [Audio's Latest start reaches back](/quest/m2/audio-latest-reach-back.md) - moq_audio's start covers min plus warmup like other consumers
- [moq-lite tails count drops](/quest/m2/lite-tail-drop-count.md) - SUBSCRIBE_DROP accounting replaces the session-tail grace from lite-05
- [Re-run the SPS fuzzer on h264-reader](/quest/m2/sps-fuzz-h264-reader.md) - the H.264 SPS overflow is rechecked on the new parser
- [Bindings see a broadcast end](/quest/m2/binding-broadcast-end.md) - `MoqBroadcastConsumer` exposes the close, and a moq-transport run end is a protocol error
- [Audio track changes are gapless](/quest/m2/gapless-audio-switch.md) - no silence beyond a frame on audio format or rendition changes, enforced by media QA
- [We own our WebRTC stack](/quest/m2/webrtc/README.md) - moq-rtc becomes moq-webrtc with moq-ice, the data-channel transport, and a vendored, trimmed str0m/dimpl/is/sctp-proto
Expand Down
16 changes: 16 additions & 0 deletions quest/m2/audio-latest-reach-back.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# [S] Audio's Latest start reaches back by min plus warmup

## Goal

`moq_audio`'s decoder under `Start::Latest` starts at the group covering live edge − (`delay.min` + warmup), as the other consumers do since [moq-dev/moq#7311](https://github.com/moq-dev/moq/pull/7311). Today it floors at the live-edge group and primes its warmup by trimming.

## Plan

Decided 2026-10-30 (maintainer, follow-up planning): m2. Use the same start resolution the container consumer uses rather than a second one. With `min` and warmup at 0, the start is unchanged. Add a test of a cold join with a 2-group-old start. [Audio join budget](https://github.com/moq-dev/moq/pull/7342) changes the same `joining_latest` path in `rs/moq-audio/src/decode/consumer.rs`; whichever lands second builds its start on the other's.

Public API: none. Wire: none.

## Related

- [A max-delay decoder joins an earlier group](https://github.com/moq-dev/moq/pull/7311) - its PR, #7311, adds the start rule
- [Audio join budget](https://github.com/moq-dev/moq/pull/7342) - the other `Start::Latest` join fix in `rs/moq-audio/src/decode/consumer.rs`
20 changes: 20 additions & 0 deletions quest/m2/lite-tail-drop-count.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# [M] moq-lite session tails count drops instead of waiting a grace

## Goal

From lite-05 on, a moq-lite session's tail is complete once every group up to the end has arrived or been reported in SUBSCRIBE_DROP. The 1 s `ledger::GRACE` (`rs/moq-net/src/ledger.rs:19`) then no longer decides it, in Rust and JS.

## Plan

Decided 2026-10-30 (maintainer, follow-up planning): m2. Found by the audit on [moq-dev/moq#7403](https://github.com/moq-dev/moq/pull/7403). The maintainer dislikes grace periods.

- Count per version. lite-01..04 have no drop message, so they keep today's behaviour.
- Account for holes left by lost datagrams.
- Guard against a peer that never accounts for a group: decide with the maintainer whether that ends the tail with an error or keeps waiting. Don't add a new grace.
- The IETF tail keeps its grace until the moq-transport gap-signal extension (planned in [moq-dev/moq#7425](https://github.com/moq-dev/moq/pull/7425)) lands, since moq-transport can't signal gaps.

Public API: none. Wire: none.

## Related

- [Drop set](/quest/m1/drop-set.md) - the per-cursor drop reporting this counts
16 changes: 16 additions & 0 deletions quest/m2/sps-fuzz-h264-reader.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# [XS] Re-run the SPS fuzzer on h264-reader

## Goal

The H.264 SPS fuzz target runs against moq-mux's parser after [moq-dev/moq#7398](https://github.com/moq-dev/moq/pull/7398) moves it onto `h264-reader`. Any overflow or panic it finds there is fixed in moq-mux's validation and recorded in the fuzz-findings SPS quest.

## Plan

Decided 2026-10-30 (maintainer, follow-up planning): [moq-dev/moq#7313](https://github.com/moq-dev/moq/pull/7313) found the crop and width overflows in `h264-parser` ([therishidesai/h264-parser#9](https://github.com/therishidesai/h264-parser/issues/9)). #7398 replaces that crate for H.264, so the finding has to be rechecked on the new parser. Run the target long enough to cover the old reproducer and a fresh corpus. If `h264-reader` is clean, narrow the SPS quest to H.265.

Public API: none. Wire: none.

## Required

- [Parse H.264 with maintained crates](/quest/m2/h264-reader.md) - its PR, #7398, moves SPS parsing to h264-reader
- [Media parsers have fuzz targets](/quest/m2/media-fuzz.md) - its PR, #7313, adds the SPS target and the fuzz-findings epic
1 change: 1 addition & 0 deletions rs/moq-net/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ The wire layer. Generic over the transport and media-agnostic: the relay never i
- New capabilities are additive SETUP extensions; receivers ignore unknown parameters.
- Closes never cascade. Aborting a group must not tear down its track, broadcast, or siblings.
- A route change is never fatal on its own. A subscription stays on the instance it started on while that instance still has a route, even after a newer route wins the path. Once none is left (e.g. the publisher's session closed), it ends with an error, never a clean end, and the app requests the path again. Never stitch it onto another instance. A clean end means only a deliberate publisher finish. Players start, stop, and restart on announcements, never on request errors. See "Publisher epochs" in `doc/concept/moq-lite.md`.
- Nothing in moq-net is local-only: every field and behaviour in its API survives the wire, so a subscription, announcement, or end means the same through a relay as in-process. A local-only knob belongs in a crate above moq-net (moq-mux, hang).
- Local and remote paths behave the same: a local close, cancel, or error reaches local readers exactly as the remote equivalent would, never as a cleaner end.
- The cache is a performance optimization. A relay behaves the same warm or cold: report upstream state (e.g. the largest group), not what the cache happens to hold.
- A relay never blocks an answer on other nodes finishing something (replay, catch-up, a full announce set). Report what is known now; a rare undercount is acceptable.
Expand Down