Skip to content
1 change: 1 addition & 0 deletions quest/m0/broadcast-epoch/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ newest epoch falls back to a still-live older one.

- [Apps](/quest/m0/broadcast-epoch/apps.md) - moq-cli, the browser publish and watch components, and demo/web restart into a new epoch and reset on the switch
- [Restart](/quest/m0/broadcast-epoch/restart.md) - a replaced broadcast reaches announce consumers as an explicit Restart, subscriptions stay sticky, and new requests never join a replaced route's front
- [Publish catalog restart](/quest/m0/broadcast-epoch/publish-catalog-restart.md) - `@moq/publish` never reuses catalog group numbers under one name and epoch after a re-announce
- [TS restart](/quest/m0/broadcast-epoch/ts-restart.md) - a signalled backward TS discontinuity finishes the broadcast and continues the same input under a fresh epoch
- [Bindings](/quest/m0/broadcast-epoch/bindings.md) - moq-ffi and every wrapper expose the epoch and let a publisher announce one
- [Stats epochs](/quest/m0/broadcast-epoch/stats-epoch.md) - moq-stats publishes each group announcement under its own epoch, so neither a restarted node nor a returning idle group stalls its viewers
Expand Down
25 changes: 25 additions & 0 deletions quest/m0/broadcast-epoch/publish-catalog-restart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# [S] @moq/publish never reuses catalog group numbers under one epoch

## Goal

`@moq/publish` never publishes different content under one name and epoch.
After an unannounce and re-announce within one run, its catalog track either
continues its group sequence or the broadcast goes out under a fresh epoch, so
a viewer or relay never mistakes a restarted catalog for the old one.

## Plan

Found by #4970 (broadcast-epoch apps): `@moq/publish` re-creates its catalog
track after an unannounce and re-announce, so catalog group numbers may
restart under the same name and epoch. That breaks the rule that a broadcast,
track, or group name always means the same content.

Decided 2026-10-07: verify it first with a test that unannounces and
re-announces a running publish and reads the catalog's group sequence. Fix it
only if it reproduces: continue the sequence, or mint a fresh epoch per
announce, whichever matches how the rest of `@moq/publish` treats a
re-announce. `js/publish/src/broadcast.ts` mints the epoch once and keeps the
publisher across unannounce and announce on purpose, which points at
continuing the sequence.

Public API: none expected. Wire: none.
2 changes: 2 additions & 0 deletions quest/m1/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ blocks. The quests that gated m0 lines moved under them.
- [Dogfood hosted worklets](/quest/m1/dogfood-assets.md) - moq.dev and the moq.pro dashboard host the worklets and call `assets()` after the release
- [More tests under load](/quest/m1/test-flakes-2/README.md) - the second round of load-only failures, one quest per flake, fixed at the cause
- [moq-uring tests under load](/quest/m1/uring-tests-under-load.md) - uring tests pass while parallel checks share locked memory
- [Dropped uring session closes](/quest/m1/uring-drop-close.md) - a moq-uring session dropped without close() closes its connection
- [Media audio-tone check](/quest/m1/media-audio-tone.md) - the media lane's audio-tone check passes under load, fixed at its cause
- [CI runner stalls](/quest/m1/ci-runner-stalls.md) - the 0.4 to 0.8 s freezes of both interop tracks on CI are attributed from a week of nightlies and fixed or told apart from playback bugs
- [Browser interop cells](/quest/m1/interop-browser-timeouts.md) - `go -> js` and `python -> js` pass, fixed at the cause rather than by a longer timeout
Expand All @@ -78,6 +79,7 @@ blocks. The quests that gated m0 lines moved under them.
- [Data consumer timestamps](/quest/m1/data-consumer-timestamps.md) - json and binary consumers return each value's timestamp, in Rust and every binding; snapshots add `latest()` beside an in-order `next()`
- [JS data consumer timestamps](/quest/m1/js-data-consumer-timestamps.md) - @moq/json and @moq/flate consumers return each value's timestamp, with snapshot `next()` and `latest()`
- [Draft-20 FETCH](/quest/m1/ietf-fetch-location.md) - a draft-20+ FETCH within one group is served from its LOCATION_FILTER, as older drafts are
- [Pipelined first FETCH](/quest/m1/pipeline-fetch-info.md) - a fetch-only reader's first FETCH goes out with the track-info request, on lite and moq-transport
- [FETCH_OK properties](/quest/m1/fetch-ok-properties.md) - our FETCH_OK carries the track properties SUBSCRIBE_OK does, as draft 16+ requires
- [Fetch without SUBSCRIBE](/quest/m1/ietf-fetch-only.md) - a relay fetches from an IETF upstream without subscribing, finished tracks included, with End of Track always reported
- [IETF object gaps](/quest/m1/ietf-object-gaps.md) - a gapped object ID is refused loudly like a subgroup in Rust and JS, and an overflowing one closes the session in Rust
Expand Down
1 change: 1 addition & 0 deletions quest/m1/archive/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,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
- [History from the start](/quest/m1/archive/replay-history.md) - history mode lists a recording from its first segment by reading stored timeline groups, not only the restated tail
- [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
- [HLS media sequence](/quest/m1/archive/hls-media-sequence.md) - `EXT-X-MEDIA-SEQUENCE` never decreases when the reference rendition switches
Expand Down
4 changes: 4 additions & 0 deletions quest/m1/archive/replay-catalog.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,7 @@ serve example.

Cover a DVR recording whose catalog outlived its first video segment, and fail
loudly on a recording with no catalog.

## Required

- [History from the start](/quest/m1/archive/replay-history.md) - history mode lists a recording from its start, which "lists the whole recording" needs
28 changes: 28 additions & 0 deletions quest/m1/archive/replay-history.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# [M] History mode lists a recording from its start

## Goal

An HLS export in history mode lists a recording from its first segment,
however long it is. Today it starts from what the timeline restates when the
exporter joins: at most 256 records (`CHECKPOINT_RECORDS` in `moq-mux`'s
`timeline.rs`), so a recording longer than about 8.5 minutes of 2 s segments
lists from its middle.

## Plan

Found by #4978 (bounded HLS playlists), which kept the full listing behind
`moq_hls::export::Config::history` and confirmed the gap with a 4-record limit:
12 replayed segments listed from `EXT-X-MEDIA-SEQUENCE:8`. The archive test
missed it because its hand-built timeline repeats every record. Start after
#4978 lands.

Decided 2026-10-07: read the stored timeline groups in history mode rather
than only the restated tail, so the exporter or the archive reader replays
the history it holds. Pick where it lives (the exporter fetching past timeline
groups, or `moq_archive::Reader` restating the whole stored timeline) by what
keeps the live path bounded: live joins must keep reading at most the window.

Test with more records than one checkpoint and assert history mode lists
segment 0.

Public API: none expected. Wire: none.
49 changes: 49 additions & 0 deletions quest/m1/pipeline-fetch-info.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# [L] The first FETCH does not wait for the track's info

## Goal

A fetch-only reader's first FETCH goes upstream together with the request for
the track's info, on moq-lite and moq-transport, removing a round trip per hop.
The fetched group is still handed out only once that route's info is known
and passes the origin's consistency check.

## Plan

Decided 2026-10-07, while landing #4974 (fetch-only IETF demand), which keeps
a sequential TRACK_STATUS before the first FETCH. Start after #4974 lands.

Why the round trip exists (facts, 2026-10-07): nothing in a FETCH request
needs the info. Two serial gates do:

- The origin accepts the logical track with the first copy's info (timescale,
max_age, priority) and refuses later copies whose info differs
(`model/front.rs` `track_info`), because a relayed group's raw timestamps go
downstream in the group's own timescale while downstream decodes them with
the logical track's advertised one. A fetch is routed only to a copy already
spliced (`resume::Fetching`, `TrackIo::splice`).
- Both sessions register their fetch handler only after their info exchange:
lite's `TrackServeRun` runs TRACK_INFO first because FETCH frames are
timestamped in its units (only on wires with a track stream; older lite
serves with default info and has no gate to remove); IETF takes the
timescale only from SUBSCRIBE_OK or TRACK_STATUS_OK.

What needs the info is decoding the response and approving the group, so:

- Origin: let `resume::Fetching` send the copy-level fetch to the copy whose
info is in flight (a staged generation), resolving only once that copy is
spliced. On a refused or mismatched info, or a detach, drop the pending
fetch and re-issue on the next route, as failover already does. Never hand
out a group before the info check passes.
- lite: register the fetch handler up front and run FETCH alongside
TRACK_INFO, leaving the response unread in the QUIC stream until the info
lands (which also covers lite-07 untimed framing). On a failed TRACK_INFO,
reject the fetch and reset the stream.
- moq-transport: send TRACK_STATUS alongside the FETCH and take timestamps'
units from it (or FETCH_OK properties where present).

Tests: a fetch-only reader's FETCH is on the wire before the info answer; a
refused info drops the fetch and retries the next route; a group is never
released before its route's info passes. Measure the first-fetch latency
before and after.

Public API: none. Wire: none (ordering only).
26 changes: 26 additions & 0 deletions quest/m1/uring-drop-close.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# [S] A dropped moq-uring session closes its connection

## Goal

A `moq-uring` `Session` dropped without `close()` closes its QUIC connection
when its last handle goes, instead of living until the idle timeout, which
never fires while the peer keeps the connection alive.

## Plan

Found by #4965 (WebTransport close backends), which made the HTTP/3 close task
keep the session alive structurally until its capsule is sent.

Decided 2026-10-07: first check whether moq-net always calls `close()` before
dropping a session (the `Connection` doc comment in
`rs/moq-uring/src/quic/noq/connection.rs` says its session machine does); if
it does, the leak only affects direct moq-uring users, which still need it
fixed. Close with a generic application code on drop of
the last handle, matching what the other backends do, with a test that drops
a session and observes the peer's close.

Public API: none expected. Wire: none.

## Related

- [moq-uring tests under load](/quest/m1/uring-tests-under-load.md) - the same crate's test isolation
Loading